Archive of the libbitcoin Slack #general channel.
[log] # --------------------------------------------------------------------------------------------- Logging Settings path = /home/eynhaender/test/log maximum_size = 10000000 application = true news = true session = true protocol = false quitting = false proxy = false remote = true fault = true verbose = false [database] # ---------------------------------------------------------------------------------------- Database Settings path = /mnt/ssd/bitcoin turbo = true point_buckets = 682988568 # statt 1365977136 → spart 5,09 GB tx_buckets = 234611262 # statt 469222525 → spart 0,87 GB strong_tx_buckets = 234611262 # statt 469222525 → spart 0,87 GB interval_depth = 11 [bitcoin] # ----------------------------------------------------------------------------------------- Bitcoin Settings milestone=000000000000000000010b93c9ea1c29fea277383f0f7d1f26de8b5802e885ff:950000 [node] # -------------------------------------------------------------------------------------------- Node Settings # Pruning snapshot occurs at first coalesced, otherwise manual snapshot only. threads = 1 thread_priority = true memory_priority = true delay_inbound = true allocation_multiple = 1 allowed_deviation = 1.5 sample_period_seconds = 10 currency_window_minutes = 5000 maximum_concurrency = 50000 minimum_fee_rate = 0.00001 maximum_height = 950000 [network] # ----------------------------------------------------------------------------------------- General Network Settings threads = 6 retry_timeout_seconds = 5 # 1s, 2s, 7s f/m/s loopback, 5s for large connect_timeout_seconds = 5 # 1s, 3s, 5s f/m/s loopback, 10s for large [peer] # -------------------------------------------------------------------------------------------- General P2P Settings path = eynhaender/development/libbitcoin/test/network # cashed hosts file goes here user_agent = "/libbitcoin4:0.14.2/" enable_alert = false # send/log alert messages (no send option) enable_reject = false # send/log reject messages enable_address = true # in/out address messages enable_address_v2 = false # in/out address v2 messages enable_witness_tx = false # in/out wtxid for tx announcements enable_compact = false # in/out compact block announcements enable_relay = false # in/out transaction announcements validate_checksum = false # validate inbound message checksum invalid_services = 176 # shitcoins services_minimum = 9 # witness node (required of peer to outbound) services_maximum = 9 # witness node (provided to peer of non-seed) [+64 for filters] protocol_maximum = 70015 # current default (send_headers). [70015 for filters, else 70012] protocol_minimum = 31800 # Satoshi:22.0.0 minimum (headers). handshake_timeout_seconds = 5 # time to complete handshake. channel_heartbeat_minutes = 5 # ping maximum_skew_minutes = 5000 [outbound] # ---------------------------------------------------------------------------------------- P2P Outbound Settings connections = 100 host_pool_capacity = 10000 connect_batch_size = 5 use_ipv6 = true seeding_timeout_seconds = 42
node.threads = 1
maximum_height = 950000
seeding_timeout_seconds = 42
namespace libbitcoin {
namespace database {
namespace schema {
/// These should be ASCII only.
namespace dir
{
constexpr auto heads = "heads";
constexpr auto primary = "primary";
constexpr auto secondary = "secondary";
constexpr auto temporary = "temporary";
}
namespace archive
{
constexpr auto header = "archive_header";
constexpr auto input = "archive_input";
constexpr auto output = "archive_output";
constexpr auto point = "archive_point";
constexpr auto ins = "archive_ins";
constexpr auto outs = "archive_outs";
constexpr auto spend = "archive_spend";
constexpr auto tx = "archive_tx";
constexpr auto txs = "archive_txs";
}
namespace indexes
{
constexpr auto candidate = "index_candidate";
constexpr auto confirmed = "index_confirmed";
constexpr auto strong_tx = "index_strong";
}
namespace caches
{
constexpr auto ecdsa = "batch_ecdsa";
constexpr auto schnorr = "batch_schnorr";
constexpr auto multisig = "batch_multisig";
constexpr auto duplicate = "cache_duplicate";
constexpr auto prevout = "cache_prevout";
constexpr auto validated_bk = "validated_bk";
constexpr auto validated_tx = "validated_tx";
}
namespace optionals
{
constexpr auto address = "option_address";
constexpr auto filter_bk = "option_filter_bk";
constexpr auto filter_tx = "option_filter_tx";
}
namespace locks
{
constexpr auto flush = "flush";
constexpr auto process = "process";
}
namespace ext
{
constexpr auto head = ".head";
constexpr auto data = ".data";
constexpr auto lock = ".lock";
}
} // namespace schema
} // namespace database
} // namespace libbitcoinschnorr: [hash(32)][xkey(32)][sig(64)][block-fk(3)] ecdsa: [hash(32)][cpk(33) ][sig(64)][block-fk(3)] multisig: [hash(32)][cpk(33) ][sig(64)][m|n(1)][group(2)][blockid-fk(3)]
500,000 downloaded/validated/confirmed/organized at 33 minutes. That's about a 2.5 hour pace to complete full validation over the Internet on a 10 year old machine without shani. Adding full Electrum address and neutrino indexing to this would bring it closer to 3 hours.800k now at 2.0 hrs with 400GiB of signature tuples stored, schnorr starting to pick up. 99% signature capture rate.3.5 hrs full validation baseline to 950k. The three signature batches accumulated 486GiB of signatures, and toward the end write time was most of the CPU time, resulting from flushing. This should mostly go away once the tables are purged in each window. Still, even 3.5 hrs isn't bad at all for full validation cost on this machine.ecdsa: [hash(32)][cpk(33)][sig(64)][block-fk(3)] multisig: [hash(32)][cpk(33)][sig(64)][m|n(1)][group(2)][blockid-fk(3)] schnorr: [hash(32)][xpk(32)][sig(64)][block-fk(3)] threshold: [hash(32)][xpk(32)][sig(64)][m|n(1)][group(2)][blockid-fk(3)]
multisig row is one ECDSA signature, a threshold (tapscript CHECKSIG / CHECKSIGADD) row is one Schnorr signature. The m|n + group bytes are accounting metadata carried in the opaque tail and are never interpreted by the bridge (m-of-n satisfaction stays your job, downstream of the per-row verdict).
Throughput (measured)
bench_lbtc_4table, measured on this machine — Intel i5-14400F + NVIDIA RTX 5060 Ti, performance governor, best-of-4, 1,000,000 signatures per table, end-to-end (host de-interleave + H2D transfer + kernel + per-row result copy — what a node actually pays, not kernel-only).
tablerecord / tailGPU (CUDA)CPU (1 core, reference)ecdsaEcdsaRecord(129) + 33.84 M/s0.034 M/s (~29.5 s / 1M)multisigEcdsaRecord(129) + 63.84 M/s0.034 M/s (~29.3 s / 1M)schnorrSchnorrRecord(128) + 34.90 M/s0.074 M/s (~13.5 s / 1M)thresholdSchnorrRecord(128) + 64.88 M/s0.074 M/s (~13.7 s / 1M)
Key result — the m-of-n tail is free. multisig/threshold (6-byte m|n+group+block-fk tail) verify at the same rate as the single ecdsa/schnorr tables (3-byte tail), on both backends. The verify core reads only the leading record and walks stride = record + key_size, so the accounting tail costs nothing — measured, not inferred.
Notes:
#include "ufsecp_libbitcoin.h"
using namespace ufsecp::lbtc;
Controller ctrl; // AUTO: GPU if present, else CPU reference
if (!ctrl.ok()) { /* no usable backend — the only failure is reported here */ }// rows: n × (EcdsaRecord(129) + block-fk(3)); key_size = 3 (the opaque tail) std::vector<uint8_t> results(n); ctrl.verify_ecdsa(rows, n, /*key_size=*/3, results.data()); // results[i] == 1 valid / 0 invalid ctrl.verify_schnorr(srows, n, /*key_size=*/3, results.data());
// Your packed table forwards directly; key_size = sizeof(Row) - record is recovered from the type. std::span<const MultisigRow> ms = ...; // EcdsaRecord + m|n + group + block-fk (135B) std::span<const ThresholdRow> th = ...; // SchnorrRecord + m|n + group + block-fk (134B) ctrl.verify_multisig (ms, r1.data()); // == ECDSA path, key_size 6 ctrl.verify_threshold(th, r2.data()); // == Schnorr path, key_size 6
// In-place: VALID rows get their key tail zeroed; INVALID rows keep it intact.
std::span<MultisigRow> batch = ...; // MUTABLE
ctrl.collect_multisig(batch);
for (auto& row : batch) // survivors = the rejected set (m|n+group+block-fk intact)
if (tail_nonzero(row)) rejected.push_back(row);// multisig record IS the ECDSA record; threshold record IS the Schnorr record. // m|n+group+block-fk is the opaque key tail (key_size = 6). ufsecp_lbtc_verify_ecdsa (ctrl, ms_rows, n, /*key_size=*/6, results); // multisig table ufsecp_lbtc_verify_schnorr(ctrl, th_rows, n, /*key_size=*/6, results); // threshold table ufsecp_lbtc_verify_ecdsa (ctrl, e_rows, n, /*key_size=*/3, results); // ecdsa table ufsecp_lbtc_verify_schnorr(ctrl, s_rows, n, /*key_size=*/3, results); // schnorr table
#pragma pack(1)):
struct MultisigRow { EcdsaRecord record; uint8_t mn; uint8_t group[2]; uint8_t block_fk[3]; }; // 135
struct ThresholdRow { SchnorrRecord record; uint8_t mn; uint8_t group[2]; uint8_t block_fk[3]; }; // 134SECP256K1_BUILD_AS_OBJEC Build fastsecp256k1 as OBJECT library (no .a — objects go directly into parent link step)We use these terms (sort of informally): Unit Test - smallest unit possible/practical (e.g. lowest level method/function). Component Test - aggregate of units (e.g. class or aggregating function/method). Functional Test - system function (e.g. endpoint communication over a socket, public API). Acceptance Test - performed over the compiled executable (e.g. customer acceptance). We target full Unit Test coverage and Component and/or Functional as necessary to ensure expected aggregate behavior. No external dependencies (e.g. Python or other tools), just Boost Test in our pattern. Can be data-driven (which is when we allow loops in a test). Acceptance is outside of the build (against the executable or compiled lib).(edited)
[log] application = false(edited)
Still, even 3.5 hrs isn't bad at all for full validation cost on this machine.

2026-06-11T17:00:45Z.0 Efficiency multisig. 99.9764% = 386926003/(386926003+91486) 2026-06-11T17:00:45Z.0 Efficiency ecdsa.... 99.9918% = 932347684/(932347684+76931) 2026-06-11T17:00:45Z.0 Efficiency multisig. 99.9764% = 386926187/(386926187+91486) 2026-06-11T17:00:45Z.0 Efficiency ecdsa.... 99.9918% = 932348544/(932348544+76931) 2026-06-11T17:00:45Z.0 Efficiency multisig. 99.9764% = 386926948/(386926948+91486) 2026-06-11T17:00:45Z.0 Efficiency ecdsa.... 99.9918% = 932352956/(932352956+76931) 2026-06-11T17:00:45Z.0 Efficiency multisig. 99.9764% = 386928147/(386928147+91486) 2026-06-11T17:00:45Z.0 Efficiency ecdsa.... 99.9917% = 932357743/(932357743+76931) 2026-06-11T17:00:45Z.0 Efficiency multisig. 99.9764% = 386929215/(386929215+91486) 2026-06-11T17:00:45Z.0 Efficiency ecdsa.... 99.9918% = 932358136/(932358136+76931) 2026-06-11T17:00:45Z.0 Efficiency multisig. 99.9764% = 386929302/(386929302+91486) 2026-06-11T17:00:45Z.0 Efficiency ecdsa.... 99.9917% = 932360468/(932360468+76932) 2026-06-11T17:00:45Z.0 Efficiency multisig. 99.9764% = 386929510/(386929510+91486) 2026-06-11T17:00:45Z.0 Efficiency ecdsa.... 99.9918% = 932361084/(932361084+76932) 2026-06-11T17:00:45Z.0 Efficiency multisig. 99.9764% = 386929564/(386929564+91486) 2026-06-11T17:00:45Z.0 Efficiency ecdsa.... 99.9918% = 932362252/(932362252+76932) 2026-06-11T17:00:45Z.0 Efficiency multisig. 99.9764% = 386929687/(386929687+91486) 2026-06-11T17:00:45Z.0 Efficiency ecdsa.... 99.9917% = 932364194/(932364194+76933) 2026-06-11T17:00:45Z.0 Efficiency multisig. 99.9764% = 386929818/(386929818+91487) 2026-06-11T17:00:45Z.0 Efficiency ecdsa.... 99.9918% = 932365434/(932365434+76933) 2026-06-11T17:00:45Z.0 Efficiency multisig. 99.9764% = 386930045/(386930045+91487) 2026-06-11T17:00:45Z.0 Efficiency ecdsa.... 99.9918% = 932368141/(932368141+76933) 2026-06-11T17:00:45Z.0 Efficiency multisig. 99.9764% = 386930076/(386930076+91487) 2026-06-11T17:00:45Z.0 Efficiency ecdsa.... 99.9917% = 932369056/(932369056+76933) 2026-06-11T17:00:45Z.0 Efficiency multisig. 99.9764% = 386930082/(386930082+91487)(edited)
2026-06-11T18:44:08Z.5 Insufficient services (1032) by [66.85.157.56:8333] showing (3085) /Satoshi:25.0.0/ 2026-06-11T18:44:09Z.0 Efficiency ecdsa.... 99.6359% = 1895305241/(1895305241+6926814) 2026-06-11T18:44:09Z.0 Efficiency multisig. 99.9601% = 725149057/(725149057+289162) 2026-06-11T18:44:09Z.0 Efficiency schnorr.. 74.0383% = 23630642/(23630642+8286152) 2026-06-11T18:44:09Z.0 Efficiency threshold 100.0000% = 185585/(185585+0)
Ultrafast built and linked with no libsecp256k1, using @eynhaender's NuGet package, system tests all passing. Building now for a baseline full validation run tonight..props integration).
// batch_verify
// ----------------------------------------------------------------------------
#if defined(HAVE_ULTRAFAST)
template <typename Batch>
data_chunk batch_verify(const std::span<Batch>& batch, bool) NOEXCEPT
{
// OOM is unrecoverable.
static thread_local ufsecp::lbtc::Controller context{ UFSECP_LBTC_AUTO };
if (!context.ok()) std::abort();
const auto count = batch.size();
data_chunk results(count);
const auto out = results.data();
const auto in = pointer_cast<const uint8_t>(batch.data());
if constexpr (is_same_type<Batch, schnorr::batch>)
{
constexpr auto extra_size = sizeof(Batch) - (sizeof(hash_digest) +
sizeof(ec_xonly) + sizeof(ec_signature));
ufsecp_lbtc_verify_schnorr(context.get(), in, count, extra_size, out);
}
else
{
constexpr auto extra_size = sizeof(Batch) - (sizeof(hash_digest) +
sizeof(ec_compressed) + sizeof(ec_signature));
ufsecp_lbtc_verify_ecdsa(context.get(), in, count, extra_size, out);
}
return results;
}
#else
template <typename Batch>
data_chunk batch_verify(const std::span<Batch>& batch, bool turbo) NOEXCEPT
{
// par_if doesn't throw.
BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT)
const auto policy = poolstl::execution::par_if(turbo);
BC_POP_WARNING()
// Used only to produce order for concurrency.
std::vector<size_t> it(batch.size());
std::iota(it.begin(), it.end(), zero);
// Collect signature validation results as corresponding integer booleans.
data_chunk results(batch.size());
std::for_each(policy, it.cbegin(), it.cend(), [&](size_t row) NOEXCEPT
{
results.at(row) = to_int<uint8_t>(verify_signature(batch[row]));
});
return results;
}
#endif (edited)ufsecp. There was one issue that took some time to track down, just a difference in expectations regarding signature encoding. This was using @eynhaender’s NuGet package with the Windows build as a drop-in replacement for secp256k1.secp256k1 time, which is not surprising given that @Vano Chkheidze has said the Windows builds aren't optimized. And the NuGet package may not be optimal either. But it at least gets the toolchain moving for signature batching and silent payment search.libbitcoin-system. Just factoring the 1 secp256k1 wrapper .cpp file into 7 (it's grown a lot over the years, with bip66, taproot, and now batching). Much easier to work with now. So now back to batching. The open work is to coordinate invocation and summation in the validation chaser. Once we get a good run in the current table format, I'll rotate from horizontal to vertical format (2 tables -> 2 sets of 4 tables), so that we get clean no-copy GPU processing over the mmaps.4.85 hrs to 950k is a new record, taking back the mantle from Brunella.secp batching in concurrency window default (50k blocks), on maxi. Concurrency model seems to be working nicely, mostly 100% CPU since startup. Averaging right around 317,000 signatures per second in each block. The maxi has 64 x 2.1 GHz threads, and note that we are not pausing other operations during batch processing, the node continues to download and confirm - only validation is suspended during the periods of batch processing.
I expect and it appears that this baseline will be slower than our traditional runs. The batching incurs overhead in buffering the signatures to mmap and in post-processing the results, in CPU, RAM and I/O. However GPU processing this same work will free most of its CPU cost and complete in fraction of the time. At block 450k the signature verification cost is about 8 minutes. That should come down to 30 secs on a typical modern GPU (not maxi), and just a few seconds on a high end GPU system.70% of CPU. 20% is going to download, parsing, hashing, and storing. and about 5% confirmation queries, and about 5% to reading.2.5 mins behind the record maxi full validation run. But because I wanted to see the behavior from genesis, this run also includes the 295k checkpointed blocks that aren't in the benchmark. That normally takes about 8 mins additional time. IOW this run is a little faster so far than the best full validation benchmark, and all we've really done is organize the signatures to be validated close together. So it does appear that the cache coherence is offsetting the marshalling overhead.std::atomic<bool>& for cancellations. The cancellable function just checks the value in the loop. We should add this to the batch APIs.std::stop_token here (which wraps std::atomic<bool>) as it's helpful to be more explicit for the public interface.ufsecp now, but maxi needs a GPU driver update (drivers from 2018) and really a new GPU (2015). So I'll be moving the trial runs over to a mini build. That will also give us avx512 and sha-ni. At the same time I'm working on several cleanup items pertaining to batching, including:
(1) passing cancellation.
(2) managing ufsecp configuration.
(3) fine control over batch sizing (configurable sigs count threshold).
(4) generalizing multisig for threshold formulations.
(5) removing dead code (multisig table, turbo parameter).
(6) vertical table storage (4 physical tables per logical table).
These are all straightforward changes, hope to be done today.2026-06-18T15:58:58Z.6 Require [5553809024] bytes and [4394893312] are free.
maxi is very consistently batching (using secp parallel loop) 360,000 sigs/sec at all block heights, and entirely independent of the download window.UFSECP_LBTC_AUTO parameter, since that's probably all we'll ever use.op_checkmultisig) in script. The use of a 1M sig threshold is pretty arbitrary, bigger may be better or worse. This is running behind the last trial I ran where the threshold was tied to the currency window. The triggering cost is def lower in that approach.626,321,7861,808 secs~346,400 sps3,784,861,520 capturable sigs and this rate of 346,000 sigs/sec, that's 3.0 hrs of signature validation. This machine downloads and fully validates the chain in 4.9 hrs, and performs a milestone sync in 0.9 hrs. That means we are net burning 1.0 hrs reading blocks and prevouts from the store for validation, signature hashing, and setting states, and confirming.95.5% of all CPU in the validation. So offloading this entirely to another processor (GPU) implies that on this old machine we should complete validation in ~2.0 hrs.(1) passing cancellation. [done]
(2) managing ufsecp configuration. [done]
(3) fine control over batch sizing (configurable sigs count threshold). [done]
(4) generalizing multisig for threshold formulations.
(5) removing dead code (multisig table, turbo parameter). [turbo done]
(6) vertical table storage (4 physical tables per logical table).secp) full validation run on mini was significantly faster than previous up to block 700k, but that could be due to previous optimizations, so I'm running again without batching.node.maximum_height setting doesn't yet finish off the residual batch, which also prevents confirmations from completing. So I've done some work to ensure that a capped run completes all batching and confirmations as in unbatched implementations.bitcoind json-rpc and REST interfaces last night, thanks @Djuri !(4) generalizing multisig for threshold formulations. [done]
(6) vertical table storage (4 physical tables per logical table).
ultrafast and did a non-batching run to 250k without checkpoints. So this is just a basic substitution of secp256k1, which is now configurable in the vs2006 IDE against @eynhaender’s published NuGet packages. Run time (Windows maxi) to 250k confirmed was 435 secs vs. the benchmark run of 410 secs. This is live Internet and a small number of blocks/sigs, so subject to some variation. But a respectable time at +6.1%. I'll do a full run validation tonight to get a solid comparison.
WRT batching, I'm running that in CPU mode as well, but as missing something in my config, so still getting only 1 out of 64 threads in ultrafast. The fallback batching, targeting chunks of 1 million sigs is slightly faster than basic algorithm (full validation parallel by block), which is nice to see. No loss at all in the marshaling, which is non-trivial, even some slight net gain.
Still running on horizontally-aligned tables, which is the last bug piece of work. Will be swapping that out for 4 tables for ecdsa and 4 tables for schnorr. Same storage cost, just more bookkeeping. But eliminates rewriting the entire table as it streams for batching, which at .5TB is significant.
Have also integrated cancellation, so that now works on ultrafast batching as well as our fallback batch implementation over secp256k1.
I completed folding the ecdsa single and multi-sig implementations into the same table, so we're capturing over 99% of all ecdsa sigs. More significantly I also completed the implementation of schnorr threshold signature scripts, which are much more complex to capture. I don't yet have a full capture rate total yet for schnorr, But I'd estimate around 90% based on previous scans, bringing the combined ecdsa/schnorr capture rate to 98% or so.is_ecdsa_batchable --------------------------------------------------- chain::script::is_pay_public_key_pattern(ops) chain::script::is_pay_key_hash_pattern(ops) chain::script::is_pay_witness_key_hash_pattern(ops) chain::script::is_pay_multisig_standard_pattern(ops) is_schnorr_batchable --------------------------------------------------- chain::script::is_pay_taproot_key_path_pattern(ops) chain::script::is_pay_tapscript_single_pattern(ops) chain::script::is_pay_tapscript_timelock_pattern(ops) chain::script::is_pay_tapscript_inscription_pattern(ops) chain::script::is_pay_tapscript_multisig_pattern(ops) chain::script::is_pay_tapscript_threshold_pattern(ops)
constexpr bool script::is_pay_public_key_pattern(
const operations& ops) NOEXCEPT
{
return ops.size() == 2
&& is_public_key(ops[0].data())
&& ops[1].code() == opcode::checksig;
}
constexpr bool script::is_pay_key_hash_pattern(const operations& ops) NOEXCEPT
{
return ops.size() == 5
&& ops[0].code() == opcode::dup
&& ops[1].code() == opcode::hash160
&& ops[2].data().size() == short_hash_size
&& ops[3].code() == opcode::equalverify
&& ops[4].code() == opcode::checksig;
}
constexpr bool script::is_pay_witness_key_hash_pattern(
const operations& ops) NOEXCEPT
{
return ops.size() == 2
&& ops[0].code() == opcode::push_size_0
&& ops[1].code() == opcode::push_size_20;
}
constexpr bool script::is_pay_multisig_standard_pattern(
const operations& ops) NOEXCEPT
{
if (ops.size() < 4u ||
ops.back().code() != opcode::checkmultisig)
return false;
const auto& m_op = ops.front();
const auto& n_op = ops[ops.size() - 2u];
// Standard form is restricted to minimal encoding.
if (!m_op.is_positive() || !n_op.is_positive())
return false;
const auto m = operation::opcode_to_positive(m_op.code());
const auto n = operation::opcode_to_positive(n_op.code());
if (is_zero(m) || is_zero(n) || (m > n) || (n > 16u) ||
(n != ops.size() - 3u))
return false;
// Standard multisig requires valid public key forms.
for (auto op = std::next(ops.begin()); op != std::prev(ops.end(), 2); ++op)
if (!is_public_key(op->data()))
return false;
return true;
}// This script type is fabricated in witness::extract_taproot.
constexpr bool script::is_pay_taproot_key_path_pattern(
const operations& ops) NOEXCEPT
{
return ops.size() == 1
&& ops[0].code() == opcode::checksig;
}
constexpr bool script::is_pay_tapscript_single_pattern(
const operations& ops) NOEXCEPT
{
return ops.size() == 2
&& ops[0].data().size() == ec_xonly_size
&& ops[1].code() == opcode::checksig;
}
constexpr bool script::is_pay_tapscript_timelock_pattern(
const operations& ops) NOEXCEPT
{
return ops.size() == 5u
&& ops[0].is_nonnegative()
&& ops[1].is_timelock()
&& ops[2].code() == opcode::drop
&& ops[3].data().size() == ec_xonly_size
&& ops[4].code() == opcode::checksig;
}
constexpr bool script::is_pay_tapscript_inscription_pattern(
const operations& ops) NOEXCEPT
{
return ops.size() > 4u
&& ops[0].data().size() == ec_xonly_size
&& ops[1].code() == opcode::checksig
&& ops[2].code() == opcode::push_size_0
&& ops[3].code() == opcode::if_
&& ops.back().code() == opcode::endif;
}
constexpr bool script::is_pay_tapscript_threshold_pattern(
const operations& ops) NOEXCEPT
{
const auto size = ops.size();
if (is_zero(size))
return false;
// opcode::within requires an additional number in the script.
const auto within = (ops.back().code() == opcode::within);
if ((is_even(size) == within) ||
(size < (4u + to_int<uint8_t>(within))))
return false;
auto op = ops.begin();
if ((op++)->data().size() != ec_xonly_size)
return false;
if ((op++)->code() != opcode::checksig)
return false;
while (op != std::prev(ops.end(), 2 + to_int(within)))
{
if (((op++)->data().size() != ec_xonly_size) ||
((op++)->code() != opcode::checksigadd))
return false;
}
if (!(op++)->is_unsigned32() || (within &&
!(op++)->is_unsigned32()))
return false;
return op->is_threshold();
}
constexpr bool script::is_pay_tapscript_multisig_pattern(
const operations& ops) NOEXCEPT
{
if (ops.size() < 2u || !is_even(ops.size()))
return false;
auto op = ops.begin();
while (op != std::prev(ops.end(), 2))
{
if (((op++)->data().size() != ec_xonly_size) ||
((op++)->code() != opcode::checksigverify))
return false;
}
if (((op++)->data().size() != ec_xonly_size) ||
((op++)->code() != opcode::checksig))
return false;
return true;
}secp1 replacement is 8.25 hrs vs. my benchmark of 4.85 hrs. So 194% of our normal runtime when not batching (just using the drop-in secp256k1 replacement). A short run was only 106.1% behind, but as blocks increase in size signature density increases and more significantly dominates the run.WITH_ULTRAFAST defined (at least in -system build), include ufsecp headers vs secp, and link bs to ufsecp vs secp.15:28:32 5,366,900 sigs 3s 1.79M sigs/s 15:30:45 1,071,533 sigs 0s ~1.07M sigs/s 15:30:55 2,315,127 sigs 0s ~2.32M sigs/s 15:31:06 4,558,730 sigs 1s 4.56M sigs/s 15:32:16 29,354,060 sigs 8s 3.67M sigs/s 15:33:31 36,497,618 sigs 10s 3.65M sigs/s 15:37:20 1,468,875 sigs 0s ~1.47M sigs/s 15:38:18 5,883,209 sigs 1s 5.88M sigs/s
[network] threads = 24 [node] threads = 24 maximum_concurrency = 300000 batch_signatures = 10000000 warn_dirty_background_ratio = 90 warn_dirty_ratio = 90
[outbound] connections = 1500 connect_batch_size = 1 host_pool_capacity = 200000 seeding_timeout_seconds = 420
node.delay_inbound = false native.bind = 127.0.0.1:8080 database.ecdsa_size = 10000000000 database.schnorr_size = 10000000000(edited)
[database] header_rate = 15 txs_rate = 15 tx_rate = 15 point_rate = 15 ins_rate = 15 outs_rate = 15 input_rate = 15 output_rate = 15 candidate_rate = 15 confirmed_rate = 15 duplicate_rate = 15 prevout_rate = 15 strong_tx_rate = 15 validated_bk_rate = 15 validated_tx_rate = 15 filter_tx_rate = 15 filter_bk_rate = 15 address_rate = 15 ecdsa_rate = 15 schnorr_rate = 15
vm.dirty_ratio = 95 vm.dirty_background_ratio = 95 vm.dirty_bytes = 0 vm.dirty_background_bytes = 0 vm.dirty_expire_centisecs = 12000 vm.dirty_writeback_centisecs = 12000 SwapTotal = 0 SwapFree = 0
[bitcoin] milestone = 0000000000000000000000000000000000000000000000000000000000000000:0 [database] address_rate = 15 candidate_rate = 15 confirmed_rate = 15 duplicate_rate = 15 ecdsa_rate = 15 ecdsa_size = 10000000000 filter_bk_rate = 15 filter_tx_rate = 15 header_rate = 15 input_rate = 15 ins_rate = 15 output_rate = 15 outs_rate = 15 path = /var/lib/libbitcoin-server/blockchain point_rate = 15 prevout_buckets = 1000000 prevout_rate = 15 prevout_size = 4000000000 schnorr_rate = 15 schnorr_size = 10000000000 silent_buckets = 1 silent_rate = 50 silent_size = 4000000000 silent_start_height = 709632 strong_tx_rate = 15 turbo = true tx_rate = 15 txs_rate = 15 validated_bk_rate = 15 validated_tx_rate = 15 [electrum] bind = 0.0.0.0:50001 maximum_history = 1000000 maximum_subscriptions = 1000000 self_bind = 136.243.9.246:50001 server_name = libbitcoin-gpu-test silent_payment_threads = 20 [inbound] bind = 0.0.0.0:8333 [log] path = /var/log/libbitcoin-server [native] bind = 136.243.9.246:8180 connections = 16 [network] threads = 20 [node] batch_signatures = 5000000 delay_inbound = false maximum_concurrency = 500000 threads = 14 warn_dirty_background_ratio = 90 warn_dirty_ratio = 90 [outbound] connect_batch_size = 5 connections = 100 host_pool_capacity = 10000 seeding_timeout_seconds = 420 [peer] enable_relay = false
because the GPU tables drained, but confirmation is still stuck at 2950001
despite us having 295002, the node kept downloading
batched_ . it seems that the chaser can acquire a lock after the rows are written but before the blocks push_batch() handler has ran. so it verifies and purges the rows but only links in batched_ are marked as validbatched_ is protected by the strand. Any pending update to batched is added after the critical section completes, because it is also stranded. // set_block_valid(batched_ excluding ecdsa/schnorr failures)
// ------------------------------------------------------------------------
if (!process_valids(residual))
{
fault(error::batch4);
return;
}for (const auto& link: batched_)
{
if (link == header_link::terminal)
continue;
size_t height{};
if (!query.get_height(height, link) ||
!query.set_block_valid(link))
return false;
notify_block(system::error::success, height, link, false);
}
batched_.clear();a has a bad sig. when validating `a` the bad signature row is written into the batch table under the batch mutex. But a’s block id is not added to batched_ at that moment, rather it is posted later to the strand via push_batch().
before push_batch() runs, block b finishes and triggers batch verification. verifier sees a’s bad signature row, correctly marks a unconfirmable, and purges the table. but because a’s id is not yet in batched_, the invalid link cleanup cannot mark a’s entry as terminal.
now a’s delayed push_batch() finally runs and appends a to batched_. On the next batch, process_valids() treats every non-terminal link in batched_ as valid, so a can be marked valid even though it was already marked unconfirmable for an invalid signature.
relevant code paths (not using line links because i also have some local changes):
// chaser_validate_parallel.cpp
{
std::shared_lock lock{ mutex_, std::defer_lock };
if (capturing) lock.lock();
if ((ec = block.connect(ctx, capture)))
return ec;
batched = capture.batched;
faulted = capture.faulted;
}// chaser_validate.cpp
if (batched)
{
++batch_backlog_;
POST(push_batch, link, height);
return;
}// chaser_validate_batch.cpp batched_.push_back(link); process_batch(is_maximum());
// chaser_validate_batch.cpp
for (const auto& link: invalids)
{
if (!query.get_height(height, link) ||
!query.set_block_unconfirmable(link))
return false;
notify_block(system::error::invalid_signature, height, link, false);
}
std::ranges::replace_if(batched_, [&](const auto& link) NOEXCEPT
{
return contains(invalids, link);
}, header_link::terminal);// chaser_validate_batch.cpp
for (const auto& link: batched_)
{
if (link == header_link::terminal)
continue;
if (!query.get_height(height, link) ||
!query.set_block_valid(link))
return false;
notify_block(system::error::success, height, link, false);
}BOOST_AUTO_TEST_CASE(chaser_validate__process_batch__late_invalid_batched_link__does_not_become_valid)
{
configuration config{ chain::selection::mainnet };
config.database.path = TEST_DIRECTORY;
config.node.batch_signatures = 1;
const auto events = [](auto, auto) NOEXCEPT {};
store store_{ config.database };
query query_{ store_ };
BOOST_REQUIRE(test::clear(TEST_DIRECTORY));
BOOST_REQUIRE_EQUAL(store_.create(events), database::error::success);
BOOST_REQUIRE(query_.initialize(config.bitcoin.genesis_block));
const chain::block invalid_block{ block1_data, true };
const chain::block trigger_block{ block2_data, true };
const chain::block valid_block{ block3_data, true };
BOOST_REQUIRE(query_.set(invalid_block, database::context{ 0, 1, 0 }, false, false));
BOOST_REQUIRE(query_.set(trigger_block, database::context{ 0, 2, 0 }, false, false));
BOOST_REQUIRE(query_.set(valid_block, database::context{ 0, 3, 0 }, false, false));
const auto invalid_link = query_.to_header(invalid_block.hash());
const auto trigger_link = query_.to_header(trigger_block.hash());
const auto valid_link = query_.to_header(valid_block.hash());
BOOST_REQUIRE(query_.push_candidate(invalid_link));
BOOST_REQUIRE(query_.push_candidate(trigger_link));
BOOST_REQUIRE(query_.push_candidate(valid_link));
// invalid_link has a bad sig row, trigger_link has a good sig row.
BOOST_REQUIRE(query_.set_signature(sighash_bad, ecdsa_compressed,
ecdsa_signature, 0, invalid_link));
BOOST_REQUIRE(query_.set_signature(ecdsa_sighash, ecdsa_compressed,
ecdsa_signature, 0, trigger_link));
BOOST_REQUIRE_EQUAL(query_.ecdsa_records(), 2u);
code invalid_state{};
code trigger_state{};
code valid_state{};
{
const network::logger log{};
full_node node{ query_, config, log };
batch_test_chaser chaser{ node };
// trigger_link push_batch() processes both table rows before
// invalid_link delayed push_batch() has appended invalid_link to batched_
chaser.complete_batched(trigger_link, 2u);
chaser.drain();
BOOST_REQUIRE_EQUAL(query_.ecdsa_records(), 0u);
BOOST_REQUIRE_EQUAL(query_.get_block_state(invalid_link),
database::error::block_unconfirmable);
BOOST_REQUIRE_EQUAL(query_.get_block_state(trigger_link),
database::error::block_valid);
// invalid_link arrives late into batched_ after its bad row was purged
chaser.complete_batched(invalid_link, 1u);
chaser.drain();
// later good batch causes process_valids() to iterate the stale
// invalid_link and call set_block_valid() on it.
BOOST_REQUIRE(query_.set_signature(ecdsa_sighash, ecdsa_compressed,
ecdsa_signature, 0, valid_link));
chaser.complete_batched(valid_link, 3u);
chaser.drain();
invalid_state = query_.get_block_state(invalid_link);
trigger_state = query_.get_block_state(trigger_link);
valid_state = query_.get_block_state(valid_link);
}
BOOST_REQUIRE(!store_.close(events));
// this fails on upstream: invalid_state becomes block_valid.
BOOST_CHECK_EQUAL(invalid_state, database::error::block_unconfirmable);
BOOST_CHECK_EQUAL(trigger_state, database::error::block_valid);
BOOST_CHECK_EQUAL(valid_state, database::error::block_valid);
} (edited)diff --git a/src/chasers/chaser_validate_batch.cpp b/src/chasers/chaser_validate_batch.cpp
index a269929f..9b8cfda7 100644
--- a/src/chasers/chaser_validate_batch.cpp
+++ b/src/chasers/chaser_validate_batch.cpp
@@ -210,8 +210,13 @@ bool chaser_validate::process_valids(bool residual) NOEXCEPT
continue;
size_t height{};
- if (!query.get_height(height, link) ||
- !query.set_block_valid(link))
+ if (!query.get_height(height, link))
+ return false;
+
+ if (query.get_block_state(link) == database::error::block_unconfirmable)
+ continue;
+
+ if (!query.set_block_valid(link))
return false;
notify_block(system::error::success, height, link, false);i think i got a decent way to control batch size without locking everything up
//...We also don't want to read block state before // every write of valid state.
which obviously comes with some performance hits
but by controlling the batch size, i am consistently seeing 5M/s which is what i wanted
also i set batch_size, i want it to be exactly that haha
secp256k1.bool chaser_validate::process_invalids(const header_links& invalids) NOEXCEPT
{
BC_ASSERT(stranded());
auto& query = archive();
for (const auto& link: invalids)
{
size_t height{};
if (!query.get_height(height, link) ||
!query.set_block_unconfirmable(link))
return false;
invalids_.push_back(link);
notify_block(system::error::invalid_signature, height, link, false);
}
// Set all invalid links in batched_ to terminal (to be skipped).
if (!invalids_.empty())
{
std::ranges::replace_if(batched_, [&](const auto& link) NOEXCEPT
{
return contains(invalids_, link);
}, header_link::terminal);
}
return true;
} (edited)Current status: - Runtime: 1h51m - Latest completed boundary: 850000 - Current tip: 955376 - Remaining: 105k blocks - Progress by height: 89% Recent 50k windows: 700k -> 750k: 13m43s 750k -> 800k: 13m50s 800k -> 850k: 13m35s Estimated finish: 21:22-21:28 CEST. Estimated total runtime: about 2h20m-2h26m.
2026-06-23T21:33:16Z.0 intel... platform:1. 2026-06-23T21:33:16Z.0 avx512.. platform:0 compiled:0. 2026-06-23T21:33:16Z.0 avx2.... platform:1 compiled:1. 2026-06-23T21:33:16Z.0 sse41... platform:1 compiled:1. 2026-06-23T21:33:16Z.0 shani... platform:0 compiled:0.
ufsecp and secp scan integration.ecdsa and schnorr. Just need to update tests and adjust libbitcoin-system secp to accept the vertical layout (and pass it to ufsecp), and we'll soon be batching without .5TB of transformation.39.4 mins.2026-06-28T20:13:35Z.0 Version Information... libbitcoin-server: 4.0.0 libbitcoin-node: 4.0.0 libbitcoin-network: 4.0.0 libbitcoin-database: 4.0.0 libbitcoin-system: 4.0.0 2026-06-28T20:13:35Z.0 intel... platform:1. 2026-06-28T20:13:35Z.0 avx512.. platform:0 compiled:0. 2026-06-28T20:13:35Z.0 avx2.... platform:1 compiled:1. 2026-06-28T20:13:35Z.0 sse41... platform:1 compiled:1. 2026-06-28T20:13:35Z.0 shani... platform:0 compiled:0.
[native] # ============================================================================= # 80/443 (use default) # native RESTful interface (http/s, stateless html/json) bind = 127.0.0.1:80 bind = [::1]:80 safe = 127.0.0.1:443 safe = [::1]:443 key_pass = libbitcoin key_path = m:/certs/server/server-ecc384-key-enc.pem cert_path = m:/certs/server/server-ecc384-cert.pem connections = 10
on an external SSD
39.4 mins on my 2.5Gbps Internet for milestone sync to 950k last night. Tough to beat, almost the theoretical limit.monitor-ibd.ps1
A PowerShell loop (meant to run continuously in the background) that every 60s:
events.logD:\libbitcoin\node\logs\ibd-progress.csvwith awall-clock UTC timestamp— this is what makes it survive node restarts, sinceevents.log's own elapsed-seconds column resets each time the node restarts.mmap writes dirty pages back as many tiny (~4 KB), scattered I/Os. Internal SSD doesn't care — low latency, deep queues, so it absorbs the pattern invisibly. USB external has high per-I/O latency and shallow queuing, so that same drizzle of small writes grinds along at single-digit MB/s, turning a ~1-minute transfer into hours.(edited)
2026-06-30T14:39:51Z.1 Batch verify rate ecdsa.... (287081769 / 820) = 350099 sps 2026-06-30T14:45:01Z.1 Batch verify rate schnorr.. (107437982 / 309) = 347695 sps 2026-06-30T14:45:02Z.1 Capture ecdsa.... (2138408877 / 2147002284) = 99.5997% 2026-06-30T14:45:02Z.1 Capture multisig. (770292387 / 771820889) = 99.8020% 2026-06-30T14:45:02Z.1 Capture schnorr.. (134916587 / 143926332) = 93.7400% 2026-06-30T14:45:02Z.1 Capture threshold (453526 / 453526) = 100.0000%
2026-06-30T15:50:00Z.1 Batch verify rate ecdsa.... (332294470 / 964) = 344703 sps 2026-06-30T15:54:25Z.1 Batch verify rate schnorr.. (90764789 / 265) = 342508 sps 2026-06-30T15:54:26Z.1 Capture ecdsa.... (2414732212 / 2425290707) = 99.5647% 2026-06-30T15:54:26Z.1 Capture multisig. (813212242 / 815289836) = 99.7452% 2026-06-30T15:54:26Z.1 Capture schnorr.. (225181854 / 235398785) = 95.6597% 2026-06-30T15:54:26Z.1 Capture threshold (953052 / 953052) = 100.0000%
99.11% CPU utilization within ecdsa::signature_verify() and 99.90% within schnorr::signature_verify() (these are executed in independent batches non-concurrently).08:02:45Z.1 Batch verify rate ecdsa.... (5347951 / 14) = 381996 sps 08:03:03Z.1 Batch verify rate ecdsa.... (1009305 / 3) = 336435 sps 08:03:17Z.1 Batch verify rate ecdsa.... (1000770 / 3) = 333590 sps 08:03:48Z.1 Batch verify rate ecdsa.... (1073483 / 3) = 357827 sps 08:04:26Z.1 Batch verify rate ecdsa.... (7842509 / 21) = 373452 sps 08:09:22Z.1 Batch verify rate ecdsa.... (62846069 / 170) = 369682 sps 08:10:16Z.1 Batch verify rate ecdsa.... (1004741 / 7) = 143534 sps 08:10:38Z.1 Batch verify rate ecdsa.... (2059874 / 6) = 343312 sps 08:10:46Z.1 Batch verify rate ecdsa.... (1101780 / 3) = 367260 sps 08:11:01Z.1 Batch verify rate ecdsa.... (2643441 / 8) = 330430 sps 08:11:20Z.1 Batch verify rate ecdsa.... (2335074 / 7) = 333582 sps 08:11:58Z.1 Batch verify rate ecdsa.... (7356151 / 21) = 350292 sps 08:13:43Z.1 Batch verify rate ecdsa.... (17513874 / 53) = 330450 sps 08:23:39Z.1 Batch verify rate ecdsa.... (121999975 / 345) = 353623 sps 08:43:07Z.1 Batch verify rate ecdsa.... (260099703 / 718) = 362255 sps 09:07:55Z.1 Batch verify rate ecdsa.... (317242859 / 930) = 341121 sps 09:30:45Z.1 Batch verify rate ecdsa.... (283132935 / 863) = 328079 sps 10:00:05Z.1 Batch verify rate ecdsa.... (328328857 / 1117) = 293938 sps 10:30:58Z.1 Batch verify rate ecdsa.... (347752711 / 1100) = 316138 sps 11:17:41Z.1 Batch verify rate ecdsa.... (378092789 / 1426) = 265142 sps 12:19:19Z.1 Batch verify rate ecdsa.... (356852026 / 1006) = 354723 sps 12:19:21Z.1 Batch verify rate schnorr.. (452495 / 1) = 452495 sps 13:27:13Z.1 Batch verify rate ecdsa.... (332047984 / 946) = 351002 sps 13:28:32Z.1 Batch verify rate schnorr.. (27479633 / 78) = 352302 sps 14:39:51Z.1 Batch verify rate ecdsa.... (287081769 / 820) = 350099 sps 14:45:01Z.1 Batch verify rate schnorr.. (107437982 / 309) = 347695 sps 15:50:00Z.1 Batch verify rate ecdsa.... (332294470 / 964) = 344703 sps 15:54:25Z.1 Batch verify rate schnorr.. (90764789 / 265) = 342508 sps
2026-06-30T16:46:57Z.1 Batch verify rate ecdsa.... (274333559 / 770) = 356277 sps 2026-06-30T16:50:13Z.1 Batch verify rate schnorr.. (64691586 / 195) = 331751 sps 2026-06-30T16:50:14Z.1 Capture ecdsa.... (2653331301 / 2666668842) = 99.4998% 2026-06-30T16:50:14Z.1 Capture multisig. (840673646 / 843602106) = 99.6529% 2026-06-30T16:50:14Z.1 Capture schnorr.. (289213432 / 300246826) = 96.3252% 2026-06-30T16:50:14Z.1 Capture threshold (1613053 / 1613053) = 100.0000% 2026-06-30T16:50:14Z.1 Batch verify rate ecdsa.... (28919 / 0) = 28919 sps 2026-06-30T16:50:14Z.1 Batch verify rate schnorr.. (1545 / 0) = 1545 sps 2026-06-30T16:50:15Z.1 Capture ecdsa.... (2653358659 / 2666696373) = 99.4998% 2026-06-30T16:50:15Z.1 Capture multisig. (840674831 / 843603291) = 99.6529% 2026-06-30T16:50:15Z.1 Capture schnorr.. (289214946 / 300248374) = 96.3252% 2026-06-30T16:50:15Z.1 Capture threshold (1613084 / 1613084) = 100.0000%
3,784,861,520 total capture 3,812,161,122 total signatures from 295,001 to 950,000 ------------------------------------------------------ 99.28% total signature capture rate!!
<= and >= threshold, that wasn't hit until almost entirely complete. So we both now have complete batched runs, me with CPU on Windows against libsecp256k1 and him with GPU on Linux with ultrafast.