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