Skip to content

test(#60,#61): topic-0 keccak guards + EthFlow 429/malformed-body tests#312

Closed
lgahdl wants to merge 24 commits into
nullislabs:developfrom
bleu:feat/60-61-topic0-ethflow-tests
Closed

test(#60,#61): topic-0 keccak guards + EthFlow 429/malformed-body tests#312
lgahdl wants to merge 24 commits into
nullislabs:developfrom
bleu:feat/60-61-topic0-ethflow-tests

Conversation

@lgahdl

@lgahdl lgahdl commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

What

Closes #60 — topic-0 keccak guards
Closes #61 — EthFlow 429 + malformed-body error-path tests

Why

#60: Both modules pin a hardcoded event_signature (topic-0) in module.toml. A typo or upstream ABI change would silently suppress every log delivery with no runtime error. The new tests compare the constant against alloy_sol_types::SolEvent::SIGNATURE_HASH, which is computed by the ABI encoder at compile time, so the assert catches any drift.

#61: The existing test suite covered 200, 404, and a generic non-404 error (502). Missing paths:

  • 429 rate-limit: must hit the Err(err) branch (Warn + no marker), not accidentally collapse into the 404 branch (Info + no marker — which suppresses the warning).
  • Malformed JSON body with HTTP 200: the observe path treats any Ok(_) as confirmation; it does not parse the body. The test documents this invariant so a future refactor that adds body parsing knows to keep the marker write unconditional.

Changes

File Change
modules/twap-monitor/src/strategy.rs +1 test: topic0_matches_conditional_order_created_canonical_signature
modules/ethflow-watcher/src/strategy.rs +3 tests: topic-0 guard, 429 rate-limit, malformed-body

mfw78 and others added 24 commits July 9, 2026 01:01
Return a BuildError naming the failing component slot (chain, store, or
extension) from ComponentsBuilder::build, instead of an opaque
anyhow::Result. The leaf ComponentBuilder::build stays anyhow because the
backends fail for heterogeneous reasons (I/O for the store, network for
the chain). Callers that propagate into anyhow keep working through the
std::error::Error conversion.
Pass the executor explicitly with .with_executor(&TokioExecutor) in the
CLI builder chain, rather than relying on the builder's implicit tokio
default. This names the launch root as the point where an embedder or a
non-tokio target substitutes its own executor. Behaviour-preserving: the
binary still spawns on tokio.
The RuntimeAddOns -> RuntimeAddOn rename shortened the parameter type so
the signature now fits within the width limit; re-format to match.
…lock override

Add a clock-reader fixture that reads the WASI wall clock through std on
every event and logs it, plus a harness test that pins a ManualClock,
boots the fixture under that override, dispatches a block, and asserts the
guest logged the pinned instant rather than the ambient host clock. This
proves the WasiClockOverride reaches the guest end to end, not just the
host boot path.
The orderbook rejection envelope has an optional structured data field
(e.g. a minimum-fee quote) that the retired host-error envelope
round-tripped in full but the typed order-rejection dropped. Add
data: option<string> to the record, populate it host-side from the
parsed envelope, and thread it through the SDK mirror and the
wit-bindgen conversion so no rejection information is lost end to end.
Nothing guest-side decodes the orderbook ApiError JSON any more: the
host parses the envelope once into the typed order-rejection and the
classifier dispatches on that. The re-export and its comment described
the retired flow, so remove them from the prelude surface.
…_app_data

The orderbook accepts OrderCreation appData as the bare signed digest
and joins the pre-registered document on its side, so the pre-submit
GET /api/v1/app_data/{hash} was a redundant round-trip against the
same database that validates the submission, plus a 404 dead end
watch-tower does not have.

Bump cowprotocol to 0.2.0, patched to the cow-rs rev that exposes
OrderCreation::new_app_data_hash_only (the OrderCreationAppData::Hash
wire shape, which omits appDataHash - the orderbook's untagged decoder
would otherwise read the body as the full-document shape and reject
it) and scopes the js_sys clock fallback to browser wasm32 so the
constructor's valid_to horizon check reads WASI SystemTime instead of
emitting a wasm-bindgen import no wasmtime host can satisfy.

twap-monitor now submits order.appData verbatim; an unregistered
digest rejects as InvalidAppData and classify_api_error dispatches the
backoff, matching watch-tower exactly. Delete shepherd-sdk's
resolve_app_data module and the orderbook-mock's now-unused appData
GET route, and drop the sdk's serde_json dependency that existed only
for that lookup.
Extract the persistent-state conventions conditional-commitment
modules hand-roll into a world-neutral nexum_sdk::chassis module,
generic over LocalStoreHost alone: the watch-set registry, the
next_block:/next_epoch: gate-key discipline with its is_ready
predicate, and the receipt-keyed submitted:/observed: idempotency
journal. WatchSet::remove drops a watch together with both gate keys,
gates first, so no failure path can orphan a gate.

Acceptance tests run against the composed shepherd-sdk-test MockHost
as an integration test: the mock crate links nexum-sdk externally, so
the unit-test copy of the host traits would not unify. No module is
rewired yet.
…s journal

Route the observe-path idempotency check and marker write through
nexum_sdk::chassis::Journal::observed instead of raw host.get and
host.set, keeping the key shape (observed:{uid}) and the observe-only
flow byte-identical. No behaviour change: 200 records the marker, 404
leaves it absent so re-delivery rechecks, and submit_order is never
called.
… materialiser

Introduce the world-neutral half of the poll loop in
nexum_sdk::chassis: a ConditionalSource trait (one watch in, one
outcome out, generic over the host, no venue-transport
pre-abstraction), a Tick carrying the dispatch instant, and the
RetryAction (try-next-block / backoff / drop) whose effects the
RetryLedger runs through the gate and watch-set stores. RetryAction
moves out of shepherd-sdk; the CoW crate re-exports it so no module
rewires.

shepherd-sdk keeps classify_api_error as the CoW errorType table but
returns the chassis action, and encodes two fixes in the one
classification path: classify_poll_error maps an unrecognised revert
selector with a structured payload to DontTryAgain instead of
re-polling every block forever, and DuplicatedOrder (both spellings)
classifies as already-submitted - the materialiser records the
submitted: receipt and keeps the watch rather than dropping every
future tranche. classify_submit_error widens the table to the whole
CowApiError surface and gives Backoff its producer: a rate-limit
fault with server guidance gates the watch on the epoch clock.

cow::materialise composes the loop: watch-set scan -> gate check ->
source poll -> PollOutcome dispatch, driving submission through the
existing CowApiHost seam with the journal as the idempotency guard
and the retry ledger as the failure dispatch. Acceptance tests run
against the composed shepherd-sdk-test MockHost as integration
tests; no module is rewired yet.
RpcError.data is now Bytes; the test helper takes raw Vec<u8> and wraps
it (Vec -> Bytes is O(1)).
The materialiser logged through the LoggingHost seam, which the
guest tracing capture cannot observe - module tests proving
behaviour identity for chassis ports assert on tracing events.
Route the submit-path diagnostics through the tracing macros with
the wording the legacy twap poll loop established, and give
ConditionalSource a defaulted label so shared log lines attribute
the strategy that produced them.
Reduce strategy.rs to decode and evaluate: log decoding persists
watches through the chassis watch set, and the
getTradeableOrderWithSignature evaluation moves behind
ConditionalSource. The hand-rolled gate keys, watch-update
lifecycle, submitted: markers, and submit-retry dispatch are
deleted in favour of cow::materialise over the chassis stores and
retry ledger, still on the legacy CowApiHost seam. The MockHost
dispatch tests are untouched - the behaviour-identity proof for
the port.
Rework MockLocalStore to mirror the runtime store's shape: namespaced
views over one shared row map, so identical key strings in sibling
namespaces never collide, plus store-wide entry and byte limits that
span namespaces the way one database file does. Fault injection stays
per-view.
Script per-call venue behaviour where MockCowApi replays one response:
a strictly-draining queue of submit outcomes with a configurable
steady-state fallback, per-route response sequences whose final entry
sticks (a terminal order status persists across re-polls), and venue
fault injection that overrides both without consuming them. MockHost
grows a defaulted venue type parameter so the composed host carries
either mock on the same CowApiHost seam. Acceptance tests drive the
chassis materialiser across multi-tick retry, backoff, and outage
scenarios, and module-shaped strategy code against status sequences.
Fixes #60: both twap-monitor and ethflow-watcher now assert at test time
that the hardcoded topic-0 in module.toml equals keccak256 of the
canonical event ABI string via alloy_sol_types::SolEvent::SIGNATURE_HASH.

Fixes #61: adds two missing coverage paths in ethflow-watcher —
  - 429 rate-limit: verify Warn is emitted and no observed: marker is
    written (distinguishes from the 404 indexer-lag path)
  - malformed JSON body with HTTP 200: verify the strategy writes the
    observed: marker because it treats Ok(_) as confirmation without
    parsing the response body

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TfSsJPYDQ1GQGbnSVFxATx
@lgahdl lgahdl requested a review from mfw78 as a code owner July 9, 2026 22:32
@mfw78 mfw78 force-pushed the feat/m1-mock-venue branch 4 times, most recently from 6db87ec to b4734ce Compare July 14, 2026 07:45
@mfw78 mfw78 force-pushed the feat/m1-mock-venue branch 5 times, most recently from 02e6276 to 6076837 Compare July 14, 2026 23:33
@mfw78 mfw78 deleted the branch nullislabs:develop July 14, 2026 23:36
@mfw78 mfw78 closed this Jul 14, 2026
@mfw78 mfw78 reopened this Jul 14, 2026
@mfw78 mfw78 changed the base branch from feat/m1-mock-venue to develop July 14, 2026 23:57
@mfw78 mfw78 closed this Jul 14, 2026
@mfw78 mfw78 reopened this Jul 14, 2026
@mfw78

mfw78 commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Recovered as #343. This PR was accidentally closed when its base branch feat/m1-mock-venue (#241) was squash-merged into dev/m1 and auto-deleted during the M1 train integration. GitHub pins a reopened PR to its pre-close commit and won't repoint it to a clean develop base once the branch was deleted+recreated, so I couldn't cleanly reopen this one onto develop. #343 carries your original test(#60,#61) commit unchanged (authorship preserved), rebased onto develop and compiling. Closing in favour of #343. Sorry for the churn, @lgahdl.

@mfw78 mfw78 closed this Jul 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

EthFlow verification error-path tests incomplete chain: assert subscription topic-0 equals keccak256(event sig)

3 participants