mirror of
https://github.com/Xahau/xahaud.git
synced 2026-07-23 15:10:10 +00:00
fix(consensus): sanitize live extension build inputs
This commit is contained in:
@@ -2849,6 +2849,60 @@ class ConsensusExtensions_test : public beast::unit_test::suite
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testLiveBuildSetStripsOnlyExtensionPseudos()
|
||||
{
|
||||
testcase("live build set strips only extension pseudo-txs");
|
||||
|
||||
using namespace jtx;
|
||||
Env env{
|
||||
*this, envconfig(validator, ""), supported_amendments(), nullptr};
|
||||
auto const seq = env.closed()->seq() + 1;
|
||||
auto const validatorKey = makeValidatorKeys().front();
|
||||
|
||||
auto const fee = std::make_shared<STTx const>(ttFEE, [&](auto& obj) {
|
||||
obj.setAccountID(sfAccount, AccountID{});
|
||||
obj.setFieldU32(sfLedgerSequence, seq);
|
||||
});
|
||||
auto const amendment =
|
||||
std::make_shared<STTx const>(ttAMENDMENT, [&](auto& obj) {
|
||||
obj.setAccountID(sfAccount, AccountID{});
|
||||
obj.setFieldU32(sfLedgerSequence, seq);
|
||||
obj.setFieldH256(sfAmendment, makeHash("legacy-amendment"));
|
||||
});
|
||||
auto const negativeUNL =
|
||||
std::make_shared<STTx const>(ttUNL_MODIFY, [&](auto& obj) {
|
||||
obj.setAccountID(sfAccount, AccountID{});
|
||||
obj.setFieldU8(sfUNLModifyDisabling, 1);
|
||||
obj.setFieldU32(sfLedgerSequence, seq);
|
||||
obj.setFieldVL(sfUNLModifyValidator, validatorKey);
|
||||
});
|
||||
auto const suppliedEntropy =
|
||||
makeConsensusEntropyTx(seq, makeHash("supplied-digest"), 7);
|
||||
auto const suppliedWitness =
|
||||
makeExportSignaturesTx(seq, makeHash("supplied-export-origin"));
|
||||
|
||||
auto const agreed = makeRCLTxSet(
|
||||
env.app(),
|
||||
{fee, amendment, negativeUNL, suppliedEntropy, suppliedWitness});
|
||||
ConsensusExtensions ce{env.app(), activeNoopJournal()};
|
||||
auto const build = ce.makeLiveBuildTxSet(agreed);
|
||||
|
||||
BEAST_EXPECT(build.suppliedEntropy == 1);
|
||||
BEAST_EXPECT(build.suppliedExportWitnesses == 1);
|
||||
BEAST_EXPECT(build.txns.exists(fee->getTransactionID()));
|
||||
BEAST_EXPECT(build.txns.exists(amendment->getTransactionID()));
|
||||
BEAST_EXPECT(build.txns.exists(negativeUNL->getTransactionID()));
|
||||
BEAST_EXPECT(!build.txns.exists(suppliedEntropy->getTransactionID()));
|
||||
BEAST_EXPECT(!build.txns.exists(suppliedWitness->getTransactionID()));
|
||||
|
||||
auto const expected =
|
||||
makeRCLTxSet(env.app(), {fee, amendment, negativeUNL});
|
||||
BEAST_EXPECT(build.txns.id() == expected.id());
|
||||
BEAST_EXPECT(agreed.exists(suppliedEntropy->getTransactionID()));
|
||||
BEAST_EXPECT(agreed.exists(suppliedWitness->getTransactionID()));
|
||||
}
|
||||
|
||||
void
|
||||
testDiagnosticsJsonAndPositionLogging()
|
||||
{
|
||||
@@ -4346,6 +4400,7 @@ public:
|
||||
testRngSidecarBuildsLocalSnapshots();
|
||||
testOnPreBuildInjectsStandaloneEntropy();
|
||||
testOnPreBuildStripsSuppliedExtensionPseudos();
|
||||
testLiveBuildSetStripsOnlyExtensionPseudos();
|
||||
testDiagnosticsJsonAndPositionLogging();
|
||||
testDecoratePositionSkipsWhenDisabled();
|
||||
testExportSigGateRequiresQuorumAlignment();
|
||||
|
||||
@@ -120,11 +120,13 @@ below-quorum conflict, which lets a minority equivocation recreate a veto.
|
||||
Under no-UNLReport / lost reveals / failed alignment / timeout / impossible
|
||||
quorum, the round mints an **explicitly labeled lower tier**, never an unlabeled
|
||||
or non-deterministic value. The tier-1 fallback is a pure function of
|
||||
*already-agreed* inputs: `H(entropyFallback, parentLedgerHash, agreedTxSetHash,
|
||||
*already-agreed* inputs: `H(entropyFallback, parentLedgerHash, buildTxSetHash,
|
||||
seq)` — and must **never** depend on the post-injection tx set (no circular
|
||||
dependency on the set that carries the pseudo-tx).
|
||||
*Enforced:* `selectEntropy` fallback path; `agreedTxSetHash` is the
|
||||
pre-injection set hash.
|
||||
dependency on the set that carries the pseudo-tx). `buildTxSetHash` is the raw
|
||||
agreed set after removing only supplied ConsensusEntropy and Export synthetic
|
||||
transactions; legacy protocol pseudos remain included.
|
||||
*Enforced:* `makeLiveBuildTxSet` before ordering and `selectEntropy` fallback;
|
||||
the original consensus-set hash remains separate bookkeeping.
|
||||
|
||||
**INV-6 — Bounded, opt-in entropy quality.**
|
||||
Hooks state `min_tier` explicitly on every draw (no hidden network default).
|
||||
|
||||
@@ -1442,20 +1442,20 @@ ConsensusExtensions::clearAcceptedEntropySet()
|
||||
|
||||
ConsensusExtensions::EntropySelection
|
||||
ConsensusExtensions::selectEntropy(
|
||||
uint256 const& agreedTxSetHash,
|
||||
uint256 const& buildTxSetHash,
|
||||
LedgerIndex seq) const
|
||||
{
|
||||
//@@start entropy-selector-fallback
|
||||
// Tier 1 fallback: consensus-bound deterministic digest over already-agreed
|
||||
// round inputs. agreedTxSetHash is the pre-injection consensus tx set hash:
|
||||
// the digest must never depend on a set that could contain the pseudo-tx
|
||||
// carrying it (circular).
|
||||
// round inputs. buildTxSetHash is the sanitized pre-injection live-build
|
||||
// set hash: the digest must never depend on a set that could contain a
|
||||
// supplied or derived extension pseudo-tx.
|
||||
auto const fallback = [&]() -> EntropySelection {
|
||||
return {
|
||||
sha512Half(
|
||||
HashPrefix::entropyFallback,
|
||||
roundPrevLedgerHash_,
|
||||
agreedTxSetHash,
|
||||
buildTxSetHash,
|
||||
seq),
|
||||
entropyTierConsensusFallback,
|
||||
0,
|
||||
@@ -1675,16 +1675,16 @@ ConsensusExtensions::rngEnabled() const
|
||||
|
||||
uint256
|
||||
ConsensusExtensions::txnOrderingSalt(
|
||||
uint256 const& agreedTxSetHash,
|
||||
uint256 const& buildTxSetHash,
|
||||
LedgerIndex seq) const
|
||||
{
|
||||
if (!rngEnabled())
|
||||
return agreedTxSetHash;
|
||||
return buildTxSetHash;
|
||||
|
||||
auto const selection = selectEntropy(agreedTxSetHash, seq);
|
||||
auto const selection = selectEntropy(buildTxSetHash, seq);
|
||||
return sha512Half(
|
||||
HashPrefix::entropyTxnOrder,
|
||||
agreedTxSetHash,
|
||||
buildTxSetHash,
|
||||
selection.digest,
|
||||
selection.tier,
|
||||
selection.count,
|
||||
@@ -2379,11 +2379,46 @@ ConsensusExtensions::observedParticipantsBitmapBin() const
|
||||
return observedParticipantsBitmapBin_;
|
||||
}
|
||||
|
||||
ConsensusExtensions::LiveBuildTxSet
|
||||
ConsensusExtensions::makeLiveBuildTxSet(RCLTxSet const& agreedTxs) const
|
||||
{
|
||||
RCLTxSet::MutableTxSet mutableSet{agreedTxs};
|
||||
std::vector<uint256> suppliedEntropy;
|
||||
std::vector<uint256> suppliedExportWitnesses;
|
||||
|
||||
for (auto const& item : *agreedTxs.map_)
|
||||
{
|
||||
try
|
||||
{
|
||||
STTx const tx{SerialIter{item.slice()}};
|
||||
if (tx.getTxnType() == ttCONSENSUS_ENTROPY)
|
||||
suppliedEntropy.push_back(item.key());
|
||||
else if (tx.getTxnType() == ttEXPORT_SIGNATURES)
|
||||
suppliedExportWitnesses.push_back(item.key());
|
||||
}
|
||||
catch (std::exception const&)
|
||||
{
|
||||
// Preserve malformed entries here. The existing canonical-set
|
||||
// construction path records their parse failure separately.
|
||||
}
|
||||
}
|
||||
|
||||
for (auto const& id : suppliedEntropy)
|
||||
mutableSet.erase(id);
|
||||
for (auto const& id : suppliedExportWitnesses)
|
||||
mutableSet.erase(id);
|
||||
|
||||
return LiveBuildTxSet{
|
||||
RCLTxSet{mutableSet},
|
||||
suppliedEntropy.size(),
|
||||
suppliedExportWitnesses.size()};
|
||||
}
|
||||
|
||||
void
|
||||
ConsensusExtensions::onPreBuild(
|
||||
CanonicalTXSet& retriableTxs,
|
||||
LedgerIndex seq,
|
||||
uint256 const& txSetHash)
|
||||
uint256 const& buildTxSetHash)
|
||||
{
|
||||
//@@start extension-live-pseudo-authority
|
||||
// The agreed user transaction set never authorizes synthetic extension
|
||||
@@ -2435,9 +2470,9 @@ ConsensusExtensions::onPreBuild(
|
||||
//@@start rng-inject-entropy-selection
|
||||
// One deterministic selector over the AGREED entropySetMap_ chooses the
|
||||
// digest and its tier/count/denominator labels. Every node derives the
|
||||
// same entropy for the same agreed round inputs. txSetHash is the
|
||||
// agreed pre-injection consensus tx set hash.
|
||||
auto const selection = selectEntropy(txSetHash, seq);
|
||||
// same entropy for the same accepted round inputs. buildTxSetHash is
|
||||
// the sanitized pre-injection live-build set hash.
|
||||
auto const selection = selectEntropy(buildTxSetHash, seq);
|
||||
uint256 const finalEntropy = selection.digest;
|
||||
std::uint8_t const entropyTier = selection.tier;
|
||||
std::uint16_t const entropyCount = selection.count;
|
||||
|
||||
@@ -112,6 +112,13 @@ class ConsensusExtensions
|
||||
clearDeferredExportShares();
|
||||
|
||||
public:
|
||||
struct LiveBuildTxSet
|
||||
{
|
||||
RCLTxSet txns;
|
||||
std::size_t suppliedEntropy;
|
||||
std::size_t suppliedExportWitnesses;
|
||||
};
|
||||
|
||||
beast::Journal j_; // public: accessed by extensionsTick template
|
||||
|
||||
/** Proof data from a proposal signature, for embedding in SHAMap
|
||||
@@ -365,15 +372,15 @@ public:
|
||||
/// quorumThreshold), participant_aligned (>= tier2Threshold) or
|
||||
/// consensus_fallback. In non-standalone mode, non-fallback labels require
|
||||
/// an UNLReport-backed active view; the trusted-fallback view is local
|
||||
/// config and mints Tier 1. agreedTxSetHash is the pre-injection consensus
|
||||
/// tx set hash used for the fallback digest.
|
||||
/// config and mints Tier 1. buildTxSetHash is the sanitized pre-injection
|
||||
/// live-build set hash used for the fallback digest.
|
||||
EntropySelection
|
||||
selectEntropy(uint256 const& agreedTxSetHash, LedgerIndex seq) const;
|
||||
selectEntropy(uint256 const& buildTxSetHash, LedgerIndex seq) const;
|
||||
|
||||
/// Extend the legacy closed-ledger transaction-order salt with the same
|
||||
/// consensus entropy selected for the ledger's entropy pseudo-tx.
|
||||
uint256
|
||||
txnOrderingSalt(uint256 const& agreedTxSetHash, LedgerIndex seq) const;
|
||||
txnOrderingSalt(uint256 const& buildTxSetHash, LedgerIndex seq) const;
|
||||
|
||||
bool
|
||||
rngEnabled() const;
|
||||
@@ -509,9 +516,15 @@ public:
|
||||
void
|
||||
onReplayBuild();
|
||||
|
||||
/// txSetHash is the agreed pre-injection consensus tx set hash — an input
|
||||
/// Return the agreed transaction set with only consensus-extension
|
||||
/// synthetic transactions removed. Legacy fee, amendment, nUNL, and other
|
||||
/// protocol pseudos remain ordinary members of the consensus set.
|
||||
LiveBuildTxSet
|
||||
makeLiveBuildTxSet(RCLTxSet const& agreedTxs) const;
|
||||
|
||||
/// txSetHash is the sanitized pre-injection live-build set hash — an input
|
||||
/// to the Tier 1 consensus_fallback digest. It must never be the hash of a
|
||||
/// set that could contain the entropy pseudo-tx itself.
|
||||
/// set that could contain an extension pseudo-tx itself.
|
||||
void
|
||||
onPreBuild(
|
||||
CanonicalTXSet& retriableTxs,
|
||||
|
||||
@@ -361,8 +361,10 @@ non-UNLReport (config-fallback) view, every case below instead mints
|
||||
- No peer entropy hash is observed in time: fall back to the Tier 1 digest.
|
||||
|
||||
The fallback pseudo-transaction is deterministic — every node derives the same
|
||||
digest from `(HashPrefix::entropyFallback, parentLedgerHash, agreedTxSetHash,
|
||||
seq)` — and labeled with `EntropyTier = consensus_fallback`,
|
||||
digest from `(HashPrefix::entropyFallback, parentLedgerHash, buildTxSetHash,
|
||||
seq)`, where `buildTxSetHash` is the agreed set after removing only supplied
|
||||
ConsensusEntropy and Export synthetic transactions — and labels it with
|
||||
`EntropyTier = consensus_fallback`,
|
||||
`EntropyCount = 0`, `EntropyDenominator = 0`, and an empty
|
||||
`EntropyContributors` bitmap. Non-fallback entropy records both the contributor
|
||||
count and the active-validator denominator used for the validator-quorum
|
||||
@@ -398,14 +400,17 @@ ledger's finalized entropy; final buildLCL execution sees the current ledger's
|
||||
entropy pseudo-tx after it updates the SLE. Hooks that need final entropy must
|
||||
treat open-ledger RNG results as previews.
|
||||
|
||||
The fallback digest derives from the agreed pre-injection tx set hash to avoid
|
||||
circularity. The agreed user transaction set is not authority for synthetic
|
||||
extension state: during a live build, every supplied `ttCONSENSUS_ENTROPY` or
|
||||
`ttEXPORT_SIGNATURES` transaction is discarded and logged as an invariant
|
||||
violation, then the canonical synthetic stream is derived from accepted
|
||||
extension evidence. Historical replay is selected before live materialization
|
||||
and consumes the persisted transaction order, including its recorded synthetic
|
||||
transactions, without regeneration.
|
||||
The fallback digest derives from the sanitized pre-injection live-build set
|
||||
hash to avoid circularity and synthetic-input authority. Sanitization removes
|
||||
only supplied `ttCONSENSUS_ENTROPY` and `ttEXPORT_SIGNATURES` transactions;
|
||||
legacy fee, amendment, NegativeUNL, and other protocol pseudos remain ordinary
|
||||
members of the agreed set. Supplied extension pseudos are discarded and logged
|
||||
as an invariant violation before they can influence fallback entropy,
|
||||
transaction ordering, or ledger state, then the canonical synthetic stream is
|
||||
derived from accepted extension evidence. The original agreed-set hash remains
|
||||
the consensus-bookkeeping value carried by validations. Historical replay is
|
||||
selected before live materialization and consumes the persisted transaction
|
||||
order, including its recorded synthetic transactions, without regeneration.
|
||||
|
||||
## Local Snapshot Alignment Rules
|
||||
|
||||
|
||||
@@ -581,20 +581,45 @@ RCLConsensus::Adaptor::doAccept(
|
||||
//--------------------------------------------------------------------------
|
||||
std::set<TxID> failed;
|
||||
|
||||
// Select replay before choosing the transaction stream used to derive the
|
||||
// ledger. Replay consumes the persisted ordered stream. A live build uses
|
||||
// a sanitized view of the agreed set so supplied extension pseudos cannot
|
||||
// influence fallback entropy, transaction ordering, or ledger state.
|
||||
auto replayData = ledgerMaster_.releaseReplay();
|
||||
auto const consensusTxSetHash = result.txns.id();
|
||||
auto const liveBuild = replayData
|
||||
? std::optional<ConsensusExtensions::LiveBuildTxSet>{}
|
||||
: std::optional<ConsensusExtensions::LiveBuildTxSet>{
|
||||
ce().makeLiveBuildTxSet(result.txns)};
|
||||
auto const& buildTxs = liveBuild ? liveBuild->txns : result.txns;
|
||||
auto const buildTxSetHash = buildTxs.id();
|
||||
|
||||
if (liveBuild &&
|
||||
(liveBuild->suppliedEntropy != 0 ||
|
||||
liveBuild->suppliedExportWitnesses != 0))
|
||||
{
|
||||
JLOG(j_.error())
|
||||
<< "ConsensusExtensions: excluded supplied synthetic txs from "
|
||||
"live build"
|
||||
<< " seq=" << (prevLedger.seq() + 1)
|
||||
<< " consensusSet=" << consensusTxSetHash
|
||||
<< " buildSet=" << buildTxSetHash
|
||||
<< " entropy=" << liveBuild->suppliedEntropy
|
||||
<< " exportWitnesses=" << liveBuild->suppliedExportWitnesses;
|
||||
}
|
||||
|
||||
// We want to put transactions in an unpredictable but deterministic order.
|
||||
// ConsensusEntropy extends the agreed tx-set salt with the selected
|
||||
// ledger entropy; when disabled this remains the legacy tx-set hash salt.
|
||||
// ConsensusEntropy extends the sanitized live-build salt with the selected
|
||||
// ledger entropy; when disabled this remains that sanitized set hash.
|
||||
//
|
||||
// FIXME: Use a std::vector and a custom sorter instead of CanonicalTXSet?
|
||||
//@@start txn-ordering-salt-build-inputs
|
||||
auto const agreedTxSetHash = result.txns.map_->getHash().as_uint256();
|
||||
auto const buildSeq = prevLedger.seq() + 1;
|
||||
CanonicalTXSet retriableTxs{
|
||||
ce().txnOrderingSalt(agreedTxSetHash, buildSeq)};
|
||||
CanonicalTXSet retriableTxs{ce().txnOrderingSalt(buildTxSetHash, buildSeq)};
|
||||
|
||||
JLOG(j_.debug()) << "Building canonical tx set: " << retriableTxs.key();
|
||||
|
||||
for (auto const& item : *result.txns.map_)
|
||||
for (auto const& item : *buildTxs.map_)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -611,11 +636,6 @@ RCLConsensus::Adaptor::doAccept(
|
||||
}
|
||||
//@@end txn-ordering-salt-build-inputs
|
||||
|
||||
// Select replay before live materialization. Replay consumes the persisted
|
||||
// ordered transaction stream, including its recorded extension pseudos;
|
||||
// live builds derive extension pseudos from accepted extension evidence.
|
||||
auto replayData = ledgerMaster_.releaseReplay();
|
||||
|
||||
//@@start auxiliary-pre-build-injection
|
||||
// Inject extension pseudo-transactions only for a live build. Entropy and
|
||||
// Export witness injection are independently gated inside onPreBuild;
|
||||
@@ -627,7 +647,7 @@ RCLConsensus::Adaptor::doAccept(
|
||||
}
|
||||
else if (ce().rngEnabled() || ce().exportEnabled())
|
||||
{
|
||||
ce().onPreBuild(retriableTxs, buildSeq, agreedTxSetHash);
|
||||
ce().onPreBuild(retriableTxs, buildSeq, buildTxSetHash);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user