diff --git a/CHANGELOG.md b/CHANGELOG.md index e6511bdc6..8c08b0e69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `Excess` enum to handle remaining amount after coin selection. - Move change creation from `Wallet::create_tx` to `CoinSelectionAlgorithm::coin_select`. - Change the interface of `SqliteDatabase::new` to accept any type that implement AsRef +- Change trait `GetBlockHash` to `GetBlockInfo`. A new function is added to the new trait `get_block_header` which expects a block height and returns corresponding block header. This is implemented on every blockchain backend. +- Amend the struct `After` and `Older` to accept and check against timestamps as well. ## [v0.20.0] - [v0.19.0] diff --git a/src/blockchain/any.rs b/src/blockchain/any.rs index 5ef1a3385..d8f0c6969 100644 --- a/src/blockchain/any.rs +++ b/src/blockchain/any.rs @@ -121,10 +121,13 @@ impl GetTx for AnyBlockchain { } #[maybe_async] -impl GetBlockHash for AnyBlockchain { +impl GetBlockInfo for AnyBlockchain { fn get_block_hash(&self, height: u64) -> Result { maybe_await!(impl_inner_method!(self, get_block_hash, height)) } + fn get_block_header(&self, height: u64) -> Result { + maybe_await!(impl_inner_method!(self, get_block_header, height)) + } } #[maybe_async] diff --git a/src/blockchain/compact_filters/mod.rs b/src/blockchain/compact_filters/mod.rs index 7ca78a2c3..2044cdb70 100644 --- a/src/blockchain/compact_filters/mod.rs +++ b/src/blockchain/compact_filters/mod.rs @@ -260,7 +260,7 @@ impl GetTx for CompactFiltersBlockchain { } } -impl GetBlockHash for CompactFiltersBlockchain { +impl GetBlockInfo for CompactFiltersBlockchain { fn get_block_hash(&self, height: u64) -> Result { self.headers .get_block_hash(height as usize)? @@ -268,6 +268,11 @@ impl GetBlockHash for CompactFiltersBlockchain { CompactFiltersError::BlockHashNotFound, )) } + fn get_block_header(&self, height: u64) -> Result { + self.headers + .get_block_header(height as usize)? + .ok_or(Error::CompactFilters(CompactFiltersError::InvalidHeaders)) + } } impl WalletSync for CompactFiltersBlockchain { diff --git a/src/blockchain/compact_filters/store.rs b/src/blockchain/compact_filters/store.rs index eeca28c03..507fa129f 100644 --- a/src/blockchain/compact_filters/store.rs +++ b/src/blockchain/compact_filters/store.rs @@ -436,6 +436,16 @@ impl ChainStore { } pub fn get_block_hash(&self, height: usize) -> Result, CompactFiltersError> { + match self.get_block_header(height)? { + Some(header) => Ok(Some(header.block_hash())), + None => Ok(None), + } + } + + pub fn get_block_header( + &self, + height: usize, + ) -> Result, CompactFiltersError> { let read_store = self.store.read().unwrap(); let cf_handle = read_store.cf_handle(&self.cf_name).unwrap(); @@ -444,7 +454,7 @@ impl ChainStore { data.map(|data| { let (header, _): (BlockHeader, Uint256) = deserialize(&data).map_err(|_| CompactFiltersError::DataCorruption)?; - Ok::<_, CompactFiltersError>(header.block_hash()) + Ok::<_, CompactFiltersError>(header) }) .transpose() } diff --git a/src/blockchain/electrum.rs b/src/blockchain/electrum.rs index faf7ea756..a7f4fbd17 100644 --- a/src/blockchain/electrum.rs +++ b/src/blockchain/electrum.rs @@ -98,11 +98,15 @@ impl GetTx for ElectrumBlockchain { } } -impl GetBlockHash for ElectrumBlockchain { +impl GetBlockInfo 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()) } + fn get_block_header(&self, height: u64) -> Result { + let block_header = self.client.block_header(height as usize)?; + Ok(block_header) + } } impl WalletSync for ElectrumBlockchain { diff --git a/src/blockchain/esplora/reqwest.rs b/src/blockchain/esplora/reqwest.rs index 302e811fd..5373bb3d2 100644 --- a/src/blockchain/esplora/reqwest.rs +++ b/src/blockchain/esplora/reqwest.rs @@ -118,11 +118,15 @@ impl GetTx for EsploraBlockchain { } #[maybe_async] -impl GetBlockHash for EsploraBlockchain { +impl GetBlockInfo 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()) } + fn get_block_header(&self, height: u64) -> Result { + let block_header = await_or_block!(self.url_client._get_header(height as u32))?; + Ok(block_header) + } } #[maybe_async] diff --git a/src/blockchain/esplora/ureq.rs b/src/blockchain/esplora/ureq.rs index 9899b9046..4762cf479 100644 --- a/src/blockchain/esplora/ureq.rs +++ b/src/blockchain/esplora/ureq.rs @@ -112,11 +112,15 @@ impl GetTx for EsploraBlockchain { } } -impl GetBlockHash for EsploraBlockchain { +impl GetBlockInfo 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()) } + fn get_block_header(&self, height: u64) -> Result { + let block_header = await_or_block!(self.url_client._get_header(height as u32))?; + Ok(block_header) + } } impl WalletSync for EsploraBlockchain { diff --git a/src/blockchain/mod.rs b/src/blockchain/mod.rs index 1dc5c95a1..bdb720235 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::{BlockHash, Transaction, Txid}; +use bitcoin::{BlockHash, BlockHeader, 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 + GetBlockHash { +pub trait Blockchain: WalletSync + GetHeight + GetTx + GetBlockInfo { /// Return the set of [`Capability`] supported by this backend fn get_capabilities(&self) -> HashSet; /// Broadcast a transaction @@ -112,9 +112,11 @@ pub trait GetTx { #[maybe_async] /// Trait for getting block hash by block height -pub trait GetBlockHash { +pub trait GetBlockInfo { /// fetch block hash given its height fn get_block_hash(&self, height: u64) -> Result; + /// fetch block header given its height + fn get_block_header(&self, height: u64) -> Result; } /// Trait for blockchains that can sync by updating the database directly. @@ -367,10 +369,13 @@ impl GetHeight for Arc { } #[maybe_async] -impl GetBlockHash for Arc { +impl GetBlockInfo for Arc { fn get_block_hash(&self, height: u64) -> Result { maybe_await!(self.deref().get_block_hash(height)) } + fn get_block_header(&self, height: u64) -> Result { + maybe_await!(self.deref().get_block_header(height)) + } } #[maybe_async] diff --git a/src/blockchain/rpc.rs b/src/blockchain/rpc.rs index 1d0d884c0..3ab294c21 100644 --- a/src/blockchain/rpc.rs +++ b/src/blockchain/rpc.rs @@ -169,10 +169,14 @@ impl GetHeight for RpcBlockchain { } } -impl GetBlockHash for RpcBlockchain { +impl GetBlockInfo for RpcBlockchain { fn get_block_hash(&self, height: u64) -> Result { Ok(self.client.get_block_hash(height)?) } + fn get_block_header(&self, height: u64) -> Result { + let block_hash = self.client.get_block_hash(height)?; + Ok(self.client.get_block_header(&block_hash)?) + } } impl WalletSync for RpcBlockchain { diff --git a/src/descriptor/policy.rs b/src/descriptor/policy.rs index 215078b60..a0b6b1c16 100644 --- a/src/descriptor/policy.rs +++ b/src/descriptor/policy.rs @@ -904,7 +904,7 @@ impl ExtractPolicy for Miniscript::check_after(&after, *value); let inputs_sat = psbt_inputs_sat(psbt) .all(|sat| Satisfier::::check_after(&sat, *value)); @@ -929,7 +929,13 @@ impl ExtractPolicy for Miniscript::check_older(&older, *value); let inputs_sat = psbt_inputs_sat(psbt) .all(|sat| Satisfier::::check_older(&sat, *value)); diff --git a/src/testutils/blockchain_tests.rs b/src/testutils/blockchain_tests.rs index 89e091335..720a5d6b4 100644 --- a/src/testutils/blockchain_tests.rs +++ b/src/testutils/blockchain_tests.rs @@ -1419,7 +1419,7 @@ macro_rules! bdk_blockchain_tests { #[test] fn test_get_block_hash() { use bitcoincore_rpc::{ RpcApi }; - use crate::blockchain::GetBlockHash; + use crate::blockchain::GetBlockInfo; // create wallet with init_wallet let (_, blockchain, _descriptors, mut test_client) = init_single_sig(); diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 4ee3835d9..62f6839ba 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -16,6 +16,7 @@ use std::cell::RefCell; use std::collections::HashMap; use std::collections::{BTreeMap, HashSet}; +use std::convert::TryInto; use std::fmt; use std::ops::{Deref, DerefMut}; use std::str::FromStr; @@ -1155,15 +1156,37 @@ where .borrow() .get_tx(&input.previous_output.txid, false)? .map(|tx| tx.confirmation_time.map(|c| c.height).unwrap_or(u32::MAX)); + let create_time = self + .database + .borrow() + .get_tx(&input.previous_output.txid, false)? + .map(|tx| { + tx.confirmation_time + .map(|c| { + c.timestamp + .try_into() + .expect("Time is greater than 0xFFFFFFFF") + }) + .unwrap_or(u32::MAX) + }); let last_sync_height = self .database() .get_sync_time()? .map(|sync_time| sync_time.block_time.height); let current_height = sign_options.assume_height.or(last_sync_height); + // TODO: Change current time to median time of latest 11 blocks with Blockchain::GetBlockInfo + // according to BIP-113 + let current_time = self.database().get_sync_time()?.map(|sync_time| { + sync_time + .block_time + .timestamp + .try_into() + .expect("Time is greater than 0xFFFFFFFF") + }); debug!( - "Input #{} - {}, using `create_height` = {:?}, `current_height` = {:?}", - n, input.previous_output, create_height, current_height + "Input #{} - {}, using `create_height` = {:?}, `current_height` = {:?}, `create_time` = {:?}, `current_time` = {:?}", + n, input.previous_output, create_height, current_height, create_time, current_time ); // - Try to derive the descriptor by looking at the txout. If it's in our database, we @@ -1197,8 +1220,16 @@ where &mut tmp_input, ( PsbtInputSatisfier::new(psbt, n), - After::new(current_height, false), - Older::new(current_height, create_height, false), + // FIXME: The satisfier doesn't call check methods of After and Older defined in wallet/utils.rs + // Instead it calls the implementations defined in miniscript + After::new(current_height, current_time, false), + Older::new( + current_height, + current_time, + create_height, + create_time, + false, + ), ), ) { Ok(_) => { @@ -1962,11 +1993,21 @@ pub(crate) mod test { "wsh(or_d(pk(cRjo6jqfVNP33HhSS76UhXETZsGTZYx8FMFvR9kpbtCSV1PmdZdu),and_v(v:pk(cMnkdebixpXMPfkcNEjjGin7s94hiehAH4mLbYkZoh9KSiNNmqC8),older(144))))" } + pub(crate) fn get_test_single_sig_csv_with_time() -> &'static str { + // and(pk(Alice),older(4194904)) // (1 << 22) | 600 -> lock of 600 seconds with type time + "wsh(and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),older(4194904)))" + } + pub(crate) fn get_test_single_sig_cltv() -> &'static str { // and(pk(Alice),after(100000)) "wsh(and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),after(100000)))" } + pub(crate) fn get_test_single_sig_cltv_with_future_time() -> &'static str { + // and(pk(Alice),after(1893456000)) // Tue Jan 01 2030 00:00:00 GMT+0000 + "wsh(and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),after(1893456000)))" + } + pub(crate) fn get_test_tr_single_sig() -> &'static str { "tr(cNJmN3fH9DDbDt131fQNkVakkpzawJBSeybCUNmP1BovpmGQ45xG)" } @@ -2157,6 +2198,54 @@ pub(crate) mod test { assert_eq!(psbt.unsigned_tx.lock_time, 100_000); } + #[test] + fn test_create_tx_locktime_cltv_with_time() { + let (wallet, _, _) = get_funded_wallet(get_test_single_sig_cltv_with_future_time()); + let addr = wallet.get_address(New).unwrap(); + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), 25_000); + let (psbt, _) = builder.finish().unwrap(); + let after = After::new(None, Some(time::get_timestamp() as u32), false); + let after_sat = miniscript::Satisfier::::check_after( + &after, + psbt.unsigned_tx.lock_time, + ); + + assert!(!after_sat); + + let after = After::new(None, Some(1893456000), false); + let after_sat = miniscript::Satisfier::::check_after( + &after, + psbt.unsigned_tx.lock_time, + ); + assert!(after_sat); + } + + #[test] + fn test_create_tx_locktime_csv_with_time() { + let (wallet, _, _) = get_funded_wallet(get_test_single_sig_csv_with_time()); + let addr = wallet.get_address(New).unwrap(); + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), 25_000); + let (psbt, _) = builder.finish().unwrap(); + let time_stamp = Some(time::get_timestamp() as u32); + let late_time_stamp = Some(time::get_timestamp() as u32 + 601); + + let older = Older::new(None, time_stamp, None, time_stamp, false); + let older_sat = miniscript::Satisfier::::check_older( + &older, + psbt.unsigned_tx.input[0].sequence, + ); + assert!(!older_sat); + + let older = Older::new(None, late_time_stamp, None, time_stamp, false); + let older_sat = miniscript::Satisfier::::check_older( + &older, + psbt.unsigned_tx.input[0].sequence, + ); + assert!(older_sat); + } + #[test] fn test_create_tx_custom_locktime() { let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); diff --git a/src/wallet/utils.rs b/src/wallet/utils.rs index cee72e40b..31408077b 100644 --- a/src/wallet/utils.rs +++ b/src/wallet/utils.rs @@ -44,14 +44,20 @@ impl IsDust for u64 { pub struct After { pub current_height: Option, - pub assume_height_reached: bool, + pub current_time: Option, + pub assume_reached: bool, } impl After { - pub(crate) fn new(current_height: Option, assume_height_reached: bool) -> After { + pub(crate) fn new( + current_height: Option, + current_time: Option, + assume_reached: bool, + ) -> After { After { current_height, - assume_height_reached, + current_time, + assume_reached, } } } @@ -96,41 +102,61 @@ pub(crate) fn check_nlocktime(nlocktime: u32, required: u32) -> bool { impl Satisfier for After { fn check_after(&self, n: u32) -> bool { - if let Some(current_height) = self.current_height { - current_height >= n + if n < BLOCKS_TIMELOCK_THRESHOLD { + if let Some(current_height) = self.current_height { + current_height >= n + } else { + self.assume_reached + } + } else if let Some(current_time) = self.current_time { + current_time >= n } else { - self.assume_height_reached + self.assume_reached } } } pub struct Older { pub current_height: Option, + pub current_time: Option, pub create_height: Option, - pub assume_height_reached: bool, + pub create_time: Option, + pub assume_reached: bool, } impl Older { pub(crate) fn new( current_height: Option, + current_time: Option, create_height: Option, - assume_height_reached: bool, + create_time: Option, + assume_reached: bool, ) -> Older { Older { current_height, + current_time, create_height, - assume_height_reached, + create_time, + assume_reached, } } } impl Satisfier for Older { fn check_older(&self, n: u32) -> bool { - if let Some(current_height) = self.current_height { + let masked_n = n & SEQUENCE_LOCKTIME_MASK; + if n & SEQUENCE_LOCKTIME_TYPE_FLAG == 0 { + if let Some(current_height) = self.current_height { + // TODO: test >= / > + current_height as u64 >= self.create_height.unwrap_or(0) as u64 + masked_n as u64 + } else { + self.assume_reached + } + } else if let Some(current_time) = self.current_time { // TODO: test >= / > - current_height as u64 >= self.create_height.unwrap_or(0) as u64 + n as u64 + current_time as u64 >= self.create_time.unwrap_or(0) as u64 + masked_n as u64 } else { - self.assume_height_reached + self.assume_reached } } }