Skip to main content

ant_protocol/
pointer.rs

1//! Pointers — paid mutable references with an immutable owner.
2//!
3//! A pointer is a mutable, owner-signed reference stored at an address derived
4//! from its owner's public key. Unlike a
5//! [`DataChunk`](crate::data_types::DataChunk) its address is **not** its
6//! content hash: the address is fixed for the pointer's life while the bytes
7//! under it change with every update.
8//!
9//! Ownership is fixed at creation. There is no transfer, no lineage, no
10//! certificates and no key rotation: a former owner keeps its key forever, so
11//! transferable ownership cannot be made fork-proof by a local rule. An earlier
12//! draft of this module implemented transfer through a genesis object and a
13//! chain of transfer certificates; it was reviewed and withdrawn. See
14//! `ADR-0016` in `ant-node`.
15//!
16//! That choice is what lets the design be this small: the owner key is inlined
17//! in the record, so validating a pointer needs nothing but the pointer — no
18//! fetch, no cache, no second close group, nothing that can be missing.
19//!
20//! # Encoding
21//!
22//! Encoded by hand: canonical, fixed-width, big-endian, no length prefixes and
23//! no `serde`. The signed payload must not depend on a serializer whose output
24//! can change — 1.0's pointer called `rmp_serde` inside its signing routine and
25//! silently signed nothing when it failed.
26//!
27//! ```text
28//! format_version  1     byte
29//! owner        1952     bytes   ML-DSA-65 public key
30//! counter         8     bytes   big-endian u64
31//! target         33     bytes   1-byte kind tag + 32-byte address, opaque here
32//! signature    3309     bytes   ML-DSA-65 over the preceding 1994 bytes
33//!             -----
34//!              5303     bytes
35//! ```
36//!
37//! # Identity
38//!
39//! ```text
40//! A        = BLAKE3::derive_key("autonomi.pointer.address.v1", owner)
41//! state_id = BLAKE3::derive_key("autonomi.pointer.state.v1",   body)
42//! ```
43//!
44//! Derive-key, not a hash of a prefix and the input. A chunk is addressed by a
45//! plain hash of its content, so a prefix would put both of these inside the
46//! chunk address space for anyone who could write the preimage down: squatting
47//! an owner's address before they used it, or buying a pointer and a chunk with
48//! one payment. Both modes still produce 32 bytes, so the ranges overlap; what
49//! changed is that crossing them is the preimage problem BLAKE3 is assumed to
50//! resist.
51//!
52//! `A` routes and decides which nodes are responsible. `state_id` names the
53//! authenticated state: it is what sync hints carry, what a fetch decision
54//! compares, and **what a quote is paid against**. They are separate because
55//! `A` must be stable for the pointer's life while the paid identifier must
56//! change with every update, or updates after the first would be free — the
57//! defect this design exists to fix.
58//!
59//! # What this module does not do
60//!
61//! Payment verification, storage and replication. Those are the node's job.
62
63use saorsa_pqc::api::sig::{
64    ml_dsa_65, MlDsaPublicKey, MlDsaSecretKey, MlDsaSignature, MlDsaVariant,
65};
66
67use crate::chunk::XORNAME_LEN;
68use crate::data_types::XorName;
69
70// =============================================================================
71// Constants
72// =============================================================================
73
74/// Data type identifier for pointers, alongside
75/// [`DATA_TYPE_CHUNK`](crate::chunk::DATA_TYPE_CHUNK).
76pub const DATA_TYPE_POINTER: u32 = 1;
77
78/// Wire format discriminator carried by every pointer.
79///
80/// Covered by the signature, so it cannot be downgraded, and part of the signed
81/// body, so it reaches `state_id`: a record differing only in this byte is a
82/// different state and is paid for separately.
83pub const POINTER_FORMAT_VERSION: u8 = 1;
84
85/// Length of a raw ML-DSA-65 public key.
86pub const ML_DSA_65_PUBLIC_KEY_LEN: usize = 1952;
87
88/// Length of a raw ML-DSA-65 signature.
89pub const ML_DSA_65_SIGNATURE_LEN: usize = 3309;
90
91/// Length of an encoded [`PointerTarget`]: a kind tag plus an address.
92pub const TARGET_WIRE_LEN: usize = 1 + XORNAME_LEN;
93
94/// Byte offset of the owner key within an encoded pointer.
95const OWNER_OFFSET: usize = 1;
96
97/// Byte offset of the counter within an encoded pointer.
98const COUNTER_OFFSET: usize = OWNER_OFFSET + ML_DSA_65_PUBLIC_KEY_LEN;
99
100/// Byte offset of the target within an encoded pointer.
101const TARGET_OFFSET: usize = COUNTER_OFFSET + 8;
102
103/// Length of the signed body: everything before the signature.
104pub const POINTER_BODY_LEN: usize = TARGET_OFFSET + TARGET_WIRE_LEN;
105
106/// Total size of an encoded pointer.
107pub const POINTER_WIRE_LEN: usize = POINTER_BODY_LEN + ML_DSA_65_SIGNATURE_LEN;
108
109/// Key-derivation context for the pointer address.
110///
111/// Used with BLAKE3's `derive_key`, not as a hash prefix. A prefix separates
112/// nothing: a chunk's address is `BLAKE3(content)`, so a chunk whose content is
113/// the prefix followed by an owner key would land on exactly that owner's
114/// pointer address, and an attacker could squat any address it could name.
115/// `derive_key` runs BLAKE3 in a different mode. The two still share a 32-byte
116/// range; what changes is that hitting a value from one mode with the other is
117/// the preimage problem BLAKE3 is assumed to resist, rather than a string
118/// anyone can write down.
119const CONTEXT_ADDRESS: &str = "autonomi.pointer.address.v1";
120
121/// Key-derivation context for the authenticated-state identifier.
122///
123/// Also `derive_key`, and for a sharper reason: `state_id` is what a pointer's
124/// payment is quoted against. Were it a plain hash, one settled quote would pay
125/// for both the pointer and a chunk sitting at the same address.
126const CONTEXT_STATE: &str = "autonomi.pointer.state.v1";
127
128/// ML-DSA signing context for a pointer.
129///
130/// Domain-separates a pointer signature from every other signature in the
131/// system, so a signature lifted from elsewhere cannot authenticate a pointer
132/// and vice versa.
133const SIGNING_CONTEXT: &[u8] = b"autonomi.pointer.head.v1";
134
135// =============================================================================
136// Errors
137// =============================================================================
138
139/// Why a pointer was refused.
140///
141/// `Display` is written by hand rather than derived, because this crate is a
142/// wire contract and does not take a macro dependency for it — the same reason
143/// [`crate::error::Error`] is hand-written.
144#[derive(Debug, Clone, PartialEq, Eq)]
145pub enum PointerError {
146    /// The encoding is not exactly [`POINTER_WIRE_LEN`] bytes.
147    WrongLength(usize),
148    /// The format version is not one this build understands.
149    UnknownFormatVersion(u8),
150    /// The owner key does not decode.
151    InvalidOwnerKey(String),
152    /// The signature does not decode.
153    InvalidSignatureFormat(String),
154    /// The signature does not verify under the owner key in the record.
155    SignatureInvalid,
156    /// Signing failed.
157    SigningFailed(String),
158    /// The counter is at its maximum and cannot be advanced.
159    CounterExhausted,
160}
161
162impl std::fmt::Display for PointerError {
163    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
164        match self {
165            Self::WrongLength(len) => {
166                write!(f, "pointer must be {POINTER_WIRE_LEN} bytes, got {len}")
167            }
168            Self::UnknownFormatVersion(version) => {
169                write!(f, "unknown pointer format version {version}")
170            }
171            Self::InvalidOwnerKey(reason) => write!(f, "invalid pointer owner key: {reason}"),
172            Self::InvalidSignatureFormat(reason) => {
173                write!(f, "invalid pointer signature format: {reason}")
174            }
175            Self::SignatureInvalid => {
176                write!(f, "pointer signature does not verify under its owner key")
177            }
178            Self::SigningFailed(reason) => write!(f, "pointer signing failed: {reason}"),
179            Self::CounterExhausted => write!(
180                f,
181                "pointer counter is at u64::MAX and is terminal; migrate to a fresh \
182                 pointer with an earlier update"
183            ),
184        }
185    }
186}
187
188impl std::error::Error for PointerError {}
189
190// =============================================================================
191// Address derivation
192// =============================================================================
193
194/// Derive a pointer address from an owner key.
195///
196/// `BLAKE3::derive_key` rather than a hash of a prefix and the key. Chunk
197/// addresses are `BLAKE3(content)` over the same 32-byte range, and derive-key
198/// is a different mode of the same function: the ranges still overlap, but
199/// putting content on one of these values is a preimage problem rather than a
200/// string anyone can write down. The node still refuses an address the other
201/// kind occupies; that guard now covers the cryptographic case only.
202#[must_use]
203pub fn pointer_address(owner: &MlDsaPublicKey) -> XorName {
204    blake3::derive_key(CONTEXT_ADDRESS, &owner.to_bytes())
205}
206
207/// Derive the authenticated-state identifier from a signed body.
208///
209/// Derive-key for the same reason as the address, and one more: this is the
210/// identifier a pointer's storage is paid against, so a chunk that could occupy
211/// it would be a second record bought with one payment.
212#[must_use]
213pub fn state_id_for_body(body: &[u8]) -> XorName {
214    blake3::derive_key(CONTEXT_STATE, body)
215}
216
217// =============================================================================
218// Target
219// =============================================================================
220
221/// What a pointer points at, as clients interpret it.
222///
223/// A node never reads this. It stores the tag as an opaque byte, so a new kind
224/// is a client-side change and not a network upgrade.
225#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
226pub enum PointerTargetKind {
227    /// An immutable content-addressed chunk.
228    Chunk,
229    /// Another pointer, which is how handover-by-indirection is expressed.
230    Pointer,
231}
232
233impl PointerTargetKind {
234    /// Wire tag for this kind.
235    #[must_use]
236    pub const fn tag(self) -> u8 {
237        match self {
238            Self::Chunk => 0,
239            Self::Pointer => 1,
240        }
241    }
242
243    /// Interpret a wire tag, if it names a kind this build knows.
244    ///
245    /// Returns `None` for anything else rather than an error: an unrecognised
246    /// tag is a target this client cannot follow, not a malformed record.
247    #[must_use]
248    pub const fn from_tag(tag: u8) -> Option<Self> {
249        match tag {
250            0 => Some(Self::Chunk),
251            1 => Some(Self::Pointer),
252            _ => None,
253        }
254    }
255}
256
257/// A kind tag plus the address it refers to.
258///
259/// The tag is carried and signed but never interpreted by a storing node.
260#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
261pub struct PointerTarget {
262    /// The raw kind tag, meaningful only to clients.
263    kind_tag: u8,
264    /// The address pointed at.
265    pub address: XorName,
266}
267
268impl PointerTarget {
269    /// Build a target of a known kind.
270    #[must_use]
271    pub const fn new(kind: PointerTargetKind, address: XorName) -> Self {
272        Self {
273            kind_tag: kind.tag(),
274            address,
275        }
276    }
277
278    /// Build a target from a raw tag, including one this build does not know.
279    #[must_use]
280    pub const fn from_raw_tag(kind_tag: u8, address: XorName) -> Self {
281        Self { kind_tag, address }
282    }
283
284    /// The raw kind tag as carried on the wire.
285    #[must_use]
286    pub const fn kind_tag(&self) -> u8 {
287        self.kind_tag
288    }
289
290    /// The kind, if this build recognises the tag.
291    #[must_use]
292    pub const fn kind(&self) -> Option<PointerTargetKind> {
293        PointerTargetKind::from_tag(self.kind_tag)
294    }
295
296    /// Encode to its [`TARGET_WIRE_LEN`] wire bytes.
297    #[must_use]
298    pub fn to_bytes(&self) -> [u8; TARGET_WIRE_LEN] {
299        let mut out = [0u8; TARGET_WIRE_LEN];
300        if let Some((tag, address)) = out.split_first_mut() {
301            *tag = self.kind_tag;
302            address.copy_from_slice(&self.address);
303        }
304        out
305    }
306
307    /// Decode from wire bytes.
308    ///
309    /// Only the length can fail: every tag value is accepted, because a node
310    /// stores the target without interpreting it.
311    fn from_bytes(bytes: &[u8]) -> Option<Self> {
312        let (tag, rest) = bytes.split_first()?;
313        if rest.len() != XORNAME_LEN {
314            return None;
315        }
316        let mut address = [0u8; XORNAME_LEN];
317        address.copy_from_slice(rest);
318        Some(Self {
319            kind_tag: *tag,
320            address,
321        })
322    }
323}
324
325// =============================================================================
326// Merge order
327// =============================================================================
328
329/// The merge rule's comparison key: larger wins.
330///
331/// One type so that every comparison — an arriving record against a parsed one,
332/// or against a store's index entry — is literally the same ordering. Deriving
333/// `Ord` over the fields in declaration order is what encodes the rule:
334/// `counter` first, then the target, inverted so that *smaller* target bytes
335/// rank higher.
336#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
337struct MergeRank {
338    /// The update counter. Larger wins.
339    counter: u64,
340    /// The encoded target, inverted: smaller bytes win.
341    target: std::cmp::Reverse<[u8; TARGET_WIRE_LEN]>,
342}
343
344// =============================================================================
345// State
346// =============================================================================
347
348/// The parts of a record that determine its authenticated state.
349///
350/// Parsed from the body alone, so obtaining one costs no signature
351/// verification. That is what lets a node recognise a re-submission of what it
352/// already holds and stop before doing expensive work.
353#[derive(Debug, Clone, Copy, PartialEq, Eq)]
354pub struct PointerState {
355    /// The authenticated-state identifier, derived from the signed body.
356    pub state_id: XorName,
357    /// The address the record belongs at.
358    pub address: XorName,
359    /// The update counter, the merge rule's first key.
360    pub counter: u64,
361    /// The target, whose bytes are the merge rule's second key.
362    pub target: PointerTarget,
363}
364
365impl PointerState {
366    /// Parse the state out of a record's bytes without verifying its signature.
367    ///
368    /// Checks length, format version and structure only. A `PointerState` says
369    /// what a record *claims*; only [`Pointer::from_bytes`] establishes that
370    /// the claim is signed.
371    ///
372    /// # Errors
373    ///
374    /// Returns [`PointerError`] for a record of the wrong length, an unknown
375    /// format version, or an unparseable owner key.
376    pub fn parse(bytes: &[u8]) -> Result<Self, PointerError> {
377        Self::parse_with_owner(bytes).map(|(state, _)| state)
378    }
379
380    /// As [`Self::parse`], also returning the decoded owner key.
381    fn parse_with_owner(bytes: &[u8]) -> Result<(Self, MlDsaPublicKey), PointerError> {
382        if bytes.len() != POINTER_WIRE_LEN {
383            return Err(PointerError::WrongLength(bytes.len()));
384        }
385        let version = bytes
386            .first()
387            .copied()
388            .ok_or(PointerError::WrongLength(bytes.len()))?;
389        if version != POINTER_FORMAT_VERSION {
390            return Err(PointerError::UnknownFormatVersion(version));
391        }
392
393        let owner_bytes = bytes
394            .get(OWNER_OFFSET..COUNTER_OFFSET)
395            .ok_or(PointerError::WrongLength(bytes.len()))?;
396        let owner = MlDsaPublicKey::from_bytes(MlDsaVariant::MlDsa65, owner_bytes)
397            .map_err(|e| PointerError::InvalidOwnerKey(e.to_string()))?;
398
399        let counter_bytes: [u8; 8] = bytes
400            .get(COUNTER_OFFSET..TARGET_OFFSET)
401            .and_then(|slice| slice.try_into().ok())
402            .ok_or(PointerError::WrongLength(bytes.len()))?;
403
404        let target = bytes
405            .get(TARGET_OFFSET..POINTER_BODY_LEN)
406            .and_then(PointerTarget::from_bytes)
407            .ok_or(PointerError::WrongLength(bytes.len()))?;
408
409        let body = bytes
410            .get(..POINTER_BODY_LEN)
411            .ok_or(PointerError::WrongLength(bytes.len()))?;
412
413        Ok((
414            Self {
415                state_id: state_id_for_body(body),
416                address: pointer_address(&owner),
417                counter: u64::from_be_bytes(counter_bytes),
418                target,
419            },
420            owner,
421        ))
422    }
423
424    /// The comparison key for [`Self::replaces`].
425    #[must_use]
426    fn rank(&self) -> MergeRank {
427        MergeRank {
428            counter: self.counter,
429            target: std::cmp::Reverse(self.target.to_bytes()),
430        }
431    }
432
433    /// Whether `self` replaces `other` under the merge rule.
434    ///
435    /// ```text
436    /// 1. larger counter
437    /// 2. smaller target bytes
438    /// ```
439    ///
440    /// A total order on the states of **one** pointer address. Records of
441    /// different owners are not comparable and neither replaces the other.
442    ///
443    /// Equal state never replaces, whatever the signature bytes: ML-DSA signing
444    /// is randomized, so one authenticated state has unboundedly many valid
445    /// encodings, and ordering records by their bytes would let an owner sign
446    /// one paid state repeatedly, sort worst-first and have every submission
447    /// win — unbounded storage, replication and audit work for a single
448    /// payment.
449    ///
450    /// At `counter == u64::MAX` the order still holds, and that has a
451    /// consequence worth stating plainly: the pointer is **not frozen**. The
452    /// counter can no longer advance, but equal counters are still resolved by
453    /// target bytes, and *smaller* bytes win. So a terminal pointer can still
454    /// be moved — but only ever toward smaller target bytes, and never back.
455    /// See [`Pointer::next_counter`] for what that means for migration.
456    #[must_use]
457    pub fn replaces(&self, other: &Self) -> bool {
458        self.address == other.address && self.rank() > other.rank()
459    }
460}
461
462// =============================================================================
463// Parsed-but-unverified record
464// =============================================================================
465
466/// Bytes that have been structurally parsed but not yet verified.
467///
468/// The bytes travel with their parse, so a caller cannot pair one record's
469/// bytes with another's state. Only [`Pointer::verify_parsed`] consumes it, and
470/// only that step establishes that the bytes are signed.
471pub struct ParsedPointer {
472    /// The record's canonical encoding.
473    bytes: Vec<u8>,
474    /// What those bytes claim.
475    state: PointerState,
476    /// The owner key decoded from those bytes.
477    owner: MlDsaPublicKey,
478}
479
480impl std::fmt::Debug for ParsedPointer {
481    /// Shows what the unverified bytes claim, not the bytes themselves.
482    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
483        f.debug_struct("ParsedPointer")
484            .field("address", &hex::encode(self.state.address))
485            .field("counter", &self.state.counter)
486            .finish_non_exhaustive()
487    }
488}
489
490impl ParsedPointer {
491    /// Parse `bytes`, without verifying the signature.
492    ///
493    /// # Errors
494    ///
495    /// As [`PointerState::parse`].
496    pub fn parse(bytes: Vec<u8>) -> Result<Self, PointerError> {
497        let (state, owner) = PointerState::parse_with_owner(&bytes)?;
498        Ok(Self {
499            bytes,
500            state,
501            owner,
502        })
503    }
504
505    /// What the unverified bytes claim.
506    #[must_use]
507    pub const fn state(&self) -> &PointerState {
508        &self.state
509    }
510}
511
512// =============================================================================
513// The record
514// =============================================================================
515
516/// A paid, mutable, owner-signed reference.
517///
518/// A value of this type has been through [`Pointer::from_bytes`], so its
519/// signature covers exactly its body and its address derives from its owner.
520#[derive(Clone)]
521pub struct Pointer {
522    /// Wire format discriminator. Signed, so it cannot be downgraded.
523    version: u8,
524    /// The owner's public key — the pointer's identity.
525    ///
526    /// The address is derived from the owner key alone, so a pointer is public-key
527    /// addressed, and the key that verifies it travels with it: ML-DSA has no
528    /// key recovery and a 1,952-byte key cannot be a 32-byte address.
529    owner: MlDsaPublicKey,
530    /// The merge rule's first key: a larger counter wins.
531    ///
532    /// [`Self::create`] starts at 0 and [`Self::update`] adds one, but nothing
533    /// requires the next state to be exactly one on. The counter orders states;
534    /// it does not meter them. Every stored state is paid for once, whatever
535    /// its number, so a number that is skipped is never stored and owes nothing.
536    counter: u64,
537    /// What this points at: a chunk or another pointer. Opaque to a node.
538    target: PointerTarget,
539    /// The owner's signature over every field above.
540    sig: MlDsaSignature,
541}
542
543impl std::fmt::Debug for Pointer {
544    /// Shows what identifies the record, not its 5 KB of key and signature.
545    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
546        f.debug_struct("Pointer")
547            .field("address", &hex::encode(self.address()))
548            .field("counter", &self.counter)
549            .field("target_tag", &self.target.kind_tag())
550            .field("target", &hex::encode(self.target.address))
551            .field("state_id", &hex::encode(self.state_id()))
552            .finish_non_exhaustive()
553    }
554}
555
556impl Pointer {
557    /// Build and sign a pointer.
558    ///
559    /// # Errors
560    ///
561    /// Returns [`PointerError::SigningFailed`] if signing fails, or a parse
562    /// error if the resulting record does not parse, which would be a bug here
563    /// rather than bad input.
564    pub fn sign(
565        secret_key: &MlDsaSecretKey,
566        owner: &MlDsaPublicKey,
567        counter: u64,
568        target: PointerTarget,
569    ) -> Result<Self, PointerError> {
570        let body = encode_body(POINTER_FORMAT_VERSION, owner, counter, target)?;
571        let signature = ml_dsa_65()
572            .sign_with_context(secret_key, &body, SIGNING_CONTEXT)
573            .map_err(|e| PointerError::SigningFailed(e.to_string()))?;
574        let mut bytes = body;
575        bytes.extend_from_slice(&signature.to_bytes());
576        Self::from_bytes(&bytes)
577    }
578
579    /// Create a pointer at counter 0.
580    ///
581    /// # Errors
582    ///
583    /// As [`Self::sign`].
584    pub fn create(
585        secret_key: &MlDsaSecretKey,
586        owner: &MlDsaPublicKey,
587        target: PointerTarget,
588    ) -> Result<Self, PointerError> {
589        Self::sign(secret_key, owner, 0, target)
590    }
591
592    /// Sign the update that follows this one: `counter + 1`, new target.
593    ///
594    /// One past this record is the smallest counter that replaces it, and so
595    /// the natural next state. A larger one would replace it just as well.
596    ///
597    /// # Errors
598    ///
599    /// Returns [`PointerError::CounterExhausted`] at `u64::MAX`, otherwise as
600    /// [`Self::sign`].
601    pub fn update(
602        &self,
603        secret_key: &MlDsaSecretKey,
604        target: PointerTarget,
605    ) -> Result<Self, PointerError> {
606        Self::sign(secret_key, &self.owner, self.next_counter()?, target)
607    }
608
609    /// Parse and fully validate a record.
610    ///
611    /// Checks, cheapest first: length, format version, structure, then the
612    /// signature. A returned `Pointer` is structurally valid and correctly
613    /// signed by the key it carries; it says nothing about payment, which is a
614    /// separate external check.
615    ///
616    /// # Errors
617    ///
618    /// Returns [`PointerError`] for a malformed record or one whose signature
619    /// does not verify.
620    pub fn from_bytes(bytes: &[u8]) -> Result<Self, PointerError> {
621        Self::verify_parsed(ParsedPointer::parse(bytes.to_vec())?)
622    }
623
624    /// Verify a record that has already been parsed.
625    ///
626    /// Decoding an ML-DSA public key is not free, and the request path parses
627    /// before it decides whether verification is worth doing at all. This lets
628    /// it hand over what it already has instead of decoding a second time.
629    ///
630    /// # Errors
631    ///
632    /// As [`Self::from_bytes`].
633    pub fn verify_parsed(parsed: ParsedPointer) -> Result<Self, PointerError> {
634        let ParsedPointer {
635            bytes,
636            state,
637            owner,
638        } = parsed;
639
640        let body = bytes
641            .get(..POINTER_BODY_LEN)
642            .ok_or(PointerError::WrongLength(bytes.len()))?;
643        let signature_bytes = bytes
644            .get(POINTER_BODY_LEN..)
645            .ok_or(PointerError::WrongLength(bytes.len()))?;
646        let signature = MlDsaSignature::from_bytes(MlDsaVariant::MlDsa65, signature_bytes)
647            .map_err(|e| PointerError::InvalidSignatureFormat(e.to_string()))?;
648
649        let valid = ml_dsa_65()
650            .verify_with_context(&owner, body, &signature, SIGNING_CONTEXT)
651            .map_err(|e| PointerError::InvalidSignatureFormat(e.to_string()))?;
652        if !valid {
653            return Err(PointerError::SignatureInvalid);
654        }
655
656        Ok(Self {
657            version: POINTER_FORMAT_VERSION,
658            owner,
659            counter: state.counter,
660            target: state.target,
661            sig: signature,
662        })
663    }
664
665    /// The canonical encoding of this record.
666    ///
667    /// Re-encoded from the fields rather than cached. Every field is
668    /// fixed-width with no optionality, so there is exactly one encoding and
669    /// this is always byte-identical to what was signed.
670    #[must_use]
671    pub fn to_bytes(&self) -> Vec<u8> {
672        let mut out = self.body();
673        out.extend_from_slice(&self.sig.to_bytes());
674        out
675    }
676
677    /// The signed body: every field the signature covers.
678    fn body(&self) -> Vec<u8> {
679        encode_body(self.version, &self.owner, self.counter, self.target)
680            .unwrap_or_else(|_| Vec::with_capacity(POINTER_BODY_LEN))
681    }
682
683    /// The owner's public key. Fixed for the pointer's life.
684    #[must_use]
685    pub const fn owner(&self) -> &MlDsaPublicKey {
686        &self.owner
687    }
688
689    /// The signed state this record carries.
690    ///
691    /// Derived, not stored: the identifiers are hashes of fields already here.
692    #[must_use]
693    pub fn state(&self) -> PointerState {
694        PointerState {
695            state_id: self.state_id(),
696            address: self.address(),
697            counter: self.counter,
698            target: self.target,
699        }
700    }
701
702    /// The update counter.
703    #[must_use]
704    pub const fn counter(&self) -> u64 {
705        self.counter
706    }
707
708    /// What this pointer points at.
709    #[must_use]
710    pub const fn target(&self) -> PointerTarget {
711        self.target
712    }
713
714    /// The address this record belongs at, derived from the owner key.
715    #[must_use]
716    pub fn address(&self) -> XorName {
717        pointer_address(&self.owner)
718    }
719
720    /// The authenticated-state identifier, derived from the signed body.
721    ///
722    /// Names the state rather than the encoding, so two records carrying
723    /// different valid signatures over one state share a `state_id`. This is
724    /// what sync hints carry and what a quote is paid against.
725    #[must_use]
726    pub fn state_id(&self) -> XorName {
727        state_id_for_body(&self.body())
728    }
729
730    /// The successor counter for an update to this pointer.
731    ///
732    /// # Errors
733    ///
734    /// Returns [`PointerError::CounterExhausted`] at `u64::MAX`.
735    ///
736    /// A pointer that must outlive its counter has to be migrated by an
737    /// **earlier** update — one that retargets it at a fresh pointer while a
738    /// successor counter still exists. A migration written *at* `u64::MAX` is
739    /// not final: the counter is spent, so the only thing that still decides
740    /// the merge is the target, and any record with smaller target bytes
741    /// displaces it. A strictly larger counter is the one move nothing can
742    /// answer, so migration must spend a counter it still has.
743    pub fn next_counter(&self) -> Result<u64, PointerError> {
744        self.counter
745            .checked_add(1)
746            .ok_or(PointerError::CounterExhausted)
747    }
748
749    /// Whether this pointer's counter is spent.
750    ///
751    /// `true` once the counter reaches `u64::MAX`. Note this does not mean the
752    /// stored value can no longer change: a record at the same counter with
753    /// smaller target bytes still wins. It means the *counter* can no longer
754    /// answer such a record, which is why clients migrate before reaching this
755    /// rather than on it.
756    #[must_use]
757    pub const fn is_terminal(&self) -> bool {
758        self.counter == u64::MAX
759    }
760
761    /// Whether `self` replaces `other` under the merge rule.
762    ///
763    /// See [`PointerState::replaces`], which is where the rule lives.
764    #[must_use]
765    pub fn replaces(&self, other: &Self) -> bool {
766        self.state().replaces(&other.state())
767    }
768}
769
770/// Encode the signed body of a pointer.
771fn encode_body(
772    version: u8,
773    owner: &MlDsaPublicKey,
774    counter: u64,
775    target: PointerTarget,
776) -> Result<Vec<u8>, PointerError> {
777    let owner_bytes = owner.to_bytes();
778    if owner_bytes.len() != ML_DSA_65_PUBLIC_KEY_LEN {
779        return Err(PointerError::InvalidOwnerKey(format!(
780            "owner key must be {ML_DSA_65_PUBLIC_KEY_LEN} bytes, got {}",
781            owner_bytes.len()
782        )));
783    }
784    let mut body = Vec::with_capacity(POINTER_BODY_LEN);
785    // The record's own version, not this build's: a record is re-encoded as
786    // what was signed. Only `POINTER_FORMAT_VERSION` parses today, so the two
787    // coincide — but the encoder must not be the thing that assumes it.
788    body.push(version);
789    body.extend_from_slice(&owner_bytes);
790    body.extend_from_slice(&counter.to_be_bytes());
791    body.extend_from_slice(&target.to_bytes());
792    Ok(body)
793}
794
795#[cfg(test)]
796#[allow(
797    clippy::unwrap_used,
798    clippy::expect_used,
799    clippy::panic,
800    reason = "the crate guide allows these in tests"
801)]
802mod tests {
803    use super::*;
804
805    fn keypair(seed: u8) -> (MlDsaPublicKey, MlDsaSecretKey) {
806        ml_dsa_65().generate_keypair_from_seed(&[seed; 32])
807    }
808
809    fn signed(seed: u8, counter: u64, target_byte: u8) -> Pointer {
810        let (pk, sk) = keypair(seed);
811        let target = PointerTarget::new(PointerTargetKind::Chunk, [target_byte; XORNAME_LEN]);
812        Pointer::sign(&sk, &pk, counter, target).expect("sign")
813    }
814
815    #[test]
816    fn sizes_match_the_design() {
817        assert_eq!(POINTER_BODY_LEN, 1994);
818        assert_eq!(POINTER_WIRE_LEN, 5303);
819        assert_eq!(signed(1, 0, 0).to_bytes().len(), POINTER_WIRE_LEN);
820    }
821
822    #[test]
823    fn roundtrips_and_validates() {
824        let record = signed(1, 42, 7);
825        let parsed = Pointer::from_bytes(&record.to_bytes()).expect("reparse");
826        assert_eq!(parsed.counter(), 42);
827        assert_eq!(parsed.address(), record.address());
828        assert_eq!(parsed.state_id(), record.state_id());
829    }
830
831    #[test]
832    fn a_tampered_record_does_not_verify() {
833        let record = signed(1, 5, 5);
834        let mut bytes = record.to_bytes();
835        if let Some(byte) = bytes.get_mut(COUNTER_OFFSET + 7) {
836            *byte ^= 1;
837        }
838        assert!(matches!(
839            Pointer::from_bytes(&bytes),
840            Err(PointerError::SignatureInvalid)
841        ));
842    }
843
844    #[test]
845    fn an_unknown_format_version_is_refused() {
846        let record = signed(1, 1, 1);
847        let mut bytes = record.to_bytes();
848        if let Some(byte) = bytes.first_mut() {
849            *byte = 2;
850        }
851        assert!(matches!(
852            Pointer::from_bytes(&bytes),
853            Err(PointerError::UnknownFormatVersion(2))
854        ));
855    }
856
857    #[test]
858    fn the_format_version_reaches_the_paid_identifier() {
859        // A record differing only in its version byte is a different state and
860        // must be paid for separately. Without this a future version could
861        // reuse a receipt bought for version 1.
862        let record = signed(1, 3, 3);
863        let mut body = record
864            .to_bytes()
865            .get(..POINTER_BODY_LEN)
866            .expect("body")
867            .to_vec();
868        let baseline = state_id_for_body(&body);
869        assert_eq!(baseline, record.state_id());
870
871        for version in 0u8..=255 {
872            if version == POINTER_FORMAT_VERSION {
873                continue;
874            }
875            if let Some(byte) = body.first_mut() {
876                *byte = version;
877            }
878            assert_ne!(
879                state_id_for_body(&body),
880                baseline,
881                "version {version} must not share version 1's paid identifier"
882            );
883        }
884    }
885
886    #[test]
887    fn different_owners_are_not_comparable() {
888        let mine = signed(1, 1, 9);
889        let theirs = signed(2, 500, 0);
890        assert!(!mine.replaces(&theirs));
891        assert!(!theirs.replaces(&mine));
892    }
893
894    #[test]
895    fn the_counter_is_terminal_at_its_maximum() {
896        let ordinary = signed(1, 0, 0);
897        assert!(!ordinary.is_terminal());
898        assert_eq!(ordinary.next_counter().expect("next"), 1);
899
900        let terminal = signed(1, u64::MAX, 0);
901        assert!(terminal.is_terminal());
902        assert_eq!(terminal.next_counter(), Err(PointerError::CounterExhausted));
903        assert!(terminal.next_counter().is_err());
904    }
905
906    #[test]
907    fn nothing_out_ranks_a_terminal_counter() {
908        // The migration rule: at u64::MAX no later record can win, so a pointer
909        // that must outlive its counter has to be retargeted *before* the
910        // terminal update, not on it.
911        let terminal = signed(1, u64::MAX, 5);
912        for counter in [0u64, 1, 42, u64::MAX - 1] {
913            let earlier = signed(1, counter, 0);
914            assert!(
915                !earlier.replaces(&terminal),
916                "counter {counter} must not replace a terminal pointer"
917            );
918            assert!(terminal.replaces(&earlier));
919        }
920    }
921
922    #[test]
923    fn a_terminal_counter_still_resolves_equal_counter_conflicts() {
924        // The order does not degenerate at the maximum: two records at
925        // u64::MAX with different targets still resolve deterministically, so
926        // replicas cannot split there.
927        let low_target = signed(1, u64::MAX, 1);
928        let high_target = signed(1, u64::MAX, 2);
929        assert!(low_target.replaces(&high_target), "smaller target wins");
930        assert!(!high_target.replaces(&low_target));
931    }
932
933    #[test]
934    fn create_starts_at_zero_and_update_adds_one() {
935        let (pk, sk) = keypair(20);
936        let target = |b: u8| PointerTarget::new(PointerTargetKind::Chunk, [b; XORNAME_LEN]);
937
938        let created = Pointer::create(&sk, &pk, target(1)).expect("create");
939        assert_eq!(created.counter(), 0);
940
941        let first = created.update(&sk, target(2)).expect("update");
942        assert_eq!(first.counter(), 1);
943        assert!(first.replaces(&created));
944
945        let second = first.update(&sk, target(3)).expect("update");
946        assert_eq!(second.counter(), 2);
947        assert!(second.replaces(&first));
948    }
949
950    #[test]
951    fn any_larger_counter_replaces_so_counters_may_skip() {
952        // The counter orders states; it does not meter them. Each stored state
953        // is paid for once whatever its number, so a skipped number is never
954        // stored and owes nothing.
955        let held = signed(21, 5, 7);
956        for later in [6u64, 7, 99, u64::MAX] {
957            assert!(
958                signed(21, later, 7).replaces(&held),
959                "counter {later} must replace counter 5"
960            );
961        }
962        for earlier in [0u64, 4] {
963            assert!(
964                !signed(21, earlier, 7).replaces(&held),
965                "counter {earlier} must not replace counter 5"
966            );
967        }
968    }
969
970    #[test]
971    fn a_fork_at_one_counter_resolves_by_target_and_any_later_counter_heals_it() {
972        // Two records at one counter are a fork. Every node that sees both
973        // keeps the smaller target, and a record at any later counter replaces
974        // whichever side a node was left holding.
975        let smaller = signed(25, 5, 1);
976        let larger = signed(25, 5, 9);
977        assert!(smaller.replaces(&larger), "smaller target bytes win");
978        assert!(!larger.replaces(&smaller));
979
980        for later in [6u64, 50] {
981            let healed = signed(25, later, 9);
982            assert!(healed.replaces(&smaller));
983            assert!(healed.replaces(&larger));
984        }
985    }
986
987    #[test]
988    fn a_terminal_pointer_cannot_be_updated() {
989        let (pk, sk) = keypair(24);
990        let target = PointerTarget::new(PointerTargetKind::Chunk, [1; XORNAME_LEN]);
991        let terminal = Pointer::sign(&sk, &pk, u64::MAX, target).expect("sign");
992        assert!(
993            terminal.update(&sk, target).is_err(),
994            "the counter must not wrap around into a fresh-looking pointer"
995        );
996    }
997
998    /// Re-encoding must be byte-identical to what was signed.
999    ///
1000    /// This is what lets the record hold its five fields and nothing else: the
1001    /// encoding is fixed-width with no optionality, so there is exactly one
1002    /// byte sequence for a given record and caching it would buy nothing. If
1003    /// this ever fails, the cache has to come back.
1004    #[test]
1005    fn re_encoding_is_byte_identical() {
1006        let (pk, sk) = keypair(30);
1007        for counter in [0u64, 1, 42, u64::MAX] {
1008            for tag in [0u8, 1, 200] {
1009                let target = PointerTarget::from_raw_tag(tag, [tag; XORNAME_LEN]);
1010                let signed = Pointer::sign(&sk, &pk, counter, target).expect("sign");
1011                let wire = signed.to_bytes();
1012
1013                // Parsed from the wire, re-encoded, identical.
1014                let parsed = Pointer::from_bytes(&wire).expect("parse");
1015                assert_eq!(parsed.to_bytes(), wire, "counter {counter}, tag {tag}");
1016
1017                // And still verifies, which is the property that actually matters.
1018                assert!(Pointer::from_bytes(&parsed.to_bytes()).is_ok());
1019            }
1020        }
1021    }
1022
1023    #[test]
1024    fn equal_state_never_replaces() {
1025        let variants: Vec<Pointer> = (0..8).map(|_| signed(1, 7, 3)).collect();
1026        for a in &variants {
1027            for b in &variants {
1028                assert!(!a.replaces(b));
1029                assert_eq!(a.state_id(), b.state_id());
1030            }
1031        }
1032    }
1033
1034    #[test]
1035    fn the_obvious_chunk_preimages_miss_both_pointer_identities() {
1036        // A chunk's address is BLAKE3 over its content, so a hash of a prefix
1037        // and a key was reachable by anyone who could write that prefix as a
1038        // chunk: they could squat an owner's address before it was created, or
1039        // buy a pointer and a chunk with one payment. These are the preimages
1040        // that construction handed out. Nothing here proves no content exists —
1041        // that is a preimage assumption, not a test.
1042        let (pk, sk) = keypair(77);
1043        let record = Pointer::create(
1044            &sk,
1045            &pk,
1046            PointerTarget::new(PointerTargetKind::Chunk, [3; XORNAME_LEN]),
1047        )
1048        .expect("create");
1049
1050        let address_preimage = [b"autonomi.pointer.address.v1".as_slice(), &pk.to_bytes()].concat();
1051        assert_ne!(
1052            *blake3::hash(&address_preimage).as_bytes(),
1053            record.address(),
1054            "a chunk holding this content must not land on the pointer's address"
1055        );
1056
1057        let body = record.to_bytes();
1058        let body = body.get(..POINTER_BODY_LEN).expect("body");
1059        let state_preimage = [b"autonomi.pointer.state.v1".as_slice(), body].concat();
1060        assert_ne!(
1061            *blake3::hash(&state_preimage).as_bytes(),
1062            record.state_id(),
1063            "a chunk holding this content must not land on the paid identifier"
1064        );
1065
1066        // Nor does the bare key, the bare body, or the context alone.
1067        for content in [
1068            pk.to_bytes().as_slice(),
1069            body,
1070            b"autonomi.pointer.address.v1",
1071        ] {
1072            let address = *blake3::hash(content).as_bytes();
1073            assert_ne!(address, record.address());
1074            assert_ne!(address, record.state_id());
1075        }
1076    }
1077
1078    #[test]
1079    fn the_rule_is_a_strict_order_so_no_fold_depends_on_arrival_order() {
1080        // Nodes and clients both pick a winner by folding `replaces` over
1081        // whatever arrives. That fold gives the same answer whatever the order
1082        // exactly when the rule is a strict total order on one address: never
1083        // both ways round, always one way for distinct states, and transitive.
1084        let records = [
1085            signed(1, 0, 9),
1086            signed(1, 2, 1),
1087            signed(1, 5, 1),
1088            signed(1, 5, 2),
1089        ];
1090        for a in &records {
1091            assert!(!a.replaces(a), "an equal state never replaces");
1092            for b in &records {
1093                assert!(
1094                    !(a.replaces(b) && b.replaces(a)),
1095                    "two records cannot each replace the other"
1096                );
1097                assert!(
1098                    a.replaces(b) || b.replaces(a) || a.state_id() == b.state_id(),
1099                    "distinct states are always ordered"
1100                );
1101                for c in &records {
1102                    assert!(
1103                        !(a.replaces(b) && b.replaces(c)) || a.replaces(c),
1104                        "the order is transitive"
1105                    );
1106                }
1107            }
1108        }
1109    }
1110}