Skip to main content

taproot_assets_core/verify/
proof.rs

1//! Proof-level verification helpers.
2
3use alloc::collections::{BTreeMap, BTreeSet};
4use alloc::string::String;
5use alloc::vec::Vec;
6
7use bitcoin::consensus::encode::serialize;
8use bitcoin::hashes::{Hash, HashEngine, sha256::Hash as Sha256Hash};
9use bitcoin::secp256k1::{Scalar, Secp256k1, XOnlyPublicKey};
10use bitcoin::taproot::TapTweakHash;
11use bitcoin::{OutPoint, Transaction, Txid};
12use serde::{Deserialize, Serialize};
13use taproot_assets_types::asset::{
14    Asset, AssetType, AssetVersion, GenesisInfo, GenesisReveal, GroupKeyReveal, PrevId,
15    PrevWitness, ScriptKeyType, SerializedKey,
16};
17use taproot_assets_types::commitment::TapCommitmentVersion;
18use taproot_assets_types::proof::{CommitmentProof, MetaReveal, Proof, TaprootProof};
19
20use crate::TaprootOps;
21use crate::verify::{group_key_reveal, taproot_proof};
22
23/// Transition version that enables STXO proofs.
24const PROOF_VERSION_V1: u32 = 1;
25/// Length in bytes of a compressed public key.
26const COMPRESSED_KEY_LEN: usize = 33;
27/// NUMS key used for burn key derivation.
28const NUMS_COMPRESSED_KEY: [u8; COMPRESSED_KEY_LEN] = [
29    0x02, 0x7c, 0x79, 0xb9, 0xb2, 0x6e, 0x46, 0x38, 0x95, 0xee, 0xf5, 0x67, 0x9d, 0x85, 0x58, 0x94,
30    0x2c, 0x86, 0xc4, 0xad, 0x22, 0x33, 0xad, 0xef, 0x01, 0xbc, 0x3e, 0x6d, 0x54, 0x0b, 0x36, 0x53,
31    0xfe,
32];
33/// TLV type for the meta reveal encoding field.
34const META_REVEAL_ENCODING_TYPE: u64 = 0;
35/// TLV type for the meta reveal data field.
36const META_REVEAL_DATA_TYPE: u64 = 2;
37
38/// Map of output indexes to the STXO script keys they must prove.
39type P2TROutputsSTXOs = BTreeMap<u32, BTreeSet<SerializedKey>>;
40/// TapCommitment versions observed per output.
41type CommittedVersions = BTreeMap<u32, Vec<TapCommitmentVersion>>;
42
43/// Minimal witness data needed to determine genesis status.
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
45pub struct GenesisWitnessInput {
46    /// Previous input reference for the witness.
47    pub prev_id: Option<PrevId>,
48    /// Whether the witness includes any transaction witness data.
49    pub has_tx_witness: bool,
50    /// Whether the witness includes a split commitment.
51    pub has_split_commitment: bool,
52}
53
54/// Minimal asset data required for genesis reveal verification.
55#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
56pub struct GenesisAssetInput {
57    /// Asset genesis identifier, if present.
58    pub asset_genesis_id: Option<Sha256Hash>,
59    /// Whether the asset declares membership in an asset group.
60    pub has_asset_group: bool,
61    /// Previous witnesses used to determine genesis status.
62    pub prev_witnesses: Vec<GenesisWitnessInput>,
63}
64
65/// Minimal input required to verify genesis and meta reveal constraints.
66#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
67pub struct GenesisRevealInput {
68    /// Previous outpoint referenced by the proof.
69    pub prev_out: OutPoint,
70    /// Output index from the inclusion proof.
71    pub inclusion_output_index: u32,
72    /// Genesis reveal payload, if present.
73    pub genesis_reveal: Option<GenesisReveal>,
74    /// Meta reveal payload, if present.
75    pub meta_reveal: Option<MetaReveal>,
76    /// Asset fields required for genesis verification.
77    pub asset: GenesisAssetInput,
78}
79
80impl GenesisRevealInput {
81    /// Builds a genesis reveal input from a full proof.
82    pub fn from_proof(proof: &Proof) -> Self {
83        GenesisRevealInput {
84            prev_out: proof.prev_out,
85            inclusion_output_index: proof.inclusion_proof.output_index,
86            genesis_reveal: proof.genesis_reveal.clone(),
87            meta_reveal: proof.meta_reveal.clone(),
88            asset: genesis_asset_input_from_asset(&proof.asset),
89        }
90    }
91}
92
93fn genesis_asset_input_from_asset(asset: &Asset) -> GenesisAssetInput {
94    let prev_witnesses = asset
95        .prev_witnesses
96        .iter()
97        .map(|witness| GenesisWitnessInput {
98            prev_id: witness.prev_id.clone(),
99            has_tx_witness: !witness.tx_witness.is_empty(),
100            has_split_commitment: witness.split_commitment.is_some(),
101        })
102        .collect();
103
104    GenesisAssetInput {
105        asset_genesis_id: asset.asset_genesis.as_ref().map(|genesis| genesis.asset_id),
106        has_asset_group: asset.asset_group.is_some(),
107        prev_witnesses,
108    }
109}
110
111/// SHA-256 hashing interface used by proof verification.
112pub trait Sha256Hasher {
113    /// Returns the SHA-256 digest of the provided bytes.
114    fn hash(&self, data: &[u8]) -> [u8; 32];
115}
116
117/// Default SHA-256 hasher backed by `bitcoin::hashes`.
118#[derive(Debug, Clone, Copy, Default)]
119pub struct BitcoinSha256Hasher;
120
121impl Sha256Hasher for BitcoinSha256Hasher {
122    /// Hashes the input using `bitcoin::hashes::sha256`.
123    fn hash(&self, data: &[u8]) -> [u8; 32] {
124        Sha256Hash::hash(data).to_byte_array()
125    }
126}
127
128/// Proof verification stage used for error reporting.
129#[derive(Debug, Clone, Copy, PartialEq, Eq)]
130pub enum ProofStage {
131    /// Inclusion proof verification stage.
132    Inclusion,
133    /// Exclusion proof verification stage.
134    Exclusion,
135    /// Split root proof verification stage.
136    SplitRoot,
137    /// STXO proof verification stage.
138    Stxo,
139}
140
141impl core::fmt::Display for ProofStage {
142    /// Formats the stage for display.
143    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
144        match self {
145            ProofStage::Inclusion => write!(f, "inclusion"),
146            ProofStage::Exclusion => write!(f, "exclusion"),
147            ProofStage::SplitRoot => write!(f, "split_root"),
148            ProofStage::Stxo => write!(f, "stxo"),
149        }
150    }
151}
152
153/// Errors returned by proof verification helpers.
154#[derive(Debug, Clone, Copy, PartialEq, Eq)]
155pub enum Error {
156    /// Taproot proof verification failed at a specific stage.
157    TaprootProof {
158        /// Stage where verification failed.
159        stage: ProofStage,
160        /// Underlying taproot proof error.
161        source: taproot_proof::Error,
162    },
163    /// Group key reveal verification failed.
164    GroupKeyReveal(group_key_reveal::Error),
165    /// Split root proof is missing for a split commitment asset.
166    MissingSplitRootProof,
167    /// A transfer-root asset is missing required STXO proofs.
168    MissingStxoProofs,
169    /// A transfer-root asset is missing STXO proofs for an output.
170    MissingStxoInputProofs,
171    /// Missing commitment proof required to verify STXO proofs.
172    MissingCommitmentProof,
173    /// Missing STXO asset for a script key in the proof.
174    MissingStxoAsset {
175        /// Script key that lacks a corresponding STXO asset.
176        key: SerializedKey,
177    },
178    /// The asset has no witnesses when STXO proofs are required.
179    MissingAssetWitnesses,
180    /// A witness is missing its PrevID.
181    MissingPrevId,
182    /// Script key length is invalid for an asset.
183    InvalidAssetScriptKeyLength {
184        /// Expected length in bytes.
185        expected: usize,
186        /// Actual length in bytes.
187        actual: usize,
188    },
189    /// The NUMS key used for burn derivation is invalid.
190    InvalidNumsKey,
191    /// Tap tweak scalar for burn key derivation is invalid.
192    InvalidBurnKeyTweak,
193    /// Mixed TapCommitment versions found across proofs.
194    MixedCommitmentVersions,
195    /// Commitment proofs are missing for taproot outputs.
196    InvalidCommitmentProof,
197    /// Genesis reveal is present for a non-genesis asset.
198    NonGenesisAssetWithGenesisReveal,
199    /// Meta reveal is present for a non-genesis asset.
200    NonGenesisAssetWithMetaReveal,
201    /// Genesis reveal is required for a genesis asset.
202    GenesisRevealRequired,
203    /// Genesis reveal is missing its base genesis information.
204    GenesisRevealMissingBase,
205    /// Genesis reveal prev out does not match the proof prev out.
206    GenesisRevealPrevOutMismatch,
207    /// Genesis reveal requires a meta reveal when meta hash is non-zero.
208    GenesisRevealMetaRevealRequired,
209    /// Genesis reveal meta hash does not match the meta reveal hash.
210    GenesisRevealMetaHashMismatch,
211    /// Genesis reveal output index does not match the inclusion proof.
212    GenesisRevealOutputIndexMismatch,
213    /// Genesis reveal asset ID does not match the asset genesis.
214    GenesisRevealAssetIdMismatch,
215    /// Asset genesis information is missing.
216    MissingAssetGenesis,
217}
218
219impl core::fmt::Display for Error {
220    /// Formats the error for display.
221    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
222        match self {
223            Error::TaprootProof { stage, source } => {
224                write!(f, "taproot proof {} error: {}", stage, source)
225            }
226            Error::GroupKeyReveal(err) => core::fmt::Display::fmt(err, f),
227            Error::MissingSplitRootProof => write!(f, "missing split root proof"),
228            Error::MissingStxoProofs => write!(f, "missing STXO proofs"),
229            Error::MissingStxoInputProofs => write!(f, "missing STXO input proofs"),
230            Error::MissingCommitmentProof => write!(f, "missing commitment proof"),
231            Error::MissingStxoAsset { key } => {
232                write!(f, "missing STXO asset for key {:?}", key)
233            }
234            Error::MissingAssetWitnesses => write!(f, "asset has no witnesses"),
235            Error::MissingPrevId => write!(f, "witness missing PrevID"),
236            Error::InvalidAssetScriptKeyLength { expected, actual } => write!(
237                f,
238                "asset script key length {}, expected {}",
239                actual, expected
240            ),
241            Error::InvalidNumsKey => write!(f, "invalid NUMS key"),
242            Error::InvalidBurnKeyTweak => write!(f, "invalid burn key tweak"),
243            Error::MixedCommitmentVersions => write!(f, "mixed commitment versions"),
244            Error::InvalidCommitmentProof => write!(f, "invalid commitment proof"),
245            Error::NonGenesisAssetWithGenesisReveal => {
246                write!(f, "non-genesis asset with genesis reveal")
247            }
248            Error::NonGenesisAssetWithMetaReveal => write!(f, "non-genesis asset with meta reveal"),
249            Error::GenesisRevealRequired => write!(f, "genesis reveal required"),
250            Error::GenesisRevealMissingBase => write!(f, "genesis reveal missing base"),
251            Error::GenesisRevealPrevOutMismatch => write!(f, "genesis reveal prev out mismatch"),
252            Error::GenesisRevealMetaRevealRequired => {
253                write!(f, "genesis reveal meta reveal required")
254            }
255            Error::GenesisRevealMetaHashMismatch => {
256                write!(f, "genesis reveal meta hash mismatch")
257            }
258            Error::GenesisRevealOutputIndexMismatch => {
259                write!(f, "genesis reveal output index mismatch")
260            }
261            Error::GenesisRevealAssetIdMismatch => write!(f, "genesis reveal asset id mismatch"),
262            Error::MissingAssetGenesis => write!(f, "missing asset genesis"),
263        }
264    }
265}
266
267/// Verifies genesis and meta reveal constraints for a proof.
268pub fn verify_genesis_reveal(proof: &Proof) -> Result<(), Error> {
269    verify_genesis_reveal_input(&GenesisRevealInput::from_proof(proof))
270}
271
272/// Verifies genesis and meta reveal constraints using the supplied SHA-256 hasher.
273pub fn verify_genesis_reveal_with_hasher<H: Sha256Hasher>(
274    proof: &Proof,
275    hasher: &H,
276) -> Result<(), Error> {
277    verify_genesis_reveal_input_with_hasher(&GenesisRevealInput::from_proof(proof), hasher)
278}
279
280/// Verifies genesis and meta reveal constraints for a minimal input.
281pub fn verify_genesis_reveal_input(input: &GenesisRevealInput) -> Result<(), Error> {
282    verify_genesis_reveal_input_with_hasher(input, &BitcoinSha256Hasher)
283}
284
285/// Verifies genesis and meta reveal constraints for a minimal input using the supplied hasher.
286pub fn verify_genesis_reveal_input_with_hasher<H: Sha256Hasher>(
287    input: &GenesisRevealInput,
288    hasher: &H,
289) -> Result<(), Error> {
290    let is_genesis = is_genesis_asset_input(&input.asset);
291    let has_genesis_reveal = input.genesis_reveal.is_some();
292    let has_meta_reveal = input.meta_reveal.is_some();
293
294    if !is_genesis {
295        if has_genesis_reveal {
296            return Err(Error::NonGenesisAssetWithGenesisReveal);
297        }
298        if has_meta_reveal {
299            return Err(Error::NonGenesisAssetWithMetaReveal);
300        }
301        return Ok(());
302    }
303
304    if !has_genesis_reveal {
305        return Err(Error::GenesisRevealRequired);
306    }
307
308    verify_genesis_reveal_fields_input(input, hasher)
309}
310
311/// Verifies that the genesis reveal matches input and asset fields.
312fn verify_genesis_reveal_fields_input<H: Sha256Hasher>(
313    input: &GenesisRevealInput,
314    hasher: &H,
315) -> Result<(), Error> {
316    let reveal = input
317        .genesis_reveal
318        .as_ref()
319        .ok_or(Error::GenesisRevealRequired)?;
320    let genesis = reveal
321        .genesis_base
322        .as_ref()
323        .ok_or(Error::GenesisRevealMissingBase)?;
324
325    if genesis.genesis_point != input.prev_out {
326        return Err(Error::GenesisRevealPrevOutMismatch);
327    }
328
329    if genesis.output_index != input.inclusion_output_index {
330        return Err(Error::GenesisRevealOutputIndexMismatch);
331    }
332
333    let zero_meta = zero_meta_hash();
334    match input.meta_reveal.as_ref() {
335        None => {
336            if genesis.meta_hash != zero_meta {
337                return Err(Error::GenesisRevealMetaRevealRequired);
338            }
339        }
340        Some(meta) => {
341            let meta_hash = meta_reveal_hash_with_hasher(meta, hasher);
342            if genesis.meta_hash != meta_hash {
343                return Err(Error::GenesisRevealMetaHashMismatch);
344            }
345        }
346    }
347
348    let asset_genesis_id = input
349        .asset
350        .asset_genesis_id
351        .ok_or(Error::MissingAssetGenesis)?;
352    let reveal_asset_id = compute_asset_id_with_hasher(genesis, hasher);
353    if reveal_asset_id != asset_genesis_id {
354        return Err(Error::GenesisRevealAssetIdMismatch);
355    }
356
357    Ok(())
358}
359
360/// Verifies inclusion, split root, and exclusion proofs for a state transition proof.
361pub fn verify_proofs<O: TaprootOps>(
362    ops: &O,
363    proof: &Proof,
364) -> Result<taproot_proof::TapCommitment, Error> {
365    let tap_commitment = verify_inclusion_proof(ops, proof)?;
366
367    if has_split_commitment_witness(&proof.asset) {
368        if proof.split_root_proof.is_none() {
369            return Err(Error::MissingSplitRootProof);
370        }
371        verify_split_root_proof(ops, proof)?;
372    }
373
374    let exclusion_version = verify_exclusion_proofs(ops, proof)?;
375    if let Some(version) = exclusion_version {
376        if !is_similar_tap_commitment_version(tap_commitment.version, version) {
377            return Err(Error::MixedCommitmentVersions);
378        }
379    }
380
381    Ok(tap_commitment)
382}
383
384/// Verifies the inclusion proof for the resulting asset.
385pub fn verify_inclusion_proof<O: TaprootOps>(
386    ops: &O,
387    proof: &Proof,
388) -> Result<taproot_proof::TapCommitment, Error> {
389    let commitment = taproot_proof::verify_taproot_proof_with_commitment(
390        ops,
391        &proof.anchor_tx,
392        &proof.inclusion_proof,
393        &proof.asset,
394        true,
395    )
396    .map_err(|err| Error::TaprootProof {
397        stage: ProofStage::Inclusion,
398        source: err,
399    })?
400    .ok_or(Error::MissingCommitmentProof)?;
401
402    let need_stxo_proofs = is_version_v1(proof.version) && is_transfer_root(&proof.asset);
403    let has_stxo_proofs = proof
404        .inclusion_proof
405        .commitment_proof
406        .as_ref()
407        .map_or(false, |commitment| !commitment.stxo_proofs.is_empty());
408
409    if need_stxo_proofs && !has_stxo_proofs {
410        return Err(Error::MissingStxoProofs);
411    }
412
413    if !is_transfer_root(&proof.asset) || !has_stxo_proofs {
414        return Ok(commitment);
415    }
416
417    let out_idx = proof.inclusion_proof.output_index;
418    let (asset_map, stxo_keys) = collect_stxo_assets(&proof.asset)?;
419    let mut p2tr_outputs = BTreeMap::new();
420    p2tr_outputs.insert(out_idx, stxo_keys);
421
422    verify_stxo_proof_set(
423        ops,
424        &proof.anchor_tx,
425        &proof.inclusion_proof,
426        &asset_map,
427        &mut p2tr_outputs,
428        true,
429    )?;
430
431    if !p2tr_outputs.is_empty() {
432        return Err(Error::MissingStxoInputProofs);
433    }
434
435    Ok(commitment)
436}
437
438/// Verifies the split root proof for split commitment assets.
439pub fn verify_split_root_proof<O: TaprootOps>(ops: &O, proof: &Proof) -> Result<(), Error> {
440    let split_proof = proof
441        .split_root_proof
442        .as_ref()
443        .ok_or(Error::MissingSplitRootProof)?;
444    let root_asset = split_root_asset(&proof.asset)?;
445    taproot_proof::verify_taproot_proof(ops, &proof.anchor_tx, split_proof, root_asset, true)
446        .map_err(|err| Error::TaprootProof {
447            stage: ProofStage::SplitRoot,
448            source: err,
449        })
450}
451
452/// Verifies the exclusion proofs and returns the observed TapCommitment version.
453pub fn verify_exclusion_proofs<O: TaprootOps>(
454    ops: &O,
455    proof: &Proof,
456) -> Result<Option<TapCommitmentVersion>, Error> {
457    let mut p2tr_outputs = BTreeSet::new();
458    for (idx, output) in proof.anchor_tx.output.iter().enumerate() {
459        let index = idx as u32;
460        if index == proof.inclusion_proof.output_index {
461            continue;
462        }
463        if output.script_pubkey.is_p2tr() {
464            p2tr_outputs.insert(index);
465        }
466    }
467
468    if p2tr_outputs.is_empty() {
469        return Ok(None);
470    }
471
472    let mut outputs_for_v0 = p2tr_outputs.clone();
473    let commit_versions = verify_v0_exclusion_proofs(ops, proof, &mut outputs_for_v0)?;
474    if commit_versions.is_empty() {
475        return Ok(None);
476    }
477
478    let need_stxo_proofs = is_version_v1(proof.version) && is_transfer_root(&proof.asset);
479    let has_stxo_proofs = proof
480        .exclusion_proofs
481        .first()
482        .and_then(|proof| proof.commitment_proof.as_ref())
483        .map_or(false, |commitment| !commitment.stxo_proofs.is_empty());
484
485    if need_stxo_proofs && !has_stxo_proofs {
486        return Err(Error::MissingStxoProofs);
487    }
488
489    if !is_transfer_root(&proof.asset) || !has_stxo_proofs {
490        return assert_version_consistency(&commit_versions);
491    }
492
493    verify_v1_exclusion_proofs(ops, proof, p2tr_outputs)?;
494    assert_version_consistency(&commit_versions)
495}
496
497/// Verifies all v0 exclusion proofs and returns observed commitment versions.
498fn verify_v0_exclusion_proofs<O: TaprootOps>(
499    ops: &O,
500    proof: &Proof,
501    p2tr_outputs: &mut BTreeSet<u32>,
502) -> Result<CommittedVersions, Error> {
503    let mut commit_versions = BTreeMap::new();
504    for exclusion_proof in &proof.exclusion_proofs {
505        let derived = taproot_proof::verify_taproot_proof_with_commitment(
506            ops,
507            &proof.anchor_tx,
508            exclusion_proof,
509            &proof.asset,
510            false,
511        )
512        .map_err(|err| Error::TaprootProof {
513            stage: ProofStage::Exclusion,
514            source: err,
515        })?;
516
517        p2tr_outputs.remove(&exclusion_proof.output_index);
518
519        if let Some(commitment) = derived {
520            commit_versions
521                .entry(exclusion_proof.output_index)
522                .or_insert_with(Vec::new)
523                .push(commitment.version);
524        }
525    }
526
527    if !p2tr_outputs.is_empty() {
528        return Err(Error::InvalidCommitmentProof);
529    }
530
531    Ok(commit_versions)
532}
533
534/// Verifies all v1 exclusion proofs, including STXO proofs.
535fn verify_v1_exclusion_proofs<O: TaprootOps>(
536    ops: &O,
537    proof: &Proof,
538    p2tr_outputs: BTreeSet<u32>,
539) -> Result<(), Error> {
540    let (asset_map, stxo_keys) = collect_stxo_assets(&proof.asset)?;
541    let mut p2tr_outputs_stxo = BTreeMap::new();
542    for out_idx in p2tr_outputs {
543        p2tr_outputs_stxo.insert(out_idx, stxo_keys.clone());
544    }
545
546    for exclusion_proof in &proof.exclusion_proofs {
547        if exclusion_proof.tapscript_proof.is_some() {
548            p2tr_outputs_stxo.remove(&exclusion_proof.output_index);
549            continue;
550        }
551
552        verify_stxo_proof_set(
553            ops,
554            &proof.anchor_tx,
555            exclusion_proof,
556            &asset_map,
557            &mut p2tr_outputs_stxo,
558            false,
559        )?;
560    }
561
562    if !p2tr_outputs_stxo.is_empty() {
563        return Err(Error::MissingStxoInputProofs);
564    }
565
566    Ok(())
567}
568
569/// Verifies a set of STXO proofs against the provided output tracking map.
570fn verify_stxo_proof_set<O: TaprootOps>(
571    ops: &O,
572    anchor_tx: &Transaction,
573    base_proof: &TaprootProof,
574    asset_map: &BTreeMap<SerializedKey, Asset>,
575    p2tr_outputs: &mut P2TROutputsSTXOs,
576    inclusion: bool,
577) -> Result<(), Error> {
578    let base_commitment = base_proof
579        .commitment_proof
580        .as_ref()
581        .ok_or(Error::MissingCommitmentProof)?;
582
583    for (key, stxo_proof) in &base_commitment.stxo_proofs {
584        let stxo_asset = asset_map
585            .get(key)
586            .ok_or(Error::MissingStxoAsset { key: *key })?;
587        let stxo_combined = make_stxo_proof(base_proof, base_commitment, stxo_proof);
588
589        taproot_proof::verify_taproot_proof(ops, anchor_tx, &stxo_combined, stxo_asset, inclusion)
590            .map_err(|err| Error::TaprootProof {
591                stage: ProofStage::Stxo,
592                source: err,
593            })?;
594
595        let out_idx = stxo_combined.output_index;
596        if let Some(keys) = p2tr_outputs.get_mut(&out_idx) {
597            keys.remove(key);
598            if keys.is_empty() {
599                p2tr_outputs.remove(&out_idx);
600            }
601        }
602    }
603
604    Ok(())
605}
606
607/// Constructs an STXO proof from a base proof and a specific commitment proof.
608fn make_stxo_proof(
609    base_proof: &TaprootProof,
610    base_commitment: &CommitmentProof,
611    stxo_proof: &taproot_assets_types::commitment::Proof,
612) -> TaprootProof {
613    TaprootProof {
614        output_index: base_proof.output_index,
615        internal_key: base_proof.internal_key,
616        commitment_proof: Some(CommitmentProof {
617            proof: stxo_proof.clone(),
618            tap_sibling_preimage: base_commitment.tap_sibling_preimage.clone(),
619            stxo_proofs: BTreeMap::new(),
620            unknown_odd_types: BTreeMap::new(),
621        }),
622        tapscript_proof: base_proof.tapscript_proof.clone(),
623        unknown_odd_types: base_proof.unknown_odd_types.clone(),
624    }
625}
626
627/// Asserts all commitment versions are mutually compatible.
628fn assert_version_consistency(
629    versions: &CommittedVersions,
630) -> Result<Option<TapCommitmentVersion>, Error> {
631    let mut values = versions.values();
632    let first_versions = match values.next() {
633        Some(versions) => versions,
634        None => return Ok(None),
635    };
636    let first = *first_versions
637        .first()
638        .ok_or(Error::InvalidCommitmentProof)?;
639
640    for versions in versions.values() {
641        for version in versions {
642            if !is_similar_tap_commitment_version(first, *version) {
643                return Err(Error::MixedCommitmentVersions);
644            }
645        }
646    }
647
648    Ok(Some(first))
649}
650
651/// Returns true if two TapCommitment versions are compatible.
652fn is_similar_tap_commitment_version(
653    left: TapCommitmentVersion,
654    right: TapCommitmentVersion,
655) -> bool {
656    if left == TapCommitmentVersion::V2 {
657        return right == TapCommitmentVersion::V2;
658    }
659
660    matches!(left, TapCommitmentVersion::V0 | TapCommitmentVersion::V1)
661        && matches!(right, TapCommitmentVersion::V0 | TapCommitmentVersion::V1)
662}
663
664/// Returns true if the proof uses the v1 transition format.
665fn is_version_v1(version: u32) -> bool {
666    version == PROOF_VERSION_V1
667}
668
669/// Returns true if the asset has a split commitment witness.
670fn has_split_commitment_witness(asset: &Asset) -> bool {
671    asset.prev_witnesses.len() == 1 && is_split_commit_witness(&asset.prev_witnesses[0])
672}
673
674/// Returns true if the witness is a split commitment witness.
675fn is_split_commit_witness(witness: &PrevWitness) -> bool {
676    witness.prev_id.is_some() && witness.tx_witness.is_empty() && witness.split_commitment.is_some()
677}
678
679/// Returns true if the asset represents a genesis asset.
680fn is_genesis_asset(asset: &Asset) -> bool {
681    has_genesis_witness(asset) || has_genesis_witness_for_group(asset)
682}
683
684/// Returns true if the input represents a genesis asset.
685fn is_genesis_asset_input(asset: &GenesisAssetInput) -> bool {
686    has_genesis_witness_input(asset) || has_genesis_witness_for_group_input(asset)
687}
688
689/// Returns true if the asset has a plain genesis witness.
690fn has_genesis_witness(asset: &Asset) -> bool {
691    if asset.prev_witnesses.len() != 1 {
692        return false;
693    }
694
695    let witness = &asset.prev_witnesses[0];
696    if witness.prev_id.is_none()
697        || !witness.tx_witness.is_empty()
698        || witness.split_commitment.is_some()
699    {
700        return false;
701    }
702
703    is_zero_prev_id(witness.prev_id.as_ref().expect("checked above"))
704}
705
706/// Returns true if the input has a plain genesis witness.
707fn has_genesis_witness_input(asset: &GenesisAssetInput) -> bool {
708    if asset.prev_witnesses.len() != 1 {
709        return false;
710    }
711
712    let witness = &asset.prev_witnesses[0];
713    if witness.prev_id.is_none() || witness.has_tx_witness || witness.has_split_commitment {
714        return false;
715    }
716
717    is_zero_prev_id(witness.prev_id.as_ref().expect("checked above"))
718}
719
720/// Returns true if the asset has a genesis witness for an asset group.
721fn has_genesis_witness_for_group(asset: &Asset) -> bool {
722    if asset.asset_group.is_none() || asset.prev_witnesses.len() != 1 {
723        return false;
724    }
725
726    let witness = &asset.prev_witnesses[0];
727    if witness.prev_id.is_none()
728        || witness.tx_witness.is_empty()
729        || witness.split_commitment.is_some()
730    {
731        return false;
732    }
733
734    is_zero_prev_id(witness.prev_id.as_ref().expect("checked above"))
735}
736
737/// Returns true if the input has a genesis witness for an asset group.
738fn has_genesis_witness_for_group_input(asset: &GenesisAssetInput) -> bool {
739    if !asset.has_asset_group || asset.prev_witnesses.len() != 1 {
740        return false;
741    }
742
743    let witness = &asset.prev_witnesses[0];
744    if witness.prev_id.is_none() || !witness.has_tx_witness || witness.has_split_commitment {
745        return false;
746    }
747
748    is_zero_prev_id(witness.prev_id.as_ref().expect("checked above"))
749}
750
751/// Returns true if the asset represents a transfer root.
752fn is_transfer_root(asset: &Asset) -> bool {
753    !is_genesis_asset(asset) && !has_split_commitment_witness(asset)
754}
755
756/// Returns the split root asset for a split commitment witness.
757fn split_root_asset(asset: &Asset) -> Result<&Asset, Error> {
758    let witness = asset
759        .prev_witnesses
760        .first()
761        .ok_or(Error::MissingSplitRootProof)?;
762    let split_commitment = witness
763        .split_commitment
764        .as_ref()
765        .ok_or(Error::MissingSplitRootProof)?;
766    Ok(split_commitment.root_asset.as_ref())
767}
768
769/// Returns true if the PrevID is the all-zero genesis reference.
770fn is_zero_prev_id(prev_id: &PrevId) -> bool {
771    prev_id.out_point.txid == Txid::from_byte_array([0u8; 32])
772        && prev_id.out_point.vout == 0
773        && prev_id.asset_id.to_byte_array() == [0u8; 32]
774        && prev_id.script_key.bytes == [0u8; COMPRESSED_KEY_LEN]
775}
776
777/// Collects STXO assets and the associated script key set.
778fn collect_stxo_assets(
779    asset: &Asset,
780) -> Result<(BTreeMap<SerializedKey, Asset>, BTreeSet<SerializedKey>), Error> {
781    if !is_transfer_root(asset) {
782        return Ok((BTreeMap::new(), BTreeSet::new()));
783    }
784
785    if asset.prev_witnesses.is_empty() {
786        return Err(Error::MissingAssetWitnesses);
787    }
788
789    let mut asset_map = BTreeMap::new();
790    let mut keys = BTreeSet::new();
791    for witness in &asset.prev_witnesses {
792        let stxo_asset = make_spent_asset(witness)?;
793        let key = serialized_key_from_script_key(&stxo_asset.script_key)?;
794        keys.insert(key);
795        asset_map.insert(key, stxo_asset);
796    }
797
798    Ok((asset_map, keys))
799}
800
801/// Builds a minimal asset that represents a spent input for STXO proofs.
802fn make_spent_asset(witness: &PrevWitness) -> Result<Asset, Error> {
803    let prev_id = witness.prev_id.as_ref().ok_or(Error::MissingPrevId)?;
804    let script_key = derive_burn_script_key(prev_id)?;
805    Ok(make_alt_leaf_asset(script_key))
806}
807
808/// Builds a minimal alt leaf asset for STXO verification.
809fn make_alt_leaf_asset(script_key: SerializedKey) -> Asset {
810    Asset {
811        version: AssetVersion::V0,
812        asset_genesis: Some(empty_genesis_info()),
813        amount: 0,
814        lock_time: 0,
815        relative_lock_time: 0,
816        script_version: 0,
817        script_key: script_key.bytes.to_vec(),
818        script_key_is_local: false,
819        asset_group: None,
820        chain_anchor: None,
821        prev_witnesses: Vec::new(),
822        split_commitment_root: None,
823        is_spent: false,
824        lease_owner: Vec::new(),
825        lease_expiry: 0,
826        is_burn: false,
827        script_key_declared_known: false,
828        script_key_has_script_path: false,
829        decimal_display: None,
830        script_key_type: ScriptKeyType::Burn,
831    }
832}
833
834/// Constructs the empty genesis info used by alt leaf assets.
835fn empty_genesis_info() -> GenesisInfo {
836    let genesis_point = OutPoint {
837        txid: Txid::from_byte_array([0u8; 32]),
838        vout: 0,
839    };
840    let mut genesis = GenesisInfo {
841        genesis_point,
842        name: String::new(),
843        meta_hash: Sha256Hash::from_byte_array([0u8; 32]),
844        asset_id: Sha256Hash::from_byte_array([0u8; 32]),
845        asset_type: AssetType::Normal,
846        output_index: 0,
847    };
848    genesis.asset_id = compute_asset_id(&genesis);
849    genesis
850}
851
852/// Computes an asset ID from genesis information.
853fn compute_asset_id(genesis: &GenesisInfo) -> Sha256Hash {
854    compute_asset_id_with_hasher(genesis, &BitcoinSha256Hasher)
855}
856
857/// Computes an asset ID from genesis information using the supplied hasher.
858fn compute_asset_id_with_hasher<H: Sha256Hasher>(genesis: &GenesisInfo, hasher: &H) -> Sha256Hash {
859    let outpoint_bytes = serialize(&genesis.genesis_point);
860    let tag_hash = hasher.hash(genesis.name.as_bytes());
861
862    let mut buf = Vec::with_capacity(outpoint_bytes.len() + 32 + 32 + 4 + 1);
863    buf.extend_from_slice(&outpoint_bytes);
864    buf.extend_from_slice(&tag_hash);
865    buf.extend_from_slice(&genesis.meta_hash.to_byte_array());
866    buf.extend_from_slice(&genesis.output_index.to_be_bytes());
867    buf.push(asset_type_byte(genesis.asset_type));
868
869    Sha256Hash::from_byte_array(hasher.hash(&buf))
870}
871
872/// Returns the sha256 hash of a meta reveal TLV encoding using the supplied hasher.
873fn meta_reveal_hash_with_hasher<H: Sha256Hasher>(meta: &MetaReveal, hasher: &H) -> Sha256Hash {
874    let encoded = encode_meta_reveal(meta);
875    Sha256Hash::from_byte_array(hasher.hash(&encoded))
876}
877
878/// Encodes a meta reveal as a TLV byte stream.
879fn encode_meta_reveal(meta: &MetaReveal) -> Vec<u8> {
880    let mut out = Vec::new();
881    encode_record(META_REVEAL_ENCODING_TYPE, &[meta.meta_type as u8], &mut out);
882    encode_record(META_REVEAL_DATA_TYPE, &meta.data, &mut out);
883    for (tlv_type, value) in meta.unknown_odd_types.iter() {
884        encode_record(*tlv_type, value, &mut out);
885    }
886    out
887}
888
889/// Encodes a TLV record into the provided buffer.
890fn encode_record(tlv_type: u64, value: &[u8], out: &mut Vec<u8>) {
891    encode_bigsize(tlv_type, out);
892    encode_bigsize(value.len() as u64, out);
893    out.extend_from_slice(value);
894}
895
896/// Encodes a BigSize varint into the provided buffer.
897fn encode_bigsize(value: u64, out: &mut Vec<u8>) {
898    match value {
899        0..=0xFC => out.push(value as u8),
900        0xFD..=0xFFFF => {
901            out.push(0xFD);
902            out.extend_from_slice(&(value as u16).to_be_bytes());
903        }
904        0x1_0000..=0xFFFF_FFFF => {
905            out.push(0xFE);
906            out.extend_from_slice(&(value as u32).to_be_bytes());
907        }
908        _ => {
909            out.push(0xFF);
910            out.extend_from_slice(&value.to_be_bytes());
911        }
912    }
913}
914
915/// Returns a zero meta hash value.
916fn zero_meta_hash() -> Sha256Hash {
917    Sha256Hash::from_byte_array([0u8; 32])
918}
919
920/// Returns the protocol byte for an asset type.
921fn asset_type_byte(asset_type: AssetType) -> u8 {
922    match asset_type {
923        AssetType::Normal => 0,
924        AssetType::Collectible => 1,
925    }
926}
927
928/// Converts a script key byte slice into a SerializedKey.
929fn serialized_key_from_script_key(bytes: &[u8]) -> Result<SerializedKey, Error> {
930    if bytes.len() != COMPRESSED_KEY_LEN {
931        return Err(Error::InvalidAssetScriptKeyLength {
932            expected: COMPRESSED_KEY_LEN,
933            actual: bytes.len(),
934        });
935    }
936    let mut array = [0u8; COMPRESSED_KEY_LEN];
937    array.copy_from_slice(bytes);
938    Ok(SerializedKey { bytes: array })
939}
940
941/// Returns the x-only serialized form of a compressed key.
942fn schnorr_key_bytes(key: &SerializedKey) -> [u8; 32] {
943    let mut bytes = [0u8; 32];
944    bytes.copy_from_slice(&key.bytes[1..]);
945    bytes
946}
947
948/// Serializes a PrevID for burn key derivation.
949fn serialize_prev_id_for_burn(prev_id: &PrevId) -> Vec<u8> {
950    let outpoint_bytes = serialize(&prev_id.out_point);
951    let mut buf = Vec::with_capacity(outpoint_bytes.len() + 32 + 32);
952    buf.extend_from_slice(&outpoint_bytes);
953    buf.extend_from_slice(&prev_id.asset_id.to_byte_array());
954    buf.extend_from_slice(&schnorr_key_bytes(&prev_id.script_key));
955    buf
956}
957
958/// Derives the burn script key for a given PrevID.
959fn derive_burn_script_key(prev_id: &PrevId) -> Result<SerializedKey, Error> {
960    let nums_xonly = nums_xonly_key()?;
961    let tweak_data = serialize_prev_id_for_burn(prev_id);
962    let tweak = tap_tweak_scalar(nums_xonly, &tweak_data)?;
963    let secp = Secp256k1::verification_only();
964    let (tweaked, _) = nums_xonly
965        .add_tweak(&secp, &tweak)
966        .map_err(|_| Error::InvalidBurnKeyTweak)?;
967
968    let mut bytes = [0u8; COMPRESSED_KEY_LEN];
969    bytes[0] = 0x02;
970    bytes[1..].copy_from_slice(&tweaked.serialize());
971    Ok(SerializedKey { bytes })
972}
973
974/// Returns the NUMS x-only internal key used for burn derivation.
975fn nums_xonly_key() -> Result<XOnlyPublicKey, Error> {
976    let pubkey = bitcoin::secp256k1::PublicKey::from_slice(&NUMS_COMPRESSED_KEY)
977        .map_err(|_| Error::InvalidNumsKey)?;
978    let (xonly, _) = pubkey.x_only_public_key();
979    Ok(xonly)
980}
981
982/// Computes the TapTweak scalar for the given internal key and tweak data.
983fn tap_tweak_scalar(internal_key: XOnlyPublicKey, tweak_data: &[u8]) -> Result<Scalar, Error> {
984    let mut eng = TapTweakHash::engine();
985    eng.input(&internal_key.serialize());
986    eng.input(tweak_data);
987    let hash = TapTweakHash::from_engine(eng);
988    Scalar::from_be_bytes(hash.to_byte_array()).map_err(|_| Error::InvalidBurnKeyTweak)
989}
990
991/// Input for the Taproot commitment claim.
992#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
993pub struct TaprootClaimInput {
994    /// The Taproot proof to verify.
995    pub taproot_proof: TaprootProof,
996    /// The asset being proven.
997    pub asset: Asset,
998    /// The expected Taproot output key, derived from the anchor claim.
999    pub expected_taproot_output_key: [u8; 32],
1000    /// Whether this is an inclusion proof.
1001    pub inclusion: bool,
1002}
1003
1004/// Output for the Taproot commitment claim.
1005#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1006pub struct TaprootClaimOutput {
1007    /// The Taproot output key that was verified against.
1008    pub taproot_output_key: [u8; 32],
1009    /// The output index proven by this claim.
1010    pub output_index: u32,
1011    /// The derived Taproot Asset commitment.
1012    pub tap_commitment: taproot_proof::TapCommitment,
1013}
1014
1015/// Input for the STXO claim.
1016#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1017pub struct StxoClaimInput {
1018    /// The Taproot proof containing STXO proofs.
1019    pub taproot_proof: TaprootProof,
1020    /// Asset that contains the prev witness set used to derive STXO keys.
1021    pub asset: Asset,
1022    /// Proof version of the enclosing proof.
1023    pub proof_version: u32,
1024    /// The expected Taproot output key.
1025    pub expected_taproot_output_key: [u8; 32],
1026    /// Whether the STXO proofs are inclusion proofs (true) or exclusion proofs (false).
1027    pub inclusion: bool,
1028}
1029
1030/// Output for the STXO claim.
1031#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1032pub struct StxoClaimOutput {
1033    /// The Taproot output key that was verified against.
1034    pub taproot_output_key: [u8; 32],
1035    /// The output index proven by this claim.
1036    pub output_index: u32,
1037    /// The list of script keys that were successfully verified.
1038    pub verified_keys: Vec<SerializedKey>,
1039}
1040
1041/// Input for the asset integrity claim.
1042#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1043pub struct AssetClaimInput {
1044    /// Previous outpoint referenced by the proof.
1045    pub prev_out: OutPoint,
1046    /// Output index from the inclusion proof.
1047    pub inclusion_output_index: u32,
1048    /// Proof version of the enclosing proof.
1049    pub proof_version: u32,
1050    /// The asset being verified.
1051    pub asset: Asset,
1052    /// Genesis reveal payload, if present.
1053    pub genesis_reveal: Option<GenesisReveal>,
1054    /// Meta reveal payload, if present.
1055    pub meta_reveal: Option<MetaReveal>,
1056    /// Group key reveal payload, if present.
1057    pub group_key_reveal: Option<GroupKeyReveal>,
1058}
1059
1060impl AssetClaimInput {
1061    /// Builds an asset integrity input from a full proof.
1062    pub fn from_proof(proof: &Proof) -> Self {
1063        AssetClaimInput {
1064            prev_out: proof.prev_out,
1065            inclusion_output_index: proof.inclusion_proof.output_index,
1066            proof_version: proof.version,
1067            asset: proof.asset.clone(),
1068            genesis_reveal: proof.genesis_reveal.clone(),
1069            meta_reveal: proof.meta_reveal.clone(),
1070            group_key_reveal: proof.group_key_reveal.clone(),
1071        }
1072    }
1073}
1074
1075/// Output for the asset integrity claim.
1076#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1077pub struct AssetClaimOutput {
1078    /// Asset ID derived from genesis information.
1079    pub asset_id: [u8; 32],
1080    /// Group key, if the asset is grouped.
1081    pub group_key: Option<SerializedKey>,
1082    /// Meta hash, if one is set.
1083    pub meta_hash: Option<[u8; 32]>,
1084    /// Proof version committed by the claim.
1085    pub proof_version: u32,
1086    /// True if the asset represents a transfer root.
1087    pub is_transfer_root: bool,
1088    /// True if the asset has a split commitment witness.
1089    pub has_split_commitment: bool,
1090    /// True if STXO proofs are required for this asset and proof version.
1091    pub stxo_required: bool,
1092}
1093
1094/// Verifies the Taproot commitment claim.
1095pub fn verify_taproot_claim_with_ops<O: TaprootOps>(
1096    ops: &O,
1097    input: &TaprootClaimInput,
1098) -> Result<TaprootClaimOutput, Error> {
1099    let expected_key =
1100        XOnlyPublicKey::from_slice(&input.expected_taproot_output_key).map_err(|_| {
1101            Error::TaprootProof {
1102                stage: if input.inclusion {
1103                    ProofStage::Inclusion
1104                } else {
1105                    ProofStage::Exclusion
1106                },
1107                source: taproot_proof::Error::InvalidTaprootOutputKey,
1108            }
1109        })?;
1110
1111    let commitment = taproot_proof::verify_taproot_proof_with_commitment_and_key(
1112        ops,
1113        expected_key,
1114        &input.taproot_proof,
1115        &input.asset,
1116        input.inclusion,
1117    )
1118    .map_err(|err| Error::TaprootProof {
1119        stage: if input.inclusion {
1120            ProofStage::Inclusion
1121        } else {
1122            ProofStage::Exclusion
1123        },
1124        source: err,
1125    })?
1126    .ok_or(Error::MissingCommitmentProof)?;
1127
1128    Ok(TaprootClaimOutput {
1129        taproot_output_key: input.expected_taproot_output_key,
1130        output_index: input.taproot_proof.output_index,
1131        tap_commitment: commitment,
1132    })
1133}
1134
1135/// Verifies the STXO claim.
1136pub fn verify_stxo_claim_with_ops<O: TaprootOps>(
1137    ops: &O,
1138    input: &StxoClaimInput,
1139) -> Result<StxoClaimOutput, Error> {
1140    let expected_key =
1141        XOnlyPublicKey::from_slice(&input.expected_taproot_output_key).map_err(|_| {
1142            Error::TaprootProof {
1143                stage: ProofStage::Stxo,
1144                source: taproot_proof::Error::InvalidTaprootOutputKey,
1145            }
1146        })?;
1147
1148    let is_transfer_root = is_transfer_root(&input.asset);
1149    let has_stxo_proofs = input
1150        .taproot_proof
1151        .commitment_proof
1152        .as_ref()
1153        .map_or(false, |commitment| !commitment.stxo_proofs.is_empty());
1154    let need_stxo_proofs = is_version_v1(input.proof_version) && is_transfer_root;
1155
1156    if input.taproot_proof.tapscript_proof.is_some() {
1157        return Ok(StxoClaimOutput {
1158            taproot_output_key: input.expected_taproot_output_key,
1159            output_index: input.taproot_proof.output_index,
1160            verified_keys: Vec::new(),
1161        });
1162    }
1163
1164    if need_stxo_proofs && !has_stxo_proofs {
1165        return Err(Error::MissingStxoProofs);
1166    }
1167
1168    if !is_transfer_root || !has_stxo_proofs {
1169        return Ok(StxoClaimOutput {
1170            taproot_output_key: input.expected_taproot_output_key,
1171            output_index: input.taproot_proof.output_index,
1172            verified_keys: Vec::new(),
1173        });
1174    }
1175
1176    let (asset_map, mut remaining_keys) = collect_stxo_assets(&input.asset)?;
1177    let base_commitment = input
1178        .taproot_proof
1179        .commitment_proof
1180        .as_ref()
1181        .ok_or(Error::MissingCommitmentProof)?;
1182
1183    let mut verified_keys = Vec::new();
1184
1185    for (key, stxo_proof) in &base_commitment.stxo_proofs {
1186        let stxo_asset = asset_map
1187            .get(key)
1188            .ok_or(Error::MissingStxoAsset { key: *key })?;
1189
1190        let stxo_combined = make_stxo_proof(&input.taproot_proof, base_commitment, stxo_proof);
1191
1192        taproot_proof::verify_taproot_proof_with_commitment_and_key(
1193            ops,
1194            expected_key,
1195            &stxo_combined,
1196            stxo_asset,
1197            input.inclusion,
1198        )
1199        .map_err(|err| Error::TaprootProof {
1200            stage: ProofStage::Stxo,
1201            source: err,
1202        })?;
1203
1204        remaining_keys.remove(key);
1205        verified_keys.push(*key);
1206    }
1207
1208    if !remaining_keys.is_empty() {
1209        return Err(Error::MissingStxoInputProofs);
1210    }
1211
1212    // Sort verified keys for deterministic output.
1213    verified_keys.sort();
1214
1215    Ok(StxoClaimOutput {
1216        taproot_output_key: input.expected_taproot_output_key,
1217        output_index: input.taproot_proof.output_index,
1218        verified_keys,
1219    })
1220}
1221
1222/// Verifies the asset integrity claim.
1223pub fn verify_asset_claim_with_ops<O: TaprootOps>(
1224    ops: &O,
1225    input: &AssetClaimInput,
1226) -> Result<AssetClaimOutput, Error> {
1227    let genesis_input = GenesisRevealInput {
1228        prev_out: input.prev_out,
1229        inclusion_output_index: input.inclusion_output_index,
1230        genesis_reveal: input.genesis_reveal.clone(),
1231        meta_reveal: input.meta_reveal.clone(),
1232        asset: genesis_asset_input_from_asset(&input.asset),
1233    };
1234
1235    verify_genesis_reveal_input(&genesis_input)?;
1236
1237    group_key_reveal::verify_group_key_reveal_with_asset(
1238        ops,
1239        &input.asset,
1240        input.group_key_reveal.as_ref(),
1241    )
1242    .map_err(Error::GroupKeyReveal)?;
1243
1244    let asset_genesis = input
1245        .asset
1246        .asset_genesis
1247        .as_ref()
1248        .ok_or(Error::MissingAssetGenesis)?;
1249    let meta_hash = if asset_genesis.meta_hash == zero_meta_hash() {
1250        None
1251    } else {
1252        Some(asset_genesis.meta_hash.to_byte_array())
1253    };
1254    let group_key = asset_group_key_from_asset(&input.asset)?;
1255    let is_transfer_root = is_transfer_root(&input.asset);
1256    let has_split_commitment = has_split_commitment_witness(&input.asset);
1257    let stxo_required = is_version_v1(input.proof_version) && is_transfer_root;
1258
1259    Ok(AssetClaimOutput {
1260        asset_id: asset_genesis.asset_id.to_byte_array(),
1261        group_key,
1262        meta_hash,
1263        proof_version: input.proof_version,
1264        is_transfer_root,
1265        has_split_commitment,
1266        stxo_required,
1267    })
1268}
1269
1270fn asset_group_key_from_asset(asset: &Asset) -> Result<Option<SerializedKey>, Error> {
1271    let asset_group = match asset.asset_group.as_ref() {
1272        Some(group) => group,
1273        None => return Ok(None),
1274    };
1275    let key_bytes = if !asset_group.tweaked_group_key.is_empty() {
1276        asset_group.tweaked_group_key.as_slice()
1277    } else {
1278        asset_group.raw_group_key.as_slice()
1279    };
1280    if key_bytes.len() != 33 {
1281        return Err(Error::GroupKeyReveal(
1282            group_key_reveal::Error::InvalidGroupKeyLength {
1283                expected: 33,
1284                actual: key_bytes.len(),
1285            },
1286        ));
1287    }
1288
1289    let mut bytes = [0u8; 33];
1290    bytes.copy_from_slice(key_bytes);
1291    Ok(Some(SerializedKey { bytes }))
1292}