feat(electrum): optimize merkle proof validation with batching#1957
feat(electrum): optimize merkle proof validation with batching#1957evanlinjin merged 6 commits intobitcoindevkit:masterfrom
Conversation
evanlinjin
left a comment
There was a problem hiding this comment.
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.
| // 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)?; | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Partially resolved. This is the next TODO:
It will be even better if full txs are fetched in a separate step too.
There was a problem hiding this comment.
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.
d69907b to
149807c
Compare
dc08959 to
bf38a8e
Compare
|
@LagginTimes could you provide the benchmark results in the PR description and compare it to results before the changes in this PR? |
|
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? |
de14241 to
bb26525
Compare
|
@LagginTimes 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. |
90a0018 to
591b51a
Compare
591b51a to
70495e2
Compare
|
These are my criterion results after benching this PR. 👍 sync_with_electrum time: [37.325 ms 38.220 ms 38.805 ms]
change: [-85.756% -85.556% -85.308%] (p = 0.00 < 0.05)
Performance has improved. |
|
In 838c247: error: this file contains an unclosed delimiter
--> crates/electrum/src/bdk_electrum_client.rs:724:3
|
32 | impl<E: ElectrumApi> BdkElectrumClient<E> {
| - unclosed delimiter
...
504 | for &(txid, height, hash) in chunk {
| - this delimiter might not be properly closed...
...
547 | }
| - ...as it matches this but it has different indentation
...
724 | } |
f12f2b2 to
8086e1e
Compare
evanlinjin
left a comment
There was a problem hiding this comment.
Here are some suggestions to improve the benchmark.
I've implemented them in this commit: evanlinjin@1ba324c
I also added a benchmark for testing sync without cache. Feel free to cherry-pick this commit if you agree with the changes.
8086e1e to
a94805a
Compare
* Actually use different spks * Do not benchmark applying updates (only fetching/contructing) * Have two benches: One with cache, one without. * Remove `black_box`.
a94805a to
156cbab
Compare
There was a problem hiding this comment.
tACK 156cbab
It looks good to me, I did run the example with the same descriptor, but a different server (and no TLS), here are the results:
master @ `63923c63dc5dbd7850ae8fa4f4d1b832170fe957`
FULL_SCAN TIME: 26.977265542s
SYNC TIME: 23.786322458s
merkle_batching @ `156cbab67f4ff91276f9f03749944f4c46210f7f`
FULL_SCAN TIME: 3.85442175s
SYNC TIME: 4.124651292s
| let histories = self | ||
| .inner | ||
| .batch_script_get_history(unique_spks.iter().map(|spk| spk.as_script()))?; |
There was a problem hiding this comment.
question to self: does this batch call guarantees order ?
There was a problem hiding this comment.
If it doesn't, then it is a major bug in bdk_client since the API implies that the response order correlates with the requests.
| // Returned heights 0 & -1 are reserved for unconfirmed txs. | ||
| Ok(height) if height > 0 => { | ||
| self.validate_merkle_for_anchor(tx_update, txid, height)?; | ||
| pending_anchors.push((tx.0, height)); |
There was a problem hiding this comment.
nit: use res.tx_hash for consistency with the branch below.
evanlinjin
left a comment
There was a problem hiding this comment.
ACK 156cbab
Styling tips for readability:
- I find that it is more readable to initialize a variable with it's type rather than relying on it being implied later on. I.e. prefer
let mut result = Vec::<(Txid, ConfirmationBlockTime)>::new()overlet mut result = Vec::new(). The latter requires the reader to use an LSP or scan head first to figure out the type. - Avoid overly-nested logic. Sometimes it is better to use
continueinstead of nesting the logic in anifclause. debug_assert!s help us to catch bugs. Don't be afraid to add them wherepanic/expectwould not be appropriate.
Replaces #1908, originally authored by @Keerthi421.
Fixes #1891.
Description
This PR optimizes
sync/full_scanperformance by batching and caching key RPC calls to slash network round-trips and eliminate redundant work.Key improvements:
blockchain.transaction.get_merklecalls into a singlebatch_callrequest.batch_script_get_historyinstead of many individualscript_get_historycalls.batch_block_headerto fetch all needed block headers in one call rather than repeatedly callingblock_header.Anchor Caching Performance Improvements
Results suggest a significant speed up with a warmed up cache. Tested on local Electrum server with:
Results before this PR (https://github.com/LagginTimes/bdk/tree/1957-master-branch):
Results after this PR:
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 with
example_electrumout of https://github.com/LagginTimes/bdk/tree/example_electrum_timing with the following parameters:Results before this PR:
Results after this PR (using this PR's
bdk_electrum_client.rs):Changelog notice
Checklists
All Submissions:
cargo fmtandcargo clippybefore committingNew Features:
Bugfixes: