Skip to content

fix(twap-monitor): skip submit_order when submitted:{uid} already in store (COW-1085)#64

Closed
brunota20 wants to merge 1 commit into
feat/m5-docker-image-and-cifrom
bruno/cow-1085-twap-skip-submitted-uid
Closed

fix(twap-monitor): skip submit_order when submitted:{uid} already in store (COW-1085)#64
brunota20 wants to merge 1 commit into
feat/m5-docker-image-and-cifrom
bruno/cow-1085-twap-skip-submitted-uid

Conversation

@brunota20

Copy link
Copy Markdown

What this PR does

Short-circuit submit_ready when submitted:{uid} is already in local-store, so a TWAP child that succeeded on a prior poll-tick does not get re-submitted on the next block. Fixes the DuplicatedOrder Warn observed in the 2026-06-23 Sepolia soak (see COW-1085 for the live evidence).

Why

composable.getTradeableOrderWithSignature returns Ready whenever the conditional order has a valid (sell, buy) tuple for the current block. It does not know shepherd has already POSTed the resulting child to the orderbook. The strategy already wrote submitted:{uid} after a successful submit, but the next poll-tick polled the chain and submitted again, producing a wasted orderbook call and a misleading Warn.

The Warn dispatch itself is correct (Drop, no retry storm), so the consequence is bounded — but on a 7-day soak (COW-1031) with N TWAPs × K children, this becomes N×K spurious Warn lines that operators have to learn to ignore.

Change

  1. compute_uid_hex(chain_id, order, owner) helper — computes the canonical 56-byte UID deterministically from on-chain inputs via OrderData::uid(domain, owner). Mirrors what the orderbook derives server-side from the signed payload.
  2. Idempotency check at top of submit_ready — looks up submitted:{uid} and short-circuits with an Info log before any network work (no appData GET, no submit_order).
  3. Marker key now uses the client-computed UID instead of the server-returned UID, so the read and write paths agree. In production the two values are the same canonical derivation; a divergence (e.g. a mock orderbook returning a stub UID, or a real protocol drift) now triggers a Warn (twap UID divergence: client=… server=…).

Tests (24 total, all green)

  • Updated poll_ready_submits_order_and_persists_submitted_uid and poll_ready_resolves_non_empty_app_data_then_submits: previously hardcoded submitted:0xfeedface against the MockHost's stub UID; now compute the expected key via compute_uid_hex(SEPOLIA, &ready_order, owner). Both also assert the divergence Warn fires (since 0xfeedface is not the canonical UID).
  • New poll_ready_skips_submit_when_submitted_uid_already_in_store: seeds the marker, dispatches a block, asserts chain.call_count() == 1 (poll still happens), cow_api.call_count() == 0 (no submit), cow_api.request_calls().len() == 0 (no appData GET), and the Info log line appears verbatim.
running 24 tests
test result: ok. 24 passed; 0 failed; 0 ignored

cargo clippy -p twap-monitor --all-targets -- -D warnings clean. cargo fmt --check clean. cargo build -p twap-monitor --target wasm32-wasip2 --release clean.

Live validation

The fix is not yet on the soak VM; deploy after review. Expected post-deploy behaviour:

  • Each successful TWAP child submit produces exactly one submitted {uid} Info line.
  • Subsequent poll-ticks for the same child in the same validity window produce one twap {uid} already submitted; skipping poll re-submit Info line and no orderbook calls.
  • Zero DuplicatedOrder Warn lines for the lifecycle-correct path.

Linear

Resolves COW-1085.

AI assistance disclosure: drafted by Claude (Opus 4.7, 1M context) following the 2026-06-23 soak inspection.

…store (COW-1085)

`getTradeableOrderWithSignature` returns the same Ready tuple in every
poll-tick during a TWAP child's validity window — the on-chain
conditional order has no way to know shepherd already POSTed it. The
strategy already wrote a `submitted:{uid}` marker after a successful
submit, but the next poll-tick polled the chain and submitted again,
producing a wasted orderbook call and a misleading
`DuplicatedOrder` Warn line in every soak that runs a TWAP.

Live evidence (2026-06-23 Sepolia soak):

    10:02:36.784  INFO  poll watch:0x8fab71c0...:0x93b1626c... -> Ready
    10:02:37.190  INFO  submitted submitted:0xd7116bd2...
    10:02:48.870  INFO  poll watch:0x8fab71c0...:0x93b1626c... -> Ready
    10:02:49.855  WARN  submit dropped watch (400): orderbook error
                       (DuplicatedOrder): order already exists

The first submission succeeded (`GET /api/v1/orders/0xd7116bd2...`
returns `status: fulfilled`); the second was wasted work.

The fix: at the top of `submit_ready`, compute the client-side UID
deterministically from the on-chain `(order, owner, chain)` tuple via
`OrderData::uid` and check `submitted:{uid}` in local-store; skip the
submit (and the appData resolve that precedes it) when the marker
exists. The marker write site is also updated to use the
client-computed UID for the key so the read and write paths agree
(in production the server-returned UID is the same value — both sides
derive it from the canonical `digest || owner || valid_to` layout —
and a divergence is now surfaced via a Warn).

Tests (24, all green natively + wasm32-wasip2):

  * Existing `poll_ready_submits_order_and_persists_submitted_uid` and
    `poll_ready_resolves_non_empty_app_data_then_submits` updated to
    compute the expected marker key via `compute_uid_hex` instead of
    hardcoding `submitted:0xfeedface` (the mock orderbook's stub UID,
    which now triggers the divergence Warn so we also assert that).
  * New `poll_ready_skips_submit_when_submitted_uid_already_in_store`:
    seeds the marker, dispatches a block tick, asserts
    `submit_order` (and the preceding appData resolve) are NOT called
    and that the expected Info log appears.

Out of scope (deferred): the same idempotency pattern could be
applied to ethflow-watcher's `observed:{uid}` marker (already correct
there — the GET-not-POST design makes this naturally idempotent).
@linear-code

linear-code Bot commented Jun 23, 2026

Copy link
Copy Markdown

COW-1085

@brunota20

Copy link
Copy Markdown
Author

Live validation note (2026-06-23 14:00 UTC)

Combined with the rest of the redesign + fix bundle on deploy/cow-1076-observe-on-m5 and deployed to the Sepolia soak VM. Container Up healthy with the new image since 13:56:50 UTC. Will accumulate live evidence in the next observation window.

Companion PRs (same redesign / soak deploy bundle)

PR Concern Layer
#62 ethflow-watcher: observe + verify (replaces submit) M2 (BLEU-833 alternative)
#63 ethflow-watcher: same redesign with BLEU-855 split M3/M4 (COW-1074 base)
#64 twap-monitor: skip submit on already-submitted UID M5 (COW-1085)
#65 nexum-engine: gap-closed log on alloy-internal reconnect M5 (COW-1086)

Each is single-concern; the deploy branch is the cumulative integration point and will fold into latest once the stack lands.

@brunota20

Copy link
Copy Markdown
Author

Superseded by deploy branch commit 94eff8b fix(twap-monitor): skip submit_order when submitted:{uid} already in store (COW-1085) (now in dev/m5-base). Closing as merged via different commit.

@brunota20 brunota20 closed this Jun 24, 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.

1 participant