Skip to main content

csv_adapter_bitcoin/
bip341.rs

1//! BIP-341 Taproot key derivation and output key tweaking
2//!
3//! Implements the tap_tweak computation needed to derive P2TR output keys
4//! from internal keys and merkle roots.
5
6use bitcoin::{
7    key::{TapTweak, XOnlyPublicKey},
8    secp256k1::Secp256k1,
9    taproot::TaprootSpendInfo,
10    Address, Network, ScriptBuf,
11};
12
13use csv_adapter_core::hash::Hash as CsvHash;
14
15/// Tapret commitment for output key derivation
16#[derive(Clone, Debug, PartialEq, Eq)]
17pub struct TapretCommitment {
18    pub protocol_id: [u8; 32],
19    pub commitment: CsvHash,
20}
21
22impl TapretCommitment {
23    pub fn new(protocol_id: [u8; 32], commitment: CsvHash) -> Self {
24        Self {
25            protocol_id,
26            commitment,
27        }
28    }
29
30    pub fn leaf_script(&self) -> ScriptBuf {
31        use bitcoin::{
32            opcodes::all::OP_RETURN,
33            script::{Builder, PushBytesBuf},
34        };
35        let mut payload = [0u8; 64];
36        payload[..32].copy_from_slice(&self.protocol_id);
37        payload[32..].copy_from_slice(self.commitment.as_bytes());
38        let push_bytes = PushBytesBuf::try_from(payload.to_vec()).unwrap();
39        Builder::new()
40            .push_opcode(OP_RETURN)
41            .push_slice(push_bytes)
42            .into_script()
43    }
44}
45
46/// Derive a BIP-341 tweaked output key from an internal key and optional merkle root
47///
48/// Q = P + tapTweak(P || merkle_root) * G
49pub fn derive_output_key(
50    internal_key: XOnlyPublicKey,
51    merkle_root: Option<bitcoin::taproot::TapNodeHash>,
52) -> Result<XOnlyPublicKey, Bip341Error> {
53    let secp = Secp256k1::new();
54    let tweaked = internal_key.tap_tweak(&secp, merkle_root);
55    // tweaked is (TweakedPublicKey, Parity) — we need the inner XOnlyPublicKey
56    Ok(tweaked.0.to_inner())
57}
58
59/// Build a Taproot output from spend info
60pub struct TaprootOutput {
61    pub output_key: XOnlyPublicKey,
62    pub merkle_root: Option<bitcoin::taproot::TapNodeHash>,
63    pub spend_info: Option<TaprootSpendInfo>,
64}
65
66impl TaprootOutput {
67    pub fn to_address(&self, network: Network) -> Address {
68        if let Some(ref spend_info) = self.spend_info {
69            Address::p2tr_tweaked(spend_info.output_key(), network)
70        } else {
71            // Key-path only — create a tweaked key with no merkle root
72            let secp = Secp256k1::new();
73            let tweaked = self.output_key.tap_tweak(&secp, None);
74            Address::p2tr_tweaked(tweaked.0, network)
75        }
76    }
77}
78
79/// BIP-341 error types
80#[derive(Debug, thiserror::Error)]
81pub enum Bip341Error {
82    #[error("Invalid internal key")]
83    InvalidKey,
84}
85
86/// Generate a test internal key pair for development
87pub fn generate_test_keypair() -> (secp256k1::SecretKey, XOnlyPublicKey) {
88    use secp256k1::rand::thread_rng as secp_rng;
89    let secp = Secp256k1::new();
90    let mut rng = secp_rng();
91    let secret_key = secp256k1::SecretKey::new(&mut rng);
92    let public_key = secp256k1::PublicKey::from_secret_key(&secp, &secret_key);
93    let xonly = XOnlyPublicKey::from(public_key);
94    (secret_key, xonly)
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn test_derive_output_key_no_merkle_root() {
103        let (_, internal_key) = generate_test_keypair();
104        let output_key = derive_output_key(internal_key, None).unwrap();
105        assert_eq!(output_key.serialize().len(), 32);
106    }
107
108    #[test]
109    fn test_tapret_commitment_leaf_script() {
110        let tapret = TapretCommitment::new([1u8; 32], CsvHash::new([2u8; 32]));
111        let script = tapret.leaf_script();
112        assert!(script.is_op_return());
113        assert_eq!(script.len(), 66);
114    }
115
116    #[test]
117    fn test_output_key_deterministic() {
118        use secp256k1::SecretKey;
119
120        let secp = Secp256k1::new();
121        let secret = SecretKey::from_slice(&[0xAB; 32]).unwrap();
122        let public_key = secp256k1::PublicKey::from_secret_key(&secp, &secret);
123        let internal_key = XOnlyPublicKey::from(public_key);
124
125        let output1 = derive_output_key(internal_key, None).unwrap();
126        let output2 = derive_output_key(internal_key, None).unwrap();
127        assert_eq!(output1, output2);
128    }
129
130    #[test]
131    fn test_output_key_differs_by_merkle_root() {
132        use std::str::FromStr;
133        let (_, internal_key) = generate_test_keypair();
134        let output_no_root = derive_output_key(internal_key, None).unwrap();
135
136        // Create a fake merkle root
137        let fake_root = bitcoin::taproot::TapNodeHash::from_str(
138            "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
139        )
140        .unwrap();
141        let output_with_root = derive_output_key(internal_key, Some(fake_root)).unwrap();
142        assert_ne!(output_no_root, output_with_root);
143    }
144}