Skip to main content

csv_adapter_core/
tapret_verify.rs

1//! RGB Tapret commitment verification
2//!
3//! Implements Tapret verification per LNP/BP standard #6 and BIP-341:
4//! 1. Verify the Tapret script structure (OP_RETURN <protocol_id || nonce || commitment>)
5//! 2. Verify the commitment is actually embedded in the script
6//! 3. Verify the control block is well-formed
7//! 4. Verify internal key + merkle root → output key (BIP-341 tap tweak)
8//!
9//! Note: Full merkle proof verification requires the TaprootBuilder API which
10//! has specific type requirements in rust-bitcoin 0.30. This module provides
11//! structural verification. For full merkle verification, use with the
12//! rust-bitcoin TaprootBuilder directly.
13
14use bitcoin::hashes::Hash as _;
15use bitcoin::key::{TapTweak, XOnlyPublicKey};
16use bitcoin::opcodes::all::OP_RETURN;
17use bitcoin::script::{Builder, PushBytesBuf, ScriptBuf};
18use bitcoin::secp256k1::{Secp256k1, Verification};
19use bitcoin::taproot::TapNodeHash;
20use sha2::{Digest, Sha256};
21
22use crate::hash::Hash;
23
24/// Result of Tapret commitment verification
25#[derive(Debug)]
26pub struct TapretVerificationResult {
27    /// Whether the Tapret commitment is valid
28    pub is_valid: bool,
29    /// The verified output key (if verification succeeded)
30    pub output_key: Option<[u8; 32]>,
31    /// The verified internal key (if verification succeeded)
32    pub internal_key: Option<[u8; 32]>,
33    /// Whether the commitment was found in the script
34    pub commitment_found: bool,
35    /// Whether the script structure is valid
36    pub script_valid: bool,
37    /// Detailed error message if verification failed
38    pub error: Option<String>,
39}
40
41/// Verify a Tapret commitment against RGB/LNP-BP standard #6
42///
43/// This performs structural verification:
44/// 1. Tapret script has correct structure: OP_RETURN <protocol_id (32) || nonce (1) || commitment (32)>
45/// 2. The expected commitment is embedded at the correct offset in the script
46/// 3. Script is well-formed (correct opcodes, push sizes)
47///
48/// For full merkle verification (control block + output key),
49/// use [`verify_tapret_output_key`] with the actual TaprootBuilder.
50///
51/// # Arguments
52/// * `tapret_script` - The Tapret leaf script
53/// * `expected_commitment` - The expected commitment hash
54///
55/// # Returns
56/// Verification result with detailed diagnostics
57pub fn verify_tapret_script(
58    tapret_script: &ScriptBuf,
59    expected_commitment: Hash,
60) -> TapretVerificationResult {
61    // Step 1: Verify script structure
62    let script_valid = verify_tapret_script_structure(tapret_script);
63
64    if !script_valid {
65        return TapretVerificationResult {
66            is_valid: false,
67            output_key: None,
68            internal_key: None,
69            commitment_found: false,
70            script_valid: false,
71            error: Some("Invalid Tapret script structure".to_string()),
72        };
73    }
74
75    // Step 2: Verify commitment is embedded
76    let commitment_found = verify_commitment_in_script(tapret_script, expected_commitment);
77
78    if !commitment_found {
79        return TapretVerificationResult {
80            is_valid: false,
81            output_key: None,
82            internal_key: None,
83            commitment_found: false,
84            script_valid: true,
85            error: Some("Commitment not found in Tapret script".to_string()),
86        };
87    }
88
89    TapretVerificationResult {
90        is_valid: true,
91        output_key: None,
92        internal_key: None,
93        commitment_found: true,
94        script_valid: true,
95        error: None,
96    }
97}
98
99/// Verify the Taproot output key derivation (BIP-341)
100///
101/// Computes: output_key = internal_key + tap_tweak(internal_key, merkle_root)
102///
103/// # Arguments
104/// * `secp` - Secp256k1 context
105/// * `internal_key` - The internal (untweaked) public key
106/// * `merkle_root` - The merkle root of the Taproot tree (None for key-path only)
107/// * `expected_output_key` - The expected output key to verify against
108///
109/// # Returns
110/// The derived output key, or None if derivation failed
111pub fn verify_tapret_output_key<C: Verification>(
112    secp: &Secp256k1<C>,
113    internal_key: XOnlyPublicKey,
114    merkle_root: Option<[u8; 32]>,
115    expected_output_key: XOnlyPublicKey,
116) -> bool {
117    let merkle_root_hash = merkle_root.map(TapNodeHash::from_byte_array);
118
119    let (tweaked_key, _parity) = internal_key.tap_tweak(secp, merkle_root_hash);
120    let tweaked_xonly = tweaked_key.to_inner();
121
122    tweaked_xonly == expected_output_key
123}
124
125/// Verify the Tapret script has the correct structure
126///
127/// RGB Tapret script: OP_RETURN <protocol_id (32) || nonce (1) || commitment (32)>
128/// Total: 1 (OP_RETURN) + 1 (push) + 65 (data) = 67 bytes
129fn verify_tapret_script_structure(script: &ScriptBuf) -> bool {
130    let bytes = script.as_bytes();
131
132    // Minimum: OP_RETURN (1) + OP_PUSHBYTES_65 (1) + 65 bytes data = 67 bytes
133    if bytes.len() < 67 {
134        return false;
135    }
136
137    // First byte must be OP_RETURN (0x6a)
138    if bytes[0] != 0x6a {
139        return false;
140    }
141
142    // Second byte should be OP_PUSHBYTES_65 (0x41)
143    if bytes[1] != 0x41 {
144        return false;
145    }
146
147    // Must have exactly 65 bytes of data after the push
148    bytes.len() == 67
149}
150
151/// Verify the commitment is embedded in the Tapret script
152///
153/// The commitment is at offset 33 in the 65-byte data:
154/// [protocol_id (32 bytes)] [nonce (1 byte)] [commitment (32 bytes)]
155fn verify_commitment_in_script(script: &ScriptBuf, expected: Hash) -> bool {
156    let bytes = script.as_bytes();
157
158    if bytes.len() < 67 {
159        return false;
160    }
161
162    // Commitment starts at byte 2 (after OP_RETURN + push) + 33 (protocol_id + nonce)
163    let commitment_offset = 2 + 33;
164
165    if bytes.len() < commitment_offset + 32 {
166        return false;
167    }
168
169    let embedded_commitment = &bytes[commitment_offset..commitment_offset + 32];
170    embedded_commitment == expected.as_bytes()
171}
172
173/// Create a Tapret commitment script for testing
174///
175/// # Arguments
176/// * `protocol_id` - The protocol identifier (32 bytes)
177/// * `nonce` - The nonce (1 byte)
178/// * `commitment` - The commitment hash (32 bytes)
179pub fn create_tapret_script(protocol_id: [u8; 32], nonce: u8, commitment: Hash) -> ScriptBuf {
180    let mut data = [0u8; 65];
181    data[..32].copy_from_slice(&protocol_id);
182    data[32] = nonce;
183    data[33..65].copy_from_slice(commitment.as_bytes());
184
185    let push_bytes = PushBytesBuf::try_from(data.to_vec()).unwrap();
186    Builder::new()
187        .push_opcode(OP_RETURN)
188        .push_slice(push_bytes)
189        .into_script()
190}
191
192/// Compute the TapTweak hash (BIP-341)
193///
194/// tap_tweak = SHA256(internal_key || merkle_root)
195pub fn compute_tap_tweak_hash(internal_key: [u8; 32], merkle_root: Option<[u8; 32]>) -> [u8; 32] {
196    let mut hasher = Sha256::new();
197    hasher.update(internal_key);
198    if let Some(root) = merkle_root {
199        hasher.update(root);
200    }
201    hasher.finalize().into()
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207
208    #[test]
209    fn test_valid_tapret_script_structure() {
210        let commitment = Hash::new([0xAB; 32]);
211        let script = create_tapret_script([0x01; 32], 0x42, commitment);
212        assert!(verify_tapret_script_structure(&script));
213    }
214
215    #[test]
216    fn test_invalid_script_too_short() {
217        let short_script = ScriptBuf::from_bytes(vec![0x6a, 0x41, 0x01]);
218        assert!(!verify_tapret_script_structure(&short_script));
219    }
220
221    #[test]
222    fn test_invalid_script_not_op_return() {
223        let script = ScriptBuf::from_bytes(vec![0x00; 67]);
224        assert!(!verify_tapret_script_structure(&script));
225    }
226
227    #[test]
228    fn test_invalid_script_wrong_push() {
229        let mut bytes = vec![0x6a, 0x40]; // OP_RETURN + OP_PUSHBYTES_64 (wrong size)
230        bytes.resize(67, 0x00);
231        let script = ScriptBuf::from_bytes(bytes);
232        assert!(!verify_tapret_script_structure(&script));
233    }
234
235    #[test]
236    fn test_commitment_in_script() {
237        let commitment = Hash::new([0xCD; 32]);
238        let script = create_tapret_script([0x01; 32], 0x42, commitment);
239        assert!(verify_commitment_in_script(&script, commitment));
240
241        // Wrong commitment should fail
242        let wrong_commitment = Hash::new([0xFF; 32]);
243        assert!(!verify_commitment_in_script(&script, wrong_commitment));
244    }
245
246    #[test]
247    fn test_full_tapret_verification_valid() {
248        let commitment = Hash::new([0xAB; 32]);
249        let script = create_tapret_script([0x01; 32], 0x42, commitment);
250        let result = verify_tapret_script(&script, commitment);
251        assert!(result.is_valid);
252        assert!(result.commitment_found);
253        assert!(result.script_valid);
254        assert!(result.error.is_none());
255    }
256
257    #[test]
258    fn test_full_tapret_verification_wrong_commitment() {
259        let commitment = Hash::new([0xAB; 32]);
260        let script = create_tapret_script([0x01; 32], 0x42, commitment);
261        let wrong_commitment = Hash::new([0xFF; 32]);
262        let result = verify_tapret_script(&script, wrong_commitment);
263        assert!(!result.is_valid);
264        assert!(!result.commitment_found);
265        assert!(result.script_valid);
266    }
267
268    #[test]
269    fn test_tap_tweak_hash_deterministic() {
270        let key = [0x01; 32];
271        let root = Some([0x02; 32]);
272
273        let h1 = compute_tap_tweak_hash(key, root);
274        let h2 = compute_tap_tweak_hash(key, root);
275        assert_eq!(h1, h2);
276    }
277
278    #[test]
279    fn test_tap_tweak_hash_different_roots() {
280        let key = [0x01; 32];
281
282        let h1 = compute_tap_tweak_hash(key, Some([0x02; 32]));
283        let h2 = compute_tap_tweak_hash(key, Some([0x03; 32]));
284        assert_ne!(h1, h2);
285    }
286
287    #[test]
288    fn test_tap_tweak_hash_no_root() {
289        let key = [0x01; 32];
290        let h = compute_tap_tweak_hash(key, None);
291        assert_ne!(h, [0u8; 32]);
292    }
293}