Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
87b9c1c
feat(llmq): add trustless quorum proof chain generation and verification
PastaPastaPasta Jan 17, 2026
8f1de20
fix(llmq): address CI failures for quorum proof chain tests
PastaPastaPasta Jan 17, 2026
621780a
fix(llmq): fix quorum proof chain algorithm for long proofs
PastaPastaPasta Jan 17, 2026
4fff568
fix(llmq): optimize quorum proof chain for size and performance
PastaPastaPasta Jan 17, 2026
5040535
fix(llmq): simplify proof chain search to prioritize speed
PastaPastaPasta Jan 17, 2026
c060fa4
Optimize quorum proof chain generation
PastaPastaPasta Jan 17, 2026
cac64c4
Optimize quorum proof generation performance
PastaPastaPasta Jan 17, 2026
0d616f7
perf(llmq): optimize quorum proof chain generation
PastaPastaPasta Jan 17, 2026
5234c12
refactor(llmq): use QuorumMerkleProof::Verify instead of local static…
PastaPastaPasta Jan 17, 2026
d95c717
perf(llmq): add quorum proof data caching for faster proof chain gene…
PastaPastaPasta Jan 17, 2026
cfe3b0b
refactor(llmq): break circular dependencies in quorumproofs
PastaPastaPasta Jan 18, 2026
372904e
fix(llmq): address PR #7107 review feedback
PastaPastaPasta Jan 19, 2026
0c0df70
test: fix build_checkpoint() to use LLMQ type 100 and update test setup
PastaPastaPasta Jan 20, 2026
7b5802f
test: add tamper_proof_hex() helper for proof chain tests
PastaPastaPasta Jan 20, 2026
2d7e3f4
fix(llmq): remove incorrect ActiveChain check in chainlock indexing
PastaPastaPasta Jan 20, 2026
41b9966
test: add test_getquorumproofchain_single_step() and fix test setup
PastaPastaPasta Jan 20, 2026
259ab20
test: add skeleton test_verifyquorumproofchain_success() (blocked by …
PastaPastaPasta Jan 20, 2026
053882c
fix(llmq): fix BuildProofChain signer detection and VerifyProofChain …
PastaPastaPasta Jan 20, 2026
7d2b7aa
test: add test_verifyquorumproofchain_tampered()
PastaPastaPasta Jan 20, 2026
906990b
test: add test_verifyquorumproofchain_wrong_target()
PastaPastaPasta Jan 20, 2026
2024c20
test: add test_verifyquorumproofchain_wrong_checkpoint()
PastaPastaPasta Jan 20, 2026
e16d9ee
test: add test_getquorumproofchain_errors()
PastaPastaPasta Jan 20, 2026
2c78742
test: add test_getquorumproofchain_multi_step()
PastaPastaPasta Jan 20, 2026
74b7c04
fix: compute proof data for chainlock block in multi-step proofs
PastaPastaPasta Jan 21, 2026
f0c2c2d
refactor: use only non-legacy BLS scheme for chainlock verification
PastaPastaPasta Jan 21, 2026
7a48736
refactor: simplify quorum proof chain implementation
PastaPastaPasta Jan 21, 2026
ce1394e
chore: remove activity.md from version control
PastaPastaPasta Jan 21, 2026
db9eec7
refactor: simplify quorum proof APIs and fix build errors
PastaPastaPasta Mar 29, 2026
53059e9
fix: resolve post-rebase build errors
PastaPastaPasta Mar 29, 2026
eb23e95
fix: add header-chainlock binding check and protect CChain access wit…
PastaPastaPasta Mar 29, 2026
17297dc
refactor: move DB key strings out of headers and add cs_main lock ann…
PastaPastaPasta Mar 29, 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 @@ -180,3 +180,4 @@ compile_commands.json
# Linux perf profiling artifacts
perf.data
perf.data.old
activity.md
3 changes: 3 additions & 0 deletions src/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,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 @@ -556,6 +558,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 @@ -19,14 +19,15 @@ CChainstateHelper::CChainstateHelper(CEvoDB& evodb, CDeterministicMNManager& dmn
llmq::CQuorumSnapshotManager& qsnapman, const ChainstateManager& chainman,
const Consensus::Params& consensus_params, const CMasternodeSync& mn_sync,
const CSporkManager& sporkman, const chainlock::Chainlocks& chainlocks,
const llmq::CQuorumManager& qman) :
const llmq::CQuorumManager& qman,
llmq::CQuorumProofManager& quorum_proof_manager) :
isman{isman},
credit_pool_manager{std::make_unique<CCreditPoolManager>(evodb, chainman)},
m_chainlocks{chainlocks},
ehf_manager{std::make_unique<CMNHFManager>(evodb, chainman, qman)},
mn_payments{std::make_unique<CMNPaymentsProcessor>(dmnman, govman, chainman, consensus_params, mn_sync, sporkman)},
special_tx{std::make_unique<CSpecialTxProcessor>(*credit_pool_manager, dmnman, *ehf_manager, qblockman, qsnapman,
chainman, consensus_params, chainlocks, qman)}
chainman, consensus_params, chainlocks, 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 @@ -33,6 +33,7 @@ namespace llmq {
class CInstantSendManager;
class CQuorumBlockProcessor;
class CQuorumManager;
class CQuorumProofManager;
class CQuorumSnapshotManager;
} // namespace llmq
namespace node {
Expand Down Expand Up @@ -62,7 +63,8 @@ class CChainstateHelper
llmq::CQuorumSnapshotManager& qsnapman, const ChainstateManager& chainman,
const Consensus::Params& consensus_params, const CMasternodeSync& mn_sync,
const CSporkManager& sporkman, const chainlock::Chainlocks& chainlocks,
const llmq::CQuorumManager& qman);
const llmq::CQuorumManager& qman,
llmq::CQuorumProofManager& quorum_proof_manager);
~CChainstateHelper();

/** Passthrough functions to chainlock::Chainlocks */
Expand Down
25 changes: 25 additions & 0 deletions src/evo/specialtxman.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,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>
#include <messagesigner.h>
Expand Down Expand Up @@ -667,6 +668,22 @@ 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,
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 @@ -724,6 +741,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 @@ -32,6 +32,7 @@ namespace Consensus { struct Params; }
namespace llmq {
class CQuorumBlockProcessor;
class CQuorumManager;
class CQuorumProofManager;
class CQuorumSnapshotManager;
} // namespace llmq

Expand All @@ -49,12 +50,14 @@ class CSpecialTxProcessor
const Consensus::Params& m_consensus_params;
const chainlock::Chainlocks& m_chainlocks;
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 chainlock::Chainlocks& chainlocks, const llmq::CQuorumManager& qman) :
const chainlock::Chainlocks& chainlocks, const llmq::CQuorumManager& qman,
llmq::CQuorumProofManager& quorum_proof_manager) :
m_cpoolman(cpoolman),
m_dmnman{dmnman},
m_mnhfman{mnhfman},
Expand All @@ -63,7 +66,8 @@ class CSpecialTxProcessor
m_chainman(chainman),
m_consensus_params{consensus_params},
m_chainlocks{chainlocks},
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 @@ -100,6 +100,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 @@ -2162,6 +2163,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>(*node.chainlocks);

Expand Down
133 changes: 133 additions & 0 deletions src/llmq/blockprocessor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
#include <evo/specialtx.h>
#include <llmq/commitment.h>
#include <llmq/options.h>
#include <llmq/quorumproofdata.h>
#include <llmq/quorumproofs.h>
#include <llmq/utils.h>
#include <util/helpers.h>
#include <util/std23.h>
Expand All @@ -18,6 +20,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 = GetMinedCommitmentHash(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(), std23::to_underlying(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::GetMinedCommitmentHash(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 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 GetMinedCommitmentHash(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 @@ -7,6 +7,7 @@
#include <bls/bls_worker.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)},
isman{std::make_unique<llmq::CInstantSendManager>(chainlocks, chainman.ActiveChainstate(), *sigman, sporkman,
mempool, mn_sync, db_params)}
Expand Down
2 changes: 2 additions & 0 deletions src/llmq/context.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ namespace llmq {
class CInstantSendManager;
class CQuorumBlockProcessor;
class CQuorumManager;
class CQuorumProofManager;
class CQuorumSnapshotManager;
class CSigningManager;
} // namespace llmq
Expand Down Expand Up @@ -54,6 +55,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::CInstantSendManager> isman;
};
Expand Down
Loading
Loading