diff --git a/src/test/consensus/ConsensusExtensions_test.cpp b/src/test/consensus/ConsensusExtensions_test.cpp index 057d3d359..bab68b00a 100644 --- a/src/test/consensus/ConsensusExtensions_test.cpp +++ b/src/test/consensus/ConsensusExtensions_test.cpp @@ -2648,9 +2648,9 @@ class ConsensusExtensions_test : public beast::unit_test::suite } void - testExportSigGateRequiresFullObservation() + testExportSigGateAllowsQuorumDespiteMissingObservation() { - testcase("Export sig gate requires full sidecar observation"); + testcase("Export sig gate allows quorum despite missing sidecar observation"); FakeExtensions ext; ExportTickHarness harness; @@ -2666,17 +2666,11 @@ class ConsensusExtensions_test : public beast::unit_test::suite BEAST_EXPECT(harness.position.exportSigSetHash == localHash); BEAST_EXPECT(ext.exportSigGateStarted_); - // Local quorum alignment is not enough if a tx-converged peer has - // not advertised any exportSigSetHash yet. + // A quorum-aligned signed exportSigSetHash is enough even if a + // tx-converged minority peer has not advertised any exportSigSetHash. result = harness.tick(ext, std::chrono::milliseconds{100}); - BEAST_EXPECT(!result.readyForAccept); - BEAST_EXPECT(!ext.exportSigConvergenceFailed_); - - result = harness.tick( - ext, - harness.parms.rngREVEAL_TIMEOUT * 2 + std::chrono::milliseconds{1}); BEAST_EXPECT(result.readyForAccept); - BEAST_EXPECT(ext.exportSigConvergenceFailed_); + BEAST_EXPECT(!ext.exportSigConvergenceFailed_); } void @@ -3120,7 +3114,7 @@ public: testRngEntropyConflictIgnoredWithQuorumAlignment(); testRngExplicitFinalProposalPublishesSyntheticTxSet(); testExportSigGateAllowsAlignedQuorumDespiteMinorityConflict(); - testExportSigGateRequiresFullObservation(); + testExportSigGateAllowsQuorumDespiteMissingObservation(); testExportSigGateFetchesAdvertisedPeerSets(); testExportSigGateObservingModeDoesNotPropose(); testExportSigGateRefreshesHashBeforeWaiting(); diff --git a/src/xrpld/app/consensus/ConsensusExtensionsDesign.md b/src/xrpld/app/consensus/ConsensusExtensionsDesign.md index 613ec468d..0ab4134c7 100644 --- a/src/xrpld/app/consensus/ConsensusExtensionsDesign.md +++ b/src/xrpld/app/consensus/ConsensusExtensionsDesign.md @@ -313,18 +313,15 @@ closing a minority ledger while sidecar convergence is already reachable. If no advertised sidecar appears by the deadline, the gate stops waiting and the export retries or expires through normal transaction rules. -Export success requires quorum alignment on `exportSigSetHash` AND full -observation of the tx-converged active set — not merely a local collector quorum. -Quorum alignment is necessary but not sufficient: even when a quorum of -tx-converged participants advertises the same export signature sidecar hash, that -hash is only treated as aligned (and below-quorum conflicts only ignored, and the -round only allowed to succeed) once the node has also observed an `exportSigSetHash` -from every tx-converged active validator (`peersSeen == txConverged`), because -export success changes ledger effects. While quorum is aligned but some -tx-converged peer's hash is unobserved, the node keeps waiting within the bounded -window. If quorum-aligned full observation is not reached by the bounded deadline, -do not choose the largest non-quorum set; the export retries or expires according -to normal transaction rules. +Export success requires quorum alignment on `exportSigSetHash`, not merely a +local collector quorum. Since `featureExport` enables signed extended proposal +fields, a quorum-aligned `exportSigSetHash` is enough to proceed even if a +tx-converged minority peer has not advertised an export sidecar hash. Do not let +one active validator with a missing sidecar force an otherwise quorum-aligned +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 must not promote unverified proposal-carried signatures into current-round quorum material. It may verify and retain them for a future retry, diff --git a/src/xrpld/consensus/ConsensusExtensionsTick.h b/src/xrpld/consensus/ConsensusExtensionsTick.h index fc04b2312..b479ee44c 100644 --- a/src/xrpld/consensus/ConsensusExtensionsTick.h +++ b/src/xrpld/consensus/ConsensusExtensionsTick.h @@ -1269,13 +1269,6 @@ extensionsTick(Ext& ext, Ctx const& ctx) auto quorumAligned = [&] { return exportState.quorumAligned(exportQuorum); }; - auto fullObservation = [&] { - // Export success changes ledger effects too. Require a - // full view of tx-converged peers before treating a local - // quorum as safe enough to succeed in this ledger. - return exportState.fullObservation(); - }; - if (exportState.conflict && !quorumAligned()) { auto const refreshedHash = @@ -1301,9 +1294,13 @@ extensionsTick(Ext& ext, Ctx const& ctx) exportState = inspectExportPeers(ctx.getPosition(), true); } - if (exportState.conflict && quorumAligned() && - fullObservation()) + if (exportState.conflict && quorumAligned()) { + // Export sidecar roots are signed through ExtendedPosition + // whenever featureExport is active. A quorum-aligned hash is + // therefore enough to proceed; requiring every tx-converged + // active peer to publish an exportSigSetHash would let a + // missing minority sidecar force retry/expiry. JLOG(ext.j_.info()) << "Export: exportSigSetHash conflict ignored" << " reason=quorum-aligned" @@ -1314,9 +1311,7 @@ extensionsTick(Ext& ext, Ctx const& ctx) << " peersSeen=" << exportState.peersSeen << " txConverged=" << exportState.txConverged; } - else if ( - exportState.conflict || !quorumAligned() || - !fullObservation()) + else if (exportState.conflict || !quorumAligned()) { auto const elapsed = ctx.nowSteady - ext.exportSigGateStart_;