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/src/Makefile.am b/src/Makefile.am index 7f8667d9be2a..eb97377d0d46 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -235,10 +235,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 \ @@ -570,6 +573,7 @@ libbitcoin_node_a_SOURCES = \ evo/simplifiedmns.cpp \ evo/smldiff.cpp \ evo/snapshot.cpp \ + evo/snapshot_config.cpp \ evo/specialtx.cpp \ evo/specialtx_filter.cpp \ evo/specialtxman.cpp \ @@ -1312,6 +1316,7 @@ libdashkernel_la_SOURCES = \ evo/providertx_util.cpp \ evo/simplifiedmns.cpp \ evo/smldiff.cpp \ + evo/snapshot_config.cpp \ evo/specialtx.cpp \ evo/specialtx_filter.cpp \ evo/specialtxman.cpp \ 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/bls/bls.cpp b/src/bls/bls.cpp index f1bb58767376..7239df0a0094 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 dad1b39d6ab2..7119d2290570 100644 --- a/src/bls/bls.h +++ b/src/bls/bls.h @@ -19,12 +19,48 @@ #include #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}; @@ -181,7 +217,7 @@ class CBLSWrapper template inline void Serialize(Stream& s) const { - Serialize(s, bls::bls_legacy_scheme.load()); + Serialize(s, bls::EffectiveLegacyScheme()); } template @@ -209,7 +245,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 @@ -232,7 +268,7 @@ class CBLSWrapper inline std::string ToString() const { - return ToString(bls::bls_legacy_scheme.load()); + return ToString(bls::EffectiveLegacyScheme()); } }; @@ -369,8 +405,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); @@ -414,7 +454,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/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..efd9e2bdb522 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}); } } diff --git a/src/chainlock/signing.cpp b/src/chainlock/signing.cpp index 121b164c760e..f379a8b5d028 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(); } 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/evo/cbtx.cpp b/src/evo/cbtx.cpp index a81a61a1648c..7741f395a389 100644 --- a/src/evo/cbtx.cpp +++ b/src/evo/cbtx.cpp @@ -4,6 +4,8 @@ #include +#include +#include #include #include #include @@ -206,6 +208,15 @@ std::optional> GetNonNullCoinbaseChainlock(co CBlock block; if (!blockman.ReadBlockFromDisk(block, *pindex)) { + // 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 && + !WITH_LOCK(::cs_main, return pindex->nStatus & BLOCK_HAVE_DATA)) { + throw BlockDataUnavailableError( + strprintf("coinbase of block %s at or below the snapshot base is not available", + pindex->GetBlockHash().ToString())); + } return std::nullopt; } diff --git a/src/evo/creditpool.cpp b/src/evo/creditpool.cpp index 3f0aeb52e665..63ba920ed302 100644 --- a/src/evo/creditpool.cpp +++ b/src/evo/creditpool.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -78,9 +79,17 @@ static std::optional GetCreditDataFromBlock(const gsl::n 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"); } if (block.vtx.empty() || block.vtx[0]->vExtraPayload.empty() || !block.vtx[0]->IsSpecialTxVersion()) { @@ -124,8 +133,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 @@ -136,7 +146,7 @@ std::optional CCreditPoolManager::GetFromCache(const CBlockIndex& b if (block_index.nHeight % DISK_SNAPSHOT_PERIOD == 0) { 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 +176,9 @@ 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.insert(block_hash, pool); + creditPoolCache.For(view).insert(block_hash, pool); } CCreditPool CCreditPoolManager::ConstructCreditPool(const gsl::not_null block_index, CCreditPool prev) @@ -278,6 +289,7 @@ CCreditPoolManager::CCreditPoolManager(CEvoDB& _evoDb, const ChainstateManager& evoDb{_evoDb}, m_chainman{chainman} { + creditPoolCache.SetOwner(_evoDb); } CCreditPoolManager::~CCreditPoolManager() = default; @@ -418,6 +430,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..85d68ba1b1e1 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 @@ -121,7 +122,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; diff --git a/src/evo/deterministicmns.cpp b/src/evo/deterministicmns.cpp index 70288671d0a6..686af2316582 100644 --- a/src/evo/deterministicmns.cpp +++ b/src/evo/deterministicmns.cpp @@ -670,10 +670,36 @@ 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; @@ -697,6 +723,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 +766,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 +812,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 +845,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 +859,32 @@ 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); // 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 +910,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 +942,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 +969,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 +1001,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 +1028,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 +1050,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 +1073,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 +1210,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 +1221,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 +1331,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 +1530,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 +1562,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..39c93de9dd52 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,42 @@ 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); + //! 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 +880,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..8d8b79abfead 100644 --- a/src/evo/evodb.cpp +++ b/src/evo/evodb.cpp @@ -4,9 +4,19 @@ #include +#include +#include #include +#include +#include +#include +#include +#include +#include +#include #include +#include CEvoDBScopedCommitter::CEvoDBScopedCommitter(CEvoDB& _evoDB, EvoDbIdentity identity) : evoDB{_evoDB}, @@ -34,10 +44,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 +102,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 +119,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() { - if (active_transaction.has_value() && active_transaction_thread == std::this_thread::get_id()) { - return *active_transaction; + 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 +{ + 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 +299,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 +322,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 +345,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 +374,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 +384,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 +414,464 @@ 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) +{ + 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 (!db->WriteBatch(batch, /*fSync=*/true)) return false; + m_snapshot_generation = record; + UpdateLiveGeneration(); + return true; +} + +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 {}; +} +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_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_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..8837a8548a62 100644 --- a/src/evo/evodb.h +++ b/src/evo/evodb.h @@ -6,16 +6,26 @@ #define BITCOIN_EVO_EVODB_H #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 +42,20 @@ 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"; +// 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 +74,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 +303,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 +340,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 +369,55 @@ 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(); } + //! 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 +439,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 +533,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 +568,93 @@ 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 + * and the consensus-configuration fingerprint it is valid for. */ + bool PublishSnapshotGeneration(const uint256& config_fingerprint, const std::optional& base_mn_list_hash) + 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 +664,187 @@ 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); + + 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}; + 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()); + return; + } + LOCK(::cs_main); + m_scope.emplace(evodb->ActiveView()); + } + + template + ActiveEvoViewScope(const CEvoDB* evodb, TipFn&& active_tip) + { + if (evodb == nullptr) return; + LOCK(::cs_main); + m_tip = active_tip(); + if (!evodb->IsBound()) m_scope.emplace(evodb->ActiveView()); + } + + //! 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: + const CBlockIndex* m_tip{nullptr}; + std::optional m_scope; +}; + +//! 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..93149bc70e39 100644 --- a/src/evo/mnhftx.cpp +++ b/src/evo/mnhftx.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -48,6 +49,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; } @@ -254,6 +256,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"); @@ -299,9 +303,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 +344,93 @@ 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; + } + 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); - } } -bool CMNHFManager::ForceSignalDBUpdate(const CBlockIndex* tip) +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::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..70a0031fde25 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,10 +133,17 @@ 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); + //! 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); private: 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 returns list of signals available on previous block. 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..b9a8e9081660 100644 --- a/src/evo/specialtxman.cpp +++ b/src/evo/specialtxman.cpp @@ -15,6 +15,8 @@ #include #include #include +#include +#include #include #include #include @@ -166,6 +168,7 @@ bool CheckCbTxBestChainlock(const CCbTx& cbTx, const CBlockIndex* pindex, const } 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,21 +178,25 @@ 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); @@ -215,7 +222,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 +231,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 +247,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 +313,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"); @@ -910,6 +927,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{}; @@ -1092,16 +1113,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 +1133,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 +1162,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 +1200,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..580163a35d6c 100644 --- a/src/evo/specialtxman.h +++ b/src/evo/specialtxman.h @@ -34,6 +34,10 @@ class TxValidationState; class uint256; 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; } @@ -91,8 +95,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) 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/init.cpp b/src/init.cpp index 4c4892f7054b..c596cb7c6d97 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -482,6 +482,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) { @@ -823,6 +824,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); @@ -1506,6 +1508,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); @@ -1565,6 +1697,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(); }); @@ -2052,12 +2187,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(); @@ -2067,7 +2197,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}); @@ -2167,6 +2298,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. @@ -2515,109 +2649,21 @@ 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")); - } + const ActiveEvoViewScope evo_view{node.evodb.get()}; + ThreadImport(chainman, vImportFiles); - 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; + // Load mempool from disk + if (auto* pool{chainman.ActiveChainstate().GetMempool()}) { + LoadMempool(*pool, ShouldPersistMempool(args) ? MempoolPath(args) : fs::path{}, chainman.ActiveChainstate(), {}); + pool->SetLoadTried(!ShutdownRequested()); } - 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..16671d284676 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) { @@ -580,12 +598,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 +622,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..1f04d276c6ef 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 @@ -80,6 +83,7 @@ class NetInstantSend final : public NetHandler, public CValidationInterface 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..98e9def8131c 100644 --- a/src/interfaces/chain.h +++ b/src/interfaces/chain.h @@ -182,8 +182,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 +283,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; 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/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 7a342c448646..21bec6ff7b0b 100644 --- a/src/llmq/blockprocessor.cpp +++ b/src/llmq/blockprocessor.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -16,6 +17,8 @@ #include #include #include +#include +#include #include #include #include @@ -77,14 +80,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 +221,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 +458,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 +474,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 +531,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 +565,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 +676,41 @@ 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; +} + +//! Whether a height-index scan from `pindex` reaches history such a view +//! lacks: a commitment of `llmqType` may have been mined at a height up to +//! both `pindex` and the base. None can be below DIP3. 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. +//! Evaluated before taking the EvoDB lock: deployment state may read EvoDB. +static bool ScanNeedsMissingHistory(const ChainstateManager& chainman, const EvoViewToken& view, + Consensus::LLMQType llmqType, const CBlockIndex* pindex) +{ + const auto& consensus{chainman.GetConsensus()}; + if (!LacksQuorumHistory(view, consensus) || pindex->nHeight < consensus.DIP0003Height) return false; + const CBlockIndex* last_missing{pindex->GetAncestor(std::min(pindex->nHeight, view.base_height))}; + if (chainman.IsQuorumTypeEnabled(llmqType, last_missing->pprev)) return true; + const CBlockIndex* first_minable{pindex->GetAncestor(consensus.DIP0003Height)}; + return chainman.IsQuorumTypeEnabled(llmqType, first_minable->pprev); +} + +//! A short scan that needs the missing history has an unknown result, not a +//! short one. +static void RequireSatisfiedScan(bool needs_missing_history, const char* func, Consensus::LLMQType llmqType) +{ + if (!needs_missing_history) 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 +724,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 +735,16 @@ 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())) { + // A quorum based at or below the base may have been mined there, in + // history the view lacks: the miss does not mean it never was. + const CBlockIndex* quorum_base{m_chainman.m_blockman.LookupBlockIndex(quorumHash)}; + if (quorum_base != nullptr && quorum_base->nHeight <= view.base_height) { + throw BlockDataUnavailableError( + strprintf("%s: commitment for quorum %s at or below the snapshot base is not available", __func__, + quorumHash.ToString())); + } } } @@ -705,6 +767,8 @@ std::pair CQuorumBlockProcessor::GetMinedCommitment(C std::vector CQuorumBlockProcessor::GetMinedCommitmentsUntilBlock(Consensus::LLMQType llmqType, gsl::not_null pindex, size_t maxCount) const { AssertLockNotHeld(m_evoDb.cs); + const bool needs_missing_history{ + ScanNeedsMissingHistory(m_chainman, m_evoDb.CurrentView(), llmqType, pindex)}; LOCK(m_evoDb.cs); auto dbIt = m_evoDb.GetCurTransaction().NewIteratorUniquePtr(); @@ -743,12 +807,19 @@ std::vector CQuorumBlockProcessor::GetMinedCommitmentsUntilB dbIt->Next(); } + if (ret.size() < maxCount) RequireSatisfiedScan(needs_missing_history, __func__, llmqType); return ret; } std::optional CQuorumBlockProcessor::GetLastMinedCommitmentsByQuorumIndexUntilBlock(Consensus::LLMQType llmqType, const CBlockIndex* pindex, int quorumIndex, size_t cycle) const { AssertLockNotHeld(m_evoDb.cs); + const bool needs_missing_history{ + ScanNeedsMissingHistory(m_chainman, m_evoDb.CurrentView(), llmqType, pindex)}; + const auto not_found{[&]() -> std::optional { + RequireSatisfiedScan(needs_missing_history, "GetLastMinedCommitmentsByQuorumIndexUntilBlock", llmqType); + return std::nullopt; + }}; LOCK(m_evoDb.cs); auto dbIt = m_evoDb.GetCurTransaction().NewIteratorUniquePtr(); @@ -764,19 +835,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 +862,7 @@ std::optional CQuorumBlockProcessor::GetLastMinedCommitments dbIt->Next(); } - return std::nullopt; + return not_found(); } std::vector CQuorumBlockProcessor::GetLastMinedCommitmentsPerQuorumIndexUntilBlock( @@ -832,6 +903,120 @@ 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 +{ + // A walk that ends short in history the view lacks did not reach the end. + const bool needs_missing_history{ + ScanNeedsMissingHistory(m_chainman, m_evoDb.CurrentView(), llmqType, pindex)}; + DataStream prefix{}; + prefix << DB_MINED_COMMITMENT_BY_INVERSED_HEIGHT << llmqType; + MinedCommitmentScan scan; + scan.complete = ReadMinedCommitmentIndex(m_evoDb, prefix, *pindex, max_count, scan.bases) && + (scan.bases.size() == max_count || !needs_missing_history); + 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; + } + const bool needs_missing_history{ + ScanNeedsMissingHistory(m_chainman, m_evoDb.CurrentView(), llmqType, pindex)}; + // 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 (needs_missing_history) { + // 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 { diff --git a/src/llmq/blockprocessor.h b/src/llmq/blockprocessor.h index f506a3c99deb..4755baa495fd 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,18 @@ 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}; +}; + /** 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 +83,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 +92,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. @@ -155,6 +172,16 @@ 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; + private: //! Called from every site that writes or erases mined commitment state. void DropQcHashesCache() EXCLUSIVE_LOCKS_REQUIRED(!m_qc_hashes_cache_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..a04aa35670f2 100644 --- a/src/llmq/net_quorum.cpp +++ b/src/llmq/net_quorum.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -645,10 +646,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 +688,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,22 +742,24 @@ 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 } @@ -749,8 +768,7 @@ void NetQuorum::StartCleanupOldQuorumDataThread(gsl::not_nullm_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 +778,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/quorumsman.cpp b/src/llmq/quorumsman.cpp index 48a8fd5bbcdf..d988ed951207 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(); }); @@ -83,8 +90,9 @@ CQuorumPtr CQuorumManager::BuildQuorumFromCommitment(const Consensus::LLMQType l auto quorum = std::make_shared(llmq_params_opt.value(), blsWorker, std::make_unique(std::move(qc)), pQuorumBaseBlockIndex, minedBlockHash, members); + const EvoViewToken view{m_evoDb.CurrentView()}; if (populate_cache && llmq_params_opt->size == 1) { - 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; } @@ -108,7 +116,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; } @@ -178,13 +186,15 @@ std::vector CQuorumManager::ScanQuorums(Consensus::LLMQType llmqTyp } template -PerLlmqTypeCache>& CQuorumManager::ScanCache() const +PerLlmqTypeCache>& CQuorumManager::ScanCache(const EvoViewToken& view) const { + // cppcheck-suppress constVariableReference + auto& caches{m_view_caches.For(view)}; if constexpr (std::is_same_v) { - return scanQuorumsCache; + return caches.scanQuorumsCache; } else { static_assert(std::is_same_v); - return scanCommitmentsCache; + return caches.scanCommitmentsCache; } } @@ -223,9 +233,10 @@ std::vector CQuorumManager::ScanMined(Consensus::LLMQType llmqType, gsl::not_ std::vector vecResult; bool resume{false}; + const EvoViewToken view{m_evoDb.CurrentView()}; if (use_cache) { LOCK(m_cs_maps); - auto& cache = ScanCache(); + auto& cache = ScanCache(view); 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. @@ -298,7 +309,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 +495,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 +517,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 +579,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; } @@ -633,6 +647,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..97a639e673bb 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); @@ -195,8 +205,9 @@ class CQuorumManager final 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 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); bool BuildQuorumContributions(const CFinalCommitmentPtr& fqc, const std::shared_ptr& quorum) const; CQuorumPtr BuildQuorumFromCommitment(Consensus::LLMQType llmqType, 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..449563640c83 100644 --- a/src/llmq/signing_shares.cpp +++ b/src/llmq/signing_shares.cpp @@ -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..f9cf47b62fa5 100644 --- a/src/llmq/snapshot.cpp +++ b/src/llmq/snapshot.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -58,6 +59,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 +80,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 +304,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 +316,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 +335,26 @@ 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); } } // namespace llmq diff --git a/src/llmq/snapshot.h b/src/llmq/snapshot.h index 725072ad84e6..607ad355e39f 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,15 @@ 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; }; } // namespace llmq diff --git a/src/llmq/utils.cpp b/src/llmq/utils.cpp index 541de71ed982..2a5c306c8134 100644 --- a/src/llmq/utils.cpp +++ b/src/llmq/utils.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -500,6 +501,15 @@ 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 the input as + // unavailable instead. + if (util_params.m_qsnapman.IsCarriedOnly(prev_cycles[idx]->m_cycle_index)) { + 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); @@ -629,12 +639,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 +662,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( @@ -702,9 +713,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 +730,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 +746,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,11 +772,11 @@ 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; } @@ -776,6 +790,7 @@ QuorumMembers GetAllQuorumMembers(Consensus::LLMQType llmqType, const UtilParame 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])); @@ -793,7 +808,7 @@ QuorumMembers GetAllQuorumMembers(Consensus::LLMQType llmqType, const UtilParame } 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..9ee926dc0c96 100644 --- a/src/llmq/utils.h +++ b/src/llmq/utils.h @@ -59,6 +59,8 @@ struct BlsCheck { 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..5864dc6d2a09 100644 --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -52,6 +52,7 @@ #include #include #include +#include #include #include #include @@ -1120,6 +1121,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); @@ -3005,14 +3011,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 +3037,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 +3053,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 +3155,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 +3201,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 +5804,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 diff --git a/src/node/blockstorage.cpp b/src/node/blockstorage.cpp index 6c27552bcd89..4385d673713f 100644 --- a/src/node/blockstorage.cpp +++ b/src/node/blockstorage.cpp @@ -1069,6 +1069,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/chainstate.cpp b/src/node/chainstate.cpp index 7ff8b0680270..877e6b3da2c2 100644 --- a/src/node/chainstate.cpp +++ b/src/node/chainstate.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include @@ -49,10 +50,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 @@ -70,28 +100,27 @@ 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)) { + // 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."); return false; } @@ -106,8 +135,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()) { @@ -133,7 +161,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; } @@ -229,6 +259,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( @@ -279,7 +310,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")}; } @@ -319,6 +350,13 @@ 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)}; + } + } if (!options.dash_dbs_in_memory && !options.reindex && !options.reindex_chainstate) { bilingual_str recovery_error; if (!RecoverSnapshotCleanup(evodb, options.data_dir, recovery_error)) { @@ -346,6 +384,15 @@ ChainstateLoadResult LoadChainstate(ChainstateManager& chainman, const CacheSize if (!chainman.DetectSnapshotChainstate(options.mempool, snapshot_error)) { return {ChainstateLoadStatus::FAILURE, snapshot_error}; } + // 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"); + } auto [init_status, init_error] = CompleteChainstateInitialization(chainman, cache_sizes, options, evodb, dmnman, special_tx); @@ -415,6 +462,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) { @@ -449,13 +497,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/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..88bd9b6ca141 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,50 @@ 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()}; +} + +//! Set the misc warning and log `e`, once per operation and active Evo view: +//! GUI models poll these operations, and each warning notifies the GUI. +void ReportEvoStateUnavailable(const CEvoDB* evodb, std::string_view operation, const evo::LocalStateError& e) +{ + static Mutex mutex; + static std::set> reported; + const EvoViewToken view{evodb ? evodb->ActiveView() : EvoViewToken{}}; + if (WITH_LOCK(mutex, return reported.emplace(operation, view.db, view.identity, view.generation).second)) { + // Without a snapshot, only pruning removes the block data Evo state is + // derived from. + const bool pruned{evodb && !evodb->HasLiveGeneration() && dynamic_cast(&e)}; + WarnLocalEvoStateUnavailable(strprintf("%s could not read it", operation), pruned); + uiInterface.NotifyAlertChanged(); + 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}; + return fn(); + } catch (const evo::LocalStateError& e) { + ReportEvoStateUnavailable(evodb, operation, e); + return on_unavailable(); + } +} + class MnEntryImpl : public MnEntry { private: @@ -265,83 +317,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 +462,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 +531,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 +590,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; - } - if (result.allocated + payment_amount > budget) { - // Budget is saturated, cannot fulfill proposal - 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; } - 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 +639,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 +685,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 +750,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 +810,147 @@ 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) { + 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, + }); + } + 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 +1365,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 { @@ -1475,21 +1708,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 +1878,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..bd45316cd67e 100644 --- a/src/node/miner.cpp +++ b/src/node/miner.cpp @@ -220,6 +220,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(); @@ -356,7 +357,7 @@ 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()); + 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 +815,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..257e3cbf40a6 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,14 @@ 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; + } + refreshCollateralCandidates(*registered); } void RegisterMasternodeWizard::refreshCollateralCandidates(const std::set& registered_collaterals) @@ -1391,7 +1425,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 +1538,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 +1581,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..ecf85bb9c1ea 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