Skip to content

Commit f71bc34

Browse files
committed
Merge #1836: Fix SQLite panic when syncing many UTXOs by enabling WAL journal mode
7c4850f release: bump version to 0.30.2 (Steve Myers) 1065511 fix(sqlite): set connection journal_mode to WAL and busy_timeout to 5000 ms (Steve Myers) 344fa3f test: repro bug with large num utxos and sqlite (Steve Myers) 957b219 deps: downgrade dev dep electrsd to 0.24.0 (Steve Myers) 3b38892 ci: fix pinned rustls and add ci/pin-msrv.sh (Steve Myers) Pull request description: ### Description fixes #1827 replaces #1828 I changed the `SqliteDatabase` struct to create new rusqlite `Connection`s with [WAL](https://www.sqlite.org/wal.html) journal mode enabled and 5000ms busy_timeout. This prevents a large sync from trying to start and commit a batch (db transaction) while the initial non-batch connection is still busy writing it's data. ### Notes to the reviewers I commented out the `electrum::test_electrum_large_num_utxos` test since it takes an hour to run. Before the fix in thie PR it would also fail with only 10 large TX. The dev dependency and pinning changes were required to run the new, and other tests. ### Changelog notice - Fix SQLite panic when syncing many UTXOs ### Checklists #### All Submissions: * [x] I've signed all my commits * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md) * [x] I ran `cargo fmt` and `cargo clippy` before committing #### Bugfixes: * [ ] This pull request breaks the existing API * [x] I've added tests to reproduce the issue which are now passing * [x] I'm linking the issue being fixed by this PR ACKs for top commit: ValuedMammal: utACK 7c4850f nymius: tACK 7c4850f evanlinjin: utACK 7c4850f Tree-SHA512: ad10b8b4354bdcbc5b9aeb1d06f68f30501dcf4ed687b387f514ff7de5e034b729dbdf25f59a8b269e562f0dab8992aa187bf187e3e0adf585838ae96afb40cc
2 parents 128a6c8 + 7c4850f commit f71bc34

File tree

7 files changed

+221
-44
lines changed

7 files changed

+221
-44
lines changed

.github/workflows/cont_integration.yml

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -57,16 +57,7 @@ jobs:
5757
components: clippy
5858
- name: Pin dependencies for MSRV
5959
if: matrix.rust.version == '1.63.0'
60-
run: |
61-
cargo update -p tokio --precise "1.38.1"
62-
cargo update -p tokio-util --precise "0.7.11"
63-
cargo update -p home --precise "0.5.5"
64-
cargo update -p regex --precise "1.7.3"
65-
cargo update -p security-framework-sys --precise "2.11.1"
66-
cargo update -p url --precise "2.5.0"
67-
cargo update -p [email protected] --precise "0.23.19"
68-
cargo update -p [email protected] --precise "0.15.0"
69-
cargo update -p ureq --precise "2.10.1"
60+
run: ./ci/pin-msrv.sh
7061
- name: Build
7162
run: cargo build --features bitcoin/std,miniscript/std,${{ matrix.features }} --no-default-features
7263
- name: Clippy
@@ -204,11 +195,6 @@ jobs:
204195
toolchain: ${{ matrix.rust.version }}
205196
- name: Pin dependencies for MSRV
206197
if: matrix.rust.version == '1.63.0'
207-
run: |
208-
cargo update -p tokio --precise "1.38.1"
209-
cargo update -p tokio-util --precise "0.7.11"
210-
cargo update -p home --precise "0.5.5"
211-
cargo update -p regex --precise "1.7.3"
212-
cargo update -p security-framework-sys --precise "2.11.1"
198+
run: ./ci/pin-msrv.sh
213199
- name: Test
214200
run: cargo test --features test-hardware-signer

CHANGELOG.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
## [Unreleased]
1111

12-
## [v0.30.1]
12+
## [v0.30.2]
1313

14-
### Fixed
14+
- Fix out of memory issue caused by batch fetching many large txs #1831
15+
- Fix SQLite panic when syncing many large txs #1836
16+
17+
## [v0.30.1]
1518

1619
- Fix electrum conftime_req filter for needs_block_height #1782
1720

@@ -707,4 +710,5 @@ final transaction is created by calling `finish` on the builder.
707710
[v0.29.0]: https://github.com/bitcoindevkit/bdk/compare/v0.28.2...v0.29.0
708711
[v0.30.0]: https://github.com/bitcoindevkit/bdk/compare/v0.29.0...v0.30.0
709712
[v0.30.1]: https://github.com/bitcoindevkit/bdk/compare/v0.30.0...v0.30.1
710-
[Unreleased]: https://github.com/bitcoindevkit/bdk/compare/v0.30.1...release/0.29
713+
[v0.30.2]: https://github.com/bitcoindevkit/bdk/compare/v0.30.1...v0.30.2
714+
[Unreleased]: https://github.com/bitcoindevkit/bdk/compare/v0.30.2...release/0.30

Cargo.toml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "bdk"
3-
version = "0.30.1"
3+
version = "0.30.2"
44
authors = ["Alekos Filini <[email protected]>", "Riccardo Casatta <[email protected]>"]
55
homepage = "https://bitcoindevkit.org"
66
repository = "https://github.com/bitcoindevkit/bdk"
@@ -94,10 +94,10 @@ reqwest-default-tls = ["esplora-client/async-https"]
9494

9595
# Debug/Test features
9696
test-blockchains = ["bitcoincore-rpc", "electrum-client"]
97-
test-electrum = ["electrum", "electrsd/electrs_0_8_10", "electrsd/bitcoind_23_1", "test-blockchains"]
98-
test-rpc = ["rpc", "electrsd/electrs_0_8_10", "electrsd/bitcoind_23_1", "test-blockchains"]
99-
test-rpc-legacy = ["rpc", "electrsd/electrs_0_8_10", "electrsd/bitcoind_23_1", "test-blockchains"]
100-
test-esplora = ["electrsd/legacy", "electrsd/esplora_a33e97e1", "electrsd/bitcoind_23_1", "test-blockchains"]
97+
test-electrum = ["electrum", "electrsd/electrs_0_8_10", "electrsd/bitcoind_23_0", "test-blockchains"]
98+
test-rpc = ["rpc", "electrsd/electrs_0_8_10", "electrsd/bitcoind_23_0", "test-blockchains"]
99+
test-rpc-legacy = ["rpc", "electrsd/electrs_0_8_10", "electrsd/bitcoind_23_0", "test-blockchains"]
100+
test-esplora = ["electrsd/legacy", "electrsd/esplora_a33e97e1", "electrsd/bitcoind_23_0", "test-blockchains"]
101101
test-md-docs = ["electrum"]
102102
test-hardware-signer = ["hardware-signer"]
103103

@@ -111,7 +111,7 @@ miniscript = { version = "10.0", features = ["std"] }
111111
bitcoin = { version = "0.30", features = ["std"] }
112112
lazy_static = "1.4"
113113
env_logger = { version = "0.7", default-features = false }
114-
electrsd = "0.29.0"
114+
electrsd = "0.24.0"
115115
assert_matches = "1.5.0"
116116

117117
[[example]]
@@ -130,7 +130,7 @@ path = "examples/policy.rs"
130130
[[example]]
131131
name = "rpcwallet"
132132
path = "examples/rpcwallet.rs"
133-
required-features = ["keys-bip39", "key-value-db", "rpc", "electrsd/bitcoind_22_1"]
133+
required-features = ["keys-bip39", "key-value-db", "rpc", "electrsd/bitcoind_22_0"]
134134

135135
[[example]]
136136
name = "psbt_signer"

README.md

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -212,16 +212,4 @@ dual licensed as above, without any additional terms or conditions.
212212

213213
This library should compile with any combination of features with Rust 1.63.0.
214214

215-
To build with the MSRV you will need to pin dependencies as follows:
216-
217-
```shell
218-
cargo update -p tokio --precise "1.38.1"
219-
cargo update -p tokio-util --precise "0.7.11"
220-
cargo update -p home --precise "0.5.5"
221-
cargo update -p regex --precise "1.7.3"
222-
cargo update -p security-framework-sys --precise "2.11.1"
223-
cargo update -p url --precise "2.5.0"
224-
cargo update -p [email protected] --precise "0.23.19"
225-
cargo update -p [email protected] --precise "0.15.0"
226-
cargo update -p ureq --precise "2.10.1"
227-
```
215+
To build with the MSRV of 1.63.0 you will need to pin dependencies by running the [`pin-msrv.sh`](./ci/pin-msrv.sh) script.

ci/pin-msrv.sh

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#!/bin/bash
2+
3+
set -x
4+
set -euo pipefail
5+
6+
# Pin dependencies for MSRV
7+
8+
# To pin deps, switch toolchain to MSRV and execute the below updates
9+
10+
# cargo clean
11+
# rustup override set 1.63.0
12+
cargo update -p tokio --precise "1.38.1"
13+
cargo update -p tokio-util --precise "0.7.11"
14+
cargo update -p home --precise "0.5.5"
15+
cargo update -p regex --precise "1.7.3"
16+
cargo update -p security-framework-sys --precise "2.11.1"
17+
cargo update -p url --precise "2.5.0"
18+
cargo update -p [email protected] --precise "0.23.19"
19+
cargo update -p [email protected] --precise "0.15.0"
20+
cargo update -p ureq --precise "2.10.1"

src/blockchain/electrum.rs

Lines changed: 177 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,8 @@ impl WalletSync for ElectrumBlockchain {
131131
let chunk_size = self.stop_gap + 1;
132132

133133
// The electrum server has been inconsistent somehow in its responses during sync. For
134-
// example, we do a batch request of transactions and the response contains less
135-
// tranascations than in the request. This should never happen but we don't want to panic.
134+
// example, we do a batch request of transactions and the response contains fewer
135+
// transactions than in the request. This should never happen, but we don't want to panic.
136136
let electrum_goof = || Error::Generic("electrum server misbehaving".to_string());
137137

138138
let batch_update = loop {
@@ -345,8 +345,6 @@ impl ConfigurableBlockchain for ElectrumBlockchain {
345345
#[cfg(test)]
346346
#[cfg(feature = "test-electrum")]
347347
mod test {
348-
use std::sync::Arc;
349-
350348
use super::*;
351349
use crate::database::MemoryDatabase;
352350
use crate::testutils::blockchain_tests::TestClient;
@@ -434,4 +432,179 @@ mod test {
434432

435433
ElectrumTester.run();
436434
}
435+
436+
#[cfg(feature = "sqlite")]
437+
#[test]
438+
#[ignore] // takes ~1 hr to complete, here as reference for future testing
439+
fn test_electrum_large_num_utxos() {
440+
use crate::database::SqliteDatabase;
441+
use crate::wallet::coin_selection::OldestFirstCoinSelection;
442+
use crate::SignOptions;
443+
use bitcoin::Amount;
444+
use bitcoincore_rpc::RpcApi;
445+
use std::time::{SystemTime, UNIX_EPOCH};
446+
447+
const NUM_TX: u32 = 50;
448+
const NUM_UTXO: u32 = 700;
449+
450+
env_logger::init();
451+
let mut test_client = TestClient::default();
452+
let electrum_blockchain =
453+
ElectrumBlockchain::from(Client::new(&test_client.electrsd.electrum_url).unwrap());
454+
455+
// fund bdk wallet 1 with regtest node coinbase txs
456+
let mem_db = MemoryDatabase::new();
457+
let wallet1_descriptor = "wpkh(tprv8i8F4EhYDMquzqiecEX8SKYMXqfmmb1Sm7deoA1Hokxzn281XgTkwsd6gL8aJevLE4aJugfVf9MKMvrcRvPawGMenqMBA3bRRfp4s1V7Eg3/0/*)";
458+
let wallet1 =
459+
Wallet::new(wallet1_descriptor, None, bitcoin::Network::Regtest, mem_db).unwrap();
460+
let wallet1_address = wallet1.get_address(AddressIndex::New).unwrap().address;
461+
test_client
462+
.send_to_address(
463+
&wallet1_address,
464+
Amount::from_btc(5.0).unwrap(),
465+
None,
466+
None,
467+
None,
468+
None,
469+
None,
470+
None,
471+
)
472+
.unwrap();
473+
test_client.generate(1, None);
474+
wallet1
475+
.sync(&electrum_blockchain, Default::default())
476+
.unwrap();
477+
assert_eq!(wallet1.get_balance().unwrap().confirmed, 5_0000_0000);
478+
// bdk wallet 1 creates NUM_TX tx * NUM_UTXO utxos and sends them back to itself
479+
for _ in 0..NUM_TX {
480+
let amount = 2715;
481+
let address_amounts = (0..NUM_UTXO)
482+
.map(|_| {
483+
(
484+
wallet1
485+
.get_address(AddressIndex::New)
486+
.unwrap()
487+
.address
488+
.script_pubkey(),
489+
amount,
490+
)
491+
})
492+
.collect::<Vec<_>>();
493+
let mut tx_builder = wallet1.build_tx().coin_selection(OldestFirstCoinSelection);
494+
// only allow spending utxos greater than 2715 sats
495+
let unspendable = wallet1
496+
.list_unspent()
497+
.unwrap()
498+
.iter()
499+
.filter(|utxo| utxo.txout.value <= amount)
500+
.map(|utxo| utxo.outpoint)
501+
.collect::<Vec<_>>();
502+
tx_builder
503+
.set_recipients(address_amounts)
504+
.unspendable(unspendable);
505+
let (mut psbt, _details) = tx_builder.finish().unwrap();
506+
assert!(wallet1.sign(&mut psbt, SignOptions::default()).unwrap());
507+
let tx = psbt.extract_tx();
508+
electrum_blockchain.broadcast(&tx).unwrap();
509+
// include test txs in a block
510+
test_client.generate(1, None);
511+
wallet1
512+
.sync(&electrum_blockchain, Default::default())
513+
.unwrap()
514+
}
515+
assert_eq!(
516+
(NUM_TX * NUM_UTXO) as usize,
517+
wallet1
518+
.list_unspent()
519+
.unwrap()
520+
.iter()
521+
.filter(|utxo| utxo.txout.value == 2715)
522+
.count()
523+
);
524+
525+
// bdk wallet 2 to receives NUM_TX tx with NUM_UTXO utxos from wallet 1
526+
let time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
527+
let mut dir = std::env::temp_dir();
528+
dir.push(format!("bdk_{}", time.as_nanos()));
529+
let sqlite_db = SqliteDatabase::new(String::from(dir.to_str().unwrap()));
530+
let wallet2_descriptor = "wpkh(tprv8i8F4EhYDMquzqiecEX8SKYMXqfmmb1Sm7deoA1Hokxzn281XgTkwsd6gL8aJevLE4aJugfVf9MKMvrcRvPawGMenqMBA3bRRfp4s1V7Eg3/1/*)";
531+
let wallet2 = Wallet::new(
532+
wallet2_descriptor,
533+
None,
534+
bitcoin::Network::Regtest,
535+
sqlite_db,
536+
)
537+
.unwrap();
538+
wallet2
539+
.sync(&electrum_blockchain, Default::default())
540+
.unwrap();
541+
assert_eq!(0, wallet2.get_balance().unwrap().confirmed);
542+
543+
// send NUM_TX tx with NUM_UTXO utxos each from wallet1 to wallet2
544+
for _ in 0..NUM_TX {
545+
let amount = 2715;
546+
let address_amounts = (0..NUM_UTXO)
547+
.map(|_| {
548+
(
549+
wallet2
550+
.get_address(AddressIndex::New)
551+
.unwrap()
552+
.address
553+
.script_pubkey(),
554+
amount,
555+
)
556+
})
557+
.collect::<Vec<_>>();
558+
let fee_utxo = wallet1
559+
.list_unspent()
560+
.unwrap()
561+
.iter()
562+
.filter(|utxo| utxo.txout.value > amount)
563+
.map(|utxo| utxo.outpoint)
564+
.last()
565+
.unwrap()
566+
.clone();
567+
let spend_utxos = wallet1
568+
.list_unspent()
569+
.unwrap()
570+
.iter()
571+
.filter(|utxo| utxo.txout.value == amount)
572+
.map(|utxo| utxo.outpoint)
573+
.take(NUM_UTXO as usize)
574+
.collect::<Vec<_>>();
575+
let mut tx_builder = wallet1.build_tx().coin_selection(OldestFirstCoinSelection);
576+
tx_builder
577+
.manually_selected_only()
578+
.set_recipients(address_amounts)
579+
.add_utxos(&spend_utxos)
580+
.unwrap()
581+
.add_utxo(fee_utxo)
582+
.unwrap();
583+
let (mut psbt, _details) = tx_builder.finish().unwrap();
584+
assert!(wallet1.sign(&mut psbt, SignOptions::default()).unwrap());
585+
let tx = psbt.extract_tx();
586+
electrum_blockchain.broadcast(&tx).unwrap();
587+
// include test txs in a block
588+
test_client.generate(1, None);
589+
wallet1
590+
.sync(&electrum_blockchain, Default::default())
591+
.unwrap()
592+
}
593+
wallet2
594+
.sync(&electrum_blockchain, Default::default())
595+
.unwrap();
596+
assert_eq!(
597+
(NUM_TX * NUM_UTXO) as usize,
598+
wallet2
599+
.list_unspent()
600+
.unwrap()
601+
.iter()
602+
.filter(|utxo| utxo.txout.value == 2715)
603+
.count()
604+
);
605+
assert_eq!(
606+
wallet2.get_balance().unwrap().confirmed,
607+
(2715 * NUM_UTXO * NUM_TX) as u64
608+
);
609+
}
437610
}

src/database/sqlite.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,13 @@ impl SqliteDatabase {
8686
/// Instantiate a new SqliteDatabase instance by creating a connection
8787
/// to the database stored at path
8888
pub fn new<T: AsRef<Path>>(path: T) -> Self {
89-
let connection = get_connection(&path).unwrap();
89+
let connection = get_connection(&path).expect("Failed to open database");
90+
connection
91+
.execute_batch("PRAGMA journal_mode = WAL")
92+
.expect("Failed to set WAL journal mode");
93+
connection
94+
.execute_batch("PRAGMA busy_timeout = 5000")
95+
.expect("Failed to set busy_timeout");
9096
SqliteDatabase {
9197
path: PathBuf::from(path.as_ref()),
9298
connection,

0 commit comments

Comments
 (0)