Skip to main content

taproot_assets_core/verify/
tx.rs

1//! Anchor transaction verification helpers.
2
3extern crate alloc;
4
5use alloc::vec::Vec;
6
7use bitcoin::block::Header;
8use bitcoin::hashes::{Hash, sha256d::Hash as Sha256dHash};
9use bitcoin::{OutPoint, Script, Transaction, TxMerkleNode};
10use serde::{Deserialize, Serialize};
11use taproot_assets_types::asset::SerializedKey;
12use taproot_assets_types::proof::{Proof, TxMerkleProof};
13use thiserror::Error;
14
15use crate::{OpsError, TaprootOps};
16
17/// Errors returned by anchor transaction verification helpers.
18#[derive(Error, Debug, Clone, Copy, PartialEq, Eq)]
19pub enum Error {
20    /// The anchor transaction does not spend the claimed previous outpoint.
21    #[error("anchor tx missing prev out")]
22    AnchorTxMissingPrevOut,
23    /// The claimed outpoint hash does not match the transaction hash.
24    #[error("outpoint hash does not match tx hash")]
25    OutpointHashMismatch,
26    /// The claimed output index is invalid for the transaction.
27    #[error("output index {index} invalid for {output_count} outputs")]
28    OutputIndexInvalid {
29        /// Claimed output index.
30        index: u32,
31        /// Total number of outputs in the transaction.
32        output_count: usize,
33    },
34    /// The output script does not match the derived Taproot output key.
35    #[error("output script does not match derived taproot output key")]
36    OutputScriptMismatch,
37    /// The merkle proof node and bit counts do not match.
38    #[error("merkle proof shape mismatch: nodes={nodes}, bits={bits}")]
39    InvalidMerkleProofShape {
40        /// Number of merkle proof nodes.
41        nodes: usize,
42        /// Number of merkle proof bits.
43        bits: usize,
44    },
45    /// The merkle proof does not match the expected root.
46    #[error("invalid transaction merkle proof")]
47    InvalidTxMerkleProof,
48    /// The block header failed verification.
49    #[error("invalid block header")]
50    InvalidBlockHeader,
51    /// Taproot output key bytes are invalid.
52    #[error("invalid taproot output key")]
53    InvalidTaprootOutputKey,
54    /// Taproot operation failed.
55    #[error(transparent)]
56    Ops(#[from] OpsError),
57}
58
59/// Input for transaction merkle proof verification.
60#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
61pub struct VerifyMerkleProofInput {
62    /// The transaction ID which is expected to be committed in the Merkle tree.
63    pub txid: [u8; 32],
64    /// The list of sibling hashes along the Merkle path from the transaction up to the root.
65    pub nodes: Vec<[u8; 32]>,
66    /// Direction bits: `false` means the node is on the left, `true` means on the right.
67    pub bits: Vec<bool>,
68    /// The expected Merkle root which commits to the transaction ID.
69    pub merkle_root: [u8; 32],
70}
71
72/// Input for anchor claim verification.
73#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
74pub struct AnchorClaimInput {
75    /// The anchor transaction.
76    pub anchor_tx: Transaction,
77    /// The merkle proof for the anchor transaction.
78    pub tx_merkle_proof: TxMerkleProof,
79    /// The block header committing to the anchor transaction.
80    pub block_header: Header,
81    /// The block height.
82    pub block_height: u32,
83    /// The previous outpoint spent by the anchor transaction.
84    pub prev_out: OutPoint,
85    /// The output index of the anchor transaction carrying the asset.
86    pub output_index: u32,
87}
88
89/// Output digest for the anchor claim.
90#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
91pub struct AnchorClaimOutput {
92    /// The transaction ID of the anchor transaction.
93    pub anchor_txid: [u8; 32],
94    /// The hash of the block header.
95    pub block_hash: [u8; 32],
96    /// The block height.
97    pub block_height: u32,
98    /// The output index.
99    pub output_index: u32,
100    /// The taproot output key derived from the anchor transaction output.
101    pub taproot_output_key: [u8; 32],
102    /// All P2TR outputs with their taproot output keys.
103    pub p2tr_outputs: Vec<AnchorP2trOutput>,
104}
105
106/// Taproot output metadata extracted from the anchor transaction.
107#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
108pub struct AnchorP2trOutput {
109    /// Output index in the anchor transaction.
110    pub output_index: u32,
111    /// The taproot output key for the P2TR output.
112    pub taproot_output_key: [u8; 32],
113}
114
115/// Trait for hashing Merkle node pairs.
116pub trait MerkleHasher {
117    /// Hashes a left/right node pair into its parent.
118    fn hash_nodes(&self, left: [u8; 32], right: [u8; 32]) -> [u8; 32];
119}
120
121/// Bitcoin merkle hasher using double-SHA-256.
122#[derive(Debug, Clone, Copy, Default)]
123pub struct BitcoinMerkleHasher;
124
125impl MerkleHasher for BitcoinMerkleHasher {
126    /// Hashes a node pair with double-SHA-256.
127    fn hash_nodes(&self, left: [u8; 32], right: [u8; 32]) -> [u8; 32] {
128        let mut buf = [0u8; 64];
129        buf[..32].copy_from_slice(&left);
130        buf[32..].copy_from_slice(&right);
131        Sha256dHash::hash(&buf).to_byte_array()
132    }
133}
134
135/// Trait for verifying a block header at a given height.
136pub trait HeaderVerifier {
137    /// Returns true if the header is valid for the provided height.
138    fn verify_header(&self, header: &Header, height: u32) -> bool;
139}
140
141/// Verifies the anchor transaction against its merkle proof and block header.
142pub fn verify_anchor_tx<H: HeaderVerifier>(proof: &Proof, verifier: &H) -> Result<(), Error> {
143    if !tx_spends_prev_out(&proof.anchor_tx, &proof.prev_out) {
144        return Err(Error::AnchorTxMissingPrevOut);
145    }
146
147    verify_tx_merkle_proof(
148        &proof.anchor_tx,
149        &proof.tx_merkle_proof,
150        proof.block_header.merkle_root,
151    )?;
152
153    if !verifier.verify_header(&proof.block_header, proof.block_height) {
154        return Err(Error::InvalidBlockHeader);
155    }
156
157    Ok(())
158}
159
160/// Verifies the anchor claim and returns the output digest.
161pub fn verify_anchor_claim_with_hasher<H: MerkleHasher>(
162    input: &AnchorClaimInput,
163    hasher: &H,
164) -> Result<AnchorClaimOutput, Error> {
165    if !tx_spends_prev_out(&input.anchor_tx, &input.prev_out) {
166        return Err(Error::AnchorTxMissingPrevOut);
167    }
168
169    let nodes: Vec<[u8; 32]> = input
170        .tx_merkle_proof
171        .nodes
172        .iter()
173        .map(|node| node.to_byte_array())
174        .collect();
175
176    verify_tx_merkle_proof_with_hasher(
177        input.anchor_tx.compute_txid().to_byte_array(),
178        &nodes,
179        &input.tx_merkle_proof.bits,
180        input.block_header.merkle_root.to_byte_array(),
181        hasher,
182    )?;
183
184    if input.output_index as usize >= input.anchor_tx.output.len() {
185        return Err(Error::OutputIndexInvalid {
186            index: input.output_index,
187            output_count: input.anchor_tx.output.len(),
188        });
189    }
190
191    let output = &input.anchor_tx.output[input.output_index as usize];
192    let taproot_output_key = extract_taproot_output_key(output.script_pubkey.as_script())?;
193    let mut p2tr_outputs = Vec::new();
194    for (idx, output) in input.anchor_tx.output.iter().enumerate() {
195        if output.script_pubkey.is_p2tr() {
196            let taproot_output_key = extract_taproot_output_key(output.script_pubkey.as_script())?;
197            p2tr_outputs.push(AnchorP2trOutput {
198                output_index: idx as u32,
199                taproot_output_key,
200            });
201        }
202    }
203
204    Ok(AnchorClaimOutput {
205        anchor_txid: input.anchor_tx.compute_txid().to_byte_array(),
206        block_hash: input.block_header.block_hash().to_byte_array(),
207        block_height: input.block_height,
208        output_index: input.output_index,
209        taproot_output_key,
210        p2tr_outputs,
211    })
212}
213
214/// Verifies that a claimed outpoint matches a transaction and Taproot output.
215pub fn verify_tx_outpoint<O: TaprootOps>(
216    ops: &O,
217    tx: &Transaction,
218    outpoint: &OutPoint,
219    internal_key: &SerializedKey,
220    tapscript_root: Option<[u8; 32]>,
221) -> Result<(), Error> {
222    if outpoint.txid != tx.compute_txid() {
223        return Err(Error::OutpointHashMismatch);
224    }
225
226    if outpoint.vout as usize >= tx.output.len() {
227        return Err(Error::OutputIndexInvalid {
228            index: outpoint.vout,
229            output_count: tx.output.len(),
230        });
231    }
232
233    let output = &tx.output[outpoint.vout as usize];
234    let expected_key = derive_taproot_output_key(ops, internal_key, tapscript_root)?;
235    let expected_xonly = xonly_from_serialized_key(&expected_key)?;
236    let claimed_xonly = extract_taproot_output_key(output.script_pubkey.as_script())?;
237
238    if expected_xonly == claimed_xonly {
239        Ok(())
240    } else {
241        Err(Error::OutputScriptMismatch)
242    }
243}
244
245/// Verifies a merkle proof for the given transaction and merkle root.
246pub fn verify_tx_merkle_proof(
247    tx: &Transaction,
248    proof: &TxMerkleProof,
249    merkle_root: TxMerkleNode,
250) -> Result<(), Error> {
251    let nodes: Vec<[u8; 32]> = proof
252        .nodes
253        .iter()
254        .map(|node| node.to_byte_array())
255        .collect();
256    verify_tx_merkle_proof_with_hasher(
257        tx.compute_txid().to_byte_array(),
258        &nodes,
259        &proof.bits,
260        merkle_root.to_byte_array(),
261        &BitcoinMerkleHasher,
262    )
263}
264
265/// Verifies a merkle proof described by a minimal input payload.
266pub fn verify_tx_merkle_proof_input(input: &VerifyMerkleProofInput) -> Result<(), Error> {
267    verify_tx_merkle_proof_input_with_hasher(input, &BitcoinMerkleHasher)
268}
269
270/// Verifies a merkle proof input using a caller-provided node hasher.
271pub fn verify_tx_merkle_proof_input_with_hasher<H: MerkleHasher>(
272    input: &VerifyMerkleProofInput,
273    hasher: &H,
274) -> Result<(), Error> {
275    verify_tx_merkle_proof_with_hasher(
276        input.txid,
277        &input.nodes,
278        &input.bits,
279        input.merkle_root,
280        hasher,
281    )
282}
283
284/// Verifies a merkle proof using a caller-provided node hasher.
285pub fn verify_tx_merkle_proof_with_hasher<H: MerkleHasher>(
286    txid: [u8; 32],
287    nodes: &[[u8; 32]],
288    bits: &[bool],
289    merkle_root: [u8; 32],
290    hasher: &H,
291) -> Result<(), Error> {
292    if nodes.len() != bits.len() {
293        return Err(Error::InvalidMerkleProofShape {
294            nodes: nodes.len(),
295            bits: bits.len(),
296        });
297    }
298
299    let mut current = txid;
300    for (node, is_right) in nodes.iter().zip(bits.iter()) {
301        let (left, right) = if *is_right {
302            (current, *node)
303        } else {
304            (*node, current)
305        };
306        current = hasher.hash_nodes(left, right);
307    }
308
309    if current == merkle_root {
310        Ok(())
311    } else {
312        Err(Error::InvalidTxMerkleProof)
313    }
314}
315
316/// Returns true if the transaction spends the specified outpoint.
317pub fn tx_spends_prev_out(tx: &Transaction, prev_out: &OutPoint) -> bool {
318    tx.input
319        .iter()
320        .any(|input| input.previous_output == *prev_out)
321}
322
323/// Derives a Taproot output key from an internal key and tapscript root.
324fn derive_taproot_output_key<O: TaprootOps>(
325    ops: &O,
326    internal_key: &SerializedKey,
327    tapscript_root: Option<[u8; 32]>,
328) -> Result<SerializedKey, Error> {
329    let internal = ops.parse_internal_key(internal_key)?;
330    ops.taproot_output_key(&internal, tapscript_root)
331        .map_err(Error::from)
332}
333
334/// Extracts the x-only taproot output key from a P2TR script.
335fn extract_taproot_output_key(script: &Script) -> Result<[u8; 32], Error> {
336    if !script.is_p2tr() {
337        return Err(Error::OutputScriptMismatch);
338    }
339
340    let bytes = script.as_bytes();
341    let mut key_bytes = [0u8; 32];
342    key_bytes.copy_from_slice(&bytes[2..34]);
343    Ok(key_bytes)
344}
345
346/// Extracts an x-only key from a serialized compressed public key.
347fn xonly_from_serialized_key(key: &SerializedKey) -> Result<[u8; 32], Error> {
348    match key.bytes[0] {
349        0x02 | 0x03 => {
350            let mut xonly = [0u8; 32];
351            xonly.copy_from_slice(&key.bytes[1..]);
352            Ok(xonly)
353        }
354        _ => Err(Error::InvalidTaprootOutputKey),
355    }
356}