Bitcoin full node and query server

August 2026

Archive of the libbitcoin Slack #general channel.

Saturday, August 1, 2026

evoskuil15:46
Been working through optimizations on low RAM memory map under full validation (read pressure). I fixed bigmac RAM at 32GB and called it bigmac32. That is now completing in the same timeframe as Linux and Windows, and both of those are also optimized down to 8.6 hrs on the the minis (neidermeyer and stork). Milestones are all consistent across the board at high and low RAM (write pressure). Basically I've tightened the custom memory map implementation so it's the same dynamic behavior as Windows, while dealing with the macOS and Linux idiosyncrasies.
eynhaender15:58
Excellent
evoskuil16:29
Windows slightly outperforms macOS and Linux because its implementation is native. Linux can slightly outperform Windows on its native mmap, but that only happens when operating-system wide paging config is manually configured to the scenario. When that is off, even as much as 5% in one case, Linux collapses entirely (grinds to a halt). The behavior must adjust dynamically based on feedback in order to be close to optimal. macOS doesn't even allow manual OS-wide config, and while Linux does it is not sufficient (and not realistic for deployment). There is a slight cost of dynamic adjustment but it's well worth the consistently high performance, lack of pathological collapse, and lack of OS level config.
(edited)
👍 1
evoskuil23:14
It's pretty sweet to have this test bench running and fully orchestrated. Even have automatic RAM resizing. I'm able to use a snapshot store on each machine, based at 750k and run to 850k (two default windows) to get a relatively quick and fully-instrumented performance evaluation on each platform, and just reset the store back to 750k. I can just keep this in a loop grinding away at independent mmap performance improvements across platforms. Current projections have all 3 platforms hitting full validation on 32GB RAM and 2.5 Gbps Internet in about 8.5 hrs. I can run two concurrently because of my independent connections.
(edited)
🔥 2

Sunday, August 2, 2026

evoskuil03:45
image.png
bigmac milestone sync. archive scale is dwarfed by validation (no-op) and confirmation (tx state changes). Cool to see it this way.
Hit the 750k stop block, validated/confirmed disabled.
image.png
evoskuil04:07
Chrome split screen, bigmac milestone from 750k after a restart (test bench).
(edited)
image.png
evoskuil18:17
bigmac time to 950k blocks on 2.5Gbps network as a function of physical RAM.
image.png
Milestone is ~network limited to 24GB. Above 24GB validation improves smoothly with RAM up to full chain in RAM (above 768GB) at which point it becomes fully CPU limited - flat (assuming sufficient network).
(edited)
evoskuil18:32
24GB is the inflection point because of configured hashmap load factor for the 950k chain size (~2.5). This results in table heads totaling 13.6GB. As these must stay memory resident, that leaves 10.4GB for all other processes and table bodies. As this gets squeezed paging increases and time goes parabolic as heads start getting pushed out of RAM. This is a hard point in the store architecture. LF can be changed via config (only at store create), resulting in smaller heads. Halving the buckets doubles the load factor and therefore the average search cost doubles. So the heads can remain in RAM at 6.8GB while the node pays a more proportional cost in body paging. This of course also has a limit, but as we have shown on the RPi5, it works well at 16GB.
💪 1
I'll produce more measures against LF and the other platforms.

Monday, August 3, 2026

evoskuil04:45
I've implemented the DoS protection throttle. This operates in the network proxy (the socket encapsulator and base class for all channels). The real work was done in every piece of network code over the past 5 years. Protection operates on backpressure, a design principle I picked up from the late great Pieter Hintjens. So really there's nothing to do except to set a rate limit, so a timer, a handler, and a config setting. The config setting was already in there, and now the rest is done. I will probably also drop in a per service override config setting. There is no limit whatsoever on how much can be sent to the node/server. The limit is on what we send back. Queries are small, responses are large and costly - and generally the larger the more costly (though not always, so there's some room for tuning this). The interesting part about the handling is that there is no moving window, accumulation or averaging. There's just a rate at which we reply (or notify), in bytes per time. The send is emitted (at network speed of course), and then the handler invocation is delayed (asynchronously) until the time allocated for that number of bytes is reached. So if the configured rate is faster than network speed, there's no limit. Otherwise the channel is reduced to the configured speed. The total cost is just the timer invocation and its handler indirection, which are trivia in terms of network perf.
Already implemented generally, any protocol violation is a channel drop. All channels have configurable (on a per service basis) lifetime limits and inactivity limits. So DoS protection is simply limiting how much resource consumption is allowed per channel per second.
evoskuil05:04
$350 Windows mini mogging a $20,000 Mac Pro at full validation below 32GB RAM (same network).
(edited)
image.png
evoskuil05:49
Worked all day on getting the Linux mmap up to at least macOS snuff. It looks like that's finally turned the corner. The pivotal issue has been that there is no way to mark a region as retention priority, all you get is global OS config. macOS provide an madvise flag for this, and Windows manages it automatically by observation of access patterns. And hashmap heads need priority retention. The issue has been that they can be accessed much less than bodies, resulting in them getting moved to swap, which is a disaster. So in the absence of a flag I figured we can just sweep each page with a byte read. No need for any guards, the value is tossed (and the call marked volatile so that it's not optimized out) and doesn't affect other reads/writes. The cost is low and it keeps the heads more current than the bodies, so cold body pages are released first. That resulted in zero swap (vs. 100% / 8GB). Not done yet, but looking hopeful. This condition arises during validation, milestone had previously been sufficiently optimized.
evoskuil19:07
image.png
evoskuil19:19
This shows Windows (stork) and macOS (bigmac) milestone and full validation projects based on a per-tx rate between 750-850k blocks. Due to it's dynamic native memory management stork consistently beats bigmac in the low RAM regime despite significantly lower CPU power. stork is network limited (milestone) at 24GB+ and bigmac at 32GB+. Validation doesn't become truly CPU limited until there is full RAM, thought the RAM effects become very small at high RAM. There is no stork above 32GB for comparison. Windows is able to validate down to 18GB platforms and macOS down to 24GB platforms. That's the legacy of Unix overcommit killing the process. Windows continues to validate at any RAM level, but the time cost goes parabolic below 18GB. These observations of viability hold regardless of CPU power. 32GB is a good RAM target for macOS and 32GB for Windows, as RAM effects are modest and smooth above those points, but 24GB is viable for macOS and 18GB for Windows.
evoskuil19:45
It's important to keep in mind that in the above plot macOS has significantly more CPU power, and all of the runs are block 750-850k projections (to save a lot of time). The values are projected through tx count normalization to a 950k run time. This tends to overestimate the milestone times by as much as 40%. They are all under 1hr in actuality. Should have some Linux numbers soon.
evoskuil21:19
image.png
There may be more we can do to bend the macOS curve down toward Windows, but this is a HUGE win as it is. Without this work macOS was entirely unusable, and Linux required OS wide config to work at all, and still fell down on validation.
evoskuil23:24
Look who's back on top! The custom (configuration free) Linux memory map is super clean even under this massive pressure at 24GB full validation!
image.png
🔥 2

Tuesday, August 4, 2026

evoskuil01:28
I just realized that we've given neidermeyer (Linux) quite an unfair advantage by making it headless. Good work @eynhaender! Though stork (Windows) does have a faster processor.
(edited)
evoskuil02:33
Custom linux mmap crushing it.
image.png
evoskuil05:45
Added per service rate limiting config, which combines with the network level config of the same name, with the most restrictive controlling. This is the DoS protection knob (connection rate limiting on outbound/query traffic/cost).
evoskuil06:13
I've updated secp256k1 to the brand new v0.8.0.0 release, which reportedly speeds sig verify by ~11%. NuGet packaged published for vc145, projects all bumped, verified in CI in my repo. Waiting on performance runs before merge.
👌🏻 1
Fielded this patch from @Bruno Garcia, added tests for it. Waiting on a full validation run (from the above NuGet verification) before merging.
Should be completing the above plot soon as well, just waiting on Linux runs and the above performance check on secp.
evoskuil14:05
The secp256k1 update came thru on its advertised ~11% sig verify improvements. Confirmed on both stork (Windows) and neidermeyer 32GB full validation runs. I ran stork all the way from genesis to 950k just to make sure.
image.png
evoskuil14:33
Linux mmap optimizations have been merged. One should no longer use the previously-required Linux OS paging config (could actually be negative). macOS is now competitive, and Windows remains so. secpp256k1 is updated 0.8.0.0, with published NuGet package. Network DoS protection is in, including global and per-service rate-limiting.
evoskuil14:42
Note that Linux still requires the MANAGE_STAGING preprocessor define to get the mmap optimizations but that will be removed in the next database PR.
evoskuil14:48
Also looks like one commit didn't make the last PR, so the Linux milestone behavior is off (slow) until the next one clears.
evoskuil16:40
Linux milestone runs filled in.
image.png
I'm bending that line down to the left. If it's possible on one platform, it's possible on the others, because we now are in direct control of the mappings.
🎉 2
evoskuil19:12
Ok, all of the latest goodness above is merged, though there's a perf regression on the mac, so I'm playing whack-a-mole with cross-platform code.
evoskuil20:55
16GB full validation (Linux)
(edited)
image.png
That's with 13.6 GB of table heads, and a (headless) operating system, almost no working space. The 18 and 24GB points are prior to the last optimization, so they will bend down and it will be more of a curve.
(edited)
evoskuil23:47
20 GB neidermeyer (Linux) full validation bent down to 9.31 hrs.
image.png
Pretty sure macOS and Windows are going to come down to similar profiles. The specs are different and the free RAM (Linux is headless) is different, but it could still be close. Above 24GB everything tracks together for all scenarios. Just trying to get that down to 16GB. Keep in mind that a week ago full validation wasn't really even reasonable at or below 32GB on any platform, macOS wasn't viable even for milestone with 768GB RAM, and Linux was only viable with OS level paging configuration.
(edited)
🚀 3

Thursday, August 6, 2026

evoskuil01:43
This is the next huge win... doubling the load factor for small RAM machines (16GB) cuts the table heads down to 6GB and completely flattens out the validation time. I would expect this to hold just as well for milestone. This is how we did it previously on the RPi5, with only 4 threads on 1Gbps in 3.0 hrs. I held off bumping up the LF until I had squeezed everything I could get out of the mmap optimizations.
image.png
evoskuil01:54
This is still a projection, passing 750k now, and I expect it to bend up due to the increasing validation search cost, as a consequence of increased hashmap saturation, due to the doubled load factor. So it should hold up very well for milestone, bringing that lower plot under control.
(edited)
evoskuil14:16
neidermeyer (Linux) finished the 16GB full validation run to 950k @ with load factor 5.0 in 8.52 hrs. That's down from 11.5 hrs at LF 2.5.
evoskuil15:12
stork (Windows) finished the 16GB full validation run to 950k @ with load factor 3.75 in 11.3 hrs. That's down from [took forever and was stopped] at LF 2.5 - which is awesome. But I'll run this one again at LF 5.0 for a straight comparison with mac/linux.
evoskuil15:33
bigmac (macOS) [with another recent mmap patch] is running the 16GB full validation with LF 5.0 and is running well behind the others- - projecting ~ 15 hrs, but is running on 16GB! On earlier runs macOS could not complete even the 16GB milestone run (projected over 28 hrs) and before the custom mmap it couldn't even complete a 768 GB milestone run.
I still have to re-run all of the low RAM milestones, but given the full validation results I expect those to flatten pretty reasonably all the way to 16GB (or lower, but not in the current matrix).
Huddle with pmienk, eynhaender, evoskuil, Eric Chennells, josie, Enligths · 233 min15:54
4 replies · evoskuil, josie · last Aug 06, 2026
evoskuil16:41
image.png
evoskuil16:54
try audio only?
josie16:54
haha audio only wont help with the promise i made to the lady that id be done at 7PM 😉
👍🏻 1
evoskuil16:55
but you could get the punch line
evoskuil22:05
bigmac was grinding through its full validation steadily until it was jetsam-killed at around 850k. I've projected a completion along its curve because it wasn't parabolic - it was steady. Notice that the leftmost solid circle shows the point after which the LF2.5 runs went parabolic with latest optimizations. The open circles show the same code running at an increased LF. For Windows the LF3.75 was better than LF5.0, since the header residency problem is less pronounced for it and the higher LF increases body paging. So you can see that we have a pretty clear pattern. The different mmap implementations and underlying support handle memory pressure differently. In full validation by 32GB this is mostly phased out, and similarly for milestone by 24GB. Yet we are avoiding thrashing down to 16GB at least, with the exception of macOS, which got close. I suspect the duplicate probing starts to hurt in this regime, which I'll verify at some point. The LF impact implies that we should tune the heads given a default store creation, and vary that by platform. There is damage on high RAM platforms in Linux, as a consequence of aggressively shedding body pages. I've disabled that above 32GB but I don't have a platform to verify it. The next round of tests will hit milestone with the latest optimizations and LF variations. It's possible and I've seen a couple of times, that optimization for one scenario hurts another, but at this point I believe it's optimal across validation and milestone. I expect that, as with the validation runs, the LF increase will significantly bend down the low end of the milestone curves, as the heads are only required for reads, and the increased LF hurts reads, but we don't read much in the big heads for milestone. So it's more like free RAM at no cost. Ideally those would entirely flatten to network limited all the way to 16GB.
image.png
evoskuil23:30
>>> Linux is 2.5Gbps network limited at 16GB! <<< Completed to 950k in 59 mins.
🔥 2

Friday, August 7, 2026

evoskuil00:42
>>> Windows is 2.5Gbps ~network limited at 16GB! <<< Completed to 950k in 67 mins.
macOS mmap is broken somewhere, which may have been throwing off the full validation numbers as well. Working on it.
evoskuil00:50
A little piece of debug trivia that I find very interesting, given that we build release with full noexcept enabled...
A race that corrupts state kills the process with SIGSEGV/SIGBUS on a wild access. noexcept is invisible to that: it governs C++ exception propagation, not signal delivery. The .ips report and any core capture the faulting instruction and all thread stacks regardless.

Here noexcept changes the shape of the dump, in our favor: the throw hits the noexcept boundary and std::terminate fires at the throw site, without unwinding — SIGABRT with the origin frames still on the stack (abort ← terminate ← __cxa_throw ← the actual thrower). In a debug build with NOEXCEPT off, the exception would unwind — and if anything up-stack caught it, the origin would vanish entirely.

So for localization, release-with-noexcept is arguably the better trap.
Paper I mentioned in the team meeting today.
PDFcomputational-floor.pdfPDF · 105.2 KB
🔥 1
evoskuil01:20
As predicted, the load factor increase absolutely crushes the milestone runs back down to network limited at 16GB. I'm now running 8GB tests.
(edited)
image.png
❤️ 1
evoskuil01:29
macOS is basically identical to Windows at milestone through 700k, so when I work out what's going on with it hopefully it will also be network limited at 16GB. There is definitely a correctness issue in its mmap code right now though.
evoskuil02:34
Despite a correctness issue (likely race) in posix mmap, macOS completed its 16GB milestone run in 1.61 hrs. The 8GB @ LF10 runs hit a wall, so I backed that off to 12GB @ LF 7.25 to find the inflection point. But it's safe to say at this point that all OSs are pretty close to network limited starting from 16GB, validation is reasonable on all three down to 16GB, and excellent from 24GB.
image.png
evoskuil03:14
Windows completed 12GB milestone at LF7.25 in 1.02 hrs completely flat, actually beat it's 16GB time.
evoskuil06:30
Above posix (not just macOS) mmap race patched, but still not fixed. macOS and Linux 12GB/7.25LF milestone runs up next. When those clear I've staged a reset back to 32GB/2.5LF for a three platforms to get a milestone refresh with latest code. At that point I'll set this aside for a bit and finish up the open preview items.
evoskuil06:46
Linux completed 12GB milestone at LF7.25 in 1.05 hrs completely flat, almost beat it's 16GB time.
🚀 1
evoskuil15:49
Two outstanding issues. (1) There is a mmap regression in the macOS mmap (likely race) that leads to a SIGBUS. (2) There is another POSIX-related limit arising on Linux (only) from the POSIX workarounds, which I am working around. In the mean time you may experience a handled (logged, node stop, corrupt store) mmap allocation error on low VM (RAM + swap).

Saturday, August 8, 2026

evoskuil02:02
image.png
👍 2🙌 3
evoskuil14:46
By updating the store's approaching disk full, using reservation backoff to a minimum headspace value, we can get to 950k on 1TB boxes. Completed the first Linux full validation on neidermeyer at 16GB @ LF5.0, right up to 950k in 9.51 hrs.
(edited)
evoskuil23:27
The recursive variadic interface templates finally hit their compile limit. I'm breaking up the big bitcoind interface into sub-interfaces to scale back the large numbers of and sizes of the recompiles. Should speed up the server build. Can't get past CI at this point, heap starvation.
😮 1
1 reply · evoskuil · last Aug 09, 2026
evoskuil03:38
Resolved by breaking up the interfaces into smaller dispatchable sets. better organization for the bitcoind interface anyway, since it's huge.

Sunday, August 9, 2026

evoskuil02:44
Built a new svg favicon for libbitcoin.info, so just added it to the build as a shared resource for both embedded sites, same byte size as the old one.
(edited)
image.png
🎉 2
Organized the bitcoind interface, added a lot of methods, implemented a few more: libbitcoin.info/#…
evoskuil03:08
macOS completed 850k milestone on 12GB in 70 mins. Inadvertently hit a disk full, and then recovered and continued when I cleared off the earlier runs. But stopped it to do some other testing. Mac has come a long way, and significantly, this run finally corrected our accounting issues on Darwin, and it's looking good on all platforms right now.
evoskuil04:13
Windows just completed 950k block pruned milestone sync on an 8GB mini at 2.5Gbps in 41.4 mins!
(edited)
evoskuil05:39
image.png
Check out that lower left corner.
evoskuil06:37
Linux just completed a 40.6 min milestone to block 950k on 8GB RAM. Slightly lesser CPU but more free RAM (due to being headless).
🚀 1
macOS is lagging, but running. It has the least amount of free RAM.
evoskuil06:53
X (formerly Twitter)
Optimizing and tuning Libbitcoin Server on low RAM. Today we reached ***41 minutes*** on an 8GB mini for both Windows and Linux, with macOS not too far behind. Full validation at 16 GB in 11.5 hrs. And Linux no longer requires OS configuration for maximum speed.
❤️ 2🎉 1
evoskuil07:22
Current work is merged, no known problems.
🔥 1
evoskuil17:17
Updated with last mac numbers and complete legend.
image.png
❤️ 2👍 1
3 replies · neillm, evoskuil · last Aug 09, 2026
neillm20:19
Libbitcoin the shitcoin 😅
neillm20:20
So many actually
Screenshot(55).png
evoskuil20:21
😂
evoskuil19:53
JFC

Monday, August 10, 2026

evoskuil06:51
Added a bunch more bitcoind methods today. Tracker is here: libbitcoin.info
libbitcoin
A Bitcoin full node and query server. One node that speaks the protocols your software already uses.
💯 1
evoskuil07:15
I also implemented bip324 (p2p privacy). This dropped into the network socket as expected. Not merged yet.
🔥 2
4 replies · Bruno Garcia, evoskuil · last Aug 15, 2026
Bruno Garcia16:04
Nice, is there an open PR for this?
evoskuil16:06
evoskuil16:07
p2p integration is pending

Thursday, August 13, 2026

Huddle with pmienk · 34 min15:55
evoskuil19:44
X (formerly Twitter)
Libbitcoin is becoming the reference implementation, for many good reasons.
❤️ 2

Saturday, August 15, 2026

evoskuil13:11
I just realized that I had never implemented the input script drop during pruned milestone, only the witness. This is a very significant difference in both size and time cost - it cuts the total store almost in half.
👍 1
evoskuil23:56
Updated with the full pruning. Given that the download is the same as the previous pruning, the times don't change too much (network limited), but macOS 10GB pruned dropped a lot, and was able to drop/normalize the pruned Linux to LF7.5. macOS 55 mins at 10GB, others 40 mins at 8GB, with pruned store size of 441GB at 950k blocks. This increases to 450GB at default LF2.5.
(edited)
image.png

Sunday, August 16, 2026

evoskuil06:12
I've reduced the address index from ~156GB including table head to just the ~6GB table head. That's still big for a table head, and can be reduced (e.g. ~3GB at 2x LF), but would still keep it optional, though maybe on by default. This will also reduce the additional sync time for the address index to almost nothing. The cost is increased search cost, which I'm still investigating. But the search is so fast that the storage space is generally a much bigger issue, and may itself offset the search cost in reduced paging.
I've also done some perf analysis on the lower RAM configurations, showing that initial table sizing isn't very impactful on any platform - which will simplify configuration.
evoskuil06:33
Also finally moved the p2p channel into a proper request/response "body" type, so that it can be processed in the socket like all other network protocols. It will now be easy to drop it into http or websockets for example. But more importantly it generalized the implementation, making bip324 (opportunistic encryption) a clean integration. The p2p protocol is now p2p/s with a proper socket variant. I also cleaned up the confusing services configuration settings and integration, which is where p2p privacy is enabled. Still have to do some acceptance testing, but otherwise this is complete.
[peer]
enable_privacy = true   # new, opportunistic encryption (bip324)
invalid_services = 176  # shitcoins (existing config, retained)
#services_minimum = 42  # obsolete/invalid
#services_maximum = 42  # obsolete/invalid

[node]
# these are new (replace services_minimum/maximum)
limited_blocks  = false
provide_blocks  = true
require_blocks  = true
provide_witness = true
require_witness = true
provide_filters = false
(edited)
evoskuil06:48
I've also fixed the crash-dump utility that's integrated into the windows server build, so get something like this reliably now:
2026-08-16T06:46:29Z.5 Insufficient services (3080) by [144.91.83.152:8333] showing (67112013) /Satoshi:31.0.0/
<<unhandled exception - start trace>>
exception 0xc0000005 on thread 21128
libbitcoin::network::socket::do_handshake -> C:\Source\evoskuil\libbitcoin-network\src\net\socket_connect.cpp(218)
libbitcoin::network::socket::do_handshake -> C:\Source\evoskuil\libbitcoin-network\src\net\socket_connect.cpp(218)
boost::asio::detail::range_connect_op<boost::asio::ip::tcp,boost::asio::any_io_executor,boost::asio::ip::basic_resolver_results<boost::asio::ip::tcp>,boost::asio::detail::default_connect_condition,std::_Binder<std::_Unforced,void (__cdecl libbitcoin::network::socket::*)(boost::system::error_code const &,boost::asio::ip::basic_endpoint<boost::asio::ip::tcp> const &,std::function<void __cdecl(std::error_code const &)> const &) noexcept,std::shared_ptr<libbitcoin::network::socket>,std::_Ph<1> const &,std::_Ph<2> const &,std::function<void __cdecl(std::error_code const &)> const &> >::process<boost::asio::ip::basic_resolver_iterator<boost::asio::ip::tcp> > -> C:\Source\evoskuil\.nuget\packages\libbitcoin-boost.1.91.0\lib\native\include\boost\asio\impl\connect.hpp(355)
boost::asio::detail::executor_function::complete<boost::asio::detail::binder1<boost::asio::detail::range_connect_op<boost::asio::ip::tcp,boost::asio::any_io_executor,boost::asio::ip::basic_resolver_results<boost::asio::ip::tcp>,boost::asio::detail::default_connect_condition,std::_Binder<std::_Unforced,void (__cdecl libbitcoin::network::socket::*)(boost::system::error_code const &,boost::asio::ip::basic_endpoint<boost::asio::ip::tcp> const &,std::function<void __cdecl(std::error_code const &)> const &) noexcept,std::shared_ptr<libbitcoin::network::socket>,std::_Ph<1> const &,std::_Ph<2> const &,std::function<void __cdecl(std::error_code const &)> const &> >,boost::system::error_code>,std::allocator<void> > -> C:\Source\evoskuil\.nuget\packages\libbitcoin-boost.1.91.0\lib\native\include\boost\asio\detail\executor_function.hpp(114)
boost::asio::detail::executor_op<boost::asio::detail::executor_function,std::allocator<void>,boost::asio::detail::scheduler_operation>::do_complete -> C:\Source\evoskuil\.nuget\packages\libbitcoin-boost.1.91.0\lib\native\include\boost\asio\detail\executor_op.hpp(71)
boost::asio::detail::strand_executor_service::run_ready_handlers -> C:\Source\evoskuil\.nuget\packages\libbitcoin-boost.1.91.0\lib\native\include\boost\asio\detail\impl\strand_executor_service.ipp(153)
boost::asio::detail::strand_executor_service::invoker<boost::asio::io_context::basic_executor_type<std::allocator<void>,4> const ,void>::operator() -> C:\Source\evoskuil\.nuget\packages\libbitcoin-boost.1.91.0\lib\native\include\boost\asio\detail\impl\strand_executor_service.hpp(122)
boost::asio::io_context::basic_executor_type<std::allocator<void>,4>::execute<boost::asio::detail::strand_executor_service::invoker<boost::asio::io_context::basic_executor_type<std::allocator<void>,4> const ,void> > -> C:\Source\evoskuil\.nuget\packages\libbitcoin-boost.1.91.0\lib\native\include\boost\asio\impl\io_context.hpp(218)
boost::asio::detail::strand_executor_service::do_execute<boost::asio::io_context::basic_executor_type<std::allocator<void>,4> const ,boost::asio::detail::executor_function,std::allocator<void> > -> C:\Source\evoskuil\.nuget\packages\libbitcoin-boost.1.91.0\lib\native\include\boost\asio\detail\impl\strand_executor_service.hpp(251)
boost::asio::execution::detail::any_executor_base::execute_ex<boost::asio::strand<boost::asio::io_context::basic_executor_type<std::allocator<void>,4> > > -> C:\Source\evoskuil\.nuget\packages\libbitcoin-boost.1.91.0\lib\native\include\boost\asio\execution\any_executor.hpp(902)
boost::asio::detail::win_iocp_socket_connect_op<boost::asio::detail::range_connect_op<boost::asio::ip::tcp,boost::asio::any_io_executor,boost::asio::ip::basic_resolver_results<boost::asio::ip::tcp>,boost::asio::detail::default_connect_condition,std::_Binder<std::_Unforced,void (__cdecl libbitcoin::network::socket::*)(boost::system::error_code const &,boost::asio::ip::basic_endpoint<boost::asio::ip::tcp> const &,std::function<void __cdecl(std::error_code const &)> const &) noexcept,std::shared_ptr<libbitcoin::network::socket>,std::_Ph<1> const &,std::_Ph<2> const &,std::function<void __cdecl(std::error_code const &)> const &> >,boost::asio::any_io_executor>::do_complete -> C:\Source\evoskuil\.nuget\packages\libbitcoin-boost.1.91.0\lib\native\include\boost\asio\detail\win_iocp_socket_connect_op.hpp(123)
boost::asio::detail::win_iocp_io_context::do_one -> C:\Source\evoskuil\.nuget\packages\libbitcoin-boost.1.91.0\lib\native\include\boost\asio\detail\impl\win_iocp_io_context.ipp(499)
boost::asio::detail::win_iocp_io_context::run -> C:\Source\evoskuil\.nuget\packages\libbitcoin-boost.1.91.0\lib\native\include\boost\asio\detail\impl\win_iocp_io_context.ipp(229)
std::thread::_Invoke<std::tuple<`libbitcoin::network::threadpool::threadpool'::`4'::<lambda_1> >,0> -> C:\Program Files\Microsoft Visual Studio\18\Community\VC\Tools\MSVC\14.51.36231\include\thread(60)
thread_start<unsigned int (__cdecl*)(void *),1> -> minkernel\crts\ucrt\src\appcrt\startup\thread.cpp(97)
BaseThreadInitThunk @ 0x00007ff9b160ccb7

<<unhandled exception - end trace>>
And added the same essential capability to the macOS implementation as well (Linux generally provides this externally).
evoskuil06:59
Here's the take on pruned sync times as a function of initial table sizing:
image.png
evoskuil07:19
The above crash dumps are available in release builds, no incurred debug cost. So great to have as a customer reporting tool.
Also, bip324 is now up and running. It had a one character bug (caused the dump above). The dump fully identified the issue and it was up and running after a rebuild.
(edited)
image.png
evoskuil07:32
Running the above bench again at 1% table reservation but with p2p/s activated. It may even be faster than p2p but I don't expect too much difference.
I should have the minimized address index ready for testing tomorrow.
evoskuil14:04
p2p/s (bip324) at 90% channel participation is milestone performance neutral on our minimal platforms, with possibly a slight increase in cost on the Linux machine. There is no indication of skew over the runs toward non-private connections, so individual channel performance remained consistent. However these are milestone runs, with plenty of excess CPU. The continuous ChaCha20-Poly1305 increases CPU utilization by 40% on all machines. This hits the CPU-weakest machine (Linux) hardest in terms of wall time (40.3 -> 41.9 mins => +4.1%). That will translate into a material cost during validation.
(edited)
image.png
evoskuil14:16
Given that Core is never CPU constrained, this cost does not show up on the wall clock for them at all. But bip324 was promoted as performance neutral and even performance positive due to the removal of the sha256 checksum. That could have been removed without the privacy/encryption - so isn't a genuine argument. But this also ignores the effects of native sha on the checksum computation. On machines with native sha, the checksum is basically free and the encryption therefore is significantly more costly.
"Each v1 P2P message uses a double-SHA256 checksum truncated to 4 bytes. Roughly the same amount of computation power is required for encrypting and authenticating a v2 P2P message as proposed."
(edited)
evoskuil14:22
Also by default we have the incoming message checksum verification disabled by default (it's a pointless waste of CPU), so if they assumed people kept using that it also throws off their analysis.
evoskuil17:47
The CPU increase was 47%. I optimized the encryption a bit, and vectorized it using our SIMD abstractions. This reduced it to +22% on AVX2, and AVX512 should cut that in half again. In any case, the continuous encryption is obviously not the free win that the bip324 authors speculated it would be. That was based on assumptions of scalar sha implementation (rare these days) and falsely on the necessity of retaining that message checksum. It's a large amount of additional processing, so if you want max full validation sync speed, leave it off.
evoskuil23:59
Benchmarked against the Core implementation. It doesn't vectorize, and in a simple scalar implementation, it's a little faster - though I still have to optimize Poly1305, which should eliminate that. Our network layer over it should also be faster given less allocation and copy of these big (e.g. block) buffers. But with SIMD we are much faster. After normalizing the implementation for 128/256/512 vectorization on Intel/AMD and Arm, to match our hashing templates design, we are now significantly faster than Core across the hardware spectrum, and the above CPU impact should be marginal. More optimizations and benchmarks pending.
(edited)
🎉 1

Monday, August 17, 2026

evoskuil00:05
By "significantly" faster I mean we process the same number of bytes in about 10% of the time (in tightly isolated benchmarking).
(edited)
evoskuil04:45
CPU utilization before and after chacha20 and poly1305 optimizations.
image.png
👍 1
evoskuil20:46
bip324 throughput relative to bitcoind, as a function of vectorization path.
image.png
These wall-clock impacts are all consistently positive, network limited, and within normal variance, so grain of salt. The important factor is the above CPU utilization cost reduction.
(edited)
image.png
evoskuil20:56
In other news, shelly completed a pruned milestone sync to 950k in 22.3 mins -- including the full chain address index (ElectrumX). Total store size with full constant time bidirectional indexation is 444GB.
evoskuil21:22
Despite dropping the entire set of address hash keys (~150GB index) in favor of dynamic in-place hashing over the full script , the queries are still fast. The large address query takes 8.5ms (data), 10ms (text), and 13.7ms (json). This returns an array of 7,453 serialized output points:
[
  {
    "point": {
      "hash": "db9847575ef720b8b5582bac74bcedef1b6a08903ddc27638024b8d0d3ba0400",
      "index": 0
    },
    "value": 1270486495
  },
  ...
]
evoskuil21:59
The largest mainnet address... http://localhost/v1/address/b1c43c684d0c53f1c40cdd0a2a00303c0e8fa5751d3afde1717bd6436b6a79b8?format=data returns 107,147,568 bytes - or 2,435,172 output points - (with coin values) in 1.5 secs (1.9 secs with turbo disabled).

Tuesday, August 18, 2026

evoskuil05:43
That wasn't the largest, this is... http://localhost/v1/address/562ce1c828ab2594470ab4de2561d993d5b5cce0b55695658afb17bec0121ea5?format=data returns 227,897,252 bytes - or 5,179,483 output points - (with coin values) in 2.6 secs (3.4s cold).
That's using an index that has been reduced from 150GB to 15GB. It's inconsequential enough that we can just leave it on by default. Sync time on shelly with a full address index pruned milestone at default LF2.5 was 22.75 mins.
eynhaender08:20
Insane
👍🏻 1
evoskuil08:21
Don't try this with ElectrumX 🫵
evoskuil17:26
These are warm queries mainly because once you start querying the store is warm. The cold queries are typically around 2x time due to paging cost, but that's still very low. And it's been reduced significantly by the index 10x compression. The mac doesn't have shani, so index compares are 5x the cost of linux/windows here. But the low RAM is really the limiter, as paging is far more costly than hashing.
image.png
This shows the stages of optimization.
image.png
evoskuil19:16
I've verified that the 8GB min scenarios remain operational and performant despite the address index, as long as we crank up the load factor to keep table heads resident with some headspace for bodies. A high LF would doom validation, but the address index is only used by wallets and remains very fast for that scenario even at LF50. So basically we can default address index to on, and autotune table creation to make it work/optimal.
🚀 1

Wednesday, August 19, 2026

evoskuil03:51
The milestone prompt is in, and batching gated on the presence of a GPU is in (requires build update and ultrafast NuGet package push).
evoskuil05:41
I've merged the above work, and there's a build break on all msvc-145 builds because of the missing NuGet package. That should resolve soon.

Thursday, August 20, 2026

evoskuil01:19
One of our most popular posts ever. https://x.com/evoskuil/status/2089611537375756605
(edited)
🔥 3😅 2
Huddle with pmienk, Eric Chennells · 40 min15:55

Friday, August 21, 2026

A
ariard17:19
Hello, To introduce myself, beeing hacking on bitcoin for a while. For a bit of a year now started to hack a full-node in rust around core's libbitcoinkernel : bitcoinbackbone.org Long-term goal would be to integrate libbitcoin's libbicoin-system as a second validation backend: github.com/libbitcoin/libbitcoin-system/issues/1923 That way avoid social and technical dependency on a single validation backend. And allow backbone node operators to pick up the validation engine they wish at build time. Voila, voila 🙂
👋 2👍🏻 1
1 reply · ariard · last Aug 21, 2026
A
ariard17:20
otherwise happy to review consensus changes, as they're landing in libbitcoin, would be good way for me to learn the internals of libbitcoin's validation logic

Tuesday, August 25, 2026

evoskuil18:26
Did some traveling, but getting caught up now. Just merged system::wallet psbt and descriptors for support of the bitcoind jrpc interface. Weird that they have psbt as a client-server interface, since it's computation only, but added for compat. Also merged an optimized database query to populate utxo totals also for bitcoind jrpc. Since we don't actually maintain a utxo accumulator, this is a full store scan. However the use cases for these are limited to wallet recovery and auditing consensus. The scan is highly optimized, using a parallel query across the store for accumulation. There is also a pair of bitcoind methods that export and import an "assume utxo" blob, for which we reject support (return not_implemented).
👌🏻 1
evoskuil19:07
Core internally returns (query return) the MTP of block N+1 when asked for block N. We never inherited this internal slop, but they actually also emit it on the wire via their json-rpc interface. So when you get the "median time past" with block N, you are actually getting the MTP for block N+1. Something to watch out for when validating the interface.
(edited)

Thursday, August 27, 2026

Huddle with pmienk, eynhaender, Eric Chennells · 42 min15:58

Monday, August 31, 2026

evoskuil22:42
After some more travel I'm back to it. I previously (8/18-19) cleared these items from the punch list:
[x] Linux OOM kill at store creation on 8GB (initialize on disk)
[x] restore after height index regress (change to headers)
[x] broken store with no snap should create empty store
[x] restore after prevouts prune (unsafe)
[x] archive immutable store settings in genesis
[x] API from ultrafast to detect GPU usability
[x] milestone prompt when not explicitly configured
Also as of today, hashmap tables carry an expected element count, from which the store derives optimal boom filter k at create (formerly suboptimal), persisted via the settings envelope across load/restore. The bs defaults are recalibrated to a mainnet 950k basis: expected element counts, buckets sized for a 2.5 load factor, and minimum body allocations at 1% of the pruned body with 1% growth. I am continuing to optimize the custom posix mmap implementations, which has resulted in more performance improvement at the low RAM end*. The default allocations and growth rates are dramatically smaller than previous defaults. They no longer have a significant impact on sync performance, and this is a much more flexible configuration. With this done I'll be moving on to default store config automation as a function of available RAM. (*) e.g. Linux 8 GB RAM mini pruned with full address index now completes a 950k milestone run on 2.5Gbps in 38 minutes. Also removed the slab scan (-a) command line option, as it has been obsoleted for some time due to a redesign of the ins/points tables. ins and outs are an association record table that each point to inputs and outputs slab tables. So in/out element counts are simply array size.
🚀 2🎉 1