Skip to content
Open
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
46 changes: 46 additions & 0 deletions crates/videre-host/tests/platform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -709,6 +709,52 @@ 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,
manifest: Some(workspace_path("crates/cow-venue/module.toml")),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new e2e test — the one meant to prove the keeper+adapter pairing end-to-end — wires twap-monitor (Sepolia-pinned, chain_id = 11155111) against module.toml, the mainnet-default manifest, not module.sepolia.toml. It passes today only because the dispatched block hits the chainless-poll fault path with no watches indexed, so no submission ever happens. If a future change causes this exact test wiring to attempt a real submit, it would silently target the wrong chain's orderbook and this test wouldn't catch it — the manifest-matching invariant this whole PR is built around is untested here. Worth pointing this test at 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
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" }
20 changes: 15 additions & 5 deletions modules/twap-monitor/module.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# twap-monitor: poll registered ComposableCoW conditional orders and
# submit ready ones via the CoW Protocol orderbook.
# submit ready ones to the CoW venue through the pool.

[module]
name = "twap-monitor"
Expand All @@ -14,13 +14,13 @@ component = "sha256:000000000000000000000000000000000000000000000000000000000000
# - local-store -> watch: / next_block: / next_epoch: / submitted: /
# backoff: / dropped: persistence
# - chain -> eth_call into ComposableCoW.getTradeableOrderWithSignature
# - cow-api -> POST /api/v1/orders submission path
required = ["logging", "local-store", "chain", "cow-api"]
# - client -> videre:venue/client submit path to the cow adapter
required = ["logging", "local-store", "chain", "client"]
optional = []

[capabilities.http]
# All outbound HTTP goes through `cow-api` (which routes through the
# host's pinned orderbook URL); no direct `http` calls.
# All outbound HTTP is the cow adapter's; the keeper makes no direct
# `http` calls.
allow = []

# --- subscriptions ------------------------------------------------------
Expand All @@ -40,3 +40,13 @@ event_signature = "0x2cceac5555b0ca45a3744ced542f54b56ad2eb45e521962372eef212a2c
[[subscription]]
kind = "block"
chain_id = 11155111

# Status transitions the registry polls back for submitted orders.
[[subscription]]
kind = "intent-status"
venue = "cow"

# The one body-schema version this keeper encodes; install refuses the
# keeper unless every installed venue adapter decodes it.
[venue]
body_version = 1
Loading
Loading