fix(rng): compute entropy from entropySetMap instead of pendingReveals

H2: Compute final entropy from the agreed-upon entropySetMap_ SHAMap
rather than from the local pendingReveals_ in-memory map.

Previously, two nodes with different reveal subsets at timeout would
compute different entropy from their local pendingReveals_ maps,
despite both passing haveConsensus() (which only checks txSetHash).
This could cause a ledger fork.

Now the entropy computation reads directly from the entropySetMap_
whose hash was published in proposals and converged via SHAMap
fetch/merge.  Nodes that agree on entropySetHash deterministically
produce the same entropy regardless of local pendingReveals_ state.

If entropySetMap_ is null (bootstrap skip, pipeline failure), the
existing shouldZeroEntropy() fallback handles it.
This commit is contained in:
Nicholas Dudfield
2026-04-09 15:18:45 +07:00
parent 939e03714c
commit c6fa973cf6

View File

@@ -1101,18 +1101,34 @@ ConsensusExtensions::onPreBuild(CanonicalTXSet& retriableTxs, LedgerIndex seq)
<< seq << " (reveals=" << pendingReveals_.size()
<< " threshold=" << quorumThreshold() << ")";
}
else
else if (entropySetMap_)
{
// Sort reveals deterministically by Validator Public Key
// Compute entropy from the agreed-upon entropySet SHAMap
// rather than from local pendingReveals_. The map's hash
// was published in proposals and converged via fetch/merge,
// so all nodes with the same entropySetHash produce the
// same entropy — preventing reveal-subset divergence at
// timeout boundaries.
//
// Each leaf is an STTx with sfSigningPubKey (validator key)
// and sfDigest (the reveal).
std::vector<std::pair<PublicKey, uint256>> sorted;
sorted.reserve(pendingReveals_.size());
for (auto const& [nodeId, reveal] : pendingReveals_)
{
auto it = nodeIdToKey_.find(nodeId);
if (it != nodeIdToKey_.end())
sorted.emplace_back(it->second, reveal);
}
entropySetMap_->visitLeaves(
[&](boost::intrusive_ptr<SHAMapItem const> const& item) {
try
{
SerialIter sit(item->slice());
STTx tx(std::ref(sit));
auto const pk = tx.getFieldVL(sfSigningPubKey);
if (!publicKeyType(makeSlice(pk)))
return;
sorted.emplace_back(
PublicKey(makeSlice(pk)), tx.getFieldH256(sfDigest));
}
catch (...)
{
}
});
if (!sorted.empty())
{
@@ -1133,7 +1149,7 @@ ConsensusExtensions::onPreBuild(CanonicalTXSet& retriableTxs, LedgerIndex seq)
JLOG(j_.info()) << "RNG: Injecting entropy " << finalEntropy
<< " from " << sorted.size() << " reveals"
<< " for ledger " << seq;
<< " (from entropySetMap) for ledger " << seq;
}
}
//@@end rng-inject-entropy-selection