Skip to content

twap: grant one next-block retry before dropping on InvalidEip1271Signature - #473

Merged
mfw78 merged 1 commit into
dev/m1from
feat/m4-eip1271-one-block-retry
Jul 23, 2026
Merged

twap: grant one next-block retry before dropping on InvalidEip1271Signature#473
mfw78 merged 1 commit into
dev/m1from
feat/m4-eip1271-one-block-retry

Conversation

@mfw78

@mfw78 mfw78 commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

What

Adds RetryAction::DropOnRepeat: the first InvalidEip1271Signature refusal for a watch retries on the next block instead of dropping; a repeat refusal on a later block drops as before. Tracked via a refused: block marker on the watch, cleared on an accepted submit. Reclassifies the InvalidEip1271Signature row in classification.toml/classification_data.rs from drop to drop-on-repeat. The denied wire error stays coarse, so cow_venue::classify_denied re-derives the action from the errorType prefix in the detail string. That re-derivation lives on the composable-cow sweep, which is the path twap-monitor runs (composable_cow::run) and therefore the path this issue is about. The generic videre_sdk::keeper::retry_action still maps Denied straight to Drop: it cannot call classify_denied without videre-sdk depending on cow-venue, which inverts the layering the venue-agnostic gate enforces. The only module on that generic path today is stop-loss, which submits unsigned pre-sign orders and so cannot receive InvalidEip1271Signature. Tracked for a venue-injectable classifier rather than papered over.

Why

A first-time TWAP user's Safe wiring and ComposableCoW.create() can land in the same block as the indexed event. Shepherd polls and submits inside that block, faster than an orderbook node can verify against its own head, so a signature that is valid one block later is rejected and the watch is dropped permanently. Treating only a repeated refusal as permanent absorbs the one-block indexing race while still dropping genuinely broken signatures.

Closes #320

Testing

nexum-sdk and videre-sdk keeper unit tests cover the marker set/clear/repeat-drop lifecycle; cow-venue adapter and classification tests cover the drop-on-repeat row and classify_denied prefix recovery; composable-cow sweep tests cover the marker clear on acceptance.

AI Assistance

Implemented with AI assistance under human direction and review.

@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.

The marker lifecycle, classification table wiring, and string-prefix matching are all solid where they were actually applied: Retrier::apply's DropOnRepeat arm correctly distinguishes "later block" from "same block" (verified: tick.block > block -> drop, tick.block <= block -> no-op), clearing on accept is idempotent and WatchSet::remove also deletes the marker so nothing orphans, classification.toml's row moved cleanly to drop-on-repeat with no dangling reference to the old drop behavior, and classify_denied's prefix match is anchored (split_once(':') then exact table lookup, not a substring/contains search) so there's no false-positive risk from an unrelated error's text.

But the PR's central claim — that this logic is applied on "both the videre-sdk keeper sweep and the legacy composable-cow sweep" — is false as shipped. I checked crates/videre-sdk/src/keeper.rs directly at this commit: retry_action's Denied arm is completely unchanged by this diff and still reads:

VenueFault::UnknownVenue
| VenueFault::InvalidBody(_)
| VenueFault::Unsupported
| VenueFault::Denied(_) => RetryAction::Drop,

(crates/videre-sdk/src/keeper.rs:194-197). Only crates/composable-cow/src/sweep.rs:64-67 was actually edited to special-case VenueFault::Denied(detail) => classify_denied(detail). That means twap-monitor — which per #469 runs on the generic videre_sdk::Keeper::sweep path, and which is the exact scenario this PR's "Why" section describes ("a first-time TWAP user's Safe wiring... lands in the same block") — still drops the watch permanently on the FIRST InvalidEip1271Signature refusal. Only stop-loss (composable-cow's legacy path, per #471) actually gets the one-block grace this PR adds. The bug this PR closes #320 for is still live for its primary named beneficiary.

Fix: add the same VenueFault::Denied(detail) => classify_denied(detail) branch inside videre_sdk::keeper::retry_action's Denied arm (or fold the re-derivation into retry_action itself so both sweeps call through one place, closing the duplication risk at the same time).

@mfw78
mfw78 force-pushed the feat/m4-conditional-order-removed branch 2 times, most recently from c0e34e0 to 2e89336 Compare July 23, 2026 11:31
Base automatically changed from feat/m4-conditional-order-removed to dev/m1 July 23, 2026 11:32
…nature

A first-time user's Safe wiring and ComposableCoW create() land in one
block, so the orderbook rejects the first submission against its own
head and the keeper dropped a valid watch. The classification table
gains a drop-on-repeat action, the retry ledger records the refused
block and gates one next-block retry, and a denied refusal re-enters
the table by its errorType prefix so the grace survives the coarse
venue-error collapse. A repeat on a later block still drops.
@mfw78

mfw78 commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

The finding is real and the PR body was overclaiming, so thank you. Two corrections though, because the module attribution is inverted and the suggested fix cannot be implemented as written.

Which module is on which path. It is the other way round. twap-monitor runs the composable-cow sweep: modules/twap-monitor/src/strategy.rs:21 is use composable_cow::{LegacyRevertAdapter, Verdict, run}; and its module doc says retry dispatch lives in composable_cow::run. stop-loss is the one on the generic path, calling videre_sdk::keeper::retry_action directly at modules/examples/stop-loss/src/strategy.rs:121. So the module this PR's "Why" section describes does get the grace; the ungraced path belongs to stop-loss.

Why the suggested fix is not available. Adding VenueFault::Denied(detail) => classify_denied(detail) to videre_sdk::keeper::retry_action would require videre-sdk to depend on cow-venue, since that is where classify_denied lives. It does not and must not: a CoW classifier inside the generic keeper SDK is precisely what scripts/check-venue-agnostic.sh exists to block. The duplication you are pointing at is real, but the single-place fix has to be a seam that lets a venue inject its classification, not a direct call.

Severity today. stop-loss builds CowIntent::Order, an unsigned order on the pre-sign path, so the orderbook holds it as signature-pending and it cannot receive InvalidEip1271Signature. No shipped module is therefore missing behaviour it can currently exercise. That is luck rather than design, which is why it is worth tracking.

What I changed. The PR body no longer claims the re-derivation applies on both sweeps. It now says where it lives, why the generic path cannot carry it, and which module is affected. Filed nullislabs/videre-nexum-module#11 for the seam: a venue-supplied denial classifier so both paths derive the same RetryAction without a generic crate naming a venue-specific symbol.

Separately, this car arrived with cow-orderbook-only and rustdoc already failing on its pre-ripple head 428527b. The rustdoc failure cleared against the current base. The gate failure was real: a comment in crates/cow-venue/data/classification.toml named ComposableCoW, which the layering gate forbids in that crate. Reworded to "conditional-order registration" rather than loosening the gate.

@mfw78
mfw78 merged commit 9cac7c3 into dev/m1 Jul 23, 2026
7 checks passed
@mfw78
mfw78 deleted the feat/m4-eip1271-one-block-retry branch July 23, 2026 11:52
mfw78 added a commit that referenced this pull request Jul 27, 2026
…nature (#473)

A first-time user's Safe wiring and ComposableCoW create() land in one
block, so the orderbook rejects the first submission against its own
head and the keeper dropped a valid watch. The classification table
gains a drop-on-repeat action, the retry ledger records the refused
block and gates one next-block retry, and a denied refusal re-enters
the table by its errorType prefix so the grace survives the coarse
venue-error collapse. A repeat on a later block still drops.
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.

2 participants