View in context in the libbitcoin Slack #general mirror.
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]; }; // 134