Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
11897c9
feat(llmq): add trustless quorum proof chain generation and verification
PastaPastaPasta Jan 17, 2026
1c3c413
fix(llmq): address CI failures for quorum proof chain tests
PastaPastaPasta Jan 17, 2026
62e6357
fix(llmq): fix quorum proof chain algorithm for long proofs
PastaPastaPasta Jan 17, 2026
8bad536
fix(llmq): optimize quorum proof chain for size and performance
PastaPastaPasta Jan 17, 2026
e1d2eea
fix(llmq): simplify proof chain search to prioritize speed
PastaPastaPasta Jan 17, 2026
5e588ce
Optimize quorum proof chain generation
PastaPastaPasta Jan 17, 2026
1dfc02e
Optimize quorum proof generation performance
PastaPastaPasta Jan 17, 2026
c641655
perf(llmq): optimize quorum proof chain generation
PastaPastaPasta Jan 17, 2026
196f96a
refactor(llmq): use QuorumMerkleProof::Verify instead of local static…
PastaPastaPasta Jan 17, 2026
243dcc0
perf(llmq): add quorum proof data caching for faster proof chain gene…
PastaPastaPasta Jan 17, 2026
c9c124f
refactor(llmq): break circular dependencies in quorumproofs
PastaPastaPasta Jan 18, 2026
935795f
fix(llmq): address PR #7107 review feedback
PastaPastaPasta Jan 19, 2026
a2d5aad
test: fix build_checkpoint() to use LLMQ type 100 and update test setup
PastaPastaPasta Jan 20, 2026
d1cf575
test: add tamper_proof_hex() helper for proof chain tests
PastaPastaPasta Jan 20, 2026
4b0d4c1
fix(llmq): remove incorrect ActiveChain check in chainlock indexing
PastaPastaPasta Jan 20, 2026
ec75ae2
test: add test_getquorumproofchain_single_step() and fix test setup
PastaPastaPasta Jan 20, 2026
cf87986
test: add skeleton test_verifyquorumproofchain_success() (blocked by …
PastaPastaPasta Jan 20, 2026
38e969a
fix(llmq): fix BuildProofChain signer detection and VerifyProofChain …
PastaPastaPasta Jan 20, 2026
3732a35
test: add test_verifyquorumproofchain_tampered()
PastaPastaPasta Jan 20, 2026
124a576
test: add test_verifyquorumproofchain_wrong_target()
PastaPastaPasta Jan 20, 2026
29e3b91
test: add test_verifyquorumproofchain_wrong_checkpoint()
PastaPastaPasta Jan 20, 2026
41256f8
test: add test_getquorumproofchain_errors()
PastaPastaPasta Jan 20, 2026
6e1be70
test: add test_getquorumproofchain_multi_step()
PastaPastaPasta Jan 20, 2026
4452aa3
fix: compute proof data for chainlock block in multi-step proofs
PastaPastaPasta Jan 21, 2026
63de60e
refactor: use only non-legacy BLS scheme for chainlock verification
PastaPastaPasta Jan 21, 2026
acf6abf
refactor: simplify quorum proof chain implementation
PastaPastaPasta Jan 21, 2026
e7a1527
chore: remove activity.md from version control
PastaPastaPasta Jan 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -175,3 +175,4 @@ test/lint/.cppcheck/*
# Editor and tooling
.vscode/
compile_commands.json
activity.md
3 changes: 3 additions & 0 deletions src/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,8 @@ BITCOIN_CORE_H = \
llmq/ehf_signals.h \
llmq/options.h \
llmq/params.h \
llmq/quorumproofdata.h \
llmq/quorumproofs.h \
llmq/quorums.h \
llmq/quorumsman.h \
llmq/signhash.h \
Expand Down Expand Up @@ -557,6 +559,7 @@ libbitcoin_node_a_SOURCES = \
llmq/ehf_signals.cpp \
llmq/net_signing.cpp \
llmq/options.cpp \
llmq/quorumproofs.cpp \
llmq/quorums.cpp \
llmq/quorumsman.cpp \
llmq/signhash.cpp \
Expand Down
2 changes: 2 additions & 0 deletions src/Makefile.test.include
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ BITCOIN_TESTS =\
test/llmq_snapshot_tests.cpp \
test/llmq_utils_tests.cpp \
test/logging_tests.cpp \
test/quorum_proofs_tests.cpp \
test/quorum_proofs_regression_tests.cpp \
test/dbwrapper_tests.cpp \
test/validation_tests.cpp \
test/mempool_tests.cpp \
Expand Down
5 changes: 3 additions & 2 deletions src/evo/chainhelper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@ CChainstateHelper::CChainstateHelper(CCreditPoolManager& cpoolman, CDeterministi
llmq::CQuorumBlockProcessor& qblockman, llmq::CQuorumSnapshotManager& qsnapman,
const ChainstateManager& chainman, const Consensus::Params& consensus_params,
const CMasternodeSync& mn_sync, const CSporkManager& sporkman,
const llmq::CChainLocksHandler& clhandler, const llmq::CQuorumManager& qman) :
const llmq::CChainLocksHandler& clhandler, const llmq::CQuorumManager& qman,
llmq::CQuorumProofManager& quorum_proof_manager) :
isman{isman},
clhandler{clhandler},
mn_payments{std::make_unique<CMNPaymentsProcessor>(dmnman, govman, chainman, consensus_params, mn_sync, sporkman)},
special_tx{std::make_unique<CSpecialTxProcessor>(cpoolman, dmnman, mnhfman, qblockman, qsnapman, chainman,
consensus_params, clhandler, qman)}
consensus_params, clhandler, qman, quorum_proof_manager)}
{}

CChainstateHelper::~CChainstateHelper() = default;
Expand Down
4 changes: 3 additions & 1 deletion src/evo/chainhelper.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class CChainLocksHandler;
class CInstantSendManager;
class CQuorumBlockProcessor;
class CQuorumManager;
class CQuorumProofManager;
class CQuorumSnapshotManager;
}

Expand All @@ -44,7 +45,8 @@ class CChainstateHelper
llmq::CQuorumBlockProcessor& qblockman, llmq::CQuorumSnapshotManager& qsnapman,
const ChainstateManager& chainman, const Consensus::Params& consensus_params,
const CMasternodeSync& mn_sync, const CSporkManager& sporkman,
const llmq::CChainLocksHandler& clhandler, const llmq::CQuorumManager& qman);
const llmq::CChainLocksHandler& clhandler, const llmq::CQuorumManager& qman,
llmq::CQuorumProofManager& quorum_proof_manager);
~CChainstateHelper();

/** Passthrough functions to CChainLocksHandler */
Expand Down
26 changes: 26 additions & 0 deletions src/evo/specialtxman.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
#include <evo/simplifiedmns.h>
#include <llmq/blockprocessor.h>
#include <llmq/commitment.h>
#include <llmq/quorumproofs.h>
#include <llmq/quorumsman.h>
#include <llmq/utils.h>

Expand Down Expand Up @@ -662,6 +663,23 @@ bool CSpecialTxProcessor::ProcessSpecialTxsInBlock(const CBlock& block, const CB
return false;
}

// Index the chainlock from cbtx for proof generation
// Only index if not just checking
// Note: We can't check ActiveChain().Contains(pindex) here because the chain tip
// hasn't been updated yet during ConnectBlock - the tip is updated AFTER this function returns
if (!fJustCheck && opt_cbTx->bestCLSignature.IsValid()) {
int chainlockedHeight = pindex->nHeight - static_cast<int>(opt_cbTx->bestCLHeightDiff) - 1;
const CBlockIndex* pChainlockedBlock = pindex->GetAncestor(chainlockedHeight);
if (pChainlockedBlock) {
m_quorum_proof_manager.IndexChainlock(
chainlockedHeight,
pChainlockedBlock->GetBlockHash(),
opt_cbTx->bestCLSignature,
pindex->GetBlockHash(),
pindex->nHeight);
}
}

int64_t nTime6_3 = GetTimeMicros();
nTimeCbTxCL += nTime6_3 - nTime6_2;
LogPrint(BCLog::BENCHMARK, " - CheckCbTxBestChainlock: %.2fms [%.2fs]\n",
Expand Down Expand Up @@ -719,6 +737,14 @@ bool CSpecialTxProcessor::UndoSpecialTxsInBlock(const CBlock& block, const CBloc
if (!m_qblockman.UndoBlock(block, pindex)) {
return false;
}

// Remove chainlock index for this block's cbtx
if (block.vtx.size() > 0 && block.vtx[0]->nType == TRANSACTION_COINBASE) {
if (const auto opt_cbTx = GetTxPayload<CCbTx>(*block.vtx[0]); opt_cbTx && opt_cbTx->bestCLSignature.IsValid()) {
int chainlockedHeight = pindex->nHeight - static_cast<int>(opt_cbTx->bestCLHeightDiff) - 1;
m_quorum_proof_manager.RemoveChainlockIndex(chainlockedHeight);
}
}
} catch (const std::exception& e) {
bls::bls_legacy_scheme.store(bls_legacy_scheme);
LogPrintf("CSpecialTxProcessor::%s -- bls_legacy_scheme=%d\n", __func__, bls::bls_legacy_scheme.load());
Expand Down
8 changes: 6 additions & 2 deletions src/evo/specialtxman.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ namespace llmq {
class CChainLocksHandler;
class CQuorumBlockProcessor;
class CQuorumManager;
class CQuorumProofManager;
class CQuorumSnapshotManager;
} // namespace llmq

Expand All @@ -47,12 +48,14 @@ class CSpecialTxProcessor
const Consensus::Params& m_consensus_params;
const llmq::CChainLocksHandler& m_clhandler;
const llmq::CQuorumManager& m_qman;
llmq::CQuorumProofManager& m_quorum_proof_manager;

public:
explicit CSpecialTxProcessor(CCreditPoolManager& cpoolman, CDeterministicMNManager& dmnman, CMNHFManager& mnhfman,
llmq::CQuorumBlockProcessor& qblockman, llmq::CQuorumSnapshotManager& qsnapman,
const ChainstateManager& chainman, const Consensus::Params& consensus_params,
const llmq::CChainLocksHandler& clhandler, const llmq::CQuorumManager& qman) :
const llmq::CChainLocksHandler& clhandler, const llmq::CQuorumManager& qman,
llmq::CQuorumProofManager& quorum_proof_manager) :
m_cpoolman(cpoolman),
m_dmnman{dmnman},
m_mnhfman{mnhfman},
Expand All @@ -61,7 +64,8 @@ class CSpecialTxProcessor
m_chainman(chainman),
m_consensus_params{consensus_params},
m_clhandler{clhandler},
m_qman{qman}
m_qman{qman},
m_quorum_proof_manager{quorum_proof_manager}
{
}

Expand Down
10 changes: 10 additions & 0 deletions src/init.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@
#include <llmq/net_signing.h>
#include <llmq/options.h>
#include <llmq/observer/context.h>
#include <llmq/quorumproofs.h>
#include <masternode/meta.h>
#include <masternode/sync.h>
#include <masternode/utils.h>
Expand Down Expand Up @@ -2171,6 +2172,15 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info)

ChainstateManager& chainman = *Assert(node.chainman);

// Migrate chainlock index for quorum proof generation (one-time on first run after upgrade)
if (node.llmq_ctx && node.llmq_ctx->quorum_proof_manager) {
LOCK(cs_main);
node.llmq_ctx->quorum_proof_manager->MigrateChainlockIndex(chainman.ActiveChain(), chainparams);
// Migrate quorum proof data index for fast proof chain generation
node.llmq_ctx->quorum_proof_manager->MigrateQuorumProofIndex(chainman.ActiveChain(), chainparams,
chainman.m_blockman);
}

assert(!node.dstxman);
node.dstxman = std::make_unique<CDSTXManager>();

Expand Down
133 changes: 133 additions & 0 deletions src/llmq/blockprocessor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
#include <llmq/blockprocessor.h>
#include <llmq/commitment.h>
#include <llmq/options.h>
#include <llmq/quorumproofdata.h>
#include <llmq/quorumproofs.h>
#include <llmq/utils.h>

#include <evo/evodb.h>
Expand All @@ -16,6 +18,7 @@
#include <consensus/params.h>
#include <consensus/validation.h>
#include <deploymentstatus.h>
#include <hash.h>
#include <net.h>
#include <primitives/block.h>
#include <primitives/transaction.h>
Expand Down Expand Up @@ -230,6 +233,78 @@ bool CQuorumBlockProcessor::ProcessBlock(const CBlock& block, gsl::not_null<cons
}
}

// Store quorum proof data for fast proof chain generation (only for actual block processing, not just checking)
if (!fJustCheck) {
// Get active commitments up to this block's previous block (matching CalcCbTxMerkleRootQuorums logic)
auto commitmentsMap = GetMinedAndActiveCommitmentsUntilBlock(pindex->pprev);

// Collect all commitment hashes for merkle root calculation
std::vector<uint256> allCommitmentHashes;
for (const auto& [type, blockIndexes] : commitmentsMap) {
for (const auto* blockIndex : blockIndexes) {
uint256 commitmentHash = GetMinedCommitmentTxHash(type, blockIndex->GetBlockHash());
if (commitmentHash != uint256::ZERO) {
allCommitmentHashes.push_back(commitmentHash);
}
}
}

// Add commitments from current block
for (size_t i = 1; i < block.vtx.size(); ++i) {
const auto& tx = block.vtx[i];
if (tx->IsSpecialTxVersion() && tx->nType == TRANSACTION_QUORUM_COMMITMENT) {
const auto opt_qc = GetTxPayload<CFinalCommitmentTxPayload>(*tx);
if (opt_qc && !opt_qc->commitment.IsNull()) {
allCommitmentHashes.push_back(::SerializeHash(opt_qc->commitment));
}
}
}

// Sort to match CalcCbTxMerkleRootQuorums
std::sort(allCommitmentHashes.begin(), allCommitmentHashes.end());

// Build coinbase merkle proof (same for all commitments in this block)
std::vector<uint256> txHashes;
txHashes.reserve(block.vtx.size());
for (const auto& tx : block.vtx) {
txHashes.push_back(tx->GetHash());
}
auto [cbPath, cbSide] = BuildMerkleProofPath(txHashes, 0);

// Store proof data for each non-null commitment in this block
for (const auto& [type, qc] : qcs) {
if (qc.IsNull()) continue;

// Find commitment hash in sorted list
uint256 targetHash = ::SerializeHash(qc);
auto it = std::find(allCommitmentHashes.begin(), allCommitmentHashes.end(), targetHash);
if (it == allCommitmentHashes.end()) {
LogPrint(BCLog::LLMQ, "[ProcessBlock] Could not find commitment hash for %s in active set\n",
qc.quorumHash.ToString());
continue;
}
size_t targetIndex = std::distance(allCommitmentHashes.begin(), it);

// Build quorum merkle proof
auto [qPath, qSide] = BuildMerkleProofPath(allCommitmentHashes, targetIndex);

// Store proof data
QuorumProofData proofData;
proofData.quorumMerkleProof.merklePath = std::move(qPath);
proofData.quorumMerkleProof.merklePathSide = std::move(qSide);
proofData.coinbaseTx = block.vtx[0];
proofData.coinbaseMerklePath = cbPath; // Copy since reused
proofData.coinbaseMerklePathSide = cbSide;
proofData.header = block.GetBlockHeader();

auto proofKey = std::make_pair(DB_QUORUM_PROOF_DATA, std::make_pair(qc.llmqType, qc.quorumHash));
m_evoDb.Write(proofKey, proofData);

LogPrint(BCLog::LLMQ, "[ProcessBlock] Stored proof data for quorum %s type=%d\n",
qc.quorumHash.ToString(), ToUnderlying(qc.llmqType));
}
}

m_evoDb.Write(DB_BEST_BLOCK_UPGRADE, blockHash);

return true;
Expand Down Expand Up @@ -399,6 +474,9 @@ bool CQuorumBlockProcessor::UndoBlock(const CBlock& block, gsl::not_null<const C

m_evoDb.Erase(std::make_pair(DB_MINED_COMMITMENT, std::make_pair(qc.llmqType, qc.quorumHash)));

// Also erase the cached proof data
m_evoDb.Erase(std::make_pair(DB_QUORUM_PROOF_DATA, std::make_pair(qc.llmqType, qc.quorumHash)));

const auto& llmq_params_opt = Params().GetLLMQ(qc.llmqType);
assert(llmq_params_opt.has_value());

Expand Down Expand Up @@ -525,6 +603,61 @@ std::pair<CFinalCommitment, uint256> CQuorumBlockProcessor::GetMinedCommitment(C
return ret;
}

uint256 CQuorumBlockProcessor::GetMinedCommitmentTxHash(Consensus::LLMQType llmqType, const uint256& quorumHash) const
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the name is confusing, it's not a txhash, it's a hash of a serialized commitment message

{
auto key = std::make_pair(DB_MINED_COMMITMENT, std::make_pair(llmqType, quorumHash));
CDataStream ssKey(SER_DISK, CLIENT_VERSION);
ssKey << key;

// Fast path: try to read raw data from disk to avoid deserializing BLS keys
CDataStream ssValue(SER_DISK, CLIENT_VERSION);
if (m_evoDb.GetRawDB().ReadDataStream(ssKey, ssValue)) {
// The data in DB is std::pair<CFinalCommitment, uint256>
// It's serialized as: [CFinalCommitment serialized data][uint256 serialized data]
// uint256 is exactly 32 bytes
if (ssValue.size() > 32) {
// We just want the hash of the CFinalCommitment part
// SerializeHash uses SER_GETHASH, but we have SER_DISK bytes.
// CFinalCommitment serialization is identical for both (as long as nVersion matches).
// We trust the data in DB is consistent.
return Hash(MakeByteSpan(ssValue).first(ssValue.size() - 32));
}
}

// Fallback: use slow path (read from memory/cache or if disk read failed)
// This will deserialize the full object
auto [commitment, _] = GetMinedCommitment(llmqType, quorumHash);
if (commitment.IsNull()) {
return uint256::ZERO;
}
return ::SerializeHash(commitment);
}

uint256 CQuorumBlockProcessor::GetMinedCommitmentBlockHash(Consensus::LLMQType llmqType, const uint256& quorumHash) const
{
auto key = std::make_pair(DB_MINED_COMMITMENT, std::make_pair(llmqType, quorumHash));
CDataStream ssKey(SER_DISK, CLIENT_VERSION);
ssKey << key;

// Fast path: try to read raw data from disk
CDataStream ssValue(SER_DISK, CLIENT_VERSION);
if (m_evoDb.GetRawDB().ReadDataStream(ssKey, ssValue)) {
// The data in DB is std::pair<CFinalCommitment, uint256>
// It's serialized as: [CFinalCommitment serialized data][uint256 serialized data]
// uint256 is exactly 32 bytes and it is at the end
if (ssValue.size() >= 32) {
uint256 blockHash;
// Read last 32 bytes
std::memcpy(blockHash.begin(), ssValue.data() + ssValue.size() - 32, 32);
return blockHash;
}
}

// Fallback: use slow path
auto [_, blockHash] = GetMinedCommitment(llmqType, quorumHash);
return blockHash;
}

// The returned quorums are in reversed order, so the most recent one is at index 0
std::vector<const CBlockIndex*> CQuorumBlockProcessor::GetMinedCommitmentsUntilBlock(Consensus::LLMQType llmqType, gsl::not_null<const CBlockIndex*> pindex, size_t maxCount) const
{
Expand Down
2 changes: 2 additions & 0 deletions src/llmq/blockprocessor.h
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ class CQuorumBlockProcessor
bool HasMinedCommitment(Consensus::LLMQType llmqType, const uint256& quorumHash) const
EXCLUSIVE_LOCKS_REQUIRED(!minableCommitmentsCs);
std::pair<CFinalCommitment, uint256> GetMinedCommitment(Consensus::LLMQType llmqType, const uint256& quorumHash) const;
uint256 GetMinedCommitmentTxHash(Consensus::LLMQType llmqType, const uint256& quorumHash) const;
uint256 GetMinedCommitmentBlockHash(Consensus::LLMQType llmqType, const uint256& quorumHash) const;

std::vector<const CBlockIndex*> GetMinedCommitmentsUntilBlock(Consensus::LLMQType llmqType, gsl::not_null<const CBlockIndex*> pindex, size_t maxCount) const;
std::map<Consensus::LLMQType, std::vector<const CBlockIndex*>> GetMinedAndActiveCommitmentsUntilBlock(gsl::not_null<const CBlockIndex*> pindex) const;
Expand Down
2 changes: 2 additions & 0 deletions src/llmq/context.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include <chainlock/chainlock.h>
#include <instantsend/instantsend.h>
#include <llmq/blockprocessor.h>
#include <llmq/quorumproofs.h>
#include <llmq/quorumsman.h>
#include <llmq/signing.h>
#include <llmq/snapshot.h>
Expand All @@ -22,6 +23,7 @@ LLMQContext::LLMQContext(CDeterministicMNManager& dmnman, CEvoDB& evo_db, CSpork
*qsnapman, bls_threads)},
qman{std::make_unique<llmq::CQuorumManager>(*bls_worker, dmnman, evo_db, *quorum_block_processor, *qsnapman,
chainman, db_params)},
quorum_proof_manager{std::make_unique<llmq::CQuorumProofManager>(evo_db, *quorum_block_processor)},
sigman{std::make_unique<llmq::CSigningManager>(*qman, db_params, max_recsigs_age)},
clhandler{std::make_unique<llmq::CChainLocksHandler>(chainman.ActiveChainstate(), *qman, sporkman, mempool, mn_sync)},
isman{std::make_unique<llmq::CInstantSendManager>(*clhandler, chainman.ActiveChainstate(), *sigman, sporkman,
Expand Down
2 changes: 2 additions & 0 deletions src/llmq/context.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class CChainLocksHandler;
class CInstantSendManager;
class CQuorumBlockProcessor;
class CQuorumManager;
class CQuorumProofManager;
class CQuorumSnapshotManager;
class CSigningManager;
} // namespace llmq
Expand Down Expand Up @@ -58,6 +59,7 @@ struct LLMQContext {
const std::unique_ptr<llmq::CQuorumSnapshotManager> qsnapman;
const std::unique_ptr<llmq::CQuorumBlockProcessor> quorum_block_processor;
const std::unique_ptr<llmq::CQuorumManager> qman;
const std::unique_ptr<llmq::CQuorumProofManager> quorum_proof_manager;
const std::unique_ptr<llmq::CSigningManager> sigman;
const std::unique_ptr<llmq::CChainLocksHandler> clhandler;
const std::unique_ptr<llmq::CInstantSendManager> isman;
Expand Down
Loading
Loading