spark_rust/wallet/utils/
bitcoin.rs

1use crate::common_types::types::SecretKey;
2use bitcoin::consensus::{deserialize, Encodable};
3use bitcoin::hashes::Hash;
4use bitcoin::key::TapTweak;
5use bitcoin::secp256k1::Scalar;
6use bitcoin::secp256k1::{PublicKey, Secp256k1};
7use bitcoin::{Address, Network, ScriptBuf, TapTweakHash, Transaction, TxOut, XOnlyPublicKey};
8
9use crate::error::{transaction::TransactionError, validation::ValidationError, SparkSdkError};
10
11// P2TR functions
12pub(crate) fn p2tr_script_from_pubkey(pubkey: &PublicKey, network: Network) -> ScriptBuf {
13    let secp = Secp256k1::new();
14    let xonly = XOnlyPublicKey::from(*pubkey);
15    let address = Address::p2tr(&secp, xonly, None, network);
16    address.script_pubkey()
17}
18
19pub(crate) fn p2tr_address_from_public_key(pubkey: &PublicKey, network: Network) -> Address {
20    let secp = Secp256k1::new();
21    let xonly = XOnlyPublicKey::from(*pubkey);
22
23    Address::p2tr(&secp, xonly, None, network)
24}
25
26// Transaction functions
27pub(crate) fn bitcoin_tx_from_hex(raw_tx_hex: &str) -> Result<Transaction, SparkSdkError> {
28    let tx_bytes = hex::decode(raw_tx_hex).map_err(|e| {
29        SparkSdkError::from(TransactionError::InvalidBitcoinTransaction(format!(
30            "Failed to decode hex: {}",
31            e
32        )))
33    })?;
34    bitcoin_tx_from_bytes(&tx_bytes)
35}
36
37pub(crate) fn serialize_bitcoin_transaction(
38    transaction: &Transaction,
39) -> Result<Vec<u8>, SparkSdkError> {
40    let mut buf = Vec::new();
41    transaction.consensus_encode(&mut buf).map_err(|e| {
42        SparkSdkError::from(TransactionError::InvalidBitcoinTransaction(format!(
43            "Failed to serialize Bitcoin transaction: {}",
44            e
45        )))
46    })?;
47
48    Ok(buf)
49}
50
51pub(crate) fn bitcoin_tx_from_bytes(raw_tx_bytes: &[u8]) -> Result<Transaction, SparkSdkError> {
52    if raw_tx_bytes.is_empty() {
53        return Err(SparkSdkError::from(
54            TransactionError::InvalidBitcoinTransaction(
55                "Cannot deserialize Bitcoin transaction: buffer is empty".to_string(),
56            ),
57        ));
58    }
59
60    let transaction = deserialize(raw_tx_bytes).map_err(|_| {
61        SparkSdkError::from(TransactionError::InvalidBitcoinTransaction(
62            "Invalid transaction".to_string(),
63        ))
64    })?;
65
66    Ok(transaction)
67}
68
69pub(crate) fn sighash_from_tx(
70    tx: &bitcoin::Transaction,
71    input_index: usize,
72    prev_output: &bitcoin::TxOut,
73) -> Result<[u8; 32], SparkSdkError> {
74    let prevouts_arr = [prev_output.clone()];
75    let prev_output_fetcher = bitcoin::sighash::Prevouts::All(&prevouts_arr);
76
77    let sighash = bitcoin::sighash::SighashCache::new(tx)
78        .taproot_key_spend_signature_hash(
79            input_index,
80            &prev_output_fetcher,
81            bitcoin::sighash::TapSighashType::Default,
82        )
83        .map_err(|e| {
84            SparkSdkError::from(ValidationError::InvalidInput {
85                field: format!("Failed to calculate sighash: {}", e),
86            })
87        })?;
88
89    Ok(sighash.to_raw_hash().to_byte_array())
90}
91
92pub(crate) fn sighash_from_tx_new(
93    tx: &Transaction,
94    input_index: usize,
95    prev_output: &TxOut,
96) -> Result<[u8; 32], SparkSdkError> {
97    // Create a vector filled with the same prev_output for all inputs
98    let prevouts = vec![prev_output.clone(); tx.input.len()];
99
100    // Create a PrevoutsFetcher that will return the same prevout for all inputs
101    let prev_output_fetcher = bitcoin::sighash::Prevouts::All(&prevouts);
102
103    // Now calculate the sighash (for Taproot, using SIGHASH_DEFAULT)
104    let sighash = bitcoin::sighash::SighashCache::new(tx)
105        .taproot_key_spend_signature_hash(
106            input_index,
107            &prev_output_fetcher,
108            bitcoin::sighash::TapSighashType::Default,
109        )
110        .map_err(|e| {
111            SparkSdkError::from(ValidationError::InvalidInput {
112                field: format!("Failed to calculate sighash: {}", e),
113            })
114        })?;
115
116    Ok(sighash.to_byte_array())
117}
118
119pub(crate) fn compute_taproot_key_no_script(
120    pubkey: bitcoin::secp256k1::PublicKey,
121) -> Result<bitcoin::XOnlyPublicKey, SparkSdkError> {
122    let (x_only_pub, _) = pubkey.x_only_public_key();
123
124    // BIP341 taproot tweak with empty script tree
125    let (tweaked_key, _parity) = x_only_pub.tap_tweak(&bitcoin::secp256k1::Secp256k1::new(), None);
126
127    Ok(tweaked_key.to_inner())
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133    use bitcoin::absolute::LockTime;
134    use bitcoin::transaction::Version;
135    use bitcoin::Network;
136    use bitcoin::{Amount, OutPoint, ScriptBuf, Transaction, TxIn, TxOut, Witness};
137
138    #[test]
139    fn test_p2tr_address_from_public_key() {
140        let test_vectors = vec![
141            (
142                "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
143                "bc1pmfr3p9j00pfxjh0zmgp99y8zftmd3s5pmedqhyptwy6lm87hf5sspknck9",
144                Network::Bitcoin,
145            ),
146            (
147                "03797dd653040d344fd048c1ad05d4cbcb2178b30c6a0c4276994795f3e833da41",
148                "tb1p8dlmzllfah294ntwatr8j5uuvcj7yg0dete94ck2krrk0ka2c9qqex96hv",
149                Network::Testnet,
150            ),
151        ];
152
153        for (pubkey_hex, expected_addr, network) in test_vectors {
154            let pubkey = PublicKey::from_slice(&hex::decode(pubkey_hex).unwrap()).unwrap();
155            let addr = p2tr_address_from_public_key(&pubkey, network);
156            assert_eq!(&addr.to_string(), expected_addr);
157        }
158    }
159
160    #[test]
161    fn test_tx_from_raw_tx_hex() {
162        let raw_tx_hex = "02000000000102dc552c6c0ef5ed0d8cd64bd1d2d1ffd7cf0ec0b5ad8df2a4c6269b59cffcc696010000000000000000603fbd40e86ee82258c57571c557b89a444aabf5b6a05574e6c6848379febe9a00000000000000000002e86905000000000022512024741d89092c5965f35a63802352fa9c7fae4a23d471b9dceb3379e8ff6b7dd1d054080000000000220020aea091435e74e3c1eba0bd964e67a05f300ace9e73efa66fe54767908f3e68800140f607486d87f59af453d62cffe00b6836d8cca2c89a340fab5fe842b20696908c77fd2f64900feb0cbb1c14da3e02271503fc465fcfb1b043c8187dccdd494558014067dff0f0c321fc8abc28bf555acfdfa5ee889b6909b24bc66cedf05e8cc2750a4d95037c3dc9c24f1e502198bade56fef61a2504809f5b2a60a62afeaf8bf52e00000000";
163        let result_hex = bitcoin_tx_from_hex(raw_tx_hex);
164        assert!(result_hex.is_ok());
165
166        let tx_bytes = hex::decode(raw_tx_hex).unwrap();
167        let result_bytes = bitcoin_tx_from_bytes(&tx_bytes);
168        assert!(result_bytes.is_ok());
169
170        assert_eq!(result_hex.unwrap(), result_bytes.unwrap());
171    }
172
173    #[test]
174    fn test_sighash_from_tx() {
175        let prev_tx_hex = "020000000001010cb9feccc0bdaac30304e469c50b4420c13c43d466e13813fcf42a73defd3f010000000000ffffffff018038010000000000225120d21e50e12ae122b4a5662c09b67cec7449c8182913bc06761e8b65f0fa2242f701400536f9b7542799f98739eeb6c6adaeb12d7bd418771bc5c6847f2abd19297bd466153600af26ccf0accb605c11ad667c842c5713832af4b7b11f1bcebe57745900000000";
176        let prev_tx = bitcoin_tx_from_hex(prev_tx_hex).unwrap();
177
178        let tx = Transaction {
179            version: Version::TWO,
180            lock_time: LockTime::ZERO,
181            input: vec![TxIn {
182                previous_output: OutPoint {
183                    txid: prev_tx.compute_txid(),
184                    vout: 0,
185                },
186                script_sig: ScriptBuf::new(),
187                sequence: bitcoin::Sequence::MAX,
188                witness: Witness::default(),
189            }],
190            output: vec![TxOut {
191                value: Amount::from_sat(70_000),
192                script_pubkey: prev_tx.output[0].script_pubkey.clone(),
193            }],
194        };
195
196        let sighash = sighash_from_tx(&tx, 0, &prev_tx.output[0]).unwrap();
197        assert_eq!(
198            hex::encode(sighash),
199            "8da5e7aa2b03491d7c2f4359ea4968dd58f69adf9af1a2c6881be0295591c293"
200        );
201    }
202}
203
204/// Computes a taproot key without script paths, equivalent to computeTaprootKeyNoScript in JS
205///
206/// # Arguments
207/// * `pubkey` - The x-only public key (32 bytes)
208///
209/// # Returns
210/// * The tweaked x-only public key (32 bytes)
211#[cfg_attr(feature = "telemetry", tracing::instrument(skip_all))]
212pub fn compute_taproot_key_no_script_from_internal_key(
213    pubkey: &[u8],
214) -> Result<[u8; 33], SparkSdkError> {
215    // Validate input
216    if pubkey.len() != 32 {
217        return Err(SparkSdkError::from(ValidationError::InvalidInput {
218            field: "Public key must be 32 bytes".to_string(),
219        }));
220    }
221
222    // Create secp context
223    let secp = Secp256k1::new();
224
225    // Parse the x-only public key
226    let x_only_key = XOnlyPublicKey::from_slice(pubkey).map_err(|_| {
227        SparkSdkError::from(ValidationError::InvalidInput {
228            field: "Invalid x-only public key".to_string(),
229        })
230    })?;
231
232    // Compute the taproot tweak (tagged hash)
233    let tweak = TapTweakHash::hash(pubkey);
234    let tweak_bytes = tweak.to_byte_array();
235    let tweak_scalar = Scalar::from_be_bytes(tweak_bytes).map_err(|_| {
236        SparkSdkError::from(ValidationError::InvalidInput {
237            field: "Failed to convert tweak to scalar".to_string(),
238        })
239    })?;
240
241    // Apply the tweak to the public key
242    let (tweaked_key, _parity) = x_only_key.add_tweak(&secp, &tweak_scalar).map_err(|_| {
243        SparkSdkError::from(ValidationError::InvalidInput {
244            field: "Failed to tweak public key".to_string(),
245        })
246    })?;
247
248    // Construct the full public key
249    let taproot_key = PublicKey::from_x_only_public_key(tweaked_key, _parity);
250
251    // Return the serialized tweaked key
252    Ok(taproot_key.serialize())
253}
254
255pub fn parse_secret_key(bytes: &Vec<u8>) -> Result<SecretKey, SparkSdkError> {
256    let secret_key = bitcoin::secp256k1::SecretKey::from_slice(bytes).map_err(|e| {
257        SparkSdkError::from(ValidationError::InvalidArgument {
258            argument: format!("Private key is not valid: {}", e),
259        })
260    })?;
261
262    Ok(secret_key)
263}
264
265pub fn parse_public_key(bytes: &Vec<u8>) -> Result<PublicKey, SparkSdkError> {
266    let public_key = bitcoin::secp256k1::PublicKey::from_slice(bytes).map_err(|e| {
267        SparkSdkError::from(ValidationError::InvalidArgument {
268            argument: format!("Public key is not valid: {}", e),
269        })
270    })?;
271
272    Ok(public_key)
273}