From 2af678aa846a82657d13bcda2ac0b4a9db4e2b80 Mon Sep 17 00:00:00 2001 From: Vladimir Fomene Date: Thu, 16 Jun 2022 20:42:02 +0100 Subject: [PATCH] Get block hash by its height Create blockchain::GetBlockHash trait with a method to get block hash given a block height. Then, implement this trait for all backends (Electrum, RPC , Esplora, CBF). Referenced in issue 603. --- CHANGELOG.md | 1 + src/blockchain/any.rs | 7 +++++++ src/blockchain/compact_filters/mod.rs | 12 +++++++++++ src/blockchain/electrum.rs | 7 +++++++ src/blockchain/esplora/reqwest.rs | 8 ++++++++ src/blockchain/esplora/ureq.rs | 7 +++++++ src/blockchain/mod.rs | 18 +++++++++++++++-- src/blockchain/rpc.rs | 6 ++++++ src/testutils/blockchain_tests.rs | 29 +++++++++++++++++++++++++++ 9 files changed, 93 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bc8f6bac..b8bf8a04d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fee sniping discouraging through nLockTime - if the user specifies a `current_height`, we use that as a nlocktime, otherwise we use the last sync height (or 0 if we never synced) - Fix hang when `ElectrumBlockchainConfig::stop_gap` is zero. - Set coin type in BIP44, BIP49, and BIP84 templates +- Get block hash given a block height - A `get_block_hash` method is now defined on the `GetBlockHash` trait and implemented on every blockchain backend. This method expects a block height and returns the corresponding block hash. ## [v0.19.0] - [v0.18.0] diff --git a/src/blockchain/any.rs b/src/blockchain/any.rs index b66984629..5ef1a3385 100644 --- a/src/blockchain/any.rs +++ b/src/blockchain/any.rs @@ -120,6 +120,13 @@ impl GetTx for AnyBlockchain { } } +#[maybe_async] +impl GetBlockHash for AnyBlockchain { + fn get_block_hash(&self, height: u64) -> Result { + maybe_await!(impl_inner_method!(self, get_block_hash, height)) + } +} + #[maybe_async] impl WalletSync for AnyBlockchain { fn wallet_sync( diff --git a/src/blockchain/compact_filters/mod.rs b/src/blockchain/compact_filters/mod.rs index eb1720592..7ca78a2c3 100644 --- a/src/blockchain/compact_filters/mod.rs +++ b/src/blockchain/compact_filters/mod.rs @@ -260,6 +260,16 @@ impl GetTx for CompactFiltersBlockchain { } } +impl GetBlockHash for CompactFiltersBlockchain { + fn get_block_hash(&self, height: u64) -> Result { + self.headers + .get_block_hash(height as usize)? + .ok_or(Error::CompactFilters( + CompactFiltersError::BlockHashNotFound, + )) + } +} + impl WalletSync for CompactFiltersBlockchain { #[allow(clippy::mutex_atomic)] // Mutex is easier to understand than a CAS loop. fn wallet_setup( @@ -536,6 +546,8 @@ pub enum CompactFiltersError { InvalidFilter, /// The peer is missing a block in the valid chain MissingBlock, + /// Block hash at specified height not found + BlockHashNotFound, /// The data stored in the block filters storage are corrupted DataCorruption, diff --git a/src/blockchain/electrum.rs b/src/blockchain/electrum.rs index b71390cb9..faf7ea756 100644 --- a/src/blockchain/electrum.rs +++ b/src/blockchain/electrum.rs @@ -98,6 +98,13 @@ impl GetTx for ElectrumBlockchain { } } +impl GetBlockHash for ElectrumBlockchain { + fn get_block_hash(&self, height: u64) -> Result { + let block_header = self.client.block_header(height as usize)?; + Ok(block_header.block_hash()) + } +} + impl WalletSync for ElectrumBlockchain { fn wallet_setup( &self, diff --git a/src/blockchain/esplora/reqwest.rs b/src/blockchain/esplora/reqwest.rs index f68bdd8a1..0d4050608 100644 --- a/src/blockchain/esplora/reqwest.rs +++ b/src/blockchain/esplora/reqwest.rs @@ -117,6 +117,14 @@ impl GetTx for EsploraBlockchain { } } +#[maybe_async] +impl GetBlockHash for EsploraBlockchain { + fn get_block_hash(&self, height: u64) -> Result { + let block_header = await_or_block!(self.url_client._get_header(height as u32))?; + Ok(block_header.block_hash()) + } +} + #[maybe_async] impl WalletSync for EsploraBlockchain { fn wallet_setup( diff --git a/src/blockchain/esplora/ureq.rs b/src/blockchain/esplora/ureq.rs index 7ce57a138..9899b9046 100644 --- a/src/blockchain/esplora/ureq.rs +++ b/src/blockchain/esplora/ureq.rs @@ -112,6 +112,13 @@ impl GetTx for EsploraBlockchain { } } +impl GetBlockHash for EsploraBlockchain { + fn get_block_hash(&self, height: u64) -> Result { + let block_header = self.url_client._get_header(height as u32)?; + Ok(block_header.block_hash()) + } +} + impl WalletSync for EsploraBlockchain { fn wallet_setup( &self, diff --git a/src/blockchain/mod.rs b/src/blockchain/mod.rs index cf593c3c8..1dc5c95a1 100644 --- a/src/blockchain/mod.rs +++ b/src/blockchain/mod.rs @@ -21,7 +21,7 @@ use std::ops::Deref; use std::sync::mpsc::{channel, Receiver, Sender}; use std::sync::Arc; -use bitcoin::{Transaction, Txid}; +use bitcoin::{BlockHash, Transaction, Txid}; use crate::database::BatchDatabase; use crate::error::Error; @@ -87,7 +87,7 @@ pub enum Capability { /// Trait that defines the actions that must be supported by a blockchain backend #[maybe_async] -pub trait Blockchain: WalletSync + GetHeight + GetTx { +pub trait Blockchain: WalletSync + GetHeight + GetTx + GetBlockHash { /// Return the set of [`Capability`] supported by this backend fn get_capabilities(&self) -> HashSet; /// Broadcast a transaction @@ -110,6 +110,13 @@ pub trait GetTx { fn get_tx(&self, txid: &Txid) -> Result, Error>; } +#[maybe_async] +/// Trait for getting block hash by block height +pub trait GetBlockHash { + /// fetch block hash given its height + fn get_block_hash(&self, height: u64) -> Result; +} + /// Trait for blockchains that can sync by updating the database directly. #[maybe_async] pub trait WalletSync { @@ -359,6 +366,13 @@ impl GetHeight for Arc { } } +#[maybe_async] +impl GetBlockHash for Arc { + fn get_block_hash(&self, height: u64) -> Result { + maybe_await!(self.deref().get_block_hash(height)) + } +} + #[maybe_async] impl WalletSync for Arc { fn wallet_setup( diff --git a/src/blockchain/rpc.rs b/src/blockchain/rpc.rs index d1366368d..9ba6f0f33 100644 --- a/src/blockchain/rpc.rs +++ b/src/blockchain/rpc.rs @@ -169,6 +169,12 @@ impl GetHeight for RpcBlockchain { } } +impl GetBlockHash for RpcBlockchain { + fn get_block_hash(&self, height: u64) -> Result { + Ok(self.client.get_block_hash(height)?) + } +} + impl WalletSync for RpcBlockchain { fn wallet_setup( &self, diff --git a/src/testutils/blockchain_tests.rs b/src/testutils/blockchain_tests.rs index a78e4a893..2ae546324 100644 --- a/src/testutils/blockchain_tests.rs +++ b/src/testutils/blockchain_tests.rs @@ -1361,6 +1361,35 @@ macro_rules! bdk_blockchain_tests { let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); assert_eq!(finalized, true); } + + #[test] + fn test_get_block_hash() { + use bitcoincore_rpc::{ RpcApi }; + use crate::blockchain::GetBlockHash; + + // create wallet with init_wallet + let (_, blockchain, _descriptors, mut test_client) = init_single_sig(); + + let height = test_client.bitcoind.client.get_blockchain_info().unwrap().blocks as u64; + let best_hash = test_client.bitcoind.client.get_best_block_hash().unwrap(); + + // use get_block_hash to get best block hash and compare with best_hash above + let block_hash = blockchain.get_block_hash(height).unwrap(); + assert_eq!(best_hash, block_hash); + + // generate blocks to address + let node_addr = test_client.get_node_address(None); + test_client.generate(10, Some(node_addr)); + + let height = test_client.bitcoind.client.get_blockchain_info().unwrap().blocks as u64; + let best_hash = test_client.bitcoind.client.get_best_block_hash().unwrap(); + + let block_hash = blockchain.get_block_hash(height).unwrap(); + assert_eq!(best_hash, block_hash); + + // try to get hash for block that has not yet been created. + assert!(blockchain.get_block_hash(height + 1).is_err()); + } } };