Skip to main content

libveritas/
lib.rs

1//! Offline verification library for the [Spaces protocol](https://spacesprotocol.org).
2//!
3//! `libveritas` verifies space handle ownership and zone records against on-chain
4//! anchors using ZK receipts and Merkle proofs. It is the verifier counterpart to
5//! the Spaces fabric / relay infrastructure.
6//!
7//! # Quick start
8//!
9//! ```ignore
10//! use libveritas::{Veritas, msg::QueryContext};
11//!
12//! let veritas = Veritas::new().with_anchors(anchors)?;
13//! let result = veritas.verify(&QueryContext::new(), message)?;
14//! for zone in &result.zones {
15//!     // ...
16//! }
17//! ```
18//!
19//! # Features
20//!
21//! - `elf` — embed the prover ELF binaries (`FOLD_ELF`, `STEP_ELF`) alongside
22//!   the image IDs. Verifiers only need the image IDs and can skip this feature.
23
24use crate::cert::{Certificate, KeyHash, Signature, Witness};
25use borsh::{BorshDeserialize, BorshSerialize};
26use libveritas_zk::guest::CommitmentKind;
27use risc0_zkvm::{Receipt, VerifierContext};
28use serde::{Deserialize, Deserializer, Serialize, Serializer};
29use spacedb::subtree::SubTree;
30use spacedb::{Hash, NodeHasher, Sha256Hasher};
31use spaces_nums::RootAnchor;
32use spaces_nums::constants::COMMITMENT_FINALITY_INTERVAL;
33use spaces_protocol::bitcoin::ScriptBuf;
34use spaces_protocol::bitcoin::hashes::{Hash as HashUtil, HashEngine, sha256};
35use spaces_protocol::bitcoin::secp256k1::{self, XOnlyPublicKey};
36use spaces_protocol::constants::SPACES_SIGNED_MSG_PREFIX;
37use spaces_protocol::slabel::SLabel;
38use spaces_protocol::sname::SName;
39use std::collections::HashSet;
40use std::fmt;
41use std::io::{Read, Write};
42use std::sync::OnceLock;
43
44pub mod builder;
45pub mod cert;
46pub mod constants;
47#[cfg(feature = "inspect")]
48pub mod inspect;
49pub mod msg;
50pub mod names;
51
52pub use sip7;
53use spaces_nums::num_id::NumId;
54pub use spaces_protocol;
55
56/// Verification option flags (combine with bitwise OR).
57pub const VERIFY_DEFAULT: u32 = 0;
58pub const VERIFY_DEV_MODE: u32 = 1 << 0;
59pub const VERIFY_ENABLE_SNARK: u32 = 1 << 1;
60
61/// Result of verifying a message.
62///
63/// Contains the verified zones and the original message data.
64/// The message can be used to construct certificates for storage.
65pub struct VerifiedMessage {
66    pub zones: Vec<Zone>,
67    pub message: msg::Message,
68}
69
70impl VerifiedMessage {
71    /// Iterate over all certificates from this verified message.
72    ///
73    /// Panics if the message was not produced by [`Veritas::verify`] and
74    /// contains handle names that don't join with their bundle's subject —
75    /// verification guarantees every kept handle is joinable.
76    pub fn certificates(&self) -> CertificateIter<'_> {
77        CertificateIter {
78            zones: &self.zones,
79            bundles: self.message.spaces.iter(),
80            // Context for building certs (handle iterator doesn't carry parent refs)
81            current_bundle: None,
82            current_epoch: None,
83            epochs: None,
84            handles: None,
85        }
86    }
87}
88
89/// Iterator over certificates from a verified message.
90pub struct CertificateIter<'a> {
91    zones: &'a [Zone],
92    bundles: std::slice::Iter<'a, msg::Bundle>,
93    current_bundle: Option<&'a msg::Bundle>,
94    current_epoch: Option<&'a msg::Epoch>,
95    epochs: Option<std::slice::Iter<'a, msg::Epoch>>,
96    handles: Option<std::slice::Iter<'a, msg::Handle>>,
97}
98
99impl<'a> Iterator for CertificateIter<'a> {
100    type Item = Certificate;
101
102    fn next(&mut self) -> Option<Self::Item> {
103        loop {
104            // Try to emit a handle from current epoch
105            if let Some(handles) = &mut self.handles {
106                if let Some(h) = handles.next() {
107                    let bundle = self.current_bundle?;
108                    let epoch = self.current_epoch?;
109                    let subject = SName::join(&h.name, &bundle.subject)
110                        .expect("handle names in a verified message are joinable");
111
112                    return Some(Certificate::new(
113                        subject,
114                        Witness::Leaf {
115                            genesis_spk: h.genesis_spk.clone(),
116                            handles: epoch.tree.clone(),
117                            signature: h.signature,
118                        },
119                    ));
120                }
121            }
122
123            // Try next epoch
124            if let Some(epochs) = &mut self.epochs {
125                if let Some(epoch) = epochs.next() {
126                    self.current_epoch = Some(epoch);
127                    self.handles = Some(epoch.handles.iter());
128                    continue;
129                }
130            }
131
132            // Try next bundle
133            let bundle = self.bundles.next()?;
134            self.current_bundle = Some(bundle);
135            self.epochs = Some(bundle.epochs.iter());
136            self.current_epoch = None;
137            self.handles = None;
138
139            // Emit root cert if zone exists
140            let root_handle = SName::from_space(&bundle.subject);
141            if self.zones.iter().any(|z| z.canonical == root_handle) {
142                return Some(Certificate::new(
143                    root_handle,
144                    Witness::Root {
145                        receipt: bundle.receipt.clone(),
146                    },
147                ));
148            }
149        }
150    }
151}
152
153#[derive(Clone)]
154pub struct Veritas {
155    anchors: Vec<RootAnchor>,
156    oldest_anchor: u32,
157    newest_anchor: u32,
158}
159
160#[derive(Copy, Clone, PartialEq, Debug, Serialize, Deserialize)]
161#[serde(rename_all = "lowercase")]
162pub enum SovereigntyState {
163    /// Fully sovereign — independent and self-governing.
164    Sovereign,
165
166    /// Pending — commitment not yet finalized.
167    /// May eventually become sovereign or remain dependent.
168    Pending,
169
170    /// Dependent — under external authority, not self-governing.
171    Dependent,
172}
173
174impl fmt::Display for SovereigntyState {
175    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
176        match self {
177            Self::Sovereign => write!(f, "sovereign"),
178            Self::Pending => write!(f, "pending"),
179            Self::Dependent => write!(f, "dependent"),
180        }
181    }
182}
183
184/// A verified zone representing ownership and state for a space handle.
185///
186/// Zones are produced by verifying certificates against on-chain anchors.
187/// They contain all proven information about a handle's current state,
188/// including ownership, delegation, and commitment data.
189#[derive(Clone, Serialize, Deserialize)]
190pub struct Zone {
191    /// The block height of the anchor used to prove this zone (snapshot version).
192    pub anchor: u32,
193    /// Hash of the root anchor this zone was verified against.
194    #[serde(
195        serialize_with = "serialize_hash",
196        deserialize_with = "deserialize_hash"
197    )]
198    pub anchor_hash: Hash,
199    /// The sovereignty state indicating finality of the zone's commitment.
200    pub sovereignty: SovereigntyState,
201    /// Human-readable name (e.g., "nested1.alice@bitcoin").
202    /// Same as `canonical` when the handle has no numeric space.
203    pub handle: SName,
204    /// Canonical on-chain form (e.g., "nested1#800-12-12").
205    pub canonical: SName,
206    /// Set if this zone has a num alias.
207    pub alias: Option<SLabel>,
208    /// The current script pubkey that controls this handle.
209    pub script_pubkey: ScriptBuf,
210    /// Verified off-chain records from the handle owner.
211    pub records: sip7::RecordSet,
212    /// Optional on-chain data associated with the handle.
213    pub fallback_records: sip7::RecordSet,
214    /// Delegate information if the handle has delegated signing authority.
215    pub delegate: ProvableOption<Delegate>,
216    /// Commitment information including state root and finality status.
217    pub commitment: ProvableOption<CommitmentInfo>,
218    /// The numeric id for this zone:
219    /// For spaces, its None.
220    /// For handles, derived from their genesis spk
221    /// For numerics, it's their num id
222    pub num_id: Option<NumId>,
223}
224
225/// Information about a space's commitment state.
226#[derive(Clone, Serialize, Deserialize)]
227pub struct CommitmentInfo {
228    /// The on-chain commitment data.
229    pub onchain: spaces_nums::Commitment,
230    /// Hash of the ZK receipt that proved this commitment (if verified).
231    #[serde(
232        serialize_with = "serialize_option_hash",
233        deserialize_with = "deserialize_option_hash"
234    )]
235    pub receipt_hash: Option<Hash>,
236}
237
238impl CommitmentInfo {
239    pub fn empty() -> Self {
240        let empty_root = SubTree::<Sha256Hasher>::empty()
241            .compute_root()
242            .expect("valid");
243        Self {
244            onchain: spaces_nums::Commitment {
245                state_root: empty_root,
246                prev_root: None,
247                rolling_hash: empty_root,
248                block_height: 0,
249            },
250            receipt_hash: None,
251        }
252    }
253}
254
255#[derive(Clone, Serialize, Deserialize)]
256pub struct Delegate {
257    pub script_pubkey: ScriptBuf,
258    /// Verified off-chain records from the delegate.
259    pub records: sip7::RecordSet,
260    pub fallback_records: sip7::RecordSet,
261}
262
263#[derive(Clone, Serialize, Deserialize)]
264#[serde(tag = "status", rename_all = "snake_case")]
265pub enum ProvableOption<T> {
266    Exists { value: T },
267    Empty,
268    Unknown,
269}
270
271impl BorshSerialize for SovereigntyState {
272    fn serialize<W: Write>(&self, writer: &mut W) -> std::io::Result<()> {
273        let variant: u8 = match self {
274            Self::Sovereign => 0,
275            Self::Pending => 1,
276            Self::Dependent => 2,
277        };
278        BorshSerialize::serialize(&variant, writer)
279    }
280}
281
282impl BorshDeserialize for SovereigntyState {
283    fn deserialize_reader<R: Read>(reader: &mut R) -> std::io::Result<Self> {
284        let variant = u8::deserialize_reader(reader)?;
285        match variant {
286            0 => Ok(Self::Sovereign),
287            1 => Ok(Self::Pending),
288            2 => Ok(Self::Dependent),
289            _ => Err(std::io::Error::new(
290                std::io::ErrorKind::InvalidData,
291                format!("invalid SovereigntyState variant: {}", variant),
292            )),
293        }
294    }
295}
296
297impl BorshSerialize for Delegate {
298    fn serialize<W: Write>(&self, writer: &mut W) -> std::io::Result<()> {
299        BorshSerialize::serialize(&self.script_pubkey.as_bytes().to_vec(), writer)?;
300        BorshSerialize::serialize(&self.fallback_records.as_slice().to_vec(), writer)?;
301        BorshSerialize::serialize(&self.records.as_slice().to_vec(), writer)
302    }
303}
304
305impl BorshDeserialize for Delegate {
306    fn deserialize_reader<R: Read>(reader: &mut R) -> std::io::Result<Self> {
307        let spk_bytes: Vec<u8> = Vec::deserialize_reader(reader)?;
308        let fallback_bytes: Vec<u8> = Vec::deserialize_reader(reader)?;
309        let records_bytes: Vec<u8> = Vec::deserialize_reader(reader)?;
310        Ok(Delegate {
311            script_pubkey: ScriptBuf::from_bytes(spk_bytes),
312            fallback_records: sip7::RecordSet::new(fallback_bytes),
313            records: sip7::RecordSet::new(records_bytes),
314        })
315    }
316}
317
318impl BorshSerialize for CommitmentInfo {
319    fn serialize<W: Write>(&self, writer: &mut W) -> std::io::Result<()> {
320        BorshSerialize::serialize(&self.onchain, writer)?;
321        BorshSerialize::serialize(&self.receipt_hash, writer)
322    }
323}
324
325impl BorshDeserialize for CommitmentInfo {
326    fn deserialize_reader<R: Read>(reader: &mut R) -> std::io::Result<Self> {
327        let onchain = spaces_nums::Commitment::deserialize_reader(reader)?;
328        let receipt_hash = Option::<Hash>::deserialize_reader(reader)?;
329        Ok(CommitmentInfo {
330            onchain,
331            receipt_hash,
332        })
333    }
334}
335
336impl<T: BorshSerialize> BorshSerialize for ProvableOption<T> {
337    fn serialize<W: Write>(&self, writer: &mut W) -> std::io::Result<()> {
338        match self {
339            Self::Exists { value } => {
340                BorshSerialize::serialize(&0u8, writer)?;
341                BorshSerialize::serialize(value, writer)
342            }
343            Self::Empty => BorshSerialize::serialize(&1u8, writer),
344            Self::Unknown => BorshSerialize::serialize(&2u8, writer),
345        }
346    }
347}
348
349impl<T: BorshDeserialize> BorshDeserialize for ProvableOption<T> {
350    fn deserialize_reader<R: Read>(reader: &mut R) -> std::io::Result<Self> {
351        let variant = u8::deserialize_reader(reader)?;
352        match variant {
353            0 => {
354                let value = T::deserialize_reader(reader)?;
355                Ok(Self::Exists { value })
356            }
357            1 => Ok(Self::Empty),
358            2 => Ok(Self::Unknown),
359            _ => Err(std::io::Error::new(
360                std::io::ErrorKind::InvalidData,
361                format!("invalid ProvableOption variant: {}", variant),
362            )),
363        }
364    }
365}
366
367impl BorshSerialize for Zone {
368    fn serialize<W: Write>(&self, writer: &mut W) -> std::io::Result<()> {
369        BorshSerialize::serialize(&self.anchor, writer)?;
370        BorshSerialize::serialize(&self.anchor_hash, writer)?;
371        BorshSerialize::serialize(&self.sovereignty, writer)?;
372        BorshSerialize::serialize(&self.canonical, writer)?;
373        BorshSerialize::serialize(&self.handle, writer)?;
374        BorshSerialize::serialize(&self.alias, writer)?;
375        BorshSerialize::serialize(&self.script_pubkey.as_bytes().to_vec(), writer)?;
376        BorshSerialize::serialize(&self.fallback_records.as_slice().to_vec(), writer)?;
377        BorshSerialize::serialize(&self.records.as_slice().to_vec(), writer)?;
378        BorshSerialize::serialize(&self.delegate, writer)?;
379        BorshSerialize::serialize(&self.commitment, writer)?;
380        BorshSerialize::serialize(&self.num_id, writer)
381    }
382}
383
384impl BorshDeserialize for Zone {
385    fn deserialize_reader<R: Read>(reader: &mut R) -> std::io::Result<Self> {
386        let anchor = u32::deserialize_reader(reader)?;
387        let anchor_hash = <[u8; 32]>::deserialize_reader(reader)?;
388        let sovereignty = SovereigntyState::deserialize_reader(reader)?;
389        let canonical = SName::deserialize_reader(reader)?;
390        let handle = SName::deserialize_reader(reader)?;
391        let alias = Option::<SLabel>::deserialize_reader(reader)?;
392        let spk_bytes: Vec<u8> = Vec::deserialize_reader(reader)?;
393        let fallback_bytes: Vec<u8> = Vec::deserialize_reader(reader)?;
394        let records_bytes: Vec<u8> = Vec::deserialize_reader(reader)?;
395        let delegate: ProvableOption<Delegate> = ProvableOption::deserialize_reader(reader)?;
396        let commitment: ProvableOption<CommitmentInfo> =
397            ProvableOption::deserialize_reader(reader)?;
398
399        let script_pubkey = ScriptBuf::from_bytes(spk_bytes);
400        let num_id = Option::<NumId>::deserialize_reader(reader)?;
401        Ok(Zone {
402            anchor,
403            anchor_hash,
404            sovereignty,
405            handle,
406            canonical,
407            alias,
408            script_pubkey,
409            fallback_records: sip7::RecordSet::new(fallback_bytes),
410            records: sip7::RecordSet::new(records_bytes),
411            delegate,
412            commitment,
413            num_id,
414        })
415    }
416}
417
418/// Compute a deterministic id for a single root anchor.
419pub fn compute_root_id(root: &RootAnchor) -> [u8; 32] {
420    let mut engine = sha256::Hash::engine();
421    engine.input(&root.block.hash[..]);
422    engine.input(&root.block.height.to_le_bytes());
423    engine.input(&root.spaces_root);
424    engine.input(&root.nums_root.unwrap_or([0u8; 32]));
425    sha256::Hash::from_engine(engine).to_byte_array()
426}
427
428/// A compact representation of a set of trusted anchors.
429#[derive(Clone)]
430pub struct TrustSet {
431    pub id: [u8; 32],
432    pub roots: Vec<[u8; 32]>,
433}
434
435/// Compute a trust set from anchors.
436pub fn compute_trust_set(anchors: &[RootAnchor]) -> TrustSet {
437    let roots: Vec<[u8; 32]> = anchors.iter().map(compute_root_id).collect();
438    let mut engine = sha256::Hash::engine();
439    for r in &roots {
440        engine.input(r);
441    }
442    TrustSet {
443        id: sha256::Hash::from_engine(engine).to_byte_array(),
444        roots,
445    }
446}
447
448/// Cached verify-only secp256k1 context.
449///
450/// Creating a `Secp256k1` context allocates and pre-computes lookup tables;
451/// reuse this for bulk verification rather than calling
452/// `verification_only()` per-signature.
453pub(crate) fn secp256k1_verify_ctx() -> &'static secp256k1::Secp256k1<secp256k1::VerifyOnly> {
454    static CTX: OnceLock<secp256k1::Secp256k1<secp256k1::VerifyOnly>> = OnceLock::new();
455    CTX.get_or_init(secp256k1::Secp256k1::verification_only)
456}
457
458pub fn hash_signable_message(msg: &[u8]) -> secp256k1::Message {
459    let mut engine = sha256::Hash::engine();
460    engine.input(SPACES_SIGNED_MSG_PREFIX);
461    engine.input(msg);
462    let digest = sha256::Hash::from_engine(engine);
463    secp256k1::Message::from_digest(digest.to_byte_array())
464}
465
466/// Verify a Schnorr signature over a message using the Spaces signed-message prefix.
467///
468/// - `msg`: the raw message bytes (will be prefixed and hashed internally)
469/// - `signature`: 64-byte Schnorr signature
470/// - `pubkey`: 32-byte x-only public key
471pub fn verify_spaces_message(
472    msg: &[u8],
473    signature: &[u8; 64],
474    pubkey: &[u8; 32],
475) -> Result<(), SignatureError> {
476    let xonly = XOnlyPublicKey::from_slice(pubkey).map_err(|_| SignatureError::InvalidPublicKey)?;
477    let sig = secp256k1::schnorr::Signature::from_slice(signature)
478        .map_err(|_| SignatureError::InvalidSignature)?;
479    let hashed = hash_signable_message(msg);
480    secp256k1_verify_ctx()
481        .verify_schnorr(&sig, &hashed, &xonly)
482        .map_err(|_| SignatureError::VerificationFailed)
483}
484
485/// Verify a raw Schnorr signature (no prefix, caller provides the 32-byte message hash).
486///
487/// - `msg_hash`: 32-byte SHA256 hash of the message
488/// - `signature`: 64-byte Schnorr signature
489/// - `pubkey`: 32-byte x-only public key
490pub fn verify_schnorr(
491    msg_hash: &[u8; 32],
492    signature: &[u8; 64],
493    pubkey: &[u8; 32],
494) -> Result<(), SignatureError> {
495    let xonly = XOnlyPublicKey::from_slice(pubkey).map_err(|_| SignatureError::InvalidPublicKey)?;
496    let sig = secp256k1::schnorr::Signature::from_slice(signature)
497        .map_err(|_| SignatureError::InvalidSignature)?;
498    let msg = secp256k1::Message::from_digest(*msg_hash);
499    secp256k1_verify_ctx()
500        .verify_schnorr(&sig, &msg, &xonly)
501        .map_err(|_| SignatureError::VerificationFailed)
502}
503
504/// Compare two record sets by seq then data hash (for Zone freshness comparison).
505fn records_cmp(a: &sip7::RecordSet, b: &sip7::RecordSet) -> std::cmp::Ordering {
506    let a_seq = a.seq().unwrap_or(0);
507    let b_seq = b.seq().unwrap_or(0);
508    a_seq.cmp(&b_seq).then_with(|| {
509        let hash_a = Sha256Hasher::hash(a.as_slice());
510        let hash_b = Sha256Hasher::hash(b.as_slice());
511        hash_a.cmp(&hash_b)
512    })
513}
514
515impl Zone {
516    pub fn from_slice(bytes: &[u8]) -> Result<Self, std::io::Error> {
517        borsh::from_slice(bytes)
518    }
519
520    pub fn to_bytes(&self) -> Vec<u8> {
521        borsh::to_vec(self).expect("zone serialization should not fail")
522    }
523
524    /// Returns the zone serialized for signing.
525    ///
526    /// The `anchor`, `anchor_hash`, and `records` fields are zeroed out so
527    /// delegate signatures remain valid across different anchor snapshots
528    /// and don't include owner-signed records.
529    pub fn signing_bytes(&self) -> Vec<u8> {
530        let mut zone = self.clone();
531        zone.anchor = 0;
532        zone.anchor_hash = [0u8; 32];
533        zone.records = sip7::RecordSet::default();
534        borsh::to_vec(&zone).expect("zone serialization should not fail")
535    }
536
537    /// Verify a schnorr signature over this zone.
538    ///
539    /// The message is the borsh-serialized zone data (with anchor zeroed),
540    /// prefixed with the spaces signed message prefix and hashed with SHA256.
541    pub fn verify_signature(
542        &self,
543        signature: &Signature,
544        signer: &ScriptBuf,
545    ) -> Result<(), SignatureError> {
546        if !signer.is_p2tr() {
547            return Err(SignatureError::InvalidPublicKey);
548        }
549        let pubkey = XOnlyPublicKey::from_slice(&signer.as_bytes()[2..])
550            .map_err(|_| SignatureError::InvalidPublicKey)?;
551
552        let msg = hash_signable_message(&self.signing_bytes());
553        let sig = secp256k1::schnorr::Signature::from_slice(&signature.0)
554            .map_err(|_| SignatureError::InvalidSignature)?;
555
556        secp256k1_verify_ctx()
557            .verify_schnorr(&sig, &msg, &pubkey)
558            .map_err(|_| SignatureError::VerificationFailed)
559    }
560
561    /// Returns true if self is fresher/better than other.
562    ///
563    /// Comparison order:
564    /// 1. Higher commitment height (receipts are expensive, keep the latest)
565    /// 2. Commitment knowledge (Exists > Empty > Unknown)
566    /// 3. Delegate knowledge (Exists > Empty > Unknown)
567    /// 4. Higher records seq (owner-signed records freshness, via sip7 Seq record)
568    /// 5. Higher anchor (fresher chain state, tiebreaker only)
569    ///
570    /// Anchor is checked last to prevent attackers from downgrading cached
571    /// state by sending messages with higher anchors but incomplete proofs.
572    ///
573    /// Returns an error if the zones are for different handles.
574    pub fn is_better_than(&self, other: &Self) -> Result<bool, ZoneCompareError> {
575        if self.canonical != other.canonical {
576            return Err(ZoneCompareError::DifferentHandles);
577        }
578
579        // Higher commitment height = newer committed state.
580        // Equal heights fall through to the delegate/records/anchor comparisons.
581        match (&self.commitment, &other.commitment) {
582            (ProvableOption::Exists { value: a }, ProvableOption::Exists { value: b })
583                if a.onchain.block_height != b.onchain.block_height =>
584            {
585                return Ok(a.onchain.block_height > b.onchain.block_height);
586            }
587            (ProvableOption::Exists { .. }, ProvableOption::Empty | ProvableOption::Unknown) => {
588                return Ok(true);
589            }
590            (ProvableOption::Empty | ProvableOption::Unknown, ProvableOption::Exists { .. }) => {
591                return Ok(false);
592            }
593            (ProvableOption::Empty, ProvableOption::Unknown) => return Ok(true),
594            (ProvableOption::Unknown, ProvableOption::Empty) => return Ok(false),
595            _ => {}
596        }
597
598        // Delegate knowledge
599        match (&self.delegate, &other.delegate) {
600            (ProvableOption::Exists { value: a }, ProvableOption::Exists { value: b })
601                if (!a.records.is_empty() || !b.records.is_empty()) =>
602            {
603                if a.records.is_empty() {
604                    return Ok(false);
605                }
606                if b.records.is_empty() {
607                    return Ok(true);
608                }
609                match records_cmp(&a.records, &b.records) {
610                    std::cmp::Ordering::Greater => return Ok(true),
611                    std::cmp::Ordering::Less => return Ok(false),
612                    std::cmp::Ordering::Equal => {}
613                }
614            }
615            (ProvableOption::Exists { .. }, ProvableOption::Empty | ProvableOption::Unknown) => {
616                return Ok(true);
617            }
618            (ProvableOption::Empty | ProvableOption::Unknown, ProvableOption::Exists { .. }) => {
619                return Ok(false);
620            }
621            (ProvableOption::Empty, ProvableOption::Unknown) => return Ok(true),
622            (ProvableOption::Unknown, ProvableOption::Empty) => return Ok(false),
623            _ => {}
624        }
625
626        // Higher records seq = newer owner-signed records
627        if !self.records.is_empty() || !other.records.is_empty() {
628            if self.records.is_empty() {
629                return Ok(false);
630            }
631            if other.records.is_empty() {
632                return Ok(true);
633            }
634            match records_cmp(&self.records, &other.records) {
635                std::cmp::Ordering::Greater => return Ok(true),
636                std::cmp::Ordering::Less => return Ok(false),
637                std::cmp::Ordering::Equal => {}
638            }
639        }
640
641        // Higher anchor = fresher chain state (tiebreaker)
642        if self.anchor != other.anchor {
643            return Ok(self.anchor > other.anchor);
644        }
645
646        Ok(false) // equal
647    }
648
649    /// Copy receipt_hash from other if commitment roots match.
650    /// Avoids re-verifying ZK receipts for commitments we've already verified.
651    pub fn update_receipt_cache(&mut self, other: &Self) {
652        if let (ProvableOption::Exists { value: mine }, ProvableOption::Exists { value: theirs }) =
653            (&mut self.commitment, &other.commitment)
654        {
655            if mine.onchain.state_root == theirs.onchain.state_root && mine.receipt_hash.is_none() {
656                mine.receipt_hash = theirs.receipt_hash;
657            }
658        }
659    }
660
661    /// Returns true if the zone has a commitment that requires ZK verification.
662    ///
663    /// Returns false if:
664    /// - Already ZK-verified (has receipt_hash)
665    /// - First commitment (prev_root is None, nothing to prove transition from)
666    /// - No commitment exists
667    /// - Commitment is unknown
668    pub fn requires_receipt(&mut self) -> Option<&mut CommitmentInfo> {
669        match &mut self.commitment {
670            ProvableOption::Exists { value } => {
671                if value.receipt_hash.is_some() {
672                    return None;
673                }
674                value.onchain.prev_root?;
675                Some(value)
676            }
677            _ => None,
678        }
679    }
680}
681
682fn verify_receipt(
683    ci: &mut CommitmentInfo,
684    space: &SLabel,
685    receipt: &Receipt,
686    options: u32,
687) -> Result<(), MessageError> {
688    let space_str = space.to_string();
689    let zkc = decode_journal(receipt, space)?;
690    verify_zk_journal_matches_onchain(space, &zkc, &ci.onchain)?;
691    let dev_mode = options & VERIFY_DEV_MODE != 0;
692    let mut ctx = VerifierContext::default().with_dev_mode(dev_mode);
693    if options & VERIFY_ENABLE_SNARK == 0 {
694        if matches!(receipt.inner, risc0_zkvm::InnerReceipt::Groth16(_)) {
695            return Err(MessageError::ReceiptInvalid {
696                space: space_str,
697                reason: "SNARK receipts require VERIFY_ENABLE_SNARK".to_string(),
698            });
699        }
700        ctx.groth16_verifier_parameters = None;
701    }
702    let image_id = match zkc.kind {
703        CommitmentKind::Fold => constants::FOLD_ID,
704        CommitmentKind::Step => constants::STEP_ID,
705    };
706    receipt
707        .verify_with_context(&ctx, image_id)
708        .map_err(|e| MessageError::ReceiptInvalid {
709            space: space_str,
710            reason: e.to_string(),
711        })?;
712    let receipt_hash = hash_receipt(receipt);
713    ci.receipt_hash = Some(receipt_hash);
714    Ok(())
715}
716
717#[derive(Debug, Clone)]
718pub enum SignatureError {
719    /// Script pubkey is not a valid schnorr public key
720    InvalidPublicKey,
721    /// Signature bytes are malformed
722    InvalidSignature,
723    /// Signature verification failed
724    VerificationFailed,
725    /// Sig record canonical name doesn't match expected
726    SignerMismatch,
727}
728
729impl fmt::Display for SignatureError {
730    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
731        match self {
732            Self::InvalidPublicKey => write!(f, "invalid schnorr public key"),
733            Self::InvalidSignature => write!(f, "invalid signature format"),
734            Self::VerificationFailed => write!(f, "signature verification failed"),
735            Self::SignerMismatch => write!(f, "sig record canonical name mismatch"),
736        }
737    }
738}
739
740impl std::error::Error for SignatureError {}
741
742#[derive(Debug, Clone)]
743pub enum ZoneCompareError {
744    /// Cannot compare zones for different handles
745    DifferentHandles,
746}
747
748impl fmt::Display for ZoneCompareError {
749    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
750        match self {
751            Self::DifferentHandles => write!(f, "cannot compare zones for different handles"),
752        }
753    }
754}
755
756impl std::error::Error for ZoneCompareError {}
757
758/// Error when loading or updating anchors.
759#[derive(Debug, Clone)]
760pub enum AnchorError {
761    NotSorted,
762}
763
764impl fmt::Display for AnchorError {
765    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
766        match self {
767            Self::NotSorted => write!(f, "anchors must be sorted by height in descending order"),
768        }
769    }
770}
771
772impl std::error::Error for AnchorError {}
773
774impl Default for Veritas {
775    fn default() -> Self {
776        Self::new()
777    }
778}
779
780impl Veritas {
781    pub fn new() -> Self {
782        Veritas {
783            anchors: vec![],
784            oldest_anchor: 0,
785            newest_anchor: 0,
786        }
787    }
788
789    pub fn with_anchors(mut self, anchors: Vec<RootAnchor>) -> Result<Self, AnchorError> {
790        if !anchors.is_empty() {
791            if !anchors.iter().rev().is_sorted_by_key(|a| a.block.height) {
792                return Err(AnchorError::NotSorted);
793            }
794            self.newest_anchor = anchors[0].block.height;
795            self.oldest_anchor = anchors.last().unwrap().block.height;
796        }
797        self.anchors = anchors;
798        Ok(self)
799    }
800
801    pub fn oldest_anchor(&self) -> u32 {
802        self.oldest_anchor
803    }
804
805    pub fn newest_anchor(&self) -> u32 {
806        self.newest_anchor
807    }
808
809    pub fn compute_trust_set(&self) -> TrustSet {
810        compute_trust_set(&self.anchors)
811    }
812
813    pub fn is_finalized(&self, commitment_height: u32) -> bool {
814        let time_passed = self.newest_anchor.saturating_sub(commitment_height);
815        time_passed >= COMMITMENT_FINALITY_INTERVAL
816    }
817
818    /// Get sovereignty state for a commitment at the given block height.
819    pub fn sovereignty_for(&self, commitment_height: u32) -> SovereigntyState {
820        if self.is_finalized(commitment_height) {
821            SovereigntyState::Sovereign
822        } else {
823            SovereigntyState::Pending
824        }
825    }
826
827    /// Verify a message with default options.
828    pub fn verify(
829        &self,
830        ctx: &msg::QueryContext,
831        msg: crate::msg::Message,
832    ) -> Result<VerifiedMessage, MessageError> {
833        self.verify_with_options(ctx, msg, VERIFY_DEFAULT)
834    }
835
836    /// Verify a message with option flags.
837    ///
838    /// Flags can be combined with bitwise OR:
839    /// - `VERIFY_DEFAULT` (0): standard verification
840    /// - `VERIFY_DEV_MODE`: accept fake ZK receipts (for testing)
841    /// - `VERIFY_ENABLE_SNARK`: allow Groth16 SNARK receipts (disabled by default)
842    pub fn verify_with_options(
843        &self,
844        ctx: &msg::QueryContext,
845        msg: crate::msg::Message,
846        options: u32,
847    ) -> Result<VerifiedMessage, MessageError> {
848        let anchor = self.check_msg_anchor(&msg)?;
849        self.check_msg_chain_proofs(&msg, &anchor)?;
850        self.check_msg_duplicate_spaces(&msg)?;
851
852        let mut zones = Vec::new();
853        let mut verified_bundles = Vec::new();
854        let nums_verified = anchor.nums_root.is_some();
855
856        for bundle in msg.spaces {
857            let (bundle_zones, verified_bundle) =
858                self.verify_bundle(ctx, &msg.chain, options, bundle, nums_verified)?;
859            zones.extend(bundle_zones);
860            if let Some(vb) = verified_bundle {
861                verified_bundles.push(vb);
862            }
863        }
864
865        let resolver = names::NameResolver::from_zones(&zones);
866        resolver.expand_zones(&mut zones);
867
868        let anchor_hash = compute_root_id(&anchor);
869        for zone in &mut zones {
870            zone.anchor_hash = anchor_hash;
871        }
872
873        Ok(VerifiedMessage {
874            zones,
875            message: msg::Message {
876                chain: msg.chain,
877                spaces: verified_bundles,
878            },
879        })
880    }
881
882    fn verify_bundle(
883        &self,
884        ctx: &msg::QueryContext,
885        chain: &msg::ChainProof,
886        options: u32,
887        bundle: msg::Bundle,
888        nums_verified: bool,
889    ) -> Result<(Vec<Zone>, Option<msg::Bundle>), MessageError> {
890        let space = bundle.subject.clone();
891        let cached_parent = ctx.get_parent_zone(&space);
892        let mut extracted = self.extract_parent_zone(chain, &bundle, nums_verified)?;
893
894        let root_handle = SName::from_space(&space);
895
896        let mut zones: Vec<Zone> = Vec::new();
897        let mut receipt_verified = false;
898
899        // Resolve which parent zone to use
900        let target_zone: &Zone = match &cached_parent {
901            Some(cached) => {
902                extracted.update_receipt_cache(cached);
903                if extracted.is_better_than(cached).unwrap_or(false) {
904                    receipt_verified = maybe_verify_receipt(
905                        &mut extracted,
906                        bundle.receipt.as_ref(),
907                        &space,
908                        options,
909                    )?;
910                    &extracted
911                } else {
912                    cached
913                }
914            }
915            None => {
916                receipt_verified =
917                    maybe_verify_receipt(&mut extracted, bundle.receipt.as_ref(), &space, options)?;
918                &extracted
919            }
920        };
921
922        let wants_root = ctx.wants(&root_handle);
923        if wants_root {
924            zones.push(target_zone.clone());
925        }
926
927        let verified_tip = match target_zone.commitment.clone() {
928            ProvableOption::Exists { value } => value,
929            ProvableOption::Empty => CommitmentInfo::empty(),
930            ProvableOption::Unknown => {
931                // Nothing left to verify - return bundle only if root was wanted
932                let verified_bundle = if wants_root {
933                    Some(msg::Bundle {
934                        subject: space,
935                        receipt: if receipt_verified {
936                            bundle.receipt
937                        } else {
938                            None
939                        },
940                        epochs: vec![],
941                        records: bundle.records,
942                        delegate_records: bundle.delegate_records,
943                    })
944                } else {
945                    None
946                };
947                return Ok((zones, verified_bundle));
948            }
949        };
950
951        let mut checked: HashSet<Hash> = HashSet::with_capacity(bundle.epochs.len());
952        let mut verified_epochs: Vec<msg::Epoch> = Vec::new();
953
954        for epoch in bundle.epochs {
955            let root =
956                epoch
957                    .tree
958                    .compute_root()
959                    .map_err(|e| MessageError::HandleProofMalformed {
960                        handle: format!("*@{}", space),
961                        reason: e.to_string(),
962                    })?;
963
964            if checked.contains(&root) {
965                return Err(MessageError::DuplicateEpoch {
966                    space: space.to_string(),
967                    root,
968                });
969            }
970            checked.insert(root);
971
972            // Determine sovereignty based on commitment
973            let sovereignty = if epoch.tree.0.is_empty() {
974                SovereigntyState::Dependent
975            } else {
976                let onchain = chain
977                    .nums
978                    .find_commitment(&space, root)
979                    .map_err(|e| MessageError::NumsProofMalformed {
980                        reason: e.to_string(),
981                    })?
982                    .ok_or_else(|| MessageError::CommitmentNotFound {
983                        space: space.to_string(),
984                        root,
985                    })?;
986
987                if onchain.block_height > verified_tip.onchain.block_height {
988                    return Err(MessageError::EpochExceedsTip {
989                        space: space.to_string(),
990                    });
991                }
992
993                self.sovereignty_for(onchain.block_height)
994            };
995
996            let mut verified_handles: Vec<msg::Handle> = Vec::new();
997
998            for handle in epoch.handles {
999                let subject = SName::join(&handle.name, &space).map_err(|_| {
1000                    MessageError::InvalidSubject {
1001                        subject: format!("{}@{}", handle.name, space),
1002                    }
1003                })?;
1004
1005                if !ctx.wants(&subject) {
1006                    continue;
1007                }
1008
1009                let zone = if handle.signature.is_some() {
1010                    if root != verified_tip.onchain.state_root {
1011                        return Err(MessageError::TemporaryRequiresTip {
1012                            handle: subject.to_string(),
1013                            tip: verified_tip.onchain.state_root,
1014                            got: root,
1015                        });
1016                    }
1017                    verify_temporary_handle(
1018                        chain.anchor.height,
1019                        &handle,
1020                        &subject,
1021                        &epoch.tree,
1022                        target_zone,
1023                    )?
1024                } else {
1025                    verify_final_handle(
1026                        chain.anchor.height,
1027                        &handle,
1028                        &subject,
1029                        &epoch.tree,
1030                        &chain.nums,
1031                        sovereignty,
1032                    )?
1033                };
1034
1035                push_best_zone(ctx, &mut zones, zone);
1036                verified_handles.push(handle);
1037            }
1038
1039            if !verified_handles.is_empty() {
1040                verified_epochs.push(msg::Epoch {
1041                    tree: epoch.tree,
1042                    handles: verified_handles,
1043                });
1044            }
1045        }
1046
1047        // Callers that only requested handles still need the parent zone —
1048        // it carries the verified receipt_hash they must cache to avoid
1049        // re-verifying receipts on future queries.
1050        if !wants_root && !zones.is_empty() {
1051            zones.insert(0, target_zone.clone());
1052        }
1053
1054        // Build verified bundle if anything was verified
1055        let verified_bundle = if wants_root || !verified_epochs.is_empty() {
1056            Some(msg::Bundle {
1057                subject: space,
1058                receipt: if receipt_verified {
1059                    bundle.receipt
1060                } else {
1061                    None
1062                },
1063                epochs: verified_epochs,
1064                records: bundle.records,
1065                delegate_records: bundle.delegate_records,
1066            })
1067        } else {
1068            None
1069        };
1070
1071        Ok((zones, verified_bundle))
1072    }
1073
1074    fn check_msg_anchor(&self, msg: &crate::msg::Message) -> Result<RootAnchor, MessageError> {
1075        let height = msg.chain.anchor.height;
1076
1077        if height < self.oldest_anchor {
1078            return Err(MessageError::AnchorStale {
1079                anchor: height,
1080                oldest: self.oldest_anchor,
1081            });
1082        }
1083        if height > self.newest_anchor {
1084            return Err(MessageError::AnchorAhead {
1085                anchor: height,
1086                tip: self.newest_anchor,
1087            });
1088        }
1089
1090        let anchor = self
1091            .find_by_anchor(height)
1092            .ok_or(MessageError::NoAnchorAtHeight { anchor: height })?
1093            .clone();
1094
1095        if msg.chain.anchor.hash != anchor.block.hash {
1096            return Err(MessageError::AnchorHashMismatch {
1097                height,
1098                expected: anchor.block.hash.to_byte_array(),
1099                got: msg.chain.anchor.hash.to_byte_array(),
1100            });
1101        }
1102
1103        Ok(anchor)
1104    }
1105
1106    fn check_msg_chain_proofs(
1107        &self,
1108        msg: &crate::msg::Message,
1109        anchor: &RootAnchor,
1110    ) -> Result<(), MessageError> {
1111        let spaces_root =
1112            msg.chain
1113                .spaces
1114                .compute_root()
1115                .map_err(|_| MessageError::SpacesRootMismatch {
1116                    expected: anchor.spaces_root,
1117                    got: [0u8; 32],
1118                })?;
1119
1120        if spaces_root != anchor.spaces_root {
1121            return Err(MessageError::SpacesRootMismatch {
1122                expected: anchor.spaces_root,
1123                got: spaces_root,
1124            });
1125        }
1126
1127        match anchor.nums_root {
1128            Some(expected) => {
1129                let nums_root =
1130                    msg.chain
1131                        .nums
1132                        .compute_root()
1133                        .map_err(|_| MessageError::NumsRootMismatch {
1134                            expected: Some(expected),
1135                            got: [0u8; 32],
1136                        })?;
1137
1138                if nums_root != expected {
1139                    return Err(MessageError::NumsRootMismatch {
1140                        expected: Some(expected),
1141                        got: nums_root,
1142                    });
1143                }
1144            }
1145            None => {
1146                // No trusted nums root: a non-empty nums proof cannot be
1147                // verified against anything and must not be accepted.
1148                if !msg.chain.nums.0.is_empty() {
1149                    return Err(MessageError::NumsRootMismatch {
1150                        expected: None,
1151                        got: msg.chain.nums.compute_root().unwrap_or([0u8; 32]),
1152                    });
1153                }
1154            }
1155        }
1156
1157        Ok(())
1158    }
1159
1160    fn check_msg_duplicate_spaces(&self, msg: &crate::msg::Message) -> Result<(), MessageError> {
1161        use std::collections::HashSet;
1162        let mut seen: HashSet<&[u8]> = HashSet::new();
1163        for bundle in &msg.spaces {
1164            if !seen.insert(bundle.subject.as_ref()) {
1165                return Err(MessageError::DuplicateSpace {
1166                    space: bundle.subject.to_string(),
1167                });
1168            }
1169        }
1170        Ok(())
1171    }
1172
1173    fn find_by_anchor(&self, anchor: u32) -> Option<&RootAnchor> {
1174        self.anchors.iter().find(|a| a.block.height == anchor)
1175    }
1176
1177    /// Find a root anchor by block height. Returns `None` if absent.
1178    pub fn find_anchor(&self, height: u32) -> Option<&RootAnchor> {
1179        self.find_by_anchor(height)
1180    }
1181
1182    /// Extract parent zone from chain proofs and set sovereignty based on commitment finality.
1183    ///
1184    /// The space's inclusion proof is always required in the chain proof —
1185    /// only the ZK receipt can be substituted by a cached zone.
1186    fn extract_parent_zone(
1187        &self,
1188        chain: &msg::ChainProof,
1189        bundle: &msg::Bundle,
1190        nums_verified: bool,
1191    ) -> Result<Zone, MessageError> {
1192        let mut num_id = None;
1193        let (spk, records) = if !bundle.subject.is_numeric() {
1194            let Some(spaceout) = chain.spaces.find_space(&bundle.subject) else {
1195                return Err(MessageError::SpaceNotFound {
1196                    space: bundle.subject.to_string(),
1197                });
1198            };
1199            let Some(space) = spaceout.space else {
1200                return Err(MessageError::SpaceNotFound {
1201                    space: bundle.subject.to_string(),
1202                });
1203            };
1204            let data = space
1205                .data()
1206                .filter(|d| !d.is_empty())
1207                .map(|d| sip7::RecordSet::new(d.to_vec()))
1208                .unwrap_or_default();
1209            (spaceout.script_pubkey, data)
1210        } else {
1211            if !nums_verified {
1212                return Err(MessageError::NumsRootMissing {
1213                    space: bundle.subject.to_string(),
1214                });
1215            }
1216            let Some(numout) = chain
1217                .nums
1218                .find_numeric(&bundle.subject.clone().try_into().expect("numeric"))
1219                .ok()
1220                .flatten()
1221            else {
1222                return Err(MessageError::NumericNotFound {
1223                    numeric: bundle.subject.to_string(),
1224                });
1225            };
1226            num_id = Some(numout.num.id);
1227            let data = numout
1228                .num
1229                .data
1230                .filter(|d| !d.is_empty())
1231                .map(|d| sip7::RecordSet::new(d.to_vec()))
1232                .unwrap_or_default();
1233            (numout.script_pubkey, data)
1234        };
1235
1236        let handle = SName::from_space(&bundle.subject);
1237
1238        let mut z = Zone {
1239            anchor: chain.anchor.height,
1240            sovereignty: SovereigntyState::Sovereign,
1241            canonical: handle.clone(),
1242            handle,
1243            alias: None,
1244            script_pubkey: spk,
1245            fallback_records: records,
1246            records: sip7::RecordSet::default(),
1247            delegate: ProvableOption::Unknown,
1248            commitment: ProvableOption::Unknown,
1249            num_id,
1250            anchor_hash: [0u8; 32],
1251        };
1252
1253        // Verify records signature if present
1254        if let Some(records) = &bundle.records {
1255            msg::verify_records(records, &z.script_pubkey, &z.canonical).map_err(|e| {
1256                MessageError::RecordsInvalid {
1257                    handle: z.handle.to_string(),
1258                    reason: e.to_string(),
1259                }
1260            })?;
1261            z.records = records.clone();
1262        }
1263
1264        // Without a trusted nums root, delegate and commitment cannot be
1265        // proven present or absent — leave them Unknown.
1266        if !nums_verified {
1267            return Ok(z);
1268        }
1269
1270        // Extract delegate info
1271        if let Ok(delegate) = chain.nums.find_num(&z.script_pubkey) {
1272            match delegate {
1273                None => z.delegate = ProvableOption::Empty,
1274                Some(delegate) => {
1275                    let mut delegate_records = sip7::RecordSet::default();
1276                    if let Some(records) = &bundle.delegate_records {
1277                        msg::verify_records(records, &delegate.script_pubkey, &z.canonical)
1278                            .map_err(|e| MessageError::RecordsInvalid {
1279                                handle: z.handle.to_string(),
1280                                reason: e.to_string(),
1281                            })?;
1282                        delegate_records = records.clone();
1283                    }
1284                    z.delegate = ProvableOption::Exists {
1285                        value: Delegate {
1286                            script_pubkey: delegate.script_pubkey,
1287                            fallback_records: delegate
1288                                .num
1289                                .data
1290                                .filter(|d| !d.is_empty())
1291                                .map(|d| sip7::RecordSet::new(d.to_vec()))
1292                                .unwrap_or_default(),
1293                            records: delegate_records,
1294                        },
1295                    }
1296                }
1297            }
1298        }
1299
1300        // Extract commitment and set sovereignty
1301        if let Ok(root) = chain.nums.get_latest_commitment_root(&bundle.subject) {
1302            match root {
1303                None => z.commitment = ProvableOption::Empty,
1304                Some(root) => {
1305                    let commitment = chain.nums.find_commitment(&bundle.subject, root);
1306                    if let Ok(Some(commitment)) = commitment {
1307                        z.commitment = ProvableOption::Exists {
1308                            value: CommitmentInfo {
1309                                onchain: commitment,
1310                                receipt_hash: None,
1311                            },
1312                        };
1313                    }
1314                }
1315            }
1316        }
1317
1318        Ok(z)
1319    }
1320}
1321
1322/// Verify a temporary handle certificate (exclusion proof + signature).
1323fn verify_temporary_handle(
1324    anchor_height: u32,
1325    handle: &msg::Handle,
1326    subject: &SName,
1327    epoch_tree: &cert::HandleSubtree,
1328    parent_zone: &Zone,
1329) -> Result<Zone, MessageError> {
1330    // Exclusion: the name must be provably absent. A leaf bound to a
1331    // different genesis key still means the name is taken.
1332    let exists =
1333        epoch_tree
1334            .contains_name(&handle.name)
1335            .map_err(|e| MessageError::HandleProofMalformed {
1336                handle: subject.to_string(),
1337                reason: e.to_string(),
1338            })?;
1339
1340    if exists {
1341        return Err(MessageError::HandleAlreadyExists {
1342            handle: subject.to_string(),
1343        });
1344    }
1345
1346    let signer = match &parent_zone.delegate {
1347        ProvableOption::Exists { value: delegate } => &delegate.script_pubkey,
1348        ProvableOption::Empty => &parent_zone.script_pubkey,
1349        ProvableOption::Unknown => {
1350            return Err(MessageError::ParentDelegateUnknown {
1351                handle: subject.to_string(),
1352            });
1353        }
1354    };
1355
1356    let mut verified_records = sip7::RecordSet::default();
1357    if let Some(records) = &handle.records {
1358        msg::verify_records(records, &handle.genesis_spk, subject).map_err(|e| {
1359            MessageError::RecordsInvalid {
1360                handle: subject.to_string(),
1361                reason: e.to_string(),
1362            }
1363        })?;
1364        verified_records = records.clone();
1365    }
1366
1367    let num_id = Some(NumId::from_spk::<KeyHash>(handle.genesis_spk.clone()));
1368    let zone = Zone {
1369        anchor: anchor_height,
1370        sovereignty: SovereigntyState::Dependent,
1371        canonical: subject.clone(),
1372        handle: subject.clone(),
1373        alias: None,
1374        script_pubkey: handle.genesis_spk.clone(),
1375        fallback_records: sip7::RecordSet::default(),
1376        records: verified_records,
1377        delegate: ProvableOption::Unknown,
1378        commitment: ProvableOption::Unknown,
1379        num_id,
1380        anchor_hash: [0u8; 32],
1381    };
1382
1383    zone.verify_signature(handle.signature.as_ref().unwrap(), signer)
1384        .map_err(|e| MessageError::SignatureInvalid {
1385            handle: zone.handle.to_string(),
1386            reason: e.to_string(),
1387        })?;
1388
1389    Ok(zone)
1390}
1391
1392/// Verify a final handle certificate (inclusion proof + key rotation).
1393fn verify_final_handle(
1394    anchor_height: u32,
1395    handle: &msg::Handle,
1396    subject: &SName,
1397    epoch_tree: &cert::HandleSubtree,
1398    nums: &cert::NumsSubtree,
1399    sovereignty: SovereigntyState,
1400) -> Result<Zone, MessageError> {
1401    if epoch_tree.0.is_empty() {
1402        return Err(MessageError::FinalCertRequiresTree {
1403            handle: subject.to_string(),
1404        });
1405    }
1406
1407    let included = epoch_tree
1408        .contains_subspace(&handle.name, &handle.genesis_spk)
1409        .map_err(|e| MessageError::HandleProofMalformed {
1410            handle: subject.to_string(),
1411            reason: e.to_string(),
1412        })?;
1413
1414    if !included {
1415        return Err(MessageError::HandleNotFound {
1416            handle: subject.to_string(),
1417        });
1418    }
1419
1420    // Key rotation lookup
1421    let numout =
1422        nums.find_num(&handle.genesis_spk)
1423            .map_err(|e| MessageError::NumsProofMalformed {
1424                reason: e.to_string(),
1425            })?;
1426
1427    let (num_id, spk, onchain_data, alias) = match numout {
1428        Some(numout) => (
1429            numout.num.id,
1430            numout.script_pubkey,
1431            numout
1432                .num
1433                .data
1434                .filter(|d| !d.is_empty())
1435                .map(|d| sip7::RecordSet::new(d.to_vec()))
1436                .unwrap_or_default(),
1437            Some(numout.num.name.to_slabel()),
1438        ),
1439        None => (
1440            NumId::from_spk::<KeyHash>(handle.genesis_spk.clone()),
1441            handle.genesis_spk.clone(),
1442            sip7::RecordSet::default(),
1443            None,
1444        ),
1445    };
1446
1447    let mut verified_records = sip7::RecordSet::default();
1448    if let Some(records) = &handle.records {
1449        msg::verify_records(records, &spk, subject).map_err(|e| MessageError::RecordsInvalid {
1450            handle: subject.to_string(),
1451            reason: e.to_string(),
1452        })?;
1453        verified_records = records.clone();
1454    }
1455
1456    let zone = Zone {
1457        anchor: anchor_height,
1458        sovereignty,
1459        canonical: subject.clone(),
1460        handle: subject.clone(),
1461        alias,
1462        script_pubkey: spk,
1463        fallback_records: onchain_data,
1464        records: verified_records,
1465        delegate: ProvableOption::Unknown,
1466        commitment: ProvableOption::Unknown,
1467        num_id: Some(num_id),
1468        anchor_hash: [0u8; 32],
1469    };
1470
1471    Ok(zone)
1472}
1473
1474/// Error during message verification.
1475#[derive(Debug, Clone)]
1476pub enum MessageError {
1477    /// Message anchor is too old
1478    AnchorStale { anchor: u32, oldest: u32 },
1479    /// Message anchor is newer than our tip
1480    AnchorAhead { anchor: u32, tip: u32 },
1481    /// No anchor exists at this height
1482    NoAnchorAtHeight { anchor: u32 },
1483    /// Anchor hash doesn't match our known anchor at this height
1484    AnchorHashMismatch {
1485        height: u32,
1486        expected: Hash,
1487        got: Hash,
1488    },
1489    /// Duplicate space in message bundles
1490    DuplicateSpace { space: String },
1491    /// Receipt journal could not be decoded
1492    MalformedReceipt { space: String, reason: String },
1493    /// Receipt policy IDs don't match expected values
1494    ReceiptPolicyMismatch { space: String },
1495    /// Spaces proof root doesn't match anchor
1496    SpacesRootMismatch { expected: Hash, got: Hash },
1497    /// Nums proof root doesn't match anchor
1498    NumsRootMismatch { expected: Option<Hash>, got: Hash },
1499    /// Anchor has no nums root, so nums-dependent data cannot be verified
1500    NumsRootMissing { space: String },
1501    /// Space not found in spaces proof
1502    SpaceNotFound { space: String },
1503    /// Numeric space not found in nums proof
1504    NumericNotFound { numeric: String },
1505    /// Commitment not found in nums proof
1506    CommitmentNotFound { space: String, root: Hash },
1507    /// Receipt required but not provided
1508    ReceiptRequired { space: String },
1509    /// Handle subtree proof is malformed
1510    HandleProofMalformed { handle: String, reason: String },
1511    /// Duplicate epoch root in bundle
1512    DuplicateEpoch { space: String, root: Hash },
1513    /// Epoch's commitment height exceeds the verified tip
1514    EpochExceedsTip { space: String },
1515    /// Subject name is invalid
1516    InvalidSubject { subject: String },
1517    /// Temporary certificate must prove against the tip state
1518    TemporaryRequiresTip {
1519        handle: String,
1520        tip: Hash,
1521        got: Hash,
1522    },
1523    /// Handle already exists when exclusion proof expected
1524    HandleAlreadyExists { handle: String },
1525    /// Parent delegate is unknown, cannot verify signature
1526    ParentDelegateUnknown { handle: String },
1527    /// Signature verification failed
1528    SignatureInvalid { handle: String, reason: String },
1529    /// Offchain data signature verification failed
1530    RecordsInvalid { handle: String, reason: String },
1531    /// Final certificate requires non-empty handle tree
1532    FinalCertRequiresTree { handle: String },
1533    /// Handle not found in handle tree
1534    HandleNotFound { handle: String },
1535    /// Nums proof is malformed
1536    NumsProofMalformed { reason: String },
1537    /// ZK receipt verification failed
1538    ReceiptInvalid { space: String, reason: String },
1539    /// On-chain commitment doesn't match receipt
1540    CommitmentReceiptMismatch { space: String, field: &'static str },
1541}
1542
1543impl fmt::Display for MessageError {
1544    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1545        match self {
1546            Self::AnchorStale { anchor, oldest } => {
1547                write!(f, "anchor {} is stale, oldest is {}", anchor, oldest)
1548            }
1549            Self::AnchorAhead { anchor, tip } => {
1550                write!(f, "anchor {} is ahead of tip {}", anchor, tip)
1551            }
1552            Self::NoAnchorAtHeight { anchor } => {
1553                write!(f, "no anchor at height {}", anchor)
1554            }
1555            Self::AnchorHashMismatch {
1556                height,
1557                expected,
1558                got,
1559            } => {
1560                write!(
1561                    f,
1562                    "anchor hash mismatch at {}: expected {}, got {}",
1563                    height,
1564                    hex::encode(expected),
1565                    hex::encode(got)
1566                )
1567            }
1568            Self::DuplicateSpace { space } => {
1569                write!(f, "duplicate space in message: {}", space)
1570            }
1571            Self::MalformedReceipt { space, reason } => {
1572                write!(f, "malformed receipt for {}: {}", space, reason)
1573            }
1574            Self::ReceiptPolicyMismatch { space } => {
1575                write!(f, "receipt policy mismatch for {}", space)
1576            }
1577            Self::SpacesRootMismatch { expected, got } => {
1578                write!(
1579                    f,
1580                    "spaces root mismatch: expected {}, got {}",
1581                    hex::encode(expected),
1582                    hex::encode(got)
1583                )
1584            }
1585            Self::NumsRootMismatch { expected, got } => {
1586                write!(
1587                    f,
1588                    "nums root mismatch: expected {}, got {}",
1589                    expected.map(hex::encode).unwrap_or_else(|| "none".into()),
1590                    hex::encode(got)
1591                )
1592            }
1593            Self::NumsRootMissing { space } => {
1594                write!(
1595                    f,
1596                    "anchor has no nums root, cannot verify nums data for {}",
1597                    space
1598                )
1599            }
1600            Self::SpaceNotFound { space } => {
1601                write!(f, "space {} not found in proof", space)
1602            }
1603            Self::NumericNotFound { numeric } => {
1604                write!(f, "numeric space {} not found in proof", numeric)
1605            }
1606            Self::CommitmentNotFound { space, root } => {
1607                write!(
1608                    f,
1609                    "commitment {} not found for {}",
1610                    hex::encode(root),
1611                    space
1612                )
1613            }
1614            Self::ReceiptRequired { space } => {
1615                write!(f, "receipt required for {}", space)
1616            }
1617            Self::HandleProofMalformed { handle, reason } => {
1618                write!(f, "handle proof malformed for {}: {}", handle, reason)
1619            }
1620            Self::DuplicateEpoch { space, root } => {
1621                write!(f, "duplicate epoch {} for {}", hex::encode(root), space)
1622            }
1623            Self::EpochExceedsTip { space } => {
1624                write!(f, "epoch commitment exceeds tip for {}", space)
1625            }
1626            Self::InvalidSubject { subject } => {
1627                write!(f, "invalid subject: {}", subject)
1628            }
1629            Self::TemporaryRequiresTip { handle, tip, got } => {
1630                write!(
1631                    f,
1632                    "Temporary handle {} verifies against {} but requires tip {}",
1633                    handle,
1634                    hex::encode(got),
1635                    hex::encode(tip)
1636                )
1637            }
1638            Self::HandleAlreadyExists { handle } => {
1639                write!(f, "handle {} already exists", handle)
1640            }
1641            Self::ParentDelegateUnknown { handle } => {
1642                write!(f, "parent delegate unknown for {}", handle)
1643            }
1644            Self::SignatureInvalid { handle, reason } => {
1645                write!(f, "signature invalid for {}: {}", handle, reason)
1646            }
1647            Self::RecordsInvalid { handle, reason } => {
1648                write!(f, "records invalid for {}: {}", handle, reason)
1649            }
1650            Self::FinalCertRequiresTree { handle } => {
1651                write!(
1652                    f,
1653                    "final certificate requires non-empty tree for {}",
1654                    handle
1655                )
1656            }
1657            Self::HandleNotFound { handle } => {
1658                write!(f, "handle {} not found", handle)
1659            }
1660            Self::NumsProofMalformed { reason } => {
1661                write!(f, "nums proof malformed: {}", reason)
1662            }
1663            Self::ReceiptInvalid { space, reason } => {
1664                write!(f, "receipt invalid for {}: {}", space, reason)
1665            }
1666            Self::CommitmentReceiptMismatch { space, field } => {
1667                write!(f, "commitment {} mismatch for {}", field, space)
1668            }
1669        }
1670    }
1671}
1672
1673impl std::error::Error for MessageError {}
1674
1675/// Push the better zone: if cached exists and is better, push cached; otherwise push the new zone.
1676fn push_best_zone(ctx: &msg::QueryContext, zones: &mut Vec<Zone>, zone: Zone) {
1677    let Some(cached) = ctx.get_zone(&zone.canonical) else {
1678        zones.push(zone);
1679        return;
1680    };
1681    if !zone.is_better_than(cached).unwrap_or(false) {
1682        zones.push(cached.clone());
1683        return;
1684    }
1685    zones.push(zone);
1686}
1687
1688/// Verify ZK receipt if the zone requires one.
1689/// Returns true if receipt was verified, false if not needed.
1690fn maybe_verify_receipt(
1691    zone: &mut Zone,
1692    receipt: Option<&risc0_zkvm::Receipt>,
1693    space: &SLabel,
1694    options: u32,
1695) -> Result<bool, MessageError> {
1696    let Some(ci) = zone.requires_receipt() else {
1697        return Ok(false);
1698    };
1699    let receipt = receipt.ok_or_else(|| MessageError::ReceiptRequired {
1700        space: space.to_string(),
1701    })?;
1702    verify_receipt(ci, space, receipt, options)?;
1703    Ok(true)
1704}
1705
1706/// Decode a receipt journal without verification.
1707fn decode_journal(
1708    receipt: &risc0_zkvm::Receipt,
1709    space: &SLabel,
1710) -> Result<libveritas_zk::guest::Commitment, MessageError> {
1711    receipt
1712        .journal
1713        .decode()
1714        .map_err(|e| MessageError::MalformedReceipt {
1715            space: space.to_string(),
1716            reason: e.to_string(),
1717        })
1718}
1719
1720pub(crate) fn serialize_hash<S>(hash: &Hash, serializer: S) -> Result<S::Ok, S::Error>
1721where
1722    S: Serializer,
1723{
1724    if serializer.is_human_readable() {
1725        serializer.serialize_str(&hex::encode(hash))
1726    } else {
1727        serializer.serialize_bytes(hash)
1728    }
1729}
1730
1731pub(crate) fn deserialize_hash<'de, D>(deserializer: D) -> Result<Hash, D::Error>
1732where
1733    D: Deserializer<'de>,
1734{
1735    if deserializer.is_human_readable() {
1736        let s: String = <String as Deserialize>::deserialize(deserializer)?;
1737        let mut bytes = [0u8; 32];
1738        hex::decode_to_slice(&s, &mut bytes).map_err(serde::de::Error::custom)?;
1739        Ok(bytes)
1740    } else {
1741        <[u8; 32] as Deserialize>::deserialize(deserializer)
1742    }
1743}
1744
1745pub(crate) fn serialize_option_hash<S>(
1746    hash: &Option<Hash>,
1747    serializer: S,
1748) -> Result<S::Ok, S::Error>
1749where
1750    S: Serializer,
1751{
1752    match hash {
1753        Some(bytes) => {
1754            if serializer.is_human_readable() {
1755                serializer.serialize_some(&hex::encode(bytes))
1756            } else {
1757                serializer.serialize_some(bytes)
1758            }
1759        }
1760        None => serializer.serialize_none(),
1761    }
1762}
1763
1764pub(crate) fn deserialize_option_hash<'de, D>(deserializer: D) -> Result<Option<Hash>, D::Error>
1765where
1766    D: Deserializer<'de>,
1767{
1768    if deserializer.is_human_readable() {
1769        let opt: Option<String> = <Option<String> as Deserialize>::deserialize(deserializer)?;
1770        match opt {
1771            None => Ok(None),
1772            Some(s) => {
1773                let mut bytes = [0u8; 32];
1774                hex::decode_to_slice(&s, &mut bytes).map_err(serde::de::Error::custom)?;
1775                Ok(Some(bytes))
1776            }
1777        }
1778    } else {
1779        let opt: Option<[u8; 32]> = <Option<[u8; 32]> as Deserialize>::deserialize(deserializer)?;
1780        Ok(opt)
1781    }
1782}
1783
1784fn verify_zk_journal_matches_onchain(
1785    space: &SLabel,
1786    zk: &libveritas_zk::guest::Commitment,
1787    onchain: &spaces_nums::Commitment,
1788) -> Result<(), MessageError> {
1789    let space_str = space.to_string();
1790    if zk.policy_fold != constants::FOLD_ID || zk.policy_step != constants::STEP_ID {
1791        return Err(MessageError::ReceiptPolicyMismatch { space: space_str });
1792    }
1793    if zk.final_root != onchain.state_root {
1794        return Err(MessageError::CommitmentReceiptMismatch {
1795            space: space_str.clone(),
1796            field: "state_root",
1797        });
1798    }
1799    if zk.rolling_hash != onchain.rolling_hash {
1800        return Err(MessageError::CommitmentReceiptMismatch {
1801            space: space_str,
1802            field: "rolling_hash",
1803        });
1804    }
1805    Ok(())
1806}
1807
1808fn hash_receipt(receipt: &Receipt) -> Hash {
1809    Sha256Hasher::hash(&borsh::to_vec(receipt).expect("receipt serialization should not fail"))
1810}