Skip to content

Commit 40b170c

Browse files
committed
Merge branch 'master' of https://github.com/bitcoindevkit/bdk into rpc-pruned
2 parents d99b9d1 + 8e0d00a commit 40b170c

14 files changed

+600
-132
lines changed

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

77
## [Unreleased]
8-
8+
- Add `descriptor::checksum::get_checksum_bytes` method.
9+
- Add `Excess` enum to handle remaining amount after coin selection.
10+
- Move change creation from `Wallet::create_tx` to `CoinSelectionAlgorithm::coin_select`.
11+
- Change the interface of `SqliteDatabase::new` to accept any type that implement AsRef<Path>
912

1013
## [v0.20.0] - [v0.19.0]
1114

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ use bdk::blockchain::ElectrumBlockchain;
9595
use bdk::electrum_client::Client;
9696
use bdk::wallet::AddressIndex::New;
9797
98+
use bitcoin::base64;
9899
use bitcoin::consensus::serialize;
99100
100101
fn main() -> Result<(), bdk::Error> {
@@ -131,6 +132,7 @@ fn main() -> Result<(), bdk::Error> {
131132
```rust,no_run
132133
use bdk::{Wallet, SignOptions, database::MemoryDatabase};
133134
135+
use bitcoin::base64;
134136
use bitcoin::consensus::deserialize;
135137
136138
fn main() -> Result<(), bdk::Error> {
@@ -154,15 +156,15 @@ fn main() -> Result<(), bdk::Error> {
154156

155157
### Unit testing
156158

157-
```
159+
```bash
158160
cargo test
159161
```
160162

161163
### Integration testing
162164

163165
Integration testing require testing features, for example:
164166

165-
```
167+
```bash
166168
cargo test --features test-electrum
167169
```
168170

src/blockchain/esplora/reqwest.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,6 @@ impl WalletSync for EsploraBlockchain {
213213
};
214214

215215
database.commit_batch(batch_update)?;
216-
217216
Ok(())
218217
}
219218
}

src/blockchain/script_sync.rs

Lines changed: 68 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ returns associated transactions i.e. electrum.
55
#![allow(dead_code)]
66
use crate::{
77
database::{BatchDatabase, BatchOperations, DatabaseUtils},
8+
error::MissingCachedScripts,
89
wallet::time::Instant,
910
BlockTime, Error, KeychainKind, LocalUtxo, TransactionDetails,
1011
};
@@ -34,11 +35,12 @@ pub fn start<D: BatchDatabase>(db: &D, stop_gap: usize) -> Result<Request<'_, D>
3435
let scripts_needed = db
3536
.iter_script_pubkeys(Some(keychain))?
3637
.into_iter()
37-
.collect();
38+
.collect::<VecDeque<_>>();
3839
let state = State::new(db);
3940

4041
Ok(Request::Script(ScriptReq {
4142
state,
43+
initial_scripts_needed: scripts_needed.len(),
4244
scripts_needed,
4345
script_index: 0,
4446
stop_gap,
@@ -50,6 +52,7 @@ pub fn start<D: BatchDatabase>(db: &D, stop_gap: usize) -> Result<Request<'_, D>
5052
pub struct ScriptReq<'a, D: BatchDatabase> {
5153
state: State<'a, D>,
5254
script_index: usize,
55+
initial_scripts_needed: usize, // if this is 1, we assume the descriptor is not derivable
5356
scripts_needed: VecDeque<Script>,
5457
stop_gap: usize,
5558
keychain: KeychainKind,
@@ -113,43 +116,71 @@ impl<'a, D: BatchDatabase> ScriptReq<'a, D> {
113116
self.script_index += 1;
114117
}
115118

116-
for _ in txids {
117-
self.scripts_needed.pop_front();
118-
}
119+
self.scripts_needed.drain(..txids.len());
119120

120-
let last_active_index = self
121+
// last active index: 0 => No last active
122+
let last = self
121123
.state
122124
.last_active_index
123125
.get(&self.keychain)
124-
.map(|x| x + 1)
125-
.unwrap_or(0); // so no addresses active maps to 0
126-
127-
Ok(
128-
if self.script_index > last_active_index + self.stop_gap
129-
|| self.scripts_needed.is_empty()
130-
{
131-
debug!(
132-
"finished scanning for transactions for keychain {:?} at index {}",
133-
self.keychain, last_active_index
134-
);
135-
// we're done here -- check if we need to do the next keychain
136-
if let Some(keychain) = self.next_keychains.pop() {
137-
self.keychain = keychain;
138-
self.script_index = 0;
139-
self.scripts_needed = self
140-
.state
141-
.db
142-
.iter_script_pubkeys(Some(keychain))?
143-
.into_iter()
144-
.collect();
145-
Request::Script(self)
146-
} else {
147-
Request::Tx(TxReq { state: self.state })
148-
}
149-
} else {
150-
Request::Script(self)
151-
},
152-
)
126+
.map(|&l| l + 1)
127+
.unwrap_or(0);
128+
// remaining scripts left to check
129+
let remaining = self.scripts_needed.len();
130+
// difference between current index and last active index
131+
let current_gap = self.script_index - last;
132+
133+
// this is a hack to check whether the scripts are coming from a derivable descriptor
134+
// we assume for non-derivable descriptors, the initial script count is always 1
135+
let is_derivable = self.initial_scripts_needed > 1;
136+
137+
debug!(
138+
"sync: last={}, remaining={}, diff={}, stop_gap={}",
139+
last, remaining, current_gap, self.stop_gap
140+
);
141+
142+
if is_derivable {
143+
if remaining > 0 {
144+
// we still have scriptPubKeys to do requests for
145+
return Ok(Request::Script(self));
146+
}
147+
148+
if last > 0 && current_gap < self.stop_gap {
149+
// current gap is not large enough to stop, but we are unable to keep checking since
150+
// we have exhausted cached scriptPubKeys, so return error
151+
let err = MissingCachedScripts {
152+
last_count: self.script_index,
153+
missing_count: self.stop_gap - current_gap,
154+
};
155+
return Err(Error::MissingCachedScripts(err));
156+
}
157+
158+
// we have exhausted cached scriptPubKeys and found no txs, continue
159+
}
160+
161+
debug!(
162+
"finished scanning for txs of keychain {:?} at index {:?}",
163+
self.keychain, last
164+
);
165+
166+
if let Some(keychain) = self.next_keychains.pop() {
167+
// we still have another keychain to request txs with
168+
let scripts_needed = self
169+
.state
170+
.db
171+
.iter_script_pubkeys(Some(keychain))?
172+
.into_iter()
173+
.collect::<VecDeque<_>>();
174+
175+
self.keychain = keychain;
176+
self.script_index = 0;
177+
self.initial_scripts_needed = scripts_needed.len();
178+
self.scripts_needed = scripts_needed;
179+
return Ok(Request::Script(self));
180+
}
181+
182+
// We have finished requesting txids, let's get the actual txs.
183+
Ok(Request::Tx(TxReq { state: self.state }))
153184
}
154185
}
155186

@@ -294,6 +325,8 @@ struct State<'a, D> {
294325
tx_missing_conftime: BTreeMap<Txid, TransactionDetails>,
295326
/// The start of the sync
296327
start_time: Instant,
328+
/// Missing number of scripts to cache per keychain
329+
missing_script_counts: HashMap<KeychainKind, usize>,
297330
}
298331

299332
impl<'a, D: BatchDatabase> State<'a, D> {
@@ -305,6 +338,7 @@ impl<'a, D: BatchDatabase> State<'a, D> {
305338
tx_needed: BTreeSet::default(),
306339
tx_missing_conftime: BTreeMap::default(),
307340
start_time: Instant::new(),
341+
missing_script_counts: HashMap::default(),
308342
}
309343
}
310344
fn into_db_update(self) -> Result<D::Batch, Error> {

src/database/sqlite.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
99
// You may not use this file except in accordance with one or both of these
1010
// licenses.
11+
use std::path::Path;
12+
use std::path::PathBuf;
1113

1214
use bitcoin::consensus::encode::{deserialize, serialize};
1315
use bitcoin::hash_types::Txid;
@@ -60,17 +62,20 @@ static MIGRATIONS: &[&str] = &[
6062
#[derive(Debug)]
6163
pub struct SqliteDatabase {
6264
/// Path on the local filesystem to store the sqlite file
63-
pub path: String,
65+
pub path: PathBuf,
6466
/// A rusqlite connection object to the sqlite database
6567
pub connection: Connection,
6668
}
6769

6870
impl SqliteDatabase {
6971
/// Instantiate a new SqliteDatabase instance by creating a connection
7072
/// to the database stored at path
71-
pub fn new(path: String) -> Self {
73+
pub fn new<T: AsRef<Path>>(path: T) -> Self {
7274
let connection = get_connection(&path).unwrap();
73-
SqliteDatabase { path, connection }
75+
SqliteDatabase {
76+
path: PathBuf::from(path.as_ref()),
77+
connection,
78+
}
7479
}
7580
fn insert_script_pubkey(
7681
&self,
@@ -908,7 +913,7 @@ impl BatchDatabase for SqliteDatabase {
908913
}
909914
}
910915

911-
pub fn get_connection(path: &str) -> Result<Connection, Error> {
916+
pub fn get_connection<T: AsRef<Path>>(path: &T) -> Result<Connection, Error> {
912917
let connection = Connection::open(path)?;
913918
migrate(&connection)?;
914919
Ok(connection)

src/descriptor/checksum.rs

Lines changed: 20 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,10 @@
1414
//! This module contains a re-implementation of the function used by Bitcoin Core to calculate the
1515
//! checksum of a descriptor
1616
17-
use std::iter::FromIterator;
18-
1917
use crate::descriptor::DescriptorError;
2018

21-
const INPUT_CHARSET: &str = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ ";
22-
const CHECKSUM_CHARSET: &str = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
19+
const INPUT_CHARSET: &[u8] = b"0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ ";
20+
const CHECKSUM_CHARSET: &[u8] = b"qpzry9x8gf2tvdw0s3jn54khce6mua7l";
2321

2422
fn poly_mod(mut c: u64, val: u64) -> u64 {
2523
let c0 = c >> 35;
@@ -43,15 +41,17 @@ fn poly_mod(mut c: u64, val: u64) -> u64 {
4341
c
4442
}
4543

46-
/// Compute the checksum of a descriptor
47-
pub fn get_checksum(desc: &str) -> Result<String, DescriptorError> {
44+
/// Computes the checksum bytes of a descriptor
45+
pub fn get_checksum_bytes(desc: &str) -> Result<[u8; 8], DescriptorError> {
4846
let mut c = 1;
4947
let mut cls = 0;
5048
let mut clscount = 0;
51-
for ch in desc.chars() {
49+
50+
for ch in desc.as_bytes() {
5251
let pos = INPUT_CHARSET
53-
.find(ch)
54-
.ok_or(DescriptorError::InvalidDescriptorCharacter(ch))? as u64;
52+
.iter()
53+
.position(|b| b == ch)
54+
.ok_or(DescriptorError::InvalidDescriptorCharacter(*ch))? as u64;
5555
c = poly_mod(c, pos & 31);
5656
cls = cls * 3 + (pos >> 5);
5757
clscount += 1;
@@ -67,17 +67,18 @@ pub fn get_checksum(desc: &str) -> Result<String, DescriptorError> {
6767
(0..8).for_each(|_| c = poly_mod(c, 0));
6868
c ^= 1;
6969

70-
let mut chars = Vec::with_capacity(8);
70+
let mut checksum = [0_u8; 8];
7171
for j in 0..8 {
72-
chars.push(
73-
CHECKSUM_CHARSET
74-
.chars()
75-
.nth(((c >> (5 * (7 - j))) & 31) as usize)
76-
.unwrap(),
77-
);
72+
checksum[j] = CHECKSUM_CHARSET[((c >> (5 * (7 - j))) & 31) as usize];
7873
}
7974

80-
Ok(String::from_iter(chars))
75+
Ok(checksum)
76+
}
77+
78+
/// Compute the checksum of a descriptor
79+
pub fn get_checksum(desc: &str) -> Result<String, DescriptorError> {
80+
// unsafe is okay here as the checksum only uses bytes in `CHECKSUM_CHARSET`
81+
get_checksum_bytes(desc).map(|b| unsafe { String::from_utf8_unchecked(b.to_vec()) })
8182
}
8283

8384
#[cfg(test)]
@@ -97,17 +98,12 @@ mod test {
9798

9899
#[test]
99100
fn test_get_checksum_invalid_character() {
100-
let sparkle_heart = vec![240, 159, 146, 150];
101-
let sparkle_heart = std::str::from_utf8(&sparkle_heart)
102-
.unwrap()
103-
.chars()
104-
.next()
105-
.unwrap();
101+
let sparkle_heart = unsafe { std::str::from_utf8_unchecked(&[240, 159, 146, 150]) };
106102
let invalid_desc = format!("wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcL{}fjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)", sparkle_heart);
107103

108104
assert!(matches!(
109105
get_checksum(&invalid_desc).err(),
110-
Some(DescriptorError::InvalidDescriptorCharacter(invalid_char)) if invalid_char == sparkle_heart
106+
Some(DescriptorError::InvalidDescriptorCharacter(invalid_char)) if invalid_char == sparkle_heart.as_bytes()[0]
111107
));
112108
}
113109
}

src/descriptor/error.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ pub enum Error {
2828
/// Error while extracting and manipulating policies
2929
Policy(crate::descriptor::policy::PolicyError),
3030

31-
/// Invalid character found in the descriptor checksum
32-
InvalidDescriptorCharacter(char),
31+
/// Invalid byte found in the descriptor checksum
32+
InvalidDescriptorCharacter(u8),
3333

3434
/// BIP32 error
3535
Bip32(bitcoin::util::bip32::Error),

src/doctest.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,6 @@
99
// You may not use this file except in accordance with one or both of these
1010
// licenses.
1111

12-
#[doc(include = "../README.md")]
12+
#[doc = include_str!("../README.md")]
1313
#[cfg(doctest)]
1414
pub struct ReadmeDoctests;

src/error.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use std::fmt;
1313

1414
use crate::bitcoin::Network;
1515
use crate::{descriptor, wallet, wallet::address_validator};
16-
use bitcoin::OutPoint;
16+
use bitcoin::{OutPoint, Txid};
1717

1818
/// Errors that can be thrown by the [`Wallet`](crate::wallet::Wallet)
1919
#[derive(Debug)]
@@ -125,6 +125,10 @@ pub enum Error {
125125
//DifferentDescriptorStructure,
126126
//Uncapable(crate::blockchain::Capability),
127127
//MissingCachedAddresses,
128+
/// [`crate::blockchain::WalletSync`] sync attempt failed due to missing scripts in cache which
129+
/// are needed to satisfy `stop_gap`.
130+
MissingCachedScripts(MissingCachedScripts),
131+
128132
#[cfg(feature = "electrum")]
129133
/// Electrum client error
130134
Electrum(electrum_client::Error),
@@ -145,6 +149,16 @@ pub enum Error {
145149
Rusqlite(rusqlite::Error),
146150
}
147151

152+
/// Represents the last failed [`crate::blockchain::WalletSync`] sync attempt in which we were short
153+
/// on cached `scriptPubKey`s.
154+
#[derive(Debug)]
155+
pub struct MissingCachedScripts {
156+
/// Number of scripts in which txs were requested during last request.
157+
pub last_count: usize,
158+
/// Minimum number of scripts to cache more of in order to satisfy `stop_gap`.
159+
pub missing_count: usize,
160+
}
161+
148162
impl fmt::Display for Error {
149163
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150164
write!(f, "{:?}", self)

src/testutils/blockchain_tests.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -743,7 +743,6 @@ macro_rules! bdk_blockchain_tests {
743743

744744
blockchain.broadcast(&tx1).expect("broadcasting first");
745745
blockchain.broadcast(&tx2).expect("broadcasting replacement");
746-
747746
receiver_wallet.sync(&blockchain, SyncOptions::default()).expect("syncing receiver");
748747
assert_eq!(receiver_wallet.get_balance().expect("balance"), 49_000, "should have received coins once and only once");
749748
}

0 commit comments

Comments
 (0)