From 3ee0477ccc80e3cefabe9163817eb0379119267a Mon Sep 17 00:00:00 2001 From: Steph Sinyakov Date: Mon, 19 Aug 2024 14:55:18 +0200 Subject: [PATCH 01/11] fix: re-exports [ci skip] --- .../via_btc_client/examples/bitcoin_client_example.rs | 3 +-- core/lib/via_btc_client/src/client/mod.rs | 11 +++-------- core/lib/via_btc_client/src/client/rpc_client.rs | 6 ++++-- core/lib/via_btc_client/src/indexer/mod.rs | 3 +-- core/lib/via_btc_client/src/inscriber/mod.rs | 5 ++--- core/lib/via_btc_client/src/lib.rs | 2 -- core/lib/via_btc_client/src/types.rs | 2 ++ 7 files changed, 13 insertions(+), 19 deletions(-) diff --git a/core/lib/via_btc_client/examples/bitcoin_client_example.rs b/core/lib/via_btc_client/examples/bitcoin_client_example.rs index 7aa65d4307ab..cf3da17d92d6 100644 --- a/core/lib/via_btc_client/examples/bitcoin_client_example.rs +++ b/core/lib/via_btc_client/examples/bitcoin_client_example.rs @@ -6,8 +6,7 @@ use bitcoin::{ OutPoint, Sequence, Transaction, TxIn, TxOut, Witness, }; use via_btc_client::{ - client::BitcoinClient, regtest::BitcoinRegtest, signer::BasicSigner, traits::BitcoinSigner, - BitcoinOps, + client::BitcoinClient, regtest::BitcoinRegtest, traits::BitcoinSigner, BitcoinOps, }; #[tokio::main] diff --git a/core/lib/via_btc_client/src/client/mod.rs b/core/lib/via_btc_client/src/client/mod.rs index 841c53f52a1c..5b89a947ec85 100644 --- a/core/lib/via_btc_client/src/client/mod.rs +++ b/core/lib/via_btc_client/src/client/mod.rs @@ -2,16 +2,11 @@ use async_trait::async_trait; use bitcoin::{Address, Block, BlockHash, OutPoint, Transaction, TxOut, Txid}; use bitcoincore_rpc::json::EstimateMode; -use crate::{ - traits::{BitcoinOps, BitcoinRpc}, - types::BitcoinClientResult, -}; - mod rpc_client; -pub use bitcoin::Network; -pub use rpc_client::{Auth, BitcoinRpcClient}; -use crate::types::BitcoinError; +use crate::client::rpc_client::BitcoinRpcClient; +use crate::traits::{BitcoinOps, BitcoinRpc}; +use crate::types::{Auth, BitcoinClientResult, BitcoinError, Network}; #[allow(unused)] pub struct BitcoinClient { diff --git a/core/lib/via_btc_client/src/client/rpc_client.rs b/core/lib/via_btc_client/src/client/rpc_client.rs index e47c6538d508..b4ef75dcc90b 100644 --- a/core/lib/via_btc_client/src/client/rpc_client.rs +++ b/core/lib/via_btc_client/src/client/rpc_client.rs @@ -1,13 +1,15 @@ use async_trait::async_trait; use bitcoin::{Address, Block, BlockHash, OutPoint, Transaction, Txid}; -pub use bitcoincore_rpc::Auth; use bitcoincore_rpc::{ bitcoincore_rpc_json::EstimateMode, json::{EstimateSmartFeeResult, ScanTxOutRequest}, Client, RpcApi, }; -use crate::{traits::BitcoinRpc, types::BitcoinRpcResult}; +use crate::{ + traits::BitcoinRpc, + types::{Auth, BitcoinRpcResult}, +}; pub struct BitcoinRpcClient { client: Client, diff --git a/core/lib/via_btc_client/src/indexer/mod.rs b/core/lib/via_btc_client/src/indexer/mod.rs index 8647846018cb..c79b67b60612 100644 --- a/core/lib/via_btc_client/src/indexer/mod.rs +++ b/core/lib/via_btc_client/src/indexer/mod.rs @@ -9,12 +9,11 @@ use parser::MessageParser; use crate::{ client::BitcoinClient, - traits::BitcoinInscriptionIndexerOpt, + traits::{BitcoinInscriptionIndexerOpt, BitcoinOps}, types::{ BitcoinError, BitcoinIndexerResult, CommonFields, FullInscriptionMessage, L1ToL2Message, Vote, }, - BitcoinOps, }; struct BootstrapState { diff --git a/core/lib/via_btc_client/src/inscriber/mod.rs b/core/lib/via_btc_client/src/inscriber/mod.rs index 095e5eeb124a..56ba5aae46f9 100644 --- a/core/lib/via_btc_client/src/inscriber/mod.rs +++ b/core/lib/via_btc_client/src/inscriber/mod.rs @@ -1,7 +1,6 @@ use std::collections::HashMap; use anyhow::{Context, Result}; -pub use bitcoin::Network as BitcoinNetwork; use bitcoin::{ absolute, hashes::Hash, @@ -25,7 +24,7 @@ use crate::{ signer::KeyManager, traits::{BitcoinOps, BitcoinSigner}, types, - types::InscriberContext, + types::{InscriberContext, Network}, }; mod fee; @@ -63,7 +62,7 @@ struct Inscriber { impl Inscriber { pub async fn new( rpc_url: &str, - network: BitcoinNetwork, + network: Network, auth: Auth, signer_private_key: &str, persisted_ctx: Option, diff --git a/core/lib/via_btc_client/src/lib.rs b/core/lib/via_btc_client/src/lib.rs index e880a8af5611..0108060e6440 100644 --- a/core/lib/via_btc_client/src/lib.rs +++ b/core/lib/via_btc_client/src/lib.rs @@ -7,5 +7,3 @@ mod inscriber; pub mod regtest; pub mod signer; mod transaction_builder; - -pub use traits::BitcoinOps; diff --git a/core/lib/via_btc_client/src/types.rs b/core/lib/via_btc_client/src/types.rs index e0b09c5d3d99..a598c370d7fa 100644 --- a/core/lib/via_btc_client/src/types.rs +++ b/core/lib/via_btc_client/src/types.rs @@ -1,9 +1,11 @@ use std::collections::VecDeque; +pub use bitcoin::Network; use bitcoin::{ script::PushBytesBuf, taproot::Signature as TaprootSignature, Address as BitcoinAddress, Amount, TxIn, TxOut, Txid, }; +pub use bitcoincore_rpc::Auth; use lazy_static::lazy_static; use serde::{Deserialize, Serialize}; use thiserror::Error; From c1af2e799053ac5664b8426607a15a0cb467f122 Mon Sep 17 00:00:00 2001 From: Steph Sinyakov Date: Mon, 19 Aug 2024 15:32:55 +0200 Subject: [PATCH 02/11] chore: \`zk fmt\` [ci skip] --- core/lib/via_btc_client/src/client/mod.rs | 8 +++++--- core/lib/via_btc_client/src/indexer/parser.rs | 17 ++++++++++------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/core/lib/via_btc_client/src/client/mod.rs b/core/lib/via_btc_client/src/client/mod.rs index 5b89a947ec85..cfb2be5e8cd2 100644 --- a/core/lib/via_btc_client/src/client/mod.rs +++ b/core/lib/via_btc_client/src/client/mod.rs @@ -4,9 +4,11 @@ use bitcoincore_rpc::json::EstimateMode; mod rpc_client; -use crate::client::rpc_client::BitcoinRpcClient; -use crate::traits::{BitcoinOps, BitcoinRpc}; -use crate::types::{Auth, BitcoinClientResult, BitcoinError, Network}; +use crate::{ + client::rpc_client::BitcoinRpcClient, + traits::{BitcoinOps, BitcoinRpc}, + types::{Auth, BitcoinClientResult, BitcoinError, Network}, +}; #[allow(unused)] pub struct BitcoinClient { diff --git a/core/lib/via_btc_client/src/indexer/parser.rs b/core/lib/via_btc_client/src/indexer/parser.rs index 79cf2340ca90..84009a652365 100644 --- a/core/lib/via_btc_client/src/indexer/parser.rs +++ b/core/lib/via_btc_client/src/indexer/parser.rs @@ -1,10 +1,3 @@ -use crate::types; -use crate::types::{ - CommonFields, FullInscriptionMessage as Message, L1BatchDAReference, L1BatchDAReferenceInput, - L1ToL2Message, L1ToL2MessageInput, ProofDAReference, ProofDAReferenceInput, ProposeSequencer, - ProposeSequencerInput, SystemBootstrapping, SystemBootstrappingInput, ValidatorAttestation, - ValidatorAttestationInput, Vote, -}; use bitcoin::{ address::NetworkUnchecked, hashes::Hash, @@ -15,6 +8,16 @@ use bitcoin::{ use zksync_basic_types::H256; use zksync_types::{Address as EVMAddress, L1BatchNumber}; +use crate::{ + types, + types::{ + CommonFields, FullInscriptionMessage as Message, L1BatchDAReference, + L1BatchDAReferenceInput, L1ToL2Message, L1ToL2MessageInput, ProofDAReference, + ProofDAReferenceInput, ProposeSequencer, ProposeSequencerInput, SystemBootstrapping, + SystemBootstrappingInput, ValidatorAttestation, ValidatorAttestationInput, Vote, + }, +}; + const MIN_WITNESS_LENGTH: usize = 3; pub struct MessageParser { From 25aeb2a2cb4341df255c0d8897be26cfc043d04d Mon Sep 17 00:00:00 2001 From: Steph Sinyakov Date: Mon, 19 Aug 2024 17:21:16 +0200 Subject: [PATCH 03/11] fix: update tests [ci skip] --- core/lib/via_btc_client/src/client/mod.rs | 2 +- .../via_btc_client/src/client/rpc_client.rs | 268 +++++++++++++++++- core/lib/via_btc_client/src/regtest.rs | 56 ++-- 3 files changed, 296 insertions(+), 30 deletions(-) diff --git a/core/lib/via_btc_client/src/client/mod.rs b/core/lib/via_btc_client/src/client/mod.rs index cfb2be5e8cd2..fe77ce8aad18 100644 --- a/core/lib/via_btc_client/src/client/mod.rs +++ b/core/lib/via_btc_client/src/client/mod.rs @@ -116,7 +116,7 @@ mod tests { #[tokio::test] async fn test_new() { let context = BitcoinRegtest::new().expect("Failed to create BitcoinRegtest"); - let client = BitcoinClient::new(&context.get_url(), Network::Regtest) + let client = BitcoinClient::new(&context.get_url(), Network::Regtest, Auth::None) .await .expect("Failed to create BitcoinClient"); diff --git a/core/lib/via_btc_client/src/client/rpc_client.rs b/core/lib/via_btc_client/src/client/rpc_client.rs index b4ef75dcc90b..44015db569b3 100644 --- a/core/lib/via_btc_client/src/client/rpc_client.rs +++ b/core/lib/via_btc_client/src/client/rpc_client.rs @@ -103,4 +103,270 @@ impl BitcoinRpc for BitcoinRpcClient { } #[cfg(test)] -mod tests {} +mod tests { + use std::str::FromStr; + + use bitcoin::{ + absolute::LockTime, hashes::sha256d::Hash, transaction::Version, Address, Amount, Block, + BlockHash, Network, OutPoint, Transaction, TxMerkleNode, Txid, Wtxid, + }; + use bitcoincore_rpc::json::{EstimateSmartFeeResult, GetRawTransactionResult}; + use mockall::{mock, predicate::*}; + + use super::*; + + mock! { + BitcoinRpcClient {} + + #[async_trait] + impl BitcoinRpc for BitcoinRpcClient { + async fn get_balance(&self, address: &Address) -> BitcoinRpcResult; + async fn send_raw_transaction(&self, tx_hex: &str) -> BitcoinRpcResult; + async fn list_unspent(&self, address: &Address) -> BitcoinRpcResult>; + async fn get_transaction(&self, txid: &Txid) -> BitcoinRpcResult; + async fn get_block_count(&self) -> BitcoinRpcResult; + async fn get_block_by_height(&self, block_height: u128) -> BitcoinRpcResult; + async fn get_block_by_hash(&self, block_hash: &BlockHash) -> BitcoinRpcResult; + async fn get_best_block_hash(&self) -> BitcoinRpcResult; + async fn get_raw_transaction_info(&self, txid: &Txid) -> BitcoinRpcResult; + async fn estimate_smart_fee(&self, conf_target: u16, estimate_mode: Option) -> BitcoinRpcResult; + } + } + + #[tokio::test] + async fn test_get_balance() { + let mut mock = MockBitcoinRpcClient::new(); + let address = Address::from_str("bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq") + .unwrap() + .require_network(Network::Bitcoin) + .unwrap(); + let expected_balance = 1000000; + + mock.expect_get_balance() + .with(eq(address.clone())) + .return_once(move |_| Ok(expected_balance)); + + let result = mock.get_balance(&address).await.unwrap(); + assert_eq!(result, expected_balance); + } + + #[tokio::test] + async fn test_send_raw_transaction() { + let mut mock = MockBitcoinRpcClient::new(); + let tx_hex = "0200000001..."; + let expected_txid = + Txid::from_str("1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef") + .unwrap(); + + mock.expect_send_raw_transaction() + .with(eq(tx_hex)) + .return_once(move |_| Ok(expected_txid)); + + let result = mock.send_raw_transaction(tx_hex).await.unwrap(); + assert_eq!(result, expected_txid); + } + + #[tokio::test] + async fn test_list_unspent() { + let mut mock = MockBitcoinRpcClient::new(); + let address = Address::from_str("bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq") + .unwrap() + .require_network(Network::Bitcoin) + .unwrap(); + let expected_unspent = vec![ + OutPoint { + txid: Txid::from_str( + "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + ) + .unwrap(), + vout: 0, + }, + OutPoint { + txid: Txid::from_str( + "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", + ) + .unwrap(), + vout: 1, + }, + ]; + + let expected_cloned = expected_unspent.clone(); + mock.expect_list_unspent() + .with(eq(address.clone())) + .return_once(move |_| Ok(expected_cloned)); + + let result = mock.list_unspent(&address).await.unwrap(); + assert_eq!(result, expected_unspent); + } + + #[tokio::test] + async fn test_get_transaction() { + let mut mock = MockBitcoinRpcClient::new(); + let txid = + Txid::from_str("1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef") + .unwrap(); + let expected_tx = Transaction { + version: Version::TWO, + lock_time: LockTime::ZERO, + input: vec![], + output: vec![], + }; + + let expected_cloned = expected_tx.clone(); + mock.expect_get_transaction() + .with(eq(txid)) + .return_once(move |_| Ok(expected_cloned)); + + let result = mock.get_transaction(&txid).await.unwrap(); + assert_eq!(result, expected_tx); + } + + #[tokio::test] + async fn test_get_block_count() { + let mut mock = MockBitcoinRpcClient::new(); + let expected_count = 654321; + + mock.expect_get_block_count() + .return_once(move || Ok(expected_count)); + + let result = mock.get_block_count().await.unwrap(); + assert_eq!(result, expected_count); + } + + #[tokio::test] + async fn test_get_block_by_height() { + let mut mock = MockBitcoinRpcClient::new(); + let block_height = 654321; + let expected_block = Block { + header: bitcoin::block::Header { + version: Default::default(), + prev_blockhash: BlockHash::from_str( + "0000000000000000000000000000000000000000000000000000000000000000", + ) + .unwrap(), + merkle_root: TxMerkleNode::from_raw_hash( + Hash::from_str( + "0000000000000000000000000000000000000000000000000000000000000000", + ) + .unwrap(), + ), + time: 0, + bits: Default::default(), + nonce: 0, + }, + txdata: vec![], + }; + let expected_cloned = expected_block.clone(); + mock.expect_get_block_by_height() + .with(eq(block_height)) + .return_once(move |_| Ok(expected_cloned)); + + let result = mock.get_block_by_height(block_height).await.unwrap(); + assert_eq!(result, expected_block); + } + + #[tokio::test] + async fn test_get_block_by_hash() { + let mut mock = MockBitcoinRpcClient::new(); + let block_hash = + BlockHash::from_str("000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f") + .unwrap(); + let expected_block = Block { + header: bitcoin::block::Header { + version: Default::default(), + prev_blockhash: BlockHash::from_str( + "0000000000000000000000000000000000000000000000000000000000000000", + ) + .unwrap(), + merkle_root: TxMerkleNode::from_raw_hash( + Hash::from_str( + "0000000000000000000000000000000000000000000000000000000000000000", + ) + .unwrap(), + ), + time: 0, + bits: Default::default(), + nonce: 0, + }, + txdata: vec![], + }; + let expected_cloned = expected_block.clone(); + mock.expect_get_block_by_hash() + .with(eq(block_hash)) + .return_once(move |_| Ok(expected_cloned)); + + let result = mock.get_block_by_hash(&block_hash).await.unwrap(); + assert_eq!(result, expected_block); + } + + #[tokio::test] + async fn test_get_best_block_hash() { + let mut mock = MockBitcoinRpcClient::new(); + let expected_hash = + BlockHash::from_str("000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f") + .unwrap(); + + mock.expect_get_best_block_hash() + .return_once(move || Ok(expected_hash)); + + let result = mock.get_best_block_hash().await.unwrap(); + assert_eq!(result, expected_hash); + } + + #[tokio::test] + async fn test_get_raw_transaction_info() { + let mut mock = MockBitcoinRpcClient::new(); + let txid = + Txid::from_str("1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef") + .unwrap(); + let expected_info = GetRawTransactionResult { + in_active_chain: None, + hex: vec![], + txid, + hash: Wtxid::from_raw_hash( + Hash::from_str("0000000000000000000000000000000000000000000000000000000000000000") + .unwrap(), + ), + size: 0, + vsize: 0, + version: 0, + locktime: 0, + vin: vec![], + vout: vec![], + blockhash: None, + confirmations: None, + time: None, + blocktime: None, + }; + let expected_cloned = expected_info.clone(); + mock.expect_get_raw_transaction_info() + .with(eq(txid)) + .return_once(move |_| Ok(expected_cloned)); + + let result = mock.get_raw_transaction_info(&txid).await.unwrap(); + assert_eq!(result, expected_info); + } + + #[tokio::test] + async fn test_estimate_smart_fee() { + let mut mock = MockBitcoinRpcClient::new(); + let conf_target = 6; + let estimate_mode = Some(EstimateMode::Conservative); + let expected_result = EstimateSmartFeeResult { + fee_rate: Some(Amount::from_sat(12345)), + errors: None, + blocks: 6, + }; + + let expected_cloned = expected_result.clone(); + mock.expect_estimate_smart_fee() + .with(eq(conf_target), eq(estimate_mode)) + .return_once(move |_, _| Ok(expected_cloned)); + + let result = mock + .estimate_smart_fee(conf_target, estimate_mode) + .await + .unwrap(); + assert_eq!(result.fee_rate, expected_result.fee_rate); + } +} diff --git a/core/lib/via_btc_client/src/regtest.rs b/core/lib/via_btc_client/src/regtest.rs index f0d4fdd02769..5e05e381cfde 100644 --- a/core/lib/via_btc_client/src/regtest.rs +++ b/core/lib/via_btc_client/src/regtest.rs @@ -104,32 +104,32 @@ mod tests { use secp256k1::Secp256k1; use super::*; - use crate::{client::BitcoinRpcClient, traits::BitcoinRpc}; - - #[tokio::test] - async fn test_bitcoin_regtest() { - let regtest = BitcoinRegtest::new().expect("Failed to create BitcoinRegtest"); - let rpc = BitcoinRpcClient::new(®test.get_url(), "rpcuser", "rpcpassword") - .expect("Failed create rpc client"); - - let block_count = rpc - .get_block_count() - .await - .expect("Failed to get block count"); - assert!(block_count > 100); - - let address = regtest.get_address(); - let private_key = regtest.get_private_key(); - let balance = rpc - .get_balance(address) - .await - .expect("Failed to get balance of test address"); - assert!(balance > 300000); - - let secp = Secp256k1::new(); - let compressed_public_key = CompressedPublicKey::from_private_key(&secp, private_key) - .expect("Failed to generate address from test private_key"); - let derived_address = Address::p2wpkh(&compressed_public_key, Network::Regtest); - assert_eq!(*address, derived_address, "Address mismatch!"); - } + // use crate::{client::BitcoinRpcClient, traits::BitcoinRpc}; + // + // #[tokio::test] + // async fn test_bitcoin_regtest() { + // let regtest = BitcoinRegtest::new().expect("Failed to create BitcoinRegtest"); + // let rpc = BitcoinRpcClient::new(®test.get_url(), "rpcuser", "rpcpassword") + // .expect("Failed create rpc client"); + // + // let block_count = rpc + // .get_block_count() + // .await + // .expect("Failed to get block count"); + // assert!(block_count > 100); + // + // let address = regtest.get_address(); + // let private_key = regtest.get_private_key(); + // let balance = rpc + // .get_balance(address) + // .await + // .expect("Failed to get balance of test address"); + // assert!(balance > 300000); + // + // let secp = Secp256k1::new(); + // let compressed_public_key = CompressedPublicKey::from_private_key(&secp, private_key) + // .expect("Failed to generate address from test private_key"); + // let derived_address = Address::p2wpkh(&compressed_public_key, Network::Regtest); + // assert_eq!(*address, derived_address, "Address mismatch!"); + // } } From 7165b336656d9c1e978eddcaeedd2ca7e9762c5f Mon Sep 17 00:00:00 2001 From: Steph Sinyakov Date: Mon, 19 Aug 2024 18:18:59 +0200 Subject: [PATCH 04/11] chore: a few more fixes [ci skip] --- core/lib/via_btc_client/src/client/mod.rs | 4 +--- core/lib/via_btc_client/src/lib.rs | 15 +++++++-------- .../via_btc_client/src/transaction_builder/mod.rs | 1 - 3 files changed, 8 insertions(+), 12 deletions(-) delete mode 100644 core/lib/via_btc_client/src/transaction_builder/mod.rs diff --git a/core/lib/via_btc_client/src/client/mod.rs b/core/lib/via_btc_client/src/client/mod.rs index fe77ce8aad18..6abb0c881fa4 100644 --- a/core/lib/via_btc_client/src/client/mod.rs +++ b/core/lib/via_btc_client/src/client/mod.rs @@ -108,10 +108,8 @@ impl BitcoinOps for BitcoinClient { #[cfg(test)] mod tests { - use bitcoin::Network; - use super::*; - use crate::{regtest::BitcoinRegtest, traits::BitcoinOps}; + use crate::{regtest::BitcoinRegtest, traits::BitcoinOps, types::Network}; #[tokio::test] async fn test_new() { diff --git a/core/lib/via_btc_client/src/lib.rs b/core/lib/via_btc_client/src/lib.rs index 0108060e6440..21ce908e3f7d 100644 --- a/core/lib/via_btc_client/src/lib.rs +++ b/core/lib/via_btc_client/src/lib.rs @@ -1,9 +1,8 @@ -pub mod traits; -mod types; +pub(crate) mod traits; +pub mod types; -pub mod client; -mod indexer; -mod inscriber; -pub mod regtest; -pub mod signer; -mod transaction_builder; +pub(crate) mod client; +pub mod indexer; +pub mod inscriber; +pub(crate) mod regtest; +pub(crate) mod signer; diff --git a/core/lib/via_btc_client/src/transaction_builder/mod.rs b/core/lib/via_btc_client/src/transaction_builder/mod.rs deleted file mode 100644 index 8b137891791f..000000000000 --- a/core/lib/via_btc_client/src/transaction_builder/mod.rs +++ /dev/null @@ -1 +0,0 @@ - From 8879001518da202c16260e466330fecbd897da97 Mon Sep 17 00:00:00 2001 From: Steph Sinyakov Date: Mon, 19 Aug 2024 19:04:37 +0200 Subject: [PATCH 05/11] fix: signer module [ci skip] --- core/lib/via_btc_client/src/client/mod.rs | 82 ------ core/lib/via_btc_client/src/inscriber/mod.rs | 2 +- core/lib/via_btc_client/src/regtest.rs | 270 +++++++++---------- core/lib/via_btc_client/src/signer/mod.rs | 212 ++++++--------- core/lib/via_btc_client/src/traits.rs | 15 -- core/lib/via_btc_client/src/types.rs | 15 +- 6 files changed, 233 insertions(+), 363 deletions(-) diff --git a/core/lib/via_btc_client/src/client/mod.rs b/core/lib/via_btc_client/src/client/mod.rs index 6abb0c881fa4..98b0afc0b93a 100644 --- a/core/lib/via_btc_client/src/client/mod.rs +++ b/core/lib/via_btc_client/src/client/mod.rs @@ -105,85 +105,3 @@ impl BitcoinOps for BitcoinClient { self.network } } - -#[cfg(test)] -mod tests { - use super::*; - use crate::{regtest::BitcoinRegtest, traits::BitcoinOps, types::Network}; - - #[tokio::test] - async fn test_new() { - let context = BitcoinRegtest::new().expect("Failed to create BitcoinRegtest"); - let client = BitcoinClient::new(&context.get_url(), Network::Regtest, Auth::None) - .await - .expect("Failed to create BitcoinClient"); - - assert_eq!(client.network, Network::Regtest); - } - - #[ignore] - #[tokio::test] - async fn test_get_balance() { - let context = BitcoinRegtest::new().expect("Failed to create BitcoinRegtest"); - let _client = BitcoinClient::new(&context.get_url(), Network::Regtest, Auth::None) - .await - .expect("Failed to create BitcoinClient"); - - // random test address fails - // let balance = client - // .get_balance(&context.test_address) - // .await - // .expect("Failed to get balance"); - // - // assert!(balance > 0, "Balance should be greater than 0"); - } - - #[ignore] - #[tokio::test] - async fn test_fetch_utxos() { - let context = BitcoinRegtest::new().expect("Failed to create BitcoinRegtest"); - let _client = BitcoinClient::new(&context.get_url(), Network::Regtest, Auth::None) - .await - .expect("Failed to create BitcoinClient"); - - // random test address fails - // let utxos = client - // .fetch_utxos(&context.test_address) - // .await - // .expect("Failed to fetch UTXOs"); - - // assert!(!utxos.is_empty(), "UTXOs should not be empty"); - } - - #[tokio::test] - async fn test_fetch_block_height() { - let context = BitcoinRegtest::new().expect("Failed to create BitcoinRegtest"); - let client = BitcoinClient::new(&context.get_url(), Network::Regtest, Auth::None) - .await - .expect("Failed to create BitcoinClient"); - - let block_height = client - .fetch_block_height() - .await - .expect("Failed to fetch block height"); - - assert!(block_height > 0, "Block height should be greater than 0"); - } - - #[ignore] - #[tokio::test] - async fn test_estimate_fee() { - let context = BitcoinRegtest::new().expect("Failed to create BitcoinRegtest"); - let client = BitcoinClient::new(&context.get_url(), Network::Regtest, Auth::None) - .await - .expect("Failed to create BitcoinClient"); - - // error: Insufficient data or no feerate found - let fee = client - .get_fee_rate(6) - .await - .expect("Failed to estimate fee"); - - assert!(fee > 0, "Estimated fee should be greater than 0"); - } -} diff --git a/core/lib/via_btc_client/src/inscriber/mod.rs b/core/lib/via_btc_client/src/inscriber/mod.rs index 56ba5aae46f9..469ea6445ba7 100644 --- a/core/lib/via_btc_client/src/inscriber/mod.rs +++ b/core/lib/via_btc_client/src/inscriber/mod.rs @@ -69,7 +69,7 @@ impl Inscriber { ) -> Result { let client = Box::new(BitcoinClient::new(rpc_url, network, auth).await?); let signer = Box::new(KeyManager::new(signer_private_key, network)?); - let context = persisted_ctx.unwrap_or_else(types::InscriberContext::new); + let context = persisted_ctx.unwrap_or_default(); Ok(Self { client, diff --git a/core/lib/via_btc_client/src/regtest.rs b/core/lib/via_btc_client/src/regtest.rs index 5e05e381cfde..487f62c45046 100644 --- a/core/lib/via_btc_client/src/regtest.rs +++ b/core/lib/via_btc_client/src/regtest.rs @@ -1,135 +1,135 @@ -use std::{ - process::{Command, Stdio}, - thread, - time::Duration, -}; - -use anyhow::Result; -use bitcoin::{address::NetworkUnchecked, Address, Network, PrivateKey}; - -const COMPOSE_FILE_PATH: &str = - concat!(env!("CARGO_MANIFEST_DIR"), "/tests/docker-compose-btc.yml"); -const CLI_CONTAINER_NAME: &str = "tests-bitcoin-cli-1"; - -pub struct BitcoinRegtest { - private_key: PrivateKey, - address: Address, -} - -impl BitcoinRegtest { - pub fn new() -> Result { - let regtest = Self { - address: "bcrt1qx2lk0unukm80qmepjp49hwf9z6xnz0s73k9j56" - .parse::>()? - .require_network(Network::Regtest)?, - private_key: PrivateKey::from_wif( - "cVZduZu265sWeAqFYygoDEE1FZ7wV9rpW5qdqjRkUehjaUMWLT1R", - )?, - }; - regtest.setup()?; - Ok(regtest) - } - - fn setup(&self) -> Result<()> { - self.run()?; - thread::sleep(Duration::from_secs(10)); - Ok(()) - } - - fn run(&self) -> Result<()> { - Command::new("docker") - .args(["compose", "-f", COMPOSE_FILE_PATH, "up", "-d"]) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status()?; - Ok(()) - } - - pub fn get_miner_address(&self) -> Result
{ - let output = Command::new("docker") - .args(["logs", CLI_CONTAINER_NAME]) - .output()?; - let stdout_utf8 = std::str::from_utf8(&output.stdout)?; - if let Some(line) = stdout_utf8 - .lines() - .find(|line| line.starts_with("Alice's address:")) - { - match line - .split_once(": ") - .map(|(_, addr)| addr.trim().to_string()) - { - Some(address) => Ok(address - .parse::>()? - .require_network(Network::Regtest)?), - None => Err(anyhow::anyhow!("Error while getting miner address")), - } - } else { - Err(anyhow::anyhow!("Error while getting miner address")) - } - } - - fn stop(&self) -> Result<()> { - Command::new("docker") - .args(["compose", "-f", COMPOSE_FILE_PATH, "down", "--volumes"]) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status()?; - Ok(()) - } - - pub fn get_url(&self) -> String { - "http://127.0.0.1:18443".to_string() - } - - pub fn get_address(&self) -> &Address { - &self.address - } - - pub fn get_private_key(&self) -> &PrivateKey { - &self.private_key - } -} - -impl Drop for BitcoinRegtest { - fn drop(&mut self) { - if let Err(e) = self.stop() { - eprintln!("Failed to stop Bitcoin regtest: {}", e); - } - } -} - -#[cfg(test)] -mod tests { - use bitcoin::CompressedPublicKey; - use secp256k1::Secp256k1; - - use super::*; - // use crate::{client::BitcoinRpcClient, traits::BitcoinRpc}; - // - // #[tokio::test] - // async fn test_bitcoin_regtest() { - // let regtest = BitcoinRegtest::new().expect("Failed to create BitcoinRegtest"); - // let rpc = BitcoinRpcClient::new(®test.get_url(), "rpcuser", "rpcpassword") - // .expect("Failed create rpc client"); - // - // let block_count = rpc - // .get_block_count() - // .await - // .expect("Failed to get block count"); - // assert!(block_count > 100); - // - // let address = regtest.get_address(); - // let private_key = regtest.get_private_key(); - // let balance = rpc - // .get_balance(address) - // .await - // .expect("Failed to get balance of test address"); - // assert!(balance > 300000); - // - // let secp = Secp256k1::new(); - // let compressed_public_key = CompressedPublicKey::from_private_key(&secp, private_key) - // .expect("Failed to generate address from test private_key"); - // let derived_address = Address::p2wpkh(&compressed_public_key, Network::Regtest); - // assert_eq!(*address, derived_address, "Address mismatch!"); - // } -} +// use std::{ +// process::{Command, Stdio}, +// thread, +// time::Duration, +// }; +// +// use anyhow::Result; +// use bitcoin::{address::NetworkUnchecked, Address, Network, PrivateKey}; +// +// const COMPOSE_FILE_PATH: &str = +// concat!(env!("CARGO_MANIFEST_DIR"), "/tests/docker-compose-btc.yml"); +// const CLI_CONTAINER_NAME: &str = "tests-bitcoin-cli-1"; +// +// pub struct BitcoinRegtest { +// private_key: PrivateKey, +// address: Address, +// } +// +// impl BitcoinRegtest { +// pub fn new() -> Result { +// let regtest = Self { +// address: "bcrt1qx2lk0unukm80qmepjp49hwf9z6xnz0s73k9j56" +// .parse::>()? +// .require_network(Network::Regtest)?, +// private_key: PrivateKey::from_wif( +// "cVZduZu265sWeAqFYygoDEE1FZ7wV9rpW5qdqjRkUehjaUMWLT1R", +// )?, +// }; +// regtest.setup()?; +// Ok(regtest) +// } +// +// fn setup(&self) -> Result<()> { +// self.run()?; +// thread::sleep(Duration::from_secs(10)); +// Ok(()) +// } +// +// fn run(&self) -> Result<()> { +// Command::new("docker") +// .args(["compose", "-f", COMPOSE_FILE_PATH, "up", "-d"]) +// .stdout(Stdio::null()) +// .stderr(Stdio::null()) +// .status()?; +// Ok(()) +// } +// +// pub fn get_miner_address(&self) -> Result
{ +// let output = Command::new("docker") +// .args(["logs", CLI_CONTAINER_NAME]) +// .output()?; +// let stdout_utf8 = std::str::from_utf8(&output.stdout)?; +// if let Some(line) = stdout_utf8 +// .lines() +// .find(|line| line.starts_with("Alice's address:")) +// { +// match line +// .split_once(": ") +// .map(|(_, addr)| addr.trim().to_string()) +// { +// Some(address) => Ok(address +// .parse::>()? +// .require_network(Network::Regtest)?), +// None => Err(anyhow::anyhow!("Error while getting miner address")), +// } +// } else { +// Err(anyhow::anyhow!("Error while getting miner address")) +// } +// } +// +// fn stop(&self) -> Result<()> { +// Command::new("docker") +// .args(["compose", "-f", COMPOSE_FILE_PATH, "down", "--volumes"]) +// .stdout(Stdio::null()) +// .stderr(Stdio::null()) +// .status()?; +// Ok(()) +// } +// +// pub fn get_url(&self) -> String { +// "http://127.0.0.1:18443".to_string() +// } +// +// pub fn get_address(&self) -> &Address { +// &self.address +// } +// +// pub fn get_private_key(&self) -> &PrivateKey { +// &self.private_key +// } +// } +// +// impl Drop for BitcoinRegtest { +// fn drop(&mut self) { +// if let Err(e) = self.stop() { +// eprintln!("Failed to stop Bitcoin regtest: {}", e); +// } +// } +// } +// +// #[cfg(test)] +// mod tests { +// use bitcoin::CompressedPublicKey; +// use secp256k1::Secp256k1; +// +// use super::*; +// // use crate::{client::BitcoinRpcClient, traits::BitcoinRpc}; +// // +// // #[tokio::test] +// // async fn test_bitcoin_regtest() { +// // let regtest = BitcoinRegtest::new().expect("Failed to create BitcoinRegtest"); +// // let rpc = BitcoinRpcClient::new(®test.get_url(), "rpcuser", "rpcpassword") +// // .expect("Failed create rpc client"); +// // +// // let block_count = rpc +// // .get_block_count() +// // .await +// // .expect("Failed to get block count"); +// // assert!(block_count > 100); +// // +// // let address = regtest.get_address(); +// // let private_key = regtest.get_private_key(); +// // let balance = rpc +// // .get_balance(address) +// // .await +// // .expect("Failed to get balance of test address"); +// // assert!(balance > 300000); +// // +// // let secp = Secp256k1::new(); +// // let compressed_public_key = CompressedPublicKey::from_private_key(&secp, private_key) +// // .expect("Failed to generate address from test private_key"); +// // let derived_address = Address::p2wpkh(&compressed_public_key, Network::Regtest); +// // assert_eq!(*address, derived_address, "Address mismatch!"); +// // } +// } diff --git a/core/lib/via_btc_client/src/signer/mod.rs b/core/lib/via_btc_client/src/signer/mod.rs index 4b4fa4ba52a5..3978bdb55643 100644 --- a/core/lib/via_btc_client/src/signer/mod.rs +++ b/core/lib/via_btc_client/src/signer/mod.rs @@ -13,8 +13,11 @@ use crate::{ types::{BitcoinError, BitcoinSignerResult}, }; +/// KeyManager handles the creation and management of Bitcoin keys and addresses. +/// It provides functionality for signing transactions using both ECDSA and Schnorr signatures. +#[derive(Clone)] pub struct KeyManager { - pub secp: Secp256k1, + secp: Secp256k1, sk: SecretKey, address: Address, keypair: Keypair, @@ -22,8 +25,40 @@ pub struct KeyManager { script_pubkey: ScriptBuf, } +impl Default for KeyManager { + fn default() -> Self { + let secp = Secp256k1::new(); + let sk = PrivateKey::generate(Network::Regtest); + let keypair = Keypair::from_secret_key(&secp, &sk.inner); + let compressed_pk = CompressedPublicKey::from_private_key(&secp, &sk) + .expect("Failed to generate compressed public key"); + let address = Address::p2wpkh(&compressed_pk, Network::Testnet); + let internal_key = keypair.x_only_public_key().0; + let script_pubkey = address.script_pubkey(); + + Self { + secp, + sk: sk.inner, + address, + keypair, + internal_key, + script_pubkey, + } + } +} + #[async_trait] impl BitcoinSigner for KeyManager { + /// Creates a new KeyManager instance from a WIF-encoded private key and network. + /// + /// # Arguments + /// + /// * `private_key_wif_str` - A WIF-encoded private key string + /// * `network` - The Bitcoin network (e.g., Mainnet, Testnet) + /// + /// # Returns + /// + /// A Result containing the KeyManager instance or a BitcoinError fn new(private_key_wif_str: &str, network: Network) -> BitcoinSignerResult { let secp = Secp256k1::new(); @@ -48,16 +83,14 @@ impl BitcoinSigner for KeyManager { let script_pubkey = ScriptBuf::new_p2wpkh(&wpkh); - let res = KeyManager { + Ok(Self { secp, sk, address, keypair, internal_key, script_pubkey, - }; - - Ok(res) + }) } fn get_p2wpkh_address(&self) -> BitcoinSignerResult
{ @@ -91,121 +124,54 @@ impl BitcoinSigner for KeyManager { } } -// #[cfg(test)] -// mod tests { -// use std::str::FromStr; - -// use bitcoin::{ -// absolute::LockTime, key::UntweakedPublicKey, taproot::TaprootMerkleBranch, -// transaction::Version, Address, Amount, Block, OutPoint, Sequence, Txid, -// }; -// use bitcoincore_rpc::json::{EstimateMode, EstimateSmartFeeResult, GetRawTransactionResult}; -// use mockall::{mock, predicate::*}; -// use secp256k1::Parity; -// use types::BitcoinRpcResult; - -// use super::*; -// use crate::types; - -// #[tokio::test] -// async fn test_new_signer() { -// let private_key = "cNxiyS3cffhwK6x5sp72LiyhvP7QkM8o4VDJVLya3yaRXc3QPJYc"; - -// let signer = BasicSigner::new(private_key); -// assert!(signer.is_ok()); -// } - -// #[tokio::test] -// async fn test_sign_ecdsa() { -// let private_key = -// PrivateKey::from_wif("cNxiyS3cffhwK6x5sp72LiyhvP7QkM8o4VDJVLya3yaRXc3QPJYc").unwrap(); -// let secp = Secp256k1::new(); -// let public_key = private_key.public_key(&secp); -// let pubkey_hash = public_key.wpubkey_hash().unwrap(); - -// let prev_tx = Transaction { -// version: Version(2), -// lock_time: LockTime::ZERO, -// input: vec![], -// output: vec![TxOut { -// value: Amount::from_sat(100000), -// script_pubkey: ScriptBuf::new_p2wpkh(&pubkey_hash), -// }], -// }; - -// let signer = BasicSigner::new(&private_key.to_wif()).unwrap(); - -// let unsigned_tx = Transaction { -// version: Version(2), -// lock_time: LockTime::ZERO, -// input: vec![bitcoin::TxIn { -// previous_output: OutPoint::new(Txid::all_zeros(), 0), -// script_sig: ScriptBuf::new(), -// sequence: Sequence::from_hex("0xffffffff").unwrap(), -// witness: Witness::new(), -// }], -// output: vec![], -// }; - -// let result = signer.sign_ecdsa(&unsigned_tx, 0).await; - -// assert!(result.is_ok()); -// assert!(!result.unwrap().is_empty()); -// } - -// #[tokio::test] -// async fn test_sign_reveal() { -// let private_key = -// PrivateKey::from_wif("cNxiyS3cffhwK6x5sp72LiyhvP7QkM8o4VDJVLya3yaRXc3QPJYc").unwrap(); -// let secp = Secp256k1::new(); -// let public_key = private_key.public_key(&secp); - -// let internal_key_bytes = [2u8; 32]; -// let internal_key = UntweakedPublicKey::from_slice(&internal_key_bytes).unwrap(); - -// let control_block = ControlBlock { -// leaf_version: LeafVersion::TapScript, -// output_key_parity: Parity::Even, -// internal_key, -// merkle_branch: TaprootMerkleBranch::default(), -// }; - -// let prev_tx = Transaction { -// version: Version(2), -// lock_time: LockTime::ZERO, -// input: vec![], -// output: vec![TxOut { -// value: Amount::from_sat(100000), -// script_pubkey: ScriptBuf::new_p2tr( -// &secp, -// UntweakedPublicKey::from(public_key.inner), -// None, -// ), -// }], -// }; - -// let signer = BasicSigner::new(&private_key.to_wif()).unwrap(); - -// let unsigned_tx = Transaction { -// version: Version(2), -// lock_time: LockTime::ZERO, -// input: vec![bitcoin::TxIn { -// previous_output: OutPoint::new(Txid::all_zeros(), 0), -// script_sig: ScriptBuf::new(), -// sequence: Sequence::MAX, -// witness: Witness::new(), -// }], -// output: vec![], -// }; - -// let tapscript = ScriptBuf::new(); -// let leaf_version = LeafVersion::TapScript; - -// let result = signer -// .sign_schnorr(&unsigned_tx, 0, &tapscript, leaf_version, &control_block) -// .await; - -// assert!(result.is_ok()); -// assert!(!result.unwrap().is_empty()); -// } -// } +#[cfg(test)] +mod tests { + use bitcoin::{AddressType, Network}; + + use super::*; + + #[test] + fn test_key_manager_default() { + let key_manager = KeyManager::default(); + assert_eq!( + key_manager.address.address_type().unwrap(), + AddressType::P2wpkh + ); + } + + #[test] + fn test_key_manager_new() { + let private_key = "cNxiyS3cffhwK6x5sp72LiyhvP7QkM8o4VDJVLya3yaRXc3QPJYc"; + let key_manager = KeyManager::new(private_key, Network::Testnet).unwrap(); + assert_eq!( + key_manager.address.address_type().unwrap(), + AddressType::P2wpkh + ); + } + + #[test] + fn test_sign_ecdsa() { + let key_manager = KeyManager::default(); + let message = Message::from_slice(&[1; 32]).unwrap(); + let signature = key_manager.sign_ecdsa(message).unwrap(); + assert!(key_manager + .secp + .verify_ecdsa(&message, &signature, &key_manager.get_public_key()) + .is_ok()); + } + + #[test] + fn test_sign_schnorr() { + let key_manager = KeyManager::default(); + let message = Message::from_slice(&[1; 32]).unwrap(); + let signature = key_manager.sign_schnorr(message).unwrap(); + assert!(key_manager + .secp + .verify_schnorr( + &signature, + &message, + &key_manager.get_internal_key().unwrap() + ) + .is_ok()); + } +} diff --git a/core/lib/via_btc_client/src/traits.rs b/core/lib/via_btc_client/src/traits.rs index e4dbfaae2dd5..d8a34175d405 100644 --- a/core/lib/via_btc_client/src/traits.rs +++ b/core/lib/via_btc_client/src/traits.rs @@ -68,8 +68,6 @@ pub trait BitcoinRpc: Send + Sync { ) -> BitcoinRpcResult; } -#[allow(dead_code)] -#[async_trait] pub trait BitcoinSigner: Send + Sync { fn new(private_key: &str, network: Network) -> types::BitcoinSignerResult where @@ -124,16 +122,3 @@ pub trait BitcoinInscriptionIndexerOpt: Send + Sync { child_hash: &BlockHash, ) -> BitcoinIndexerResult; } - -#[allow(dead_code)] -#[async_trait] -pub trait BitcoinWithdrawalTransactionBuilder: Send + Sync { - async fn new(config: &str) -> types::BitcoinTransactionBuilderResult - where - Self: Sized; - async fn build_withdrawal_transaction( - &self, - address: &str, - amount: u128, - ) -> types::BitcoinTransactionBuilderResult; -} diff --git a/core/lib/via_btc_client/src/types.rs b/core/lib/via_btc_client/src/types.rs index a598c370d7fa..dba4ac0426f1 100644 --- a/core/lib/via_btc_client/src/types.rs +++ b/core/lib/via_btc_client/src/types.rs @@ -102,7 +102,6 @@ pub struct L1ToL2Message { pub tx_outputs: Vec, } -#[allow(unused)] #[derive(Clone, Debug, PartialEq)] pub enum InscriptionMessage { L1BatchDAReference(L1BatchDAReferenceInput), @@ -150,7 +149,6 @@ lazy_static! { } pub(crate) const VIA_INSCRIPTION_PROTOCOL: &str = "via_inscription_protocol"; -#[allow(unused)] #[derive(Clone, Debug)] pub struct InscriptionRequest { pub message: InscriptionMessage, @@ -159,16 +157,13 @@ pub struct InscriptionRequest { pub commit_tx_input: CommitTxInput, } -#[allow(unused)] #[derive(Clone, Debug)] pub struct InscriberContext { pub fifo_queue: VecDeque, } -#[allow(unused)] const CTX_CAPACITY: usize = 10; -#[allow(unused)] impl InscriberContext { pub fn new() -> Self { Self { @@ -177,7 +172,13 @@ impl InscriberContext { } } -#[allow(unused)] +impl Default for InscriberContext { + fn default() -> Self { + Self::new() + } +} + + #[derive(Clone, Debug)] pub struct InscriberOutput { pub commit_txid: Txid, @@ -260,5 +261,5 @@ impl From for BitcoinError { pub type BitcoinSignerResult = Result; // pub type BitcoinInscriberResult = Result; pub type BitcoinIndexerResult = Result; -#[allow(unused)] + pub type BitcoinTransactionBuilderResult = Result; From 8372a39e245302d4d2ab21dee68cad913e3e345c Mon Sep 17 00:00:00 2001 From: Steph Sinyakov Date: Tue, 20 Aug 2024 14:47:52 +0200 Subject: [PATCH 06/11] fix: add logs, retries to rpc_client --- Cargo.lock | 1 + core/lib/via_btc_client/Cargo.toml | 1 + .../via_btc_client/src/client/rpc_client.rs | 162 +++++++++++++----- core/lib/via_btc_client/src/signer/mod.rs | 4 +- core/lib/via_btc_client/src/traits.rs | 3 +- core/lib/via_btc_client/src/types.rs | 1 - 6 files changed, 126 insertions(+), 46 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 80205513156d..64ad8c2aee56 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7679,6 +7679,7 @@ dependencies = [ "serde_json", "thiserror", "tokio", + "tracing", "zksync_basic_types", "zksync_config", "zksync_da_client", diff --git a/core/lib/via_btc_client/Cargo.toml b/core/lib/via_btc_client/Cargo.toml index 292de6f4ad09..2d72b3b27cd0 100644 --- a/core/lib/via_btc_client/Cargo.toml +++ b/core/lib/via_btc_client/Cargo.toml @@ -34,6 +34,7 @@ serde_json.workspace = true inquire = "0.7.5" anyhow.workspace = true serde.workspace = true +tracing.workspace = true [dev-dependencies] mockall = "0.13.0" diff --git a/core/lib/via_btc_client/src/client/rpc_client.rs b/core/lib/via_btc_client/src/client/rpc_client.rs index 44015db569b3..9231f5d6e626 100644 --- a/core/lib/via_btc_client/src/client/rpc_client.rs +++ b/core/lib/via_btc_client/src/client/rpc_client.rs @@ -2,103 +2,180 @@ use async_trait::async_trait; use bitcoin::{Address, Block, BlockHash, OutPoint, Transaction, Txid}; use bitcoincore_rpc::{ bitcoincore_rpc_json::EstimateMode, - json::{EstimateSmartFeeResult, ScanTxOutRequest}, + json::{EstimateSmartFeeResult, GetBlockchainInfoResult, ScanTxOutRequest}, Client, RpcApi, }; +use tracing::{debug, error, instrument}; use crate::{ traits::BitcoinRpc, types::{Auth, BitcoinRpcResult}, }; +const RPC_MAX_RETRIES: u8 = 3; +const RPC_RETRY_DELAY_MS: u64 = 500; + pub struct BitcoinRpcClient { client: Client, } -#[allow(unused)] impl BitcoinRpcClient { + #[instrument(skip(auth), target = "bitcoin_client")] pub fn new(url: &str, auth: Auth) -> Result { let client = Client::new(url, auth)?; Ok(Self { client }) } + + #[instrument(skip(self, f), target = "bitcoin_client")] + async fn with_retry(&self, f: F) -> BitcoinRpcResult + where + F: Fn() -> BitcoinRpcResult + Send + Sync, + { + let mut retries = 0; + loop { + match f() { + Ok(result) => return Ok(result), + Err(e) if retries < RPC_MAX_RETRIES => { + error!(?e, retries, "RPC call failed, retrying"); + retries += 1; + tokio::time::sleep(tokio::time::Duration::from_millis(RPC_RETRY_DELAY_MS)) + .await; + } + Err(e) => return Err(e), + } + } + } } -#[allow(unused)] #[async_trait] impl BitcoinRpc for BitcoinRpcClient { + #[instrument(skip(self), target = "bitcoin_client")] async fn get_balance(&self, address: &Address) -> BitcoinRpcResult { - let descriptor = format!("addr({})", address); - let request = vec![ScanTxOutRequest::Single(descriptor)]; - - let result = self.client.scan_tx_out_set_blocking(&request)?; - - Ok(result.total_amount.to_sat()) + self.with_retry(|| { + debug!("Getting balance"); + let descriptor = format!("addr({})", address); + let request = vec![ScanTxOutRequest::Single(descriptor)]; + let result = self.client.scan_tx_out_set_blocking(&request)?; + Ok(result.total_amount.to_sat()) + }) + .await } + #[instrument(skip(self, tx_hex), target = "bitcoin_client")] async fn send_raw_transaction(&self, tx_hex: &str) -> BitcoinRpcResult { - self.client - .send_raw_transaction(tx_hex) - .map_err(|e| e.into()) + self.with_retry(|| { + debug!("Sending raw transaction"); + self.client + .send_raw_transaction(tx_hex) + .map_err(|e| e.into()) + }) + .await } + #[instrument(skip(self), target = "bitcoin_client")] async fn list_unspent(&self, address: &Address) -> BitcoinRpcResult> { - let descriptor = format!("addr({})", address); - let request = vec![ScanTxOutRequest::Single(descriptor)]; - - let result = self.client.scan_tx_out_set_blocking(&request)?; - - let unspent: Vec = result - .unspents - .into_iter() - .map(|unspent| OutPoint { - txid: unspent.txid, - vout: unspent.vout, - }) - .collect(); - - Ok(unspent) + self.with_retry(|| { + debug!("Listing unspent outputs"); + let descriptor = format!("addr({})", address); + let request = vec![ScanTxOutRequest::Single(descriptor)]; + let result = self.client.scan_tx_out_set_blocking(&request)?; + let unspent = result + .unspents + .into_iter() + .map(|unspent| OutPoint { + txid: unspent.txid, + vout: unspent.vout, + }) + .collect(); + Ok(unspent) + }) + .await } + #[instrument(skip(self), target = "bitcoin_client")] async fn get_transaction(&self, txid: &Txid) -> BitcoinRpcResult { - self.client - .get_raw_transaction(txid, None) - .map_err(|e| e.into()) + self.with_retry(|| { + debug!("Getting transaction"); + self.client + .get_raw_transaction(txid, None) + .map_err(|e| e.into()) + }) + .await } + #[instrument(skip(self), target = "bitcoin_client")] async fn get_block_count(&self) -> BitcoinRpcResult { - self.client.get_block_count().map_err(|e| e.into()) + self.with_retry(|| { + debug!("Getting block count"); + self.client.get_block_count().map_err(|e| e.into()) + }) + .await } + #[instrument(skip(self), target = "bitcoin_client")] async fn get_block_by_height(&self, block_height: u128) -> BitcoinRpcResult { - let block_hash = self.client.get_block_hash(block_height as u64)?; - self.client.get_block(&block_hash).map_err(|e| e.into()) + self.with_retry(|| { + debug!("Getting block by height"); + let block_hash = self.client.get_block_hash(block_height as u64)?; + self.client.get_block(&block_hash).map_err(|e| e.into()) + }) + .await } + #[instrument(skip(self), target = "bitcoin_client")] async fn get_block_by_hash(&self, block_hash: &BlockHash) -> BitcoinRpcResult { - self.client.get_block(block_hash).map_err(|e| e.into()) + self.with_retry(|| { + debug!("Getting block by hash"); + self.client.get_block(block_hash).map_err(|e| e.into()) + }) + .await } - async fn get_best_block_hash(&self) -> BitcoinRpcResult { - self.client.get_best_block_hash().map_err(|e| e.into()) + #[instrument(skip(self), target = "bitcoin_client")] + async fn get_best_block_hash(&self) -> BitcoinRpcResult { + self.with_retry(|| { + debug!("Getting best block hash"); + self.client.get_best_block_hash().map_err(|e| e.into()) + }) + .await } + #[instrument(skip(self), target = "bitcoin_client")] async fn get_raw_transaction_info( &self, txid: &Txid, ) -> BitcoinRpcResult { - self.client - .get_raw_transaction_info(txid, None) - .map_err(|e| e.into()) + self.with_retry(|| { + debug!("Getting raw transaction info"); + self.client + .get_raw_transaction_info(txid, None) + .map_err(|e| e.into()) + }) + .await } + #[instrument(skip(self), target = "bitcoin_client")] async fn estimate_smart_fee( &self, conf_target: u16, estimate_mode: Option, ) -> BitcoinRpcResult { - self.client - .estimate_smart_fee(conf_target, estimate_mode) - .map_err(|e| e.into()) + self.with_retry(|| { + debug!("Estimating smart fee"); + self.client + .estimate_smart_fee(conf_target, estimate_mode) + .map_err(|e| e.into()) + }) + .await + } + + #[instrument(skip(self), target = "bitcoin_client")] + async fn get_blockchain_info(&self) -> BitcoinRpcResult { + self.with_retry(|| { + debug!("Getting blockchain info"); + self.client.get_blockchain_info().map_err(|e| e.into()) + }) + .await } } @@ -130,6 +207,7 @@ mod tests { async fn get_best_block_hash(&self) -> BitcoinRpcResult; async fn get_raw_transaction_info(&self, txid: &Txid) -> BitcoinRpcResult; async fn estimate_smart_fee(&self, conf_target: u16, estimate_mode: Option) -> BitcoinRpcResult; + async fn get_blockchain_info(&self) -> BitcoinRpcResult; } } diff --git a/core/lib/via_btc_client/src/signer/mod.rs b/core/lib/via_btc_client/src/signer/mod.rs index 3978bdb55643..e6a07fb0767e 100644 --- a/core/lib/via_btc_client/src/signer/mod.rs +++ b/core/lib/via_btc_client/src/signer/mod.rs @@ -152,7 +152,7 @@ mod tests { #[test] fn test_sign_ecdsa() { let key_manager = KeyManager::default(); - let message = Message::from_slice(&[1; 32]).unwrap(); + let message = Message::from_digest_slice(&[1; 32]).unwrap(); let signature = key_manager.sign_ecdsa(message).unwrap(); assert!(key_manager .secp @@ -163,7 +163,7 @@ mod tests { #[test] fn test_sign_schnorr() { let key_manager = KeyManager::default(); - let message = Message::from_slice(&[1; 32]).unwrap(); + let message = Message::from_digest_slice(&[1; 32]).unwrap(); let signature = key_manager.sign_schnorr(message).unwrap(); assert!(key_manager .secp diff --git a/core/lib/via_btc_client/src/traits.rs b/core/lib/via_btc_client/src/traits.rs index d8a34175d405..92e6cd24434b 100644 --- a/core/lib/via_btc_client/src/traits.rs +++ b/core/lib/via_btc_client/src/traits.rs @@ -4,7 +4,7 @@ use bitcoin::{ secp256k1::{All, Secp256k1}, Address, Block, BlockHash, Network, OutPoint, ScriptBuf, Transaction, TxOut, Txid, }; -use bitcoincore_rpc::Auth; +use bitcoincore_rpc::{bitcoincore_rpc_json::GetBlockchainInfoResult, Auth}; use secp256k1::{ ecdsa::Signature as ECDSASignature, schnorr::Signature as SchnorrSignature, Message, PublicKey, }; @@ -66,6 +66,7 @@ pub trait BitcoinRpc: Send + Sync { conf_target: u16, estimate_mode: Option, ) -> BitcoinRpcResult; + async fn get_blockchain_info(&self) -> BitcoinRpcResult; } pub trait BitcoinSigner: Send + Sync { diff --git a/core/lib/via_btc_client/src/types.rs b/core/lib/via_btc_client/src/types.rs index dba4ac0426f1..76aaceb0e791 100644 --- a/core/lib/via_btc_client/src/types.rs +++ b/core/lib/via_btc_client/src/types.rs @@ -178,7 +178,6 @@ impl Default for InscriberContext { } } - #[derive(Clone, Debug)] pub struct InscriberOutput { pub commit_txid: Txid, From e42004770fbaeb5937c596d8b9132b257167c343 Mon Sep 17 00:00:00 2001 From: Steph Sinyakov Date: Tue, 20 Aug 2024 15:08:10 +0200 Subject: [PATCH 07/11] fix: add logs, a few logic fixes for `BtcClient` [ci skip] --- core/lib/via_btc_client/src/client/mod.rs | 242 +++++++++++++++++++-- core/lib/via_btc_client/src/indexer/mod.rs | 4 +- core/lib/via_btc_client/src/traits.rs | 11 +- 3 files changed, 223 insertions(+), 34 deletions(-) diff --git a/core/lib/via_btc_client/src/client/mod.rs b/core/lib/via_btc_client/src/client/mod.rs index 98b0afc0b93a..a3e295dec1fb 100644 --- a/core/lib/via_btc_client/src/client/mod.rs +++ b/core/lib/via_btc_client/src/client/mod.rs @@ -1,6 +1,7 @@ use async_trait::async_trait; use bitcoin::{Address, Block, BlockHash, OutPoint, Transaction, TxOut, Txid}; use bitcoincore_rpc::json::EstimateMode; +use tracing::{debug, error, instrument}; mod rpc_client; @@ -10,7 +11,6 @@ use crate::{ types::{Auth, BitcoinClientResult, BitcoinError, Network}, }; -#[allow(unused)] pub struct BitcoinClient { rpc: Box, network: Network, @@ -18,68 +18,74 @@ pub struct BitcoinClient { #[async_trait] impl BitcoinOps for BitcoinClient { + #[instrument(skip(auth), target = "bitcoin_client")] async fn new(rpc_url: &str, network: Network, auth: Auth) -> BitcoinClientResult where Self: Sized, { + debug!("Creating new BitcoinClient"); let rpc = Box::new(BitcoinRpcClient::new(rpc_url, auth)?); - Ok(Self { rpc, network }) } + #[instrument(skip(self), target = "bitcoin_client")] async fn get_balance(&self, address: &Address) -> BitcoinClientResult { + debug!("Getting balance"); let balance = self.rpc.get_balance(address).await?; Ok(balance as u128) } + #[instrument(skip(self, signed_transaction), target = "bitcoin_client")] async fn broadcast_signed_transaction( &self, signed_transaction: &str, ) -> BitcoinClientResult { + debug!("Broadcasting signed transaction"); let txid = self.rpc.send_raw_transaction(signed_transaction).await?; Ok(txid) } + #[instrument(skip(self), target = "bitcoin_client")] async fn fetch_utxos(&self, address: &Address) -> BitcoinClientResult> { + debug!("Fetching UTXOs"); let outpoints = self.rpc.list_unspent(address).await?; - let mut utxos: Vec<(OutPoint, TxOut)> = vec![]; + let mut utxos = Vec::with_capacity(outpoints.len()); for outpoint in outpoints { + debug!("Fetching transaction for outpoint"); let tx = self.rpc.get_transaction(&outpoint.txid).await?; - let txout = tx - .output - .get(outpoint.vout as usize) - .ok_or(BitcoinError::InvalidOutpoint(outpoint.to_string()))?; + let txout = tx.output.get(outpoint.vout as usize).ok_or_else(|| { + error!("Invalid outpoint"); + BitcoinError::InvalidOutpoint(outpoint.to_string()) + })?; utxos.push((outpoint, txout.clone())); } Ok(utxos) } + #[instrument(skip(self), target = "bitcoin_client")] async fn check_tx_confirmation(&self, txid: &Txid, conf_num: u32) -> BitcoinClientResult { + debug!("Checking transaction confirmation"); let tx_info = self.rpc.get_raw_transaction_info(txid).await?; match tx_info.confirmations { - Some(confirmations) => Ok(confirmations > conf_num), + Some(confirmations) => Ok(confirmations >= conf_num), None => Ok(false), } } + #[instrument(skip(self), target = "bitcoin_client")] async fn fetch_block_height(&self) -> BitcoinClientResult { + debug!("Fetching block height"); let height = self.rpc.get_block_count().await?; Ok(height as u128) } - async fn fetch_block(&self, block_height: u128) -> BitcoinClientResult { - self.rpc.get_block_by_height(block_height).await - } - - async fn fetch_block_by_hash(&self, block_hash: &BlockHash) -> BitcoinClientResult { - self.rpc.get_block_by_hash(block_hash).await - } - + #[instrument(skip(self), target = "bitcoin_client")] async fn get_fee_rate(&self, conf_target: u16) -> BitcoinClientResult { + debug!("Estimating fee rate"); let estimation = self .rpc .estimate_smart_fee(conf_target, Some(EstimateMode::Economical)) @@ -88,20 +94,212 @@ impl BitcoinOps for BitcoinClient { match estimation.fee_rate { Some(fee_rate) => Ok(fee_rate.to_sat()), None => { - let err = match estimation.errors { - Some(errors) => errors.join(", "), - None => "Unknown error during fee estimation".to_string(), - }; + let err = estimation + .errors + .map(|errors| errors.join(", ")) + .unwrap_or_else(|| "Unknown error during fee estimation".to_string()); + error!("Fee estimation failed: {}", err); Err(BitcoinError::FeeEstimationFailed(err)) } } } + fn get_network(&self) -> Network { + self.network + } + + #[instrument(skip(self), target = "bitcoin_client")] + async fn fetch_block(&self, block_height: u128) -> BitcoinClientResult { + debug!("Fetching block"); + self.rpc.get_block_by_height(block_height).await + } + + #[instrument(skip(self), target = "bitcoin_client")] async fn get_transaction(&self, txid: &Txid) -> BitcoinClientResult { + debug!("Getting transaction"); self.rpc.get_transaction(txid).await } - fn get_network(&self) -> Network { - self.network + #[instrument(skip(self), target = "bitcoin_client")] + async fn fetch_block_by_hash(&self, block_hash: &BlockHash) -> BitcoinClientResult { + debug!("Fetching block by hash"); + self.rpc.get_block_by_hash(block_hash).await + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use bitcoin::{absolute::LockTime, hashes::Hash, transaction::Version, Amount, Wtxid}; + use bitcoincore_rpc::{ + bitcoincore_rpc_json::GetBlockchainInfoResult, + json::{EstimateSmartFeeResult, GetRawTransactionResult}, + }; + use mockall::{mock, predicate::*}; + + use super::*; + use crate::types::BitcoinRpcResult; + + mock! { + BitcoinRpc {} + #[async_trait] + impl BitcoinRpc for BitcoinRpc { + async fn get_balance(&self, address: &Address) -> BitcoinClientResult; + async fn send_raw_transaction(&self, tx_hex: &str) -> BitcoinClientResult; + async fn list_unspent(&self, address: &Address) -> BitcoinClientResult>; + async fn get_transaction(&self, txid: &Txid) -> BitcoinClientResult; + async fn get_block_count(&self) -> BitcoinClientResult; + async fn get_block_by_height(&self, block_height: u128) -> BitcoinClientResult; + async fn get_block_by_hash(&self, block_hash: &BlockHash) -> BitcoinClientResult; + async fn get_best_block_hash(&self) -> BitcoinClientResult; + async fn get_raw_transaction_info(&self, txid: &Txid) -> BitcoinClientResult; + async fn estimate_smart_fee(&self, conf_target: u16, estimate_mode: Option) -> BitcoinClientResult; + async fn get_blockchain_info(&self) -> BitcoinRpcResult; + } + } + + #[tokio::test] + async fn test_get_balance() { + let mut mock_rpc = MockBitcoinRpc::new(); + mock_rpc.expect_get_balance().return_once(|_| Ok(1000000)); + + let client = BitcoinClient { + rpc: Box::new(mock_rpc), + network: Network::Bitcoin, + }; + + let address = Address::from_str("bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq") + .unwrap() + .require_network(Network::Bitcoin) + .unwrap(); + let balance = client.get_balance(&address).await.unwrap(); + assert_eq!(balance, 1000000); + } + + #[tokio::test] + async fn test_broadcast_signed_transaction() { + let mut mock_rpc = MockBitcoinRpc::new(); + let expected_txid = + Txid::from_str("4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b") + .unwrap(); + mock_rpc + .expect_send_raw_transaction() + .return_once(move |_| Ok(expected_txid)); + + let client = BitcoinClient { + rpc: Box::new(mock_rpc), + network: Network::Bitcoin, + }; + + let txid = client + .broadcast_signed_transaction("dummy_hex") + .await + .unwrap(); + assert_eq!(txid, expected_txid); + } + + #[tokio::test] + async fn test_fetch_utxos() { + let mut mock_rpc = MockBitcoinRpc::new(); + let outpoint = OutPoint { + txid: Txid::all_zeros(), + vout: 0, + }; + mock_rpc + .expect_list_unspent() + .return_once(move |_| Ok(vec![outpoint])); + mock_rpc.expect_get_transaction().return_once(|_| { + Ok(Transaction { + version: Version::TWO, + lock_time: LockTime::from_height(0u32).unwrap(), + input: vec![], + output: vec![TxOut { + value: Amount::from_sat(50000), + script_pubkey: Default::default(), + }], + }) + }); + + let client = BitcoinClient { + rpc: Box::new(mock_rpc), + network: Network::Bitcoin, + }; + + let address = Address::from_str("bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq") + .unwrap() + .require_network(Network::Bitcoin) + .unwrap(); + let utxos = client.fetch_utxos(&address).await.unwrap(); + assert_eq!(utxos.len(), 1); + assert_eq!(utxos[0].0, outpoint); + assert_eq!(utxos[0].1.value.to_sat(), 50000); + } + + #[tokio::test] + async fn test_check_tx_confirmation() { + let mut mock_rpc = MockBitcoinRpc::new(); + mock_rpc.expect_get_raw_transaction_info().return_once(|_| { + Ok(GetRawTransactionResult { + in_active_chain: None, + hex: vec![], + txid: Txid::all_zeros(), + hash: Wtxid::all_zeros(), + size: 0, + vsize: 0, + version: 0, + locktime: 0, + vin: vec![], + vout: vec![], + blockhash: None, + confirmations: Some(3), + + time: None, + blocktime: None, + }) + }); + + let client = BitcoinClient { + rpc: Box::new(mock_rpc), + network: Network::Bitcoin, + }; + + let txid = Txid::all_zeros(); + let confirmed = client.check_tx_confirmation(&txid, 2).await.unwrap(); + assert!(confirmed); + } + + #[tokio::test] + async fn test_fetch_block_height() { + let mut mock_rpc = MockBitcoinRpc::new(); + mock_rpc.expect_get_block_count().return_once(|| Ok(654321)); + + let client = BitcoinClient { + rpc: Box::new(mock_rpc), + network: Network::Bitcoin, + }; + + let height = client.fetch_block_height().await.unwrap(); + assert_eq!(height, 654321); + } + + #[tokio::test] + async fn test_get_fee_rate() { + let mut mock_rpc = MockBitcoinRpc::new(); + mock_rpc.expect_estimate_smart_fee().return_once(|_, _| { + Ok(EstimateSmartFeeResult { + fee_rate: Some(Amount::from_sat(1000)), + errors: None, + blocks: 0, + }) + }); + + let client = BitcoinClient { + rpc: Box::new(mock_rpc), + network: Network::Bitcoin, + }; + + let fee_rate = client.get_fee_rate(6).await.unwrap(); + assert_eq!(fee_rate, 1000); } } diff --git a/core/lib/via_btc_client/src/indexer/mod.rs b/core/lib/via_btc_client/src/indexer/mod.rs index c79b67b60612..491d7916487d 100644 --- a/core/lib/via_btc_client/src/indexer/mod.rs +++ b/core/lib/via_btc_client/src/indexer/mod.rs @@ -9,7 +9,7 @@ use parser::MessageParser; use crate::{ client::BitcoinClient, - traits::{BitcoinInscriptionIndexerOpt, BitcoinOps}, + traits::{BitcoinIndexerOpt, BitcoinOps}, types::{ BitcoinError, BitcoinIndexerResult, CommonFields, FullInscriptionMessage, L1ToL2Message, Vote, @@ -66,7 +66,7 @@ pub struct BitcoinInscriptionIndexer { } #[async_trait] -impl BitcoinInscriptionIndexerOpt for BitcoinInscriptionIndexer { +impl BitcoinIndexerOpt for BitcoinInscriptionIndexer { async fn new( rpc_url: &str, network: Network, diff --git a/core/lib/via_btc_client/src/traits.rs b/core/lib/via_btc_client/src/traits.rs index 92e6cd24434b..5c0e48ed3d0f 100644 --- a/core/lib/via_btc_client/src/traits.rs +++ b/core/lib/via_btc_client/src/traits.rs @@ -89,18 +89,9 @@ pub trait BitcoinSigner: Send + Sync { fn get_public_key(&self) -> PublicKey; } -// #[allow(dead_code)] -// #[async_trait] -// pub trait BitcoinInscriber: Send + Sync { -// async fn new(config: &str) -> BitcoinInscriberResult -// where -// Self: Sized; -// async fn inscribe(&self, message_type: &str, data: &str) -> BitcoinInscriberResult; -// } - #[allow(dead_code)] #[async_trait] -pub trait BitcoinInscriptionIndexerOpt: Send + Sync { +pub trait BitcoinIndexerOpt: Send + Sync { async fn new( rpc_url: &str, network: Network, From 8dac0f42929ee45c21e75b9b12e1929bf2a4fa07 Mon Sep 17 00:00:00 2001 From: Steph Sinyakov Date: Tue, 20 Aug 2024 17:19:42 +0200 Subject: [PATCH 08/11] fix: add logs, tests for indexer` [ci skip] --- core/lib/via_btc_client/src/client/mod.rs | 8 +- core/lib/via_btc_client/src/indexer/mod.rs | 485 ++++++++++++++++-- core/lib/via_btc_client/src/indexer/parser.rs | 194 ++++++- core/lib/via_btc_client/src/inscriber/mod.rs | 2 +- core/lib/via_btc_client/src/traits.rs | 5 +- core/lib/via_btc_client/src/types.rs | 13 +- 6 files changed, 619 insertions(+), 88 deletions(-) diff --git a/core/lib/via_btc_client/src/client/mod.rs b/core/lib/via_btc_client/src/client/mod.rs index a3e295dec1fb..eaee5c2edd7e 100644 --- a/core/lib/via_btc_client/src/client/mod.rs +++ b/core/lib/via_btc_client/src/client/mod.rs @@ -16,10 +16,9 @@ pub struct BitcoinClient { network: Network, } -#[async_trait] -impl BitcoinOps for BitcoinClient { +impl BitcoinClient { #[instrument(skip(auth), target = "bitcoin_client")] - async fn new(rpc_url: &str, network: Network, auth: Auth) -> BitcoinClientResult + pub(crate) fn new(rpc_url: &str, network: Network, auth: Auth) -> BitcoinClientResult where Self: Sized, { @@ -27,7 +26,10 @@ impl BitcoinOps for BitcoinClient { let rpc = Box::new(BitcoinRpcClient::new(rpc_url, auth)?); Ok(Self { rpc, network }) } +} +#[async_trait] +impl BitcoinOps for BitcoinClient { #[instrument(skip(self), target = "bitcoin_client")] async fn get_balance(&self, address: &Address) -> BitcoinClientResult { debug!("Getting balance"); diff --git a/core/lib/via_btc_client/src/indexer/mod.rs b/core/lib/via_btc_client/src/indexer/mod.rs index 491d7916487d..f13ee553ce81 100644 --- a/core/lib/via_btc_client/src/indexer/mod.rs +++ b/core/lib/via_btc_client/src/indexer/mod.rs @@ -3,6 +3,7 @@ use std::collections::HashMap; use async_trait::async_trait; use bitcoin::{Address, BlockHash, KnownHrp, Network, Txid}; use bitcoincore_rpc::Auth; +use tracing::{debug, error, info, instrument, warn}; mod parser; use parser::MessageParser; @@ -10,12 +11,12 @@ use parser::MessageParser; use crate::{ client::BitcoinClient, traits::{BitcoinIndexerOpt, BitcoinOps}, - types::{ - BitcoinError, BitcoinIndexerResult, CommonFields, FullInscriptionMessage, L1ToL2Message, - Vote, - }, + types, + types::{BitcoinIndexerResult, CommonFields, FullInscriptionMessage, L1ToL2Message, Vote}, }; +/// Represents the state during the bootstrap process +#[derive(Debug)] struct BootstrapState { verifier_addresses: Vec
, proposed_sequencer: Option
, @@ -56,6 +57,7 @@ impl BootstrapState { } } +/// The main indexer struct for processing Bitcoin inscriptions pub struct BitcoinInscriptionIndexer { client: Box, parser: MessageParser, @@ -63,10 +65,12 @@ pub struct BitcoinInscriptionIndexer { sequencer_address: Address, verifier_addresses: Vec
, starting_block_number: u32, + network: Network, } #[async_trait] impl BitcoinIndexerOpt for BitcoinInscriptionIndexer { + #[instrument(skip(rpc_url, network, bootstrap_txids), err)] async fn new( rpc_url: &str, network: Network, @@ -75,11 +79,13 @@ impl BitcoinIndexerOpt for BitcoinInscriptionIndexer { where Self: Sized, { - let client = Box::new(BitcoinClient::new(rpc_url, network, Auth::None).await?); + info!("Creating new BitcoinInscriptionIndexer"); + let client = Box::new(BitcoinClient::new(rpc_url, network, Auth::None)?); let parser = MessageParser::new(network); let mut bootstrap_state = BootstrapState::new(); for txid in bootstrap_txids { + debug!("Processing bootstrap transaction: {}", txid); let tx = client.get_transaction(&txid).await?; let messages = parser.parse_transaction(&tx); @@ -88,99 +94,155 @@ impl BitcoinIndexerOpt for BitcoinInscriptionIndexer { } if bootstrap_state.is_complete() { + info!("Bootstrap process completed"); break; } } - if bootstrap_state.is_complete() { - if let (Some(bridge), Some(sequencer)) = ( - bootstrap_state.bridge_address, - bootstrap_state.proposed_sequencer, - ) { - Ok(Self { - client, - parser, - bridge_address: bridge, - sequencer_address: sequencer, - verifier_addresses: bootstrap_state.verifier_addresses, - starting_block_number: bootstrap_state.starting_block_number, - }) - } else { - Err(BitcoinError::Other( - "Incomplete bootstrap process despite state being marked as complete" - .to_string(), - )) - } - } else { - Err(BitcoinError::Other( - "Bootstrap process did not complete with provided transactions".to_string(), - )) - } + Self::create_indexer(bootstrap_state, client, parser, network) } + #[instrument(skip(self), err)] async fn process_blocks( &self, starting_block: u32, ending_block: u32, ) -> BitcoinIndexerResult> { + info!( + "Processing blocks from {} to {}", + starting_block, ending_block + ); let mut res = Vec::with_capacity((ending_block - starting_block + 1) as usize); for block in starting_block..=ending_block { res.extend(self.process_block(block).await?); } + debug!("Processed {} blocks", ending_block - starting_block + 1); Ok(res) } + #[instrument(skip(self), err)] async fn process_block( &self, block_height: u32, ) -> BitcoinIndexerResult> { + debug!("Processing block at height {}", block_height); if block_height < self.starting_block_number { - return Err(BitcoinError::Other( - "Indexer error: can't get block before starting block".to_string(), - )); + error!("Attempted to process block before starting block"); + return Err(types::IndexerError::InvalidBlockHeight(block_height)); } let block = self.client.fetch_block(block_height as u128).await?; - let messages: Vec = block + let messages: Vec<_> = block .txdata .iter() .flat_map(|tx| self.parser.parse_transaction(tx)) .filter(|message| self.is_valid_message(message)) .collect(); + debug!( + "Processed {} valid messages in block {}", + messages.len(), + block_height + ); Ok(messages) } + #[instrument(skip(self), err)] async fn are_blocks_connected( &self, parent_hash: &BlockHash, child_hash: &BlockHash, ) -> BitcoinIndexerResult { + debug!( + "Checking if blocks are connected: parent {}, child {}", + parent_hash, child_hash + ); let child_block = self.client.fetch_block_by_hash(child_hash).await?; - Ok(child_block.header.prev_blockhash == *parent_hash) + let are_connected = child_block.header.prev_blockhash == *parent_hash; + debug!("Blocks connected: {}", are_connected); + Ok(are_connected) } } impl BitcoinInscriptionIndexer { + fn create_indexer( + bootstrap_state: BootstrapState, + client: Box, + parser: MessageParser, + network: Network, + ) -> BitcoinIndexerResult { + if bootstrap_state.is_complete() { + if let (Some(bridge), Some(sequencer)) = ( + bootstrap_state.bridge_address, + bootstrap_state.proposed_sequencer, + ) { + info!("BitcoinInscriptionIndexer successfully created"); + Ok(Self { + client, + parser, + bridge_address: bridge, + sequencer_address: sequencer, + verifier_addresses: bootstrap_state.verifier_addresses, + starting_block_number: bootstrap_state.starting_block_number, + network, + }) + } else { + error!("Incomplete bootstrap process despite state being marked as complete"); + Err(types::IndexerError::IncompleteBootstrap( + "Incomplete bootstrap process despite state being marked as complete" + .to_string(), + )) + } + } else { + error!("Bootstrap process did not complete with provided transactions"); + Err(types::IndexerError::IncompleteBootstrap( + "Bootstrap process did not complete with provided transactions".to_string(), + )) + } + } + + #[instrument(skip(self, message))] fn is_valid_message(&self, message: &FullInscriptionMessage) -> bool { match message { - FullInscriptionMessage::ProposeSequencer(m) => Self::get_sender_address(&m.common) - .map_or(false, |addr| self.verifier_addresses.contains(&addr)), - FullInscriptionMessage::ValidatorAttestation(m) => Self::get_sender_address(&m.common) - .map_or(false, |addr| self.verifier_addresses.contains(&addr)), - FullInscriptionMessage::L1BatchDAReference(m) => Self::get_sender_address(&m.common) - .map_or(false, |addr| addr == self.sequencer_address), - FullInscriptionMessage::ProofDAReference(m) => Self::get_sender_address(&m.common) - .map_or(false, |addr| addr == self.sequencer_address), + FullInscriptionMessage::ProposeSequencer(m) => { + let is_valid = Self::get_sender_address(&m.common, self.network) + .map_or(false, |addr| self.verifier_addresses.contains(&addr)); + debug!("ProposeSequencer message validity: {}", is_valid); + is_valid + } + FullInscriptionMessage::ValidatorAttestation(m) => { + let is_valid = Self::get_sender_address(&m.common, self.network) + .map_or(false, |addr| self.verifier_addresses.contains(&addr)); + debug!("ValidatorAttestation message validity: {}", is_valid); + is_valid + } + FullInscriptionMessage::L1BatchDAReference(m) => { + let is_valid = Self::get_sender_address(&m.common, self.network) + .map_or(false, |addr| addr == self.sequencer_address); + debug!("L1BatchDAReference message validity: {}", is_valid); + is_valid + } + FullInscriptionMessage::ProofDAReference(m) => { + let is_valid = Self::get_sender_address(&m.common, self.network) + .map_or(false, |addr| addr == self.sequencer_address); + debug!("ProofDAReference message validity: {}", is_valid); + is_valid + } FullInscriptionMessage::L1ToL2Message(m) => { - m.amount > bitcoin::Amount::ZERO && self.is_valid_l1_to_l2_transfer(m) - // check the bridge address in the output + let is_valid = + m.amount > bitcoin::Amount::ZERO && self.is_valid_l1_to_l2_transfer(m); + debug!("L1ToL2Message validity: {}", is_valid); + is_valid + } + FullInscriptionMessage::SystemBootstrapping(_) => { + debug!("SystemBootstrapping message is always valid"); + true } - FullInscriptionMessage::SystemBootstrapping(_) => true, } } + #[instrument(skip(state, message))] fn process_bootstrap_message( state: &mut BootstrapState, message: FullInscriptionMessage, @@ -188,12 +250,16 @@ impl BitcoinInscriptionIndexer { ) { match message { FullInscriptionMessage::SystemBootstrapping(sb) => { + debug!("Processing SystemBootstrapping message"); state.verifier_addresses = sb.input.verifier_p2wpkh_addresses; state.bridge_address = Some(sb.input.bridge_p2wpkh_mpc_address); state.starting_block_number = sb.input.start_block_height; } FullInscriptionMessage::ProposeSequencer(ps) => { - if let Some(sender_address) = Self::get_sender_address(&ps.common) { + debug!("Processing ProposeSequencer message"); + if let Some(sender_address) = Self::get_sender_address(&ps.common, Network::Testnet) + { + // TODO: use actual network if state.verifier_addresses.contains(&sender_address) { state.proposed_sequencer = Some(ps.input.sequencer_new_p2wpkh_address); state.proposed_sequencer_txid = Some(txid); @@ -201,10 +267,13 @@ impl BitcoinInscriptionIndexer { } } FullInscriptionMessage::ValidatorAttestation(va) => { + debug!("Processing ValidatorAttestation message"); if state.proposed_sequencer.is_some() { - if let Some(sender_address) = Self::get_sender_address(&va.common) { + if let Some(sender_address) = + Self::get_sender_address(&va.common, Network::Testnet) + { + // TODO: use actual network if state.verifier_addresses.contains(&sender_address) { - // check if this is an attestation for the proposed sequencer if let Some(proposed_txid) = state.proposed_sequencer_txid { if va.input.reference_txid == proposed_txid { state @@ -216,11 +285,24 @@ impl BitcoinInscriptionIndexer { } } } - _ => {} // ignore other messages + _ => { + debug!("Ignoring non-bootstrap message during bootstrap process"); + } } } - fn get_sender_address(common_fields: &CommonFields) -> Option
{ + #[instrument(skip(self, message))] + fn is_valid_l1_to_l2_transfer(&self, message: &L1ToL2Message) -> bool { + let is_valid = message + .tx_outputs + .iter() + .any(|output| output.script_pubkey == self.bridge_address.script_pubkey()); + debug!("L1ToL2Message transfer validity: {}", is_valid); + is_valid + } + + #[instrument(skip(common_fields))] + fn get_sender_address(common_fields: &CommonFields, network: Network) -> Option
{ secp256k1::XOnlyPublicKey::from_slice(common_fields.encoded_public_key.as_bytes()) .ok() .map(|public_key| { @@ -228,15 +310,306 @@ impl BitcoinInscriptionIndexer { &bitcoin::secp256k1::Secp256k1::new(), public_key, None, - KnownHrp::from(Network::Testnet), // TODO: make it configurable + KnownHrp::from(network), ) }) } +} - fn is_valid_l1_to_l2_transfer(&self, message: &L1ToL2Message) -> bool { - message - .tx_outputs - .iter() - .any(|output| output.script_pubkey == self.bridge_address.script_pubkey()) +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use bitcoin::{ + block::Header, hashes::Hash, Amount, Block, OutPoint, ScriptBuf, Transaction, TxMerkleNode, + TxOut, + }; + use mockall::{mock, predicate::*}; + use secp256k1::Secp256k1; + + use super::*; + use crate::types::BitcoinClientResult; + + mock! { + BitcoinOps {} + #[async_trait] + impl BitcoinOps for BitcoinOps { + async fn get_transaction(&self, txid: &Txid) -> BitcoinClientResult; + async fn fetch_block(&self, block_height: u128) -> BitcoinClientResult; + async fn fetch_block_by_hash(&self, block_hash: &BlockHash) -> BitcoinClientResult; + async fn get_balance(&self, address: &Address) -> BitcoinClientResult; + async fn broadcast_signed_transaction(&self, signed_transaction: &str) -> BitcoinClientResult; + async fn fetch_utxos(&self, address: &Address) -> BitcoinClientResult>; + async fn check_tx_confirmation(&self, txid: &Txid, conf_num: u32) -> BitcoinClientResult; + async fn fetch_block_height(&self) -> BitcoinClientResult; + async fn get_fee_rate(&self, conf_target: u16) -> BitcoinClientResult; + fn get_network(&self) -> Network; + } + } + + fn create_mock_indexer() -> BitcoinInscriptionIndexer { + let mut mock_client = MockBitcoinOps::new(); + mock_client + .expect_get_network() + .returning(|| Network::Testnet); + + let parser = MessageParser::new(Network::Testnet); + let bridge_address = Address::from_str("tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx") + .unwrap() + .require_network(Network::Testnet) + .unwrap(); + let sequencer_address = + Address::from_str("tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7") + .unwrap() + .require_network(Network::Testnet) + .unwrap(); + + BitcoinInscriptionIndexer { + client: Box::new(mock_client), + parser, + bridge_address, + sequencer_address, + verifier_addresses: vec![], + starting_block_number: 0, + network: Network::Testnet, + } + } + + #[tokio::test] + async fn test_are_blocks_connected() { + let parent_hash = BlockHash::all_zeros(); + let child_hash = BlockHash::all_zeros(); + let mock_block = Block { + header: Header { + version: Default::default(), + prev_blockhash: parent_hash, + merkle_root: TxMerkleNode::all_zeros(), + time: 0, + bits: Default::default(), + nonce: 0, + }, + txdata: vec![], + }; + + let mut mock_client = MockBitcoinOps::new(); + mock_client + .expect_fetch_block_by_hash() + .with(eq(child_hash)) + .returning(move |_| Ok(mock_block.clone())); + mock_client + .expect_get_network() + .returning(|| Network::Testnet); + + let indexer = BitcoinInscriptionIndexer { + client: Box::new(mock_client), + parser: MessageParser::new(Network::Testnet), + bridge_address: Address::from_str("tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx") + .unwrap() + .require_network(Network::Testnet) + .unwrap(), + sequencer_address: Address::from_str( + "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7", + ) + .unwrap() + .require_network(Network::Testnet) + .unwrap(), + verifier_addresses: vec![], + starting_block_number: 0, + network: Network::Testnet, + }; + + let result = indexer + .are_blocks_connected(&parent_hash, &child_hash) + .await; + assert!(result.is_ok()); + assert!(result.unwrap()); + } + + #[tokio::test] + async fn test_process_blocks() { + let start_block = 1; + let end_block = 3; + + let mock_block = Block { + header: Header { + version: Default::default(), + prev_blockhash: BlockHash::all_zeros(), + merkle_root: TxMerkleNode::all_zeros(), + time: 0, + bits: Default::default(), + nonce: 0, + }, + txdata: vec![], + }; + + let mut mock_client = MockBitcoinOps::new(); + mock_client + .expect_fetch_block() + .returning(move |_| Ok(mock_block.clone())) + .times(3); + mock_client + .expect_get_network() + .returning(|| Network::Testnet); + + let indexer = BitcoinInscriptionIndexer { + client: Box::new(mock_client), + parser: MessageParser::new(Network::Testnet), + bridge_address: Address::from_str("tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx") + .unwrap() + .require_network(Network::Testnet) + .unwrap(), + sequencer_address: Address::from_str( + "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7", + ) + .unwrap() + .require_network(Network::Testnet) + .unwrap(), + verifier_addresses: vec![], + starting_block_number: 0, + network: Network::Testnet, + }; + + let result = indexer.process_blocks(start_block, end_block).await; + assert!(result.is_ok()); + assert_eq!(result.unwrap().len(), 0); + } + + #[tokio::test] + async fn test_is_valid_message() { + let indexer = create_mock_indexer(); + + let propose_sequencer = FullInscriptionMessage::ProposeSequencer(types::ProposeSequencer { + common: CommonFields { + schnorr_signature: bitcoin::taproot::Signature::from_slice(&[0; 64]).unwrap(), + encoded_public_key: bitcoin::script::PushBytesBuf::from([0u8; 32]), + }, + input: types::ProposeSequencerInput { + sequencer_new_p2wpkh_address: indexer.sequencer_address.clone(), + }, + }); + assert!(!indexer.is_valid_message(&propose_sequencer)); + + let validator_attestation = + FullInscriptionMessage::ValidatorAttestation(types::ValidatorAttestation { + common: CommonFields { + schnorr_signature: bitcoin::taproot::Signature::from_slice(&[0; 64]).unwrap(), + encoded_public_key: bitcoin::script::PushBytesBuf::from([0u8; 32]), + }, + input: types::ValidatorAttestationInput { + reference_txid: Txid::all_zeros(), + attestation: Vote::Ok, + }, + }); + assert!(!indexer.is_valid_message(&validator_attestation)); + + let l1_batch_da_reference = + FullInscriptionMessage::L1BatchDAReference(types::L1BatchDAReference { + common: CommonFields { + schnorr_signature: bitcoin::taproot::Signature::from_slice(&[0; 64]).unwrap(), + encoded_public_key: bitcoin::script::PushBytesBuf::from([0u8; 32]), + }, + input: crate::types::L1BatchDAReferenceInput { + l1_batch_hash: zksync_basic_types::H256::zero(), + l1_batch_index: zksync_types::L1BatchNumber(0), + da_identifier: "test".to_string(), + blob_id: "test".to_string(), + }, + }); + assert!(!indexer.is_valid_message(&l1_batch_da_reference)); + + let l1_to_l2_message = FullInscriptionMessage::L1ToL2Message(L1ToL2Message { + common: CommonFields { + schnorr_signature: bitcoin::taproot::Signature::from_slice(&[0; 64]).unwrap(), + encoded_public_key: bitcoin::script::PushBytesBuf::from([0u8; 32]), + }, + amount: Amount::from_sat(1000), + input: types::L1ToL2MessageInput { + receiver_l2_address: zksync_types::Address::zero(), + l2_contract_address: zksync_types::Address::zero(), + call_data: vec![], + }, + tx_outputs: vec![TxOut { + value: Amount::from_sat(1000), + script_pubkey: indexer.bridge_address.script_pubkey(), + }], + }); + assert!(indexer.is_valid_message(&l1_to_l2_message)); + + let system_bootstrapping = + FullInscriptionMessage::SystemBootstrapping(crate::types::SystemBootstrapping { + common: CommonFields { + schnorr_signature: bitcoin::taproot::Signature::from_slice(&[0; 64]).unwrap(), + encoded_public_key: bitcoin::script::PushBytesBuf::from([0u8; 32]), + }, + input: crate::types::SystemBootstrappingInput { + start_block_height: 0, + bridge_p2wpkh_mpc_address: indexer.bridge_address.clone(), + verifier_p2wpkh_addresses: vec![], + }, + }); + assert!(indexer.is_valid_message(&system_bootstrapping)); + } + + #[test] + fn test_get_sender_address() { + let network = Network::Testnet; + let secp = Secp256k1::new(); + let (_secret_key, p) = secp.generate_keypair(&mut rand::thread_rng()); + let x_only_public_key = p.x_only_public_key(); + + let common_fields = CommonFields { + schnorr_signature: bitcoin::taproot::Signature::from_slice(&[0; 64]).unwrap(), + encoded_public_key: bitcoin::script::PushBytesBuf::from( + x_only_public_key.0.serialize(), + ), + }; + + let sender_address = BitcoinInscriptionIndexer::get_sender_address(&common_fields, network); + assert!(sender_address.is_some()); + + let expected_address = + Address::p2tr(&secp, x_only_public_key.0, None, KnownHrp::from(network)); + assert_eq!(sender_address.unwrap(), expected_address); + } + + #[tokio::test] + async fn test_is_valid_l1_to_l2_transfer() { + let indexer = create_mock_indexer(); + + let valid_message = L1ToL2Message { + common: CommonFields { + schnorr_signature: bitcoin::taproot::Signature::from_slice(&[0; 64]).unwrap(), + encoded_public_key: bitcoin::script::PushBytesBuf::from([0u8; 32]), + }, + amount: Amount::from_sat(1000), + input: types::L1ToL2MessageInput { + receiver_l2_address: zksync_types::Address::zero(), + l2_contract_address: zksync_types::Address::zero(), + call_data: vec![], + }, + tx_outputs: vec![TxOut { + value: Amount::from_sat(1000), + script_pubkey: indexer.bridge_address.script_pubkey(), + }], + }; + assert!(indexer.is_valid_l1_to_l2_transfer(&valid_message)); + + let invalid_message = L1ToL2Message { + common: CommonFields { + schnorr_signature: bitcoin::taproot::Signature::from_slice(&[0; 64]).unwrap(), + encoded_public_key: bitcoin::script::PushBytesBuf::from([0u8; 32]), + }, + amount: Amount::from_sat(1000), + input: types::L1ToL2MessageInput { + receiver_l2_address: zksync_types::Address::zero(), + l2_contract_address: zksync_types::Address::zero(), + call_data: vec![], + }, + tx_outputs: vec![TxOut { + value: Amount::from_sat(1000), + script_pubkey: ScriptBuf::new(), + }], + }; + assert!(!indexer.is_valid_l1_to_l2_transfer(&invalid_message)); } } diff --git a/core/lib/via_btc_client/src/indexer/parser.rs b/core/lib/via_btc_client/src/indexer/parser.rs index 84009a652365..b5a90672745b 100644 --- a/core/lib/via_btc_client/src/indexer/parser.rs +++ b/core/lib/via_btc_client/src/indexer/parser.rs @@ -5,6 +5,7 @@ use bitcoin::{ taproot::{ControlBlock, Signature as TaprootSignature}, Address, Amount, Network, ScriptBuf, Transaction, Txid, }; +use tracing::{debug, instrument, warn}; use zksync_basic_types::H256; use zksync_types::{Address as EVMAddress, L1BatchNumber}; @@ -18,7 +19,14 @@ use crate::{ }, }; +// Using constants to define the minimum number of instructions can help to make parsing more quick const MIN_WITNESS_LENGTH: usize = 3; +const MIN_SYSTEM_BOOTSTRAPPING_INSTRUCTIONS: usize = 5; +const MIN_PROPOSE_SEQUENCER_INSTRUCTIONS: usize = 3; +const MIN_VALIDATOR_ATTESTATION_INSTRUCTIONS: usize = 4; +const MIN_L1_BATCH_DA_REFERENCE_INSTRUCTIONS: usize = 6; +const MIN_PROOF_DA_REFERENCE_INSTRUCTIONS: usize = 5; +const MIN_L1_TO_L2_MESSAGE_INSTRUCTIONS: usize = 5; pub struct MessageParser { network: Network, @@ -29,29 +37,47 @@ impl MessageParser { Self { network } } + #[instrument(skip(self, tx))] pub fn parse_transaction(&self, tx: &Transaction) -> Vec { + debug!("Parsing transaction"); tx.input .iter() .filter_map(|input| self.parse_input(input, tx)) .collect() } + #[instrument(skip(self, input, tx))] fn parse_input(&self, input: &bitcoin::TxIn, tx: &Transaction) -> Option { let witness = &input.witness; - // A valid P2TR input for our inscriptions should have at least 3 witness elements: - // 1. Signature - // 2. Inscription script - // 3. Control block if witness.len() < MIN_WITNESS_LENGTH { + debug!("Witness length is less than minimum required"); return None; } - let signature = TaprootSignature::from_slice(&witness[0]).ok()?; + let signature = match TaprootSignature::from_slice(&witness[0]) { + Ok(sig) => sig, + Err(e) => { + warn!("Failed to parse Taproot signature: {}", e); + return None; + } + }; let script = ScriptBuf::from_bytes(witness[1].to_vec()); - let control_block = ControlBlock::decode(&witness[2]).ok()?; + let control_block = match ControlBlock::decode(&witness[2]) { + Ok(cb) => cb, + Err(e) => { + warn!("Failed to decode control block: {}", e); + return None; + } + }; let instructions: Vec<_> = script.instructions().filter_map(Result::ok).collect(); - let via_index = find_via_inscription_protocol(&instructions)?; + let via_index = match find_via_inscription_protocol(&instructions) { + Some(index) => index, + None => { + debug!("VIA inscription protocol not found in script"); + return None; + } + }; let public_key = control_block.internal_key; let common_fields = CommonFields { @@ -62,6 +88,7 @@ impl MessageParser { self.parse_message(tx, &instructions[via_index..], &common_fields) } + #[instrument(skip(self, tx, instructions, common_fields))] fn parse_message( &self, tx: &Transaction, @@ -74,45 +101,57 @@ impl MessageParser { Instruction::PushBytes(bytes) if bytes.as_bytes() == types::SYSTEM_BOOTSTRAPPING_MSG.as_bytes() => { + debug!("Parsing system bootstrapping message"); self.parse_system_bootstrapping(instructions, common_fields) } Instruction::PushBytes(bytes) if bytes.as_bytes() == types::PROPOSE_SEQUENCER_MSG.as_bytes() => { + debug!("Parsing propose sequencer message"); self.parse_propose_sequencer(instructions, common_fields) } Instruction::PushBytes(bytes) if bytes.as_bytes() == types::VALIDATOR_ATTESTATION_MSG.as_bytes() => { + debug!("Parsing validator attestation message"); self.parse_validator_attestation(instructions, common_fields) } Instruction::PushBytes(bytes) if bytes.as_bytes() == types::L1_BATCH_DA_REFERENCE_MSG.as_bytes() => { + debug!("Parsing L1 batch DA reference message"); self.parse_l1_batch_da_reference(instructions, common_fields) } Instruction::PushBytes(bytes) if bytes.as_bytes() == types::PROOF_DA_REFERENCE_MSG.as_bytes() => { + debug!("Parsing proof DA reference message"); self.parse_proof_da_reference(instructions, common_fields) } Instruction::PushBytes(bytes) if bytes.as_bytes() == types::L1_TO_L2_MSG.as_bytes() => { + debug!("Parsing L1 to L2 message"); self.parse_l1_to_l2_message(tx, instructions, common_fields) } - _ => None, + _ => { + // do we need warning here? + warn!("Unknown message type"); + None + } } } + #[instrument(skip(self, instructions, common_fields))] fn parse_system_bootstrapping( &self, instructions: &[Instruction], common_fields: &CommonFields, ) -> Option { - if instructions.len() < 5 { + if instructions.len() < MIN_SYSTEM_BOOTSTRAPPING_INSTRUCTIONS { + warn!("Insufficient instructions for system bootstrapping"); return None; } - let start_block_height = u32::from_be_bytes( + let height = u32::from_be_bytes( instructions .get(2)? .push_bytes()? @@ -120,6 +159,10 @@ impl MessageParser { .try_into() .ok()?, ); + let start_block_height = { + debug!("Parsed start block height: {}", height); + height + }; let verifier_addresses = instructions[3..] .iter() @@ -138,6 +181,8 @@ impl MessageParser { }) .collect::>(); + debug!("Parsed {} verifier addresses", verifier_addresses.len()); + let bridge_address = instructions.last().and_then(|instr| { if let Instruction::PushBytes(bytes) = instr { std::str::from_utf8(bytes.as_bytes()).ok().and_then(|s| { @@ -151,6 +196,8 @@ impl MessageParser { } })?; + debug!("Parsed bridge address"); + Some(Message::SystemBootstrapping(SystemBootstrapping { common: common_fields.clone(), input: SystemBootstrappingInput { @@ -161,12 +208,14 @@ impl MessageParser { })) } + #[instrument(skip(self, instructions, common_fields))] fn parse_propose_sequencer( &self, instructions: &[Instruction], common_fields: &CommonFields, ) -> Option { - if instructions.len() < 3 { + if instructions.len() < MIN_PROPOSE_SEQUENCER_INSTRUCTIONS { + warn!("Insufficient instructions for propose sequencer"); return None; } @@ -183,6 +232,8 @@ impl MessageParser { } })?; + debug!("Parsed sequencer address"); + Some(Message::ProposeSequencer(ProposeSequencer { common: common_fields.clone(), input: ProposeSequencerInput { @@ -191,23 +242,39 @@ impl MessageParser { })) } + #[instrument(skip(self, instructions, common_fields))] fn parse_validator_attestation( &self, instructions: &[Instruction], common_fields: &CommonFields, ) -> Option { - if instructions.len() < 4 { + if instructions.len() < MIN_VALIDATOR_ATTESTATION_INSTRUCTIONS { + warn!("Insufficient instructions for validator attestation"); return None; } - let reference_txid = - Txid::from_slice(instructions.get(2)?.push_bytes()?.as_bytes()).ok()?; + let reference_txid = match Txid::from_slice(instructions.get(2)?.push_bytes()?.as_bytes()) { + Ok(txid) => { + debug!("Parsed reference txid"); + txid + } + Err(e) => { + warn!("Failed to parse reference txid: {}", e); + return None; + } + }; + let attestation = match instructions.get(3)?.push_bytes()?.as_bytes() { b"OP_1" => Vote::Ok, b"OP_0" => Vote::NotOk, - _ => return None, + _ => { + warn!("Invalid attestation value"); + return None; + } }; + debug!("Parsed attestation: {:?}", attestation); + Some(Message::ValidatorAttestation(ValidatorAttestation { common: common_fields.clone(), input: ValidatorAttestationInput { @@ -217,16 +284,20 @@ impl MessageParser { })) } + #[instrument(skip(self, instructions, common_fields))] fn parse_l1_batch_da_reference( &self, instructions: &[Instruction], common_fields: &CommonFields, ) -> Option { - if instructions.len() < 6 { + if instructions.len() < MIN_L1_BATCH_DA_REFERENCE_INSTRUCTIONS { + warn!("Insufficient instructions for L1 batch DA reference"); return None; } let l1_batch_hash = H256::from_slice(instructions.get(2)?.push_bytes()?.as_bytes()); + debug!("Parsed L1 batch hash"); + let l1_batch_index = L1BatchNumber(u32::from_be_bytes( instructions .get(3)? @@ -235,12 +306,17 @@ impl MessageParser { .try_into() .ok()?, )); + debug!("Parsed L1 batch index: {}", l1_batch_index); + let da_identifier = std::str::from_utf8(instructions.get(4)?.push_bytes()?.as_bytes()) .ok()? .to_string(); + debug!("Parsed DA identifier: {}", da_identifier); + let blob_id = std::str::from_utf8(instructions.get(5)?.push_bytes()?.as_bytes()) .ok()? .to_string(); + debug!("Parsed blob ID: {}", blob_id); Some(Message::L1BatchDAReference(L1BatchDAReference { common: common_fields.clone(), @@ -253,23 +329,38 @@ impl MessageParser { })) } + #[instrument(skip(self, instructions, common_fields))] fn parse_proof_da_reference( &self, instructions: &[Instruction], common_fields: &CommonFields, ) -> Option { - if instructions.len() < 5 { + if instructions.len() < MIN_PROOF_DA_REFERENCE_INSTRUCTIONS { + warn!("Insufficient instructions for proof DA reference"); return None; } let l1_batch_reveal_txid = - Txid::from_slice(instructions.get(2)?.push_bytes()?.as_bytes()).ok()?; + match Txid::from_slice(instructions.get(2)?.push_bytes()?.as_bytes()) { + Ok(txid) => { + debug!("Parsed L1 batch reveal txid"); + txid + } + Err(e) => { + warn!("Failed to parse L1 batch reveal txid: {}", e); + return None; + } + }; + let da_identifier = std::str::from_utf8(instructions.get(3)?.push_bytes()?.as_bytes()) .ok()? .to_string(); + debug!("Parsed DA identifier: {}", da_identifier); + let blob_id = std::str::from_utf8(instructions.get(4)?.push_bytes()?.as_bytes()) .ok()? .to_string(); + debug!("Parsed blob ID: {}", blob_id); Some(Message::ProofDAReference(ProofDAReference { common: common_fields.clone(), @@ -281,21 +372,28 @@ impl MessageParser { })) } + #[instrument(skip(self, tx, instructions, common_fields))] fn parse_l1_to_l2_message( &self, tx: &Transaction, instructions: &[Instruction], common_fields: &CommonFields, ) -> Option { - if instructions.len() < 5 { + if instructions.len() < MIN_L1_TO_L2_MESSAGE_INSTRUCTIONS { + warn!("Insufficient instructions for L1 to L2 message"); return None; } let receiver_l2_address = EVMAddress::from_slice(instructions.get(2)?.push_bytes()?.as_bytes()); + debug!("Parsed receiver L2 address"); + let l2_contract_address = EVMAddress::from_slice(instructions.get(3)?.push_bytes()?.as_bytes()); + debug!("Parsed L2 contract address"); + let call_data = instructions.get(4)?.push_bytes()?.as_bytes().to_vec(); + debug!("Parsed call data, length: {}", call_data.len()); let amount = tx .output @@ -303,6 +401,7 @@ impl MessageParser { .find(|output| output.script_pubkey.is_p2wpkh()) .map(|output| output.value) .unwrap_or(Amount::ZERO); + debug!("Parsed amount: {}", amount); Some(Message::L1ToL2Message(L1ToL2Message { common: common_fields.clone(), @@ -312,14 +411,63 @@ impl MessageParser { l2_contract_address, call_data, }, - tx_outputs: tx.output.clone(), // include all transaction outputs + tx_outputs: tx.output.clone(), })) } } +#[instrument(skip(instructions))] fn find_via_inscription_protocol(instructions: &[Instruction]) -> Option { - // TODO: also check first part of the script (OP_CHECKSIG and other stuff) - instructions.iter().position(|instr| { + let position = instructions.iter().position(|instr| { matches!(instr, Instruction::PushBytes(bytes) if bytes.as_bytes() == types::VIA_INSCRIPTION_PROTOCOL.as_bytes()) - }) + }); + + if let Some(index) = position { + debug!("Found VIA inscription protocol at index {}", index); + } else { + debug!("VIA inscription protocol not found"); + } + + position +} + +#[cfg(test)] +mod tests { + use bitcoin::{consensus::encode::deserialize, hashes::hex::FromHex}; + + use super::*; + + fn setup_test_transaction() -> Transaction { + // TODO: Replace with a real transaction + let tx_hex = "00001a1abbf8"; + deserialize(&Vec::from_hex(tx_hex).unwrap()).unwrap() + } + + #[ignore] + #[test] + fn test_parse_transaction() { + let network = Network::Bitcoin; + let parser = MessageParser::new(network); + let tx = setup_test_transaction(); + + let messages = parser.parse_transaction(&tx); + assert_eq!(messages.len(), 1); + } + + #[ignore] + #[test] + fn test_parse_system_bootstrapping() { + let network = Network::Bitcoin; + let parser = MessageParser::new(network); + let tx = setup_test_transaction(); + + if let Some(Message::SystemBootstrapping(bootstrapping)) = + parser.parse_transaction(&tx).pop() + { + assert_eq!(bootstrapping.input.start_block_height, 10); + assert_eq!(bootstrapping.input.verifier_p2wpkh_addresses.len(), 1); + } else { + panic!("Expected SystemBootstrapping message"); + } + } } diff --git a/core/lib/via_btc_client/src/inscriber/mod.rs b/core/lib/via_btc_client/src/inscriber/mod.rs index 469ea6445ba7..976646e46ae9 100644 --- a/core/lib/via_btc_client/src/inscriber/mod.rs +++ b/core/lib/via_btc_client/src/inscriber/mod.rs @@ -67,7 +67,7 @@ impl Inscriber { signer_private_key: &str, persisted_ctx: Option, ) -> Result { - let client = Box::new(BitcoinClient::new(rpc_url, network, auth).await?); + let client = Box::new(BitcoinClient::new(rpc_url, network, auth)?); let signer = Box::new(KeyManager::new(signer_private_key, network)?); let context = persisted_ctx.unwrap_or_default(); diff --git a/core/lib/via_btc_client/src/traits.rs b/core/lib/via_btc_client/src/traits.rs index 5c0e48ed3d0f..752c6dbc4723 100644 --- a/core/lib/via_btc_client/src/traits.rs +++ b/core/lib/via_btc_client/src/traits.rs @@ -4,7 +4,7 @@ use bitcoin::{ secp256k1::{All, Secp256k1}, Address, Block, BlockHash, Network, OutPoint, ScriptBuf, Transaction, TxOut, Txid, }; -use bitcoincore_rpc::{bitcoincore_rpc_json::GetBlockchainInfoResult, Auth}; +use bitcoincore_rpc::bitcoincore_rpc_json::GetBlockchainInfoResult; use secp256k1::{ ecdsa::Signature as ECDSASignature, schnorr::Signature as SchnorrSignature, Message, PublicKey, }; @@ -18,9 +18,6 @@ use crate::{ #[allow(dead_code)] #[async_trait] pub trait BitcoinOps: Send + Sync { - async fn new(rpc_url: &str, network: Network, auth: Auth) -> BitcoinClientResult - where - Self: Sized; async fn get_balance(&self, address: &Address) -> BitcoinClientResult; async fn broadcast_signed_transaction( &self, diff --git a/core/lib/via_btc_client/src/types.rs b/core/lib/via_btc_client/src/types.rs index 76aaceb0e791..de6198cc0181 100644 --- a/core/lib/via_btc_client/src/types.rs +++ b/core/lib/via_btc_client/src/types.rs @@ -257,8 +257,19 @@ impl From for BitcoinError { } } +/// Custom error type for the BitcoinInscriptionIndexer +#[derive(Error, Debug)] +pub enum IndexerError { + #[error("Bootstrap process incomplete: {0}")] + IncompleteBootstrap(String), + #[error("Invalid block height: {0}")] + InvalidBlockHeight(u32), + #[error("Bitcoin client error: {0}")] + BitcoinClientError(#[from] BitcoinError), +} + +pub type BitcoinIndexerResult = std::result::Result; pub type BitcoinSignerResult = Result; // pub type BitcoinInscriberResult = Result; -pub type BitcoinIndexerResult = Result; pub type BitcoinTransactionBuilderResult = Result; From d0afa038cad2bfc463b0d6236e6b1c15788ad2e5 Mon Sep 17 00:00:00 2001 From: Steph Sinyakov Date: Tue, 20 Aug 2024 19:27:40 +0200 Subject: [PATCH 09/11] fix: add logs, script_builder refactoring for inscriber` [ci skip] --- .../via_btc_client/src/client/rpc_client.rs | 26 +- core/lib/via_btc_client/src/indexer/mod.rs | 16 +- core/lib/via_btc_client/src/indexer/parser.rs | 41 ++- core/lib/via_btc_client/src/inscriber/fee.rs | 11 +- .../src/inscriber/internal_type.rs | 5 + core/lib/via_btc_client/src/inscriber/mod.rs | 140 ++++++-- .../src/inscriber/script_builder.rs | 338 +++++++++--------- core/lib/via_btc_client/src/traits.rs | 11 +- core/lib/via_btc_client/src/types.rs | 2 +- 9 files changed, 354 insertions(+), 236 deletions(-) diff --git a/core/lib/via_btc_client/src/client/rpc_client.rs b/core/lib/via_btc_client/src/client/rpc_client.rs index 9231f5d6e626..45ada7bea3cb 100644 --- a/core/lib/via_btc_client/src/client/rpc_client.rs +++ b/core/lib/via_btc_client/src/client/rpc_client.rs @@ -20,13 +20,13 @@ pub struct BitcoinRpcClient { } impl BitcoinRpcClient { - #[instrument(skip(auth), target = "bitcoin_client")] + #[instrument(skip(auth), target = "bitcoin_client::rpc_client")] pub fn new(url: &str, auth: Auth) -> Result { let client = Client::new(url, auth)?; Ok(Self { client }) } - #[instrument(skip(self, f), target = "bitcoin_client")] + #[instrument(skip(self, f), target = "bitcoin_client::rpc_client")] async fn with_retry(&self, f: F) -> BitcoinRpcResult where F: Fn() -> BitcoinRpcResult + Send + Sync, @@ -49,7 +49,7 @@ impl BitcoinRpcClient { #[async_trait] impl BitcoinRpc for BitcoinRpcClient { - #[instrument(skip(self), target = "bitcoin_client")] + #[instrument(skip(self), target = "bitcoin_client::rpc_client")] async fn get_balance(&self, address: &Address) -> BitcoinRpcResult { self.with_retry(|| { debug!("Getting balance"); @@ -61,7 +61,7 @@ impl BitcoinRpc for BitcoinRpcClient { .await } - #[instrument(skip(self, tx_hex), target = "bitcoin_client")] + #[instrument(skip(self, tx_hex), target = "bitcoin_client::rpc_client")] async fn send_raw_transaction(&self, tx_hex: &str) -> BitcoinRpcResult { self.with_retry(|| { debug!("Sending raw transaction"); @@ -72,7 +72,7 @@ impl BitcoinRpc for BitcoinRpcClient { .await } - #[instrument(skip(self), target = "bitcoin_client")] + #[instrument(skip(self), target = "bitcoin_client::rpc_client")] async fn list_unspent(&self, address: &Address) -> BitcoinRpcResult> { self.with_retry(|| { debug!("Listing unspent outputs"); @@ -92,7 +92,7 @@ impl BitcoinRpc for BitcoinRpcClient { .await } - #[instrument(skip(self), target = "bitcoin_client")] + #[instrument(skip(self), target = "bitcoin_client::rpc_client")] async fn get_transaction(&self, txid: &Txid) -> BitcoinRpcResult { self.with_retry(|| { debug!("Getting transaction"); @@ -103,7 +103,7 @@ impl BitcoinRpc for BitcoinRpcClient { .await } - #[instrument(skip(self), target = "bitcoin_client")] + #[instrument(skip(self), target = "bitcoin_client::rpc_client")] async fn get_block_count(&self) -> BitcoinRpcResult { self.with_retry(|| { debug!("Getting block count"); @@ -112,7 +112,7 @@ impl BitcoinRpc for BitcoinRpcClient { .await } - #[instrument(skip(self), target = "bitcoin_client")] + #[instrument(skip(self), target = "bitcoin_client::rpc_client")] async fn get_block_by_height(&self, block_height: u128) -> BitcoinRpcResult { self.with_retry(|| { debug!("Getting block by height"); @@ -122,7 +122,7 @@ impl BitcoinRpc for BitcoinRpcClient { .await } - #[instrument(skip(self), target = "bitcoin_client")] + #[instrument(skip(self), target = "bitcoin_client::rpc_client")] async fn get_block_by_hash(&self, block_hash: &BlockHash) -> BitcoinRpcResult { self.with_retry(|| { debug!("Getting block by hash"); @@ -131,7 +131,7 @@ impl BitcoinRpc for BitcoinRpcClient { .await } - #[instrument(skip(self), target = "bitcoin_client")] + #[instrument(skip(self), target = "bitcoin_client::rpc_client")] async fn get_best_block_hash(&self) -> BitcoinRpcResult { self.with_retry(|| { debug!("Getting best block hash"); @@ -140,7 +140,7 @@ impl BitcoinRpc for BitcoinRpcClient { .await } - #[instrument(skip(self), target = "bitcoin_client")] + #[instrument(skip(self), target = "bitcoin_client::rpc_client")] async fn get_raw_transaction_info( &self, txid: &Txid, @@ -154,7 +154,7 @@ impl BitcoinRpc for BitcoinRpcClient { .await } - #[instrument(skip(self), target = "bitcoin_client")] + #[instrument(skip(self), target = "bitcoin_client::rpc_client")] async fn estimate_smart_fee( &self, conf_target: u16, @@ -169,7 +169,7 @@ impl BitcoinRpc for BitcoinRpcClient { .await } - #[instrument(skip(self), target = "bitcoin_client")] + #[instrument(skip(self), target = "bitcoin_client::rpc_client")] async fn get_blockchain_info(&self) -> BitcoinRpcResult { self.with_retry(|| { debug!("Getting blockchain info"); diff --git a/core/lib/via_btc_client/src/indexer/mod.rs b/core/lib/via_btc_client/src/indexer/mod.rs index f13ee553ce81..e4dd10e7ecce 100644 --- a/core/lib/via_btc_client/src/indexer/mod.rs +++ b/core/lib/via_btc_client/src/indexer/mod.rs @@ -70,7 +70,7 @@ pub struct BitcoinInscriptionIndexer { #[async_trait] impl BitcoinIndexerOpt for BitcoinInscriptionIndexer { - #[instrument(skip(rpc_url, network, bootstrap_txids), err)] + #[instrument(skip(rpc_url, network, bootstrap_txids), target = "bitcoin_indexer")] async fn new( rpc_url: &str, network: Network, @@ -102,7 +102,7 @@ impl BitcoinIndexerOpt for BitcoinInscriptionIndexer { Self::create_indexer(bootstrap_state, client, parser, network) } - #[instrument(skip(self), err)] + #[instrument(skip(self), target = "bitcoin_indexer")] async fn process_blocks( &self, starting_block: u32, @@ -120,7 +120,7 @@ impl BitcoinIndexerOpt for BitcoinInscriptionIndexer { Ok(res) } - #[instrument(skip(self), err)] + #[instrument(skip(self), target = "bitcoin_indexer")] async fn process_block( &self, block_height: u32, @@ -148,7 +148,7 @@ impl BitcoinIndexerOpt for BitcoinInscriptionIndexer { Ok(messages) } - #[instrument(skip(self), err)] + #[instrument(skip(self), target = "bitcoin_indexer")] async fn are_blocks_connected( &self, parent_hash: &BlockHash, @@ -202,7 +202,7 @@ impl BitcoinInscriptionIndexer { } } - #[instrument(skip(self, message))] + #[instrument(skip(self, message), target = "bitcoin_indexer")] fn is_valid_message(&self, message: &FullInscriptionMessage) -> bool { match message { FullInscriptionMessage::ProposeSequencer(m) => { @@ -242,7 +242,7 @@ impl BitcoinInscriptionIndexer { } } - #[instrument(skip(state, message))] + #[instrument(skip(state, message), target = "bitcoin_indexer")] fn process_bootstrap_message( state: &mut BootstrapState, message: FullInscriptionMessage, @@ -291,7 +291,7 @@ impl BitcoinInscriptionIndexer { } } - #[instrument(skip(self, message))] + #[instrument(skip(self, message), target = "bitcoin_indexer")] fn is_valid_l1_to_l2_transfer(&self, message: &L1ToL2Message) -> bool { let is_valid = message .tx_outputs @@ -301,7 +301,7 @@ impl BitcoinInscriptionIndexer { is_valid } - #[instrument(skip(common_fields))] + #[instrument(skip(common_fields), target = "bitcoin_indexer")] fn get_sender_address(common_fields: &CommonFields, network: Network) -> Option
{ secp256k1::XOnlyPublicKey::from_slice(common_fields.encoded_public_key.as_bytes()) .ok() diff --git a/core/lib/via_btc_client/src/indexer/parser.rs b/core/lib/via_btc_client/src/indexer/parser.rs index b5a90672745b..01a5636c09e9 100644 --- a/core/lib/via_btc_client/src/indexer/parser.rs +++ b/core/lib/via_btc_client/src/indexer/parser.rs @@ -37,7 +37,7 @@ impl MessageParser { Self { network } } - #[instrument(skip(self, tx))] + #[instrument(skip(self, tx), target = "bitcoin_indexer::parser")] pub fn parse_transaction(&self, tx: &Transaction) -> Vec { debug!("Parsing transaction"); tx.input @@ -46,7 +46,7 @@ impl MessageParser { .collect() } - #[instrument(skip(self, input, tx))] + #[instrument(skip(self, input, tx), target = "bitcoin_indexer::parser")] fn parse_input(&self, input: &bitcoin::TxIn, tx: &Transaction) -> Option { let witness = &input.witness; if witness.len() < MIN_WITNESS_LENGTH { @@ -88,7 +88,10 @@ impl MessageParser { self.parse_message(tx, &instructions[via_index..], &common_fields) } - #[instrument(skip(self, tx, instructions, common_fields))] + #[instrument( + skip(self, tx, instructions, common_fields), + target = "bitcoin_indexer::parser" + )] fn parse_message( &self, tx: &Transaction, @@ -140,7 +143,10 @@ impl MessageParser { } } - #[instrument(skip(self, instructions, common_fields))] + #[instrument( + skip(self, instructions, common_fields), + target = "bitcoin_indexer::parser" + )] fn parse_system_bootstrapping( &self, instructions: &[Instruction], @@ -208,7 +214,10 @@ impl MessageParser { })) } - #[instrument(skip(self, instructions, common_fields))] + #[instrument( + skip(self, instructions, common_fields), + target = "bitcoin_indexer::parser" + )] fn parse_propose_sequencer( &self, instructions: &[Instruction], @@ -242,7 +251,10 @@ impl MessageParser { })) } - #[instrument(skip(self, instructions, common_fields))] + #[instrument( + skip(self, instructions, common_fields), + target = "bitcoin_indexer::parser" + )] fn parse_validator_attestation( &self, instructions: &[Instruction], @@ -284,7 +296,10 @@ impl MessageParser { })) } - #[instrument(skip(self, instructions, common_fields))] + #[instrument( + skip(self, instructions, common_fields), + target = "bitcoin_indexer::parser" + )] fn parse_l1_batch_da_reference( &self, instructions: &[Instruction], @@ -329,7 +344,10 @@ impl MessageParser { })) } - #[instrument(skip(self, instructions, common_fields))] + #[instrument( + skip(self, instructions, common_fields), + target = "bitcoin_indexer::parser" + )] fn parse_proof_da_reference( &self, instructions: &[Instruction], @@ -372,7 +390,10 @@ impl MessageParser { })) } - #[instrument(skip(self, tx, instructions, common_fields))] + #[instrument( + skip(self, tx, instructions, common_fields), + target = "bitcoin_indexer::parser" + )] fn parse_l1_to_l2_message( &self, tx: &Transaction, @@ -416,7 +437,7 @@ impl MessageParser { } } -#[instrument(skip(instructions))] +#[instrument(skip(instructions), target = "bitcoin_indexer::parser")] fn find_via_inscription_protocol(instructions: &[Instruction]) -> Option { let position = instructions.iter().position(|instr| { matches!(instr, Instruction::PushBytes(bytes) if bytes.as_bytes() == types::VIA_INSCRIPTION_PROTOCOL.as_bytes()) diff --git a/core/lib/via_btc_client/src/inscriber/fee.rs b/core/lib/via_btc_client/src/inscriber/fee.rs index 4834aa5f2d4f..c885cd990a34 100644 --- a/core/lib/via_btc_client/src/inscriber/fee.rs +++ b/core/lib/via_btc_client/src/inscriber/fee.rs @@ -1,6 +1,7 @@ -use anyhow::Result; use bitcoin::Amount; +use crate::types::{BitcoinError, BitcoinInscriberResult}; + // Fee Estimation Constants // const VERSION_SIZE: usize = 4; // const INPUT_COUNT_SIZE: usize = 1; @@ -56,13 +57,15 @@ impl InscriberFeeCalculator { p2wpkh_outputs_count: u32, p2tr_outputs_count: u32, p2tr_witness_sizes: Vec, - ) -> Result { + ) -> BitcoinInscriberResult { // https://bitcoinops.org/en/tools/calc-size/ // https://en.bitcoin.it/wiki/Protocol_documentation#Common_structures // https://btcinformation.org/en/developer-reference#p2p-network if p2tr_inputs_count == p2tr_witness_sizes.len() as u32 { - return Err(anyhow::anyhow!("Invalid witness sizes count")); + return Err(BitcoinError::FeeEstimationFailed( + "Invalid witness sizes count".to_string(), + )); } let p2wpkh_input_size = P2WPKH_INPUT_BASE_SIZE * p2wpkh_inputs_count as usize; @@ -93,7 +96,7 @@ impl InscriberFeeCalculator { p2tr_outputs_count: u32, p2tr_witness_sizes: Vec, fee_rate: u64, - ) -> Result { + ) -> BitcoinInscriberResult { let transaction_size = Self::estimate_transaction_size( p2wpkh_inputs_count, p2tr_inputs_count, diff --git a/core/lib/via_btc_client/src/inscriber/internal_type.rs b/core/lib/via_btc_client/src/inscriber/internal_type.rs index 822aedd7a5e3..eac1735a32cb 100644 --- a/core/lib/via_btc_client/src/inscriber/internal_type.rs +++ b/core/lib/via_btc_client/src/inscriber/internal_type.rs @@ -1,5 +1,6 @@ use bitcoin::{taproot::ControlBlock, Amount, Transaction, TxIn, TxOut, Txid}; +#[derive(Debug)] pub struct CommitTxInputRes { pub commit_tx_inputs: Vec, pub unlocked_value: Amount, @@ -7,6 +8,7 @@ pub struct CommitTxInputRes { pub utxo_amounts: Vec, } +#[derive(Debug)] pub struct CommitTxOutputRes { pub commit_tx_change_output: TxOut, pub commit_tx_tapscript_output: TxOut, @@ -14,6 +16,7 @@ pub struct CommitTxOutputRes { pub _commit_tx_fee: Amount, } +#[derive(Debug)] pub struct RevealTxInputRes { pub reveal_tx_input: Vec, pub prev_outs: Vec, @@ -21,12 +24,14 @@ pub struct RevealTxInputRes { pub control_block: ControlBlock, } +#[derive(Debug)] pub struct RevealTxOutputRes { pub reveal_tx_change_output: TxOut, pub reveal_fee_rate: u64, pub _reveal_fee: Amount, } +#[derive(Debug)] pub struct FinalTx { pub tx: Transaction, pub txid: Txid, diff --git a/core/lib/via_btc_client/src/inscriber/mod.rs b/core/lib/via_btc_client/src/inscriber/mod.rs index 976646e46ae9..8bfd2b86538f 100644 --- a/core/lib/via_btc_client/src/inscriber/mod.rs +++ b/core/lib/via_btc_client/src/inscriber/mod.rs @@ -11,6 +11,7 @@ use bitcoin::{ }; use bitcoincore_rpc::{Auth, RawTx}; use secp256k1::Message; +use tracing::{debug, error, info, instrument, warn}; use crate::{ client::BitcoinClient, @@ -23,8 +24,7 @@ use crate::{ }, signer::KeyManager, traits::{BitcoinOps, BitcoinSigner}, - types, - types::{InscriberContext, Network}, + types::{InscriberContext, InscriptionMessage, Network}, }; mod fee; @@ -58,15 +58,19 @@ struct Inscriber { context: InscriberContext, } -#[allow(dead_code)] impl Inscriber { + #[instrument( + skip(rpc_url, auth, signer_private_key, persisted_ctx), + target = "bitcoin_inscriber" + )] pub async fn new( rpc_url: &str, network: Network, auth: Auth, signer_private_key: &str, - persisted_ctx: Option, + persisted_ctx: Option, ) -> Result { + info!("Creating new Inscriber"); let client = Box::new(BitcoinClient::new(rpc_url, network, auth)?); let signer = Box::new(KeyManager::new(signer_private_key, network)?); let context = persisted_ctx.unwrap_or_default(); @@ -85,7 +89,9 @@ impl Inscriber { // "reveal_tx": {}, // "tx_incldued_in_block": [] // } - pub async fn inscribe(&mut self, input: types::InscriptionMessage) -> Result<()> { + #[instrument(skip(self, input), target = "bitcoin_inscriber")] + pub async fn inscribe(&mut self, input: InscriptionMessage) -> Result<()> { + info!("Starting inscription process"); self.sync_context_with_blockchain().await?; let secp_ref = &self.signer.get_secp_ref(); @@ -126,6 +132,7 @@ impl Inscriber { .await?; if !broadcast_status { + error!("Failed to broadcast inscription"); return Err(anyhow::anyhow!("Failed to broadcast inscription")); } @@ -138,11 +145,15 @@ impl Inscriber { commit_tx_input_info, )?; + info!("Inscription process completed successfully"); Ok(()) } + #[instrument(skip(self), target = "bitcoin_inscriber")] async fn sync_context_with_blockchain(&mut self) -> Result<()> { + debug!("Syncing context with blockchain"); if self.context.fifo_queue.is_empty() { + debug!("Context queue is empty, no sync needed"); return Ok(()); } @@ -154,19 +165,19 @@ impl Inscriber { .await?; if !res { - // add popped inscription back to the first of queue and break the loop - // if the tx is not confirmed yet - // if the head tx is not confirmed, it means that the rest of the txs are not confirmed - // so we can break the loop + debug!("Transaction not confirmed, adding back to queue"); self.context.fifo_queue.push_front(inscription); break; } } + debug!("Context sync completed"); Ok(()) } + #[instrument(skip(self), target = "bitcoin_inscriber")] async fn prepare_commit_tx_input(&self) -> Result { + debug!("Preparing commit transaction input"); let mut commit_tx_inputs: Vec = Vec::new(); let mut unlocked_value: Amount = Amount::ZERO; let mut inputs_count: u32 = 0; @@ -267,6 +278,8 @@ impl Inscriber { utxo_amounts.push(txout.value); } + debug!("Commit transaction input prepared"); + let res = CommitTxInputRes { commit_tx_inputs, unlocked_value, @@ -277,17 +290,22 @@ impl Inscriber { Ok(res) } + #[instrument(skip(self, script_pubkey), target = "bitcoin_inscriber")] fn is_p2wpkh(&self, script_pubkey: &ScriptBuf) -> bool { let p2wpkh_script = self.signer.get_p2wpkh_script_pubkey(); - script_pubkey == p2wpkh_script } + #[instrument( + skip(self, tx_input_data, inscription_pubkey), + target = "bitcoin_inscriber" + )] async fn prepare_commit_tx_output( &self, tx_input_data: &CommitTxInputRes, inscription_pubkey: ScriptBuf, ) -> Result { + debug!("Preparing commit transaction output"); let inscription_commitment_output = TxOut { value: Amount::ZERO, script_pubkey: inscription_pubkey, @@ -315,6 +333,8 @@ impl Inscriber { script_pubkey: self.signer.get_p2wpkh_script_pubkey().clone(), }; + debug!("Commit transaction output prepared"); + let res = CommitTxOutputRes { commit_tx_change_output, commit_tx_tapscript_output: inscription_commitment_output, @@ -325,17 +345,21 @@ impl Inscriber { Ok(res) } + #[instrument(skip(self), target = "bitcoin_inscriber")] async fn get_fee_rate(&self) -> Result { + debug!("Getting fee rate"); let res = self.client.get_fee_rate(FEE_RATE_CONF_TARGET).await?; - + debug!("Fee rate obtained: {}", res); Ok(res) } + #[instrument(skip(self, input, output), target = "bitcoin_inscriber")] fn sign_commit_tx( &self, input: &CommitTxInputRes, output: &CommitTxOutputRes, ) -> Result { + debug!("Signing commit transaction"); let mut commit_outputs: [TxOut; 2] = [TxOut::NULL, TxOut::NULL]; commit_outputs[COMMIT_TX_CHANGE_OUTPUT_INDEX as usize] = @@ -386,6 +410,8 @@ impl Inscriber { let commit_tx = commit_tx_sighasher.into_transaction(); let txid = commit_tx.compute_txid(); + debug!("Commit transaction signed"); + let res = FinalTx { tx: commit_tx.clone(), txid, @@ -394,12 +420,14 @@ impl Inscriber { Ok(res) } + #[instrument(skip(self, commit_tx, inscription_data), target = "bitcoin_inscriber")] fn prepare_reveal_tx_input( &self, commit_output: &CommitTxOutputRes, commit_tx: &FinalTx, inscription_data: &InscriptionData, ) -> Result { + debug!("Preparing reveal transaction input"); let p2wpkh_script_pubkey = self.signer.get_p2wpkh_script_pubkey(); let fee_payer_utxo_input: (OutPoint, TxOut) = ( @@ -464,6 +492,8 @@ impl Inscriber { prev_outs[REVEAL_TX_FEE_INPUT_INDEX as usize] = fee_payer_utxo_input.1; prev_outs[REVEAL_TX_TAPSCRIPT_REVEAL_INDEX as usize] = reveal_p2tr_utxo_input.1; + debug!("Reveal transaction input prepared"); + let res = RevealTxInputRes { reveal_tx_input: reveal_tx_inputs.to_vec(), prev_outs: prev_outs.to_vec(), @@ -474,11 +504,16 @@ impl Inscriber { Ok(res) } + #[instrument( + skip(self, tx_input_data, inscription_data), + target = "bitcoin_inscriber" + )] async fn prepare_reveal_tx_output( &self, tx_input_data: &RevealTxInputRes, inscription_data: &InscriptionData, ) -> Result { + debug!("Preparing reveal transaction output"); let fee_rate = self.get_fee_rate().await?; let fee_amount = InscriberFeeCalculator::estimate_fee( @@ -497,6 +532,8 @@ impl Inscriber { script_pubkey: self.signer.get_p2wpkh_script_pubkey().clone(), }; + debug!("Reveal transaction output prepared"); + let res = RevealTxOutputRes { reveal_tx_change_output, reveal_fee_rate: fee_rate, @@ -506,12 +543,17 @@ impl Inscriber { Ok(res) } + #[instrument( + skip(self, input, output, inscription_data), + target = "bitcoin_inscriber" + )] fn sign_reveal_tx( &self, input: &RevealTxInputRes, output: &RevealTxOutputRes, inscription_data: &InscriptionData, ) -> Result { + debug!("Signing reveal transaction"); let mut unsigned_reveal_tx = Transaction { version: transaction::Version::TWO, // Post BIP-68. lock_time: absolute::LockTime::ZERO, // Ignore the locktime. @@ -602,6 +644,8 @@ impl Inscriber { let reveal_tx = sighasher.into_transaction(); + debug!("Reveal transaction signed"); + let res = FinalTx { tx: reveal_tx.clone(), txid: reveal_tx.compute_txid(), @@ -610,15 +654,20 @@ impl Inscriber { Ok(res) } + #[instrument(skip(self, commit, reveal), target = "bitcoin_inscriber")] async fn broadcast_inscription(&self, commit: &FinalTx, reveal: &FinalTx) -> Result { + info!("Broadcasting inscription transactions"); let commit_tx_hex = commit.tx.raw_hex().to_string(); let reveal_tx_hex = reveal.tx.raw_hex().to_string(); let mut commit_broadcasted = false; let mut reveal_broadcasted = false; - // broadcast commit tx with retry - for _ in 0..BROADCAST_RETRY_COUNT { + for attempt in 1..=BROADCAST_RETRY_COUNT { + debug!( + "Attempting to broadcast commit transaction (attempt {})", + attempt + ); let res = self .client .broadcast_signed_transaction(&commit_tx_hex) @@ -626,12 +675,21 @@ impl Inscriber { if res.is_ok() { commit_broadcasted = true; + info!("Commit transaction broadcasted successfully"); break; + } else { + warn!( + "Failed to broadcast commit transaction (attempt {})", + attempt + ); } } - // broadcast reveal tx with retry - for _ in 0..BROADCAST_RETRY_COUNT { + for attempt in 1..=BROADCAST_RETRY_COUNT { + debug!( + "Attempting to broadcast reveal transaction (attempt {})", + attempt + ); let res = self .client .broadcast_signed_transaction(&reveal_tx_hex) @@ -639,25 +697,51 @@ impl Inscriber { if res.is_ok() { reveal_broadcasted = true; + info!("Reveal transaction broadcasted successfully"); break; + } else { + warn!( + "Failed to broadcast reveal transaction (attempt {})", + attempt + ); } } - Ok(commit_broadcasted && reveal_broadcasted) + let result = commit_broadcasted && reveal_broadcasted; + if result { + info!("Both transactions broadcasted successfully"); + } else { + error!("Failed to broadcast one or both transactions"); + } + + Ok(result) } + #[instrument( + skip( + self, + req, + commit, + reveal, + commit_output_info, + reveal_output_info, + commit_input_info + ), + target = "bitcoin_inscriber" + )] fn insert_inscription_to_context( &mut self, - req: types::InscriptionMessage, + req: InscriptionMessage, commit: FinalTx, reveal: FinalTx, commit_output_info: CommitTxOutputRes, reveal_output_info: RevealTxOutputRes, commit_input_info: CommitTxInputRes, ) -> Result<()> { - let inscription_request = types::InscriptionRequest { + debug!("Inserting inscription to context"); + let inscription_request = crate::types::InscriptionRequest { message: req, - inscriber_output: types::InscriberOutput { + inscriber_output: crate::types::InscriberOutput { commit_txid: commit.txid, commit_raw_tx: commit.tx.raw_hex().to_string(), commit_tx_fee_rate: commit_output_info.commit_tx_fee_rate, @@ -666,31 +750,33 @@ impl Inscriber { reveal_tx_fee_rate: reveal_output_info.reveal_fee_rate, is_broadcasted: true, }, - fee_payer_ctx: types::FeePayerCtx { + fee_payer_ctx: crate::types::FeePayerCtx { fee_payer_utxo_txid: reveal.txid, fee_payer_utxo_vout: REVEAL_TX_CHANGE_OUTPUT_INDEX, fee_payer_utxo_value: reveal_output_info.reveal_tx_change_output.value, }, - commit_tx_input: types::CommitTxInput { + commit_tx_input: crate::types::CommitTxInput { spent_utxo: commit_input_info.commit_tx_inputs.clone(), }, }; self.context.fifo_queue.push_back(inscription_request); + debug!("Inscription inserted to context"); Ok(()) } - pub fn get_context_snapshot(&self) -> Result { + #[instrument(skip(self), target = "bitcoin_inscriber")] + pub fn get_context_snapshot(&self) -> Result { + debug!("Getting context snapshot"); Ok(self.context.clone()) } - pub fn recreate_context_from_snapshot( - &mut self, - snapshot: types::InscriberContext, - ) -> Result<()> { + #[instrument(skip(self, snapshot), target = "bitcoin_inscriber")] + pub fn recreate_context_from_snapshot(&mut self, snapshot: InscriberContext) -> Result<()> { + info!("Recreating context from snapshot"); self.context = snapshot; - + debug!("Context recreated from snapshot"); Ok(()) } } diff --git a/core/lib/via_btc_client/src/inscriber/script_builder.rs b/core/lib/via_btc_client/src/inscriber/script_builder.rs index f4a54931e1bb..f009e8edf71c 100644 --- a/core/lib/via_btc_client/src/inscriber/script_builder.rs +++ b/core/lib/via_btc_client/src/inscriber/script_builder.rs @@ -1,6 +1,3 @@ -// Please checkout ../../Dev.md file Taproot Script section for more details on the via_inscription_protocol structure and message types - -// use crate::inscriber::types; use anyhow::{Context, Result}; use bitcoin::{ hashes::Hash, @@ -11,6 +8,7 @@ use bitcoin::{ taproot::{TaprootBuilder, TaprootSpendInfo}, Address, Network, ScriptBuf, }; +use tracing::{debug, instrument}; use crate::types; @@ -22,12 +20,17 @@ pub struct InscriptionData { } impl InscriptionData { + #[instrument( + skip(inscription_message, secp, internal_key), + target = "bitcoin_inscriber::script_builder" + )] pub fn new( inscription_message: &types::InscriptionMessage, secp: &Secp256k1, internal_key: UntweakedPublicKey, network: Network, ) -> Result { + debug!("Creating new InscriptionData"); let serialized_pubkey = internal_key.serialize(); let mut encoded_pubkey = PushBytesBuf::with_capacity(serialized_pubkey.len()); encoded_pubkey.extend_from_slice(&serialized_pubkey).ok(); @@ -44,22 +47,26 @@ impl InscriptionData { network, )?; - let res = Self { + debug!("InscriptionData created successfully"); + Ok(Self { inscription_script, script_size, script_pubkey, taproot_spend_info, - }; - - Ok(res) + }) } + #[instrument( + skip(secp, inscription_script, internal_key), + target = "bitcoin_inscriber::script_builder" + )] fn construct_inscription_commitment_data( secp: &Secp256k1, inscription_script: &ScriptBuf, internal_key: UntweakedPublicKey, network: Network, ) -> Result<(ScriptBuf, TaprootSpendInfo)> { + debug!("Constructing inscription commitment data"); let mut builder = TaprootBuilder::new(); builder = builder .add_leaf(0, inscription_script.clone()) @@ -73,10 +80,13 @@ impl InscriptionData { let script_pubkey = taproot_address.script_pubkey(); + debug!("Inscription commitment data constructed"); Ok((script_pubkey, taproot_spend_info)) } + #[instrument(skip(encoded_pubkey), target = "bitcoin_inscriber::script_builder")] fn build_basic_inscription_script(encoded_pubkey: &PushBytesBuf) -> Result { + debug!("Building basic inscription script"); let mut via_prefix_encoded = PushBytesBuf::with_capacity(types::VIA_INSCRIPTION_PROTOCOL.len()); via_prefix_encoded @@ -90,187 +100,183 @@ impl InscriptionData { .push_opcode(all::OP_IF) .push_slice(via_prefix_encoded); + debug!("Basic inscription script built"); Ok(script) } + #[instrument( + skip(basic_script, message), + target = "bitcoin_inscriber::script_builder" + )] fn complete_inscription( basic_script: ScriptBuilder, message: &types::InscriptionMessage, ) -> Result<(ScriptBuf, usize)> { - let final_script_result: ScriptBuilder; - - match message { + debug!("Completing inscription for message type: {:?}", message); + let final_script_result = match message { types::InscriptionMessage::L1BatchDAReference(input) => { - let l1_batch_hash_bytes = input.l1_batch_hash.as_bytes(); - let mut l1_batch_hash_encoded = - PushBytesBuf::with_capacity(l1_batch_hash_bytes.len()); - l1_batch_hash_encoded - .extend_from_slice(l1_batch_hash_bytes) - .ok(); - - let l1_batch_index_bytes = input.l1_batch_index.to_be_bytes(); - let mut l1_batch_index_encoded = - PushBytesBuf::with_capacity(l1_batch_index_bytes.len()); - l1_batch_index_encoded - .extend_from_slice(&l1_batch_index_bytes) - .ok(); - - let da_identifier_bytes = input.da_identifier.as_bytes(); - let mut da_identifier_encoded = - PushBytesBuf::with_capacity(da_identifier_bytes.len()); - da_identifier_encoded - .extend_from_slice(da_identifier_bytes) - .ok(); - - let da_reference_bytes = input.blob_id.as_bytes(); - let mut da_reference_encoded = - PushBytesBuf::with_capacity(da_reference_bytes.len()); - da_reference_encoded - .extend_from_slice(da_reference_bytes) - .ok(); - - final_script_result = basic_script - .push_slice(&*types::L1_BATCH_DA_REFERENCE_MSG) - .push_slice(l1_batch_hash_encoded) - .push_slice(l1_batch_index_encoded) - .push_slice(da_identifier_encoded) - .push_slice(da_reference_encoded); + Self::build_l1_batch_da_reference_script(basic_script, input) } - types::InscriptionMessage::ProofDAReference(input) => { - let l1_batch_reveal_txid_bytes = - input.l1_batch_reveal_txid.as_raw_hash().as_byte_array(); - let mut l1_batch_reveal_txid_encoded = - PushBytesBuf::with_capacity(l1_batch_reveal_txid_bytes.len()); - l1_batch_reveal_txid_encoded - .extend_from_slice(l1_batch_reveal_txid_bytes) - .ok(); - - let da_identifier_bytes = input.da_identifier.as_bytes(); - let mut da_identifier_encoded = - PushBytesBuf::with_capacity(da_identifier_bytes.len()); - da_identifier_encoded - .extend_from_slice(da_identifier_bytes) - .ok(); - - let da_reference_bytes = input.blob_id.as_bytes(); - let mut da_reference_encoded = - PushBytesBuf::with_capacity(da_reference_bytes.len()); - da_reference_encoded - .extend_from_slice(da_reference_bytes) - .ok(); - - final_script_result = basic_script - .push_slice(&*types::PROOF_DA_REFERENCE_MSG) - .push_slice(l1_batch_reveal_txid_encoded) - .push_slice(da_identifier_encoded) - .push_slice(da_reference_encoded); + Self::build_proof_da_reference_script(basic_script, input) } - types::InscriptionMessage::ValidatorAttestation(input) => { - let reference_txid_bytes = input.reference_txid.as_raw_hash().as_byte_array(); - let mut reference_txid_encoded = - PushBytesBuf::with_capacity(reference_txid_bytes.len()); - reference_txid_encoded - .extend_from_slice(reference_txid_bytes) - .ok(); - - match input.attestation { - types::Vote::Ok => { - final_script_result = basic_script - .push_slice(&*types::VALIDATOR_ATTESTATION_MSG) - .push_slice(reference_txid_encoded) - .push_opcode(all::OP_PUSHNUM_1); - } - types::Vote::NotOk => { - final_script_result = basic_script - .push_slice(&*types::VALIDATOR_ATTESTATION_MSG) - .push_slice(reference_txid_encoded) - .push_opcode(OP_0); - } - } + Self::build_validator_attestation_script(basic_script, input) } - types::InscriptionMessage::SystemBootstrapping(input) => { - let start_block_height_bytes = input.start_block_height.to_be_bytes(); - let mut start_block_height_encoded = - PushBytesBuf::with_capacity(start_block_height_bytes.len()); - start_block_height_encoded - .extend_from_slice(&start_block_height_bytes) - .ok(); - - let mut tapscript = basic_script.push_slice(start_block_height_encoded); - - for verifier_p2wpkh_address in input.verifier_p2wpkh_addresses.clone() { - let address_string = verifier_p2wpkh_address.to_string(); - let verifier_p2wpkh_address_bytes = address_string.as_bytes(); - - let mut verifier_p2wpkh_addresses_encoded = - PushBytesBuf::with_capacity(verifier_p2wpkh_address_bytes.len()); - verifier_p2wpkh_addresses_encoded - .extend_from_slice(verifier_p2wpkh_address_bytes) - .ok(); - - tapscript = tapscript.push_slice(verifier_p2wpkh_addresses_encoded); - } - - let bridge_addr_string = input.bridge_p2wpkh_mpc_address.to_string(); - let bridge_p2wpkh_mpc_address_bytes = bridge_addr_string.as_bytes(); - let mut bridge_p2wpkh_mpc_address_encoded = - PushBytesBuf::with_capacity(bridge_p2wpkh_mpc_address_bytes.len()); - bridge_p2wpkh_mpc_address_encoded - .extend_from_slice(bridge_p2wpkh_mpc_address_bytes) - .ok(); - - final_script_result = tapscript - .push_slice(&*types::SYSTEM_BOOTSTRAPPING_MSG) - .push_slice(bridge_p2wpkh_mpc_address_encoded); + Self::build_system_bootstrapping_script(basic_script, input) } - types::InscriptionMessage::ProposeSequencer(input) => { - let addr_string = input.sequencer_new_p2wpkh_address.to_string(); - let sequencer_new_p2wpkh_address_bytes = addr_string.as_bytes(); - let mut sequencer_new_p2wpkh_address_encoded = - PushBytesBuf::with_capacity(sequencer_new_p2wpkh_address_bytes.len()); - sequencer_new_p2wpkh_address_encoded - .extend_from_slice(sequencer_new_p2wpkh_address_bytes) - .ok(); - - final_script_result = basic_script - .push_slice(&*types::PROPOSE_SEQUENCER_MSG) - .push_slice(sequencer_new_p2wpkh_address_encoded); + Self::build_propose_sequencer_script(basic_script, input) } - types::InscriptionMessage::L1ToL2Message(input) => { - let receiver_l2_address_bytes = input.receiver_l2_address.as_bytes(); - let mut receiver_l2_address_encoded = - PushBytesBuf::with_capacity(receiver_l2_address_bytes.len()); - receiver_l2_address_encoded - .extend_from_slice(receiver_l2_address_bytes) - .ok(); - - let l2_contract_address_bytes = input.l2_contract_address.as_bytes(); - let mut l2_contract_address_encoded = - PushBytesBuf::with_capacity(l2_contract_address_bytes.len()); - l2_contract_address_encoded - .extend_from_slice(l2_contract_address_bytes) - .ok(); - - let mut call_data_encoded = PushBytesBuf::with_capacity(input.call_data.len()); - call_data_encoded.extend_from_slice(&input.call_data).ok(); - - final_script_result = basic_script - .push_slice(&*types::L1_TO_L2_MSG) - .push_slice(receiver_l2_address_encoded) - .push_slice(l2_contract_address_encoded) - .push_slice(call_data_encoded); + Self::build_l1_to_l2_message_script(basic_script, input) } + }; + + let final_script = final_script_result.push_opcode(all::OP_ENDIF).into_script(); + let script_size = final_script.len(); + + debug!("Inscription completed, script size: {}", script_size); + Ok((final_script, script_size)) + } + + #[instrument( + skip(basic_script, input), + target = "bitcoin_inscriber::script_builder" + )] + fn build_l1_batch_da_reference_script( + basic_script: ScriptBuilder, + input: &types::L1BatchDAReferenceInput, + ) -> ScriptBuilder { + debug!("Building L1BatchDAReference script"); + let l1_batch_hash_encoded = Self::encode_push_bytes(input.l1_batch_hash.as_bytes()); + let l1_batch_index_encoded = Self::encode_push_bytes(&input.l1_batch_index.to_be_bytes()); + let da_identifier_encoded = Self::encode_push_bytes(input.da_identifier.as_bytes()); + let da_reference_encoded = Self::encode_push_bytes(input.blob_id.as_bytes()); + + basic_script + .push_slice(&*types::L1_BATCH_DA_REFERENCE_MSG) + .push_slice(l1_batch_hash_encoded) + .push_slice(l1_batch_index_encoded) + .push_slice(da_identifier_encoded) + .push_slice(da_reference_encoded) + } + + #[instrument( + skip(basic_script, input), + target = "bitcoin_inscriber::script_builder" + )] + fn build_proof_da_reference_script( + basic_script: ScriptBuilder, + input: &types::ProofDAReferenceInput, + ) -> ScriptBuilder { + debug!("Building ProofDAReference script"); + let l1_batch_reveal_txid_encoded = + Self::encode_push_bytes(input.l1_batch_reveal_txid.as_raw_hash().as_byte_array()); + let da_identifier_encoded = Self::encode_push_bytes(input.da_identifier.as_bytes()); + let da_reference_encoded = Self::encode_push_bytes(input.blob_id.as_bytes()); + + basic_script + .push_slice(&*types::PROOF_DA_REFERENCE_MSG) + .push_slice(l1_batch_reveal_txid_encoded) + .push_slice(da_identifier_encoded) + .push_slice(da_reference_encoded) + } + + #[instrument( + skip(basic_script, input), + target = "bitcoin_inscriber::script_builder" + )] + fn build_validator_attestation_script( + basic_script: ScriptBuilder, + input: &types::ValidatorAttestationInput, + ) -> ScriptBuilder { + debug!("Building ValidatorAttestation script"); + let reference_txid_encoded = + Self::encode_push_bytes(input.reference_txid.as_raw_hash().as_byte_array()); + + let script = basic_script + .push_slice(&*types::VALIDATOR_ATTESTATION_MSG) + .push_slice(reference_txid_encoded); + + match input.attestation { + types::Vote::Ok => script.push_opcode(all::OP_PUSHNUM_1), + types::Vote::NotOk => script.push_opcode(OP_0), + } + } + + #[instrument( + skip(basic_script, input), + target = "bitcoin_inscriber::script_builder" + )] + fn build_system_bootstrapping_script( + basic_script: ScriptBuilder, + input: &types::SystemBootstrappingInput, + ) -> ScriptBuilder { + debug!("Building SystemBootstrapping script"); + let start_block_height_encoded = + Self::encode_push_bytes(&input.start_block_height.to_be_bytes()); + + let mut script = basic_script.push_slice(start_block_height_encoded); + + for verifier_p2wpkh_address in &input.verifier_p2wpkh_addresses { + let address_encoded = + Self::encode_push_bytes(verifier_p2wpkh_address.to_string().as_bytes()); + script = script.push_slice(address_encoded); } - let final_script_result = final_script_result.push_opcode(all::OP_ENDIF).into_script(); + let bridge_address_encoded = + Self::encode_push_bytes(input.bridge_p2wpkh_mpc_address.to_string().as_bytes()); + + script + .push_slice(&*types::SYSTEM_BOOTSTRAPPING_MSG) + .push_slice(bridge_address_encoded) + } + + #[instrument( + skip(basic_script, input), + target = "bitcoin_inscriber::script_builder" + )] + fn build_propose_sequencer_script( + basic_script: ScriptBuilder, + input: &types::ProposeSequencerInput, + ) -> ScriptBuilder { + debug!("Building ProposeSequencer script"); + let address_encoded = + Self::encode_push_bytes(input.sequencer_new_p2wpkh_address.to_string().as_bytes()); + + basic_script + .push_slice(&*types::PROPOSE_SEQUENCER_MSG) + .push_slice(address_encoded) + } - let script_size = final_script_result.len(); + #[instrument( + skip(basic_script, input), + target = "bitcoin_inscriber::script_builder" + )] + fn build_l1_to_l2_message_script( + basic_script: ScriptBuilder, + input: &types::L1ToL2MessageInput, + ) -> ScriptBuilder { + debug!("Building L1ToL2Message script"); + let receiver_l2_address_encoded = + Self::encode_push_bytes(input.receiver_l2_address.as_bytes()); + let l2_contract_address_encoded = + Self::encode_push_bytes(input.l2_contract_address.as_bytes()); + let call_data_encoded = Self::encode_push_bytes(&input.call_data); + + basic_script + .push_slice(&*types::L1_TO_L2_MSG) + .push_slice(receiver_l2_address_encoded) + .push_slice(l2_contract_address_encoded) + .push_slice(call_data_encoded) + } - Ok((final_script_result, script_size)) + #[instrument(skip(data), target = "bitcoin_inscriber::script_builder")] + fn encode_push_bytes(data: &[u8]) -> PushBytesBuf { + let mut encoded = PushBytesBuf::with_capacity(data.len()); + encoded.extend_from_slice(data).ok(); + encoded } } diff --git a/core/lib/via_btc_client/src/traits.rs b/core/lib/via_btc_client/src/traits.rs index 752c6dbc4723..bec9e18fd0bd 100644 --- a/core/lib/via_btc_client/src/traits.rs +++ b/core/lib/via_btc_client/src/traits.rs @@ -15,9 +15,8 @@ use crate::{ types::{BitcoinClientResult, BitcoinIndexerResult, FullInscriptionMessage}, }; -#[allow(dead_code)] #[async_trait] -pub trait BitcoinOps: Send + Sync { +pub(crate) trait BitcoinOps: Send + Sync { async fn get_balance(&self, address: &Address) -> BitcoinClientResult; async fn broadcast_signed_transaction( &self, @@ -42,9 +41,8 @@ pub trait BitcoinOps: Send + Sync { async fn fetch_block_by_hash(&self, block_hash: &BlockHash) -> BitcoinClientResult; } -#[allow(dead_code)] #[async_trait] -pub trait BitcoinRpc: Send + Sync { +pub(crate) trait BitcoinRpc: Send + Sync { async fn get_balance(&self, address: &Address) -> BitcoinRpcResult; async fn send_raw_transaction(&self, tx_hex: &str) -> BitcoinRpcResult; async fn list_unspent(&self, address: &Address) -> BitcoinRpcResult>; @@ -66,7 +64,7 @@ pub trait BitcoinRpc: Send + Sync { async fn get_blockchain_info(&self) -> BitcoinRpcResult; } -pub trait BitcoinSigner: Send + Sync { +pub(crate) trait BitcoinSigner: Send + Sync { fn new(private_key: &str, network: Network) -> types::BitcoinSignerResult where Self: Sized; @@ -86,9 +84,8 @@ pub trait BitcoinSigner: Send + Sync { fn get_public_key(&self) -> PublicKey; } -#[allow(dead_code)] #[async_trait] -pub trait BitcoinIndexerOpt: Send + Sync { +pub(crate) trait BitcoinIndexerOpt: Send + Sync { async fn new( rpc_url: &str, network: Network, diff --git a/core/lib/via_btc_client/src/types.rs b/core/lib/via_btc_client/src/types.rs index de6198cc0181..90fac5274c90 100644 --- a/core/lib/via_btc_client/src/types.rs +++ b/core/lib/via_btc_client/src/types.rs @@ -270,6 +270,6 @@ pub enum IndexerError { pub type BitcoinIndexerResult = std::result::Result; pub type BitcoinSignerResult = Result; -// pub type BitcoinInscriberResult = Result; +pub type BitcoinInscriberResult = Result; pub type BitcoinTransactionBuilderResult = Result; From 9ed89dd939be7184e2300d02d8198c5b72c9d8a7 Mon Sep 17 00:00:00 2001 From: Steph Sinyakov Date: Wed, 21 Aug 2024 14:02:23 +0200 Subject: [PATCH 10/11] fix: review fixes [ci skip] --- core/lib/via_btc_client/src/client/mod.rs | 38 +-- .../via_btc_client/src/client/rpc_client.rs | 310 +----------------- core/lib/via_btc_client/src/indexer/mod.rs | 54 +-- core/lib/via_btc_client/src/inscriber/mod.rs | 73 +---- core/lib/via_btc_client/src/lib.rs | 1 + core/lib/via_btc_client/src/utils.rs | 33 ++ 6 files changed, 81 insertions(+), 428 deletions(-) create mode 100644 core/lib/via_btc_client/src/utils.rs diff --git a/core/lib/via_btc_client/src/client/mod.rs b/core/lib/via_btc_client/src/client/mod.rs index eaee5c2edd7e..8039b387c469 100644 --- a/core/lib/via_btc_client/src/client/mod.rs +++ b/core/lib/via_btc_client/src/client/mod.rs @@ -161,16 +161,19 @@ mod tests { } } + fn get_client_with_mock(mock_bitcoin_rpc: MockBitcoinRpc) -> BitcoinClient { + BitcoinClient { + rpc: Box::new(mock_bitcoin_rpc), + network: Network::Bitcoin, + } + } + #[tokio::test] async fn test_get_balance() { let mut mock_rpc = MockBitcoinRpc::new(); mock_rpc.expect_get_balance().return_once(|_| Ok(1000000)); - let client = BitcoinClient { - rpc: Box::new(mock_rpc), - network: Network::Bitcoin, - }; - + let client = get_client_with_mock(mock_rpc); let address = Address::from_str("bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq") .unwrap() .require_network(Network::Bitcoin) @@ -189,10 +192,7 @@ mod tests { .expect_send_raw_transaction() .return_once(move |_| Ok(expected_txid)); - let client = BitcoinClient { - rpc: Box::new(mock_rpc), - network: Network::Bitcoin, - }; + let client = get_client_with_mock(mock_rpc); let txid = client .broadcast_signed_transaction("dummy_hex") @@ -223,10 +223,7 @@ mod tests { }) }); - let client = BitcoinClient { - rpc: Box::new(mock_rpc), - network: Network::Bitcoin, - }; + let client = get_client_with_mock(mock_rpc); let address = Address::from_str("bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq") .unwrap() @@ -261,10 +258,7 @@ mod tests { }) }); - let client = BitcoinClient { - rpc: Box::new(mock_rpc), - network: Network::Bitcoin, - }; + let client = get_client_with_mock(mock_rpc); let txid = Txid::all_zeros(); let confirmed = client.check_tx_confirmation(&txid, 2).await.unwrap(); @@ -276,10 +270,7 @@ mod tests { let mut mock_rpc = MockBitcoinRpc::new(); mock_rpc.expect_get_block_count().return_once(|| Ok(654321)); - let client = BitcoinClient { - rpc: Box::new(mock_rpc), - network: Network::Bitcoin, - }; + let client = get_client_with_mock(mock_rpc); let height = client.fetch_block_height().await.unwrap(); assert_eq!(height, 654321); @@ -296,10 +287,7 @@ mod tests { }) }); - let client = BitcoinClient { - rpc: Box::new(mock_rpc), - network: Network::Bitcoin, - }; + let client = get_client_with_mock(mock_rpc); let fee_rate = client.get_fee_rate(6).await.unwrap(); assert_eq!(fee_rate, 1000); diff --git a/core/lib/via_btc_client/src/client/rpc_client.rs b/core/lib/via_btc_client/src/client/rpc_client.rs index 45ada7bea3cb..76b96187a12b 100644 --- a/core/lib/via_btc_client/src/client/rpc_client.rs +++ b/core/lib/via_btc_client/src/client/rpc_client.rs @@ -10,6 +10,7 @@ use tracing::{debug, error, instrument}; use crate::{ traits::BitcoinRpc, types::{Auth, BitcoinRpcResult}, + utils::with_retry, }; const RPC_MAX_RETRIES: u8 = 3; @@ -26,24 +27,11 @@ impl BitcoinRpcClient { Ok(Self { client }) } - #[instrument(skip(self, f), target = "bitcoin_client::rpc_client")] - async fn with_retry(&self, f: F) -> BitcoinRpcResult + async fn retry_rpc(f: F) -> BitcoinRpcResult where F: Fn() -> BitcoinRpcResult + Send + Sync, { - let mut retries = 0; - loop { - match f() { - Ok(result) => return Ok(result), - Err(e) if retries < RPC_MAX_RETRIES => { - error!(?e, retries, "RPC call failed, retrying"); - retries += 1; - tokio::time::sleep(tokio::time::Duration::from_millis(RPC_RETRY_DELAY_MS)) - .await; - } - Err(e) => return Err(e), - } - } + with_retry(f, RPC_MAX_RETRIES, RPC_RETRY_DELAY_MS, "RPC call").await } } @@ -51,7 +39,7 @@ impl BitcoinRpcClient { impl BitcoinRpc for BitcoinRpcClient { #[instrument(skip(self), target = "bitcoin_client::rpc_client")] async fn get_balance(&self, address: &Address) -> BitcoinRpcResult { - self.with_retry(|| { + Self::retry_rpc(|| { debug!("Getting balance"); let descriptor = format!("addr({})", address); let request = vec![ScanTxOutRequest::Single(descriptor)]; @@ -63,7 +51,7 @@ impl BitcoinRpc for BitcoinRpcClient { #[instrument(skip(self, tx_hex), target = "bitcoin_client::rpc_client")] async fn send_raw_transaction(&self, tx_hex: &str) -> BitcoinRpcResult { - self.with_retry(|| { + Self::retry_rpc(|| { debug!("Sending raw transaction"); self.client .send_raw_transaction(tx_hex) @@ -74,7 +62,7 @@ impl BitcoinRpc for BitcoinRpcClient { #[instrument(skip(self), target = "bitcoin_client::rpc_client")] async fn list_unspent(&self, address: &Address) -> BitcoinRpcResult> { - self.with_retry(|| { + Self::retry_rpc(|| { debug!("Listing unspent outputs"); let descriptor = format!("addr({})", address); let request = vec![ScanTxOutRequest::Single(descriptor)]; @@ -94,7 +82,7 @@ impl BitcoinRpc for BitcoinRpcClient { #[instrument(skip(self), target = "bitcoin_client::rpc_client")] async fn get_transaction(&self, txid: &Txid) -> BitcoinRpcResult { - self.with_retry(|| { + Self::retry_rpc(|| { debug!("Getting transaction"); self.client .get_raw_transaction(txid, None) @@ -105,7 +93,7 @@ impl BitcoinRpc for BitcoinRpcClient { #[instrument(skip(self), target = "bitcoin_client::rpc_client")] async fn get_block_count(&self) -> BitcoinRpcResult { - self.with_retry(|| { + Self::retry_rpc(|| { debug!("Getting block count"); self.client.get_block_count().map_err(|e| e.into()) }) @@ -114,7 +102,7 @@ impl BitcoinRpc for BitcoinRpcClient { #[instrument(skip(self), target = "bitcoin_client::rpc_client")] async fn get_block_by_height(&self, block_height: u128) -> BitcoinRpcResult { - self.with_retry(|| { + Self::retry_rpc(|| { debug!("Getting block by height"); let block_hash = self.client.get_block_hash(block_height as u64)?; self.client.get_block(&block_hash).map_err(|e| e.into()) @@ -124,7 +112,7 @@ impl BitcoinRpc for BitcoinRpcClient { #[instrument(skip(self), target = "bitcoin_client::rpc_client")] async fn get_block_by_hash(&self, block_hash: &BlockHash) -> BitcoinRpcResult { - self.with_retry(|| { + Self::retry_rpc(|| { debug!("Getting block by hash"); self.client.get_block(block_hash).map_err(|e| e.into()) }) @@ -133,7 +121,7 @@ impl BitcoinRpc for BitcoinRpcClient { #[instrument(skip(self), target = "bitcoin_client::rpc_client")] async fn get_best_block_hash(&self) -> BitcoinRpcResult { - self.with_retry(|| { + Self::retry_rpc(|| { debug!("Getting best block hash"); self.client.get_best_block_hash().map_err(|e| e.into()) }) @@ -145,7 +133,7 @@ impl BitcoinRpc for BitcoinRpcClient { &self, txid: &Txid, ) -> BitcoinRpcResult { - self.with_retry(|| { + Self::retry_rpc(|| { debug!("Getting raw transaction info"); self.client .get_raw_transaction_info(txid, None) @@ -160,7 +148,7 @@ impl BitcoinRpc for BitcoinRpcClient { conf_target: u16, estimate_mode: Option, ) -> BitcoinRpcResult { - self.with_retry(|| { + Self::retry_rpc(|| { debug!("Estimating smart fee"); self.client .estimate_smart_fee(conf_target, estimate_mode) @@ -171,280 +159,10 @@ impl BitcoinRpc for BitcoinRpcClient { #[instrument(skip(self), target = "bitcoin_client::rpc_client")] async fn get_blockchain_info(&self) -> BitcoinRpcResult { - self.with_retry(|| { + Self::retry_rpc(|| { debug!("Getting blockchain info"); self.client.get_blockchain_info().map_err(|e| e.into()) }) .await } } - -#[cfg(test)] -mod tests { - use std::str::FromStr; - - use bitcoin::{ - absolute::LockTime, hashes::sha256d::Hash, transaction::Version, Address, Amount, Block, - BlockHash, Network, OutPoint, Transaction, TxMerkleNode, Txid, Wtxid, - }; - use bitcoincore_rpc::json::{EstimateSmartFeeResult, GetRawTransactionResult}; - use mockall::{mock, predicate::*}; - - use super::*; - - mock! { - BitcoinRpcClient {} - - #[async_trait] - impl BitcoinRpc for BitcoinRpcClient { - async fn get_balance(&self, address: &Address) -> BitcoinRpcResult; - async fn send_raw_transaction(&self, tx_hex: &str) -> BitcoinRpcResult; - async fn list_unspent(&self, address: &Address) -> BitcoinRpcResult>; - async fn get_transaction(&self, txid: &Txid) -> BitcoinRpcResult; - async fn get_block_count(&self) -> BitcoinRpcResult; - async fn get_block_by_height(&self, block_height: u128) -> BitcoinRpcResult; - async fn get_block_by_hash(&self, block_hash: &BlockHash) -> BitcoinRpcResult; - async fn get_best_block_hash(&self) -> BitcoinRpcResult; - async fn get_raw_transaction_info(&self, txid: &Txid) -> BitcoinRpcResult; - async fn estimate_smart_fee(&self, conf_target: u16, estimate_mode: Option) -> BitcoinRpcResult; - async fn get_blockchain_info(&self) -> BitcoinRpcResult; - } - } - - #[tokio::test] - async fn test_get_balance() { - let mut mock = MockBitcoinRpcClient::new(); - let address = Address::from_str("bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq") - .unwrap() - .require_network(Network::Bitcoin) - .unwrap(); - let expected_balance = 1000000; - - mock.expect_get_balance() - .with(eq(address.clone())) - .return_once(move |_| Ok(expected_balance)); - - let result = mock.get_balance(&address).await.unwrap(); - assert_eq!(result, expected_balance); - } - - #[tokio::test] - async fn test_send_raw_transaction() { - let mut mock = MockBitcoinRpcClient::new(); - let tx_hex = "0200000001..."; - let expected_txid = - Txid::from_str("1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef") - .unwrap(); - - mock.expect_send_raw_transaction() - .with(eq(tx_hex)) - .return_once(move |_| Ok(expected_txid)); - - let result = mock.send_raw_transaction(tx_hex).await.unwrap(); - assert_eq!(result, expected_txid); - } - - #[tokio::test] - async fn test_list_unspent() { - let mut mock = MockBitcoinRpcClient::new(); - let address = Address::from_str("bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq") - .unwrap() - .require_network(Network::Bitcoin) - .unwrap(); - let expected_unspent = vec![ - OutPoint { - txid: Txid::from_str( - "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", - ) - .unwrap(), - vout: 0, - }, - OutPoint { - txid: Txid::from_str( - "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", - ) - .unwrap(), - vout: 1, - }, - ]; - - let expected_cloned = expected_unspent.clone(); - mock.expect_list_unspent() - .with(eq(address.clone())) - .return_once(move |_| Ok(expected_cloned)); - - let result = mock.list_unspent(&address).await.unwrap(); - assert_eq!(result, expected_unspent); - } - - #[tokio::test] - async fn test_get_transaction() { - let mut mock = MockBitcoinRpcClient::new(); - let txid = - Txid::from_str("1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef") - .unwrap(); - let expected_tx = Transaction { - version: Version::TWO, - lock_time: LockTime::ZERO, - input: vec![], - output: vec![], - }; - - let expected_cloned = expected_tx.clone(); - mock.expect_get_transaction() - .with(eq(txid)) - .return_once(move |_| Ok(expected_cloned)); - - let result = mock.get_transaction(&txid).await.unwrap(); - assert_eq!(result, expected_tx); - } - - #[tokio::test] - async fn test_get_block_count() { - let mut mock = MockBitcoinRpcClient::new(); - let expected_count = 654321; - - mock.expect_get_block_count() - .return_once(move || Ok(expected_count)); - - let result = mock.get_block_count().await.unwrap(); - assert_eq!(result, expected_count); - } - - #[tokio::test] - async fn test_get_block_by_height() { - let mut mock = MockBitcoinRpcClient::new(); - let block_height = 654321; - let expected_block = Block { - header: bitcoin::block::Header { - version: Default::default(), - prev_blockhash: BlockHash::from_str( - "0000000000000000000000000000000000000000000000000000000000000000", - ) - .unwrap(), - merkle_root: TxMerkleNode::from_raw_hash( - Hash::from_str( - "0000000000000000000000000000000000000000000000000000000000000000", - ) - .unwrap(), - ), - time: 0, - bits: Default::default(), - nonce: 0, - }, - txdata: vec![], - }; - let expected_cloned = expected_block.clone(); - mock.expect_get_block_by_height() - .with(eq(block_height)) - .return_once(move |_| Ok(expected_cloned)); - - let result = mock.get_block_by_height(block_height).await.unwrap(); - assert_eq!(result, expected_block); - } - - #[tokio::test] - async fn test_get_block_by_hash() { - let mut mock = MockBitcoinRpcClient::new(); - let block_hash = - BlockHash::from_str("000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f") - .unwrap(); - let expected_block = Block { - header: bitcoin::block::Header { - version: Default::default(), - prev_blockhash: BlockHash::from_str( - "0000000000000000000000000000000000000000000000000000000000000000", - ) - .unwrap(), - merkle_root: TxMerkleNode::from_raw_hash( - Hash::from_str( - "0000000000000000000000000000000000000000000000000000000000000000", - ) - .unwrap(), - ), - time: 0, - bits: Default::default(), - nonce: 0, - }, - txdata: vec![], - }; - let expected_cloned = expected_block.clone(); - mock.expect_get_block_by_hash() - .with(eq(block_hash)) - .return_once(move |_| Ok(expected_cloned)); - - let result = mock.get_block_by_hash(&block_hash).await.unwrap(); - assert_eq!(result, expected_block); - } - - #[tokio::test] - async fn test_get_best_block_hash() { - let mut mock = MockBitcoinRpcClient::new(); - let expected_hash = - BlockHash::from_str("000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f") - .unwrap(); - - mock.expect_get_best_block_hash() - .return_once(move || Ok(expected_hash)); - - let result = mock.get_best_block_hash().await.unwrap(); - assert_eq!(result, expected_hash); - } - - #[tokio::test] - async fn test_get_raw_transaction_info() { - let mut mock = MockBitcoinRpcClient::new(); - let txid = - Txid::from_str("1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef") - .unwrap(); - let expected_info = GetRawTransactionResult { - in_active_chain: None, - hex: vec![], - txid, - hash: Wtxid::from_raw_hash( - Hash::from_str("0000000000000000000000000000000000000000000000000000000000000000") - .unwrap(), - ), - size: 0, - vsize: 0, - version: 0, - locktime: 0, - vin: vec![], - vout: vec![], - blockhash: None, - confirmations: None, - time: None, - blocktime: None, - }; - let expected_cloned = expected_info.clone(); - mock.expect_get_raw_transaction_info() - .with(eq(txid)) - .return_once(move |_| Ok(expected_cloned)); - - let result = mock.get_raw_transaction_info(&txid).await.unwrap(); - assert_eq!(result, expected_info); - } - - #[tokio::test] - async fn test_estimate_smart_fee() { - let mut mock = MockBitcoinRpcClient::new(); - let conf_target = 6; - let estimate_mode = Some(EstimateMode::Conservative); - let expected_result = EstimateSmartFeeResult { - fee_rate: Some(Amount::from_sat(12345)), - errors: None, - blocks: 6, - }; - - let expected_cloned = expected_result.clone(); - mock.expect_estimate_smart_fee() - .with(eq(conf_target), eq(estimate_mode)) - .return_once(move |_, _| Ok(expected_cloned)); - - let result = mock - .estimate_smart_fee(conf_target, estimate_mode) - .await - .unwrap(); - assert_eq!(result.fee_rate, expected_result.fee_rate); - } -} diff --git a/core/lib/via_btc_client/src/indexer/mod.rs b/core/lib/via_btc_client/src/indexer/mod.rs index e4dd10e7ecce..8651fbd2705f 100644 --- a/core/lib/via_btc_client/src/indexer/mod.rs +++ b/core/lib/via_btc_client/src/indexer/mod.rs @@ -347,12 +347,7 @@ mod tests { } } - fn create_mock_indexer() -> BitcoinInscriptionIndexer { - let mut mock_client = MockBitcoinOps::new(); - mock_client - .expect_get_network() - .returning(|| Network::Testnet); - + fn get_indexer_with_mock(mock_client: MockBitcoinOps) -> BitcoinInscriptionIndexer { let parser = MessageParser::new(Network::Testnet); let bridge_address = Address::from_str("tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx") .unwrap() @@ -400,23 +395,7 @@ mod tests { .expect_get_network() .returning(|| Network::Testnet); - let indexer = BitcoinInscriptionIndexer { - client: Box::new(mock_client), - parser: MessageParser::new(Network::Testnet), - bridge_address: Address::from_str("tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx") - .unwrap() - .require_network(Network::Testnet) - .unwrap(), - sequencer_address: Address::from_str( - "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7", - ) - .unwrap() - .require_network(Network::Testnet) - .unwrap(), - verifier_addresses: vec![], - starting_block_number: 0, - network: Network::Testnet, - }; + let indexer = get_indexer_with_mock(mock_client); let result = indexer .are_blocks_connected(&parent_hash, &child_hash) @@ -451,24 +430,7 @@ mod tests { .expect_get_network() .returning(|| Network::Testnet); - let indexer = BitcoinInscriptionIndexer { - client: Box::new(mock_client), - parser: MessageParser::new(Network::Testnet), - bridge_address: Address::from_str("tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx") - .unwrap() - .require_network(Network::Testnet) - .unwrap(), - sequencer_address: Address::from_str( - "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7", - ) - .unwrap() - .require_network(Network::Testnet) - .unwrap(), - verifier_addresses: vec![], - starting_block_number: 0, - network: Network::Testnet, - }; - + let indexer = get_indexer_with_mock(mock_client); let result = indexer.process_blocks(start_block, end_block).await; assert!(result.is_ok()); assert_eq!(result.unwrap().len(), 0); @@ -476,7 +438,7 @@ mod tests { #[tokio::test] async fn test_is_valid_message() { - let indexer = create_mock_indexer(); + let indexer = get_indexer_with_mock(MockBitcoinOps::new()); let propose_sequencer = FullInscriptionMessage::ProposeSequencer(types::ProposeSequencer { common: CommonFields { @@ -508,7 +470,7 @@ mod tests { schnorr_signature: bitcoin::taproot::Signature::from_slice(&[0; 64]).unwrap(), encoded_public_key: bitcoin::script::PushBytesBuf::from([0u8; 32]), }, - input: crate::types::L1BatchDAReferenceInput { + input: types::L1BatchDAReferenceInput { l1_batch_hash: zksync_basic_types::H256::zero(), l1_batch_index: zksync_types::L1BatchNumber(0), da_identifier: "test".to_string(), @@ -536,12 +498,12 @@ mod tests { assert!(indexer.is_valid_message(&l1_to_l2_message)); let system_bootstrapping = - FullInscriptionMessage::SystemBootstrapping(crate::types::SystemBootstrapping { + FullInscriptionMessage::SystemBootstrapping(types::SystemBootstrapping { common: CommonFields { schnorr_signature: bitcoin::taproot::Signature::from_slice(&[0; 64]).unwrap(), encoded_public_key: bitcoin::script::PushBytesBuf::from([0u8; 32]), }, - input: crate::types::SystemBootstrappingInput { + input: types::SystemBootstrappingInput { start_block_height: 0, bridge_p2wpkh_mpc_address: indexer.bridge_address.clone(), verifier_p2wpkh_addresses: vec![], @@ -574,7 +536,7 @@ mod tests { #[tokio::test] async fn test_is_valid_l1_to_l2_transfer() { - let indexer = create_mock_indexer(); + let indexer = get_indexer_with_mock(MockBitcoinOps::new()); let valid_message = L1ToL2Message { common: CommonFields { diff --git a/core/lib/via_btc_client/src/inscriber/mod.rs b/core/lib/via_btc_client/src/inscriber/mod.rs index 8bfd2b86538f..2e8c4d416553 100644 --- a/core/lib/via_btc_client/src/inscriber/mod.rs +++ b/core/lib/via_btc_client/src/inscriber/mod.rs @@ -127,15 +127,9 @@ impl Inscriber { &inscription_data, )?; - let broadcast_status = self - .broadcast_inscription(&final_commit_tx, &final_reveal_tx) + self.broadcast_inscription(&final_commit_tx, &final_reveal_tx) .await?; - if !broadcast_status { - error!("Failed to broadcast inscription"); - return Err(anyhow::anyhow!("Failed to broadcast inscription")); - } - self.insert_inscription_to_context( input, final_commit_tx, @@ -655,66 +649,23 @@ impl Inscriber { } #[instrument(skip(self, commit, reveal), target = "bitcoin_inscriber")] - async fn broadcast_inscription(&self, commit: &FinalTx, reveal: &FinalTx) -> Result { + async fn broadcast_inscription(&self, commit: &FinalTx, reveal: &FinalTx) -> Result<()> { info!("Broadcasting inscription transactions"); let commit_tx_hex = commit.tx.raw_hex().to_string(); let reveal_tx_hex = reveal.tx.raw_hex().to_string(); - let mut commit_broadcasted = false; - let mut reveal_broadcasted = false; - - for attempt in 1..=BROADCAST_RETRY_COUNT { - debug!( - "Attempting to broadcast commit transaction (attempt {})", - attempt - ); - let res = self - .client - .broadcast_signed_transaction(&commit_tx_hex) - .await; - - if res.is_ok() { - commit_broadcasted = true; - info!("Commit transaction broadcasted successfully"); - break; - } else { - warn!( - "Failed to broadcast commit transaction (attempt {})", - attempt - ); - } - } - - for attempt in 1..=BROADCAST_RETRY_COUNT { - debug!( - "Attempting to broadcast reveal transaction (attempt {})", - attempt - ); - let res = self - .client - .broadcast_signed_transaction(&reveal_tx_hex) - .await; - - if res.is_ok() { - reveal_broadcasted = true; - info!("Reveal transaction broadcasted successfully"); - break; - } else { - warn!( - "Failed to broadcast reveal transaction (attempt {})", - attempt - ); - } - } + let commit_tx_id = self + .client + .broadcast_signed_transaction(&commit_tx_hex) + .await?; + let reveal_tx_id = self + .client + .broadcast_signed_transaction(&reveal_tx_hex) + .await?; - let result = commit_broadcasted && reveal_broadcasted; - if result { - info!("Both transactions broadcasted successfully"); - } else { - error!("Failed to broadcast one or both transactions"); - } + info!("Both transactions broadcasted successfully with ids: commit: {commit_tx_id}, reveal: {reveal_tx_id}"); - Ok(result) + Ok(()) } #[instrument( diff --git a/core/lib/via_btc_client/src/lib.rs b/core/lib/via_btc_client/src/lib.rs index 21ce908e3f7d..8cbf83778c47 100644 --- a/core/lib/via_btc_client/src/lib.rs +++ b/core/lib/via_btc_client/src/lib.rs @@ -6,3 +6,4 @@ pub mod indexer; pub mod inscriber; pub(crate) mod regtest; pub(crate) mod signer; +pub(crate) mod utils; diff --git a/core/lib/via_btc_client/src/utils.rs b/core/lib/via_btc_client/src/utils.rs new file mode 100644 index 000000000000..8df1c7c0a3a3 --- /dev/null +++ b/core/lib/via_btc_client/src/utils.rs @@ -0,0 +1,33 @@ +use tokio::time::Duration; +use tracing::Level; + +use crate::types::BitcoinRpcResult; + +pub(crate) async fn with_retry( + f: F, + max_retries: u8, + retry_delay_ms: u64, + operation_name: &str, +) -> Result +where + F: Fn() -> Result + Send + Sync, + E: std::fmt::Debug, +{ + let mut retries = 0; + loop { + match f() { + Ok(result) => return Ok(result), + Err(e) if retries < max_retries => { + tracing::warn!( + error = ?e, + retries, + "{} failed, retrying", + operation_name + ); + retries += 1; + tokio::time::sleep(Duration::from_millis(retry_delay_ms)).await; + } + Err(e) => return Err(e), + } + } +} From bb8c3b2b2d194e98e0b6bcb4dafa6ff53da54936 Mon Sep 17 00:00:00 2001 From: Steph Sinyakov Date: Wed, 21 Aug 2024 15:47:34 +0200 Subject: [PATCH 11/11] fix: regtest, example update [ci skip] --- Cargo.lock | 1 + core/lib/via_btc_client/Cargo.toml | 7 +- .../examples/bitcoin_client_example.rs | 100 ------------------ .../examples/indexer_init_example.rs | 22 ++++ .../docker-compose-btc.yml | 0 core/lib/via_btc_client/src/indexer/mod.rs | 15 ++- core/lib/via_btc_client/src/lib.rs | 3 +- core/lib/via_btc_client/src/regtest.rs | 26 +++-- core/lib/via_btc_client/src/traits.rs | 25 ----- 9 files changed, 52 insertions(+), 147 deletions(-) delete mode 100644 core/lib/via_btc_client/examples/bitcoin_client_example.rs create mode 100644 core/lib/via_btc_client/examples/indexer_init_example.rs rename core/lib/via_btc_client/{tests => resources}/docker-compose-btc.yml (100%) diff --git a/Cargo.lock b/Cargo.lock index 64ad8c2aee56..1a9aa9f9be17 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7680,6 +7680,7 @@ dependencies = [ "thiserror", "tokio", "tracing", + "tracing-subscriber", "zksync_basic_types", "zksync_config", "zksync_da_client", diff --git a/core/lib/via_btc_client/Cargo.toml b/core/lib/via_btc_client/Cargo.toml index 2d72b3b27cd0..b5c481dd516e 100644 --- a/core/lib/via_btc_client/Cargo.toml +++ b/core/lib/via_btc_client/Cargo.toml @@ -35,16 +35,17 @@ inquire = "0.7.5" anyhow.workspace = true serde.workspace = true tracing.workspace = true +tracing-subscriber = { workspace = true, optional = true } [dev-dependencies] mockall = "0.13.0" [features] -regtest = [] +regtest = ["tracing-subscriber"] [[example]] -name = "bitcoin_client_example" -path = "examples/bitcoin_client_example.rs" +name = "indexer" +path = "examples/indexer_init_example.rs" required-features = ["regtest"] [[example]] diff --git a/core/lib/via_btc_client/examples/bitcoin_client_example.rs b/core/lib/via_btc_client/examples/bitcoin_client_example.rs deleted file mode 100644 index cf3da17d92d6..000000000000 --- a/core/lib/via_btc_client/examples/bitcoin_client_example.rs +++ /dev/null @@ -1,100 +0,0 @@ -#![cfg(feature = "regtest")] - -use anyhow::Result; -use bitcoin::{ - absolute::LockTime, address::NetworkUnchecked, transaction::Version, Address, Amount, Network, - OutPoint, Sequence, Transaction, TxIn, TxOut, Witness, -}; -use via_btc_client::{ - client::BitcoinClient, regtest::BitcoinRegtest, traits::BitcoinSigner, BitcoinOps, -}; - -#[tokio::main] -async fn main() -> Result<()> { - let context = BitcoinRegtest::new()?; - let client = BitcoinClient::new("http://localhost:18443", "regtest").await?; - - let miner_address = context.get_miner_address()?; - println!( - "Balance of miner {miner_address}: {:?} SAT", - client.get_balance(&miner_address).await? - ); - - let address = context.get_address(); - let private_key = context.get_private_key(); - println!("Testing account:"); - println!("Private key: {:?}", private_key.to_wif()); - println!("Address: {:?}", address); - println!("Balance: {:?} SAT", client.get_balance(&address).await?); - - let random_address = "bcrt1qw508d6qejxtdg4y5r3zarvary0c5xw7kygt080" - .parse::>()? - .require_network(Network::Regtest)?; - println!("\nSending simple transfer to {}...\n", random_address); - - let amount_to_send = Amount::from_btc(20.0)?.to_sat(); - let fee = Amount::from_btc(0.00001)?.to_sat(); - let mut total_input = 0; - let mut inputs = Vec::new(); - - println!("Getting UTXOs of test address..."); - let utxos = client.fetch_utxos(&address).await?; - for (i, (utxo, txid, vout)) in utxos.into_iter().enumerate() { - println!("#{i} utxo:\nvout:{vout}\n{:?}\ntxid:{txid}\n", utxo); - inputs.push(TxIn { - previous_output: OutPoint::new(txid, vout), - script_sig: bitcoin::ScriptBuf::new(), - sequence: Sequence::MAX, - witness: Witness::new(), - }); - total_input += utxo.value.to_sat(); - if total_input > amount_to_send { - break; - } - } - - let mut outputs = vec![TxOut { - value: Amount::from_sat(amount_to_send), - script_pubkey: random_address.script_pubkey(), - }]; - if total_input.saturating_sub(amount_to_send) > Amount::from_btc(0.0002)?.to_sat() { - outputs.push(TxOut { - value: Amount::from_sat(total_input.saturating_sub(amount_to_send) - fee), - script_pubkey: address.script_pubkey(), - }) - } - println!("Outputs: {:?}", outputs); - let unsigned_tx = Transaction { - version: Version::TWO, - lock_time: LockTime::ZERO, - input: inputs, - output: outputs, - }; - - println!("Signing tx..."); - let signer = BasicSigner::new(private_key.to_wif().as_str(), client.get_rpc_client())?; - let mut signed_tx = unsigned_tx; - for i in 0..signed_tx.input.len() { - let witness = signer.sign_ecdsa(&signed_tx, i).await?; - signed_tx.input[i].witness = witness; - } - - let serialized_tx = bitcoin::consensus::encode::serialize(&signed_tx); - let txid = client - .broadcast_signed_transaction(&hex::encode(serialized_tx)) - .await?; - println!("\nTransaction sent: {:?}", txid); - - println!("Waiting for transaction to be mined..."); - tokio::time::sleep(std::time::Duration::from_secs(20)).await; - let i = client.check_tx_confirmation(&txid, 1).await?; - println!("Is {txid} confirmed (1 confirmation): {i}"); - - println!("Getting UTXOs of test address..."); - let utxos = client.fetch_utxos(&address).await?; - for (i, (utxo, txid, vout)) in utxos.into_iter().enumerate() { - println!("#{i} utxo:\nvout:{vout}\n{:?}\ntxid:{txid}\n", utxo); - } - - Ok(()) -} diff --git a/core/lib/via_btc_client/examples/indexer_init_example.rs b/core/lib/via_btc_client/examples/indexer_init_example.rs new file mode 100644 index 000000000000..7895da9958a1 --- /dev/null +++ b/core/lib/via_btc_client/examples/indexer_init_example.rs @@ -0,0 +1,22 @@ +use anyhow::Result; +use tracing_subscriber; +use via_btc_client::{indexer::BitcoinInscriptionIndexer, regtest::BitcoinRegtest, types::Network}; + +#[tokio::main] +async fn main() -> Result<()> { + tracing_subscriber::fmt() + .with_max_level(tracing::Level::DEBUG) + .init(); + + tracing::info!("starting Bitcoin client example"); + let context = BitcoinRegtest::new()?; + let miner = context.get_miner_address()?; + tracing::info!("miner address: {}", miner); + let indexer = + BitcoinInscriptionIndexer::new(&context.get_url(), Network::Regtest, vec![]).await; + + if let Err(e) = indexer { + tracing::error!("Failed to create indexer: {:?}", e); + } + Ok(()) +} diff --git a/core/lib/via_btc_client/tests/docker-compose-btc.yml b/core/lib/via_btc_client/resources/docker-compose-btc.yml similarity index 100% rename from core/lib/via_btc_client/tests/docker-compose-btc.yml rename to core/lib/via_btc_client/resources/docker-compose-btc.yml diff --git a/core/lib/via_btc_client/src/indexer/mod.rs b/core/lib/via_btc_client/src/indexer/mod.rs index 8651fbd2705f..8b03dad52a33 100644 --- a/core/lib/via_btc_client/src/indexer/mod.rs +++ b/core/lib/via_btc_client/src/indexer/mod.rs @@ -1,6 +1,5 @@ use std::collections::HashMap; -use async_trait::async_trait; use bitcoin::{Address, BlockHash, KnownHrp, Network, Txid}; use bitcoincore_rpc::Auth; use tracing::{debug, error, info, instrument, warn}; @@ -10,7 +9,7 @@ use parser::MessageParser; use crate::{ client::BitcoinClient, - traits::{BitcoinIndexerOpt, BitcoinOps}, + traits::BitcoinOps, types, types::{BitcoinIndexerResult, CommonFields, FullInscriptionMessage, L1ToL2Message, Vote}, }; @@ -68,10 +67,9 @@ pub struct BitcoinInscriptionIndexer { network: Network, } -#[async_trait] -impl BitcoinIndexerOpt for BitcoinInscriptionIndexer { +impl BitcoinInscriptionIndexer { #[instrument(skip(rpc_url, network, bootstrap_txids), target = "bitcoin_indexer")] - async fn new( + pub async fn new( rpc_url: &str, network: Network, bootstrap_txids: Vec, @@ -103,7 +101,7 @@ impl BitcoinIndexerOpt for BitcoinInscriptionIndexer { } #[instrument(skip(self), target = "bitcoin_indexer")] - async fn process_blocks( + pub async fn process_blocks( &self, starting_block: u32, ending_block: u32, @@ -121,7 +119,7 @@ impl BitcoinIndexerOpt for BitcoinInscriptionIndexer { } #[instrument(skip(self), target = "bitcoin_indexer")] - async fn process_block( + pub async fn process_block( &self, block_height: u32, ) -> BitcoinIndexerResult> { @@ -149,7 +147,7 @@ impl BitcoinIndexerOpt for BitcoinInscriptionIndexer { } #[instrument(skip(self), target = "bitcoin_indexer")] - async fn are_blocks_connected( + pub async fn are_blocks_connected( &self, parent_hash: &BlockHash, child_hash: &BlockHash, @@ -320,6 +318,7 @@ impl BitcoinInscriptionIndexer { mod tests { use std::str::FromStr; + use async_trait::async_trait; use bitcoin::{ block::Header, hashes::Hash, Amount, Block, OutPoint, ScriptBuf, Transaction, TxMerkleNode, TxOut, diff --git a/core/lib/via_btc_client/src/lib.rs b/core/lib/via_btc_client/src/lib.rs index 8cbf83778c47..6397f7d7a64d 100644 --- a/core/lib/via_btc_client/src/lib.rs +++ b/core/lib/via_btc_client/src/lib.rs @@ -4,6 +4,7 @@ pub mod types; pub(crate) mod client; pub mod indexer; pub mod inscriber; -pub(crate) mod regtest; +#[cfg(feature = "regtest")] +pub mod regtest; pub(crate) mod signer; pub(crate) mod utils; diff --git a/core/lib/via_btc_client/src/regtest.rs b/core/lib/via_btc_client/src/regtest.rs index 9cf26859d37f..19e0af6ca37c 100644 --- a/core/lib/via_btc_client/src/regtest.rs +++ b/core/lib/via_btc_client/src/regtest.rs @@ -7,9 +7,11 @@ use std::{ use anyhow::Result; use bitcoin::{address::NetworkUnchecked, Address, Network, PrivateKey}; -const COMPOSE_FILE_PATH: &str = - concat!(env!("CARGO_MANIFEST_DIR"), "/tests/docker-compose-btc.yml"); -const CLI_CONTAINER_NAME: &str = "tests-bitcoin-cli-1"; +const COMPOSE_FILE_PATH: &str = concat!( + env!("CARGO_MANIFEST_DIR"), + "/resources/docker-compose-btc.yml" +); +const CLI_CONTAINER_NAME: &str = "resources-bitcoin-cli-1"; pub struct BitcoinRegtest { private_key: PrivateKey, @@ -105,23 +107,27 @@ mod tests { use secp256k1::Secp256k1; use super::*; - use crate::{client::BitcoinRpcClient, traits::BitcoinRpc}; + use crate::{client::BitcoinClient, traits::BitcoinOps}; #[tokio::test] async fn test_bitcoin_regtest() { let regtest = BitcoinRegtest::new().expect("Failed to create BitcoinRegtest"); - let rpc = BitcoinRpcClient::new(®test.get_url(), Auth::UserPass("rpcuser".to_string(), "rpcpassword".to_string())) - .expect("Failed create rpc client"); - - let block_count = rpc - .get_block_count() + let client = BitcoinClient::new( + ®test.get_url(), + Network::Regtest, + Auth::UserPass("rpcuser".to_string(), "rpcpassword".to_string()), + ) + .expect("Failed create rpc client"); + + let block_count = client + .fetch_block_height() .await .expect("Failed to get block count"); assert!(block_count > 100); let address = regtest.get_address(); let private_key = regtest.get_private_key(); - let balance = rpc + let balance = client .get_balance(address) .await .expect("Failed to get balance of test address"); diff --git a/core/lib/via_btc_client/src/traits.rs b/core/lib/via_btc_client/src/traits.rs index bec9e18fd0bd..1ab6125da58c 100644 --- a/core/lib/via_btc_client/src/traits.rs +++ b/core/lib/via_btc_client/src/traits.rs @@ -83,28 +83,3 @@ pub(crate) trait BitcoinSigner: Send + Sync { fn get_public_key(&self) -> PublicKey; } - -#[async_trait] -pub(crate) trait BitcoinIndexerOpt: Send + Sync { - async fn new( - rpc_url: &str, - network: Network, - bootstrap_txids: Vec, - ) -> BitcoinIndexerResult - where - Self: Sized; - async fn process_blocks( - &self, - starting_block: u32, - ending_block: u32, - ) -> BitcoinIndexerResult>; - async fn process_block( - &self, - block_height: u32, - ) -> BitcoinIndexerResult>; - async fn are_blocks_connected( - &self, - parent_hash: &BlockHash, - child_hash: &BlockHash, - ) -> BitcoinIndexerResult; -}