mirror of
https://github.com/Xahau/xahaud.git
synced 2026-08-23 08:20:52 +00:00
Pin extension apply to accepted sidecar roots
Require entropy injection and export apply to consume only sidecar roots accepted by their convergence gates. Local timeout state and later collector growth no longer participate in closed-ledger material once a sidecar root is accepted. Also guard RCLConsensus::extensionsBusy() under the consensus mutex, cap exported multisig signer arrays to the protocol maximum after canonical signer sorting, and document the remaining accept-vs-timeout liveness tradeoff. Add regressions for unaccepted export sidecar roots, late export collector mutation, entropy accepted-root handling, exported signer caps, and CSF accepted-sidecar behavior.
This commit is contained in:
@@ -206,12 +206,59 @@ public:
|
||||
BEAST_EXPECT(!unsignedMulti.isFieldPresent(sfSigners));
|
||||
}
|
||||
|
||||
void
|
||||
testCapsSignerArray()
|
||||
{
|
||||
testcase("caps exported signer array");
|
||||
|
||||
auto const src = randomKeyPair(KeyType::secp256k1);
|
||||
auto const dst = randomKeyPair(KeyType::secp256k1);
|
||||
auto const innerTx = makeExportedPayment(
|
||||
calcAccountID(src.first), calcAccountID(dst.first));
|
||||
|
||||
ExportResultBuilder::SignatureSnapshot signatures;
|
||||
while (signatures.size() < STTx::maxMultiSigners() + 5)
|
||||
{
|
||||
auto const signer = randomKeyPair(KeyType::secp256k1);
|
||||
signatures.emplace(
|
||||
signer.first,
|
||||
ExportResultBuilder::signExportedTxn(
|
||||
innerTx, signer.first, signer.second));
|
||||
}
|
||||
|
||||
auto assembled = ExportResultBuilder::assemble(
|
||||
innerTx, signatures, 789, makeHash("many-sig-export"));
|
||||
|
||||
BEAST_EXPECT(assembled.signerCount == STTx::maxMultiSigners());
|
||||
|
||||
auto const& multiSigned =
|
||||
assembled.metadata.peekAtField(sfExportedTxn).downcast<STObject>();
|
||||
BEAST_EXPECT(multiSigned.isFieldPresent(sfSigners));
|
||||
|
||||
if (multiSigned.isFieldPresent(sfSigners))
|
||||
{
|
||||
auto const& signers = multiSigned.getFieldArray(sfSigners);
|
||||
BEAST_EXPECT(signers.size() == STTx::maxMultiSigners());
|
||||
for (std::size_t i = 1; i < signers.size(); ++i)
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
signers[i - 1].getAccountID(sfAccount) <
|
||||
signers[i].getAccountID(sfAccount));
|
||||
}
|
||||
}
|
||||
|
||||
BEAST_EXPECT(
|
||||
assembled.signedTxHash ==
|
||||
multiSigned.getHash(HashPrefix::transactionID));
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testAssemblesSignedMetadata();
|
||||
testSkipsEmptySignatures();
|
||||
testBuildMultiSignedExportedTxnDirect();
|
||||
testCapsSignerArray();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -811,10 +811,12 @@ struct Export_test : public beast::unit_test::suite
|
||||
txHash, valPK, originalSig, applySeq);
|
||||
auto const agreedHash = ce.buildExportSigSet(applySeq);
|
||||
BEAST_EXPECT(ce.isSidecarSet(agreedHash));
|
||||
ce.acceptExportSigSet(agreedHash);
|
||||
ce.setExportSigConvergenceFailed();
|
||||
|
||||
// Simulate an asynchronous collector mutation after sidecar agreement.
|
||||
// A bad revert to the live collector at apply would assemble this late
|
||||
// signature and write a different shadow-ticket hash.
|
||||
// Closed-ledger apply must derive the signature snapshot from the
|
||||
// agreed sidecar, not from the live collector or a local timeout flag.
|
||||
std::uint8_t const lateBytes[] = {9, 8, 7};
|
||||
Buffer const lateSig{lateBytes, sizeof(lateBytes)};
|
||||
ce.exportSigCollector().addVerifiedSignature(
|
||||
@@ -895,6 +897,7 @@ struct Export_test : public beast::unit_test::suite
|
||||
txHash, valPK, sig, applySeq);
|
||||
auto const agreedHash = ce.buildExportSigSet(applySeq);
|
||||
BEAST_EXPECT(ce.isSidecarSet(agreedHash));
|
||||
ce.acceptExportSigSet(agreedHash);
|
||||
|
||||
auto const parent = env.app().getLedgerMaster().getClosedLedger();
|
||||
auto next = std::make_shared<Ledger>(
|
||||
@@ -964,6 +967,7 @@ struct Export_test : public beast::unit_test::suite
|
||||
txHash, valPK, sig, applySeq);
|
||||
auto const agreedHash = ce.buildExportSigSet(applySeq);
|
||||
BEAST_EXPECT(ce.isSidecarSet(agreedHash));
|
||||
ce.acceptExportSigSet(agreedHash);
|
||||
}
|
||||
|
||||
auto next = std::make_shared<Ledger>(
|
||||
|
||||
@@ -494,6 +494,16 @@ struct FakeExtensions
|
||||
return entropyHash;
|
||||
}
|
||||
|
||||
void
|
||||
acceptEntropySet(uint256 const&)
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
clearAcceptedEntropySet()
|
||||
{
|
||||
}
|
||||
|
||||
uint256
|
||||
getEntropySecret() const
|
||||
{
|
||||
@@ -556,6 +566,16 @@ struct FakeExtensions
|
||||
exportSigConvergenceFailed_ = true;
|
||||
}
|
||||
|
||||
void
|
||||
acceptExportSigSet(uint256 const&)
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
clearAcceptedExportSigSet()
|
||||
{
|
||||
}
|
||||
|
||||
template <class PeerPositions>
|
||||
void
|
||||
recordParticipantDiagnostics(ConsensusMode, PeerPositions const&)
|
||||
@@ -1331,7 +1351,12 @@ class ConsensusExtensions_test : public beast::unit_test::suite
|
||||
BEAST_EXPECT(ce.hasMinimumReveals());
|
||||
|
||||
auto const entropySetHash = ce.buildEntropySet(seq);
|
||||
ce.acceptEntropySet(entropySetHash);
|
||||
BEAST_EXPECT(ce.isSidecarSet(entropySetHash));
|
||||
// Once the sidecar gate has accepted a hash, injection is derived from
|
||||
// that sidecar snapshot. A local failure flag is diagnostic state, not
|
||||
// an additional selector input.
|
||||
ce.setEntropyFailed();
|
||||
|
||||
CanonicalTXSet retriableTxs{makeHash("entropy-set-prebuild-salt")};
|
||||
ce.onPreBuild(retriableTxs, seq, txSetHash);
|
||||
@@ -1419,7 +1444,8 @@ class ConsensusExtensions_test : public beast::unit_test::suite
|
||||
prevLedger,
|
||||
reveal);
|
||||
}
|
||||
ce.buildEntropySet(seq);
|
||||
auto const entropySetHash = ce.buildEntropySet(seq);
|
||||
ce.acceptEntropySet(entropySetHash);
|
||||
CanonicalTXSet txs{makeHash("tier2-salt")};
|
||||
ce.onPreBuild(txs, seq, txSetHash);
|
||||
auto const tx = singleCanonicalTx(txs);
|
||||
@@ -1585,7 +1611,8 @@ class ConsensusExtensions_test : public beast::unit_test::suite
|
||||
prevLedger,
|
||||
reveal);
|
||||
}
|
||||
ce.buildEntropySet(seq);
|
||||
auto const entropySetHash = ce.buildEntropySet(seq);
|
||||
ce.acceptEntropySet(entropySetHash);
|
||||
CanonicalTXSet txs{makeHash("tier2-nunl-salt")};
|
||||
ce.onPreBuild(txs, seq, txSetHash);
|
||||
auto const tx = singleCanonicalTx(txs);
|
||||
@@ -1958,6 +1985,14 @@ class ConsensusExtensions_test : public beast::unit_test::suite
|
||||
txHash, valPK, originalSig, seq);
|
||||
auto const exportSigSetHash = ce.buildExportSigSet(seq);
|
||||
BEAST_EXPECT(ce.isSidecarSet(exportSigSetHash));
|
||||
auto const view = ce.activeValidatorView();
|
||||
|
||||
// A locally-built export signature map is not closed-ledger material
|
||||
// until the export sidecar gate accepts that exact root.
|
||||
BEAST_EXPECT(!ce.agreedExportSignatures(*exportTx, txHash, *view, 1));
|
||||
ce.acceptExportSigSet(makeHash("wrong-export-sigset-root"));
|
||||
BEAST_EXPECT(!ce.agreedExportSignatures(*exportTx, txHash, *view, 1));
|
||||
ce.acceptExportSigSet(exportSigSetHash);
|
||||
|
||||
// Simulate a late local collector mutation after the sidecar hash has
|
||||
// converged. The live collector now differs from the agreed sidecar
|
||||
@@ -1967,7 +2002,6 @@ class ConsensusExtensions_test : public beast::unit_test::suite
|
||||
ce.exportSigCollector().addVerifiedSignature(
|
||||
txHash, valPK, lateSig, seq);
|
||||
|
||||
auto const view = ce.activeValidatorView();
|
||||
auto const live = ce.exportSigCollector().checkQuorumAndSnapshot(
|
||||
txHash, 1, [&](PublicKey const& pk) {
|
||||
return ce.isActiveValidator(pk, *view);
|
||||
|
||||
@@ -341,12 +341,12 @@ struct Peer
|
||||
hash_map<PeerID, PeerKey> 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).
|
||||
// (buildEntropySet). The tick gate copies it to
|
||||
// acceptedEntropySetHash_ after observation/alignment checks pass; only
|
||||
// that accepted snapshot may feed finalizeRoundEntropy.
|
||||
uint256 lastEntropySetHash_{};
|
||||
std::optional<uint256> acceptedEntropySetHash_;
|
||||
std::optional<uint256> acceptedExportSigSetHash_;
|
||||
bool entropyFailed_ = false;
|
||||
|
||||
// Last round summary (for test assertions)
|
||||
@@ -572,6 +572,30 @@ struct Peer
|
||||
entropyFailed_ = true;
|
||||
}
|
||||
|
||||
void
|
||||
acceptEntropySet(uint256 const& hash)
|
||||
{
|
||||
acceptedEntropySetHash_ = hash;
|
||||
}
|
||||
|
||||
void
|
||||
clearAcceptedEntropySet()
|
||||
{
|
||||
acceptedEntropySetHash_.reset();
|
||||
}
|
||||
|
||||
void
|
||||
acceptExportSigSet(uint256 const& hash)
|
||||
{
|
||||
acceptedExportSigSetHash_ = hash;
|
||||
}
|
||||
|
||||
void
|
||||
clearAcceptedExportSigSet()
|
||||
{
|
||||
acceptedExportSigSetHash_.reset();
|
||||
}
|
||||
|
||||
enum class SidecarKind : uint8_t {
|
||||
commitSet,
|
||||
entropySet,
|
||||
@@ -668,6 +692,8 @@ struct Peer
|
||||
likelyParticipants_.clear();
|
||||
myEntropySecret_.zero();
|
||||
lastEntropySetHash_.zero();
|
||||
acceptedEntropySetHash_.reset();
|
||||
acceptedExportSigSetHash_.reset();
|
||||
entropyFailed_ = false;
|
||||
exportSigGateStarted_ = false;
|
||||
exportSigGateStart_ = {};
|
||||
@@ -817,8 +843,8 @@ struct Peer
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback when the round failed alignment (entropyFailed_ is set
|
||||
// by the tick entropy gate) or yielded no reveals.
|
||||
// Fallback when the tick gate did not accept an entropy sidecar or
|
||||
// the accepted sidecar yielded no reveals.
|
||||
auto const fallback = [&] {
|
||||
lastEntropyDigest_ = fallbackEntropy();
|
||||
lastEntropyCount_ = 0;
|
||||
@@ -832,8 +858,9 @@ struct Peer
|
||||
// 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_);
|
||||
auto const* acceptedSet = acceptedEntropySetHash_
|
||||
? peer.sidecarStore.fetch(*acceptedEntropySetHash_)
|
||||
: nullptr;
|
||||
|
||||
std::vector<std::pair<PeerKey, uint256>> ordered;
|
||||
// Defensive: lastEntropySetHash_ only ever names a reveal set (the
|
||||
@@ -852,7 +879,7 @@ struct Peer
|
||||
}
|
||||
}
|
||||
|
||||
if (entropyFailed_ || ordered.empty())
|
||||
if (ordered.empty())
|
||||
{
|
||||
fallback();
|
||||
return;
|
||||
@@ -907,15 +934,22 @@ struct Peer
|
||||
return;
|
||||
}
|
||||
|
||||
auto const activeSigCount = std::count_if(
|
||||
pendingExportSigs_.begin(),
|
||||
pendingExportSigs_.end(),
|
||||
[&](auto const& entry) {
|
||||
return isUNLReportMember(entry.first);
|
||||
});
|
||||
lastExportSucceeded_ = !exportSigConvergenceFailed_ &&
|
||||
static_cast<std::size_t>(activeSigCount) >=
|
||||
exportSigQuorumThreshold();
|
||||
auto const* acceptedSet = acceptedExportSigSetHash_
|
||||
? peer.sidecarStore.fetch(*acceptedExportSigSetHash_)
|
||||
: nullptr;
|
||||
std::size_t activeSigCount = 0;
|
||||
if (acceptedSet &&
|
||||
acceptedSet->type == SidecarStore::Type::exportSig)
|
||||
{
|
||||
activeSigCount = std::count_if(
|
||||
acceptedSet->entries.begin(),
|
||||
acceptedSet->entries.end(),
|
||||
[&](auto const& entry) {
|
||||
return isUNLReportMember(entry.first);
|
||||
});
|
||||
}
|
||||
|
||||
lastExportSucceeded_ = activeSigCount >= exportSigQuorumThreshold();
|
||||
lastExportRetried_ = !lastExportSucceeded_;
|
||||
}
|
||||
|
||||
|
||||
@@ -442,6 +442,18 @@ ConsensusExtensions::hasAnyReveals() const
|
||||
return !pendingReveals_.empty();
|
||||
}
|
||||
|
||||
void
|
||||
ConsensusExtensions::acceptEntropySet(uint256 const& hash)
|
||||
{
|
||||
acceptedEntropySetHash_ = hash;
|
||||
}
|
||||
|
||||
void
|
||||
ConsensusExtensions::clearAcceptedEntropySet()
|
||||
{
|
||||
acceptedEntropySetHash_.reset();
|
||||
}
|
||||
|
||||
ConsensusExtensions::EntropySelection
|
||||
ConsensusExtensions::selectEntropy(
|
||||
uint256 const& baseTxSetHash,
|
||||
@@ -503,11 +515,11 @@ ConsensusExtensions::selectEntropy(
|
||||
}
|
||||
//@@end entropy-selector-unlreport-gate
|
||||
|
||||
// No agreed entropy set (round failed, or none was built) → fallback. A
|
||||
// sub-quorum-but-aligned set may still qualify for participant_aligned
|
||||
// (tier 2); the tier ladder below decides from the agreed participant
|
||||
// count.
|
||||
if (entropyFailed_ || !entropySetMap_)
|
||||
// A cached entropySetMap_ is only candidate material. The tick gate marks
|
||||
// the sidecar hash accepted after peer-observation/alignment checks have
|
||||
// completed; injection never consults timeout state directly.
|
||||
if (!acceptedEntropySetHash_ || !entropySetMap_ ||
|
||||
entropySetMap_->getHash().as_uint256() != *acceptedEntropySetHash_)
|
||||
return fallback();
|
||||
|
||||
// Derive from the AGREED entropySetMap_ — NOT local pendingReveals_. The
|
||||
@@ -830,6 +842,18 @@ ConsensusExtensions::exportSigConvergenceFailed() const
|
||||
return exportSigConvergenceFailed_;
|
||||
}
|
||||
|
||||
void
|
||||
ConsensusExtensions::acceptExportSigSet(uint256 const& hash)
|
||||
{
|
||||
acceptedExportSigSetHash_ = hash;
|
||||
}
|
||||
|
||||
void
|
||||
ConsensusExtensions::clearAcceptedExportSigSet()
|
||||
{
|
||||
acceptedExportSigSetHash_.reset();
|
||||
}
|
||||
|
||||
std::optional<ConsensusExtensions::ExportSignatureSnapshot>
|
||||
ConsensusExtensions::agreedExportSignatures(
|
||||
STTx const& exportTx,
|
||||
@@ -847,6 +871,27 @@ ConsensusExtensions::agreedExportSignatures(
|
||||
ExportSignatureSnapshot signatures;
|
||||
bool invalid = false;
|
||||
auto const agreedHash = exportSigSetMap_->getHash().as_uint256();
|
||||
// A local exportSigSetMap_ is only candidate material until the sidecar
|
||||
// gate accepts its root. Without this guard, a timed-out node with a local
|
||||
// partial-but-quorum map could mint a different signed export blob from
|
||||
// the quorum-aligned nodes.
|
||||
if (!acceptedExportSigSetHash_)
|
||||
{
|
||||
JLOG(j_.warn()) << "Export: exportSigSet not accepted"
|
||||
<< " setHash=" << agreedHash << " txHash=" << txHash
|
||||
<< " threshold=" << threshold;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (agreedHash != *acceptedExportSigSetHash_)
|
||||
{
|
||||
JLOG(j_.warn()) << "Export: exportSigSet hash not accepted"
|
||||
<< " setHash=" << agreedHash
|
||||
<< " acceptedHash=" << *acceptedExportSigSetHash_
|
||||
<< " txHash=" << txHash << " threshold=" << threshold;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
exportSigSetMap_->visitLeaves(
|
||||
[&](boost::intrusive_ptr<SHAMapItem const> const& item) {
|
||||
if (invalid)
|
||||
@@ -983,6 +1028,7 @@ ConsensusExtensions::clearRngStatePreservingExport()
|
||||
entropyFailed_ = false;
|
||||
commitSetMap_.reset();
|
||||
entropySetMap_.reset();
|
||||
acceptedEntropySetHash_.reset();
|
||||
rngRoundSeq_.reset();
|
||||
consensusTxSetMap_.reset();
|
||||
consensusExportTxns_.clear();
|
||||
@@ -1017,6 +1063,7 @@ ConsensusExtensions::clearRngState()
|
||||
exportSigCollector_.clearAll();
|
||||
}
|
||||
exportSigSetMap_.reset();
|
||||
acceptedExportSigSetHash_.reset();
|
||||
consensusExportTxns_.clear();
|
||||
exportSigGateStarted_ = false;
|
||||
exportSigGateStart_ = {};
|
||||
|
||||
@@ -70,6 +70,14 @@ private:
|
||||
std::shared_ptr<SHAMap> commitSetMap_;
|
||||
std::shared_ptr<SHAMap> entropySetMap_;
|
||||
std::shared_ptr<SHAMap> exportSigSetMap_;
|
||||
// Candidate entropy maps can be fetched/merged before they are safe to
|
||||
// inject. This hash is set only by the entropy sidecar gate after the
|
||||
// alignment/observation checks pass.
|
||||
std::optional<uint256> acceptedEntropySetHash_;
|
||||
// Export signature maps are also built from local collector state before
|
||||
// accept. Closed-ledger export apply may only consume the map root the
|
||||
// sidecar gate accepted for this round.
|
||||
std::optional<uint256> acceptedExportSigSetHash_;
|
||||
std::optional<LedgerIndex> rngRoundSeq_;
|
||||
// Consensus parent ledger hash, pinned at round start. Input to the
|
||||
// Tier 1 consensus_fallback entropy digest.
|
||||
@@ -215,6 +223,12 @@ public:
|
||||
bool
|
||||
hasAnyReveals() const;
|
||||
|
||||
void
|
||||
acceptEntropySet(uint256 const& hash);
|
||||
|
||||
void
|
||||
clearAcceptedEntropySet();
|
||||
|
||||
/// Result of the shared deterministic entropy selector: the digest to
|
||||
/// inject plus its tier/count labels. Both injection paths derive these
|
||||
/// identically from the AGREED entropySetMap_ so they cannot drift.
|
||||
@@ -226,13 +240,13 @@ public:
|
||||
};
|
||||
|
||||
/// Deterministically choose the entropy to inject for this round from the
|
||||
/// AGREED entropySetMap_ (never local pendingReveals_), labelled by agreed
|
||||
/// participant count: validator_quorum (>= 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.
|
||||
/// baseTxSetHash is the BASE (pre-injection) tx set hash used for the
|
||||
/// fallback digest.
|
||||
/// entropy sidecar accepted by the tick gate (never local pendingReveals_),
|
||||
/// labelled by agreed participant count: validator_quorum (>=
|
||||
/// 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. baseTxSetHash is the BASE (pre-injection) tx
|
||||
/// set hash used for the fallback digest.
|
||||
EntropySelection
|
||||
selectEntropy(uint256 const& baseTxSetHash, LedgerIndex seq) const;
|
||||
|
||||
@@ -269,6 +283,12 @@ public:
|
||||
bool
|
||||
exportSigConvergenceFailed() const;
|
||||
|
||||
void
|
||||
acceptExportSigSet(uint256 const& hash);
|
||||
|
||||
void
|
||||
clearAcceptedExportSigSet();
|
||||
|
||||
std::optional<ExportSignatureSnapshot>
|
||||
agreedExportSignatures(
|
||||
STTx const& exportTx,
|
||||
|
||||
@@ -165,6 +165,11 @@ ledger forever.
|
||||
Final entropy is computed from the agreed entropy sidecar SHAMap, not from a
|
||||
node's opportunistic local `pendingReveals_` map. This prevents different
|
||||
local reveal subsets at timeout boundaries from producing different entropy.
|
||||
The accepted-hash latch is the ledger-material boundary: a node that misses the
|
||||
bounded observation window falls back instead of injecting from a local candidate
|
||||
map. That does not make accept-vs-timeout decisions global; it makes any
|
||||
non-fallback injection depend only on the sidecar root that this node accepted,
|
||||
with ordinary validation quorum resolving boundary timing.
|
||||
|
||||
## Entropy Alignment Rules
|
||||
|
||||
@@ -337,6 +342,10 @@ export round to retry or expire. Full observation remains useful diagnostics; it
|
||||
is not an Export success precondition. If no export signature hash reaches quorum
|
||||
alignment by the bounded deadline, do not choose the largest non-quorum set; the
|
||||
export retries or expires according to normal transaction rules.
|
||||
Closed-ledger apply consumes only the accepted `exportSigSetHash` root. A node
|
||||
that times out before accepting a root retries; a node that proceeds assembles
|
||||
from the accepted sidecar map, never from its live collector. This avoids
|
||||
successful-but-different export blobs while preserving the bounded wait model.
|
||||
|
||||
Closed-ledger apply must not promote unverified proposal-carried signatures into
|
||||
current-round quorum material. It may verify and retain them for a future retry,
|
||||
|
||||
@@ -999,6 +999,10 @@ RCLConsensus::phase() const
|
||||
bool
|
||||
RCLConsensus::extensionsBusy() const
|
||||
{
|
||||
// ConsensusExtensions state is mutated by timer, peer-proposal and
|
||||
// sidecar-acquisition paths under this mutex. Busy polling observes the
|
||||
// same state, so it must share the same synchronization boundary.
|
||||
std::lock_guard _{mutex_};
|
||||
return consensus_->extensionsBusy();
|
||||
}
|
||||
|
||||
|
||||
@@ -182,8 +182,13 @@ Export::doApply()
|
||||
<< " txHash=" << txId << " ledgerSeq=" << currentSeq
|
||||
<< " unlSize=" << unlSize << " threshold=" << threshold;
|
||||
}
|
||||
else if (!consensusExtensions.exportSigConvergenceFailed())
|
||||
else
|
||||
{
|
||||
// The tick gate decides when a round may stop waiting.
|
||||
// Closed-ledger apply must still be a pure function of the agreed
|
||||
// export sidecar and the parent-ledger validator view; local
|
||||
// timeout flags must not veto a sidecar that already carries
|
||||
// deterministic quorum.
|
||||
collectedSigs = consensusExtensions.agreedExportSignatures(
|
||||
ctx_.tx, txId, *validatorView, threshold);
|
||||
}
|
||||
|
||||
@@ -34,6 +34,13 @@ buildSigners(SignatureSnapshot const& signatures)
|
||||
return a.getAccountID(sfAccount) < b.getAccountID(sfAccount);
|
||||
});
|
||||
|
||||
// XRPL validates the Signers array size before checking signer weights.
|
||||
// Export quorum is decided earlier from the agreed sidecar snapshot; this
|
||||
// cap only materializes a target-chain-valid canonical prefix.
|
||||
auto const maxSigners = STTx::maxMultiSigners();
|
||||
if (signers.size() > maxSigners)
|
||||
signers.erase(signers.begin() + maxSigners, signers.end());
|
||||
|
||||
return signers;
|
||||
}
|
||||
|
||||
|
||||
@@ -817,6 +817,7 @@ extensionsTick(Ext& ext, Ctx const& ctx)
|
||||
};
|
||||
auto clearEntropyHash = [&] {
|
||||
auto failedPos = ctx.getPosition();
|
||||
ext.clearAcceptedEntropySet();
|
||||
if (!failedPos.entropySetHash)
|
||||
return;
|
||||
failedPos.entropySetHash.reset();
|
||||
@@ -973,6 +974,9 @@ extensionsTick(Ext& ext, Ctx const& ctx)
|
||||
<< " txConverged=" << entropyState.txConverged
|
||||
<< " conflict="
|
||||
<< (entropyState.conflict ? "yes" : "no");
|
||||
|
||||
if (auto const accepted = ctx.getPosition().entropySetHash)
|
||||
ext.acceptEntropySet(*accepted);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1046,6 +1050,7 @@ extensionsTick(Ext& ext, Ctx const& ctx)
|
||||
}
|
||||
|
||||
ext.setExportSigConvergenceFailed();
|
||||
ext.clearAcceptedExportSigSet();
|
||||
JLOG(ext.j_.warn())
|
||||
<< "Export: advertised exportSigSet fetch timeout"
|
||||
<< " buildSeq=" << ctx.buildSeq
|
||||
@@ -1077,6 +1082,7 @@ extensionsTick(Ext& ext, Ctx const& ctx)
|
||||
}
|
||||
|
||||
ext.setExportSigConvergenceFailed();
|
||||
ext.clearAcceptedExportSigSet();
|
||||
JLOG(ext.j_.warn())
|
||||
<< "Export: exportSigSet advertisement timeout"
|
||||
<< " buildSeq=" << ctx.buildSeq
|
||||
@@ -1169,6 +1175,7 @@ extensionsTick(Ext& ext, Ctx const& ctx)
|
||||
return detail::exportSigSetQuorumAligned(
|
||||
exportState.alignedParticipants(), exportQuorum);
|
||||
};
|
||||
bool acceptedExportSigHash = false;
|
||||
//@@start export-sigset-alignment-check
|
||||
if (exportState.conflict && !quorumAligned())
|
||||
{
|
||||
@@ -1213,6 +1220,7 @@ extensionsTick(Ext& ext, Ctx const& ctx)
|
||||
<< " quorum=" << exportQuorum
|
||||
<< " peersSeen=" << exportState.peersSeen
|
||||
<< " txConverged=" << exportState.txConverged;
|
||||
acceptedExportSigHash = true;
|
||||
}
|
||||
else if (quorumAligned() && !exportState.fullObservation())
|
||||
{
|
||||
@@ -1226,6 +1234,7 @@ extensionsTick(Ext& ext, Ctx const& ctx)
|
||||
<< " quorum=" << exportQuorum
|
||||
<< " peersSeen=" << exportState.peersSeen
|
||||
<< " txConverged=" << exportState.txConverged;
|
||||
acceptedExportSigHash = true;
|
||||
}
|
||||
//@@end export-no-veto-quorum-branches
|
||||
else if (exportState.conflict || !quorumAligned())
|
||||
@@ -1252,6 +1261,7 @@ extensionsTick(Ext& ext, Ctx const& ctx)
|
||||
}
|
||||
|
||||
ext.setExportSigConvergenceFailed();
|
||||
ext.clearAcceptedExportSigSet();
|
||||
JLOG(ext.j_.warn())
|
||||
<< "Export: exportSigSet quorum alignment timeout"
|
||||
<< " buildSeq=" << buildSeqExport
|
||||
@@ -1265,6 +1275,21 @@ extensionsTick(Ext& ext, Ctx const& ctx)
|
||||
<< " elapsedMs=" << toMs(elapsed)
|
||||
<< " deadlineMs=" << toMs(deadline);
|
||||
}
|
||||
else
|
||||
{
|
||||
acceptedExportSigHash = true;
|
||||
}
|
||||
|
||||
if (acceptedExportSigHash)
|
||||
{
|
||||
// Apply must consume exactly the sidecar root that passed
|
||||
// the export gate. Local collector state may continue to
|
||||
// grow after this point, but it is not part of the agreed
|
||||
// closed-ledger export material.
|
||||
if (auto const accepted =
|
||||
ctx.getPosition().exportSigSetHash)
|
||||
ext.acceptExportSigSet(*accepted);
|
||||
}
|
||||
}
|
||||
//@@end export-sigset-conflict-wait
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user