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}