Skip to content
Closed
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
10 changes: 5 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crates/composable-cow/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ nexum-sdk = { git = "https://github.com/nullislabs/nexum-runtime", rev = "2ed882
# `run` slice: the keeper run over the typed CoW client on the
# `videre:venue/client` seam.
cow-venue = { path = "../cow-venue", features = ["client", "assembly"], optional = true }
videre-sdk = { git = "https://github.com/nullislabs/videre-nexum-module", rev = "fd8af027d9ae84823f8fcdcc6902b3af80cab451", optional = true }
videre-sdk = { git = "https://github.com/nullislabs/videre-nexum-module", rev = "9ab15152b9279974cb42c3ee3aff054344f6050d", optional = true }
tracing = { workspace = true, optional = true }

[features]
Expand Down
19 changes: 8 additions & 11 deletions crates/composable-cow/src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@
//! never dropped.
//!
//! Store faults abort the run (the next tick replays it); a submission
//! failure folds into a [`RetryAction`], a `denied` refusal re-entering
//! the CoW classification by its errorType prefix ([`classify_denied`]).
//! failure folds into a [`RetryAction`] through [`CowFaults`], which the
//! reconcile pass reads too, so both paths classify a refusal alike.
//! Diagnostics go through the guest `tracing` facade.

use alloy_primitives::{Address, Bytes, hex};
use cow_venue::assembly::{gpv2_to_order_data, order_data_to_body};
use cow_venue::{CowClient, CowIntent, CowIntentBody, CowVenue, SignedOrder, classify_denied};
use cow_venue::{CowClient, CowFaults, CowIntent, CowIntentBody, CowVenue, SignedOrder};
use cowprotocol::GPv2OrderData;
use nexum_sdk::host::{Fault, ListQuery, LocalStoreHost};
use nexum_sdk::keeper::{
Expand All @@ -26,11 +26,10 @@ use nexum_sdk::keeper::{
};
use std::task::Poll;

use videre_sdk::FaultPolicy as _;
use videre_sdk::client::poll_once;
use videre_sdk::keeper::{retry_action, submission_key};
use videre_sdk::{
ClientError, IntentBody as _, SubmitOutcome, Venue as _, VenueFault, VenueTransport,
};
use videre_sdk::keeper::submission_key;
use videre_sdk::{ClientError, IntentBody as _, SubmitOutcome, Venue as _, VenueTransport};

use crate::{NextPoll, Verdict};

Expand All @@ -57,6 +56,7 @@ where
&journal,
tick,
videre_sdk::DEFAULT_RECONCILE_BUDGET,
&CowFaults,
)) {
Poll::Ready(res) => {
res?;
Expand Down Expand Up @@ -429,10 +429,7 @@ where
tracing::error!("intent body encode failed: {err}");
}
Err(ClientError::Venue(fault)) => {
let action = match &fault {
VenueFault::Denied(detail) => classify_denied(detail),
other => retry_action(other),
};
let action = CowFaults.action(&fault);
Retrier::new(host).apply(commitment, action, tick)?;
match action {
RetryAction::TryNextBlock => tracing::warn!("submit retry-next-block: {fault}"),
Expand Down
34 changes: 34 additions & 0 deletions crates/composable-cow/tests/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1946,3 +1946,37 @@ fn an_insufficient_valid_to_keeps_the_commitment() {

assert!(host.store.snapshot().contains_key(&key));
}

/// The reconcile pass must read the venue's policy, not the platform
/// default. A stranded reservation answered with a receipt this keeper
/// cannot correlate is kept, because the orderbook dedupes on the order
/// uid and the order may be on the book; releasing it forgets a submit
/// that is owed.
#[test]
fn reconcile_keeps_a_reservation_the_cow_policy_can_retry() {
let host = MockHost::new();
seed_commitment(&host);
let key = "cow:0xdeadbeef";
Journal::submitted(&host)
.reserve(key, b"body")
.expect("reserve");

let venue = MockVenue::default();
venue.enqueue_submit(Err(VenueFault::ReceiptMismatch));

run(
&host,
&client(&venue),
&src(|_, _, _, _| Verdict::TryNextBlock {
reason: Selector::ZERO,
}),
&sample_tick(),
)
.unwrap();

assert_eq!(
Journal::submitted(&host).mark(key).unwrap(),
Some(Mark::Reserved),
"the reservation survives a fault the venue can retry",
);
}
6 changes: 3 additions & 3 deletions crates/cow-venue/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ workspace = true
borsh = { workspace = true, optional = true }
# Source of the `IntentBody` derive and trait the version enum implements,
# and the typed intent client the `client` slice binds to the CoW venue.
videre-sdk = { git = "https://github.com/nullislabs/videre-nexum-module", rev = "fd8af027d9ae84823f8fcdcc6902b3af80cab451", optional = true }
videre-sdk = { git = "https://github.com/nullislabs/videre-nexum-module", rev = "9ab15152b9279974cb42c3ee3aff054344f6050d", optional = true }
# `client` slice only: the keeper `RetryAction` the generated
# classification table maps each errorType to. The TOML parse happens in
# `build.rs`, so serde/toml/thiserror are build- and dev-only and never
Expand All @@ -39,7 +39,7 @@ serde_json = { workspace = true, optional = true }
http = { workspace = true, optional = true }
url = { workspace = true, optional = true }
# The registration seam: `VenueInvoker`, `VenueRegistry`, and `Liveness`.
videre-host = { git = "https://github.com/nullislabs/videre-nexum-module", rev = "fd8af027d9ae84823f8fcdcc6902b3af80cab451", optional = true }
videre-host = { git = "https://github.com/nullislabs/videre-nexum-module", rev = "9ab15152b9279974cb42c3ee3aff054344f6050d", optional = true }
# `BoxFuture` plus `FutureExt::boxed`, the shape `VenueInvoker` returns.
futures = { workspace = true, optional = true }
# The venue owns its HTTP client; there is no scoped host import left to
Expand All @@ -59,7 +59,7 @@ serde = { workspace = true }
toml = { workspace = true }
thiserror = { workspace = true }
# The conformance kit: holds the body codec to its published vector set.
videre-test = { git = "https://github.com/nullislabs/videre-nexum-module", rev = "fd8af027d9ae84823f8fcdcc6902b3af80cab451" }
videre-test = { git = "https://github.com/nullislabs/videre-nexum-module", rev = "9ab15152b9279974cb42c3ee3aff054344f6050d" }
# Parity tests: the upstream `retry_hint()` the shipped table is
# reconciled against.
cowprotocol = { version = "0.2.0", default-features = false }
Expand Down
93 changes: 93 additions & 0 deletions crates/cow-venue/src/classification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,39 @@ pub fn is_already_submitted(error_type: OrderbookApiErrorType) -> bool {
table().is_already_submitted(&error_type)
}

/// How long a receipt this keeper cannot correlate holds a commitment
/// out of the rotation.
pub const RECEIPT_BACKOFF_S: u64 = 300;

/// The CoW orderbook's fault policy.
///
/// A submission is an order keyed by its uid, and the orderbook dedupes
/// on that uid, so re-sending one it already accepted comes back as
/// already held rather than executed twice. That idempotency is what
/// makes a receipt this keeper cannot correlate worth retrying: the
/// order may be on the book, and removing the commitment would forget
/// it. `videre` cannot assume that of a venue, which is why it is
/// asserted here rather than in the default.
///
/// Denials route through the shipped table, so the reconcile pass and
/// the submit path read the same policy. They did not before: reconcile
/// took the platform default while the submit path took the table.
#[derive(Clone, Copy, Debug, Default)]
pub struct CowFaults;

impl videre_sdk::FaultPolicy for CowFaults {
fn action(&self, fault: &videre_sdk::VenueFault) -> RetryAction {
use videre_sdk::VenueFault;
match fault {
VenueFault::Denied(detail) => classify_denied(detail),
VenueFault::InvalidReceipt | VenueFault::ReceiptMismatch => RetryAction::Backoff {
seconds: RECEIPT_BACKOFF_S,
},
other => videre_sdk::retry_action(other),
}
}
}

/// Retry action for a coarse `denied` refusal: the `{errorType}:`
/// prefix re-enters the table.
///
Expand Down Expand Up @@ -376,4 +409,64 @@ mod tests {
assert!(first.contains_key("error-type"));
assert!(first.contains_key("action"));
}

/// The orderbook dedupes on the order uid, so a receipt this keeper
/// cannot correlate may still name an order that is on the book.
/// Removing the commitment would forget it.
#[test]
fn a_receipt_fault_backs_off_rather_than_removing() {
use videre_sdk::FaultPolicy as _;

for fault in [
videre_sdk::VenueFault::ReceiptMismatch,
videre_sdk::VenueFault::InvalidReceipt,
] {
assert_eq!(
CowFaults.action(&fault),
RetryAction::Backoff {
seconds: RECEIPT_BACKOFF_S,
},
"{fault:?}",
);
assert!(!CowFaults.is_terminal(&fault), "{fault:?} must not release");
}
}

/// Denials route through the shipped table, so the reconcile pass
/// reads the same policy the submit path does. It took the platform
/// default before, which knows nothing of the table.
#[test]
fn a_denial_routes_through_the_table() {
use videre_sdk::FaultPolicy as _;

let clearable =
videre_sdk::VenueFault::Denied("InsufficientBalance: not enough".to_owned());
assert_eq!(
CowFaults.action(&clearable),
RetryAction::Backoff { seconds: 600 },
);
assert!(!CowFaults.is_terminal(&clearable));

let permanent = videre_sdk::VenueFault::Denied("WrongOwner: not yours".to_owned());
assert_eq!(CowFaults.action(&permanent), RetryAction::Drop);
assert!(CowFaults.is_terminal(&permanent));
}

/// Everything the venue says nothing special about keeps the
/// platform default.
#[test]
fn other_faults_keep_the_platform_default() {
use videre_sdk::FaultPolicy as _;

for fault in [
videre_sdk::VenueFault::Timeout,
videre_sdk::VenueFault::InvalidBody("bad".to_owned()),
] {
assert_eq!(
CowFaults.action(&fault),
videre_sdk::retry_action(&fault),
"{fault:?}",
);
}
}
}
4 changes: 3 additions & 1 deletion crates/cow-venue/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ pub use cowprotocol::Chain;
pub use transport::{OrderbookHttp, Transport};

#[cfg(feature = "client")]
pub use classification::{ClassificationTable, classify, classify_denied, is_already_submitted};
pub use classification::{
ClassificationTable, CowFaults, classify, classify_denied, is_already_submitted,
};
#[cfg(feature = "client")]
pub use client::{CowClient, CowVenue, VENUE_ID, intent_id};
4 changes: 2 additions & 2 deletions crates/shepherd-engine/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ path = "src/main.rs"
[dependencies]
nexum-launch = { git = "https://github.com/nullislabs/nexum-runtime", rev = "2ed882f21e685921cd27d1edec870e8adbb312aa" }
nexum-runtime = { git = "https://github.com/nullislabs/nexum-runtime", rev = "2ed882f21e685921cd27d1edec870e8adbb312aa" }
videre-host = { git = "https://github.com/nullislabs/videre-nexum-module", rev = "fd8af027d9ae84823f8fcdcc6902b3af80cab451" }
videre-host = { git = "https://github.com/nullislabs/videre-nexum-module", rev = "9ab15152b9279974cb42c3ee3aff054344f6050d" }

# The CoW venue, a native `videre_host::VenueInvoker` linked into this
# binary. It was a guest wasm component the operator wired by path until
Expand Down Expand Up @@ -43,4 +43,4 @@ nexum-runtime = { git = "https://github.com/nullislabs/nexum-runtime", rev = "2e
# The versioned status-body codec the intent-status envelope carries; the
# tests build a `StatusBody` and encode it into an `IntentStatusUpdate`.
# Pinned to the same videre rev as `videre-host`.
videre-status-body = { git = "https://github.com/nullislabs/videre-nexum-module", rev = "fd8af027d9ae84823f8fcdcc6902b3af80cab451" }
videre-status-body = { git = "https://github.com/nullislabs/videre-nexum-module", rev = "9ab15152b9279974cb42c3ee3aff054344f6050d" }
2 changes: 1 addition & 1 deletion modules/ccow-monitor/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ crate-type = ["cdylib"]
composable-cow = { path = "../../crates/composable-cow", features = ["run"] }
cow-venue = { path = "../../crates/cow-venue", features = ["client"] }
nexum-sdk = { git = "https://github.com/nullislabs/nexum-runtime", rev = "2ed882f21e685921cd27d1edec870e8adbb312aa" }
videre-sdk = { git = "https://github.com/nullislabs/videre-nexum-module", rev = "fd8af027d9ae84823f8fcdcc6902b3af80cab451" }
videre-sdk = { git = "https://github.com/nullislabs/videre-nexum-module", rev = "9ab15152b9279974cb42c3ee3aff054344f6050d" }
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 }
Expand Down
2 changes: 1 addition & 1 deletion modules/ethflow-watcher/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ crate-type = ["cdylib"]

[dependencies]
nexum-sdk = { git = "https://github.com/nullislabs/nexum-runtime", rev = "2ed882f21e685921cd27d1edec870e8adbb312aa" }
videre-sdk = { git = "https://github.com/nullislabs/videre-nexum-module", rev = "fd8af027d9ae84823f8fcdcc6902b3af80cab451" }
videre-sdk = { git = "https://github.com/nullislabs/videre-nexum-module", rev = "9ab15152b9279974cb42c3ee3aff054344f6050d" }
cow-venue = { path = "../../crates/cow-venue", features = ["client", "assembly"] }
cowprotocol = { version = "0.2.0", default-features = false }
alloy-primitives = { version = "1.6", default-features = false, features = ["std"] }
Expand Down
Loading