Skip to content
Merged
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
1 change: 1 addition & 0 deletions Cargo.lock

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

3 changes: 3 additions & 0 deletions crates/cow-venue/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ toml = { workspace = true }
thiserror = { workspace = true }
# The conformance kit: holds the body codec to its published vector set.
videre-test = { path = "../videre-test" }
# Parity tests only: the upstream errorType enum and `retry_hint()` the
# shipped table is reconciled against. Never a runtime dependency.
cowprotocol = { version = "0.2.0", default-features = false }

[features]
# The body-type + codec slice ships by default; the `client` slice layers
Expand Down
35 changes: 20 additions & 15 deletions crates/cow-venue/data/classification.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,28 +33,33 @@
#
# Relationship to `cowprotocol::ApiError::retry_hint()`. The upstream
# `cowprotocol` crate (a shepherd-sdk dependency) also classifies
# orderbook `errorType`s, via `RetryHint`. This table is deliberately
# NOT delegated to it: it is shepherd's own, more conservative retry
# policy, kept as data of record here so a non-Rust author owns it and
# so the guest `client` slice stays free of the upstream error module.
# The two intentionally diverge on several types - e.g. this table drops
# `InvalidEip1271Signature`, `InsufficientBalance`, `InsufficientAllowance`
# and `InvalidAppData` where upstream retries or backs off, and backs off
# `TooManyLimitOrders` for 30s rather than an hour. These are ratified
# shepherd decisions (a permanent-looking contract rejection is dropped
# rather than retried every block); revisit them here, not by switching
# the source of truth to `RetryHint`.
# orderbook `errorType`s, via `RetryHint`. Ratified: this table, not
# `RetryHint`, is shepherd's classification source of truth. It is
# shepherd's own, more conservative retry policy, kept as data of record
# here so a non-Rust author owns it and so the guest `client` slice
# stays free of the upstream error module. The ratified divergences
# (a permanent-looking contract rejection is dropped rather than
# retried, and the limit-order backoff is shorter) are exactly:
#
# InvalidEip1271Signature drop upstream: retry next block
# InsufficientBalance drop upstream: backoff 10 min
# InsufficientAllowance drop upstream: backoff 10 min
# InvalidAppData drop upstream: backoff 60 s
# TooManyLimitOrders backoff 30 s upstream: backoff 1 h
#
# Every `error-type` below must name a member of the upstream orderbook
# errorType enum (`cowprotocol::OrderbookApiErrorType`). Parity tests
# reject phantom types and pin the divergence set to the list above, so
# both a data edit and an upstream policy change force re-ratification.
# Revisit policy here, not by switching the source of truth to
# `RetryHint`.

# --- Transient: retry on the next block ------------------------------

[[entry]]
error-type = "InsufficientFee"
action = "try-next-block"

[[entry]]
error-type = "PriceExceedsMarketPrice"
action = "try-next-block"

# --- Throttle: wait, then retry --------------------------------------

# The account already holds the maximum number of open limit orders. A
Expand Down
60 changes: 60 additions & 0 deletions crates/cow-venue/src/classification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,66 @@ mod tests {
);
}

/// Every listed `error-type` names a member of the upstream
/// orderbook errorType enum, in its exact wire spelling: no phantom
/// rows.
#[test]
fn every_row_names_a_real_error_type() {
let entries = parse_and_validate(CLASSIFICATION_TOML).expect("shipped data is valid");
for entry in &entries {
let kind = cowprotocol::OrderbookApiErrorType::from(entry.error_type.as_str());
assert!(
!matches!(kind, cowprotocol::OrderbookApiErrorType::Unknown(_)),
"phantom errorType {}",
entry.error_type,
);
assert_eq!(kind.as_str(), entry.error_type, "wire spelling");
}
}

/// The table's divergence from upstream `retry_hint()` is exactly
/// the ratified set in the data header. A data edit or an upstream
/// policy change lands here and forces re-ratification.
#[test]
fn divergence_from_upstream_is_exactly_the_ratified_set() {
const RATIFIED: [&str; 5] = [
"InsufficientAllowance",
"InsufficientBalance",
"InvalidAppData",
"InvalidEip1271Signature",
"TooManyLimitOrders",
];
let entries = parse_and_validate(CLASSIFICATION_TOML).expect("shipped data is valid");
let mut divergent: Vec<&str> = Vec::new();
for entry in &entries {
let api = cowprotocol::ApiError {
error_type: entry.error_type.clone(),
description: String::new(),
data: None,
};
// Project the upstream hint into the table's model; a hint
// variant this projection does not know is a divergence.
let upstream = match api.retry_hint() {
cowprotocol::RetryHint::Retry => Some((RetryAction::TryNextBlock, false)),
cowprotocol::RetryHint::Backoff { seconds } => {
Some((RetryAction::Backoff { seconds }, false))
}
cowprotocol::RetryHint::Drop => Some((RetryAction::Drop, false)),
cowprotocol::RetryHint::AlreadySubmitted => Some((RetryAction::TryNextBlock, true)),
_ => None,
};
let shepherd = (
classify(&entry.error_type),
is_already_submitted(&entry.error_type),
);
if upstream != Some(shepherd) {
divergent.push(&entry.error_type);
}
}
divergent.sort_unstable();
assert_eq!(divergent, RATIFIED);
}

/// A non-Rust reader sees the same file as plain data: parsing it
/// with the untyped TOML value model (no Rust schema) exposes the
/// entries and their fields, proving any TOML library reads it.
Expand Down
13 changes: 5 additions & 8 deletions crates/shepherd-sdk/src/cow/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,14 +170,11 @@ mod tests {
}

#[test]
fn retriable_kinds_yield_try_next_block() {
for kind in ["InsufficientFee", "PriceExceedsMarketPrice"] {
assert_eq!(
classify_api_error(&rejection(kind)),
RetryAction::TryNextBlock,
"{kind}",
);
}
fn retriable_kind_yields_try_next_block() {
assert_eq!(
classify_api_error(&rejection("InsufficientFee")),
RetryAction::TryNextBlock,
);
}

/// A throttle errorType backs off rather than retrying next block,
Expand Down
Loading