diff --git a/Cargo.lock b/Cargo.lock index b3e8a457..d9207111 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -600,6 +600,7 @@ dependencies = [ "serde_json", "sha2 0.10.6", "thiserror", + "tokio", "zeroize", ] diff --git a/coins/bitcoin/Cargo.toml b/coins/bitcoin/Cargo.toml index 6c4d318e..9c50e908 100644 --- a/coins/bitcoin/Cargo.toml +++ b/coins/bitcoin/Cargo.toml @@ -30,3 +30,5 @@ reqwest = { version = "0.11", features = ["json"] } [dev-dependencies] frost = { package = "modular-frost", path = "../../crypto/frost", version = "0.6", features = ["tests"] } + +tokio = { version = "1", features = ["full"] } diff --git a/coins/bitcoin/src/lib.rs b/coins/bitcoin/src/lib.rs index 2de4a062..31b455b2 100644 --- a/coins/bitcoin/src/lib.rs +++ b/coins/bitcoin/src/lib.rs @@ -4,7 +4,7 @@ pub mod crypto; pub mod algorithm; /// Wallet functionality to create transactions. pub mod wallet; -/// A minimal async RPC. +/// A minimal asynchronous Bitcoin RPC client. pub mod rpc; #[cfg(test)] diff --git a/coins/bitcoin/src/rpc.rs b/coins/bitcoin/src/rpc.rs index e4853296..ab7507ee 100644 --- a/coins/bitcoin/src/rpc.rs +++ b/coins/bitcoin/src/rpc.rs @@ -16,11 +16,12 @@ use bitcoin::{ #[derive(Clone, Debug, Deserialize)] #[serde(untagged)] -pub(crate) enum RpcResponse { +enum RpcResponse { Ok { result: T }, Err { error: String }, } +/// A minimal asynchronous Bitcoin RPC client. #[derive(Clone, Debug)] pub struct Rpc(String); @@ -35,10 +36,14 @@ pub enum RpcError { } impl Rpc { - pub fn new(url: String) -> Rpc { - Rpc(url) + pub async fn new(url: String) -> Result { + let rpc = Rpc(url); + // Make an RPC request to verify the node is reachable and sane + rpc.get_latest_block_number().await?; + Ok(rpc) } + /// Perform an arbitrary RPC call. pub async fn rpc_call( &self, method: &str, @@ -63,17 +68,21 @@ impl Rpc { } } + /// Get the latest block's number. pub async fn get_latest_block_number(&self) -> Result { self.rpc_call("getblockcount", json!([])).await } + /// Get the hash of a block by the block's number. pub async fn get_block_hash(&self, number: usize) -> Result<[u8; 32], RpcError> { let mut hash = self.rpc_call::("getblockhash", json!([number])).await?.as_hash().into_inner(); + // bitcoin stores the inner bytes in reverse order. hash.reverse(); Ok(hash) } + /// Get a block's number by its hash. pub async fn get_block_number(&self, hash: &[u8; 32]) -> Result { #[derive(Deserialize, Debug)] struct Number { @@ -82,19 +91,42 @@ impl Rpc { Ok(self.rpc_call::("getblockheader", json!([hash.to_hex()])).await?.height) } + /// Get a block by its hash. pub async fn get_block(&self, hash: &[u8; 32]) -> Result { let hex = self.rpc_call::("getblock", json!([hash.to_hex(), 0])).await?; let bytes: Vec = FromHex::from_hex(&hex).map_err(|_| RpcError::InvalidResponse)?; - encode::deserialize(&bytes).map_err(|_| RpcError::InvalidResponse) + let block: Block = encode::deserialize(&bytes).map_err(|_| RpcError::InvalidResponse)?; + + let mut block_hash = block.block_hash().as_hash().into_inner(); + block_hash.reverse(); + if hash != &block_hash { + Err(RpcError::InvalidResponse)?; + } + + Ok(block) } + /// Publish a transaction. pub async fn send_raw_transaction(&self, tx: &Transaction) -> Result { - self.rpc_call("sendrawtransaction", json!([encode::serialize_hex(tx)])).await + let txid = self.rpc_call("sendrawtransaction", json!([encode::serialize_hex(tx)])).await?; + if txid != tx.txid() { + Err(RpcError::InvalidResponse)?; + } + Ok(txid) } + /// Get a transaction by its hash. pub async fn get_transaction(&self, hash: &[u8; 32]) -> Result { let hex = self.rpc_call::("getrawtransaction", json!([hash.to_hex()])).await?; let bytes: Vec = FromHex::from_hex(&hex).map_err(|_| RpcError::InvalidResponse)?; - encode::deserialize(&bytes).map_err(|_| RpcError::InvalidResponse) + let tx: Transaction = encode::deserialize(&bytes).map_err(|_| RpcError::InvalidResponse)?; + + let mut tx_hash = tx.txid().as_hash().into_inner(); + tx_hash.reverse(); + if hash != &tx_hash { + Err(RpcError::InvalidResponse)?; + } + + Ok(tx) } } diff --git a/coins/bitcoin/src/tests/mod.rs b/coins/bitcoin/src/tests/mod.rs index 2b0afce4..96827ab1 100644 --- a/coins/bitcoin/src/tests/mod.rs +++ b/coins/bitcoin/src/tests/mod.rs @@ -13,7 +13,11 @@ use frost::{ tests::{algorithm_machines, key_gen, sign}, }; -use crate::{crypto::{x_only, make_even}, algorithm::Schnorr}; +use crate::{ + crypto::{x_only, make_even}, + algorithm::Schnorr, + rpc::Rpc, +}; #[test] fn test_algorithm() { @@ -25,7 +29,8 @@ fn test_algorithm() { *keys = keys.offset(Scalar::from(offset)); } - let algo = Schnorr::::new(RecommendedTranscript::new(b"bitcoin-serai sign test")); + let algo = + Schnorr::::new(RecommendedTranscript::new(b"bitcoin-serai sign test")); let sig = sign( &mut OsRng, algo.clone(), @@ -42,3 +47,14 @@ fn test_algorithm() { ) .unwrap() } + +#[tokio::test] +async fn test_rpc() { + let rpc = Rpc::new("http://serai:seraidex@127.0.0.1:18443".to_string()).await.unwrap(); + + let latest = rpc.get_latest_block_number().await.unwrap(); + assert_eq!( + rpc.get_block_number(&rpc.get_block_hash(latest).await.unwrap()).await.unwrap(), + latest + ); +} diff --git a/processor/src/coins/bitcoin.rs b/processor/src/coins/bitcoin.rs index 3c7ec235..e137fe56 100644 --- a/processor/src/coins/bitcoin.rs +++ b/processor/src/coins/bitcoin.rs @@ -215,8 +215,8 @@ impl PartialEq for Bitcoin { impl Eq for Bitcoin {} impl Bitcoin { - pub fn new(url: String) -> Bitcoin { - Bitcoin { rpc: Rpc::new(url) } + pub async fn new(url: String) -> Bitcoin { + Bitcoin { rpc: Rpc::new(url).await.expect("couldn't create a Bitcoin RPC") } } #[cfg(test)] diff --git a/processor/src/main.rs b/processor/src/main.rs index 41935d85..20d5c96e 100644 --- a/processor/src/main.rs +++ b/processor/src/main.rs @@ -450,7 +450,7 @@ async fn main() { let url = env::var("COIN_RPC").expect("coin rpc wasn't specified as an env var"); match env::var("COIN").expect("coin wasn't specified as an env var").as_str() { #[cfg(feature = "bitcoin")] - "bitcoin" => run(db, Bitcoin::new(url), coordinator).await, + "bitcoin" => run(db, Bitcoin::new(url).await, coordinator).await, #[cfg(feature = "monero")] "monero" => run(db, Monero::new(url), coordinator).await, _ => panic!("unrecognized coin"), diff --git a/processor/src/tests/literal/mod.rs b/processor/src/tests/literal/mod.rs index 78cf75a7..8b749f85 100644 --- a/processor/src/tests/literal/mod.rs +++ b/processor/src/tests/literal/mod.rs @@ -3,7 +3,7 @@ mod bitcoin { use crate::coins::Bitcoin; async fn bitcoin() -> Bitcoin { - let bitcoin = Bitcoin::new("http://serai:seraidex@127.0.0.1:18443".to_string()); + let bitcoin = Bitcoin::new("http://serai:seraidex@127.0.0.1:18443".to_string()).await; bitcoin.fresh_chain().await; bitcoin }