Skip to content

feat(electrum): optimize merkle proof validation with batching #1957

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

Open
wants to merge 5 commits into
base: master
Choose a base branch
from

Conversation

LagginTimes
Copy link
Contributor

@LagginTimes LagginTimes commented May 15, 2025

Replaces #1908, originally authored by @Keerthi421.
Fixes #1891.

Description

This PR optimizes sync/full_scan performance by batching and caching key RPC calls to slash network round-trips and eliminate redundant work.

Key improvements:

  • Gather all blockchain.transaction.get_merkle calls into a single batch_call request.
  • Use batch_script_get_history instead of many individual script_get_history calls.
  • Use batch_block_header to fetch all needed block headers in one call rather than repeatedly calling block_header.
  • Introduce a cache of transaction anchors to skip re-validating already confirmed transactions.

Anchor Caching Performance Improvements

Results suggest a significant speed up with a warmed up cache. Tested on local Electrum server with:

$ cargo bench -p bdk_electrum --bench test_sync

Results before this PR (https://github.com/LagginTimes/bdk/tree/1957-master-branch):

sync_with_electrum      time:   [1.3702 s 1.3732 s 1.3852 s]

Results after this PR:

sync_with_electrum      time:   [175.58 ms 176.92 ms 182.28 ms]

Batch Call Performance Improvements

No persisted data was carried over between runs, so each test started with cold caches and measured only raw batching performance. Tested withexample_electrum out of https://github.com/LagginTimes/bdk/tree/example_electrum_timing with the following parameters:

$ example_electrum init "tr([62f3f3af/86'/1'/0']tpubDD4Kse29e47rSP5paSuNPhWnGMcdEDAuiG42LEd5yaRDN2CFApWiLTAzxQSLS7MpvxrpxvRJBVcjhVPRk7gec4iWfwvLrEhns1LA4h7i3c2/0/*)#cn4sudyq"
$ example_electrum scan tcp://signet-electrumx.wakiyamap.dev:50001

Results before this PR:

FULL_SCAN TIME: 8.145874476s

Results after this PR (using this PR's bdk_electrum_client.rs):

FULL_SCAN TIME: 2.594050112s

Changelog notice

  • Add transaction anchor cache to prevent redundant network calls.
  • Batch Merkle proof, script history, and header requests.

Checklists

All Submissions:

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

New Features:

  • I've added tests for the new feature
  • I've added docs for the new feature

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

@LagginTimes LagginTimes requested a review from evanlinjin May 15, 2025 19:06
@LagginTimes LagginTimes self-assigned this May 15, 2025
Copy link
Member

@evanlinjin evanlinjin left a comment

Choose a reason for hiding this comment

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

Thanks for moving this forward.

This is not a full review, but I think it's enough to push this PR in a good direction.

Comment on lines 318 to 322
// Batch validate all collected transactions.
if !txs_to_validate.is_empty() {
let proofs = self.batch_fetch_merkle_proofs(&txs_to_validate)?;
self.batch_validate_merkle_proofs(tx_update, proofs)?;
}
Copy link
Member

Choose a reason for hiding this comment

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

Instead of having every populate_with_{} method call this internally, it will be more efficient and make more logical sense if we extract this so that we only call it at the end of full_scan and sync.

In other words, populate_with_{} should no longer fetch anchors. Instead, they should either mutate, or return a list of (Txid, BlockId) for which we try to fetch anchors for in a separate step.

It will be even better if full txs are fetched in a separate step too.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Partially resolved. This is the next TODO:

It will be even better if full txs are fetched in a separate step too.

Copy link
Contributor Author

@LagginTimes LagginTimes May 25, 2025

Choose a reason for hiding this comment

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

This will likely be included in a separate PR.
Fetching all full txs in a batch call at the beginning of sync actually ended up doubling sync time.

Copy link
Member

Choose a reason for hiding this comment

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

@LagginTimes did you figure out why though?

@LagginTimes LagginTimes marked this pull request as draft May 20, 2025 18:06
@LagginTimes LagginTimes force-pushed the merkle_batching branch 2 times, most recently from d69907b to 149807c Compare May 21, 2025 18:42
@notmandatory notmandatory moved this to In Progress in BDK Chain May 23, 2025
@LagginTimes LagginTimes force-pushed the merkle_batching branch 2 times, most recently from dc08959 to bf38a8e Compare May 25, 2025 18:30
@LagginTimes LagginTimes marked this pull request as ready for review May 25, 2025 18:48
@evanlinjin
Copy link
Member

@LagginTimes could you provide the benchmark results in the PR description and compare it to results before the changes in this PR?

@evanlinjin evanlinjin added this to the Wallet 2.0.0 milestone May 26, 2025
@notmandatory
Copy link
Member

Based on above benchmark results it looks like this change is 1s faster on sync, is that due to a small test size? Do we expect it to make more of a difference with wallets with many addresses?

@LagginTimes LagginTimes marked this pull request as draft May 26, 2025 23:18
@evanlinjin
Copy link
Member

evanlinjin commented Jun 4, 2025

@LagginTimes can you provide the code you used to test with a remote electrum server (instead of the testenv)?

Edit: how about we just test with the example-cli with a pre-populated signet wallet?

I suggested writing benchmarks with the assumption that local io (against testenv) would be slower than allocating memory (collecting requests into vec before batch requesting), however that assumption seems incorrect.

@LagginTimes LagginTimes force-pushed the merkle_batching branch 2 times, most recently from 90a0018 to 591b51a Compare June 8, 2025 17:32
@LagginTimes LagginTimes marked this pull request as ready for review June 10, 2025 09:30
@LagginTimes LagginTimes force-pushed the merkle_batching branch 3 times, most recently from 99fdd57 to e4191f9 Compare June 14, 2025 18:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: In Progress
Development

Successfully merging this pull request may close these issues.

Electrum client Performance issues
5 participants