Skip to content

Commit b9c2810

Browse files
committed
Merge bitcoin#31689: Benchmark Chainstate::ConnectBlock duration
7edaf8b Benchmark Chainstate::ConnectBlock duration (Eunovo) Pull request description: Introduce benchmarks to evaluate ConnectBlock performance for: - Blocks containing only Schnorr signatures - Blocks containing both Schnorr and ECDSA signatures - Blocks containing only ECDSA signatures The benchmarks in this PR, focus on signature validation. Additional benchmarks may be added in the future to assess other aspects of ConnectBlock. This is the first step toward implementing Batch Verification of Schnorr Signatures in Core. It provides a way to test and measure the performance improvements of batch verification on Core. For more details on batch validation, refer to the [batch-verify module on secp](bitcoin-core/secp256k1#1134) and [batch-verify on core](bitcoin#29491). ACKs for top commit: josibake: reACK bitcoin@7edaf8b fjahr: utACK 7edaf8b l0rinc: ACK 7edaf8b Tree-SHA512: 883c8a5e4e4de401ffb9ac9b6789b7fe0737afefbdaf02c6d7e1645392efc4f0d2d28b423ba7e34366a33608e0835793f5e7a1312b5c8063de14446319529cc7
2 parents af3dee0 + 7edaf8b commit b9c2810

File tree

2 files changed

+138
-0
lines changed

2 files changed

+138
-0
lines changed

src/bench/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ add_executable(bench_bitcoin
1818
checkblockindex.cpp
1919
checkqueue.cpp
2020
cluster_linearize.cpp
21+
connectblock.cpp
2122
crypto_hash.cpp
2223
descriptors.cpp
2324
disconnected_transactions.cpp

src/bench/connectblock.cpp

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
// Copyright (c) 2025 The Bitcoin Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
#include <addresstype.h>
6+
#include <bench/bench.h>
7+
#include <interfaces/chain.h>
8+
#include <kernel/cs_main.h>
9+
#include <script/interpreter.h>
10+
#include <sync.h>
11+
#include <test/util/setup_common.h>
12+
#include <validation.h>
13+
14+
#include <cassert>
15+
#include <vector>
16+
17+
/*
18+
* Creates a test block containing transactions with the following properties:
19+
* - Each transaction has the same number of inputs and outputs
20+
* - All Taproot inputs use simple key path spends (no script path spends)
21+
* - All signatures use SIGHASH_ALL (default sighash)
22+
* - Each transaction spends all outputs from the previous transaction
23+
*/
24+
CBlock CreateTestBlock(
25+
TestChain100Setup& test_setup,
26+
const std::vector<CKey>& keys,
27+
const std::vector<CTxOut>& outputs,
28+
int num_txs = 1000)
29+
{
30+
Chainstate& chainstate{test_setup.m_node.chainman->ActiveChainstate()};
31+
32+
const WitnessV1Taproot coinbase_taproot{XOnlyPubKey(test_setup.coinbaseKey.GetPubKey())};
33+
34+
// Create the outputs that will be spent in the first transaction of the test block
35+
// Doing this in a separate block excludes the validation of its inputs from the benchmark
36+
auto& coinbase_to_spend{test_setup.m_coinbase_txns[0]};
37+
const auto [first_tx, _]{test_setup.CreateValidTransaction(
38+
{coinbase_to_spend},
39+
{COutPoint(coinbase_to_spend->GetHash(), 0)},
40+
chainstate.m_chain.Height() + 1, keys, outputs, {}, {})};
41+
const CScript coinbase_spk{GetScriptForDestination(coinbase_taproot)};
42+
test_setup.CreateAndProcessBlock({first_tx}, coinbase_spk, &chainstate);
43+
44+
std::vector<CMutableTransaction> txs;
45+
txs.reserve(num_txs);
46+
CTransactionRef tx_to_spend{MakeTransactionRef(first_tx)};
47+
for (int i{0}; i < num_txs; i++) {
48+
std::vector<COutPoint> inputs;
49+
inputs.reserve(outputs.size());
50+
51+
for (size_t j{0}; j < outputs.size(); j++) {
52+
inputs.emplace_back(tx_to_spend->GetHash(), j);
53+
}
54+
55+
const auto [taproot_tx, _]{test_setup.CreateValidTransaction(
56+
{tx_to_spend}, inputs, chainstate.m_chain.Height() + 1, keys, outputs, {}, {})};
57+
txs.emplace_back(taproot_tx);
58+
tx_to_spend = MakeTransactionRef(taproot_tx);
59+
}
60+
61+
// Coinbase output can use any output type as it is not spent and will not change the benchmark
62+
return test_setup.CreateBlock(txs, coinbase_spk, chainstate);
63+
}
64+
65+
/*
66+
* Creates key pairs and corresponding outputs for the benchmark transactions.
67+
* - For Schnorr signatures: Creates simple key path spendable outputs
68+
* - For Ecdsa signatures: Creates P2WPKH (native SegWit v0) outputs
69+
* - All outputs have value of 1 BTC
70+
*/
71+
std::pair<std::vector<CKey>, std::vector<CTxOut>> CreateKeysAndOutputs(const CKey& coinbaseKey, size_t num_schnorr, size_t num_ecdsa)
72+
{
73+
std::vector<CKey> keys{coinbaseKey};
74+
keys.reserve(num_schnorr + num_ecdsa + 1);
75+
76+
std::vector<CTxOut> outputs;
77+
outputs.reserve(num_schnorr + num_ecdsa);
78+
79+
for (size_t i{0}; i < num_ecdsa; ++i) {
80+
keys.emplace_back(GenerateRandomKey());
81+
outputs.emplace_back(COIN, GetScriptForDestination(WitnessV0KeyHash{keys.back().GetPubKey()}));
82+
}
83+
84+
for (size_t i{0}; i < num_schnorr; ++i) {
85+
keys.emplace_back(GenerateRandomKey());
86+
outputs.emplace_back(COIN, GetScriptForDestination(WitnessV1Taproot{XOnlyPubKey(keys.back().GetPubKey())}));
87+
}
88+
89+
return {keys, outputs};
90+
}
91+
92+
void BenchmarkConnectBlock(benchmark::Bench& bench, std::vector<CKey>& keys, std::vector<CTxOut>& outputs, TestChain100Setup& test_setup)
93+
{
94+
const auto& test_block{CreateTestBlock(test_setup, keys, outputs)};
95+
bench.unit("block").run([&] {
96+
LOCK(cs_main);
97+
auto& chainman{test_setup.m_node.chainman};
98+
auto& chainstate{chainman->ActiveChainstate()};
99+
BlockValidationState test_block_state;
100+
auto* pindex{chainman->m_blockman.AddToBlockIndex(test_block, chainman->m_best_header)}; // Doing this here doesn't impact the benchmark
101+
CCoinsViewCache viewNew{&chainstate.CoinsTip()};
102+
103+
assert(chainstate.ConnectBlock(test_block, test_block_state, pindex, viewNew));
104+
});
105+
}
106+
107+
static void ConnectBlockAllSchnorr(benchmark::Bench& bench)
108+
{
109+
const auto test_setup{MakeNoLogFileContext<TestChain100Setup>()};
110+
auto [keys, outputs]{CreateKeysAndOutputs(test_setup->coinbaseKey, /*num_schnorr=*/4, /*num_ecdsa=*/0)};
111+
BenchmarkConnectBlock(bench, keys, outputs, *test_setup);
112+
}
113+
114+
/**
115+
* This benchmark is expected to be slower than the AllSchnorr or Ecdsa benchmark
116+
* because it uses transactions with both Schnorr and Ecdsa signatures
117+
* which requires the transaction to be hashed multiple times for
118+
* the different signature algorithms
119+
*/
120+
static void ConnectBlockMixedEcdsaSchnorr(benchmark::Bench& bench)
121+
{
122+
const auto test_setup{MakeNoLogFileContext<TestChain100Setup>()};
123+
// Blocks in range 848000 to 868000 have a roughly 20 to 80 ratio of schnorr to ecdsa inputs
124+
auto [keys, outputs]{CreateKeysAndOutputs(test_setup->coinbaseKey, /*num_schnorr=*/1, /*num_ecdsa=*/4)};
125+
BenchmarkConnectBlock(bench, keys, outputs, *test_setup);
126+
}
127+
128+
static void ConnectBlockAllEcdsa(benchmark::Bench& bench)
129+
{
130+
const auto test_setup{MakeNoLogFileContext<TestChain100Setup>()};
131+
auto [keys, outputs]{CreateKeysAndOutputs(test_setup->coinbaseKey, /*num_schnorr=*/0, /*num_ecdsa=*/4)};
132+
BenchmarkConnectBlock(bench, keys, outputs, *test_setup);
133+
}
134+
135+
BENCHMARK(ConnectBlockAllSchnorr, benchmark::PriorityLevel::HIGH);
136+
BENCHMARK(ConnectBlockMixedEcdsaSchnorr, benchmark::PriorityLevel::HIGH);
137+
BENCHMARK(ConnectBlockAllEcdsa, benchmark::PriorityLevel::HIGH);

0 commit comments

Comments
 (0)