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}