From 946f25249b077cf0e36345e7aa44a88f5da5b4f4 Mon Sep 17 00:00:00 2001 From: Nicholas Dudfield Date: Mon, 15 Jun 2026 18:44:05 +0700 Subject: [PATCH] fix(rng): CSF finalizes from advertised set, not live pendingReveals_ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex review caught a CSF/production fidelity gap: production injects entropy from the AGREED entropySetMap_ (selectEntropy), which is frozen at advertise time — late-fetched or conflicting reveals merge into pendingReveals_ but are NOT injected unless a rebuild republishes the hash. The CSF peer fetched into pendingReveals_ AND finalized from pendingReveals_, so a conflict/fetch sim could count reveals production would never inject from. Track the last advertised entropy-set hash (buildEntropySet) and finalize from the sidecar-store snapshot under that hash — the analog of the frozen entropySetMap_. Clean/no-conflict sims are unchanged (the snapshot equals pendingReveals_ there); the model is now faithful for conflict/fetch cases too. Production unaffected (test-harness only). Addresses finding 1 of codex-tier2-final-review. --- src/test/csf/Peer.h | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/src/test/csf/Peer.h b/src/test/csf/Peer.h index be2f018f7..cc3ef6ae7 100644 --- a/src/test/csf/Peer.h +++ b/src/test/csf/Peer.h @@ -342,6 +342,13 @@ struct Peer hash_map pendingExportSigs_; hash_map nodeKeys_; uint256 myEntropySecret_; + // Hash of the entropy reveal set this peer last advertised + // (buildEntropySet). finalizeRoundEntropy injects from the snapshot the + // sidecar store holds under this hash — the analog of production's + // FROZEN entropySetMap_ — not live pendingReveals_, so late-fetched or + // conflicting reveals that never entered the advertised set are not + // counted (matches ConsensusExtensions::selectEntropy). + uint256 lastEntropySetHash_{}; bool entropyFailed_ = false; // Last round summary (for test assertions) @@ -497,10 +504,14 @@ struct Peer buildEntropySet(Ledger::Seq seq) { if (forcedEntropySetHash_) + { + lastEntropySetHash_ = *forcedEntropySetHash_; return *forcedEntropySetHash_; + } auto const hash = hashRngSet(pendingReveals_, seq, "reveal"); peer.sidecarStore.publish( hash, SidecarStore::Type::reveal, pendingReveals_); + lastEntropySetHash_ = hash; return hash; } @@ -607,6 +618,7 @@ struct Peer nodeKeys_.clear(); likelyParticipants_.clear(); myEntropySecret_.zero(); + lastEntropySetHash_.zero(); entropyFailed_ = false; exportSigGateStarted_ = false; exportSigGateStart_ = {}; @@ -759,14 +771,26 @@ struct Peer lastEntropyTier_ = 1; // consensus_fallback }; + // Finalize from the snapshot of the entropy set this peer last + // advertised (the sidecar-store entry for lastEntropySetHash_) — + // the analog of production injecting from the frozen + // entropySetMap_, NOT live pendingReveals_. Late-fetched or + // conflicting reveals that never entered the advertised/aligned set + // are not counted, matching ConsensusExtensions::selectEntropy. + auto const* acceptedSet = + peer.sidecarStore.fetch(lastEntropySetHash_); + std::vector> ordered; - ordered.reserve(pendingReveals_.size()); - for (auto const& [nodeId, reveal] : pendingReveals_) + if (acceptedSet) { - auto const it = nodeKeys_.find(nodeId); - if (it == nodeKeys_.end()) - continue; - ordered.emplace_back(it->second, reveal); + ordered.reserve(acceptedSet->entries.size()); + for (auto const& [nodeId, reveal] : acceptedSet->entries) + { + auto const it = nodeKeys_.find(nodeId); + if (it == nodeKeys_.end()) + continue; + ordered.emplace_back(it->second, reveal); + } } if (entropyFailed_ || ordered.empty())