Skip to main content

csv_adapter_bitcoin/
adapter.rs

1//! Bitcoin AnchorLayer implementation with HD wallet support
2//!
3//! This adapter implements the AnchorLayer trait for Bitcoin,
4//! using UTXOs as single-use seals and Tapret commitments for anchoring.
5//!
6//! ## Architecture
7//!
8//! - **Seals**: UTXOs locked to Taproot output keys derived from HD wallet
9//! - **Commitments**: Published via Taproot OP_RETURN or Tapscript tapret
10//! - **Finality**: Based on confirmation depth
11
12#![allow(dead_code)]
13
14use bitcoin;
15use bitcoin_hashes::Hash as _;
16use std::sync::Mutex;
17
18use csv_adapter_core::commitment::Commitment;
19use csv_adapter_core::dag::DAGSegment;
20use csv_adapter_core::error::AdapterError;
21use csv_adapter_core::error::Result as CoreResult;
22use csv_adapter_core::proof::{FinalityProof, ProofBundle};
23use csv_adapter_core::seal::AnchorRef as CoreAnchorRef;
24use csv_adapter_core::seal::SealRef as CoreSealRef;
25use csv_adapter_core::AnchorLayer;
26use csv_adapter_core::Hash;
27
28use crate::config::BitcoinConfig;
29use crate::error::{BitcoinError, BitcoinResult};
30use crate::rpc::BitcoinRpc;
31use crate::seal::SealRegistry;
32use crate::tx_builder::CommitmentTxBuilder;
33use crate::types::{BitcoinAnchorRef, BitcoinFinalityProof, BitcoinInclusionProof, BitcoinSealRef};
34use crate::wallet::SealWallet;
35
36/// Bitcoin implementation of the AnchorLayer trait with HD wallet support
37pub struct BitcoinAnchorLayer {
38    config: BitcoinConfig,
39    wallet: SealWallet,
40    tx_builder: CommitmentTxBuilder,
41    seal_registry: Mutex<SealRegistry>,
42    domain_separator: [u8; 32],
43    /// RPC client for broadcasting transactions (optional)
44    rpc: Option<Box<dyn BitcoinRpc + Send + Sync>>,
45    next_seal_index: Mutex<u32>,
46}
47
48impl BitcoinAnchorLayer {
49    /// Create with an existing HD wallet.
50    pub fn with_wallet(config: BitcoinConfig, wallet: SealWallet) -> BitcoinResult<Self> {
51        let mut domain = [0u8; 32];
52        domain[..8].copy_from_slice(b"CSV-BTC-");
53        let magic = config.network.magic_bytes();
54        domain[8..12].copy_from_slice(&magic);
55
56        let mut protocol_id = [0u8; 32];
57        let magic = config.network.magic_bytes();
58        protocol_id[..4].copy_from_slice(&magic);
59        let tx_builder = CommitmentTxBuilder::new(protocol_id, config.finality_depth as u64);
60
61        Ok(Self {
62            config,
63            wallet,
64            tx_builder,
65            seal_registry: Mutex::new(SealRegistry::new()),
66            domain_separator: domain,
67            rpc: None,
68            next_seal_index: Mutex::new(0),
69        })
70    }
71
72    /// Create a new adapter with an HD wallet from an xpub key
73    pub fn from_xpub(config: BitcoinConfig, xpub: &str) -> BitcoinResult<Self> {
74        let wallet = SealWallet::from_xpub(xpub, config.network.to_bitcoin_network())
75            .map_err(|e| BitcoinError::RpcError(format!("Wallet creation failed: {}", e)))?;
76        Self::with_wallet(config, wallet)
77    }
78
79    /// Create with default config for signet (random wallet)
80    pub fn signet() -> BitcoinResult<Self> {
81        let wallet = SealWallet::generate_random(bitcoin::Network::Signet);
82        Self::with_wallet(BitcoinConfig::default(), wallet)
83    }
84
85    /// Attach a real RPC client for broadcasting transactions
86    pub fn with_rpc(mut self, rpc: Box<dyn BitcoinRpc + Send + Sync>) -> Self {
87        self.rpc = Some(rpc);
88        self
89    }
90
91    /// Get a reference to the wallet
92    pub fn wallet(&self) -> &SealWallet {
93        &self.wallet
94    }
95
96    /// Get a mutable reference to the tx_builder
97    pub fn tx_builder_mut(&mut self) -> &mut CommitmentTxBuilder {
98        &mut self.tx_builder
99    }
100
101    /// Derive a new seal at the next available path
102    fn derive_next_seal(
103        &self,
104        value_sat: u64,
105    ) -> Result<(BitcoinSealRef, crate::wallet::Bip86Path), AdapterError> {
106        let mut next_index = self
107            .next_seal_index
108            .lock()
109            .unwrap_or_else(|e| e.into_inner());
110        let path = crate::wallet::Bip86Path::external(0, *next_index);
111
112        // Derive the Taproot key for this path
113        let key = self
114            .wallet
115            .derive_key(&path)
116            .map_err(|e| AdapterError::Generic(format!("Key derivation failed: {}", e)))?;
117
118        // Create a seal reference from the derived key
119        let txid: [u8; 32] = key.output_key.serialize();
120
121        *next_index += 1;
122
123        Ok((BitcoinSealRef::new(txid, 0, Some(value_sat)), path))
124    }
125
126    /// Create a seal backed by a real on-chain UTXO
127    ///
128    /// This method creates a seal from an existing UTXO in the wallet.
129    /// The UTXO must have been previously added to the wallet via `add_utxo()`
130    /// or discovered via `scan_wallet_for_utxos()`.
131    ///
132    /// # Arguments
133    /// * `outpoint` - The outpoint of the UTXO to use as the seal
134    ///
135    /// # Returns
136    /// A seal reference and the derivation path, or an error if the UTXO is not found
137    pub fn fund_seal(
138        &self,
139        outpoint: bitcoin::OutPoint,
140    ) -> Result<(BitcoinSealRef, crate::wallet::Bip86Path), AdapterError> {
141        // Get the UTXO from the wallet
142        let utxo = self.wallet.get_utxo(&outpoint).ok_or_else(|| {
143            AdapterError::Generic(format!(
144                "UTXO {}:{} not found in wallet - fund the address first",
145                outpoint.txid, outpoint.vout
146            ))
147        })?;
148
149        // Create a seal reference from the actual outpoint
150        let txid = outpoint.txid.to_byte_array();
151        let seal_ref = BitcoinSealRef::new(txid, outpoint.vout, Some(utxo.amount_sat));
152
153        // Check if seal is already used
154        if self
155            .seal_registry
156            .lock()
157            .unwrap_or_else(|e| e.into_inner())
158            .is_seal_used(&seal_ref)
159        {
160            return Err(AdapterError::Generic(format!(
161                "Seal {}:{} already used",
162                outpoint.txid, outpoint.vout
163            )));
164        }
165
166        Ok((seal_ref, utxo.path))
167    }
168
169    /// Scan the wallet's addresses for on-chain UTXOs
170    ///
171    /// This method requires an RPC client to be attached. It will scan addresses
172    /// and populate the wallet with any discovered UTXOs.
173    ///
174    /// # Arguments
175    /// * `account` - The account number to scan (typically 0)
176    /// * `gap_limit` - Number of consecutive empty addresses before stopping (typically 20)
177    ///
178    /// # Returns
179    /// The number of UTXOs discovered
180    pub fn scan_wallet_for_utxos(
181        &self,
182        account: u32,
183        gap_limit: usize,
184    ) -> Result<usize, AdapterError> {
185        use bitcoin::Address;
186
187        let rpc = self.rpc.as_ref().ok_or_else(|| {
188            AdapterError::Generic("No RPC client configured - call with_rpc() first".to_string())
189        })?;
190
191        let wallet = &self.wallet;
192        let utxos_discovered = wallet
193            .scan_chain_for_utxos(
194                |address: &Address| {
195                    // Use the RPC to fetch UTXOs for this address
196                    match get_address_utxos(rpc.as_ref(), address) {
197                        Ok(utxos) => Ok(utxos),
198                        Err(e) => Err(e.to_string()),
199                    }
200                },
201                account,
202                gap_limit,
203            )
204            .map_err(|e| AdapterError::Generic(format!("Failed to scan chain for UTXOs: {}", e)))?;
205
206        log::info!(
207            "Discovered {} UTXOs on account {}",
208            utxos_discovered,
209            account
210        );
211        Ok(utxos_discovered)
212    }
213
214    /// Build commitment data for a commitment transaction
215    pub fn build_commitment_data(
216        &self,
217        commitment: Hash,
218        protocol_id: [u8; 32],
219    ) -> Result<crate::tx_builder::CommitmentData, AdapterError> {
220        let tx_builder = CommitmentTxBuilder::new(protocol_id, 10);
221        Ok(tx_builder.build_commitment_data(commitment))
222    }
223
224    /// Get current block height (would call RPC in production)
225    fn get_current_height(&self) -> u64 {
226        if let Some(rpc) = &self.rpc {
227            if let Ok(h) = rpc.get_block_count() {
228                return h;
229            }
230        }
231        200
232    }
233
234    /// Get current block height (public, for testing)
235    pub fn get_current_height_for_test(&self) -> u64 {
236        self.get_current_height()
237    }
238
239    /// Verify a UTXO is unspent
240    fn verify_utxo_unspent(&self, seal: &BitcoinSealRef) -> BitcoinResult<()> {
241        if let Some(rpc) = &self.rpc {
242            let unspent = rpc
243                .is_utxo_unspent(seal.txid, seal.vout)
244                .map_err(|e| BitcoinError::RpcError(format!("Failed to check UTXO: {}", e)))?;
245            if unspent {
246                return Ok(());
247            } else {
248                return Err(BitcoinError::UTXOSpent(format!(
249                    "UTXO {}:{} is spent",
250                    seal.txid_hex(),
251                    seal.vout
252                )));
253            }
254        }
255        // In mock mode, always return OK
256        Ok(())
257    }
258
259    /// Get the funding address for a specific account and index
260    pub fn get_funding_address(
261        &self,
262        account: u32,
263        index: u32,
264    ) -> Result<bitcoin::Address, AdapterError> {
265        let key = self
266            .wallet
267            .get_funding_address(account, index)
268            .map_err(|e| AdapterError::Generic(format!("Failed to derive address: {}", e)))?;
269        Ok(key.address)
270    }
271
272    /// Add a UTXO to the wallet from a known outpoint
273    pub fn add_utxo(&self, outpoint: bitcoin::OutPoint, amount_sat: u64, account: u32, index: u32) {
274        let path = crate::wallet::Bip86Path::external(account, index);
275        self.wallet.add_utxo(outpoint, amount_sat, path);
276    }
277}
278
279/// Helper to get address UTXOs from any RPC implementation
280fn get_address_utxos(
281    _rpc: &dyn BitcoinRpc,
282    _address: &bitcoin::Address,
283) -> Result<Vec<(bitcoin::OutPoint, u64)>, String> {
284    // This is a placeholder - actual implementation depends on the RPC backend
285    // For mempool.space, we'd use REST API
286    // For bitcoincore-rpc, we'd use listunspent
287    // The adapter's scan_wallet_for_utxos handles this via the wallet's callback
288    Err("get_address_utxos not implemented for this RPC backend".to_string())
289}
290
291impl AnchorLayer for BitcoinAnchorLayer {
292    type SealRef = BitcoinSealRef;
293    type AnchorRef = BitcoinAnchorRef;
294    type InclusionProof = BitcoinInclusionProof;
295    type FinalityProof = BitcoinFinalityProof;
296
297    fn publish(&self, commitment: Hash, seal: Self::SealRef) -> CoreResult<Self::AnchorRef> {
298        self.verify_utxo_unspent(&seal)
299            .map_err(AdapterError::from)?;
300
301        // If RPC client is available, use real broadcasting
302        if let Some(rpc) = &self.rpc {
303            // Find the UTXO matching this seal in the wallet
304            let outpoint = bitcoin::OutPoint::new(
305                bitcoin::Txid::from_slice(&seal.txid)
306                    .map_err(|e| AdapterError::Generic(format!("Invalid seal txid: {}", e)))?,
307                seal.vout,
308            );
309            let utxo = self.wallet.get_utxo(&outpoint).ok_or_else(|| {
310                AdapterError::PublishFailed(format!(
311                    "UTXO {}:{} not found in wallet",
312                    seal.txid_hex(),
313                    seal.vout
314                ))
315            })?;
316
317            // Build and sign the Taproot commitment transaction
318            let tx_result = self
319                .tx_builder
320                .build_commitment_tx(
321                    &self.wallet,
322                    &utxo,
323                    *commitment.as_bytes(),
324                    None, // No change path — single UTXO, single output
325                )
326                .map_err(|e| AdapterError::PublishFailed(e.to_string()))?;
327
328            // Broadcast the signed transaction via RPC
329            let broadcast_txid =
330                rpc.send_raw_transaction(tx_result.raw_tx.clone())
331                    .map_err(|e| {
332                        AdapterError::PublishFailed(format!(
333                            "Failed to broadcast transaction: {}",
334                            e
335                        ))
336                    })?;
337
338            log::info!(
339                "Published commitment tx {} on {:?} (tx_builder txid: {})",
340                hex::encode(broadcast_txid),
341                self.config.network,
342                tx_result.txid,
343            );
344
345            let current_height = self.get_current_height();
346            return Ok(BitcoinAnchorRef::new(broadcast_txid, 0, current_height));
347        }
348
349        // Fall back to mock mode if no RPC client attached
350        let mut txid = [0u8; 32];
351        txid[..10].copy_from_slice(b"sim-commit");
352        txid[10..].copy_from_slice(&commitment.as_bytes()[..22]);
353
354        let mut registry = self.seal_registry.lock().unwrap_or_else(|e| e.into_inner());
355        let _ = registry.mark_seal_used(&seal).map_err(AdapterError::from);
356
357        let current_height = self.get_current_height();
358        Ok(BitcoinAnchorRef::new(txid, 0, current_height))
359    }
360
361    fn verify_inclusion(&self, anchor: Self::AnchorRef) -> CoreResult<Self::InclusionProof> {
362        // If we have an RPC client, fetch real Merkle proof from the blockchain
363        if let Some(rpc) = &self.rpc {
364            // Get the block containing the anchor transaction
365            let block_hash = rpc.get_block_hash(anchor.block_height).map_err(|e| {
366                AdapterError::InclusionProofFailed(format!("Failed to get block hash: {}", e))
367            })?;
368
369            // Extract the Merkle proof for the anchor transaction
370            // This would require block fetching which is RPC-backend specific
371            // For now, return a proof with just the block hash
372            return Ok(BitcoinInclusionProof::new(
373                vec![],
374                block_hash,
375                0,
376                anchor.block_height,
377            ));
378        }
379
380        // Without RPC, return empty proof (mock mode)
381        let proof = BitcoinInclusionProof::new(vec![], anchor.txid, 0, anchor.block_height);
382        Ok(proof)
383    }
384
385    fn verify_finality(&self, anchor: Self::AnchorRef) -> CoreResult<Self::FinalityProof> {
386        let current_height = self.get_current_height();
387
388        if anchor.block_height == 0 {
389            let confirmations = self.config.finality_depth as u64;
390            let proof = BitcoinFinalityProof::new(confirmations, self.config.finality_depth);
391            return Ok(proof);
392        }
393
394        let confirmations = current_height.saturating_sub(anchor.block_height);
395        let proof = BitcoinFinalityProof::new(confirmations, self.config.finality_depth);
396
397        if !proof.meets_required_depth {
398            return Err(AdapterError::FinalityNotReached(format!(
399                "Only {} confirmations, need {}",
400                confirmations, self.config.finality_depth
401            )));
402        }
403
404        Ok(proof)
405    }
406
407    fn enforce_seal(&self, seal: Self::SealRef) -> CoreResult<()> {
408        let mut registry = self.seal_registry.lock().unwrap_or_else(|e| e.into_inner());
409        registry.mark_seal_used(&seal).map_err(AdapterError::from)
410    }
411
412    fn create_seal(&self, value: Option<u64>) -> CoreResult<Self::SealRef> {
413        let value_sat = value.unwrap_or(100_000);
414        let (seal_ref, _path) = self.derive_next_seal(value_sat)?;
415        Ok(seal_ref)
416    }
417
418    fn hash_commitment(
419        &self,
420        contract_id: Hash,
421        previous_commitment: Hash,
422        transition_payload_hash: Hash,
423        seal_ref: &Self::SealRef,
424    ) -> Hash {
425        let core_seal =
426            CoreSealRef::new(seal_ref.to_vec(), seal_ref.nonce).expect("valid seal reference");
427        Commitment::simple(
428            contract_id,
429            previous_commitment,
430            transition_payload_hash,
431            &core_seal,
432            self.domain_separator,
433        )
434        .hash()
435    }
436
437    fn build_proof_bundle(
438        &self,
439        anchor: Self::AnchorRef,
440        transition_dag: DAGSegment,
441    ) -> CoreResult<ProofBundle> {
442        let inclusion = self.verify_inclusion(anchor.clone())?;
443        let finality = self.verify_finality(anchor.clone())?;
444
445        let seal_ref = CoreSealRef::new(anchor.txid.to_vec(), Some(0))
446            .map_err(|e| AdapterError::Generic(e.to_string()))?;
447
448        let anchor_ref = CoreAnchorRef::new(anchor.txid.to_vec(), anchor.block_height, vec![])
449            .map_err(|e| AdapterError::Generic(e.to_string()))?;
450
451        let mut proof_bytes = Vec::new();
452        proof_bytes.extend_from_slice(&inclusion.block_hash);
453        proof_bytes.extend_from_slice(&inclusion.tx_index.to_le_bytes());
454
455        let inclusion_proof = csv_adapter_core::InclusionProof::new(
456            proof_bytes,
457            Hash::new(inclusion.block_hash),
458            inclusion.tx_index as u64,
459        )
460        .map_err(|e| AdapterError::Generic(e.to_string()))?;
461
462        let finality_proof = FinalityProof::new(
463            finality.confirmations.to_le_bytes().to_vec(),
464            finality.confirmations,
465            finality.meets_required_depth,
466        )
467        .map_err(|e| AdapterError::Generic(e.to_string()))?;
468
469        ProofBundle::new(
470            transition_dag,
471            vec![],
472            seal_ref,
473            anchor_ref,
474            inclusion_proof,
475            finality_proof,
476        )
477        .map_err(|e| AdapterError::Generic(e.to_string()))
478    }
479
480    fn rollback(&self, anchor: Self::AnchorRef) -> CoreResult<()> {
481        let current_height = self.get_current_height();
482        if anchor.block_height > current_height {
483            return Err(AdapterError::ReorgInvalid(format!(
484                "Block {} is beyond current height {}",
485                anchor.block_height, current_height
486            )));
487        }
488
489        // If the anchor's block is before current height, the transaction may have been reorged out
490        // In this case, we should clear the seal from the registry to allow reuse
491        if anchor.block_height < current_height {
492            // Attempt to clear the seal from registry
493            // The seal txid is derived from the anchor's txid
494            let mut registry = self.seal_registry.lock().unwrap_or_else(|e| e.into_inner());
495            // Try to clear using anchor txid as seal identifier
496            let dummy_seal = BitcoinSealRef::new(anchor.txid, anchor.output_index, None);
497            registry.clear_seal(&dummy_seal);
498        }
499
500        Ok(())
501    }
502
503    fn domain_separator(&self) -> [u8; 32] {
504        self.domain_separator
505    }
506
507    fn signature_scheme(&self) -> csv_adapter_core::SignatureScheme {
508        csv_adapter_core::SignatureScheme::Secp256k1
509    }
510}
511
512#[cfg(test)]
513mod tests {
514    use super::*;
515
516    fn test_adapter() -> BitcoinAnchorLayer {
517        BitcoinAnchorLayer::signet().unwrap()
518    }
519
520    #[test]
521    fn test_create_seal() {
522        let adapter = test_adapter();
523        let seal = adapter.create_seal(None).unwrap();
524        assert_eq!(seal.nonce, Some(100_000));
525    }
526
527    #[test]
528    fn test_enforce_seal_replay() {
529        let adapter = test_adapter();
530        let seal = adapter.create_seal(None).unwrap();
531        adapter.enforce_seal(seal.clone()).unwrap();
532        assert!(adapter.enforce_seal(seal).is_err());
533    }
534
535    #[test]
536    fn test_domain_separator() {
537        let adapter = test_adapter();
538        assert_eq!(&adapter.domain_separator()[..8], b"CSV-BTC-");
539    }
540
541    #[test]
542    fn test_verify_finality() {
543        let adapter = test_adapter();
544        // block_height = 100 means it's confirmed at height 100
545        // current_height is 200, so confirmations = 100 which is > 6 (default finality_depth)
546        let anchor = BitcoinAnchorRef::new([1u8; 32], 0, 100);
547        let result = adapter.verify_finality(anchor);
548        assert!(result.is_ok());
549    }
550
551    #[test]
552    fn test_hd_wallet_seal_derivation() {
553        let adapter = test_adapter();
554        let seal1 = adapter.create_seal(Some(50_000)).unwrap();
555        let seal2 = adapter.create_seal(Some(50_000)).unwrap();
556        assert_ne!(seal1.txid, seal2.txid);
557    }
558
559    #[test]
560    fn test_hd_wallet_seal_derivation_deterministic() {
561        let wallet = SealWallet::generate_random(bitcoin::Network::Signet);
562        let config = BitcoinConfig::default();
563        let adapter = BitcoinAnchorLayer::with_wallet(config, wallet).unwrap();
564        let seal1 = adapter.create_seal(Some(100_000)).unwrap();
565        assert_eq!(seal1.nonce, Some(100_000));
566    }
567
568    #[test]
569    fn test_build_commitment_data() {
570        let adapter = test_adapter();
571        let data = adapter
572            .build_commitment_data(Hash::new([1u8; 32]), [2u8; 32])
573            .unwrap();
574        match data {
575            crate::tx_builder::CommitmentData::Tapret { payload, .. } => {
576                assert_eq!(payload[..32], [2u8; 32]);
577            }
578            crate::tx_builder::CommitmentData::Opret { .. } => {
579                panic!("Expected Tapret variant");
580            }
581        }
582    }
583
584    #[test]
585    fn test_derive_seal_deterministic() {
586        let adapter = test_adapter();
587        let seal1 = adapter.create_seal(None).unwrap();
588        let adapter2 = test_adapter();
589        let seal2 = adapter2.create_seal(None).unwrap();
590        assert_ne!(seal1.txid, seal2.txid);
591    }
592}