Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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