diff --git a/doc/design/assumeutxo.md b/doc/design/assumeutxo.md index d02930538606..4f3355c35433 100644 --- a/doc/design/assumeutxo.md +++ b/doc/design/assumeutxo.md @@ -1,7 +1,7 @@ # assumeutxo Assumeutxo is a feature that allows fast bootstrapping of a validating dashd -instance with a very similar security model to assumevalid. +instance. ## Loading a snapshot @@ -9,12 +9,19 @@ There is currently no canonical source for snapshots, but any downloaded snapsho will be checked against a hash that's been hardcoded in source code. Once you've obtained the snapshot, you can use the RPC command `loadtxoutset` -(yet to be merged) to load it. +to load it. `loadtxoutset` and `getchainstates` are hidden and available on +regtest only. + +A masternode does not load snapshots, and a node does not start with +`-masternodeblsprivkey` while it has a snapshot chainstate, validated or not, +until a restart has promoted it. ### Pruning -A pruned node can load a snapshot. To save space, it's possible to delete the -snapshot file as soon as `loadtxoutset` finishes. +A pruned node cannot load a snapshot, and a node does not start with `-prune` +while it has a snapshot chainstate, validated or not, until a restart has +promoted it. It's possible to delete the snapshot file as soon as +`loadtxoutset` finishes. The minimum `-dbcache` setting is 550 MiB, but this functionality ignores that minimum and uses at least 1100 MiB. @@ -25,10 +32,17 @@ downloaded snapshot). ### Indexes -Indexes work but don't take advantage of this feature. They always start building +A node with `-addressindex`, `-spentindex` or `-timestampindex` cannot load a +snapshot, and does not start with them while it has a snapshot chainstate. + +Other indexes work but don't take advantage of this feature. They always start building from the genesis block. Once the background validation reaches the snapshot block, indexes will continue to build all the way to the tip. +Governance waits until background validation has completed and the governance +collateral index has caught up: until then the node neither processes nor relays +governance objects, and `gobject prepare` and `gobject submit` refuse. + For indexes that support pruning, note that no pruning will take place between the snapshot and the tip, until the background sync has completed - after which everything is pruned. Depending on how old the snapshot is, this may temporarily diff --git a/doc/release-notes-7593.md b/doc/release-notes-7593.md new file mode 100644 index 000000000000..79db45766e96 --- /dev/null +++ b/doc/release-notes-7593.md @@ -0,0 +1,16 @@ +Updated RPCs +------------ + +- On a pruned node, `quorum info`, `quorum memberof` and `quorum selectquorum` + fail with `RPC_INTERNAL_ERROR` for a quorum whose members this node cannot + compute because the block that fixes them was pruned. They used to work with + members computed from a substitute value, which could be wrong. (#7593) + +Other changes +------------- + +- A pruned node no longer substitutes the block hash for the quorum score + modifier of a pruned work block. It keeps such a quorum's public key, from + its mined commitment, without members, so it still verifies signatures + by that quorum (for example on masternode hard-fork signals) and never + stalls on them. (#7593) diff --git a/doc/release-notes-7887.md b/doc/release-notes-7887.md new file mode 100644 index 000000000000..b07f910c4b77 --- /dev/null +++ b/doc/release-notes-7887.md @@ -0,0 +1,39 @@ +Updated RPCs +------------ + +- An RPC that fails with an error because this node cannot read the + masternode, quorum or other Evo state it needs, for example because the + block data it is derived from was pruned, now returns error code + `RPC_INTERNAL_ERROR` (-32603) instead of `RPC_MISC_ERROR` (-1). The request + was not at fault. The package RPCs report such a failure as described + below. (#7887) + +- `testmempoolaccept` with more than one transaction reports such a failure as + the `package-error` `evo-local-unavailable`, and `submitpackage` fails with + that reason. (#7887) + +Other changes +------------- + +- A block this node has stored but cannot read back while deriving the credit + pool or the EHF signals no longer makes the block being validated invalid. + The node shuts down with a "Failed to read block" error instead, as it does + when it cannot read a block it is connecting. (#7887) + +- On a node without an assumeutxo snapshot, a warning about Evo state that + cannot be derived because block data was pruned now says so and suggests + restarting with `-reindex`. Startup still fails when the EHF signals at the + tip cannot be rebuilt. (#7887) + +- On a pruned node, a block whose masternode, quorum or other Evo state + depends on block data that was pruned is no longer marked invalid, which + left the node on a local fork until `reconsiderblock`. The node now stops + advancing at that block, logs an error and shows a warning that block data + was pruned; restart with `-reindex` to download it again. (#7887) + +- Developers: on regtest, every Evo database access must now be bound to a + chainstate's view (the hidden `-checkevobinding` option, on by default + there). A code path that reads Evo state without a binding fails with an + internal error, a dropped task or a stalled chainstate instead of reading + the default view. Local regtest networks run with it unless started with + `-checkevobinding=0`. (#7887) diff --git a/doc/zmq.md b/doc/zmq.md index d9e2709b2f81..3fd715d7762d 100644 --- a/doc/zmq.md +++ b/doc/zmq.md @@ -141,11 +141,11 @@ Where the 8-byte uints correspond to the mempool sequence number. | hashtx | <32-byte transaction hash in Little Endian> | -`rawblock`: Notifies when the chain tip is updated. Messages are ZMQ multipart messages with three parts. The first part is the topic (`rawblock`), the second part is the serialized block, and the last part is a sequence number (representing the message count to detect lost messages). +`rawblock`: Notifies when the chain tip is updated. When assumeutxo is in use, this notification will not be issued for historical blocks connected to the background validation chainstate. Messages are ZMQ multipart messages with three parts. The first part is the topic (`rawblock`), the second part is the serialized block, and the last part is a sequence number (representing the message count to detect lost messages). | rawblock | | -`hashblock`: Notifies when the chain tip is updated. Messages are ZMQ multipart messages with three parts. The first part is the topic (`hashblock`), the second part is the 32-byte block hash, and the last part is a sequence number (representing the message count to detect lost messages). +`hashblock`: Notifies when the chain tip is updated. When assumeutxo is in use, this notification will not be issued for historical blocks connected to the background validation chainstate. Messages are ZMQ multipart messages with three parts. The first part is the topic (`hashblock`), the second part is the 32-byte block hash, and the last part is a sequence number (representing the message count to detect lost messages). | hashblock | <32-byte block hash in Little Endian> | diff --git a/src/Makefile.am b/src/Makefile.am index 9532eb185852..f751fa6d49b9 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -247,10 +247,13 @@ BITCOIN_CORE_H = \ evo/simplifiedmns.h \ evo/smldiff.h \ evo/snapshot.h \ + evo/snapshot_config.h \ + evo/snapshot_types.h \ evo/specialtx.h \ evo/specialtx_filter.h \ evo/specialtxman.h \ evo/types.h \ + evo/viewcache.h \ external_signer.h \ flat-database.h \ flatfile.h \ @@ -581,6 +584,8 @@ libbitcoin_node_a_SOURCES = \ evo/simplifiedmns.cpp \ evo/smldiff.cpp \ evo/snapshot.cpp \ + evo/snapshot_chain.cpp \ + evo/snapshot_config.cpp \ evo/specialtx.cpp \ evo/specialtx_filter.cpp \ evo/specialtxman.cpp \ @@ -1322,6 +1327,9 @@ libdashkernel_la_SOURCES = \ evo/providertx_util.cpp \ evo/simplifiedmns.cpp \ evo/smldiff.cpp \ + evo/snapshot.cpp \ + evo/snapshot_chain.cpp \ + evo/snapshot_config.cpp \ evo/specialtx.cpp \ evo/specialtx_filter.cpp \ evo/specialtxman.cpp \ diff --git a/src/Makefile.test_util.include b/src/Makefile.test_util.include index 91a9ca2788dc..fa9d1013f13c 100644 --- a/src/Makefile.test_util.include +++ b/src/Makefile.test_util.include @@ -11,6 +11,7 @@ TEST_UTIL_H = \ test/util/blockfilter.h \ test/util/chainstate.h \ test/util/coins.h \ + test/util/evo_snapshot.h \ test/util/json.h \ test/util/index.h \ test/util/llmq_tests.h \ diff --git a/src/active/dkgsessionhandler.cpp b/src/active/dkgsessionhandler.cpp index 1d540275efda..870800125171 100644 --- a/src/active/dkgsessionhandler.cpp +++ b/src/active/dkgsessionhandler.cpp @@ -17,6 +17,8 @@ #include #include +#include + namespace llmq { ActiveDKGSessionHandler::ActiveDKGSessionHandler( CBLSWorker& bls_worker, CDeterministicMNManager& dmnman, CMasternodeMetaMan& mn_metaman, @@ -114,8 +116,18 @@ void ActiveDKGSessionHandler::WaitForNextPhase(std::optional curPha { LogPrint(BCLog::LLMQ_DKG, "ActiveDKGSessionHandler::%s -- %s qi[%d] - starting, curPhase=%d, nextPhase=%d\n", __func__, params.name, quorumIndex, curPhase.has_value() ? std23::to_underlying(*curPhase) : -1, std23::to_underlying(nextPhase)); + bool logged_snapshot_wait{false}; while (true) { if (m_chainman.IsSnapshotActiveAndUnvalidated()) { + if (!curPhase.has_value() && !stopRequested) { + // No round can start before background validation completes, so there is nothing to abort + if (!std::exchange(logged_snapshot_wait, true)) { + LogPrint(BCLog::LLMQ_DKG, "%s -- %s qi[%d] - waiting for snapshot background validation\n", + __func__, params.name, quorumIndex); + } + UninterruptibleSleep(std::chrono::milliseconds{100}); + continue; + } LogPrint(BCLog::LLMQ_DKG, "ActiveDKGSessionHandler::%s -- %s qi[%d] - aborting because snapshot background validation is incomplete\n", __func__, params.name, quorumIndex); throw AbortPhaseException(); } diff --git a/src/bench/wallet_create_tx.cpp b/src/bench/wallet_create_tx.cpp index 67b1dd1d904d..04f9f7196576 100644 --- a/src/bench/wallet_create_tx.cpp +++ b/src/bench/wallet_create_tx.cpp @@ -71,7 +71,7 @@ void generateFakeBlock(const CChainParams& params, // notify wallet const auto& pindex = WITH_LOCK(::cs_main, return context.chainman->ActiveChain().Tip()); - wallet.blockConnected(kernel::MakeBlockInfo(pindex, &block)); + wallet.blockConnected(ChainstateRole::NORMAL, kernel::MakeBlockInfo(pindex, &block)); } struct PreSelectInputs { diff --git a/src/bls/bls.cpp b/src/bls/bls.cpp index 96a188e2ae93..1a8bab710d83 100644 --- a/src/bls/bls.cpp +++ b/src/bls/bls.cpp @@ -15,9 +15,40 @@ #include namespace bls { - std::atomic bls_legacy_scheme = std::atomic(true); +std::atomic bls_legacy_scheme = std::atomic(true); + +namespace { +//! Trivially destructible, like util/threadnames.cpp's thread-local name. +thread_local std::optional g_thread_legacy_scheme; +} // namespace + +bool EffectiveLegacyScheme() { return g_thread_legacy_scheme ? *g_thread_legacy_scheme : bls_legacy_scheme.load(); } + +void SetEffectiveLegacyScheme(bool legacy) +{ + if (g_thread_legacy_scheme) { + g_thread_legacy_scheme = legacy; + } else { + bls_legacy_scheme.store(legacy); + } } +ScopedLegacySchemeOverride::ScopedLegacySchemeOverride(bool legacy) noexcept : + m_saved{g_thread_legacy_scheme} +{ + g_thread_legacy_scheme = legacy; +} + +ScopedLegacySchemeOverride::~ScopedLegacySchemeOverride() noexcept +{ + if (!m_committed || !m_saved) { + g_thread_legacy_scheme = m_saved; + } +} + +bool ScopedLegacySchemeOverride::Active() noexcept { return g_thread_legacy_scheme.has_value(); } +} // namespace bls + static const std::unique_ptr pSchemeLegacy{std::make_unique()}; static const std::unique_ptr pScheme(std::make_unique()); @@ -145,7 +176,7 @@ void CBLSPublicKey::AggregateInsecure(const CBLSPublicKey& o) { assert(IsValid() && o.IsValid()); try { - impl = Scheme(bls::bls_legacy_scheme.load())->Aggregate({impl, o.impl}); + impl = Scheme(bls::EffectiveLegacyScheme())->Aggregate({impl, o.impl}); } catch (...) { fValid = false; } @@ -166,7 +197,7 @@ CBLSPublicKey CBLSPublicKey::AggregateInsecure(Span pks) CBLSPublicKey ret; try { - ret.impl = Scheme(bls::bls_legacy_scheme.load())->Aggregate(vecPublicKeys); + ret.impl = Scheme(bls::EffectiveLegacyScheme())->Aggregate(vecPublicKeys); ret.fValid = true; } catch (...) { ret.fValid = false; @@ -230,7 +261,7 @@ void CBLSSignature::AggregateInsecure(const CBLSSignature& o) { assert(IsValid() && o.IsValid()); try { - impl = Scheme(bls::bls_legacy_scheme.load())->Aggregate({impl, o.impl}); + impl = Scheme(bls::EffectiveLegacyScheme())->Aggregate({impl, o.impl}); } catch (...) { fValid = false; } @@ -251,7 +282,7 @@ CBLSSignature CBLSSignature::AggregateInsecure(Span sigs) CBLSSignature ret; try { - ret.impl = Scheme(bls::bls_legacy_scheme.load())->Aggregate(v); + ret.impl = Scheme(bls::EffectiveLegacyScheme())->Aggregate(v); ret.fValid = true; } catch (...) { ret.fValid = false; @@ -283,7 +314,7 @@ CBLSSignature CBLSSignature::AggregateSecure(Span sigs, CBLSSignature ret; try { - ret.impl = Scheme(bls::bls_legacy_scheme.load())->AggregateSecure(vecPublicKeys, vecSignatures, bls::Bytes(hash.begin(), hash.size())); + ret.impl = Scheme(bls::EffectiveLegacyScheme())->AggregateSecure(vecPublicKeys, vecSignatures, bls::Bytes(hash.begin(), hash.size())); ret.fValid = true; } catch (...) { ret.fValid = false; @@ -322,10 +353,16 @@ bool CBLSSignature::VerifyInsecure(const CBLSPublicKey& pubKey, const uint256& h bool CBLSSignature::VerifyInsecure(const CBLSPublicKey& pubKey, const uint256& hash) const { - return VerifyInsecure(pubKey, hash, bls::bls_legacy_scheme.load()); + return VerifyInsecure(pubKey, hash, bls::EffectiveLegacyScheme()); } bool CBLSSignature::VerifyInsecureAggregated(Span pubKeys, Span hashes) const +{ + return VerifyInsecureAggregated(pubKeys, hashes, bls::EffectiveLegacyScheme()); +} + +bool CBLSSignature::VerifyInsecureAggregated(Span pubKeys, Span hashes, + const bool specificLegacyScheme) const { if (!IsValid()) { return false; @@ -346,13 +383,18 @@ bool CBLSSignature::VerifyInsecureAggregated(Span pubKeys, SpanAggregateVerify(pubKeyVec, hashes2, impl); + return Scheme(specificLegacyScheme)->AggregateVerify(pubKeyVec, hashes2, impl); } catch (...) { return false; } } bool CBLSSignature::VerifySecureAggregated(Span pks, const uint256& hash) const +{ + return VerifySecureAggregated(pks, hash, bls::EffectiveLegacyScheme()); +} + +bool CBLSSignature::VerifySecureAggregated(Span pks, const uint256& hash, const bool specificLegacyScheme) const { if (pks.empty()) { return false; @@ -365,7 +407,7 @@ bool CBLSSignature::VerifySecureAggregated(Span pks, const uint25 } try { - return Scheme(bls::bls_legacy_scheme.load())->VerifySecure(vecPublicKeys, impl, bls::Bytes(hash.begin(), hash.size())); + return Scheme(specificLegacyScheme)->VerifySecure(vecPublicKeys, impl, bls::Bytes(hash.begin(), hash.size())); } catch (...) { return false; } diff --git a/src/bls/bls.h b/src/bls/bls.h index 82abc2c6de04..023b927312cb 100644 --- a/src/bls/bls.h +++ b/src/bls/bls.h @@ -24,11 +24,47 @@ #include #include #include +#include #include namespace bls { - extern std::atomic bls_legacy_scheme; -} +//! Process-wide default scheme of BLS operations that are not given one. It +//! follows the active chainstate's tip only: other threads read it outside +//! cs_main, so work for any other chain context (a background chainstate, a dry +//! run) sets a ScopedLegacySchemeOverride instead. +extern std::atomic bls_legacy_scheme; + +//! The scheme of BLS operations on this thread that are not given one: the +//! innermost ScopedLegacySchemeOverride if one is set, else bls_legacy_scheme. +bool EffectiveLegacyScheme(); + +//! Switches EffectiveLegacyScheme(): this thread's override if one is set, +//! else bls_legacy_scheme. +void SetEffectiveLegacyScheme(bool legacy); + +//! Sets EffectiveLegacyScheme() on this thread only, for the object's lifetime. +//! Work handed to another thread does not see it and must capture the scheme. +class ScopedLegacySchemeOverride +{ +public: + explicit ScopedLegacySchemeOverride(bool legacy) noexcept; + ~ScopedLegacySchemeOverride() noexcept; + + ScopedLegacySchemeOverride(const ScopedLegacySchemeOverride&) = delete; + ScopedLegacySchemeOverride& operator=(const ScopedLegacySchemeOverride&) = delete; + + //! Leaves the scheme in force at scope exit to the enclosing override. The + //! outermost override always ends with its scope. + void Commit() noexcept { m_committed = true; } + + //! Whether an override is set on this thread. + static bool Active() noexcept; + +private: + const std::optional m_saved; + bool m_committed{false}; +}; +} // namespace bls // reversed BLS12-381 constexpr int BLS_CURVE_ID_SIZE{32}; @@ -177,7 +213,7 @@ class CBLSWrapper template inline void Serialize(Stream& s) const { - Serialize(s, bls::bls_legacy_scheme.load()); + Serialize(s, bls::EffectiveLegacyScheme()); } template @@ -205,7 +241,7 @@ class CBLSWrapper template inline void Unserialize(Stream& s) { - Unserialize(s, bls::bls_legacy_scheme.load()); + Unserialize(s, bls::EffectiveLegacyScheme()); } inline bool CheckMalleable(Span vecBytes, const bool specificLegacyScheme) const @@ -228,7 +264,7 @@ class CBLSWrapper inline std::string ToString() const { - return ToString(bls::bls_legacy_scheme.load()); + return ToString(bls::EffectiveLegacyScheme()); } }; @@ -365,8 +401,12 @@ class CBLSSignature : public CBLSWrapper pubKeys, Span hashes, + const bool specificLegacyScheme) const; [[nodiscard]] bool VerifyInsecureAggregated(Span pubKeys, Span hashes) const; + [[nodiscard]] bool VerifySecureAggregated(Span pks, const uint256& hash, + const bool specificLegacyScheme) const; [[nodiscard]] bool VerifySecureAggregated(Span pks, const uint256& hash) const; bool Recover(Span sigs, Span ids); @@ -410,7 +450,7 @@ class CBLSLazyWrapper public: CBLSLazyWrapper() : - bufLegacyScheme(bls::bls_legacy_scheme.load()) + bufLegacyScheme(bls::EffectiveLegacyScheme()) {} explicit CBLSLazyWrapper(const CBLSLazyWrapper& r) diff --git a/src/bls/bls_batchverifier.h b/src/bls/bls_batchverifier.h index 647d38e675e3..843fe85cfcbf 100644 --- a/src/bls/bls_batchverifier.h +++ b/src/bls/bls_batchverifier.h @@ -47,6 +47,8 @@ class CBLSBatchVerifier bool secureVerification; bool perMessageFallback; size_t subBatchSize; + //! Taken from the constructing thread, so a batch verifies under one scheme wherever Verify() runs + const bool legacyScheme; MessageMap messages; MessagesBySourceMap messagesBySource; @@ -61,7 +63,8 @@ class CBLSBatchVerifier CBLSBatchVerifier(bool _secureVerification, bool _perMessageFallback, size_t _subBatchSize = 0) : secureVerification(_secureVerification), perMessageFallback(_perMessageFallback), - subBatchSize(_subBatchSize) + subBatchSize(_subBatchSize), + legacyScheme(bls::EffectiveLegacyScheme()) { } @@ -135,7 +138,7 @@ class CBLSBatchVerifier } const auto& msg = msgIt->second; - if (!msg.sig.VerifyInsecure(msg.pubKey, msg.msgHash)) { + if (!msg.sig.VerifyInsecure(msg.pubKey, msg.msgHash, legacyScheme)) { badMessages.emplace(msg.msgId); } } @@ -201,7 +204,7 @@ class CBLSBatchVerifier } CBLSSignature aggSig = CBLSSignature::AggregateInsecure(sigsToAggregate); - return aggSig.VerifyInsecureAggregated(pubKeys, msgHashes); + return aggSig.VerifyInsecureAggregated(pubKeys, msgHashes, legacyScheme); } bool VerifyBatchSecure(std::map>& byMessageHash) @@ -251,7 +254,7 @@ class CBLSBatchVerifier assert(!msgHashes.empty()); CBLSSignature aggSig = CBLSSignature::AggregateInsecure(sigsToAggregate); - return aggSig.VerifyInsecureAggregated(pubKeys, msgHashes); + return aggSig.VerifyInsecureAggregated(pubKeys, msgHashes, legacyScheme); } }; diff --git a/src/bls/bls_worker.cpp b/src/bls/bls_worker.cpp index be8bef23226e..7266cc788ec5 100644 --- a/src/bls/bls_worker.cpp +++ b/src/bls/bls_worker.cpp @@ -825,8 +825,8 @@ std::set CBLSWorker::FindVerificationVectorsWithSharedCoefficients(Span< void CBLSWorker::AsyncSign(const CBLSSecretKey& secKey, const uint256& msgHash, const CBLSWorker::SignDoneCallback& doneCallback) { - workerPool.push([secKey, msgHash, doneCallback](int threadId) { - doneCallback(secKey.Sign(msgHash, bls::bls_legacy_scheme.load())); + workerPool.push([secKey, msgHash, doneCallback, legacy = bls::EffectiveLegacyScheme()](int threadId) { + doneCallback(secKey.Sign(msgHash, legacy)); }); } @@ -838,18 +838,19 @@ void CBLSWorker::AsyncVerifySig(const CBLSSignature& sig, const CBLSPublicKey& p return; } + const bool legacy{bls::EffectiveLegacyScheme()}; std::unique_lock l(sigVerifyMutex); bool foundDuplicate = std::ranges::any_of(sigVerifyQueue, [&msgHash](const auto& job) { return job.msgHash == msgHash; }); - if (foundDuplicate) { - // batched/aggregated verification does not allow duplicate hashes, so we push what we currently have and start - // with a fresh batch + if (foundDuplicate || (!sigVerifyQueue.empty() && sigVerifyQueue.front().legacyScheme != legacy)) { + // batched/aggregated verification does not allow duplicate hashes or mixed schemes, so we push what we + // currently have and start with a fresh batch PushSigVerifyBatch(); } - sigVerifyQueue.emplace_back(std::move(doneCallback), std::move(cancelCond), sig, pubKey, msgHash); + sigVerifyQueue.emplace_back(std::move(doneCallback), std::move(cancelCond), sig, pubKey, msgHash, legacy); if (sigVerifyBatchesInProgress == 0 || sigVerifyQueue.size() >= SIG_VERIFY_BATCH_SIZE) { PushSigVerifyBatch(); } @@ -876,7 +877,7 @@ void CBLSWorker::PushSigVerifyBatch() if (jobs.size() == 1) { const auto& job = jobs[0]; if (!job.cancelCond()) { - bool valid = job.sig.VerifyInsecure(job.pubKey, job.msgHash); + bool valid = job.sig.VerifyInsecure(job.pubKey, job.msgHash, job.legacyScheme); job.doneCallback(valid); } std::unique_lock l(sigVerifyMutex); @@ -910,7 +911,7 @@ void CBLSWorker::PushSigVerifyBatch() } if (!pubKeys.empty()) { - bool allValid = aggSig.VerifyInsecureAggregated(pubKeys, msgHashes); + bool allValid = aggSig.VerifyInsecureAggregated(pubKeys, msgHashes, jobs.front().legacyScheme); if (allValid) { for (size_t i = 0; i < pubKeys.size(); i++) { jobs[indexes[i]].doneCallback(true); @@ -920,7 +921,7 @@ void CBLSWorker::PushSigVerifyBatch() // TODO this could be improved if we would cache pairing results in some way as the previous aggregated verification already calculated all the pairings for the hashes for (size_t i = 0; i < pubKeys.size(); i++) { const auto& job = jobs[indexes[i]]; - bool valid = job.sig.VerifyInsecure(job.pubKey, job.msgHash); + bool valid = job.sig.VerifyInsecure(job.pubKey, job.msgHash, job.legacyScheme); job.doneCallback(valid); } } diff --git a/src/bls/bls_worker.h b/src/bls/bls_worker.h index 74c7ffe67d9b..fe3fc71410b6 100644 --- a/src/bls/bls_worker.h +++ b/src/bls/bls_worker.h @@ -35,12 +35,16 @@ class CBLSWorker CBLSSignature sig; CBLSPublicKey pubKey; uint256 msgHash; - SigVerifyJob(SigVerifyDoneCallback&& _doneCallback, CancelCond&& _cancelCond, const CBLSSignature& _sig, CBLSPublicKey _pubKey, const uint256& _msgHash) : + //! The submitting thread's scheme; the job runs on a worker thread + bool legacyScheme; + SigVerifyJob(SigVerifyDoneCallback&& _doneCallback, CancelCond&& _cancelCond, const CBLSSignature& _sig, + CBLSPublicKey _pubKey, const uint256& _msgHash, bool _legacyScheme) : doneCallback(_doneCallback), cancelCond(_cancelCond), sig(_sig), pubKey(std::move(_pubKey)), - msgHash(_msgHash) + msgHash(_msgHash), + legacyScheme(_legacyScheme) { } }; diff --git a/src/chain.h b/src/chain.h index a67a88641e9f..ddc0c01a45a5 100644 --- a/src/chain.h +++ b/src/chain.h @@ -260,8 +260,12 @@ class CBlockIndex * * Does not imply the transactions are consensus-valid (ConnectTip might fail) * Does not imply the transactions are still stored on disk. (IsBlockPruned might return true) + * + * Note that this will be true for the snapshot base block, if one is loaded (and + * all subsequent assumed-valid blocks) since its nChainTx value will have been set + * manually based on the related AssumeutxoData entry. */ - bool HaveTxsDownloaded() const { return nChainTx != 0; } + bool HaveNumChainTxs() const { return nChainTx != 0; } NodeSeconds Time() const { diff --git a/src/chainlock/clsig.cpp b/src/chainlock/clsig.cpp index 9313824eb007..8b1dc44491a7 100644 --- a/src/chainlock/clsig.cpp +++ b/src/chainlock/clsig.cpp @@ -86,9 +86,9 @@ class DecodedSignatureCache CBLSSignature Decode(const std::array& bytes) EXCLUSIVE_LOCKS_REQUIRED(!m_mutex) { // Decode exactly as the coinbase payload was always decoded: under the - // process-wide scheme flag, including its opposite-scheme rejection. The - // flag is part of the key so a transient value cannot leak into later calls. - const bool legacy = bls::bls_legacy_scheme.load(); + // effective scheme, including its opposite-scheme rejection. The scheme is + // part of the key so another context's value cannot leak into later calls. + const bool legacy = bls::EffectiveLegacyScheme(); const uint256 key = (CHashWriter(SER_GETHASH, 0) << Span{bytes} << legacy).GetHash(); CBLSSignature sig; { diff --git a/src/chainlock/handler.cpp b/src/chainlock/handler.cpp index 69f2efb47067..b8e161e392ed 100644 --- a/src/chainlock/handler.cpp +++ b/src/chainlock/handler.cpp @@ -53,6 +53,9 @@ ChainlockHandler::ChainlockHandler(chainlock::Chainlocks& chainlocks, Chainstate std::make_unique(std::thread(util::TraceThread, "cl-schdlr", [&] { scheduler->serviceQueue(); }))}, seenChainLocks{SEEN_CHAINLOCKS_CUTOFF_SIZE, SEEN_CHAINLOCKS_MAX_SIZE} { + // Tasks are scheduled only from Start(), after this is set. + scheduler->SetTaskWrapper( + [this](const CScheduler::Function& task) { RunActiveEvoRound(m_chainman.EvoDb(), "cl-schdlr", task); }); } ChainlockHandler::~ChainlockHandler() @@ -169,12 +172,11 @@ MessageProcessingResult ChainlockHandler::ProcessNewChainLock(const NodeId from, if (!m_chainlocks.UpdateBestChainlock(hash, clsig, pindex)) return {}; if (pindex) { - scheduler->scheduleFromNow( - [&]() { - CheckActiveState(); - EnforceBestChainLock(); - }, - std::chrono::seconds{0}); + scheduler->scheduleFromNow(WithCapturedEvoView([&]() { + CheckActiveState(); + EnforceBestChainLock(); + }), + std::chrono::seconds{0}); LogPrint(BCLog::CHAINLOCKS, "ChainlockHandler::%s -- processed new CLSIG (%s), peer=%d\n", __func__, clsig.ToString(), from); @@ -196,14 +198,19 @@ void ChainlockHandler::UpdatedBlockTip(const CBlockIndex* pindexNew, const CBloc // atomic[If tryLockChainTipScheduled is false, do (set it to true] and schedule signing). if (bool expected = false; tryLockChainTipScheduled.compare_exchange_strong(expected, true)) { - scheduler->scheduleFromNow( - [&]() { - CheckActiveState(); - EnforceBestChainLock(); - Cleanup(); - tryLockChainTipScheduled = false; - }, - std::chrono::seconds{0}); + const auto unschedule{[this] { tryLockChainTipScheduled = false; }}; + scheduler->scheduleFromNow(WithCapturedEvoView( + [&, unschedule]() { + const struct Unschedule { + decltype(unschedule) f; + ~Unschedule() { f(); } + } done{unschedule}; + CheckActiveState(); + EnforceBestChainLock(); + Cleanup(); + }, + unschedule), + std::chrono::seconds{0}); } } @@ -237,8 +244,12 @@ void ChainlockHandler::AcceptedBlockHeader(const CBlockIndex* pindexNew) m_chainlocks.AcceptedBlockHeader(pindexNew); } -void ChainlockHandler::BlockConnected(const std::shared_ptr& pblock, const CBlockIndex* pindex) +void ChainlockHandler::BlockConnected(ChainstateRole role, const std::shared_ptr& pblock, const CBlockIndex* pindex) { + // Only the active chainstate's blocks concern this subscriber. + if (role == ChainstateRole::BACKGROUND) { + return; + } if (!m_mn_sync.IsBlockchainSynced()) { return; } diff --git a/src/chainlock/handler.h b/src/chainlock/handler.h index 76dca1672a95..fa1c3b0fca2f 100644 --- a/src/chainlock/handler.h +++ b/src/chainlock/handler.h @@ -104,7 +104,7 @@ class ChainlockHandler final : public CValidationInterface void TransactionAddedToMempool(const CTransactionRef& tx, int64_t nAcceptTime, uint64_t mempool_sequence) override EXCLUSIVE_LOCKS_REQUIRED(!cs); - void BlockConnected(const std::shared_ptr& pblock, const CBlockIndex* pindex) override + void BlockConnected(ChainstateRole role, const std::shared_ptr& pblock, const CBlockIndex* pindex) override EXCLUSIVE_LOCKS_REQUIRED(!cs); private: diff --git a/src/chainlock/signing.cpp b/src/chainlock/signing.cpp index 121b164c760e..04686028d11c 100644 --- a/src/chainlock/signing.cpp +++ b/src/chainlock/signing.cpp @@ -36,6 +36,9 @@ ChainLockSigner::ChainLockSigner(const ChainstateManager& chainman, const chainl m_scheduler_thread{ std::make_unique(std::thread(util::TraceThread, "cls-schdlr", [&] { m_scheduler->serviceQueue(); }))} { + // Tasks are scheduled only from Start(), after this is set. + m_scheduler->SetTaskWrapper( + [this](const CScheduler::Function& task) { RunActiveEvoRound(m_chainman.EvoDb(), "cls-schdlr", task); }); } ChainLockSigner::~ChainLockSigner() { Stop(); } @@ -185,8 +188,12 @@ void ChainLockSigner::BlockDisconnected(const std::shared_ptr& blo } -void ChainLockSigner::BlockConnected(const std::shared_ptr& block, const CBlockIndex* pindex) +void ChainLockSigner::BlockConnected(ChainstateRole role, const std::shared_ptr& block, const CBlockIndex* pindex) { + // Only the active chainstate's blocks concern this subscriber. + if (role == ChainstateRole::BACKGROUND) { + return; + } if (!m_mn_sync.IsBlockchainSynced()) { return; } diff --git a/src/chainlock/signing.h b/src/chainlock/signing.h index b077026fe303..1dafed891ef1 100644 --- a/src/chainlock/signing.h +++ b/src/chainlock/signing.h @@ -72,7 +72,7 @@ class ChainLockSigner final : public llmq::CRecoveredSigsListener, public CValid void UnregisterRecoveryInterface(); // implements validation interface: - void BlockConnected(const std::shared_ptr& block, const CBlockIndex* pindex) override + void BlockConnected(ChainstateRole role, const std::shared_ptr& block, const CBlockIndex* pindex) override EXCLUSIVE_LOCKS_REQUIRED(!cs_signer); void BlockDisconnected(const std::shared_ptr& block, const CBlockIndex* pindex) override EXCLUSIVE_LOCKS_REQUIRED(!cs_signer); diff --git a/src/chainparams.cpp b/src/chainparams.cpp index 9e06cfb41278..b8921d82ddf1 100644 --- a/src/chainparams.cpp +++ b/src/chainparams.cpp @@ -199,6 +199,32 @@ static void ReadRegTestLLMQInstantSendDIP0024Args(const ArgsManager& args, CChai options.llmq_dip0024_instantsend = args.GetArg("-llmqtestinstantsenddip0024", ""); } +static void ReadRegTestAssumeutxoArgs(const ArgsManager& args, CChainParams::RegTestOptions& options) +{ + for (const std::string& arg : args.GetArgs("-assumeutxodata")) { + const std::vector fields{SplitString(arg, ':')}; + const auto valid_hash = [](const std::string& value) { + return value.size() == uint256::size() * 2 && IsHex(value) && !uint256S(value).IsNull(); + }; + int32_t height; + uint32_t n_chain_tx; + if (fields.size() != 5 || !ParseInt32(fields[0], &height) || height <= 0 || !valid_hash(fields[1]) || + !valid_hash(fields[2]) || !ParseUInt32(fields[3], &n_chain_tx) || n_chain_tx == 0 || + !valid_hash(fields[4])) { + throw std::runtime_error(strprintf( + "Invalid value (%s) for -assumeutxodata=::::", + arg)); + } + options.assumeutxo_data.push_back(AssumeutxoData{ + .height = height, + .hash_serialized = AssumeutxoHash{uint256S(fields[1])}, + .evo_hash = EvoSnapshotHash{uint256S(fields[2])}, + .nChainTx = n_chain_tx, + .blockhash = uint256S(fields[4]), + }); + } +} + void ReadRegTestArgs(const ArgsManager& args, CChainParams::RegTestOptions& options) { if (auto value = args.GetBoolArg("-fastprune")) options.fastprune = *value; @@ -210,6 +236,7 @@ void ReadRegTestArgs(const ArgsManager& args, CChainParams::RegTestOptions& opti ReadRegTestLLMQTestArgs(args, options, Consensus::LLMQType::LLMQ_TEST_INSTANTSEND); ReadRegTestLLMQTestArgs(args, options, Consensus::LLMQType::LLMQ_TEST_PLATFORM); ReadRegTestLLMQInstantSendDIP0024Args(args, options); + ReadRegTestAssumeutxoArgs(args, options); } static void ReadDevNetSubsidyAndDiffArgs(const ArgsManager& args, CChainParams::DevNetOptions& options) @@ -330,6 +357,7 @@ void SetupChainParamsOptions(ArgsManager& argsman) { SetupChainParamsBaseOptions(argsman); + argsman.AddArg("-assumeutxodata=::::", "Add an exact AssumeUTXO snapshot authorization (regtest-only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS); argsman.AddArg("-budgetparams=::", "Override masternode, budget and superblock start heights (regtest-only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS); argsman.AddArg("-dip3params=:", "Override DIP3 activation and enforcement heights (regtest-only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS); argsman.AddArg("-highsubsidyblocks=", "The number of blocks with a higher than normal subsidy to mine at the start of a chain. Block after that height will have fixed subsidy base. (default: 0, devnet-only)", ArgsManager::ALLOW_ANY, OptionsCategory::CHAINPARAMS); diff --git a/src/consensus/validation.h b/src/consensus/validation.h index 80d5a88b7524..66c3f4f6eee7 100644 --- a/src/consensus/validation.h +++ b/src/consensus/validation.h @@ -116,7 +116,28 @@ class ValidationState } }; +/** Why a block could not be processed against this node's own Evo state. This + * never describes the block itself, so it accompanies an error, not a + * BlockValidationResult. */ +enum class LocalEvoStateResult { + NONE = 0, //!< not a local Evo state error + UNAVAILABLE, //!< state the block needs is not available to this chainstate + BINDING, //!< the operation was not bound to this chainstate's Evo view + MISMATCH, //!< state disagrees with the snapshot, which could not be rejected +}; + class TxValidationState : public ValidationState {}; -class BlockValidationState : public ValidationState {}; +class BlockValidationState : public ValidationState +{ + LocalEvoStateResult m_local_evo_result{LocalEvoStateResult::NONE}; + +public: + bool LocalEvoStateError(LocalEvoStateResult result, const std::string& reject_reason) + { + if (IsValid()) m_local_evo_result = result; + return Error(reject_reason); + } + LocalEvoStateResult GetLocalEvoStateResult() const { return m_local_evo_result; } +}; #endif // BITCOIN_CONSENSUS_VALIDATION_H diff --git a/src/dbwrapper.cpp b/src/dbwrapper.cpp index ecc406699b32..1e6541a50664 100644 --- a/src/dbwrapper.cpp +++ b/src/dbwrapper.cpp @@ -427,6 +427,7 @@ Span CDBIterator::GetValueImpl() const CDBIterator::~CDBIterator() = default; bool CDBIterator::Valid() const { return m_impl_iter->iter->Valid(); } +bool CDBIterator::HasError() const { return !m_impl_iter->iter->status().ok(); } void CDBIterator::SeekToFirst() { m_impl_iter->iter->SeekToFirst(); } void CDBIterator::Next() { m_impl_iter->iter->Next(); } diff --git a/src/dbwrapper.h b/src/dbwrapper.h index affdb583438a..f997592fff94 100644 --- a/src/dbwrapper.h +++ b/src/dbwrapper.h @@ -163,6 +163,8 @@ class CDBIterator ~CDBIterator(); bool Valid() const; + //! True once a read failed, e.g. on a corrupt block. Entries may then have been skipped. + bool HasError() const; void SeekToFirst(); @@ -423,6 +425,10 @@ class CDBTransactionIterator return transactionIt != transaction.writes.end() || parentIt->Valid(); } + bool HasError() const { + return parentIt->HasError(); + } + void Next() { if (transactionIt == transaction.writes.end() && !parentIt->Valid()) { return; diff --git a/src/dsnotificationinterface.cpp b/src/dsnotificationinterface.cpp index 43c9345e172a..9ac4028cc24b 100644 --- a/src/dsnotificationinterface.cpp +++ b/src/dsnotificationinterface.cpp @@ -77,8 +77,12 @@ void CDSNotificationInterface::TransactionAddedToMempool(const CTransactionRef& m_dstxman.TransactionAddedToMempool(ptx); } -void CDSNotificationInterface::BlockConnected(const std::shared_ptr& pblock, const CBlockIndex* pindex) +void CDSNotificationInterface::BlockConnected(ChainstateRole role, const std::shared_ptr& pblock, const CBlockIndex* pindex) { + // Only the active chainstate's blocks concern this subscriber. + if (role == ChainstateRole::BACKGROUND) { + return; + } m_dstxman.BlockConnected(pblock, pindex); } diff --git a/src/dsnotificationinterface.h b/src/dsnotificationinterface.h index bb4a42792cde..2d138a7115db 100644 --- a/src/dsnotificationinterface.h +++ b/src/dsnotificationinterface.h @@ -34,7 +34,7 @@ class CDSNotificationInterface : public CValidationInterface void SynchronousUpdatedBlockTip(const CBlockIndex *pindexNew, const CBlockIndex *pindexFork, bool fInitialDownload) override; void UpdatedBlockTip(const CBlockIndex *pindexNew, const CBlockIndex *pindexFork, bool fInitialDownload) override; void TransactionAddedToMempool(const CTransactionRef& tx, int64_t nAcceptTime, uint64_t mempool_sequence) override; - void BlockConnected(const std::shared_ptr& pblock, const CBlockIndex* pindex) override; + void BlockConnected(ChainstateRole role, const std::shared_ptr& pblock, const CBlockIndex* pindex) override; void BlockDisconnected(const std::shared_ptr& pblock, const CBlockIndex* pindexDisconnected) override; void NotifyMasternodeListChanged(bool undo, const CDeterministicMNList& oldMNList, const CDeterministicMNListDiff& diff) override; void NotifyChainLock(const CBlockIndex* pindex, const std::shared_ptr& clsig) override; diff --git a/src/evo/cbtx.cpp b/src/evo/cbtx.cpp index a81a61a1648c..673d67526bbd 100644 --- a/src/evo/cbtx.cpp +++ b/src/evo/cbtx.cpp @@ -4,6 +4,8 @@ #include +#include +#include #include #include #include @@ -193,6 +195,32 @@ std::string CCbTx::ToString() const creditPoolBalance / COIN, creditPoolBalance % COIN, merkleRootAssetUnlocks.ToString()); } +std::optional DecodeCbTxPayload(const CTransaction& tx, bool legacy_bls_scheme) +{ + if (!tx.IsCoinBase() || tx.nType != TRANSACTION_COINBASE) return std::nullopt; + CDataStream ds(tx.vExtraPayload, SER_NETWORK, PROTOCOL_VERSION); + CCbTx cbtx; + try { + ds >> cbtx.nVersion >> cbtx.nHeight >> cbtx.merkleRootMNList; + if (cbtx.nVersion >= CCbTx::Version::MERKLE_ROOT_QUORUMS) { + ds >> cbtx.merkleRootQuorums; + if (cbtx.nVersion >= CCbTx::Version::CLSIG_AND_BALANCE) { + ds >> COMPACTSIZE(cbtx.bestCLHeightDiff); + // Decoded eagerly: bytes valid in neither scheme reject the payload. + CBLSSignature signature; + ds >> CBLSSignatureVersionWrapper(signature, legacy_bls_scheme); + cbtx.bestCLSignature.Set(signature, legacy_bls_scheme); + ds >> cbtx.creditPoolBalance; + if (cbtx.nVersion >= CCbTx::Version::MERKLE_ROOT_ASSETUNLOCKS) ds >> cbtx.merkleRootAssetUnlocks; + } + } + } catch (const std::exception&) { + return std::nullopt; + } + if (!ds.empty()) return std::nullopt; + return cbtx; +} + std::optional> GetNonNullCoinbaseChainlock(const node::BlockManager& blockman, const CBlockIndex* pindex) { if (pindex == nullptr) { @@ -204,22 +232,37 @@ std::optional> GetNonNullCoinbaseChainlock(co return std::nullopt; } - CBlock block; - if (!blockman.ReadBlockFromDisk(block, *pindex)) { + const auto [have_data, block_pos]{WITH_LOCK(::cs_main, return std::make_pair( + (pindex->nStatus & BLOCK_HAVE_DATA) != 0, pindex->GetBlockPos()))}; + if (!have_data) { + // A snapshot view lacks the bodies at or below its base, where a + // coinbase ChainLock that cannot be read cannot be told from none. + if (const auto view{EvoViewScope::InnermostAny()}; + view && view->identity == EvoDbIdentity::SNAPSHOT && pindex->nHeight <= view->base_height) { + throw BlockDataUnavailableError( + strprintf("coinbase of block %s at or below the snapshot base is not available", + pindex->GetBlockHash().ToString())); + } return std::nullopt; } + CBlock block; + if (blockman.ReadBlockFromDisk(block, block_pos) != pindex->GetBlockHash()) { + throw BlockDataReadError(strprintf("coinbase of block %s cannot be read", pindex->GetBlockHash().ToString())); + } - return GetNonNullCoinbaseChainlock(block, pindex->nHeight); + return GetNonNullCoinbaseChainlock(block, *pindex, Params().GetConsensus()); } -std::optional> GetNonNullCoinbaseChainlock(const CBlock& block, int32_t height) +std::optional> GetNonNullCoinbaseChainlock(const CBlock& block, const CBlockIndex& index, + const Consensus::Params& consensus) { - if (block.vtx.empty() || !block.vtx[0]->IsCoinBase() || block.vtx[0]->nType != TRANSACTION_COINBASE) { - return std::nullopt; - } - const auto payload = GetTxPayload(*block.vtx[0]); - if (!payload || payload->nVersion < CCbTx::Version::CLSIG_AND_BALANCE || payload->nHeight != height || - !payload->bestCLSignature.Get().IsValid() || payload->bestCLHeightDiff >= uint32_t(height)) { + if (block.vtx.empty()) return std::nullopt; + // The signature encoding is the block's own scheme, not the one this node + // happens to be in (a background chainstate connects pre-v19 blocks in + // the legacy scheme while other threads read v19 blocks). + const auto payload{DecodeCbTxPayload(*block.vtx[0], !DeploymentActiveAt(index, consensus, Consensus::DEPLOYMENT_V19))}; + if (!payload || payload->nVersion < CCbTx::Version::CLSIG_AND_BALANCE || payload->nHeight != index.nHeight || + !payload->bestCLSignature.Get().IsValid() || payload->bestCLHeightDiff >= static_cast(index.nHeight)) { return std::nullopt; } return std::make_pair(payload->bestCLSignature.Get(), payload->bestCLHeightDiff); diff --git a/src/evo/cbtx.h b/src/evo/cbtx.h index 09201841ef8c..0e46e7d4f330 100644 --- a/src/evo/cbtx.h +++ b/src/evo/cbtx.h @@ -17,6 +17,9 @@ class BlockValidationState; class CBlock; class CBlockIndex; +namespace Consensus { +struct Params; +} // namespace Consensus class CDeterministicMNList; class TxValidationState; struct RPCResult; @@ -87,7 +90,17 @@ bool CalcCbTxMerkleRootQuorums(const CBlock& block, const CBlockIndex* pindexPre BlockValidationState& state); uint256 CalcCbTxMerkleRootAssetUnlocks(const CBlock& block); +/** Decode a coinbase's CbTx payload with an explicit BLS scheme for its + * ChainLock signature, independent of the scheme the node is in. */ +std::optional DecodeCbTxPayload(const CTransaction& tx, bool legacy_bls_scheme); +/** The non-null ChainLock in the coinbase of `pindex`'s block. A block this + * node has but cannot read raises BlockDataReadError; one it lacks reads as + * none, except at or below the base in a snapshot view, where it raises + * BlockDataUnavailableError. */ std::optional> GetNonNullCoinbaseChainlock(const node::BlockManager& blockman, const CBlockIndex* pindex); -std::optional> GetNonNullCoinbaseChainlock(const CBlock& block, int32_t height); +/** The non-null ChainLock in `block`'s coinbase, decoded with the BLS scheme in + * force at `index`, the block's own index. */ +std::optional> GetNonNullCoinbaseChainlock(const CBlock& block, const CBlockIndex& index, + const Consensus::Params& consensus); #endif // BITCOIN_EVO_CBTX_H diff --git a/src/evo/creditpool.cpp b/src/evo/creditpool.cpp index 3f0aeb52e665..c9a6a1e3b315 100644 --- a/src/evo/creditpool.cpp +++ b/src/evo/creditpool.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -17,6 +18,7 @@ #include #include #include +#include #include #include @@ -28,6 +30,9 @@ static const std::string DB_CREDITPOOL_SNAPSHOT = "cpm_S"; +// Window entries carried by an assumeutxo snapshot, for blocks at or below its +// base. Written only into a snapshot generation. +static const std::string DB_CREDITPOOL_HISTORY = "cpm_H"; static bool GetDataFromUnlockTx(const CTransaction& tx, CAmount& toUnlock, uint64_t& index, TxValidationState& state) { @@ -57,8 +62,30 @@ struct CreditPoolDataPerBlock { CAmount unlocked{0}; std::unordered_set indexes; }; + +//! What each block body contributes, as parsed from the body. +struct BlockDataCache { + Mutex mutex; + Uint256LruHashMap blocks GUARDED_BY(mutex){ + static_cast(Params().CreditPoolPeriodBlocks()) * 2}; +}; + +BlockDataCache& GetBlockDataCache() +{ + static BlockDataCache cache; + return cache; +} } // anonymous namespace +void CCreditPoolManager::ClearBlockDataCacheForTesting() +{ + auto& cache{GetBlockDataCache()}; + LOCK(cache.mutex); + cache.blocks.clear(); +} + +static std::optional ParseCreditData(const CBlock& block, int height); + // it throws exception if anything went wrong static std::optional GetCreditDataFromBlock(const gsl::not_null block_index, const node::BlockManager& blockman, @@ -71,28 +98,44 @@ static std::optional GetCreditDataFromBlock(const gsl::n CreditPoolDataPerBlock blockData; - static Mutex cache_mutex; - static Uint256LruHashMap block_data_cache GUARDED_BY(cache_mutex){ - static_cast(Params().CreditPoolPeriodBlocks()) * 2}; - if (LOCK(cache_mutex); block_data_cache.get(block_index->GetBlockHash(), blockData)) { + auto& cache{GetBlockDataCache()}; + if (LOCK(cache.mutex); cache.blocks.get(block_index->GetBlockHash(), blockData)) { return blockData; } + // A body this node does not have (pruned, or below an unvalidated snapshot + // base), or has but cannot read, is a local state error, never the block's + // fault. + const auto [have_data, block_pos]{WITH_LOCK(::cs_main, return std::make_pair( + (block_index->nStatus & BLOCK_HAVE_DATA) != 0, block_index->GetBlockPos()))}; + if (!have_data) { + throw BlockDataUnavailableError("failed-getcbforblock-read"); + } CBlock block; - if (!blockman.ReadBlockFromDisk(block, *block_index)) { - throw std::runtime_error("failed-getcbforblock-read"); + if (blockman.ReadBlockFromDisk(block, block_pos) != block_index->GetBlockHash()) { + throw BlockDataReadError("failed-getcbforblock-read"); } + auto parsed{ParseCreditData(block, block_index->nHeight)}; + if (!parsed) return std::nullopt; + blockData = std::move(*parsed); + LOCK(cache.mutex); + cache.blocks.insert(block_index->GetBlockHash(), blockData); + return blockData; +} + +static std::optional ParseCreditData(const CBlock& block, int height) +{ + CreditPoolDataPerBlock blockData; if (block.vtx.empty() || block.vtx[0]->vExtraPayload.empty() || !block.vtx[0]->IsSpecialTxVersion()) { - LogPrintf("%s: ERROR: empty CbTx for CreditPool at height=%d\n", __func__, block_index->nHeight); + LogPrintf("%s: ERROR: empty CbTx for CreditPool at height=%d\n", __func__, height); return std::nullopt; } - if (const auto opt_cbTx = GetTxPayload(block.vtx[0]->vExtraPayload); opt_cbTx) { blockData.credit_pool = opt_cbTx->creditPoolBalance; } else { - LogPrintf("%s: WARNING: No valid CbTx at height=%d\n", __func__, block_index->nHeight); + LogPrintf("%s: WARNING: No valid CbTx at height=%d\n", __func__, height); return std::nullopt; } for (const CTransactionRef& tx : block.vtx) { @@ -107,9 +150,6 @@ static std::optional GetCreditDataFromBlock(const gsl::n blockData.unlocked += unlocked; blockData.indexes.insert(index); } - - LOCK(cache_mutex); - block_data_cache.insert(block_index->GetBlockHash(), blockData); return blockData; } @@ -124,8 +164,9 @@ std::optional CCreditPoolManager::GetFromCache(const CBlockIndex& b if (!DeploymentActiveAt(block_index, m_chainman.GetConsensus(), Consensus::DEPLOYMENT_V20)) return CCreditPool{}; const uint256 block_hash = block_index.GetBlockHash(); + const EvoViewToken view{evoDb.CurrentView()}; CCreditPool pool; - if (WITH_LOCK(cache_mutex, return creditPoolCache.get(block_hash, pool))) { + if (WITH_LOCK(cache_mutex, return creditPoolCache.For(view).get(block_hash, pool))) { // The pool may have been constructed, and cached, outside a block-scoped transaction // (mempool acceptance, template creation, RPC), where its snapshot cannot be written. // Persist it from the first transaction-scoped lookup instead, or the snapshot would be @@ -133,10 +174,12 @@ std::optional CCreditPoolManager::GetFromCache(const CBlockIndex& b MaybeWriteSnapshot(block_hash, block_index.nHeight, pool); return pool; } - if (block_index.nHeight % DISK_SNAPSHOT_PERIOD == 0) { + // A snapshot base carries its full pool at whatever height the base has. + if (block_index.nHeight % DISK_SNAPSHOT_PERIOD == 0 || + (view.identity == EvoDbIdentity::SNAPSHOT && block_index.nHeight == view.base_height)) { if (evoDb.Read(std::make_pair(DB_CREDITPOOL_SNAPSHOT, block_hash), pool)) { LOCK(cache_mutex); - creditPoolCache.insert(block_hash, pool); + creditPoolCache.For(view).insert(block_hash, pool); return pool; } } @@ -166,8 +209,93 @@ void CCreditPoolManager::MaybeWriteSnapshot(const uint256& block_hash, int heigh void CCreditPoolManager::AddToCache(const uint256& block_hash, int height, const CCreditPool& pool) { MaybeWriteSnapshot(block_hash, height, pool); + const EvoViewToken view{evoDb.CurrentView()}; + LOCK(cache_mutex); + creditPoolCache.For(view).insert(block_hash, pool); +} + +bool CCreditPoolManager::SeedSnapshot(const EvoViewToken& view, const CBlockIndex* block, const CCreditPool& pool) +{ + if (!Assume(block != nullptr) || !Assume(evoDb.HasActiveTransaction())) return false; + const EvoViewScope scope{view}; + if (!evoDb.WriteSeed(std::make_pair(DB_CREDITPOOL_SNAPSHOT, block->GetBlockHash()), pool, + EvoSeedPolicy::BYTE_DETERMINISTIC)) { + return false; + } LOCK(cache_mutex); - creditPoolCache.insert(block_hash, pool); + creditPoolCache.For(view).erase(block->GetBlockHash()); + return true; +} + +bool CCreditPoolManager::SeedHistory(const EvoViewToken& view, const CBlockIndex* base, + const std::vector& entries) +{ + if (!Assume(base != nullptr) || !Assume(evoDb.HasActiveTransaction())) return false; + // Seeding may precede the chain checks, which refuse such a window. + if (entries.size() > static_cast(base->nHeight) + 1) return false; + const EvoViewScope scope{view}; + const int first_height{base->nHeight - static_cast(entries.size()) + 1}; + for (size_t i{0}; i < entries.size(); ++i) { + const CBlockIndex* index{base->GetAncestor(first_height + static_cast(i))}; + if (!evoDb.WriteSeed(std::make_pair(DB_CREDITPOOL_HISTORY, index->GetBlockHash()), entries[i], + EvoSeedPolicy::SCAFFOLDING)) { + return false; + } + } + return true; +} + +std::optional CCreditPoolManager::ReadHistoryEntry(gsl::not_null block_index, + const node::BlockManager& blockman, + const Consensus::Params& consensus_params, + std::string& error) +{ + if (!DeploymentActiveAt(*block_index, consensus_params, Consensus::DEPLOYMENT_DIP0003)) return CreditHistoryEntry{}; + CBlock block; + if (!blockman.ReadBlockFromDisk(block, *block_index)) { + error = strprintf("credit-pool window block %s is unreadable", block_index->GetBlockHash().ToString()); + return std::nullopt; + } + try { + if (const auto data{ParseCreditData(block, block_index->nHeight)}) { + return CreditHistoryEntry{data->credit_pool, data->unlocked}; + } + } catch (const std::exception& e) { + error = e.what(); + return std::nullopt; + } + error = strprintf("credit-pool window block %s has no valid CbTx", block_index->GetBlockHash().ToString()); + return std::nullopt; +} + +std::optional CCreditPoolManager::GetDistantEntry(gsl::not_null block_index) +{ + const auto& consensus{m_chainman.GetConsensus()}; + const EvoViewToken view{evoDb.CurrentView()}; + if (view.identity != EvoDbIdentity::SNAPSHOT || block_index->nHeight > view.base_height) { + const auto data{GetCreditDataFromBlock(block_index, m_chainman.m_blockman, consensus)}; + if (!data) return std::nullopt; + return CreditHistoryEntry{data->credit_pool, data->unlocked}; + } + // At or below the base the snapshot view carries the window entries. A + // body this node also has must agree with them; the carried record never + // enters the cache of entries parsed from real blocks. + if (!DeploymentActiveAt(*block_index, consensus, Consensus::DEPLOYMENT_DIP0003)) return std::nullopt; + CreditHistoryEntry carried; + if (!evoDb.Read(std::make_pair(DB_CREDITPOOL_HISTORY, block_index->GetBlockHash()), carried)) { + throw BlockDataUnavailableError( + strprintf("credit-pool window entry for block %s is not carried", block_index->GetBlockHash().ToString())); + } + if (WITH_LOCK(::cs_main, return (block_index->nStatus & BLOCK_HAVE_DATA) != 0)) { + std::string error; + const auto real{ReadHistoryEntry(block_index, m_chainman.m_blockman, consensus, error)}; + if (real && *real != carried) { + throw evo::SnapshotStateMismatchError( + strprintf("carried credit-pool window entry for block %s differs from the block", + block_index->GetBlockHash().ToString())); + } + } + return carried; } CCreditPool CCreditPoolManager::ConstructCreditPool(const gsl::not_null block_index, CCreditPool prev) @@ -203,11 +331,9 @@ CCreditPool CCreditPoolManager::ConstructCreditPool(const gsl::not_null distant_block{ - GetCreditDataFromBlock(distant_block_index, m_chainman.m_blockman, m_chainman.GetConsensus())}; - distant_block) { - distantUnlocked = distant_block->unlocked; - distantBalance = distant_block->credit_pool; + if (const auto distant{GetDistantEntry(distant_block_index)}) { + distantUnlocked = distant->unlocked; + distantBalance = distant->credit_pool; } } @@ -278,6 +404,7 @@ CCreditPoolManager::CCreditPoolManager(CEvoDB& _evoDb, const ChainstateManager& evoDb{_evoDb}, m_chainman{chainman} { + creditPoolCache.SetOwner(_evoDb); } CCreditPoolManager::~CCreditPoolManager() = default; @@ -418,6 +545,8 @@ std::optional GetCreditPoolDiffForBlock(CCreditPoolManager& cpo // M_ERROR so the block is not marked invalid. state.Error(e.what()); return std::nullopt; + } catch (const evo::LocalStateError&) { + throw; } catch (const std::exception& e) { LogPrintf("%s -- failed: %s\n", __func__, e.what()); state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "failed-getcreditpooldiff"); diff --git a/src/evo/creditpool.h b/src/evo/creditpool.h index f73baa1981dd..980789c1eed5 100644 --- a/src/evo/creditpool.h +++ b/src/evo/creditpool.h @@ -6,6 +6,7 @@ #define BITCOIN_EVO_CREDITPOOL_H #include +#include #include #include #include @@ -39,6 +40,16 @@ namespace llmq { class CQuorumManager; } // namespace llmq +/** One block's entry in the credit-pool sliding window: the pool balance its + * CbTx records and the amount its asset unlocks withdraw. */ +struct CreditHistoryEntry { + CAmount credit_pool{0}; + CAmount unlocked{0}; + + bool operator==(const CreditHistoryEntry&) const = default; + SERIALIZE_METHODS(CreditHistoryEntry, obj) { READWRITE(obj.credit_pool, obj.unlocked); } +}; + struct CCreditPool { CAmount locked{0}; @@ -121,7 +132,8 @@ class CCreditPoolManager private: static constexpr size_t CreditPoolCacheSize = 1000; Mutex cache_mutex; - Uint256LruHashMap creditPoolCache GUARDED_BY(cache_mutex){CreditPoolCacheSize}; + EvoViewCache> creditPoolCache GUARDED_BY(cache_mutex){ + [] { return Uint256LruHashMap{CreditPoolCacheSize}; }}; CEvoDB& evoDb; const ChainstateManager& m_chainman; @@ -177,6 +189,23 @@ class CCreditPoolManager * it can happen if there limits of withdrawal (unlock) exceed */ CCreditPool GetCreditPool(const CBlockIndex* block) EXCLUSIVE_LOCKS_REQUIRED(!cache_mutex); + /** Seed the full pool at a snapshot base into `view`, inside the caller's + * open EvoDB transaction for that view. */ + bool SeedSnapshot(const EvoViewToken& view, const CBlockIndex* block, const CCreditPool& pool) + EXCLUSIVE_LOCKS_REQUIRED(!cache_mutex); + /** Seed the window entries of the blocks ending at `base`, oldest first, + * into `view`, inside the caller's open EvoDB transaction for that view. */ + bool SeedHistory(const EvoViewToken& view, const CBlockIndex* base, const std::vector& entries); + + /** A block's window entry from its body: {0, 0} before DIP3. Without a + * readable body, or with a malformed CbTx, nullopt and `error`. */ + static std::optional ReadHistoryEntry(gsl::not_null block_index, + const node::BlockManager& blockman, + const Consensus::Params& consensus_params, + std::string& error); + + /** Forget the block bodies parsed so far, as a restart would. */ + static void ClearBlockDataCacheForTesting(); private: std::optional GetFromCache(const CBlockIndex& block_index) EXCLUSIVE_LOCKS_REQUIRED(!cache_mutex); @@ -187,6 +216,9 @@ class CCreditPoolManager CCreditPool ConstructCreditPool(const gsl::not_null block_index, CCreditPool prev) EXCLUSIVE_LOCKS_REQUIRED(!cache_mutex); + /** The entry of the block leaving the window: the carried record at or + * below a snapshot base in the snapshot view, the block body otherwise. */ + std::optional GetDistantEntry(gsl::not_null block_index); }; std::optional GetCreditPoolDiffForBlock(CCreditPoolManager& cpoolman, diff --git a/src/evo/deterministicmns.cpp b/src/evo/deterministicmns.cpp index 70288671d0a6..3dcbd8db471b 100644 --- a/src/evo/deterministicmns.cpp +++ b/src/evo/deterministicmns.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include @@ -670,14 +671,53 @@ void CDeterministicMNList::RemoveMN(const uint256& proTxHash) InvalidateSMLCache(); } + +bool EncodedMNListsCanonicallyEqual(Span a, Span b) +{ + CDeterministicMNList list_a, list_b; + try { + CDataStream{a, SER_DISK, CLIENT_VERSION} >> list_a; + CDataStream{b, SER_DISK, CLIENT_VERSION} >> list_b; + } catch (const std::exception&) { + return false; + } + if (list_a.GetBlockHash() != list_b.GetBlockHash() || list_a.GetHeight() != list_b.GetHeight() || + list_a.GetTotalRegisteredCount() != list_b.GetTotalRegisteredCount() || + list_a.GetCounts().total() != list_b.GetCounts().total()) { + return false; + } + bool equal{true}; + list_a.ForEachMNShared(/*onlyValid=*/false, [&](const CDeterministicMNCPtr& dmn) { + if (!equal) return; + const CDeterministicMNCPtr other{list_b.GetMN(dmn->proTxHash)}; + equal = other && SerializeHash(*other, SER_DISK, CLIENT_VERSION) == SerializeHash(*dmn, SER_DISK, CLIENT_VERSION); + }); + return equal; +} + CDeterministicMNManager::CDeterministicMNManager(CEvoDB& evoDb, CMasternodeMetaMan& mn_metaman) : m_evoDb{evoDb}, m_mn_metaman{mn_metaman} { + m_views.SetOwner(evoDb); + evoDb.SetMNListEquivalence(EncodedMNListsCanonicallyEqual); } CDeterministicMNManager::~CDeterministicMNManager() = default; +bool CDeterministicMNManager::SeedListForBlock(const EvoViewToken& view, const CDeterministicMNList& list) +{ + if (!Assume(m_evoDb.HasActiveTransaction())) return false; + const EvoViewScope scope{view}; + if (!m_evoDb.WriteSeed(std::make_pair(DB_LIST_SNAPSHOT, list.GetBlockHash()), list, EvoSeedPolicy::MN_LIST)) { + return false; + } + LOCK(cs); + View().mnListsCache.erase(list.GetBlockHash()); + View().mnListDiffsCache.erase(list.GetBlockHash()); + return true; +} + bool CDeterministicMNManager::ProcessBlock(const CBlock& block, gsl::not_null pindex, BlockValidationState& state, const CDeterministicMNList& oldList, const CDeterministicMNList& newList, MNListUpdates& updatesRet) @@ -697,6 +737,7 @@ bool CDeterministicMNManager::ProcessBlock(const CBlock& block, gsl::not_nullpprev == m_initial_snapshot_index) { + if ((nHeight % DISK_SNAPSHOT_PERIOD) == 0 || pindex->pprev == view.m_initial_snapshot_index) { if (!m_evoDb.WriteDerived(std::make_pair(DB_LIST_SNAPSHOT, newList.GetBlockHash()), newList)) { const std::string msg = strprintf("CDeterministicMNManager::%s -- EvoDB list snapshot mismatch for block %s", __func__, newList.GetBlockHash().ToString()); AbortNode(msg); return state.Error(msg); } - mnListsCache.insert_or_assign(newList.GetBlockHash(), newList); + view.mnListsCache.insert_or_assign(newList.GetBlockHash(), newList); LogPrintf("CDeterministicMNManager::%s -- Wrote snapshot. nHeight=%d, mapCurMNs.allMNsCount=%d\n", __func__, nHeight, newList.GetCounts().total()); } @@ -739,8 +780,11 @@ bool CDeterministicMNManager::ProcessBlock(const CBlock& block, gsl::not_nullnHeight; - mnListDiffsCache.emplace(pindex->GetBlockHash(), diff); - mnListsCache.insert_or_assign(newList.GetBlockHash(), newList); + view.mnListDiffsCache.emplace(pindex->GetBlockHash(), diff); + view.processed_height = std::max(view.processed_height, nHeight); + view.mnListsCache.insert_or_assign(newList.GetBlockHash(), newList); + } catch (const evo::LocalStateError&) { + throw; } catch (const std::exception& e) { LogPrintf("CDeterministicMNManager::%s -- internal error: %s\n", __func__, e.what()); return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "failed-dmn-block"); @@ -782,15 +826,16 @@ bool CDeterministicMNManager::UndoBlock(gsl::not_null pindex CDeterministicMNListDiff diff; { LOCK(cs); + auto& view{View()}; m_evoDb.Read(std::make_pair(DB_LIST_DIFF, blockHash), diff); if (diff.HasChanges()) { // need to call this before erasing - prevList = GetListForBlockInternal(pindex->pprev); + prevList = GetListForBlockInternal(pindex->pprev, view); } - mnListsCache.erase(blockHash); - mnListDiffsCache.erase(blockHash); + view.mnListsCache.erase(blockHash); + view.mnListDiffsCache.erase(blockHash); } if (diff.HasChanges()) { CDeterministicMNList curList{prevList}; @@ -814,10 +859,11 @@ void CDeterministicMNManager::UpdatedBlockTip(gsl::not_null { LOCK(cs); - tipIndex = pindex; + View().tipIndex = pindex; } -CDeterministicMNList CDeterministicMNManager::GetListForBlockInternal(gsl::not_null pindex) +CDeterministicMNList CDeterministicMNManager::GetListForBlockInternal(gsl::not_null pindex, + ViewState& view) { CDeterministicMNList snapshot; @@ -827,28 +873,41 @@ CDeterministicMNList CDeterministicMNManager::GetListForBlockInternal(gsl::not_n AssertLockHeld(cs); - if (tipIndex != nullptr && mnListsCache.size() > MAX_CACHED_LISTS) { - CleanupCache(tipIndex->nHeight); + // A view stops receiving tip notifications when its chainstate stops being + // the active one, so its tip can lag far behind the blocks it processes. + if (const int cleanup_height{std::max(view.tipIndex != nullptr ? view.tipIndex->nHeight : -1, view.processed_height)}; + cleanup_height >= 0 && view.mnListsCache.size() > MAX_CACHED_LISTS) { + CleanupCache(view, cleanup_height); } std::list listDiffIndexes; while (true) { // try using cache before reading from disk - auto itLists = mnListsCache.find(pindex->GetBlockHash()); - if (itLists != mnListsCache.end()) { + auto itLists = view.mnListsCache.find(pindex->GetBlockHash()); + if (itLists != view.mnListsCache.end()) { snapshot = itLists->second; break; } if (m_evoDb.Read(std::make_pair(DB_LIST_SNAPSHOT, pindex->GetBlockHash()), snapshot)) { - mnListsCache.emplace(pindex->GetBlockHash(), snapshot); + view.mnListsCache.emplace(pindex->GetBlockHash(), snapshot); break; } + if (m_list_snapshot_miss_hook) m_list_snapshot_miss_hook(pindex); + // At or below a snapshot base the snapshot view carries full lists + // only. A missing one is outside the carried set: never reconstruct it + // from diffs that do not exist there, and never guess an empty list. + if (const EvoViewToken current{m_evoDb.CurrentView()}; + current.identity == EvoDbIdentity::SNAPSHOT && pindex->nHeight <= current.base_height) { + throw BlockDataUnavailableError(strprintf("CDeterministicMNManager::%s -- masternode list for block %s %s", + __func__, pindex->GetBlockHash().ToString(), + BLOCK_DATA_UNAVAILABLE_SUFFIX)); + } // no snapshot found yet, check diffs - auto itDiffs = mnListDiffsCache.find(pindex->GetBlockHash()); - if (itDiffs != mnListDiffsCache.end()) { + auto itDiffs = view.mnListDiffsCache.find(pindex->GetBlockHash()); + if (itDiffs != view.mnListDiffsCache.end()) { listDiffIndexes.emplace_front(pindex); pindex = pindex->pprev; continue; @@ -874,22 +933,22 @@ CDeterministicMNList CDeterministicMNManager::GetListForBlockInternal(gsl::not_n __func__, pindex->GetBlockHash().ToString(), BLOCK_DATA_UNAVAILABLE_SUFFIX)); } // no snapshot and no diff on disk means that it's the initial snapshot - m_initial_snapshot_index = pindex; + view.m_initial_snapshot_index = pindex; snapshot = CDeterministicMNList(pindex->GetBlockHash(), pindex->nHeight, 0); - mnListsCache.emplace(pindex->GetBlockHash(), snapshot); + view.mnListsCache.emplace(pindex->GetBlockHash(), snapshot); LogPrintf("CDeterministicMNManager::%s -- initial snapshot. blockHash=%s nHeight=%d\n", __func__, snapshot.GetBlockHash().ToString(), snapshot.GetHeight()); break; } diff.nHeight = pindex->nHeight; - mnListDiffsCache.emplace(pindex->GetBlockHash(), std::move(diff)); + view.mnListDiffsCache.emplace(pindex->GetBlockHash(), std::move(diff)); listDiffIndexes.emplace_front(pindex); pindex = pindex->pprev; } for (const auto& diffIndex : listDiffIndexes) { - const auto& diff = mnListDiffsCache.at(diffIndex->GetBlockHash()); + const auto& diff = view.mnListDiffsCache.at(diffIndex->GetBlockHash()); snapshot.ApplyDiff(diffIndex, diff); static constexpr int MINI_SNAPSHOT_INTERVAL = 32; @@ -906,24 +965,22 @@ CDeterministicMNList CDeterministicMNManager::GetListForBlockInternal(gsl::not_n // There is also separate in-memory caching for the current tip and active quorums, // but this mini-snapshot cache specifically speeds up repeated requests // for nearby historical blocks. - mnListsCache.emplace(snapshot.GetBlockHash(), snapshot); + view.mnListsCache.emplace(snapshot.GetBlockHash(), snapshot); } } - if (tipIndex) { + if (view.tipIndex) { // always keep a snapshot for the tip - if (const auto snapshot_hash = snapshot.GetBlockHash(); snapshot_hash == tipIndex->GetBlockHash()) { - mnListsCache.emplace(snapshot_hash, snapshot); + if (const auto snapshot_hash = snapshot.GetBlockHash(); snapshot_hash == view.tipIndex->GetBlockHash()) { + view.mnListsCache.emplace(snapshot_hash, snapshot); } else { // keep snapshots for yet alive quorums - if (std::ranges::any_of(Params().GetConsensus().llmqs, [&snapshot, this]( - const auto& params) EXCLUSIVE_LOCKS_REQUIRED(cs) { - AssertLockHeld(cs); + if (std::ranges::any_of(Params().GetConsensus().llmqs, [&snapshot, &view](const auto& params) { return (snapshot.GetHeight() % params.dkgInterval == 0) && (snapshot.GetHeight() + params.dkgInterval * (params.keepOldConnections + 1) >= - tipIndex->nHeight); + view.tipIndex->nHeight); })) { - mnListsCache.emplace(snapshot_hash, snapshot); + view.mnListsCache.emplace(snapshot_hash, snapshot); } } } @@ -935,10 +992,11 @@ CDeterministicMNList CDeterministicMNManager::GetListForBlockInternal(gsl::not_n CDeterministicMNList CDeterministicMNManager::GetListAtChainTip() { LOCK(cs); - if (!tipIndex) { + auto& view{View()}; + if (!view.tipIndex) { return {}; } - return GetListForBlockInternal(tipIndex); + return GetListForBlockInternal(view.tipIndex, view); } bool CDeterministicMNManager::IsProTxWithCollateral(const CTransactionRef& tx, uint32_t n) @@ -966,19 +1024,18 @@ bool CDeterministicMNManager::IsProTxWithCollateral(const CTransactionRef& tx, u return true; } -void CDeterministicMNManager::CleanupCache(int nHeight) +void CDeterministicMNManager::CleanupCache(ViewState& view, int nHeight) { - AssertLockHeld(cs); std::vector toDeleteLists; std::vector toDeleteDiffs; - for (const auto& p : mnListsCache) { + for (const auto& p : view.mnListsCache) { if (p.second.GetHeight() + LIST_DIFFS_CACHE_SIZE < nHeight) { // too old, drop it toDeleteLists.emplace_back(p.first); continue; } - if (tipIndex != nullptr && p.first == tipIndex->GetBlockHash()) { + if (view.tipIndex != nullptr && p.first == view.tipIndex->GetBlockHash()) { // it's a snapshot for the tip, keep it continue; } @@ -994,15 +1051,15 @@ void CDeterministicMNManager::CleanupCache(int nHeight) toDeleteLists.emplace_back(p.first); } for (const auto& h : toDeleteLists) { - mnListsCache.erase(h); + view.mnListsCache.erase(h); } - for (const auto& p : mnListDiffsCache) { + for (const auto& p : view.mnListDiffsCache) { if (p.second.nHeight + LIST_DIFFS_CACHE_SIZE < nHeight) { toDeleteDiffs.emplace_back(p.first); } } for (const auto& h : toDeleteDiffs) { - mnListDiffsCache.erase(h); + view.mnListDiffsCache.erase(h); } } @@ -1016,6 +1073,8 @@ bool CDeterministicMNManager::IsMigrationRequired() const AssertLockHeld(::cs_main); + // NORMAL-only lifecycle operation: it uses the raw legacy keyspace and + // never sees snapshot-generation records. std::unique_ptr pcursor{m_evoDb.GetRawDB().NewIterator()}; auto start{std::make_tuple(DB_LIST_DIFF_LEGACY, uint256{})}; pcursor->Seek(start); @@ -1037,6 +1096,8 @@ bool CDeterministicMNManager::IsMigrationRequired() const bool CDeterministicMNManager::MigrateLegacyDiffs(const CBlockIndex* const tip_index) { + // NORMAL-only lifecycle operation: it uses the raw legacy keyspace and + // never sees snapshot-generation records. // CRITICAL: This migration converts ALL stored CDeterministicMNListDiff entries // from legacy database key (DB_LIST_DIFF_LEGACY) to new key (DB_LIST_DIFF) format @@ -1172,8 +1233,7 @@ bool CDeterministicMNManager::MigrateLegacyDiffs(const CBlockIndex* const tip_in // Clear caches to force reload with new format LOCK(cs); - mnListsCache.clear(); - mnListDiffsCache.clear(); + m_views.Clear(); LogPrintf("CDeterministicMNManager::%s -- Successfully migrated %d diffs to nVersion-first format\n", __func__, keys_to_erase.size()); @@ -1184,6 +1244,8 @@ bool CDeterministicMNManager::MigrateLegacyDiffs(const CBlockIndex* const tip_in CDeterministicMNManager::RecalcDiffsResult CDeterministicMNManager::RecalculateAndRepairDiffs( const node::BlockManager& blockman, const CBlockIndex* start_index, const CBlockIndex* stop_index, BuildListFromBlockFunc build_list_func, bool repair) { + // Verification and repair are NORMAL-only lifecycle operations. + const EvoViewScope evo_view{m_evoDb.ViewFor(EvoDbIdentity::NORMAL)}; RecalcDiffsResult result; result.start_height = start_index->nHeight; result.stop_height = stop_index->nHeight; @@ -1292,11 +1354,18 @@ CDeterministicMNManager::RecalcDiffsResult CDeterministicMNManager::RecalculateA return result; } -bool CDeterministicMNManager::IsRepaired() const { return m_evoDb.Exists(DB_LIST_REPAIRED); } +bool CDeterministicMNManager::IsRepaired() const +{ + // Repair is a NORMAL-only lifecycle operation. + const EvoViewScope evo_view{m_evoDb.ViewFor(EvoDbIdentity::NORMAL)}; + return m_evoDb.Exists(DB_LIST_REPAIRED); +} void CDeterministicMNManager::CompleteRepair() { AssertLockHeld(::cs_main); + // Repair is a NORMAL-only lifecycle operation. + const EvoViewScope evo_view{m_evoDb.ViewFor(EvoDbIdentity::NORMAL)}; auto dbTx = m_evoDb.BeginTransaction(); m_evoDb.Write(DB_LIST_REPAIRED, 1); dbTx->Commit(); @@ -1484,6 +1553,8 @@ void CDeterministicMNManager::WriteRepairedDiffs( return; } + // NORMAL-only lifecycle operation: it uses the raw legacy keyspace and + // never sees snapshot-generation records. CDBBatch batch(m_evoDb.GetRawDB()); const size_t BATCH_SIZE_THRESHOLD = 1 << 24; // 16MB size_t diffs_written = 0; @@ -1514,10 +1585,12 @@ void CDeterministicMNManager::WriteRepairedDiffs( // Clear caches for repaired diffs so next read gets fresh data from disk // Must clear both diff cache and list cache since lists were built from old diffs LOCK(cs); - for (const auto& [block_hash, diff] : recalculated_diffs) { - mnListDiffsCache.erase(block_hash); - mnListsCache.erase(block_hash); - } + m_views.ForEach([&](ViewState& view) { + for (const auto& [block_hash, diff] : recalculated_diffs) { + view.mnListDiffsCache.erase(block_hash); + view.mnListsCache.erase(block_hash); + } + }); LogPrintf("CDeterministicMNManager::%s -- Successfully repaired %d diffs (caches cleared)\n", __func__, recalculated_diffs.size()); diff --git a/src/evo/deterministicmns.h b/src/evo/deterministicmns.h index ce4df7367fab..25148e9267ad 100644 --- a/src/evo/deterministicmns.h +++ b/src/evo/deterministicmns.h @@ -7,7 +7,9 @@ #include #include +#include #include +#include #include #include @@ -21,6 +23,7 @@ #include #include +#include #include #include #include @@ -747,18 +750,9 @@ struct MNListUpdates inline constexpr std::string_view BLOCK_DATA_UNAVAILABLE_SUFFIX{ "is not available (pruned or below an unvalidated snapshot base)"}; -/** Thrown when the masternode list for a block cannot be reconstructed because - * the data is not on this node yet (pruned, or below an unvalidated snapshot - * base, or pending in another chainstate's unflushed EvoDB overlay). Distinct - * from the plain std::runtime_error that CDeterministicMNList::ApplyDiff - * raises for genuine local corruption, which must never be swallowed. - * The message carries BLOCK_DATA_UNAVAILABLE_SUFFIX, matched by - * IsBlockDataUnavailableError(). */ -class BlockDataUnavailableError : public std::runtime_error -{ -public: - using std::runtime_error::runtime_error; -}; +//! Whether two encoded masternode lists hold the same masternodes, internal +//! ids and registration counter, however their maps are laid out. +bool EncodedMNListsCanonicallyEqual(Span a, Span b); class CDeterministicMNManager { @@ -779,10 +773,22 @@ class CDeterministicMNManager CEvoDB& m_evoDb; CMasternodeMetaMan& m_mn_metaman; - Uint256HashMap mnListsCache GUARDED_BY(cs); - Uint256HashMap mnListDiffsCache GUARDED_BY(cs); - const CBlockIndex* tipIndex GUARDED_BY(cs) {nullptr}; - const CBlockIndex* m_initial_snapshot_index GUARDED_BY(cs) {nullptr}; + //! Lists and diffs derived under one Evo view, with the view's own tip and + //! initial-snapshot anchor. + struct ViewState { + Uint256HashMap mnListsCache; + Uint256HashMap mnListDiffsCache; + const CBlockIndex* tipIndex{nullptr}; + const CBlockIndex* m_initial_snapshot_index{nullptr}; + //! Highest block this view connected. Bounds the cache of a view that + //! is not active, and so never learns a tip from UpdatedBlockTip. + int processed_height{-1}; + }; + EvoViewCache m_views GUARDED_BY(cs); + std::function m_list_snapshot_miss_hook GUARDED_BY(cs); + + //! The partition of the calling operation's Evo view. + ViewState& View() EXCLUSIVE_LOCKS_REQUIRED(cs) { return m_views.For(m_evoDb.CurrentView()); } public: CDeterministicMNManager() = delete; @@ -800,14 +806,46 @@ class CDeterministicMNManager CDeterministicMNList GetListForBlock(gsl::not_null pindex) EXCLUSIVE_LOCKS_REQUIRED(!cs) { LOCK(cs); - return GetListForBlockInternal(pindex); + return GetListForBlockInternal(pindex, View()); }; CDeterministicMNList GetListAtChainTip() EXCLUSIVE_LOCKS_REQUIRED(!cs); + /** Seed a canonical full list into `view`, inside the caller's open EvoDB + * transaction for that view. */ + bool SeedListForBlock(const EvoViewToken& view, const CDeterministicMNList& list) EXCLUSIVE_LOCKS_REQUIRED(!cs); + //! The EvoDB this manager reads, for callers that bind an Evo view. + const CEvoDB& EvoDb() const { return m_evoDb; } + void SetListForBlockForTesting(const CDeterministicMNList& list) EXCLUSIVE_LOCKS_REQUIRED(!cs) { LOCK(cs); - mnListsCache.insert_or_assign(list.GetBlockHash(), list); + View().mnListsCache.insert_or_assign(list.GetBlockHash(), list); + } + + /** Test-only hook invoked whenever a list lookup misses both the cache and + * the stored full lists, before diff-chain reconstruction. */ + void SetListSnapshotMissHookForTesting(std::function hook) EXCLUSIVE_LOCKS_REQUIRED(!cs) + { + LOCK(cs); + m_list_snapshot_miss_hook = std::move(hook); + } + + //! Heights of the lists cached for the current view. + std::vector CachedListHeightsForTesting() EXCLUSIVE_LOCKS_REQUIRED(!cs) + { + LOCK(cs); + std::vector heights; + for (const auto& [_, list] : View().mnListsCache) { + heights.push_back(list.GetHeight()); + } + return heights; + } + + //! The tip the current view was last told of. + const CBlockIndex* TipForTesting() EXCLUSIVE_LOCKS_REQUIRED(!cs) + { + LOCK(cs); + return View().tipIndex; } // Test if given TX is a ProRegTx which also contains the collateral at index n @@ -846,8 +884,9 @@ class CDeterministicMNManager [[nodiscard]] bool MigrateLegacyDiffs(const CBlockIndex* const tip_index) EXCLUSIVE_LOCKS_REQUIRED(!cs, ::cs_main); private: - void CleanupCache(int nHeight) EXCLUSIVE_LOCKS_REQUIRED(cs); - CDeterministicMNList GetListForBlockInternal(gsl::not_null pindex) EXCLUSIVE_LOCKS_REQUIRED(cs); + static void CleanupCache(ViewState& view, int nHeight); + CDeterministicMNList GetListForBlockInternal(gsl::not_null pindex, ViewState& view) + EXCLUSIVE_LOCKS_REQUIRED(cs); // Helper methods for RecalculateAndRepairDiffs static std::vector CollectSnapshotBlocks(const CBlockIndex* start_index, diff --git a/src/evo/evodb.cpp b/src/evo/evodb.cpp index 84fc87902d02..f0f8a13fc378 100644 --- a/src/evo/evodb.cpp +++ b/src/evo/evodb.cpp @@ -4,9 +4,20 @@ #include +#include +#include #include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include CEvoDBScopedCommitter::CEvoDBScopedCommitter(CEvoDB& _evoDB, EvoDbIdentity identity) : evoDB{_evoDB}, @@ -34,10 +45,56 @@ void CEvoDBScopedCommitter::Rollback() evoDB.RollbackCurTransaction(identity); } -CEvoDB::CEvoDB(const util::DbWrapperParams& db_params) : - db{util::MakeDbWrapper({db_params.path / "evodb", db_params.memory, db_params.wipe, /*cache_size=*/64 << 20, db_params.options})} +namespace { +DataStream NamespaceTag() { - transaction_contexts.emplace(EvoDbIdentity::NORMAL, std::make_unique(*db)); + DataStream tag{}; + tag << EVODB_GENERATION_NAMESPACE; + return tag; +} + +bool StartsWith(const DataStream& key, const DataStream& prefix) +{ + return key.size() >= prefix.size() && std::equal(prefix.begin(), prefix.end(), key.begin()); +} + +bool SameBytes(const DataStream& a, const DataStream& b) { return std::equal(a.begin(), a.end(), b.begin(), b.end()); } + +auto GenerationRecordKey(uint32_t generation) { return std::make_pair(EVODB_GENERATION_RECORD, generation); } +} // namespace + +DataStream CEvoDBStore::GenerationPrefix(uint32_t generation) +{ + DataStream prefix{}; + if (generation != 0) prefix << EVODB_GENERATION_NAMESPACE << generation; + return prefix; +} + +bool CEvoDBStore::Owns(const DataStream& raw_key) const +{ + if (m_prefix.empty()) return !StartsWith(raw_key, NamespaceTag()); + return StartsWith(raw_key, m_prefix); +} + +void CEvoDBStore::Iterator::SkipForeign() +{ + // Generation namespaces form one contiguous key range. The NORMAL view + // steps over it as a whole; a generation view ends where its prefix ends. + if (!m_store.m_prefix.empty() || !m_it->Valid()) return; + const DataStream tag{NamespaceTag()}; + if (!StartsWith(m_it->GetKey(), tag)) return; + DataStream after_tag{tag}; + std::byte& last{after_tag[after_tag.size() - 1]}; + last = std::byte{static_cast(std::to_integer(last) + 1)}; + m_it->Seek(after_tag); +} + +CEvoDB::CEvoDB(const util::DbWrapperParams& db_params, bool check_binding) : + db{util::MakeDbWrapper({db_params.path / "evodb", db_params.memory, db_params.wipe, /*cache_size=*/64 << 20, db_params.options})}, + m_check_binding{check_binding} +{ + transaction_contexts.emplace(EvoDbIdentity::NORMAL, std::make_unique(*db, DataStream{})); + ++g_view_cache_epoch; } CEvoDB::~CEvoDB() = default; @@ -46,9 +103,14 @@ CEvoDB::TransactionContext& CEvoDB::GetContext(EvoDbIdentity identity) { auto it = transaction_contexts.find(identity); if (it == transaction_contexts.end()) { + if (identity == EvoDbIdentity::SNAPSHOT && !m_snapshot_generation) { + throw EvoViewBindingError("EvoDB: the snapshot identity has no generation bound"); + } + const uint32_t generation{identity == EvoDbIdentity::SNAPSHOT ? m_snapshot_generation->generation : 0}; // Construct the context before inserting so a throwing constructor // cannot leave a null entry behind for later dereference. - it = transaction_contexts.emplace(identity, std::make_unique(*db)).first; + auto context{std::make_unique(*db, CEvoDBStore::GenerationPrefix(generation))}; + it = transaction_contexts.emplace(identity, std::move(context)).first; } return *it->second; } @@ -58,12 +120,179 @@ const CEvoDB::TransactionContext& CEvoDB::GetContext(EvoDbIdentity identity) con return *transaction_contexts.at(identity); } -EvoDbIdentity CEvoDB::GetCurrentIdentity() const +namespace { +thread_local const EvoViewScope* g_innermost_scope{nullptr}; +} // namespace + +EvoViewScope::EvoViewScope(const EvoViewToken& token) : + m_token{token}, + m_outer{g_innermost_scope} +{ + if (m_token.db == nullptr) throw EvoViewBindingError("EvoViewScope: token names no EvoDB"); + m_token.db->CheckScopeAgainstTransaction(m_token); + g_innermost_scope = this; +} + +EvoViewScope::~EvoViewScope() +{ + Assume(g_innermost_scope == this); + g_innermost_scope = m_outer; +} + +const EvoViewToken* EvoViewScope::Innermost(const CEvoDB& db) +{ + for (const EvoViewScope* scope{g_innermost_scope}; scope != nullptr; scope = scope->m_outer) { + if (scope->m_token.db == &db) return &scope->m_token; + } + return nullptr; +} + +namespace { +//! How long an operation's rounds must go without failing before it counts as +//! recovered. One operation can stand for many tasks (every scheduler task is +//! "scheduler"), and one of them failing between the others' successes is not +//! a recovery. +constexpr std::chrono::seconds EVO_ROUND_RECOVERY_QUIET{60}; + +//! Causes logged per failing operation, beyond which the oldest are forgotten. +constexpr size_t MAX_LOGGED_EVO_ROUND_CAUSES{32}; + +struct FailingEvoRound { + //! The (view generation, reason) pairs logged, oldest first. + std::vector, std::string>> causes; + std::chrono::seconds last_failure; +}; +GlobalMutex g_evo_round_mutex; +std::map> g_failing_evo_rounds GUARDED_BY(g_evo_round_mutex); +std::atomic g_evo_rounds_failing{false}; +} // namespace + +void LogEvoRoundFailure(std::string_view what, std::optional generation, std::string_view reason) +{ + const auto now{GetTime()}; + LOCK(g_evo_round_mutex); + FailingEvoRound& failing{g_failing_evo_rounds[std::string{what}]}; + failing.last_failure = now; + const std::pair, std::string> cause{generation, reason}; + if (std::ranges::find(failing.causes, cause) != failing.causes.end()) return; + if (failing.causes.size() == MAX_LOGGED_EVO_ROUND_CAUSES) failing.causes.erase(failing.causes.begin()); + failing.causes.push_back(cause); + g_evo_rounds_failing = true; + LogPrintf("ERROR: %s: Evo state unavailable: %s. Repeats are not logged until the cause changes or %s recovers.\n", + what, reason, what); +} + +void LogEvoRoundRecovery(std::string_view what) +{ + if (!g_evo_rounds_failing.load(std::memory_order_relaxed)) return; + const auto now{GetTime()}; + LOCK(g_evo_round_mutex); + const auto it{g_failing_evo_rounds.find(what)}; + if (it == g_failing_evo_rounds.end() || now - it->second.last_failure < EVO_ROUND_RECOVERY_QUIET) return; + g_failing_evo_rounds.erase(it); + g_evo_rounds_failing = !g_failing_evo_rounds.empty(); + LogPrintf("%s: Evo state available again\n", what); +} + +std::optional EvoViewScope::InnermostAny() +{ + if (g_innermost_scope == nullptr) return std::nullopt; + return g_innermost_scope->m_token; +} + +bool CEvoDB::IsBound() const +{ + LOCK(cs); + return HoldsTransaction() || EvoViewScope::Innermost(*this) != nullptr; +} + +void CEvoDB::UpdateLiveGeneration() +{ + m_has_live_generation = m_snapshot_generation && (m_snapshot_generation->state == EvoGenerationState::PUBLISHED || + m_snapshot_generation->state == EvoGenerationState::PROMOTING); +} + +EvoViewToken CEvoDB::ActiveView() const +{ + LOCK(cs); + return MakeView(m_default_identity); +} + +bool CEvoDB::IsLive(const EvoViewToken& view) const +{ + LOCK(cs); + return IsLiveLocked(view); +} + +bool CEvoDB::IsLiveLocked(const EvoViewToken& view) const +{ + if (view.db != this) return false; + if (view.identity == EvoDbIdentity::NORMAL) return true; + return m_snapshot_generation && MakeView(EvoDbIdentity::SNAPSHOT) == view; +} + +EvoViewToken CEvoDB::MakeView(EvoDbIdentity identity) const +{ + if (identity == EvoDbIdentity::SNAPSHOT && m_snapshot_generation) { + return EvoViewToken{.db = this, + .identity = identity, + .generation = m_snapshot_generation->generation, + .base = m_snapshot_generation->base, + .base_height = m_snapshot_generation->base_height}; + } + return EvoViewToken{.db = this, .identity = identity}; +} + +EvoViewToken CEvoDB::ResolveView() const +{ + EvoViewToken view; + if (HoldsTransaction()) { + view = MakeView(*active_transaction); + } else if (const EvoViewToken * scoped{EvoViewScope::Innermost(*this)}) { + view = *scoped; + } else if (m_has_live_generation) { + // With two chainstates, an absent binding says nothing about which one + // the access serves; never guess. + throw EvoViewBindingError("unbound Evo access during dual-chainstate operation"); + } else if (m_check_binding) { + throw EvoViewBindingError("unbound Evo access (-checkevobinding)"); + } else { + return MakeView(m_default_identity); + } + if (view.identity == EvoDbIdentity::SNAPSHOT && view != MakeView(EvoDbIdentity::SNAPSHOT)) { + throw EvoViewBindingError(strprintf("Evo access bound to retired snapshot generation %u", view.generation)); + } + return view; +} + +bool CEvoDB::IsCurrentView(const EvoViewToken& view) const +{ + LOCK(cs); + try { + return ResolveView() == view; + } catch (const EvoViewBindingError&) { + return false; + } +} + +EvoViewToken CEvoDB::CurrentView() const +{ + LOCK(cs); + return ResolveView(); +} + +EvoViewToken CEvoDB::ViewFor(EvoDbIdentity identity) const +{ + LOCK(cs); + return MakeView(identity); +} + +void CEvoDB::CheckScopeAgainstTransaction(const EvoViewToken& token) const { - if (active_transaction.has_value() && active_transaction_thread == std::this_thread::get_id()) { - return *active_transaction; + LOCK(cs); + if (HoldsTransaction() && MakeView(*active_transaction) != token) { + throw EvoViewBindingError("EvoViewScope conflicts with this thread's open EvoDB transaction"); } - return m_default_identity; } std::unique_ptr CEvoDB::BeginTransaction(EvoDbIdentity identity) @@ -71,9 +300,12 @@ std::unique_ptr CEvoDB::BeginTransaction(EvoDbIdentity id AssertLockHeld(::cs_main); LOCK(cs); assert(!active_transaction.has_value()); + if (const EvoViewToken * scoped{EvoViewScope::Innermost(*this)}; scoped && *scoped != MakeView(identity)) { + throw EvoViewBindingError("EvoDB transaction conflicts with this thread's EvoViewScope"); + } + GetContext(identity); active_transaction = identity; active_transaction_thread = std::this_thread::get_id(); - GetContext(identity); return std::make_unique(*this, identity); } @@ -91,7 +323,11 @@ void CEvoDB::RollbackCurTransaction(EvoDbIdentity identity) AssertLockHeld(::cs_main); LOCK(cs); assert(active_transaction == identity); - GetContext(identity).cur_transaction.Clear(); + // The open transaction's context exists. Never create one here: that can + // throw, and this runs from CEvoDBScopedCommitter's destructor. + if (const auto it{transaction_contexts.find(identity)}; it != transaction_contexts.end()) { + it->second->cur_transaction.Clear(); + } active_transaction.reset(); } @@ -110,6 +346,7 @@ bool CEvoDB::CommitRootTransaction(EvoDbIdentity identity, bool sync) bool CEvoDB::ReadBestBlock(EvoDbIdentity identity, uint256& hash) { LOCK(cs); + if (identity == EvoDbIdentity::SNAPSHOT && !m_snapshot_generation) return false; auto& transaction = GetContext(identity).cur_transaction; if (identity == EvoDbIdentity::NORMAL) { return transaction.Read(EVODB_BEST_BLOCK, hash); @@ -138,7 +375,8 @@ void CEvoDB::WriteBestBlock(EvoDbIdentity identity, const uint256& hash) void CEvoDB::WriteDualChainstateMarker() { - Write(EVODB_DUAL_CHAINSTATE, uint8_t{1}); + LOCK(cs); + db->Write(EVODB_DUAL_CHAINSTATE, uint8_t{1}); } bool CEvoDB::HasDualChainstateMarker() @@ -147,19 +385,10 @@ bool CEvoDB::HasDualChainstateMarker() return db->Exists(EVODB_DUAL_CHAINSTATE); } -void CEvoDB::EraseSnapshotMarkers() -{ - LOCK(cs); - auto& transaction = GetContext(GetCurrentIdentity()).cur_transaction; - transaction.Erase(std::make_pair(EVODB_BEST_BLOCK, uint8_t{1})); - transaction.Erase(EVODB_SNAPSHOT_MNLIST_HASH); - transaction.Erase(EVODB_BACKGROUND_MNLIST_HASH); - transaction.Erase(EVODB_DUAL_CHAINSTATE); -} - void CEvoDB::WriteSnapshotBaseMNListHash(const uint256& hash) { - Write(EVODB_SNAPSHOT_MNLIST_HASH, hash); + LOCK(cs); + db->Write(EVODB_SNAPSHOT_MNLIST_HASH, hash); } bool CEvoDB::ReadSnapshotBaseMNListHash(uint256& hash) @@ -186,59 +415,518 @@ bool CEvoDB::ReadBackgroundMNListHash(uint256& block_hash, uint256& mn_list_hash return true; } -bool CEvoDB::PromoteSnapshotMarkers(const uint256& expected_snapshot_tip) +std::optional CEvoDB::GetSnapshotGeneration() const { LOCK(cs); - assert(!active_transaction.has_value()); - for (const auto& [_, context] : transaction_contexts) { - if (!context) continue; - assert(context->cur_transaction.IsClean()); - assert(context->root_transaction.IsClean()); + return m_snapshot_generation; +} + +void CEvoDB::BindSnapshotGeneration(std::optional record) +{ + // A context's store is keyed by the generation it was built for. + if (auto it{transaction_contexts.find(EvoDbIdentity::SNAPSHOT)}; it != transaction_contexts.end()) { + Assume(it->second->cur_transaction.IsClean() && it->second->root_transaction.IsClean()); + transaction_contexts.erase(it); } + m_snapshot_generation = std::move(record); + UpdateLiveGeneration(); + if (!m_snapshot_generation) m_default_identity = EvoDbIdentity::NORMAL; +} - const auto snapshot_key = std::make_pair(EVODB_BEST_BLOCK, uint8_t{1}); - uint256 snapshot_tip; - if (!db->Read(snapshot_key, snapshot_tip)) { - uint256 normal_tip; - const bool already_promoted = db->Read(EVODB_BEST_BLOCK, normal_tip) && normal_tip == expected_snapshot_tip && - !db->Exists(EVODB_DUAL_CHAINSTATE) && !db->Exists(EVODB_SNAPSHOT_MNLIST_HASH) && - !db->Exists(EVODB_BACKGROUND_MNLIST_HASH); - if (already_promoted) m_default_identity = EvoDbIdentity::NORMAL; - return already_promoted; +bool CEvoDB::TransactionsClean() const +{ + if (active_transaction.has_value()) return false; + return std::ranges::all_of(transaction_contexts, [](const auto& entry) { + return !entry.second || (entry.second->cur_transaction.IsClean() && entry.second->root_transaction.IsClean()); + }); +} + +namespace { +bool IsKnown(EvoGenerationState state) { return state <= EvoGenerationState::PROMOTING; } + +bool IsValid(const EvoPromotionJournal& journal) +{ + return journal.schema_version == EvoGenerationRecord::SCHEMA_VERSION && journal.phase <= EvoPromotionPhase::MERGED; +} + +std::string DescribePhase(const EvoGenerationRecord& record, std::optional promotion) +{ + if (record.state != EvoGenerationState::PROMOTING) return "published, not promoted"; + switch (promotion.value_or(EvoPromotionPhase::PREPARED)) { + case EvoPromotionPhase::PREPARED: return "promotion prepared"; + case EvoPromotionPhase::MERGING: return "promotion merging"; + case EvoPromotionPhase::MERGED: return "promotion merged"; } - if (snapshot_tip != expected_snapshot_tip) return false; + return "promoting"; +} +} // namespace +bool CEvoDB::CreateSnapshotGeneration(const uint256& base, int base_height) +{ + LOCK(cs); + if (m_snapshot_generation || m_revoked_generation) return false; + uint32_t generation{1}; + db->Read(EVODB_GENERATION_NEXT, generation); + // Generation 0 names the NORMAL keyspace; never wrap into it. + if (generation == 0 || generation == std::numeric_limits::max()) { + LogPrintf("ERROR: %s: snapshot EvoDB generation ids are exhausted\n", __func__); + return false; + } + EvoGenerationRecord record; + record.generation = generation; + record.state = EvoGenerationState::UNPUBLISHED; + record.base = base; + record.base_height = base_height; + record.tip = base; + CDBBatch batch{*db}; + batch.Write(EVODB_GENERATION_NEXT, generation + 1); + batch.Write(GenerationRecordKey(generation), record); + if (!db->WriteBatch(batch, /*fSync=*/true)) return false; + BindSnapshotGeneration(record); + return true; +} + +bool CEvoDB::PublishSnapshotGeneration(const uint256& config_fingerprint, const std::optional& base_mn_list_hash, + const std::vector& evo_section) +{ + LOCK(cs); + if (!m_snapshot_generation || m_snapshot_generation->state != EvoGenerationState::UNPUBLISHED) return false; + EvoGenerationRecord record{*m_snapshot_generation}; + record.state = EvoGenerationState::PUBLISHED; + record.config_fingerprint = config_fingerprint; + CDBBatch batch{*db}; + batch.Write(GenerationRecordKey(record.generation), record); + batch.Write(EVODB_DUAL_CHAINSTATE, uint8_t{1}); + if (base_mn_list_hash) batch.Write(EVODB_SNAPSHOT_MNLIST_HASH, *base_mn_list_hash); + if (!evo_section.empty()) batch.Write(EVODB_SNAPSHOT_EVO_SECTION, evo_section); + if (!db->WriteBatch(batch, /*fSync=*/true)) return false; + m_snapshot_generation = record; + UpdateLiveGeneration(); + return true; +} + +std::optional> CEvoDB::ReadRetainedEvoSection() const +{ + LOCK(cs); + std::vector evo_section; + if (!db->Read(EVODB_SNAPSHOT_EVO_SECTION, evo_section)) return std::nullopt; + return evo_section; +} + +util::Result CEvoDB::LoadSnapshotGenerations(const uint256& config_fingerprint) +{ + LOCK(cs); + if (!Assume(TransactionsClean())) return util::Error{_("Evo database transactions are pending at startup.")}; + BindSnapshotGeneration(std::nullopt); + m_revoked_generation = false; + std::vector records; + { + std::unique_ptr it{db->NewIterator()}; + it->Seek(GenerationRecordKey(0)); + for (; it->Valid() && !it->HasError(); it->Next()) { + std::pair key; + if (!it->GetKey(key) || key.first != EVODB_GENERATION_RECORD) break; + EvoGenerationRecord record; + if (!it->GetValue(record) || record.schema_version != EvoGenerationRecord::SCHEMA_VERSION || + !IsKnown(record.state) || record.generation == 0 || record.generation != key.second) { + return util::Error{_("The Evo database holds assumeutxo snapshot state in an unsupported format. " + "Restart with -reindex-chainstate to discard it.")}; + } + records.push_back(record); + } + if (it->HasError()) { + return util::Error{_("The Evo database's assumeutxo snapshot state cannot be read. " + "Restart with -reindex-chainstate to discard it.")}; + } + } + + std::optional live; + for (const EvoGenerationRecord& record : records) { + if (record.state != EvoGenerationState::PUBLISHED && record.state != EvoGenerationState::PROMOTING) continue; + if (live) { + return util::Error{_("The Evo database holds more than one live assumeutxo snapshot state. " + "Restart with -reindex-chainstate to discard them.")}; + } + live = record; + } + + // Once a promotion has merged and collected its generation, only the + // journal remains to finish it. + EvoPromotionJournal journal; + const bool finishing_promotion{db->Read(EVODB_PROMOTION_JOURNAL, journal)}; + if (db->Exists(EVODB_PROMOTION_JOURNAL) && (!finishing_promotion || !IsValid(journal))) { + return util::Error{_("The Evo database holds an assumeutxo promotion journal in an unsupported format. " + "Restart with -reindex-chainstate to discard it.")}; + } + // Refuse before changing anything on disk, so that the previous version or + // configuration can still resume the snapshot. + if (live && live->config_fingerprint != config_fingerprint) { + return util::Error{strprintf( + _("The consensus configuration or its fingerprint version changed since the assumeutxo snapshot's " + "Evo state (generation %u, base block %s at height %d, %s) was recorded, so it can no longer be " + "trusted. Changed component unknown (only the aggregate fingerprint was recorded). Restart with the " + "previous version and configuration, or with -reindex-chainstate to discard the snapshot."), + live->generation, live->base.ToString(), live->base_height, + DescribePhase(*live, finishing_promotion && journal.generation == live->generation + ? std::optional{journal.phase} + : std::nullopt))}; + } + for (const EvoGenerationRecord& record : records) { + if (record.state == EvoGenerationState::UNPUBLISHED && + !CollectGeneration(record.generation, /*erase_record=*/true)) { + return util::Error{_("Failed to remove discarded assumeutxo snapshot state from the Evo database. " + "Restart with -reindex-chainstate to discard it.")}; + } + // A rejected generation is kept until startup recovery has invalidated + // its coins directory. + } + if (!live && !finishing_promotion && + (db->Exists(EVODB_DUAL_CHAINSTATE) || db->Exists(std::make_pair(EVODB_BEST_BLOCK, uint8_t{1})))) { + return util::Error{_("An assumeutxo snapshot in progress was loaded by an older version that did not " + "isolate its Evo state. Restart with -reindex-chainstate to discard it.")}; + } + BindSnapshotGeneration(live); + return {}; +} + +namespace { +//! The CEvoDBs this thread holds an active-chain turn on. +thread_local std::vector t_active_turns; +} // namespace + +void CEvoDB::EnterActiveTurn() const +{ + t_active_turns.push_back(this); + ++m_active_turns; +} + +void CEvoDB::LeaveActiveTurn() const +{ + if (const auto it{std::find(t_active_turns.rbegin(), t_active_turns.rend(), this)}; it != t_active_turns.rend()) { + t_active_turns.erase(std::next(it).base()); + } + --m_active_turns; + // A waiter counts itself before it reads the turns under the mutex. + // Taking the mutex here orders this decrement before that read, or after + // the waiter went to sleep, so the notification wakes it. + if (m_turn_waiters.load() == 0) return; + { + LOCK(m_turns_mutex); + } + m_turns_cv.notify_all(); +} + +int CEvoDB::WaitForOtherActiveTurns(std::chrono::milliseconds timeout) const +{ + AssertLockNotHeld(::cs_main); + const int own{static_cast(std::count(t_active_turns.begin(), t_active_turns.end(), this))}; + ++m_turn_waiters; + int others; + { + WAIT_LOCK(m_turns_mutex, lock); + m_turns_cv.wait_for(lock, timeout, [&] { return m_active_turns.load() <= own; }); + others = m_active_turns.load() - own; + } + --m_turn_waiters; + return others; +} + +bool CEvoDB::HasRejectedGeneration() const +{ + LOCK(cs); + // Records that cannot be read count, so that the recovery that collects + // rejected generations runs and fails. + const auto rejected{RejectedGenerations()}; + return !rejected || !rejected->empty(); +} + +std::optional> CEvoDB::RejectedGenerations() const +{ + std::vector rejected; + std::unique_ptr it{db->NewIterator()}; + for (it->Seek(GenerationRecordKey(0)); it->Valid() && !it->HasError(); it->Next()) { + std::pair key; + if (!it->GetKey(key) || key.first != EVODB_GENERATION_RECORD) break; + EvoGenerationRecord record; + if (it->GetValue(record) && record.state == EvoGenerationState::REJECTED) rejected.push_back(key.second); + } + if (it->HasError()) { + LogPrintf("ERROR: %s: cannot read the snapshot generation records\n", __func__); + return std::nullopt; + } + return rejected; +} + +bool CEvoDB::CollectRejectedGenerations() +{ + LOCK(cs); + const auto rejected{RejectedGenerations()}; + if (!rejected) return false; + for (const uint32_t generation : *rejected) { + if (!CollectGeneration(generation, /*erase_record=*/true)) return false; + } + return true; +} + +bool CEvoDB::CollectGeneration(uint32_t generation, bool erase_record) +{ + if (!Assume(generation != 0)) return false; + const DataStream prefix{CEvoDBStore::GenerationPrefix(generation)}; + std::unique_ptr it{db->NewIterator()}; + CDBBatch batch{*db}; + size_t pending{0}; + for (it->Seek(prefix); it->Valid() && !it->HasError() && StartsWith(it->GetKey(), prefix); it->Next()) { + batch.Erase(it->GetKey()); + if (++pending >= m_merge_batch_size) { + if (!db->WriteBatch(batch)) return false; + batch.Clear(); + pending = 0; + } + } + // A read error ends the scan early: keep the record that owns what is left. + if (it->HasError()) { + LogPrintf("ERROR: %s: cannot read snapshot generation %u; it is kept\n", __func__, generation); + return false; + } + if (erase_record) batch.Erase(GenerationRecordKey(generation)); + return db->WriteBatch(batch, /*fSync=*/true); +} + +bool CEvoDB::DiscardBoundGeneration() +{ CDBBatch batch{*db}; - batch.Write(EVODB_BEST_BLOCK, snapshot_tip); - batch.Erase(snapshot_key); batch.Erase(EVODB_SNAPSHOT_MNLIST_HASH); batch.Erase(EVODB_BACKGROUND_MNLIST_HASH); + batch.Erase(EVODB_SNAPSHOT_EVO_SECTION); batch.Erase(EVODB_DUAL_CHAINSTATE); + batch.Erase(EVODB_PROMOTION_JOURNAL); + if (m_snapshot_generation) { + EvoGenerationRecord record{*m_snapshot_generation}; + record.state = EvoGenerationState::REJECTED; + batch.Write(GenerationRecordKey(record.generation), record); + } + // A failure to persist the rejection leaves the generation live, and the + // dual-chainstate markers with it; startup recovery then retries. The + // REJECTED record stays until the snapshot's coins directory is + // invalidated (CollectRejectedGenerations), so a crash in between can + // still finish the invalidation. if (!db->WriteBatch(batch, /*fSync=*/true)) return false; - // The dual-chainstate run is over: the promoted state is the NORMAL - // identity, so transaction-less access must resolve there again. - m_default_identity = EvoDbIdentity::NORMAL; + BindSnapshotGeneration(std::nullopt); return true; } +bool CEvoDB::AbandonSnapshotGeneration() +{ + AssertLockHeld(::cs_main); + LOCK(cs); + if (!Assume(!active_transaction.has_value())) return false; + ClearSnapshotTransactions(); + return DiscardBoundGeneration(); +} + +void CEvoDB::RevokeSnapshotGeneration() +{ + AssertLockHeld(::cs_main); + LOCK(cs); + if (m_snapshot_generation) m_revoked_generation = true; + ClearSnapshotTransactions(); + BindSnapshotGeneration(std::nullopt); +} + +void CEvoDB::ClearSnapshotTransactions() +{ + if (auto it{transaction_contexts.find(EvoDbIdentity::SNAPSHOT)}; it != transaction_contexts.end()) { + it->second->cur_transaction.Clear(); + it->second->root_transaction.Clear(); + it->second->root_batch.Clear(); + } +} + bool CEvoDB::DiscardSnapshotMarkers() { LOCK(cs); - assert(!active_transaction.has_value()); - for (const auto& [_, context] : transaction_contexts) { - if (!context) continue; - assert(context->cur_transaction.IsClean()); - assert(context->root_transaction.IsClean()); + if (!Assume(TransactionsClean()) || m_rejection_failpoint) return false; + return DiscardBoundGeneration(); +} + +bool CEvoDB::PrepareSnapshotPromotion(const uint256& snapshot_tip) +{ + LOCK(cs); + if (!m_snapshot_generation) return false; + // The coins tip being promoted must be the tip the generation's Evo state + // was flushed at. + uint256 evo_tip; + if (!GetContext(EvoDbIdentity::SNAPSHOT).cur_transaction.Read(std::make_pair(EVODB_BEST_BLOCK, uint8_t{1}), evo_tip) || + evo_tip != snapshot_tip) { + return false; } + EvoPromotionJournal journal; + if (db->Read(EVODB_PROMOTION_JOURNAL, journal)) { + // Only a promotion that has not renamed anything yet may be redone. + if (!IsValid(journal) || journal.generation != m_snapshot_generation->generation || + journal.phase != EvoPromotionPhase::PREPARED) { + return false; + } + } + journal = EvoPromotionJournal{}; + journal.generation = m_snapshot_generation->generation; + journal.base = m_snapshot_generation->base; + journal.tip = snapshot_tip; + uint256 base_mn_list_hash; + if (db->Read(EVODB_SNAPSHOT_MNLIST_HASH, base_mn_list_hash)) journal.base_mn_list_hash = base_mn_list_hash; + EvoGenerationRecord record{*m_snapshot_generation}; + record.state = EvoGenerationState::PROMOTING; + record.tip = snapshot_tip; + CDBBatch batch{*db}; + batch.Write(EVODB_PROMOTION_JOURNAL, journal); + batch.Write(GenerationRecordKey(record.generation), record); + if (!db->WriteBatch(batch, /*fSync=*/true)) return false; + m_snapshot_generation = record; + UpdateLiveGeneration(); + return true; +} + +std::optional CEvoDB::ReadPromotionJournal() const +{ + LOCK(cs); + EvoPromotionJournal journal; + if (!db->Read(EVODB_PROMOTION_JOURNAL, journal)) return std::nullopt; + return journal; +} + +bool CEvoDB::MergeGeneration(EvoPromotionJournal& journal) +{ + const DataStream prefix{CEvoDBStore::GenerationPrefix(journal.generation)}; + DataStream best_block_key{}; + best_block_key << std::make_pair(EVODB_BEST_BLOCK, uint8_t{1}); + DataStream llmq_best_block_key{}; + llmq_best_block_key << EVODB_LLMQ_BEST_BLOCK; + + DataStream manifest_tag{}; + manifest_tag << EVODB_SEED_MANIFEST; + + std::unique_ptr it{db->NewIterator()}; + DataStream start{prefix}; + start.write(MakeByteSpan(journal.cursor)); + it->Seek(start); + // The cursor names the last merged key; resume after it. + if (!journal.cursor.empty() && it->Valid() && SameBytes(it->GetKey(), start)) it->Next(); CDBBatch batch{*db}; - batch.Erase(std::make_pair(EVODB_BEST_BLOCK, uint8_t{1})); + size_t pending{0}; + // A read error can skip records and leave the iterator valid past them: + // stop before handling, or moving the cursor past, any later key. + for (; it->Valid() && !it->HasError(); it->Next()) { + const DataStream raw_key{it->GetKey()}; + if (!StartsWith(raw_key, prefix)) break; + DataStream key{raw_key}; + key.ignore(prefix.size()); + // The promoted best block is published with the final markers, and the + // seed manifest describes this generation only. + if (!SameBytes(key, best_block_key) && !StartsWith(key, manifest_tag)) { + CDataStream value{SER_DISK, CLIENT_VERSION}; + if (!db->ReadDataStream(raw_key, value)) return false; + CDataStream normal_value{SER_DISK, CLIENT_VERSION}; + const bool in_normal{db->ReadDataStream(key, normal_value)}; + DataStream manifest_key{prefix}; + manifest_key << SeedManifestKey(key); + CDataStream policy{SER_DISK, CLIENT_VERSION}; + if (db->ReadDataStream(manifest_key, policy)) { + if (policy.size() != 1 || + !SeedAgreesWithNormal(std::to_integer(policy[0]), in_normal ? &normal_value : nullptr, value)) { + LogPrintf("ERROR: %s: seed of snapshot generation %u disagrees with NORMAL state for key %s\n", + __func__, journal.generation, HexStr(key)); + return false; + } + } else if (in_normal && !SameBytes(key, llmq_best_block_key)) { + // Unseeded records are derived from blocks above the base, which + // NORMAL never connected while the generation was live. + LogPrintf("ERROR: %s: snapshot generation %u overlaps NORMAL state for key %s\n", __func__, + journal.generation, HexStr(key)); + return false; + } else { + batch.Write(key, Span{value}); + } + } + journal.cursor.assign(UCharCast(key.data()), UCharCast(key.data() + key.size())); + if (++pending >= m_merge_batch_size) { + batch.Write(EVODB_PROMOTION_JOURNAL, journal); + if (!db->WriteBatch(batch, /*fSync=*/true)) return false; + batch.Clear(); + pending = 0; + if (m_promotion_failpoint == PromotionFailpoint::MID_MERGE) return false; + } + } + if (it->HasError()) { + LogPrintf("ERROR: %s: cannot read snapshot generation %u; the promotion stays journaled\n", __func__, + journal.generation); + return false; + } + journal.phase = EvoPromotionPhase::MERGED; + journal.cursor.clear(); + batch.Write(EVODB_PROMOTION_JOURNAL, journal); + return db->WriteBatch(batch, /*fSync=*/true); +} + +bool CEvoDB::SeedAgreesWithNormal(uint8_t policy, const CDataStream* normal, const CDataStream& seed) const +{ + switch (static_cast(policy)) { + case EvoSeedPolicy::MN_LIST: + return normal == nullptr || + (m_mn_list_equivalence && m_mn_list_equivalence(MakeByteSpan(*normal), MakeByteSpan(seed))); + case EvoSeedPolicy::BYTE_DETERMINISTIC: + return normal == nullptr || std::equal(normal->begin(), normal->end(), seed.begin(), seed.end()); + case EvoSeedPolicy::SCAFFOLDING: + return true; + } + // A family this version does not know: fail closed. + return false; +} + +bool CEvoDB::PromoteSnapshotMarkers(const uint256& expected_snapshot_tip) +{ + LOCK(cs); + if (!Assume(TransactionsClean())) return false; + + EvoPromotionJournal journal; + if (!db->Read(EVODB_PROMOTION_JOURNAL, journal)) { + uint256 normal_tip; + const bool already_promoted = db->Read(EVODB_BEST_BLOCK, normal_tip) && normal_tip == expected_snapshot_tip && + !m_snapshot_generation && !db->Exists(EVODB_DUAL_CHAINSTATE) && + !db->Exists(EVODB_SNAPSHOT_MNLIST_HASH) && + !db->Exists(EVODB_BACKGROUND_MNLIST_HASH); + if (already_promoted) m_default_identity = EvoDbIdentity::NORMAL; + return already_promoted; + } + if (!IsValid(journal) || journal.tip != expected_snapshot_tip) return false; + + if (journal.phase == EvoPromotionPhase::PREPARED) { + journal.phase = EvoPromotionPhase::MERGING; + if (!db->Write(EVODB_PROMOTION_JOURNAL, journal, /*fSync=*/true)) return false; + } + if (journal.phase == EvoPromotionPhase::MERGING && !MergeGeneration(journal)) return false; + if (m_promotion_failpoint == PromotionFailpoint::AFTER_MERGE) return false; + + // Everything is merged: collecting the namespace (idempotent) cannot lose + // data, and the journal still carries the promoted tip. The PROMOTING + // record, with the configuration fingerprint startup checks, stays until + // the final batch. + BindSnapshotGeneration(std::nullopt); + if (!CollectGeneration(journal.generation, /*erase_record=*/false)) return false; + if (m_promotion_failpoint == PromotionFailpoint::AFTER_COLLECT) return false; + + CDBBatch batch{*db}; + batch.Write(EVODB_BEST_BLOCK, journal.tip); batch.Erase(EVODB_SNAPSHOT_MNLIST_HASH); batch.Erase(EVODB_BACKGROUND_MNLIST_HASH); + batch.Erase(EVODB_SNAPSHOT_EVO_SECTION); batch.Erase(EVODB_DUAL_CHAINSTATE); + batch.Erase(EVODB_PROMOTION_JOURNAL); + batch.Erase(GenerationRecordKey(journal.generation)); if (!db->WriteBatch(batch, /*fSync=*/true)) return false; - // The snapshot chainstate is gone; transaction-less access must resolve - // against the NORMAL identity again. + // The dual-chainstate run is over: the promoted state is the NORMAL + // identity, so transaction-less access must resolve there again. m_default_identity = EvoDbIdentity::NORMAL; + ++g_view_cache_epoch; return true; } diff --git a/src/evo/evodb.h b/src/evo/evodb.h index f9485eb09b89..cf90361e4610 100644 --- a/src/evo/evodb.h +++ b/src/evo/evodb.h @@ -6,16 +6,28 @@ #define BITCOIN_EVO_EVODB_H #include +#include #include +#include +#include +#include #include +#include +#include #include +#include +#include +#include +#include +#include #include #include #include +#include +#include #include -class uint256; namespace util { struct DbWrapperParams; } // namespace util @@ -32,6 +44,22 @@ static const std::string EVODB_BEST_BLOCK = "b_b4"; static const std::string EVODB_DUAL_CHAINSTATE = "b_dcs"; static const std::string EVODB_SNAPSHOT_MNLIST_HASH = "b_dcs_mn"; static const std::string EVODB_BACKGROUND_MNLIST_HASH = "b_dcs_bg_mn"; +//! The evo section the live snapshot generation was seeded from. +static const std::string EVODB_SNAPSHOT_EVO_SECTION = "b_dcs_evo"; +// Snapshot-identity lifecycle metadata. It lives in the legacy keyspace and is +// never part of a generation namespace. +static const std::string EVODB_GENERATION_RECORD = "b_evo_gen"; +static const std::string EVODB_GENERATION_NEXT = "b_evo_gen_next"; +static const std::string EVODB_PROMOTION_JOURNAL = "b_evo_promote"; +// Every key the snapshot identity writes is stored as +// (EVODB_GENERATION_NAMESPACE, generation, key). +static const std::string EVODB_GENERATION_NAMESPACE = "S"; +// Mutable singleton records whose snapshot-identity value supersedes the +// NORMAL one when a generation is promoted. +static const std::string EVODB_LLMQ_BEST_BLOCK = "q_bbu2"; +// A generation's seed manifest: (EVODB_SEED_MANIFEST, key) -> EvoSeedPolicy for +// every key it was seeded with, stored in the generation's own namespace. +static const std::string EVODB_SEED_MANIFEST = "b_evo_seed"; enum class EvoDbIdentity { NORMAL, @@ -50,6 +78,213 @@ class EvoDbInconsistencyError : public std::runtime_error class CEvoDB; +/** Names the Evo state one operation reads and writes: the NORMAL view, or + * one generation of the SNAPSHOT view, whose snapshot starts at `base`. */ +struct EvoViewToken { + const CEvoDB* db{nullptr}; + EvoDbIdentity identity{EvoDbIdentity::NORMAL}; + uint32_t generation{0}; + uint256 base{}; + int base_height{-1}; + + bool operator==(const EvoViewToken&) const = default; +}; + +/** How promotion merges a key a snapshot generation was seeded with. A seed + * restates state below the snapshot base that NORMAL derives for itself, so + * it is never copied into NORMAL; where NORMAL holds the same key, the two + * must agree, or promotion fails. */ +enum class EvoSeedPolicy : uint8_t { + //! A masternode list, whose honest encodings can differ: canonically equal. + MN_LIST = 1, + //! A byte-deterministic block-derived record: byte-equal. + BYTE_DETERMINISTIC = 2, + //! Snapshot-only scaffolding, never compared. + SCAFFOLDING = 3, +}; + +enum class EvoGenerationState : uint8_t { + UNPUBLISHED = 0, + PUBLISHED = 1, + REJECTED = 2, + PROMOTING = 3, +}; + +/** Lifecycle record of one snapshot-identity generation. */ +struct EvoGenerationRecord { + //! Local on-disk schema of generations and the promotion journal. It is + //! independent of the snapshot wire format. + static constexpr uint16_t SCHEMA_VERSION{1}; + + uint16_t schema_version{SCHEMA_VERSION}; + uint32_t generation{0}; + EvoGenerationState state{EvoGenerationState::UNPUBLISHED}; + uint256 base; + int32_t base_height{-1}; + uint256 tip; + //! Fingerprint of the consensus configuration the generation was + //! published under; see EvoSnapshotConsensusConfig. + uint256 config_fingerprint; + + SERIALIZE_METHODS(EvoGenerationRecord, obj) + { + uint8_t state{static_cast(obj.state)}; + READWRITE(obj.schema_version, obj.generation, state, obj.base, obj.base_height, obj.tip, obj.config_fingerprint); + SER_READ(obj, obj.state = static_cast(state)); + } +}; + +enum class EvoPromotionPhase : uint8_t { + //! Decided, before the first coins-directory rename. + PREPARED = 0, + //! Both coins-directory renames are durable; merging into NORMAL keys. + MERGING = 1, + //! Every record is merged; the generation namespace may be collected. + MERGED = 2, +}; + +/** Journal of a snapshot-generation promotion, written before the first + * coins-directory rename and removed together with the final markers. */ +struct EvoPromotionJournal { + uint16_t schema_version{EvoGenerationRecord::SCHEMA_VERSION}; + uint32_t generation{0}; + uint256 base; + uint256 tip; + uint256 base_mn_list_hash; + EvoPromotionPhase phase{EvoPromotionPhase::PREPARED}; + //! Generation-relative key the merge resumes after; empty to start over. + std::vector cursor; + + SERIALIZE_METHODS(EvoPromotionJournal, obj) + { + uint8_t phase{static_cast(obj.phase)}; + READWRITE(obj.schema_version, obj.generation, obj.base, obj.tip, obj.base_mn_list_hash, phase, obj.cursor); + SER_READ(obj, obj.phase = static_cast(phase)); + } +}; + +/** Read side of one view of the EvoDB keyspace: every key is stored behind + * `prefix`. The NORMAL view has an empty prefix and skips all generation + * namespaces, so neither view ever sees the other's records. */ +class CEvoDBStore +{ +public: + class Iterator + { + public: + Iterator(const CEvoDBStore& store, CDBIterator* it) : + m_store{store}, + m_it{it} + { + } + void SeekToFirst() + { + m_it->Seek(m_store.m_prefix); + SkipForeign(); + } + void Seek(const DataStream& key) + { + m_it->Seek(m_store.Key(key)); + SkipForeign(); + } + void Next() + { + m_it->Next(); + SkipForeign(); + } + bool Valid() { return m_it->Valid() && m_store.Owns(m_it->GetKey()); } + bool HasError() const { return m_it->HasError(); } + DataStream GetKey() + { + DataStream key{m_it->GetKey()}; + key.ignore(m_store.m_prefix.size()); + return key; + } + unsigned int GetKeySize() { return m_it->GetKeySize() - m_store.m_prefix.size(); } + + private: + void SkipForeign(); + + const CEvoDBStore& m_store; + std::unique_ptr m_it; + }; + + CEvoDBStore(CDBWrapper& db, DataStream prefix) : + m_db{db}, + m_prefix{std::move(prefix)} + { + } + + //! Prefix of generation `generation`'s namespace; generation 0 names the + //! NORMAL view, whose prefix is empty. + static DataStream GenerationPrefix(uint32_t generation); + + DataStream Key(const DataStream& key) const + { + DataStream out{m_prefix}; + out.write(Span{key.data(), key.size()}); + return out; + } + bool Owns(const DataStream& raw_key) const; + + template + bool Read(const DataStream& key, V& value) const + { + return m_db.Read(Key(key), value); + } + bool Exists(const DataStream& key) const { return m_db.Exists(Key(key)); } + Iterator* NewIterator() const { return new Iterator(*this, m_db.NewIterator()); } + +private: + CDBWrapper& m_db; + const DataStream m_prefix; +}; + +/** Write side of one view: a commit target that stores keys behind the view's + * prefix. */ +class CEvoDBStoreBatch +{ +public: + CEvoDBStoreBatch(CDBBatch& batch, const CEvoDBStore& store) : + m_batch{batch}, + m_store{store} + { + } + template + void Write(const DataStream& key, const V& value) + { + m_batch.Write(m_store.Key(key), value); + } + void Erase(const DataStream& key) { m_batch.Erase(m_store.Key(key)); } + +private: + CDBBatch& m_batch; + const CEvoDBStore& m_store; +}; + +/** Binds the calling thread's Evo accesses on `token.db` to `token` until + * destruction, which restores the enclosing binding. Scopes nest; the + * innermost one for a CEvoDB wins. A scope must agree with a transaction the + * same thread holds open on that CEvoDB, or construction raises + * EvoViewBindingError. */ +class EvoViewScope +{ +public: + explicit EvoViewScope(const EvoViewToken& token); + ~EvoViewScope(); + EvoViewScope(const EvoViewScope&) = delete; + EvoViewScope& operator=(const EvoViewScope&) = delete; + + //! The innermost token this thread bound for `db`, if any. + static const EvoViewToken* Innermost(const CEvoDB& db); + //! The innermost token this thread bound for any CEvoDB, if any. + static std::optional InnermostAny(); + +private: + const EvoViewToken m_token; + const EvoViewScope* const m_outer; +}; + class CEvoDBScopedCommitter { private: @@ -72,17 +307,21 @@ class CEvoDB private: std::unique_ptr db; - using RootTransaction = CDBTransaction; + using RootTransaction = CDBTransaction; using CurTransaction = CDBTransaction; struct TransactionContext { + CEvoDBStore store; CDBBatch root_batch; + CEvoDBStoreBatch commit_target; RootTransaction root_transaction; CurTransaction cur_transaction; - explicit TransactionContext(CDBWrapper& db) : + TransactionContext(CDBWrapper& db, DataStream prefix) : + store{db, std::move(prefix)}, root_batch{db}, - root_transaction{db, root_batch}, + commit_target{root_batch, store}, + root_transaction{store, commit_target}, cur_transaction{root_transaction, root_transaction} { } @@ -105,13 +344,23 @@ class CEvoDB TransactionContext& GetContext(EvoDbIdentity identity) EXCLUSIVE_LOCKS_REQUIRED(cs); const TransactionContext& GetContext(EvoDbIdentity identity) const EXCLUSIVE_LOCKS_REQUIRED(cs); - EvoDbIdentity GetCurrentIdentity() const EXCLUSIVE_LOCKS_REQUIRED(cs); + EvoDbIdentity GetCurrentIdentity() const EXCLUSIVE_LOCKS_REQUIRED(cs) { return ResolveView().identity; } + EvoViewToken MakeView(EvoDbIdentity identity) const EXCLUSIVE_LOCKS_REQUIRED(cs); + bool IsLiveLocked(const EvoViewToken& view) const EXCLUSIVE_LOCKS_REQUIRED(cs); + EvoViewToken ResolveView() const EXCLUSIVE_LOCKS_REQUIRED(cs); + bool HoldsTransaction() const EXCLUSIVE_LOCKS_REQUIRED(cs) + { + return active_transaction.has_value() && active_transaction_thread == std::this_thread::get_id(); + } public: CEvoDB() = delete; CEvoDB(const CEvoDB&) = delete; CEvoDB& operator=(const CEvoDB&) = delete; - explicit CEvoDB(const util::DbWrapperParams& db_params); + //! With `check_binding` (-checkevobinding), an access that no transaction + //! or scope binds raises EvoViewBindingError even while only one view + //! exists, as it always does during dual-chainstate operation. + explicit CEvoDB(const util::DbWrapperParams& db_params, bool check_binding = false); ~CEvoDB(); std::unique_ptr BeginTransaction(EvoDbIdentity identity = EvoDbIdentity::NORMAL) @@ -124,9 +373,61 @@ class CEvoDB bool HasActiveTransaction() const EXCLUSIVE_LOCKS_REQUIRED(!cs) { LOCK(cs); - return active_transaction.has_value() && active_transaction_thread == std::this_thread::get_id(); + return HoldsTransaction(); + } + + //! Changes whenever cached Evo-derived state of every view may have gone + //! stale: a CEvoDB was opened, or a snapshot generation was promoted into + //! NORMAL. EvoViewCache drops every partition when it changes. Retiring a + //! generation any other way leaves NORMAL untouched. + static uint64_t ViewCacheEpoch() { return g_view_cache_epoch.load(); } + //! Run `f(ViewCacheEpoch(), live_generation)` if `view` is a live view of + //! this CEvoDB (NORMAL, or the snapshot generation bound now, published or + //! not), and return whether it ran. `live_generation` is that bound + //! generation, or 0 if there is none. `f` runs under the lock that retires + //! generations, so this CEvoDB cannot retire one before `f` returns. `f` + //! must not call back into this CEvoDB. + template + bool WithLiveView(const EvoViewToken& view, F&& f) const EXCLUSIVE_LOCKS_REQUIRED(!cs) + { + if (view.db != this) return false; + LOCK(cs); + if (!IsLiveLocked(view)) return false; + f(g_view_cache_epoch.load(), m_snapshot_generation ? m_snapshot_generation->generation : uint32_t{0}); + return true; } + //! The view this thread's Evo accesses resolve to: its open transaction, + //! else its innermost EvoViewScope, else the default identity. While a + //! published snapshot generation is live there is no default: an unbound access, or + //! one bound to a retired generation, raises EvoViewBindingError before it + //! touches any state. So does every read, write, iterator and cache lookup + //! that resolves its view through here. + EvoViewToken CurrentView() const EXCLUSIVE_LOCKS_REQUIRED(!cs); + //! Whether this thread's accesses currently resolve to exactly `view`. + bool IsCurrentView(const EvoViewToken& view) const EXCLUSIVE_LOCKS_REQUIRED(!cs); + //! The current view of a chainstate with the given identity. + EvoViewToken ViewFor(EvoDbIdentity identity) const EXCLUSIVE_LOCKS_REQUIRED(!cs); + //! Whether this thread already binds its accesses to this CEvoDB, through + //! an open transaction or an EvoViewScope. + bool IsBound() const EXCLUSIVE_LOCKS_REQUIRED(!cs); + //! Whether a published snapshot generation is live, so that two views exist. + bool HasLiveGeneration() const { return m_has_live_generation.load(); } + //! Wait up to `timeout` for the active-chain turns (ActiveEvoViewScope) of + //! other threads to end. Returns how many were still running when it + //! stopped waiting. Those turns may need cs_main, so the caller must not + //! hold it. + int WaitForOtherActiveTurns(std::chrono::milliseconds timeout) const + EXCLUSIVE_LOCKS_REQUIRED(!m_turns_mutex) LOCKS_EXCLUDED(::cs_main); + //! The view of the active chainstate. + EvoViewToken ActiveView() const EXCLUSIVE_LOCKS_REQUIRED(!cs); + //! Whether `view` still names a view of this CEvoDB: NORMAL, or the snapshot + //! generation bound now, whether or not it is published yet. + bool IsLive(const EvoViewToken& view) const EXCLUSIVE_LOCKS_REQUIRED(!cs); + //! Raise EvoViewBindingError if this thread holds a transaction on this + //! CEvoDB that disagrees with `token`. + void CheckScopeAgainstTransaction(const EvoViewToken& token) const EXCLUSIVE_LOCKS_REQUIRED(!cs); + CurTransaction& GetCurTransaction() EXCLUSIVE_LOCKS_REQUIRED(cs) { AssertLockHeld(cs); // lock must be held from outside as long as the DB transaction is used @@ -148,62 +449,86 @@ class CEvoDB } /** - * Write immutable block-derived data, accepting an identical existing value. - * V must have canonical serialization: its bytes must be a pure function of - * its logical content. Audited call-site types are CDeterministicMNListDiff, - * CDeterministicMNList, AbstractEHFManager::Signals, the mined-commitment - * pair, and CCreditPool. - * TODO(assumeutxo): WriteDerived spot-checks are not the holistic base-state - * comparison required at snapshot completion. + * Write immutable block-derived data, accepting an identical existing value + * in the current view. V must have canonical serialization: its bytes must + * be a pure function of its logical content. Audited call-site types are + * CDeterministicMNListDiff, CDeterministicMNList, + * AbstractEHFManager::Signals, the mined-commitment pair, and CCreditPool. */ template bool WriteDerived(const K& key, const V& value) EXCLUSIVE_LOCKS_REQUIRED(!cs) { LOCK(cs); - const EvoDbIdentity identity = GetCurrentIdentity(); - auto& transaction = GetContext(identity).cur_transaction; + auto& transaction = GetContext(GetCurrentIdentity()).cur_transaction; V existing; - bool write{true}; if (transaction.Read(key, existing)) { - CDataStream existing_stream{SER_DISK, CLIENT_VERSION}; - CDataStream value_stream{SER_DISK, CLIENT_VERSION}; - existing_stream << existing; - value_stream << value; - const bool matches = existing_stream.size() == value_stream.size() && - std::equal(existing_stream.begin(), existing_stream.end(), value_stream.begin()); - if (!matches) { + if (!SameSerialization(existing, value)) { LogPrintf("ERROR: CEvoDB::WriteDerived: block-derived payload mismatch in EvoDB\n"); return false; } - write = false; + return true; } + transaction.Write(key, value); + return true; + } + + /** Write `value` under `key` in the current view at once, outside any + * transaction and without syncing: like any unsynced write it survives a + * process crash, but an OS crash or power loss before a later synced write + * can lose it. */ + template + bool WriteImmediate(const K& key, const V& value) EXCLUSIVE_LOCKS_REQUIRED(!cs) + { + LOCK(cs); + const auto& store{GetContext(GetCurrentIdentity()).store}; + CDBBatch batch{*db}; + DataStream ssKey{}; + ssKey << key; + batch.Write(store.Key(ssKey), value); + return db->WriteBatch(batch); + } - // Cross-identity writes of the same key are disjoint by construction - // (background validates blocks <= snapshot base; the snapshot chainstate - // validates blocks > base; seeded data is flushed at activation). This check is - // verification-only insurance for that invariant and must never suppress the - // caller's own write. - for (const auto& [other_identity, context] : transaction_contexts) { - if (other_identity == identity || !context) continue; - V pending; - if (!context->root_transaction.ReadPending(key, pending)) continue; - - CDataStream pending_stream{SER_DISK, CLIENT_VERSION}; - CDataStream value_stream{SER_DISK, CLIENT_VERSION}; - pending_stream << pending; - value_stream << value; - const bool matches = pending_stream.size() == value_stream.size() && - std::equal(pending_stream.begin(), pending_stream.end(), value_stream.begin()); - if (!matches) { - LogPrintf("ERROR: CEvoDB::WriteDerived: cross-identity block-derived payload mismatch in EvoDB\n"); + /** Write `value` under `key` as WriteDerived does, as part of a snapshot + * generation's seed. In a SNAPSHOT view, which must be an unpublished + * generation, the key is also recorded in the generation's seed manifest + * with `policy`, which decides how promotion merges it. NORMAL has no + * manifest: there a seed is an ordinary derived write. */ + template + bool WriteSeed(const K& key, const V& value, EvoSeedPolicy policy) EXCLUSIVE_LOCKS_REQUIRED(!cs) + { + LOCK(cs); + const EvoDbIdentity identity{GetCurrentIdentity()}; + if (identity == EvoDbIdentity::SNAPSHOT && m_snapshot_generation->state != EvoGenerationState::UNPUBLISHED) { + throw EvoViewBindingError("EvoDB: a published snapshot generation cannot be seeded"); + } + auto& transaction{GetContext(identity).cur_transaction}; + V existing; + if (transaction.Read(key, existing)) { + if (!SameSerialization(existing, value)) { + LogPrintf("ERROR: CEvoDB::WriteSeed: seeded payload mismatch in EvoDB\n"); return false; } + } else { + transaction.Write(key, value); + } + if (identity == EvoDbIdentity::SNAPSHOT) { + DataStream raw_key{}; + raw_key << key; + transaction.Write(SeedManifestKey(raw_key), static_cast(policy)); } - - if (write) transaction.Write(key, value); return true; } + //! Canonical equality of two encoded masternode lists, for + //! EvoSeedPolicy::MN_LIST. Without one, a seeded list NORMAL also holds + //! fails promotion. + using MNListEquivalence = std::function, Span)>; + void SetMNListEquivalence(MNListEquivalence equivalence) EXCLUSIVE_LOCKS_REQUIRED(!cs) + { + LOCK(cs); + m_mn_list_equivalence = std::move(equivalence); + } + template bool Exists(const K& key) EXCLUSIVE_LOCKS_REQUIRED(!cs) { @@ -218,6 +543,9 @@ class CEvoDB GetContext(GetCurrentIdentity()).cur_transaction.Erase(key); } + /** The raw database, keyed in the NORMAL view's legacy keyspace. Only + * NORMAL-only lifecycle operations (DMN migration and repair, quorum + * cleanup) may use it: it bypasses view isolation. */ CDBWrapper& GetRawDB() { return *db; @@ -250,29 +578,96 @@ class CEvoDB bool ReadBestBlock(EvoDbIdentity identity, uint256& hash) EXCLUSIVE_LOCKS_REQUIRED(!cs); bool VerifyBestBlock(EvoDbIdentity identity, const uint256& hash) EXCLUSIVE_LOCKS_REQUIRED(!cs); void WriteBestBlock(EvoDbIdentity identity, const uint256& hash) EXCLUSIVE_LOCKS_REQUIRED(!cs); - void WriteDualChainstateMarker() EXCLUSIVE_LOCKS_REQUIRED(!cs); bool HasDualChainstateMarker() EXCLUSIVE_LOCKS_REQUIRED(!cs); - //! Undo every snapshot lifecycle marker (SNAPSHOT best block, base and - //! background MN-list hashes, dual-chainstate marker). Needed when snapshot - //! activation is abandoned after those markers were already committed: a - //! stale dual-chainstate marker turns supported legacy bootstrapping into - //! "unavailable history", and a stale SNAPSHOT marker would let a later - //! snapshot directory pass ActivateExistingSnapshot(). - void EraseSnapshotMarkers() EXCLUSIVE_LOCKS_REQUIRED(!cs); + //! Lifecycle markers are written to the legacy keyspace immediately, never + //! into a transaction or a generation namespace. + void WriteDualChainstateMarker() EXCLUSIVE_LOCKS_REQUIRED(!cs); void WriteSnapshotBaseMNListHash(const uint256& hash) EXCLUSIVE_LOCKS_REQUIRED(!cs); bool ReadSnapshotBaseMNListHash(uint256& hash) EXCLUSIVE_LOCKS_REQUIRED(!cs); void WriteBackgroundMNListHash(const uint256& block_hash, const uint256& mn_list_hash) EXCLUSIVE_LOCKS_REQUIRED(!cs); bool ReadBackgroundMNListHash(uint256& block_hash, uint256& mn_list_hash) EXCLUSIVE_LOCKS_REQUIRED(!cs); + //! The live (published or promoting) snapshot generation, if any. + std::optional GetSnapshotGeneration() const EXCLUSIVE_LOCKS_REQUIRED(!cs); + + /** Start a new, unpublished snapshot generation for a snapshot whose base + * block is `base`, and bind the SNAPSHOT identity to it. Fails if a + * generation is already live, a revoked one included. */ + bool CreateSnapshotGeneration(const uint256& base, int base_height) EXCLUSIVE_LOCKS_REQUIRED(!cs); + + /** Publish the bound generation together with the dual-chainstate markers, + * the consensus-configuration fingerprint it is valid for and the encoded + * evo section it was seeded from (empty for a UTXO-only snapshot). */ + bool PublishSnapshotGeneration(const uint256& config_fingerprint, const std::optional& base_mn_list_hash, + const std::vector& evo_section = {}) EXCLUSIVE_LOCKS_REQUIRED(!cs); + //! The evo section the live generation was published with, if any. + std::optional> ReadRetainedEvoSection() const EXCLUSIVE_LOCKS_REQUIRED(!cs); + + /** Startup: collect unpublished and rejected generations, then bind the + * live one. Fails closed if the live generation was published under a + * different consensus configuration, or if dual-chainstate markers + * predate generation isolation. */ + util::Result LoadSnapshotGenerations(const uint256& config_fingerprint) EXCLUSIVE_LOCKS_REQUIRED(!cs); + + /** Abandon the bound generation of a snapshot whose activation failed. */ + bool AbandonSnapshotGeneration() EXCLUSIVE_LOCKS_REQUIRED(::cs_main, !cs); + + /** Record the decision to promote the bound generation at `snapshot_tip`. + * Must precede the first coins-directory rename. */ + bool PrepareSnapshotPromotion(const uint256& snapshot_tip) EXCLUSIVE_LOCKS_REQUIRED(!cs); + std::optional ReadPromotionJournal() const EXCLUSIVE_LOCKS_REQUIRED(!cs); + /** - * Atomically promote the surviving snapshot marker to the legacy NORMAL key - * and remove all dual-chainstate metadata. Both identity transaction trees - * must already be fully committed by the caller. + * Merge the promoted generation into the NORMAL keyspace, then atomically + * write the promoted best block and remove the journal and every + * dual-chainstate marker. Resumable from any crash after + * PrepareSnapshotPromotion. Both identity transaction trees must already be + * fully committed by the caller. */ bool PromoteSnapshotMarkers(const uint256& expected_snapshot_tip) EXCLUSIVE_LOCKS_REQUIRED(!cs); - /** Remove snapshot metadata after rejecting a snapshot, preserving NORMAL. */ + /** Reject the bound generation and remove the dual-chainstate markers in + * one synced batch, preserving NORMAL. The REJECTED record remains until + * CollectRejectedGenerations(), after the coins directory is invalidated. */ bool DiscardSnapshotMarkers() EXCLUSIVE_LOCKS_REQUIRED(!cs); + /** Stop using the bound generation for the rest of this run when its + * rejection could not be saved: views bound to it are retired, as a saved + * rejection retires them, and what it has not written is dropped. Its + * records stay on disk for startup recovery to reject; until then no new + * generation can be created. */ + void RevokeSnapshotGeneration() EXCLUSIVE_LOCKS_REQUIRED(::cs_main, !cs); + //! Whether a rejected generation still awaits its coins invalidation, or + //! the generation records cannot be read. + bool HasRejectedGeneration() const EXCLUSIVE_LOCKS_REQUIRED(!cs); + //! Remove every rejected generation's records and namespace (idempotent). + bool CollectRejectedGenerations() EXCLUSIVE_LOCKS_REQUIRED(!cs); + + enum class PromotionFailpoint { + NONE, + MID_MERGE, + AFTER_MERGE, + AFTER_COLLECT, + }; + //! Make PromoteSnapshotMarkers stop with failure at `failpoint`, as if the + //! node crashed there. + void SetPromotionFailpointForTesting(PromotionFailpoint failpoint) EXCLUSIVE_LOCKS_REQUIRED(!cs) + { + LOCK(cs); + m_promotion_failpoint = failpoint; + } + //! Make DiscardSnapshotMarkers fail without writing, as if the rejection + //! could not be persisted. + void SetRejectionFailpointForTesting(bool fail) EXCLUSIVE_LOCKS_REQUIRED(!cs) + { + LOCK(cs); + m_rejection_failpoint = fail; + } + //! Records merged per batch, so tests can stop a merge part-way. + void SetMergeBatchSizeForTesting(size_t size) EXCLUSIVE_LOCKS_REQUIRED(!cs) + { + LOCK(cs); + m_merge_batch_size = size; + } bool VerifyBestBlock(const uint256& hash) EXCLUSIVE_LOCKS_REQUIRED(!cs) { return VerifyBestBlock(EvoDbIdentity::NORMAL, hash); } void WriteBestBlock(const uint256& hash) EXCLUSIVE_LOCKS_REQUIRED(!cs) { WriteBestBlock(EvoDbIdentity::NORMAL, hash); } @@ -282,6 +677,220 @@ class CEvoDB friend class CEvoDBScopedCommitter; void CommitCurTransaction(EvoDbIdentity identity) EXCLUSIVE_LOCKS_REQUIRED(::cs_main, !cs); void RollbackCurTransaction(EvoDbIdentity identity) EXCLUSIVE_LOCKS_REQUIRED(::cs_main, !cs); + + // only ActiveEvoViewScope is allowed to invoke these + friend class ActiveEvoViewScope; + //! An active-chain turn bound on this thread starts or ends. + void EnterActiveTurn() const; + void LeaveActiveTurn() const EXCLUSIVE_LOCKS_REQUIRED(!m_turns_mutex); + + static std::pair> SeedManifestKey(const DataStream& key) + { + return {EVODB_SEED_MANIFEST, {UCharCast(key.data()), UCharCast(key.data() + key.size())}}; + } + //! Whether a seeded record agrees with NORMAL's copy (`normal`, if any). + bool SeedAgreesWithNormal(uint8_t policy, const CDataStream* normal, const CDataStream& seed) const + EXCLUSIVE_LOCKS_REQUIRED(cs); + + template + static bool SameSerialization(const V& a, const V& b) + { + CDataStream a_stream{SER_DISK, CLIENT_VERSION}; + CDataStream b_stream{SER_DISK, CLIENT_VERSION}; + a_stream << a; + b_stream << b; + return a_stream.size() == b_stream.size() && std::equal(a_stream.begin(), a_stream.end(), b_stream.begin()); + } + + void BindSnapshotGeneration(std::optional record) EXCLUSIVE_LOCKS_REQUIRED(cs); + //! Drop what the SNAPSHOT transaction trees hold, without writing it. + void ClearSnapshotTransactions() EXCLUSIVE_LOCKS_REQUIRED(cs); + void UpdateLiveGeneration() EXCLUSIVE_LOCKS_REQUIRED(cs); + //! Whether no transaction is open and every transaction tree is committed. + bool TransactionsClean() const EXCLUSIVE_LOCKS_REQUIRED(cs); + bool CollectGeneration(uint32_t generation, bool erase_record) EXCLUSIVE_LOCKS_REQUIRED(cs); + //! The rejected generations, or std::nullopt if their records cannot be read. + std::optional> RejectedGenerations() const EXCLUSIVE_LOCKS_REQUIRED(cs); + bool MergeGeneration(EvoPromotionJournal& journal) EXCLUSIVE_LOCKS_REQUIRED(cs); + bool DiscardBoundGeneration() EXCLUSIVE_LOCKS_REQUIRED(cs); + + static inline std::atomic g_view_cache_epoch{0}; + std::atomic m_has_live_generation{false}; + //! Turns start and end without taking m_turns_mutex; one that ends takes + //! it and notifies only while a thread waits for turns to end. + mutable std::atomic m_active_turns{0}; + mutable std::atomic m_turn_waiters{0}; + mutable Mutex m_turns_mutex; + mutable std::condition_variable m_turns_cv; + const bool m_check_binding; + + //! The live generation the SNAPSHOT identity reads and writes. + std::optional m_snapshot_generation GUARDED_BY(cs); + PromotionFailpoint m_promotion_failpoint GUARDED_BY(cs){PromotionFailpoint::NONE}; + bool m_rejection_failpoint GUARDED_BY(cs){false}; + //! A revoked generation is still live on disk. + bool m_revoked_generation GUARDED_BY(cs){false}; + MNListEquivalence m_mn_list_equivalence GUARDED_BY(cs); + size_t m_merge_batch_size GUARDED_BY(cs){10000}; +}; + +class CBlockIndex; + +/** Binds one active-chain dispatch turn (an RPC call, a P2P message handler + * turn, an interfaces call, a service work round) to the active chainstate's + * Evo view for its duration, unless the thread is already bound (a captured + * background token, an open transaction). It binds with a single view too, + * so a turn that a snapshot publication overtakes finishes in the view it + * started in. + * + * While a snapshot generation is live, the view is read under cs_main, where + * the active chainstate and its view change together; with a single view + * there is nothing to pair it with, so the turn does not wait for cs_main. + * `active_tip`, if given, is always read under cs_main with the view, so + * Tip() is the tip of the chainstate the bound view belongs to. Readers in + * the turn that need the active tip use Tip(), not a later look at the + * active chainstate. */ +class ActiveEvoViewScope +{ +public: + explicit ActiveEvoViewScope(const CEvoDB* evodb) + { + if (evodb == nullptr || evodb->IsBound()) return; + if (!evodb->HasLiveGeneration()) { + m_scope.emplace(evodb->ActiveView()); + } else { + LOCK(::cs_main); + m_scope.emplace(evodb->ActiveView()); + } + Enter(*evodb); + } + + template + ActiveEvoViewScope(const CEvoDB* evodb, TipFn&& active_tip) + { + if (evodb == nullptr) return; + { + LOCK(::cs_main); + m_tip = active_tip(); + if (evodb->IsBound()) return; + m_scope.emplace(evodb->ActiveView()); + } + Enter(*evodb); + } + + ActiveEvoViewScope(const ActiveEvoViewScope&) = delete; + ActiveEvoViewScope& operator=(const ActiveEvoViewScope&) = delete; + + ~ActiveEvoViewScope() + { + m_scope.reset(); + if (m_turn_db) m_turn_db->LeaveActiveTurn(); + } + + //! The active tip read with the bound view, if the scope was given a way + //! to read it. + const CBlockIndex* Tip() const { return m_tip; } + +private: + void Enter(const CEvoDB& evodb) + { + m_turn_db = &evodb; + evodb.EnterActiveTurn(); + } + + const CBlockIndex* m_tip{nullptr}; + std::optional m_scope; + const CEvoDB* m_turn_db{nullptr}; }; +//! Log a failed round of `what` unless a failure with the same view generation +//! and reason was logged for it since it last recovered. +void LogEvoRoundFailure(std::string_view what, std::optional generation, std::string_view reason); +//! Log that rounds of `what` succeed again, once they have not failed for a +//! while after a logged failure. +void LogEvoRoundRecovery(std::string_view what); + +//! Run one round of active-chain service work bound to the active view, and +//! return whether it completed. A local Evo state error ends the round, never +//! the thread. +template +bool RunActiveEvoRound(const CEvoDB* evodb, std::string_view what, F&& round) +{ + std::optional generation; + try { + const ActiveEvoViewScope evo_view{evodb}; + if (const auto view{EvoViewScope::InnermostAny()}) generation = view->generation; + round(); + } catch (const evo::LocalStateError& e) { + LogEvoRoundFailure(what, generation, e.what()); + return false; + } + LogEvoRoundRecovery(what); + return true; +} + +/** The Evo view binding of a task handed to another thread: captured from the + * enqueuing thread's innermost scope and reinstalled when the task runs, so + * the executing thread's own binding never replaces it. A task whose + * snapshot generation was retired in between is dropped. */ +class EvoViewCapture +{ +public: + EvoViewCapture() : + m_view{EvoViewScope::InnermostAny()} + { + } + + enum class Outcome { + RAN, //!< the task ran to completion + FAILED, //!< the task started and ended in a local Evo state error + DROPPED, //!< the task never started + }; + + //! Run `task` under the captured binding. A task whose generation was + //! retired, or whose binding cannot be installed, is dropped. + template + Outcome Run(F&& task) const + { + std::optional scope; + if (m_view) { + if (!m_view->db->IsLive(*m_view)) { + LogPrint(BCLog::VALIDATION, "Dropping a task bound to retired snapshot generation %u\n", + m_view->generation); + return Outcome::DROPPED; + } + try { + scope.emplace(*m_view); + } catch (const evo::LocalStateError& e) { + LogPrintf("ERROR: dropping a queued task: %s\n", e.what()); + return Outcome::DROPPED; + } + } + try { + task(); + } catch (const evo::LocalStateError& e) { + LogPrintf("ERROR: Evo state unavailable to a queued task: %s\n", e.what()); + return Outcome::FAILED; + } + return Outcome::RAN; + } + +private: + std::optional m_view; +}; + +//! Wrap `task` so that it runs under the calling thread's current Evo view +//! binding wherever it is eventually executed. `on_drop` releases whatever +//! the enqueuer claimed for the task when it is dropped without starting; a +//! task that started releases its claims itself. +template +auto WithCapturedEvoView(F task, D on_drop = [] {}) +{ + return [task = std::move(task), on_drop = std::move(on_drop), evo_view = EvoViewCapture{}](auto&&... args) mutable { + if (evo_view.Run([&] { task(std::forward(args)...); }) == EvoViewCapture::Outcome::DROPPED) { + on_drop(); + } + }; +} + #endif // BITCOIN_EVO_EVODB_H diff --git a/src/evo/mnhftx.cpp b/src/evo/mnhftx.cpp index f17b520c1189..43f5e5c08dda 100644 --- a/src/evo/mnhftx.cpp +++ b/src/evo/mnhftx.cpp @@ -6,12 +6,14 @@ #include #include #include +#include #include #include #include #include #include #include +#include #include #include @@ -48,6 +50,7 @@ CMNHFManager::CMNHFManager(CEvoDB& evoDb, const node::BlockManager& blockman, co m_blockman{blockman}, m_consensus_params{consensus_params} { + mnhfCache.SetOwner(evoDb); assert(globalInstance == nullptr); globalInstance = this; } @@ -62,7 +65,7 @@ CMNHFManager::Signals CMNHFManager::GetSignalsStage(const CBlockIndex* const pin { if (!DeploymentActiveAfter(pindexPrev, m_consensus_params, Consensus::DEPLOYMENT_V20)) return {}; - Signals signals_tmp = GetForBlock(pindexPrev); + Signals signals_tmp = GetRecord(pindexPrev, /*derive_requested=*/true); if (pindexPrev == nullptr) return {}; const int height = pindexPrev->nHeight + 1; @@ -254,6 +257,8 @@ std::optional CMNHFManager::ProcessBlock(const CBlock& bl // M_ERROR so the block is not marked invalid. state.Error(e.what()); return std::nullopt; + } catch (const evo::LocalStateError&) { + throw; } catch (const std::exception& e) { LogPrintf("CMNHFManager::ProcessBlock -- failed: %s\n", e.what()); state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "failed-proc-mnhf-inblock"); @@ -283,13 +288,46 @@ bool CMNHFManager::UndoBlock(const CBlock& block, const CBlockIndex* const pinde } CMNHFManager::Signals CMNHFManager::GetForBlock(const CBlockIndex* pindex) +{ + return GetRecord(pindex, /*derive_requested=*/false); +} + +std::optional> CMNHFManager::SeededBase(const EvoViewToken& view) +{ + if (view.identity != EvoDbIdentity::SNAPSHOT) return std::nullopt; + const CBlockIndex* base{WITH_LOCK(::cs_main, return m_blockman.LookupBlockIndex(view.base))}; + if (base == nullptr) return std::nullopt; + auto record{GetFromCache(base)}; + if (!record) return std::nullopt; + return std::make_pair(base, std::move(*record)); +} + +CMNHFManager::Signals CMNHFManager::GetRecord(const CBlockIndex* pindex, bool derive_requested) { if (pindex == nullptr) return {}; + const CBlockIndex* const requested{pindex}; + const EvoViewToken view{m_evoDb.CurrentView()}; + std::optional>> seeded_base; std::stack to_calculate; std::optional signalsTmp; while (!(signalsTmp = GetFromCache(pindex)).has_value()) { + // A derived record is exact once filtered, so it may only start a + // rebuild or answer a stage, never stand in for a raw record. + if ((derive_requested || pindex != requested) && view.identity == EvoDbIdentity::SNAPSHOT && + pindex->nHeight <= view.base_height) { + if (!seeded_base) seeded_base = SeededBase(view); + // Off the base's chain the base record says nothing: such a block + // is rebuilt from its own branch. + if (const auto& base{*seeded_base}; base && base->first->GetAncestor(pindex->nHeight) == pindex) { + signalsTmp.emplace(); + for (const auto& [bit, height] : base->second) { + if (height <= pindex->nHeight) signalsTmp->emplace(bit, height); + } + break; + } + } to_calculate.push(pindex); pindex = pindex->pprev; } @@ -299,9 +337,17 @@ CMNHFManager::Signals CMNHFManager::GetForBlock(const CBlockIndex* pindex) if (pindex_top->nHeight % 1000 == 0) { LogPrintf("re-index EHF signals at block %d\n", pindex_top->nHeight); } + // A body this node does not have (pruned, or below an unvalidated + // snapshot base), or has but cannot read, is a local state error, + // never the block's fault. + const auto [have_data, block_pos]{WITH_LOCK(::cs_main, return std::make_pair( + (pindex_top->nStatus & BLOCK_HAVE_DATA) != 0, pindex_top->GetBlockPos()))}; + if (!have_data) { + throw BlockDataUnavailableError("failed-getehfforblock-read"); + } CBlock block; - if (!m_blockman.ReadBlockFromDisk(block, *pindex_top)) { - throw std::runtime_error("failed-getehfforblock-read"); + if (m_blockman.ReadBlockFromDisk(block, block_pos) != pindex_top->GetBlockHash()) { + throw BlockDataReadError("failed-getehfforblock-read"); } BlockValidationState state; signalsTmp = ProcessBlock(block, pindex_top, false, state); @@ -332,69 +378,116 @@ std::optional CMNHFManager::GetFromCache(const CBlockInde const uint256& blockHash = pindex->GetBlockHash(); - { - LOCK(cs_cache); - if (mnhfCache.get(blockHash, signals)) { - return signals; - } + const EvoViewToken view{m_evoDb.CurrentView()}; + if (WITH_LOCK(cs_cache, return mnhfCache.For(view).get(blockHash, signals))) { + MaybePersist(signals, pindex); + return signals; } { LOCK(cs_cache); if (!DeploymentActiveAt(*pindex, m_consensus_params, Consensus::DEPLOYMENT_V20)) { - mnhfCache.insert(blockHash, signals); + mnhfCache.For(view).insert(blockHash, signals); return signals; } } if (m_evoDb.Read(std::make_pair(DB_SIGNALS_v2, blockHash), signals)) { LOCK(cs_cache); - mnhfCache.insert(blockHash, signals); + mnhfCache.For(view).insert(blockHash, signals); return signals; } if (!DeploymentActiveAt(*pindex, m_consensus_params, Consensus::DEPLOYMENT_MN_RR)) { // before mn_rr activation we are safe if (m_evoDb.Read(std::make_pair(DB_SIGNALS, blockHash), signals)) { LOCK(cs_cache); - mnhfCache.insert(blockHash, signals); + mnhfCache.For(view).insert(blockHash, signals); return signals; } } return std::nullopt; } -void CMNHFManager::AddToCache(const Signals& signals, const CBlockIndex* const pindex) +void CMNHFManager::MaybePersist(const Signals& signals, const CBlockIndex* const pindex) { - assert(pindex != nullptr); - const uint256& blockHash = pindex->GetBlockHash(); - if (DeploymentActiveAt(*pindex, m_consensus_params, Consensus::DEPLOYMENT_V20) && - !m_evoDb.WriteDerived(std::make_pair(DB_SIGNALS_v2, blockHash), signals)) { + // Outside a block-scoped transaction (mempool acceptance, mining, RPC) a + // write would never be committed and trip the clean-transaction assertion + // at the next root commit. The signals stay cached; GetFromCache persists + // them from the next transaction-scoped lookup. + if (!DeploymentActiveAt(*pindex, m_consensus_params, Consensus::DEPLOYMENT_V20) || !m_evoDb.HasActiveTransaction()) { + return; + } + // A snapshot view's only record at or below its base is the seeded one: + // promotion fails on any other record NORMAL holds too. + if (const EvoViewToken view{m_evoDb.CurrentView()}; + view.identity == EvoDbIdentity::SNAPSHOT && pindex->nHeight <= view.base_height) { + return; + } + if (!m_evoDb.WriteDerived(std::make_pair(DB_SIGNALS_v2, pindex->GetBlockHash()), signals)) { // A mismatch is local EvoDB corruption, not a statement about the // block. Abort here: some callers (miner, RPC) never pass through a // validation-state catch, and the block-connect catches must not // translate this into a consensus rejection. - const std::string msg = strprintf("CMNHFManager::%s -- EvoDB MNHF state mismatch for block %s", - __func__, blockHash.ToString()); + const std::string msg = strprintf("CMNHFManager::%s -- EvoDB MNHF state mismatch for block %s", __func__, + pindex->GetBlockHash().ToString()); AbortNode(msg); throw EvoDbInconsistencyError(msg); } - { - LOCK(cs_cache); - mnhfCache.insert(blockHash, signals); +} + +void CMNHFManager::AddToCache(const Signals& signals, const CBlockIndex* const pindex) +{ + assert(pindex != nullptr); + MaybePersist(signals, pindex); + const EvoViewToken view{m_evoDb.CurrentView()}; + LOCK(cs_cache); + mnhfCache.For(view).insert(pindex->GetBlockHash(), signals); +} + +bool CMNHFManager::SeedSignals(const EvoViewToken& view, const CBlockIndex* pindex, const Signals& signals) +{ + if (!Assume(pindex != nullptr) || !Assume(m_evoDb.HasActiveTransaction())) return false; + // Seeding may precede the chain checks, which refuse such a record. + if (std::ranges::any_of(signals, [&](const auto& signal) { return signal.second < 0 || signal.second > pindex->nHeight; })) { + return false; } + const EvoViewScope scope{view}; + if (!m_evoDb.WriteSeed(std::make_pair(DB_SIGNALS_v2, pindex->GetBlockHash()), signals, + EvoSeedPolicy::BYTE_DETERMINISTIC)) { + return false; + } + LOCK(cs_cache); + mnhfCache.For(view).erase(pindex->GetBlockHash()); + return true; } -bool CMNHFManager::ForceSignalDBUpdate(const CBlockIndex* tip) +bool CMNHFManager::ForceSignalDBUpdate(const EvoViewToken& view, const CBlockIndex* tip) { + const EvoViewScope evo_view{view}; // force ehf signals db update - auto dbTx = m_evoDb.BeginTransaction(); + auto dbTx = m_evoDb.BeginTransaction(view.identity); - const bool last_legacy = bls::bls_legacy_scheme.load(); - bls::bls_legacy_scheme.store(false); - GetSignalsStage(tip); - bls::bls_legacy_scheme.store(last_legacy); + try { + const bls::ScopedLegacySchemeOverride basic_scheme{/*legacy=*/false}; + GetSignalsStage(tip); + } catch (const evo::LocalStateError& e) { + // The signals are rebuilt on demand anyway. History a snapshot view + // lacks must not keep startup from, say, promoting a validated + // snapshot, which is what makes it readable. NORMAL lacks no history + // unless it was pruned, and no view can do without a body it has but + // cannot read. + if (view.identity == EvoDbIdentity::SNAPSHOT && !dynamic_cast(&e)) { + LogPrintf("CMNHFManager::%s -- skipping the EHF signal update: %s\n", __func__, e.what()); + return true; + } + const bool missing_data{dynamic_cast(&e) || + dynamic_cast(&e)}; + LogPrintf("CMNHFManager::%s -- cannot rebuild the EHF signals at block %s: %s%s\n", __func__, + tip->GetBlockHash().ToString(), e.what(), missing_data ? ". Restart with -reindex." : ""); + return false; + } dbTx->Commit(); // flush it to disk - if (!m_evoDb.CommitRootTransaction()) { + if (!m_evoDb.CommitRootTransaction(view.identity)) { LogPrintf("CMNHFManager::%s -- failed to commit to evoDB\n", __func__); return false; } diff --git a/src/evo/mnhftx.h b/src/evo/mnhftx.h index 36d4f47bee92..1818b34dffae 100644 --- a/src/evo/mnhftx.h +++ b/src/evo/mnhftx.h @@ -11,6 +11,7 @@ #include #include +#include #include #include @@ -105,7 +106,8 @@ class CMNHFManager : public AbstractEHFManager static constexpr size_t MNHFCacheSize = 1000; Mutex cs_cache; // versionBit <-> height - Uint256LruHashMap mnhfCache GUARDED_BY(cs_cache){MNHFCacheSize}; + EvoViewCache> mnhfCache GUARDED_BY(cs_cache){ + [] { return Uint256LruHashMap{MNHFCacheSize}; }}; public: CMNHFManager() = delete; @@ -131,19 +133,48 @@ class CMNHFManager : public AbstractEHFManager // Implements interface Signals GetSignalsStage(const CBlockIndex* const pindexPrev) override EXCLUSIVE_LOCKS_REQUIRED(!cs_cache); - bool ForceSignalDBUpdate(const CBlockIndex* tip) EXCLUSIVE_LOCKS_REQUIRED(::cs_main, !cs_cache); - -private: - void AddToCache(const Signals& signals, const CBlockIndex* const pindex) EXCLUSIVE_LOCKS_REQUIRED(!cs_cache); + //! Recompute and persist the signals at `tip` under `view`, the view of + //! the chainstate whose tip it is. Signals a snapshot view lacks the + //! history for are left to be rebuilt on demand; failing to rebuild them + //! for NORMAL, or to read a block body, fails. + bool ForceSignalDBUpdate(const EvoViewToken& view, const CBlockIndex* tip) + EXCLUSIVE_LOCKS_REQUIRED(::cs_main, !cs_cache); + /** Seed the signal record of a snapshot base into `view`, inside the + * caller's open EvoDB transaction for that view. */ + bool SeedSignals(const EvoViewToken& view, const CBlockIndex* pindex, const Signals& signals) + EXCLUSIVE_LOCKS_REQUIRED(!cs_cache); /** - * This function returns list of signals available on previous block. - * if the signals for previous block is not available in cache it would read blocks from disk - * until state won't be recovered. + * The raw signal record of a block, before GetSignalsStage() filters it. + * If neither the cache nor EvoDB has it, it is rebuilt from block data, + * persisting the rebuilt records only inside a block-scoped transaction + * and, in a snapshot view, only above the base. A body the rebuild needs + * and this node lacks raises BlockDataUnavailableError. * NOTE: that some signals could expired between blocks. */ Signals GetForBlock(const CBlockIndex* const pindex) EXCLUSIVE_LOCKS_REQUIRED(!cs_cache); +private: + /** + * GetForBlock(), except that with `derive_requested` the record of + * `pindex` itself may be derived. A snapshot view holds the record of its + * base and none below it; for a block on the base's chain at or below the + * base, the base record's signals mined up to that block are a record with + * the block's own stage: a signal is only ever dropped by the expiry rule, + * which depends on the signal alone and which GetSignalsStage() re-applies. + * A derived record is never cached or persisted, and never derived for a + * block off the base's chain, which only that branch's own records or + * blocks can answer, or where the view carries no base record. + */ + Signals GetRecord(const CBlockIndex* pindex, bool derive_requested) EXCLUSIVE_LOCKS_REQUIRED(!cs_cache); + //! The base of a snapshot view and the signal record it carries, if any. + std::optional> SeededBase(const EvoViewToken& view) + EXCLUSIVE_LOCKS_REQUIRED(!cs_cache); + + void AddToCache(const Signals& signals, const CBlockIndex* const pindex) EXCLUSIVE_LOCKS_REQUIRED(!cs_cache); + //! Persist the signals at `pindex` when a block-scoped transaction can commit them. + void MaybePersist(const Signals& signals, const CBlockIndex* const pindex); + /** * This function access to in-memory cache or to evo db but does not calculate anything * NOTE: that some signals could expired between blocks. diff --git a/src/evo/smldiff.cpp b/src/evo/smldiff.cpp index 6413b3bf58c1..57fde2de1555 100644 --- a/src/evo/smldiff.cpp +++ b/src/evo/smldiff.cpp @@ -20,8 +20,10 @@ #include -// Forward declaration +// Forward declarations std::optional> GetNonNullCoinbaseChainlock(const node::BlockManager& blockman, const CBlockIndex* pindex); +std::optional> GetNonNullCoinbaseChainlock(const CBlock& block, const CBlockIndex& index, + const Consensus::Params& consensus); CSimplifiedMNListDiff::CSimplifiedMNListDiff() = default; @@ -65,7 +67,8 @@ bool CSimplifiedMNListDiff::BuildQuorumsDiff(const CBlockIndex* baseBlockIndex, return true; } -bool CSimplifiedMNListDiff::BuildQuorumChainlockInfo(const llmq::CQuorumManager& qman, const node::BlockManager& blockman, const CBlockIndex* blockIndex) +bool CSimplifiedMNListDiff::BuildQuorumChainlockInfo(const llmq::CQuorumManager& qman, const node::BlockManager& blockman, + const CBlockIndex* blockIndex, std::string& error) { // Group quorums (indexes corresponding to entries of newQuorums) per CBlockIndex containing the expected CL // signature in CbTx. We want to avoid to load CbTx now, as more than one quorum will target the same block: hence @@ -94,7 +97,22 @@ bool CSimplifiedMNListDiff::BuildQuorumChainlockInfo(const llmq::CQuorumManager& for (auto it = workBaseBlockIndexMap.begin(); it != workBaseBlockIndexMap.end();) { // Process each key (CBlockIndex containing the expected CL signature in CbTx) of the std::multimap once const CBlockIndex* pWorkBaseBlockIndex = it->first; - const auto cbcl = GetNonNullCoinbaseChainlock(blockman, pWorkBaseBlockIndex); + std::optional> cbcl; + if (qman.CurrentView().identity == EvoDbIdentity::SNAPSHOT) { + // Without the work block's body the snapshot view cannot tell a + // certificate from none; never serve a guessed null signature. + const auto& consensus{Params().GetConsensus()}; + CBlock block; + if (WITH_LOCK(::cs_main, return (pWorkBaseBlockIndex->nStatus & BLOCK_HAVE_DATA) == 0) || + !blockman.ReadBlockFromDisk(block, *pWorkBaseBlockIndex)) { + error = strprintf("coinbase ChainLock of block %s %s", pWorkBaseBlockIndex->GetBlockHash().ToString(), + BLOCK_DATA_UNAVAILABLE_SUFFIX); + return false; + } + cbcl = GetNonNullCoinbaseChainlock(block, *pWorkBaseBlockIndex, consensus); + } else { + cbcl = GetNonNullCoinbaseChainlock(blockman, pWorkBaseBlockIndex); + } CBLSSignature sig; if (cbcl.has_value()) { sig = cbcl.value().first; @@ -218,8 +236,16 @@ bool BuildSimplifiedMNListDiff(CDeterministicMNManager& dmnman, const Chainstate } if (DeploymentActiveAfter(blockIndex, chainman.GetConsensus(), Consensus::DEPLOYMENT_V20)) { - if (!mnListDiffRet.BuildQuorumChainlockInfo(qman, chainman.m_blockman, blockIndex)) { - errorRet = strprintf("failed to build quorum chainlock info"); + std::string chainlock_error; + try { + if (!mnListDiffRet.BuildQuorumChainlockInfo(qman, chainman.m_blockman, blockIndex, chainlock_error)) { + errorRet = chainlock_error.empty() ? "failed to build quorum chainlock info" : chainlock_error; + return false; + } + } catch (const BlockDataUnavailableError& e) { + // Unavailable snapshot-view state; the sentinel keeps the requesting + // peer from being penalized. + errorRet = strprintf("%s %s", e.what(), BLOCK_DATA_UNAVAILABLE_SUFFIX); return false; } } diff --git a/src/evo/smldiff.h b/src/evo/smldiff.h index 8c551018aa9e..c42b8657fced 100644 --- a/src/evo/smldiff.h +++ b/src/evo/smldiff.h @@ -86,7 +86,11 @@ class CSimplifiedMNListDiff bool BuildQuorumsDiff(const CBlockIndex* baseBlockIndex, const CBlockIndex* blockIndex, const llmq::CQuorumBlockProcessor& quorum_block_processor); - bool BuildQuorumChainlockInfo(const llmq::CQuorumManager& qman, const node::BlockManager& blockman, const CBlockIndex* blockIndex); + /** Collect the coinbase ChainLock each new quorum's work block certifies. + * A certificate this node cannot read is an error carrying + * BLOCK_DATA_UNAVAILABLE_SUFFIX, never a null signature. */ + bool BuildQuorumChainlockInfo(const llmq::CQuorumManager& qman, const node::BlockManager& blockman, + const CBlockIndex* blockIndex, std::string& error); [[nodiscard]] static RPCResult GetJsonHelp(const std::string& key, bool optional); [[nodiscard]] UniValue ToJson(bool extended = false) const; diff --git a/src/evo/snapshot.cpp b/src/evo/snapshot.cpp index 89c1f06cd529..3f3e7adac310 100644 --- a/src/evo/snapshot.cpp +++ b/src/evo/snapshot.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -16,6 +17,7 @@ #include #include #include +#include #include #include @@ -124,28 +126,6 @@ void ValidateCanonicalMNInvariants(const CDeterministicMNList& list) } // namespace -std::vector EvoSnapshotReconstructionHeights( - int base_height, const std::vector& enabled_llmqs) -{ - if (base_height < 0) throw std::invalid_argument("invalid reconstruction base height"); - std::vector heights; - for (const auto& params : enabled_llmqs) { - if (params.dkgInterval <= 0 || params.signingActiveQuorumCount <= 0) { - throw std::invalid_argument("invalid reconstruction LLMQ parameters"); - } - const int h{base_height - base_height % params.dkgInterval}; - const size_t count{params.useRotation ? EVO_SNAPSHOT_ROTATION_CYCLES - : SnapshotCommitmentCount(params, /*rotation_enabled=*/false)}; - const size_t first{params.useRotation ? 1U : 0U}; - for (size_t i{first}; i < first + count; ++i) { - const int quorum_height{h - static_cast(i) * params.dkgInterval}; - heights.push_back({params.type, params.useRotation, quorum_height, - quorum_height - llmq::WORK_DIFF_DEPTH}); - } - } - return heights; -} - uint256 CanonicalMNListHash(const CDeterministicMNList& list) { CHashWriter writer{SER_DISK, CLIENT_VERSION}; @@ -226,6 +206,30 @@ void EvoSnapshot::Validate(bool require_canonical_order) const } ValidateCanonicalMNInvariants(mn_list); mn_list.ForEachMN(/*onlyValid=*/false, [](const auto& dmn) { ValidateMNEncoding(dmn); }); + if (base_coinbase) { + const CTransaction& coinbase{*base_coinbase}; + // A 64-byte transaction could pass for an inner merkle node. + if (!coinbase.IsCoinBase() || coinbase.nType != TRANSACTION_COINBASE || + coinbase.vExtraPayload.size() > MAX_TX_EXTRA_PAYLOAD || ::GetSerializeSize(coinbase, PROTOCOL_VERSION) <= 64 || + ::GetSerializeSize(coinbase, PROTOCOL_VERSION) > EVO_SNAPSHOT_MAX_COINBASE_BYTES) { + throw std::ios_base::failure("invalid evo snapshot base coinbase"); + } + // The CbTx header precedes its scheme-dependent fields, so the height + // is readable without knowing the base's BLS scheme. + CDataStream payload{coinbase.vExtraPayload, SER_NETWORK, PROTOCOL_VERSION}; + uint16_t cbtx_version; + int32_t cbtx_height; + payload >> cbtx_version >> cbtx_height; + if (cbtx_height != mn_list.GetHeightForSnapshotCodec()) { + throw std::ios_base::failure("evo snapshot base coinbase height mismatch"); + } + if (base_coinbase_branch.size() > EVO_SNAPSHOT_MAX_COINBASE_BRANCH) { + throw std::ios_base::failure("oversized evo snapshot base coinbase branch"); + } + } else if (!base_coinbase_branch.empty() || mn_list.GetCounts().total() != 0 || !quorums.empty()) { + // Without a CbTx at the base there can be no MN or quorum state. + throw std::ios_base::failure("evo snapshot state without a base coinbase"); + } if (quorums.size() > Consensus::available_llmqs.size() || historical_mn_list_diffs.size() > EvoSnapshotMaxHistoricalMNLists() || quorum_modifiers.size() > EVO_SNAPSHOT_MAX_MODIFIERS || @@ -247,6 +251,23 @@ void EvoSnapshot::Validate(bool require_canonical_order) const if (credit_pool.indexes.RangeCount() > EVO_SNAPSHOT_MAX_RANGES) { throw std::ios_base::failure("oversized CRangesSet range count"); } + // The window entries sum to the pool's lately unlocked amount and end at + // the base, whose recorded balance is the pool's. + if (credit_history.size() > EVO_SNAPSHOT_MAX_CREDIT_HISTORY) { + throw std::ios_base::failure("oversized evo snapshot credit history"); + } + CAmount window_unlocked{0}; + for (const auto& entry : credit_history) { + const auto sum{CheckedAdd(window_unlocked, entry.unlocked)}; + if (!MoneyRange(entry.credit_pool) || !MoneyRange(entry.unlocked) || !sum || !MoneyRange(*sum)) { + throw std::ios_base::failure("invalid evo snapshot credit history amount"); + } + window_unlocked = *sum; + } + if (window_unlocked != credit_pool.latelyUnlocked || + (!credit_history.empty() && credit_history.back().credit_pool != credit_pool.locked)) { + throw std::ios_base::failure("evo snapshot credit history disagrees with the credit pool"); + } // Consensus admits MNHF signals only for bits below VERSIONBITS_NUM_BITS // and records the mined height, which cannot exceed the base height. for (const auto& [bit, height] : mnhf_signals) { @@ -278,13 +299,11 @@ void EvoSnapshot::Validate(bool require_canonical_order) const } const size_t active_count{static_cast(params.signingActiveQuorumCount)}; const size_t total_count{SnapshotCommitmentCount(params, data.rotation_enabled)}; - // Parameter-derived counts are maxima, not exact requirements: a young - // chain carries however much quorum history exists. The chain-aware - // validation and the completion-time CbTx quorum merkle root establish - // that nothing available was withheld. - if (data.active_commitments.size() > active_count || - data.safety_commitments.size() > total_count - active_count || - data.rotation_snapshots.size() > (data.rotation_enabled ? EVO_SNAPSHOT_ROTATION_CYCLES : size_t{0})) { + // Parameter-derived counts are maxima here. The chain-aware validation + // requires the exact sets the dependency planner derives. + if (data.active_commitments.size() > active_count || data.safety_commitments.size() > total_count - active_count || + data.rotation_snapshots.size() > (data.rotation_enabled ? EvoSnapshotMaxRotationSnapshots(params) : size_t{0}) || + data.bootstrap_work.size() > EVO_SNAPSHOT_MAX_BOOTSTRAP_WORK) { throw std::ios_base::failure("invalid params-derived evo per-type quorum counts"); } std::set quorum_hashes; @@ -303,7 +322,7 @@ void EvoSnapshot::Validate(bool require_canonical_order) const for (const auto& entry : data.rotation_snapshots) { if (entry.cycle_base_block_hash.IsNull() || entry.work_block_hash.IsNull() || !cycle_hashes.insert(entry.cycle_base_block_hash).second || - !historical_hashes.contains(entry.work_block_hash) || + (!historical_hashes.contains(entry.work_block_hash) && entry.work_block_hash != base_block_hash) || entry.snapshot.mnSkipListMode < SnapshotSkipMode::MODE_NO_SKIPPING || entry.snapshot.mnSkipListMode > SnapshotSkipMode::MODE_ALL_SKIPPED || entry.snapshot.activeQuorumMembers.size() > EVO_SNAPSHOT_MAX_MNS || @@ -318,7 +337,22 @@ void EvoSnapshot::Validate(bool require_canonical_order) const required_work_hashes.insert(entry.work_block_hash); required_modifiers.emplace(data.llmq_type, entry.work_block_hash); } + if (require_canonical_order && !IsCanonicallySorted(data.bootstrap_work)) { + throw std::ios_base::failure("noncanonical evo bootstrap work order"); + } + std::set bootstrap_heights; + for (const auto& entry : data.bootstrap_work) { + if (entry.cycle_base_height < 0 || entry.work_block_hash.IsNull() || + !bootstrap_heights.insert(entry.cycle_base_height).second) { + throw std::ios_base::failure("invalid evo bootstrap work entry"); + } + required_work_hashes.insert(entry.work_block_hash); + required_modifiers.emplace(data.llmq_type, entry.work_block_hash); + } } + // The base list is the work list for a work block equal to the base, so + // it has no historical diff of its own. + required_work_hashes.erase(base_block_hash); if (historical_hashes != required_work_hashes) { throw std::ios_base::failure("missing or extra historical MN-list diff target"); } @@ -345,6 +379,37 @@ uint256 GetEvoSnapshotHash(const EvoSnapshot& snapshot) return hash; } +uint256 ComputeMerkleRootFromBranch(const uint256& leaf, const std::vector& branch, uint32_t index) +{ + uint256 hash{leaf}; + for (const uint256& node : branch) { + hash = (index & 1) ? Hash(node, hash) : Hash(hash, node); + index >>= 1; + } + return hash; +} + +std::vector BlockMerkleBranch(const CBlock& block, uint32_t index) +{ + std::vector level; + level.reserve(block.vtx.size()); + for (const auto& tx : block.vtx) + level.push_back(tx->GetHash()); + std::vector branch; + while (level.size() > 1) { + // ComputeMerkleRoot pairs an odd last node with itself. + const size_t sibling{(index ^ 1) < level.size() ? (index ^ 1) : index}; + branch.push_back(level[sibling]); + std::vector next; + for (size_t i{0}; i < level.size(); i += 2) { + next.push_back(Hash(level[i], level[std::min(i + 1, level.size() - 1)])); + } + level = std::move(next); + index >>= 1; + } + return branch; +} + bool VerifyEvoSnapshotCbTx(const EvoSnapshot& snapshot, const CCbTx& cbtx, std::string& error) { error.clear(); diff --git a/src/evo/snapshot.h b/src/evo/snapshot.h index 00d97aff56d0..78c6aa44f210 100644 --- a/src/evo/snapshot.h +++ b/src/evo/snapshot.h @@ -9,13 +9,16 @@ #include #include #include +#include #include #include #include +#include #include #include #include +#include #include #include @@ -25,21 +28,37 @@ #include #include #include +#include #include #include #include #include +class CBlockIndex; +class CChainParams; +class ChainstateManager; class CCbTx; +class CBlock; +class CCreditPoolManager; +class CMNHFManager; +class CEvoDB; +class CSpecialTxProcessor; +struct EvoViewToken; + +namespace llmq { +class CQuorumBlockProcessor; +class CQuorumSnapshotManager; +struct ScanCoverageRecord; +} // namespace llmq +namespace node { +class BlockManager; +} // namespace node namespace evo { -static constexpr uint16_t EVO_SNAPSHOT_VERSION{3}; +static constexpr uint16_t EVO_SNAPSHOT_VERSION{4}; /** Serialized little-endian bytes are "DASHEVO\0". */ static constexpr uint64_t EVO_SNAPSHOT_MARKER{0x004f564548534144ULL}; -// ComputeQuorumMembersByQuarterRotation consumes H-C, H-2C and H-3C. To -// reconstruct both H and the safety cycle H-C, the union is H-C..H-4C. -static constexpr size_t EVO_SNAPSHOT_ROTATION_CYCLES{4}; // A hard allocation bound, not a network population target. 100,000 full MN // records is already far beyond today's list while limiting hostile snapshots // to a tractable decode. Changes above this require a format-version review. @@ -55,7 +74,6 @@ static constexpr size_t EVO_SNAPSHOT_MAX_PAYOUT_SHARES{8}; // each MN a cumulative budget so nested counts cannot multiply decode work. // This comfortably covers protocol-valid scripts and network information. static constexpr size_t EVO_SNAPSHOT_MAX_MN_COMPACT_ITEMS{10'000}; -static constexpr size_t EVO_SNAPSHOT_MAX_MODIFIERS{4'096}; // A cycle's skip list accumulates across every quorum index and the build can // wrap the combined MN list more than once, so a single quorum's size does not // bound its legitimate length. This is a decode ceiling on claimed sizes only, @@ -76,15 +94,73 @@ static constexpr size_t EVO_SNAPSHOT_MAX_QUORUM_SIZE{10'000}; static constexpr size_t EVO_SNAPSHOT_MAX_HASH_PREFIX_RUN{8}; // Every historical entry costs a full traversal, sort, and canonical hash of // the reconstructed list, so a few hundred bytes of zero-operation diffs could -// otherwise drag a maximum-size list across the whole table-wide history -// horizon (~19M record visits). The horizon that sums every table entry is -// unreachable on a real chain: no network enables more than a fraction of the -// LLMQ table at once, so even a ceiling-sized list on a fully loaded mainnet -// configuration stays well below half of this cumulative record budget. +// otherwise drag a maximum-size list across every planned work block: the +// table-wide bound is 436 lists (330 for the mainnet table), ~44M record visits +// at the 100,000-MN ceiling, so this budget refuses such inputs. An honest base +// plans only the work blocks its commitments and h/N name: tens of lists at +// today's populations of a few thousand MNs, and even the worst case of every +// LLMQ_60_75 index failing its DKG in a different cycle (262 lists) stays below +// a million visits at 3,000 MNs. static constexpr size_t EVO_SNAPSHOT_MAX_RECONSTRUCTION_RECORDS{8'000'000}; +// The base coinbase is length-delimited and bounded before allocation; its +// merkle branch proves it against the base header, so it is at most as deep as +// a tree over 2^32 transactions. +static constexpr size_t EVO_SNAPSHOT_MAX_COINBASE_BYTES{1'000'000}; +static constexpr size_t EVO_SNAPSHOT_MAX_COINBASE_BRANCH{32}; +// The credit-pool window is CreditPoolPeriodBlocks() long: 576 on main, test +// and devnet, 100 on regtest. A test pins every network to this bound. +static constexpr size_t EVO_SNAPSHOT_MAX_CREDIT_HISTORY{576}; +// At most two bootstrap work entries per type: the cycle containing the base +// and the next one. +static constexpr size_t EVO_SNAPSHOT_MAX_BOOTSTRAP_WORK{2}; static_assert(std::ranges::all_of(Consensus::available_llmqs, [](const auto& params) { return !params.useRotation || params.keepOldConnections <= 2 * params.signingActiveQuorumCount; }), "rotated LLMQ retention exceeds the two serialized cycles"); +// Bootstrap work entries are ordered by cycle height, and a cycle's work block +// precedes it. Both need every cycle to be longer than the work depth. +static_assert(std::ranges::all_of(Consensus::available_llmqs, + [](const auto& params) { return params.dkgInterval > llmq::WORK_DIFF_DEPTH; }), + "an LLMQ cycle must be longer than the quorum work depth"); + +/** Commitments carried for one type: max(a + 1, keepOld) without rotation, 2a with it. */ +constexpr size_t SnapshotCommitmentCountFor(const Consensus::LLMQParams& params, bool rotation_enabled) +{ + return rotation_enabled + ? 2 * static_cast(params.signingActiveQuorumCount) + : static_cast(std::max(params.signingActiveQuorumCount + 1, params.keepOldConnections)); +} + +/** Rotation quarters a rotating type can need: three per commitment cycle, plus h..h-3C. */ +constexpr size_t EvoSnapshotMaxRotationSnapshots(const Consensus::LLMQParams& params) +{ + return 3 * SnapshotCommitmentCountFor(params, /*rotation_enabled=*/true) + 4; +} + +/** Work blocks one type can need, over every mode the type can run in. */ +constexpr size_t EvoSnapshotMaxWorkBlocks(const Consensus::LLMQParams& params) +{ + const size_t plain{SnapshotCommitmentCountFor(params, /*rotation_enabled=*/false) + 2}; + if (!params.useRotation) return plain; + return std::max(plain, 4 * SnapshotCommitmentCountFor(params, /*rotation_enabled=*/true) + 6); +} + +/** + * Maximum number of distinct historical work-block lists a snapshot can need. + * The serialized set is deduplicated, so summing every type's bound over the + * static table is conservative. + */ +constexpr size_t EvoSnapshotMaxHistoricalMNLists() +{ + size_t count{0}; + for (const auto& params : Consensus::available_llmqs) + count += EvoSnapshotMaxWorkBlocks(params); + return count; +} +static constexpr size_t EVO_SNAPSHOT_MAX_MODIFIERS{4'096}; +// Modifiers are one per (type, work block), so they number at most the sum of +// the per-type work-block bounds. +static_assert(EvoSnapshotMaxHistoricalMNLists() <= EVO_SNAPSHOT_MAX_MODIFIERS, + "one modifier per (type, work block) must fit the modifier bound"); template size_t ReadBoundedCompactSize(Stream& s, size_t limit, const char* field) @@ -111,32 +187,12 @@ inline const Consensus::LLMQParams& SnapshotLLMQParams(Consensus::LLMQType type) inline size_t SnapshotCommitmentCount(const Consensus::LLMQParams& params, bool rotation_enabled) { - if (!rotation_enabled) { - return static_cast(std::max(params.signingActiveQuorumCount + 1, params.keepOldConnections)); - } - const size_t active{static_cast(params.signingActiveQuorumCount)}; - const size_t retained{static_cast(params.keepOldConnections)}; // Rotation seeding promises the active and previous complete cycles. A // future parameter set retaining more must extend the serialized cycles. - if (retained > 2 * active) throw std::ios_base::failure("rotated LLMQ retention exceeds two cycles"); - return 2 * active; -} - -/** - * Maximum number of distinct historical work-block lists a snapshot can need. - * The serialized set is deduplicated, so summing every enabled-type horizon is - * conservative: two retained commitment cycles plus H-C..H-4C for rotated - * types, or the retained commitment horizon for non-rotated types. - */ -inline size_t EvoSnapshotMaxHistoricalMNLists() -{ - size_t count{0}; - for (const auto& params : Consensus::available_llmqs) { - count += params.useRotation - ? SnapshotCommitmentCount(params, /*rotation_enabled=*/true) + EVO_SNAPSHOT_ROTATION_CYCLES - : SnapshotCommitmentCount(params, /*rotation_enabled=*/false); + if (rotation_enabled && params.keepOldConnections > 2 * params.signingActiveQuorumCount) { + throw std::ios_base::failure("rotated LLMQ retention exceeds two cycles"); } - return count; + return SnapshotCommitmentCountFor(params, rotation_enabled); } /** @@ -476,12 +532,27 @@ struct QuorumModifier { SERIALIZE_METHODS(QuorumModifier, obj) { READWRITE(obj.llmq_type, obj.work_block_hash, obj.modifier); } }; +/** + * A work block the snapshot carries for the cycle containing the base (h) or + * the next one (N) that no carried commitment or rotation quarter references. + * The cycle need not be mined: only its height and the work block are named. + */ +struct BootstrapWork { + int32_t cycle_base_height{-1}; + uint256 work_block_hash; + + bool operator==(const BootstrapWork&) const = default; + SERIALIZE_METHODS(BootstrapWork, obj) { READWRITE(obj.cycle_base_height, obj.work_block_hash); } +}; + struct QuorumSnapshotData { Consensus::LLMQType llmq_type{Consensus::LLMQType::LLMQ_NONE}; bool rotation_enabled{false}; std::vector active_commitments; std::vector safety_commitments; std::vector rotation_snapshots; + //! Strictly ascending by cycle height. + std::vector bootstrap_work; template void Serialize(Stream& s) const; template void Unserialize(Stream& s); @@ -494,10 +565,16 @@ class EvoSnapshot uint16_t version{EVO_SNAPSHOT_VERSION}; uint256 base_block_hash; CDeterministicMNList mn_list; + //! The base block's coinbase, null exactly when DIP3 is inactive at the base. + CTransactionRef base_coinbase; + //! Merkle branch of the coinbase (index 0) to the base header, leaf to root. + std::vector base_coinbase_branch; std::vector quorums; std::vector historical_mn_list_diffs; std::vector quorum_modifiers; CCreditPool credit_pool; + //! The window entries of the blocks ending at the base, oldest first. + std::vector credit_history; AbstractEHFManager::Signals mnhf_signals; template void Serialize(Stream& s) const; @@ -534,6 +611,11 @@ inline bool IsCanonicallyBefore(const QuorumModifier& a, const QuorumModifier& b return std::tie(a.llmq_type, a.work_block_hash) < std::tie(b.llmq_type, b.work_block_hash); } +inline bool IsCanonicallyBefore(const BootstrapWork& a, const BootstrapWork& b) +{ + return a.cycle_base_height < b.cycle_base_height; +} + inline bool IsCanonicallyBefore(const QuorumSnapshotData& a, const QuorumSnapshotData& b) { return a.llmq_type < b.llmq_type; @@ -599,6 +681,7 @@ void QuorumSnapshotData::Serialize(Stream& s) const const auto snapshots{CanonicallySortedCopy(rotation_snapshots)}; s << llmq_type << rotation_enabled << active << safety; WriteSnapshotVector(s, snapshots, [&](const auto& entry) { WriteRotationSnapshot(s, entry); }); + s << CanonicallySortedCopy(bootstrap_work); } template @@ -614,6 +697,7 @@ void QuorumSnapshotData::Unserialize(Stream& s) active_commitments.clear(); safety_commitments.clear(); rotation_snapshots.clear(); + bootstrap_work.clear(); const auto& params{SnapshotLLMQParams(llmq_type)}; const size_t total_count{SnapshotCommitmentCount(params, rotation_enabled)}; const size_t expected_active{static_cast(params.signingActiveQuorumCount)}; @@ -627,9 +711,20 @@ void QuorumSnapshotData::Unserialize(Stream& s) for (size_t i{0}; i < safety_count; ++i) { safety_commitments.emplace_back(ReadMinedQuorumCommitment(s)); } - const size_t snapshot_count{ReadBoundedCompactSize(s, EVO_SNAPSHOT_ROTATION_CYCLES, "rotation snapshots")}; + const size_t snapshot_count{ + ReadBoundedCompactSize(s, rotation_enabled ? EvoSnapshotMaxRotationSnapshots(params) : size_t{0}, + "rotation snapshots")}; rotation_snapshots.reserve(snapshot_count); for (size_t i{0}; i < snapshot_count; ++i) rotation_snapshots.emplace_back(ReadRotationSnapshot(s, params)); + const size_t bootstrap_count{ReadBoundedCompactSize(s, EVO_SNAPSHOT_MAX_BOOTSTRAP_WORK, "bootstrap work")}; + for (size_t i{0}; i < bootstrap_count; ++i) { + BootstrapWork entry; + s >> entry; + if (!bootstrap_work.empty() && bootstrap_work.back().cycle_base_height >= entry.cycle_base_height) { + throw std::ios_base::failure("noncanonical evo bootstrap work order"); + } + bootstrap_work.emplace_back(entry); + } } template @@ -640,6 +735,15 @@ void EvoSnapshot::Serialize(Stream& s) const const auto sorted_modifiers{CanonicallySortedCopy(quorum_modifiers)}; s << version << base_block_hash; SerializeCanonicalMNList(s, mn_list); + if (base_coinbase) { + CDataStream coinbase{SER_NETWORK, PROTOCOL_VERSION}; + coinbase << *base_coinbase; + WriteCompactSize(s, coinbase.size()); + s.write(MakeByteSpan(coinbase)); + } else { + WriteCompactSize(s, 0); + } + s << base_coinbase_branch; s << sorted_quorums; WriteCompactSize(s, sorted_history.size()); for (const auto& entry : sorted_history) { @@ -648,6 +752,7 @@ void EvoSnapshot::Serialize(Stream& s) const } s << sorted_modifiers; s << credit_pool.locked << credit_pool.currentLimit << credit_pool.latelyUnlocked << credit_pool.indexes; + s << credit_history; WriteCompactSize(s, mnhf_signals.size()); for (const auto& signal : mnhf_signals) s << signal; } @@ -664,8 +769,28 @@ void EvoSnapshot::Unserialize(Stream& s) quorums.clear(); historical_mn_list_diffs.clear(); quorum_modifiers.clear(); + credit_history.clear(); mnhf_signals.clear(); mn_list = UnserializeCanonicalMNList(s); + base_coinbase.reset(); + if (const size_t coinbase_size{ReadBoundedCompactSize(s, EVO_SNAPSHOT_MAX_COINBASE_BYTES, "base coinbase")}) { + std::vector bytes(coinbase_size); + s.read(bytes); + CDataStream coinbase{bytes, SER_NETWORK, PROTOCOL_VERSION}; + CMutableTransaction tx; + coinbase >> tx; + if (!coinbase.empty()) throw std::ios_base::failure("trailing bytes after the evo snapshot base coinbase"); + base_coinbase = MakeTransactionRef(std::move(tx)); + CDataStream reencoded{SER_NETWORK, PROTOCOL_VERSION}; + reencoded << *base_coinbase; + if (reencoded.size() != bytes.size() || !std::equal(bytes.begin(), bytes.end(), reencoded.begin())) { + throw std::ios_base::failure("noncanonical evo snapshot base coinbase"); + } + } + const size_t branch_size{ReadBoundedCompactSize(s, EVO_SNAPSHOT_MAX_COINBASE_BRANCH, "coinbase branch")}; + base_coinbase_branch.resize(branch_size); + for (auto& node : base_coinbase_branch) + s >> node; const size_t quorum_count{ReadBoundedCompactSize(s, Consensus::available_llmqs.size(), "quorum-type count")}; quorums.reserve(quorum_count); for (size_t i{0}; i < quorum_count; ++i) { @@ -692,6 +817,10 @@ void EvoSnapshot::Unserialize(Stream& s) } s >> credit_pool.locked >> credit_pool.currentLimit >> credit_pool.latelyUnlocked; credit_pool.indexes.UnserializeBounded(s, EVO_SNAPSHOT_MAX_RANGES); + const size_t history_entries{ReadBoundedCompactSize(s, EVO_SNAPSHOT_MAX_CREDIT_HISTORY, "credit history")}; + credit_history.resize(history_entries); + for (auto& entry : credit_history) + s >> entry; const size_t signal_count{ReadBoundedCompactSize(s, VERSIONBITS_NUM_BITS, "MNHF signals")}; // The signal map normalizes iteration order, so wire order is observable // only here: require the strictly ascending bit order the serializer @@ -712,24 +841,175 @@ void EvoSnapshot::Unserialize(Stream& s) /** Single SHA256 of the canonical SER_DISK/CLIENT_VERSION encoding. */ uint256 GetEvoSnapshotHash(const EvoSnapshot& snapshot); -struct QuorumReconstructionHeight { - Consensus::LLMQType llmq_type; - bool rotation; - int quorum_height; - int work_height; +/** A work block the snapshot carries for one type, and the cycle (or, without + * rotation, the quorum) whose score modifier it carries. */ +struct EvoSnapshotWorkBlock { + const CBlockIndex* work_index{nullptr}; + int cycle_height{-1}; +}; + +/** The Evo state one LLMQ type needs at a snapshot base: every quorum member + * computation the snapshot chainstate makes at or below the base reads only + * these, plus the carried commitments. */ +struct EvoSnapshotDependencyPlan { + Consensus::LLMQType llmq_type{Consensus::LLMQType::LLMQ_NONE}; + bool rotation_enabled{false}; + //! S_T: cycles whose rotation quarters are carried, ascending. + std::vector rotation_cycles; + //! W_T: work blocks whose lists and modifiers are carried, ascending. + std::vector work_blocks; + //! The entries of W_T for h and N that nothing else references. + std::vector bootstrap_work; +}; + +/** The work block a quorum's members are computed from: the cycle's work block + * with rotation, otherwise the quorum's own block before v20 and its work + * block after. */ +int EvoSnapshotWorkHeight(const Consensus::Params& consensus, const Consensus::LLMQParams& params, + bool rotation_enabled, int quorum_height); + +/** Derive the exact dependency sets of `llmq_type` at `base_index` from the + * carried commitments' quorum blocks. The builder, the chain validator and + * the completion rebuild all use it. */ +bool PlanEvoSnapshotDependencies(const ChainstateManager& chainman, const CBlockIndex* base_index, + Consensus::LLMQType llmq_type, const std::vector& commitments, + EvoSnapshotDependencyPlan& plan, std::string& error) EXCLUSIVE_LOCKS_REQUIRED(::cs_main); + +/** The inputs of a section that only block bodies provide, as the background + * chainstate captured them while connecting those blocks. */ +struct EvoSnapshotCaptures { + std::map, uint256> modifiers; + std::map credit_history; + CTransactionRef base_coinbase; + std::vector base_coinbase_branch; + std::optional base_credit_pool; +}; + +/** The blocks at or below the base whose bodies a section's inputs come from: + * the work blocks of the v20 modifiers, the credit-pool window and the base, + * whose pool folds in every block back to the last pool on disk. */ +struct EvoSnapshotCapturePlan { + uint256 base; + int base_height{-1}; + int window_first{-1}; + std::map> modifier_blocks; + //! Heights of every block whose body is an input. + std::set heights; }; -/** Pure conservative reconstruction horizon for the supplied enabled types. */ -std::vector EvoSnapshotReconstructionHeights( - int base_height, const std::vector& enabled_llmqs); +std::optional PlanEvoSnapshotCaptures(const EvoSnapshot& snapshot, const ChainstateManager& chainman) + EXCLUSIVE_LOCKS_REQUIRED(::cs_main); + +/** Record, in the calling thread's open transaction, what `block` contributes + * to the planned inputs. At the base that includes its credit pool, read + * through `cpoolman` in the calling thread's Evo view; a LocalStateError + * from that read propagates. */ +bool WriteEvoSnapshotCaptures(CEvoDB& evodb, CCreditPoolManager& cpoolman, const node::BlockManager& blockman, + const EvoSnapshotCapturePlan& plan, const CBlock& block, const CBlockIndex& index, + const Consensus::Params& consensus, std::string& error); + +/** The planned inputs captured so far. */ +EvoSnapshotCaptures ReadEvoSnapshotCaptures(CEvoDB& evodb, const EvoSnapshotCapturePlan& plan, + const ChainstateManager& chainman) EXCLUSIVE_LOCKS_REQUIRED(::cs_main); + +/** Remove every capture on disk, whatever snapshot it was for. Only for when + * no snapshot generation is live, e.g. after a crash before publication. */ +bool EraseAllEvoSnapshotCaptures(CEvoDB& evodb); + +/** Remove every capture, in the calling thread's open transaction. */ +void EraseEvoSnapshotCaptures(CEvoDB& evodb, const EvoSnapshotCapturePlan& plan, const ChainstateManager& chainman) + EXCLUSIVE_LOCKS_REQUIRED(::cs_main); + +/** Why the evo section could not be built or checked. Only a disagreement + * says anything about the section; the others are about this node. */ +enum class EvoSectionFailure : uint8_t { + MISSING_INPUT, //!< data this node lacks, which a later attempt may have + LOCAL_CORRUPTION, //!< data this node has but cannot read, or that is inconsistent + INTERNAL_ERROR, //!< a bug in this node + DISAGREEMENT, //!< the section disagrees with the chain or with validated state +}; + +std::string EvoSectionFailureName(EvoSectionFailure failure); + +/** The failure that an exception raised while building or checking the section + * stands for. */ +EvoSectionFailure ClassifyEvoSectionError(const std::exception& e); + +/** Build the Evo section at `base_index` from the calling thread's Evo view. + * Body-derived inputs come from `captures` where present, otherwise from the + * blocks. On failure `failure`, if given, says why; an exception is + * classified by ClassifyEvoSectionError(). */ +bool BuildEvoSnapshot(const CChainParams& chainparams, const ChainstateManager& chainman, CDeterministicMNManager& dmnman, + const llmq::CQuorumBlockProcessor& qblockman, llmq::CQuorumSnapshotManager& qsnapman, + CCreditPoolManager& cpoolman, CMNHFManager& mnhfman, const CBlockIndex* base_index, + EvoSnapshot& snapshot, std::string& error, const EvoSnapshotCaptures* captures = nullptr, + EvoSectionFailure* failure = nullptr) EXCLUSIVE_LOCKS_REQUIRED(::cs_main); /** Apply the complete diff chain and return lists keyed by target block hash. */ bool ReconstructHistoricalMNLists(const EvoSnapshot& snapshot, std::map& lists, std::string& error, size_t max_records = EVO_SNAPSHOT_MAX_RECONSTRUCTION_RECORDS); +/** Validate all snapshot invariants requiring the block index or deployments, + * including exact equality with the planned dependency sets. */ +bool ValidateEvoSnapshotAgainstChain(const EvoSnapshot& snapshot, const ChainstateManager& chainman, + const CBlockIndex* base_index, std::string& error) + EXCLUSIVE_LOCKS_REQUIRED(::cs_main); + /** Pure CbTx checks over already-built snapshot content. */ bool VerifyEvoSnapshotCbTx(const EvoSnapshot& snapshot, const CCbTx& cbtx, std::string& error); +/** The merkle root a leaf at `index` reaches through `branch` (leaf to root). */ +uint256 ComputeMerkleRootFromBranch(const uint256& leaf, const std::vector& branch, uint32_t index); +/** The merkle branch of the transaction at `index` in `block`, leaf to root. */ +std::vector BlockMerkleBranch(const CBlock& block, uint32_t index); + +/** Load-time anchor of the section to the base header: the carried coinbase + * must hash through its branch to the header's merkle root, and its CbTx, + * decoded with the BLS scheme in force at the base, must commit to the + * carried MN list, active quorums and credit pool. */ +bool VerifyEvoSnapshotBaseBlock(const EvoSnapshot& snapshot, const CBlockIndex& base_index, + const Consensus::Params& consensus, std::string& error); + +/** The chain checks export and load share: ValidateEvoSnapshotAgainstChain() + * and, once DIP3 is active at the base, VerifyEvoSnapshotBaseBlock(). They + * read headers, the section and the calling thread's Evo view: the exporter + * runs them on NORMAL, the loader in the unpublished generation it seeded the + * section into, so deployment state at or below the base comes from the + * carried signal record and never from history the loading node lacks. */ +bool PreflightEvoSnapshot(const EvoSnapshot& snapshot, const ChainstateManager& chainman, const CBlockIndex* base_index, + std::string& error, EvoSectionFailure* failure = nullptr) EXCLUSIVE_LOCKS_REQUIRED(::cs_main); + +/** The scan-coverage gate of ValidateEvoSnapshotAgainstChain on its own: + * every ChainLock verification scan B + 1 may make and the asset-unlock scan + * of B + 1 must be proven from the section. */ +bool CheckEvoSnapshotScanCoverage(const EvoSnapshot& snapshot, const ChainstateManager& chainman, + const CBlockIndex* base_index, std::string& error) EXCLUSIVE_LOCKS_REQUIRED(::cs_main); + +/** How the scan sequences of `params`' type end in the section: END where the + * canonical commitment selection carries fewer records than it takes, so no + * older one exists; UNKNOWN otherwise. Local-only, recorded at seeding. + * + * END rests on the carried counts alone. The CbTx quorum root binds only the + * active set and headers say nothing about how many older commitments exist, + * so validation bounds the safety commitments from above only: a section + * omitting one would turn UNKNOWN into END and a short scan into a wrong + * answer. Only the mandatory evo-hash pin binds the counts, so no section may + * be seeded without it. */ +llmq::ScanCoverageRecord EvoSnapshotScanCoverage(const EvoSnapshot& snapshot, const Consensus::LLMQParams& params); + +/** Seed the decoded snapshot's state into `view`, inside the caller's open + * EvoDB transaction for that view. */ +bool SeedEvoSnapshotState(const EvoViewToken& view, const EvoSnapshot& snapshot, CSpecialTxProcessor& special_tx, + const node::BlockManager& blockman, const CBlockIndex* base_index, std::string& error) + EXCLUSIVE_LOCKS_REQUIRED(::cs_main); + +/** Compute, in the calling thread's (seeded) view, the members of every + * carried quorum, of the base's cycle, and of the next cycle when its work + * block is at or below the base. Seeded state that cannot support them is an + * error. */ +bool DryRunEvoSnapshot(const EvoSnapshot& snapshot, const ChainstateManager& chainman, CSpecialTxProcessor& special_tx, + const CBlockIndex* base_index, std::string& error) EXCLUSIVE_LOCKS_REQUIRED(::cs_main); + } // namespace evo #endif // BITCOIN_EVO_SNAPSHOT_H diff --git a/src/evo/snapshot_chain.cpp b/src/evo/snapshot_chain.cpp new file mode 100644 index 000000000000..3ea7c63d3d1e --- /dev/null +++ b/src/evo/snapshot_chain.cpp @@ -0,0 +1,1176 @@ +// Copyright (c) 2026 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +// Chain-aware companions to evo/snapshot.cpp, deliberately header-less: these +// implementations are declared in evo/snapshot.h but need validation.h and +// llmq internals, so they live in their own compilation unit to keep the +// snapshot codec free of an evo/snapshot -> validation -> evo/snapshot cycle. + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace evo { +namespace { + +//! V20 is buried, so its state after a height is known even for a cycle that +//! is not mined yet. +bool V20ActiveAfterHeight(const Consensus::Params& consensus, int height) +{ + return height + 1 >= consensus.DeploymentHeight(Consensus::DEPLOYMENT_V20); +} + +bool ValidateCommitmentAgainstChain(const MinedQuorumCommitment& entry, const ChainstateManager& chainman, + const CBlockIndex* base_index, const Consensus::LLMQParams& params, + bool rotation_enabled) EXCLUSIVE_LOCKS_REQUIRED(::cs_main) +{ + const CBlockIndex* quorum_index{chainman.m_blockman.LookupBlockIndex(entry.quorum_base_block_hash)}; + const CBlockIndex* mined_index{chainman.m_blockman.LookupBlockIndex(entry.mined_block_hash)}; + if (quorum_index == nullptr || mined_index == nullptr || + base_index->GetAncestor(quorum_index->nHeight) != quorum_index || + base_index->GetAncestor(mined_index->nHeight) != mined_index) + return false; + const int cycle_height{quorum_index->nHeight - quorum_index->nHeight % params.dkgInterval}; + if (rotation_enabled) { + if (entry.commitment.quorumIndex != quorum_index->nHeight % params.dkgInterval || + entry.commitment.quorumIndex < 0 || entry.commitment.quorumIndex >= params.signingActiveQuorumCount) + return false; + } else if (quorum_index->nHeight != cycle_height || entry.commitment.quorumIndex != 0) { + return false; + } + const int mined_cycle{mined_index->nHeight - mined_index->nHeight % params.dkgInterval}; + if (mined_cycle != cycle_height || mined_index->nHeight % params.dkgInterval < params.dkgMiningWindowStart || + mined_index->nHeight % params.dkgInterval > params.dkgMiningWindowEnd) + return false; + const uint16_t expected_version{ + llmq::CFinalCommitment::GetVersion(rotation_enabled, DeploymentActiveAfter(quorum_index, chainman.GetConsensus(), + Consensus::DEPLOYMENT_V19))}; + return entry.commitment.nVersion == expected_version && entry.commitment.VerifySizes(params); +} + +//! Applicable(Y): a rotation cycle at or below the base whose quarter the +//! ordinary computation builds and later reads. +bool IsApplicableRotationCycle(const ChainstateManager& chainman, const Consensus::LLMQParams& params, + const CBlockIndex* base_index, int cycle_height) EXCLUSIVE_LOCKS_REQUIRED(::cs_main) +{ + if (cycle_height < 0 || cycle_height > base_index->nHeight || cycle_height % params.dkgInterval != 0) return false; + const CBlockIndex* cycle{base_index->GetAncestor(cycle_height)}; + return cycle != nullptr && llmq::IsQuorumRotationEnabled(params, cycle) && + chainman.IsQuorumTypeEnabled(params.type, cycle->pprev); +} + +//! The modifier of the cycle at `work.cycle_height`, from the work block's +//! data or, when this node lacks the block, from `seeded`; a block it has but +//! cannot read raises BlockDataReadError. Mirrors the ordinary calculation +//! (llmq/utils.cpp), but reports a v20 work block it can neither read nor +//! take from `seeded` through `error`, and needs no unmined cycle's block. +std::optional ExactWorkModifier(const Consensus::LLMQParams& params, const Consensus::Params& consensus, + const node::BlockManager& blockman, const CBlockIndex* base_index, + const EvoSnapshotWorkBlock& work, const std::optional& seeded, + std::string& error, EvoSectionFailure& failure) EXCLUSIVE_LOCKS_REQUIRED(::cs_main) +{ + const CBlockIndex* modifier_work{base_index->GetAncestor(work.cycle_height - llmq::WORK_DIFF_DEPTH)}; + if (modifier_work == nullptr) { + error = "evo quorum modifier work block is not an ancestor of the base"; + failure = EvoSectionFailure::INTERNAL_ERROR; + return std::nullopt; + } + if (DeploymentActiveAfter(modifier_work, consensus, Consensus::DEPLOYMENT_V20)) { + if ((modifier_work->nStatus & BLOCK_HAVE_DATA) == 0) { + if (!seeded) { + error = "quorum work block data unavailable for its exact score modifier"; + failure = EvoSectionFailure::MISSING_INPUT; + } + return seeded; + } + CBlock block; + if (blockman.ReadBlockFromDisk(block, modifier_work->GetBlockPos()) != modifier_work->GetBlockHash()) { + throw BlockDataReadError( + strprintf("quorum work block %s cannot be read", modifier_work->GetBlockHash().ToString())); + } + return llmq::utils::GetWorkBlockModifier(params, consensus, block, *modifier_work); + } + if (params.useRotation) return ::SerializeHash(std::make_pair(params.type, modifier_work->GetBlockHash())); + if (work.cycle_height > base_index->nHeight) { + error = "pre-v20 modifier of a cycle above the base is undecidable"; + failure = EvoSectionFailure::INTERNAL_ERROR; + return std::nullopt; + } + return ::SerializeHash(std::make_pair(params.type, base_index->GetAncestor(work.cycle_height)->GetBlockHash())); +} + +//! A carried list for the snapshot: an unbound pre-DIP3 list is the empty list +//! bound to its block, which is what the snapshot view reads there. +CDeterministicMNList BoundList(const CDeterministicMNList& list, const CBlockIndex* index) +{ + if (list.GetBlockHash().IsNull()) return CDeterministicMNList{index->GetBlockHash(), index->nHeight, 0}; + return list; +} + +//! Whether every activation input of a quorum type is the same for the block +//! after `target_height` as for the block after the base: no buried deployment +//! or DIP0024 quorum height switches in between, and no versionbits deployment +//! that is not yet active crosses a period boundary. +bool ContextUnchangedUntil(const ChainstateManager& chainman, const CBlockIndex* base_index, int64_t target_height) + EXCLUSIVE_LOCKS_REQUIRED(::cs_main) +{ + const auto& consensus{chainman.GetConsensus()}; + const int64_t base_height{base_index->nHeight}; + for (int16_t dep{Consensus::DEPLOYMENT_HEIGHTINCB}; + Consensus::ValidDeployment(static_cast(dep)); ++dep) { + const int64_t height{consensus.DeploymentHeight(static_cast(dep))}; + if ((base_height + 1 >= height) != (target_height + 1 >= height)) return false; + } + if ((base_height >= consensus.DIP0024QuorumsHeight) != (target_height >= consensus.DIP0024QuorumsHeight)) { + return false; + } + for (int pos{0}; pos < Consensus::MAX_VERSION_BITS_DEPLOYMENTS; ++pos) { + const auto dep{static_cast(pos)}; + if (DeploymentActiveAfter(base_index, chainman, dep)) continue; + const auto& deployment{consensus.vDeployments[dep]}; + const int64_t period{deployment.nWindowSize ? deployment.nWindowSize : int64_t{consensus.nMinerConfirmationWindow}}; + // A state is constant within a period: blocks base+1 .. target+1 share + // one unless one of them starts a new period. + if (period > 0 && (target_height + 1) / period != (base_height + 1) / period) return false; + } + return true; +} + +//! I6 for the next cycle N: its members read work data at or below the base +//! once N - D <= B, and the planner derives h and N from the context at B. +//! A type enabled, or switching rotation, between B and N would need inputs +//! nothing carries, so such a base is refused at export and load alike. +bool CheckNextCycleContext(const ChainstateManager& chainman, const CBlockIndex* base_index, std::string& error) + EXCLUSIVE_LOCKS_REQUIRED(::cs_main) +{ + for (const auto& params : chainman.GetConsensus().llmqs) { + const int64_t base_height{base_index->nHeight}; + const int64_t next{base_height - base_height % params.dkgInterval + params.dkgInterval}; + // N - 1 == B makes N's context the base's own. + if (next - llmq::WORK_DIFF_DEPTH > base_height || next - 1 == base_height) continue; + if (!ContextUnchangedUntil(chainman, base_index, next - 1)) { + error = strprintf("evo snapshot base precedes an undecidable activation context at the next %s cycle %d", + params.name, next); + return false; + } + } + return true; +} + +//! The carried records of `params`' type in the family its scans read, newest +//! mined first: per quorum index with rotation, one sequence otherwise. +std::vector>> CarriedScanRecords(const EvoSnapshot& snapshot, + const ChainstateManager& chainman, + const Consensus::LLMQParams& params) + EXCLUSIVE_LOCKS_REQUIRED(::cs_main) +{ + std::vector>> records( + params.useRotation ? static_cast(params.signingActiveQuorumCount) : 1); + const auto data{std::ranges::find_if(snapshot.quorums, [&](const auto& q) { return q.llmq_type == params.type; })}; + // Scans of a rotating type read the indexed family only. Without rotation + // at the base there is no indexed record at or below it. + if (data == snapshot.quorums.end() || data->rotation_enabled != params.useRotation) return records; + for (const auto* commitments : {&data->active_commitments, &data->safety_commitments}) { + for (const auto& entry : *commitments) { + const size_t sequence{params.useRotation ? static_cast(entry.commitment.quorumIndex) : 0}; + const CBlockIndex* mined{chainman.m_blockman.LookupBlockIndex(entry.mined_block_hash)}; + const CBlockIndex* quorum{chainman.m_blockman.LookupBlockIndex(entry.quorum_base_block_hash)}; + if (sequence < records.size() && mined != nullptr && quorum != nullptr) { + records[sequence].emplace_back(mined->nHeight, quorum); + } + } + } + for (auto& sequence : records) { + std::sort(sequence.begin(), sequence.end(), [](const auto& a, const auto& b) { return a.first > b.first; }); + } + return records; +} + +//! Prove the scans the snapshot chainstate must answer from carried history +//! alone: ChainLock verification for every height B + 1 may certify, and the +//! asset-unlock quorum check of B + 1. With `full_index`, the exporter's +//! complete commitment index, the proven vectors must also equal it. +bool CheckScanCoverage(const EvoSnapshot& snapshot, const ChainstateManager& chainman, const CBlockIndex* base_index, + const llmq::CQuorumBlockProcessor* full_index, std::string& error) + EXCLUSIVE_LOCKS_REQUIRED(::cs_main) +{ + const auto& consensus{chainman.GetConsensus()}; + struct Request { + Consensus::LLMQType type{Consensus::LLMQType::LLMQ_NONE}; + const CBlockIndex* start{nullptr}; + size_t count{0}; + }; + std::vector requests; + // Without v20 at B + 1 its CbTx carries no ChainLock to verify. + const auto chainlocks{chainman.GetParams().GetLLMQ(consensus.llmqTypeChainLocks)}; + if (chainlocks && DeploymentActiveAfter(base_index, consensus, Consensus::DEPLOYMENT_V20)) { + // B + 1 may certify any height from the base's certified one on; with + // no certificate at the base, any height at all. + int first_height{0}; + const bool legacy{!DeploymentActiveAt(*base_index, consensus, Consensus::DEPLOYMENT_V19)}; + if (const auto cbtx{snapshot.base_coinbase ? DecodeCbTxPayload(*snapshot.base_coinbase, legacy) : std::nullopt}; + cbtx && cbtx->nVersion >= CCbTx::Version::CLSIG_AND_BALANCE && cbtx->bestCLSignature.Get().IsValid() && + cbtx->bestCLHeightDiff < static_cast(base_index->nHeight)) { + first_height = base_index->nHeight - static_cast(cbtx->bestCLHeightDiff) - 1; + } + // With a fixed type and count, coverage only grows with the cutoff, + // and the cutoff with the start. So only the first start of each + // context (enablement and whether the scan is empty by genesis) needs + // proving. + std::optional> context; + for (int height{std::max(first_height, llmq::SIGN_HEIGHT_OFFSET)}; height <= base_index->nHeight; ++height) { + const CBlockIndex* start{base_index->GetAncestor(height - llmq::SIGN_HEIGHT_OFFSET)}; + const bool enabled{chainman.IsQuorumTypeEnabled(chainlocks->type, start)}; + const std::pair current{enabled, enabled && llmq::NormalizeScanStart(*chainlocks, start)}; + if (context == current) continue; + context = current; + if (current.second) { + requests.push_back({chainlocks->type, start, static_cast(chainlocks->signingActiveQuorumCount)}); + } + } + } + if (const auto platform{chainman.GetParams().GetLLMQ(consensus.llmqTypePlatform)}) { + requests.push_back({platform->type, base_index, static_cast(platform->signingActiveQuorumCount) + 1}); + } + + std::map>>> carried; + std::set> checked; + for (const auto& request : requests) { + const auto params{*Assert(chainman.GetParams().GetLLMQ(request.type))}; + if (!chainman.IsQuorumTypeEnabled(request.type, request.start)) continue; + const auto cutoff{llmq::NormalizeScanStart(params, request.start)}; + if (!cutoff || !checked.emplace(request.type, (*cutoff)->nHeight, request.count).second) continue; + auto [it, inserted]{carried.try_emplace(request.type)}; + if (inserted) it->second = CarriedScanRecords(snapshot, chainman, params); + const auto coverage{EvoSnapshotScanCoverage(snapshot, params)}; + std::vector sequences; + for (size_t i{0}; i < it->second.size(); ++i) { + llmq::ScanSequence sequence{{}, coverage.End(i)}; + for (const auto& [mined_height, quorum] : it->second[i]) { + if (mined_height <= (*cutoff)->nHeight) sequence.records.push_back(quorum); + } + sequences.push_back(std::move(sequence)); + } + const auto proven{llmq::TraverseScanSequences(params.useRotation, sequences, request.count)}; + if (!proven) { + error = strprintf("evo snapshot cannot prove the llmqType=%d scan of %d from height %d", + static_cast(request.type), request.count, (*cutoff)->nHeight); + return false; + } + if (full_index == nullptr) continue; + std::vector complete; + if (params.useRotation) { + for (int index{0}; index < params.signingActiveQuorumCount; ++index) { + complete.push_back( + {full_index->GetMinedCommitmentsByQuorumIndexUntilBlock(request.type, *cutoff, index, request.count), + llmq::ScanSequenceEnd::END}); + } + } else { + complete.push_back({full_index->GetMinedCommitmentsUntilBlock(request.type, *cutoff, request.count), + llmq::ScanSequenceEnd::END}); + } + if (llmq::TraverseScanSequences(params.useRotation, complete, request.count) != proven) { + error = strprintf("evo snapshot llmqType=%d scan from height %d differs from the full index", + static_cast(request.type), (*cutoff)->nHeight); + return false; + } + } + return true; +} + +} // namespace + +bool CheckEvoSnapshotScanCoverage(const EvoSnapshot& snapshot, const ChainstateManager& chainman, + const CBlockIndex* base_index, std::string& error) +{ + AssertLockHeld(::cs_main); + return CheckScanCoverage(snapshot, chainman, base_index, /*full_index=*/nullptr, error); +} + +llmq::ScanCoverageRecord EvoSnapshotScanCoverage(const EvoSnapshot& snapshot, const Consensus::LLMQParams& params) +{ + llmq::ScanCoverageRecord coverage; + const auto data{std::ranges::find_if(snapshot.quorums, [&](const auto& q) { return q.llmq_type == params.type; })}; + const auto end_of = [](size_t carried, size_t selected) { + return static_cast(carried < selected ? llmq::ScanSequenceEnd::END : llmq::ScanSequenceEnd::UNKNOWN); + }; + if (params.useRotation) { + // The canonical selection carries the last two of every quorum index; + // fewer means none older exists. Without rotation at the base the + // indexed family is empty there. + std::vector counts(static_cast(params.signingActiveQuorumCount)); + if (data != snapshot.quorums.end() && data->rotation_enabled) { + for (const auto* commitments : {&data->active_commitments, &data->safety_commitments}) { + for (const auto& entry : *commitments) { + if (entry.commitment.quorumIndex >= 0 && + static_cast(entry.commitment.quorumIndex) < counts.size()) { + ++counts[entry.commitment.quorumIndex]; + } + } + } + } + for (const size_t count : counts) + coverage.ends.push_back(end_of(count, 2)); + } else { + const size_t carried{data == snapshot.quorums.end() + ? size_t{0} + : data->active_commitments.size() + data->safety_commitments.size()}; + coverage.ends.push_back(end_of(carried, SnapshotCommitmentCount(params, /*rotation_enabled=*/false))); + } + return coverage; +} + +int EvoSnapshotWorkHeight(const Consensus::Params& consensus, const Consensus::LLMQParams& params, + bool rotation_enabled, int quorum_height) +{ + if (rotation_enabled) return quorum_height - quorum_height % params.dkgInterval - llmq::WORK_DIFF_DEPTH; + return V20ActiveAfterHeight(consensus, quorum_height) ? quorum_height - llmq::WORK_DIFF_DEPTH : quorum_height; +} + +bool PlanEvoSnapshotDependencies(const ChainstateManager& chainman, const CBlockIndex* base_index, + Consensus::LLMQType llmq_type, const std::vector& commitments, + EvoSnapshotDependencyPlan& plan, std::string& error) +{ + AssertLockHeld(::cs_main); + plan = EvoSnapshotDependencyPlan{}; + plan.llmq_type = llmq_type; + const auto params_opt{chainman.GetParams().GetLLMQ(llmq_type)}; + if (base_index == nullptr || !params_opt) { + error = "evo snapshot dependency plan without a base or LLMQ parameters"; + return false; + } + const auto& params{*params_opt}; + const auto& consensus{chainman.GetConsensus()}; + const int base_height{base_index->nHeight}; + const int interval{params.dkgInterval}; + const int h{base_height - base_height % interval}; + const int next{h + interval}; + plan.rotation_enabled = llmq::IsQuorumRotationEnabled(params, base_index); + + // A_T for a rotating type (cycle heights), or the quorum heights K_T ∪ {h, N}. + std::set cycles; + for (const CBlockIndex* quorum : commitments) { + if (quorum == nullptr || base_index->GetAncestor(quorum->nHeight) != quorum) { + error = "evo snapshot commitment is not an ancestor of the base"; + return false; + } + cycles.insert(plan.rotation_enabled ? quorum->nHeight - quorum->nHeight % interval : quorum->nHeight); + } + // A type that forms no quorum at the base is carried only for its retained + // commitments, so it needs no members for h or N. + const bool enabled{chainman.IsQuorumTypeEnabled(llmq_type, base_index)}; + if (enabled) { + cycles.insert(h); + cycles.insert(next); + } + + std::map work_cycles; // work height -> the cycle whose modifier it carries + const auto add_work = [&](int work_height, int cycle_height) { + if (work_height < 0 || work_height > base_height) return; + work_cycles.emplace(work_height, cycle_height); + }; + std::set referenced_work; + for (const CBlockIndex* quorum : commitments) { + referenced_work.insert(EvoSnapshotWorkHeight(consensus, params, plan.rotation_enabled, quorum->nHeight)); + } + + if (plan.rotation_enabled) { + std::set quarters; + for (const int cycle : cycles) { + for (int k{1}; k <= 3; ++k) { + if (IsApplicableRotationCycle(chainman, params, base_index, cycle - k * interval)) { + quarters.insert(cycle - k * interval); + } + } + } + for (const int quarter : quarters) { + plan.rotation_cycles.push_back(base_index->GetAncestor(quarter)); + add_work(quarter - llmq::WORK_DIFF_DEPTH, quarter); + referenced_work.insert(quarter - llmq::WORK_DIFF_DEPTH); + } + for (const int cycle : cycles) + add_work(cycle - llmq::WORK_DIFF_DEPTH, cycle); + } else { + for (const int quorum : cycles) { + add_work(EvoSnapshotWorkHeight(consensus, params, /*rotation_enabled=*/false, quorum), quorum); + } + } + + for (const auto& [work_height, cycle_height] : work_cycles) { + plan.work_blocks.push_back({base_index->GetAncestor(work_height), cycle_height}); + } + if (enabled) { + for (const int cycle : {h, next}) { + const int work_height{EvoSnapshotWorkHeight(consensus, params, plan.rotation_enabled, cycle)}; + if (!work_cycles.contains(work_height) || referenced_work.contains(work_height)) continue; + plan.bootstrap_work.push_back({cycle, base_index->GetAncestor(work_height)->GetBlockHash()}); + } + } + return true; +} + +std::string EvoSectionFailureName(EvoSectionFailure failure) +{ + switch (failure) { + case EvoSectionFailure::MISSING_INPUT: + return "missing input"; + case EvoSectionFailure::LOCAL_CORRUPTION: + return "local data unreadable or inconsistent"; + case EvoSectionFailure::INTERNAL_ERROR: + return "internal error"; + case EvoSectionFailure::DISAGREEMENT: + return "disagreement"; + } // no default case, so the compiler can warn about missing cases + assert(false); +} + +EvoSectionFailure ClassifyEvoSectionError(const std::exception& e) +{ + if (dynamic_cast(&e)) return EvoSectionFailure::DISAGREEMENT; + if (dynamic_cast(&e)) return EvoSectionFailure::MISSING_INPUT; + if (dynamic_cast(&e) || dynamic_cast(&e) || + dynamic_cast(&e) || dynamic_cast(&e)) { + return EvoSectionFailure::LOCAL_CORRUPTION; + } + return EvoSectionFailure::INTERNAL_ERROR; +} + +bool BuildEvoSnapshot(const CChainParams& chainparams, const ChainstateManager& chainman, + CDeterministicMNManager& dmnman, const llmq::CQuorumBlockProcessor& qblockman, + llmq::CQuorumSnapshotManager& qsnapman, CCreditPoolManager& cpoolman, CMNHFManager& mnhfman, + const CBlockIndex* base_index, EvoSnapshot& snapshot, std::string& error, + const EvoSnapshotCaptures* captures, EvoSectionFailure* failure_out) +{ + AssertLockHeld(::cs_main); + error.clear(); + EvoSectionFailure unused; + EvoSectionFailure& failure{failure_out ? *failure_out : unused}; + failure = EvoSectionFailure::INTERNAL_ERROR; + const auto fail = [&](EvoSectionFailure why) { + failure = why; + return false; + }; + // A block body this node lacks may come back; one it has is unreadable. + const auto body_failure = [](const CBlockIndex& index) EXCLUSIVE_LOCKS_REQUIRED(::cs_main) { + return (index.nStatus & BLOCK_HAVE_DATA) ? EvoSectionFailure::LOCAL_CORRUPTION : EvoSectionFailure::MISSING_INPUT; + }; + if (base_index == nullptr) { + error = "evo snapshot base block is null"; + return fail(EvoSectionFailure::INTERNAL_ERROR); + } + const auto& consensus{chainparams.GetConsensus()}; + + EvoSnapshot result; + result.base_block_hash = base_index->GetBlockHash(); + if (!DeploymentActiveAt(*base_index, consensus, Consensus::DEPLOYMENT_DIP0003)) { + result.mn_list = CDeterministicMNList{base_index->GetBlockHash(), base_index->nHeight, 0}; + try { + result.Validate(); + } catch (const std::exception& e) { + error = e.what(); + return fail(EvoSectionFailure::INTERNAL_ERROR); + } + snapshot = std::move(result); + return true; + } + // The context at the base derives from the signal record the section + // carries; validated state deciding otherwise disagrees with it. + if (!CheckNextCycleContext(chainman, base_index, error)) return fail(EvoSectionFailure::DISAGREEMENT); + result.mn_list = dmnman.GetListForBlock(base_index); + if (captures && captures->base_coinbase) { + result.base_coinbase = captures->base_coinbase; + result.base_coinbase_branch = captures->base_coinbase_branch; + } else { + CBlock base_block; + if (!chainman.m_blockman.ReadBlockFromDisk(base_block, *base_index) || base_block.vtx.empty()) { + error = "evo snapshot base block is unreadable"; + return fail(body_failure(*base_index)); + } + result.base_coinbase = base_block.vtx[0]; + result.base_coinbase_branch = BlockMerkleBranch(base_block, 0); + } + std::map historical; + + for (const auto& params : consensus.llmqs) { + QuorumSnapshotData data; + data.llmq_type = params.type; + data.rotation_enabled = llmq::IsQuorumRotationEnabled(params, base_index); + + const size_t active_count{static_cast(params.signingActiveQuorumCount)}; + const size_t total_count{SnapshotCommitmentCount(params, data.rotation_enabled)}; + std::vector active; + std::vector safety; + if (data.rotation_enabled) { + active = qblockman.GetLastMinedCommitmentsPerQuorumIndexUntilBlock(params.type, base_index, 0); + safety = qblockman.GetLastMinedCommitmentsPerQuorumIndexUntilBlock(params.type, base_index, 1); + } else { + active = qblockman.GetMinedCommitmentsUntilBlock(params.type, base_index, total_count); + const size_t emit_active{std::min(active.size(), active_count)}; + safety.assign(active.begin() + emit_active, active.end()); + active.resize(emit_active); + } + if (active.size() > active_count) active.resize(active_count); + if (safety.size() > total_count - active_count) safety.resize(total_count - active_count); + // A type that can no longer form quorums (e.g. LLMQ_50_60 after DIP24 + // on mainnet) still contributes its retained commitments to the CbTx + // quorum merkle root, so it is carried whenever it has any. + if (active.empty() && safety.empty() && !chainman.IsQuorumTypeEnabled(params.type, base_index)) continue; + + std::vector carried{active}; + carried.insert(carried.end(), safety.begin(), safety.end()); + EvoSnapshotDependencyPlan plan; + if (!PlanEvoSnapshotDependencies(chainman, base_index, params.type, carried, plan, error)) { + return fail(EvoSectionFailure::INTERNAL_ERROR); + } + + // The commitment index lists the quorum, so its record must be there. + const auto read_commitment = [&](const CBlockIndex* quorum) -> std::optional { + auto [commitment, mined_hash] = qblockman.GetMinedCommitment(params.type, quorum->GetBlockHash()); + if (mined_hash.IsNull()) { + error = "mined quorum commitment not found for " + quorum->GetBlockHash().ToString(); + failure = EvoSectionFailure::LOCAL_CORRUPTION; + return std::nullopt; + } + const int work_height{EvoSnapshotWorkHeight(consensus, params, data.rotation_enabled, quorum->nHeight)}; + if (work_height < 0) { + error = "mined quorum commitment below its work depth"; + failure = EvoSectionFailure::INTERNAL_ERROR; + return std::nullopt; + } + return MinedQuorumCommitment{quorum->GetBlockHash(), base_index->GetAncestor(work_height)->GetBlockHash(), + std::move(commitment), mined_hash}; + }; + for (const CBlockIndex* quorum : active) { + auto entry{read_commitment(quorum)}; + if (!entry) return false; + data.active_commitments.emplace_back(std::move(*entry)); + } + for (const CBlockIndex* quorum : safety) { + auto entry{read_commitment(quorum)}; + if (!entry) return false; + data.safety_commitments.emplace_back(std::move(*entry)); + } + + for (const CBlockIndex* cycle : plan.rotation_cycles) { + auto stored{qsnapman.GetSnapshotForBlock(params.type, cycle)}; + const auto inputs_complete = [&]() EXCLUSIVE_LOCKS_REQUIRED(::cs_main) { + for (int k{1}; k <= 3; ++k) { + const int previous{cycle->nHeight - k * params.dkgInterval}; + if (IsApplicableRotationCycle(chainman, params, base_index, previous) && + !qsnapman.GetSnapshotForBlock(params.type, base_index->GetAncestor(previous))) { + return false; + } + } + return true; + }; + if (!stored && inputs_complete()) { + // Block connection stores every applicable quarter when its + // cycle connects (PreComputeQuorumMembers), so this only + // restores a lost one, from complete inputs, the same way. + (void)llmq::utils::GetAllQuorumMembers(params.type, {dmnman, qsnapman, chainman, cycle}); + stored = qsnapman.GetSnapshotForBlock(params.type, cycle); + } + // Block connection stored it, and its inputs too unless they are lost. + if (!stored) { + error = strprintf("rotation quarter for applicable cycle %d is unavailable", cycle->nHeight); + return fail(EvoSectionFailure::LOCAL_CORRUPTION); + } + const CBlockIndex* work{cycle->GetAncestor(cycle->nHeight - llmq::WORK_DIFF_DEPTH)}; + data.rotation_snapshots.push_back({cycle->GetBlockHash(), work->GetBlockHash(), *stored}); + } + for (const auto& work : plan.work_blocks) { + std::optional modifier; + if (captures) { + const auto captured{captures->modifiers.find({params.type, work.work_index->GetBlockHash()})}; + if (captured != captures->modifiers.end()) modifier = captured->second; + } + if (!modifier) { + modifier = ExactWorkModifier(params, consensus, chainman.m_blockman, base_index, work, + qsnapman.GetSeededQuorumModifier(params.type, work.work_index->GetBlockHash()), + error, failure); + } + if (!modifier) return false; + result.quorum_modifiers.push_back({params.type, work.work_index->GetBlockHash(), *modifier}); + if (work.work_index != base_index) historical.emplace(work.work_index->GetBlockHash(), work.work_index); + } + data.bootstrap_work = plan.bootstrap_work; + data.active_commitments = CanonicallySortedCopy(std::move(data.active_commitments)); + data.safety_commitments = CanonicallySortedCopy(std::move(data.safety_commitments)); + data.rotation_snapshots = CanonicallySortedCopy(std::move(data.rotation_snapshots)); + result.quorums.emplace_back(std::move(data)); + } + + std::vector ordered_history; + ordered_history.reserve(historical.size()); + for (const auto& [_, index] : historical) + ordered_history.push_back(index); + std::sort(ordered_history.begin(), ordered_history.end(), [](const auto* a, const auto* b) { + return std::make_tuple(a->nHeight, a->GetBlockHash()) > std::make_tuple(b->nHeight, b->GetBlockHash()); + }); + CDeterministicMNList previous_list{result.mn_list}; + uint256 previous_hash{result.base_block_hash}; + for (const CBlockIndex* index : ordered_history) { + const CDeterministicMNList list{BoundList(dmnman.GetListForBlock(index), index)}; + result.historical_mn_list_diffs.push_back({previous_hash, index->GetBlockHash(), index->nHeight, + list.GetTotalRegisteredCount(), CanonicalMNListHash(list), + previous_list.BuildDiff(list)}); + previous_hash = index->GetBlockHash(); + previous_list = list; + } + result.credit_pool = captures && captures->base_credit_pool ? *captures->base_credit_pool + : cpoolman.GetCreditPool(base_index); + const int window{chainparams.CreditPoolPeriodBlocks()}; + for (int height{std::max(0, base_index->nHeight - window + 1)}; height <= base_index->nHeight; ++height) { + const CBlockIndex* index{base_index->GetAncestor(height)}; + if (captures) { + if (const auto captured{captures->credit_history.find(index->GetBlockHash())}; + captured != captures->credit_history.end()) { + result.credit_history.push_back(captured->second); + continue; + } + } + const auto entry{CCreditPoolManager::ReadHistoryEntry(index, chainman.m_blockman, consensus, error)}; + if (!entry) return fail(body_failure(*index)); + result.credit_history.push_back(*entry); + } + // The raw record: GetSignalsStage() would filter it for the next block, + // which the snapshot chainstate does itself. + result.mnhf_signals = mnhfman.GetForBlock(base_index); + result.quorums = CanonicallySortedCopy(std::move(result.quorums)); + result.historical_mn_list_diffs = CanonicallySortedCopy(std::move(result.historical_mn_list_diffs)); + result.quorum_modifiers = CanonicallySortedCopy(std::move(result.quorum_modifiers)); + try { + result.Validate(); + } catch (const std::exception& e) { + error = e.what(); + return fail(EvoSectionFailure::INTERNAL_ERROR); + } + // Export and load apply the same coverage gate; the exporter also checks + // the proven scans against its complete index. + if (!CheckScanCoverage(result, chainman, base_index, &qblockman, error)) + return fail(EvoSectionFailure::INTERNAL_ERROR); + snapshot = std::move(result); + return true; +} + +bool ValidateEvoSnapshotAgainstChain(const EvoSnapshot& snapshot, const ChainstateManager& chainman, + const CBlockIndex* base_index, std::string& error) +{ + AssertLockHeld(::cs_main); + error.clear(); + const auto fail = [&](const std::string& message) { + error = message; + return false; + }; + if (base_index == nullptr || snapshot.base_block_hash != base_index->GetBlockHash() || + snapshot.mn_list.GetBlockHash() != base_index->GetBlockHash() || + snapshot.mn_list.GetHeightForSnapshotCodec() != base_index->nHeight) { + return fail("evo snapshot base block/height mismatch"); + } + try { + snapshot.Validate(/*require_canonical_order=*/true); + } catch (const std::exception& e) { + return fail(e.what()); + } + + const auto& consensus{chainman.GetConsensus()}; + if (!DeploymentActiveAt(*base_index, consensus, Consensus::DEPLOYMENT_DIP0003)) { + if (!snapshot.quorums.empty() || !snapshot.historical_mn_list_diffs.empty() || + !snapshot.quorum_modifiers.empty() || snapshot.mn_list.GetCounts().total() != 0 || + snapshot.mn_list.GetTotalRegisteredCount() != 0 || snapshot.credit_pool.locked != 0 || + snapshot.credit_pool.currentLimit != 0 || snapshot.credit_pool.latelyUnlocked != 0 || + !snapshot.credit_pool.indexes.IsEmpty() || !snapshot.credit_history.empty() || + !snapshot.mnhf_signals.empty() || snapshot.base_coinbase || !snapshot.base_coinbase_branch.empty()) { + return fail("nonempty pre-DIP3 evo snapshot"); + } + return true; + } + if (!CheckNextCycleContext(chainman, base_index, error)) return false; + // Anchor the section to the base header. + if (!snapshot.base_coinbase || + ComputeMerkleRootFromBranch(snapshot.base_coinbase->GetHash(), snapshot.base_coinbase_branch, 0) != + base_index->hashMerkleRoot) { + return fail("evo snapshot base coinbase is not proven by the base header"); + } + + // The credit-pool window: one entry per block of the window ending at the + // base, zero wherever the pool did not exist yet. + const int window{chainman.GetParams().CreditPoolPeriodBlocks()}; + if (window <= 0 || static_cast(window) > EVO_SNAPSHOT_MAX_CREDIT_HISTORY || + snapshot.credit_history.size() != static_cast(std::min(window, base_index->nHeight + 1))) { + return fail("evo credit history does not cover the credit-pool window"); + } + const int first_height{base_index->nHeight - static_cast(snapshot.credit_history.size()) + 1}; + for (size_t i{0}; i < snapshot.credit_history.size(); ++i) { + if (first_height + static_cast(i) < consensus.DeploymentHeight(Consensus::DEPLOYMENT_V20) && + snapshot.credit_history[i] != CreditHistoryEntry{}) { + return fail("evo credit history before v20 is not empty"); + } + } + if (!DeploymentActiveAt(*base_index, consensus, Consensus::DEPLOYMENT_V20) && + (snapshot.credit_pool.locked != 0 || snapshot.credit_pool.currentLimit != 0 || + snapshot.credit_pool.latelyUnlocked != 0 || !snapshot.credit_pool.indexes.IsEmpty())) { + return fail("evo credit pool before v20 is not empty"); + } + + std::map historical_lists; + if (!ReconstructHistoricalMNLists(snapshot, historical_lists, error)) return false; + historical_lists.emplace(snapshot.base_block_hash, snapshot.mn_list); + for (const auto& entry : snapshot.historical_mn_list_diffs) { + const CBlockIndex* index{chainman.m_blockman.LookupBlockIndex(entry.block_hash)}; + if (index == nullptr || base_index->GetAncestor(index->nHeight) != index || entry.height != index->nHeight) { + return fail("invalid historical evo MN list chain data"); + } + } + + std::map, uint256> carried_modifiers; + for (const auto& entry : snapshot.quorum_modifiers) { + carried_modifiers.emplace(std::make_pair(entry.llmq_type, entry.work_block_hash), entry.modifier); + } + std::map actual; + for (const auto& data : snapshot.quorums) + actual.emplace(data.llmq_type, &data); + size_t known_count{0}; + std::set required_work_hashes; + std::set> required_modifiers; + for (const auto& params : consensus.llmqs) { + const bool enabled{chainman.IsQuorumTypeEnabled(params.type, base_index)}; + const auto it{actual.find(params.type)}; + if (it == actual.end()) { + if (enabled) return fail("missing enabled evo quorum type"); + continue; + } + ++known_count; + const auto& data{*it->second}; + // A disabled type is carried only for the retained commitments it + // still contributes to the CbTx quorum merkle root. + if (!enabled && data.active_commitments.empty()) return fail("empty disabled evo quorum type"); + const bool rotation_enabled{llmq::IsQuorumRotationEnabled(params, base_index)}; + const size_t active_count{static_cast(params.signingActiveQuorumCount)}; + const size_t total_count{SnapshotCommitmentCount(params, rotation_enabled)}; + if (data.rotation_enabled != rotation_enabled || data.active_commitments.size() > active_count || + data.safety_commitments.size() > total_count - active_count) { + return fail("evo quorum params/count mismatch"); + } + + std::set active_indexes; + std::vector carried; + const auto check_commitment = [&](const MinedQuorumCommitment& entry) EXCLUSIVE_LOCKS_REQUIRED(::cs_main) { + if (!ValidateCommitmentAgainstChain(entry, chainman, base_index, params, rotation_enabled)) return false; + const CBlockIndex* quorum{chainman.m_blockman.LookupBlockIndex(entry.quorum_base_block_hash)}; + const CBlockIndex* work{ + base_index->GetAncestor(EvoSnapshotWorkHeight(consensus, params, rotation_enabled, quorum->nHeight))}; + if (work == nullptr || entry.work_block_hash != work->GetBlockHash()) return false; + carried.push_back(quorum); + return true; + }; + for (const auto& entry : data.active_commitments) { + if (!check_commitment(entry) || + (rotation_enabled && !active_indexes.insert(entry.commitment.quorumIndex).second)) { + return fail("invalid active evo quorum commitment chain data"); + } + } + for (const auto& entry : data.safety_commitments) { + if (!check_commitment(entry)) return fail("invalid safety evo quorum commitment chain data"); + } + + EvoSnapshotDependencyPlan plan; + if (!PlanEvoSnapshotDependencies(chainman, base_index, params.type, carried, plan, error)) return false; + + std::set expected_cycles; + for (const CBlockIndex* cycle : plan.rotation_cycles) + expected_cycles.insert(cycle->GetBlockHash()); + std::set actual_cycles; + for (const auto& entry : data.rotation_snapshots) + actual_cycles.insert(entry.cycle_base_block_hash); + if (actual_cycles != expected_cycles) return fail("evo rotation cycles differ from the planned set"); + for (const auto& entry : data.rotation_snapshots) { + // A planned cycle: a known ancestor of the base, above the work depth. + const CBlockIndex* cycle{Assert(chainman.m_blockman.LookupBlockIndex(entry.cycle_base_block_hash))}; + const CBlockIndex* work{Assert(cycle->GetAncestor(cycle->nHeight - llmq::WORK_DIFF_DEPTH))}; + if (entry.work_block_hash != work->GetBlockHash()) return fail("evo rotation cycle/work ancestor mismatch"); + const auto historical{historical_lists.find(work->GetBlockHash())}; + if (historical == historical_lists.end() || + entry.snapshot.activeQuorumMembers.size() != historical->second.GetCounts().total()) { + return fail("evo rotation bitset/work-block MN count mismatch"); + } + // The ordinary computation stores no quarter for a cycle with too + // few masternodes, so later cycles read without one, and a base + // that needs such a cycle is unsupported. This catches the first + // no-store path only; the pin covers the other (no member added). + if (historical->second.GetCounts().enabled() < static_cast(params.size / 4)) { + return fail("evo rotation quarter carried for a cycle the ordinary computation leaves without one"); + } + } + if (data.bootstrap_work != plan.bootstrap_work) return fail("evo bootstrap work differs from the planned set"); + + for (const auto& work : plan.work_blocks) { + const uint256& work_hash{work.work_index->GetBlockHash()}; + required_work_hashes.insert(work_hash); + required_modifiers.emplace(params.type, work_hash); + const auto carried_modifier{carried_modifiers.find(std::make_pair(params.type, work_hash))}; + if (carried_modifier == carried_modifiers.end()) return fail("missing planned evo quorum modifier"); + // Recompute wherever the chain allows; only an unreadable v20 work + // block leaves the carried value to the pin. + std::string modifier_error; + EvoSectionFailure modifier_failure; + const auto expected{ExactWorkModifier(params, consensus, chainman.m_blockman, base_index, work, + carried_modifier->second, modifier_error, modifier_failure)}; + if (!expected) return fail(modifier_error); + if (*expected != carried_modifier->second) return fail("evo carried quorum modifier mismatch"); + } + } + if (actual.size() != known_count) return fail("unknown evo quorum type"); + + std::set historical_hashes; + for (const auto& entry : snapshot.historical_mn_list_diffs) + historical_hashes.insert(entry.block_hash); + required_work_hashes.erase(base_index->GetBlockHash()); + if (historical_hashes != required_work_hashes) return fail("historical evo MN lists differ from the planned set"); + std::set> actual_modifiers; + for (const auto& [key, _] : carried_modifiers) + actual_modifiers.insert(key); + if (actual_modifiers != required_modifiers) return fail("evo quorum modifiers differ from the planned set"); + + if (!MoneyRange(snapshot.credit_pool.locked) || !MoneyRange(snapshot.credit_pool.currentLimit) || + !MoneyRange(snapshot.credit_pool.latelyUnlocked)) + return fail("invalid evo credit pool monetary value"); + for (const auto& [bit, height] : snapshot.mnhf_signals) { + if (bit >= VERSIONBITS_NUM_BITS || height < 0 || height > base_index->nHeight) { + return fail("invalid evo MNHF signal bit/height"); + } + } + return CheckScanCoverage(snapshot, chainman, base_index, /*full_index=*/nullptr, error); +} + +bool VerifyEvoSnapshotBaseBlock(const EvoSnapshot& snapshot, const CBlockIndex& base_index, + const Consensus::Params& consensus, std::string& error) +{ + if (!snapshot.base_coinbase) { + error = "evo snapshot without a base coinbase"; + return false; + } + if (ComputeMerkleRootFromBranch(snapshot.base_coinbase->GetHash(), snapshot.base_coinbase_branch, 0) != + base_index.hashMerkleRoot) { + error = "evo snapshot base coinbase is not proven by the base header"; + return false; + } + const bool legacy{!DeploymentActiveAt(base_index, consensus, Consensus::DEPLOYMENT_V19)}; + const auto cbtx{DecodeCbTxPayload(*snapshot.base_coinbase, legacy)}; + if (!cbtx) { + error = "evo snapshot base coinbase has no valid CbTx payload"; + return false; + } + return VerifyEvoSnapshotCbTx(snapshot, *cbtx, error); +} + +bool PreflightEvoSnapshot(const EvoSnapshot& snapshot, const ChainstateManager& chainman, const CBlockIndex* base_index, + std::string& error, EvoSectionFailure* failure) +{ + AssertLockHeld(::cs_main); + if (failure) *failure = base_index ? EvoSectionFailure::DISAGREEMENT : EvoSectionFailure::INTERNAL_ERROR; + if (!ValidateEvoSnapshotAgainstChain(snapshot, chainman, base_index, error)) return false; + return !DeploymentActiveAt(*base_index, chainman.GetConsensus(), Consensus::DEPLOYMENT_DIP0003) || + VerifyEvoSnapshotBaseBlock(snapshot, *base_index, chainman.GetConsensus(), error); +} + +bool SeedEvoSnapshotState(const EvoViewToken& view, const EvoSnapshot& snapshot, CSpecialTxProcessor& special_tx, + const node::BlockManager& blockman, const CBlockIndex* base_index, std::string& error) +{ + auto& dmnman{special_tx.DeterministicMNManager()}; + auto& qblockman{special_tx.QuorumBlockProcessor()}; + auto& qsnapman{special_tx.QuorumSnapshotManager()}; + auto& cpoolman{*special_tx.m_cpoolman}; + auto& mnhfman{*special_tx.m_mnhfman}; + AssertLockHeld(::cs_main); + const auto fail = [&](const std::string& message) { + error = message; + return false; + }; + if (base_index == nullptr || base_index->GetBlockHash() != snapshot.base_block_hash) { + return fail("evo snapshot seeding base mismatch"); + } + std::map historical_lists; + if (!ReconstructHistoricalMNLists(snapshot, historical_lists, error)) return false; + if (!dmnman.SeedListForBlock(view, snapshot.mn_list)) return fail("failed to seed the base MN list"); + for (const auto& [_, list] : historical_lists) { + if (!dmnman.SeedListForBlock(view, list)) return fail("failed to seed a historical MN list"); + } + for (const auto& modifier : snapshot.quorum_modifiers) { + if (!qsnapman.SeedQuorumModifier(view, modifier.llmq_type, modifier.work_block_hash, modifier.modifier)) { + return fail("failed to seed a quorum score modifier"); + } + } + for (const auto& data : snapshot.quorums) { + for (const auto* commitments : {&data.active_commitments, &data.safety_commitments}) { + for (const auto& entry : *commitments) { + if (!qblockman.SeedMinedCommitment(view, data.llmq_type, entry.quorum_base_block_hash, entry.commitment, + entry.mined_block_hash)) { + return fail("failed to seed a mined quorum commitment"); + } + } + } + for (const auto& rotation : data.rotation_snapshots) { + const CBlockIndex* cycle{blockman.LookupBlockIndex(rotation.cycle_base_block_hash)}; + if (cycle == nullptr || !qsnapman.SeedSnapshotForBlock(view, data.llmq_type, cycle, rotation.snapshot)) { + return fail("failed to seed a rotation quarter"); + } + } + } + if (!cpoolman.SeedSnapshot(view, base_index, snapshot.credit_pool) || + !cpoolman.SeedHistory(view, base_index, snapshot.credit_history)) { + return fail("failed to seed the credit pool"); + } + if (!mnhfman.SeedSignals(view, base_index, snapshot.mnhf_signals)) return fail("failed to seed the MNHF signals"); + if (snapshot.base_coinbase && !special_tx.SeedBaseCoinbase(view, base_index, *snapshot.base_coinbase)) { + return fail("failed to seed the base coinbase"); + } + for (const auto& params : special_tx.ConsensusParams().llmqs) { + if (!qblockman.SeedScanCoverage(view, params.type, EvoSnapshotScanCoverage(snapshot, params))) { + return fail("failed to seed quorum scan coverage"); + } + } + return true; +} + +bool DryRunEvoSnapshot(const EvoSnapshot& snapshot, const ChainstateManager& chainman, CSpecialTxProcessor& special_tx, + const CBlockIndex* base_index, std::string& error) +{ + AssertLockHeld(::cs_main); + const auto& consensus{chainman.GetConsensus()}; + const llmq::UtilParameters util{special_tx.DeterministicMNManager(), special_tx.QuorumSnapshotManager(), chainman, + Assert(base_index)}; + try { + for (const auto& data : snapshot.quorums) { + const auto& params{SnapshotLLMQParams(data.llmq_type)}; + for (const auto* commitments : {&data.active_commitments, &data.safety_commitments}) { + for (const auto& entry : *commitments) { + const CBlockIndex* quorum{chainman.m_blockman.LookupBlockIndex(entry.quorum_base_block_hash)}; + if (quorum == nullptr) { + error = "evo snapshot dry run: unknown carried quorum"; + return false; + } + (void)llmq::utils::GetAllQuorumMembers(data.llmq_type, util.replace_index(quorum), + /*reset_cache=*/true); + } + } + if (!chainman.IsQuorumTypeEnabled(data.llmq_type, base_index)) continue; + const int h{base_index->nHeight - base_index->nHeight % params.dkgInterval}; + if (h > 0) { + (void)llmq::utils::GetAllQuorumMembers(data.llmq_type, util.replace_index(base_index->GetAncestor(h)), + /*reset_cache=*/true); + } + // The next cycle has no header yet; its members are computable + // from the section only when its work block is at or below the base. + const int next{h + params.dkgInterval}; + const int work_height{EvoSnapshotWorkHeight(consensus, params, data.rotation_enabled, next)}; + if (work_height >= 0 && work_height <= base_index->nHeight) { + const CBlockIndex* work{base_index->GetAncestor(work_height)}; + // Before v20 the modifier needs the next cycle's own block hash. + if (!llmq::utils::ComputeQuorumMembersFromWorkBlock(data.llmq_type, util.replace_index(work), work, next) && + DeploymentActiveAfter(work, consensus, Consensus::DEPLOYMENT_V20)) { + error = "evo snapshot dry run: the members of the next cycle are not computable"; + return false; + } + } + } + } catch (const LocalStateError& e) { + error = strprintf("evo snapshot dry run: %s", e.what()); + return false; + } + return true; +} + +namespace { +// Captures are NORMAL records that exist only while a snapshot is being +// validated: (prefix, kind, block hash, LLMQ type). +const std::string EVODB_SNAPSHOT_CAPTURE{"b_evo_cap"}; +enum class CaptureKind : uint8_t { + MODIFIER, + CREDIT_HISTORY, + BASE_COINBASE, + BASE_CREDIT_POOL +}; +using CaptureKey = std::tuple; + +CaptureKey MakeCaptureKey(CaptureKind kind, const uint256& block_hash, + Consensus::LLMQType llmq_type = Consensus::LLMQType::LLMQ_NONE) +{ + return {EVODB_SNAPSHOT_CAPTURE, static_cast(kind), block_hash, static_cast(llmq_type)}; +} +} // namespace + +std::optional PlanEvoSnapshotCaptures(const EvoSnapshot& snapshot, const ChainstateManager& chainman) +{ + AssertLockHeld(::cs_main); + const CBlockIndex* base{chainman.m_blockman.LookupBlockIndex(snapshot.base_block_hash)}; + if (base == nullptr) return std::nullopt; + EvoSnapshotCapturePlan plan; + plan.base = base->GetBlockHash(); + plan.base_height = base->nHeight; + if (!snapshot.base_coinbase) return plan; + // The chain checks, which refuse a credit window reaching below genesis, + // run after the captures. + if (snapshot.credit_history.size() > static_cast(base->nHeight) + 1) return std::nullopt; + plan.heights.insert(base->nHeight); + plan.window_first = base->nHeight - static_cast(snapshot.credit_history.size()) + 1; + for (int height{plan.window_first}; height <= base->nHeight; ++height) + plan.heights.insert(height); + for (const auto& entry : snapshot.quorum_modifiers) { + const CBlockIndex* work{chainman.m_blockman.LookupBlockIndex(entry.work_block_hash)}; + if (work == nullptr || base->GetAncestor(work->nHeight) != work) return std::nullopt; + // Only a v20 work block's modifier comes from its body. + if (!DeploymentActiveAfter(work, chainman.GetConsensus(), Consensus::DEPLOYMENT_V20)) continue; + plan.modifier_blocks[work->GetBlockHash()].push_back(entry.llmq_type); + plan.heights.insert(work->nHeight); + } + return plan; +} + +bool WriteEvoSnapshotCaptures(CEvoDB& evodb, CCreditPoolManager& cpoolman, const node::BlockManager& blockman, + const EvoSnapshotCapturePlan& plan, const CBlock& block, const CBlockIndex& index, + const Consensus::Params& consensus, std::string& error) +{ + if (!plan.heights.contains(index.nHeight) || index.nHeight > plan.base_height) return true; + const uint256 hash{index.GetBlockHash()}; + if (const auto it{plan.modifier_blocks.find(hash)}; it != plan.modifier_blocks.end()) { + for (const auto llmq_type : it->second) { + const uint256 modifier{ + llmq::utils::GetWorkBlockModifier(SnapshotLLMQParams(llmq_type), consensus, block, index)}; + if (!evodb.WriteDerived(MakeCaptureKey(CaptureKind::MODIFIER, hash, llmq_type), modifier)) { + error = "conflicting quorum modifier capture"; + return false; + } + } + } + if (index.nHeight >= plan.window_first) { + const auto entry{CCreditPoolManager::ReadHistoryEntry(&index, blockman, consensus, error)}; + if (!entry || !evodb.WriteDerived(MakeCaptureKey(CaptureKind::CREDIT_HISTORY, hash), *entry)) { + if (error.empty()) error = "conflicting credit-pool window capture"; + return false; + } + } + if (hash == plan.base) { + if (block.vtx.empty() || !evodb.WriteDerived(MakeCaptureKey(CaptureKind::BASE_COINBASE, hash), + std::make_pair(block.vtx[0], BlockMerkleBranch(block, 0)))) { + error = "conflicting base coinbase capture"; + return false; + } + // The pool at the base folds in every block back to the last pool on + // disk, which may lie below the window. + std::optional pool; + try { + pool = cpoolman.GetCreditPool(&index); + } catch (const LocalStateError&) { + // This node's own data, e.g. a pruned body: the caller stalls. + throw; + } catch (const std::exception& e) { + error = strprintf("the credit pool at the base is not computable: %s", e.what()); + return false; + } + if (!evodb.WriteDerived(MakeCaptureKey(CaptureKind::BASE_CREDIT_POOL, hash), *pool)) { + error = "conflicting base credit pool capture"; + return false; + } + } + return true; +} + +EvoSnapshotCaptures ReadEvoSnapshotCaptures(CEvoDB& evodb, const EvoSnapshotCapturePlan& plan, + const ChainstateManager& chainman) +{ + AssertLockHeld(::cs_main); + EvoSnapshotCaptures captures; + for (const auto& [hash, types] : plan.modifier_blocks) { + for (const auto llmq_type : types) { + uint256 modifier; + if (evodb.Read(MakeCaptureKey(CaptureKind::MODIFIER, hash, llmq_type), modifier)) { + captures.modifiers.emplace(std::make_pair(llmq_type, hash), modifier); + } + } + } + const CBlockIndex* base{chainman.m_blockman.LookupBlockIndex(plan.base)}; + if (base == nullptr || plan.window_first < 0) return captures; + for (int height{plan.window_first}; height <= plan.base_height; ++height) { + const uint256 hash{base->GetAncestor(height)->GetBlockHash()}; + CreditHistoryEntry entry; + if (evodb.Read(MakeCaptureKey(CaptureKind::CREDIT_HISTORY, hash), entry)) { + captures.credit_history.emplace(hash, entry); + } + } + std::pair> coinbase; + if (evodb.Read(MakeCaptureKey(CaptureKind::BASE_COINBASE, plan.base), coinbase)) { + std::tie(captures.base_coinbase, captures.base_coinbase_branch) = std::move(coinbase); + } + if (CCreditPool pool; evodb.Read(MakeCaptureKey(CaptureKind::BASE_CREDIT_POOL, plan.base), pool)) { + captures.base_credit_pool = std::move(pool); + } + return captures; +} + +bool EraseAllEvoSnapshotCaptures(CEvoDB& evodb) +{ + CDBWrapper& db{evodb.GetRawDB()}; + DataStream prefix; + prefix << EVODB_SNAPSHOT_CAPTURE; + std::unique_ptr it{db.NewIterator()}; + CDBBatch batch{db}; + for (it->Seek(prefix); it->Valid(); it->Next()) { + const auto key{it->GetKey()}; + if (key.size() < prefix.size() || !std::equal(prefix.begin(), prefix.end(), key.begin())) break; + batch.Erase(key); + } + // Most startups have nothing to erase; spare them the synced write. + return batch.SizeEstimate() == 0 || db.WriteBatch(batch, /*fSync=*/true); +} + +void EraseEvoSnapshotCaptures(CEvoDB& evodb, const EvoSnapshotCapturePlan& plan, const ChainstateManager& chainman) +{ + AssertLockHeld(::cs_main); + for (const auto& [hash, types] : plan.modifier_blocks) { + for (const auto llmq_type : types) + evodb.Erase(MakeCaptureKey(CaptureKind::MODIFIER, hash, llmq_type)); + } + const CBlockIndex* base{chainman.m_blockman.LookupBlockIndex(plan.base)}; + if (base == nullptr || plan.window_first < 0) return; + for (int height{plan.window_first}; height <= plan.base_height; ++height) { + evodb.Erase(MakeCaptureKey(CaptureKind::CREDIT_HISTORY, base->GetAncestor(height)->GetBlockHash())); + } + evodb.Erase(MakeCaptureKey(CaptureKind::BASE_COINBASE, plan.base)); + evodb.Erase(MakeCaptureKey(CaptureKind::BASE_CREDIT_POOL, plan.base)); +} + +} // namespace evo diff --git a/src/evo/snapshot_config.cpp b/src/evo/snapshot_config.cpp new file mode 100644 index 000000000000..00c137c95197 --- /dev/null +++ b/src/evo/snapshot_config.cpp @@ -0,0 +1,51 @@ +// Copyright (c) 2026 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include + +#include +#include + +#include + +EvoSnapshotConsensusConfig EvoSnapshotConsensusConfig::FromParams(const CChainParams& params) +{ + const Consensus::Params& consensus{params.GetConsensus()}; + EvoSnapshotConsensusConfig config; + config.deployments.assign(std::begin(consensus.vDeployments), std::end(consensus.vDeployments)); + config.miner_confirmation_window = consensus.nMinerConfirmationWindow; + config.rule_change_activation_threshold = consensus.nRuleChangeActivationThreshold; + config.min_bip9_warning_height = consensus.MinBIP9WarningHeight; + for (int dep{std::numeric_limits::min()}; + Consensus::ValidDeployment(static_cast(dep)); ++dep) { + config.buried_heights.push_back(consensus.DeploymentHeight(static_cast(dep))); + } + config.dip3_enforcement_height = consensus.DIP0003EnforcementHeight; + config.dip24_quorums_height = consensus.DIP0024QuorumsHeight; + config.llmqs = consensus.llmqs; + for (const Consensus::LLMQType role : {consensus.llmqTypeChainLocks, consensus.llmqTypeDIP0024InstantSend, + consensus.llmqTypePlatform, consensus.llmqTypeMnhf}) { + config.llmq_roles.push_back(static_cast(role)); + } + config.credit_pool_period_blocks = params.CreditPoolPeriodBlocks(); + config.dip3_enforcement_hash = consensus.DIP0003EnforcementHash; + config.masternode_minimum_confirmations = consensus.nMasternodeMinimumConfirmations; + config.subsidy_halving_interval = consensus.nSubsidyHalvingInterval; + config.budget_payments_start_block = consensus.nBudgetPaymentsStartBlock; + config.high_subsidy_blocks = consensus.nHighSubsidyBlocks; + config.high_subsidy_factor = consensus.nHighSubsidyFactor; + config.superblock_cycle = consensus.nSuperblockCycle; + config.masternode_payments_increase_block = consensus.nMasternodePaymentsIncreaseBlock; + config.masternode_payments_increase_period = consensus.nMasternodePaymentsIncreasePeriod; + config.network_id = params.GetChainTypeString(); + config.require_routable_external_ip = params.RequireRoutableExternalIP(); + return config; +} + +uint256 EvoSnapshotConsensusConfig::Fingerprint() const +{ + HashWriter writer{}; + writer << *this; + return writer.GetSHA256(); +} diff --git a/src/evo/snapshot_config.h b/src/evo/snapshot_config.h new file mode 100644 index 000000000000..c4215116ff32 --- /dev/null +++ b/src/evo/snapshot_config.h @@ -0,0 +1,108 @@ +// Copyright (c) 2026 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_EVO_SNAPSHOT_CONFIG_H +#define BITCOIN_EVO_SNAPSHOT_CONFIG_H + +#include +#include +#include + +#include +#include +#include + +class CChainParams; + +/** The effective consensus and deployment configuration that stored Evo state + * depends on. Deployment and LLMQ parameters decide which raw records (MNHF + * signals, commitments, credit-pool windows) were stored, so a snapshot + * generation is valid only for the configuration it was published under. + * + * Fingerprint() is SHA256 over this canonical serialization, led by + * fingerprint_version. Bump FINGERPRINT_VERSION whenever the covered set + * changes. */ +struct EvoSnapshotConsensusConfig { + static constexpr uint16_t FINGERPRINT_VERSION{2}; + + uint16_t fingerprint_version{FINGERPRINT_VERSION}; + std::vector deployments; + uint32_t miner_confirmation_window{0}; + uint32_t rule_change_activation_threshold{0}; + int32_t min_bip9_warning_height{0}; + //! DeploymentHeight() of every BuriedDeployment, in enum order. + std::vector buried_heights; + int32_t dip3_enforcement_height{0}; + int32_t dip24_quorums_height{0}; + std::vector llmqs; + std::vector llmq_roles; + int32_t credit_pool_period_blocks{0}; + //! The block DIP3 enforcement requires at its height. + uint256 dip3_enforcement_hash; + int32_t masternode_minimum_confirmations{0}; + //! Inputs of the block subsidy and masternode reward, which the credit + //! pool balance is checked against. + int32_t subsidy_halving_interval{0}; + int32_t budget_payments_start_block{0}; + int32_t high_subsidy_blocks{0}; + int32_t high_subsidy_factor{0}; + int32_t superblock_cycle{0}; + int32_t masternode_payments_increase_block{0}; + int32_t masternode_payments_increase_period{0}; + //! Network-specific rules in quorum membership, quorum type enablement and + //! masternode service validation. + std::string network_id; + bool require_routable_external_ip{false}; + + static EvoSnapshotConsensusConfig FromParams(const CChainParams& params); + uint256 Fingerprint() const; + + SERIALIZE_METHODS(EvoSnapshotConsensusConfig, obj) + { + READWRITE(obj.fingerprint_version); + READWRITE(Using>(obj.deployments)); + READWRITE(obj.miner_confirmation_window, obj.rule_change_activation_threshold, obj.min_bip9_warning_height); + READWRITE(obj.buried_heights, obj.dip3_enforcement_height, obj.dip24_quorums_height); + READWRITE(Using>(obj.llmqs)); + READWRITE(obj.llmq_roles, obj.credit_pool_period_blocks); + READWRITE(obj.dip3_enforcement_hash, obj.masternode_minimum_confirmations); + READWRITE(obj.subsidy_halving_interval, obj.budget_payments_start_block, obj.high_subsidy_blocks, + obj.high_subsidy_factor, obj.superblock_cycle, obj.masternode_payments_increase_block, + obj.masternode_payments_increase_period); + READWRITE(obj.network_id, obj.require_routable_external_ip); + } + +private: + struct DeploymentFormatter { + template + static void Ser(Stream& s, const Consensus::BIP9Deployment& d) + { + s << int32_t{d.bit} << d.nStartTime << d.nTimeout << int32_t{d.min_activation_height} << d.nWindowSize + << d.nThresholdStart << d.nThresholdMin << d.nFalloffCoeff << d.useEHF; + } + template + static void Unser(Stream&, Consensus::BIP9Deployment&) + { + static_assert(!sizeof(Stream*), "fingerprint input is serialize-only"); + } + }; + struct LLMQParamsFormatter { + template + static void Ser(Stream& s, const Consensus::LLMQParams& p) + { + s << static_cast(p.type) << std::string{p.name} << p.useRotation << int32_t{p.size} + << int32_t{p.minSize} << int32_t{p.threshold} << int32_t{p.dkgInterval} << int32_t{p.dkgPhaseBlocks} + << int32_t{p.dkgMiningWindowStart} << int32_t{p.dkgMiningWindowEnd} << int32_t{p.dkgBadVotesThreshold} + << int32_t{p.signingActiveQuorumCount} << int32_t{p.keepOldConnections} << int32_t{p.keepOldKeys} + << int32_t{p.recoveryMembers}; + } + template + static void Unser(Stream&, Consensus::LLMQParams&) + { + static_assert(!sizeof(Stream*), "fingerprint input is serialize-only"); + } + }; +}; + +#endif // BITCOIN_EVO_SNAPSHOT_CONFIG_H diff --git a/src/evo/snapshot_types.h b/src/evo/snapshot_types.h new file mode 100644 index 000000000000..5b7240a93add --- /dev/null +++ b/src/evo/snapshot_types.h @@ -0,0 +1,63 @@ +// Copyright (c) 2026 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_EVO_SNAPSHOT_TYPES_H +#define BITCOIN_EVO_SNAPSHOT_TYPES_H + +#include + +namespace evo { + +/** Base of the errors that describe this node's own Evo state rather than the + * block, transaction or peer being processed. Generic catch sites rethrow it + * unchanged so that the chainstate boundary classifies it: it never becomes + * consensus invalidity or a peer penalty. */ +class LocalStateError : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + +/** Evo state carried by an assumeutxo snapshot disagrees with state derived + * independently. The snapshot is rejected; the block is not at fault. */ +class SnapshotStateMismatchError : public LocalStateError +{ +public: + using LocalStateError::LocalStateError; +}; + +} // namespace evo + +/** Thrown when Evo state for a block cannot be reconstructed because the data + * is not on this node yet (pruned, or below an unvalidated snapshot base, or + * pending in another chainstate's unflushed EvoDB overlay). Distinct from the + * plain std::runtime_error that CDeterministicMNList::ApplyDiff raises for + * genuine local corruption, which must never be swallowed. Messages about a + * masternode list carry BLOCK_DATA_UNAVAILABLE_SUFFIX, matched by + * IsBlockDataUnavailableError(). */ +class BlockDataUnavailableError : public evo::LocalStateError +{ +public: + using evo::LocalStateError::LocalStateError; +}; + +/** A block body this node has (BLOCK_HAVE_DATA) could not be read back to + * derive Evo state: local disk corruption or I/O failure. Like a block that + * ConnectTip cannot read, it stops the node; it is never the block's or a + * peer's fault. */ +class BlockDataReadError : public evo::LocalStateError +{ +public: + using evo::LocalStateError::LocalStateError; +}; + +/** An Evo read, write or cache lookup was not bound to the view of the + * chainstate it serves, or was bound to a conflicting or stale view. */ +class EvoViewBindingError : public evo::LocalStateError +{ +public: + using evo::LocalStateError::LocalStateError; +}; + +#endif // BITCOIN_EVO_SNAPSHOT_TYPES_H diff --git a/src/evo/specialtxman.cpp b/src/evo/specialtxman.cpp index eb2f68cb730a..b02e8d7d5e12 100644 --- a/src/evo/specialtxman.cpp +++ b/src/evo/specialtxman.cpp @@ -15,6 +15,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -28,6 +31,7 @@ #include #include #include +#include #include #include @@ -38,6 +42,7 @@ CSpecialTxProcessor::CSpecialTxProcessor(CEvoDB& evodb, CDeterministicMNManager& const chainlock::Chainlocks& chainlocks, const llmq::CQuorumManager& qman) : m_cpoolman{std::make_unique(evodb, chainman)}, m_mnhfman{std::make_unique(evodb, blockman, consensus_params)}, + m_evodb{evodb}, m_dmnman{dmnman}, m_qblockman{qblockman}, m_qsnapman{qsnapman}, @@ -51,6 +56,54 @@ CSpecialTxProcessor::CSpecialTxProcessor(CEvoDB& evodb, CDeterministicMNManager& CSpecialTxProcessor::~CSpecialTxProcessor() = default; +// The coinbase of an assumeutxo snapshot base, carried by the snapshot and +// proven against the base header. Written only into a snapshot generation. +static const std::string DB_BASE_COINBASE{"cb_S"}; + +CoinbaseChainlock CSpecialTxProcessor::GetCoinbaseChainlock(const CBlockIndex* pindex) const +{ + CoinbaseChainlock result; + // There's no CL in CbTx before v20 activation + if (pindex == nullptr || !DeploymentActiveAt(*pindex, m_consensus_params, Consensus::DEPLOYMENT_V20)) return result; + const auto from_coinbase = [&](const CTransaction& coinbase) { + const bool legacy{!DeploymentActiveAt(*pindex, m_consensus_params, Consensus::DEPLOYMENT_V19)}; + const auto cbtx{DecodeCbTxPayload(coinbase, legacy)}; + if (cbtx && cbtx->nVersion >= CCbTx::Version::CLSIG_AND_BALANCE && cbtx->nHeight == pindex->nHeight && + cbtx->bestCLSignature.Get().IsValid() && cbtx->bestCLHeightDiff < static_cast(pindex->nHeight)) { + result = {CoinbaseChainlock::Status::CERTIFIED, cbtx->bestCLSignature.Get(), cbtx->bestCLHeightDiff}; + } + return result; + }; + const EvoViewToken view{m_evodb.CurrentView()}; + const bool snapshot_view{view.identity == EvoDbIdentity::SNAPSHOT}; + if (snapshot_view && pindex->nHeight == view.base_height) { + CMutableTransaction carried; + if (m_evodb.Read(std::make_pair(DB_BASE_COINBASE, pindex->GetBlockHash()), carried)) { + return from_coinbase(CTransaction{carried}); + } + } + const auto [have_data, block_pos]{WITH_LOCK(::cs_main, return std::make_pair( + (pindex->nStatus & BLOCK_HAVE_DATA) != 0, pindex->GetBlockPos()))}; + if (!have_data) { + if (snapshot_view) result.status = CoinbaseChainlock::Status::UNAVAILABLE; + return result; + } + CBlock block; + if (m_blockman.ReadBlockFromDisk(block, block_pos) != pindex->GetBlockHash()) { + throw BlockDataReadError(strprintf("coinbase of block %s cannot be read", pindex->GetBlockHash().ToString())); + } + if (block.vtx.empty()) return result; + return from_coinbase(*block.vtx[0]); +} + +bool CSpecialTxProcessor::SeedBaseCoinbase(const EvoViewToken& view, const CBlockIndex* base, const CTransaction& coinbase) +{ + if (!Assume(base != nullptr) || !Assume(m_evodb.HasActiveTransaction())) return false; + const EvoViewScope scope{view}; + return m_evodb.WriteSeed(std::make_pair(DB_BASE_COINBASE, base->GetBlockHash()), CMutableTransaction{coinbase}, + EvoSeedPolicy::SCAFFOLDING); +} + static bool AddNetInfoEntries(const std::shared_ptr& net_info, NetInfoPurpose purpose, const NetInfoList& entries, BlockValidationState& state) { @@ -158,14 +211,15 @@ static bool ApplyProUpServTx(CDeterministicMNState& state_mn, const CProUpServTx } bool CheckCbTxBestChainlock(const CCbTx& cbTx, const CBlockIndex* pindex, const Consensus::Params& consensus_params, - const node::BlockManager& blockman, const CChain& chain, const llmq::CQuorumManager& qman, - const chainlock::Chainlocks& chainlocks, BlockValidationState& state) + const CChain& chain, const llmq::CQuorumManager& qman, const chainlock::Chainlocks& chainlocks, + const CSpecialTxProcessor& special_tx, BlockValidationState& state) { if (cbTx.nVersion < CCbTx::Version::CLSIG_AND_BALANCE) { return true; } const CBLSSignature& cbtx_sig{cbTx.bestCLSignature.Get()}; + // The previous block's coinbase ChainLock, remembered per Evo view. struct CachedChainlock { const CBlockIndex* pindex{nullptr}; CBLSSignature sig; @@ -175,24 +229,35 @@ bool CheckCbTxBestChainlock(const CCbTx& cbTx, const CBlockIndex* pindex, const bool verified_here{false}; }; static Mutex cached_mutex; - static std::optional cached GUARDED_BY(cached_mutex); + static EvoViewCache> cached_by_view GUARDED_BY(cached_mutex); + const EvoViewToken view{qman.CurrentView()}; auto best_clsig = chainlocks.GetBestChainLock(); if (best_clsig.getHeight() == pindex->nHeight - 1 && cbTx.bestCLHeightDiff == 0 && cbtx_sig == best_clsig.getSig()) { // matches our best clsig which still hold values for the previous block - WITH_LOCK(cached_mutex, cached.emplace(pindex, cbtx_sig, cbTx.bestCLHeightDiff, /*verified_here=*/false)); + WITH_LOCK(cached_mutex, + cached_by_view.For(view).emplace(pindex, cbtx_sig, cbTx.bestCLHeightDiff, /*verified_here=*/false)); return true; } std::optional> prevBlockCoinbaseChainlock{std::nullopt}; bool prev_verified_here{false}; - if (LOCK(cached_mutex); cached && cached->pindex == pindex->pprev) { - prevBlockCoinbaseChainlock = std::make_pair(cached->sig, cached->height_diff); - prev_verified_here = cached->verified_here; + if (LOCK(cached_mutex); const auto& cached{cached_by_view.For(view)}) { + if (cached->pindex == pindex->pprev) { + prevBlockCoinbaseChainlock = std::make_pair(cached->sig, cached->height_diff); + prev_verified_here = cached->verified_here; + } } if (!prevBlockCoinbaseChainlock.has_value()) { - prevBlockCoinbaseChainlock = GetNonNullCoinbaseChainlock(blockman, pindex->pprev); + const CoinbaseChainlock previous{special_tx.GetCoinbaseChainlock(pindex->pprev)}; + if (previous.status == CoinbaseChainlock::Status::UNAVAILABLE) { + throw BlockDataUnavailableError( + strprintf("coinbase ChainLock of block %s is not available", pindex->pprev->GetBlockHash().ToString())); + } + if (previous.status == CoinbaseChainlock::Status::CERTIFIED) { + prevBlockCoinbaseChainlock = std::make_pair(previous.signature, previous.height_diff); + } } // If std::optional prevBlockCoinbaseChainlock is empty, then up to the previous block, coinbase Chainlock is null. if (prevBlockCoinbaseChainlock.has_value()) { @@ -215,7 +280,8 @@ bool CheckCbTxBestChainlock(const CCbTx& cbTx, const CBlockIndex* pindex, const int curBlockCoinbaseCLHeight = pindex->nHeight - static_cast(cbTx.bestCLHeightDiff) - 1; if (best_clsig.getHeight() == curBlockCoinbaseCLHeight && best_clsig.getSig() == cbtx_sig) { // matches our best (but outdated) clsig, no need to verify it again - WITH_LOCK(cached_mutex, cached.emplace(pindex, cbtx_sig, cbTx.bestCLHeightDiff, /*verified_here=*/false)); + WITH_LOCK(cached_mutex, cached_by_view.For(view).emplace(pindex, cbtx_sig, cbTx.bestCLHeightDiff, + /*verified_here=*/false)); return true; } // The previous block's coinbase chainlock was verified by this process; carried into this @@ -223,7 +289,8 @@ bool CheckCbTxBestChainlock(const CCbTx& cbTx, const CBlockIndex* pindex, const if (prev_verified_here && prevBlockCoinbaseChainlock.has_value() && cbTx.bestCLHeightDiff == prevBlockCoinbaseChainlock->second + 1 && cbtx_sig == prevBlockCoinbaseChainlock->first) { - WITH_LOCK(cached_mutex, cached.emplace(pindex, cbtx_sig, cbTx.bestCLHeightDiff, /*verified_here=*/true)); + WITH_LOCK(cached_mutex, cached_by_view.For(view).emplace(pindex, cbtx_sig, cbTx.bestCLHeightDiff, + /*verified_here=*/true)); return true; } const CBlockIndex* pAncestor = pindex->GetAncestor(curBlockCoinbaseCLHeight); @@ -238,7 +305,8 @@ bool CheckCbTxBestChainlock(const CCbTx& cbTx, const CBlockIndex* pindex, const if (ret != llmq::VerifyRecSigStatus::Valid) { return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-cbtx-invalid-clsig"); } - WITH_LOCK(cached_mutex, cached.emplace(pindex, cbtx_sig, cbTx.bestCLHeightDiff, /*verified_here=*/true)); + WITH_LOCK(cached_mutex, cached_by_view.For(view).emplace(pindex, cbtx_sig, cbTx.bestCLHeightDiff, + /*verified_here=*/true)); } else if (cbTx.bestCLHeightDiff != 0) { // Null bestCLSignature is allowed only with bestCLHeightDiff = 0 return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-cbtx-cldiff"); @@ -303,6 +371,13 @@ bool CSpecialTxProcessor::CheckSpecialTxInner(const CChain* chain, const CTransa return chain ? CheckAssetUnlockTx(m_blockman, m_qman, *chain, tx, pindexPrev, indexes, rules.v24, state) : CheckAssetUnlockTx(m_blockman, m_qman, tx, pindexPrev, indexes, rules.v24, state); } + } catch (const evo::LocalStateError& e) { + // This node's Evo state is missing or wrong, not the transaction. + // During block connection the chainstate boundary classifies it. + // Mempool and mining callers have no such boundary: fail the + // operation without blaming the transaction or its relayer. + if (chain != nullptr) throw; + return state.Error(e.what()); } catch (const std::exception& e) { LogPrintf("%s -- failed: %s\n", __func__, e.what()); return state.Invalid(TxValidationResult::TX_CONSENSUS, "failed-check-special-tx"); @@ -895,9 +970,17 @@ bool CMNListBuilder::Finish(const CBlock& block, BlockValidationState& state, CD mnListRet); } +uint256 CanonicalDeterministicMNListHash(gsl::not_null pindex, const CDeterministicMNList& list) +{ + if (list.GetBlockHash().IsNull()) { + return evo::CanonicalMNListHash(CDeterministicMNList{pindex->GetBlockHash(), pindex->nHeight, 0}); + } + return evo::CanonicalMNListHash(list); +} + uint256 CSpecialTxProcessor::GetDeterministicMNListHash(gsl::not_null pindex) const { - return SerializeHash(m_dmnman.GetListForBlock(pindex)); + return CanonicalDeterministicMNListHash(pindex, m_dmnman.GetListForBlock(pindex)); } void CSpecialTxProcessor::UpdatedMNListTip(gsl::not_null pindex) { m_dmnman.UpdatedBlockTip(pindex); } @@ -910,6 +993,10 @@ bool CSpecialTxProcessor::ProcessSpecialTxsInBlock(Chainstate& chainstate, const { AssertLockHeld(::cs_main); + if (!IsEvoViewBound(chainstate)) { + throw EvoViewBindingError(strprintf("%s: not bound to the chainstate's Evo view", __func__)); + } + try { static SteadyClock::duration nTimeLoop{}; static SteadyClock::duration nTimeQuorum{}; @@ -1069,7 +1156,7 @@ bool CSpecialTxProcessor::ProcessSpecialTxsInBlock(Chainstate& chainstate, const LogPrint(BCLog::BENCHMARK, " - CalcCbTxMerkleRootQuorums: %.2fms [%.2fs]\n", Ticks(nTime6_2 - nTime6_1), Ticks(nTimeMerkleQuorums)); - if (!CheckCbTxBestChainlock(*opt_cbTx, pindex, m_consensus_params, m_blockman, chain, m_qman, m_chainlocks, state)) { + if (!CheckCbTxBestChainlock(*opt_cbTx, pindex, m_consensus_params, chain, m_qman, m_chainlocks, *this, state)) { // pass the state returned by the function above return false; } @@ -1092,16 +1179,18 @@ bool CSpecialTxProcessor::ProcessSpecialTxsInBlock(Chainstate& chainstate, const LogPrint(BCLog::BENCHMARK, " - m_mnhfman->ProcessBlock: %.2fms [%.2fs]\n", Ticks(nTime8 - nTime7), Ticks(nTimeMnehf)); - if (DeploymentActiveAfter(pindex, m_consensus_params, Consensus::DEPLOYMENT_V19) && bls::bls_legacy_scheme.load()) { + if (DeploymentActiveAfter(pindex, m_consensus_params, Consensus::DEPLOYMENT_V19) && bls::EffectiveLegacyScheme()) { // NOTE: The block next to the activation is the one that is using new rules. // V19 activated just activated, so we must switch to the new rules here. - bls::bls_legacy_scheme.store(false); - LogPrintf("CSpecialTxProcessor::%s -- bls_legacy_scheme=%d\n", __func__, bls::bls_legacy_scheme.load()); + bls::SetEffectiveLegacyScheme(false); + LogPrintf("CSpecialTxProcessor::%s -- bls_legacy_scheme=%d\n", __func__, bls::EffectiveLegacyScheme()); } } catch (const EvoDbInconsistencyError& e) { // Local EvoDB corruption detected below (the node is already // aborting): fail with M_ERROR so the block is not marked invalid. return state.Error(e.what()); + } catch (const evo::LocalStateError&) { + throw; } catch (const std::exception& e) { LogPrintf("CSpecialTxProcessor::%s -- FAILURE! %s\n", __func__, e.what()); return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "failed-procspectxsinblock"); @@ -1110,18 +1199,22 @@ bool CSpecialTxProcessor::ProcessSpecialTxsInBlock(Chainstate& chainstate, const return true; } -bool CSpecialTxProcessor::UndoSpecialTxsInBlock(const Chainstate& chainstate, const CBlock& block, const CBlockIndex* pindex, MNListUpdates& updatesRet) +bool CSpecialTxProcessor::UndoSpecialTxsInBlock(const Chainstate& chainstate, const CBlock& block, + const CBlockIndex* pindex, MNListUpdates& updatesRet) { AssertLockHeld(::cs_main); + if (!IsEvoViewBound(chainstate)) { + throw EvoViewBindingError(strprintf("%s: not bound to the chainstate's Evo view", __func__)); + } - auto bls_legacy_scheme = bls::bls_legacy_scheme.load(); + const bool bls_legacy_scheme{bls::EffectiveLegacyScheme()}; try { if (!DeploymentActiveAt(*pindex, m_consensus_params, Consensus::DEPLOYMENT_V19) && !bls_legacy_scheme) { // NOTE: The block next to the activation is the one that is using new rules. // Removing the activation block here, so we must switch back to the old rules. - bls::bls_legacy_scheme.store(true); - LogPrintf("CSpecialTxProcessor::%s -- bls_legacy_scheme=%d\n", __func__, bls::bls_legacy_scheme.load()); + bls::SetEffectiveLegacyScheme(true); + LogPrintf("CSpecialTxProcessor::%s -- bls_legacy_scheme=%d\n", __func__, bls::EffectiveLegacyScheme()); } if (!m_mnhfman->UndoBlock(block, pindex)) { @@ -1135,9 +1228,12 @@ bool CSpecialTxProcessor::UndoSpecialTxsInBlock(const Chainstate& chainstate, co if (!m_qblockman.UndoBlock(chainstate, block, pindex)) { return false; } + } catch (const evo::LocalStateError&) { + bls::SetEffectiveLegacyScheme(bls_legacy_scheme); + throw; } 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()); + bls::SetEffectiveLegacyScheme(bls_legacy_scheme); + LogPrintf("CSpecialTxProcessor::%s -- bls_legacy_scheme=%d\n", __func__, bls::EffectiveLegacyScheme()); LogError("CSpecialTxProcessor::%s -- FAILURE! %s\n", __func__, e.what()); return false; } @@ -1170,6 +1266,8 @@ bool CSpecialTxProcessor::CheckCreditPoolDiffForBlock(const CBlock& block, const // Local EvoDB corruption detected below (the node is already // aborting): fail with M_ERROR so the block is not marked invalid. return state.Error(e.what()); + } catch (const evo::LocalStateError&) { + throw; } catch (const std::exception& e) { LogPrintf("CSpecialTxProcessor::%s -- FAILURE! %s\n", __func__, e.what()); return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "failed-checkcreditpooldiff"); diff --git a/src/evo/specialtxman.h b/src/evo/specialtxman.h index f9caf98ef4c7..8a0fc01ec677 100644 --- a/src/evo/specialtxman.h +++ b/src/evo/specialtxman.h @@ -5,6 +5,7 @@ #ifndef BITCOIN_EVO_SPECIALTXMAN_H #define BITCOIN_EVO_SPECIALTXMAN_H +#include #include #include #include @@ -32,8 +33,13 @@ class Chainstate; class CMNHFManager; class TxValidationState; class uint256; +struct EvoViewToken; struct MNListUpdates; +//! Whether the calling thread is bound to `chainstate`'s Evo view (defined in +//! validation.cpp, so callers need only this declaration). +bool IsEvoViewBound(const Chainstate& chainstate); + namespace chainlock { class Chainlocks; } @@ -57,6 +63,21 @@ struct SpecialTxRules { bool ipv4_optional{false}; }; +/** The ChainLock a block's coinbase certifies, as far as this node can tell. */ +struct CoinbaseChainlock { + enum class Status { + //! The coinbase carries a non-null ChainLock. + CERTIFIED, + //! The coinbase is known to carry none (or there is no CbTx ChainLock yet). + NONE, + //! This node cannot tell: the block's data is not available to its view. + UNAVAILABLE, + }; + Status status{Status::NONE}; + CBLSSignature signature{}; + uint32_t height_diff{0}; +}; + class CSpecialTxProcessor { public: @@ -66,6 +87,7 @@ class CSpecialTxProcessor const std::unique_ptr m_mnhfman; private: + CEvoDB& m_evodb; CDeterministicMNManager& m_dmnman; llmq::CQuorumBlockProcessor& m_qblockman; llmq::CQuorumSnapshotManager& m_qsnapman; @@ -91,8 +113,8 @@ class CSpecialTxProcessor CAmount blockSubsidy, bool fJustCheck, bool fCheckCbTxMerkleRoots, BlockValidationState& state, MNListUpdates& updatesRet) EXCLUSIVE_LOCKS_REQUIRED(::cs_main); - bool UndoSpecialTxsInBlock(const Chainstate& chainstate, const CBlock& block, const CBlockIndex* pindex, MNListUpdates& updatesRet) - EXCLUSIVE_LOCKS_REQUIRED(::cs_main); + bool UndoSpecialTxsInBlock(const Chainstate& chainstate, const CBlock& block, const CBlockIndex* pindex, + MNListUpdates& updatesRet) EXCLUSIVE_LOCKS_REQUIRED(::cs_main); // the returned list will not contain the correct block hash (we can't know it yet as the coinbase TX is not updated yet) @@ -110,6 +132,20 @@ class CSpecialTxProcessor /** Return a canonical hash of the deterministic MN list derived at a block. */ uint256 GetDeterministicMNListHash(gsl::not_null pindex) const; + /** The ChainLock certified by `pindex`'s coinbase. At a snapshot base, in + * the snapshot view, it comes from the carried coinbase. Without the + * block's data the snapshot view answers UNAVAILABLE; NORMAL keeps the + * legacy answer NONE. A block this node has but cannot read raises + * BlockDataReadError. */ + CoinbaseChainlock GetCoinbaseChainlock(const CBlockIndex* pindex) const; + /** Seed the header-proven coinbase of a snapshot base into `view`, inside + * the caller's open EvoDB transaction for that view. */ + bool SeedBaseCoinbase(const EvoViewToken& view, const CBlockIndex* base, const CTransaction& coinbase); + + const Consensus::Params& ConsensusParams() const { return m_consensus_params; } + CDeterministicMNManager& DeterministicMNManager() { return m_dmnman; } + llmq::CQuorumBlockProcessor& QuorumBlockProcessor() { return m_qblockman; } + llmq::CQuorumSnapshotManager& QuorumSnapshotManager() { return m_qsnapman; } /** Passthrough to CDeterministicMNManager::UpdatedBlockTip */ void UpdatedMNListTip(gsl::not_null pindex); @@ -136,6 +172,10 @@ class CSpecialTxProcessor CAmount blockSubsidy, BlockValidationState& state) EXCLUSIVE_LOCKS_REQUIRED(::cs_main); }; +/** Canonical hash of a block's deterministic MN list. An unbound list (none is + * built before DIP3) hashes as the empty list bound to the block. */ +uint256 CanonicalDeterministicMNListHash(gsl::not_null pindex, const CDeterministicMNList& list); + /** * Builds the masternode list for a block one transaction at a time, with the same result as * CSpecialTxProcessor::RebuildListFromBlock() over the same transactions. Block assembly uses it to @@ -181,8 +221,8 @@ std::optional GetValidatedPayload(const CTransaction& tx, gsl::not_null pindexPrev, CDeterministicMNManager& dmnman, const CCoinsViewCache& view, const Consensus::Params& consensus_params, SpecialTxRules rules, diff --git a/src/evo/viewcache.h b/src/evo/viewcache.h new file mode 100644 index 000000000000..e079a4404d04 --- /dev/null +++ b/src/evo/viewcache.h @@ -0,0 +1,97 @@ +// Copyright (c) 2026 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_EVO_VIEWCACHE_H +#define BITCOIN_EVO_VIEWCACHE_H + +#include +#include +#include + +#include +#include +#include +#include +#include + +/** A cache of Evo-derived state, partitioned by Evo view so that state derived + * under one view is never served to another. A retired snapshot generation's + * partitions are dropped at the next lookup, and every partition when a + * generation is promoted into NORMAL. Callers resolve the view + * once per operation and use the returned partition throughout it. Not + * thread-safe: guard it with the owner's lock. + * + * For() may drop every partition, so a reference it returned is valid only + * until the next For() on the same cache: never hold one across another + * For() call, and hand out shared_ptr partitions when a reference must + * outlive the owner's lock. `make` may run under CEvoDB::cs, so it must not + * call into the CEvoDB. */ +template +class EvoViewCache +{ +public: + explicit EvoViewCache(std::function make = [] { return T{}; }) : + m_make{std::move(make)} + { + } + + //! Restrict the cache to views of `owner`. + void SetOwner(const CEvoDB& owner) { m_owner = &owner; } + + //! The partition of `view`, which must be a live view of the owner's + //! CEvoDB: a retired snapshot generation or another CEvoDB's view raises + //! EvoViewBindingError before any partition is touched. + T& For(const EvoViewToken& view) + { + if (view.db == nullptr || (m_owner != nullptr && view.db != m_owner)) { + throw EvoViewBindingError("Evo view cache: view of another EvoDB"); + } + // The partitions are checked against the epoch, and the partition of + // `view` found, while its generation is held live; dropped partitions + // are destroyed after CEvoDB::cs is released. + std::map dropped; + T* part{nullptr}; + const bool live{view.db->WithLiveView(view, [&](uint64_t epoch, uint32_t live_generation) { + if (epoch > m_epoch) { + dropped.swap(m_parts); + m_epoch = epoch; + } + for (auto it{m_parts.begin()}; it != m_parts.end();) { + const auto& [db, identity, generation] = it->first; + if (db == view.db && identity == EvoDbIdentity::SNAPSHOT && generation != live_generation) { + dropped.insert(m_parts.extract(it++)); + } else { + ++it; + } + } + const Key key{view.db, view.identity, view.generation}; + auto it{m_parts.find(key)}; + if (it == m_parts.end()) it = m_parts.emplace(key, m_make()).first; + part = &it->second; + })}; + if (!live) { + throw EvoViewBindingError(strprintf("Evo view cache: snapshot generation %u is retired", view.generation)); + } + return *part; + } + + template + void ForEach(F&& f) + { + for (auto& [_, part] : m_parts) + f(part); + } + + void Clear() { m_parts.clear(); } + +private: + using Key = std::tuple; + + std::function m_make; + const CEvoDB* m_owner{nullptr}; + std::map m_parts; + uint64_t m_epoch{CEvoDB::ViewCacheEpoch()}; +}; + +#endif // BITCOIN_EVO_VIEWCACHE_H diff --git a/src/governance/governance.cpp b/src/governance/governance.cpp index c9559ec039c3..50fe63df31f0 100644 --- a/src/governance/governance.cpp +++ b/src/governance/governance.cpp @@ -882,30 +882,32 @@ void CGovernanceManager::CheckPostponedObjects() AssertLockNotHeld(cs_relay); if (!m_mn_sync.IsSynced()) return; - // Check postponed proposals - for (auto it = mapPostponedObjects.begin(); it != mapPostponedObjects.end();) { - const uint256& nHash = it->first; - auto& govobj = *Assert(it->second); - - assert(govobj.GetObjectType() != GovernanceObject::TRIGGER); - - std::string strError; - bool fMissingConfirmations; - if (govobj.IsCollateralValid(m_collateral, m_chainman, strError, fMissingConfirmations)) { - if (govobj.IsValidLocally(m_dmnman.GetListAtChainTip(), m_chainman, /*collateral=*/nullptr, strError, false)) { - AddGovernanceObjectInternal(govobj, ""); - } else { - LogPrint(BCLog::GOBJECT, "CGovernanceManager::CheckPostponedObjects -- %s invalid\n", nHash.ToString()); + // Check postponed proposals, unless their collateral can't be looked up yet: a failed lookup drops them + if (!IsCollateralSourceBehind()) { + for (auto it = mapPostponedObjects.begin(); it != mapPostponedObjects.end();) { + const uint256& nHash = it->first; + auto& govobj = *Assert(it->second); + + assert(govobj.GetObjectType() != GovernanceObject::TRIGGER); + + std::string strError; + bool fMissingConfirmations; + if (govobj.IsCollateralValid(m_collateral, m_chainman, strError, fMissingConfirmations)) { + if (govobj.IsValidLocally(m_dmnman.GetListAtChainTip(), m_chainman, /*collateral=*/nullptr, strError, false)) { + AddGovernanceObjectInternal(govobj, ""); + } else { + LogPrint(BCLog::GOBJECT, "CGovernanceManager::CheckPostponedObjects -- %s invalid\n", nHash.ToString()); + } + + } else if (fMissingConfirmations) { + // wait for more confirmations + ++it; + continue; } - } else if (fMissingConfirmations) { - // wait for more confirmations - ++it; - continue; + // remove processed or invalid object from the queue + mapPostponedObjects.erase(it++); } - - // remove processed or invalid object from the queue - mapPostponedObjects.erase(it++); } diff --git a/src/governance/net_governance.cpp b/src/governance/net_governance.cpp index 4fd4d898468a..dec95a38182c 100644 --- a/src/governance/net_governance.cpp +++ b/src/governance/net_governance.cpp @@ -181,23 +181,23 @@ void NetGovernance::ProcessMessage(CNode& peer, const std::string& msg_type, CDa LogPrint(BCLog::GOBJECT, "MNGOVERNANCEOBJECT -- Received object: %s\n", strHash); - // Only accept an object if this peer announced it or we requested it from this peer. The - // net-layer per-peer request tracker is the authorization source (already bounded), so no - // separate governance-side request cache is needed. Consume only after the sync gate, so a - // message dropped while not synced does not burn the authorization for a later retransmit. - const bool announced_or_requested = WITH_LOCK( - ::cs_main, return m_peer_manager->PeerConsumeObjectRequest(peer.GetId(), CInv{MSG_GOVERNANCE_OBJECT, nHash})); - if (!announced_or_requested) { - LogPrint(BCLog::GOBJECT, "MNGOVERNANCEOBJECT -- Received unrequested object: %s\n", strHash); - return; - } - - if (WITH_LOCK(::cs_main, return m_gov_manager.ProcessObject(peer.GetLogString(), nHash, govobj))) { - WITH_LOCK(::cs_main, m_peer_manager->PeerForgetObjectRequest(CInv{MSG_GOVERNANCE_OBJECT, nHash})); - } else { - // apply node's ban score - m_peer_manager->PeerMisbehaving(peer.GetId(), 20); + // Accept only an object this peer announced or we requested from it, and consume that authorization only + // after the readiness gates, so an object they drop can still be accepted when it is sent again. + const CInv inv{MSG_GOVERNANCE_OBJECT, nHash}; + bool valid; + { + LOCK(::cs_main); + // Snapshot activation can leave collateral impossible to look up since the check above + // cppcheck-suppress knownConditionTrueFalse + if (m_gov_manager.IsCollateralSourceBehind()) return; + if (!m_peer_manager->PeerConsumeObjectRequest(peer.GetId(), inv)) { + LogPrint(BCLog::GOBJECT, "MNGOVERNANCEOBJECT -- Received unrequested object: %s\n", strHash); + return; + } + valid = m_gov_manager.ProcessObject(peer.GetLogString(), nHash, govobj); + if (valid) m_peer_manager->PeerForgetObjectRequest(inv); } + if (!valid) m_peer_manager->PeerMisbehaving(peer.GetId(), 20); } // A NEW GOVERNANCE OBJECT VOTE HAS ARRIVED diff --git a/src/index/base.cpp b/src/index/base.cpp index 7c9b9056b2ce..eea382039df9 100644 --- a/src/index/base.cpp +++ b/src/index/base.cpp @@ -101,10 +101,13 @@ BaseIndex::~BaseIndex() bool BaseIndex::Init() { + AssertLockNotHeld(cs_main); + const auto locator{GetDB().ReadBestBlock()}; LOCK(cs_main); - CChain& active_chain = m_chainstate->m_chain; + CChain& index_chain = m_chainstate->m_chain; + const CBlockIndex* locator_index{nullptr}; if (!locator.IsNull()) { // Setting the best block to the locator's top block. If it is not part of the @@ -132,24 +135,24 @@ bool BaseIndex::Init() // Skip pruning check if indexes are not ready to sync (because reindex-chainstate has wiped the chain). const CBlockIndex* start_block = m_best_block_index.load(); - bool synced = start_block == active_chain.Tip(); + bool synced = start_block == index_chain.Tip(); if (!synced && g_indexes_ready_to_sync) { bool prune_violation = false; if (!start_block) { // index is not built yet // make sure we have all block data back to the genesis - prune_violation = m_chainstate->m_blockman.GetFirstStoredBlock(*active_chain.Tip()) != active_chain.Genesis(); + prune_violation = m_chainstate->m_blockman.GetFirstStoredBlock(*index_chain.Tip()) != index_chain.Genesis(); } // in case the index has a best block set and is not fully synced // check if we have the required blocks to continue building the index else { const CBlockIndex* block_to_test = start_block; - if (!active_chain.Contains(block_to_test)) { + if (!index_chain.Contains(block_to_test)) { // if the bestblock is not part of the mainchain, find the fork // and make sure we have all data down to the fork - block_to_test = active_chain.FindFork(block_to_test); + block_to_test = index_chain.FindFork(block_to_test); } - const CBlockIndex* block = active_chain.Tip(); + const CBlockIndex* block = index_chain.Tip(); prune_violation = true; // check backwards from the tip if we have all block data until we reach the indexes bestblock while (block_to_test && block && (block->nStatus & BLOCK_HAVE_DATA)) { @@ -216,6 +219,8 @@ void BaseIndex::ThreadSync() auto last_locator_write_time{last_log_time}; while (true) { if (m_interrupt) { + LogPrintf("%s: m_interrupt set; exiting ThreadSync\n", GetName()); + SetBestBlockIndex(pindex); // No need to handle errors in Commit. If it fails, the error will be already be // logged. The best way to recover is to continue, as index cannot be corrupted by @@ -375,8 +380,19 @@ bool BaseIndex::Rewind(const CBlockIndex* current_tip, const CBlockIndex* new_ti return true; } -void BaseIndex::BlockConnected(const std::shared_ptr& block, const CBlockIndex* pindex) +void BaseIndex::BlockConnected(ChainstateRole role, const std::shared_ptr& block, const CBlockIndex* pindex) { + // Ignore events from the assumed-valid chain; we will process its blocks + // (sequentially) after it is fully verified by the background chainstate. This + // is to avoid any out-of-order indexing. + // + // TODO at some point we could parameterize whether a particular index can be + // built out of order, but for now just do the conservative simple thing. + if (role == ChainstateRole::ASSUMEDVALID) { + return; + } + + // Ignore BlockConnected signals until we have fully indexed the chain. if (!m_synced) { return; } @@ -445,8 +461,14 @@ void BaseIndex::BlockDisconnected(const std::shared_ptr& block, co } } -void BaseIndex::ChainStateFlushed(const CBlockLocator& locator) +void BaseIndex::ChainStateFlushed(ChainstateRole role, const CBlockLocator& locator) { + // Ignore events from the assumed-valid chain; we will process its blocks + // (sequentially) after it is fully verified by the background chainstate. + if (role == ChainstateRole::ASSUMEDVALID) { + return; + } + if (!m_synced) { return; } @@ -515,9 +537,13 @@ void BaseIndex::Interrupt() bool BaseIndex::Start() { + // May need reset if index is being restarted. + m_interrupt.reset(); + // m_chainstate member gives indexing code access to node internals. It is // removed in followup https://github.com/bitcoin/bitcoin/pull/24230 - m_chainstate = &m_chain->context()->chainman->ActiveChainstate(); + m_chainstate = WITH_LOCK(::cs_main, + return &m_chain->context()->chainman->GetChainstateForIndexing()); // Need to register this ValidationInterface before running Init(), so that // callbacks are not missed if Init sets m_synced to true. RegisterValidationInterface(this); diff --git a/src/index/base.h b/src/index/base.h index d59e535f653e..91e2cf8f98c4 100644 --- a/src/index/base.h +++ b/src/index/base.h @@ -19,6 +19,7 @@ class CBlock; class CBlockIndex; class Chainstate; +class ChainstateManager; namespace node { class BlockManager; } // namespace node @@ -36,6 +37,11 @@ struct IndexSummary { * Base class for indices of blockchain data. This implements * CValidationInterface and ensures blocks are indexed sequentially according * to their position in the active chain. + * + * In the presence of multiple chainstates (i.e. if a UTXO snapshot is loaded), + * only the background "IBD" chainstate will be indexed to avoid building the + * index out of order. When the background chainstate completes validation, the + * index will be reinitialized and indexing will continue. */ class BaseIndex : public CValidationInterface { @@ -115,11 +121,11 @@ class BaseIndex : public CValidationInterface Chainstate* m_chainstate{nullptr}; const std::string m_name; - void BlockConnected(const std::shared_ptr& block, const CBlockIndex* pindex) override; + void BlockConnected(ChainstateRole role, const std::shared_ptr& block, const CBlockIndex* pindex) override; void BlockDisconnected(const std::shared_ptr& block, const CBlockIndex* pindex) override; - void ChainStateFlushed(const CBlockLocator& locator) override; + void ChainStateFlushed(ChainstateRole role, const CBlockLocator& locator) override; /// Initialize internal state from the database and block index. [[nodiscard]] virtual bool CustomInit(const std::optional& block) { return true; } @@ -153,9 +159,6 @@ class BaseIndex : public CValidationInterface virtual DB& GetDB() const = 0; - /// Get the name of the index for display in logs. - const std::string& GetName() const LIFETIMEBOUND { return m_name; } - /// Trigger a fatal index error and initiate shutdown. static void FatalErrorImpl(const std::string& message); @@ -173,6 +176,9 @@ class BaseIndex : public CValidationInterface /// Destructor interrupts sync thread if running and blocks until it exits. virtual ~BaseIndex(); + /// Get the name of the index for display in logs. + const std::string& GetName() const LIFETIMEBOUND { return m_name; } + /// Blocks the current thread until the index is caught up to the current /// state of the block chain. This only blocks if the index has gotten in /// sync once and only needs to process blocks in the ValidationInterface diff --git a/src/index/govcollateralindex.cpp b/src/index/govcollateralindex.cpp index 068101cc4b5a..8dc4b5e903d6 100644 --- a/src/index/govcollateralindex.cpp +++ b/src/index/govcollateralindex.cpp @@ -80,11 +80,13 @@ GovCollateralIndex::~GovCollateralIndex() = default; bool GovCollateralIndex::CustomInit(const std::optional& block) { + AssertLockHeld(::cs_main); // BaseIndex only locks pruning once a new index has read its first block, so pruning at startup could // still delete the blocks it is about to read. if (!block) { - WITH_LOCK(::cs_main, m_chainstate->m_blockman.UpdatePruneLock(GetName(), node::PruneLockInfo{/*height_first=*/0})); + m_chainstate->m_blockman.UpdatePruneLock(GetName(), node::PruneLockInfo{/*height_first=*/0}); } + m_indexed_chainstate = m_chainstate; return true; } @@ -144,7 +146,13 @@ bool GovCollateralIndex::CanSync(Chainstate& chainstate) const return true; } -bool GovCollateralIndex::IsCollateralSynced() const { return GetSummary().synced; } +bool GovCollateralIndex::IsCollateralSynced() const +{ + // While a snapshot is validated in the background, the index follows the background chainstate. + LOCK(::cs_main); + return m_indexed_chainstate && m_indexed_chainstate == &m_indexed_chainstate->m_chainman.ActiveChainstate() && + GetSummary().synced; +} std::optional GovCollateralIndex::FindCollateral(const uint256& txid) const { diff --git a/src/index/govcollateralindex.h b/src/index/govcollateralindex.h index b39b37b56406..82d707867a3c 100644 --- a/src/index/govcollateralindex.h +++ b/src/index/govcollateralindex.h @@ -27,13 +27,14 @@ class GovCollateralIndex final : public BaseIndex, public governance::Collateral private: const std::unique_ptr m_db; + const Chainstate* m_indexed_chainstate GUARDED_BY(::cs_main){nullptr}; bool AllowPrune() const override { return true; } protected: bool WritesEagerly() const override { return true; } - bool CustomInit(const std::optional& block) override; + bool CustomInit(const std::optional& block) override EXCLUSIVE_LOCKS_REQUIRED(::cs_main); bool CustomAppend(const interfaces::BlockInfo& block) override; diff --git a/src/init.cpp b/src/init.cpp index a77364d91b56..9bc1b2ef574d 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -67,6 +67,7 @@ #include #include #include +#include #include #include #include @@ -480,6 +481,7 @@ void PrepareShutdown(NodeContext& node) node.llmq_ctx.reset(); node.isman.reset(); node.dmnman.reset(); + node.evodb_for_dispatch = nullptr; node.evodb.reset(); } for (const auto& client : node.chain_clients) { @@ -657,8 +659,8 @@ void SetupServerArgs(ArgsManager& argsman) argsman.AddArg("-version", "Print version and exit", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS); argsman.AddArg("-addressindex", strprintf("Maintain a full address index, used to query for the balance, txids and unspent outputs for addresses (default: %u)", DEFAULT_ADDRESSINDEX), ArgsManager::ALLOW_ANY, OptionsCategory::INDEXING); - argsman.AddArg("-reindex", "Rebuild chain state and block index from the blk*.dat files on disk. This will also rebuild active optional indexes.", ArgsManager::ALLOW_ANY, OptionsCategory::INDEXING); - 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.", ArgsManager::ALLOW_ANY, OptionsCategory::INDEXING); + argsman.AddArg("-reindex", "If enabled, wipe chain state and block index, and rebuild them from blk*.dat files on disk. Also wipe and rebuild other optional indexes that are active. If an assumeutxo snapshot was loaded, its chainstate will be wiped as well. The snapshot can then be reloaded via RPC.", ArgsManager::ALLOW_ANY, OptionsCategory::INDEXING); + argsman.AddArg("-reindex-chainstate", "If enabled, wipe chain state, and rebuild it from blk*.dat files on disk. If an assumeutxo snapshot was loaded, its chainstate will be wiped as well. The snapshot can then be reloaded via RPC.", 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("-txindex", strprintf("Maintain a full transaction index, used by the getrawtransaction rpc call (default: %u)", DEFAULT_TXINDEX), ArgsManager::ALLOW_ANY, OptionsCategory::INDEXING); @@ -820,6 +822,7 @@ void SetupServerArgs(ArgsManager& argsman) #endif argsman.AddArg("-checkblockindex", strprintf("Do a consistency check for the block tree, and occasionally. (default: %u, regtest: %u)", defaultChainParams->DefaultConsistencyChecks(), regtestChainParams->DefaultConsistencyChecks()), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST); + argsman.AddArg("-checkevobinding", strprintf("Fail every Evo database access that is not bound to a chainstate's view, as dual-chainstate operation does (default: %u, regtest: %u)", defaultChainParams->DefaultConsistencyChecks(), regtestChainParams->DefaultConsistencyChecks()), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST); argsman.AddArg("-checkblocks=", strprintf("How many blocks to check at startup (default: %u, 0 = all)", DEFAULT_CHECKBLOCKS), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST); argsman.AddArg("-checklevel=", strprintf("How thorough the block verification of -checkblocks is: %s (0-4, default: %u)", Join(CHECKLEVEL_DOC, ", "), DEFAULT_CHECKLEVEL), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST); argsman.AddArg("-checkaddrman=", strprintf("Run addrman consistency checks every operations. Use 0 to disable. (default: %u)", DEFAULT_ADDRMAN_CONSISTENCY_CHECKS), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST); @@ -1205,6 +1208,11 @@ bool AppInitParameterInteraction(const ArgsManager& args) const CChainParams& chainparams = Params(); // ********************************************************* Step 2: parameter interactions + // Snapshot authorizations are reviewed chain parameters on every other network. + if (args.IsArgSet("-assumeutxodata") && chainparams.GetChainType() != ChainType::REGTEST) { + return InitError(_("-assumeutxodata is only available on regtest.")); + } + // also see: InitParameterInteraction() // Error if network-specific options (-addnode, -connect, etc) are @@ -1446,6 +1454,20 @@ bool AppInitParameterInteraction(const ArgsManager& args) return true; } +//! The indexes the node runs, in the order they were started. +static std::vector RunningIndexes(NodeContext& node) +{ + std::vector indexes; + if (g_txindex) indexes.push_back(g_txindex.get()); + if (node.gov_collateral_index) indexes.push_back(node.gov_collateral_index.get()); + if (node.address_index) indexes.push_back(node.address_index.get()); + if (node.timestamp_index) indexes.push_back(node.timestamp_index.get()); + if (node.spent_index) indexes.push_back(node.spent_index.get()); + ForEachBlockFilterIndex([&](BlockFilterIndex& index) { indexes.push_back(&index); }); + if (g_coin_stats_index) indexes.push_back(g_coin_stats_index.get()); + return indexes; +} + static bool LockDataDirectory(bool probeOnly) { // Make sure only a single Dash Core process is using the data directory. @@ -1492,6 +1514,136 @@ bool AppInitInterfaces(NodeContext& node) return true; } +void ReleaseChainstateLoadAttempt(NodeContext& node) +{ + // Validation events a failed attempt queued carry Evo view bindings to the + // databases released below, so they run first. + SyncWithValidationInterfaceQueue(); + node.mempool.reset(); + node.special_tx.reset(); + node.llmq_ctx.reset(); + node.isman.reset(); + node.dmnman.reset(); + node.evodb.reset(); +} + +void RunSchedulerTask(const NodeContext& node, const std::function& task) +{ + RunActiveEvoRound(node.evodb_for_dispatch.load(), "scheduler", task); +} + +void FinishImportedStartup(NodeContext& node, const ArgsManager& args, bool skip_evodb_repair_on_reindex) +{ + // The startup tail serves the active chain, like any other dispatch. + const ActiveEvoViewScope evo_view{node.evodb.get(), + [&]() EXCLUSIVE_LOCKS_REQUIRED(::cs_main) { return node.chainman->ActiveTip(); }}; + + { + const CBlockIndex* tip{evo_view.Tip()}; + const bool ibd = node.chainman->IsInitialBlockDownload(); + if (node.active_ctx) { + // On masternodes, defer the full broadcast until after + // nodeman->Init() so that GetProTxHash() is available + // for quorum connection setup and skShare derivation. + // Only kick CDSNotificationInterface here (cached block + // height for DS/MN payments/budgets). + node.ds_notification_interface->InitializeCurrentBlockTip(tip, ibd); + } else { + // Non-masternode nodes (including observer-only): broadcast + // to all subscribers now; no proTxHash dependency. + GetMainSignals().InitializeCurrentBlockTip(tip, ibd); + } + } + + // Seed InstantSend tip-height cache; NetInstantSend receives future + // updates via CValidationInterface but misses InitializeCurrentBlockTip. + // TODO: move cache updates from NetInstantSend to g_ds_notification due to specific of Tip's processing + node.isman->CacheTipHeight(WITH_LOCK(::cs_main, return node.chainman->ActiveChain().Tip())); + + { + // Get all UTXOs for each MN collateral in one go so that we can fill coin cache early + // and reduce further locking overhead for cs_main in other parts of code including GUI + LogPrintf("Filling coin cache with masternode UTXOs...\n"); + LOCK(cs_main); + const auto start{SteadyClock::now()}; + const auto mnList{node.dmnman->GetListAtChainTip()}; + mnList.ForEachMN(/*onlyValid=*/false, [&](const auto& dmn) { + Coin coin; + GetUTXOCoin(node.chainman->ActiveChainstate(), dmn.collateralOutpoint, coin); + }); + LogPrintf("Filling coin cache with masternode UTXOs: done in %dms\n", + Ticks(SteadyClock::now() - start)); + } + + if (skip_evodb_repair_on_reindex) { + LogPrintf("Skipping evodb repair during reindex\n"); + WITH_LOCK(::cs_main, node.dmnman->CompleteRepair()); // Mark as repaired since we're rebuilding fresh + } else if (node.dmnman->IsRepaired() && !args.GetBoolArg("-forceevodbrepair", false)) { + LogPrintf("Masternode list diffs are already repaired\n"); + } else { + const CBlockIndex* start_index; + const CBlockIndex* stop_index; + { + LOCK(cs_main); + const auto& consensus_params = Params().GetConsensus(); + // Repair works on NORMAL state, so walk the chain of the chainstate + // that owns it: the background one while a snapshot is in use. + const CChain* chain{&node.chainman->ActiveChain()}; + for (const Chainstate* chainstate : node.chainman->GetAll()) { + if (chainstate->EvoDbIdentity() == EvoDbIdentity::NORMAL) chain = &chainstate->m_chain; + } + start_index = (*chain)[consensus_params.DIP0003Height]; + stop_index = chain->Tip(); + } + + if (start_index && stop_index && start_index->nHeight < stop_index->nHeight) { + LogPrintf("Verifying and repairing masternode list diffs...\n"); + const auto start{SteadyClock::now()}; + // Create a callback that wraps CSpecialTxProcessor::BuildNewListFromBlock + auto build_list_func = [&node](const CBlock& block, const CBlockIndex* const pindexPrev, + const CDeterministicMNList& prevList, const CCoinsViewCache& view, + bool debugLogs, BlockValidationState& state, + CDeterministicMNList& mnListRet) -> bool { + const bool is_v24_active{DeploymentActiveAfter(pindexPrev, *node.chainman, Consensus::DEPLOYMENT_V24)}; + return node.special_tx->RebuildListFromBlock(block, pindexPrev, is_v24_active, prevList, view, + debugLogs, state, mnListRet); + }; + auto result = node.dmnman->RecalculateAndRepairDiffs(node.chainman->m_blockman, start_index, stop_index, + build_list_func, true); + + if (!result.verification_errors.empty()) { + LogPrintf("WARNING: Verification errors:\n%s\n", Join(result.verification_errors, "\n")); + } + + if (!result.repair_errors.empty()) { + // Critical errors occurred - reindex required + LogPrintf("Failed to repair masternode list diffs. Database corruption detected. " /* Continued */ + "Please restart with -reindex to rebuild the database.\n" + "Errors:\n%s\n", + Join(result.repair_errors, "\n")); + StartShutdown(); + return; + } + WITH_LOCK(::cs_main, node.dmnman->CompleteRepair()); + LogPrintf("Successfully repaired %d masternode list diffs, verified %d snapshots in %ds\n", + result.diffs_recalculated, result.snapshots_verified, + Ticks(SteadyClock::now() - start)); + } + } + + if (node.active_ctx) { + node.active_ctx->nodeman->Init(WITH_LOCK(::cs_main, return node.chainman->ActiveTip())); + // Now that nodeman->Init has set proTxHash, fan out the + // startup tip to all CValidationInterface subscribers. + // The earlier call only kicked CDSNotificationInterface + // directly because the full broadcast (NetQuorum, + // ActiveContext) depends on a valid proTxHash. + const CBlockIndex* tip = WITH_LOCK(::cs_main, return node.chainman->ActiveTip()); + const bool ibd = node.chainman->IsInitialBlockDownload(); + GetMainSignals().InitializeCurrentBlockTip(tip, ibd); + } +} + bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) { const ArgsManager& args = *Assert(node.args); @@ -1551,6 +1703,9 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) assert(!node.scheduler); node.scheduler = std::make_unique(); + // Each scheduled round serves the active chain unless the task carries + // its own Evo view binding. + node.scheduler->SetTaskWrapper([&node](const CScheduler::Function& task) { RunSchedulerTask(node, task); }); // Start the lightweight task scheduler thread node.scheduler->m_service_thread = std::thread(util::TraceThread, "scheduler", [&] { node.scheduler->serviceQueue(); }); @@ -1974,6 +2129,25 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) // ********************************************************* Step 7b: load block chain + // A snapshot chainstate, validated or not, stays in use until a restart + // promotes it, and neither chainstate can run with these options. + if (node::FindSnapshotChainstateDir(args.GetDataDirNet())) { + if (args.GetIntArg("-prune", 0)) { + return InitError(_("An assumeutxo snapshot chainstate is in use, which pruning would break. Restart without " + "-prune until the snapshot has been validated and a restart has promoted it.")); + } + if (node::LegacyIndexesEnabled(args)) { + return InitError(_("An assumeutxo snapshot chainstate is in use, which -addressindex, -spentindex and " + "-timestampindex do not support. Restart without them until the snapshot has been " + "validated and a restart has promoted it.")); + } + if (args.IsArgSet("-masternodeblsprivkey")) { + return InitError(_("An assumeutxo snapshot chainstate is in use, which a masternode cannot run on. Restart " + "without -masternodeblsprivkey until the snapshot has been validated and a restart has " + "promoted it.")); + } + } + node.notifications = std::make_unique(); fReindex = args.GetBoolArg("-reindex", false); bool fReindexChainState = args.GetBoolArg("-reindex-chainstate", false); @@ -2038,12 +2212,7 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) for (bool fLoaded = false; !fLoaded && !ShutdownRequested();) { // On a retry iteration the previous instances still hold the on-disk // database locks, so release them before opening the databases again. - node.mempool.reset(); - node.special_tx.reset(); - node.llmq_ctx.reset(); - node.isman.reset(); - node.dmnman.reset(); - node.evodb.reset(); + ReleaseChainstateLoadAttempt(node); auto catch_exceptions = [](auto&& f) { try { return f(); @@ -2053,7 +2222,8 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) } }; auto [status, error] = catch_exceptions([&]() -> node::ChainstateLoadResult { - node.evodb = std::make_unique(util::DbWrapperParams{.path = args.GetDataDirNet(), .memory = false, .wipe = node::fReindex || fReindexChainState, .options = dash_db_options}); + node.evodb = std::make_unique(util::DbWrapperParams{.path = args.GetDataDirNet(), .memory = false, .wipe = node::fReindex || fReindexChainState, .options = dash_db_options}, + args.GetBoolArg("-checkevobinding", chainparams.DefaultConsistencyChecks())); node.dmnman = std::make_unique(*node.evodb, *node.mn_metaman); node.isman = std::make_unique(*node.sporkman, util::DbWrapperParams{.path = args.GetDataDirNet(), .memory = false, .wipe = node::fReindex || fReindexChainState, .options = dash_db_options}); @@ -2066,6 +2236,25 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) node.chainman = std::make_unique(chainman_opts, blockman_opts); ChainstateManager& chainman = *node.chainman; + // This is defined and set here instead of inline in validation.h to avoid a hard + // dependency between validation and index/base, since the latter is not in + // libbitcoinkernel. + chainman.restart_indexes = [&node]() { + LogPrintf("[snapshot] restarting indexes\n"); + + // Drain the validation interface queue to ensure that the old indexes + // don't have any pending work. + SyncWithValidationInterfaceQueue(); + + for (auto* index : RunningIndexes(node)) { + index->Interrupt(); + index->Stop(); + if (!index->Start()) { + LogPrintf("[snapshot] WARNING failed to restart index %s on snapshot chain\n", index->GetName()); + } + } + }; + node::ChainstateLoadOptions options; options.chainlocks = Assert(node.chainlocks.get()); options.data_dir = args.GetDataDirNet(); @@ -2153,6 +2342,9 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) } } + // No load retry replaces the Evo database from here on. + node.evodb_for_dispatch = node.evodb.get(); + // As LoadBlockIndex can take several minutes, it's possible the user // requested to kill the GUI during the last operation. If so, exit. // As the program has not fully started yet, Shutdown() is possibly overkill. @@ -2174,7 +2366,7 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) // The index can't catch up once the node has pruned blocks it hasn't read, e.g. after upgrading a pruned // node, because a new index has to start from the genesis block. Starting it would fail with an init // error, so leave governance off instead. - if (WITH_LOCK(::cs_main, return gov_collateral_index->CanSync(chainman.ActiveChainstate()))) { + if (WITH_LOCK(::cs_main, return gov_collateral_index->CanSync(chainman.GetChainstateForIndexing()))) { node.gov_collateral_index = std::move(gov_collateral_index); } else if (is_governance_enabled) { is_governance_enabled = false; @@ -2501,109 +2693,24 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) chainman.m_load_block = std::thread(&util::TraceThread, "loadblk", [=, &args, &chainman, &node] { // ThreadImport can switch fReindex from true to false, fetch its original state here to use later bool skip_evodb_repair_on_reindex = fReindex || fReindexChainState; - ThreadImport(chainman, vImportFiles); - - // Load mempool from disk - if (auto* pool{chainman.ActiveChainstate().GetMempool()}) { - LoadMempool(*pool, ShouldPersistMempool(args) ? MempoolPath(args) : fs::path{}, chainman.ActiveChainstate(), {}); - pool->SetLoadTried(!ShutdownRequested()); - } - - { - const CBlockIndex* tip = WITH_LOCK(::cs_main, return chainman.ActiveTip()); - const bool ibd = chainman.IsInitialBlockDownload(); - if (node.active_ctx) { - // On masternodes, defer the full broadcast until after - // nodeman->Init() so that GetProTxHash() is available - // for quorum connection setup and skShare derivation. - // Only kick CDSNotificationInterface here (cached block - // height for DS/MN payments/budgets). - node.ds_notification_interface->InitializeCurrentBlockTip(tip, ibd); - } else { - // Non-masternode nodes (including observer-only): broadcast - // to all subscribers now; no proTxHash dependency. - GetMainSignals().InitializeCurrentBlockTip(tip, ibd); - } - } - - // Seed InstantSend tip-height cache; NetInstantSend receives future - // updates via CValidationInterface but misses InitializeCurrentBlockTip. - // TODO: move cache updates from NetInstantSend to g_ds_notification due to specific of Tip's processing - node.isman->CacheTipHeight(WITH_LOCK(::cs_main, return chainman.ActiveChain().Tip())); - - { - // Get all UTXOs for each MN collateral in one go so that we can fill coin cache early - // and reduce further locking overhead for cs_main in other parts of code including GUI - LogPrintf("Filling coin cache with masternode UTXOs...\n"); - LOCK(cs_main); - const auto start{SteadyClock::now()}; - const auto mnList{node.dmnman->GetListAtChainTip()}; - mnList.ForEachMN(/*onlyValid=*/false, [&](const auto& dmn) { - Coin coin; - GetUTXOCoin(chainman.ActiveChainstate(), dmn.collateralOutpoint, coin); - }); - LogPrintf("Filling coin cache with masternode UTXOs: done in %dms\n", Ticks(SteadyClock::now() - start)); - } - - if (skip_evodb_repair_on_reindex) { - LogPrintf("Skipping evodb repair during reindex\n"); - WITH_LOCK(::cs_main, node.dmnman->CompleteRepair()); // Mark as repaired since we're rebuilding fresh - } else if (node.dmnman->IsRepaired() && !args.GetBoolArg("-forceevodbrepair", false)) { - LogPrintf("Masternode list diffs are already repaired\n"); - } else { - const CBlockIndex* start_index; - const CBlockIndex* stop_index; + try { { - LOCK(cs_main); - const auto& consensus_params = Params().GetConsensus(); - start_index = chainman.ActiveChain()[consensus_params.DIP0003Height]; - stop_index = chainman.ActiveChain().Tip(); - } - - if (start_index && stop_index && start_index->nHeight < stop_index->nHeight) { - LogPrintf("Verifying and repairing masternode list diffs...\n"); - const auto start{SteadyClock::now()}; - // Create a callback that wraps CSpecialTxProcessor::BuildNewListFromBlock - auto build_list_func = - [&node, &chainman](const CBlock& block, const CBlockIndex* const pindexPrev, - const CDeterministicMNList& prevList, const CCoinsViewCache& view, bool debugLogs, - BlockValidationState& state, CDeterministicMNList& mnListRet) -> bool { - const bool is_v24_active{DeploymentActiveAfter(pindexPrev, chainman, Consensus::DEPLOYMENT_V24)}; - return node.special_tx->RebuildListFromBlock(block, pindexPrev, is_v24_active, prevList, - view, debugLogs, state, mnListRet); - }; - auto result = node.dmnman->RecalculateAndRepairDiffs(chainman.m_blockman, start_index, stop_index, build_list_func, true); - - if (!result.verification_errors.empty()) { - LogPrintf("WARNING: Verification errors:\n%s\n", Join(result.verification_errors, "\n")); - } - - if (!result.repair_errors.empty()) { - // Critical errors occurred - reindex required - LogPrintf("Failed to repair masternode list diffs. Database corruption detected. " /* Continued */ - "Please restart with -reindex to rebuild the database.\n" - "Errors:\n%s\n", - Join(result.repair_errors, "\n")); - StartShutdown(); - return; + const ActiveEvoViewScope evo_view{node.evodb.get()}; + ThreadImport(chainman, vImportFiles); + + // Load mempool from disk + Chainstate& chainstate{chainman.ActiveChainstate()}; + if (auto* pool{chainstate.GetMempool()}) { + LoadMempool(*pool, ShouldPersistMempool(args) ? MempoolPath(args) : fs::path{}, chainstate, {}); + // A load stopped because the mempool moved to another chainstate did not finish. + const bool load_stopped{WITH_LOCK(::cs_main, return chainstate.GetMempool() != pool)}; + pool->SetLoadTried(!ShutdownRequested() && !load_stopped); } - WITH_LOCK(::cs_main, node.dmnman->CompleteRepair()); - LogPrintf("Successfully repaired %d masternode list diffs, verified %d snapshots in %ds\n", - result.diffs_recalculated, result.snapshots_verified, - Ticks(SteadyClock::now() - start)); } - } - - if (node.active_ctx) { - node.active_ctx->nodeman->Init(WITH_LOCK(::cs_main, return chainman.ActiveTip())); - // Now that nodeman->Init has set proTxHash, fan out the - // startup tip to all CValidationInterface subscribers. - // The earlier call only kicked CDSNotificationInterface - // directly because the full broadcast (NetQuorum, - // ActiveContext) depends on a valid proTxHash. - const CBlockIndex* tip = WITH_LOCK(::cs_main, return chainman.ActiveTip()); - const bool ibd = chainman.IsInitialBlockDownload(); - GetMainSignals().InitializeCurrentBlockTip(tip, ibd); + FinishImportedStartup(node, args, skip_evodb_repair_on_reindex); + } catch (const evo::LocalStateError& e) { + LogPrintf("ERROR: Evo state unavailable during startup: %s\n", e.what()); + StartShutdown(); } }); #ifdef ENABLE_WALLET diff --git a/src/init.h b/src/init.h index b0c6feb26031..9a8a54d5e347 100644 --- a/src/init.h +++ b/src/init.h @@ -9,6 +9,7 @@ #include #include +#include #include #include @@ -69,6 +70,22 @@ bool AppInitInterfaces(node::NodeContext& node); * @pre Parameters should be parsed and config file should be read, AppInitLockDataDirectory should have been called. */ bool AppInitMain(node::NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info = nullptr); +/** + * Startup work that follows the initial block import: announce the tip, + * warm caches and repair EvoDB. Runs on the loadblk thread, bound to the + * active chainstate's Evo view. + */ +void FinishImportedStartup(node::NodeContext& node, const ArgsManager& args, bool skip_evodb_repair_on_reindex); +/** + * Release the databases and managers a chainstate load attempt opened, once + * the validation events it queued have run. + */ +void ReleaseChainstateLoadAttempt(node::NodeContext& node); +/** + * Run a node scheduler task as a round of active-chain work, bound to the + * active chainstate's Evo view once startup has published the Evo database. + */ +void RunSchedulerTask(const node::NodeContext& node, const std::function& task); void PrepareShutdown(node::NodeContext& node); /** diff --git a/src/instantsend/net_instantsend.cpp b/src/instantsend/net_instantsend.cpp index edbe314aadab..eedaf8d49d99 100644 --- a/src/instantsend/net_instantsend.cpp +++ b/src/instantsend/net_instantsend.cpp @@ -20,6 +20,7 @@ #include #include +#include #include #include @@ -59,6 +60,9 @@ static std::optional GetBlockHeight(llmq::CInstantSendManager& is_manager, struct NetInstantSend::BatchVerificationData { CBLSBatchVerifier batchVerifier{false, true, BATCH_VERIFIER_SOURCE_THRESHOLD}; Uint256HashMap recSigs; + //! Locks signed by a quorum this node has no record of: below its snapshot + //! base. They can be neither verified nor blamed on the peer. + Uint256HashSet unverifiable; size_t verifyCount{0}; size_t alreadyVerified{0}; }; @@ -142,10 +146,19 @@ std::unique_ptr NetInstantSend::BuildVeri if (cycleHeight + dkgInterval < tipHeight) { nSignHeight = cycleHeight + dkgInterval - 1; } - // For RegTest non-rotating quorum cycleHash has directly quorum hash - auto quorum = llmq_params.useRotation ? llmq::SelectQuorumForSigning(llmq_params, active_chain, m_qman, - id, nSignHeight, signOffset) - : m_qman.GetQuorum(llmq_params.type, islock->cycleHash); + llmq::CQuorumCPtr quorum; + try { + // For RegTest non-rotating quorum cycleHash has directly quorum hash + quorum = llmq_params.useRotation ? llmq::SelectQuorumForSigning(llmq_params, active_chain, m_qman, id, + nSignHeight, signOffset) + : m_qman.GetQuorum(llmq_params.type, islock->cycleHash); + } catch (const evo::LocalStateError& e) { + // Skip only this lock: the rest of the batch stays verifiable. + LogPrint(BCLog::INSTANTSEND, "NetInstantSend::%s -- txid=%s, islock=%s: cannot select quorum: %s\n", + __func__, islock->txid.ToString(), hash.ToString(), e.what()); + data->unverifiable.emplace(hash); + continue; + } if (!quorum) { // Different cycle hashes can select different quorums within the same batch. @@ -189,6 +202,8 @@ Uint256HashSet NetInstantSend::ApplyVerificationResults( auto nodeId = pending.node_id; const auto& islock = pending.islock; + if (data.unverifiable.count(hash)) continue; + const bool source_bad = data.batchVerifier.badSources.count(nodeId); const bool message_bad = data.batchVerifier.badMessages.count(hash); @@ -404,7 +419,7 @@ void NetInstantSend::ProcessInstantSendLock(NodeId from, const uint256& hash, co // We don't need the recovered sigs for the inputs anymore. This prevents unnecessary propagation of these sigs. // We only need the ISLOCK from now on to detect conflicts TruncateRecoveredSigsForInputs(*islock); - ResolveBlockConflicts(hash, *islock); + ResolveBlockConflicts(hash, islock); if (found_transaction) { RemoveMempoolConflictsForLock(hash, *islock); @@ -427,18 +442,20 @@ void NetInstantSend::ProcessInstantSendLock(NodeId from, const uint256& hash, co void NetInstantSend::WorkThreadMain() { while (!workInterrupt) { - bool fMoreWork = [&]() -> bool { - if (!m_is_manager.IsInstantSendEnabled()) return false; + bool fMoreWork{false}; + RunActiveEvoRound(m_chainman.EvoDb(), "isman", [&] { + if (!m_is_manager.IsInstantSendEnabled()) return; auto [more_work, locks] = m_is_manager.FetchPendingLocks(); if (!locks.empty()) { ProcessPendingISLocks(std::move(locks)); } + RetryUnresolvedConflicts(); if (m_signer) { m_signer->ProcessPendingRetryLockTxs(m_is_manager.PrepareTxToRetry()); } - return more_work; - }(); + fMoreWork = more_work; + }); if (!fMoreWork && !workInterrupt.sleep_for(WORK_THREAD_SLEEP_INTERVAL)) { return; @@ -514,6 +531,7 @@ void NetInstantSend::SynchronousUpdatedBlockTip(const CBlockIndex* pindexNew, co void NetInstantSend::UpdatedBlockTip(const CBlockIndex* pindexNew, const CBlockIndex* pindexFork, bool fInitialDownload) { + m_retry_conflicts = true; bool fDIP0008Active = pindexNew->pprev && pindexNew->pprev->nHeight >= Params().GetConsensus().DIP0008Height; if (m_chainlocks.IsEnabled() && fDIP0008Active) { @@ -535,8 +553,12 @@ void NetInstantSend::TransactionRemovedFromMempool(const CTransactionRef& tx, Me m_is_manager.TransactionIsRemoved(tx); } -void NetInstantSend::BlockConnected(const std::shared_ptr& pblock, const CBlockIndex* pindex) +void NetInstantSend::BlockConnected(ChainstateRole role, const std::shared_ptr& pblock, const CBlockIndex* pindex) { + // Only the active chainstate's blocks concern this subscriber. + if (role == ChainstateRole::BACKGROUND) { + return; + } if (!m_is_manager.IsInstantSendEnabled()) { return; } @@ -580,12 +602,13 @@ void NetInstantSend::BlockDisconnected(const std::shared_ptr& pblo void NetInstantSend::NotifyChainLock(const CBlockIndex* pindex, const std::shared_ptr& clsig) { + m_retry_conflicts = true; HandleFullyConfirmedBlock(pindex); } -void NetInstantSend::ResolveBlockConflicts(const uint256& islockHash, const instantsend::InstantSendLock& islock) +void NetInstantSend::ResolveBlockConflicts(const uint256& islockHash, const instantsend::InstantSendLockPtr& islock) { - auto conflicts = m_is_manager.RetrieveISConflicts(islockHash, islock); + auto conflicts = m_is_manager.RetrieveISConflicts(islockHash, *islock); // Lets see if any of the conflicts was already mined into a ChainLocked block bool hasChainLockedConflict = false; @@ -603,44 +626,105 @@ void NetInstantSend::ResolveBlockConflicts(const uint256& islockHash, const inst // consensus and its better to sacrifice individual ISLOCKs then to sacrifice whole ChainLocks. if (hasChainLockedConflict) { LogPrintf("NetInstantSend::%s -- txid=%s, islock=%s: at least one conflicted TX already got a ChainLock\n", - __func__, islock.txid.ToString(), islockHash.ToString()); - m_is_manager.RemoveConflictingLock(islockHash, islock); + __func__, islock->txid.ToString(), islockHash.ToString()); + m_is_manager.RemoveConflictingLock(islockHash, *islock); return; } - bool hasTxForLock = m_is_manager.HasTxForLock(islockHash); - - bool activateBestChain = false; + UnresolvedConflicts unresolved{.islock = islock, .has_tx_for_lock = m_is_manager.HasTxForLock(islockHash)}; for (const auto& p : conflicts) { - const auto* pindex = p.first; ClearConflicting(p.second); + unresolved.blocks.push_back(p.first->GetBlockHash()); + } + InvalidateConflictingBlocks(islockHash, std::move(unresolved)); +} - LogPrintf("NetInstantSend::%s -- invalidating block %s\n", __func__, pindex->GetBlockHash().ToString()); +namespace { +//! Clears, when it goes out of scope, the failure flags an invalidation set, +//! unless that invalidation is meant to last. +class ResetTemporaryInvalidation +{ +public: + ResetTemporaryInvalidation(const ChainstateManager& chainman, CBlockIndex* pindex, bool temporary) : + m_chainman{chainman}, + m_pindex{pindex}, + m_temporary{temporary} + { + } + ResetTemporaryInvalidation(const ResetTemporaryInvalidation&) = delete; + ResetTemporaryInvalidation& operator=(const ResetTemporaryInvalidation&) = delete; + ~ResetTemporaryInvalidation() + { + if (!m_temporary) return; + LogPrintf("NetInstantSend::InvalidateConflictingBlocks -- resetting block %s\n", + m_pindex->GetBlockHash().ToString()); + LOCK(::cs_main); + m_chainman.ActiveChainstate().ResetBlockFailureFlags(m_pindex); + } + +private: + const ChainstateManager& m_chainman; + CBlockIndex* const m_pindex{nullptr}; + const bool m_temporary{false}; +}; +} // namespace + +void NetInstantSend::InvalidateConflictingBlocks(const uint256& islockHash, UnresolvedConflicts conflicts) +{ + // A chainstate that stalled on its own Evo state cannot move away from a + // conflicting block yet. Keep what is left for a later attempt. + const auto defer{[&](const BlockValidationState& state) { + if (!IsLocalEvoStateError(state)) { + // This should not have happened and we are in a state were it's not safe to continue anymore + assert(false); + } + LogPrintf("NetInstantSend::InvalidateConflictingBlocks -- islock=%s: chainstate stalled, retrying later\n", + islockHash.ToString()); + m_unresolved_conflicts.insert_or_assign(islockHash, std::move(conflicts)); + }}; + + while (!conflicts.blocks.empty()) { + const uint256 block_hash{conflicts.blocks.back()}; + LogPrintf("NetInstantSend::%s -- invalidating block %s\n", __func__, block_hash.ToString()); BlockValidationState state; // need non-const pointer - auto pindex2 = WITH_LOCK(::cs_main, return m_chainman.ActiveChainstate().m_blockman.LookupBlockIndex(pindex->GetBlockHash())); + auto pindex2 = WITH_LOCK(::cs_main, return m_chainman.ActiveChainstate().m_blockman.LookupBlockIndex(block_hash)); + // Without the locked transaction the invalidation is only temporary: + // however InvalidateBlock ends, a stall partway through included, the + // blocks it marked invalid are valid again afterwards. + const ResetTemporaryInvalidation reset{m_chainman, pindex2, !conflicts.has_tx_for_lock}; if (!m_chainman.ActiveChainstate().InvalidateBlock(state, pindex2)) { LogPrintf("NetInstantSend::%s -- InvalidateBlock failed: %s\n", __func__, state.ToString()); - // This should not have happened and we are in a state were it's not safe to continue anymore - assert(false); - } - if (hasTxForLock) { - activateBestChain = true; - } else { - LogPrintf("NetInstantSend::%s -- resetting block %s\n", __func__, pindex2->GetBlockHash().ToString()); - LOCK(::cs_main); - m_chainman.ActiveChainstate().ResetBlockFailureFlags(pindex2); + return defer(state); } + if (conflicts.has_tx_for_lock) conflicts.activate_best_chain = true; + conflicts.blocks.pop_back(); } - if (activateBestChain) { + if (conflicts.activate_best_chain) { BlockValidationState state; if (!m_chainman.ActiveChainstate().ActivateBestChain(state)) { LogPrintf("NetInstantSend::%s -- ActivateBestChain failed: %s\n", __func__, state.ToString()); - // This should not have happened and we are in a state were it's not safe to continue anymore - assert(false); + return defer(state); + } + } +} + +void NetInstantSend::RetryUnresolvedConflicts() +{ + if (!m_retry_conflicts.exchange(false) || m_unresolved_conflicts.empty()) return; + auto unresolved{std::exchange(m_unresolved_conflicts, {})}; + for (auto& [islock_hash, conflicts] : unresolved) { + const bool chainlocked{std::ranges::any_of(conflicts.blocks, [&](const uint256& block_hash) { + const CBlockIndex* pindex{WITH_LOCK(::cs_main, return m_chainman.m_blockman.LookupBlockIndex(block_hash))}; + return pindex && m_chainlocks.HasChainLock(pindex->nHeight, block_hash); + })}; + if (chainlocked) { + m_is_manager.RemoveConflictingLock(islock_hash, *conflicts.islock); + continue; } + InvalidateConflictingBlocks(islock_hash, std::move(conflicts)); } } diff --git a/src/instantsend/net_instantsend.h b/src/instantsend/net_instantsend.h index 89db71809080..80ee2696d89e 100644 --- a/src/instantsend/net_instantsend.h +++ b/src/instantsend/net_instantsend.h @@ -6,9 +6,12 @@ #define BITCOIN_INSTANTSEND_NET_INSTANTSEND_H #include +#include #include #include +#include +#include #include #include #include @@ -74,12 +77,13 @@ class NetInstantSend final : public NetHandler, public CValidationInterface void TransactionAddedToMempool(const CTransactionRef&, int64_t, uint64_t mempool_sequence) override; void TransactionRemovedFromMempool(const CTransactionRef& ptx, MemPoolRemovalReason reason, uint64_t mempool_sequence) override; - void BlockConnected(const std::shared_ptr& pblock, const CBlockIndex* pindex) override; + void BlockConnected(ChainstateRole role, const std::shared_ptr& pblock, const CBlockIndex* pindex) override; void BlockDisconnected(const std::shared_ptr& pblock, const CBlockIndex* pindexDisconnected) override; void NotifyChainLock(const CBlockIndex* pindex, const std::shared_ptr& clsig) override; private: friend struct NetInstantSendTest; + friend struct NetInstantSendConflictsTest; struct BatchVerificationData; bool ValidateIncomingISLock(const instantsend::InstantSendLock& islock, NodeId node_id); @@ -102,7 +106,19 @@ class NetInstantSend final : public NetHandler, public CValidationInterface const Consensus::LLMQParams& llmq_params, int signOffset, bool ban, const std::vector& pend); - void ResolveBlockConflicts(const uint256& islockHash, const instantsend::InstantSendLock& islock); + //! Blocks with transactions that conflict with a lock, still to be + //! invalidated because the chainstate stalled on its Evo state. + struct UnresolvedConflicts { + instantsend::InstantSendLockPtr islock{}; + std::vector blocks{}; + bool has_tx_for_lock{false}; + bool activate_best_chain{false}; + }; + + void ResolveBlockConflicts(const uint256& islockHash, const instantsend::InstantSendLockPtr& islock); + void InvalidateConflictingBlocks(const uint256& islockHash, UnresolvedConflicts conflicts); + //! Retry the conflicts a stall left unresolved, once per tip or ChainLock change. + void RetryUnresolvedConflicts(); void TruncateRecoveredSigsForInputs(const instantsend::InstantSendLock& islock); @@ -118,6 +134,10 @@ class NetInstantSend final : public NetHandler, public CValidationInterface CTxMemPool& m_mempool; const CMasternodeSync& m_mn_sync; + //! Only the work thread resolves conflicts, so only it touches this map. + std::map m_unresolved_conflicts; + std::atomic m_retry_conflicts{false}; + std::thread workThread; CThreadInterrupt workInterrupt; }; diff --git a/src/interfaces/chain.h b/src/interfaces/chain.h index 47c1199deeb5..3823c83ff49d 100644 --- a/src/interfaces/chain.h +++ b/src/interfaces/chain.h @@ -30,6 +30,7 @@ class CBlockIndex; class Coin; class uint256; enum class MemPoolRemovalReason; +enum class ChainstateRole; struct bilingual_str; struct CBlockLocator; struct FeeCalculation; @@ -182,8 +183,10 @@ class Chain //! Check if block is chainlocked. virtual bool hasChainLock(int height, const uint256& hash) = 0; - //! Return list of MN Collateral from outputs - virtual std::vector listMNCollaterials(const std::vector>& outputs) = 0; + //! Return list of MN Collateral from outputs, or nullopt when the + //! masternode list is not available. + virtual std::optional> listMNCollaterials( + const std::vector>& outputs) = 0; //! Return the loader that registers wallets with the node's CoinJoin //! client manager, or nullptr when the node runs without one. @@ -281,8 +284,9 @@ class Chain virtual bool isNonStandardSpecialTx(const CTransactionRef& tx, std::string& reason) = 0; //! Whether v24 hard fork rules apply to the next block, which is what - //! enables version 2 asset lock transactions by consensus. - virtual bool isV24Active() = 0; + //! enables version 2 asset lock transactions by consensus. Nullopt when + //! this node cannot evaluate the deployment yet. + virtual std::optional isV24Active() = 0; //! Check if any block has been pruned. virtual bool havePruned() = 0; @@ -315,10 +319,10 @@ class Chain virtual ~Notifications() {} virtual void transactionAddedToMempool(const CTransactionRef& tx, int64_t nAcceptTime) {} virtual void transactionRemovedFromMempool(const CTransactionRef& tx, MemPoolRemovalReason reason) {} - virtual void blockConnected(const BlockInfo& block) {} + virtual void blockConnected(ChainstateRole role, const BlockInfo& block) {} virtual void blockDisconnected(const BlockInfo& block) {} virtual void updatedBlockTip() {} - virtual void chainStateFlushed(const CBlockLocator& locator) {} + virtual void chainStateFlushed(ChainstateRole role, const CBlockLocator& locator) {} virtual void notifyChainLock(const CBlockIndex* pindexChainLock, const std::shared_ptr& clsig) {} virtual void notifyTransactionLock(const CTransactionRef &tx, const std::shared_ptr& islock) {} }; diff --git a/src/interfaces/node.h b/src/interfaces/node.h index 559746221f13..bbfc6e2fbf91 100644 --- a/src/interfaces/node.h +++ b/src/interfaces/node.h @@ -23,6 +23,7 @@ #include #include +#include #include #include #include @@ -149,18 +150,22 @@ class EVO { public: virtual ~EVO() {} + //! The masternode list at the chain tip, or nulls when it is not available. virtual std::pair getListAtChainTip() = 0; - virtual ProviderTxCapabilities getProviderTxCapabilities() = 0; + //! The provider transaction rules, or nullopt when this node cannot evaluate them yet. + virtual std::optional getProviderTxCapabilities() = 0; //! Validate provider endpoints against a capability snapshot without changing node or wallet state or //! taking cs_main. Transaction creation validates them again against the then-active rules. virtual std::optional validateProviderNetInfo(const ProviderNetInfo& net_info, MnType type, uint16_t version, bool ipv4_optional, bool optional) = 0; //! The first endpoint a masternode at the chain tip already uses, which a - //! registration with a new collateral cannot reuse. Nothing is reported for - //! endpoints validateProviderNetInfo refuses. - virtual std::optional findProviderNetInfoInUse(const ProviderNetInfo& net_info, MnType type, - uint16_t version, bool ipv4_optional) = 0; + //! registration with a new collateral cannot reuse, or nullopt when none is. + //! Nothing is reported for endpoints validateProviderNetInfo refuses. An + //! error means the masternode list could not be read, so nothing was checked. + virtual ProviderTxResult> findProviderNetInfoInUse(const ProviderNetInfo& net_info, + MnType type, uint16_t version, + bool ipv4_optional) = 0; //! Successful registration with an existing collateral leaves that coin locked, //! including when the signed transaction is returned without submission. virtual ProviderTxResult registerMasternode(Wallet& wallet, @@ -202,13 +207,15 @@ class GOV int32_t m_no{0}; int32_t m_yes{0}; }; - virtual Votes getObjVotes(const CGovernanceObject& obj, vote_signal_enum_t vote_signal) = 0; + //! Vote counts against the current masternode list, or nullopt when that list is not available. + virtual std::optional getObjVotes(const CGovernanceObject& obj, vote_signal_enum_t vote_signal) = 0; struct UniqueVoters { uint16_t m_regular{0}; uint16_t m_evo{0}; }; - virtual UniqueVoters getObjUniqueVoters(const CGovernanceObject& obj, vote_signal_enum_t vote_signal) = 0; - virtual std::vector getCurrentVotes(const uint256& hash) = 0; + virtual std::optional getObjUniqueVoters(const CGovernanceObject& obj, vote_signal_enum_t vote_signal) = 0; + //! The current votes on an object, nullopt while the masternode list is unavailable. + virtual std::optional> getCurrentVotes(const uint256& hash) = 0; virtual bool existsObj(const uint256& hash) = 0; virtual bool isEnabled() = 0; virtual bool processVoteAndRelay(const CGovernanceVote& vote, std::string& error) = 0; @@ -218,7 +225,8 @@ class GOV int superblockmaturitywindow{0}; int lastsuperblock{0}; int nextsuperblock{0}; - int fundingthreshold{0}; + //! Absolute yes votes a proposal needs; nullopt when the masternode list is not available. + std::optional fundingthreshold; CAmount governancebudget{0}; int64_t targetSpacing{0}; int relayRequiredConfs{1}; @@ -230,7 +238,7 @@ class GOV std::unordered_set hashes; CAmount allocated{0}; }; - virtual FundableResult getFundableProposalHashes() = 0; + virtual std::optional getFundableProposalHashes() = 0; virtual std::optional createProposal(int32_t revision, int64_t created_time, const std::string& data_hex, std::string& error) = 0; virtual bool submitProposal(const uint256& parent, int32_t revision, int64_t created_time, const std::string& data_hex, @@ -254,7 +262,8 @@ class LLMQ CAmount m_limit{0}; CAmount m_locked{0}; }; - virtual CreditPoolCounts getCreditPoolCounts() = 0; + //! Nullopt when the credit pool at the tip is not available. + virtual std::optional getCreditPoolCounts() = 0; struct InstantSendCounts { size_t m_verified{0}; size_t m_unverified{0}; @@ -272,17 +281,23 @@ class LLMQ int32_t m_newest_height{0}; int32_t m_expiry_height{0}; }; - virtual std::vector getQuorumStats() = 0; - struct PlatformQuorum { - uint256 m_quorum_hash{}; - std::vector m_pubkey{}; //!< serialized BLS public key (basic scheme) - int32_t m_height{0}; + //! Nullopt when the quorums at the tip are not available. + virtual std::optional> getQuorumStats() = 0; + enum class PlatformQuorumStatus { FOUND, INVALID, UNAVAILABLE }; + struct PlatformQuorumResult { + PlatformQuorumStatus status{PlatformQuorumStatus::UNAVAILABLE}; + std::vector public_key; //!< 48-byte basic-scheme key for FOUND; empty otherwise }; - //! Locally retained quorums of the given LLMQ type with their public - //! keys. Used by the GUI Platform client to verify Platform state-root - //! quorum signatures against locally synced quorum data. This includes - //! retained signing quorums that are no longer in the active signing set. - virtual std::vector getPlatformQuorums(uint8_t llmq_type) = 0; + //! Whether `quorum_hash` (Core internal byte order) could sign Platform state whose signed metadata + //! carries `core_chain_locked_height`, judged from this node's active chain. FOUND means it is among + //! the quorums eligible at that height. INVALID means the local chain proves it could not: the + //! height is stale against a ChainLock this node knows, or the hash is no block of the active chain + //! below that height, so no quorum with it was mined by then, which counts only if the active + //! chain's block at that height is, or is an ancestor of, the block of the best ChainLock whose + //! block this node knows. UNAVAILABLE means this node proves neither, as for an older quorum, which + //! may still sign the one Platform block that first carries a height after a jump. + virtual PlatformQuorumResult getPlatformQuorum(uint8_t llmq_type, const uint256& quorum_hash, + uint32_t core_chain_locked_height) = 0; //! Serialized InstantSend lock for the given txid, or empty if the //! transaction has no islock (used to build asset lock proofs). virtual std::vector getInstantSendLock(const uint256& txid) = 0; @@ -489,8 +504,9 @@ class Node //! Is loading blocks. virtual bool isLoadingBlocks() = 0; - //! Whether the v24 hard fork rules are active at the chain tip. - virtual bool isV24Active() = 0; + //! Whether the v24 hard fork rules are active at the chain tip; nullopt + //! when this node cannot evaluate the deployment yet. + virtual std::optional isV24Active() = 0; //! Set network active. virtual void setNetworkActive(bool active) = 0; diff --git a/src/interfaces/providertx.h b/src/interfaces/providertx.h index fe13093c72c1..06dc8dd53a5b 100644 --- a/src/interfaces/providertx.h +++ b/src/interfaces/providertx.h @@ -39,9 +39,9 @@ enum class ProviderTxErrorCode { struct ProviderTxError { ProviderTxErrorCode code{ProviderTxErrorCode::INTERNAL_ERROR}; - bilingual_str message; - std::string reject_reason; - std::optional transaction_error; + bilingual_str message{}; + std::string reject_reason{}; + std::optional transaction_error{}; }; template diff --git a/src/interfaces/wallet.h b/src/interfaces/wallet.h index 7f08ba98b510..143bb7e894d7 100644 --- a/src/interfaces/wallet.h +++ b/src/interfaces/wallet.h @@ -243,8 +243,8 @@ class Wallet //! Lock all existing dust UTXOs that match the current threshold. virtual void lockExistingDustOutputs() = 0; - //! List protx coins. - virtual std::vector listProTxCoins() = 0; + //! List protx coins, or nullopt when the masternode list is not available. + virtual std::optional> listProTxCoins() = 0; //! Create transaction. virtual util::Result createTransaction(const std::vector& recipients, diff --git a/src/kernel/chain.cpp b/src/kernel/chain.cpp index 1c877866d0a2..318c956b386d 100644 --- a/src/kernel/chain.cpp +++ b/src/kernel/chain.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include @@ -25,3 +26,13 @@ interfaces::BlockInfo MakeBlockInfo(const CBlockIndex* index, const CBlock* data return info; } } // namespace kernel + +std::ostream& operator<<(std::ostream& os, const ChainstateRole& role) { + switch(role) { + case ChainstateRole::NORMAL: os << "normal"; break; + case ChainstateRole::ASSUMEDVALID: os << "assumedvalid"; break; + case ChainstateRole::BACKGROUND: os << "background"; break; + default: os.setstate(std::ios_base::failbit); + } + return os; +} diff --git a/src/kernel/chain.h b/src/kernel/chain.h index f0750f82663f..feba24a557e6 100644 --- a/src/kernel/chain.h +++ b/src/kernel/chain.h @@ -5,6 +5,8 @@ #ifndef BITCOIN_KERNEL_CHAIN_H #define BITCOIN_KERNEL_CHAIN_H +#include + class CBlock; class CBlockIndex; namespace interfaces { @@ -14,6 +16,24 @@ struct BlockInfo; namespace kernel { //! Return data from block index. interfaces::BlockInfo MakeBlockInfo(const CBlockIndex* block_index, const CBlock* data = nullptr); + } // namespace kernel +//! This enum describes the various roles a specific Chainstate instance can take. +//! Other parts of the system sometimes need to vary in behavior depending on the +//! existence of a background validation chainstate, e.g. when building indexes. +enum class ChainstateRole { + // Single chainstate in use, "normal" IBD mode. + NORMAL, + + // Doing IBD-style validation in the background. Implies use of an assumed-valid + // chainstate. + BACKGROUND, + + // Active assumed-valid chainstate. Implies use of a background IBD chainstate. + ASSUMEDVALID, +}; + +std::ostream& operator<<(std::ostream& os, const ChainstateRole& role); + #endif // BITCOIN_KERNEL_CHAIN_H diff --git a/src/kernel/chainparams.cpp b/src/kernel/chainparams.cpp index 6d0f643ad20f..4ad26ef5ff8a 100644 --- a/src/kernel/chainparams.cpp +++ b/src/kernel/chainparams.cpp @@ -360,8 +360,8 @@ class CMainParams : public CChainParams { } }; - m_assumeutxo_data = MapAssumeutxo{ - // TODO to be specified in a future patch. + m_assumeutxo_data = { + // TODO to be specified in a future patch. }; // getchaintxstats 17280 000000000000000346a1804da4832b0b69dd9a7f96b14c7cb54585a9ac6ae98f @@ -545,7 +545,7 @@ class CTestNetParams : public CChainParams { } }; - m_assumeutxo_data = MapAssumeutxo{ + m_assumeutxo_data = { // TODO to be specified in a future patch. }; @@ -979,16 +979,31 @@ class CRegTestParams : public CChainParams { } }; - m_assumeutxo_data = MapAssumeutxo{ + m_assumeutxo_data = { { - 110, - {AssumeutxoHash{uint256S("0xffb210087e1ed14526c0c08a3ec3a7c8e288079eaa68acb87d3d4d9fd746079f")}, EvoSnapshotHash{uint256{}}, 110}, + .height = 110, + .hash_serialized = AssumeutxoHash{uint256S("0xffb210087e1ed14526c0c08a3ec3a7c8e288079eaa68acb87d3d4d9fd746079f")}, + .evo_hash = EvoSnapshotHash{uint256{}}, + .nChainTx = 111, + .blockhash = uint256S("0x729bcb1479ff9f4968439f0276bd76bcb2de0f0720b7a16f383321f6a41cb238"), }, { - 200, - {AssumeutxoHash{uint256S("0x16e00a64db4fa48dd989dce86d8677f41797d52044e5fc86021aa88cc22b665b")}, EvoSnapshotHash{uint256{}}, 200}, + .height = 200, + .hash_serialized = AssumeutxoHash{uint256S("0x16e00a64db4fa48dd989dce86d8677f41797d52044e5fc86021aa88cc22b665b")}, + .evo_hash = EvoSnapshotHash{uint256{}}, + .nChainTx = 201, + .blockhash = uint256S("0x19c1b203b5a960c7f3619e0e805b24c94e684b1aa261f3a79c84d291638a6e1f"), }, }; + for (const auto& data : opts.assumeutxo_data) { + if (std::ranges::any_of(m_assumeutxo_data, [&](const auto& entry) { + return entry.height == data.height || entry.blockhash == data.blockhash; + })) { + throw std::runtime_error(strprintf("Duplicate height or block hash in -assumeutxodata (%d:%s)", + data.height, data.blockhash.ToString())); + } + m_assumeutxo_data.push_back(data); + } chainTxData = ChainTxData{ 0, diff --git a/src/kernel/chainparams.h b/src/kernel/chainparams.h index a96b48821baa..4b0f4513d961 100644 --- a/src/kernel/chainparams.h +++ b/src/kernel/chainparams.h @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -49,20 +50,24 @@ struct EvoSnapshotHash : public BaseHash { * as valid. */ struct AssumeutxoData { + int height; + //! The expected hash of the deserialized UTXO set. - const AssumeutxoHash hash_serialized; + AssumeutxoHash hash_serialized; //! The expected single-SHA256 hash of the canonical Dash evo section. - const EvoSnapshotHash evo_hash; + EvoSnapshotHash evo_hash; //! Used to populate the nChainTx value, which is used during BlockManager::LoadBlockIndex(). //! //! We need to hardcode the value here because this is computed cumulatively using block data, //! which we do not necessarily have at the time of snapshot load. - const unsigned int nChainTx; -}; + unsigned int nChainTx; -using MapAssumeutxo = std::map; + //! The hash of the base block for this snapshot. Used to refer to assumeutxo data + //! prior to having a loaded blockindex. + uint256 blockhash; +}; /** * Holds various statistics on transactions within a chain. Used to estimate @@ -146,7 +151,16 @@ class CChainParams //! Get allowed assumeutxo configuration. //! @see ChainstateManager - const MapAssumeutxo& Assumeutxo() const { return m_assumeutxo_data; } + const std::vector& Assumeutxo() const { return m_assumeutxo_data; } + + std::optional AssumeutxoForHeight(int height) const + { + return FindFirst(m_assumeutxo_data, [&](const auto& d) { return d.height == height; }); + } + std::optional AssumeutxoForBlockhash(const uint256& blockhash) const + { + return FindFirst(m_assumeutxo_data, [&](const auto& d) { return d.blockhash == blockhash; }); + } const ChainTxData& TxData() const { return chainTxData; } void UpdateDIP3Parameters(int nActivationHeight, int nEnforcementHeight); @@ -243,6 +257,8 @@ class CChainParams std::unordered_map llmq_test_parameters{}; //! Name of the LLMQ to use for InstantSend (DIP0024) std::optional llmq_dip0024_instantsend{}; + //! Extra exact snapshot authorizations + std::vector assumeutxo_data{}; }; static std::unique_ptr RegTest(const RegTestOptions& options); @@ -278,7 +294,7 @@ class CChainParams bool m_is_mockable_chain; int nLLMQConnectionRetryTimeout; CCheckpointData checkpointData; - MapAssumeutxo m_assumeutxo_data; + std::vector m_assumeutxo_data; ChainTxData chainTxData; int nPoolMinParticipants; int nPoolMaxParticipants; diff --git a/src/kernel/chainstatemanager_opts.h b/src/kernel/chainstatemanager_opts.h index 7fc875f26a30..efc521238bfc 100644 --- a/src/kernel/chainstatemanager_opts.h +++ b/src/kernel/chainstatemanager_opts.h @@ -55,6 +55,12 @@ struct ChainstateManagerOpts { CoinsViewOptions coins_view{}; Notifications& notifications; int stop_at_height{DEFAULT_STOPATHEIGHT}; + //! Dash: the node runs as a masternode. It does not load UTXO snapshots. + bool masternode{false}; + //! Dash: the node keeps the -addressindex, -spentindex or -timestampindex + //! index, which is not supported with a snapshot chainstate. It does not + //! load UTXO snapshots. + bool legacy_indexes{false}; }; } // namespace kernel diff --git a/src/kernel/mempool_persist.cpp b/src/kernel/mempool_persist.cpp index 2e9d26a87472..9093d1af3d8a 100644 --- a/src/kernel/mempool_persist.cpp +++ b/src/kernel/mempool_persist.cpp @@ -91,6 +91,11 @@ bool LoadMempool(CTxMemPool& pool, const fs::path& load_path, Chainstate& active } if (nTime > TicksSinceEpoch(now - pool.m_expiry)) { LOCK(cs_main); + // A snapshot activation or rejection moves the mempool to another chainstate, or detaches it. + if (active_chainstate.GetMempool() != &pool) { + LogPrintf("Stopping the mempool load: the chainstate no longer holds this mempool\n"); + return false; + } const auto& accepted = AcceptToMemoryPool(active_chainstate, tx, nTime, /*bypass_limits=*/false, /*test_accept=*/false); if (accepted.m_result_type == MempoolAcceptResult::ResultType::VALID) { ++count; diff --git a/src/kernel/notifications_interface.h b/src/kernel/notifications_interface.h index af5867af9989..d77723eb27a9 100644 --- a/src/kernel/notifications_interface.h +++ b/src/kernel/notifications_interface.h @@ -29,6 +29,7 @@ class Notifications virtual void progress(const bilingual_str& title, int progress_percent, bool resume_possible) {} virtual void warning(const bilingual_str& warning) {} virtual void masternodeListChanged(const CDeterministicMNList& list, const CBlockIndex* index) {} + virtual void evoStateWarningChanged() {} }; } // namespace kernel diff --git a/src/llmq/blockprocessor.cpp b/src/llmq/blockprocessor.cpp index 71038471930d..350159b6c5ea 100644 --- a/src/llmq/blockprocessor.cpp +++ b/src/llmq/blockprocessor.cpp @@ -5,9 +5,11 @@ #include #include +#include #include #include #include +#include #include #include @@ -16,6 +18,8 @@ #include #include #include +#include +#include #include #include #include @@ -47,6 +51,8 @@ static const std::string DB_MINED_COMMITMENT_BY_INVERSED_HEIGHT = "q_mcih"; static const std::string DB_MINED_COMMITMENT_BY_INVERSED_HEIGHT_Q_INDEXED = "q_mcihi"; static const std::string DB_BEST_BLOCK_UPGRADE = "q_bbu2"; +// Written only into a snapshot generation. +static const std::string DB_SCAN_COVERAGE = "q_scov"; bool EraseMinedCommitmentIfUnreferenced(CEvoDB& evo_db, const Chainstate& chainstate, gsl::not_null pindex, @@ -77,14 +83,20 @@ static bool SerializedEqual(const T& lhs, const T& rhs) std::equal(lhs_stream.begin(), lhs_stream.end(), rhs_stream.begin()); } -CQuorumBlockProcessor::CQuorumBlockProcessor(ChainstateManager& chainman, CDeterministicMNManager& dmnman, CEvoDB& evoDb, - CQuorumSnapshotManager& qsnapman, int8_t bls_threads) : +CQuorumBlockProcessor::CQuorumBlockProcessor(ChainstateManager& chainman, CDeterministicMNManager& dmnman, + CEvoDB& evoDb, CQuorumSnapshotManager& qsnapman, int8_t bls_threads) : m_chainman{chainman}, m_dmnman{dmnman}, m_evoDb{evoDb}, - m_qsnapman{qsnapman} + m_qsnapman{qsnapman}, + mapMinedCommitmentBlockCache{[&consensus = chainman.GetConsensus()] { + PerLlmqTypeCache cache; + cache.Init(consensus); + return cache; + }} { - mapMinedCommitmentBlockCache.Init(m_chainman.GetConsensus()); + mapMinedCommitmentBlockCache.SetOwner(evoDb); + m_qc_hashes.SetOwner(evoDb); LogPrintf("BLS verification uses %d additional threads\n", bls_threads); m_bls_queue.StartWorkerThreads(bls_threads); } @@ -212,6 +224,10 @@ bool CQuorumBlockProcessor::ProcessBlock(Chainstate& chainstate, const CBlock& b { AssertLockHeld(::cs_main); + if (!chainstate.IsEvoViewBound()) { + throw EvoViewBindingError(strprintf("%s: not bound to the Evo view of %s", __func__, chainstate.ToString())); + } + const auto blockHash = pindex->GetBlockHash(); if (!DeploymentActiveAt(*pindex, m_chainman.GetConsensus(), Consensus::DEPLOYMENT_DIP0003)) { @@ -445,8 +461,9 @@ bool CQuorumBlockProcessor::ProcessCommitment(Chainstate& chainstate, int nHeigh DropQcHashesCache(); { + const EvoViewToken view{m_evoDb.CurrentView()}; LOCK(minableCommitmentsCs); - mapMinedCommitmentBlockCache.erase(qc.llmqType, qc.quorumHash); + mapMinedCommitmentBlockCache.For(view).erase(qc.llmqType, qc.quorumHash); minableCommitmentsByQuorum.erase(cacheKey); minableCommitments.erase(::SerializeHash(qc)); } @@ -460,17 +477,16 @@ bool CQuorumBlockProcessor::ProcessCommitment(Chainstate& chainstate, int nHeigh void CQuorumBlockProcessor::DropQcHashesCache() { LOCK(m_qc_hashes_cache_mutex); - m_quorums_cached.clear(); - m_qc_hashes_cached.clear(); - m_qc_indexed_hashes_cached.clear(); - m_qc_hashes_lru.clear(); + m_qc_hashes.Clear(); } std::optional> CQuorumBlockProcessor::GetQcHashes(const CBlockIndex* pindexPrev) const { auto quorums = GetMinedAndActiveCommitmentsUntilBlock(pindexPrev); + const EvoViewToken view{m_evoDb.CurrentView()}; LOCK(m_qc_hashes_cache_mutex); + auto& [m_quorums_cached, m_qc_hashes_lru, m_qc_hashes_cached, m_qc_indexed_hashes_cached] = m_qc_hashes.For(view); if (quorums == m_quorums_cached) { return std::make_pair(m_qc_hashes_cached, m_qc_indexed_hashes_cached); } @@ -518,6 +534,9 @@ std::optional> CQuorumBlockProcessor::Get bool CQuorumBlockProcessor::UndoBlock(const Chainstate& chainstate, const CBlock& block, gsl::not_null pindex) { AssertLockHeld(::cs_main); + if (!chainstate.IsEvoViewBound()) { + throw EvoViewBindingError(strprintf("%s: not bound to the chainstate's Evo view", __func__)); + } PreComputeQuorumMembers(m_dmnman, m_qsnapman, m_chainman, pindex, /*reset_cache=*/true); @@ -549,7 +568,8 @@ bool CQuorumBlockProcessor::UndoBlock(const Chainstate& chainstate, const CBlock // Only once this commitment's state change is complete; see ProcessCommitment. DropQcHashesCache(); - WITH_LOCK(minableCommitmentsCs, mapMinedCommitmentBlockCache.erase(qc.llmqType, qc.quorumHash)); + const EvoViewToken view{m_evoDb.CurrentView()}; + WITH_LOCK(minableCommitmentsCs, mapMinedCommitmentBlockCache.For(view).erase(qc.llmqType, qc.quorumHash)); // if a reorg happened, we should allow to mine this commitment later AddMineableCommitment(qc); @@ -659,6 +679,64 @@ uint256 CQuorumBlockProcessor::GetQuorumBlockHash(const Consensus::LLMQParams& l return pindex->GetBlockHash(); } +//! Whether `view` is a snapshot view lacking quorum history: it holds the +//! commitments mined above its base and none at or below it, where DIP3 may +//! already have let quorums be mined. +static bool LacksQuorumHistory(const EvoViewToken& view, const Consensus::Params& consensus) +{ + return view.identity == EvoDbIdentity::SNAPSHOT && view.base_height >= consensus.DIP0003Height; +} + +ScanSequenceEnd CQuorumBlockProcessor::SequenceEnd(Consensus::LLMQType llmqType, bool indexed, size_t sequence) const +{ + AssertLockNotHeld(m_evoDb.cs); + // A full view, or a snapshot view of a base no quorum can precede, holds + // every commitment mined on its chain. + if (!LacksQuorumHistory(m_evoDb.CurrentView(), m_chainman.GetConsensus())) return ScanSequenceEnd::END; + const auto llmq_params{m_chainman.GetParams().GetLLMQ(llmqType)}; + if (!llmq_params) return ScanSequenceEnd::UNKNOWN; + // A family the type never writes is empty: a type is indexed exactly when + // it rotates, and a rotating type is enabled exactly when rotation is + // (both follow DIP0024 at the cycle base). + if (llmq_params->useRotation != indexed) return ScanSequenceEnd::END; + const auto coverage{GetScanCoverage(llmqType)}; + return coverage ? coverage->End(sequence) : ScanSequenceEnd::UNKNOWN; +} + +bool CQuorumBlockProcessor::ShortScanIsComplete(Consensus::LLMQType llmqType, bool indexed, size_t sequence, + const CBlockIndex* pindex) const +{ + const auto& consensus{m_chainman.GetConsensus()}; + // A scan from below DIP3 is complete: no commitment can be mined there. + if (pindex->nHeight < consensus.DIP0003Height) return true; + // The missing history holds a commitment of `llmqType` only if one may + // have been mined at a height up to both `pindex` and the base. A + // deployment-gated type stays enabled once enabled, and the types DIP0024 + // retires are enabled from DIP3 on, so a type enabled at neither end of + // that range has none in it. Callers release the EvoDB lock first: + // deployment state may read EvoDB. + if (const EvoViewToken view{m_evoDb.CurrentView()}; LacksQuorumHistory(view, consensus)) { + const CBlockIndex* last_missing{pindex->GetAncestor(std::min(pindex->nHeight, view.base_height))}; + if (!m_chainman.IsQuorumTypeEnabled(llmqType, last_missing->pprev) && + !m_chainman.IsQuorumTypeEnabled(llmqType, pindex->GetAncestor(consensus.DIP0003Height)->pprev)) { + return true; + } + } + // A scan running out where its sequence ends has found everything mined; + // anywhere else it would need history the view lacks, so its result is + // unknown, not short. + return SequenceEnd(llmqType, indexed, sequence) == ScanSequenceEnd::END; +} + +void CQuorumBlockProcessor::RequireSatisfiedScan(Consensus::LLMQType llmqType, bool indexed, size_t sequence, + const CBlockIndex* pindex, const char* func) const +{ + if (ShortScanIsComplete(llmqType, indexed, sequence, pindex)) return; + throw BlockDataUnavailableError( + strprintf("%s: commitments of LLMQ type %d at or below the snapshot base are not available", func, + std23::to_underlying(llmqType))); +} + bool CQuorumBlockProcessor::HasMinedCommitment(Consensus::LLMQType llmqType, const uint256& quorumHash) const { LOCK(::cs_main); @@ -672,9 +750,10 @@ bool CQuorumBlockProcessor::HasMinedCommitment(Consensus::LLMQType llmqType, con uint256 mined_block_hash; bool cached; + const EvoViewToken view{m_evoDb.CurrentView()}; { LOCK(minableCommitmentsCs); - cached = mapMinedCommitmentBlockCache.get(llmqType, quorumHash, mined_block_hash); + cached = mapMinedCommitmentBlockCache.For(view).get(llmqType, quorumHash, mined_block_hash); } if (!cached) { mined_block_hash = GetMinedCommitment(llmqType, quorumHash).second; @@ -682,7 +761,18 @@ bool CQuorumBlockProcessor::HasMinedCommitment(Consensus::LLMQType llmqType, con // outside ProcessCommitment's normal cache-invalidation path. if (!mined_block_hash.IsNull()) { LOCK(minableCommitmentsCs); - mapMinedCommitmentBlockCache.insert(llmqType, quorumHash, mined_block_hash); + mapMinedCommitmentBlockCache.For(view).insert(llmqType, quorumHash, mined_block_hash); + } else if (LacksQuorumHistory(view, m_chainman.GetConsensus())) { + // The snapshot view holds every commitment for a quorum above its + // base, but of the history at or below it only the newest: a miss + // there means "never mined" only where that can be proven. + const CBlockIndex* quorum_base{m_chainman.m_blockman.LookupBlockIndex(quorumHash)}; + if (quorum_base != nullptr && quorum_base->nHeight <= view.base_height && + !ProvenNeverMined(llmqType, quorum_base, view)) { + throw BlockDataUnavailableError( + strprintf("%s: commitment for quorum %s at or below the snapshot base is not available", __func__, + quorumHash.ToString())); + } } } @@ -690,6 +780,85 @@ bool CQuorumBlockProcessor::HasMinedCommitment(Consensus::LLMQType llmqType, con return mined_block != nullptr && chain.Contains(mined_block); } +bool CQuorumBlockProcessor::ProvenNeverMined(Consensus::LLMQType llmqType, gsl::not_null quorum_index, + const EvoViewToken& view) const +{ + AssertLockHeld(::cs_main); + const auto llmq_params_opt{m_chainman.GetParams().GetLLMQ(llmqType)}; + if (!llmq_params_opt) return true; + const auto& llmq_params{*llmq_params_opt}; + // Only this view's chain can have mined it, only at a quorum height of an + // enabled type. + const CBlockIndex* base{m_chainman.m_blockman.LookupBlockIndex(view.base)}; + if (base == nullptr) return false; + if (base->GetAncestor(quorum_index->nHeight) != quorum_index) return true; + const int offset{quorum_index->nHeight % llmq_params.dkgInterval}; + const bool rotation{IsQuorumRotationEnabled(llmq_params, quorum_index)}; + if (offset >= (rotation ? llmq_params.signingActiveQuorumCount : 1) || + !m_chainman.IsQuorumTypeEnabled(llmqType, quorum_index->pprev)) { + return true; + } + // The carried commitments are the newest of their sequence, so a quorum no + // older than the oldest of them would be among them, and a sequence that + // ends proves there is nothing older either. A family the scans do not + // read is not carried at all. + if (rotation != llmq_params.useRotation) return false; + const size_t sequence{rotation ? static_cast(offset) : 0}; + if (SequenceEnd(llmqType, rotation, sequence) == ScanSequenceEnd::END) return true; + const auto carried{rotation ? GetMinedCommitmentsByQuorumIndexUntilBlock(llmqType, base, offset, 2) + : GetMinedCommitmentsUntilBlock(llmqType, base, + std::max(llmq_params.signingActiveQuorumCount + 1, + llmq_params.keepOldConnections))}; + return !carried.empty() && quorum_index->nHeight >= carried.back()->nHeight; +} + +bool CQuorumBlockProcessor::SeedMinedCommitment(const EvoViewToken& view, Consensus::LLMQType llmqType, + const uint256& quorum_hash, const CFinalCommitment& commitment, + const uint256& mined_block_hash) +{ + AssertLockHeld(::cs_main); + if (!Assume(m_evoDb.HasActiveTransaction())) return false; + const EvoViewScope scope{view}; + const auto llmq_params{m_chainman.GetParams().GetLLMQ(llmqType)}; + const CBlockIndex* mined_index{m_chainman.m_blockman.LookupBlockIndex(mined_block_hash)}; + const CBlockIndex* quorum_base_index{m_chainman.m_blockman.LookupBlockIndex(quorum_hash)}; + if (!llmq_params || mined_index == nullptr || quorum_base_index == nullptr) return false; + if (!m_evoDb.WriteSeed(std::make_pair(DB_MINED_COMMITMENT, std::make_pair(llmqType, quorum_hash)), + std::make_pair(commitment, mined_block_hash), EvoSeedPolicy::BYTE_DETERMINISTIC)) { + return false; + } + // Replay ProcessCommitment's height index exactly: it drives the scans and + // the first post-base CbTx quorum merkle root. + const bool indexed{ + IsQuorumRotationEnabled(*llmq_params, quorum_base_index) + ? m_evoDb.WriteSeed(BuildInversedHeightKeyIndexed(llmqType, mined_index->nHeight, int{commitment.quorumIndex}), + quorum_base_index->nHeight, EvoSeedPolicy::BYTE_DETERMINISTIC) + : m_evoDb.WriteSeed(BuildInversedHeightKey(llmqType, mined_index->nHeight), quorum_base_index->nHeight, + EvoSeedPolicy::BYTE_DETERMINISTIC)}; + if (!indexed) return false; + DropQcHashesCache(); + WITH_LOCK(minableCommitmentsCs, mapMinedCommitmentBlockCache.For(view).erase(llmqType, quorum_hash)); + return true; +} + +bool CQuorumBlockProcessor::SeedScanCoverage(const EvoViewToken& view, Consensus::LLMQType llmqType, + const ScanCoverageRecord& coverage) +{ + if (!Assume(m_evoDb.HasActiveTransaction())) return false; + const EvoViewScope scope{view}; + return m_evoDb.WriteSeed(std::make_pair(DB_SCAN_COVERAGE, llmqType), coverage, EvoSeedPolicy::SCAFFOLDING); +} + +std::optional CQuorumBlockProcessor::GetScanCoverage(Consensus::LLMQType llmqType) const +{ + ScanCoverageRecord coverage; + if (!m_evoDb.Read(std::make_pair(DB_SCAN_COVERAGE, llmqType), coverage) || + coverage.schema_version != ScanCoverageRecord::SCHEMA_VERSION) { + return std::nullopt; + } + return coverage; +} + std::pair CQuorumBlockProcessor::GetMinedCommitment(Consensus::LLMQType llmqType, const uint256& quorumHash) const { @@ -705,7 +874,7 @@ std::pair CQuorumBlockProcessor::GetMinedCommitment(C std::vector CQuorumBlockProcessor::GetMinedCommitmentsUntilBlock(Consensus::LLMQType llmqType, gsl::not_null pindex, size_t maxCount) const { AssertLockNotHeld(m_evoDb.cs); - LOCK(m_evoDb.cs); + WAIT_LOCK(m_evoDb.cs, evodb_lock); auto dbIt = m_evoDb.GetCurTransaction().NewIteratorUniquePtr(); @@ -743,15 +912,27 @@ std::vector CQuorumBlockProcessor::GetMinedCommitmentsUntilB dbIt->Next(); } + if (ret.size() < maxCount) { + dbIt.reset(); + REVERSE_LOCK(evodb_lock); + RequireSatisfiedScan(llmqType, /*indexed=*/false, /*sequence=*/0, pindex, __func__); + } return ret; } std::optional CQuorumBlockProcessor::GetLastMinedCommitmentsByQuorumIndexUntilBlock(Consensus::LLMQType llmqType, const CBlockIndex* pindex, int quorumIndex, size_t cycle) const { AssertLockNotHeld(m_evoDb.cs); - LOCK(m_evoDb.cs); + WAIT_LOCK(m_evoDb.cs, evodb_lock); auto dbIt = m_evoDb.GetCurTransaction().NewIteratorUniquePtr(); + const auto not_found{[&]() -> std::optional { + dbIt.reset(); + REVERSE_LOCK(evodb_lock); + RequireSatisfiedScan(llmqType, /*indexed=*/true, static_cast(quorumIndex), pindex, + "GetLastMinedCommitmentsByQuorumIndexUntilBlock"); + return std::nullopt; + }}; auto firstKey = BuildInversedHeightKeyIndexed(llmqType, pindex->nHeight, quorumIndex); auto lastKey = BuildInversedHeightKeyIndexed(llmqType, 0, quorumIndex); @@ -764,19 +945,19 @@ std::optional CQuorumBlockProcessor::GetLastMinedCommitments decltype(firstKey) curKey; int quorumHeight; if (!dbIt->GetKey(curKey) || curKey >= lastKey) { - return std::nullopt; + return not_found(); } if (std::get<0>(curKey) != DB_MINED_COMMITMENT_BY_INVERSED_HEIGHT_Q_INDEXED || std::get<1>(curKey) != llmqType) { - return std::nullopt; + return not_found(); } if (uint32_t nMinedHeight = std::numeric_limits::max() - be32toh_internal(std::get<3>(curKey)); nMinedHeight > static_cast(pindex->nHeight)) { - return std::nullopt; + return not_found(); } if (!dbIt->GetValue(quorumHeight)) { - return std::nullopt; + return not_found(); } const auto* pQuorumBaseBlockIndex = pindex->GetAncestor(quorumHeight); @@ -791,7 +972,38 @@ std::optional CQuorumBlockProcessor::GetLastMinedCommitments dbIt->Next(); } - return std::nullopt; + return not_found(); +} + +std::vector CQuorumBlockProcessor::GetMinedCommitmentsByQuorumIndexUntilBlock( + Consensus::LLMQType llmqType, const CBlockIndex* pindex, int quorumIndex, size_t maxCount) const +{ + AssertLockNotHeld(m_evoDb.cs); + LOCK(m_evoDb.cs); + + auto dbIt = m_evoDb.GetCurTransaction().NewIteratorUniquePtr(); + auto firstKey = BuildInversedHeightKeyIndexed(llmqType, pindex->nHeight, quorumIndex); + auto lastKey = BuildInversedHeightKeyIndexed(llmqType, 0, quorumIndex); + dbIt->Seek(firstKey); + + std::vector ret; + while (dbIt->Valid() && ret.size() < maxCount) { + decltype(firstKey) curKey; + int quorumHeight; + if (!dbIt->GetKey(curKey) || curKey >= lastKey || + std::get<0>(curKey) != DB_MINED_COMMITMENT_BY_INVERSED_HEIGHT_Q_INDEXED || + std::get<1>(curKey) != llmqType || std::get<2>(curKey) != quorumIndex) { + break; + } + if (std::numeric_limits::max() - be32toh_internal(std::get<3>(curKey)) > + static_cast(pindex->nHeight)) { + break; + } + if (!dbIt->GetValue(quorumHeight)) break; + ret.emplace_back(Assert(pindex->GetAncestor(quorumHeight))); + dbIt->Next(); + } + return ret; } std::vector CQuorumBlockProcessor::GetLastMinedCommitmentsPerQuorumIndexUntilBlock( @@ -832,6 +1044,117 @@ std::vector CQuorumBlockProcessor::GetMinedCommitmentsIndexe return ret; } +// Appends quorum bases from one inversed-height index, newest first, until `bases` holds +// `max_count` entries. Returns false if an entry inside `prefix` could not be read or contradicts +// the index, keeping the entries before it that the contradiction does not implicate; a key outside +// `prefix` ends the walk because no older entry exists. +static bool ReadMinedCommitmentIndex(CEvoDB& evo_db, const DataStream& prefix, const CBlockIndex& pindex, + size_t max_count, std::vector& bases) +{ + AssertLockNotHeld(evo_db.cs); + LOCK(evo_db.cs); + + const auto unreadable{[&](const char* reason) { + LogPrint(BCLog::LLMQ, "Mined commitment index below height %d unreadable after %d entries: %s\n", + pindex.nHeight, bases.size(), reason); + return false; + }}; + + DataStream first_key{prefix}; + first_key << htobe32_internal(std::numeric_limits::max() - pindex.nHeight); + + auto it{evo_db.GetCurTransaction().NewIteratorUniquePtr()}; + try { + for (it->Seek(first_key); bases.size() < max_count; it->Next()) { + // The iterator steps over a block it cannot read, so later entries are not the next newest. + if (it->HasError()) return unreadable("read error"); + if (!it->Valid()) break; + const DataStream key{it->GetKey()}; + if (key.size() < prefix.size() || !std::equal(prefix.begin(), prefix.end(), key.begin())) { + break; + } + int base_height{-1}; + if (key.size() != prefix.size() + sizeof(uint32_t) || !it->GetValue(base_height)) { + return unreadable("malformed entry"); + } + const int64_t mined_height{std::numeric_limits::max() - + be32toh_internal(ReadLE32(UCharCast(key.data() + prefix.size())))}; + const CBlockIndex* base{pindex.GetAncestor(base_height)}; + if (!base || base_height >= mined_height) return unreadable("inconsistent entry"); + // A commitment is mined within its own quorum's DKG interval, so along one index the + // quorum bases strictly decrease. Either of the two entries may be the wrong one. + if (!bases.empty() && base_height >= bases.back()->nHeight) { + bases.pop_back(); + return unreadable("out-of-order entry"); + } + bases.push_back(base); + } + } catch (const dbwrapper_error& e) { + return unreadable(e.what()); + } + return true; +} + +MinedCommitmentScan CQuorumBlockProcessor::ScanMinedCommitmentsUntilBlock(Consensus::LLMQType llmqType, + gsl::not_null pindex, + size_t max_count) const +{ + DataStream prefix{}; + prefix << DB_MINED_COMMITMENT_BY_INVERSED_HEIGHT << llmqType; + MinedCommitmentScan scan; + // A walk that ends short in history the view lacks did not reach the end. + scan.complete = ReadMinedCommitmentIndex(m_evoDb, prefix, *pindex, max_count, scan.bases) && + (scan.bases.size() == max_count || + ShortScanIsComplete(llmqType, /*indexed=*/false, /*sequence=*/0, pindex)); + return scan; +} + +MinedCommitmentScan CQuorumBlockProcessor::ScanMinedCommitmentsIndexedUntilBlock(Consensus::LLMQType llmqType, + gsl::not_null pindex, + size_t max_count) const +{ + MinedCommitmentScan scan; + const auto llmq_params{Params().GetLLMQ(llmqType)}; + if (!Assume(llmq_params.has_value())) { + return scan; + } + // Cycle by cycle, the cycle-th newest commitment of every quorum index in index order. + std::vector> taken(llmq_params->signingActiveQuorumCount); + for (size_t cycle{0}; scan.bases.size() < max_count; ++cycle) { + const size_t found_before{scan.bases.size()}; + for (const int quorum_index : util::irange(llmq_params->signingActiveQuorumCount)) { + if (scan.bases.size() == max_count) break; + DataStream prefix{}; + prefix << DB_MINED_COMMITMENT_BY_INVERSED_HEIGHT_Q_INDEXED << llmqType << quorum_index; + std::vector index_bases; + if (!ReadMinedCommitmentIndex(m_evoDb, prefix, *pindex, cycle + 1, index_bases)) { + // Earlier cycles took this index's newer entries; drop any the failed read no longer keeps. + for (size_t rank{index_bases.size()}; rank < taken[quorum_index].size(); ++rank) { + std::erase(scan.bases, taken[quorum_index][rank]); + } + return scan; + } + if (index_bases.size() > cycle) { + // Quorums of different quorum indexes have different base blocks, so either entry may + // be the wrong one. + if (std::erase(scan.bases, index_bases[cycle]) > 0) { + LogPrint(BCLog::LLMQ, "Mined commitment index of quorum index %d repeats base height %d\n", + quorum_index, index_bases[cycle]->nHeight); + return scan; + } + scan.bases.push_back(index_bases[cycle]); + } else if (!ShortScanIsComplete(llmqType, /*indexed=*/true, static_cast(quorum_index), pindex)) { + // This quorum index ends short in history the view lacks. + return scan; + } + taken[quorum_index] = std::move(index_bases); + } + if (scan.bases.size() == found_before) break; + } + scan.complete = true; + return scan; +} + // The returned quorums are in reversed order, so the most recent one is at index 0 std::map> CQuorumBlockProcessor::GetMinedAndActiveCommitmentsUntilBlock(gsl::not_null pindex) const { @@ -849,6 +1172,38 @@ std::map> CQuorumBlockProce return ret; } +std::optional> TraverseScanSequences(bool indexed, + const std::vector& sequences, + size_t count) +{ + std::vector ret; + if (!indexed) { + if (sequences.empty()) return ret; + const auto& sequence{sequences.front()}; + if (sequence.records.size() >= count) + return std::vector{sequence.records.begin(), sequence.records.begin() + count}; + if (sequence.end == ScanSequenceEnd::UNKNOWN) return std::nullopt; + return sequence.records; + } + // GetMinedCommitmentsIndexedUntilBlock: rank by rank, each quorum index in + // ascending order, skipping exhausted indexes, until `count` or a rank at + // which every index is exhausted. + for (size_t rank{0}; ret.size() < count; ++rank) { + bool any{false}; + for (const auto& sequence : sequences) { + if (rank < sequence.records.size()) { + any = true; + ret.push_back(sequence.records[rank]); + if (ret.size() == count) return ret; + } else if (sequence.end == ScanSequenceEnd::UNKNOWN) { + return std::nullopt; + } + } + if (!any) break; + } + return ret; +} + bool CQuorumBlockProcessor::HasMineableCommitment(const uint256& hash) const { LOCK(minableCommitmentsCs); diff --git a/src/llmq/blockprocessor.h b/src/llmq/blockprocessor.h index f506a3c99deb..d1ff48c879ce 100644 --- a/src/llmq/blockprocessor.h +++ b/src/llmq/blockprocessor.h @@ -7,6 +7,7 @@ #include #include +#include #include // IWYU pragma: export #include #include @@ -22,6 +23,7 @@ #include #include #include +#include class BlockValidationState; class CBlock; @@ -47,6 +49,63 @@ using QcHashMap = std::map>; //! As above, but keyed by quorumIndex, for rotation-enabled types. using QcIndexedHashMap = std::map>; +struct MinedCommitmentScan { + //! Quorum base blocks of mined commitments. Non-indexed scans return them newest first. Indexed + //! scans return them by cycle rank, newest rank first, and by quorum index within each rank. + std::vector bases; + //! True if `bases` holds the requested number of entries or every older entry was read; false + //! if an index entry could not be read or contradicts another, or the walk ended short in + //! history a snapshot view lacks, so an absent quorum may still have been mined. Entries a + //! contradiction implicates are left out. A wrong entry that fits the index's order is not + //! detected. + bool complete{false}; +}; + +/** How a scan sequence of mined commitments, as a snapshot generation holds + * it, continues past its oldest record. */ +enum class ScanSequenceEnd : uint8_t { + //! No older record exists. + END = 0, + //! Older records may exist; this node does not have them. + UNKNOWN = 1, +}; + +/** One sequence a commitment scan reads: the whole history of a type without + * rotation, or of one quorum index with it. */ +struct ScanSequence { + //! Quorum base blocks of the eligible records, newest mined first. + std::vector records; + ScanSequenceEnd end{ScanSequenceEnd::END}; +}; + +/** The quorum base blocks a scan of `count` returns from `sequences`, in the + * order the ordinary scan returns them: newest first without rotation, rank + * by rank in ascending quorum index with it. nullopt when an UNKNOWN end is + * reached before the prefix is complete, so the vector cannot be proven. */ +std::optional> TraverseScanSequences(bool indexed, + const std::vector& sequences, + size_t count); + +/** Local record of how each scan sequence of a type ends in a snapshot + * generation. Never part of the snapshot wire format. */ +struct ScanCoverageRecord { + static constexpr uint16_t SCHEMA_VERSION{1}; + uint16_t schema_version{SCHEMA_VERSION}; + //! ScanSequenceEnd of each sequence: one per quorum index with rotation, + //! one otherwise. + std::vector ends; + + //! An unrecorded or unknown value never proves an end. + ScanSequenceEnd End(size_t sequence) const + { + return sequence < ends.size() && ends[sequence] == static_cast(ScanSequenceEnd::END) + ? ScanSequenceEnd::END + : ScanSequenceEnd::UNKNOWN; + } + + SERIALIZE_METHODS(ScanCoverageRecord, obj) { READWRITE(obj.schema_version, obj.ends); } +}; + /** Erase a mined commitment unless another chainstate still contains its block. */ bool EraseMinedCommitmentIfUnreferenced(CEvoDB& evo_db, const Chainstate& chainstate, gsl::not_null pindex, @@ -69,7 +128,7 @@ class CQuorumBlockProcessor // Cache the block in which a commitment was mined. Membership in a // particular chain is checked on every call so reorgs need no cache flush. - mutable PerLlmqTypeCache mapMinedCommitmentBlockCache GUARDED_BY(minableCommitmentsCs); + mutable EvoViewCache> mapMinedCommitmentBlockCache GUARDED_BY(minableCommitmentsCs); // Memoizes GetQcHashes(). The whole-result cache is keyed on the set of active // quorum base blocks, the LRU on those base-block hashes; neither key identifies @@ -78,10 +137,13 @@ class CQuorumBlockProcessor // invalidation next to the writes it has to follow, and ties their lifetime to the // block index whose CBlockIndex* the outer cache stores. mutable Mutex m_qc_hashes_cache_mutex; - mutable std::map> m_quorums_cached GUARDED_BY(m_qc_hashes_cache_mutex); - mutable PerLlmqTypeCache> m_qc_hashes_lru GUARDED_BY(m_qc_hashes_cache_mutex); - mutable QcHashMap m_qc_hashes_cached GUARDED_BY(m_qc_hashes_cache_mutex); - mutable QcIndexedHashMap m_qc_indexed_hashes_cached GUARDED_BY(m_qc_hashes_cache_mutex); + struct QcHashesCache { + std::map> m_quorums_cached; + PerLlmqTypeCache> m_qc_hashes_lru; + QcHashMap m_qc_hashes_cached; + QcIndexedHashMap m_qc_indexed_hashes_cached; + }; + mutable EvoViewCache m_qc_hashes GUARDED_BY(m_qc_hashes_cache_mutex); //! A block's commitment payloads are needed by CheckSpecialTx, the masternode list update, //! ProcessBlock and the CbTx quorum merkle root to avoid duplicated decodes of BLS elements. @@ -137,6 +199,21 @@ class CQuorumBlockProcessor bool HasMinedCommitment(Consensus::LLMQType llmqType, const uint256& quorumHash, const CChain& chain) const EXCLUSIVE_LOCKS_REQUIRED(::cs_main, !minableCommitmentsCs); std::pair GetMinedCommitment(Consensus::LLMQType llmqType, const uint256& quorumHash) const; + /** Seed how the scan sequences of a type end in `view`, inside the + * caller's open EvoDB transaction for that view. */ + bool SeedScanCoverage(const EvoViewToken& view, Consensus::LLMQType llmqType, const ScanCoverageRecord& coverage); + //! The scan coverage seeded in the calling operation's view, if any. + std::optional GetScanCoverage(Consensus::LLMQType llmqType) const; + //! How the current view's history of one scan sequence (a quorum index of + //! the `indexed` family, or the non-indexed one) ends: END where it holds + //! every commitment mined on its chain, UNKNOWN where older ones may exist + //! that it lacks. + ScanSequenceEnd SequenceEnd(Consensus::LLMQType llmqType, bool indexed, size_t sequence) const; + /** Seed a mined commitment and its height index into `view`, inside the + * caller's open EvoDB transaction for that view. */ + bool SeedMinedCommitment(const EvoViewToken& view, Consensus::LLMQType llmqType, const uint256& quorum_hash, + const CFinalCommitment& commitment, const uint256& mined_block_hash) + EXCLUSIVE_LOCKS_REQUIRED(::cs_main, !minableCommitmentsCs, !m_qc_hashes_cache_mutex); /** * Serialized hashes of the commitments mined for the quorums active as of pindexPrev. @@ -155,9 +232,39 @@ class CQuorumBlockProcessor const CBlockIndex* pindex, size_t cycle) const; std::optional GetLastMinedCommitmentsByQuorumIndexUntilBlock(Consensus::LLMQType llmqType, const CBlockIndex* pindex, int quorumIndex, size_t cycle) const; + + //! GetMinedCommitmentsUntilBlock, stopping at an entry it cannot read or trust and reporting + //! whether the walk read every entry it needed. + MinedCommitmentScan ScanMinedCommitmentsUntilBlock(Consensus::LLMQType llmqType, + gsl::not_null pindex, size_t max_count) const; + //! GetMinedCommitmentsIndexedUntilBlock for rotation-enabled types, reporting the same way. + MinedCommitmentScan ScanMinedCommitmentsIndexedUntilBlock(Consensus::LLMQType llmqType, + gsl::not_null pindex, + size_t max_count) const; + + //! The up to `maxCount` most recent commitments of one quorum index mined + //! at or below `pindex`, newest first. + std::vector GetMinedCommitmentsByQuorumIndexUntilBlock(Consensus::LLMQType llmqType, + const CBlockIndex* pindex, + int quorumIndex, size_t maxCount) const; + private: //! Called from every site that writes or erases mined commitment state. void DropQcHashesCache() EXCLUSIVE_LOCKS_REQUIRED(!m_qc_hashes_cache_mutex); + //! Whether headers and the carried sequences of the snapshot view `view` + //! prove that the quorum at `quorum_index`, at or below its base, was + //! never mined. + bool ProvenNeverMined(Consensus::LLMQType llmqType, gsl::not_null quorum_index, + const EvoViewToken& view) const EXCLUSIVE_LOCKS_REQUIRED(::cs_main); + //! Whether a scan of one sequence of `llmqType` from `pindex` that came up + //! short in the current view read everything mined: it started below DIP3, + //! or SequenceEnd() says the sequence ends there. + bool ShortScanIsComplete(Consensus::LLMQType llmqType, bool indexed, size_t sequence, + const CBlockIndex* pindex) const; + //! Raise BlockDataUnavailableError for a short scan unless + //! ShortScanIsComplete(). + void RequireSatisfiedScan(Consensus::LLMQType llmqType, bool indexed, size_t sequence, const CBlockIndex* pindex, + const char* func) const; bool GetCommitmentsFromBlock(const CBlock& block, gsl::not_null pindex, std::multimap& ret, BlockValidationState& state) const EXCLUSIVE_LOCKS_REQUIRED(::cs_main, !m_commitment_payload_mutex); diff --git a/src/llmq/dkgmessages.h b/src/llmq/dkgmessages.h index 7080779abf2f..6ddba6a752a2 100644 --- a/src/llmq/dkgmessages.h +++ b/src/llmq/dkgmessages.h @@ -55,7 +55,7 @@ class CDKGContribution s >> quorumHash; s >> proTxHash; // Decode the whole vvec under one scheme, so that vvecLegacyScheme describes every point in it - vvecLegacyScheme = bls::bls_legacy_scheme.load(); + vvecLegacyScheme = bls::EffectiveLegacyScheme(); for (uint64_t i = 0, size = ReadCompactSize(s); i < size; i++) { s >> CBLSPublicKeyVersionWrapper(tmp1.emplace_back(), vvecLegacyScheme); } diff --git a/src/llmq/net_dkg.cpp b/src/llmq/net_dkg.cpp index d320c234af90..cef0509374d3 100644 --- a/src/llmq/net_dkg.cpp +++ b/src/llmq/net_dkg.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -28,6 +29,7 @@ #include #include #include +#include #include namespace llmq { @@ -783,15 +785,24 @@ void NetDKG::Interrupt() void NetDKG::PhaseHandlerThread(ActiveDKGSessionHandler& handler) { + const std::string round_name{strprintf("dkg %s qi[%d]", handler.params.name, handler.QuorumIndex())}; while (!handler.IsStopRequested()) { - try { - LogPrint(BCLog::LLMQ_DKG, "NetDKG::%s -- %s qi[%d] - starting HandleDKGRound\n", __func__, - handler.params.name, handler.QuorumIndex()); - HandleDKGRound(handler); - } catch (AbortPhaseException& e) { - m_active->dkgdbgman.MarkAborted(handler.params.type, handler.QuorumIndex()); - LogPrint(BCLog::LLMQ_DKG, "NetDKG::%s -- %s qi[%d] - aborted current DKG session\n", __func__, - handler.params.name, handler.QuorumIndex()); + const bool completed{RunActiveEvoRound(m_chainman.EvoDb(), round_name, [&] { + try { + LogPrint(BCLog::LLMQ_DKG, "NetDKG::PhaseHandlerThread -- %s qi[%d] - starting HandleDKGRound\n", + handler.params.name, handler.QuorumIndex()); + HandleDKGRound(handler); + } catch (AbortPhaseException& e) { + m_active->dkgdbgman.MarkAborted(handler.params.type, handler.QuorumIndex()); + LogPrint(BCLog::LLMQ_DKG, "NetDKG::PhaseHandlerThread -- %s qi[%d] - aborted current DKG session\n", + handler.params.name, handler.QuorumIndex()); + } + })}; + if (!completed) { + // Retry after a second, unless asked to stop meanwhile. + for (int i{0}; i < 10 && !handler.IsStopRequested(); ++i) { + UninterruptibleSleep(std::chrono::milliseconds{100}); + } } } } diff --git a/src/llmq/net_quorum.cpp b/src/llmq/net_quorum.cpp index cde8776470b1..97943b50310d 100644 --- a/src/llmq/net_quorum.cpp +++ b/src/llmq/net_quorum.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -239,6 +240,12 @@ void NetQuorum::ProcessMessage(CNode& pfrom, const std::string& msg_type, CDataS LogPrint(BCLog::LLMQ, "NetQuorum::%s -- %s: Quorum not found, from peer=%d\n", __func__, msg_type, pfrom.GetId()); return; } + if (!pQuorum->HasMembers()) { + // Known by its key only: member data has nothing to attach to. + LogPrint(BCLog::LLMQ, "NetQuorum::%s -- %s: Quorum members unavailable, from peer=%d\n", __func__, msg_type, + pfrom.GetId()); + return; + } // Check if request has QUORUM_VERIFICATION_VECTOR data if (request.GetDataMask() & CQuorumDataRequest::QUORUM_VERIFICATION_VECTOR) { @@ -464,9 +471,9 @@ void NetQuorum::CheckQuorumConnections(const Consensus::LLMQParams& llmqParams, Uint256HashSet connections; const auto& cindexes = utils::CalcDeterministicWatchConnections(llmqParams.type, quorum->m_quorum_base_block_index, - quorum->members.size(), 1); + quorum->Members().size(), 1); for (auto idx : cindexes) { - connections.emplace(quorum->members[idx]->proTxHash); + connections.emplace(quorum->Members()[idx]->proTxHash); } if (!connections.empty()) { if (!m_connman.HasMasternodeQuorumNodes(llmqParams.type, quorum->m_quorum_base_block_index->GetBlockHash())) { @@ -562,7 +569,7 @@ void NetQuorum::DataRecoveryThread(CQuorumCPtr pQuorum, uint16_t data_mask, cons } vecMemberHashes.reserve(pQuorum->qc->validMembers.size()); - for (auto& member : pQuorum->members) { + for (auto& member : pQuorum->Members()) { if (pQuorum->IsValidMember(member->proTxHash) && member->proTxHash != protx_hash) { vecMemberHashes.push_back(member->proTxHash); } @@ -645,10 +652,18 @@ void NetQuorum::StartVvecSyncThread(CQuorumCPtr pQuorum) const return; } - workerPool.push([pQuorum = std::move(pQuorum), this](int threadId) mutable { - DataRecoveryThread(std::move(pQuorum), CQuorumDataRequest::QUORUM_VERIFICATION_VECTOR, - /*protx_hash=*/uint256(), /*start_offset=*/0); - }); + const auto release{[pQuorum] { pQuorum->ReleaseRecovery(); }}; + workerPool.push(WithCapturedEvoView( + [pQuorum = std::move(pQuorum), release, this](int threadId) mutable { + try { + DataRecoveryThread(std::move(pQuorum), CQuorumDataRequest::QUORUM_VERIFICATION_VECTOR, + /*protx_hash=*/uint256(), /*start_offset=*/0); + } catch (const evo::LocalStateError&) { + release(); + throw; + } + }, + release)); } void NetQuorum::TryStartVvecSyncThread(gsl::not_null block_index, CQuorumCPtr pQuorum, @@ -679,10 +694,18 @@ void NetQuorum::StartSkShareRecoveryThread(gsl::not_null pIn return; } - workerPool.push([pQuorum = std::move(pQuorum), pIndex, nDataMaskIn, this](int threadId) mutable { - const size_t size_offset = GetQuorumRecoveryStartOffset(*pQuorum, pIndex); - DataRecoveryThread(std::move(pQuorum), nDataMaskIn, m_role->GetProTxHash(), size_offset); - }); + const auto release{[pQuorum] { pQuorum->ReleaseRecovery(); }}; + workerPool.push(WithCapturedEvoView( + [pQuorum = std::move(pQuorum), pIndex, nDataMaskIn, release, this](int threadId) mutable { + try { + const size_t size_offset = GetQuorumRecoveryStartOffset(*pQuorum, pIndex); + DataRecoveryThread(std::move(pQuorum), nDataMaskIn, m_role->GetProTxHash(), size_offset); + } catch (const evo::LocalStateError&) { + release(); + throw; + } + }, + release)); } size_t NetQuorum::GetQuorumRecoveryStartOffset(const CQuorum& quorum, @@ -725,32 +748,35 @@ void NetQuorum::StartCleanupOldQuorumDataThread(gsl::not_nullnHeight - pindex_loop->nHeight < params.max_store_depth()) { uint256 quorum_key; - if (cleanupQuorumsCache.get(params.type, pindex_loop->GetBlockHash(), quorum_key)) { + if (cache.get(params.type, pindex_loop->GetBlockHash(), quorum_key)) { quorum_keys.insert(quorum_key); if (quorum_keys.size() >= static_cast(params.keepOldKeys)) break; // extra safety belt } pindex_loop = pindex_loop->pprev; } for (const auto& pQuorum : m_qman.ScanQuorums(params.type, pIndex, params.keepOldKeys - quorum_keys.size())) { + // A quorum known by its key only never reads stored contributions. + if (!pQuorum->HasMembers()) continue; const uint256 quorum_key = MakeQuorumKey(*pQuorum); quorum_keys.insert(quorum_key); - cleanupQuorumsCache.insert(params.type, pQuorum->m_quorum_base_block_index->GetBlockHash(), - quorum_key); + cache.insert(params.type, pQuorum->m_quorum_base_block_index->GetBlockHash(), quorum_key); } dbKeysToSkip.merge(quorum_keys); } @@ -760,7 +786,7 @@ void NetQuorum::StartCleanupOldQuorumDataThread(gsl::not_null #include #include #include @@ -117,8 +118,8 @@ class NetQuorum final : public NetHandler, public CValidationInterface const bool m_quorums_recovery; mutable Mutex cs_cleanup; - mutable PerLlmqTypeCache cleanupQuorumsCache - GUARDED_BY(cs_cleanup); + //! Quorum keys by base block, per Evo view. + mutable EvoViewCache> cleanupQuorumsCache GUARDED_BY(cs_cleanup); mutable ctpl::thread_pool workerPool; mutable CThreadInterrupt quorumThreadInterrupt; diff --git a/src/llmq/net_signing.cpp b/src/llmq/net_signing.cpp index 5ab60edb8d9c..e968eb6e9a65 100644 --- a/src/llmq/net_signing.cpp +++ b/src/llmq/net_signing.cpp @@ -4,6 +4,8 @@ #include +#include + #include #include #include @@ -284,12 +286,15 @@ bool NetSigning::ProcessPendingRecoveredSigs() void NetSigning::WorkThreadSigning() { while (!workInterrupt) { - bool fMoreWork = ProcessPendingRecoveredSigs(); + bool fMoreWork{false}; + RunActiveEvoRound(&m_sig_manager.EvoDb(), "recsig-signing", [&] { + fMoreWork = ProcessPendingRecoveredSigs(); - constexpr auto CLEANUP_INTERVAL{5s}; - if (cleanupThrottler.TryCleanup(CLEANUP_INTERVAL)) { - m_sig_manager.Cleanup(); - } + constexpr auto CLEANUP_INTERVAL{5s}; + if (cleanupThrottler.TryCleanup(CLEANUP_INTERVAL)) { + m_sig_manager.Cleanup(); + } + }); // TODO Wakeup when pending signing is needed? if (!fMoreWork && !workInterrupt.sleep_for(std::chrono::milliseconds(100))) { @@ -324,10 +329,12 @@ void NetSigning::WorkThreadCleaning() assert(m_shares_manager); while (!workInterrupt) { - RemoveDiscouragedNodeStates(); + RunActiveEvoRound(&m_sig_manager.EvoDb(), "sigshares-cleaning", [&] { + RemoveDiscouragedNodeStates(); - m_shares_manager->SendMessages(); - m_shares_manager->Cleanup(); + m_shares_manager->SendMessages(); + m_shares_manager->Cleanup(); + }); workInterrupt.sleep_for(std::chrono::milliseconds(100)); } @@ -338,52 +345,58 @@ void NetSigning::WorkThreadDispatcher() assert(m_shares_manager); while (!workInterrupt) { - // Dispatch all pending signs (individual tasks) - { - auto signs = m_shares_manager->DispatchPendingSigns(); - // Dispatch all signs to worker pool - for (auto& work : signs) { - if (workInterrupt) break; - - worker_pool.push([this, work = std::move(work)](int) mutable { - auto rs = m_shares_manager->SignAndProcessSingleShare(std::move(work)); - ProcessRecoveredSig(rs, true); - }); + // Tasks pushed below run under this round's binding. + RunActiveEvoRound(&m_sig_manager.EvoDb(), "sigshares-dispatch", [&] { + // Dispatch all pending signs (individual tasks) + { + auto signs = m_shares_manager->DispatchPendingSigns(); + // Dispatch all signs to worker pool + for (auto& work : signs) { + if (workInterrupt) break; + + worker_pool.push(WithCapturedEvoView([this, work = std::move(work)](int) mutable { + auto rs = m_shares_manager->SignAndProcessSingleShare(std::move(work)); + ProcessRecoveredSig(rs, true); + })); + } } - } - // Collect pending sig shares synchronously and dispatch each batch to a worker for parallel BLS verification. - // Batches awaiting verification are bounded so that under flood shares back up in the capped - // pending maps instead of migrating into the unbounded worker pool task queue. - // - // Each batch is bounded by actual share count (not unique-session count), so at most - // MAX_UNVERIFIED_BATCHES * MAX_SHARES_PER_BATCH shares can be sitting in / on the worker pool at once. - static constexpr int MAX_UNVERIFIED_BATCHES{4}; - static constexpr size_t MAX_SHARES_PER_BATCH{32}; - while (!workInterrupt && unverified_batches < MAX_UNVERIFIED_BATCHES) { - std::unordered_map> sigSharesByNodes; - std::unordered_map, CQuorumCPtr, StaticSaltedHasher> quorums; - - bool more_work = m_shares_manager->CollectPendingSigSharesToVerify(MAX_SHARES_PER_BATCH, sigSharesByNodes, quorums); - - if (sigSharesByNodes.empty()) { - break; + // Collect pending sig shares synchronously and dispatch each batch to a worker for parallel BLS + // verification. Batches awaiting verification are bounded so that under flood shares back up in the capped + // pending maps instead of migrating into the unbounded worker pool task queue. + // + // Each batch is bounded by actual share count (not unique-session count), so at most + // MAX_UNVERIFIED_BATCHES * MAX_SHARES_PER_BATCH shares can be sitting in / on the worker pool at once. + static constexpr int MAX_UNVERIFIED_BATCHES{4}; + static constexpr size_t MAX_SHARES_PER_BATCH{32}; + while (!workInterrupt && unverified_batches < MAX_UNVERIFIED_BATCHES) { + std::unordered_map> sigSharesByNodes; + std::unordered_map, CQuorumCPtr, StaticSaltedHasher> quorums; + + bool more_work = m_shares_manager->CollectPendingSigSharesToVerify(MAX_SHARES_PER_BATCH, + sigSharesByNodes, quorums); + + if (sigSharesByNodes.empty()) { + break; + } + + ++unverified_batches; + worker_pool.push(WithCapturedEvoView( + [this, sigSharesByNodes = std::move(sigSharesByNodes), quorums = std::move(quorums)](int) mutable { + // Ensures unverified_batches is decremented on every exit path, including exceptions. + struct UnverifiedBatchGuard { + std::atomic& count; + ~UnverifiedBatchGuard() { --count; } + } guard{unverified_batches}; + ProcessPendingSigShares(std::move(sigSharesByNodes), std::move(quorums)); + }, + [this] { --unverified_batches; })); + + if (!more_work) { + break; + } } - - ++unverified_batches; - worker_pool.push([this, sigSharesByNodes = std::move(sigSharesByNodes), quorums = std::move(quorums)](int) mutable { - // Ensures unverified_batches is decremented on every exit path, including exceptions. - struct UnverifiedBatchGuard { - std::atomic& count; - ~UnverifiedBatchGuard() { --count; } - } guard{unverified_batches}; - ProcessPendingSigShares(std::move(sigSharesByNodes), std::move(quorums)); - }); - - if (!more_work) { - break; - } - } + }); // Always sleep briefly between checks workInterrupt.sleep_for(std::chrono::milliseconds(10)); diff --git a/src/llmq/params.h b/src/llmq/params.h index 2c975d828cb8..b9701e7f46f7 100644 --- a/src/llmq/params.h +++ b/src/llmq/params.h @@ -5,6 +5,7 @@ #ifndef BITCOIN_LLMQ_PARAMS_H #define BITCOIN_LLMQ_PARAMS_H +#include #include #include #include @@ -512,6 +513,35 @@ static constexpr std::array available_llmqs = { } // namespace Consensus +namespace llmq { +//! DKG intervals beyond its signing-active period during which a Platform quorum is still accepted +//! as the signer of Platform state. +static constexpr int PLATFORM_QUORUM_SLACK{2}; + +//! DKG intervals during which a Platform quorum may sign Platform state. A quorum of a non-rotating +//! type is signing-active for signingActiveQuorumCount intervals, one of a rotating type for one. +[[nodiscard]] constexpr int PlatformEligibleIntervals(const Consensus::LLMQParams& params) +{ + return (params.useRotation ? 1 : params.signingActiveQuorumCount) + PLATFORM_QUORUM_SLACK; +} + +//! How many of the newest mined Platform quorums may sign Platform state. A non-rotating type mines +//! one quorum per DKG interval, a rotating type one per quorum index. +[[nodiscard]] constexpr int PlatformEligibleQuorumCount(const Consensus::LLMQParams& params) +{ + return PlatformEligibleIntervals(params) * (params.useRotation ? params.signingActiveQuorumCount : 1); +} + +//! Lowest Core chain-locked height that Platform state may carry for this node to verify its +//! quorum signature, given the node's ChainLock height. Bounding by the ChainLock rather than the +//! tip keeps state signed before a ChainLock stall verifiable while the tip keeps moving. +[[nodiscard]] constexpr int PlatformQuorumMinSignedHeight(const Consensus::LLMQParams& params, int cl_height) +{ + const int64_t window{int64_t{PlatformEligibleIntervals(params)} * params.dkgInterval}; + return static_cast(std::max(0, int64_t{cl_height} - window)); +} +} // namespace llmq + // This must be outside of all namespaces. We must also duplicate the forward declaration of is_serializable_enum to // avoid inclusion of serialize.h here. template struct is_serializable_enum; diff --git a/src/llmq/quorumproofs.cpp b/src/llmq/quorumproofs.cpp index e23a31160fe5..b1f22e1c8eeb 100644 --- a/src/llmq/quorumproofs.cpp +++ b/src/llmq/quorumproofs.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -60,6 +61,28 @@ class ProofCache } }; +/** A ProofCache of Evo-derived values, partitioned by the Evo view they were + * read under: a value read under one view is never served to another. */ +template +class ViewProofCache +{ + Mutex m_mutex; + EvoViewCache> m_parts GUARDED_BY(m_mutex); + +public: + bool Get(const EvoViewToken& view, const uint256& key, T& value) EXCLUSIVE_LOCKS_REQUIRED(!m_mutex) + { + LOCK(m_mutex); + return m_parts.For(view).get(key, value); + } + + void Insert(const EvoViewToken& view, const uint256& key, const T& value) EXCLUSIVE_LOCKS_REQUIRED(!m_mutex) + { + LOCK(m_mutex); + m_parts.For(view).insert(key, value); + } +}; + static void Require(bool condition, const char* message) { if (!condition) throw std::runtime_error(message); @@ -483,9 +506,10 @@ std::vector QuorumProofBuilder::ActiveCommitments(const CBlock // checked against the block's own quorum root below before it is kept. Every // proof from one checkpoint needs the same set, as does every quorum record at // one target. Reading it decodes each commitment's BLS key and signatures. - static ProofCache, 64> sets; + static ViewProofCache, 64> sets; + const EvoViewToken view{m_qman.CurrentView()}; std::vector result; - if (sets.Get(index->GetBlockHash(), result)) return result; + if (sets.Get(view, index->GetBlockHash(), result)) return result; { LOCK(cs_main); for (const auto& [type, indexes] : m_quorum_block_processor.GetMinedAndActiveCommitmentsUntilBlock(index)) { @@ -512,7 +536,7 @@ std::vector QuorumProofBuilder::ActiveCommitments(const CBlock bool mutated{false}; Require(ComputeMerkleRoot(hashes, &mutated) == StateAt(m_chainman.m_blockman, index).quorumRoot && !mutated, "active quorum root mismatch"); - sets.Insert(index->GetBlockHash(), sorted); + sets.Insert(view, index->GetBlockHash(), sorted); return sorted; } @@ -549,9 +573,10 @@ MasternodeLeaves MasternodeLeavesAt(CDeterministicMNManager& dmnman, const node: // Every EvoNode record at one target opens the same list, and hashing its // entries costs more than the lookup. The list is kept only once it hashes // to the block's own masternode root. - static ProofCache cache; + static ViewProofCache cache; + const EvoViewToken view{dmnman.EvoDb().CurrentView()}; MasternodeLeaves result; - if (cache.Get(index->GetBlockHash(), result)) return result; + if (cache.Get(view, index->GetBlockHash(), result)) return result; const auto list = WITH_LOCK(cs_main, return dmnman.GetListForBlock(index)); result.sml = list.to_sml(); result.leaves.reserve(result.sml->mnList.size()); @@ -560,7 +585,7 @@ MasternodeLeaves MasternodeLeavesAt(CDeterministicMNManager& dmnman, const node: const auto root = QuorumProofBuilder::StateAt(blockman, index).masternodeRoot; bool mutated{false}; Require(ComputeMerkleRoot(result.leaves, &mutated) == root && !mutated, "masternode root mismatch"); - cache.Insert(index->GetBlockHash(), result); + cache.Insert(view, index->GetBlockHash(), result); return result; } @@ -577,11 +602,12 @@ std::optional QuorumProofBuilder::DetermineChainlockSigningCom // share a few dozen commitments. Heights map to a commitment's content hash, // which cannot alias across branches, and each commitment is stored once. // A year of testnet (hourly quorums) is ~80k candidate heights. - static ProofCache selected; - static ProofCache commitments; + static ViewProofCache selected; + static ViewProofCache commitments; + const EvoViewToken view{m_qman.CurrentView()}; uint256 commitment_hash; CFinalCommitment cached; - if (selected.Get(key, commitment_hash) && commitments.Get(commitment_hash, cached)) return cached; + if (selected.Get(view, key, commitment_hash) && commitments.Get(view, commitment_hash, cached)) return cached; std::optional result; { LOCK(cs_main); @@ -592,8 +618,8 @@ std::optional QuorumProofBuilder::DetermineChainlockSigningCom } if (result) { commitment_hash = ::SerializeHash(*result); - commitments.Insert(commitment_hash, *result); - selected.Insert(key, commitment_hash); + commitments.Insert(view, commitment_hash, *result); + selected.Insert(view, key, commitment_hash); } return result; } @@ -603,24 +629,24 @@ static bool OnBranch(const CBlockIndex* tip, const CBlockIndex* index) return tip->GetAncestor(index->nHeight) == index; } -static const CBlockIndex* MinedCommitmentBlock(const CQuorumBlockProcessor& processor, const CBlockIndex* tip, - const node::BlockManager& blocks, Consensus::LLMQType kind, - const uint256& quorum) +static const CBlockIndex* MinedCommitmentBlock(const CQuorumBlockProcessor& processor, const EvoViewToken& view, + const CBlockIndex* tip, const node::BlockManager& blocks, + Consensus::LLMQType kind, const uint256& quorum) { const auto key = (CHashWriter(SER_GETHASH, 0) << kind << quorum).GetHash(); - static ProofCache cache; + static ViewProofCache cache; uint256 mined_hash; LOCK(cs_main); // A quorum can be mined differently after a reorg. Reuse an entry only if // its exact mining block is an ancestor of this request's tip. - if (cache.Get(key, mined_hash)) { + if (cache.Get(view, key, mined_hash)) { const auto* mined = blocks.LookupBlockIndex(mined_hash); if (mined && OnBranch(tip, mined)) return mined; } const auto result = processor.GetMinedCommitment(kind, quorum); const auto* mined = blocks.LookupBlockIndex(result.second); if (result.first.IsNull() || !mined || !OnBranch(tip, mined)) return nullptr; - cache.Insert(key, result.second); + cache.Insert(view, key, result.second); return mined; } @@ -664,6 +690,7 @@ std::optional QuorumProofBuilder::Build(const CBlockIndex* che const chainlock::ChainLockSig& target) const { const auto& blocks = m_chainman.m_blockman; + const EvoViewToken view{m_qman.CurrentView()}; const auto* targetIndex = m_tip->GetAncestor(target.getHeight()); Require(checkpoint && targetIndex && OnBranch(m_tip, checkpoint) && checkpoint->nHeight < targetIndex->nHeight, "checkpoint/target not on chain"); @@ -690,7 +717,7 @@ std::optional QuorumProofBuilder::Build(const CBlockIndex* che while (!seedHashes.count(needed->quorumHash)) { Require(proof.links.size() < MAX_PROOF_CERTIFICATES - 1, "certificate budget exhausted"); Require(!ShutdownRequested(), "proof construction interrupted"); - const auto* mined = MinedCommitmentBlock(m_quorum_block_processor, m_tip, blocks, kind, needed->quorumHash); + const auto* mined = MinedCommitmentBlock(m_quorum_block_processor, view, m_tip, blocks, kind, needed->quorumHash); Require(mined != nullptr, "mining block unavailable"); if (mined->nHeight <= checkpoint->nHeight) return std::nullopt; const auto mining = MiningTransaction(blocks, mined, kind, needed->quorumHash); @@ -708,7 +735,8 @@ std::optional QuorumProofBuilder::Build(const CBlockIndex* che if (height > mined->nHeight + 8 && best) break; auto signer = DetermineChainlockSigningCommitment(height); if (!signer || signer->quorumHash == needed->quorumHash) continue; - const auto* previous = MinedCommitmentBlock(m_quorum_block_processor, m_tip, blocks, kind, signer->quorumHash); + const auto* previous = MinedCommitmentBlock(m_quorum_block_processor, view, m_tip, blocks, kind, + signer->quorumHash); if (!previous || previous->nHeight >= mined->nHeight || (previous->nHeight <= checkpoint->nHeight && !seedHashes.count(signer->quorumHash))) continue; auto cert = certificate(*entry); if (!cert.Verify(*signer, checkpoint->nHeight, kind)) continue; diff --git a/src/llmq/quorums.cpp b/src/llmq/quorums.cpp index 3a0d466fa699..60fdd2d350a1 100644 --- a/src/llmq/quorums.cpp +++ b/src/llmq/quorums.cpp @@ -22,7 +22,7 @@ uint256 MakeQuorumKey(const CQuorum& q) CHashWriter hw(SER_NETWORK, 0); hw << q.params.type; hw << q.qc->quorumHash; - for (const auto& dmn : q.members) { + for (const auto& dmn : q.Members()) { hw << dmn->proTxHash; } return hw.GetHash(); @@ -107,13 +107,34 @@ CQuorum::CQuorum(const Consensus::LLMQParams& _params, CBLSWorker& _blsWorker, qc{std::move(_qc)}, m_quorum_base_block_index{_pQuorumBaseBlockIndex}, minedBlockHash{_minedBlockHash}, - members{_members.begin(), _members.end()}, + m_members{std::in_place, _members.begin(), _members.end()}, blsCache{_blsWorker} { assert(qc != nullptr); assert(m_quorum_base_block_index != nullptr); } +CQuorum::CQuorum(const Consensus::LLMQParams& _params, CBLSWorker& _blsWorker, CFinalCommitmentPtr _qc, + const CBlockIndex* _pQuorumBaseBlockIndex, const uint256& _minedBlockHash) : + params{_params}, + qc{std::move(_qc)}, + m_quorum_base_block_index{_pQuorumBaseBlockIndex}, + minedBlockHash{_minedBlockHash}, + blsCache{_blsWorker} +{ + assert(qc != nullptr); + assert(m_quorum_base_block_index != nullptr); +} + +const std::vector& CQuorum::Members() const +{ + if (!m_members) { + throw BlockDataUnavailableError(strprintf("members of quorum %s %s", qc->quorumHash.ToString(), + BLOCK_DATA_UNAVAILABLE_SUFFIX)); + } + return *m_members; +} + CQuorum::~CQuorum() = default; bool CQuorum::SetVerificationVector(const std::vector& quorumVecIn) @@ -140,11 +161,12 @@ bool CQuorum::SetSecretKeyShare(const CBLSSecretKey& secretKeyShare, const uint2 bool CQuorum::IsMember(const uint256& proTxHash) const { - return std::ranges::any_of(members, [&proTxHash](const auto& dmn) { return dmn->proTxHash == proTxHash; }); + return std::ranges::any_of(Members(), [&proTxHash](const auto& dmn) { return dmn->proTxHash == proTxHash; }); } bool CQuorum::IsValidMember(const uint256& proTxHash) const { + const auto& members{Members()}; for (const auto i : util::irange(members.size())) { // cppcheck-suppress useStlAlgorithm if (members[i]->proTxHash == proTxHash) { @@ -157,10 +179,10 @@ bool CQuorum::IsValidMember(const uint256& proTxHash) const CBLSPublicKey CQuorum::GetPubKeyShare(size_t memberIdx) const { LOCK(cs_vvec_shShare); - if (!HasVerificationVectorInternal() || memberIdx >= members.size() || !qc->validMembers[memberIdx]) { + if (!HasVerificationVectorInternal() || memberIdx >= Members().size() || !qc->validMembers[memberIdx]) { return CBLSPublicKey(); } - const auto& m = members[memberIdx]; + const auto& m = Members()[memberIdx]; return blsCache.BuildPubKeyShare(m->proTxHash, quorumVvec, CBLSId(m->proTxHash)); } @@ -182,6 +204,7 @@ CBLSSecretKey CQuorum::GetSkShare() const int CQuorum::GetMemberIndex(const uint256& proTxHash) const { + const auto& members{Members()}; for (const auto i : util::irange(members.size())) { // cppcheck-suppress useStlAlgorithm if (members[i]->proTxHash == proTxHash) { diff --git a/src/llmq/quorums.h b/src/llmq/quorums.h index 6f6d403da91b..b65748903cea 100644 --- a/src/llmq/quorums.h +++ b/src/llmq/quorums.h @@ -159,8 +159,9 @@ class CQuorumDataRequest /** * An object of this class represents a quorum which was mined on-chain (through a quorum commitment) - * It at least contains information about the members and the quorum public key which is needed to verify recovered - * signatures from this quorum. + * It at least contains the quorum public key which is needed to verify recovered signatures from this quorum, and + * its members unless this node cannot compute them exactly (the work block's score modifier needs a block body it + * pruned); Members() then raises BlockDataUnavailableError instead of returning a guess. * * In case the local node is a member of the same quorum and successfully participated in the DKG, the quorum object * will also contain the secret key share and the quorum verification vector. The quorum vvec is then used to recover @@ -175,9 +176,9 @@ class CQuorum const CFinalCommitmentPtr qc; const CBlockIndex* m_quorum_base_block_index; const uint256 minedBlockHash; - const std::vector members; private: + const std::optional> m_members; // Recovery of public key shares is very slow, so we start a background thread that pre-populates a cache so that // the public key shares are ready when needed later mutable CBLSWorkerCache blsCache; @@ -191,6 +192,9 @@ class CQuorum public: CQuorum(const Consensus::LLMQParams& _params, CBLSWorker& _blsWorker, CFinalCommitmentPtr _qc, const CBlockIndex* _pQuorumBaseBlockIndex, const uint256& _minedBlockHash, Span _members); + //! A quorum known from its commitment only, without its members. + CQuorum(const Consensus::LLMQParams& _params, CBLSWorker& _blsWorker, CFinalCommitmentPtr _qc, + const CBlockIndex* _pQuorumBaseBlockIndex, const uint256& _minedBlockHash); ~CQuorum(); @@ -209,6 +213,9 @@ class CQuorum LOCK(cs_vvec_shShare); return quorumVvec; } + bool HasMembers() const { return m_members.has_value(); } + //! The members; BlockDataUnavailableError for a quorum known by its key only. + const std::vector& Members() const; bool IsMember(const uint256& proTxHash) const; bool IsValidMember(const uint256& proTxHash) const; int GetMemberIndex(const uint256& proTxHash) const; diff --git a/src/llmq/quorumsman.cpp b/src/llmq/quorumsman.cpp index 48a8fd5bbcdf..403540a8c817 100644 --- a/src/llmq/quorumsman.cpp +++ b/src/llmq/quorumsman.cpp @@ -38,10 +38,17 @@ CQuorumManager::CQuorumManager(CBLSWorker& _blsWorker, CDeterministicMNManager& quorumBlockProcessor{_quorumBlockProcessor}, m_qsnapman{qsnapman}, m_chainman{chainman}, - db{util::MakeDbWrapper({db_params.path / "llmq" / "quorumdb", db_params.memory, db_params.wipe, /*cache_size=*/1 << 20, db_params.options})} + m_evoDb{_evoDb}, + db{util::MakeDbWrapper({db_params.path / "llmq" / "quorumdb", db_params.memory, db_params.wipe, + /*cache_size=*/1 << 20, db_params.options})}, + m_view_caches{[&consensus = chainman.GetConsensus()] { + ViewCaches caches; + caches.mapQuorumsCache.Init(consensus, /*limit_by_connections=*/false); + return caches; + }} { AssertLockHeld(::cs_main); - mapQuorumsCache.Init(m_chainman.GetConsensus(), /*limit_by_connections=*/false); + m_view_caches.SetOwner(_evoDb); MigrateOldQuorumDB(_evoDb); m_cache_interrupt.reset(); m_cache_thread = std::thread(&util::TraceThread, "q-cache", [this] { CacheWarmingThreadMain(); }); @@ -78,13 +85,27 @@ CQuorumPtr CQuorumManager::BuildQuorumFromCommitment(const Consensus::LLMQType l const auto& llmq_params_opt = m_chainman.GetParams().GetLLMQ(llmqType); assert(llmq_params_opt.has_value()); - auto members = utils::GetAllQuorumMembers(qc.llmqType, {m_dmnman, m_qsnapman, m_chainman, pQuorumBaseBlockIndex}); - - auto quorum = std::make_shared(llmq_params_opt.value(), blsWorker, - std::make_unique(std::move(qc)), pQuorumBaseBlockIndex, minedBlockHash, members); - - if (populate_cache && llmq_params_opt->size == 1) { - WITH_LOCK(m_cs_maps, mapQuorumsCache.insert(llmqType, quorumHash, quorum)); + const EvoViewToken view{m_evoDb.CurrentView()}; + std::optional> members; + try { + members = utils::GetAllQuorumMembers(qc.llmqType, {m_dmnman, m_qsnapman, m_chainman, pQuorumBaseBlockIndex}); + } catch (const utils::WorkBlockModifierUnavailableError& e) { + // NORMAL cannot compute the members without the work block that fixes + // their score modifier, which it pruned. The mined commitment still + // proves the quorum and its key. + if (view.identity != EvoDbIdentity::NORMAL) throw; + LogPrint(BCLog::LLMQ, "CQuorumManager::%s -- llmqType[%d] quorumHash[%s] without members: %s\n", __func__, + std23::to_underlying(llmqType), quorumHash.ToString(), e.what()); + } + + auto commitment{std::make_unique(std::move(qc))}; + auto quorum = members ? std::make_shared(llmq_params_opt.value(), blsWorker, std::move(commitment), + pQuorumBaseBlockIndex, minedBlockHash, *members) + : std::make_shared(llmq_params_opt.value(), blsWorker, std::move(commitment), + pQuorumBaseBlockIndex, minedBlockHash); + + if (!members || (populate_cache && llmq_params_opt->size == 1)) { + WITH_LOCK(m_cs_maps, m_view_caches.For(view).mapQuorumsCache.insert(llmqType, quorumHash, quorum)); return quorum; } @@ -108,7 +129,7 @@ CQuorumPtr CQuorumManager::BuildQuorumFromCommitment(const Consensus::LLMQType l QueueQuorumForWarming(quorum); } - WITH_LOCK(m_cs_maps, mapQuorumsCache.insert(llmqType, quorumHash, quorum)); + WITH_LOCK(m_cs_maps, m_view_caches.For(view).mapQuorumsCache.insert(llmqType, quorumHash, quorum)); return quorum; } @@ -156,6 +177,67 @@ bool CQuorumManager::HasQuorum(Consensus::LLMQType llmqType, const CQuorumBlockP return quorum_block_processor.HasMinedCommitment(llmqType, quorumHash, chain); } +std::optional NormalizeScanStart(const Consensus::LLMQParams& llmq_params, + gsl::not_null start) +{ + // Quorum sets can only change during the mining phase of DKG. + // Find the closest known block index. + const int quorumCycleStartHeight = start->nHeight - (start->nHeight % llmq_params.dkgInterval); + const int quorumCycleMiningStartHeight = quorumCycleStartHeight + llmq_params.dkgMiningWindowStart; + const int quorumCycleMiningEndHeight = quorumCycleStartHeight + llmq_params.dkgMiningWindowEnd; + + if (start->nHeight < quorumCycleMiningStartHeight) { + // too early for this cycle, use the previous one + // bail out if it's below genesis block + if (quorumCycleMiningEndHeight < llmq_params.dkgInterval) return std::nullopt; + return start->GetAncestor(quorumCycleMiningEndHeight - llmq_params.dkgInterval); + } + if (start->nHeight > quorumCycleMiningEndHeight) { + // we are past the mining phase of this cycle, use it + return start->GetAncestor(quorumCycleMiningEndHeight); + } + // everything else is inside the mining phase of this cycle, no adjustment needed + return start.get(); +} + +std::vector CQuorumManager::ProveScan(Consensus::LLMQType llmqType, + gsl::not_null start, size_t count) const +{ + const auto& llmq_params_opt = m_chainman.GetParams().GetLLMQ(llmqType); + if (count == 0 || !llmq_params_opt || !m_chainman.IsQuorumTypeEnabled(llmqType, start)) return {}; + const auto cutoff{NormalizeScanStart(*llmq_params_opt, start)}; + if (!cutoff) return {}; + return ProveScanFromCutoff(llmqType, *cutoff, count); +} + +std::vector CQuorumManager::ProveScanFromCutoff(Consensus::LLMQType llmqType, + gsl::not_null cutoff, + size_t count) const +{ + const auto llmq_params_opt{m_chainman.GetParams().GetLLMQ(llmqType)}; + assert(llmq_params_opt.has_value()); + const auto& llmq_params{*llmq_params_opt}; + const auto end_of = [&](size_t sequence) { + return quorumBlockProcessor.SequenceEnd(llmqType, llmq_params.useRotation, sequence); + }; + std::vector sequences; + if (llmq_params.useRotation) { + for (int quorum_index{0}; quorum_index < llmq_params.signingActiveQuorumCount; ++quorum_index) { + sequences.push_back( + {quorumBlockProcessor.GetMinedCommitmentsByQuorumIndexUntilBlock(llmqType, cutoff, quorum_index, count), + end_of(quorum_index)}); + } + } else { + sequences.push_back({quorumBlockProcessor.GetMinedCommitmentsUntilBlock(llmqType, cutoff, count), end_of(0)}); + } + auto proven{TraverseScanSequences(llmq_params.useRotation, sequences, count)}; + if (!proven) { + throw BlockDataUnavailableError(strprintf("quorum scan of %d for llmqType=%d from block %s is not covered", count, + std23::to_underlying(llmqType), cutoff->GetBlockHash().ToString())); + } + return std::move(*proven); +} + std::vector CQuorumManager::ScanQuorums(Consensus::LLMQType llmqType, size_t nCountRequested) const { const CBlockIndex* pindex = WITH_LOCK(::cs_main, return m_chainman.ActiveTip()); @@ -178,14 +260,29 @@ std::vector CQuorumManager::ScanQuorums(Consensus::LLMQType llmqTyp } template -PerLlmqTypeCache>& CQuorumManager::ScanCache() const +PerLlmqTypeCache>& CQuorumManager::ScanCache(const EvoViewToken& view) const { - if constexpr (std::is_same_v) { - return scanQuorumsCache; - } else { - static_assert(std::is_same_v); - return scanCommitmentsCache; + // cppcheck-suppress constVariableReference + auto& caches{m_view_caches.For(view)}; + auto& cache = [&]() -> PerLlmqTypeCache>& { + if constexpr (std::is_same_v) { + return caches.scanQuorumsCache; + } else { + static_assert(std::is_same_v); + return caches.scanCommitmentsCache; + } + }(); + if (!cache.IsInitialized()) { + // NOTE: We store it for each block hash in the DKG mining phase here + // and not for a single quorum hash per quorum like we do for other caches. + // And we only do this for max_cycles() of the most recent quorums + // because signing by old quorums requires the exact quorum hash to be specified + // and quorum scanning isn't needed there. + cache.Init(m_chainman.GetConsensus(), [](const Consensus::LLMQParams& llmq) { + return llmq.max_cycles(llmq.keepOldConnections) * (llmq.dkgMiningWindowEnd - llmq.dkgMiningWindowStart); + }); } + return cache; } template @@ -197,46 +294,53 @@ std::vector CQuorumManager::ScanMined(Consensus::LLMQType llmqType, gsl::not_ return {}; } - gsl::not_null pindexStore{pindexStart}; const auto& llmq_params_opt = m_chainman.GetParams().GetLLMQ(llmqType); assert(llmq_params_opt.has_value()); - - // Quorum sets can only change during the mining phase of DKG. - // Find the closest known block index. - const int quorumCycleStartHeight = pindexStart->nHeight - (pindexStart->nHeight % llmq_params_opt->dkgInterval); - const int quorumCycleMiningStartHeight = quorumCycleStartHeight + llmq_params_opt->dkgMiningWindowStart; - const int quorumCycleMiningEndHeight = quorumCycleStartHeight + llmq_params_opt->dkgMiningWindowEnd; - - if (pindexStart->nHeight < quorumCycleMiningStartHeight) { - // too early for this cycle, use the previous one - // bail out if it's below genesis block - if (quorumCycleMiningEndHeight < llmq_params_opt->dkgInterval) return {}; - pindexStore = pindexStart->GetAncestor(quorumCycleMiningEndHeight - llmq_params_opt->dkgInterval); - } else if (pindexStart->nHeight > quorumCycleMiningEndHeight) { - // we are past the mining phase of this cycle, use it - pindexStore = pindexStart->GetAncestor(quorumCycleMiningEndHeight); - } - // everything else is inside the mining phase of this cycle, no pindexStore adjustment needed + const auto cutoff{NormalizeScanStart(*llmq_params_opt, pindexStart)}; + if (!cutoff) return {}; + gsl::not_null pindexStore{*cutoff}; gsl::not_null pIndexScanCommitments{pindexStore}; size_t nScanCommitments{nCountRequested}; std::vector vecResult; bool resume{false}; + const EvoViewToken view{m_evoDb.CurrentView()}; + if (view.identity == EvoDbIdentity::SNAPSHOT) { + // Only the carried history exists at or below the base: prove the + // requested prefix from the original cutoff, never continue a shorter + // cached one, and cache only proven prefixes. + if (use_cache) { + LOCK(m_cs_maps); + if (ScanCache(view).get(llmqType, pindexStore->GetBlockHash(), vecResult) && + vecResult.size() >= nCountRequested) { + return {vecResult.begin(), vecResult.begin() + nCountRequested}; + } + vecResult.clear(); + } + for (const CBlockIndex* pQuorumBaseBlockIndex : ProveScanFromCutoff(llmqType, pindexStore, nCountRequested)) { + const bool populate_cache{vecResult.size() < static_cast(llmq_params_opt->keepOldConnections)}; + std::optional entry = build(pQuorumBaseBlockIndex, populate_cache); + // The scan proved this quorum is mined: failing to build it is + // this node's state, never an empty (quorum-less) answer. + if (!entry) { + throw BlockDataUnavailableError(strprintf("quorum llmqType=%d at block %s is not available", + std23::to_underlying(llmqType), + pQuorumBaseBlockIndex->GetBlockHash().ToString())); + } + vecResult.emplace_back(std::move(*entry)); + } + if (!vecResult.empty() && use_cache) { + LOCK(m_cs_maps); + const size_t cached{std::min(vecResult.size(), static_cast(llmq_params_opt->keepOldConnections))}; + ScanCache(view).emplace(llmqType, pindexStore->GetBlockHash(), + {vecResult.begin(), vecResult.begin() + cached}); + } + return vecResult; + } if (use_cache) { LOCK(m_cs_maps); - auto& cache = ScanCache(); - if (!cache.IsInitialized()) { - // NOTE: We store it for each block hash in the DKG mining phase here - // and not for a single quorum hash per quorum like we do for other caches. - // And we only do this for max_cycles() of the most recent quorums - // because signing by old quorums requires the exact quorum hash to be specified - // and quorum scanning isn't needed there. - cache.Init(m_chainman.GetConsensus(), [](const Consensus::LLMQParams& llmq) { - return llmq.max_cycles(llmq.keepOldConnections) * (llmq.dkgMiningWindowEnd - llmq.dkgMiningWindowStart); - }); - } - bool fCacheExists = cache.get(llmqType, pindexStore->GetBlockHash(), vecResult); + bool fCacheExists = ScanCache(view).get(llmqType, pindexStore->GetBlockHash(), vecResult); if (fCacheExists) { // We have exactly what requested so just return it if (vecResult.size() == nCountRequested) { @@ -298,7 +402,7 @@ std::vector CQuorumManager::ScanMined(Consensus::LLMQType llmqType, gsl::not_ // because signing by old quorums requires the exact quorum hash // to be specified and quorum scanning isn't needed there. const size_t nCacheEndIndex = std::min(nCountResult, static_cast(llmq_params_opt->keepOldConnections)); - ScanCache().emplace(llmqType, pindexStore->GetBlockHash(), {vecResult.begin(), vecResult.begin() + nCacheEndIndex}); + ScanCache(view).emplace(llmqType, pindexStore->GetBlockHash(), {vecResult.begin(), vecResult.begin() + nCacheEndIndex}); } // Don't return more than nCountRequested elements const size_t nResultEndIndex = std::min(nCountResult, nCountRequested); @@ -484,8 +588,9 @@ CQuorumCPtr CQuorumManager::GetQuorum(Consensus::LLMQType llmqType, gsl::not_nul CQuorumPtr pQuorum; { + const EvoViewToken view{m_evoDb.CurrentView()}; LOCK(m_cs_maps); - if (mapQuorumsCache.get(llmqType, quorumHash, pQuorum)) { + if (m_view_caches.For(view).mapQuorumsCache.get(llmqType, quorumHash, pQuorum)) { return pQuorum; } } @@ -505,7 +610,8 @@ CQuorumCPtr CQuorumManager::GetQuorum(Consensus::LLMQType llmqType, } CQuorumPtr pQuorum; - if (LOCK(m_cs_maps); mapQuorumsCache.get(llmqType, quorumHash, pQuorum)) { + const EvoViewToken view{m_evoDb.CurrentView()}; + if (LOCK(m_cs_maps); m_view_caches.For(view).mapQuorumsCache.get(llmqType, quorumHash, pQuorum)) { return pQuorum; } @@ -566,8 +672,9 @@ CQuorumManager::DataResponseValidation CQuorumManager::ValidateDataResponse( CQuorumPtr CQuorumManager::GetCachedMutableQuorum(Consensus::LLMQType llmqType, const uint256& quorumHash) const { CQuorumPtr pQuorum; + const EvoViewToken view{m_evoDb.CurrentView()}; LOCK(m_cs_maps); - mapQuorumsCache.get(llmqType, quorumHash, pQuorum); + m_view_caches.For(view).mapQuorumsCache.get(llmqType, quorumHash, pQuorum); return pQuorum; } @@ -600,7 +707,7 @@ void CQuorumManager::CacheWarmingThreadMain() const pQuorum->m_quorum_base_block_index->GetBlockHash().ToString()); // when then later some other thread tries to get keys, it will be much faster - for (const auto i : util::irange(pQuorum->members.size())) { + for (const auto i : util::irange(pQuorum->Members().size())) { if (m_cache_interrupt) { break; } @@ -617,7 +724,7 @@ void CQuorumManager::CacheWarmingThreadMain() const void CQuorumManager::QueueQuorumForWarming(CQuorumCPtr pQuorum) const { - if (pQuorum->HasVerificationVector()) { + if (pQuorum->HasMembers() && pQuorum->HasVerificationVector()) { LOCK(m_cache_cs); m_cache_queue.push_back(std::move(pQuorum)); } @@ -633,6 +740,8 @@ void CQuorumManager::MigrateOldQuorumDB(CEvoDB& evoDb) const LogPrint(BCLog::LLMQ, "CQuorumManager::%s -- start\n", __func__); + // NORMAL-only lifecycle operation: it uses the raw legacy keyspace and + // never sees snapshot-generation records. CDBBatch batch(*db); std::unique_ptr pcursor(evoDb.GetRawDB().NewIterator()); diff --git a/src/llmq/quorumsman.h b/src/llmq/quorumsman.h index 5312fbd78b3a..81f3079c1df7 100644 --- a/src/llmq/quorumsman.h +++ b/src/llmq/quorumsman.h @@ -7,6 +7,7 @@ #include #include +#include #include // IWYU pragma: export #include #include @@ -71,6 +72,7 @@ class CQuorumManager final CQuorumBlockProcessor& quorumBlockProcessor; CQuorumSnapshotManager& m_qsnapman; const ChainstateManager& m_chainman; + CEvoDB& m_evoDb; llmq::CDKGSessionManager* m_qdkgsman{nullptr}; llmq::QuorumRole* m_handler{nullptr}; @@ -85,9 +87,12 @@ class CQuorumManager final mutable size_t m_inbound_request_count GUARDED_BY(cs_data_requests){0}; mutable Mutex m_cs_maps; - mutable PerLlmqTypeCache> scanCommitmentsCache GUARDED_BY(m_cs_maps); - mutable PerLlmqTypeCache mapQuorumsCache GUARDED_BY(m_cs_maps); - mutable PerLlmqTypeCache> scanQuorumsCache GUARDED_BY(m_cs_maps); + struct ViewCaches { + PerLlmqTypeCache> scanCommitmentsCache; + PerLlmqTypeCache mapQuorumsCache; + PerLlmqTypeCache> scanQuorumsCache; + }; + mutable EvoViewCache m_view_caches GUARDED_BY(m_cs_maps); // On mainnet, we have around 62 quorums active at any point; let's cache a little more than double that to be safe. // it maps `quorum_hash` to `pindex` @@ -133,6 +138,11 @@ class CQuorumManager final const uint256& quorumHash, const CChain& chain) EXCLUSIVE_LOCKS_REQUIRED(::cs_main); + //! The Evo view the calling operation reads. + EvoViewToken CurrentView() const { return m_evoDb.CurrentView(); } + //! The EvoDB this manager reads, for callers that bind an Evo view. + const CEvoDB& EvoDb() const { return m_evoDb; } + // all these methods will lock cs_main for a short period of time CQuorumCPtr GetQuorum(Consensus::LLMQType llmqType, const uint256& quorumHash) const EXCLUSIVE_LOCKS_REQUIRED(!cs_db, !m_cs_maps, !m_cache_cs); @@ -156,6 +166,14 @@ class CQuorumManager final size_t nCountRequested) const EXCLUSIVE_LOCKS_REQUIRED(!cs_db, !m_cs_maps); + /** The quorum base blocks a scan of `count` from `start` returns, proven + * in the calling view: ScanQuorums' normalization and index-family + * dispatch over this view's commitment index, whose carried sequences + * end as recorded when its snapshot was seeded. Throws + * BlockDataUnavailableError when the prefix is not covered. */ + std::vector ProveScan(Consensus::LLMQType llmqType, gsl::not_null start, + size_t count) const; + bool IsMasternode() const; bool IsWatching() const; @@ -190,13 +208,18 @@ class CQuorumManager final size_t nCountRequested, const CChain* chain) const EXCLUSIVE_LOCKS_REQUIRED(!cs_db, !m_cs_maps, !m_cache_cs); //! Walks the mined commitments back from the DKG mining phase closest to pindexStart and builds an - //! element for each. resume_block maps the last cached element to the block to continue below. + //! element for each. resume_block maps the last cached element to the block to continue below. In a + //! snapshot view the requested prefix is proven from the carried history instead (ProveScanFromCutoff). template std::vector ScanMined(Consensus::LLMQType llmqType, gsl::not_null pindexStart, size_t nCountRequested, bool use_cache, ResumeFn&& resume_block, BuildFn&& build) const EXCLUSIVE_LOCKS_REQUIRED(!m_cs_maps); + //! The initialized scan cache of `view`'s partition; a reference is valid only until the next + //! m_view_caches lookup. template - PerLlmqTypeCache>& ScanCache() const EXCLUSIVE_LOCKS_REQUIRED(m_cs_maps); + PerLlmqTypeCache>& ScanCache(const EvoViewToken& view) const EXCLUSIVE_LOCKS_REQUIRED(m_cs_maps); + std::vector ProveScanFromCutoff(Consensus::LLMQType llmqType, + gsl::not_null cutoff, size_t count) const; bool BuildQuorumContributions(const CFinalCommitmentPtr& fqc, const std::shared_ptr& quorum) const; CQuorumPtr BuildQuorumFromCommitment(Consensus::LLMQType llmqType, @@ -215,6 +238,12 @@ class CQuorumManager final void MigrateOldQuorumDB(CEvoDB& evoDb) const EXCLUSIVE_LOCKS_REQUIRED(::cs_main, !cs_db); }; +/** The block a scan from `start` reads up to: inside a DKG mining window the + * start itself, past it the window's end, before it the previous cycle's + * window end. nullopt when that precedes genesis. */ +std::optional NormalizeScanStart(const Consensus::LLMQParams& llmq_params, + gsl::not_null start); + // when selecting a quorum for signing and verification, we use CQuorumManager::SelectQuorum with this offset as // starting height for scanning. This is because otherwise the resulting signatures would not be verifiable by nodes // which are not 100% at the chain tip. diff --git a/src/llmq/signing.cpp b/src/llmq/signing.cpp index f374d28963cf..d1b90d9ee434 100644 --- a/src/llmq/signing.cpp +++ b/src/llmq/signing.cpp @@ -429,6 +429,8 @@ void CRecoveredSigsDb::CleanupOldVotes(int64_t maxAge) ////////////////// +const CEvoDB& CSigningManager::EvoDb() const { return qman.EvoDb(); } + CSigningManager::CSigningManager(const CQuorumManager& _qman, const util::DbWrapperParams& db_params, int64_t max_recsigs_age) : db{db_params}, diff --git a/src/llmq/signing.h b/src/llmq/signing.h index b2ee2d22ba00..e4091a364542 100644 --- a/src/llmq/signing.h +++ b/src/llmq/signing.h @@ -24,6 +24,7 @@ class Chainstate; class CDataStream; class CDBBatch; class CDBWrapper; +class CEvoDB; class CInv; class CTransaction; struct RPCResult; @@ -82,7 +83,7 @@ class CRecoveredSig : virtual public CSigBase CRecoveredSig(Consensus::LLMQType _llmqType, const uint256& _quorumHash, const uint256& _id, const uint256& _msgHash, const CBLSLazySignature& _sig) : CSigBase(_llmqType, _quorumHash, _id, _msgHash), sig(_sig) {UpdateHash();}; CRecoveredSig(Consensus::LLMQType _llmqType, const uint256& _quorumHash, const uint256& _id, const uint256& _msgHash, const CBLSSignature& _sig) : - CSigBase(_llmqType, _quorumHash, _id, _msgHash) {const_cast(sig).Set(_sig, bls::bls_legacy_scheme.load()); UpdateHash();}; + CSigBase(_llmqType, _quorumHash, _id, _msgHash) {const_cast(sig).Set(_sig, bls::EffectiveLegacyScheme()); UpdateHash();}; private: // only in-memory @@ -199,6 +200,9 @@ class CSigningManager CSigningManager(const CSigningManager&) = delete; CSigningManager& operator=(const CSigningManager&) = delete; explicit CSigningManager(const CQuorumManager& _qman, const util::DbWrapperParams& db_params, int64_t max_recsigs_age); + + //! The EvoDB the quorum manager reads, for workers that bind an Evo view. + const CEvoDB& EvoDb() const; ~CSigningManager(); bool AlreadyHave(const CInv& inv) const EXCLUSIVE_LOCKS_REQUIRED(!cs_pending); diff --git a/src/llmq/signing_shares.cpp b/src/llmq/signing_shares.cpp index 5aa2df60dea1..b777ac2888d8 100644 --- a/src/llmq/signing_shares.cpp +++ b/src/llmq/signing_shares.cpp @@ -372,7 +372,7 @@ bool PreVerifyBatchedSigShares(const CSigSharesNodeState::SessionInfo& session, return false; } - if (quorumMember >= session.quorum->members.size()) { + if (quorumMember >= session.quorum->Members().size()) { LogPrint(BCLog::LLMQ_SIGS, "%s -- quorumMember out of bounds\n", __func__); return false; } @@ -456,7 +456,7 @@ bool CSigSharesManager::ProcessMessageSigShare(NodeId fromId, const CSigShare& s return true; } - if (sigShare.getQuorumMember() >= quorum->members.size()) { + if (sigShare.getQuorumMember() >= quorum->Members().size()) { LogPrint(BCLog::LLMQ_SIGS, "CSigSharesManager::%s -- quorumMember out of bounds\n", __func__); return false; } @@ -719,7 +719,7 @@ std::shared_ptr CSigSharesManager::TryRecoverSig(const CQuorum& q for (auto it = sigSharesForSignHash->begin(); it != sigSharesForSignHash->end() && sigSharesForRecovery.size() < static_cast(quorum.params.threshold); ++it) { const auto& sigShare = it->second; sigSharesForRecovery.emplace_back(sigShare.sigShare.Get()); - idsForRecovery.emplace_back(quorum.members[sigShare.getQuorumMember()]->proTxHash); + idsForRecovery.emplace_back(quorum.Members()[sigShare.getQuorumMember()]->proTxHash); } // check if we can recover the final signature @@ -764,8 +764,8 @@ std::shared_ptr CSigSharesManager::TryRecoverSig(const CQuorum& q std::vector CSigSharesManager::GetRecoveryMemberOrder(const CQuorum& quorum, const uint256& id) { std::vector> v; - v.reserve(quorum.members.size()); - for (const auto& dmn : quorum.members) { + v.reserve(quorum.Members().size()); + for (const auto& dmn : quorum.Members()) { auto h = ::SerializeHash(std::make_pair(dmn->proTxHash, id)); v.emplace_back(h, dmn); } @@ -1403,9 +1403,9 @@ void CSigSharesManager::Cleanup() if (LogAcceptDebug(BCLog::LLMQ_SIGS)) { if (const auto quorumIt = quorums.find(std::make_pair(oneSigShare.getLlmqType(), oneSigShare.getQuorumHash())); quorumIt != quorums.end()) { const auto& quorum = quorumIt->second; - for (const auto i : util::irange(quorum->members.size())) { + for (const auto i : util::irange(quorum->Members().size())) { if (m->count(static_cast(i)) == 0) { - const auto& dmn = quorum->members[i]; + const auto& dmn = quorum->Members()[i]; strMissingMembers += strprintf("\n %s", dmn->proTxHash.ToString()); } } @@ -1569,7 +1569,7 @@ std::optional CSigSharesManager::CreateSigShareForSingleMember(const // TODO: This one should be SIGN by QUORUM key, not by OPERATOR key // see TODO in CDKGSession::FinalizeSingleCommitment for details - auto bls_scheme = bls::bls_legacy_scheme.load(); + auto bls_scheme = bls::EffectiveLegacyScheme(); sigShare.sigShare.Set(m_mn_activeman.Sign(signHash, bls_scheme), bls_scheme); if (!sigShare.sigShare.Get().IsValid()) { @@ -1624,7 +1624,7 @@ std::optional CSigSharesManager::CreateSigShare(const CQuorum& quorum CSigShare sigShare(quorum.params.type, quorum.qc->quorumHash, id, msgHash, static_cast(memberIdx), {}); uint256 signHash = sigShare.buildSignHash().Get(); - auto bls_scheme = bls::bls_legacy_scheme.load(); + auto bls_scheme = bls::EffectiveLegacyScheme(); sigShare.sigShare.Set(skShare.Sign(signHash, bls_scheme), bls_scheme); if (!sigShare.sigShare.Get().IsValid()) { LogPrintf("CSigSharesManager::%s -- failed to sign sigShare. signHash=%s, id=%s, msgHash=%s, time=%s\n", __func__, diff --git a/src/llmq/snapshot.cpp b/src/llmq/snapshot.cpp index bde0d32fb783..e48d99906ff2 100644 --- a/src/llmq/snapshot.cpp +++ b/src/llmq/snapshot.cpp @@ -9,9 +9,11 @@ #include #include #include +#include #include #include #include +#include #include #include @@ -22,6 +24,7 @@ using node::CBlockIndexHeightOnlyComparator; namespace { constexpr std::string_view DB_QUORUM_SNAPSHOT{"llmq_S"}; +constexpr std::string_view DB_QUORUM_MODIFIER{"llmq_M3"}; bool CheckBlockDataAvailable(gsl::not_null pindex, std::string& error) EXCLUSIVE_LOCKS_REQUIRED(::cs_main) @@ -58,6 +61,11 @@ std::optional ConstructCycle(llmq::CQuorumSnapshotManager& qsna ret.m_snap = opt_snap.value(); } else { error = "Cannot find quorum snapshot"; + // Locally missing below a snapshot base, not the requester's fault. + if (qsnapman.IsCarriedOnly(ret.m_cycle_index)) { + error = strprintf("Quorum snapshot for block %s %s", ret.m_cycle_index->GetBlockHash().ToString(), + BLOCK_DATA_UNAVAILABLE_SUFFIX); + } return std::nullopt; } } @@ -74,6 +82,12 @@ void InsertBaseBlockSorted(std::vector& baseBlockIndexes, co } // anonymous namespace namespace llmq { +bool CQuorumSnapshotManager::IsCarriedOnly(const CBlockIndex* pindex) const +{ + const EvoViewToken view{m_evoDb.CurrentView()}; + return view.identity == EvoDbIdentity::SNAPSHOT && pindex->nHeight <= view.base_height; +} + bool BuildQuorumRotationInfo(CDeterministicMNManager& dmnman, CQuorumSnapshotManager& qsnapman, const ChainstateManager& chainman, const CQuorumManager& qman, const CQuorumBlockProcessor& qblockman, const CGetQuorumRotationInfo& request, @@ -292,8 +306,9 @@ std::vector CQuorumRotationInfo::GetCycles() CQuorumSnapshotManager::CQuorumSnapshotManager(CEvoDB& evoDb) : m_evoDb{evoDb}, - quorumSnapshotCache{32} + quorumSnapshotCache{[] { return Uint256LruHashMap{32}; }} { + quorumSnapshotCache.SetOwner(evoDb); } CQuorumSnapshotManager::~CQuorumSnapshotManager() = default; @@ -303,14 +318,16 @@ std::optional CQuorumSnapshotManager::GetSnapshotForBlock(const CQuorumSnapshot snapshot = {}; auto snapshotHash = ::SerializeHash(std::make_pair(llmqType, pindex->GetBlockHash())); + const EvoViewToken view{m_evoDb.CurrentView()}; LOCK(snapshotCacheCs); + auto& cache{quorumSnapshotCache.For(view)}; // try using cache before reading from disk - if (quorumSnapshotCache.get(snapshotHash, snapshot)) { + if (cache.get(snapshotHash, snapshot)) { return snapshot; } if (m_evoDb.Read(std::make_pair(DB_QUORUM_SNAPSHOT, snapshotHash), snapshot)) { - quorumSnapshotCache.insert(snapshotHash, snapshot); + cache.insert(snapshotHash, snapshot); return snapshot; } @@ -320,11 +337,58 @@ std::optional CQuorumSnapshotManager::GetSnapshotForBlock(const void CQuorumSnapshotManager::StoreSnapshotForBlock(const Consensus::LLMQType llmqType, const CBlockIndex* pindex, const CQuorumSnapshot& snapshot) { auto snapshotHash = ::SerializeHash(std::make_pair(llmqType, pindex->GetBlockHash())); + const auto key{std::make_pair(DB_QUORUM_SNAPSHOT, snapshotHash)}; - // LOCK(::cs_main); AssertLockNotHeld(m_evoDb.cs); - LOCK2(snapshotCacheCs, m_evoDb.cs); - m_evoDb.GetRawDB().Write(std::make_pair(DB_QUORUM_SNAPSHOT, snapshotHash), snapshot); - quorumSnapshotCache.insert(snapshotHash, snapshot); + const EvoViewToken view{m_evoDb.CurrentView()}; + if (view.identity == EvoDbIdentity::SNAPSHOT && pindex->nHeight <= view.base_height) { + // At or below the snapshot base the snapshot view holds only carried + // state. A carried quarter must match the one just computed from + // complete inputs. An uncarried one is a reconstruction output, never a + // required input, so it is neither persisted nor cached. + CQuorumSnapshot carried; + if (m_evoDb.Read(key, carried) && ::SerializeHash(carried) != ::SerializeHash(snapshot)) { + throw evo::SnapshotStateMismatchError( + strprintf("carried quorum snapshot for %s at height %d differs from the computed one", + pindex->GetBlockHash().ToString(), pindex->nHeight)); + } + return; + } + + LOCK(snapshotCacheCs); + m_evoDb.WriteImmediate(key, snapshot); + quorumSnapshotCache.For(view).insert(snapshotHash, snapshot); +} + +bool CQuorumSnapshotManager::SeedSnapshotForBlock(const EvoViewToken& view, const Consensus::LLMQType llmqType, + const CBlockIndex* pindex, const CQuorumSnapshot& snapshot) +{ + if (!Assume(pindex != nullptr) || !Assume(m_evoDb.HasActiveTransaction())) return false; + const EvoViewScope scope{view}; + const auto snapshot_hash{::SerializeHash(std::make_pair(llmqType, pindex->GetBlockHash()))}; + if (!m_evoDb.WriteSeed(std::make_pair(DB_QUORUM_SNAPSHOT, snapshot_hash), snapshot, + EvoSeedPolicy::BYTE_DETERMINISTIC)) { + return false; + } + LOCK(snapshotCacheCs); + quorumSnapshotCache.For(view).erase(snapshot_hash); + return true; +} + +bool CQuorumSnapshotManager::SeedQuorumModifier(const EvoViewToken& view, Consensus::LLMQType llmq_type, + const uint256& work_block_hash, const uint256& modifier) +{ + if (!Assume(m_evoDb.HasActiveTransaction())) return false; + const EvoViewScope scope{view}; + return m_evoDb.WriteSeed(std::make_tuple(DB_QUORUM_MODIFIER, llmq_type, work_block_hash), modifier, + EvoSeedPolicy::SCAFFOLDING); +} + +std::optional CQuorumSnapshotManager::GetSeededQuorumModifier(Consensus::LLMQType llmq_type, + const uint256& work_block_hash) const +{ + uint256 modifier; + if (!m_evoDb.Read(std::make_tuple(DB_QUORUM_MODIFIER, llmq_type, work_block_hash), modifier)) return std::nullopt; + return modifier; } } // namespace llmq diff --git a/src/llmq/snapshot.h b/src/llmq/snapshot.h index 725072ad84e6..d6340d2b76c9 100644 --- a/src/llmq/snapshot.h +++ b/src/llmq/snapshot.h @@ -6,6 +6,7 @@ #define BITCOIN_LLMQ_SNAPSHOT_H #include +#include #include #include #include @@ -237,7 +238,8 @@ class CQuorumSnapshotManager CEvoDB& m_evoDb; - Uint256LruHashMap quorumSnapshotCache GUARDED_BY(snapshotCacheCs); + EvoViewCache> quorumSnapshotCache GUARDED_BY(snapshotCacheCs){ + [] { return Uint256LruHashMap{32}; }}; public: CQuorumSnapshotManager() = delete; @@ -246,8 +248,26 @@ class CQuorumSnapshotManager explicit CQuorumSnapshotManager(CEvoDB& evoDb); ~CQuorumSnapshotManager(); + //! The Evo view the calling operation reads, for callers that partition + //! their own caches of state derived from quorum snapshots. + EvoViewToken CurrentView() const { return m_evoDb.CurrentView(); } + std::optional GetSnapshotForBlock(Consensus::LLMQType llmqType, const CBlockIndex* pindex); void StoreSnapshotForBlock(Consensus::LLMQType llmqType, const CBlockIndex* pindex, const CQuorumSnapshot& snapshot); + //! Whether the calling operation reads the snapshot view and `pindex` is + //! at or below its base, where only carried quarters exist. + bool IsCarriedOnly(const CBlockIndex* pindex) const; + + /** Seed a carried rotation quarter into `view`, inside the caller's open + * EvoDB transaction for that view. */ + bool SeedSnapshotForBlock(const EvoViewToken& view, Consensus::LLMQType llmqType, const CBlockIndex* pindex, + const CQuorumSnapshot& snapshot); + /** Seed the exact score modifier of a work block into `view`, inside the + * caller's open EvoDB transaction for that view. */ + bool SeedQuorumModifier(const EvoViewToken& view, Consensus::LLMQType llmq_type, const uint256& work_block_hash, + const uint256& modifier); + //! The modifier seeded for a work block in the calling operation's view. + std::optional GetSeededQuorumModifier(Consensus::LLMQType llmq_type, const uint256& work_block_hash) const; }; } // namespace llmq diff --git a/src/llmq/utils.cpp b/src/llmq/utils.cpp index 541de71ed982..aeaf6d95c770 100644 --- a/src/llmq/utils.cpp +++ b/src/llmq/utils.cpp @@ -6,6 +6,8 @@ #include #include +#include +#include #include #include #include @@ -16,6 +18,7 @@ #include #include +#include #include #include #include @@ -29,7 +32,8 @@ /** * Forward declarations */ -std::optional> GetNonNullCoinbaseChainlock(const node::BlockManager& blockman, const CBlockIndex* pindex); +std::optional> GetNonNullCoinbaseChainlock(const CBlock& block, const CBlockIndex& index, + const Consensus::Params& consensus); namespace { using QuorumMembers = std::vector; @@ -86,36 +90,89 @@ arith_uint256 calculateQuorumScore(const CDeterministicMNCPtr& dmn, const uint25 return UintToArith256(h); } -uint256 GetHashModifierFromWorkBlock(const Consensus::LLMQParams& llmqParams, const node::BlockManager& blockman, const CBlockIndex* pWorkBlockIndex) -{ - auto cbcl = GetNonNullCoinbaseChainlock(blockman, pWorkBlockIndex); - if (cbcl.has_value()) { - // We have a non-null CL signature: calculate modifier using this CL signature - auto& [bestCLSignature, bestCLHeightDiff] = cbcl.value(); - return ::SerializeHash(std::make_tuple(llmqParams.type, pWorkBlockIndex->nHeight, bestCLSignature)); - } - // No non-null CL signature found in coinbase: calculate modifier using block hash only - return ::SerializeHash(std::make_pair(llmqParams.type, pWorkBlockIndex->GetBlockHash())); -} - -uint256 GetHashModifier(const Consensus::LLMQParams& llmqParams, const Consensus::Params& consensus_params, - const node::BlockManager& blockman, gsl::not_null pCycleQuorumBaseBlockIndex) +//! The modifier of a cycle whose work block precedes v20. +uint256 CalculateHashModifier(const Consensus::LLMQParams& llmqParams, const Consensus::Params& consensus_params, + gsl::not_null pCycleQuorumBaseBlockIndex) { ASSERT_IF_DEBUG(pCycleQuorumBaseBlockIndex->nHeight % llmqParams.dkgInterval == 0); const CBlockIndex* pWorkBlockIndex = pCycleQuorumBaseBlockIndex->GetAncestor(pCycleQuorumBaseBlockIndex->nHeight - llmq::WORK_DIFF_DEPTH); + Assume(!DeploymentActiveAfter(pWorkBlockIndex, consensus_params, Consensus::DEPLOYMENT_V20)); - if (DeploymentActiveAfter(pWorkBlockIndex, consensus_params, Consensus::DEPLOYMENT_V20)) { - // v20 is active: calculate modifier using the new way. - return GetHashModifierFromWorkBlock(llmqParams, blockman, pWorkBlockIndex); - } - - // v20 isn't active yet: calculate modifier using the usual way if (llmqParams.useRotation) { return ::SerializeHash(std::make_pair(llmqParams.type, pWorkBlockIndex->GetBlockHash())); } return ::SerializeHash(std::make_pair(llmqParams.type, pCycleQuorumBaseBlockIndex->GetBlockHash())); } +//! The ChainLock-derived modifier of a v20 work block from its own data. The +//! signature is hashed in the scheme of the block that carries it, never in +//! the one the process happens to run. +uint256 ModifierFromBlock(const Consensus::LLMQParams& llmq_params, const Consensus::Params& consensus_params, + const CBlock& block, const CBlockIndex& work_index) +{ + if (const auto chainlock{GetNonNullCoinbaseChainlock(block, work_index, consensus_params)}) { + CHashWriter hasher{SER_GETHASH, PROTOCOL_VERSION}; + hasher << llmq_params.type << work_index.nHeight; + chainlock->first.Serialize(hasher, /*specificLegacyScheme=*/!DeploymentActiveAt(work_index, consensus_params, + Consensus::DEPLOYMENT_V19)); + return hasher.GetHash(); + } + return ::SerializeHash(std::make_pair(llmq_params.type, work_index.GetBlockHash())); +} + +//! The modifier from the work block's own data, or nullopt when this node +//! does not have the block. One it has but cannot read raises +//! BlockDataReadError. +std::optional ModifierFromWorkBlockData(const Consensus::LLMQParams& llmq_params, + const Consensus::Params& consensus_params, + const node::BlockManager& blockman, + gsl::not_null work_index) +{ + const auto [have_data, block_pos]{WITH_LOCK(::cs_main, return std::make_pair( + (work_index->nStatus & BLOCK_HAVE_DATA) != 0, work_index->GetBlockPos()))}; + if (!have_data) return std::nullopt; + CBlock block; + if (blockman.ReadBlockFromDisk(block, block_pos) != work_index->GetBlockHash()) { + throw BlockDataReadError( + strprintf("quorum work block %s cannot be read", work_index->GetBlockHash().ToString())); + } + return ModifierFromBlock(llmq_params, consensus_params, block, *work_index); +} + +//! The exact modifier of a v20 work block: the seeded value or the block's +//! own, which must agree when both exist. With neither, as for a block this +//! node pruned, it is unavailable in every view; it is never guessed from the +//! block hash. A block this node has but cannot read is a read error in any +//! view. +uint256 ResolveWorkBlockModifier(const Consensus::LLMQParams& llmq_params, const Consensus::Params& consensus_params, + const node::BlockManager& blockman, gsl::not_null work_index, + const llmq::CQuorumSnapshotManager& qsnapman) +{ + const auto seeded{qsnapman.GetSeededQuorumModifier(llmq_params.type, work_index->GetBlockHash())}; + const auto from_block{ModifierFromWorkBlockData(llmq_params, consensus_params, blockman, work_index)}; + if (seeded && from_block && *seeded != *from_block) { + throw evo::SnapshotStateMismatchError( + strprintf("seeded quorum score modifier for work block %s differs", work_index->GetBlockHash().ToString())); + } + if (from_block) return *from_block; + if (seeded) return *seeded; + throw llmq::utils::WorkBlockModifierUnavailableError( + strprintf("quorum score modifier of work block %s is not available", work_index->GetBlockHash().ToString())); +} + +//! The modifier of a cycle, through the exact work-block resolver once v20 is +//! active at its work block. +uint256 GetHashModifier(const Consensus::LLMQParams& llmq_params, const Consensus::Params& consensus_params, + const node::BlockManager& blockman, gsl::not_null cycle_index, + const llmq::CQuorumSnapshotManager& qsnapman) +{ + const CBlockIndex* work_index{cycle_index->GetAncestor(cycle_index->nHeight - llmq::WORK_DIFF_DEPTH)}; + if (DeploymentActiveAfter(work_index, consensus_params, Consensus::DEPLOYMENT_V20)) { + return ResolveWorkBlockModifier(llmq_params, consensus_params, blockman, Assert(work_index), qsnapman); + } + return CalculateHashModifier(llmq_params, consensus_params, cycle_index); +} + std::vector CalculateScoresForQuorum(QuorumMembers&& dmns, const uint256& modifier, const bool onlyEvoNodes) { std::vector scores; @@ -189,6 +246,7 @@ QuorumMembers CalculateQuorum(List&& mn_list, const uint256& modifier, size_t ma std::vector GetQuorumQuarterMembersBySnapshot(const Consensus::LLMQParams& llmqParams, CDeterministicMNManager& dmnman, + const llmq::CQuorumSnapshotManager& qsnapman, const Consensus::Params& consensus_params, const node::BlockManager& blockman, const CBlockIndex* pCycleQuorumBaseBlockIndex, @@ -204,7 +262,7 @@ std::vector GetQuorumQuarterMembersBySnapshot(const Consensus::LL const CBlockIndex* pWorkBlockIndex = pCycleQuorumBaseBlockIndex->GetAncestor( pCycleQuorumBaseBlockIndex->nHeight - llmq::WORK_DIFF_DEPTH); auto mn_list = dmnman.GetListForBlock(pWorkBlockIndex); - const auto modifier = GetHashModifier(llmqParams, consensus_params, blockman, pCycleQuorumBaseBlockIndex); + const auto modifier = GetHashModifier(llmqParams, consensus_params, blockman, pCycleQuorumBaseBlockIndex, qsnapman); auto sortedAllMns = CalculateQuorum(mn_list, modifier); std::vector usedMNs; @@ -292,7 +350,8 @@ std::vector GetQuorumQuarterMembersBySnapshot(const Consensus::LL } QuorumMembers ComputeQuorumMembers(Consensus::LLMQType llmqType, const CChainParams& chainparams, const node::BlockManager& blockman, - const CDeterministicMNList& mn_list, const CBlockIndex* pQuorumBaseBlockIndex) + const CDeterministicMNList& mn_list, const CBlockIndex* pQuorumBaseBlockIndex, + const llmq::CQuorumSnapshotManager& qsnapman) { bool EvoOnly = (chainparams.GetConsensus().llmqTypePlatform == llmqType) && DeploymentActiveAfter(pQuorumBaseBlockIndex, chainparams.GetConsensus(), Consensus::DEPLOYMENT_V19); @@ -303,14 +362,14 @@ QuorumMembers ComputeQuorumMembers(Consensus::LLMQType llmqType, const CChainPar return {}; } - const auto modifier = GetHashModifier(llmq_params_opt.value(), chainparams.GetConsensus(), blockman, pQuorumBaseBlockIndex); + const auto modifier = GetHashModifier(llmq_params_opt.value(), chainparams.GetConsensus(), blockman, pQuorumBaseBlockIndex, + qsnapman); return CalculateQuorum(mn_list, modifier, llmq_params_opt->size, EvoOnly); } -void BuildQuorumSnapshot(const Consensus::LLMQParams& llmqParams, const Consensus::Params& consensus_params, const node::BlockManager& blockman, - const CDeterministicMNList& allMns, const CDeterministicMNList& mnUsedAtH, - llmq::CQuorumSnapshot& quorumSnapshot, std::vector& skipList, - const CBlockIndex* pCycleQuorumBaseBlockIndex) +void BuildQuorumSnapshot(const Consensus::LLMQParams& llmqParams, const CDeterministicMNList& allMns, + const CDeterministicMNList& mnUsedAtH, llmq::CQuorumSnapshot& quorumSnapshot, + std::vector& skipList, const CBlockIndex* pCycleQuorumBaseBlockIndex, const uint256& modifier) { if (!llmqParams.useRotation || pCycleQuorumBaseBlockIndex->nHeight % llmqParams.dkgInterval != 0) { ASSERT_IF_DEBUG(false); @@ -319,7 +378,6 @@ void BuildQuorumSnapshot(const Consensus::LLMQParams& llmqParams, const Consensu const auto allMnsTotal = allMns.GetCounts().total(); quorumSnapshot.activeQuorumMembers.resize(allMnsTotal); - const auto modifier = GetHashModifier(llmqParams, consensus_params, blockman, pCycleQuorumBaseBlockIndex); auto sortedAllMns = CalculateQuorum(allMns, modifier); LogPrint(BCLog::LLMQ, "BuildQuorumSnapshot h[%d] numMns[%d]\n", pCycleQuorumBaseBlockIndex->nHeight, @@ -460,8 +518,9 @@ std::vector BuildNewQuorumQuarterMembers(const Consensus::LLMQPar if (storeSnapshot) { llmq::CQuorumSnapshot quorumSnapshot{}; - BuildQuorumSnapshot(llmqParams, util_params.m_chainman.GetConsensus(), util_params.m_chainman.m_blockman, allMns, - MnsUsedAtH, quorumSnapshot, skipList, util_params.m_base_index); + // Index the bitset in member-selection order, including when the + // modifier came from a seeded value. + BuildQuorumSnapshot(llmqParams, allMns, MnsUsedAtH, quorumSnapshot, skipList, util_params.m_base_index, modifier); util_params.m_qsnapman.StoreSnapshotForBlock(llmqParams.type, util_params.m_base_index, quorumSnapshot); } @@ -500,17 +559,29 @@ std::vector ComputeQuorumMembersByQuarterRotation(const Consensus opt_snap.has_value()) { prev_cycles[idx]->m_snap = opt_snap.value(); } else { + // Below a snapshot base only carried quarters exist. Computing or + // predicting from fewer quarters, or reporting them as not yet + // available, would misstate the result, so report a missing + // applicable quarter as unavailable instead. A cycle that never + // had one (before rotation or the type) keeps the legacy break. + const CBlockIndex* missing{prev_cycles[idx]->m_cycle_index}; + if (util_params.m_qsnapman.IsCarriedOnly(missing) && llmq::IsQuorumRotationEnabled(llmqParams, missing) && + util_params.m_chainman.IsQuorumTypeEnabled(llmqParams.type, missing->pprev)) { + throw BlockDataUnavailableError(strprintf("Quorum snapshot for block %s %s", + prev_cycles[idx]->m_cycle_index->GetBlockHash().ToString(), + BLOCK_DATA_UNAVAILABLE_SUFFIX)); + } if (predicting) return {}; // TODO: Check if it is triggered from outside (P2P, block validation) and maybe throw an exception // assert(false); break; } prev_cycles[idx]->m_members = GetQuorumQuarterMembersBySnapshot(llmqParams, util_params.m_dmnman, + util_params.m_qsnapman, util_params.m_chainman.GetConsensus(), util_params.m_chainman.m_blockman, prev_cycles[idx]->m_cycle_index, - prev_cycles[idx]->m_snap, - cycleBaseHeight); + prev_cycles[idx]->m_snap, cycleBaseHeight); } auto newQuarterMembers = BuildNewQuorumQuarterMembers(llmqParams, util_params, allMns, previousQuarters, modifier, @@ -614,6 +685,19 @@ std::vector> GetEnabledQuoru } namespace utils { +uint256 GetQuorumModifierForWorkBlock(const Consensus::LLMQParams& llmq_params, const Consensus::Params& consensus_params, + const node::BlockManager& blockman, gsl::not_null work_index, + const CQuorumSnapshotManager& qsnapman) +{ + return ResolveWorkBlockModifier(llmq_params, consensus_params, blockman, work_index, qsnapman); +} + +uint256 GetWorkBlockModifier(const Consensus::LLMQParams& llmq_params, const Consensus::Params& consensus_params, + const CBlock& block, const CBlockIndex& work_index) +{ + return ModifierFromBlock(llmq_params, consensus_params, block, work_index); +} + BlsCheck::BlsCheck() = default; BlsCheck::BlsCheck(CBLSSignature sig, std::vector pubkeys, uint256 msg_hash, std::string id_string) : @@ -629,12 +713,12 @@ BlsCheck::~BlsCheck() = default; bool BlsCheck::operator()() { if (m_pubkeys.size() > 1) { - if (!m_sig.VerifySecureAggregated(m_pubkeys, m_msg_hash)) { + if (!m_sig.VerifySecureAggregated(m_pubkeys, m_msg_hash, m_legacy_scheme)) { LogPrint(BCLog::LLMQ, "%s\n", m_id_string); return false; } } else if (m_pubkeys.size() == 1) { - if (!m_sig.VerifyInsecure(m_pubkeys.back(), m_msg_hash)) { + if (!m_sig.VerifyInsecure(m_pubkeys.back(), m_msg_hash, m_legacy_scheme)) { LogPrint(BCLog::LLMQ, "%s\n", m_id_string); return false; } @@ -652,6 +736,7 @@ void BlsCheck::swap(BlsCheck& obj) noexcept std::swap(m_pubkeys, obj.m_pubkeys); std::swap(m_msg_hash, obj.m_msg_hash); std::swap(m_id_string, obj.m_id_string); + std::swap(m_legacy_scheme, obj.m_legacy_scheme); } std::optional> ComputeQuorumMembersFromWorkBlock( @@ -679,7 +764,8 @@ std::optional> ComputeQuorumMembersFromWorkBlo return std::nullopt; } - const auto modifier = GetHashModifierFromWorkBlock(llmq_params, util_params.m_chainman.m_blockman, pWorkBlockIndex.get()); + const auto modifier = ResolveWorkBlockModifier(llmq_params, consensus, util_params.m_chainman.m_blockman, pWorkBlockIndex, + util_params.m_qsnapman); if (!llmq_params.useRotation) { // Canonical member selection gates EvoOnly on V19 at the future quorum base block; V19 @@ -702,9 +788,11 @@ std::optional> ComputeQuorumMembersFromWorkBlo QuorumMembers GetAllQuorumMembers(Consensus::LLMQType llmqType, const UtilParameters& util_params, bool reset_cache) { static RecursiveMutex cs_members; - static PerLlmqTypeCache mapQuorumMembers GUARDED_BY(cs_members); + static EvoViewCache> members_by_view GUARDED_BY(cs_members); static RecursiveMutex cs_indexed_members; - static PerLlmqTypeCache> mapIndexedQuorumMembers GUARDED_BY(cs_indexed_members); + static EvoViewCache>> indexed_members_by_view GUARDED_BY( + cs_indexed_members); + const EvoViewToken view{util_params.m_qsnapman.CurrentView()}; // A parentless base index (genesis) can never host a quorum. IsQuorumTypeEnabled() handles the // null, but say so explicitly here: this is reached with an attacker-supplied quorumHash via @@ -717,6 +805,7 @@ QuorumMembers GetAllQuorumMembers(Consensus::LLMQType llmqType, const UtilParame std::vector quorumMembers; { LOCK(cs_members); + auto& mapQuorumMembers{members_by_view.For(view)}; if (!mapQuorumMembers.IsInitialized()) { mapQuorumMembers.Init(util_params.m_chainman.GetConsensus()); } @@ -732,8 +821,8 @@ QuorumMembers GetAllQuorumMembers(Consensus::LLMQType llmqType, const UtilParame const auto& llmq_params = llmq_params_opt.value(); if (IsQuorumRotationEnabled(llmq_params, util_params.m_base_index)) { - if (LOCK(cs_indexed_members); !mapIndexedQuorumMembers.IsInitialized()) { - mapIndexedQuorumMembers.Init(util_params.m_chainman.GetConsensus()); + if (LOCK(cs_indexed_members); !indexed_members_by_view.For(view).IsInitialized()) { + indexed_members_by_view.For(view).Init(util_params.m_chainman.GetConsensus()); } /* * Quorums created with rotation are now created in a different way. All signingActiveQuorumCount are created @@ -758,24 +847,26 @@ QuorumMembers GetAllQuorumMembers(Consensus::LLMQType llmqType, const UtilParame */ if (reset_cache) { LOCK(cs_indexed_members); - mapIndexedQuorumMembers.clear(llmqType); - } else if (LOCK(cs_indexed_members); mapIndexedQuorumMembers.get( + indexed_members_by_view.For(view).clear(llmqType); + } else if (LOCK(cs_indexed_members); indexed_members_by_view.For(view).get( llmqType, std::pair(pCycleQuorumBaseBlockIndex->GetBlockHash(), quorumIndex), quorumMembers)) { LOCK(cs_members); - mapQuorumMembers.insert(llmqType, util_params.m_base_index->GetBlockHash(), quorumMembers); + members_by_view.For(view).insert(llmqType, util_params.m_base_index->GetBlockHash(), quorumMembers); return quorumMembers; } const CBlockIndex* pWorkBlockIndex = pCycleQuorumBaseBlockIndex->GetAncestor(cycleQuorumBaseHeight - WORK_DIFF_DEPTH); const auto modifier = GetHashModifier(llmq_params, util_params.m_chainman.GetConsensus(), - util_params.m_chainman.m_blockman, pCycleQuorumBaseBlockIndex); + util_params.m_chainman.m_blockman, pCycleQuorumBaseBlockIndex, + util_params.m_qsnapman); auto q = ComputeQuorumMembersByQuarterRotation(llmq_params, util_params.replace_index(pCycleQuorumBaseBlockIndex), pWorkBlockIndex, cycleQuorumBaseHeight, modifier, /*predicting=*/false); quorumMembers = q[quorumIndex]; LOCK(cs_indexed_members); + auto& mapIndexedQuorumMembers{indexed_members_by_view.For(view)}; for (const size_t i : util::irange(q.size())) { mapIndexedQuorumMembers.emplace(llmqType, std::make_pair(pCycleQuorumBaseBlockIndex->GetBlockHash(), i), std::move(q[i])); @@ -789,11 +880,11 @@ QuorumMembers GetAllQuorumMembers(Consensus::LLMQType llmqType, const UtilParame : util_params.m_base_index.get(); CDeterministicMNList mn_list = util_params.m_dmnman.GetListForBlock(pWorkBlockIndex); quorumMembers = ComputeQuorumMembers(llmqType, util_params.m_chainman.GetParams(), util_params.m_chainman.m_blockman, - mn_list, util_params.m_base_index); + mn_list, util_params.m_base_index, util_params.m_qsnapman); } LOCK(cs_members); - mapQuorumMembers.insert(llmqType, util_params.m_base_index->GetBlockHash(), quorumMembers); + members_by_view.For(view).insert(llmqType, util_params.m_base_index->GetBlockHash(), quorumMembers); return quorumMembers; } diff --git a/src/llmq/utils.h b/src/llmq/utils.h index b471c084a767..15a4436019d0 100644 --- a/src/llmq/utils.h +++ b/src/llmq/utils.h @@ -6,6 +6,7 @@ #define BITCOIN_LLMQ_UTILS_H #include +#include #include #include #include @@ -21,6 +22,7 @@ #include #include +class CBlock; class CBlockIndex; class CDeterministicMNManager; class ChainstateManager; @@ -28,6 +30,9 @@ class CSporkManager; namespace llmq { class CQuorumSnapshotManager; } // namespace llmq +namespace node { +class BlockManager; +} // namespace node namespace llmq { struct UtilParameters { @@ -54,11 +59,34 @@ std::vector> GetEnabledQuoru const ChainstateManager& chainman, gsl::not_null pindex); namespace utils { +//! The exact score modifier of a v20 work block is not known: nothing was +//! seeded for it and this node does not have the block. +class WorkBlockModifierUnavailableError : public BlockDataUnavailableError +{ +public: + using BlockDataUnavailableError::BlockDataUnavailableError; +}; + +/** The exact score modifier of a v20 work block: its seeded value or the one + * its coinbase ChainLock gives, checked against each other when both exist. + * Without either it raises WorkBlockModifierUnavailableError in every view. + * A work block this node has but cannot read raises BlockDataReadError. */ +uint256 GetQuorumModifierForWorkBlock(const Consensus::LLMQParams& llmq_params, const Consensus::Params& consensus_params, + const node::BlockManager& blockman, gsl::not_null work_index, + const CQuorumSnapshotManager& qsnapman); +/** The score modifier `block`, a v20 work block whose index is `work_index`, + * gives from its coinbase ChainLock, decoded with the block's own BLS scheme + * rather than the one the node is in. */ +uint256 GetWorkBlockModifier(const Consensus::LLMQParams& llmq_params, const Consensus::Params& consensus_params, + const CBlock& block, const CBlockIndex& work_index); + struct BlsCheck { CBLSSignature m_sig; std::vector m_pubkeys; uint256 m_msg_hash; std::string m_id_string; + //! The building thread's scheme; CCheckQueue runs the check on another thread + bool m_legacy_scheme{bls::EffectiveLegacyScheme()}; public: BlsCheck(); diff --git a/src/net.cpp b/src/net.cpp index d072c33aa305..be8473c34537 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -42,6 +42,7 @@ #include #include +#include #include #include #include @@ -61,6 +62,7 @@ #include #include #include +#include #include #include @@ -3168,7 +3170,12 @@ void CConnman::ThreadOpenConnections(const std::vector connect, CDe addrman.ResolveCollisions(); - auto mnList = dmnman.GetListAtChainTip(); + // Without the masternode list, connected masternodes cannot be + // skipped below; try again next round. + std::optional tip_list; + RunActiveEvoRound(&dmnman.EvoDb(), "opencon", [&] { tip_list = dmnman.GetListAtChainTip(); }); + if (!tip_list) continue; + const auto& mnList{*tip_list}; const auto current_time{NodeClock::now()}; int nTries = 0; @@ -3445,7 +3452,10 @@ void CConnman::ThreadOpenMasternodeConnections(CDeterministicMNManager& dmnman, } }); - auto mnList = dmnman.GetListAtChainTip(); + std::optional tip_list; + RunActiveEvoRound(&dmnman.EvoDb(), "mncon", [&] { tip_list = dmnman.GetListAtChainTip(); }); + if (!tip_list) continue; + const auto& mnList{*tip_list}; if (interruptNet) return; @@ -3692,7 +3702,28 @@ void CConnman::OpenMasternodeConnection(const CAddress &addrConnect, bool use_v2 Mutex NetEventsInterface::g_msgproc_mutex; -void CConnman::ThreadMessageHandler() +bool CConnman::ServicePeer(const CEvoDB& evodb, CNode& node, bool send) +{ + bool more_work{false}; + try { + const ActiveEvoViewScope evo_view{&evodb}; + more_work = m_msgproc->ProcessMessages(&node, flagInterruptMsgProc); + } catch (const evo::LocalStateError& e) { + LogPrint(BCLog::NET, "peer=%d: Evo state unavailable: %s\n", node.GetId(), e.what()); + } + // Sends run even when receiving stopped early: they include the peer's + // discouragement and ping timeout. + if (!send || flagInterruptMsgProc) return more_work; + try { + const ActiveEvoViewScope evo_view{&evodb}; + m_msgproc->SendMessages(&node); + } catch (const evo::LocalStateError& e) { + LogPrint(BCLog::NET, "peer=%d: Evo state unavailable: %s\n", node.GetId(), e.what()); + } + return more_work; +} + +void CConnman::ThreadMessageHandler(const CEvoDB& evodb) { LOCK(NetEventsInterface::g_msgproc_mutex); @@ -3718,16 +3749,9 @@ void CConnman::ThreadMessageHandler() if (pnode->fDisconnect) continue; - // Receive messages - bool fMoreNodeWork = m_msgproc->ProcessMessages(pnode, flagInterruptMsgProc); + const bool send{!fSkipSendMessagesForMasternodes || !pnode->m_masternode_connection}; + const bool fMoreNodeWork{ServicePeer(evodb, *pnode, send)}; fMoreWork |= (fMoreNodeWork && !pnode->fPauseSend); - if (flagInterruptMsgProc) - return; - // Send messages - if (!fSkipSendMessagesForMasternodes || !pnode->m_masternode_connection) { - m_msgproc->SendMessages(pnode); - } - if (flagInterruptMsgProc) return; } @@ -4124,7 +4148,8 @@ bool CConnman::Start(CDeterministicMNManager& dmnman, CMasternodeMetaMan& mn_met } // Process messages - threadMessageHandler = std::thread(&util::TraceThread, "msghand", [this] { ThreadMessageHandler(); }); + threadMessageHandler = std::thread(&util::TraceThread, "msghand", + [this, &evodb = dmnman.EvoDb()] { ThreadMessageHandler(evodb); }); if (m_i2p_sam_session) { threadI2PAcceptIncoming = diff --git a/src/net.h b/src/net.h index bfd88be7a5d9..30778728ade9 100644 --- a/src/net.h +++ b/src/net.h @@ -57,6 +57,7 @@ class BanMan; class CConnman; class CDeterministicMNList; class CDeterministicMNManager; +class CEvoDB; class CMasternodeMetaMan; class CMasternodeSync; class CNode; @@ -1629,7 +1630,14 @@ friend class CNode; void ThreadOpenConnections(const std::vector connect, CDeterministicMNManager& dmnman) EXCLUSIVE_LOCKS_REQUIRED(!m_addr_fetches_mutex, !m_added_nodes_mutex, !m_nodes_mutex, !m_reconnections_mutex, !m_unused_i2p_sessions_mutex, !mutexMsgProc, !cs_mapSocketToNode); - void ThreadMessageHandler() EXCLUSIVE_LOCKS_REQUIRED(!m_nodes_mutex, !mutexMsgProc); + void ThreadMessageHandler(const CEvoDB& evodb) EXCLUSIVE_LOCKS_REQUIRED(!m_nodes_mutex, !mutexMsgProc); + /** One message-handler turn for a peer: receive (its queued requests, + * orphans to reconsider, then its next message) and, if `send`, send. + * Both halves are bound to the active Evo view. A local Evo state error + * ends the half it occurs in, never the thread, and the sends still run + * after receiving stopped early. Returns whether the peer has more work. */ + bool ServicePeer(const CEvoDB& evodb, CNode& node, bool send) + EXCLUSIVE_LOCKS_REQUIRED(NetEventsInterface::g_msgproc_mutex, !m_nodes_mutex, !mutexMsgProc); void ThreadI2PAcceptIncoming(CMasternodeSync& mn_sync) EXCLUSIVE_LOCKS_REQUIRED(!m_nodes_mutex, !mutexMsgProc, !cs_mapSocketToNode); void AcceptConnection(const ListenSocket& hListenSocket, CMasternodeSync& mn_sync) EXCLUSIVE_LOCKS_REQUIRED(!m_nodes_mutex, !mutexMsgProc, !cs_mapSocketToNode); diff --git a/src/net_processing.cpp b/src/net_processing.cpp index a38b79088cec..4e706e6afbc0 100644 --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -52,6 +53,7 @@ #include #include #include +#include #include #include #include @@ -593,7 +595,7 @@ class PeerManagerImpl final : public PeerManager } /** Overridden from CValidationInterface. */ - void BlockConnected(const std::shared_ptr& pblock, const CBlockIndex* pindexConnected) override + void BlockConnected(ChainstateRole role, const std::shared_ptr& pblock, const CBlockIndex* pindexConnected) override EXCLUSIVE_LOCKS_REQUIRED(!m_object_request_mutex, !m_peer_mutex, !m_recent_confirmed_transactions_mutex); void BlockDisconnected(const std::shared_ptr &block, const CBlockIndex* pindex) override EXCLUSIVE_LOCKS_REQUIRED(!m_recent_confirmed_transactions_mutex); @@ -1106,6 +1108,38 @@ class PeerManagerImpl final : public PeerManager */ void FindNextBlocksToDownload(const Peer& peer, unsigned int count, std::vector& vBlocks, NodeId& nodeStaller) EXCLUSIVE_LOCKS_REQUIRED(cs_main); + /** Request blocks for the background chainstate, if one is in use. */ + void TryDownloadingHistoricalBlocks(const Peer& peer, unsigned int count, std::vector& vBlocks, const CBlockIndex* from_tip, const CBlockIndex* target_block) EXCLUSIVE_LOCKS_REQUIRED(cs_main); + + /** + * \brief Find next blocks to download from a peer after a starting block. + * + * \param vBlocks Vector of blocks to download which will be appended to. + * \param peer Peer which blocks will be downloaded from. + * \param state Pointer to the state of the peer. + * \param pindexWalk Pointer to the starting block to add to vBlocks. + * \param count Maximum number of blocks to allow in vBlocks. No more + * blocks will be added if it reaches this size. + * \param nWindowEnd Maximum height of blocks to allow in vBlocks. No + * blocks will be added above this height. + * \param activeChain Optional pointer to a chain to compare against. If + * provided, any next blocks which are already contained + * in this chain will not be appended to vBlocks, but + * instead will be used to update the + * state->pindexLastCommonBlock pointer. + * \param nodeStaller Optional pointer to a NodeId variable that will receive + * the ID of another peer that might be causing this peer + * to stall. This is set to the ID of the peer which + * first requested the first in-flight block in the + * download window. It is only set if vBlocks is empty at + * the end of this function call and if increasing + * nWindowEnd by 1 would cause it to be non-empty (which + * indicates the download might be stalled because every + * block in the window is in flight and no other peer is + * trying to download the next block). + */ + void FindNextBlocks(std::vector& vBlocks, const Peer& peer, CNodeState *state, const CBlockIndex *pindexWalk, unsigned int count, int nWindowEnd, const CChain* activeChain=nullptr, NodeId* nodeStaller=nullptr) EXCLUSIVE_LOCKS_REQUIRED(cs_main); + /* Multimap used to preserve insertion order */ typedef std::multimap::iterator>> BlockDownloadMap; BlockDownloadMap mapBlocksInFlight GUARDED_BY(cs_main); @@ -1120,6 +1154,11 @@ class PeerManagerImpl final : public PeerManager void ProcessGetData(CNode& pfrom, Peer& peer, const std::atomic& interruptMsgProc) EXCLUSIVE_LOCKS_REQUIRED(!m_most_recent_block_mutex, peer.m_getdata_requests_mutex, NetEventsInterface::g_msgproc_mutex) LOCKS_EXCLUDED(::cs_main); + /** Serve the requests at the front of the peer's GETDATA queue, collecting + * the ones not found, or whose lookup threw, in vNotFound. */ + void ServeGetData(CNode& pfrom, Peer& peer, const std::atomic& interruptMsgProc, std::vector& vNotFound) + EXCLUSIVE_LOCKS_REQUIRED(!m_most_recent_block_mutex, peer.m_getdata_requests_mutex, + NetEventsInterface::g_msgproc_mutex) LOCKS_EXCLUDED(::cs_main); /** Process a new block. Perform any post-processing housekeeping */ void ProcessBlock(CNode& from, const std::shared_ptr& pblock, bool force_processing); @@ -1510,6 +1549,7 @@ void PeerManagerImpl::UpdateBlockAvailability(NodeId nodeid, const uint256 &hash } } +// Logic for calculating which blocks to download from a given peer, given our current tip. void PeerManagerImpl::FindNextBlocksToDownload(const Peer& peer, unsigned int count, std::vector& vBlocks, NodeId& nodeStaller) { if (count == 0) @@ -1539,12 +1579,47 @@ void PeerManagerImpl::FindNextBlocksToDownload(const Peer& peer, unsigned int co if (state->pindexLastCommonBlock == state->pindexBestKnownBlock) return; - std::vector vToFetch; const CBlockIndex *pindexWalk = state->pindexLastCommonBlock; // Never fetch further than the best block we know the peer has, or more than BLOCK_DOWNLOAD_WINDOW + 1 beyond the last // linked block we have in common with this peer. The +1 is so we can detect stalling, namely if we would be able to // download that next block if the window were 1 larger. int nWindowEnd = state->pindexLastCommonBlock->nHeight + BLOCK_DOWNLOAD_WINDOW; + + FindNextBlocks(vBlocks, peer, state, pindexWalk, count, nWindowEnd, &m_chainman.ActiveChain(), &nodeStaller); +} + +void PeerManagerImpl::TryDownloadingHistoricalBlocks(const Peer& peer, unsigned int count, std::vector& vBlocks, const CBlockIndex *from_tip, const CBlockIndex* target_block) +{ + Assert(from_tip); + Assert(target_block); + + if (vBlocks.size() >= count) { + return; + } + + vBlocks.reserve(count); + CNodeState *state = Assert(State(peer.m_id)); + + if (state->pindexBestKnownBlock == nullptr || state->pindexBestKnownBlock->GetAncestor(target_block->nHeight) != target_block) { + // This peer can't provide us the complete series of blocks leading up to the + // assumeutxo snapshot base. + // + // Presumably this peer's chain has less work than our ActiveChain()'s tip, or else we + // will eventually crash when we try to reorg to it. Let other logic + // deal with whether we disconnect this peer. + // + // TODO at some point in the future, we might choose to request what blocks + // this peer does have from the historical chain, despite it not having a + // complete history beneath the snapshot base. + return; + } + + FindNextBlocks(vBlocks, peer, state, from_tip, count, std::min(from_tip->nHeight + BLOCK_DOWNLOAD_WINDOW, target_block->nHeight)); +} + +void PeerManagerImpl::FindNextBlocks(std::vector& vBlocks, const Peer& peer, CNodeState *state, const CBlockIndex *pindexWalk, unsigned int count, int nWindowEnd, const CChain* activeChain, NodeId* nodeStaller) +{ + std::vector vToFetch; int nMaxHeight = std::min(state->pindexBestKnownBlock->nHeight, nWindowEnd + 1); NodeId waitingfor = -1; while (pindexWalk->nHeight < nMaxHeight) { @@ -1568,8 +1643,8 @@ void PeerManagerImpl::FindNextBlocksToDownload(const Peer& peer, unsigned int co // We consider the chain that this peer is on invalid. return; } - if (pindex->nStatus & BLOCK_HAVE_DATA || m_chainman.ActiveChain().Contains(pindex)) { - if (pindex->HaveTxsDownloaded()) + if (pindex->nStatus & BLOCK_HAVE_DATA || (activeChain && activeChain->Contains(pindex))) { + if (activeChain && pindex->HaveNumChainTxs()) state->pindexLastCommonBlock = pindex; } else if (!IsBlockRequested(pindex->GetBlockHash())) { // The block is not already downloaded, and not yet in flight. @@ -1577,7 +1652,7 @@ void PeerManagerImpl::FindNextBlocksToDownload(const Peer& peer, unsigned int co // We reached the end of the window. if (vBlocks.size() == 0 && waitingfor != peer.m_id) { // We aren't able to fetch anything, but we would be if the download window was one larger. - nodeStaller = waitingfor; + if (nodeStaller) *nodeStaller = waitingfor; } return; } @@ -2217,8 +2292,31 @@ void PeerManagerImpl::StartScheduledTasks(CScheduler& scheduler) * block. Also save the time of the last tip update and * possibly reduce dynamic block stalling timeout. */ -void PeerManagerImpl::BlockConnected(const std::shared_ptr& pblock, const CBlockIndex* pindex) +void PeerManagerImpl::BlockConnected( + ChainstateRole role, + const std::shared_ptr& pblock, + const CBlockIndex* pindex) { + // Update this for all chainstate roles so that we don't mistakenly see peers + // helping us do background IBD as having a stale tip. + m_last_tip_update = GetTime(); + + // In case the dynamic timeout was doubled once or more, reduce it slowly back to its default value + auto stalling_timeout = m_block_stalling_timeout.load(); + Assume(stalling_timeout >= BLOCK_STALLING_TIMEOUT_DEFAULT); + if (stalling_timeout != BLOCK_STALLING_TIMEOUT_DEFAULT) { + const auto new_timeout = std::max(std::chrono::duration_cast(stalling_timeout * 0.85), BLOCK_STALLING_TIMEOUT_DEFAULT); + if (m_block_stalling_timeout.compare_exchange_strong(stalling_timeout, new_timeout)) { + LogPrint(BCLog::NET, "Decreased stalling timeout to %d seconds\n", count_seconds(new_timeout)); + } + } + + // The following tasks can be skipped since we don't maintain a mempool, or + // follow ChainLocks, for the ibd/background chainstate. + if (role == ChainstateRole::BACKGROUND) { + return; + } + PostProcessMessage(m_clhandler.ProcessCoinbaseChainLock(*pblock, pindex, *m_llmq_ctx.qman), /*node=*/-1); // Orphans included in or conflicted by the block can never be accepted, so drop them before @@ -2240,8 +2338,6 @@ void PeerManagerImpl::BlockConnected(const std::shared_ptr& pblock } } - m_last_tip_update = GetTime(); - { LOCK(m_recent_confirmed_transactions_mutex); for (const auto& ptx : pblock->vtx) { @@ -2253,16 +2349,6 @@ void PeerManagerImpl::BlockConnected(const std::shared_ptr& pblock } } } - - // In case the dynamic timeout was doubled once or more, reduce it slowly back to its default value - auto stalling_timeout = m_block_stalling_timeout.load(); - Assume(stalling_timeout >= BLOCK_STALLING_TIMEOUT_DEFAULT); - if (stalling_timeout != BLOCK_STALLING_TIMEOUT_DEFAULT) { - const auto new_timeout = std::max(std::chrono::duration_cast(stalling_timeout * 0.85), BLOCK_STALLING_TIMEOUT_DEFAULT); - if (m_block_stalling_timeout.compare_exchange_strong(stalling_timeout, new_timeout)) { - LogPrint(BCLog::NET, "Decreased stalling timeout to %d seconds\n", count_seconds(new_timeout)); - } - } } void PeerManagerImpl::BlockDisconnected(const std::shared_ptr &block, const CBlockIndex* pindex) @@ -2821,7 +2907,7 @@ void PeerManagerImpl::ProcessGetBlockData(CNode& pfrom, Peer& peer, const CInv& LOCK(cs_main); const CBlockIndex* pindex = m_chainman.m_blockman.LookupBlockIndex(inv.hash); if (pindex) { - if (pindex->HaveTxsDownloaded() && !pindex->IsValid(BLOCK_VALID_SCRIPTS) && + if (pindex->HaveNumChainTxs() && !pindex->IsValid(BLOCK_VALID_SCRIPTS) && pindex->IsValid(BLOCK_VALID_TREE)) { // If we have the block and all of its parents, but have not yet validated it, // we might be in the middle of connecting it (ie in the unlock of cs_main @@ -3005,14 +3091,14 @@ CTransactionRef PeerManagerImpl::FindTxForGetData(const Peer::TxRelay& tx_relay, return {}; } -void PeerManagerImpl::ProcessGetData(CNode& pfrom, Peer& peer, const std::atomic& interruptMsgProc) +void PeerManagerImpl::ServeGetData(CNode& pfrom, Peer& peer, const std::atomic& interruptMsgProc, + std::vector& vNotFound) { AssertLockNotHeld(cs_main); auto tx_relay = peer.GetTxRelay(); std::deque::iterator it = peer.m_getdata_requests.begin(); - std::vector vNotFound; const CNetMsgMaker msgMaker(pfrom.GetCommonVersion()); const auto now{GetTime()}; @@ -3031,12 +3117,14 @@ void PeerManagerImpl::ProcessGetData(CNode& pfrom, Peer& peer, const std::atomic if (pfrom.fPauseSend) break; - const CInv &inv = *it; + const CInv inv{*it}; if (inv.type == MSG_BLOCK || inv.type == MSG_FILTERED_BLOCK || inv.type == MSG_CMPCT_BLOCK) { break; } - ++it; + // Dequeued before it is served: a lookup that throws leaves neither + // this request nor the ones served before it queued. + it = peer.m_getdata_requests.erase(it); if (tx_relay == nullptr && NetMessageViolatesBlocksOnly(inv.GetCommand())) { // Note that if we receive a getdata for non-block messages @@ -3045,6 +3133,8 @@ void PeerManagerImpl::ProcessGetData(CNode& pfrom, Peer& peer, const std::atomic continue; } + // Not found until it is served, including when a lookup throws. + vNotFound.push_back(inv); bool push = false; if (inv.IsGenTxMsg()) { CTransactionRef tx = FindTxForGetData(*tx_relay, inv.hash, mempool_req, now); @@ -3145,23 +3235,36 @@ void PeerManagerImpl::ProcessGetData(CNode& pfrom, Peer& peer, const std::atomic } } - if (!push) { - vNotFound.push_back(inv); + if (push) { + vNotFound.pop_back(); } } // Only process one BLOCK item per call, since they're uncommon and can be // expensive to process. if (it != peer.m_getdata_requests.end() && !pfrom.fPauseSend) { - const CInv &inv = *it++; + const CInv inv{*it}; + it = peer.m_getdata_requests.erase(it); if (inv.IsGenBlkMsg()) { ProcessGetBlockData(pfrom, peer, inv); } // else: If the first item on the queue is an unknown type, we erase it // and continue processing the queue on the next call. } +} - peer.m_getdata_requests.erase(peer.m_getdata_requests.begin(), it); +void PeerManagerImpl::ProcessGetData(CNode& pfrom, Peer& peer, const std::atomic& interruptMsgProc) +{ + std::vector vNotFound; + // A lookup this node's Evo state cannot answer ends the batch. The + // requests found missing before it, and the one that failed, have left the + // queue, so they are still answered: this node cannot serve them. + std::exception_ptr local_error; + try { + ServeGetData(pfrom, peer, interruptMsgProc, vNotFound); + } catch (const evo::LocalStateError&) { + local_error = std::current_exception(); + } if (!vNotFound.empty()) { // Let the peer know that we didn't find what it asked for, so it doesn't @@ -3178,8 +3281,9 @@ void PeerManagerImpl::ProcessGetData(CNode& pfrom, Peer& peer, const std::atomic // In normal operation, we often send NOTFOUND messages for parents of // transactions that we relay; if a peer is missing a parent, they may // assume we have them and request the parents from us. - m_connman.PushMessage(&pfrom, msgMaker.Make(NetMsgType::NOTFOUND, vNotFound)); + m_connman.PushMessage(&pfrom, CNetMsgMaker{pfrom.GetCommonVersion()}.Make(NetMsgType::NOTFOUND, vNotFound)); } + if (local_error) std::rethrow_exception(local_error); } void PeerManagerImpl::SendBlockTransactions(CNode& pfrom, Peer& peer, const CBlock& block, const BlockTransactionsRequest& req) { @@ -5780,9 +5884,16 @@ void PeerManagerImpl::ProcessMessage( llmq::CQuorumRotationInfo quorumRotationInfoRet; std::string strError; bool use_legacy_construction = pfrom.GetCommonVersion() < EFFICIENT_QRINFO_VERSION;; - const bool built{BuildQuorumRotationInfo(m_dmnman, *m_llmq_ctx.qsnapman, m_chainman, *m_llmq_ctx.qman, - *m_llmq_ctx.quorum_block_processor, cmd, use_legacy_construction, - quorumRotationInfoRet, strError)}; + bool built{false}; + try { + built = BuildQuorumRotationInfo(m_dmnman, *m_llmq_ctx.qsnapman, m_chainman, *m_llmq_ctx.qman, + *m_llmq_ctx.quorum_block_processor, cmd, use_legacy_construction, + quorumRotationInfoRet, strError); + } catch (const evo::LocalStateError& e) { + // This node's Evo state cannot answer: not the peer's fault, but + // charged below like any build that stopped partway. + strError = strprintf("%s %s", e.what(), BLOCK_DATA_UNAVAILABLE_SUFFIX); + } if (!pfrom.HasPermission(NetPermissionFlags::NoBan)) { // The token taken above paid for one diff. Charge the rest of the reply (the tip, H, the // target cycles and the snapshot diffs) now that its size is known. A build that failed @@ -6942,7 +7053,20 @@ bool PeerManagerImpl::SendMessages(CNode* pto) if (CanServeBlocks(*peer) && pto->CanRelay() && ((sync_blocks_and_headers_from_peer && !IsLimitedPeer(*peer)) || !m_chainman.IsInitialBlockDownload()) && state.vBlocksInFlight.size() < MAX_BLOCKS_IN_TRANSIT_PER_PEER) { std::vector vToDownload; NodeId staller = -1; - FindNextBlocksToDownload(*peer, MAX_BLOCKS_IN_TRANSIT_PER_PEER - state.vBlocksInFlight.size(), vToDownload, staller); + auto get_inflight_budget = [&state]() { + return std::max(0, MAX_BLOCKS_IN_TRANSIT_PER_PEER - static_cast(state.vBlocksInFlight.size())); + }; + + // If a snapshot chainstate is in use, we want to find its next blocks + // before the background chainstate to prioritize getting to network tip. + FindNextBlocksToDownload(*peer, get_inflight_budget(), vToDownload, staller); + if (m_chainman.BackgroundSyncInProgress() && !IsLimitedPeer(*peer)) { + TryDownloadingHistoricalBlocks( + *peer, + get_inflight_budget(), + vToDownload, m_chainman.GetBackgroundSyncTip(), + Assert(m_chainman.GetSnapshotBaseBlock())); + } for (const CBlockIndex *pindex : vToDownload) { vGetData.emplace_back(MSG_BLOCK, pindex->GetBlockHash()); BlockRequested(pto->GetId(), *pindex); diff --git a/src/node/blockstorage.cpp b/src/node/blockstorage.cpp index 6323a77a20c7..1eb2495fd876 100644 --- a/src/node/blockstorage.cpp +++ b/src/node/blockstorage.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -159,41 +160,57 @@ void BlockManager::PruneOneBlockFile(const int fileNumber) m_dirty_fileinfo.insert(fileNumber); } -void BlockManager::FindFilesToPruneManual(std::set& setFilesToPrune, int nManualPruneHeight, int chain_tip_height) +void BlockManager::FindFilesToPruneManual( + std::set& setFilesToPrune, + int nManualPruneHeight, + const Chainstate& chain, + ChainstateManager& chainman) { assert(IsPruneMode() && nManualPruneHeight > 0); LOCK2(cs_main, cs_LastBlockFile); - if (chain_tip_height < 0) { + if (chain.m_chain.Height() < 0) { return; } - // last block to prune is the lesser of (user-specified height, MinBlocksToKeep() from the tip) - unsigned int nLastBlockWeCanPrune = std::min((unsigned)nManualPruneHeight, - static_cast(chain_tip_height - GetParams().MinBlocksToKeep())); + const auto [min_block_to_prune, last_block_can_prune] = chainman.GetPruneRange(chain, nManualPruneHeight); + int count = 0; for (int fileNumber = 0; fileNumber < m_last_blockfile; fileNumber++) { - if (m_blockfile_info[fileNumber].nSize == 0 || m_blockfile_info[fileNumber].nHeightLast > nLastBlockWeCanPrune) { + const auto& fileinfo = m_blockfile_info[fileNumber]; + if (fileinfo.nSize == 0 || fileinfo.nHeightLast > (unsigned)last_block_can_prune || fileinfo.nHeightFirst < (unsigned)min_block_to_prune) { continue; } + PruneOneBlockFile(fileNumber); setFilesToPrune.insert(fileNumber); count++; } - LogPrintf("Prune (Manual): prune_height=%d removed %d blk/rev pairs\n", nLastBlockWeCanPrune, count); + LogPrintf("[%s] Prune (Manual): prune_height=%d removed %d blk/rev pairs\n", + chain.GetRole(), last_block_can_prune, count); } -void BlockManager::FindFilesToPrune(std::set& setFilesToPrune, uint64_t nPruneAfterHeight, int chain_tip_height, int prune_height, bool is_ibd) +void BlockManager::FindFilesToPrune( + std::set& setFilesToPrune, + int last_prune, + const Chainstate& chain, + ChainstateManager& chainman) { LOCK2(cs_main, cs_LastBlockFile); - if (chain_tip_height < 0 || GetPruneTarget() == 0) { + // Distribute our -prune budget over all chainstates. The floor of a share is capped + // at the budget itself, because regtest allows -prune below MIN_DISK_SPACE_FOR_BLOCK_FILES. + const auto target = std::max( + std::min(GetPruneTarget(), MIN_DISK_SPACE_FOR_BLOCK_FILES), GetPruneTarget() / chainman.GetAll().size()); + + if (chain.m_chain.Height() < 0 || target == 0) { return; } - if ((uint64_t)chain_tip_height <= nPruneAfterHeight) { + if (static_cast(chain.m_chain.Height()) <= chainman.GetParams().PruneAfterHeight()) { return; } - unsigned int nLastBlockWeCanPrune{(unsigned)std::min(prune_height, chain_tip_height - GetParams().MinBlocksToKeep())}; + const auto [min_block_to_prune, last_block_can_prune] = chainman.GetPruneRange(chain, last_prune); + uint64_t nCurrentUsage = CalculateCurrentUsage(); // We don't check to prune until after we've allocated new space for files // So we should leave a buffer under our target to account for another allocation @@ -202,29 +219,31 @@ void BlockManager::FindFilesToPrune(std::set& setFilesToPrune, uint64_t nPr uint64_t nBytesToPrune; int count = 0; - if (nCurrentUsage + nBuffer >= GetPruneTarget()) { + if (nCurrentUsage + nBuffer >= target) { // On a prune event, the chainstate DB is flushed. // To avoid excessive prune events negating the benefit of high dbcache // values, we should not prune too rapidly. // So when pruning in IBD, increase the buffer a bit to avoid a re-prune too soon. - if (is_ibd) { + if (chainman.IsInitialBlockDownload()) { // Since this is only relevant during IBD, we use a fixed 10% - nBuffer += GetPruneTarget() / 10; + nBuffer += target / 10; } for (int fileNumber = 0; fileNumber < m_last_blockfile; fileNumber++) { - nBytesToPrune = m_blockfile_info[fileNumber].nSize + m_blockfile_info[fileNumber].nUndoSize; + const auto& fileinfo = m_blockfile_info[fileNumber]; + nBytesToPrune = fileinfo.nSize + fileinfo.nUndoSize; - if (m_blockfile_info[fileNumber].nSize == 0) { + if (fileinfo.nSize == 0) { continue; } - if (nCurrentUsage + nBuffer < GetPruneTarget()) { // are we below our target? + if (nCurrentUsage + nBuffer < target) { // are we below our target? break; } - // don't prune files that could have a block within MinBlocksToKeep() of the main chain's tip but keep scanning - if (m_blockfile_info[fileNumber].nHeightLast > nLastBlockWeCanPrune) { + // don't prune files that could have a block that's not within the allowable + // prune range for the chain being pruned. + if (fileinfo.nHeightLast > (unsigned)last_block_can_prune || fileinfo.nHeightFirst < (unsigned)min_block_to_prune) { continue; } @@ -236,10 +255,10 @@ void BlockManager::FindFilesToPrune(std::set& setFilesToPrune, uint64_t nPr } } - LogPrint(BCLog::PRUNE, "target=%dMiB actual=%dMiB diff=%dMiB max_prune_height=%d removed %d blk/rev pairs\n", - GetPruneTarget() / 1024 / 1024, nCurrentUsage / 1024 / 1024, - (int64_t(GetPruneTarget()) - int64_t(nCurrentUsage)) / 1024 / 1024, - nLastBlockWeCanPrune, count); + LogPrint(BCLog::PRUNE, "[%s] target=%dMiB actual=%dMiB diff=%dMiB min_height=%d max_prune_height=%d removed %d blk/rev pairs\n", + chain.GetRole(), target / 1024 / 1024, nCurrentUsage / 1024 / 1024, + (int64_t(target) - int64_t(nCurrentUsage)) / 1024 / 1024, + min_block_to_prune, last_block_can_prune, count); } void BlockManager::UpdatePruneLock(const std::string& name, const PruneLockInfo& lock_info) { @@ -263,7 +282,7 @@ CBlockIndex* BlockManager::InsertBlockIndex(const uint256& hash) return pindex; } -bool BlockManager::LoadBlockIndex() +bool BlockManager::LoadBlockIndex(const std::optional& snapshot_blockhash) { // Snapshot promotion reloads the block index a second time in the same // process (see node::LoadChainstate), and both containers below are rebuilt @@ -284,6 +303,25 @@ bool BlockManager::LoadBlockIndex() } } + int snapshot_height = -1; + if (snapshot_blockhash) { + const std::optional maybe_au_data = GetParams().AssumeutxoForBlockhash(*snapshot_blockhash); + if (!maybe_au_data) { + AbortNode(strprintf("Assumeutxo data not found for the given blockhash '%s'.", snapshot_blockhash->ToString())); + return false; + } + const AssumeutxoData& au_data = *Assert(maybe_au_data); + snapshot_height = au_data.height; + // A base the block index lacks is reported by ChainstateManager::LoadBlockIndex(). + if (CBlockIndex* base{LookupBlockIndex(*snapshot_blockhash)}) { + // Since nChainTx (responsible for estimated progress) isn't persisted + // to disk, we must bootstrap the value for assumedvalid chainstates + // from the hardcoded assumeutxo chainparams. + base->nChainTx = au_data.nChainTx; + LogPrintf("[snapshot] set nChainTx=%d for %s\n", au_data.nChainTx, snapshot_blockhash->ToString()); + } + } + // Calculate nChainWork std::vector vSortedByHeight{GetAllBlockIndices()}; std::sort(vSortedByHeight.begin(), vSortedByHeight.end(), @@ -305,8 +343,15 @@ bool BlockManager::LoadBlockIndex() // PopulateAndValidateSnapshot()). // Pruned nodes may have deleted the block. if (pindex->nTx > 0) { + if ((pindex->nStatus & BLOCK_VALID_MASK) < BLOCK_VALID_TRANSACTIONS) { + m_faked_tx_height = std::max(m_faked_tx_height, pindex->nHeight); + } if (pindex->pprev) { - if (pindex->pprev->nChainTx > 0) { + if (snapshot_blockhash && pindex->nHeight == snapshot_height && + pindex->GetBlockHash() == *snapshot_blockhash) { + // Should have been set above; don't disturb it with code below. + Assert(pindex->nChainTx > 0); + } else if (pindex->pprev->nChainTx > 0) { pindex->nChainTx = pindex->pprev->nChainTx + pindex->nTx; } else { pindex->nChainTx = 0; @@ -349,9 +394,9 @@ bool BlockManager::WriteBlockIndexDB() return true; } -bool BlockManager::LoadBlockIndexDB() +bool BlockManager::LoadBlockIndexDB(const std::optional& snapshot_blockhash) { - if (!LoadBlockIndex()) { + if (!LoadBlockIndex(snapshot_blockhash)) { return false; } @@ -969,6 +1014,12 @@ void ThreadImport(ChainstateManager& chainman, std::vector vImportFile for (Chainstate* chainstate : WITH_LOCK(::cs_main, return chainman.GetAll())) { BlockValidationState state; if (!chainstate->ActivateBestChain(state, nullptr)) { + if (IsLocalEvoStateError(state)) { + // The chainstate stays eligible; the other chainstates + // and the rest of startup proceed without it. + LogPrintf("Chainstate cannot advance yet (%s), continuing\n", state.ToString()); + continue; + } AbortNode(strprintf("Failed to connect best block (%s)", state.ToString())); return; } diff --git a/src/node/blockstorage.h b/src/node/blockstorage.h index f70b1b6731b4..fcf30bcf4f6a 100644 --- a/src/node/blockstorage.h +++ b/src/node/blockstorage.h @@ -26,6 +26,7 @@ class CBlockUndo; class CChainParams; class Chainstate; class ChainstateManager; +enum class ChainstateRole; struct CCheckpointData; struct FlatFilePos; namespace Consensus { @@ -90,7 +91,7 @@ class BlockManager * per index entry (nStatus, nChainWork, nTimeMax, etc.) as well as peripheral * collections like m_dirty_blockindex. */ - bool LoadBlockIndex() + bool LoadBlockIndex(const std::optional& snapshot_blockhash) EXCLUSIVE_LOCKS_REQUIRED(cs_main); /** Return false if block file or undo file flushing fails. */ @@ -111,7 +112,11 @@ class BlockManager bool UndoWriteToDisk(const CBlockUndo& blockundo, FlatFilePos& pos, const uint256& hashBlock, const CMessageHeader::MessageStartChars& messageStart) const; /* Calculate the block/rev files to delete based on height specified by user with RPC command pruneblockchain */ - void FindFilesToPruneManual(std::set& setFilesToPrune, int nManualPruneHeight, int chain_tip_height); + void FindFilesToPruneManual( + std::set& setFilesToPrune, + int nManualPruneHeight, + const Chainstate& chain, + ChainstateManager& chainman); /** * Prune block and undo files (blk???.dat and rev???.dat) so that the disk space used is less than a user-defined target. @@ -127,8 +132,13 @@ class BlockManager * A db flag records the fact that at least some block files have been pruned. * * @param[out] setFilesToPrune The set of file indices that can be unlinked will be returned + * @param last_prune The last height we're able to prune, according to the prune locks */ - void FindFilesToPrune(std::set& setFilesToPrune, uint64_t nPruneAfterHeight, int chain_tip_height, int prune_height, bool is_ibd); + void FindFilesToPrune( + std::set& setFilesToPrune, + int last_prune, + const Chainstate& chain, + ChainstateManager& chainman); RecursiveMutex cs_LastBlockFile; std::vector m_blockfile_info; @@ -185,10 +195,14 @@ class BlockManager */ std::multimap m_blocks_unlinked; + //! Height of the highest block LoadBlockIndex() found with a faked nTx (set without BLOCK_VALID_TRANSACTIONS), or -1 + int m_faked_tx_height GUARDED_BY(::cs_main){-1}; + std::unique_ptr m_block_tree_db GUARDED_BY(::cs_main); bool WriteBlockIndexDB() EXCLUSIVE_LOCKS_REQUIRED(::cs_main); - bool LoadBlockIndexDB() EXCLUSIVE_LOCKS_REQUIRED(::cs_main); + bool LoadBlockIndexDB(const std::optional& snapshot_blockhash) + EXCLUSIVE_LOCKS_REQUIRED(::cs_main); /** * Remove any pruned block & undo files that are still on disk. diff --git a/src/node/chainstate.cpp b/src/node/chainstate.cpp index 302f17efc32d..2845b832371c 100644 --- a/src/node/chainstate.cpp +++ b/src/node/chainstate.cpp @@ -6,14 +6,16 @@ #include #include -#include #include +#include +#include #include #include #include #include #include #include +#include #include #include #include @@ -28,6 +30,8 @@ #include #include #include +#include +#include #include #include @@ -48,10 +52,39 @@ static bool RecoverSnapshotCleanup(CEvoDB& evodb, const fs::path& data_dir, bili fs::path invalid{snapshot}; invalid += SNAPSHOT_INVALID_SUFFIX; + // A persisted rejection whose coins directory was not invalidated yet: + // finish the invalidation, then drop the rejected generation. A live + // generation owns the snapshot directory, so the rejected one's is gone. + if (evodb.HasRejectedGeneration()) { + if (fs::exists(snapshot) && !evodb.GetSnapshotGeneration()) { + LogPrintf("[snapshot] completing interrupted invalid-snapshot coins invalidation\n"); + try { + // An earlier rejection's directory keeps its place: it may be + // the one the user was asked to report. + if (fs::exists(invalid)) { + LogPrintf("[snapshot] %s already exists; deleting %s instead\n", fs::PathToString(invalid), + fs::PathToString(snapshot)); + RemoveAllDurably(snapshot); + } else { + RenameDurably(snapshot, invalid); + } + } catch (const fs::filesystem_error& e) { + error = strprintf(_("Failed to finish interrupted invalid snapshot cleanup: %s"), e.what()); + return false; + } + } + if (!evodb.CollectRejectedGenerations()) { + error = _("Failed to remove rejected assumeutxo snapshot state from the Evo database. " + "Restart with -reindex-chainstate to discard it."); + return false; + } + } + uint256 snapshot_tip; const bool has_snapshot_tip{evodb.ReadBestBlock(EvoDbIdentity::SNAPSHOT, snapshot_tip)}; uint256 ignored; - const bool has_metadata{has_snapshot_tip || evodb.HasDualChainstateMarker() || + const auto journal{evodb.ReadPromotionJournal()}; + const bool has_metadata{has_snapshot_tip || journal || evodb.HasDualChainstateMarker() || evodb.ReadSnapshotBaseMNListHash(ignored)}; // The first validated-cleanup rename completed. Roll it back so the normal @@ -69,29 +102,29 @@ static bool RecoverSnapshotCleanup(CEvoDB& evodb, const fs::path& data_dir, bili // Both validated-cleanup renames completed. The NORMAL coins directory now // contains the snapshot tip, while lifecycle markers still describe the dual - // state. Match those durable facts before promoting the markers. + // state. Match those durable facts before finishing. if (fs::exists(normal) && !fs::exists(snapshot) && has_metadata) { - uint256 coins_tip; - try { - CCoinsViewDB coins_db{DBParams{.path = normal, .cache_bytes = 1 << 20, .obfuscate = true}, CoinsViewOptions{}}; - coins_tip = coins_db.GetBestBlock(); - } catch (const std::exception& e) { - error = strprintf(_("Failed to inspect interrupted snapshot cleanup: %s"), e.what()); - return false; - } - // Caution: this condition can also match a crash between an - // invalid-snapshot rename and its marker discard when validation - // failed with the background tip AT the base block (HASH_MISMATCH / - // EVO_STATE_MISMATCH): the background coins tip and the SNAPSHOT - // marker both hold the base hash. Taking the promote branch there is - // deliberate and equivalent: PromoteSnapshotMarkers rewrites the - // NORMAL best-block with the value it already has and erases the same - // marker set DiscardSnapshotMarkers would. Keep the two functions' - // side effects equivalent under that overlap, or disambiguate here. - if (has_snapshot_tip && coins_tip == snapshot_tip) { - LogPrintf("[snapshot] completing interrupted snapshot marker promotion\n"); - if (!evodb.PromoteSnapshotMarkers(snapshot_tip)) { - error = _("Failed to finish interrupted snapshot marker promotion."); + // A promotion is journaled before its first rename, so only a + // journaled cleanup may promote; anything else is an interrupted + // invalid-snapshot cleanup. + if (journal) { + uint256 coins_tip; + try { + CCoinsViewDB coins_db{DBParams{.path = normal, .cache_bytes = 1 << 20, .obfuscate = true}, + CoinsViewOptions{}}; + coins_tip = coins_db.GetBestBlock(); + } catch (const std::exception& e) { + error = strprintf(_("Failed to inspect interrupted snapshot cleanup: %s"), e.what()); + return false; + } + if (coins_tip != journal->tip) { + error = _("Interrupted snapshot cleanup has inconsistent coins and EvoDB tips."); + return false; + } + LogPrintf("[snapshot] completing interrupted snapshot EvoDB promotion\n"); + if (!evodb.PromoteSnapshotMarkers(journal->tip)) { + error = _("Failed to finish interrupted snapshot marker promotion. Restart to retry; if it keeps " + "failing, restart with -reindex-chainstate."); return false; } if (fs::exists(to_delete)) { @@ -105,8 +138,7 @@ static bool RecoverSnapshotCleanup(CEvoDB& evodb, const fs::path& data_dir, bili } // Invalid snapshot rename completed, but the synced marker discard did - // not. A stale _INVALID directory cannot mask a completed valid swap: - // the coins/snapshot-tip match above always takes precedence. + // not. if (fs::exists(invalid)) { LogPrintf("[snapshot] completing interrupted invalid-snapshot marker cleanup\n"); if (!evodb.DiscardSnapshotMarkers()) { @@ -132,7 +164,9 @@ static bool RecoverSnapshotCleanup(CEvoDB& evodb, const fs::path& data_dir, bili } } - // No metadata is otherwise the already-promoted/no-snapshot state and needs no work. + // No metadata is otherwise the already-promoted/no-snapshot state and needs + // no work. A journal with both coins directories still in place records a + // promotion that renamed nothing yet: completion re-runs and redoes it. return true; } @@ -223,6 +257,7 @@ static ChainstateLoadResult CompleteChainstateInitialization(ChainstateManager& // block tree into BlockIndex()! for (Chainstate* chainstate : chainman.GetAll()) { + const EvoViewScope evo_view{chainstate->EvoView()}; LogPrintf("Initializing chainstate %s\n", chainstate->ToString()); chainstate->InitCoinsDB( @@ -273,7 +308,7 @@ static ChainstateLoadResult CompleteChainstateInitialization(ChainstateManager& } } - if (!special_tx.m_mnhfman->ForceSignalDBUpdate(chainman.ActiveTip())) { + if (!special_tx.m_mnhfman->ForceSignalDBUpdate(chainman.ActiveChainstate().EvoView(), chainman.ActiveTip())) { return {ChainstateLoadStatus::FAILURE, _("Error upgrading evo database for EHF")}; } @@ -290,6 +325,46 @@ static ChainstateLoadResult CompleteChainstateInitialization(ChainstateManager& return {ChainstateLoadStatus::SUCCESS, {}}; } +//! The live generation's retained evo section must still match the pin its +//! base is authorized by: the seeded state is only as good as that match. +static util::Result RevalidateRetainedEvoSection(const ChainstateManager& chainman, const CEvoDB& evodb) +{ + const auto generation{evodb.GetSnapshotGeneration()}; + if (!generation) return {}; + const auto refuse = [](const std::string& reason) -> util::Result { + LogPrintf("[snapshot] %s\n", reason); + return util::Error{_("The assumeutxo snapshot's Evo state no longer matches its authorization. Restart with " + "-reindex-chainstate to discard the snapshot.")}; + }; + const auto au_data{chainman.GetParams().AssumeutxoForBlockhash(generation->base)}; + if (!au_data || au_data->height != generation->base_height) { + return refuse("no assumeutxo authorization for the snapshot base"); + } + const bool dip3_base{generation->base_height >= + chainman.GetConsensus().DeploymentHeight(Consensus::DEPLOYMENT_DIP0003)}; + const auto retained{evodb.ReadRetainedEvoSection()}; + if (!retained) { + if (dip3_base || au_data->evo_hash != EvoSnapshotHash{uint256::ZERO}) { + return refuse("the retained evo section is missing"); + } + return {}; + } + evo::EvoSnapshot snapshot; + try { + CDataStream stream{*retained, SER_DISK, CLIENT_VERSION}; + stream >> snapshot; + if (!stream.empty()) return refuse("trailing data after the retained evo section"); + } catch (const std::ios_base::failure& e) { + return refuse(strprintf("the retained evo section does not decode: %s", e.what())); + } + if (snapshot.base_block_hash != generation->base) return refuse("the retained evo section is for another base"); + if ((dip3_base || au_data->evo_hash != EvoSnapshotHash{uint256::ZERO}) && + EvoSnapshotHash{evo::GetEvoSnapshotHash(snapshot)} != au_data->evo_hash) { + return refuse("the retained evo section does not match its pin"); + } + return {}; +} + ChainstateLoadResult LoadChainstate(ChainstateManager& chainman, const CacheSizes& cache_sizes, const ChainstateLoadOptions& options, CEvoDB& evodb, CDeterministicMNManager& dmnman, CSpecialTxProcessor& special_tx) @@ -313,6 +388,19 @@ ChainstateLoadResult LoadChainstate(ChainstateManager& chainman, const CacheSize LOCK(cs_main); + if (!options.reindex && !options.reindex_chainstate) { + if (auto result{evodb.LoadSnapshotGenerations( + EvoSnapshotConsensusConfig::FromParams(chainman.GetParams()).Fingerprint())}; + !result) { + return {ChainstateLoadStatus::FAILURE, util::ErrorString(result)}; + } + } + // Evo inputs captured for a snapshot that never got published, e.g. after + // a crash, are of no use. + if (!evodb.GetSnapshotGeneration() && !evo::EraseAllEvoSnapshotCaptures(evodb)) { + return {ChainstateLoadStatus::FAILURE, + _("Error removing stale assumeutxo snapshot state from the Evo database")}; + } if (!options.dash_dbs_in_memory && !options.reindex && !options.reindex_chainstate) { bilingual_str recovery_error; if (!RecoverSnapshotCleanup(evodb, options.data_dir, recovery_error)) { @@ -337,9 +425,27 @@ ChainstateLoadResult LoadChainstate(ChainstateManager& chainman, const CacheSize // Load a chain created from a UTXO snapshot, if any exist. bilingual_str snapshot_error; - if (!chainman.DetectSnapshotChainstate(options.mempool, snapshot_error)) { + if (!chainman.DetectSnapshotChainstate(snapshot_error)) { return {ChainstateLoadStatus::FAILURE, snapshot_error}; } + if (chainman.IsSnapshotActive()) { + if (auto result{RevalidateRetainedEvoSection(chainman, evodb)}; !result) { + return {ChainstateLoadStatus::FAILURE, util::ErrorString(result)}; + } + } + // A crash between publishing a generation and persisting the snapshot + // directory's base block leaves a generation that no chainstate uses. + if (evodb.GetSnapshotGeneration() && !chainman.IsSnapshotActive()) { + if (evodb.ReadPromotionJournal() || !evodb.DiscardSnapshotMarkers()) { + return {ChainstateLoadStatus::FAILURE, + _("Snapshot EvoDB state has no snapshot chainstate. Reindex is required.")}; + } + LogPrintf("[snapshot] discarded a snapshot EvoDB generation that never activated\n"); + if (!evo::EraseAllEvoSnapshotCaptures(evodb)) { + return {ChainstateLoadStatus::FAILURE, + _("Error removing stale assumeutxo snapshot state from the Evo database")}; + } + } auto [init_status, init_error] = CompleteChainstateInitialization(chainman, cache_sizes, options, evodb, dmnman, special_tx); @@ -357,8 +463,9 @@ ChainstateLoadResult LoadChainstate(ChainstateManager& chainman, const CacheSize // too risky to do in the middle of normal runtime. const auto snapshot_completion = chainman.MaybeCompleteSnapshotValidation(); - if (snapshot_completion == SnapshotCompletionResult::SKIPPED) { - // Do nothing; expected case. + if (snapshot_completion == SnapshotCompletionResult::SKIPPED || + snapshot_completion == SnapshotCompletionResult::EVO_DATA_UNAVAILABLE) { + // Do nothing; expected case, or completion is retried once the data is there. } else if (snapshot_completion == SnapshotCompletionResult::SUCCESS) { LogPrintf("[snapshot] cleaning up unneeded background chainstate, then reinitializing\n"); if (!chainman.ValidatedSnapshotCleanup()) { @@ -383,6 +490,9 @@ ChainstateLoadResult LoadChainstate(ChainstateManager& chainman, const CacheSize if (init_status != ChainstateLoadStatus::SUCCESS) { return {init_status, init_error}; } + } else if (snapshot_completion == SnapshotCompletionResult::EVO_LOCAL_FAILURE || + snapshot_completion == SnapshotCompletionResult::EVO_INTERNAL_ERROR) { + return {ChainstateLoadStatus::FAILURE, SnapshotCompletionStopMessage(snapshot_completion, "see debug.log")}; } else { // The UTXO hashing inside completion takes minutes and aborts with // STATS_FAILED when shutdown is requested mid-way. That says nothing @@ -409,6 +519,7 @@ ChainstateLoadResult VerifyLoadedChainstate(ChainstateManager& chainman, const C LOCK(cs_main); for (Chainstate* chainstate : chainman.GetAll()) { + const EvoViewScope evo_view{chainstate->EvoView()}; if (!is_coinsview_empty(chainstate)) { const CBlockIndex* tip = chainstate->m_chain.Tip(); if (tip && tip->nTime > GetTime() + MAX_FUTURE_BLOCK_TIME) { @@ -443,13 +554,6 @@ ChainstateLoadResult VerifyLoadedChainstate(ChainstateManager& chainman, const C break; } // no default case, so the compiler can warn about missing cases - // VerifyDB() disconnects blocks which might result in us switching back to legacy. - // Make sure we use the right scheme. - if (v19active && bls::bls_legacy_scheme.load()) { - bls::bls_legacy_scheme.store(false); - if (notify_bls_state) notify_bls_state(bls::bls_legacy_scheme.load()); - } - if (options.check_level >= 3) { chainstate->ResetBlockFailureFlags(nullptr); } diff --git a/src/node/chainstatemanager_args.cpp b/src/node/chainstatemanager_args.cpp index 40d1c9540157..5e836bdebab1 100644 --- a/src/node/chainstatemanager_args.cpp +++ b/src/node/chainstatemanager_args.cpp @@ -6,6 +6,9 @@ #include #include +#include +#include +#include #include #include #include @@ -20,6 +23,12 @@ #include namespace node { +bool LegacyIndexesEnabled(const ArgsManager& args) +{ + return args.GetBoolArg("-addressindex", DEFAULT_ADDRESSINDEX) || args.GetBoolArg("-spentindex", DEFAULT_SPENTINDEX) || + args.GetBoolArg("-timestampindex", DEFAULT_TIMESTAMPINDEX); +} + std::optional ApplyArgsManOptions(const ArgsManager& args, ChainstateManager::Options& opts) { if (auto value{args.GetBoolArg("-checkblockindex")}) opts.check_block_index = *value; @@ -39,6 +48,9 @@ std::optional ApplyArgsManOptions(const ArgsManager& args, Chains if (auto value{args.GetIntArg("-stopatheight")}) opts.stop_at_height = *value; + opts.masternode = args.IsArgSet("-masternodeblsprivkey"); + opts.legacy_indexes = LegacyIndexesEnabled(args); + ReadDatabaseArgs(args, opts.block_tree_db); ReadDatabaseArgs(args, opts.coins_db); ReadCoinsViewArgs(args, opts.coins_view); diff --git a/src/node/chainstatemanager_args.h b/src/node/chainstatemanager_args.h index 6c46b998f213..cd5064a2b2a2 100644 --- a/src/node/chainstatemanager_args.h +++ b/src/node/chainstatemanager_args.h @@ -13,6 +13,9 @@ class ArgsManager; struct bilingual_str; namespace node { +//! Whether -addressindex, -spentindex or -timestampindex is enabled. +bool LegacyIndexesEnabled(const ArgsManager& args); + std::optional ApplyArgsManOptions(const ArgsManager& args, ChainstateManager::Options& opts); } // namespace node diff --git a/src/node/context.h b/src/node/context.h index eb72fdb2ddd7..6f8efd5416a4 100644 --- a/src/node/context.h +++ b/src/node/context.h @@ -108,6 +108,11 @@ struct NodeContext { std::unique_ptr cj_walletman; std::unique_ptr dstxman; std::unique_ptr evodb; + //! The Evo database for dispatchers that start before the block databases + //! load (the scheduler, REST) to bind to. Startup publishes it once loading, + //! retries included, has finished and withdraws it before destroying it, + //! after those dispatchers have stopped. Until then their work runs unbound. + std::atomic evodb_for_dispatch{nullptr}; std::unique_ptr special_tx; std::unique_ptr dmnman; std::unique_ptr isman; diff --git a/src/node/interfaces.cpp b/src/node/interfaces.cpp index 5e7f32d36184..8c847d461300 100644 --- a/src/node/interfaces.cpp +++ b/src/node/interfaces.cpp @@ -13,10 +13,13 @@ #include #include #include +#include #include #include #include +#include #include +#include #include #include #include @@ -36,6 +39,7 @@ #include #include #include +#include #include #include #include @@ -86,9 +90,13 @@ #include #include +#include #include #include #include +#include +#include +#include #include #include @@ -132,6 +140,65 @@ namespace node { // All members of the classes in this namespace are intentionally public, as the // classes themselves are private. namespace { +bilingual_str EvoStateUnavailableMessage() +{ + return _("Masternode state is not available yet. See debug.log for details."); +} + +ProviderTxError UnavailableProviderTxError() +{ + return {.code = interfaces::ProviderTxErrorCode::INTERNAL_ERROR, .message = EvoStateUnavailableMessage()}; +} + +std::string EvoStateWarningSource(std::string_view operation) { return strprintf("interfaces:%s", operation); } + +//! Set the operation's warning, which notifies the GUI only when it changes, +//! and log `e` once per operation and active Evo view: GUI models poll these +//! operations. +void ReportEvoStateUnavailable(const CEvoDB* evodb, std::string_view operation, const evo::LocalStateError& e) +{ + // Without a snapshot, only pruning removes the block data Evo state is + // derived from. + const bool pruned{evodb && !evodb->HasLiveGeneration() && dynamic_cast(&e)}; + if (WarnLocalEvoStateUnavailable(EvoStateWarningSource(operation), strprintf("%s could not read it", operation), + pruned)) { + uiInterface.NotifyAlertChanged(); + } + static Mutex mutex; + static std::set> logged; + const EvoViewToken view{evodb ? evodb->ActiveView() : EvoViewToken{}}; + if (WITH_LOCK(mutex, return logged.emplace(operation, view.db, view.identity, view.generation).second)) { + LogPrintf("ERROR: %s: Evo state unavailable: %s\n", operation, e.what()); + } +} + +/** Run a GUI-facing operation that reads Evo state on the active chainstate's + * view. When this node cannot provide that state, the operation reports it + * and returns `on_unavailable()`: the error must not reach Qt, and callers + * must not mistake it for data. */ +template +std::invoke_result_t WithActiveEvoView(const CEvoDB* evodb, std::string_view operation, Fn&& fn, + OnUnavailable&& on_unavailable) +{ + try { + const ActiveEvoViewScope evo_view{evodb}; + const auto read_again = [&] { + if (SetEvoStateWarning(EvoStateWarningSource(operation), std::nullopt)) uiInterface.NotifyAlertChanged(); + }; + if constexpr (std::is_void_v>) { + fn(); + read_again(); + } else { + auto result{fn()}; + read_again(); + return result; + } + } catch (const evo::LocalStateError& e) { + ReportEvoStateUnavailable(evodb, operation, e); + return on_unavailable(); + } +} + class MnEntryImpl : public MnEntry { private: @@ -265,83 +332,128 @@ class EVOImpl : public EVO public: std::pair getListAtChainTip() override { - const auto *tip = WITH_LOCK(::cs_main, return chainman().ActiveChain().Tip()); - if (tip && context().dmnman) { - MnListImpl mnList = context().dmnman->GetListForBlock(tip); - if (!mnList.getBlockHash().IsNull()) { - mnList.setContext(m_context); - return {std::make_shared(mnList), tip}; - } - } - return {nullptr, nullptr}; + return WithActiveEvoView( + context().evodb.get(), __func__, + [&]() -> std::pair { + const auto* tip = WITH_LOCK(::cs_main, return chainman().ActiveChain().Tip()); + if (tip && context().dmnman) { + MnListImpl mnList = context().dmnman->GetListForBlock(tip); + if (!mnList.getBlockHash().IsNull()) { + mnList.setContext(m_context); + return {std::make_shared(mnList), tip}; + } + } + return {nullptr, nullptr}; + }, + [] { return std::pair{nullptr, nullptr}; }); + } + std::optional getProviderTxCapabilities() override + { + return WithActiveEvoView( + context().evodb.get(), __func__, + [&]() -> std::optional { return evo::provider::GetCapabilities(context()); }, + [] { return std::nullopt; }); } - ProviderTxCapabilities getProviderTxCapabilities() override { return evo::provider::GetCapabilities(context()); } std::optional validateProviderNetInfo(const ProviderNetInfo& net_info, MnType type, uint16_t version, bool ipv4_optional, bool optional) override { return evo::provider::ValidateNetInfo(net_info, type, version, ipv4_optional, optional); } - std::optional findProviderNetInfoInUse(const ProviderNetInfo& net_info, MnType type, uint16_t version, - bool ipv4_optional) override + ProviderTxResult> findProviderNetInfoInUse(const ProviderNetInfo& net_info, MnType type, + uint16_t version, bool ipv4_optional) override { - return evo::provider::FindNetInfoInUse(context(), net_info, type, version, ipv4_optional); + return WithActiveEvoView( + context().evodb.get(), __func__, + [&]() -> ProviderTxResult> { + if (!context().dmnman) return UnavailableProviderTxError(); + return evo::provider::FindNetInfoInUse(context(), net_info, type, version, ipv4_optional); + }, + UnavailableProviderTxError); } ProviderTxResult registerMasternode(Wallet& wallet, const ProviderRegistrationRequest& request) override { - return evo::provider::Register(context(), wallet, request); + return WithActiveEvoView( + context().evodb.get(), __func__, [&] { return evo::provider::Register(context(), wallet, request); }, + UnavailableProviderTxError); } ProviderTxResult prepareMasternodeRegistration( Wallet& wallet, const ProviderRegistrationRequest& request) override { - return evo::provider::PrepareRegistration(context(), wallet, request); + return WithActiveEvoView( + context().evodb.get(), __func__, + [&] { return evo::provider::PrepareRegistration(context(), wallet, request); }, UnavailableProviderTxError); } ProviderTxResult submitMasternodeRegistration( Wallet& wallet, const CTransactionRef& tx, const std::vector& collateral_signature) override { - return evo::provider::SubmitRegistration(context(), wallet, tx, collateral_signature); + return WithActiveEvoView( + context().evodb.get(), __func__, + [&] { return evo::provider::SubmitRegistration(context(), wallet, tx, collateral_signature); }, + UnavailableProviderTxError); } ProviderTxResult updateMasternodeService(Wallet& wallet, const ProviderUpdateServiceRequest& request) override { - return evo::provider::UpdateService(context(), wallet, request); + return WithActiveEvoView( + context().evodb.get(), __func__, [&] { return evo::provider::UpdateService(context(), wallet, request); }, + UnavailableProviderTxError); } ProviderTxResult updateMasternodeRegistrar(Wallet& wallet, const ProviderUpdateRegistrarRequest& request) override { - return evo::provider::UpdateRegistrar(context(), wallet, request); + return WithActiveEvoView( + context().evodb.get(), __func__, [&] { return evo::provider::UpdateRegistrar(context(), wallet, request); }, + UnavailableProviderTxError); } ProviderTxResult revokeMasternode(Wallet& wallet, const ProviderRevokeRequest& request) override { - return evo::provider::Revoke(context(), wallet, request); + return WithActiveEvoView( + context().evodb.get(), __func__, [&] { return evo::provider::Revoke(context(), wallet, request); }, + UnavailableProviderTxError); } ProviderTxResult prepareSharedRegistration(const SharedRegistrationRequest& request) override { - return evo::provider::PrepareSharedRegistration(context(), request); + return WithActiveEvoView( + context().evodb.get(), __func__, + [&] { return evo::provider::PrepareSharedRegistration(context(), request); }, UnavailableProviderTxError); } ProviderTxResult signShared(Wallet& wallet, const SharedSignRequest& request) override { - return evo::provider::SignShared(context(), wallet, request); + return WithActiveEvoView( + context().evodb.get(), __func__, [&] { return evo::provider::SignShared(context(), wallet, request); }, + UnavailableProviderTxError); } ProviderTxResult combineShared(Wallet& wallet, const SharedCombineRequest& request) override { - return evo::provider::CombineShared(context(), &wallet, request); + return WithActiveEvoView( + context().evodb.get(), __func__, [&] { return evo::provider::CombineShared(context(), &wallet, request); }, + UnavailableProviderTxError); } ProviderTxResult dissolveShared(Wallet& wallet, const SharedDissolveRequest& request) override { - return evo::provider::DissolveShared(context(), wallet, request); + return WithActiveEvoView( + context().evodb.get(), __func__, [&] { return evo::provider::DissolveShared(context(), wallet, request); }, + UnavailableProviderTxError); } ProviderTxResult prepareSharedDissolution(const SharedDissolvePrepareRequest& request) override { - return evo::provider::PrepareSharedDissolution(context(), request); + return WithActiveEvoView( + context().evodb.get(), __func__, + [&] { return evo::provider::PrepareSharedDissolution(context(), request); }, UnavailableProviderTxError); } ProviderTxResult updateShare(Wallet& wallet, const SharedUpdateShareRequest& request) override { - return evo::provider::UpdateShare(context(), wallet, request); + return WithActiveEvoView( + context().evodb.get(), __func__, [&] { return evo::provider::UpdateShare(context(), wallet, request); }, + UnavailableProviderTxError); } ProviderTxResult prepareSharedRegistrarUpdate( Wallet& wallet, const SharedRegistrarUpdatePrepareRequest& request) override { - return evo::provider::PrepareSharedRegistrarUpdate(context(), wallet, request); + return WithActiveEvoView( + context().evodb.get(), __func__, + [&] { return evo::provider::PrepareSharedRegistrarUpdate(context(), wallet, request); }, + UnavailableProviderTxError); } void setContext(NodeContext* context) override { @@ -365,43 +477,58 @@ class GOVImpl : public GOV context().govman->GetAllNewerThan(objs, nMoreThanTime, include_postponed); } } - Votes getObjVotes(const CGovernanceObject& obj, vote_signal_enum_t vote_signal) override - { - Votes ret; - if (context().govman != nullptr && context().dmnman != nullptr) { - const auto& tip_mn_list{context().dmnman->GetListAtChainTip()}; - if (auto govobj{context().govman->FindGovernanceObject(obj.GetHash())}) { - ret.m_abs = govobj->GetAbstainCount(tip_mn_list, vote_signal); - ret.m_no = govobj->GetNoCount(tip_mn_list, vote_signal); - ret.m_yes = govobj->GetYesCount(tip_mn_list, vote_signal); - } else { - ret.m_abs = obj.GetAbstainCount(tip_mn_list, vote_signal); - ret.m_no = obj.GetNoCount(tip_mn_list, vote_signal); - ret.m_yes = obj.GetYesCount(tip_mn_list, vote_signal); - } - } - return ret; - } - UniqueVoters getObjUniqueVoters(const CGovernanceObject& obj, vote_signal_enum_t vote_signal) override - { - if (context().govman != nullptr && context().dmnman != nullptr) { - const auto& tip_mn_list{context().dmnman->GetListAtChainTip()}; - if (auto govobj{context().govman->FindGovernanceObject(obj.GetHash())}) { - const auto count = govobj->GetUniqueVoterCount(tip_mn_list, vote_signal); - return {.m_regular = count.m_regular, .m_evo = count.m_evo}; - } else { - const auto count = obj.GetUniqueVoterCount(tip_mn_list, vote_signal); - return {.m_regular = count.m_regular, .m_evo = count.m_evo}; - } - } - return {0, 0}; + std::optional getObjVotes(const CGovernanceObject& obj, vote_signal_enum_t vote_signal) override + { + return WithActiveEvoView( + context().evodb.get(), __func__, + [&]() -> std::optional { + Votes ret; + if (context().govman != nullptr && context().dmnman != nullptr) { + const auto& tip_mn_list{context().dmnman->GetListAtChainTip()}; + if (auto govobj{context().govman->FindGovernanceObject(obj.GetHash())}) { + ret.m_abs = govobj->GetAbstainCount(tip_mn_list, vote_signal); + ret.m_no = govobj->GetNoCount(tip_mn_list, vote_signal); + ret.m_yes = govobj->GetYesCount(tip_mn_list, vote_signal); + } else { + ret.m_abs = obj.GetAbstainCount(tip_mn_list, vote_signal); + ret.m_no = obj.GetNoCount(tip_mn_list, vote_signal); + ret.m_yes = obj.GetYesCount(tip_mn_list, vote_signal); + } + } + return ret; + }, + [] { return std::nullopt; }); + } + std::optional getObjUniqueVoters(const CGovernanceObject& obj, vote_signal_enum_t vote_signal) override + { + return WithActiveEvoView( + context().evodb.get(), __func__, + [&]() -> std::optional { + if (context().govman != nullptr && context().dmnman != nullptr) { + const auto& tip_mn_list{context().dmnman->GetListAtChainTip()}; + if (auto govobj{context().govman->FindGovernanceObject(obj.GetHash())}) { + const auto count = govobj->GetUniqueVoterCount(tip_mn_list, vote_signal); + return UniqueVoters{.m_regular = count.m_regular, .m_evo = count.m_evo}; + } else { + const auto count = obj.GetUniqueVoterCount(tip_mn_list, vote_signal); + return UniqueVoters{.m_regular = count.m_regular, .m_evo = count.m_evo}; + } + } + return UniqueVoters{}; + }, + [] { return std::nullopt; }); } - std::vector getCurrentVotes(const uint256& hash) override + std::optional> getCurrentVotes(const uint256& hash) override { - if (context().govman != nullptr) { - return context().govman->GetCurrentVotes(hash, COutPoint{}); - } - return {}; + return WithActiveEvoView( + context().evodb.get(), __func__, + [&]() -> std::optional> { + if (context().govman != nullptr) { + return context().govman->GetCurrentVotes(hash, COutPoint{}); + } + return std::vector{}; + }, + [] { return std::nullopt; }); } bool existsObj(const uint256& hash) override { @@ -419,28 +546,41 @@ class GOVImpl : public GOV } bool processVoteAndRelay(const CGovernanceVote& vote, std::string& error) override { - if (context().govman != nullptr) { - CGovernanceException exception; - bool result = context().govman->ProcessVoteAndRelay(vote, exception); - if (!result) { - error = exception.GetMessage(); - } - return result; - } - error = "Governance manager not available"; - return false; + return WithActiveEvoView( + context().evodb.get(), __func__, + [&]() -> bool { + if (context().govman != nullptr) { + CGovernanceException exception; + bool result = context().govman->ProcessVoteAndRelay(vote, exception); + if (!result) { + error = exception.GetMessage(); + } + return result; + } + error = "Governance manager not available"; + return false; + }, + [&] { + error = EvoStateUnavailableMessage().original; + return false; + }); } GovernanceInfo getGovernanceInfo() override { GovernanceInfo info; if (context().chainman) { const Consensus::Params& consensusParams = context().chainman->GetConsensus(); + if (context().dmnman) { + info.fundingthreshold = WithActiveEvoView( + context().evodb.get(), __func__, + [&]() -> std::optional { + return static_cast(context().dmnman->GetListAtChainTip().GetCounts().m_valid_weighted / 10); + }, + [] { return std::nullopt; }); + } LOCK(::cs_main); CSuperblock::GetNearestSuperblocksHeights(context().chainman->ActiveHeight(), info.lastsuperblock, info.nextsuperblock); info.governancebudget = CSuperblock::GetPaymentsLimit(info.nextsuperblock); - if (context().dmnman) { - info.fundingthreshold = static_cast(context().dmnman->GetListAtChainTip().GetCounts().m_valid_weighted / 10); - } info.superblockcycle = consensusParams.nSuperblockCycle; info.superblockmaturitywindow = consensusParams.nSuperblockMaturityWindow; info.targetSpacing = consensusParams.nPowTargetSpacing; @@ -465,38 +605,43 @@ class GOVImpl : public GOV } return std::nullopt; } - FundableResult getFundableProposalHashes() override - { - FundableResult result; - if (context().govman != nullptr && context().chainman != nullptr && context().dmnman != nullptr) { - const auto tip_mn_list{context().dmnman->GetListAtChainTip()}; - if (const auto proposals{context().govman->GetApprovedProposals(tip_mn_list)}; !proposals.empty()) { - int32_t last_sb{0}, next_sb{0}; - CAmount budget{0}; - { - LOCK(::cs_main); - CSuperblock::GetNearestSuperblocksHeights(context().chainman->ActiveHeight(), last_sb, next_sb); - budget = CSuperblock::GetPaymentsLimit(next_sb); - } - for (const auto& proposal : proposals) { - UniValue json = proposal->GetJSONObject(); - CAmount payment_amount{0}; - try { - payment_amount = ParsePaymentAmount(json["payment_amount"].getValStr()); - } catch (...) { - continue; + std::optional getFundableProposalHashes() override + { + return WithActiveEvoView( + context().evodb.get(), __func__, + [&]() -> std::optional { + FundableResult result; + if (context().govman != nullptr && context().chainman != nullptr && context().dmnman != nullptr) { + const auto tip_mn_list{context().dmnman->GetListAtChainTip()}; + if (const auto proposals{context().govman->GetApprovedProposals(tip_mn_list)}; !proposals.empty()) { + int32_t last_sb{0}, next_sb{0}; + CAmount budget{0}; + { + LOCK(::cs_main); + CSuperblock::GetNearestSuperblocksHeights(context().chainman->ActiveHeight(), last_sb, next_sb); + budget = CSuperblock::GetPaymentsLimit(next_sb); + } + for (const auto& proposal : proposals) { + UniValue json = proposal->GetJSONObject(); + CAmount payment_amount{0}; + try { + payment_amount = ParsePaymentAmount(json["payment_amount"].getValStr()); + } catch (...) { + continue; + } + if (result.allocated + payment_amount > budget) { + // Budget is saturated, cannot fulfill proposal + continue; + } + result.allocated += payment_amount; + result.hashes.insert(proposal->GetHash()); + } + return result; } - if (result.allocated + payment_amount > budget) { - // Budget is saturated, cannot fulfill proposal - continue; - } - result.allocated += payment_amount; - result.hashes.insert(proposal->GetHash()); } return result; - } - } - return result; + }, + [] { return std::nullopt; }); } std::optional createProposal(int32_t revision, int64_t created_time, const std::string& data_hex, std::string& error) override @@ -509,33 +654,44 @@ class GOVImpl : public GOV error = "This node is still building its governance collateral index, so it can't submit proposals yet"; return std::nullopt; } - CGovernanceObject govobj(uint256{}, revision, created_time, uint256{}, data_hex); - if (govobj.GetObjectType() != GovernanceObject::PROPOSAL) { - error = "Invalid object type, only proposals can be validated"; - return std::nullopt; - } - std::string strValidationError; - if (!governance::ValidateProposal(data_hex, strValidationError)) { - error = "Invalid proposal data: " + strValidationError; - return std::nullopt; - } - const ChainstateManager& chainman = *Assert(context().chainman); - { - LOCK(::cs_main); - std::string strError; - if (!govobj.IsValidLocally(Assert(context().dmnman)->GetListAtChainTip(), chainman, /*collateral=*/nullptr, - strError, false)) { - error = "Governance object is not valid - " + govobj.GetHash().ToString() + " - " + strError; + return WithActiveEvoView( + context().evodb.get(), __func__, + [&]() -> std::optional { + CGovernanceObject govobj(uint256{}, revision, created_time, uint256{}, data_hex); + if (govobj.GetObjectType() != GovernanceObject::PROPOSAL) { + error = "Invalid object type, only proposals can be validated"; + return std::nullopt; + } + std::string strValidationError; + if (!governance::ValidateProposal(data_hex, strValidationError)) { + error = "Invalid proposal data: " + strValidationError; + return std::nullopt; + } + const ChainstateManager& chainman = *Assert(context().chainman); + { + LOCK(::cs_main); + std::string strError; + if (!govobj.IsValidLocally(Assert(context().dmnman)->GetListAtChainTip(), chainman, + /*collateral=*/nullptr, strError, false)) { + error = "Governance object is not valid - " + govobj.GetHash().ToString() + " - " + strError; + return std::nullopt; + } + } + return govobj; + }, + [&] { + error = EvoStateUnavailableMessage().original; return std::nullopt; - } - } - return govobj; + }); } bool submitProposal(const uint256& parent, int32_t revision, int64_t created_time, const std::string& data_hex, const uint256& fee_txid, std::string& out_object_hash, std::string& error) override { - if (!context().govman || !context().dmnman || !context().chainman) { error = "Governance not available"; return false; } + if (!context().govman || !context().dmnman || !context().chainman) { + error = "Governance not available"; + return false; + } if (!context().govman->IsValid() || !context().gov_collateral_index) { error = "Governance is disabled on this node, so it can't submit proposals"; return false; @@ -544,35 +700,55 @@ class GOVImpl : public GOV error = "This node is still building its governance collateral index, so it can't submit proposals yet"; return false; } - if(!Assert(context().mn_sync)->IsBlockchainSynced()) { error = "Client not synced"; return false; } - const auto mnList = Assert(context().dmnman)->GetListAtChainTip(); - CGovernanceObject govobj(parent, revision, created_time, fee_txid, data_hex); - if (govobj.GetObjectType() == GovernanceObject::TRIGGER) { error = "Submission of triggers is not available"; return false; } - if (govobj.GetObjectType() == GovernanceObject::PROPOSAL) { - std::string strValidationError; - if (!governance::ValidateProposal(data_hex, strValidationError)) { error = "Invalid proposal data: " + strValidationError; return false; } - } - const CTxMemPool& mempool = *Assert(context().mempool); - bool fMissingConfirmations{false}; - { - LOCK2(cs_main, mempool.cs); - std::string strError; - if (!govobj.IsValidLocally(mnList, *Assert(context().chainman), context().gov_collateral_index.get(), - strError, fMissingConfirmations, true) && - !fMissingConfirmations) { - error = "Governance object is not valid - " + govobj.GetHash().ToString() + " - " + strError; + return WithActiveEvoView( + context().evodb.get(), __func__, + [&]() -> bool { + if (!Assert(context().mn_sync)->IsBlockchainSynced()) { + error = "Client not synced"; + return false; + } + const auto mnList = Assert(context().dmnman)->GetListAtChainTip(); + CGovernanceObject govobj(parent, revision, created_time, fee_txid, data_hex); + if (govobj.GetObjectType() == GovernanceObject::TRIGGER) { + error = "Submission of triggers is not available"; + return false; + } + if (govobj.GetObjectType() == GovernanceObject::PROPOSAL) { + std::string strValidationError; + if (!governance::ValidateProposal(data_hex, strValidationError)) { + error = "Invalid proposal data: " + strValidationError; + return false; + } + } + const CTxMemPool& mempool = *Assert(context().mempool); + bool fMissingConfirmations{false}; + { + LOCK2(cs_main, mempool.cs); + std::string strError; + if (!govobj.IsValidLocally(mnList, *Assert(context().chainman), context().gov_collateral_index.get(), + strError, fMissingConfirmations, true) && + !fMissingConfirmations) { + error = "Governance object is not valid - " + govobj.GetHash().ToString() + " - " + strError; + return false; + } + } + if (!Assert(context().govman)->MasternodeRateCheck(govobj)) { + error = "Object creation rate limit exceeded"; + return false; + } + if (fMissingConfirmations) { + context().govman->AddPostponedObject(govobj); + context().govman->RelayObject(govobj); + } else { + context().govman->AddGovernanceObject(govobj, ""); + } + out_object_hash = govobj.GetHash().ToString(); + return true; + }, + [&] { + error = EvoStateUnavailableMessage().original; return false; - } - } - if (!Assert(context().govman)->MasternodeRateCheck(govobj)) { error = "Object creation rate limit exceeded"; return false; } - if (fMissingConfirmations) { - context().govman->AddPostponedObject(govobj); - context().govman->RelayObject(govobj); - } else { - context().govman->AddGovernanceObject(govobj, ""); - } - out_object_hash = govobj.GetHash().ToString(); - return true; + }); } void setContext(NodeContext* context) override { @@ -589,22 +765,27 @@ class LLMQImpl : public LLMQ NodeContext& context() { return *Assert(m_context); } public: - CreditPoolCounts getCreditPoolCounts() override - { - CreditPoolCounts ret{}; - if (!context().chainman || !context().special_tx) { - return ret; - } - const auto* pindex{WITH_LOCK(::cs_main, return context().chainman->ActiveChain().Tip())}; - if (!pindex || !pindex->pprev) { - return ret; - } - auto& cpoolman{*context().special_tx->m_cpoolman}; - const auto pool{cpoolman.GetCreditPool(pindex)}; - ret.m_locked = pool.locked; - ret.m_limit = pool.currentLimit; - ret.m_diff = pool.locked - cpoolman.GetCreditPool(pindex->pprev).locked; - return ret; + std::optional getCreditPoolCounts() override + { + return WithActiveEvoView( + context().evodb.get(), __func__, + [&]() -> std::optional { + CreditPoolCounts ret{}; + if (!context().chainman || !context().special_tx) { + return ret; + } + const auto* pindex{WITH_LOCK(::cs_main, return context().chainman->ActiveChain().Tip())}; + if (!pindex || !pindex->pprev) { + return ret; + } + auto& cpoolman{*context().special_tx->m_cpoolman}; + const auto pool{cpoolman.GetCreditPool(pindex)}; + ret.m_locked = pool.locked; + ret.m_limit = pool.currentLimit; + ret.m_diff = pool.locked - cpoolman.GetCreditPool(pindex->pprev).locked; + return ret; + }, + [] { return std::nullopt; }); } ChainLockInfo getBestChainLock() override { @@ -644,85 +825,148 @@ class LLMQImpl : public LLMQ return entry.GetTx().IsPlatformTransfer(); })); } - std::vector getQuorumStats() override + std::optional> getQuorumStats() override + { + return WithActiveEvoView( + context().evodb.get(), __func__, + [&]() -> std::optional> { + std::vector stats{}; + if (!context().llmq_ctx || !context().llmq_ctx->qman || !context().chainman) { + return stats; + } + const auto* pindex{WITH_LOCK(::cs_main, return context().chainman->ActiveChain().Tip())}; + if (!pindex) { + return stats; + } + for (const auto& type : llmq::GetEnabledQuorumTypes(*context().chainman, pindex)) { + const auto llmq_params{Params().GetLLMQ(type)}; + if (!llmq_params.has_value()) { + continue; + } + const auto quorums{ + context().llmq_ctx->qman->ScanQuorums(type, pindex, llmq_params->signingActiveQuorumCount)}; + double health{0.0}; + for (const auto& q : quorums) { + // A quorum known by its key only counts its commitment's member slots. + const size_t numMembers{q->HasMembers() ? q->Members().size() : q->qc->validMembers.size()}; + size_t numValidMembers = q->qc->CountValidMembers(); + health += (numMembers > 0) ? (double(numValidMembers) / double(numMembers)) : 0.0; + } + health = quorums.empty() ? 0.0 : (health / quorums.size()); + const int32_t newest_height{(!quorums.empty() && quorums[0]->m_quorum_base_block_index) + ? quorums[0]->m_quorum_base_block_index->nHeight + : 0}; + const int32_t expiry_height{ + (newest_height > 0) ? newest_height + llmq_params->signingActiveQuorumCount * llmq_params->dkgInterval + : 0}; + stats.emplace_back(QuorumInfo{ + .m_name = std::string(llmq_params->name), + .m_count = quorums.size(), + .m_health = health, + .m_rotates = llmq_params->useRotation, + .m_data_retention_blocks = llmq_params->max_store_depth(), + .m_newest_height = newest_height, + .m_expiry_height = expiry_height, + }); + } + return stats; + }, + [] { return std::nullopt; }); + } + PlatformQuorumResult getPlatformQuorum(uint8_t llmq_type, const uint256& quorum_hash, + uint32_t core_chain_locked_height) override + { + // Unbound, or unable to read its own Evo state, this node cannot tell. + return WithActiveEvoView( + context().evodb.get(), __func__, + [&] { return LookupPlatformQuorum(llmq_type, quorum_hash, core_chain_locked_height); }, + [] { return PlatformQuorumResult{}; }); + } + +private: + //! getPlatformQuorum in the Evo view the calling thread is bound to. + PlatformQuorumResult LookupPlatformQuorum(uint8_t llmq_type, const uint256& quorum_hash, + uint32_t core_chain_locked_height) { - std::vector stats{}; - if (!context().llmq_ctx || !context().llmq_ctx->qman || !context().chainman) { - return stats; + PlatformQuorumResult invalid{PlatformQuorumStatus::INVALID, {}}; + const auto type{static_cast(llmq_type)}; + const auto llmq_params{Params().GetLLMQ(type)}; + if (type != Params().GetConsensus().llmqTypePlatform || !llmq_params.has_value()) { + return invalid; } - const auto* pindex{WITH_LOCK(::cs_main, return context().chainman->ActiveChain().Tip())}; - if (!pindex) { - return stats; + if (core_chain_locked_height > static_cast(std::numeric_limits::max())) { + return invalid; } - for (const auto& type : llmq::GetEnabledQuorumTypes(*context().chainman, pindex)) { - const auto llmq_params{Params().GetLLMQ(type)}; - if (!llmq_params.has_value()) { - continue; - } - const auto quorums{context().llmq_ctx->qman->ScanQuorums(type, pindex, llmq_params->signingActiveQuorumCount)}; - double health{0.0}; - for (const auto& q : quorums) { - size_t numMembers = q->members.size(); - size_t numValidMembers = q->qc->CountValidMembers(); - health += (numMembers > 0) ? (double(numValidMembers) / double(numMembers)) : 0.0; - } - health = quorums.empty() ? 0.0 : (health / quorums.size()); - const int32_t newest_height{(!quorums.empty() && quorums[0]->m_quorum_base_block_index) - ? quorums[0]->m_quorum_base_block_index->nHeight : 0}; - const int32_t expiry_height{(newest_height > 0) - ? newest_height + llmq_params->signingActiveQuorumCount * llmq_params->dkgInterval - : 0}; - stats.emplace_back(QuorumInfo{ - .m_name = std::string(llmq_params->name), - .m_count = quorums.size(), - .m_health = health, - .m_rotates = llmq_params->useRotation, - .m_data_retention_blocks = llmq_params->max_store_depth(), - .m_newest_height = newest_height, - .m_expiry_height = expiry_height, - }); + const int signed_height{static_cast(core_chain_locked_height)}; + if (!context().llmq_ctx || !context().llmq_ctx->quorum_block_processor || !context().chainman || + !context().evodb) { + return {}; } - return stats; - } - std::vector getPlatformQuorums(uint8_t llmq_type) override - { - std::vector ret; - if (!context().llmq_ctx || !context().llmq_ctx->quorum_block_processor || !context().chainman) { - return ret; + // Only a ChainLock whose block is known can lock part of the active chain; the best height may + // belong to a block this node has not seen. + const CBlockIndex* chainlocked_block{nullptr}; + int32_t best_cl_height{-1}; + if (const auto& chainlocks{context().chainlocks}) { + chainlocked_block = chainlocks->GetBestChainlockWithPindex().second; + best_cl_height = chainlocks->GetBestChainLockHeight(); } - const auto* pindex{WITH_LOCK(::cs_main, return context().chainman->ActiveChain().Tip())}; - if (!pindex) { - return ret; + + LOCK(::cs_main); + // Only the active chain of the bound view can answer: a snapshot + // published since answers the next call. + if (context().evodb->ActiveView() != context().evodb->CurrentView()) { + return {}; } - const auto type{static_cast(llmq_type)}; - const auto llmq_params{Params().GetLLMQ(type)}; - if (!llmq_params.has_value()) { - return ret; + const CChain& active{context().chainman->ActiveChain()}; + const CBlockIndex* tip{active.Tip()}; + if (!tip || signed_height > tip->nHeight) { + return {}; } - // Drive proofs may be signed by an older Platform quorum while they - // are still consensus-valid and retained locally. Export the full - // retained-key window, not only the current signing-active set. - const auto quorum_count{static_cast(std::max(llmq_params->signingActiveQuorumCount, - llmq_params->keepOldKeys))}; - // Read mined final commitments directly: they already carry the quorum - // hash and public key, so there is no need to materialize full CQuorum - // objects (member lists, vvec/contribution reads, quorum cache inserts) - // via ScanQuorums. Newest-first, matching ScanQuorums' ordering. + // A ChainLock can arrive before its block. Without one the tip still proves a height fresh, + // but not stale: Platform may carry a ChainLock this node has not learned yet. + const int cl_height{best_cl_height >= 0 ? std::min(best_cl_height, tip->nHeight) : tip->nHeight}; + if (signed_height < llmq::PlatformQuorumMinSignedHeight(*llmq_params, cl_height)) { + return best_cl_height >= 0 ? invalid : PlatformQuorumResult{}; + } + const auto& qbp{*context().llmq_ctx->quorum_block_processor}; - const auto quorum_base_block_indexes{llmq_params->useRotation - ? qbp.GetMinedCommitmentsIndexedUntilBlock(type, pindex, quorum_count) - : qbp.GetMinedCommitmentsUntilBlock(type, pindex, quorum_count)}; - for (const auto* pQuorumBaseBlockIndex : quorum_base_block_indexes) { - const auto qc{qbp.GetMinedCommitment(type, pQuorumBaseBlockIndex->GetBlockHash()).first}; - if (!qc.quorumPublicKey.IsValid()) continue; - ret.emplace_back(PlatformQuorum{ - .m_quorum_hash = qc.quorumHash, - .m_pubkey = qc.quorumPublicKey.ToByteVector(/*specificLegacyScheme=*/false), - .m_height = pQuorumBaseBlockIndex->nHeight, - }); + const CBlockIndex* signed_block{active[signed_height]}; + const auto window{static_cast(llmq::PlatformEligibleQuorumCount(*llmq_params))}; + const auto scan{llmq_params->useRotation ? qbp.ScanMinedCommitmentsIndexedUntilBlock(type, signed_block, window) + : qbp.ScanMinedCommitmentsUntilBlock(type, signed_block, window)}; + if (std::ranges::none_of(scan.bases, [&](const CBlockIndex* base) { return base->GetBlockHash() == quorum_hash; })) { + // Platform verifies a block against its last committed validator set, so the block that + // first carries a height may be signed by a quorum no longer in the window at that height. + // A quorum mined at or below the height has its base block on the active chain below it; + // any other hash could never have signed. + const CBlockIndex* base{context().chainman->m_blockman.LookupBlockIndex(quorum_hash)}; + const bool never_mined{!base || base->nHeight >= signed_height || !active.Contains(base)}; + // That is final only on the part of the active chain a ChainLock locks; beyond it the + // active chain may be a competing branch. Unlike Chainlocks::HasChainLock, this keeps the + // lock while spork 19 is off. A node whose quorum index reads as corrupt may have bad storage + // elsewhere too, so it rejects nothing until the window reads cleanly. + const bool chainlocked{chainlocked_block && chainlocked_block->GetAncestor(signed_height) == signed_block}; + return never_mined && chainlocked && scan.complete ? invalid : PlatformQuorumResult{}; } - return ret; + + // Mined commitments passed consensus validation, so an unreadable or inconsistent record is + // local corruption, not evidence against the quorum. + std::pair record; + try { + record = qbp.GetMinedCommitment(type, quorum_hash); + } catch (const dbwrapper_error&) { + return {}; + } + const auto& [qc, mined_block_hash]{record}; + const CBlockIndex* mined_block{context().chainman->m_blockman.LookupBlockIndex(mined_block_hash)}; + if (qc.llmqType != type || qc.quorumHash != quorum_hash || !mined_block || !active.Contains(mined_block) || + mined_block->nHeight > signed_height || !qc.quorumPublicKey.IsValid()) { + return {}; + } + return {PlatformQuorumStatus::FOUND, qc.quorumPublicKey.ToByteVector(/*specificLegacyScheme=*/false)}; } + +public: std::vector getInstantSendLock(const uint256& txid) override { if (!context().isman) { @@ -1137,10 +1381,15 @@ class NodeImpl : public Node return m_context->active_ctx != nullptr; } bool isLoadingBlocks() override { return chainman().m_blockman.LoadingBlocks(); } - bool isV24Active() override + std::optional isV24Active() override { - LOCK(::cs_main); - return DeploymentActiveAfter(chainman().ActiveChain().Tip(), chainman(), Consensus::DEPLOYMENT_V24); + return WithActiveEvoView( + m_context->evodb.get(), __func__, + [&]() -> std::optional { + LOCK(::cs_main); + return DeploymentActiveAfter(chainman().ActiveChain().Tip(), chainman(), Consensus::DEPLOYMENT_V24); + }, + [] { return std::nullopt; }); } void setNetworkActive(bool active) override { @@ -1312,9 +1561,9 @@ class NotificationsProxy : public CValidationInterface { m_notifications->transactionRemovedFromMempool(tx, reason); } - void BlockConnected(const std::shared_ptr& block, const CBlockIndex* index) override + void BlockConnected(ChainstateRole role, const std::shared_ptr& block, const CBlockIndex* index) override { - m_notifications->blockConnected(kernel::MakeBlockInfo(index, block.get())); + m_notifications->blockConnected(role, kernel::MakeBlockInfo(index, block.get())); } void BlockDisconnected(const std::shared_ptr& block, const CBlockIndex* index) override { @@ -1324,7 +1573,9 @@ class NotificationsProxy : public CValidationInterface { m_notifications->updatedBlockTip(); } - void ChainStateFlushed(const CBlockLocator& locator) override { m_notifications->chainStateFlushed(locator); } + void ChainStateFlushed(ChainstateRole role, const CBlockLocator& locator) override { + m_notifications->chainStateFlushed(role, locator); + } void NotifyChainLock(const CBlockIndex* pindexChainLock, const std::shared_ptr& clsig) override { m_notifications->notifyChainLock(pindexChainLock, clsig); @@ -1475,21 +1726,27 @@ class ChainImpl : public Chain if (m_node.chainlocks == nullptr) return false; return m_node.chainlocks->HasChainLock(height, hash); } - std::vector listMNCollaterials(const std::vector>& outputs) override + std::optional> listMNCollaterials( + const std::vector>& outputs) override { - const CBlockIndex *tip = WITH_LOCK(::cs_main, return chainman().ActiveChain().Tip()); - CDeterministicMNList mnList{}; - if (tip != nullptr && m_node.dmnman != nullptr) { - mnList = m_node.dmnman->GetListForBlock(tip); - } - std::vector listRet; - for (const auto& [tx, index]: outputs) { - COutPoint nextOut{tx->GetHash(), index}; - if (CDeterministicMNManager::IsProTxWithCollateral(tx, index) || mnList.HasMNByCollateral(nextOut)) { - listRet.emplace_back(nextOut); - } - } - return listRet; + return WithActiveEvoView( + m_node.evodb.get(), __func__, + [&]() -> std::optional> { + const CBlockIndex* tip = WITH_LOCK(::cs_main, return chainman().ActiveChain().Tip()); + CDeterministicMNList mnList{}; + if (tip != nullptr && m_node.dmnman != nullptr) { + mnList = m_node.dmnman->GetListForBlock(tip); + } + std::vector listRet; + for (const auto& [tx, index] : outputs) { + COutPoint nextOut{tx->GetHash(), index}; + if (CDeterministicMNManager::IsProTxWithCollateral(tx, index) || mnList.HasMNByCollateral(nextOut)) { + listRet.emplace_back(nextOut); + } + } + return listRet; + }, + [] { return std::nullopt; }); } interfaces::CoinJoin::Loader* coinJoinLoader() override { return m_node.coinjoin_loader.get(); } bool findBlock(const uint256& hash, const FoundBlock& block) override @@ -1639,10 +1896,15 @@ class ChainImpl : public Chain if (!m_node.mempool || !m_node.mempool->m_require_standard) return false; return !IsStandardSpecialTx(*tx, reason); } - bool isV24Active() override + std::optional isV24Active() override { - LOCK(::cs_main); - return DeploymentActiveAfter(chainman().ActiveChain().Tip(), chainman(), Consensus::DEPLOYMENT_V24); + return WithActiveEvoView( + m_node.evodb.get(), __func__, + [&]() -> std::optional { + LOCK(::cs_main); + return DeploymentActiveAfter(chainman().ActiveChain().Tip(), chainman(), Consensus::DEPLOYMENT_V24); + }, + [] { return std::nullopt; }); } bool havePruned() override { diff --git a/src/node/kernel_notifications.cpp b/src/node/kernel_notifications.cpp index cf73279c10ca..3a30e083eabb 100644 --- a/src/node/kernel_notifications.cpp +++ b/src/node/kernel_notifications.cpp @@ -77,4 +77,9 @@ void KernelNotifications::masternodeListChanged(const CDeterministicMNList& list uiInterface.NotifyMasternodeListChanged(list, index); } +void KernelNotifications::evoStateWarningChanged() +{ + uiInterface.NotifyAlertChanged(); +} + } // namespace node diff --git a/src/node/kernel_notifications.h b/src/node/kernel_notifications.h index f1591f13825f..b436fc26e57e 100644 --- a/src/node/kernel_notifications.h +++ b/src/node/kernel_notifications.h @@ -28,6 +28,8 @@ class KernelNotifications : public kernel::Notifications void warning(const bilingual_str& warning) override; void masternodeListChanged(const CDeterministicMNList& list, const CBlockIndex* index) override; + + void evoStateWarningChanged() override; }; } // namespace node diff --git a/src/node/miner.cpp b/src/node/miner.cpp index 1def21c59a01..65a5168168fd 100644 --- a/src/node/miner.cpp +++ b/src/node/miner.cpp @@ -27,13 +27,14 @@ #include #include -#include #include #include +#include #include #include -#include #include +#include +#include #include #include #include @@ -114,8 +115,8 @@ void BlockAssembler::resetBlock() } // Helper to calculate best chainlock -static bool CalcCbTxBestChainlock(const chainlock::Chainlocks& chainlocks, const node::BlockManager& blockman, const CBlockIndex* pindexPrev, - uint32_t& bestCLHeightDiff, CBLSSignature& bestCLSignature) +static bool CalcCbTxBestChainlock(const chainlock::Chainlocks& chainlocks, const CSpecialTxProcessor& special_tx, + const CBlockIndex* pindexPrev, uint32_t& bestCLHeightDiff, CBLSSignature& bestCLSignature) { auto best_clsig = chainlocks.GetBestChainLock(); if (best_clsig.getHeight() < Params().GetConsensus().DeploymentHeight(Consensus::DEPLOYMENT_V19)) { @@ -129,7 +130,17 @@ static bool CalcCbTxBestChainlock(const chainlock::Chainlocks& chainlocks, const return true; } - auto prevBlockCoinbaseChainlock = GetNonNullCoinbaseChainlock(blockman, pindexPrev); + // The same reader block validation uses, so a snapshot chainstate mining on + // its base sees the carried ChainLock rather than "none". + const CoinbaseChainlock previous{special_tx.GetCoinbaseChainlock(pindexPrev)}; + if (previous.status == CoinbaseChainlock::Status::UNAVAILABLE) { + throw BlockDataUnavailableError( + strprintf("coinbase ChainLock of block %s is not available", pindexPrev->GetBlockHash().ToString())); + } + std::optional> prevBlockCoinbaseChainlock; + if (previous.status == CoinbaseChainlock::Status::CERTIFIED) { + prevBlockCoinbaseChainlock = std::make_pair(previous.signature, previous.height_diff); + } if (prevBlockCoinbaseChainlock.has_value()) { // Previous block Coinbase contains a non-null CL: We must insert the same sig or a better (newest) one if (best_clsig.IsNull()) { @@ -220,6 +231,7 @@ void BlockAssembler::FillBlockPayments(CMutableTransaction& txNew, const CBlockI std::unique_ptr BlockAssembler::CreateNewBlock(const CScript& scriptPubKeyIn) { + const EvoViewScope evo_view{m_chainstate.EvoView()}; const auto time_start{SteadyClock::now()}; resetBlock(); @@ -355,8 +367,9 @@ std::unique_ptr BlockAssembler::CreateNewBlock(const CScript& sc } if (fV20Active_context) { CBLSSignature best_cl_sig; - const bool has_best_cl{CalcCbTxBestChainlock(m_chainlocks, m_chainstate.m_blockman, pindexPrev, cbTx.bestCLHeightDiff, best_cl_sig)}; - cbTx.bestCLSignature.Set(best_cl_sig, bls::bls_legacy_scheme.load()); + const bool has_best_cl{CalcCbTxBestChainlock(m_chainlocks, m_chainstate.m_special_tx, pindexPrev, + cbTx.bestCLHeightDiff, best_cl_sig)}; + cbTx.bestCLSignature.Set(best_cl_sig, bls::EffectiveLegacyScheme()); if (has_best_cl) { LogPrintf("CreateNewBlock() h[%d] CbTx bestCLHeightDiff[%d] CLSig[%s]\n", nHeight, cbTx.bestCLHeightDiff, cbTx.bestCLSignature.ToString()); } else { @@ -814,6 +827,8 @@ void BlockAssembler::addPackageTxs(const CTxMemPool& mempool, int& nPackagesSele std::string error; try { if (!package_mn_list->ApplyTx(tx, state)) error = state.ToString(); + } catch (const evo::LocalStateError&) { + throw; } catch (const std::runtime_error& e) { // AddMN()/UpdateMN() report a unique-property conflict by throwing error = e.what(); diff --git a/src/qt/bitcoingui.cpp b/src/qt/bitcoingui.cpp index bccb33c102de..210183c99bf4 100644 --- a/src/qt/bitcoingui.cpp +++ b/src/qt/bitcoingui.cpp @@ -1867,7 +1867,9 @@ void BitcoinGUI::updateGovernanceCycleIcon() ? tr("~%1 (%2 blocks) left for superblock").arg(remaining_str).arg(remaining_blocks) : tr("~%1 (%2 blocks) left for voting").arg(voting_str).arg(voting_remaining))}; const auto tooltip2{[&]() { - const auto allocated_budget{m_node.gov().getFundableProposalHashes().allocated}; + const auto fundable{m_node.gov().getFundableProposalHashes()}; + if (!fundable) return tr("Budget commitment is not available yet"); + const auto allocated_budget{fundable->allocated}; const auto budget_pct{gov_info.governancebudget > 0 ? static_cast(static_cast(allocated_budget) / static_cast(gov_info.governancebudget) * 100.0) : 0}; diff --git a/src/qt/clientfeeds.cpp b/src/qt/clientfeeds.cpp index 3383c67a3b81..fec4224c8175 100644 --- a/src/qt/clientfeeds.cpp +++ b/src/qt/clientfeeds.cpp @@ -91,8 +91,13 @@ void CreditPoolFeed::fetch() return; } + // Unavailable counts keep the current data until a later fetch. + const auto counts{m_client_model.node().llmq().getCreditPoolCounts()}; + if (!counts) { + return; + } auto ret = std::make_shared(); - ret->m_counts = m_client_model.node().llmq().getCreditPoolCounts(); + ret->m_counts = *counts; ret->m_pending_unlocks = m_client_model.node().llmq().getPendingAssetUnlocks(); setData(std::move(ret)); @@ -132,8 +137,12 @@ void QuorumFeed::fetch() return; } + auto quorums{m_client_model.node().llmq().getQuorumStats()}; + if (!quorums) { + return; + } auto ret = std::make_shared(); - ret->m_quorums = m_client_model.node().llmq().getQuorumStats(); + ret->m_quorums = std::move(*quorums); setData(std::move(ret)); } @@ -220,22 +229,31 @@ void ProposalFeed::fetch() const auto nMinQuorum = static_cast(Params().GetConsensus().nGovernanceMinQuorum); ret->m_abs_vote_req = static_cast(std::max(nMinQuorum, data_mn->m_counts.m_valid_weighted / 10)); ret->m_gov_info = m_client_model.node().gov().getGovernanceInfo(); + auto fundable{m_client_model.node().gov().getFundableProposalHashes()}; + // Proposals are judged against the masternode list; without it, keep the + // current data until a later fetch. + if (!ret->m_gov_info.fundingthreshold || !fundable) { + return; + } std::vector govObjList; m_client_model.getAllGovernanceObjects(govObjList); for (const auto& govObj : govObjList) { if (govObj.GetObjectType() != GovernanceObject::PROPOSAL) { continue; // Skip triggers. } - ret->m_proposals.emplace_back(std::make_shared(m_client_model, govObj, ret->m_gov_info, ret->m_gov_info.requiredConfs, - /*is_broadcast=*/true)); + auto proposal{std::make_shared(m_client_model, govObj, ret->m_gov_info, ret->m_gov_info.requiredConfs, + /*is_broadcast=*/true)}; const auto voters = m_client_model.node().gov().getObjUniqueVoters(govObj, VOTE_SIGNAL_FUNDING); - ret->m_max_regular_voters = std::max(ret->m_max_regular_voters, voters.m_regular); - ret->m_max_evo_voters = std::max(ret->m_max_evo_voters, voters.m_evo); + if (!proposal->hasVotes() || !voters) { + return; + } + ret->m_proposals.emplace_back(std::move(proposal)); + ret->m_max_regular_voters = std::max(ret->m_max_regular_voters, voters->m_regular); + ret->m_max_evo_voters = std::max(ret->m_max_evo_voters, voters->m_evo); } - auto fundable{m_client_model.node().gov().getFundableProposalHashes()}; - ret->m_fundable_hashes = std::move(fundable.hashes); - ret->m_allocated = fundable.allocated; + ret->m_fundable_hashes = std::move(fundable->hashes); + ret->m_allocated = fundable->allocated; setData(std::move(ret)); } diff --git a/src/qt/masternodedialogs.cpp b/src/qt/masternodedialogs.cpp index d2432d64a1e2..820a9e4fffac 100644 --- a/src/qt/masternodedialogs.cpp +++ b/src/qt/masternodedialogs.cpp @@ -339,12 +339,6 @@ void MasternodeActionDialog::finishSubmission(MasternodeOperationRunner::Submiss accept(); } -UpdateServiceDialog::UpdateServiceDialog(interfaces::Node& node, WalletModel* wallet_model, - const MasternodeEntry& entry, QWidget* parent) : - UpdateServiceDialog(node, wallet_model, entry, node.evo().getProviderTxCapabilities(), parent) -{ -} - UpdateServiceDialog::UpdateServiceDialog(interfaces::Node& node, WalletModel* wallet_model, const MasternodeEntry& entry, interfaces::ProviderTxCapabilities capabilities, QWidget* parent) : MasternodeActionDialog(node, wallet_model, entry, parent), @@ -516,12 +510,6 @@ void UpdateServiceDialog::submit() }); } -UpdateRegistrarDialog::UpdateRegistrarDialog(interfaces::Node& node, WalletModel* wallet_model, - const MasternodeEntry& entry, QWidget* parent) : - UpdateRegistrarDialog(node, wallet_model, entry, node.evo().getProviderTxCapabilities(), parent) -{ -} - UpdateRegistrarDialog::UpdateRegistrarDialog(interfaces::Node& node, WalletModel* wallet_model, const MasternodeEntry& entry, interfaces::ProviderTxCapabilities capabilities, QWidget* parent) : diff --git a/src/qt/masternodedialogs.h b/src/qt/masternodedialogs.h index 0375d5d9bbd5..1da9fcf80e55 100644 --- a/src/qt/masternodedialogs.h +++ b/src/qt/masternodedialogs.h @@ -136,15 +136,13 @@ class UpdateServiceDialog : public MasternodeActionDialog public: UpdateServiceDialog(interfaces::Node& node, WalletModel* wallet_model, const MasternodeEntry& entry, - QWidget* parent = nullptr); + interfaces::ProviderTxCapabilities capabilities, QWidget* parent = nullptr); protected: void validate() override; void submit() override; private: - UpdateServiceDialog(interfaces::Node& node, WalletModel* wallet_model, const MasternodeEntry& entry, - interfaces::ProviderTxCapabilities capabilities, QWidget* parent); std::optional buildNetInfo(QString& error) const; std::optional buildRequest(const CBLSSecretKey& operator_key, QString& error) const; @@ -172,15 +170,13 @@ class UpdateRegistrarDialog : public MasternodeActionDialog public: UpdateRegistrarDialog(interfaces::Node& node, WalletModel* wallet_model, const MasternodeEntry& entry, - QWidget* parent = nullptr); + interfaces::ProviderTxCapabilities capabilities, QWidget* parent = nullptr); protected: void validate() override; void submit() override; private: - UpdateRegistrarDialog(interfaces::Node& node, WalletModel* wallet_model, const MasternodeEntry& entry, - interfaces::ProviderTxCapabilities capabilities, QWidget* parent); std::optional buildRequest(QString& error) const; bool operatorChanged() const; bool votingChanged() const; diff --git a/src/qt/masternodelist.cpp b/src/qt/masternodelist.cpp index 43f89210cde3..492fa3bc62eb 100644 --- a/src/qt/masternodelist.cpp +++ b/src/qt/masternodelist.cpp @@ -254,8 +254,12 @@ void MasternodeList::updateRegistrationAvailability() } // isV24Active() takes cs_main and this runs on every block; activation is - // monotonic, so ask only until it is - if (!m_v24_active && clientModel != nullptr) m_v24_active = clientModel->node().isV24Active(); + // monotonic, so ask only until it is. An unknown state asks again later. + std::optional v24_state{m_v24_active}; + if (!m_v24_active && clientModel != nullptr) { + v24_state = clientModel->node().isV24Active(); + m_v24_active = v24_state.value_or(false); + } const bool v24_active{clientModel != nullptr && m_v24_active}; const bool can_shared{can_register && v24_active}; ui->btnSharedMasternode->setEnabled(can_shared); @@ -265,6 +269,8 @@ void MasternodeList::updateRegistrationAvailability() ui->btnSharedMasternode->setToolTip(tr("Managing shared masternodes requires a wallet with private keys.")); } else if (clientModel == nullptr) { ui->btnSharedMasternode->setToolTip(tr("Shared masternode management is unavailable until the node is ready.")); + } else if (!v24_state) { + ui->btnSharedMasternode->setToolTip(MasternodeWidgetUtil::masternodeStateUnavailableText()); } else if (!v24_active) { ui->btnSharedMasternode->setToolTip(tr("Shared masternodes require the v24 hard fork, which is not active on this network yet.")); } else { @@ -478,10 +484,22 @@ const MasternodeEntry* MasternodeList::selectedEntryForDialog() return entry != nullptr && clientModel != nullptr && walletModel != nullptr ? entry : nullptr; } +std::optional MasternodeList::providerTxCapabilities() +{ + auto capabilities{clientModel->node().evo().getProviderTxCapabilities()}; + if (!capabilities) { + QMessageBox::warning(this, tr("Masternode state unavailable"), + MasternodeWidgetUtil::masternodeStateUnavailableText()); + } + return capabilities; +} + void MasternodeList::onUpdateService() { if (const auto* entry{selectedEntryForDialog()}) { - UpdateServiceDialog dialog(clientModel->node(), walletModel, *entry, this); + const auto capabilities{providerTxCapabilities()}; + if (!capabilities) return; + UpdateServiceDialog dialog(clientModel->node(), walletModel, *entry, *capabilities, this); dialog.exec(); } } @@ -490,7 +508,9 @@ void MasternodeList::onUpdateRegistrar() { if (const auto* entry{selectedEntryForDialog()}; entry != nullptr && walletModel->wallet().isSpendable(PKHash(entry->keyIdOwnerRaw()))) { - UpdateRegistrarDialog dialog(clientModel->node(), walletModel, *entry, this); + const auto capabilities{providerTxCapabilities()}; + if (!capabilities) return; + UpdateRegistrarDialog dialog(clientModel->node(), walletModel, *entry, *capabilities, this); dialog.exec(); } } @@ -574,10 +594,11 @@ void MasternodeList::updateMasternodeList() return; } - std::set setOutpts; - for (const auto& outpt : walletModel->wallet().listProTxCoins()) { - setOutpts.emplace(outpt); - } + // Without the wallet's collaterals ownership cannot be told; keep the + // current list until a later update. + const auto protx_coins{walletModel->wallet().listProTxCoins()}; + if (!protx_coins) return; + const std::set setOutpts{protx_coins->begin(), protx_coins->end()}; QSet owned_mns; QHash my_share_counts; diff --git a/src/qt/masternodelist.h b/src/qt/masternodelist.h index 332aafcbb5d6..994c682670a5 100644 --- a/src/qt/masternodelist.h +++ b/src/qt/masternodelist.h @@ -5,6 +5,7 @@ #ifndef BITCOIN_QT_MASTERNODELIST_H #define BITCOIN_QT_MASTERNODELIST_H +#include #include #include @@ -17,6 +18,7 @@ #include #include +#include #include class ClientModel; @@ -142,6 +144,8 @@ class MasternodeList : public QWidget const MasternodeEntry* GetSelectedEntry(); const MasternodeEntry* selectedEntryForDialog(); + //! The provider transaction rules, or nullopt after telling the user they are not available yet + std::optional providerTxCapabilities(); Q_SIGNALS: void doubleClicked(const QModelIndex&); diff --git a/src/qt/masternodewidgets.cpp b/src/qt/masternodewidgets.cpp index 616dfaa10840..cc3699105fbd 100644 --- a/src/qt/masternodewidgets.cpp +++ b/src/qt/masternodewidgets.cpp @@ -56,6 +56,11 @@ bool isP2PKHorP2SHAddress(const QString& address) return std::holds_alternative(dest) || std::holds_alternative(dest); } +QString masternodeStateUnavailableText() +{ + return QObject::tr("Masternode state is not available on this node yet. Try again later."); +} + QLabel* makeTitle(const QString& text, QWidget* parent, double point_size) { auto* label{new QLabel(text, parent)}; diff --git a/src/qt/masternodewidgets.h b/src/qt/masternodewidgets.h index 2d527ad6d8a7..6818061ff2e3 100644 --- a/src/qt/masternodewidgets.h +++ b/src/qt/masternodewidgets.h @@ -44,6 +44,9 @@ bool isP2PKHorP2SHAddress(const QString& address); //! translated reason in `error` QString freshAddress(WalletModel* wallet_model, QString& error); +//! Shown when the node cannot evaluate its masternode state yet +QString masternodeStateUnavailableText(); + //! Spacing tokens (px) keeping one vertical rhythm across the dialogs' pages constexpr int GROUP_SPACING{16}; //!< between top-level groups of a page constexpr int TITLE_SPACING{8}; //!< between a group's title and its body diff --git a/src/qt/masternodewizard.cpp b/src/qt/masternodewizard.cpp index 2278492d37fd..4246968f5657 100644 --- a/src/qt/masternodewizard.cpp +++ b/src/qt/masternodewizard.cpp @@ -63,10 +63,21 @@ using MasternodeWidgetUtil::makePageLayout; using MasternodeWidgetUtil::makePageTitle; using MasternodeWidgetUtil::makeValue; -std::set RegisteredCollaterals(interfaces::Wallet& wallet) +//! Whether the provider transaction rules still have `version`. Rules this +//! node cannot evaluate yet count as changed. +bool CurrentProviderVersionIs(interfaces::Node& node, uint16_t version) +{ + const auto capabilities{node.evo().getProviderTxCapabilities()}; + return capabilities && capabilities->version == version; +} + +//! The wallet's coins registered as masternode collateral, or nullopt when the +//! masternode list is not available +std::optional> RegisteredCollaterals(interfaces::Wallet& wallet) { const auto registered{wallet.listProTxCoins()}; - return {registered.begin(), registered.end()}; + if (!registered) return std::nullopt; + return std::set{registered->begin(), registered->end()}; } //! Formats collateral amounts, which are whole coins in every display unit, so @@ -686,7 +697,8 @@ bool RegisterMasternodeWizard::isEvo() const bool RegisterMasternodeWizard::usesExtendedAddresses() const { - return m_node.evo().getProviderTxCapabilities().extended_addresses; + const auto capabilities{m_node.evo().getProviderTxCapabilities()}; + return capabilities && capabilities->extended_addresses; } bool RegisterMasternodeWizard::isExternalCollateral() const @@ -846,10 +858,13 @@ void RegisterMasternodeWizard::enterPage(Page page) } break; case PagePlatform: { + // Without the rules, no version is recorded and validating the page + // reports them unavailable. const auto capabilities{m_node.evo().getProviderTxCapabilities()}; - const bool v3{capabilities.extended_addresses}; - m_platform_provider_version = capabilities.version; - m_platform_ipv4_optional = capabilities.ipv4_optional; + if (!capabilities) showError(MasternodeWidgetUtil::masternodeStateUnavailableText()); + const bool v3{capabilities && capabilities->extended_addresses}; + m_platform_provider_version = capabilities ? capabilities->version : 0; + m_platform_ipv4_optional = capabilities && capabilities->ipv4_optional; m_platform_extended_addresses = v3; m_platform_addr_box->setVisible(v3); m_platform_port_box->setVisible(!v3); @@ -919,7 +934,9 @@ bool RegisterMasternodeWizard::validatePage(Page page, QString& err) break; case PageService: { const QStringList list{MasternodeWidgetUtil::tokenizeEndpointList(m_service_edit->text())}; - if (list.isEmpty() && isEvo() && !usesExtendedAddresses()) { + if (list.isEmpty() && isEvo() && !m_node.evo().getProviderTxCapabilities()) { + err = MasternodeWidgetUtil::masternodeStateUnavailableText(); + } else if (list.isEmpty() && isEvo() && !usesExtendedAddresses()) { err = tr("EvoNodes need at least one service address to carry the Platform ports before v24 " "activation."); } else if (!isEvo()) { @@ -927,9 +944,12 @@ bool RegisterMasternodeWizard::validatePage(Page page, QString& err) } else if (!list.isEmpty()) { interfaces::ProviderNetInfo core_only; for (const QString& entry : list) core_only.core_p2p.push_back(entry.toStdString()); - const auto capabilities{m_node.evo().getProviderTxCapabilities()}; - if (const auto error{m_node.evo().validateProviderNetInfo( - core_only, MnType::Regular, capabilities.version, capabilities.ipv4_optional, /*optional=*/true)}) { + if (const auto capabilities{m_node.evo().getProviderTxCapabilities()}; !capabilities) { + err = MasternodeWidgetUtil::masternodeStateUnavailableText(); + } else if (const auto error{m_node.evo().validateProviderNetInfo(core_only, MnType::Regular, + capabilities->version, + capabilities->ipv4_optional, + /*optional=*/true)}) { err = QString::fromStdString(error->message.translated); } } @@ -1049,14 +1069,21 @@ interfaces::ProviderNetInfo RegisterMasternodeWizard::providerNetInfo() const bool RegisterMasternodeWizard::validateProviderNetInfo(bool optional, QString& err) const { // The Platform page pins the rules it was entered with, so typing there doesn't wait for cs_main - const interfaces::ProviderTxCapabilities pinned{.version = m_platform_provider_version, - .extended_addresses = m_platform_extended_addresses, - .ipv4_optional = m_platform_ipv4_optional}; - const auto capabilities{isEvo() && m_platform_provider_version != 0 ? pinned - : m_node.evo().getProviderTxCapabilities()}; + std::optional capabilities; + if (isEvo() && m_platform_provider_version != 0) { + capabilities = interfaces::ProviderTxCapabilities{.version = m_platform_provider_version, + .extended_addresses = m_platform_extended_addresses, + .ipv4_optional = m_platform_ipv4_optional}; + } else { + capabilities = m_node.evo().getProviderTxCapabilities(); + } + if (!capabilities) { + err = MasternodeWidgetUtil::masternodeStateUnavailableText(); + return false; + } const MnType type{isEvo() ? MnType::Evo : MnType::Regular}; - if (const auto error{m_node.evo().validateProviderNetInfo(providerNetInfo(), type, capabilities.version, - capabilities.ipv4_optional, optional)}) { + if (const auto error{m_node.evo().validateProviderNetInfo(providerNetInfo(), type, capabilities->version, + capabilities->ipv4_optional, optional)}) { err = QString::fromStdString(error->message.translated); return false; } @@ -1186,7 +1213,15 @@ void RegisterMasternodeWizard::refreshCollateralCandidates() m_col_utxo_none->setVisible(true); return; } - refreshCollateralCandidates(RegisteredCollaterals(m_walletModel->wallet())); + const auto registered{RegisteredCollaterals(m_walletModel->wallet())}; + if (!registered) { + // Keep the current candidates: a registered collateral cannot be told + // from a free one yet. + showError(MasternodeWidgetUtil::masternodeStateUnavailableText()); + return; + } + if (m_error_label->text() == MasternodeWidgetUtil::masternodeStateUnavailableText()) showError(QString()); + refreshCollateralCandidates(*registered); } void RegisterMasternodeWizard::refreshCollateralCandidates(const std::set& registered_collaterals) @@ -1391,7 +1426,12 @@ std::optional RegisterMasternodeWizard: const COutPoint outpoint{hash, static_cast( m_col_utxo_combo->itemData(m_col_utxo_combo->currentIndex(), VOUT_ROLE).toInt())}; - if (RegisteredCollaterals(m_walletModel->wallet()).count(outpoint) != 0) { + const auto registered{RegisteredCollaterals(m_walletModel->wallet())}; + if (!registered) { + error = MasternodeWidgetUtil::masternodeStateUnavailableText(); + return std::nullopt; + } + if (registered->count(outpoint) != 0) { error = tr("The selected collateral output is already used by another masternode."); return std::nullopt; } @@ -1499,7 +1539,7 @@ void RegisterMasternodeWizard::startRegistration(bool skip_confirmation) goToPage(PageSecret); return; } - if (isEvo() && m_node.evo().getProviderTxCapabilities().version != m_platform_provider_version) { + if (isEvo() && !CurrentProviderVersionIs(m_node, m_platform_provider_version)) { goToPage(PagePlatform); showError(tr("The network's provider-transaction rules changed while this wizard was open. Review the " "Platform services page again before registering.")); @@ -1542,7 +1582,7 @@ void RegisterMasternodeWizard::startRegistration(bool skip_confirmation) void RegisterMasternodeWizard::startSubmit(bool skip_confirmation) { if (m_walletModel == nullptr || !m_runner || !m_prepared_tx) return; - if (isEvo() && m_node.evo().getProviderTxCapabilities().version != m_platform_provider_version) { + if (isEvo() && !CurrentProviderVersionIs(m_node, m_platform_provider_version)) { showError(tr("The network's provider-transaction rules changed while the collateral signature was being " "collected. Discard this prepared registration and review the Platform services page again.")); return; diff --git a/src/qt/proposallist.cpp b/src/qt/proposallist.cpp index 23a2287a1b71..471c1c01df6b 100644 --- a/src/qt/proposallist.cpp +++ b/src/qt/proposallist.cpp @@ -258,16 +258,23 @@ void ProposalList::updateProposalList() // Include unrelayed wallet proposals (0 confs, not yet broadcast) for (const auto& obj : getWalletProposals(/*pending=*/true)) { CGovernanceObject govObj(obj.hashParent, obj.revision, obj.time, obj.collateralHash, obj.GetDataAsHexString()); - ret.m_proposals.emplace_back(std::make_shared(*clientModel, govObj, ret.m_gov_info, queryCollateralDepth(obj.collateralHash), - /*is_broadcast=*/false)); + auto proposal{std::make_shared(*clientModel, govObj, ret.m_gov_info, + queryCollateralDepth(obj.collateralHash), + /*is_broadcast=*/false)}; + // Without vote counts keep the current list until a later update. + if (!proposal->hasVotes()) return; + ret.m_proposals.emplace_back(std::move(proposal)); } } else if (m_proposal_source == ProposalSource::Local) { // Discard network proposals and only include wallet proposals ret.m_proposals.clear(); for (const auto& obj : getWalletProposals(/*pending=*/std::nullopt)) { CGovernanceObject govObj(obj.hashParent, obj.revision, obj.time, obj.collateralHash, obj.GetDataAsHexString()); - ret.m_proposals.emplace_back(std::make_shared(*clientModel, govObj, ret.m_gov_info, queryCollateralDepth(obj.collateralHash), - /*is_broadcast=*/clientModel->node().gov().existsObj(obj.GetHash()))); + auto proposal{ + std::make_shared(*clientModel, govObj, ret.m_gov_info, queryCollateralDepth(obj.collateralHash), + /*is_broadcast=*/clientModel->node().gov().existsObj(obj.GetHash()))}; + if (!proposal->hasVotes()) return; + ret.m_proposals.emplace_back(std::move(proposal)); } } diff --git a/src/qt/proposalmodel.cpp b/src/qt/proposalmodel.cpp index 4e2ce36addb7..6f58ba9dffa8 100644 --- a/src/qt/proposalmodel.cpp +++ b/src/qt/proposalmodel.cpp @@ -43,8 +43,7 @@ QString formatProposalAmount(const BitcoinUnit& unit, const CAmount& amount) } // anonymous namespace Proposal::Proposal(ClientModel& client_model, const CGovernanceObject& govObj, - const interfaces::GOV::GovernanceInfo& govInfo, int collateral_confs, - bool is_broadcast) : + const interfaces::GOV::GovernanceInfo& govInfo, int collateral_confs, bool is_broadcast) : m_is_broadcast{is_broadcast}, m_block_height{client_model.getNumBlocks()}, m_collateral_confs{collateral_confs}, @@ -54,7 +53,8 @@ Proposal::Proposal(ClientModel& client_model, const CGovernanceObject& govObj, m_hash_object{QString::fromStdString(govObj.GetHash().ToString())}, m_hash_parent{QString::fromStdString(govObj.Object().hashParent.ToString())}, m_json{QString::fromStdString(govObj.GetInnerJson().write(2))}, - m_objHash{govObj.GetHash()} + m_objHash{govObj.GetHash()}, + m_votes{client_model.node().gov().getObjVotes(govObj, VOTE_SIGNAL_FUNDING)} { UniValue prop_data; if (!prop_data.read(govObj.GetDataAsPlainString())) { @@ -62,11 +62,16 @@ Proposal::Proposal(ClientModel& client_model, const CGovernanceObject& govObj, } m_funded_height = client_model.node().gov().getProposalFundedHeight(govObj.GetHash()); - m_votes = client_model.node().gov().getObjVotes(govObj, VOTE_SIGNAL_FUNDING); - for (const auto& vote : client_model.node().gov().getCurrentVotes(govObj.GetHash())) { - if (vote.GetSignal() == VOTE_SIGNAL_FUNDING) { - m_current_votes.emplace(vote.GetMasternodeOutpoint(), vote); + // Without the masternode list the votes are unknown, not absent: the + // proposal then reports no votes, and its feed keeps its current data. + if (const auto current_votes{client_model.node().gov().getCurrentVotes(govObj.GetHash())}) { + for (const auto& vote : *current_votes) { + if (vote.GetSignal() == VOTE_SIGNAL_FUNDING) { + m_current_votes.emplace(vote.GetMasternodeOutpoint(), vote); + } } + } else { + m_votes.reset(); } if (const UniValue& titleValue = prop_data.find_value("name"); titleValue.isStr()) { @@ -169,7 +174,8 @@ ProposalStatus Proposal::status(bool is_fundable) const if (m_block_height % m_gov_info.superblockcycle >= m_gov_info.superblockcycle - m_gov_info.superblockmaturitywindow) { return is_fundable ? ProposalStatus::Passing : ProposalStatus::Failing; } - if (getAbsoluteYesCount() < m_gov_info.fundingthreshold) { + // Only proposals judged against a known masternode list are listed. + if (Assume(m_gov_info.fundingthreshold) && getAbsoluteYesCount() < *m_gov_info.fundingthreshold) { return ProposalStatus::Voting; } return is_fundable ? ProposalStatus::Passing : ProposalStatus::Unfunded; diff --git a/src/qt/proposalmodel.h b/src/qt/proposalmodel.h index 06d6505bc559..4e9db45072ed 100644 --- a/src/qt/proposalmodel.h +++ b/src/qt/proposalmodel.h @@ -9,6 +9,7 @@ #include #include #include +#include #include @@ -47,7 +48,6 @@ class Proposal int m_block_height{0}; int m_collateral_confs{0}; interfaces::GOV::GovernanceInfo m_gov_info; - interfaces::GOV::Votes m_votes; std::map m_current_votes; QDateTime m_date_collateral{}; QDateTime m_endDate{}; @@ -61,6 +61,14 @@ class Proposal QString m_url{}; std::optional m_funded_height{}; uint256 m_objHash{}; + //! Nullopt when the masternode list to count votes against is not available + std::optional m_votes; + + interfaces::GOV::Votes votes() const + { + Assume(m_votes.has_value()); + return m_votes.value_or(interfaces::GOV::Votes{}); + } public: explicit Proposal(ClientModel& client_model, const CGovernanceObject& _govObj, @@ -75,10 +83,13 @@ class Proposal int collateralConfs() const; int paymentsRequested() const; int requiredConfs() const; - int32_t getAbsoluteYesCount() const { return m_votes.m_yes - m_votes.m_no; } - int32_t getAbstainCount() const { return m_votes.m_abs; } - int32_t getNoCount() const { return m_votes.m_no; } - int32_t getYesCount() const { return m_votes.m_yes; } + //! Whether the vote counts are known. Only proposals with known counts are + //! listed, so the counts below are read only for those. + bool hasVotes() const { return m_votes.has_value(); } + int32_t getAbsoluteYesCount() const { return votes().m_yes - votes().m_no; } + int32_t getAbstainCount() const { return votes().m_abs; } + int32_t getNoCount() const { return votes().m_no; } + int32_t getYesCount() const { return votes().m_yes; } ProposalStatus status(bool is_fundable) const; QDateTime collateralDate() const { return m_date_collateral; } QDateTime endDate() const { return m_endDate; } diff --git a/src/qt/sharedmncreatedialog.cpp b/src/qt/sharedmncreatedialog.cpp index 5588e7236022..4466f6d7e69a 100644 --- a/src/qt/sharedmncreatedialog.cpp +++ b/src/qt/sharedmncreatedialog.cpp @@ -185,7 +185,8 @@ QString ShareName(const MnShareSession& session, int index) } //! Why the network would refuse these service addresses because a registered -//! masternode already uses one of them (empty = none is in use) +//! masternode already uses one of them, or that this cannot be checked yet +//! (empty = none is in use) QString ServiceInUseProblem(interfaces::Node& node, const QString& services) { interfaces::ProviderNetInfo net_info; @@ -193,12 +194,15 @@ QString ServiceInUseProblem(interfaces::Node& node, const QString& services) net_info.core_p2p.push_back(entry.toStdString()); } const auto capabilities{node.evo().getProviderTxCapabilities()}; - const auto in_use{node.evo().findProviderNetInfoInUse(net_info, MnType::Regular, capabilities.version, - capabilities.ipv4_optional)}; - if (!in_use) return {}; + if (!capabilities) return MasternodeWidgetUtil::masternodeStateUnavailableText(); + const auto result{node.evo().findProviderNetInfoInUse(net_info, MnType::Regular, capabilities->version, + capabilities->ipv4_optional)}; + const auto* in_use{std::get_if>(&result)}; + if (!in_use) return MasternodeWidgetUtil::masternodeStateUnavailableText(); + if (!*in_use) return {}; return SharedMnCreateDialog::tr("%1 is already the service address of a registered masternode. Every service " "address must be unique on the network.") - .arg(QString::fromStdString(*in_use)); + .arg(QString::fromStdString(**in_use)); } } // anonymous namespace @@ -1271,8 +1275,8 @@ void SharedMnCreateDialog::enterPage(Page page) QString gate; if (m_wallet_model == nullptr) { gate = tr("No wallet is available."); - } else if (!m_v24_active) { - gate = SharedMnV24InactiveMessage(); + } else if (!m_v24_active.value_or(false)) { + gate = SharedMnV24InactiveMessage(m_v24_active); } else if (!canSign()) { gate = tr("This wallet cannot sign. You can view, but not take part."); } @@ -1380,9 +1384,11 @@ bool SharedMnCreateDialog::validatePage(Page page, QString& err) for (const QString& entry : MasternodeWidgetUtil::tokenizeEndpointList(m_service_edit->text())) { net_info.core_p2p.push_back(entry.toStdString()); } - const auto capabilities{m_node.evo().getProviderTxCapabilities()}; - if (const auto error{m_node.evo().validateProviderNetInfo(net_info, MnType::Regular, capabilities.version, - capabilities.ipv4_optional, /*optional=*/true)}) { + if (const auto capabilities{m_node.evo().getProviderTxCapabilities()}; !capabilities) { + err = MasternodeWidgetUtil::masternodeStateUnavailableText(); + } else if (const auto error{m_node.evo().validateProviderNetInfo(net_info, MnType::Regular, capabilities->version, + capabilities->ipv4_optional, + /*optional=*/true)}) { err = QString::fromStdString(error->message.translated); } else { err = ServiceInUseProblem(m_node, m_service_edit->text()); @@ -2343,8 +2349,8 @@ void SharedMnCreateDialog::pushSessionToWidgets() void SharedMnCreateDialog::startSession() { - if (!m_v24_active) { - showError(SharedMnV24InactiveMessage()); + if (!m_v24_active.value_or(false)) { + showError(SharedMnV24InactiveMessage(m_v24_active)); return; } if (m_wallet_model == nullptr) { diff --git a/src/qt/sharedmncreatedialog.h b/src/qt/sharedmncreatedialog.h index 9a33083524a1..01b1fdb45abd 100644 --- a/src/qt/sharedmncreatedialog.h +++ b/src/qt/sharedmncreatedialog.h @@ -386,7 +386,7 @@ private Q_SLOTS: interfaces::Node& m_node; WalletModel* const m_wallet_model; - const bool m_v24_active; + const std::optional m_v24_active; MnShareSession m_session; //! Null without a wallet, when nothing can be prepared or signed here MasternodeOperationRunner* const m_runner; diff --git a/src/qt/sharedmndialogs.cpp b/src/qt/sharedmndialogs.cpp index afd83188146a..6b303e3aca5a 100644 --- a/src/qt/sharedmndialogs.cpp +++ b/src/qt/sharedmndialogs.cpp @@ -406,8 +406,8 @@ void UpdateShareDialog::validate() if (!m_is_shared) { showError(tr("This masternode is not shared and has no share table.")); - } else if (!m_v24_active) { - showError(SharedMnV24InactiveMessage()); + } else if (!m_v24_active.value_or(false)) { + showError(SharedMnV24InactiveMessage(m_v24_active)); } else if (m_share_combo->count() == 0) { showError(NoShareKeysMessage()); } else if (!m_reward_edit->text().trimmed().isEmpty() && !problem.isEmpty()) { @@ -418,7 +418,7 @@ void UpdateShareDialog::validate() clearError(); } - setOkValid(m_v24_active && m_is_shared && has_share && problem.isEmpty() && + setOkValid(m_v24_active.value_or(false) && m_is_shared && has_share && problem.isEmpty() && !m_fee_source->selectedAddress().isEmpty()); } @@ -894,9 +894,9 @@ bool SharedMnDialog::gateButton(QPushButton* button) const button->setToolTip(tr("This wallet is watch-only and cannot sign.")); return false; } - if (!m_v24_active) { + if (!m_v24_active.value_or(false)) { button->setEnabled(false); - button->setToolTip(SharedMnV24InactiveMessage()); + button->setToolTip(SharedMnV24InactiveMessage(m_v24_active)); return false; } return true; diff --git a/src/qt/sharedmndialogs.h b/src/qt/sharedmndialogs.h index a39536f6f846..ff68ecb2b026 100644 --- a/src/qt/sharedmndialogs.h +++ b/src/qt/sharedmndialogs.h @@ -63,7 +63,7 @@ class UpdateShareDialog : public MasternodeActionDialog //! Fill the reward field with a fresh address of this wallet void useNewAddress(); - const bool m_v24_active; + const std::optional m_v24_active; const bool m_is_shared; const QString m_voting_address; const std::vector m_shares; @@ -243,7 +243,7 @@ class SharedMnDialog : public QDialog const CAmount m_early_penalty; const uint32_t m_early_period_blocks; const int m_registered_height; - const bool m_v24_active; + const std::optional m_v24_active; //! Null without a wallet, in which case every action button is gated off MasternodeOperationRunner* const m_runner; diff --git a/src/qt/sharedmnwidgets.cpp b/src/qt/sharedmnwidgets.cpp index f459f7d50e39..5f8b9646ce9d 100644 --- a/src/qt/sharedmnwidgets.cpp +++ b/src/qt/sharedmnwidgets.cpp @@ -89,8 +89,13 @@ bool SharedMnWalletOwnsShare(const WalletModel* wallet_model, const interfaces:: return wallet_model != nullptr && wallet_model->wallet().isSpendable(PKHash(share.keyIDOwner)); } -QString SharedMnV24InactiveMessage() +QString SharedMnV24InactiveMessage(std::optional v24_active) { + if (!v24_active) { + return QCoreApplication::translate("SharedMnDialog", + "This node cannot tell yet whether the v24 upgrade is active. Try again " + "later."); + } return QCoreApplication::translate("SharedMnDialog", "Shared masternodes need the v24 upgrade, which is not active on this network " "yet."); diff --git a/src/qt/sharedmnwidgets.h b/src/qt/sharedmnwidgets.h index c2b7b981c1ee..248bc379edc0 100644 --- a/src/qt/sharedmnwidgets.h +++ b/src/qt/sharedmnwidgets.h @@ -47,7 +47,7 @@ BitcoinUnits::Unit SharedMnDisplayUnit(const WalletModel* wallet_model); bool SharedMnWalletOwnsShare(const WalletModel* wallet_model, const interfaces::MnShare& share); //! The one wording for the v24 activation gate -QString SharedMnV24InactiveMessage(); +QString SharedMnV24InactiveMessage(std::optional v24_active); //! Broadcast a fully signed transaction under the same fee cap "sendrawtransaction" //! applies. Returns why it was rejected, ready to show to the user, or an empty diff --git a/src/qt/test/masternodewidgettests.cpp b/src/qt/test/masternodewidgettests.cpp index 86c04735b327..194db3cc9f86 100644 --- a/src/qt/test/masternodewidgettests.cpp +++ b/src/qt/test/masternodewidgettests.cpp @@ -10,13 +10,16 @@ #include #include #include +#include #include +#include #include #include #include #include #include #include +#include #include #include #include @@ -26,7 +29,6 @@ #include #include #include -#include #include #include