Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix SQLite panic when syncing many UTXOs by enabling WAL journal mode #1836

Open
wants to merge 5 commits into
base: release/0.30
Choose a base branch
from

Conversation

notmandatory
Copy link
Member

@notmandatory notmandatory commented Feb 15, 2025

Description

fixes #1827
replaces #1828

I changed the SqliteDatabase struct to create new rusqlite Connections with WAL 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:

  • I've signed all my commits
  • I followed the contribution guidelines
  • I ran cargo fmt and cargo clippy before committing

Bugfixes:

  • This pull request breaks the existing API
  • I've added tests to reproduce the issue which are now passing
  • I'm linking the issue being fixed by this PR

@notmandatory notmandatory added the bug Something isn't working label Feb 15, 2025
@notmandatory notmandatory self-assigned this Feb 15, 2025
@notmandatory
Copy link
Member Author

I verified this builds with bdk-ffi and it passes the 50 TX test passed in ~1hr.

test blockchain::electrum::test::test_electrum_large_num_utxos ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 327 filtered out; finished in 3684.70s

@reez
Copy link

reez commented Feb 17, 2025

I used bdk-ffi tag branch v0.31.2, and then pointed to this PR branch, and built successfully.

I also build bdk-swift bindings successfully.

And then also locally imported those bdk-swift bindings into the pre-v1 branch of BDK iOS wallet, which built and ran successfully.

@nymius
Copy link
Contributor

nymius commented Feb 18, 2025

For those who are trying to reproduce the DatabaseBusy error you can apply the following diff over release/0.30:

diff --git a/Cargo.toml b/Cargo.toml
index 1e322643..525e586f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -94,10 +94,10 @@ reqwest-default-tls = ["esplora-client/async-https"]
 
 # Debug/Test features
 test-blockchains = ["bitcoincore-rpc", "electrum-client"]
-test-electrum = ["electrum", "electrsd/electrs_0_8_10", "electrsd/bitcoind_23_1", "test-blockchains"]
-test-rpc = ["rpc", "electrsd/electrs_0_8_10", "electrsd/bitcoind_23_1", "test-blockchains"]
-test-rpc-legacy = ["rpc", "electrsd/electrs_0_8_10", "electrsd/bitcoind_23_1", "test-blockchains"]
-test-esplora = ["electrsd/legacy", "electrsd/esplora_a33e97e1", "electrsd/bitcoind_23_1", "test-blockchains"]
+test-electrum = ["electrum", "electrsd/electrs_0_8_10", "electrsd/bitcoind_23_0", "test-blockchains"]
+test-rpc = ["rpc", "electrsd/electrs_0_8_10", "electrsd/bitcoind_23_0", "test-blockchains"]
+test-rpc-legacy = ["rpc", "electrsd/electrs_0_8_10", "electrsd/bitcoind_23_0", "test-blockchains"]
+test-esplora = ["electrsd/legacy", "electrsd/esplora_a33e97e1", "electrsd/bitcoind_23_0", "test-blockchains"]
 test-md-docs = ["electrum"]
 test-hardware-signer = ["hardware-signer"]
 
@@ -111,7 +111,7 @@ miniscript = { version = "10.0", features = ["std"] }
 bitcoin = { version = "0.30", features = ["std"] }
 lazy_static = "1.4"
 env_logger = { version = "0.7", default-features = false }
-electrsd = "0.29.0"
+electrsd = "0.24.0"
 assert_matches = "1.5.0"
 
 [[example]]
diff --git a/src/blockchain/electrum.rs b/src/blockchain/electrum.rs
index 5bfd51ef..12e85a67 100644
--- a/src/blockchain/electrum.rs
+++ b/src/blockchain/electrum.rs
@@ -429,4 +429,178 @@ mod test {
 
         ElectrumTester.run();
     }
+
+    #[cfg(feature = "sqlite")]
+    #[test]
+    fn test_electrum_large_num_utxos() {
+        use crate::database::SqliteDatabase;
+        use crate::wallet::coin_selection::OldestFirstCoinSelection;
+        use crate::SignOptions;
+        use bitcoin::Amount;
+        use bitcoincore_rpc::RpcApi;
+        use std::time::{SystemTime, UNIX_EPOCH};
+
+        const NUM_TX: u32 = 50;
+        const NUM_UTXO: u32 = 700;
+
+        env_logger::init();
+        let mut test_client = TestClient::default();
+        let electrum_blockchain =
+            ElectrumBlockchain::from(Client::new(&test_client.electrsd.electrum_url).unwrap());
+
+        // fund bdk wallet 1 with regtest node coinbase txs
+        let mem_db = MemoryDatabase::new();
+        let wallet1_descriptor = "wpkh(tprv8i8F4EhYDMquzqiecEX8SKYMXqfmmb1Sm7deoA1Hokxzn281XgTkwsd6gL8aJevLE4aJugfVf9MKMvrcRvPawGMenqMBA3bRRfp4s1V7Eg3/0/*)";
+        let wallet1 =
+            Wallet::new(wallet1_descriptor, None, bitcoin::Network::Regtest, mem_db).unwrap();
+        let wallet1_address = wallet1.get_address(AddressIndex::New).unwrap().address;
+        test_client
+            .send_to_address(
+                &wallet1_address,
+                Amount::from_btc(5.0).unwrap(),
+                None,
+                None,
+                None,
+                None,
+                None,
+                None,
+            )
+            .unwrap();
+        test_client.generate(1, None);
+        wallet1
+            .sync(&electrum_blockchain, Default::default())
+            .unwrap();
+        assert_eq!(wallet1.get_balance().unwrap().confirmed, 5_0000_0000);
+        // bdk wallet 1 creates NUM_TX tx * NUM_UTXO utxos and sends them back to itself
+        for _ in 0..NUM_TX {
+            let amount = 2715;
+            let address_amounts = (0..NUM_UTXO)
+                .map(|_| {
+                    (
+                        wallet1
+                            .get_address(AddressIndex::New)
+                            .unwrap()
+                            .address
+                            .script_pubkey(),
+                        amount,
+                    )
+                })
+                .collect::<Vec<_>>();
+            let mut tx_builder = wallet1.build_tx().coin_selection(OldestFirstCoinSelection);
+            // only allow spending utxos greater than 2715 sats
+            let unspendable = wallet1
+                .list_unspent()
+                .unwrap()
+                .iter()
+                .filter(|utxo| utxo.txout.value <= amount)
+                .map(|utxo| utxo.outpoint)
+                .collect::<Vec<_>>();
+            tx_builder
+                .set_recipients(address_amounts)
+                .unspendable(unspendable);
+            let (mut psbt, _details) = tx_builder.finish().unwrap();
+            assert!(wallet1.sign(&mut psbt, SignOptions::default()).unwrap());
+            let tx = psbt.extract_tx();
+            electrum_blockchain.broadcast(&tx).unwrap();
+            // include test txs in a block
+            test_client.generate(1, None);
+            wallet1
+                .sync(&electrum_blockchain, Default::default())
+                .unwrap()
+        }
+        assert_eq!(
+            (NUM_TX * NUM_UTXO) as usize,
+            wallet1
+                .list_unspent()
+                .unwrap()
+                .iter()
+                .filter(|utxo| utxo.txout.value == 2715)
+                .count()
+        );
+
+        // bdk wallet 2 to receives NUM_TX tx with NUM_UTXO utxos from wallet 1
+        let time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
+        let mut dir = std::env::temp_dir();
+        dir.push(format!("bdk_{}", time.as_nanos()));
+        let sqlite_db = SqliteDatabase::new(String::from(dir.to_str().unwrap()));
+        let wallet2_descriptor = "wpkh(tprv8i8F4EhYDMquzqiecEX8SKYMXqfmmb1Sm7deoA1Hokxzn281XgTkwsd6gL8aJevLE4aJugfVf9MKMvrcRvPawGMenqMBA3bRRfp4s1V7Eg3/1/*)";
+        let wallet2 = Wallet::new(
+            wallet2_descriptor,
+            None,
+            bitcoin::Network::Regtest,
+            sqlite_db,
+        )
+        .unwrap();
+        wallet2
+            .sync(&electrum_blockchain, Default::default())
+            .unwrap();
+        assert_eq!(0, wallet2.get_balance().unwrap().confirmed);
+
+        // send NUM_TX tx with NUM_UTXO utxos each from wallet1 to wallet2
+        for _ in 0..NUM_TX {
+            let amount = 2715;
+            let address_amounts = (0..NUM_UTXO)
+                .map(|_| {
+                    (
+                        wallet2
+                            .get_address(AddressIndex::New)
+                            .unwrap()
+                            .address
+                            .script_pubkey(),
+                        amount,
+                    )
+                })
+                .collect::<Vec<_>>();
+            let fee_utxo = wallet1
+                .list_unspent()
+                .unwrap()
+                .iter()
+                .filter(|utxo| utxo.txout.value > amount)
+                .map(|utxo| utxo.outpoint)
+                .last()
+                .unwrap()
+                .clone();
+            let spend_utxos = wallet1
+                .list_unspent()
+                .unwrap()
+                .iter()
+                .filter(|utxo| utxo.txout.value == amount)
+                .map(|utxo| utxo.outpoint)
+                .take(NUM_UTXO as usize)
+                .collect::<Vec<_>>();
+            let mut tx_builder = wallet1.build_tx().coin_selection(OldestFirstCoinSelection);
+            tx_builder
+                .manually_selected_only()
+                .set_recipients(address_amounts)
+                .add_utxos(&spend_utxos)
+                .unwrap()
+                .add_utxo(fee_utxo)
+                .unwrap();
+            let (mut psbt, _details) = tx_builder.finish().unwrap();
+            assert!(wallet1.sign(&mut psbt, SignOptions::default()).unwrap());
+            let tx = psbt.extract_tx();
+            electrum_blockchain.broadcast(&tx).unwrap();
+            // include test txs in a block
+            test_client.generate(1, None);
+            wallet1
+                .sync(&electrum_blockchain, Default::default())
+                .unwrap()
+        }
+        wallet2
+            .sync(&electrum_blockchain, Default::default())
+            .unwrap();
+        assert_eq!(
+            (NUM_TX * NUM_UTXO) as usize,
+            wallet2
+                .list_unspent()
+                .unwrap()
+                .iter()
+                .filter(|utxo| utxo.txout.value == 2715)
+                .count()
+        );
+        assert_eq!(
+            wallet2.get_balance().unwrap().confirmed,
+            (2715 * NUM_UTXO * NUM_TX) as u64
+        );
+    }
 }

and run the test with the following command:

cargo test --features test-electrum,sqlite --no-default-features -- test_electrum_large_num_utxos

…000 ms

This prevents: Error { code: DatabaseBusy, extended_code: 5 }, Some("database is locked")
    which occured when syncing very large number of utxos.
    See: electrum::test::test_electrum_large_num_utxos
@notmandatory
Copy link
Member Author

I've rebased this PR and it's ready to review/merge.

Copy link
Contributor

@ValuedMammal ValuedMammal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

utACK 7c4850f

Copy link
Contributor

@nymius nymius left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tACK 7c4850f

I executed the test with the failing tip and took me ~2hs. Did the same on 7c4850f and got the following output:

test blockchain::electrum::test::test_electrum_large_num_utxos has been running for over 60 seconds
test blockchain::electrum::test::test_electrum_large_num_utxos ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 327 filtered out; finished in 8520.98s

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
Status: Ready to Review
Development

Successfully merging this pull request may close these issues.

4 participants