Merge bitcoin/bitcoin#31689: Benchmark Chainstate::ConnectBlock duration

7edaf8b64c 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](https://github.com/bitcoin-core/secp256k1/pull/1134) and [batch-verify on core](https://github.com/bitcoin/bitcoin/pull/29491).

ACKs for top commit:
  josibake:
    reACK 7edaf8b64c
  fjahr:
    utACK 7edaf8b64c
  l0rinc:
    ACK 7edaf8b64c

Tree-SHA512: 883c8a5e4e4de401ffb9ac9b6789b7fe0737afefbdaf02c6d7e1645392efc4f0d2d28b423ba7e34366a33608e0835793f5e7a1312b5c8063de14446319529cc7
This commit is contained in:
Ryan Ofsky 2025-03-23 08:25:44 -04:00
commit b9c281011b
No known key found for this signature in database
GPG key ID: 46800E30FC748A66
2 changed files with 138 additions and 0 deletions

View file

@ -18,6 +18,7 @@ add_executable(bench_bitcoin
checkblockindex.cpp
checkqueue.cpp
cluster_linearize.cpp
connectblock.cpp
crypto_hash.cpp
descriptors.cpp
disconnected_transactions.cpp

137
src/bench/connectblock.cpp Normal file
View file

@ -0,0 +1,137 @@
// Copyright (c) 2025 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include <addresstype.h>
#include <bench/bench.h>
#include <interfaces/chain.h>
#include <kernel/cs_main.h>
#include <script/interpreter.h>
#include <sync.h>
#include <test/util/setup_common.h>
#include <validation.h>
#include <cassert>
#include <vector>
/*
* Creates a test block containing transactions with the following properties:
* - Each transaction has the same number of inputs and outputs
* - All Taproot inputs use simple key path spends (no script path spends)
* - All signatures use SIGHASH_ALL (default sighash)
* - Each transaction spends all outputs from the previous transaction
*/
CBlock CreateTestBlock(
TestChain100Setup& test_setup,
const std::vector<CKey>& keys,
const std::vector<CTxOut>& outputs,
int num_txs = 1000)
{
Chainstate& chainstate{test_setup.m_node.chainman->ActiveChainstate()};
const WitnessV1Taproot coinbase_taproot{XOnlyPubKey(test_setup.coinbaseKey.GetPubKey())};
// Create the outputs that will be spent in the first transaction of the test block
// Doing this in a separate block excludes the validation of its inputs from the benchmark
auto& coinbase_to_spend{test_setup.m_coinbase_txns[0]};
const auto [first_tx, _]{test_setup.CreateValidTransaction(
{coinbase_to_spend},
{COutPoint(coinbase_to_spend->GetHash(), 0)},
chainstate.m_chain.Height() + 1, keys, outputs, {}, {})};
const CScript coinbase_spk{GetScriptForDestination(coinbase_taproot)};
test_setup.CreateAndProcessBlock({first_tx}, coinbase_spk, &chainstate);
std::vector<CMutableTransaction> txs;
txs.reserve(num_txs);
CTransactionRef tx_to_spend{MakeTransactionRef(first_tx)};
for (int i{0}; i < num_txs; i++) {
std::vector<COutPoint> inputs;
inputs.reserve(outputs.size());
for (size_t j{0}; j < outputs.size(); j++) {
inputs.emplace_back(tx_to_spend->GetHash(), j);
}
const auto [taproot_tx, _]{test_setup.CreateValidTransaction(
{tx_to_spend}, inputs, chainstate.m_chain.Height() + 1, keys, outputs, {}, {})};
txs.emplace_back(taproot_tx);
tx_to_spend = MakeTransactionRef(taproot_tx);
}
// Coinbase output can use any output type as it is not spent and will not change the benchmark
return test_setup.CreateBlock(txs, coinbase_spk, chainstate);
}
/*
* Creates key pairs and corresponding outputs for the benchmark transactions.
* - For Schnorr signatures: Creates simple key path spendable outputs
* - For Ecdsa signatures: Creates P2WPKH (native SegWit v0) outputs
* - All outputs have value of 1 BTC
*/
std::pair<std::vector<CKey>, std::vector<CTxOut>> CreateKeysAndOutputs(const CKey& coinbaseKey, size_t num_schnorr, size_t num_ecdsa)
{
std::vector<CKey> keys{coinbaseKey};
keys.reserve(num_schnorr + num_ecdsa + 1);
std::vector<CTxOut> outputs;
outputs.reserve(num_schnorr + num_ecdsa);
for (size_t i{0}; i < num_ecdsa; ++i) {
keys.emplace_back(GenerateRandomKey());
outputs.emplace_back(COIN, GetScriptForDestination(WitnessV0KeyHash{keys.back().GetPubKey()}));
}
for (size_t i{0}; i < num_schnorr; ++i) {
keys.emplace_back(GenerateRandomKey());
outputs.emplace_back(COIN, GetScriptForDestination(WitnessV1Taproot{XOnlyPubKey(keys.back().GetPubKey())}));
}
return {keys, outputs};
}
void BenchmarkConnectBlock(benchmark::Bench& bench, std::vector<CKey>& keys, std::vector<CTxOut>& outputs, TestChain100Setup& test_setup)
{
const auto& test_block{CreateTestBlock(test_setup, keys, outputs)};
bench.unit("block").run([&] {
LOCK(cs_main);
auto& chainman{test_setup.m_node.chainman};
auto& chainstate{chainman->ActiveChainstate()};
BlockValidationState test_block_state;
auto* pindex{chainman->m_blockman.AddToBlockIndex(test_block, chainman->m_best_header)}; // Doing this here doesn't impact the benchmark
CCoinsViewCache viewNew{&chainstate.CoinsTip()};
assert(chainstate.ConnectBlock(test_block, test_block_state, pindex, viewNew));
});
}
static void ConnectBlockAllSchnorr(benchmark::Bench& bench)
{
const auto test_setup{MakeNoLogFileContext<TestChain100Setup>()};
auto [keys, outputs]{CreateKeysAndOutputs(test_setup->coinbaseKey, /*num_schnorr=*/4, /*num_ecdsa=*/0)};
BenchmarkConnectBlock(bench, keys, outputs, *test_setup);
}
/**
* This benchmark is expected to be slower than the AllSchnorr or Ecdsa benchmark
* because it uses transactions with both Schnorr and Ecdsa signatures
* which requires the transaction to be hashed multiple times for
* the different signature algorithms
*/
static void ConnectBlockMixedEcdsaSchnorr(benchmark::Bench& bench)
{
const auto test_setup{MakeNoLogFileContext<TestChain100Setup>()};
// Blocks in range 848000 to 868000 have a roughly 20 to 80 ratio of schnorr to ecdsa inputs
auto [keys, outputs]{CreateKeysAndOutputs(test_setup->coinbaseKey, /*num_schnorr=*/1, /*num_ecdsa=*/4)};
BenchmarkConnectBlock(bench, keys, outputs, *test_setup);
}
static void ConnectBlockAllEcdsa(benchmark::Bench& bench)
{
const auto test_setup{MakeNoLogFileContext<TestChain100Setup>()};
auto [keys, outputs]{CreateKeysAndOutputs(test_setup->coinbaseKey, /*num_schnorr=*/0, /*num_ecdsa=*/4)};
BenchmarkConnectBlock(bench, keys, outputs, *test_setup);
}
BENCHMARK(ConnectBlockAllSchnorr, benchmark::PriorityLevel::HIGH);
BENCHMARK(ConnectBlockMixedEcdsaSchnorr, benchmark::PriorityLevel::HIGH);
BENCHMARK(ConnectBlockAllEcdsa, benchmark::PriorityLevel::HIGH);