Skip to content
Open
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
7 changes: 7 additions & 0 deletions doc/release-notes-1578.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
New RPCs
--------

- The new `getnodegeneration` RPC returns a process-scoped `startup_id`, a
monotonic `chainstate_revision`, and the active tip height and hash. Callers
can use these fields to detect daemon restarts, stale responses, and active
chain ABA changes.
40 changes: 40 additions & 0 deletions src/rpc/blockchain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3757,6 +3757,45 @@ static RPCHelpMan getsidechaininfo()
};
}

UniValue NodeGenerationToJSON(const NodeGenerationSnapshot& generation)
{
UniValue result(UniValue::VOBJ);
result.pushKV("startup_id", generation.startup_id.GetHex());
result.pushKV("chainstate_revision", generation.chainstate_revision);
result.pushKV("blocks", generation.blocks);
result.pushKV("bestblockhash", generation.bestblockhash.GetHex());
return result;
}

static RPCHelpMan getnodegeneration()
{
return RPCHelpMan{"getnodegeneration",
"Returns process and active-chain generation fields for stale-response and reorganization ABA detection.\n"
"Treat a startup_id mismatch as a process restart, and use chainstate_revision changes to detect ABA.\n"
"These fields do not establish binary provenance and do not prove that the connected node is honest.\n",
{},
RPCResult{
RPCResult::Type::OBJ, "", "",
{
{RPCResult::Type::STR_HEX, "startup_id", "a cryptographically random 256-bit identifier that is constant for this daemon process"},
{RPCResult::Type::NUM, "chainstate_revision", "an unsigned counter advanced for every active-chain connect, disconnect, or active chainstate switch"},
{RPCResult::Type::NUM, "blocks", "the active chain height bound atomically to chainstate_revision"},
{RPCResult::Type::STR_HEX, "bestblockhash", "the active best block hash bound atomically to chainstate_revision"},
}},
RPCExamples{
HelpExampleCli("getnodegeneration", "")
+ HelpExampleRpc("getnodegeneration", "")
},
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
{
ChainstateManager& chainman = EnsureAnyChainman(request.context);
LOCK(chainman.GetMutex());
const NodeGenerationSnapshot generation{chainman.GetNodeGeneration()};
return NodeGenerationToJSON(generation);
},
};
}

// END ELEMENTS
//

Expand All @@ -3765,6 +3804,7 @@ void RegisterBlockchainRPCCommands(CRPCTable &t)
static const CRPCCommand commands[] =
{
{"blockchain", &getblockchaininfo},
{"blockchain", &getnodegeneration},
{"blockchain", &getchaintxstats},
{"blockchain", &getblockstats},
{"blockchain", &getbestblockhash},
Expand Down
3 changes: 3 additions & 0 deletions src/rpc/blockchain.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ UniValue blockheaderToJSON(const CBlockIndex& tip, const CBlockIndex& blockindex
/** Used by getblockstats to get feerates at different percentiles by weight */
void CalculatePercentilesByWeight(CAmount result[NUM_GETBLOCKSTATS_PERCENTILES], std::vector<std::pair<CAmount, int64_t>>& scores, int64_t total_weight);

/** Serialize an atomic node-generation snapshot without narrowing its unsigned revision. */
UniValue NodeGenerationToJSON(const NodeGenerationSnapshot& generation);

/**
* Test-only helper to create UTXO snapshots given a chainstate and a file handle.
* @return a UniValue map containing metadata about the snapshot.
Expand Down
1 change: 1 addition & 0 deletions src/test/fuzz/rpc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ const std::vector<std::string> RPC_COMMANDS_SAFE_FOR_FUZZING{
"finalizecompactblock",
"getcompactsketch",
"getnewblockhex",
"getnodegeneration",
"getpakinfo",
"getsidechaininfo",
"parsepsbt",
Expand Down
17 changes: 17 additions & 0 deletions src/test/rpc_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include <util/time.h>

#include <any>
#include <limits>

#include <boost/test/unit_test.hpp>

Expand Down Expand Up @@ -84,6 +85,22 @@ UniValue RPCTestingSetup::CallRPC(std::string args)

BOOST_FIXTURE_TEST_SUITE(rpc_tests, RPCTestingSetup)

BOOST_AUTO_TEST_CASE(node_generation_revision_json_boundary)
{
const NodeGenerationSnapshot generation{
.startup_id = uint256{},
.chainstate_revision = std::numeric_limits<uint64_t>::max(),
.blocks = 0,
.bestblockhash = uint256{},
};
const UniValue result{NodeGenerationToJSON(generation)};
const UniValue& revision{result["chainstate_revision"]};

BOOST_CHECK(revision.isNum());
BOOST_CHECK_EQUAL(revision.getValStr(), "18446744073709551615");
BOOST_CHECK_EQUAL(revision.getInt<uint64_t>(), std::numeric_limits<uint64_t>::max());
}

BOOST_AUTO_TEST_CASE(rpc_namedparams)
{
const std::vector<std::pair<std::string, bool>> arg_names{{"arg1", false}, {"arg2", false}, {"arg3", false}, {"arg4", false}, {"arg5", false}};
Expand Down
28 changes: 28 additions & 0 deletions src/test/validation_chainstatemanager_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager, TestChain100Setup)
Chainstate& c1 = manager.ActiveChainstate();
chainstates.push_back(&c1);

const NodeGenerationSnapshot initial_generation{
WITH_LOCK(manager.GetMutex(), return manager.GetNodeGeneration())};
BOOST_CHECK_GT(initial_generation.chainstate_revision, 0U);

BOOST_CHECK(!manager.IsSnapshotActive());
BOOST_CHECK(WITH_LOCK(::cs_main, return !manager.IsSnapshotValidated()));
auto all = manager.GetAll();
Expand All @@ -59,6 +63,10 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager, TestChain100Setup)

// Get to a valid assumeutxo tip (per chainparams);
mineBlocks(10);
const NodeGenerationSnapshot mined_generation{
WITH_LOCK(manager.GetMutex(), return manager.GetNodeGeneration())};
BOOST_CHECK_EQUAL(mined_generation.startup_id, initial_generation.startup_id);
BOOST_CHECK_EQUAL(mined_generation.chainstate_revision, initial_generation.chainstate_revision + 10);
BOOST_CHECK_EQUAL(WITH_LOCK(manager.GetMutex(), return manager.ActiveHeight()), 110);
auto active_tip = WITH_LOCK(manager.GetMutex(), return manager.ActiveTip());
auto exp_tip = c1.m_chain.Tip();
Expand All @@ -83,6 +91,11 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager, TestChain100Setup)
BlockValidationState _;
BOOST_CHECK(c2.ActivateBestChain(_, nullptr));

const NodeGenerationSnapshot snapshot_generation{
WITH_LOCK(manager.GetMutex(), return manager.GetNodeGeneration())};
BOOST_CHECK_EQUAL(snapshot_generation.startup_id, initial_generation.startup_id);
BOOST_CHECK_EQUAL(snapshot_generation.chainstate_revision, mined_generation.chainstate_revision + 1);

BOOST_CHECK_EQUAL(manager.SnapshotBlockhash().value(), snapshot_blockhash);
BOOST_CHECK(manager.IsSnapshotActive());
BOOST_CHECK(WITH_LOCK(::cs_main, return !manager.IsSnapshotValidated()));
Expand All @@ -96,6 +109,9 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager, TestChain100Setup)

BOOST_CHECK_EQUAL(WITH_LOCK(manager.GetMutex(), return manager.ActiveHeight()), 110);
mineBlocks(1);
const NodeGenerationSnapshot snapshot_mined_generation{
WITH_LOCK(manager.GetMutex(), return manager.GetNodeGeneration())};
BOOST_CHECK_EQUAL(snapshot_mined_generation.chainstate_revision, snapshot_generation.chainstate_revision + 1);
BOOST_CHECK_EQUAL(WITH_LOCK(manager.GetMutex(), return manager.ActiveHeight()), 111);
BOOST_CHECK_EQUAL(WITH_LOCK(manager.GetMutex(), return c1.m_chain.Height()), 110);

Expand Down Expand Up @@ -574,6 +590,8 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_snapshot_init, SnapshotTestSetup)
BOOST_CHECK(chainman.IsSnapshotActive());
const uint256 snapshot_tip_hash = WITH_LOCK(chainman.GetMutex(),
return chainman.ActiveTip()->GetBlockHash());
const NodeGenerationSnapshot generation_before_background_disconnect{
WITH_LOCK(chainman.GetMutex(), return chainman.GetNodeGeneration())};

auto all_chainstates = chainman.GetAll();
BOOST_CHECK_EQUAL(all_chainstates.size(), 2);
Expand All @@ -591,6 +609,12 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_snapshot_init, SnapshotTestSetup)
unused_pool.clear(); // to avoid queuedTx assertion errors on teardown
}
BOOST_CHECK_EQUAL(bg_chainstate.m_chain.Height(), 109);
const NodeGenerationSnapshot generation_after_background_disconnect{
WITH_LOCK(chainman.GetMutex(), return chainman.GetNodeGeneration())};
BOOST_CHECK_EQUAL(generation_after_background_disconnect.startup_id, generation_before_background_disconnect.startup_id);
BOOST_CHECK_EQUAL(generation_after_background_disconnect.chainstate_revision, generation_before_background_disconnect.chainstate_revision);
BOOST_CHECK_EQUAL(generation_after_background_disconnect.blocks, generation_before_background_disconnect.blocks);
BOOST_CHECK_EQUAL(generation_after_background_disconnect.bestblockhash, generation_before_background_disconnect.bestblockhash);

// Test that simulating a shutdown (resetting ChainstateManager) and then performing
// chainstate reinitializing successfully cleans up the background-validation
Expand All @@ -610,6 +634,10 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_snapshot_init, SnapshotTestSetup)

BOOST_CHECK_EQUAL(chainman_restarted.ActiveTip()->GetBlockHash(), snapshot_tip_hash);
BOOST_CHECK_EQUAL(chainman_restarted.ActiveHeight(), 210);

const NodeGenerationSnapshot restarted_generation{chainman_restarted.GetNodeGeneration()};
BOOST_CHECK_EQUAL(restarted_generation.startup_id, generation_before_background_disconnect.startup_id);
BOOST_CHECK_GT(restarted_generation.chainstate_revision, generation_before_background_disconnect.chainstate_revision);
}

BOOST_TEST_MESSAGE(
Expand Down
78 changes: 72 additions & 6 deletions src/validation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,27 @@ const std::vector<std::string> CHECKLEVEL_DOC {
* */
static constexpr int PRUNE_LOCK_BUFFER{10};

namespace {

/**
* Generation state belongs to the daemon process rather than a datadir or a
* ChainstateManager instance. The function-local static ensures that retries
* which reconstruct the chain manager in one process retain the same startup
* identifier and monotonically increasing revision.
*/
struct ProcessNodeGeneration {
const uint256 startup_id{GetRandHash()};
uint64_t chainstate_revision GUARDED_BY(::cs_main){0};
};

ProcessNodeGeneration& GetProcessNodeGeneration()
{
static ProcessNodeGeneration generation;
return generation;
}

} // namespace

TRACEPOINT_SEMAPHORE(validation, block_connected);
TRACEPOINT_SEMAPHORE(utxocache, flush);
TRACEPOINT_SEMAPHORE(mempool, replaced);
Expand Down Expand Up @@ -3588,6 +3609,7 @@ bool Chainstate::DisconnectTip(BlockValidationState& state, DisconnectedBlockTra
}

m_chain.SetTip(*pindexDelete->pprev);
m_chainman.NotifyChainstateMutation(*this);

UpdateTip(pindexDelete->pprev);
// Let wallets know transactions went from 1-confirmed to
Expand Down Expand Up @@ -3730,6 +3752,7 @@ bool Chainstate::ConnectTip(BlockValidationState& state, CBlockIndex* pindexNew,
}
// Update m_chain & related variables.
m_chain.SetTip(*pindexNew);
m_chainman.NotifyChainstateMutation(*this);
UpdateTip(pindexNew);

const auto time_6{SteadyClock::now()};
Expand Down Expand Up @@ -6308,14 +6331,53 @@ std::vector<Chainstate*> ChainstateManager::GetAll()
return out;
}

void ChainstateManager::AdvanceChainstateRevision()
{
AssertLockHeld(::cs_main);
ProcessNodeGeneration& generation{GetProcessNodeGeneration()};
// Fail-stop rather than wrap because reuse would break ABA detection.
Assert(generation.chainstate_revision != std::numeric_limits<uint64_t>::max());
++generation.chainstate_revision;
}

void ChainstateManager::NotifyChainstateMutation(const Chainstate& chainstate)
{
AssertLockHeld(::cs_main);
if (&chainstate == m_active_chainstate) {
AdvanceChainstateRevision();
}
}

void ChainstateManager::SetActiveChainstate(Chainstate* chainstate)
{
AssertLockHeld(::cs_main);
if (chainstate != nullptr && chainstate != m_active_chainstate) {
AdvanceChainstateRevision();
}
m_active_chainstate = chainstate;
}

NodeGenerationSnapshot ChainstateManager::GetNodeGeneration() const
{
AssertLockHeld(::cs_main);
const CBlockIndex& tip{*Assert(Assert(m_active_chainstate)->m_chain.Tip())};
const ProcessNodeGeneration& generation{GetProcessNodeGeneration()};
return {
generation.startup_id,
generation.chainstate_revision,
tip.nHeight,
tip.GetBlockHash(),
};
}

Chainstate& ChainstateManager::InitializeChainstate(CTxMemPool* mempool)
{
AssertLockHeld(::cs_main);
assert(!m_ibd_chainstate);
assert(!m_active_chainstate);

m_ibd_chainstate = std::make_unique<Chainstate>(mempool, m_blockman, *this);
m_active_chainstate = m_ibd_chainstate.get();
SetActiveChainstate(m_ibd_chainstate.get());
return *m_active_chainstate;
}

Expand Down Expand Up @@ -6500,7 +6562,7 @@ util::Result<CBlockIndex*> ChainstateManager::ActivateSnapshot(
Assert(!m_snapshot_chainstate->m_mempool);
m_snapshot_chainstate->m_mempool = m_active_chainstate->m_mempool;
m_active_chainstate->m_mempool = nullptr;
m_active_chainstate = m_snapshot_chainstate.get();
SetActiveChainstate(m_snapshot_chainstate.get());
m_blockman.m_snapshot_height = this->GetSnapshotBaseHeight();

LogPrintf("[snapshot] successfully activated snapshot %s\n", base_blockhash.ToString());
Expand Down Expand Up @@ -6794,7 +6856,7 @@ SnapshotCompletionResult ChainstateManager::MaybeCompleteSnapshotValidation()
LogError("[snapshot] !!! %s\n", user_error.original);
LogError("[snapshot] deleting snapshot, reverting to validated chain, and stopping node\n");

m_active_chainstate = m_ibd_chainstate.get();
SetActiveChainstate(m_ibd_chainstate.get());
m_snapshot_chainstate->m_disabled = true;
assert(!this->IsUsable(m_snapshot_chainstate.get()));
assert(this->IsUsable(m_ibd_chainstate.get()));
Expand Down Expand Up @@ -6934,9 +6996,9 @@ void ChainstateManager::MaybeRebalanceCaches()

void ChainstateManager::ResetChainstates()
{
SetActiveChainstate(nullptr);
m_ibd_chainstate.reset();
m_snapshot_chainstate.reset();
m_active_chainstate = nullptr;
}

/**
Expand All @@ -6959,6 +7021,10 @@ ChainstateManager::ChainstateManager(const util::SignalInterrupt& interrupt, Opt
m_blockman{interrupt, std::move(blockman_options)},
m_validation_cache{m_options.script_execution_cache_bytes, m_options.signature_cache_bytes}
{
// The daemon constructs kernel::Context (which calls RandomInit) before it
// constructs the chain manager. Force the process generation here, before
// RPC warmup can finish. GetRandHash itself also obtains strong randomness.
(void)GetProcessNodeGeneration();
}

ChainstateManager::~ChainstateManager()
Expand Down Expand Up @@ -6998,7 +7064,7 @@ Chainstate& ChainstateManager::ActivateExistingSnapshot(uint256 base_blockhash)
Assert(!m_snapshot_chainstate->m_mempool);
m_snapshot_chainstate->m_mempool = m_active_chainstate->m_mempool;
m_active_chainstate->m_mempool = nullptr;
m_active_chainstate = m_snapshot_chainstate.get();
SetActiveChainstate(m_snapshot_chainstate.get());
return *m_snapshot_chainstate;
}

Expand Down Expand Up @@ -7070,7 +7136,7 @@ bool ChainstateManager::DeleteSnapshotChainstate()
fs::PathToString(snapshot_datadir));
return false;
}
m_active_chainstate = m_ibd_chainstate.get();
SetActiveChainstate(m_ibd_chainstate.get());
m_active_chainstate->m_mempool = m_snapshot_chainstate->m_mempool;
m_snapshot_chainstate.reset();
return true;
Expand Down
21 changes: 21 additions & 0 deletions src/validation.h
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,15 @@ class DisconnectedBlockTransactions;
struct PrecomputedTransactionData;
struct LockPoints;
struct AssumeutxoData;

/** Process and active-chain generation fields returned by getnodegeneration. */
struct NodeGenerationSnapshot {
uint256 startup_id;
uint64_t chainstate_revision;
int blocks;
uint256 bestblockhash;
};

namespace node {
class SnapshotMetadata;
} // namespace node
Expand Down Expand Up @@ -959,6 +968,15 @@ class ChainstateManager
return cs && !cs->m_disabled;
}

/** Advance the process-local active-chain revision without allowing wraparound. */
void AdvanceChainstateRevision() EXCLUSIVE_LOCKS_REQUIRED(::cs_main);

/** Record a successful chain mutation if it affected the active chainstate. */
void NotifyChainstateMutation(const Chainstate& chainstate) EXCLUSIVE_LOCKS_REQUIRED(::cs_main);

/** Replace the active chainstate and record the newly exposed state. */
void SetActiveChainstate(Chainstate* chainstate) EXCLUSIVE_LOCKS_REQUIRED(::cs_main);

//! A queue for script verifications that have to be performed by worker threads.
CCheckQueue<CCheck> m_script_check_queue;

Expand Down Expand Up @@ -1125,6 +1143,9 @@ class ChainstateManager
int ActiveHeight() const EXCLUSIVE_LOCKS_REQUIRED(GetMutex()) { return ActiveChain().Height(); }
CBlockIndex* ActiveTip() const EXCLUSIVE_LOCKS_REQUIRED(GetMutex()) { return ActiveChain().Tip(); }

/** Atomically snapshot the process generation and active chain tip. */
NodeGenerationSnapshot GetNodeGeneration() const EXCLUSIVE_LOCKS_REQUIRED(::cs_main);

//! The state of a background sync (for net processing)
bool BackgroundSyncInProgress() const EXCLUSIVE_LOCKS_REQUIRED(GetMutex()) {
return IsUsable(m_snapshot_chainstate.get()) && IsUsable(m_ibd_chainstate.get());
Expand Down
Loading