Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 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
b196460
feat(llmq): generate mining-only snapshot proofs
PastaPastaPasta Sep 8, 2026
6598615
merge: integrate develop with snapshot proof RPCs
PastaPastaPasta Sep 8, 2026
ebf2d28
fix(llmq): satisfy proof builder locking and dependency checks
PastaPastaPasta Sep 8, 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 @@ -181,3 +181,4 @@ compile_commands.json
# Linux perf profiling artifacts
perf.data
perf.data.old
activity.md
7 changes: 7 additions & 0 deletions doc/release-notes-7107.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
New RPCs
--------

- Add `getquorumproofchain` and `verifyquorumproofchain` for compact mining-transaction
proofs from an independently trusted snapshot, plus `getchainlockbyheight` for
archived certificates. Proof generation requires `-quorumproofindex` on an
unpruned node; enabling it scans historical blocks at startup. (#7107)
3 changes: 3 additions & 0 deletions src/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,8 @@ BITCOIN_CORE_H = \
llmq/observer.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 @@ -598,6 +600,7 @@ libbitcoin_node_a_SOURCES = \
llmq/net_signing.cpp \
llmq/observer.cpp \
llmq/options.cpp \
llmq/quorumproofs.cpp \
llmq/quorums.cpp \
llmq/quorumsman.cpp \
llmq/signhash.cpp \
Expand Down
1 change: 1 addition & 0 deletions src/Makefile.test.include
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ BITCOIN_TESTS =\
test/llmq_snapshot_tests.cpp \
test/llmq_utils_tests.cpp \
test/logging_tests.cpp \
test/quorum_proofs_tests.cpp \
test/masternode_payments_tests.cpp \
test/dbwrapper_tests.cpp \
test/validation_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 @@ -23,7 +23,8 @@ CChainstateHelper::CChainstateHelper(CEvoDB& evodb, CDeterministicMNManager& dmn
llmq::CInstantSendManager& isman, llmq::CQuorumBlockProcessor& qblockman,
llmq::CQuorumSnapshotManager& qsnapman, const ChainstateManager& chainman,
const Consensus::Params& consensus_params, const chainlock::Chainlocks& chainlocks,
const llmq::CQuorumManager& qman) :
const llmq::CQuorumManager& qman,
llmq::CQuorumProofManager& quorum_proof_manager) :
isman{isman},
mn_sync{mn_sync},
m_dmnman{dmnman},
Expand All @@ -33,7 +34,7 @@ CChainstateHelper::CChainstateHelper(CEvoDB& evodb, CDeterministicMNManager& dmn
superblocks{std::make_unique<governance::SuperblockManager>()},
mn_payments{std::make_unique<CMNPaymentsProcessor>(dmnman, *superblocks, consensus_params)},
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 @@ -35,6 +35,7 @@ namespace llmq {
class CInstantSendManager;
class CQuorumBlockProcessor;
class CQuorumManager;
class CQuorumProofManager;
class CQuorumSnapshotManager;
} // namespace llmq
class CChainstateHelper
Expand All @@ -60,7 +61,8 @@ class CChainstateHelper
llmq::CInstantSendManager& isman, llmq::CQuorumBlockProcessor& qblockman,
llmq::CQuorumSnapshotManager& qsnapman, const ChainstateManager& chainman,
const Consensus::Params& consensus_params, const chainlock::Chainlocks& chainlocks,
const llmq::CQuorumManager& qman);
const llmq::CQuorumManager& qman,
llmq::CQuorumProofManager& quorum_proof_manager);
~CChainstateHelper();

bool IsSuperblockValidationRequired(const CBlockIndex* const pindex);
Expand Down
25 changes: 25 additions & 0 deletions src/evo/specialtxman.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,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 @@ -857,6 +858,22 @@ bool CSpecialTxProcessor::ProcessSpecialTxsInBlock(Chainstate& chainstate, const
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 @@ -918,6 +935,14 @@ bool CSpecialTxProcessor::UndoSpecialTxsInBlock(const Chainstate& chainstate, co
if (!m_qblockman.UndoBlock(chainstate, 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, pindex->GetBlockHash());
}
}
Comment on lines +939 to +945

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Blocking: RemoveChainlockIndex unconditionally erases entries still referenced by other blocks

IndexChainlock keys the index purely on chainlockedHeight (src/llmq/quorumproofs.cpp:217). Miners intentionally copy the same best ChainLock forward across consecutive coinbase transactions, incrementing bestCLHeightDiff until a newer ChainLock appears. ProcessSpecialTxsInBlock therefore overwrites the same DB slot from multiple consecutive blocks, and UndoSpecialTxsInBlock later calls RemoveChainlockIndex(chainlockedHeight) unconditionally on disconnect. After disconnecting block N+1, the previous block N (still in the active chain) may still embed a ChainLock for the same height, but the index entry is gone — getchainlockbyheight and BuildProofChain start reporting missing coverage until a newer ChainLock or a re-migration restores it.

Fix: either reference-count by cbtxBlockHash, only erase if the disconnected block's cbtxBlockHash matches the stored entry, or rebuild on next connect.

source: ['claude', 'codex']

} 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 @@ -34,6 +34,7 @@ namespace Consensus { struct Params; }
namespace llmq {
class CQuorumBlockProcessor;
class CQuorumManager;
class CQuorumProofManager;
class CQuorumSnapshotManager;
} // namespace llmq

Expand All @@ -51,12 +52,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 @@ -65,7 +68,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
12 changes: 12 additions & 0 deletions src/init.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@
#include <llmq/net_signing.h>
#include <llmq/observer.h>
#include <llmq/options.h>
#include <llmq/quorumproofs.h>
#include <masternode/meta.h>
#include <masternode/sync.h>
#include <masternode/utils.h>
Expand Down Expand Up @@ -638,6 +639,7 @@ void SetupServerArgs(ArgsManager& argsman)
argsman.AddArg("-reindex-chainstate", "Rebuild chain state from the currently indexed blocks. When in pruning mode or if blocks on disk might be corrupted, use full -reindex instead. Deactivate all optional indexes before running this.", ArgsManager::ALLOW_ANY, OptionsCategory::INDEXING);
argsman.AddArg("-spentindex", strprintf("Maintain a full spent index, used to query the spending txid and input index for an outpoint (default: %u)", DEFAULT_SPENTINDEX), ArgsManager::ALLOW_ANY, OptionsCategory::INDEXING);
argsman.AddArg("-timestampindex", strprintf("Maintain a timestamp index for block hashes, used to query blocks hashes by a range of timestamps (default: %u)", DEFAULT_TIMESTAMPINDEX), ArgsManager::ALLOW_ANY, OptionsCategory::INDEXING);
argsman.AddArg("-quorumproofindex", "Index historical ChainLocks and quorum mining witnesses for SDK proofs (requires unpruned blocks)", ArgsManager::ALLOW_ANY, OptionsCategory::INDEXING);
argsman.AddArg("-txindex", strprintf("Maintain a full transaction index, used by the getrawtransaction rpc call (default: %u)", DEFAULT_TXINDEX), ArgsManager::ALLOW_ANY, OptionsCategory::INDEXING);
argsman.AddArg("-blockfilterindex=<type>",
strprintf("Maintain an index of compact filters by block (default: %s, values: %s).", DEFAULT_BLOCKFILTERINDEX, ListBlockFilterTypes()) +
Expand Down Expand Up @@ -2090,6 +2092,16 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info)

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

if (node.llmq_ctx && llmq::QuorumProofIndexEnabled()) {
if (node::fPruneMode) return InitError(_("Quorum proof indexing requires unpruned blocks"));
LOCK(cs_main);
try {
node.llmq_ctx->quorum_proof_manager->MigrateChainlockIndex(chainman.ActiveChain(), chainparams);
} catch (const std::exception& e) {
return InitError(Untranslated(strprintf("Quorum proof indexing failed: %s", e.what())));
}
}

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

Expand Down
15 changes: 15 additions & 0 deletions src/llmq/blockprocessor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include <evo/specialtx.h>
#include <llmq/commitment.h>
#include <llmq/options.h>
#include <llmq/quorumproofdata.h>
#include <llmq/utils.h>
#include <util/helpers.h>
#include <util/std23.h>
Expand All @@ -18,6 +19,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 @@ -275,6 +277,17 @@ bool CQuorumBlockProcessor::ProcessBlock(Chainstate& chainstate, const CBlock& b
}
}

if (!fJustCheck && QuorumProofIndexEnabled()) {
for (size_t i = 1; i < block.vtx.size(); ++i) {
if (block.vtx[i]->nType != TRANSACTION_QUORUM_COMMITMENT) continue;
const auto payload = GetTxPayload<CFinalCommitmentTxPayload>(*block.vtx[i]);
if (!payload || payload->commitment.IsNull()) continue;
QuorumProofData data{ProofTransaction::Build(block, i), block.GetBlockHeader()};
m_evoDb.Write(std::make_pair(DB_QUORUM_PROOF_DATA,
std::make_pair(payload->commitment.llmqType, payload->commitment.quorumHash)), data);
}
}

m_evoDb.Write(DB_BEST_BLOCK_UPGRADE, blockHash);

return true;
Expand Down Expand Up @@ -523,6 +536,8 @@ bool CQuorumBlockProcessor::UndoBlock(const Chainstate& chainstate, const CBlock
LogPrint(BCLog::LLMQ, "%s -- retaining commitment for block %s used by another chainstate\n",
__func__, pindex->GetBlockHash().ToString());
} else {
// Proof data follows the commitment's lifetime across chainstates.
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
2 changes: 2 additions & 0 deletions src/llmq/context.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

#include <bls/bls_worker.h>
#include <llmq/blockprocessor.h>
#include <llmq/quorumproofs.h>
#include <llmq/quorumsman.h>
#include <llmq/signing.h>
#include <llmq/snapshot.h>
Expand All @@ -20,6 +21,7 @@ LLMQContext::LLMQContext(CDeterministicMNManager& dmnman, CEvoDB& evo_db, Chains
std::make_unique<llmq::CQuorumBlockProcessor>(chainman, dmnman, evo_db, *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)}
{
// Have to start it early to let VerifyDB check ChainLock signatures in coinbase
Expand Down
2 changes: 2 additions & 0 deletions src/llmq/context.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class PeerManager;
namespace llmq {
class CQuorumBlockProcessor;
class CQuorumManager;
class CQuorumProofManager;
class CQuorumSnapshotManager;
class CSigningManager;
} // namespace llmq
Expand Down Expand Up @@ -45,6 +46,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;
};

Expand Down
37 changes: 37 additions & 0 deletions src/llmq/quorumproofdata.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright (c) 2025-2026 The Dash Core developers
// Distributed under the MIT software license, see the accompanying file COPYING.
#ifndef BITCOIN_LLMQ_QUORUMPROOFDATA_H
#define BITCOIN_LLMQ_QUORUMPROOFDATA_H
#include <primitives/block.h>
#include <serialize.h>
#include <uint256.h>
#include <string>
#include <vector>
namespace llmq {
inline constexpr size_t MAX_PROOF_BYTES = 1024 * 1024;
inline constexpr size_t MAX_PROOF_CERTIFICATES = 4096;
inline constexpr size_t MAX_PROOF_HEADERS = 4096;
struct ProofMerklePath {
uint32_t index{0};
uint32_t count{0};
std::vector<uint256> siblings;
SERIALIZE_METHODS(ProofMerklePath, obj) { READWRITE(obj.index, obj.count, obj.siblings); }
bool Verify(uint256 leaf, const uint256& root) const;
static ProofMerklePath Build(const std::vector<uint256>& leaves, uint32_t index);
};
struct ProofTransaction {
std::vector<unsigned char> transaction;
ProofMerklePath path;
SERIALIZE_METHODS(ProofTransaction, obj) { READWRITE(obj.transaction, obj.path); }
bool Verify(const CBlockHeader& header) const;
static ProofTransaction Build(const CBlock& block, uint32_t index);
};
struct QuorumProofData {
ProofTransaction mining;
CBlockHeader header;
SERIALIZE_METHODS(QuorumProofData, obj) { READWRITE(obj.mining, obj.header); }
};
extern const std::string DB_QUORUM_PROOF_DATA;
bool QuorumProofIndexEnabled();
} // namespace llmq
#endif // BITCOIN_LLMQ_QUORUMPROOFDATA_H
Loading