1#![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
36pub struct BitcoinAnchorLayer {
38 config: BitcoinConfig,
39 wallet: SealWallet,
40 tx_builder: CommitmentTxBuilder,
41 seal_registry: Mutex<SealRegistry>,
42 domain_separator: [u8; 32],
43 rpc: Option<Box<dyn BitcoinRpc + Send + Sync>>,
45 next_seal_index: Mutex<u32>,
46}
47
48impl BitcoinAnchorLayer {
49 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 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 pub fn signet() -> BitcoinResult<Self> {
81 let wallet = SealWallet::generate_random(bitcoin::Network::Signet);
82 Self::with_wallet(BitcoinConfig::default(), wallet)
83 }
84
85 pub fn with_rpc(mut self, rpc: Box<dyn BitcoinRpc + Send + Sync>) -> Self {
87 self.rpc = Some(rpc);
88 self
89 }
90
91 pub fn wallet(&self) -> &SealWallet {
93 &self.wallet
94 }
95
96 pub fn tx_builder_mut(&mut self) -> &mut CommitmentTxBuilder {
98 &mut self.tx_builder
99 }
100
101 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 let key = self
114 .wallet
115 .derive_key(&path)
116 .map_err(|e| AdapterError::Generic(format!("Key derivation failed: {}", e)))?;
117
118 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 pub fn fund_seal(
138 &self,
139 outpoint: bitcoin::OutPoint,
140 ) -> Result<(BitcoinSealRef, crate::wallet::Bip86Path), AdapterError> {
141 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 let txid = outpoint.txid.to_byte_array();
151 let seal_ref = BitcoinSealRef::new(txid, outpoint.vout, Some(utxo.amount_sat));
152
153 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 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 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 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 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 pub fn get_current_height_for_test(&self) -> u64 {
236 self.get_current_height()
237 }
238
239 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 Ok(())
257 }
258
259 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 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
279fn get_address_utxos(
281 _rpc: &dyn BitcoinRpc,
282 _address: &bitcoin::Address,
283) -> Result<Vec<(bitcoin::OutPoint, u64)>, String> {
284 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 let Some(rpc) = &self.rpc {
303 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 let tx_result = self
319 .tx_builder
320 .build_commitment_tx(
321 &self.wallet,
322 &utxo,
323 *commitment.as_bytes(),
324 None, )
326 .map_err(|e| AdapterError::PublishFailed(e.to_string()))?;
327
328 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 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 let Some(rpc) = &self.rpc {
364 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 return Ok(BitcoinInclusionProof::new(
373 vec![],
374 block_hash,
375 0,
376 anchor.block_height,
377 ));
378 }
379
380 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 anchor.block_height < current_height {
492 let mut registry = self.seal_registry.lock().unwrap_or_else(|e| e.into_inner());
495 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 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}