Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
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
8 changes: 8 additions & 0 deletions doc/release-notes-7107.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
New RPCs
--------

- Add `getquorumproofchain` and `verifyquorumproofchain` for compact mining-transaction
proofs from an independently trusted snapshot, plus `getchainlockbyheight` for
historical coinbase-carried certificates. The RPCs read required blocks on
demand, without an additional index or startup scan. Nodes must retain the
required historical blocks to generate proofs. (#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
83 changes: 82 additions & 1 deletion src/chainlock/clsig.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,95 @@

#include <chainlock/clsig.h>

#include <chainparams.h>
#include <chain.h>
#include <chainlock/chainlock.h>
#include <chainparams.h>
#include <evo/cbtx.h>
#include <evo/specialtx.h>
#include <llmq/quorumsman.h>
#include <node/blockstorage.h>
#include <shutdown.h>

#include <algorithm>
#include <stdexcept>
#include <string_view>

namespace chainlock {
static constexpr std::string_view CLSIG_REQUESTID_PREFIX{"clsig"};
static constexpr size_t MAX_HISTORICAL_CARRIER_READS{16384};

CoinbaseChainLockReader::CoinbaseChainLockReader(const CChain& chain) :
m_tip(chain.Tip())
{
}

std::optional<CoinbaseChainLock> CoinbaseChainLockReader::Read(int carrier_height)
{
const auto* carrier = m_tip ? m_tip->GetAncestor(carrier_height) : nullptr;
if (!carrier || carrier_height < Params().GetConsensus().V20Height) return std::nullopt;
if (const auto it = m_cache.find(carrier_height); it != m_cache.end()) return it->second;
if (ShutdownRequested()) throw std::runtime_error("ChainLock lookup interrupted");
if (m_cache.size() >= MAX_HISTORICAL_CARRIER_READS)
throw std::runtime_error("ChainLock disk-read budget exhausted");
CBlock block;
if (!node::ReadBlockFromDisk(block, carrier, Params().GetConsensus()) || block.vtx.empty()) {
throw std::runtime_error("Historical ChainLock block data unavailable");
}
const auto cb = GetTxPayload<CCbTx>(*block.vtx[0]);
if (!block.vtx[0]->IsCoinBase() || block.vtx[0]->nType != TRANSACTION_COINBASE || !cb ||
cb->nVersion < CCbTx::Version::CLSIG_AND_BALANCE || cb->nHeight != carrier_height) {
throw std::runtime_error("Invalid historical ChainLock coinbase");
}
if (cb->bestCLHeightDiff >= uint32_t(carrier_height)) {
throw std::runtime_error("Invalid historical coinbase ChainLock height");
}
const auto chainlock = GetNonNullCoinbaseChainlock(block, carrier_height);
if (!chainlock) {
return m_cache.emplace(carrier_height, std::nullopt).first->second;
}
const int height = carrier_height - int(chainlock->second) - 1;
const auto* signed_block = m_tip->GetAncestor(height);
if (!signed_block) throw std::runtime_error("Invalid historical ChainLock block height");
return m_cache
.emplace(carrier_height,
CoinbaseChainLock{ChainLockSig{height, signed_block->GetBlockHash(), chainlock->first}, carrier})
.first->second;
}
Comment thread
PastaPastaPasta marked this conversation as resolved.

std::optional<CoinbaseChainLock> CoinbaseChainLockReader::Find(int minimum_height, int maximum_height)
{
const int tip_height = m_tip ? m_tip->nHeight : -1;
if (minimum_height < 0 || minimum_height > maximum_height || minimum_height >= tip_height)
return std::nullopt;
const int first_carrier = std::max(minimum_height + 1, Params().GetConsensus().V20Height);
if (first_carrier > tip_height) return std::nullopt;

// CheckCbTxBestChainlock enforces nondecreasing certified heights and
// forbids null signatures after the first certificate on a validated chain.
auto below_minimum = [&](int height) {
const auto entry = Read(height);
return !entry || entry->clsig.getHeight() < minimum_height;
};
int low = first_carrier;
int high = first_carrier;
int64_t step = 1;
while (below_minimum(high)) {
if (high == tip_height) return std::nullopt;
low = high + 1;
high = int(std::min<int64_t>(tip_height, int64_t(high) + step));
step *= 2;
}
while (low < high) {
const int middle = low + (high - low) / 2;
if (below_minimum(middle)) {
low = middle + 1;
} else {
high = middle;
}
}
const auto entry = Read(low);
return entry && entry->clsig.getHeight() <= maximum_height ? entry : std::nullopt;
}

uint256 GenSigRequestId(const int32_t nHeight)
{
Expand Down
26 changes: 25 additions & 1 deletion src/chainlock/clsig.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@
#ifndef BITCOIN_CHAINLOCK_CLSIG_H
#define BITCOIN_CHAINLOCK_CLSIG_H

#include <chainlock/chainlock.h>

#include <cstdint>
#include <map>
#include <optional>

class CChain;
class CBlockIndex;
Expand All @@ -21,7 +25,27 @@ enum class VerifyRecSigStatus : uint8_t;
} // namespace llmq

namespace chainlock {
struct ChainLockSig;
struct CoinbaseChainLock {
ChainLockSig clsig;
const CBlockIndex* carrier{nullptr};
};

/** Reads historical signatures from a fixed, validated chain view.
* Cache lifetime is one request; missing block data throws instead of implying
* that a certificate does not exist. Disk reads do not hold cs_main.
*/
class CoinbaseChainLockReader
{
const CBlockIndex* m_tip;
std::map<int, std::optional<CoinbaseChainLock>> m_cache;

public:
explicit CoinbaseChainLockReader(const CChain& chain);
explicit CoinbaseChainLockReader(const CBlockIndex* tip) : m_tip(tip) {}
std::optional<CoinbaseChainLock> Read(int carrier_height);
/** First certificate at or above minimum_height, limited by maximum_height. */
std::optional<CoinbaseChainLock> Find(int minimum_height, int maximum_height);
};

//! Generate clsig request ID with block height
uint256 GenSigRequestId(const int32_t nHeight);
Expand Down
16 changes: 9 additions & 7 deletions src/evo/cbtx.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -170,16 +170,18 @@ std::optional<std::pair<CBLSSignature, uint32_t>> GetNonNullCoinbaseChainlock(co
return std::nullopt;
}

const CTransactionRef cbTx = block.vtx[0];
const auto opt_cbtx = GetTxPayload<CCbTx>(*cbTx);
return GetNonNullCoinbaseChainlock(block, pindex->nHeight);
}

if (!opt_cbtx.has_value()) {
std::optional<std::pair<CBLSSignature, uint32_t>> GetNonNullCoinbaseChainlock(const CBlock& block, int32_t height)
{
if (block.vtx.empty() || !block.vtx[0]->IsCoinBase() || block.vtx[0]->nType != TRANSACTION_COINBASE) {
return std::nullopt;
}

if (!opt_cbtx->bestCLSignature.IsValid()) {
const auto payload = GetTxPayload<CCbTx>(*block.vtx[0]);
if (!payload || payload->nVersion < CCbTx::Version::CLSIG_AND_BALANCE || payload->nHeight != height ||
!payload->bestCLSignature.IsValid() || payload->bestCLHeightDiff >= uint32_t(height)) {
return std::nullopt;
}

return std::make_pair(opt_cbtx->bestCLSignature, opt_cbtx->bestCLHeightDiff);
return std::make_pair(payload->bestCLSignature, payload->bestCLHeightDiff);
}
1 change: 1 addition & 0 deletions src/evo/cbtx.h
Original file line number Diff line number Diff line change
Expand Up @@ -75,5 +75,6 @@ bool CalcCbTxMerkleRootQuorums(const CBlock& block, const CBlockIndex* pindexPre
BlockValidationState& state);

std::optional<std::pair<CBLSSignature, uint32_t>> GetNonNullCoinbaseChainlock(const CBlockIndex* pindex);
std::optional<std::pair<CBLSSignature, uint32_t>> GetNonNullCoinbaseChainlock(const CBlock& block, int32_t height);

#endif // BITCOIN_EVO_CBTX_H
32 changes: 32 additions & 0 deletions src/llmq/quorumproofdata.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright (c) 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 <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);
};
} // namespace llmq
#endif // BITCOIN_LLMQ_QUORUMPROOFDATA_H
Loading
Loading