One of my annual projects since 2018 is to perform full validation syncs of every Bitcoin node implementation.
If you've been following along then you may have noticed that Bitcoin Core tends to get faster every year while less maintained implementations tend to get slower. This is because if you're not constantly improving performance, it will take longer to process the ever-increasing amount of data being added to the blockchain. But, I recently wondered... how slowly would Bitcoin Core sync if project contributors had not continually optimized the software?
If you check the available downloads at https://bitcoincore.org/bin/ you'll note that precompiled binaries are available dating back to version 0.8.6 (under "insecure") which was released in December 2013. You can download binaries of the v0.7.0 and earlier releases from Luke's web site, through none of them will sync to chain tip without some extra configuration for reasons that I explore in another post. As such, I've chosen to test all 15 major versions from v0.8.6 to the most recent v22 release.
In order to keep the performance comparisons fair I'm setting the following configuration parameters:
- assumevalid=0 (This forces all signatures to be verified, otherwise each release would be verifying a different number of total signatures. This config gets ignored by versions prior to v0.14 but doesn't have a huge effect because they are only skipping verifying signatures for blocks that don't contain many transactions.)
- connect=<local network peer> (only sync from a machine on my local network to remove unpredictable effects of poorly performing public peer nodes)
- dbcache=24000 (what I use on all of my syncing tests)
- disablewallet=1 (to ensure that the node doesn't attempt any wallet related data processing)
Next I wrote this script in order to parse the debug.log files generated by the syncing runs so that I could extract the elapsed time to reach each block height into a CSV from which I could build the following chart. You can view the raw data here.
To make the chart easier to view, I've truncated the results for block heights under 200,000 - those blocks are all nearly empty and thus there is negligible performance difference between versions.
From block 200,000 until the second halving (block 420,000) we see the performance slope decreasing as blocks sizes are increasing and there is more data being processed. After that, performance stabilizes because we hit the block size limit and the node is processing the same volume of data.
We then see another slope change around block 520,000 in which syncing performance improves around 10%. This is likely due to increased SegWit adoption (SegWit activated at block height 481,824.) This lines up with Bitcoin Core releasing SegWit wallet support on February 26, 2018 (block height 511,000.) We then can observe a final ~10% performance bump around block 690,000 which lines up with Blockchain.com's wallet finally adding SegWit support.
Note that performance improves for EVERY version of Bitcoin Core when SegWit adoption increases, even the ones that don't support SegWit (versions older than v0.13.0.) For versions of Bitcoin Core that support SegWit, my best guess is that it's because SegWit solved the quadratic hashing scaling problem. What's that, you may wonder? Murch answered it well on this StackExchange post:
The quadratic hashing issue appears in the verification of all pre-segwit transaction formats. It stems from the method of verifying the input scripts. For each input the transaction has, all the other inputs are stripped from the transaction to check that remaining input against the output it spends as wells as the corresponding signature. As the effort of stripping the transaction is linearly dependent on the number inputs and the stripping is repeated for each input, we do n-times work that scales linearly with n: O(n)*O(n) = O(n²), the cost grows quadratically with the number of inputs. This means that with twice the number of inputs, the computational effort for the verification quadruples.
SegWit changes the calculation of the transaction hash for signatures so that each byte of a transaction only needs to be hashed at most twice, whereas pre-SegWit transactions with many inputs could require a huge number of hashing operations.
When I asked SegWit author Pieter Wuille if he thought that was a reasonable explanation, he figured that any such gains would be negligible because only a small number of transactions have enough inputs to the point that quadratic hashing scaling becomes a noticeable performance hit. He suspects there may some other unknown performance gain at play.
Just to try to get an idea of the magnitude that large many-input transactions might have on performance, I wrote this script to find all transactions with > 50 inputs and categorize them as SegWit or Legacy. We can see that there are an average of 10 such transactions per block and SegWit spends became the dominant type around block 540,000.
Could this trend alone explain ~10% performance increases given that we're talking about a subset of transactions with attributes that make up less than 1% of all transactions? Doubtful. This could be an area of future research to dig deeper and uncover the culprit.
OK, so why do older versions of Bitcoin Core also see a performance gain post-SegWit activation? Because older versions are not downloading, processing, and validating the witness data. This is particularly remarkable if you observe the performance lines for v0.12 and v0.13 which are nearly indistinguishable until SegWit activates, at which point you can see v0.12 suddenly syncing much faster since it's performing fewer computations.
A Streak of Performance Gains
After surveying the 15 most recent releases of Bitcoin Core we can see that each release syncs faster that its predecessor with only 2 exceptions:
- v0.10 was slower than v0.9 despite v0.10 rolling out headers-first syncing. But those network performance increases won't show up in my testing because I'm syncing from a single peer node. Oddly, the discrepancy seems to be a result of v0.9 getting a greater performance gain post-SegWit activation than v0.8, v0.10, and v0.11. It's odd because all 4 of those releases should be skipping the same amount of computations.
- v0.13 was slower than v0.12 but only due to v0.12 skipping witness validation, so I don't really consider this a performance decrease. This is the same reason I run all of my syncs with "assumevalid=0" to force signature checking - otherwise the more recent releases would have an unfair advantage because they would perform far fewer validation computations.
Can the Trend Continue?
I'm pleased to report that my pessimism was misplaced back in 2019; Jeremy Rubin was right. There are plenty of performance gains to be had.
At the time I believe my perspective was that because syncing on my benchmark machine was bottlenecked by the CPU, it wouldn't matter much if additional improvements were made to network management or data structures. What I failed to appreciate was that there were plenty of optimizations to be made to ECDSA verification.
And again, in the year following the addition of GLV endomorphism we continued to see additional performance improvements to libsecp256k1 that resulted in another 5% speedup.
As of 2022, I'm now much more optimistic that engineers can continue to fight back against the ever-increasing computational resources required to sync a fully validated Bitcoin node!