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}