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}
174
175/// Wire-format wrapper that pairs a sender-assigned `request_id` with
176/// a [`ChunkMessageBody`].
177///
178/// The sender picks a unique `request_id`; the handler echoes it back
179/// in the response so callers can correlate replies by ID rather than
180/// by source peer.
181#[derive(Debug, Clone, Serialize, Deserialize)]
182pub struct ChunkMessage {
183    /// Sender-assigned identifier, echoed back in the response.
184    pub request_id: u64,
185    /// The protocol message body.
186    pub body: ChunkMessageBody,
187}
188
189impl ChunkMessage {
190    /// Encode the message to bytes using postcard.
191    ///
192    /// # Errors
193    ///
194    /// Returns an error if serialization fails.
195    pub fn encode(&self) -> Result<Vec<u8>, ProtocolError> {
196        postcard::to_stdvec(self).map_err(|e| ProtocolError::SerializationFailed(e.to_string()))
197    }
198
199    /// Decode a message from bytes using postcard.
200    ///
201    /// Rejects payloads larger than [`MAX_WIRE_MESSAGE_SIZE`] before
202    /// attempting deserialization.
203    ///
204    /// # Errors
205    ///
206    /// Returns [`ProtocolError::MessageTooLarge`] if the input exceeds the
207    /// size limit, or [`ProtocolError::DeserializationFailed`] if postcard
208    /// cannot parse the data.
209    pub fn decode(data: &[u8]) -> Result<Self, ProtocolError> {
210        if data.len() > MAX_WIRE_MESSAGE_SIZE {
211            return Err(ProtocolError::MessageTooLarge {
212                size: data.len(),
213                max_size: MAX_WIRE_MESSAGE_SIZE,
214            });
215        }
216        postcard::from_bytes(data).map_err(|e| ProtocolError::DeserializationFailed(e.to_string()))
217    }
218}
219
220// =============================================================================
221// PUT Request/Response
222// =============================================================================
223
224/// Request to store a chunk.
225///
226/// `content` is held as `bytes::Bytes` so that callers fanning the same
227/// chunk out to multiple recipients (e.g. close-group replication) share a
228/// single backing buffer via refcount instead of deep-copying the 4 MB
229/// payload per peer. Wire format is unchanged: `Bytes` serializes as a
230/// byte sequence, identical to `Vec<u8>` under postcard/serde.
231#[derive(Debug, Clone, Serialize, Deserialize)]
232pub struct ChunkPutRequest {
233    /// The content-addressed identifier (BLAKE3 of content).
234    pub address: XorName,
235    /// The chunk data.
236    pub content: Bytes,
237    /// Optional payment proof (serialized `ProofOfPayment`).
238    /// Required for new chunks unless already verified.
239    pub payment_proof: Option<Vec<u8>>,
240}
241
242impl ChunkPutRequest {
243    /// Create a new PUT request.
244    #[must_use]
245    pub fn new(address: XorName, content: Bytes) -> Self {
246        Self {
247            address,
248            content,
249            payment_proof: None,
250        }
251    }
252
253    /// Create a new PUT request with payment proof.
254    #[must_use]
255    pub fn with_payment(address: XorName, content: Bytes, payment_proof: Vec<u8>) -> Self {
256        Self {
257            address,
258            content,
259            payment_proof: Some(payment_proof),
260        }
261    }
262}
263
264/// Response to a PUT request.
265#[derive(Debug, Clone, Serialize, Deserialize)]
266#[non_exhaustive]
267pub enum ChunkPutResponse {
268    /// Chunk stored successfully.
269    Success {
270        /// The address where the chunk was stored.
271        address: XorName,
272    },
273    /// Chunk already exists (idempotent success).
274    AlreadyExists {
275        /// The existing chunk address.
276        address: XorName,
277    },
278    /// Payment is required to store this chunk.
279    PaymentRequired {
280        /// Error message.
281        message: String,
282    },
283    /// An error occurred.
284    Error(ProtocolError),
285}
286
287// =============================================================================
288// GET Request/Response
289// =============================================================================
290
291/// Request to retrieve a chunk.
292#[derive(Debug, Clone, Serialize, Deserialize)]
293pub struct ChunkGetRequest {
294    /// The content-addressed identifier to retrieve.
295    pub address: XorName,
296}
297
298impl ChunkGetRequest {
299    /// Create a new GET request.
300    #[must_use]
301    pub fn new(address: XorName) -> Self {
302        Self { address }
303    }
304}
305
306/// Response to a GET request.
307#[derive(Debug, Clone, Serialize, Deserialize)]
308#[non_exhaustive]
309pub enum ChunkGetResponse {
310    /// Chunk found and returned.
311    Success {
312        /// The chunk address.
313        address: XorName,
314        /// The chunk data.
315        content: Vec<u8>,
316    },
317    /// Chunk not found.
318    NotFound {
319        /// The requested address.
320        address: XorName,
321    },
322    /// An error occurred.
323    Error(ProtocolError),
324}
325
326// =============================================================================
327// Quote Request/Response
328// =============================================================================
329
330/// Request a storage quote for a chunk.
331#[derive(Debug, Clone, Serialize, Deserialize)]
332pub struct ChunkQuoteRequest {
333    /// The content address of the data to store.
334    pub address: XorName,
335    /// Size of the data in bytes.
336    pub data_size: u64,
337    /// Data type identifier (0 for chunks).
338    pub data_type: u32,
339}
340
341impl ChunkQuoteRequest {
342    /// Create a new quote request.
343    #[must_use]
344    pub fn new(address: XorName, data_size: u64) -> Self {
345        Self {
346            address,
347            data_size,
348            data_type: DATA_TYPE_CHUNK,
349        }
350    }
351}
352
353/// Request a storage quote, declaring the settlement rules the client pays
354/// under.
355///
356/// Same fields as [`ChunkQuoteRequest`] plus `settlement_version`. A separate
357/// struct rather than a field on the original, because [`ChunkMessage`] is
358/// postcard-encoded and postcard is not self-describing: adding a field would
359/// silently change how every existing peer reads the message, while adding a
360/// variant is rejected cleanly by peers that do not know it.
361#[derive(Debug, Clone, Serialize, Deserialize)]
362pub struct ChunkQuoteRequestV2 {
363    /// The content address of the data to store.
364    pub address: XorName,
365    /// Size of the data in bytes.
366    pub data_size: u64,
367    /// Data type identifier (0 for chunks).
368    pub data_type: u32,
369    /// The settlement rules this client pays under. See
370    /// [`CURRENT_SETTLEMENT_VERSION`].
371    pub settlement_version: u32,
372}
373
374impl ChunkQuoteRequestV2 {
375    /// Create a new quote request declaring this build's settlement version.
376    #[must_use]
377    pub fn new(address: XorName, data_size: u64) -> Self {
378        Self {
379            address,
380            data_size,
381            data_type: DATA_TYPE_CHUNK,
382            settlement_version: CURRENT_SETTLEMENT_VERSION,
383        }
384    }
385}
386
387/// Response with a storage quote.
388#[derive(Debug, Clone, Serialize, Deserialize)]
389#[non_exhaustive]
390pub enum ChunkQuoteResponse {
391    /// Quote generated successfully.
392    ///
393    /// When `already_stored` is `true` the node already holds this chunk and no
394    /// payment is required — the client should skip the pay-then-PUT cycle for
395    /// this address. The quote is still included for informational purposes.
396    Success {
397        /// Serialized `PaymentQuote`.
398        quote: Vec<u8>,
399        /// `true` when the chunk already exists on this node (skip payment).
400        already_stored: bool,
401        /// ADR-0004: the serialized signed storage commitment the quote's price
402        /// was derived from, so the client can verify the binding before paying
403        /// ("the commitment arrived with the quote") and forward it as a sidecar
404        /// in the PUT bundle. `None` for a baseline quote (no commitment to
405        /// pin), or from a node that has not yet rotated a commitment. Opaque
406        /// bytes: `ant-protocol` stays agnostic of `ant-node`'s commitment type;
407        /// the client resolves it only to match the quote's `commitment_pin`.
408        ///
409        /// NOTE: this enum is encoded with **postcard** (see [`ChunkMessage::encode`]),
410        /// which is non-self-describing — `#[serde(default)]` does NOT make an
411        /// old-format `Success` (without this field) decode against new code, and
412        /// vice versa. ADR-0004 is a HARD CUTOVER: the whole fleet and clients
413        /// upgrade together, so old/new `ChunkQuoteResponse` never interoperate.
414        /// The attribute only keeps `Default`-based construction ergonomic; it is
415        /// not a wire-compat guarantee. (Contrast `PaymentQuote`/`PaymentProof`,
416        /// which ARE rmp-encoded, where tail `serde(default)` is decode-compatible.)
417        #[serde(default)]
418        commitment: Option<Vec<u8>>,
419    },
420    /// Quote generation failed.
421    Error(ProtocolError),
422}
423
424// =============================================================================
425// Merkle Candidate Quote Request/Response
426// =============================================================================
427
428/// Request a merkle candidate quote for batch payments.
429///
430/// Part of the merkle batch payment system where clients collect
431/// signed candidate quotes from 16 closest peers per pool.
432#[derive(Debug, Clone, Serialize, Deserialize)]
433pub struct MerkleCandidateQuoteRequest {
434    /// The candidate pool address (hash of midpoint || root || timestamp).
435    pub address: XorName,
436    /// Data type identifier (0 for chunks).
437    pub data_type: u32,
438    /// Size of the data in bytes.
439    pub data_size: u64,
440    /// Client-provided merkle payment timestamp (unix seconds).
441    pub merkle_payment_timestamp: u64,
442}
443
444/// Request a merkle candidate quote, declaring the settlement rules the client
445/// pays under.
446///
447/// Same fields as [`MerkleCandidateQuoteRequest`] plus `settlement_version`.
448/// See [`ChunkQuoteRequestV2`] for why this is a separate struct.
449///
450/// This is the variant that matters most for the merkle path: a batch pays
451/// on-chain **before** any storer sees a PUT, so a storer that only checks the
452/// settlement rule at PUT time is checking it after the money is gone.
453#[derive(Debug, Clone, Serialize, Deserialize)]
454pub struct MerkleCandidateQuoteRequestV2 {
455    /// The candidate pool address (hash of midpoint || root || timestamp).
456    pub address: XorName,
457    /// Data type identifier (0 for chunks).
458    pub data_type: u32,
459    /// Size of the data in bytes.
460    pub data_size: u64,
461    /// Client-provided merkle payment timestamp (unix seconds).
462    pub merkle_payment_timestamp: u64,
463    /// The settlement rules this client pays under. See
464    /// [`CURRENT_SETTLEMENT_VERSION`].
465    pub settlement_version: u32,
466}
467
468impl MerkleCandidateQuoteRequestV2 {
469    /// Create a new merkle candidate quote request declaring this build's
470    /// settlement version.
471    #[must_use]
472    pub fn new(address: XorName, data_size: u64, merkle_payment_timestamp: u64) -> Self {
473        Self {
474            address,
475            data_type: DATA_TYPE_CHUNK,
476            data_size,
477            merkle_payment_timestamp,
478            settlement_version: CURRENT_SETTLEMENT_VERSION,
479        }
480    }
481}
482
483/// Response with a merkle candidate quote.
484#[derive(Debug, Clone, Serialize, Deserialize)]
485#[non_exhaustive]
486pub enum MerkleCandidateQuoteResponse {
487    /// Candidate quote generated successfully.
488    /// Contains the serialized `MerklePaymentCandidateNode`.
489    Success {
490        /// Serialized `MerklePaymentCandidateNode`.
491        candidate_node: Vec<u8>,
492        /// ADR-0004: the serialized signed storage commitment the candidate's
493        /// price was derived from, so the client can fully resolve the binding
494        /// BEFORE paying (resolve-before-pay). `None` for a baseline candidate.
495        /// Unlike the single-node path, this commitment is NOT forwarded in the
496        /// merkle PUT bundle — sixteen per-candidate sidecars exceeded the
497        /// storer's payment-proof size budget, so current clients omit them and
498        /// storers resolve merkle pins from gossip or a `GetCommitmentByPin`
499        /// fetch (`MerklePaymentProof.commitment_sidecars` stays for legacy
500        /// bundles). Same semantics as
501        /// [`ChunkQuoteResponse::Success::commitment`]; postcard-encoded, so
502        /// this is a hard-cutover field, not an interop guarantee.
503        #[serde(default)]
504        commitment: Option<Vec<u8>>,
505    },
506    /// Quote generation failed.
507    Error(ProtocolError),
508}
509
510// =============================================================================
511// Payment Proof Type Tags
512// =============================================================================
513
514/// Version byte prefix for payment proof serialization.
515/// Allows the verifier to detect proof type before deserialization.
516pub const PROOF_TAG_SINGLE_NODE: u8 = 0x01;
517/// Version byte prefix for merkle payment proofs.
518pub const PROOF_TAG_MERKLE: u8 = 0x02;
519
520// =============================================================================
521// Protocol Errors
522// =============================================================================
523
524/// Errors that can occur during protocol operations.
525#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
526#[non_exhaustive]
527pub enum ProtocolError {
528    /// Message serialization failed.
529    SerializationFailed(String),
530    /// Message deserialization failed.
531    DeserializationFailed(String),
532    /// Wire message exceeds the maximum allowed size.
533    MessageTooLarge {
534        /// Actual size of the message in bytes.
535        size: usize,
536        /// Maximum allowed size.
537        max_size: usize,
538    },
539    /// Chunk exceeds maximum size.
540    ChunkTooLarge {
541        /// Size of the chunk in bytes.
542        size: usize,
543        /// Maximum allowed size.
544        max_size: usize,
545    },
546    /// Content address mismatch (hash(content) != address).
547    AddressMismatch {
548        /// Expected address.
549        expected: XorName,
550        /// Actual address computed from content.
551        actual: XorName,
552    },
553    /// Storage operation failed.
554    StorageFailed(String),
555    /// Payment verification failed.
556    PaymentFailed(String),
557    /// Quote generation failed.
558    QuoteFailed(String),
559    /// Internal error.
560    Internal(String),
561    /// The client settles payments under rules this node no longer accepts, so
562    /// no quote was issued.
563    ///
564    /// Refused at quote time on purpose. A client that pays under superseded
565    /// rules produces an on-chain payment every storer will reject, and that
566    /// payment cannot be refunded, so the only useful place to stop it is
567    /// before the client spends anything.
568    ///
569    /// Appended last so existing variants keep their wire discriminants. A
570    /// peer old enough not to know this variant cannot be sent it: it would
571    /// have had to send a V2 request to earn it, and only builds that carry
572    /// this variant do that.
573    ClientUpdateRequired {
574        /// Settlement version the client declared.
575        client_settlement_version: u32,
576        /// Oldest settlement version this node will quote for.
577        min_settlement_version: u32,
578    },
579    /// This node settles under older rules than the client, so it declined to
580    /// quote rather than promise to accept a payment it may not recognise.
581    ///
582    /// The mirror image of [`Self::ClientUpdateRequired`], and deliberately a
583    /// separate variant: it is not a verdict about the client, and a client
584    /// that receives it should quietly use a different storer rather than tell
585    /// its user anything. During a client-first rollout most of the fleet is
586    /// briefly in this state, so treating it as a client fault would strand
587    /// every up-to-date user.
588    StorerUpdateRequired {
589        /// Settlement version the client declared.
590        client_settlement_version: u32,
591        /// Newest settlement version this node understands.
592        node_settlement_version: u32,
593    },
594}
595
596impl std::fmt::Display for ProtocolError {
597    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
598        match self {
599            Self::SerializationFailed(msg) => write!(f, "serialization failed: {msg}"),
600            Self::DeserializationFailed(msg) => write!(f, "deserialization failed: {msg}"),
601            Self::MessageTooLarge { size, max_size } => {
602                write!(f, "message size {size} exceeds maximum {max_size}")
603            }
604            Self::ChunkTooLarge { size, max_size } => {
605                write!(f, "chunk size {size} exceeds maximum {max_size}")
606            }
607            Self::AddressMismatch { expected, actual } => {
608                write!(
609                    f,
610                    "address mismatch: expected {}, got {}",
611                    hex::encode(expected),
612                    hex::encode(actual)
613                )
614            }
615            Self::StorageFailed(msg) => write!(f, "storage failed: {msg}"),
616            Self::PaymentFailed(msg) => write!(f, "payment failed: {msg}"),
617            Self::QuoteFailed(msg) => write!(f, "quote failed: {msg}"),
618            Self::Internal(msg) => write!(f, "internal error: {msg}"),
619            Self::ClientUpdateRequired {
620                client_settlement_version,
621                min_settlement_version,
622            } => write!(
623                f,
624                "{}",
625                client_update_required_message(*client_settlement_version, *min_settlement_version)
626            ),
627            Self::StorerUpdateRequired {
628                client_settlement_version,
629                node_settlement_version,
630            } => write!(
631                f,
632                "this node settles under version {node_settlement_version} and cannot \
633                 promise to accept a version {client_settlement_version} payment, so it \
634                 issued no quote. Nothing was charged; use a different storer."
635            ),
636        }
637    }
638}
639
640/// The upgrade instruction shown to a user whose client cannot settle
641/// correctly.
642///
643/// A free function rather than only a `Display` arm so the storer can log the
644/// same wording it sends back, and so the client can reuse it when it
645/// translates the rejection into a CLI error. Kept deliberately plain: the
646/// reader is an end user staring at a failed upload, not an operator.
647#[must_use]
648pub fn client_update_required_message(
649    client_settlement_version: u32,
650    min_settlement_version: u32,
651) -> String {
652    format!(
653        "your client is too old to pay the current storage rate \
654         (it settles under version {client_settlement_version}, this node requires \
655         at least {min_settlement_version}), so no quote was issued and nothing was \
656         charged. Run `ant update` to upgrade, or reinstall from \
657         https://github.com/WithAutonomi/ant-client/releases/latest"
658    )
659}
660
661impl std::error::Error for ProtocolError {}
662
663#[cfg(test)]
664#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
665mod tests {
666    use super::*;
667
668    #[test]
669    fn test_put_request_encode_decode() {
670        let address = [0xAB; 32];
671        let content = Bytes::from_static(&[1, 2, 3, 4, 5]);
672        let request = ChunkPutRequest::new(address, content.clone());
673        let msg = ChunkMessage {
674            request_id: 42,
675            body: ChunkMessageBody::PutRequest(request),
676        };
677
678        let encoded = msg.encode().expect("encode should succeed");
679        let decoded = ChunkMessage::decode(&encoded).expect("decode should succeed");
680
681        assert_eq!(decoded.request_id, 42);
682        if let ChunkMessageBody::PutRequest(req) = decoded.body {
683            assert_eq!(req.address, address);
684            assert_eq!(req.content, content);
685            assert!(req.payment_proof.is_none());
686        } else {
687            panic!("expected PutRequest");
688        }
689    }
690
691    #[test]
692    fn test_put_request_with_payment() {
693        let address = [0xAB; 32];
694        let content = Bytes::from_static(&[1, 2, 3, 4, 5]);
695        let payment = vec![10, 20, 30];
696        let request = ChunkPutRequest::with_payment(address, content.clone(), payment.clone());
697
698        assert_eq!(request.address, address);
699        assert_eq!(request.content, content);
700        assert_eq!(request.payment_proof, Some(payment));
701    }
702
703    #[test]
704    fn test_get_request_encode_decode() {
705        let address = [0xCD; 32];
706        let request = ChunkGetRequest::new(address);
707        let msg = ChunkMessage {
708            request_id: 7,
709            body: ChunkMessageBody::GetRequest(request),
710        };
711
712        let encoded = msg.encode().expect("encode should succeed");
713        let decoded = ChunkMessage::decode(&encoded).expect("decode should succeed");
714
715        assert_eq!(decoded.request_id, 7);
716        if let ChunkMessageBody::GetRequest(req) = decoded.body {
717            assert_eq!(req.address, address);
718        } else {
719            panic!("expected GetRequest");
720        }
721    }
722
723    #[test]
724    fn test_put_response_success() {
725        let address = [0xEF; 32];
726        let response = ChunkPutResponse::Success { address };
727        let msg = ChunkMessage {
728            request_id: 99,
729            body: ChunkMessageBody::PutResponse(response),
730        };
731
732        let encoded = msg.encode().expect("encode should succeed");
733        let decoded = ChunkMessage::decode(&encoded).expect("decode should succeed");
734
735        assert_eq!(decoded.request_id, 99);
736        if let ChunkMessageBody::PutResponse(ChunkPutResponse::Success { address: addr }) =
737            decoded.body
738        {
739            assert_eq!(addr, address);
740        } else {
741            panic!("expected PutResponse::Success");
742        }
743    }
744
745    #[test]
746    fn test_get_response_not_found() {
747        let address = [0x12; 32];
748        let response = ChunkGetResponse::NotFound { address };
749        let msg = ChunkMessage {
750            request_id: 0,
751            body: ChunkMessageBody::GetResponse(response),
752        };
753
754        let encoded = msg.encode().expect("encode should succeed");
755        let decoded = ChunkMessage::decode(&encoded).expect("decode should succeed");
756
757        assert_eq!(decoded.request_id, 0);
758        if let ChunkMessageBody::GetResponse(ChunkGetResponse::NotFound { address: addr }) =
759            decoded.body
760        {
761            assert_eq!(addr, address);
762        } else {
763            panic!("expected GetResponse::NotFound");
764        }
765    }
766
767    #[test]
768    fn test_quote_request_encode_decode() {
769        let address = [0x34; 32];
770        let request = ChunkQuoteRequest::new(address, 1024);
771        let msg = ChunkMessage {
772            request_id: 1,
773            body: ChunkMessageBody::QuoteRequest(request),
774        };
775
776        let encoded = msg.encode().expect("encode should succeed");
777        let decoded = ChunkMessage::decode(&encoded).expect("decode should succeed");
778
779        assert_eq!(decoded.request_id, 1);
780        if let ChunkMessageBody::QuoteRequest(req) = decoded.body {
781            assert_eq!(req.address, address);
782            assert_eq!(req.data_size, 1024);
783            assert_eq!(req.data_type, DATA_TYPE_CHUNK);
784        } else {
785            panic!("expected QuoteRequest");
786        }
787    }
788
789    #[test]
790    fn test_protocol_error_display() {
791        let err = ProtocolError::ChunkTooLarge {
792            size: 5_000_000,
793            max_size: MAX_CHUNK_SIZE,
794        };
795        assert!(err.to_string().contains("5000000"));
796        assert!(err.to_string().contains(&MAX_CHUNK_SIZE.to_string()));
797
798        let err = ProtocolError::AddressMismatch {
799            expected: [0xAA; 32],
800            actual: [0xBB; 32],
801        };
802        let display = err.to_string();
803        assert!(display.contains("address mismatch"));
804    }
805
806    #[test]
807    fn test_decode_rejects_oversized_payload() {
808        let oversized = vec![0u8; MAX_WIRE_MESSAGE_SIZE + 1];
809        let result = ChunkMessage::decode(&oversized);
810        assert!(result.is_err());
811        let err = result.unwrap_err();
812        assert!(
813            matches!(err, ProtocolError::MessageTooLarge { .. }),
814            "expected MessageTooLarge, got {err:?}"
815        );
816    }
817
818    #[test]
819    fn test_invalid_decode() {
820        let invalid_data = vec![0xFF, 0xFF, 0xFF];
821        let result = ChunkMessage::decode(&invalid_data);
822        assert!(result.is_err());
823    }
824
825    #[test]
826    fn test_constants() {
827        assert_eq!(CHUNK_PROTOCOL_ID, "autonomi.ant.chunk.v1");
828        assert_eq!(PROTOCOL_VERSION, 1);
829        assert_eq!(MAX_CHUNK_SIZE, 4 * 1024 * 1024);
830        assert_eq!(DATA_TYPE_CHUNK, 0);
831    }
832
833    #[test]
834    fn test_proof_tag_constants() {
835        // Tags must be distinct non-zero bytes
836        assert_ne!(PROOF_TAG_SINGLE_NODE, PROOF_TAG_MERKLE);
837        assert_ne!(PROOF_TAG_SINGLE_NODE, 0x00);
838        assert_ne!(PROOF_TAG_MERKLE, 0x00);
839        assert_eq!(PROOF_TAG_SINGLE_NODE, 0x01);
840        assert_eq!(PROOF_TAG_MERKLE, 0x02);
841    }
842
843    #[test]
844    fn test_merkle_candidate_quote_request_encode_decode() {
845        let address = [0x56; 32];
846        let request = MerkleCandidateQuoteRequest {
847            address,
848            data_type: DATA_TYPE_CHUNK,
849            data_size: 2048,
850            merkle_payment_timestamp: 1_700_000_000,
851        };
852        let msg = ChunkMessage {
853            request_id: 500,
854            body: ChunkMessageBody::MerkleCandidateQuoteRequest(request),
855        };
856
857        let encoded = msg.encode().expect("encode should succeed");
858        let decoded = ChunkMessage::decode(&encoded).expect("decode should succeed");
859
860        assert_eq!(decoded.request_id, 500);
861        if let ChunkMessageBody::MerkleCandidateQuoteRequest(req) = decoded.body {
862            assert_eq!(req.address, address);
863            assert_eq!(req.data_type, DATA_TYPE_CHUNK);
864            assert_eq!(req.data_size, 2048);
865            assert_eq!(req.merkle_payment_timestamp, 1_700_000_000);
866        } else {
867            panic!("expected MerkleCandidateQuoteRequest");
868        }
869    }
870
871    #[test]
872    fn test_merkle_candidate_quote_response_success_encode_decode() {
873        let candidate_node_bytes = vec![0xAA, 0xBB, 0xCC, 0xDD];
874        let response = MerkleCandidateQuoteResponse::Success {
875            candidate_node: candidate_node_bytes.clone(),
876            commitment: Some(vec![0x11, 0x22]),
877        };
878        let msg = ChunkMessage {
879            request_id: 501,
880            body: ChunkMessageBody::MerkleCandidateQuoteResponse(response),
881        };
882
883        let encoded = msg.encode().expect("encode should succeed");
884        let decoded = ChunkMessage::decode(&encoded).expect("decode should succeed");
885
886        assert_eq!(decoded.request_id, 501);
887        if let ChunkMessageBody::MerkleCandidateQuoteResponse(
888            MerkleCandidateQuoteResponse::Success {
889                candidate_node,
890                commitment,
891            },
892        ) = decoded.body
893        {
894            assert_eq!(candidate_node, candidate_node_bytes);
895            assert_eq!(commitment, Some(vec![0x11, 0x22]));
896        } else {
897            panic!("expected MerkleCandidateQuoteResponse::Success");
898        }
899    }
900
901    #[test]
902    fn test_merkle_candidate_quote_response_error_encode_decode() {
903        let error = ProtocolError::QuoteFailed("no libp2p keypair".to_string());
904        let response = MerkleCandidateQuoteResponse::Error(error.clone());
905        let msg = ChunkMessage {
906            request_id: 502,
907            body: ChunkMessageBody::MerkleCandidateQuoteResponse(response),
908        };
909
910        let encoded = msg.encode().expect("encode should succeed");
911        let decoded = ChunkMessage::decode(&encoded).expect("decode should succeed");
912
913        assert_eq!(decoded.request_id, 502);
914        if let ChunkMessageBody::MerkleCandidateQuoteResponse(
915            MerkleCandidateQuoteResponse::Error(err),
916        ) = decoded.body
917        {
918            assert_eq!(err, error);
919        } else {
920            panic!("expected MerkleCandidateQuoteResponse::Error");
921        }
922    }
923
924    // =========================================================================
925    // Settlement version
926    // =========================================================================
927
928    /// Discriminant of a message body, read straight off the wire.
929    ///
930    /// `request_id: 0` encodes as a single varint byte, so the body's variant
931    /// index is byte 1. Reading it directly is the point: it is what a peer
932    /// built against an older `ant-protocol` sees.
933    fn wire_discriminant(body: ChunkMessageBody) -> u8 {
934        let encoded = ChunkMessage {
935            request_id: 0,
936            body,
937        }
938        .encode()
939        .expect("encode should succeed");
940        *encoded.get(1).expect("body discriminant should be present")
941    }
942
943    /// The load-bearing test for this whole design.
944    ///
945    /// The settlement version ships as appended variants precisely so existing
946    /// peers keep decoding. That only holds while every prior variant keeps
947    /// its wire index, which postcard assigns by declaration order. Insert a
948    /// variant anywhere but the end and every older peer silently misreads
949    /// every message from this one. Pinning the indices turns that from a
950    /// production incident into a failing test.
951    #[test]
952    fn appending_v2_variants_leaves_existing_discriminants_untouched() {
953        let address = [0x11; 32];
954
955        assert_eq!(
956            wire_discriminant(ChunkMessageBody::PutRequest(ChunkPutRequest::new(
957                address,
958                Bytes::from_static(&[1]),
959            ))),
960            0,
961        );
962        assert_eq!(
963            wire_discriminant(ChunkMessageBody::GetRequest(ChunkGetRequest { address })),
964            2,
965        );
966        assert_eq!(
967            wire_discriminant(ChunkMessageBody::QuoteRequest(ChunkQuoteRequest::new(
968                address, 1024,
969            ))),
970            4,
971        );
972        assert_eq!(
973            wire_discriminant(ChunkMessageBody::MerkleCandidateQuoteRequest(
974                MerkleCandidateQuoteRequest {
975                    address,
976                    data_type: DATA_TYPE_CHUNK,
977                    data_size: 1024,
978                    merkle_payment_timestamp: 1_785_855_600,
979                }
980            )),
981            6,
982        );
983
984        // Responses carry the same obligation as requests: a peer decoding a
985        // reply reads the same discriminant space, so pinning only the request
986        // half would let a response variant be reordered without any test
987        // noticing.
988        assert_eq!(
989            wire_discriminant(ChunkMessageBody::PutResponse(ChunkPutResponse::Success {
990                address
991            })),
992            1,
993        );
994        assert_eq!(
995            wire_discriminant(ChunkMessageBody::GetResponse(ChunkGetResponse::NotFound {
996                address
997            })),
998            3,
999        );
1000        assert_eq!(
1001            wire_discriminant(ChunkMessageBody::QuoteResponse(ChunkQuoteResponse::Error(
1002                ProtocolError::Internal(String::new())
1003            ))),
1004            5,
1005        );
1006        assert_eq!(
1007            wire_discriminant(ChunkMessageBody::MerkleCandidateQuoteResponse(
1008                MerkleCandidateQuoteResponse::Error(ProtocolError::Internal(String::new()))
1009            )),
1010            7,
1011        );
1012
1013        // The new variants take the next free indices, above everything an
1014        // older peer knows, so it rejects them as unknown rather than
1015        // misreading a variant it does know.
1016        assert_eq!(
1017            wire_discriminant(ChunkMessageBody::QuoteRequestV2(ChunkQuoteRequestV2::new(
1018                address, 1024,
1019            ))),
1020            8,
1021        );
1022        assert_eq!(
1023            wire_discriminant(ChunkMessageBody::MerkleCandidateQuoteRequestV2(
1024                MerkleCandidateQuoteRequestV2::new(address, 1024, 1_785_855_600)
1025            )),
1026            9,
1027        );
1028    }
1029
1030    /// `ProtocolError` rides inside quote responses, so its variants carry the
1031    /// same append-only obligation as the message bodies.
1032    #[test]
1033    fn client_update_required_is_appended_to_protocol_error() {
1034        let encode = |e: &ProtocolError| postcard::to_stdvec(e).expect("encode should succeed");
1035
1036        assert_eq!(
1037            encode(&ProtocolError::SerializationFailed(String::new()))
1038                .first()
1039                .copied(),
1040            Some(0),
1041        );
1042        assert_eq!(
1043            encode(&ProtocolError::DeserializationFailed(String::new()))
1044                .first()
1045                .copied(),
1046            Some(1),
1047        );
1048        assert_eq!(
1049            encode(&ProtocolError::MessageTooLarge {
1050                size: 0,
1051                max_size: 0
1052            })
1053            .first()
1054            .copied(),
1055            Some(2),
1056        );
1057        assert_eq!(
1058            encode(&ProtocolError::ChunkTooLarge {
1059                size: 0,
1060                max_size: 0
1061            })
1062            .first()
1063            .copied(),
1064            Some(3),
1065        );
1066        assert_eq!(
1067            encode(&ProtocolError::AddressMismatch {
1068                expected: [0u8; 32],
1069                actual: [0u8; 32]
1070            })
1071            .first()
1072            .copied(),
1073            Some(4),
1074        );
1075        assert_eq!(
1076            encode(&ProtocolError::StorageFailed(String::new()))
1077                .first()
1078                .copied(),
1079            Some(5),
1080        );
1081        assert_eq!(
1082            encode(&ProtocolError::PaymentFailed(String::new()))
1083                .first()
1084                .copied(),
1085            Some(6),
1086        );
1087        assert_eq!(
1088            encode(&ProtocolError::QuoteFailed(String::new()))
1089                .first()
1090                .copied(),
1091            Some(7),
1092        );
1093        assert_eq!(
1094            encode(&ProtocolError::Internal(String::new()))
1095                .first()
1096                .copied(),
1097            Some(8),
1098        );
1099        assert_eq!(
1100            encode(&ProtocolError::ClientUpdateRequired {
1101                client_settlement_version: 0,
1102                min_settlement_version: 1,
1103            })
1104            .first()
1105            .copied(),
1106            Some(9),
1107        );
1108        assert_eq!(
1109            encode(&ProtocolError::StorerUpdateRequired {
1110                client_settlement_version: 2,
1111                node_settlement_version: 1,
1112            })
1113            .first()
1114            .copied(),
1115            Some(10),
1116        );
1117    }
1118
1119    #[test]
1120    fn v2_quote_request_round_trips_with_the_settlement_version() {
1121        let address = [0x22; 32];
1122        let msg = ChunkMessage {
1123            request_id: 600,
1124            body: ChunkMessageBody::QuoteRequestV2(ChunkQuoteRequestV2::new(address, 4096)),
1125        };
1126
1127        let encoded = msg.encode().expect("encode should succeed");
1128        let decoded = ChunkMessage::decode(&encoded).expect("decode should succeed");
1129
1130        assert_eq!(decoded.request_id, 600);
1131        if let ChunkMessageBody::QuoteRequestV2(req) = decoded.body {
1132            assert_eq!(req.address, address);
1133            assert_eq!(req.data_size, 4096);
1134            assert_eq!(req.data_type, DATA_TYPE_CHUNK);
1135            assert_eq!(req.settlement_version, CURRENT_SETTLEMENT_VERSION);
1136        } else {
1137            panic!("expected QuoteRequestV2");
1138        }
1139    }
1140
1141    #[test]
1142    fn v2_merkle_candidate_request_round_trips_with_the_settlement_version() {
1143        let address = [0x33; 32];
1144        let msg = ChunkMessage {
1145            request_id: 601,
1146            body: ChunkMessageBody::MerkleCandidateQuoteRequestV2(
1147                MerkleCandidateQuoteRequestV2::new(address, 4096, 1_785_855_600),
1148            ),
1149        };
1150
1151        let encoded = msg.encode().expect("encode should succeed");
1152        let decoded = ChunkMessage::decode(&encoded).expect("decode should succeed");
1153
1154        assert_eq!(decoded.request_id, 601);
1155        if let ChunkMessageBody::MerkleCandidateQuoteRequestV2(req) = decoded.body {
1156            assert_eq!(req.address, address);
1157            assert_eq!(req.data_size, 4096);
1158            assert_eq!(req.merkle_payment_timestamp, 1_785_855_600);
1159            assert_eq!(req.settlement_version, CURRENT_SETTLEMENT_VERSION);
1160        } else {
1161            panic!("expected MerkleCandidateQuoteRequestV2");
1162        }
1163    }
1164
1165    #[test]
1166    fn settlement_compatibility_is_bounded_at_both_ends() {
1167        assert_eq!(
1168            settlement_compatibility(CURRENT_SETTLEMENT_VERSION),
1169            SettlementCompatibility::Compatible,
1170        );
1171        // The lower bound is inclusive: the oldest version still served is
1172        // served, not refused.
1173        assert_eq!(
1174            settlement_compatibility(MIN_SUPPORTED_SETTLEMENT_VERSION),
1175            SettlementCompatibility::Compatible,
1176        );
1177        assert_eq!(
1178            settlement_compatibility(MIN_SUPPORTED_SETTLEMENT_VERSION.saturating_sub(1)),
1179            SettlementCompatibility::ClientTooOld,
1180        );
1181    }
1182
1183    /// The correction this replaced an earlier revision for. Serving a client
1184    /// whose settlement rules this build does not know means promising to
1185    /// accept a payment it may reject, and by the time it rejects, the client
1186    /// has settled on-chain and cannot be refunded. Only monotonic increases
1187    /// are safe to wave through, and nothing here can tell whether the next
1188    /// change is one.
1189    #[test]
1190    fn a_newer_settlement_version_is_refused_rather_than_assumed_compatible() {
1191        assert_eq!(
1192            settlement_compatibility(CURRENT_SETTLEMENT_VERSION.saturating_add(1)),
1193            SettlementCompatibility::NodeTooOld,
1194        );
1195    }
1196
1197    /// The two refusals must stay distinct on the wire. One tells a user to
1198    /// upgrade; the other tells a client to pick a different peer and say
1199    /// nothing. Rendering them alike would strand every up-to-date user during
1200    /// a client-first rollout, when most of the fleet is briefly the old side.
1201    #[test]
1202    fn the_two_refusals_do_not_blame_the_same_party() {
1203        let client_at_fault = ProtocolError::ClientUpdateRequired {
1204            client_settlement_version: 0,
1205            min_settlement_version: 1,
1206        }
1207        .to_string();
1208        let node_at_fault = ProtocolError::StorerUpdateRequired {
1209            client_settlement_version: 2,
1210            node_settlement_version: 1,
1211        }
1212        .to_string();
1213
1214        assert!(
1215            client_at_fault.contains("your client is too old"),
1216            "{client_at_fault}"
1217        );
1218        assert!(
1219            !node_at_fault.contains("your client is too old"),
1220            "{node_at_fault}"
1221        );
1222        assert!(
1223            node_at_fault.contains("use a different storer"),
1224            "{node_at_fault}"
1225        );
1226        // Neither costs the user anything, and both must say so.
1227        assert!(
1228            client_at_fault.contains("nothing was charged"),
1229            "{client_at_fault}"
1230        );
1231        assert!(
1232            node_at_fault.contains("Nothing was charged"),
1233            "{node_at_fault}"
1234        );
1235    }
1236
1237    /// The rejection exists to get a user unstuck, so the wording is part of
1238    /// the contract, not decoration.
1239    #[test]
1240    fn update_required_message_tells_the_user_how_to_fix_it() {
1241        let rendered = ProtocolError::ClientUpdateRequired {
1242            client_settlement_version: 0,
1243            min_settlement_version: 1,
1244        }
1245        .to_string();
1246
1247        assert!(rendered.contains("ant update"), "{rendered}");
1248        assert!(rendered.contains("too old"), "{rendered}");
1249        // The whole point is that refusing to quote costs the user nothing.
1250        assert!(rendered.contains("nothing was charged"), "{rendered}");
1251    }
1252}