Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,12 @@ COPY --from=build /src/modules/examples/price-alert/module.toml /opt/shepher
COPY --from=build /src/modules/examples/balance-tracker/module.toml /opt/shepherd/manifests/balance-tracker.toml
COPY --from=build /src/modules/examples/stop-loss/module.toml /opt/shepherd/manifests/stop-loss.toml

# The bundled cow venue adapter's manifest; installed via the
# The bundled cow venue adapter's manifests; installed via the
# engine.toml [[adapters]] stanza, never compiled into the engine.
# One manifest per chain: mainnet (cow-venue.toml) and Sepolia
# (cow-venue.sepolia.toml); pick the one matching the run's chain.
COPY --from=build /src/crates/cow-venue/module.toml /opt/shepherd/manifests/cow-venue.toml
COPY --from=build /src/crates/cow-venue/module.sepolia.toml /opt/shepherd/manifests/cow-venue.sepolia.toml

# Drop privileges. The engine never needs root at runtime: it only
# reads /etc/shepherd/engine.toml, writes to /var/lib/shepherd, and
Expand Down
24 changes: 24 additions & 0 deletions crates/cow-venue/module.load.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Load-test variant of the cow adapter manifest: Sepolia chain id with
# the orderbook re-pointed at tools/orderbook-mock (no live cow.fi).

[module]
name = "cow"
version = "0.1.0"
kind = "venue-adapter"
# Placeholder content hash; parsed but not verified in 0.2.
component = "sha256:0000000000000000000000000000000000000000000000000000000000000000"

[capabilities]
required = ["http"]
optional = []

[capabilities.http]
allow = ["localhost"]

[config]
chain = "11155111"
orderbook-url = "http://localhost:9999"

# Body-schema versions this adapter decodes: the handshake authority.
[venue]
body_versions = [1]
24 changes: 24 additions & 0 deletions crates/cow-venue/module.sepolia.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Sepolia variant of the cow adapter manifest: same component, chain
# 11155111, so submits land on the Sepolia orderbook. Wire this from
# any engine config whose watchers index Sepolia.

[module]
name = "cow"
version = "0.1.0"
kind = "venue-adapter"
# Placeholder content hash; parsed but not verified in 0.2.
component = "sha256:0000000000000000000000000000000000000000000000000000000000000000"

[capabilities]
required = ["http"]
optional = []

[capabilities.http]
allow = ["api.cow.fi"]

[config]
chain = "11155111"

# Body-schema versions this adapter decodes: the handshake authority.
[venue]
body_versions = [1]
3 changes: 2 additions & 1 deletion crates/cow-venue/module.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ allow = ["api.cow.fi"]

# One adapter instance speaks one chain's orderbook. `orderbook-url`,
# `owner` (enables the pre-sign path), and `http-timeout-ms` are
# optional overrides.
# optional overrides. Sepolia and load-mock variants sit alongside
# (`module.sepolia.toml`, `module.load.toml`).
[config]
chain = "1"

Expand Down
44 changes: 9 additions & 35 deletions crates/shepherd-cow-host/tests/cow_boot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,32 +144,6 @@ async fn boot_production_module(
.expect("boot_single")
}

/// twap-monitor imports `shepherd:cow/cow-api`; with the cow extension
/// registered it boots, and a block dispatch reaches it and keeps it alive.
#[tokio::test]
async fn e2e_twap_monitor_block_dispatch() {
let Some(wasm) = module_wasm_or_skip("twap-monitor") else {
return;
};
let manifest = production_module_toml("modules/twap-monitor/module.toml");
let engine = make_wasmtime_engine();
let linker = make_linker(&engine);
let (_dir, store) = temp_local_store();

let mut supervisor = boot_production_module(&engine, &linker, &store, &wasm, &manifest).await;
assert_eq!(supervisor.module_count(), 1);
assert_eq!(supervisor.alive_count(), 1);

// twap-monitor subscribes to Sepolia blocks (poll path). A real poll
// would call chain::request, which ProviderPool::empty() does not
// satisfy - the module surfaces a fault and warns; the supervisor
// must keep the module alive because the strategy catches the error
// and returns Ok(()).
let dispatched = supervisor.dispatch_block(synthetic_sepolia_block()).await;
assert_eq!(dispatched, 1);
assert_eq!(supervisor.alive_count(), 1);
}

/// ethflow-watcher imports `shepherd:cow/cow-api` and subscribes to logs;
/// it boots with the cow extension and a synthetic log is delivered.
#[tokio::test]
Expand Down Expand Up @@ -221,17 +195,17 @@ async fn e2e_stop_loss_block_dispatch() {
}

/// The boot-order invariant, exercised (not merely asserted in prose):
/// a module that imports `shepherd:cow/cow-api` (twap-monitor) must NOT
/// boot when the cow extension is absent from the linker AND the
/// a module that imports `shepherd:cow/cow-api` (ethflow-watcher) must
/// NOT boot when the cow extension is absent from the linker AND the
/// capability registry. The paired linker-hook + capability-namespace
/// registration is what makes the same module boot in the tests above;
/// drop the pairing and boot fails.
#[tokio::test]
async fn twap_monitor_without_cow_extension_fails_to_boot() {
let Some(wasm) = module_wasm_or_skip("twap-monitor") else {
async fn ethflow_watcher_without_cow_extension_fails_to_boot() {
let Some(wasm) = module_wasm_or_skip("ethflow-watcher") else {
return;
};
let manifest = production_module_toml("modules/twap-monitor/module.toml");
let manifest = production_module_toml("modules/ethflow-watcher/module.toml");
let engine = make_wasmtime_engine();
// Core-only: no cow linker hook, no cow capability namespace.
let linker = build_linker::<CowTestTypes>(&engine, &[]).expect("build_linker");
Expand All @@ -254,10 +228,10 @@ async fn twap_monitor_without_cow_extension_fails_to_boot() {
let err = result
.err()
.expect("cow-importing module must not boot without the cow extension registered");
// Pin the failure to its specific cause: twap-monitor declares the
// cow-api capability, which a core-only registry does not recognise
// (registering it is exactly what the cow extension does). Rules out
// an unrelated failure masquerading as the invariant.
// Pin the failure to its specific cause: ethflow-watcher declares
// the cow-api capability, which a core-only registry does not
// recognise (registering it is exactly what the cow extension does).
// Rules out an unrelated failure masquerading as the invariant.
let chain = format!("{err:#}");
assert!(
chain.contains(r#"unknown capability "cow-api""#),
Expand Down
49 changes: 49 additions & 0 deletions crates/videre-host/tests/platform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -736,6 +736,55 @@ async fn e2e_keeper_module_drives_the_venue_through_the_typed_client() {
}
}

/// The shepherd bundle pair: twap-monitor (a `#[videre_sdk::keeper]`
/// worker) boots against the installed cow adapter - the body-version
/// handshake admits the pair - and a Sepolia block dispatch reaches it
/// and keeps it alive. The chainless poll surfaces a fault the strategy
/// absorbs, so no orderbook traffic occurs.
#[tokio::test]
async fn e2e_twap_monitor_boots_against_the_cow_adapter() {
let (Some(adapter_wasm), Some(module_wasm)) = (
module_wasm_or_skip("cow-venue"),
module_wasm_or_skip("twap-monitor"),
) else {
return;
};

let components = mock_components();
let engine = make_wasmtime_engine();
let config = EngineConfig {
adapters: vec![AdapterEntry {
path: adapter_wasm,
// Sepolia variant: twap-monitor pins chain 11155111, so the
// adapter manifest must name the same chain for the pair to
// submit to the right orderbook.
manifest: Some(workspace_path("crates/cow-venue/module.sepolia.toml")),
http_allow: Vec::new(),
messaging_topics: Vec::new(),
}],
modules: vec![ModuleEntry {
path: module_wasm,
manifest: Some(workspace_path("modules/twap-monitor/module.toml")),
}],
..Default::default()
};
let videre = Arc::new(platform(&config));
let extensions = videre_assembly(&videre);
let linker = make_linker(&engine, &extensions);

let mut supervisor =
Supervisor::boot(&engine, &linker, &config, &components, &extensions, None)
.await
.expect("boot");
assert_eq!(supervisor.adapter_alive_count(), 1, "cow is routable");
assert_eq!(supervisor.alive_count(), 1, "twap-monitor is alive");

// twap-monitor subscribes to Sepolia blocks (poll path); with no
// watches indexed the sweep is empty and the keeper stays alive.
assert_eq!(supervisor.dispatch_block(block(11_155_111)).await, 1);
assert_eq!(supervisor.alive_count(), 1);
}

/// The body-version handshake refuses a mismatched pair: an adapter
/// decoding only v1 against a keeper encoding v2 fails the boot at the
/// keeper's install, before instantiation, naming both sides' versions.
Expand Down
12 changes: 12 additions & 0 deletions docs/deployment/multi-chain.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ The engine dispatches each module only to the chains it subscribes to, so a
single `nexum` process can serve modules watching Mainnet, Gnosis Chain,
Arbitrum One, and Base at the same time.

That covers keeper modules, which subscribe per chain. It does not extend to
venue adapters. The CoW adapter fixes its orderbook chain at `init` from its
own manifest `[config] chain`, and registers under the fixed venue id `cow`
(`CowVenue::ID`), which carries no chain component. Two cow adapters installed
in one process would therefore register under the same id, with nothing at the
pool router to tell them apart.

Single-process multi-chain *submission* is an explicit non-goal for M4. Run one
engine process per submitting chain, each paired with the matching adapter
manifest (`module.toml` for Mainnet, `module.sepolia.toml` for Sepolia).
Modules that only watch chains are unaffected and may span chains freely.

---

## Chain support matrix
Expand Down
6 changes: 4 additions & 2 deletions engine.docker.toml
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,11 @@ manifest = "/opt/shepherd/manifests/stop-loss.toml"
#
# The bundled cow venue adapter: the pool router resolves the `cow`
# venue id through it. The operator grant scopes its outbound HTTP to
# the production orderbook.
# the production orderbook host. The Sepolia manifest matches
# twap-monitor's Sepolia subscriptions; a mainnet run swaps in
# /opt/shepherd/manifests/cow-venue.toml.

[[adapters]]
path = "/opt/shepherd/modules/cow_venue.wasm"
manifest = "/opt/shepherd/manifests/cow-venue.toml"
manifest = "/opt/shepherd/manifests/cow-venue.sepolia.toml"
http_allow = ["api.cow.fi"]
10 changes: 10 additions & 0 deletions engine.e2e.toml
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,13 @@ manifest = "modules/examples/balance-tracker/module.toml"
[[modules]]
path = "target/wasm32-wasip2/release/stop_loss.wasm"
manifest = "modules/examples/stop-loss/module.toml"

# --- adapters ---------------------------------------------------------

# The cow venue adapter twap-monitor submits through (`just
# build-cow-venue`). Sepolia manifest: the adapter's orderbook must
# match the chain twap indexes.
[[adapters]]
path = "target/wasm32-wasip2/release/cow_venue.wasm"
manifest = "crates/cow-venue/module.sepolia.toml"
http_allow = ["api.cow.fi"]
4 changes: 3 additions & 1 deletion engine.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,9 @@ rpc_url = "${BASE_RPC_URL}"
# The operator, not the adapter author, grants the transport scope:
# `http_allow` is the outbound wasi:http host allowlist. The bundled
# cow adapter builds with `just build-cow-venue`; its venue id is the
# manifest name (`cow`).
# manifest name (`cow`). One manifest per chain: `module.toml` is
# mainnet, `module.sepolia.toml` Sepolia - wire the one matching the
# chain the submitting modules index.

# [[adapters]]
# path = "target/wasm32-wasip2/release/cow_venue.wasm"
Expand Down
9 changes: 9 additions & 0 deletions engine.load.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,15 @@ rpc_url = "ws://localhost:8545"
path = "./target/wasm32-wasip2/release/twap_monitor.wasm"
manifest = "./modules/twap-monitor/module.toml"

# The cow venue adapter twap-monitor submits through (`just
# build-cow-venue`). Load-variant manifest: Sepolia chain id with the
# orderbook re-pointed at tools/orderbook-mock, matching
# [extensions.cow.orderbook_urls] above.
[[adapters]]
path = "./target/wasm32-wasip2/release/cow_venue.wasm"
manifest = "./crates/cow-venue/module.load.toml"
http_allow = ["localhost"]

[[modules]]
path = "./target/wasm32-wasip2/release/ethflow_watcher.wasm"
manifest = "./modules/ethflow-watcher/module.toml"
8 changes: 8 additions & 0 deletions engine.m2.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,11 @@ manifest = "modules/twap-monitor/module.toml"
[[modules]]
path = "target/wasm32-wasip2/release/ethflow_watcher.wasm"
manifest = "modules/ethflow-watcher/module.toml"

# The cow venue adapter twap-monitor submits through (`just
# build-cow-venue`). Sepolia manifest: the adapter's orderbook must
# match the chain twap indexes.
[[adapters]]
path = "target/wasm32-wasip2/release/cow_venue.wasm"
manifest = "crates/cow-venue/module.sepolia.toml"
http_allow = ["api.cow.fi"]
9 changes: 9 additions & 0 deletions engine.soak.docker.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,12 @@ manifest = "/opt/shepherd/manifests/balance-tracker.toml"
[[modules]]
path = "/opt/shepherd/modules/stop_loss.wasm"
manifest = "/opt/shepherd/manifests/stop-loss.toml"

# --- adapters -----------------------------------------------------------

# The cow venue adapter twap-monitor submits through. Sepolia
# manifest: the adapter's orderbook must match the chain twap indexes.
[[adapters]]
path = "/opt/shepherd/modules/cow_venue.wasm"
manifest = "/opt/shepherd/manifests/cow-venue.sepolia.toml"
http_allow = ["api.cow.fi"]
12 changes: 12 additions & 0 deletions engine.soak.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
# cargo build --target wasm32-wasip2 --release \
# -p twap-monitor -p ethflow-watcher -p price-alert \
# -p balance-tracker -p stop-loss
# cargo build --target wasm32-wasip2 --release \
# -p cow-venue --features cow-venue/adapter
# ./target/release/nexum --engine-config engine.soak.toml
#
# Swap rpc_url below for your paid endpoint before starting, or set it
Expand Down Expand Up @@ -66,3 +68,13 @@ manifest = "modules/examples/balance-tracker/module.toml"
[[modules]]
path = "target/wasm32-wasip2/release/stop_loss.wasm"
manifest = "modules/examples/stop-loss/module.toml"

# --- adapters ---------------------------------------------------------

# The cow venue adapter twap-monitor submits through (`just
# build-cow-venue`). Sepolia manifest: the adapter's orderbook must
# match the chain twap indexes.
[[adapters]]
path = "target/wasm32-wasip2/release/cow_venue.wasm"
manifest = "crates/cow-venue/module.sepolia.toml"
http_allow = ["api.cow.fi"]
4 changes: 2 additions & 2 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ build-m2:
# (Sepolia, both M2 modules). See `docs/operations/m2-testnet-runbook.md`.
# --pretty-logs keeps the runbook-friendly human-readable formatter;
# production deploys omit the flag and emit JSON.
run-m2: build-m2 build-engine
run-m2: build-m2 build-cow-venue build-engine
cargo run -p shepherd -- --engine-config engine.m2.toml --pretty-logs

# Build the M3 example modules (price-alert + balance-tracker + stop-loss)
Expand Down Expand Up @@ -74,7 +74,7 @@ build-e2e: build-m2 build-m3
# 127.0.0.1:9100/metrics. JSON logs (no --pretty-logs) so a
# downstream `jq` filter can mine submitted/dropped/backoff markers
# for the e2e report. See `docs/operations/e2e-testnet-runbook.md`.
run-e2e: build-e2e build-engine
run-e2e: build-e2e build-cow-venue build-engine
cargo run -p shepherd -- --engine-config engine.e2e.toml

# Zero-leak gate: host-layer crate graphs, runtime charter-symbol and
Expand Down
4 changes: 3 additions & 1 deletion modules/twap-monitor/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,17 @@ crate-type = ["cdylib"]

[dependencies]
composable-cow = { path = "../../crates/composable-cow" }
cow-venue = { path = "../../crates/cow-venue", features = ["client"] }
nexum-sdk = { path = "../../crates/nexum-sdk" }
shepherd-sdk = { path = "../../crates/shepherd-sdk" }
videre-sdk = { path = "../../crates/videre-sdk" }
cowprotocol = { version = "0.2.0", default-features = false }
alloy-primitives = { version = "1.6", default-features = false, features = ["std"] }
alloy-sol-types = { version = "1.6", default-features = false, features = ["std"] }
tracing = { version = "0.1", default-features = false }
wit-bindgen = { version = "0.59", default-features = false, features = ["macros", "realloc"] }

[dev-dependencies]
cow-venue = { path = "../../crates/cow-venue", features = ["client", "assembly"] }
serde_json = { version = "1", default-features = false, features = ["alloc"] }
shepherd-sdk-test = { path = "../../crates/shepherd-sdk-test" }
nexum-sdk-test = { path = "../../crates/nexum-sdk-test" }
Loading
Loading