Skip to content

refactor(twap-monitor): port poll loop onto keeper#240

Merged
mfw78 merged 2 commits into
dev/m1from
feat/m1-twap-chassis-port
Jul 14, 2026
Merged

refactor(twap-monitor): port poll loop onto keeper#240
mfw78 merged 2 commits into
dev/m1from
feat/m1-twap-chassis-port

Conversation

@mfw78

@mfw78 mfw78 commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

What

Ports twap-monitor off its hand-rolled poll loop onto the shared keeper composition (ConditionalSource + WatchSet), via the run loop in shepherd_sdk::cow::run, reusing the existing CowApiHost seam.

  • crates/nexum-sdk/src/keeper.rs: ConditionalSource gains a defaulted label() hook (default "conditional") so shared log lines can attribute the emitting strategy.
  • crates/shepherd-sdk/src/cow/run.rs: the run loop's submit-path diagnostics ("submit retry-next-block: ...", "{label} UID divergence: client=... server=...", submitted markers) now go through the guest tracing facade instead of host.log, so they are observable via capture_tracing.
  • modules/twap-monitor/src/strategy.rs: shrinks from ~1140 to ~830 lines by deleting the hand-rolled poll loop (gate keys, watch-update lifecycle, submitted: markers, submit-retry dispatch, BuildError, build_order_creation, compute_uid_hex, outcome_to_update) in favour of a TwapSource: ConditionalSource<H> implementation (label() -> "twap") driven by shepherd_sdk::cow::run. The strategy's own revert-decode dispatch is also dropped in favour of the shared classify_poll_error policy. on_chain_logs now persists via WatchSet::put, and on_block builds a Tick.
  • crates/shepherd-sdk/tests/run.rs: updated the run-loop integration test's log assertion to capture via capture_tracing instead of host.logging.
  • crates/shepherd-sdk/Cargo.toml, modules/twap-monitor/Cargo.toml, Cargo.lock: dependency updates to support the above (tracing as a direct dependency, nexum-sdk-test dev-dependency, strum/thiserror/serde_json dropped from twap-monitor's non-dev dependencies).

Why

twap-monitor carried its own gate bookkeeping, watch lifecycle and submit-retry dispatch, duplicating behaviour the keeper composition already provides generically. Porting it onto ConditionalSource + WatchSet removes that duplication and gives twap-monitor the same submit-path diagnostics and retry policy as every other strategy running on the keeper, while label() keeps shared log lines attributable once more than one composed strategy is running.

Testing

  • cargo fmt --all -- --check
  • cargo clippy --workspace --all-targets -- -D warnings (host and wasm32-wasip2)
  • cargo test --workspace (twap-monitor, shepherd-sdk, nexum-sdk; wall-clock-dependent tests re-run twice)
  • wasm32-wasip2 release build
  • rustdoc link check

Acceptance: the MockHost dispatch tests are byte-identical to base and pass, demonstrating behavioural identity of the port. No .wit files touched, so no new WIT packages or bindgen smoke required.

AI Assistance

Claude (Opus) used for the port.

Closes #147

@mfw78
mfw78 force-pushed the feat/m1-chassis-conditional-source branch from 75e2e89 to 0633f28 Compare July 6, 2026 21:18
@mfw78
mfw78 force-pushed the feat/m1-twap-chassis-port branch from 0a7b417 to 6712cee Compare July 6, 2026 21:18
@mfw78
mfw78 force-pushed the feat/m1-chassis-conditional-source branch from 0633f28 to e0b2392 Compare July 7, 2026 23:05
@mfw78
mfw78 force-pushed the feat/m1-twap-chassis-port branch from 6712cee to e6f49c2 Compare July 7, 2026 23:05
@mfw78
mfw78 force-pushed the feat/m1-chassis-conditional-source branch from e0b2392 to 304b14e Compare July 8, 2026 00:42
@mfw78
mfw78 force-pushed the feat/m1-twap-chassis-port branch from e6f49c2 to 2763a44 Compare July 8, 2026 00:42
@mfw78
mfw78 force-pushed the feat/m1-chassis-conditional-source branch from 304b14e to 013c78c Compare July 8, 2026 01:19
@mfw78
mfw78 force-pushed the feat/m1-twap-chassis-port branch from 2763a44 to 49caf33 Compare July 8, 2026 01:19
@mfw78
mfw78 force-pushed the feat/m1-chassis-conditional-source branch from 013c78c to ed46d40 Compare July 8, 2026 01:44
@mfw78
mfw78 force-pushed the feat/m1-twap-chassis-port branch from 49caf33 to a0f4a80 Compare July 8, 2026 01:44
@mfw78
mfw78 force-pushed the feat/m1-chassis-conditional-source branch from ed46d40 to bad52bd Compare July 8, 2026 02:10
@mfw78
mfw78 force-pushed the feat/m1-twap-chassis-port branch from a0f4a80 to 8bf30dc Compare July 8, 2026 02:10
@mfw78
mfw78 force-pushed the feat/m1-chassis-conditional-source branch from bad52bd to 1c68b61 Compare July 8, 2026 02:19
@mfw78
mfw78 force-pushed the feat/m1-twap-chassis-port branch from 8bf30dc to a9580f9 Compare July 8, 2026 02:19
@mfw78
mfw78 force-pushed the feat/m1-chassis-conditional-source branch from 1c68b61 to c767d21 Compare July 8, 2026 02:22
@mfw78
mfw78 force-pushed the feat/m1-twap-chassis-port branch from a9580f9 to 9dcf0f2 Compare July 8, 2026 02:22
@mfw78
mfw78 force-pushed the feat/m1-chassis-conditional-source branch from c767d21 to 8a22007 Compare July 8, 2026 03:03
@mfw78
mfw78 force-pushed the feat/m1-twap-chassis-port branch from 9dcf0f2 to 06e7f3d Compare July 8, 2026 03:03
@mfw78
mfw78 force-pushed the feat/m1-chassis-conditional-source branch from 8a22007 to 8fec813 Compare July 8, 2026 04:59
@mfw78
mfw78 force-pushed the feat/m1-twap-chassis-port branch from 06e7f3d to 47c5181 Compare July 8, 2026 04:59
@mfw78
mfw78 force-pushed the feat/m1-chassis-conditional-source branch from 8fec813 to 283c938 Compare July 8, 2026 05:30
@mfw78
mfw78 force-pushed the feat/m1-twap-chassis-port branch from 47c5181 to 318103a Compare July 8, 2026 05:30
@mfw78
mfw78 force-pushed the feat/m1-chassis-conditional-source branch from 283c938 to d4fbbdf Compare July 8, 2026 06:21
@mfw78
mfw78 force-pushed the feat/m1-twap-chassis-port branch from 318103a to 4c45d7f Compare July 8, 2026 06:21
@mfw78
mfw78 force-pushed the feat/m1-chassis-conditional-source branch from d4fbbdf to ffa0586 Compare July 8, 2026 06:40
@mfw78
mfw78 force-pushed the feat/m1-twap-chassis-port branch from 4c45d7f to 2598743 Compare July 8, 2026 06:40
@mfw78
mfw78 force-pushed the feat/m1-chassis-conditional-source branch from ffa0586 to 7ef3f63 Compare July 8, 2026 07:06
@mfw78
mfw78 force-pushed the feat/m1-twap-chassis-port branch from 2598743 to 725162b Compare July 8, 2026 07:06
@mfw78
mfw78 force-pushed the feat/m1-chassis-conditional-source branch from 7ef3f63 to c7a609c Compare July 8, 2026 07:20
@mfw78
mfw78 force-pushed the feat/m1-twap-chassis-port branch from 725162b to 745712c Compare July 8, 2026 07:20
@mfw78
mfw78 force-pushed the feat/m1-chassis-conditional-source branch from b4fd03b to 787757a Compare July 8, 2026 08:54
@mfw78
mfw78 force-pushed the feat/m1-twap-chassis-port branch from 15e60c1 to c0cce78 Compare July 8, 2026 08:54
@mfw78
mfw78 force-pushed the feat/m1-chassis-conditional-source branch from 787757a to 79335b9 Compare July 8, 2026 09:02
@mfw78
mfw78 force-pushed the feat/m1-twap-chassis-port branch from c0cce78 to 1f9c305 Compare July 8, 2026 09:02
@mfw78
mfw78 force-pushed the feat/m1-chassis-conditional-source branch from 79335b9 to e734f8e Compare July 8, 2026 09:18
@mfw78
mfw78 force-pushed the feat/m1-twap-chassis-port branch from 1f9c305 to b4ff875 Compare July 8, 2026 09:18
@mfw78
mfw78 force-pushed the feat/m1-chassis-conditional-source branch from e734f8e to 71e0cc8 Compare July 8, 2026 12:09
@mfw78
mfw78 force-pushed the feat/m1-twap-chassis-port branch from b4ff875 to 61d7c89 Compare July 8, 2026 12:09
@mfw78
mfw78 force-pushed the feat/m1-chassis-conditional-source branch from 71e0cc8 to ff15ae2 Compare July 8, 2026 12:20
@mfw78
mfw78 force-pushed the feat/m1-twap-chassis-port branch from 61d7c89 to 831dbea Compare July 8, 2026 12:20
@mfw78
mfw78 force-pushed the feat/m1-chassis-conditional-source branch from ff15ae2 to 6566b23 Compare July 8, 2026 12:49
@mfw78
mfw78 force-pushed the feat/m1-twap-chassis-port branch from 831dbea to 1395917 Compare July 8, 2026 12:49
@mfw78
mfw78 force-pushed the feat/m1-chassis-conditional-source branch from 6566b23 to dd7a7be Compare July 8, 2026 13:36
@mfw78
mfw78 force-pushed the feat/m1-twap-chassis-port branch 2 times, most recently from a888dc2 to b258116 Compare July 8, 2026 13:59
@mfw78
mfw78 force-pushed the feat/m1-chassis-conditional-source branch from b75d9ca to e5da3e1 Compare July 8, 2026 14:07
@mfw78
mfw78 force-pushed the feat/m1-twap-chassis-port branch from b258116 to e67ba0a Compare July 8, 2026 14:08

@lgahdl lgahdl left a comment

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.

Clean port — the deletion of BuildError, WatchUpdate, apply_watch_update, apply_submit_retry, and all hand-rolled gate helpers is the right move. Two things worth calling out explicitly:

Behaviour change, not identity: The PR description says the acceptance tests are "byte-identical to base, demonstrating behaviour identity of the port." This is true for the success path only. poll_one now delegates to classify_poll_error, which maps any ChainError::Rpc with 4+ bytes of data that decode_revert does not recognise to DontTryAgain (permanent drop). The old code always fell back to TryNextBlock. Any live TWAP whose handler contract emits an unrecognised custom error will have its watches permanently dropped after this merge. That is the right policy direction, but it should be stated as a deliberate tightening, not subsumed under "byte-identical."

Silent bug fix: The deleted apply_submit_retry wrote Backoff gates as the raw string "next_epoch:{owner_hex}:{hash_hex}" instead of going through Gates::set_next_epoch. If that key format ever differed from what is_ready read, the backoff gate was invisible to the gating check. The new path through RetryLedger::apply -> Gates::set_next_epoch is provably consistent. Worth a sentence in the PR description.

// fault warrants its own diagnostic here.
Err(err) => {
if let ChainError::Fault(fault) = &err {
tracing::warn!("eth_call failed ({fault}); retrying next block");

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.

Only transport faults are logged here; when classify_poll_error returns DontTryAgain for an unrecognised Rpc revert the watch disappears with zero diagnostic at the poll site (materialise does not log for DontTryAgain either). The old code always emitted a warn for this path — an operator has no visibility into why a TWAP watch dropped.

Consider logging for the Rpc case too after the classify_poll_error call, e.g.: if matches!(outcome, PollOutcome::DontTryAgain) { tracing::warn!("eth_call unrecognised revert for {}; dropping watch", watch.key()); }

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.

Cross-ref for when this thread gets swept: the diagnostic half of this finding is superseded by the ADR-0013 Verdict seam (#334) — Verdict::Invalid { reason: [u8; 4] } now carries the selector that this path drops silently, and the strategy's drop diagnostic logs it. So on the train tip the watch no longer disappears with zero trace; this branch's transport-only logging is fine as an interim state.

Comment on lines +316 to +318
/// Short strategy name compositions prefix shared log lines with
/// (for example `"twap"`). Diagnostic only - no behaviour keys
/// off it.

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.

Grammar makes this hard to parse: "compositions" should be "implementations", and "prefix ... with" inverts the normal subject-verb order.

Suggested change
/// Short strategy name compositions prefix shared log lines with
/// (for example `"twap"`). Diagnostic only - no behaviour keys
/// off it.
/// Short name that implementations return to prefix shared log lines.
/// Diagnostic only - no behaviour keys off this value.
/// Default: `"conditional"`.

Comment on lines +109 to +111
/// source cannot decode polls again next block rather than tearing
/// down the sweep.
struct TwapSource;

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.

"Row" is informal and inconsistent with the SDK's terminology (watch, WatchSet). The rest of the PR uses "watch" throughout.

Suggested change
/// source cannot decode polls again next block rather than tearing
/// down the sweep.
struct TwapSource;
/// and evaluate `getTradeableOrderWithSignature` on chain. A watch
/// whose params cannot be decoded returns `TryNextBlock` rather than
/// tearing down the sweep.

@mfw78
mfw78 force-pushed the feat/m1-chassis-conditional-source branch from e5da3e1 to dc6b70b Compare July 8, 2026 22:33
@mfw78
mfw78 force-pushed the feat/m1-twap-chassis-port branch 2 times, most recently from 2195569 to d4db1a0 Compare July 8, 2026 23:05
@mfw78
mfw78 force-pushed the feat/m1-chassis-conditional-source branch from dc6b70b to d03e955 Compare July 8, 2026 23:05
@mfw78
mfw78 force-pushed the feat/m1-twap-chassis-port branch 2 times, most recently from 9f6016a to dbc8f12 Compare July 8, 2026 23:29
@mfw78
mfw78 force-pushed the feat/m1-chassis-conditional-source branch from 003de79 to 757f00b Compare July 8, 2026 23:29
@mfw78
mfw78 force-pushed the feat/m1-twap-chassis-port branch from dbc8f12 to cf2a241 Compare July 9, 2026 02:09
mfw78 added 2 commits July 14, 2026 23:21
The run logged through the LoggingHost seam, which the
guest tracing capture cannot observe - module tests proving
behaviour identity for keeper 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 keeper 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::run over the keeper stores and
retry ledger, still on the legacy CowApiHost seam. The MockHost
dispatch tests are untouched - the behaviour-identity proof for
the port.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr-train Part of the current stacked PR train; lands in sequence, do not merge out of order.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants