-
Notifications
You must be signed in to change notification settings - Fork 2
cow: settle the idempotency seam on the venue-and-body intent-id #466
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,6 +9,8 @@ | |
| //! marker enums are canonical wire forms, not on-chain keccak markers, | ||
| //! so the adapter, not this type, owns the projection to and from chain. | ||
|
|
||
| use alloc::vec::Vec; | ||
| use core::fmt; | ||
| use core::marker::PhantomData; | ||
|
|
||
| use borsh::{BorshDeserialize, BorshSerialize}; | ||
|
|
@@ -259,6 +261,70 @@ impl OrderBuilder<Ready> { | |
| } | ||
| } | ||
|
|
||
| /// An owner-signed order ready for the orderbook: what a | ||
| /// conditional-order keeper emits after a poll. | ||
| #[derive(BorshSerialize, BorshDeserialize, Clone, Debug, PartialEq, Eq)] | ||
| pub struct SignedOrder { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The fact is confirmed at the end-of-train tip: the sweep builds The concrete scenario does not arise on this path though, for two independent reasons, both checked rather than assumed. The keeper never signs. Even a genuinely different signature costs only one redundant POST. The orderbook's identity for an order is signature-independent: So the failure mode is milder than "defeats dedup": dedup degrades to the venue's own idempotency rather than breaking. What genuinely remains is your second option, documenting that the key scopes to this exact signed payload and not to the economic order, plus the latent hazard that the argument above leans on a venue whose duplicate response is idempotent. #558 carries both, with documenting as the recommendation and excluding the signature from the key as the alternative.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Revisited this and folded it into the PR rather than leaving it to the tracker, taking your documentation option.
Folding was safe on this car specifically: The analysis above still stands on why the runtime behaviour is sound today, so this is a precision fix to the contract rather than a behaviour change. #558 stays open only to record the alternative (keying on |
||
| /// The order to place. | ||
| pub order: OrderBody, | ||
| /// Order owner: the EIP-1271 verifier and the `from` of the | ||
| /// orderbook submission. | ||
| pub owner: Address, | ||
| /// Raw EIP-1271 signature bytes; the settlement verifies them | ||
| /// against `owner`. | ||
| pub signature: Vec<u8>, | ||
| } | ||
|
|
||
| /// Canonical 56-byte orderbook order UID (order digest, owner, | ||
| /// `valid_to`) in wire form: the receipt bytes an accepted CoW submit | ||
| /// carries. | ||
| #[derive(Clone, Copy, PartialEq, Eq, Hash)] | ||
| pub struct OrderUid(pub [u8; 56]); | ||
|
|
||
| impl OrderUid { | ||
| /// The raw 56 bytes. | ||
| #[must_use] | ||
| pub const fn as_bytes(&self) -> &[u8; 56] { | ||
| &self.0 | ||
| } | ||
| } | ||
|
|
||
| impl From<[u8; 56]> for OrderUid { | ||
| fn from(bytes: [u8; 56]) -> Self { | ||
| Self(bytes) | ||
| } | ||
| } | ||
|
|
||
| impl TryFrom<&[u8]> for OrderUid { | ||
| type Error = core::array::TryFromSliceError; | ||
|
|
||
| fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> { | ||
| Ok(Self(<[u8; 56]>::try_from(bytes)?)) | ||
| } | ||
| } | ||
|
|
||
| impl From<OrderUid> for Vec<u8> { | ||
| fn from(uid: OrderUid) -> Self { | ||
| uid.0.to_vec() | ||
| } | ||
| } | ||
|
|
||
| impl fmt::Display for OrderUid { | ||
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
| f.write_str("0x")?; | ||
| for byte in self.0 { | ||
| write!(f, "{byte:02x}")?; | ||
| } | ||
| Ok(()) | ||
| } | ||
| } | ||
|
|
||
| impl fmt::Debug for OrderUid { | ||
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
| fmt::Display::fmt(self, f) | ||
| } | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
|
|
@@ -367,4 +433,36 @@ mod tests { | |
| } | ||
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn signed_order_borsh_round_trips() { | ||
| let signed = SignedOrder { | ||
| order: sample(), | ||
| owner: [0x55; 20], | ||
| signature: vec![0xC0, 0xFF, 0xEE], | ||
| }; | ||
| let bytes = borsh::to_vec(&signed).expect("encode"); | ||
| assert_eq!(SignedOrder::try_from_slice(&bytes).expect("decode"), signed); | ||
| } | ||
|
|
||
| #[test] | ||
| fn order_uid_converts_only_from_56_bytes() { | ||
| let uid = OrderUid([0xAB; 56]); | ||
| assert_eq!(OrderUid::try_from(&uid.0[..]).expect("56 bytes"), uid); | ||
| assert!(OrderUid::try_from(&uid.0[..55]).is_err()); | ||
| assert_eq!(Vec::from(uid), vec![0xAB; 56]); | ||
| } | ||
|
|
||
| #[test] | ||
| fn order_uid_displays_as_prefixed_hex() { | ||
| let mut bytes = [0u8; 56]; | ||
| bytes[0] = 0x01; | ||
| bytes[55] = 0xFF; | ||
| let uid = OrderUid(bytes); | ||
| let hex = uid.to_string(); | ||
| assert_eq!(hex.len(), 2 + 56 * 2); | ||
| assert!(hex.starts_with("0x01")); | ||
| assert!(hex.ends_with("ff")); | ||
| assert_eq!(format!("{uid:?}"), hex); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment (and the PR body) states "
SubmitOutcome's accepted receipt is now the canonical 56-byteOrderUid," but nothing in this diff actually wiresOrderUidintosubmit_order's return type orSubmitOutcome— neither symbol is touched here.OrderUidis added with solid conversions and round-trip tests, but it's constructed only in this PR's own unit tests; no production call site builds one from a real submit response yet. Worth either wiring it into the real return path in this PR, or scoping this comment/the PR description to "the type is added, integration follows" so it doesn't read as already-shipped.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct as written, and the diff confirms it: this PR adds exactly that one doc line to
client.rsand touches neithersubmit_ordernorSubmitOutcome.OrderUidis built only in this PR's own unit tests.The wiring lands in the very next car, #467 (
feat/m4-cow-adapter-cdylib), which addscow-venue/src/adapter.rs. Therepost_orderdecodes the orderbook's uid from the success response and submit returnsSubmitOutcome::Accepted(uid.as_slice().to_vec())(:174), the already-held path falls back to the derivedassembly::order_uidrather than inventing a receipt (:172), the status path rejects anything that is not a 56-byte order uid (:203-204), and the tests assertreceipt == uid.as_slice(). So the claim is accurate one car later, and byte-identical at the end-of-train tip where it holds.I have left the code comment alone rather than spend a re-push and a full CI run on a line that is true from the next car onward, and scoped the PR body instead, which is where it read as already shipped. If you would rather the comment itself carry the "type added here, integration in #467" hedge for the duration of one car, say so and I will fold it into the next ripple.