1use 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
23const PROOF_VERSION_V1: u32 = 1;
25const COMPRESSED_KEY_LEN: usize = 33;
27const 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];
33const META_REVEAL_ENCODING_TYPE: u64 = 0;
35const META_REVEAL_DATA_TYPE: u64 = 2;
37
38type P2TROutputsSTXOs = BTreeMap<u32, BTreeSet<SerializedKey>>;
40type CommittedVersions = BTreeMap<u32, Vec<TapCommitmentVersion>>;
42
43#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
45pub struct GenesisWitnessInput {
46 pub prev_id: Option<PrevId>,
48 pub has_tx_witness: bool,
50 pub has_split_commitment: bool,
52}
53
54#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
56pub struct GenesisAssetInput {
57 pub asset_genesis_id: Option<Sha256Hash>,
59 pub has_asset_group: bool,
61 pub prev_witnesses: Vec<GenesisWitnessInput>,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
67pub struct GenesisRevealInput {
68 pub prev_out: OutPoint,
70 pub inclusion_output_index: u32,
72 pub genesis_reveal: Option<GenesisReveal>,
74 pub meta_reveal: Option<MetaReveal>,
76 pub asset: GenesisAssetInput,
78}
79
80impl GenesisRevealInput {
81 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
111pub trait Sha256Hasher {
113 fn hash(&self, data: &[u8]) -> [u8; 32];
115}
116
117#[derive(Debug, Clone, Copy, Default)]
119pub struct BitcoinSha256Hasher;
120
121impl Sha256Hasher for BitcoinSha256Hasher {
122 fn hash(&self, data: &[u8]) -> [u8; 32] {
124 Sha256Hash::hash(data).to_byte_array()
125 }
126}
127
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
130pub enum ProofStage {
131 Inclusion,
133 Exclusion,
135 SplitRoot,
137 Stxo,
139}
140
141impl core::fmt::Display for ProofStage {
142 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
155pub enum Error {
156 TaprootProof {
158 stage: ProofStage,
160 source: taproot_proof::Error,
162 },
163 GroupKeyReveal(group_key_reveal::Error),
165 MissingSplitRootProof,
167 MissingStxoProofs,
169 MissingStxoInputProofs,
171 MissingCommitmentProof,
173 MissingStxoAsset {
175 key: SerializedKey,
177 },
178 MissingAssetWitnesses,
180 MissingPrevId,
182 InvalidAssetScriptKeyLength {
184 expected: usize,
186 actual: usize,
188 },
189 InvalidNumsKey,
191 InvalidBurnKeyTweak,
193 MixedCommitmentVersions,
195 InvalidCommitmentProof,
197 NonGenesisAssetWithGenesisReveal,
199 NonGenesisAssetWithMetaReveal,
201 GenesisRevealRequired,
203 GenesisRevealMissingBase,
205 GenesisRevealPrevOutMismatch,
207 GenesisRevealMetaRevealRequired,
209 GenesisRevealMetaHashMismatch,
211 GenesisRevealOutputIndexMismatch,
213 GenesisRevealAssetIdMismatch,
215 MissingAssetGenesis,
217}
218
219impl core::fmt::Display for Error {
220 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
267pub fn verify_genesis_reveal(proof: &Proof) -> Result<(), Error> {
269 verify_genesis_reveal_input(&GenesisRevealInput::from_proof(proof))
270}
271
272pub 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
280pub fn verify_genesis_reveal_input(input: &GenesisRevealInput) -> Result<(), Error> {
282 verify_genesis_reveal_input_with_hasher(input, &BitcoinSha256Hasher)
283}
284
285pub 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
311fn 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
360pub 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
384pub 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
438pub 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
452pub 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
497fn 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
534fn 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
569fn 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
607fn 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
627fn 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
651fn 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
664fn is_version_v1(version: u32) -> bool {
666 version == PROOF_VERSION_V1
667}
668
669fn has_split_commitment_witness(asset: &Asset) -> bool {
671 asset.prev_witnesses.len() == 1 && is_split_commit_witness(&asset.prev_witnesses[0])
672}
673
674fn 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
679fn is_genesis_asset(asset: &Asset) -> bool {
681 has_genesis_witness(asset) || has_genesis_witness_for_group(asset)
682}
683
684fn is_genesis_asset_input(asset: &GenesisAssetInput) -> bool {
686 has_genesis_witness_input(asset) || has_genesis_witness_for_group_input(asset)
687}
688
689fn 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
706fn 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
720fn 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
737fn 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
751fn is_transfer_root(asset: &Asset) -> bool {
753 !is_genesis_asset(asset) && !has_split_commitment_witness(asset)
754}
755
756fn 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
769fn 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
777fn 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
801fn 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
808fn 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
834fn 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
852fn compute_asset_id(genesis: &GenesisInfo) -> Sha256Hash {
854 compute_asset_id_with_hasher(genesis, &BitcoinSha256Hasher)
855}
856
857fn 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
872fn 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
878fn 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
889fn 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
896fn 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
915fn zero_meta_hash() -> Sha256Hash {
917 Sha256Hash::from_byte_array([0u8; 32])
918}
919
920fn asset_type_byte(asset_type: AssetType) -> u8 {
922 match asset_type {
923 AssetType::Normal => 0,
924 AssetType::Collectible => 1,
925 }
926}
927
928fn 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
941fn 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
948fn 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
958fn 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
974fn 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
982fn 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
993pub struct TaprootClaimInput {
994 pub taproot_proof: TaprootProof,
996 pub asset: Asset,
998 pub expected_taproot_output_key: [u8; 32],
1000 pub inclusion: bool,
1002}
1003
1004#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1006pub struct TaprootClaimOutput {
1007 pub taproot_output_key: [u8; 32],
1009 pub output_index: u32,
1011 pub tap_commitment: taproot_proof::TapCommitment,
1013}
1014
1015#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1017pub struct StxoClaimInput {
1018 pub taproot_proof: TaprootProof,
1020 pub asset: Asset,
1022 pub proof_version: u32,
1024 pub expected_taproot_output_key: [u8; 32],
1026 pub inclusion: bool,
1028}
1029
1030#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1032pub struct StxoClaimOutput {
1033 pub taproot_output_key: [u8; 32],
1035 pub output_index: u32,
1037 pub verified_keys: Vec<SerializedKey>,
1039}
1040
1041#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1043pub struct AssetClaimInput {
1044 pub prev_out: OutPoint,
1046 pub inclusion_output_index: u32,
1048 pub proof_version: u32,
1050 pub asset: Asset,
1052 pub genesis_reveal: Option<GenesisReveal>,
1054 pub meta_reveal: Option<MetaReveal>,
1056 pub group_key_reveal: Option<GroupKeyReveal>,
1058}
1059
1060impl AssetClaimInput {
1061 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1077pub struct AssetClaimOutput {
1078 pub asset_id: [u8; 32],
1080 pub group_key: Option<SerializedKey>,
1082 pub meta_hash: Option<[u8; 32]>,
1084 pub proof_version: u32,
1086 pub is_transfer_root: bool,
1088 pub has_split_commitment: bool,
1090 pub stxo_required: bool,
1092}
1093
1094pub 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
1135pub 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 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
1222pub 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}