From d56ef1bfa36bb056d74fa50eb9cfe59c7e5aaba7 Mon Sep 17 00:00:00 2001 From: Chris O'Neil Date: Tue, 18 Aug 2026 22:52:45 +0100 Subject: [PATCH] revert: drop the settlement-version quote gate from this train (#23) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reverts the merge of #23. The change is held off the 2026-08-18 release train and will land with its coordinated set. #23's own body states "Release readiness: coordinated set, do not merge alone" and that it "should not land until the coordinated set is fleet ready", naming ant-node #204 and ant-client #171 as carrying the open gates. Both are still open with changes requested, so the enforcement and adversarial evidence for this T3 change set — and its ADR — exist only on unmerged branches. Nothing here changes behaviour on its own: the new variants are never constructed unless a client sends them and a node acts on them, both of which are separate PRs. This revert is therefore inert on the wire, and restores ant-protocol to exactly the released v2.3.2 tree. Re-land as a set once #204 and #171 are ready. Note that ADR-0010 is now taken on ant-node by the beta upgrade channel ADR, so the settlement ADR needs renumbering before #204 lands. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 40 ---- Cargo.lock | 16 +- src/chunk.rs | 573 --------------------------------------------------- src/lib.rs | 13 +- 4 files changed, 13 insertions(+), 629 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5d239e..77334fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,46 +5,6 @@ All notable changes to `ant-protocol` will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres to [Semantic Versioning](https://semver.org/). -## [Unreleased] - -### Added -- **Settlement version on quote requests.** `CURRENT_SETTLEMENT_VERSION`, - `MIN_SUPPORTED_SETTLEMENT_VERSION` and `settlement_version_is_supported` - describe which payment rule set a build settles under, tracked separately - from `PROTOCOL_VERSION` because the wire format and the payment arithmetic - change independently. -- `ChunkQuoteRequestV2` and `MerkleCandidateQuoteRequestV2`, reached through - the new `ChunkMessageBody::QuoteRequestV2` and - `ChunkMessageBody::MerkleCandidateQuoteRequestV2` variants, carry that - version so a storer can refuse to quote a client that cannot pay correctly. -- `ProtocolError::ClientUpdateRequired` and `client_update_required_message`, - which tell the user their client is too old, that nothing was charged, and - how to upgrade. -- `ProtocolError::StorerUpdateRequired`, the mirror image: the storer is the - old side and declined rather than promise to accept a payment it may not - recognise. Kept a separate variant because the client should quietly use a - different peer rather than tell its user anything, which is the normal state - during a client-first rollout. -- `settlement_compatibility` and `SettlementCompatibility` replace the earlier - `settlement_version_is_supported`. The range is now bounded at **both** ends. - Accepting anything at or above the minimum is only safe when a settlement - change raises what is paid, as ADR-0008's 3x did; a change that redefines the - median rule or the payable field produces a payment an older verifier - rejects, after the client has already settled on-chain and can no longer be - refunded. - -Additive only: both message variants and the error variant are appended, so -every existing discriminant keeps its wire value and peers built against -`2.3.x` decode unchanged. A peer that does not know the new variants rejects -them cleanly rather than misreading them, and a regression test pins the -existing discriminants so a future insertion cannot break that silently. - -Motivated by ADR-0008, which raised the merkle settlement multiplier to 3x in -client and node code without changing any wire type. Nothing tied the two -together, so older clients kept collecting quotes, paid 1x on-chain, and had -every storer refuse the upload after the payment had already settled and -become unrefundable. - ## [2.3.0] ### Changed diff --git a/Cargo.lock b/Cargo.lock index b2efaaa..33ac654 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2189,7 +2189,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -3832,7 +3832,7 @@ dependencies = [ "once_cell", "socket2 0.5.10", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -3845,7 +3845,7 @@ dependencies = [ "libc", "socket2 0.5.10", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -4187,9 +4187,9 @@ dependencies = [ [[package]] name = "ruint" -version = "1.20.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5e99bff0393163bb25029a6af25d3d8d202ba5b5438a74d1bd8789f5c822970" +checksum = "45caf26f647c19115bf9c453c70ffe4a4a3a6390dceebd942610584f99b8ddce" dependencies = [ "alloy-rlp", "ark-ff 0.3.0", @@ -4269,7 +4269,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -4336,7 +4336,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -5126,7 +5126,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/src/chunk.rs b/src/chunk.rs index 29c67ae..82db826 100644 --- a/src/chunk.rs +++ b/src/chunk.rs @@ -28,96 +28,6 @@ pub const MAX_WIRE_MESSAGE_SIZE: usize = 5 * 1024 * 1024; /// Data type identifier for chunks. pub const DATA_TYPE_CHUNK: u32 = 0; -/// Settlement rules this build pays and verifies under. -/// -/// Separate from [`PROTOCOL_VERSION`] on purpose. That one tracks the *wire*: -/// what a peer can parse. This one tracks the *money*: how a client turns a -/// signed quote into an on-chain payment. The two move independently, and -/// conflating them is what made this constant necessary. -/// -/// Version 1 is the ADR-0008 rule set: a merkle batch settles at -/// `3 x median16(price) x 2^depth`, matching the single-node path. -/// -/// **Bump this whenever a change makes an older client pay an amount storers -/// will refuse.** The multiplier moving, the median rule changing, the payable -/// field being redefined: all of those. A change that only alters *how much* a -/// node quotes does not qualify, because the client pays whatever it is -/// quoted; a change to the arithmetic *applied* to that quote does. -/// -/// # Why this exists -/// -/// ADR-0008 raised the merkle multiplier to 3x in client code and enforced it -/// in node code, but changed no wire type. Nothing gated the two together, so -/// clients built before the change kept collecting quotes, kept paying 1x, and -/// had their uploads refused by every storer *after* the on-chain payment had -/// already settled. The money was unrecoverable and the client had no idea why. -/// -/// Carrying the version in the quote request lets a storer refuse to quote a -/// client it knows cannot pay correctly, before that client spends anything. -pub const CURRENT_SETTLEMENT_VERSION: u32 = 1; - -/// Oldest settlement version this build will issue a quote for. -/// -/// Kept as a separate constant from [`CURRENT_SETTLEMENT_VERSION`] so a -/// settlement change can ship without immediately locking out the previous -/// client generation: raise `CURRENT` first, let clients adopt, raise `MIN` -/// once the payment rule actually changes. They are equal today because -/// version 1 is the first versioned rule set, so there is no earlier one to -/// keep serving. -pub const MIN_SUPPORTED_SETTLEMENT_VERSION: u32 = 1; - -/// Whether a storer on this build can promise to accept what a client -/// settling under `client_version` will pay. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum SettlementCompatibility { - /// Both sides settle the same way. Safe to quote. - Compatible, - /// The client settles under rules this build has superseded. Its payment - /// would be refused, so it must not be quoted. - ClientTooOld, - /// The client settles under rules this build does not know. **This build - /// is the old one**, and it cannot promise to accept the resulting - /// payment, so it must not be quoted either. - NodeTooOld, -} - -/// Can a storer on this build safely quote a client settling under -/// `client_version`? -/// -/// Unversioned clients never reach this: they send the legacy request variants -/// and are handled by policy in the storer, not here. -/// -/// # Why both ends are bounded -/// -/// An earlier revision accepted everything at or above -/// [`MIN_SUPPORTED_SETTLEMENT_VERSION`], on the reasoning that a storer -/// verifies whatever payment actually arrives so letting a newer client -/// through weakens nothing. That is only true when a settlement change raises -/// what is paid: ADR-0008's 3x cleared an old node's 1x minimum, so old nodes -/// accepted new clients for free. It is **not** true in general. A change that -/// redefines the median rule, or which field the contract pays from, produces -/// a payment an older verifier rejects, and by then the client has already -/// settled on-chain and cannot be refunded. That is the exact failure this -/// mechanism exists to prevent, so an unknown-newer version is refused rather -/// than assumed compatible. -/// -/// The two refusals are kept apart because they need opposite handling. -/// [`SettlementCompatibility::ClientTooOld`] is terminal and the user must -/// upgrade. [`SettlementCompatibility::NodeTooOld`] says nothing about the -/// client, which should simply use a different storer. Collapsing them would -/// either tell up-to-date users to upgrade, or strand new clients whenever the -/// node fleet lags, which is the normal state during a client-first rollout. -#[must_use] -pub const fn settlement_compatibility(client_version: u32) -> SettlementCompatibility { - if client_version < MIN_SUPPORTED_SETTLEMENT_VERSION { - SettlementCompatibility::ClientTooOld - } else if client_version > CURRENT_SETTLEMENT_VERSION { - SettlementCompatibility::NodeTooOld - } else { - SettlementCompatibility::Compatible - } -} - /// Number of nodes in a Kademlia close group. /// /// Clients fetch quotes from the `CLOSE_GROUP_SIZE` closest nodes to a target @@ -160,16 +70,6 @@ pub enum ChunkMessageBody { MerkleCandidateQuoteRequest(MerkleCandidateQuoteRequest), /// Response with a merkle candidate quote. MerkleCandidateQuoteResponse(MerkleCandidateQuoteResponse), - /// Request a storage quote, declaring the client's settlement version. - /// - /// Appended after [`Self::MerkleCandidateQuoteResponse`] so every - /// discriminant above keeps its wire value and existing peers decode - /// unchanged. A peer built before this variant existed rejects it cleanly - /// as an unknown discriminant rather than misreading it. - QuoteRequestV2(ChunkQuoteRequestV2), - /// Request a merkle candidate quote, declaring the client's settlement - /// version. Appended for the same reason as [`Self::QuoteRequestV2`]. - MerkleCandidateQuoteRequestV2(MerkleCandidateQuoteRequestV2), } /// Wire-format wrapper that pairs a sender-assigned `request_id` with @@ -350,40 +250,6 @@ impl ChunkQuoteRequest { } } -/// Request a storage quote, declaring the settlement rules the client pays -/// under. -/// -/// Same fields as [`ChunkQuoteRequest`] plus `settlement_version`. A separate -/// struct rather than a field on the original, because [`ChunkMessage`] is -/// postcard-encoded and postcard is not self-describing: adding a field would -/// silently change how every existing peer reads the message, while adding a -/// variant is rejected cleanly by peers that do not know it. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ChunkQuoteRequestV2 { - /// The content address of the data to store. - pub address: XorName, - /// Size of the data in bytes. - pub data_size: u64, - /// Data type identifier (0 for chunks). - pub data_type: u32, - /// The settlement rules this client pays under. See - /// [`CURRENT_SETTLEMENT_VERSION`]. - pub settlement_version: u32, -} - -impl ChunkQuoteRequestV2 { - /// Create a new quote request declaring this build's settlement version. - #[must_use] - pub fn new(address: XorName, data_size: u64) -> Self { - Self { - address, - data_size, - data_type: DATA_TYPE_CHUNK, - settlement_version: CURRENT_SETTLEMENT_VERSION, - } - } -} - /// Response with a storage quote. #[derive(Debug, Clone, Serialize, Deserialize)] #[non_exhaustive] @@ -441,45 +307,6 @@ pub struct MerkleCandidateQuoteRequest { pub merkle_payment_timestamp: u64, } -/// Request a merkle candidate quote, declaring the settlement rules the client -/// pays under. -/// -/// Same fields as [`MerkleCandidateQuoteRequest`] plus `settlement_version`. -/// See [`ChunkQuoteRequestV2`] for why this is a separate struct. -/// -/// This is the variant that matters most for the merkle path: a batch pays -/// on-chain **before** any storer sees a PUT, so a storer that only checks the -/// settlement rule at PUT time is checking it after the money is gone. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MerkleCandidateQuoteRequestV2 { - /// The candidate pool address (hash of midpoint || root || timestamp). - pub address: XorName, - /// Data type identifier (0 for chunks). - pub data_type: u32, - /// Size of the data in bytes. - pub data_size: u64, - /// Client-provided merkle payment timestamp (unix seconds). - pub merkle_payment_timestamp: u64, - /// The settlement rules this client pays under. See - /// [`CURRENT_SETTLEMENT_VERSION`]. - pub settlement_version: u32, -} - -impl MerkleCandidateQuoteRequestV2 { - /// Create a new merkle candidate quote request declaring this build's - /// settlement version. - #[must_use] - pub fn new(address: XorName, data_size: u64, merkle_payment_timestamp: u64) -> Self { - Self { - address, - data_type: DATA_TYPE_CHUNK, - data_size, - merkle_payment_timestamp, - settlement_version: CURRENT_SETTLEMENT_VERSION, - } - } -} - /// Response with a merkle candidate quote. #[derive(Debug, Clone, Serialize, Deserialize)] #[non_exhaustive] @@ -558,39 +385,6 @@ pub enum ProtocolError { QuoteFailed(String), /// Internal error. Internal(String), - /// The client settles payments under rules this node no longer accepts, so - /// no quote was issued. - /// - /// Refused at quote time on purpose. A client that pays under superseded - /// rules produces an on-chain payment every storer will reject, and that - /// payment cannot be refunded, so the only useful place to stop it is - /// before the client spends anything. - /// - /// Appended last so existing variants keep their wire discriminants. A - /// peer old enough not to know this variant cannot be sent it: it would - /// have had to send a V2 request to earn it, and only builds that carry - /// this variant do that. - ClientUpdateRequired { - /// Settlement version the client declared. - client_settlement_version: u32, - /// Oldest settlement version this node will quote for. - min_settlement_version: u32, - }, - /// This node settles under older rules than the client, so it declined to - /// quote rather than promise to accept a payment it may not recognise. - /// - /// The mirror image of [`Self::ClientUpdateRequired`], and deliberately a - /// separate variant: it is not a verdict about the client, and a client - /// that receives it should quietly use a different storer rather than tell - /// its user anything. During a client-first rollout most of the fleet is - /// briefly in this state, so treating it as a client fault would strand - /// every up-to-date user. - StorerUpdateRequired { - /// Settlement version the client declared. - client_settlement_version: u32, - /// Newest settlement version this node understands. - node_settlement_version: u32, - }, } impl std::fmt::Display for ProtocolError { @@ -616,48 +410,10 @@ impl std::fmt::Display for ProtocolError { Self::PaymentFailed(msg) => write!(f, "payment failed: {msg}"), Self::QuoteFailed(msg) => write!(f, "quote failed: {msg}"), Self::Internal(msg) => write!(f, "internal error: {msg}"), - Self::ClientUpdateRequired { - client_settlement_version, - min_settlement_version, - } => write!( - f, - "{}", - client_update_required_message(*client_settlement_version, *min_settlement_version) - ), - Self::StorerUpdateRequired { - client_settlement_version, - node_settlement_version, - } => write!( - f, - "this node settles under version {node_settlement_version} and cannot \ - promise to accept a version {client_settlement_version} payment, so it \ - issued no quote. Nothing was charged; use a different storer." - ), } } } -/// The upgrade instruction shown to a user whose client cannot settle -/// correctly. -/// -/// A free function rather than only a `Display` arm so the storer can log the -/// same wording it sends back, and so the client can reuse it when it -/// translates the rejection into a CLI error. Kept deliberately plain: the -/// reader is an end user staring at a failed upload, not an operator. -#[must_use] -pub fn client_update_required_message( - client_settlement_version: u32, - min_settlement_version: u32, -) -> String { - format!( - "your client is too old to pay the current storage rate \ - (it settles under version {client_settlement_version}, this node requires \ - at least {min_settlement_version}), so no quote was issued and nothing was \ - charged. Run `ant update` to upgrade, or reinstall from \ - https://github.com/WithAutonomi/ant-client/releases/latest" - ) -} - impl std::error::Error for ProtocolError {} #[cfg(test)] @@ -920,333 +676,4 @@ mod tests { panic!("expected MerkleCandidateQuoteResponse::Error"); } } - - // ========================================================================= - // Settlement version - // ========================================================================= - - /// Discriminant of a message body, read straight off the wire. - /// - /// `request_id: 0` encodes as a single varint byte, so the body's variant - /// index is byte 1. Reading it directly is the point: it is what a peer - /// built against an older `ant-protocol` sees. - fn wire_discriminant(body: ChunkMessageBody) -> u8 { - let encoded = ChunkMessage { - request_id: 0, - body, - } - .encode() - .expect("encode should succeed"); - *encoded.get(1).expect("body discriminant should be present") - } - - /// The load-bearing test for this whole design. - /// - /// The settlement version ships as appended variants precisely so existing - /// peers keep decoding. That only holds while every prior variant keeps - /// its wire index, which postcard assigns by declaration order. Insert a - /// variant anywhere but the end and every older peer silently misreads - /// every message from this one. Pinning the indices turns that from a - /// production incident into a failing test. - #[test] - fn appending_v2_variants_leaves_existing_discriminants_untouched() { - let address = [0x11; 32]; - - assert_eq!( - wire_discriminant(ChunkMessageBody::PutRequest(ChunkPutRequest::new( - address, - Bytes::from_static(&[1]), - ))), - 0, - ); - assert_eq!( - wire_discriminant(ChunkMessageBody::GetRequest(ChunkGetRequest { address })), - 2, - ); - assert_eq!( - wire_discriminant(ChunkMessageBody::QuoteRequest(ChunkQuoteRequest::new( - address, 1024, - ))), - 4, - ); - assert_eq!( - wire_discriminant(ChunkMessageBody::MerkleCandidateQuoteRequest( - MerkleCandidateQuoteRequest { - address, - data_type: DATA_TYPE_CHUNK, - data_size: 1024, - merkle_payment_timestamp: 1_785_855_600, - } - )), - 6, - ); - - // Responses carry the same obligation as requests: a peer decoding a - // reply reads the same discriminant space, so pinning only the request - // half would let a response variant be reordered without any test - // noticing. - assert_eq!( - wire_discriminant(ChunkMessageBody::PutResponse(ChunkPutResponse::Success { - address - })), - 1, - ); - assert_eq!( - wire_discriminant(ChunkMessageBody::GetResponse(ChunkGetResponse::NotFound { - address - })), - 3, - ); - assert_eq!( - wire_discriminant(ChunkMessageBody::QuoteResponse(ChunkQuoteResponse::Error( - ProtocolError::Internal(String::new()) - ))), - 5, - ); - assert_eq!( - wire_discriminant(ChunkMessageBody::MerkleCandidateQuoteResponse( - MerkleCandidateQuoteResponse::Error(ProtocolError::Internal(String::new())) - )), - 7, - ); - - // The new variants take the next free indices, above everything an - // older peer knows, so it rejects them as unknown rather than - // misreading a variant it does know. - assert_eq!( - wire_discriminant(ChunkMessageBody::QuoteRequestV2(ChunkQuoteRequestV2::new( - address, 1024, - ))), - 8, - ); - assert_eq!( - wire_discriminant(ChunkMessageBody::MerkleCandidateQuoteRequestV2( - MerkleCandidateQuoteRequestV2::new(address, 1024, 1_785_855_600) - )), - 9, - ); - } - - /// `ProtocolError` rides inside quote responses, so its variants carry the - /// same append-only obligation as the message bodies. - #[test] - fn client_update_required_is_appended_to_protocol_error() { - let encode = |e: &ProtocolError| postcard::to_stdvec(e).expect("encode should succeed"); - - assert_eq!( - encode(&ProtocolError::SerializationFailed(String::new())) - .first() - .copied(), - Some(0), - ); - assert_eq!( - encode(&ProtocolError::DeserializationFailed(String::new())) - .first() - .copied(), - Some(1), - ); - assert_eq!( - encode(&ProtocolError::MessageTooLarge { - size: 0, - max_size: 0 - }) - .first() - .copied(), - Some(2), - ); - assert_eq!( - encode(&ProtocolError::ChunkTooLarge { - size: 0, - max_size: 0 - }) - .first() - .copied(), - Some(3), - ); - assert_eq!( - encode(&ProtocolError::AddressMismatch { - expected: [0u8; 32], - actual: [0u8; 32] - }) - .first() - .copied(), - Some(4), - ); - assert_eq!( - encode(&ProtocolError::StorageFailed(String::new())) - .first() - .copied(), - Some(5), - ); - assert_eq!( - encode(&ProtocolError::PaymentFailed(String::new())) - .first() - .copied(), - Some(6), - ); - assert_eq!( - encode(&ProtocolError::QuoteFailed(String::new())) - .first() - .copied(), - Some(7), - ); - assert_eq!( - encode(&ProtocolError::Internal(String::new())) - .first() - .copied(), - Some(8), - ); - assert_eq!( - encode(&ProtocolError::ClientUpdateRequired { - client_settlement_version: 0, - min_settlement_version: 1, - }) - .first() - .copied(), - Some(9), - ); - assert_eq!( - encode(&ProtocolError::StorerUpdateRequired { - client_settlement_version: 2, - node_settlement_version: 1, - }) - .first() - .copied(), - Some(10), - ); - } - - #[test] - fn v2_quote_request_round_trips_with_the_settlement_version() { - let address = [0x22; 32]; - let msg = ChunkMessage { - request_id: 600, - body: ChunkMessageBody::QuoteRequestV2(ChunkQuoteRequestV2::new(address, 4096)), - }; - - let encoded = msg.encode().expect("encode should succeed"); - let decoded = ChunkMessage::decode(&encoded).expect("decode should succeed"); - - assert_eq!(decoded.request_id, 600); - if let ChunkMessageBody::QuoteRequestV2(req) = decoded.body { - assert_eq!(req.address, address); - assert_eq!(req.data_size, 4096); - assert_eq!(req.data_type, DATA_TYPE_CHUNK); - assert_eq!(req.settlement_version, CURRENT_SETTLEMENT_VERSION); - } else { - panic!("expected QuoteRequestV2"); - } - } - - #[test] - fn v2_merkle_candidate_request_round_trips_with_the_settlement_version() { - let address = [0x33; 32]; - let msg = ChunkMessage { - request_id: 601, - body: ChunkMessageBody::MerkleCandidateQuoteRequestV2( - MerkleCandidateQuoteRequestV2::new(address, 4096, 1_785_855_600), - ), - }; - - let encoded = msg.encode().expect("encode should succeed"); - let decoded = ChunkMessage::decode(&encoded).expect("decode should succeed"); - - assert_eq!(decoded.request_id, 601); - if let ChunkMessageBody::MerkleCandidateQuoteRequestV2(req) = decoded.body { - assert_eq!(req.address, address); - assert_eq!(req.data_size, 4096); - assert_eq!(req.merkle_payment_timestamp, 1_785_855_600); - assert_eq!(req.settlement_version, CURRENT_SETTLEMENT_VERSION); - } else { - panic!("expected MerkleCandidateQuoteRequestV2"); - } - } - - #[test] - fn settlement_compatibility_is_bounded_at_both_ends() { - assert_eq!( - settlement_compatibility(CURRENT_SETTLEMENT_VERSION), - SettlementCompatibility::Compatible, - ); - // The lower bound is inclusive: the oldest version still served is - // served, not refused. - assert_eq!( - settlement_compatibility(MIN_SUPPORTED_SETTLEMENT_VERSION), - SettlementCompatibility::Compatible, - ); - assert_eq!( - settlement_compatibility(MIN_SUPPORTED_SETTLEMENT_VERSION.saturating_sub(1)), - SettlementCompatibility::ClientTooOld, - ); - } - - /// The correction this replaced an earlier revision for. Serving a client - /// whose settlement rules this build does not know means promising to - /// accept a payment it may reject, and by the time it rejects, the client - /// has settled on-chain and cannot be refunded. Only monotonic increases - /// are safe to wave through, and nothing here can tell whether the next - /// change is one. - #[test] - fn a_newer_settlement_version_is_refused_rather_than_assumed_compatible() { - assert_eq!( - settlement_compatibility(CURRENT_SETTLEMENT_VERSION.saturating_add(1)), - SettlementCompatibility::NodeTooOld, - ); - } - - /// The two refusals must stay distinct on the wire. One tells a user to - /// upgrade; the other tells a client to pick a different peer and say - /// nothing. Rendering them alike would strand every up-to-date user during - /// a client-first rollout, when most of the fleet is briefly the old side. - #[test] - fn the_two_refusals_do_not_blame_the_same_party() { - let client_at_fault = ProtocolError::ClientUpdateRequired { - client_settlement_version: 0, - min_settlement_version: 1, - } - .to_string(); - let node_at_fault = ProtocolError::StorerUpdateRequired { - client_settlement_version: 2, - node_settlement_version: 1, - } - .to_string(); - - assert!( - client_at_fault.contains("your client is too old"), - "{client_at_fault}" - ); - assert!( - !node_at_fault.contains("your client is too old"), - "{node_at_fault}" - ); - assert!( - node_at_fault.contains("use a different storer"), - "{node_at_fault}" - ); - // Neither costs the user anything, and both must say so. - assert!( - client_at_fault.contains("nothing was charged"), - "{client_at_fault}" - ); - assert!( - node_at_fault.contains("Nothing was charged"), - "{node_at_fault}" - ); - } - - /// The rejection exists to get a user unstuck, so the wording is part of - /// the contract, not decoration. - #[test] - fn update_required_message_tells_the_user_how_to_fix_it() { - let rendered = ProtocolError::ClientUpdateRequired { - client_settlement_version: 0, - min_settlement_version: 1, - } - .to_string(); - - assert!(rendered.contains("ant update"), "{rendered}"); - assert!(rendered.contains("too old"), "{rendered}"); - // The whole point is that refusing to quote costs the user nothing. - assert!(rendered.contains("nothing was charged"), "{rendered}"); - } } diff --git a/src/lib.rs b/src/lib.rs index 0c03ea5..cd47dbc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -54,14 +54,11 @@ pub mod payment; // ============================================================================= pub use chunk::{ - client_update_required_message, settlement_compatibility, ChunkGetRequest, ChunkGetResponse, - ChunkMessage, ChunkMessageBody, ChunkPutRequest, ChunkPutResponse, ChunkQuoteRequest, - ChunkQuoteRequestV2, ChunkQuoteResponse, MerkleCandidateQuoteRequest, - MerkleCandidateQuoteRequestV2, MerkleCandidateQuoteResponse, ProtocolError, - SettlementCompatibility, XorName, CHUNK_PROTOCOL_ID, CLOSE_GROUP_MAJORITY, CLOSE_GROUP_SIZE, - CURRENT_SETTLEMENT_VERSION, DATA_TYPE_CHUNK, MAX_CHUNK_SIZE, MAX_WIRE_MESSAGE_SIZE, - MIN_SUPPORTED_SETTLEMENT_VERSION, PROOF_TAG_MERKLE, PROOF_TAG_SINGLE_NODE, PROTOCOL_VERSION, - XORNAME_LEN, + ChunkGetRequest, ChunkGetResponse, ChunkMessage, ChunkMessageBody, ChunkPutRequest, + ChunkPutResponse, ChunkQuoteRequest, ChunkQuoteResponse, MerkleCandidateQuoteRequest, + MerkleCandidateQuoteResponse, ProtocolError, XorName, CHUNK_PROTOCOL_ID, CLOSE_GROUP_MAJORITY, + CLOSE_GROUP_SIZE, DATA_TYPE_CHUNK, MAX_CHUNK_SIZE, MAX_WIRE_MESSAGE_SIZE, PROOF_TAG_MERKLE, + PROOF_TAG_SINGLE_NODE, PROTOCOL_VERSION, XORNAME_LEN, }; pub use chunk_protocol::send_and_await_chunk_response; pub use data_types::{compute_address, peer_id_to_xor_name, xor_distance, ChunkStats, DataChunk};