Skip to main content

ant_protocol/
chunk.rs

1//! Chunk message types for the ANT protocol.
2//!
3//! Chunks are immutable, content-addressed data blocks where the address
4//! is the BLAKE3 hash of the content. Maximum size is 4MB.
5//!
6//! This module defines the wire protocol messages for chunk operations
7//! using postcard serialization for compact, fast encoding.
8
9use bytes::Bytes;
10use serde::{Deserialize, Serialize};
11
12/// Protocol identifier for chunk operations.
13pub const CHUNK_PROTOCOL_ID: &str = "autonomi.ant.chunk.v1";
14
15/// Current protocol version.
16pub const PROTOCOL_VERSION: u16 = 1;
17
18/// Maximum chunk size in bytes (4MB).
19pub const MAX_CHUNK_SIZE: usize = 4 * 1024 * 1024;
20
21/// Maximum wire message size in bytes (5MB).
22///
23/// Limits the input buffer accepted by [`ChunkMessage::decode`] to prevent
24/// unbounded allocation from malicious or corrupted payloads. Set slightly
25/// above [`MAX_CHUNK_SIZE`] to accommodate message envelope overhead.
26pub const MAX_WIRE_MESSAGE_SIZE: usize = 5 * 1024 * 1024;
27
28/// Data type identifier for chunks.
29pub const DATA_TYPE_CHUNK: u32 = 0;
30
31/// Settlement rules this build pays and verifies under.
32///
33/// Separate from [`PROTOCOL_VERSION`] on purpose. That one tracks the *wire*:
34/// what a peer can parse. This one tracks the *money*: how a client turns a
35/// signed quote into an on-chain payment. The two move independently, and
36/// conflating them is what made this constant necessary.
37///
38/// Version 1 is the ADR-0008 rule set: a merkle batch settles at
39/// `3 x median16(price) x 2^depth`, matching the single-node path.
40///
41/// **Bump this whenever a change makes an older client pay an amount storers
42/// will refuse.** The multiplier moving, the median rule changing, the payable
43/// field being redefined: all of those. A change that only alters *how much* a
44/// node quotes does not qualify, because the client pays whatever it is
45/// quoted; a change to the arithmetic *applied* to that quote does.
46///
47/// # Why this exists
48///
49/// ADR-0008 raised the merkle multiplier to 3x in client code and enforced it
50/// in node code, but changed no wire type. Nothing gated the two together, so
51/// clients built before the change kept collecting quotes, kept paying 1x, and
52/// had their uploads refused by every storer *after* the on-chain payment had
53/// already settled. The money was unrecoverable and the client had no idea why.
54///
55/// Carrying the version in the quote request lets a storer refuse to quote a
56/// client it knows cannot pay correctly, before that client spends anything.
57pub const CURRENT_SETTLEMENT_VERSION: u32 = 1;
58
59/// Oldest settlement version this build will issue a quote for.
60///
61/// Kept as a separate constant from [`CURRENT_SETTLEMENT_VERSION`] so a
62/// settlement change can ship without immediately locking out the previous
63/// client generation: raise `CURRENT` first, let clients adopt, raise `MIN`
64/// once the payment rule actually changes. They are equal today because
65/// version 1 is the first versioned rule set, so there is no earlier one to
66/// keep serving.
67pub const MIN_SUPPORTED_SETTLEMENT_VERSION: u32 = 1;
68
69/// Whether a storer on this build can promise to accept what a client
70/// settling under `client_version` will pay.
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub enum SettlementCompatibility {
73    /// Both sides settle the same way. Safe to quote.
74    Compatible,
75    /// The client settles under rules this build has superseded. Its payment
76    /// would be refused, so it must not be quoted.
77    ClientTooOld,
78    /// The client settles under rules this build does not know. **This build
79    /// is the old one**, and it cannot promise to accept the resulting
80    /// payment, so it must not be quoted either.
81    NodeTooOld,
82}
83
84/// Can a storer on this build safely quote a client settling under
85/// `client_version`?
86///
87/// Unversioned clients never reach this: they send the legacy request variants
88/// and are handled by policy in the storer, not here.
89///
90/// # Why both ends are bounded
91///
92/// An earlier revision accepted everything at or above
93/// [`MIN_SUPPORTED_SETTLEMENT_VERSION`], on the reasoning that a storer
94/// verifies whatever payment actually arrives so letting a newer client
95/// through weakens nothing. That is only true when a settlement change raises
96/// what is paid: ADR-0008's 3x cleared an old node's 1x minimum, so old nodes
97/// accepted new clients for free. It is **not** true in general. A change that
98/// redefines the median rule, or which field the contract pays from, produces
99/// a payment an older verifier rejects, and by then the client has already
100/// settled on-chain and cannot be refunded. That is the exact failure this
101/// mechanism exists to prevent, so an unknown-newer version is refused rather
102/// than assumed compatible.
103///
104/// The two refusals are kept apart because they need opposite handling.
105/// [`SettlementCompatibility::ClientTooOld`] is terminal and the user must
106/// upgrade. [`SettlementCompatibility::NodeTooOld`] says nothing about the
107/// client, which should simply use a different storer. Collapsing them would
108/// either tell up-to-date users to upgrade, or strand new clients whenever the
109/// node fleet lags, which is the normal state during a client-first rollout.
110#[must_use]
111pub const fn settlement_compatibility(client_version: u32) -> SettlementCompatibility {
112    if client_version < MIN_SUPPORTED_SETTLEMENT_VERSION {
113        SettlementCompatibility::ClientTooOld
114    } else if client_version > CURRENT_SETTLEMENT_VERSION {
115        SettlementCompatibility::NodeTooOld
116    } else {
117        SettlementCompatibility::Compatible
118    }
119}
120
121/// Number of nodes in a Kademlia close group.
122///
123/// Clients fetch quotes from the `CLOSE_GROUP_SIZE` closest nodes to a target
124/// address and select the median-priced quote for payment.
125pub const CLOSE_GROUP_SIZE: usize = 7;
126
127/// Minimum number of close group members that must agree for a decision to be valid.
128///
129/// This is a simple majority: `(CLOSE_GROUP_SIZE / 2) + 1`.
130pub const CLOSE_GROUP_MAJORITY: usize = (CLOSE_GROUP_SIZE / 2) + 1;
131
132/// Content-addressed identifier (32 bytes).
133pub type XorName = [u8; 32];
134
135/// Byte length of an [`XorName`].
136pub const XORNAME_LEN: usize = std::mem::size_of::<XorName>();
137
138/// Enum of all chunk protocol message types.
139///
140/// Uses a single-byte discriminant for efficient wire encoding.
141///
142/// Marked `#[non_exhaustive]` so new message variants can be added
143/// in a minor release without breaking downstream `match` expressions.
144#[derive(Debug, Clone, Serialize, Deserialize)]
145#[non_exhaustive]
146pub enum ChunkMessageBody {
147    /// Request to store a chunk.
148    PutRequest(ChunkPutRequest),
149    /// Response to a PUT request.
150    PutResponse(ChunkPutResponse),
151    /// Request to retrieve a chunk.
152    GetRequest(ChunkGetRequest),
153    /// Response to a GET request.
154    GetResponse(ChunkGetResponse),
155    /// Request a storage quote.
156    QuoteRequest(ChunkQuoteRequest),
157    /// Response with a storage quote.
158    QuoteResponse(ChunkQuoteResponse),
159    /// Request a merkle candidate quote for batch payments.
160    MerkleCandidateQuoteRequest(MerkleCandidateQuoteRequest),
161    /// Response with a merkle candidate quote.
162    MerkleCandidateQuoteResponse(MerkleCandidateQuoteResponse),
163    /// Request a storage quote, declaring the client's settlement version.
164    ///
165    /// Appended after [`Self::MerkleCandidateQuoteResponse`] so every
166    /// discriminant above keeps its wire value and existing peers decode
167    /// unchanged. A peer built before this variant existed rejects it cleanly
168    /// as an unknown discriminant rather than misreading it.
169    QuoteRequestV2(ChunkQuoteRequestV2),
170    /// Request a merkle candidate quote, declaring the client's settlement
171    /// version. Appended for the same reason as [`Self::QuoteRequestV2`].
172    MerkleCandidateQuoteRequestV2(MerkleCandidateQuoteRequestV2),
173    /// Request to store or update a pointer.
174    ///
175    /// Appended after every existing variant for the reason given on
176    /// [`Self::QuoteRequestV2`]: discriminants above keep their wire values, and
177    /// a peer built before pointers existed rejects this cleanly as an unknown
178    /// discriminant rather than misreading it.
179    PointerPutRequest(PointerPutRequest),
180    /// Response to a pointer PUT.
181    PointerPutResponse(PointerPutResponse),
182    /// Request to retrieve a pointer.
183    PointerGetRequest(PointerGetRequest),
184    /// Response to a pointer GET.
185    PointerGetResponse(PointerGetResponse),
186}
187
188// =============================================================================
189// Pointer PUT / GET
190// =============================================================================
191
192/// Request to store or update a pointer.
193///
194/// `record` is the whole [`Pointer`](crate::pointer::Pointer) encoding. There is
195/// no separate address field: a pointer's address is a pure function of the
196/// owner key inside the record, so a second copy could only ever disagree with
197/// it.
198#[derive(Debug, Clone, Serialize, Deserialize)]
199pub struct PointerPutRequest {
200    /// The canonical pointer encoding.
201    pub record: Bytes,
202    /// Optional payment proof (serialized `ProofOfPayment`).
203    ///
204    /// Quoted and paid against the record's `state_id`, not its address: the
205    /// address is stable for the pointer's life, so paying against it would
206    /// make every update after the first free.
207    pub payment_proof: Option<Vec<u8>>,
208}
209
210impl PointerPutRequest {
211    /// Create a PUT request with no payment proof.
212    #[must_use]
213    pub const fn new(record: Bytes) -> Self {
214        Self {
215            record,
216            payment_proof: None,
217        }
218    }
219
220    /// Create a PUT request carrying a payment proof.
221    #[must_use]
222    pub const fn with_payment(record: Bytes, payment_proof: Vec<u8>) -> Self {
223        Self {
224            record,
225            payment_proof: Some(payment_proof),
226        }
227    }
228}
229
230/// Response to a pointer PUT.
231#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
232pub enum PointerPutResponse {
233    /// The pointer was stored, or replaced what was held.
234    ///
235    /// `state_id` names what the node now holds, so a client can tell an
236    /// accepted update from one that lost a race.
237    Success {
238        /// The pointer's address.
239        address: XorName,
240        /// The state now held at that address.
241        state_id: XorName,
242    },
243    /// The node already held this exact state. Nothing was written.
244    ///
245    /// Distinct from `Success` because it is what a re-submission of a paid
246    /// state gets: the update is not lost, but it bought nothing either.
247    Unchanged {
248        /// The pointer's address.
249        address: XorName,
250        /// The state held, which equals the one submitted.
251        state_id: XorName,
252    },
253    /// The submitted record lost to what the node already holds.
254    Stale {
255        /// The pointer's address.
256        address: XorName,
257        /// The state the node holds instead.
258        state_id: XorName,
259    },
260    /// Payment is required to store this state.
261    PaymentRequired {
262        /// Error message.
263        message: String,
264    },
265    /// An error occurred.
266    Error(ProtocolError),
267}
268
269/// Request to retrieve a pointer.
270#[derive(Debug, Clone, Serialize, Deserialize)]
271pub struct PointerGetRequest {
272    /// The pointer address to retrieve.
273    pub address: XorName,
274}
275
276impl PointerGetRequest {
277    /// Create a GET request for `address`.
278    #[must_use]
279    pub const fn new(address: XorName) -> Self {
280        Self { address }
281    }
282}
283
284/// Response to a pointer GET.
285#[derive(Debug, Clone, Serialize, Deserialize)]
286pub enum PointerGetResponse {
287    /// The pointer held at the requested address.
288    Success {
289        /// The canonical pointer encoding.
290        record: Bytes,
291    },
292    /// No pointer is held at that address.
293    NotFound {
294        /// The address asked for.
295        address: XorName,
296    },
297    /// An error occurred.
298    Error(ProtocolError),
299}
300
301/// Wire-format wrapper that pairs a sender-assigned `request_id` with
302/// a [`ChunkMessageBody`].
303///
304/// The sender picks a unique `request_id`; the handler echoes it back
305/// in the response so callers can correlate replies by ID rather than
306/// by source peer.
307#[derive(Debug, Clone, Serialize, Deserialize)]
308pub struct ChunkMessage {
309    /// Sender-assigned identifier, echoed back in the response.
310    pub request_id: u64,
311    /// The protocol message body.
312    pub body: ChunkMessageBody,
313}
314
315impl ChunkMessage {
316    /// Encode the message to bytes using postcard.
317    ///
318    /// # Errors
319    ///
320    /// Returns an error if serialization fails.
321    pub fn encode(&self) -> Result<Vec<u8>, ProtocolError> {
322        postcard::to_stdvec(self).map_err(|e| ProtocolError::SerializationFailed(e.to_string()))
323    }
324
325    /// Decode a message from bytes using postcard.
326    ///
327    /// Rejects payloads larger than [`MAX_WIRE_MESSAGE_SIZE`] before
328    /// attempting deserialization.
329    ///
330    /// # Errors
331    ///
332    /// Returns [`ProtocolError::MessageTooLarge`] if the input exceeds the
333    /// size limit, or [`ProtocolError::DeserializationFailed`] if postcard
334    /// cannot parse the data.
335    pub fn decode(data: &[u8]) -> Result<Self, ProtocolError> {
336        if data.len() > MAX_WIRE_MESSAGE_SIZE {
337            return Err(ProtocolError::MessageTooLarge {
338                size: data.len(),
339                max_size: MAX_WIRE_MESSAGE_SIZE,
340            });
341        }
342        postcard::from_bytes(data).map_err(|e| ProtocolError::DeserializationFailed(e.to_string()))
343    }
344}
345
346// =============================================================================
347// PUT Request/Response
348// =============================================================================
349
350/// Request to store a chunk.
351///
352/// `content` is held as `bytes::Bytes` so that callers fanning the same
353/// chunk out to multiple recipients (e.g. close-group replication) share a
354/// single backing buffer via refcount instead of deep-copying the 4 MB
355/// payload per peer. Wire format is unchanged: `Bytes` serializes as a
356/// byte sequence, identical to `Vec<u8>` under postcard/serde.
357#[derive(Debug, Clone, Serialize, Deserialize)]
358pub struct ChunkPutRequest {
359    /// The content-addressed identifier (BLAKE3 of content).
360    pub address: XorName,
361    /// The chunk data.
362    pub content: Bytes,
363    /// Optional payment proof (serialized `ProofOfPayment`).
364    /// Required for new chunks unless already verified.
365    pub payment_proof: Option<Vec<u8>>,
366}
367
368impl ChunkPutRequest {
369    /// Create a new PUT request.
370    #[must_use]
371    pub fn new(address: XorName, content: Bytes) -> Self {
372        Self {
373            address,
374            content,
375            payment_proof: None,
376        }
377    }
378
379    /// Create a new PUT request with payment proof.
380    #[must_use]
381    pub fn with_payment(address: XorName, content: Bytes, payment_proof: Vec<u8>) -> Self {
382        Self {
383            address,
384            content,
385            payment_proof: Some(payment_proof),
386        }
387    }
388}
389
390/// Response to a PUT request.
391#[derive(Debug, Clone, Serialize, Deserialize)]
392#[non_exhaustive]
393pub enum ChunkPutResponse {
394    /// Chunk stored successfully.
395    Success {
396        /// The address where the chunk was stored.
397        address: XorName,
398    },
399    /// Chunk already exists (idempotent success).
400    AlreadyExists {
401        /// The existing chunk address.
402        address: XorName,
403    },
404    /// Payment is required to store this chunk.
405    PaymentRequired {
406        /// Error message.
407        message: String,
408    },
409    /// An error occurred.
410    Error(ProtocolError),
411}
412
413// =============================================================================
414// GET Request/Response
415// =============================================================================
416
417/// Request to retrieve a chunk.
418#[derive(Debug, Clone, Serialize, Deserialize)]
419pub struct ChunkGetRequest {
420    /// The content-addressed identifier to retrieve.
421    pub address: XorName,
422}
423
424impl ChunkGetRequest {
425    /// Create a new GET request.
426    #[must_use]
427    pub fn new(address: XorName) -> Self {
428        Self { address }
429    }
430}
431
432/// Response to a GET request.
433#[derive(Debug, Clone, Serialize, Deserialize)]
434#[non_exhaustive]
435pub enum ChunkGetResponse {
436    /// Chunk found and returned.
437    Success {
438        /// The chunk address.
439        address: XorName,
440        /// The chunk data.
441        content: Vec<u8>,
442    },
443    /// Chunk not found.
444    NotFound {
445        /// The requested address.
446        address: XorName,
447    },
448    /// An error occurred.
449    Error(ProtocolError),
450}
451
452// =============================================================================
453// Quote Request/Response
454// =============================================================================
455
456/// Request a storage quote for a chunk.
457#[derive(Debug, Clone, Serialize, Deserialize)]
458pub struct ChunkQuoteRequest {
459    /// The content address of the data to store.
460    pub address: XorName,
461    /// Size of the data in bytes.
462    pub data_size: u64,
463    /// Data type identifier (0 for chunks).
464    pub data_type: u32,
465}
466
467impl ChunkQuoteRequest {
468    /// Create a new quote request.
469    #[must_use]
470    pub fn new(address: XorName, data_size: u64) -> Self {
471        Self {
472            address,
473            data_size,
474            data_type: DATA_TYPE_CHUNK,
475        }
476    }
477}
478
479/// Request a storage quote, declaring the settlement rules the client pays
480/// under.
481///
482/// Same fields as [`ChunkQuoteRequest`] plus `settlement_version`. A separate
483/// struct rather than a field on the original, because [`ChunkMessage`] is
484/// postcard-encoded and postcard is not self-describing: adding a field would
485/// silently change how every existing peer reads the message, while adding a
486/// variant is rejected cleanly by peers that do not know it.
487#[derive(Debug, Clone, Serialize, Deserialize)]
488pub struct ChunkQuoteRequestV2 {
489    /// The content address of the data to store.
490    pub address: XorName,
491    /// Size of the data in bytes.
492    pub data_size: u64,
493    /// Data type identifier (0 for chunks).
494    pub data_type: u32,
495    /// The settlement rules this client pays under. See
496    /// [`CURRENT_SETTLEMENT_VERSION`].
497    pub settlement_version: u32,
498}
499
500impl ChunkQuoteRequestV2 {
501    /// Create a new quote request declaring this build's settlement version.
502    #[must_use]
503    pub fn new(address: XorName, data_size: u64) -> Self {
504        Self {
505            address,
506            data_size,
507            data_type: DATA_TYPE_CHUNK,
508            settlement_version: CURRENT_SETTLEMENT_VERSION,
509        }
510    }
511}
512
513/// Response with a storage quote.
514#[derive(Debug, Clone, Serialize, Deserialize)]
515#[non_exhaustive]
516pub enum ChunkQuoteResponse {
517    /// Quote generated successfully.
518    ///
519    /// When `already_stored` is `true` the node already holds this chunk and no
520    /// payment is required — the client should skip the pay-then-PUT cycle for
521    /// this address. The quote is still included for informational purposes.
522    Success {
523        /// Serialized `PaymentQuote`.
524        quote: Vec<u8>,
525        /// `true` when the chunk already exists on this node (skip payment).
526        already_stored: bool,
527        /// ADR-0004: the serialized signed storage commitment the quote's price
528        /// was derived from, so the client can verify the binding before paying
529        /// ("the commitment arrived with the quote") and forward it as a sidecar
530        /// in the PUT bundle. `None` for a baseline quote (no commitment to
531        /// pin), or from a node that has not yet rotated a commitment. Opaque
532        /// bytes: `ant-protocol` stays agnostic of `ant-node`'s commitment type;
533        /// the client resolves it only to match the quote's `commitment_pin`.
534        ///
535        /// NOTE: this enum is encoded with **postcard** (see [`ChunkMessage::encode`]),
536        /// which is non-self-describing — `#[serde(default)]` does NOT make an
537        /// old-format `Success` (without this field) decode against new code, and
538        /// vice versa. ADR-0004 is a HARD CUTOVER: the whole fleet and clients
539        /// upgrade together, so old/new `ChunkQuoteResponse` never interoperate.
540        /// The attribute only keeps `Default`-based construction ergonomic; it is
541        /// not a wire-compat guarantee. (Contrast `PaymentQuote`/`PaymentProof`,
542        /// which ARE rmp-encoded, where tail `serde(default)` is decode-compatible.)
543        #[serde(default)]
544        commitment: Option<Vec<u8>>,
545    },
546    /// Quote generation failed.
547    Error(ProtocolError),
548}
549
550// =============================================================================
551// Merkle Candidate Quote Request/Response
552// =============================================================================
553
554/// Request a merkle candidate quote for batch payments.
555///
556/// Part of the merkle batch payment system where clients collect
557/// signed candidate quotes from 16 closest peers per pool.
558#[derive(Debug, Clone, Serialize, Deserialize)]
559pub struct MerkleCandidateQuoteRequest {
560    /// The candidate pool address (hash of midpoint || root || timestamp).
561    pub address: XorName,
562    /// Data type identifier (0 for chunks).
563    pub data_type: u32,
564    /// Size of the data in bytes.
565    pub data_size: u64,
566    /// Client-provided merkle payment timestamp (unix seconds).
567    pub merkle_payment_timestamp: u64,
568}
569
570/// Request a merkle candidate quote, declaring the settlement rules the client
571/// pays under.
572///
573/// Same fields as [`MerkleCandidateQuoteRequest`] plus `settlement_version`.
574/// See [`ChunkQuoteRequestV2`] for why this is a separate struct.
575///
576/// This is the variant that matters most for the merkle path: a batch pays
577/// on-chain **before** any storer sees a PUT, so a storer that only checks the
578/// settlement rule at PUT time is checking it after the money is gone.
579#[derive(Debug, Clone, Serialize, Deserialize)]
580pub struct MerkleCandidateQuoteRequestV2 {
581    /// The candidate pool address (hash of midpoint || root || timestamp).
582    pub address: XorName,
583    /// Data type identifier (0 for chunks).
584    pub data_type: u32,
585    /// Size of the data in bytes.
586    pub data_size: u64,
587    /// Client-provided merkle payment timestamp (unix seconds).
588    pub merkle_payment_timestamp: u64,
589    /// The settlement rules this client pays under. See
590    /// [`CURRENT_SETTLEMENT_VERSION`].
591    pub settlement_version: u32,
592}
593
594impl MerkleCandidateQuoteRequestV2 {
595    /// Create a new merkle candidate quote request declaring this build's
596    /// settlement version.
597    #[must_use]
598    pub fn new(address: XorName, data_size: u64, merkle_payment_timestamp: u64) -> Self {
599        Self {
600            address,
601            data_type: DATA_TYPE_CHUNK,
602            data_size,
603            merkle_payment_timestamp,
604            settlement_version: CURRENT_SETTLEMENT_VERSION,
605        }
606    }
607}
608
609/// Response with a merkle candidate quote.
610#[derive(Debug, Clone, Serialize, Deserialize)]
611#[non_exhaustive]
612pub enum MerkleCandidateQuoteResponse {
613    /// Candidate quote generated successfully.
614    /// Contains the serialized `MerklePaymentCandidateNode`.
615    Success {
616        /// Serialized `MerklePaymentCandidateNode`.
617        candidate_node: Vec<u8>,
618        /// ADR-0004: the serialized signed storage commitment the candidate's
619        /// price was derived from, so the client can fully resolve the binding
620        /// BEFORE paying (resolve-before-pay). `None` for a baseline candidate.
621        /// Unlike the single-node path, this commitment is NOT forwarded in the
622        /// merkle PUT bundle — sixteen per-candidate sidecars exceeded the
623        /// storer's payment-proof size budget, so current clients omit them and
624        /// storers resolve merkle pins from gossip or a `GetCommitmentByPin`
625        /// fetch (`MerklePaymentProof.commitment_sidecars` stays for legacy
626        /// bundles). Same semantics as
627        /// [`ChunkQuoteResponse::Success::commitment`]; postcard-encoded, so
628        /// this is a hard-cutover field, not an interop guarantee.
629        #[serde(default)]
630        commitment: Option<Vec<u8>>,
631    },
632    /// Quote generation failed.
633    Error(ProtocolError),
634}
635
636// =============================================================================
637// Payment Proof Type Tags
638// =============================================================================
639
640/// Version byte prefix for payment proof serialization.
641/// Allows the verifier to detect proof type before deserialization.
642pub const PROOF_TAG_SINGLE_NODE: u8 = 0x01;
643/// Version byte prefix for merkle payment proofs.
644pub const PROOF_TAG_MERKLE: u8 = 0x02;
645
646// =============================================================================
647// Protocol Errors
648// =============================================================================
649
650/// Errors that can occur during protocol operations.
651#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
652#[non_exhaustive]
653pub enum ProtocolError {
654    /// Message serialization failed.
655    SerializationFailed(String),
656    /// Message deserialization failed.
657    DeserializationFailed(String),
658    /// Wire message exceeds the maximum allowed size.
659    MessageTooLarge {
660        /// Actual size of the message in bytes.
661        size: usize,
662        /// Maximum allowed size.
663        max_size: usize,
664    },
665    /// Chunk exceeds maximum size.
666    ChunkTooLarge {
667        /// Size of the chunk in bytes.
668        size: usize,
669        /// Maximum allowed size.
670        max_size: usize,
671    },
672    /// Content address mismatch (hash(content) != address).
673    AddressMismatch {
674        /// Expected address.
675        expected: XorName,
676        /// Actual address computed from content.
677        actual: XorName,
678    },
679    /// Storage operation failed.
680    StorageFailed(String),
681    /// Payment verification failed.
682    PaymentFailed(String),
683    /// Quote generation failed.
684    QuoteFailed(String),
685    /// Internal error.
686    Internal(String),
687    /// The client settles payments under rules this node no longer accepts, so
688    /// no quote was issued.
689    ///
690    /// Refused at quote time on purpose. A client that pays under superseded
691    /// rules produces an on-chain payment every storer will reject, and that
692    /// payment cannot be refunded, so the only useful place to stop it is
693    /// before the client spends anything.
694    ///
695    /// Appended last so existing variants keep their wire discriminants. A
696    /// peer old enough not to know this variant cannot be sent it: it would
697    /// have had to send a V2 request to earn it, and only builds that carry
698    /// this variant do that.
699    ClientUpdateRequired {
700        /// Settlement version the client declared.
701        client_settlement_version: u32,
702        /// Oldest settlement version this node will quote for.
703        min_settlement_version: u32,
704    },
705    /// This node settles under older rules than the client, so it declined to
706    /// quote rather than promise to accept a payment it may not recognise.
707    ///
708    /// The mirror image of [`Self::ClientUpdateRequired`], and deliberately a
709    /// separate variant: it is not a verdict about the client, and a client
710    /// that receives it should quietly use a different storer rather than tell
711    /// its user anything. During a client-first rollout most of the fleet is
712    /// briefly in this state, so treating it as a client fault would strand
713    /// every up-to-date user.
714    StorerUpdateRequired {
715        /// Settlement version the client declared.
716        client_settlement_version: u32,
717        /// Newest settlement version this node understands.
718        node_settlement_version: u32,
719    },
720}
721
722impl std::fmt::Display for ProtocolError {
723    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
724        match self {
725            Self::SerializationFailed(msg) => write!(f, "serialization failed: {msg}"),
726            Self::DeserializationFailed(msg) => write!(f, "deserialization failed: {msg}"),
727            Self::MessageTooLarge { size, max_size } => {
728                write!(f, "message size {size} exceeds maximum {max_size}")
729            }
730            Self::ChunkTooLarge { size, max_size } => {
731                write!(f, "chunk size {size} exceeds maximum {max_size}")
732            }
733            Self::AddressMismatch { expected, actual } => {
734                write!(
735                    f,
736                    "address mismatch: expected {}, got {}",
737                    hex::encode(expected),
738                    hex::encode(actual)
739                )
740            }
741            Self::StorageFailed(msg) => write!(f, "storage failed: {msg}"),
742            Self::PaymentFailed(msg) => write!(f, "payment failed: {msg}"),
743            Self::QuoteFailed(msg) => write!(f, "quote failed: {msg}"),
744            Self::Internal(msg) => write!(f, "internal error: {msg}"),
745            Self::ClientUpdateRequired {
746                client_settlement_version,
747                min_settlement_version,
748            } => write!(
749                f,
750                "{}",
751                client_update_required_message(*client_settlement_version, *min_settlement_version)
752            ),
753            Self::StorerUpdateRequired {
754                client_settlement_version,
755                node_settlement_version,
756            } => write!(
757                f,
758                "this node settles under version {node_settlement_version} and cannot \
759                 promise to accept a version {client_settlement_version} payment, so it \
760                 issued no quote. Nothing was charged; use a different storer."
761            ),
762        }
763    }
764}
765
766/// The upgrade instruction shown to a user whose client cannot settle
767/// correctly.
768///
769/// A free function rather than only a `Display` arm so the storer can log the
770/// same wording it sends back, and so the client can reuse it when it
771/// translates the rejection into a CLI error. Kept deliberately plain: the
772/// reader is an end user staring at a failed upload, not an operator.
773#[must_use]
774pub fn client_update_required_message(
775    client_settlement_version: u32,
776    min_settlement_version: u32,
777) -> String {
778    format!(
779        "your client is too old to pay the current storage rate \
780         (it settles under version {client_settlement_version}, this node requires \
781         at least {min_settlement_version}), so no quote was issued and nothing was \
782         charged. Run `ant update` to upgrade, or reinstall from \
783         https://github.com/WithAutonomi/ant-client/releases/latest"
784    )
785}
786
787impl std::error::Error for ProtocolError {}
788
789#[cfg(test)]
790#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
791mod tests {
792    use super::*;
793
794    #[test]
795    fn test_put_request_encode_decode() {
796        let address = [0xAB; 32];
797        let content = Bytes::from_static(&[1, 2, 3, 4, 5]);
798        let request = ChunkPutRequest::new(address, content.clone());
799        let msg = ChunkMessage {
800            request_id: 42,
801            body: ChunkMessageBody::PutRequest(request),
802        };
803
804        let encoded = msg.encode().expect("encode should succeed");
805        let decoded = ChunkMessage::decode(&encoded).expect("decode should succeed");
806
807        assert_eq!(decoded.request_id, 42);
808        if let ChunkMessageBody::PutRequest(req) = decoded.body {
809            assert_eq!(req.address, address);
810            assert_eq!(req.content, content);
811            assert!(req.payment_proof.is_none());
812        } else {
813            panic!("expected PutRequest");
814        }
815    }
816
817    #[test]
818    fn test_put_request_with_payment() {
819        let address = [0xAB; 32];
820        let content = Bytes::from_static(&[1, 2, 3, 4, 5]);
821        let payment = vec![10, 20, 30];
822        let request = ChunkPutRequest::with_payment(address, content.clone(), payment.clone());
823
824        assert_eq!(request.address, address);
825        assert_eq!(request.content, content);
826        assert_eq!(request.payment_proof, Some(payment));
827    }
828
829    #[test]
830    fn test_get_request_encode_decode() {
831        let address = [0xCD; 32];
832        let request = ChunkGetRequest::new(address);
833        let msg = ChunkMessage {
834            request_id: 7,
835            body: ChunkMessageBody::GetRequest(request),
836        };
837
838        let encoded = msg.encode().expect("encode should succeed");
839        let decoded = ChunkMessage::decode(&encoded).expect("decode should succeed");
840
841        assert_eq!(decoded.request_id, 7);
842        if let ChunkMessageBody::GetRequest(req) = decoded.body {
843            assert_eq!(req.address, address);
844        } else {
845            panic!("expected GetRequest");
846        }
847    }
848
849    #[test]
850    fn test_put_response_success() {
851        let address = [0xEF; 32];
852        let response = ChunkPutResponse::Success { address };
853        let msg = ChunkMessage {
854            request_id: 99,
855            body: ChunkMessageBody::PutResponse(response),
856        };
857
858        let encoded = msg.encode().expect("encode should succeed");
859        let decoded = ChunkMessage::decode(&encoded).expect("decode should succeed");
860
861        assert_eq!(decoded.request_id, 99);
862        if let ChunkMessageBody::PutResponse(ChunkPutResponse::Success { address: addr }) =
863            decoded.body
864        {
865            assert_eq!(addr, address);
866        } else {
867            panic!("expected PutResponse::Success");
868        }
869    }
870
871    #[test]
872    fn test_get_response_not_found() {
873        let address = [0x12; 32];
874        let response = ChunkGetResponse::NotFound { address };
875        let msg = ChunkMessage {
876            request_id: 0,
877            body: ChunkMessageBody::GetResponse(response),
878        };
879
880        let encoded = msg.encode().expect("encode should succeed");
881        let decoded = ChunkMessage::decode(&encoded).expect("decode should succeed");
882
883        assert_eq!(decoded.request_id, 0);
884        if let ChunkMessageBody::GetResponse(ChunkGetResponse::NotFound { address: addr }) =
885            decoded.body
886        {
887            assert_eq!(addr, address);
888        } else {
889            panic!("expected GetResponse::NotFound");
890        }
891    }
892
893    #[test]
894    fn test_quote_request_encode_decode() {
895        let address = [0x34; 32];
896        let request = ChunkQuoteRequest::new(address, 1024);
897        let msg = ChunkMessage {
898            request_id: 1,
899            body: ChunkMessageBody::QuoteRequest(request),
900        };
901
902        let encoded = msg.encode().expect("encode should succeed");
903        let decoded = ChunkMessage::decode(&encoded).expect("decode should succeed");
904
905        assert_eq!(decoded.request_id, 1);
906        if let ChunkMessageBody::QuoteRequest(req) = decoded.body {
907            assert_eq!(req.address, address);
908            assert_eq!(req.data_size, 1024);
909            assert_eq!(req.data_type, DATA_TYPE_CHUNK);
910        } else {
911            panic!("expected QuoteRequest");
912        }
913    }
914
915    #[test]
916    fn test_protocol_error_display() {
917        let err = ProtocolError::ChunkTooLarge {
918            size: 5_000_000,
919            max_size: MAX_CHUNK_SIZE,
920        };
921        assert!(err.to_string().contains("5000000"));
922        assert!(err.to_string().contains(&MAX_CHUNK_SIZE.to_string()));
923
924        let err = ProtocolError::AddressMismatch {
925            expected: [0xAA; 32],
926            actual: [0xBB; 32],
927        };
928        let display = err.to_string();
929        assert!(display.contains("address mismatch"));
930    }
931
932    #[test]
933    fn test_decode_rejects_oversized_payload() {
934        let oversized = vec![0u8; MAX_WIRE_MESSAGE_SIZE + 1];
935        let result = ChunkMessage::decode(&oversized);
936        assert!(result.is_err());
937        let err = result.unwrap_err();
938        assert!(
939            matches!(err, ProtocolError::MessageTooLarge { .. }),
940            "expected MessageTooLarge, got {err:?}"
941        );
942    }
943
944    #[test]
945    fn test_invalid_decode() {
946        let invalid_data = vec![0xFF, 0xFF, 0xFF];
947        let result = ChunkMessage::decode(&invalid_data);
948        assert!(result.is_err());
949    }
950
951    #[test]
952    fn test_constants() {
953        assert_eq!(CHUNK_PROTOCOL_ID, "autonomi.ant.chunk.v1");
954        assert_eq!(PROTOCOL_VERSION, 1);
955        assert_eq!(MAX_CHUNK_SIZE, 4 * 1024 * 1024);
956        assert_eq!(DATA_TYPE_CHUNK, 0);
957    }
958
959    #[test]
960    fn test_proof_tag_constants() {
961        // Tags must be distinct non-zero bytes
962        assert_ne!(PROOF_TAG_SINGLE_NODE, PROOF_TAG_MERKLE);
963        assert_ne!(PROOF_TAG_SINGLE_NODE, 0x00);
964        assert_ne!(PROOF_TAG_MERKLE, 0x00);
965        assert_eq!(PROOF_TAG_SINGLE_NODE, 0x01);
966        assert_eq!(PROOF_TAG_MERKLE, 0x02);
967    }
968
969    #[test]
970    fn test_merkle_candidate_quote_request_encode_decode() {
971        let address = [0x56; 32];
972        let request = MerkleCandidateQuoteRequest {
973            address,
974            data_type: DATA_TYPE_CHUNK,
975            data_size: 2048,
976            merkle_payment_timestamp: 1_700_000_000,
977        };
978        let msg = ChunkMessage {
979            request_id: 500,
980            body: ChunkMessageBody::MerkleCandidateQuoteRequest(request),
981        };
982
983        let encoded = msg.encode().expect("encode should succeed");
984        let decoded = ChunkMessage::decode(&encoded).expect("decode should succeed");
985
986        assert_eq!(decoded.request_id, 500);
987        if let ChunkMessageBody::MerkleCandidateQuoteRequest(req) = decoded.body {
988            assert_eq!(req.address, address);
989            assert_eq!(req.data_type, DATA_TYPE_CHUNK);
990            assert_eq!(req.data_size, 2048);
991            assert_eq!(req.merkle_payment_timestamp, 1_700_000_000);
992        } else {
993            panic!("expected MerkleCandidateQuoteRequest");
994        }
995    }
996
997    #[test]
998    fn test_merkle_candidate_quote_response_success_encode_decode() {
999        let candidate_node_bytes = vec![0xAA, 0xBB, 0xCC, 0xDD];
1000        let response = MerkleCandidateQuoteResponse::Success {
1001            candidate_node: candidate_node_bytes.clone(),
1002            commitment: Some(vec![0x11, 0x22]),
1003        };
1004        let msg = ChunkMessage {
1005            request_id: 501,
1006            body: ChunkMessageBody::MerkleCandidateQuoteResponse(response),
1007        };
1008
1009        let encoded = msg.encode().expect("encode should succeed");
1010        let decoded = ChunkMessage::decode(&encoded).expect("decode should succeed");
1011
1012        assert_eq!(decoded.request_id, 501);
1013        if let ChunkMessageBody::MerkleCandidateQuoteResponse(
1014            MerkleCandidateQuoteResponse::Success {
1015                candidate_node,
1016                commitment,
1017            },
1018        ) = decoded.body
1019        {
1020            assert_eq!(candidate_node, candidate_node_bytes);
1021            assert_eq!(commitment, Some(vec![0x11, 0x22]));
1022        } else {
1023            panic!("expected MerkleCandidateQuoteResponse::Success");
1024        }
1025    }
1026
1027    #[test]
1028    fn test_merkle_candidate_quote_response_error_encode_decode() {
1029        let error = ProtocolError::QuoteFailed("no libp2p keypair".to_string());
1030        let response = MerkleCandidateQuoteResponse::Error(error.clone());
1031        let msg = ChunkMessage {
1032            request_id: 502,
1033            body: ChunkMessageBody::MerkleCandidateQuoteResponse(response),
1034        };
1035
1036        let encoded = msg.encode().expect("encode should succeed");
1037        let decoded = ChunkMessage::decode(&encoded).expect("decode should succeed");
1038
1039        assert_eq!(decoded.request_id, 502);
1040        if let ChunkMessageBody::MerkleCandidateQuoteResponse(
1041            MerkleCandidateQuoteResponse::Error(err),
1042        ) = decoded.body
1043        {
1044            assert_eq!(err, error);
1045        } else {
1046            panic!("expected MerkleCandidateQuoteResponse::Error");
1047        }
1048    }
1049
1050    // =========================================================================
1051    // Settlement version
1052    // =========================================================================
1053
1054    /// Discriminant of a message body, read straight off the wire.
1055    ///
1056    /// `request_id: 0` encodes as a single varint byte, so the body's variant
1057    /// index is byte 1. Reading it directly is the point: it is what a peer
1058    /// built against an older `ant-protocol` sees.
1059    fn wire_discriminant(body: ChunkMessageBody) -> u8 {
1060        let encoded = ChunkMessage {
1061            request_id: 0,
1062            body,
1063        }
1064        .encode()
1065        .expect("encode should succeed");
1066        *encoded.get(1).expect("body discriminant should be present")
1067    }
1068
1069    /// The load-bearing test for this whole design.
1070    ///
1071    /// The settlement version ships as appended variants precisely so existing
1072    /// peers keep decoding. That only holds while every prior variant keeps
1073    /// its wire index, which postcard assigns by declaration order. Insert a
1074    /// variant anywhere but the end and every older peer silently misreads
1075    /// every message from this one. Pinning the indices turns that from a
1076    /// production incident into a failing test.
1077    #[test]
1078    fn appending_v2_variants_leaves_existing_discriminants_untouched() {
1079        let address = [0x11; 32];
1080
1081        assert_eq!(
1082            wire_discriminant(ChunkMessageBody::PutRequest(ChunkPutRequest::new(
1083                address,
1084                Bytes::from_static(&[1]),
1085            ))),
1086            0,
1087        );
1088        assert_eq!(
1089            wire_discriminant(ChunkMessageBody::GetRequest(ChunkGetRequest { address })),
1090            2,
1091        );
1092        assert_eq!(
1093            wire_discriminant(ChunkMessageBody::QuoteRequest(ChunkQuoteRequest::new(
1094                address, 1024,
1095            ))),
1096            4,
1097        );
1098        assert_eq!(
1099            wire_discriminant(ChunkMessageBody::MerkleCandidateQuoteRequest(
1100                MerkleCandidateQuoteRequest {
1101                    address,
1102                    data_type: DATA_TYPE_CHUNK,
1103                    data_size: 1024,
1104                    merkle_payment_timestamp: 1_785_855_600,
1105                }
1106            )),
1107            6,
1108        );
1109
1110        // Responses carry the same obligation as requests: a peer decoding a
1111        // reply reads the same discriminant space, so pinning only the request
1112        // half would let a response variant be reordered without any test
1113        // noticing.
1114        assert_eq!(
1115            wire_discriminant(ChunkMessageBody::PutResponse(ChunkPutResponse::Success {
1116                address
1117            })),
1118            1,
1119        );
1120        assert_eq!(
1121            wire_discriminant(ChunkMessageBody::GetResponse(ChunkGetResponse::NotFound {
1122                address
1123            })),
1124            3,
1125        );
1126        assert_eq!(
1127            wire_discriminant(ChunkMessageBody::QuoteResponse(ChunkQuoteResponse::Error(
1128                ProtocolError::Internal(String::new())
1129            ))),
1130            5,
1131        );
1132        assert_eq!(
1133            wire_discriminant(ChunkMessageBody::MerkleCandidateQuoteResponse(
1134                MerkleCandidateQuoteResponse::Error(ProtocolError::Internal(String::new()))
1135            )),
1136            7,
1137        );
1138
1139        // The new variants take the next free indices, above everything an
1140        // older peer knows, so it rejects them as unknown rather than
1141        // misreading a variant it does know.
1142        assert_eq!(
1143            wire_discriminant(ChunkMessageBody::QuoteRequestV2(ChunkQuoteRequestV2::new(
1144                address, 1024,
1145            ))),
1146            8,
1147        );
1148        assert_eq!(
1149            wire_discriminant(ChunkMessageBody::MerkleCandidateQuoteRequestV2(
1150                MerkleCandidateQuoteRequestV2::new(address, 1024, 1_785_855_600)
1151            )),
1152            9,
1153        );
1154    }
1155
1156    /// `ProtocolError` rides inside quote responses, so its variants carry the
1157    /// same append-only obligation as the message bodies.
1158    #[test]
1159    fn client_update_required_is_appended_to_protocol_error() {
1160        let encode = |e: &ProtocolError| postcard::to_stdvec(e).expect("encode should succeed");
1161
1162        assert_eq!(
1163            encode(&ProtocolError::SerializationFailed(String::new()))
1164                .first()
1165                .copied(),
1166            Some(0),
1167        );
1168        assert_eq!(
1169            encode(&ProtocolError::DeserializationFailed(String::new()))
1170                .first()
1171                .copied(),
1172            Some(1),
1173        );
1174        assert_eq!(
1175            encode(&ProtocolError::MessageTooLarge {
1176                size: 0,
1177                max_size: 0
1178            })
1179            .first()
1180            .copied(),
1181            Some(2),
1182        );
1183        assert_eq!(
1184            encode(&ProtocolError::ChunkTooLarge {
1185                size: 0,
1186                max_size: 0
1187            })
1188            .first()
1189            .copied(),
1190            Some(3),
1191        );
1192        assert_eq!(
1193            encode(&ProtocolError::AddressMismatch {
1194                expected: [0u8; 32],
1195                actual: [0u8; 32]
1196            })
1197            .first()
1198            .copied(),
1199            Some(4),
1200        );
1201        assert_eq!(
1202            encode(&ProtocolError::StorageFailed(String::new()))
1203                .first()
1204                .copied(),
1205            Some(5),
1206        );
1207        assert_eq!(
1208            encode(&ProtocolError::PaymentFailed(String::new()))
1209                .first()
1210                .copied(),
1211            Some(6),
1212        );
1213        assert_eq!(
1214            encode(&ProtocolError::QuoteFailed(String::new()))
1215                .first()
1216                .copied(),
1217            Some(7),
1218        );
1219        assert_eq!(
1220            encode(&ProtocolError::Internal(String::new()))
1221                .first()
1222                .copied(),
1223            Some(8),
1224        );
1225        assert_eq!(
1226            encode(&ProtocolError::ClientUpdateRequired {
1227                client_settlement_version: 0,
1228                min_settlement_version: 1,
1229            })
1230            .first()
1231            .copied(),
1232            Some(9),
1233        );
1234        assert_eq!(
1235            encode(&ProtocolError::StorerUpdateRequired {
1236                client_settlement_version: 2,
1237                node_settlement_version: 1,
1238            })
1239            .first()
1240            .copied(),
1241            Some(10),
1242        );
1243    }
1244
1245    #[test]
1246    fn v2_quote_request_round_trips_with_the_settlement_version() {
1247        let address = [0x22; 32];
1248        let msg = ChunkMessage {
1249            request_id: 600,
1250            body: ChunkMessageBody::QuoteRequestV2(ChunkQuoteRequestV2::new(address, 4096)),
1251        };
1252
1253        let encoded = msg.encode().expect("encode should succeed");
1254        let decoded = ChunkMessage::decode(&encoded).expect("decode should succeed");
1255
1256        assert_eq!(decoded.request_id, 600);
1257        if let ChunkMessageBody::QuoteRequestV2(req) = decoded.body {
1258            assert_eq!(req.address, address);
1259            assert_eq!(req.data_size, 4096);
1260            assert_eq!(req.data_type, DATA_TYPE_CHUNK);
1261            assert_eq!(req.settlement_version, CURRENT_SETTLEMENT_VERSION);
1262        } else {
1263            panic!("expected QuoteRequestV2");
1264        }
1265    }
1266
1267    #[test]
1268    fn v2_merkle_candidate_request_round_trips_with_the_settlement_version() {
1269        let address = [0x33; 32];
1270        let msg = ChunkMessage {
1271            request_id: 601,
1272            body: ChunkMessageBody::MerkleCandidateQuoteRequestV2(
1273                MerkleCandidateQuoteRequestV2::new(address, 4096, 1_785_855_600),
1274            ),
1275        };
1276
1277        let encoded = msg.encode().expect("encode should succeed");
1278        let decoded = ChunkMessage::decode(&encoded).expect("decode should succeed");
1279
1280        assert_eq!(decoded.request_id, 601);
1281        if let ChunkMessageBody::MerkleCandidateQuoteRequestV2(req) = decoded.body {
1282            assert_eq!(req.address, address);
1283            assert_eq!(req.data_size, 4096);
1284            assert_eq!(req.merkle_payment_timestamp, 1_785_855_600);
1285            assert_eq!(req.settlement_version, CURRENT_SETTLEMENT_VERSION);
1286        } else {
1287            panic!("expected MerkleCandidateQuoteRequestV2");
1288        }
1289    }
1290
1291    #[test]
1292    fn settlement_compatibility_is_bounded_at_both_ends() {
1293        assert_eq!(
1294            settlement_compatibility(CURRENT_SETTLEMENT_VERSION),
1295            SettlementCompatibility::Compatible,
1296        );
1297        // The lower bound is inclusive: the oldest version still served is
1298        // served, not refused.
1299        assert_eq!(
1300            settlement_compatibility(MIN_SUPPORTED_SETTLEMENT_VERSION),
1301            SettlementCompatibility::Compatible,
1302        );
1303        assert_eq!(
1304            settlement_compatibility(MIN_SUPPORTED_SETTLEMENT_VERSION.saturating_sub(1)),
1305            SettlementCompatibility::ClientTooOld,
1306        );
1307    }
1308
1309    /// The correction this replaced an earlier revision for. Serving a client
1310    /// whose settlement rules this build does not know means promising to
1311    /// accept a payment it may reject, and by the time it rejects, the client
1312    /// has settled on-chain and cannot be refunded. Only monotonic increases
1313    /// are safe to wave through, and nothing here can tell whether the next
1314    /// change is one.
1315    #[test]
1316    fn a_newer_settlement_version_is_refused_rather_than_assumed_compatible() {
1317        assert_eq!(
1318            settlement_compatibility(CURRENT_SETTLEMENT_VERSION.saturating_add(1)),
1319            SettlementCompatibility::NodeTooOld,
1320        );
1321    }
1322
1323    /// The two refusals must stay distinct on the wire. One tells a user to
1324    /// upgrade; the other tells a client to pick a different peer and say
1325    /// nothing. Rendering them alike would strand every up-to-date user during
1326    /// a client-first rollout, when most of the fleet is briefly the old side.
1327    #[test]
1328    fn the_two_refusals_do_not_blame_the_same_party() {
1329        let client_at_fault = ProtocolError::ClientUpdateRequired {
1330            client_settlement_version: 0,
1331            min_settlement_version: 1,
1332        }
1333        .to_string();
1334        let node_at_fault = ProtocolError::StorerUpdateRequired {
1335            client_settlement_version: 2,
1336            node_settlement_version: 1,
1337        }
1338        .to_string();
1339
1340        assert!(
1341            client_at_fault.contains("your client is too old"),
1342            "{client_at_fault}"
1343        );
1344        assert!(
1345            !node_at_fault.contains("your client is too old"),
1346            "{node_at_fault}"
1347        );
1348        assert!(
1349            node_at_fault.contains("use a different storer"),
1350            "{node_at_fault}"
1351        );
1352        // Neither costs the user anything, and both must say so.
1353        assert!(
1354            client_at_fault.contains("nothing was charged"),
1355            "{client_at_fault}"
1356        );
1357        assert!(
1358            node_at_fault.contains("Nothing was charged"),
1359            "{node_at_fault}"
1360        );
1361    }
1362
1363    /// The rejection exists to get a user unstuck, so the wording is part of
1364    /// the contract, not decoration.
1365    #[test]
1366    fn update_required_message_tells_the_user_how_to_fix_it() {
1367        let rendered = ProtocolError::ClientUpdateRequired {
1368            client_settlement_version: 0,
1369            min_settlement_version: 1,
1370        }
1371        .to_string();
1372
1373        assert!(rendered.contains("ant update"), "{rendered}");
1374        assert!(rendered.contains("too old"), "{rendered}");
1375        // The whole point is that refusing to quote costs the user nothing.
1376        assert!(rendered.contains("nothing was charged"), "{rendered}");
1377    }
1378}