From e6237b34ab15acdceebbb01777a29c9efdc66410 Mon Sep 17 00:00:00 2001 From: Nicholas Dudfield Date: Mon, 13 Jul 2026 14:48:59 +0700 Subject: [PATCH] Remove Export sidecar consensus authority --- .testnet/scenarios/export-suite.yml | 7 - .../export_no_veto_missing_observation.py | 88 --- .testnet/scenarios/latency-suite.yml | 8 - .../scenarios/perf/ce_export_latency_probe.py | 4 - src/test/app/Export_test.cpp | 86 +- .../consensus/ConsensusExtensions_test.cpp | 734 +----------------- src/test/consensus/ConsensusRng_test.cpp | 295 ------- src/test/consensus/ExtendedPosition_test.cpp | 13 +- src/test/consensus/ProposalPrecheck_test.cpp | 4 +- src/test/csf/Peer.h | 169 +--- src/test/csf/Proposal.h | 4 - src/test/rpc/RuntimeConfig_test.cpp | 16 +- .../app/consensus/ConsensusExtensions.cpp | 452 +---------- src/xrpld/app/consensus/ConsensusExtensions.h | 53 +- .../consensus/ConsensusExtensionsDesign.md | 289 ++----- src/xrpld/app/consensus/ExportIntent.md | 213 +---- src/xrpld/app/consensus/ProposalPrecheck.h | 2 +- src/xrpld/app/consensus/RCLCxPeerPos.h | 19 +- src/xrpld/app/misc/RuntimeConfig.h | 6 +- src/xrpld/app/misc/detail/RuntimeConfig.cpp | 11 +- src/xrpld/app/tx/detail/Export.cpp | 4 - src/xrpld/consensus/ConsensusExtensionsTick.h | 309 +------- 22 files changed, 144 insertions(+), 2642 deletions(-) delete mode 100644 .testnet/scenarios/export/export_no_veto_missing_observation.py diff --git a/.testnet/scenarios/export-suite.yml b/.testnet/scenarios/export-suite.yml index b6833b0b2..f3c2e47b4 100644 --- a/.testnet/scenarios/export-suite.yml +++ b/.testnet/scenarios/export-suite.yml @@ -46,13 +46,6 @@ tests: - Export unl_report: false - - name: export_no_veto_missing_observation - script: .testnet/scenarios/export/export_no_veto_missing_observation.py - network: - rc: - - rng_poll_ms=333 - - n4:no_export_sig_hash=true - # CE + Export: 1 node suppressed, 4/5 = 80% quorum, should succeed - name: export_ce_one_node_down script: .testnet/scenarios/export/export_quorum.py diff --git a/.testnet/scenarios/export/export_no_veto_missing_observation.py b/.testnet/scenarios/export/export_no_veto_missing_observation.py deleted file mode 100644 index 501fe86be..000000000 --- a/.testnet/scenarios/export/export_no_veto_missing_observation.py +++ /dev/null @@ -1,88 +0,0 @@ -""":descr: Export succeeds when quorum sidecar material exists but one active -validator withholds exportSigSetHash observation. - -Node 4 has runtime_config no_export_sig_hash=true. It still attaches export -signatures, but it does not publish its exportSigSetHash in proposals. The -remaining 4/5 active validators can still align on the same export sidecar -hash, so the round must not retry/expire just because fullObservation is false. -""" - -from __future__ import annotations - -from export_helpers import ( - EXPORT_RETRY_LEDGER_WINDOW, - require_export, - assert_export_result, - assert_shadow_ticket, -) - - -async def scenario(ctx, log): - await require_export(ctx, log) - - await ctx.fund_accounts({"alice": 10000, "bob": 1000}) - log("Accounts funded") - - alice = ctx.account("alice") - bob = ctx.account("bob") - current_seq = ctx.validated_ledger_index(0) - - log(f"Current ledger: {current_seq}") - log("Node 4 withholds exportSigSetHash but still attaches export signatures") - - export_start = ctx.mark("export-no-veto-submit-start") - result = await ctx.submit_and_wait( - { - "TransactionType": "Export", - "LastLedgerSequence": current_seq + EXPORT_RETRY_LEDGER_WINDOW, - "Fee": "1000000", - "ExportedTxn": { - "TransactionType": "Payment", - "Account": alice.address, - "Destination": bob.address, - "Amount": "1000000", - "Fee": "10", - "Sequence": 0, - "TicketSequence": 1, - "FirstLedgerSequence": current_seq + 1, - "LastLedgerSequence": current_seq + EXPORT_RETRY_LEDGER_WINDOW, - "Flags": 2147483648, - "SigningPubKey": "", - }, - }, - alice.wallet, - timeout=60, - ) - export_end = ctx.mark("export-no-veto-submit-end") - - final_seq = ctx.validated_ledger_index(0) - engine_result = result.get("engine_result", "") - meta = result.get("meta", {}) - - log(f"Export completed at ledger {final_seq}, result: {engine_result}") - if engine_result != "tesSUCCESS": - raise AssertionError(f"Expected tesSUCCESS, got {engine_result}") - - export_result = assert_export_result(meta, log, ctx=ctx, require_signers=True) - signers = export_result.get("_WitnessSigners", []) - if len(signers) < 4: - raise AssertionError(f"Expected at least 4 signers, got {len(signers)}") - log(f"Export signer count: {len(signers)}") - - no_veto_logs = ctx.assert_log( - r"Export: missing exportSigSetHash observation ignored", - since=export_start, - until=export_end, - ) - log(f"Export no-veto missing-observation logs: {no_veto_logs.count}") - - withhold_logs = ctx.assert_log( - r"Export: withholding exportSigSetHash", - since=export_start, - until=export_end, - ) - log(f"Export sidecar hash withholding logs: {withhold_logs.count}") - - assert_shadow_ticket(ctx, alice.address, log, expect_exists=True) - - log("PASS") diff --git a/.testnet/scenarios/latency-suite.yml b/.testnet/scenarios/latency-suite.yml index fa7fa3ab0..5d20d72d1 100644 --- a/.testnet/scenarios/latency-suite.yml +++ b/.testnet/scenarios/latency-suite.yml @@ -80,11 +80,3 @@ tests: - n0->n4:delay=500,jitter=100,msg=proposal - n1->n4:delay=500,jitter=100,msg=proposal - n2->n4:delay=500,jitter=100,msg=proposal - - - name: latency_export_no_veto_with_delay - script: .testnet/scenarios/export/export_no_veto_missing_observation.py - network: - rc: - - rng_poll_ms=250 - - delay=300,jitter=100,msg=proposal - - n4:no_export_sig_hash=true diff --git a/.testnet/scenarios/perf/ce_export_latency_probe.py b/.testnet/scenarios/perf/ce_export_latency_probe.py index 581b518eb..651fd19b0 100644 --- a/.testnet/scenarios/perf/ce_export_latency_probe.py +++ b/.testnet/scenarios/perf/ce_export_latency_probe.py @@ -98,10 +98,6 @@ def _summarize_logs(ctx, log, *, label, started, ended): "rng_participant_aligned": r"tier=2", "rng_validator_quorum": r"tier=3", "export_retry": r"terRETRY_EXPORT", - "export_quorum_timeout": r"Export: exportSigSet quorum alignment timeout", - "export_missing_observation_ignored": ( - r"Export: missing exportSigSetHash observation ignored" - ), } for name, pattern in patterns.items(): result = ctx.search_logs(pattern, since=started, until=ended, limit=500) diff --git a/src/test/app/Export_test.cpp b/src/test/app/Export_test.cpp index 228c8bd41..f64c036e7 100644 --- a/src/test/app/Export_test.cpp +++ b/src/test/app/Export_test.cpp @@ -808,38 +808,26 @@ struct Export_test : public beast::unit_test::suite // applies the emitted ttEXPORT through the transactor. env.close(); - // The emitted ttEXPORT and its per-export signature witness should now - // appear in the closed ledger. The witness is transaction-stream input, - // not metadata decoration, so replay has the same signatures apply saw. + // The emitted ttEXPORT retries and is not included. The retired + // same-ledger sidecar path no longer synthesizes a signature witness. { auto const ledger = env.closed(); int exportCount = 0; int witnessCount = 0; - std::optional exportHash; - std::optional witnessExportHash; for (auto const& [stx, meta] : ledger->txs) { if (stx->getTxnType() == ttEXPORT) { BEAST_EXPECT(stx->isFieldPresent(sfEmitDetails)); - exportHash = stx->getTransactionID(); ++exportCount; continue; } - BEAST_EXPECT(stx->getTxnType() == ttEXPORT_SIGNATURES); - BEAST_EXPECT(stx->isFieldPresent(sfTransactionHash)); - witnessExportHash = stx->getFieldH256(sfTransactionHash); - auto signatures = - ExportResultBuilder::signaturesFromWitness(*stx); - BEAST_EXPECT(signatures); - ++witnessCount; + if (stx->getTxnType() == ttEXPORT_SIGNATURES) + ++witnessCount; } - BEAST_EXPECT(exportCount == 1); - BEAST_EXPECT(witnessCount == 1); - BEAST_EXPECT(exportHash && witnessExportHash); - if (exportHash && witnessExportHash) - BEAST_EXPECT(*witnessExportHash == *exportHash); + BEAST_EXPECT(exportCount == 0); + BEAST_EXPECT(witnessCount == 0); } } @@ -1173,9 +1161,9 @@ struct Export_test : public beast::unit_test::suite } void - testExportNetworkApplyUsesAgreedSidecar(FeatureBitset features) + testExportNetworkApplyUsesProvidedWitness(FeatureBitset features) { - testcase("ttEXPORT network apply uses agreed export sidecar"); + testcase("ttEXPORT network apply uses provided witness"); using namespace jtx; @@ -1216,26 +1204,9 @@ struct Export_test : public beast::unit_test::suite ce.cacheUNLReport(env.app().getLedgerMaster().getClosedLedger()); auto const view = ce.activeValidatorView(); BEAST_EXPECT(view->fromUNLReport); - ce.cacheConsensusTxSet(makeRCLTxSet(env.app(), {exportTx})); - auto const originalSig = ExportResultBuilder::signExportedTxn(innerTx, valPK, valSK); auto const applySeq = env.closed()->seq() + 1; - ce.exportSigCollector().addVerifiedSignature( - txHash, valPK, originalSig, applySeq); - auto const agreedHash = ce.buildExportSigSet(applySeq); - BEAST_EXPECT( - env.app().getInboundTransactions().getSet(agreedHash, false)); - ce.acceptExportSigSet(agreedHash); - ce.setExportSigConvergenceFailed(); - - // Simulate an asynchronous collector mutation after sidecar agreement. - // 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( - txHash, valPK, lateSig, applySeq); ExportResultBuilder::SignatureSnapshot expectedSigs; expectedSigs.emplace(valPK, originalSig); @@ -1648,43 +1619,21 @@ struct Export_test : public beast::unit_test::suite forceNonStandalone(env.app()); BEAST_EXPECT(!env.app().config().standalone()); - auto const& valKeys = env.app().getValidatorKeys(); - BEAST_EXPECT(valKeys.keys); - if (!valKeys.keys) - return; - - auto const& valPK = valKeys.keys->publicKey; - auto const& valSK = valKeys.keys->secretKey; auto const seq = env.current()->seq(); auto const ticketSeq = std::uint32_t{1}; auto const lls = seq + ExportLimits::maxRetryLedgers; auto innerObj = buildExportedPayment( alice.id(), carol.id(), seq + 1, lls, ticketSeq); - auto const innerTx = makeSTTx(innerObj); auto jt = makeExportJTx(env, alice, innerObj, lls); auto const exportTx = jt.stx; BEAST_EXPECT(exportTx); if (!exportTx) return; - auto const txHash = exportTx->getTransactionID(); - auto& ce = env.app().getConsensusExtensions(); ce.setExportEnabledThisRound(true); ce.cacheUNLReport(env.app().getLedgerMaster().getClosedLedger()); auto const view = ce.activeValidatorView(); BEAST_EXPECT(!view->fromUNLReport); - ce.cacheConsensusTxSet(makeRCLTxSet(env.app(), {exportTx})); - - auto const sig = - ExportResultBuilder::signExportedTxn(innerTx, valPK, valSK); - auto const applySeq = env.closed()->seq() + 1; - ce.exportSigCollector().addVerifiedSignature( - txHash, valPK, sig, applySeq); - auto const agreedHash = ce.buildExportSigSet(applySeq); - BEAST_EXPECT( - env.app().getInboundTransactions().getSet(agreedHash, false)); - ce.acceptExportSigSet(agreedHash); - auto const parent = env.app().getLedgerMaster().getClosedLedger(); auto next = std::make_shared( *parent, env.app().timeKeeper().closeTime()); @@ -1743,21 +1692,12 @@ struct Export_test : public beast::unit_test::suite ce.cacheUNLReport(parent); auto const view = ce.activeValidatorView(); BEAST_EXPECT(view->fromUNLReport); - ce.cacheConsensusTxSet(makeRCLTxSet(env.app(), {exportTx})); - ExportResultBuilder::SignatureWitnesses exportSignatureWitnesses; if (withQuorum) { auto const sig = ExportResultBuilder::signExportedTxn(innerTx, valPK, valSK); - ce.exportSigCollector().addVerifiedSignature( - txHash, valPK, sig, applySeq); - auto const agreedHash = ce.buildExportSigSet(applySeq); - BEAST_EXPECT(env.app().getInboundTransactions().getSet( - agreedHash, false)); - ce.acceptExportSigSet(agreedHash); - ExportResultBuilder::SignatureSnapshot signatures; signatures.emplace(valPK, sig); exportSignatureWitnesses = @@ -2364,8 +2304,7 @@ struct Export_test : public beast::unit_test::suite // ttEXPORT transactor tests testExportTxnOpenLedger(allWithExport); testExportNetworkRetryWithoutQuorum(allWithExport); - testExportProposalSigningRequiresBoundedUNLReport(allWithExport); - testExportNetworkApplyUsesAgreedSidecar(allWithExport); + testExportNetworkApplyUsesProvidedWitness(allWithExport); testExportNetworkRetriesOversizedActiveView(allWithExport); testExportShadowTicketInsufficientReserve(allWithExport); testExportHistoricalReplayIgnoresCurrentManifestMap(allWithExport); @@ -2373,19 +2312,12 @@ struct Export_test : public beast::unit_test::suite testExportNetworkRetryWithoutUNLReport(allWithExport); testExportNetworkLastLedgerSequenceBoundary(allWithExport); testOpenLedgerExportLimit(allWithExport); - testShadowTicketLimit(allWithExport); - testShadowTicketLifecycle(allWithExport); - testCancelShadowTicketViaTxn(allWithExport); testExportRejectsNoTicketSequence(allWithExport); testExportRejectsMissingLastLedgerSequence(allWithExport); testExportRejectsSignedInnerTransaction(allWithExport); testExportRejectsLongRetryWindow(allWithExport); testExportRejectsMalformed(allWithExport); - // Round-trip test - testExportImportRoundTrip(allWithExport); - testExportImportWaitsForShadowTicket(allWithExport); - testExportImportRejectsStaleImportVL(allWithExport); } }; diff --git a/src/test/consensus/ConsensusExtensions_test.cpp b/src/test/consensus/ConsensusExtensions_test.cpp index d762e28f0..ab5b85ae1 100644 --- a/src/test/consensus/ConsensusExtensions_test.cpp +++ b/src/test/consensus/ConsensusExtensions_test.cpp @@ -190,36 +190,6 @@ makeExportedPayment( return obj; } -std::shared_ptr -makeExportTx(STObject const& inner, AccountID const& account) -{ - STObject exportObj(sfGeneric); - exportObj.setFieldU16(sfTransactionType, ttEXPORT); - exportObj.setAccountID(sfAccount, account); - exportObj.setFieldU32(sfSequence, 0); - exportObj.setFieldVL(sfSigningPubKey, Blob{}); - exportObj.setFieldU32(sfFirstLedgerSequence, 2); - exportObj.setFieldU32(sfLastLedgerSequence, 6); - exportObj.setFieldAmount(sfFee, XRPAmount{0}); - exportObj.set(std::make_unique(inner)); - - return std::make_shared(makeSTTx(exportObj)); -} - -std::shared_ptr -makeCancelExportTx(AccountID const& account, std::uint32_t sequence) -{ - STObject exportObj(sfGeneric); - exportObj.setFieldU16(sfTransactionType, ttEXPORT); - exportObj.setAccountID(sfAccount, account); - exportObj.setFieldU32(sfSequence, sequence); - exportObj.setFieldU32(sfCancelTicketSequence, 1000 + sequence); - exportObj.setFieldVL(sfSigningPubKey, Blob{}); - exportObj.setFieldAmount(sfFee, XRPAmount{0}); - - return std::make_shared(makeSTTx(exportObj)); -} - std::shared_ptr makeConsensusEntropyTx( std::uint32_t ledgerSeq, @@ -279,15 +249,6 @@ makeRCLTxSet(Application& app, std::vector> txns) return RCLTxSet{map->snapShot(false)}; } -std::size_t -sidecarLeafCount(SHAMap const& map) -{ - std::size_t count = 0; - map.visitLeaves( - [&](boost::intrusive_ptr const&) { ++count; }); - return count; -} - void forceNonStandalone(Application& app) { @@ -380,13 +341,8 @@ struct FakeExtensions std::chrono::steady_clock::time_point commitHashConflictStart_{}; bool entropySetPublished_{false}; std::chrono::steady_clock::time_point entropyPublishStart_{}; - bool exportSigGateStarted_{false}; - std::chrono::steady_clock::time_point exportSigGateStart_{}; - bool exportSigConvergenceFailed_{false}; bool rngOn{false}; - bool localExportSigs{true}; - bool consensusExportTxns{false}; - bool exportOn{true}; + bool exportOn{false}; bool entropyFailed{false}; bool commitFrozen{false}; std::size_t sidecarQuorum{4}; @@ -396,14 +352,11 @@ struct FakeExtensions bool commitQuorum{true}; bool minimumReveals{true}; bool anyReveals{true}; - uint256 exportHash{makeHash("local-export-sig-set")}; uint256 commitHash{makeHash("local-commit-set")}; uint256 entropyHash{makeHash("local-entropy-set")}; - std::deque exportHashSequence; std::deque commitHashSequence; std::deque entropyHashSequence; int commitBuilds = 0; - int exportBuilds = 0; int entropyBuilds = 0; int participantDiagnostics = 0; int selfSeeds = 0; @@ -420,12 +373,6 @@ struct FakeExtensions return exportOn; } - bool - testSuppressExportSigSetHash() const - { - return false; - } - std::size_t quorumThreshold() const { @@ -440,12 +387,6 @@ struct FakeExtensions return sidecarQuorum; } - std::size_t - exportRootAlignmentThreshold() const - { - return sidecarQuorum; - } - // Membership is a no-op in the FakeExtensions tick tests (every peer // counts, local counts) so the gate behavior is unchanged; F1 active-view // filtering is exercised directly against inspectTxConvergedSidecarPeers. @@ -564,47 +505,6 @@ struct FakeExtensions commitFrozen = true; } - bool - hasPendingExportSigs() const - { - return localExportSigs; - } - - bool - hasConsensusExportTxns() const - { - return consensusExportTxns; - } - - uint256 - buildExportSigSet(LedgerIndex) - { - ++exportBuilds; - if (!exportHashSequence.empty()) - { - auto ret = exportHashSequence.front(); - exportHashSequence.pop_front(); - return ret; - } - return exportHash; - } - - void - setExportSigConvergenceFailed() - { - exportSigConvergenceFailed_ = true; - } - - void - acceptExportSigSet(uint256 const&) - { - } - - void - clearAcceptedExportSigSet() - { - } - template void recordParticipantDiagnostics(ConsensusMode, PeerPositions const&) @@ -644,18 +544,6 @@ struct ExtensionTickHarness int updates = 0; int proposes = 0; - void - addPeer( - std::uint8_t id, - std::optional exportSigSetHash, - uint256 txSetHash = makeHash("tx-set")) - { - ExtendedPosition peerPosition{txSetHash}; - peerPosition.exportSigSetHash = exportSigSetHash; - peers.emplace( - makeNode(id), FakePeerPosition{makeNode(id), peerPosition}); - } - void addEntropyPeer( std::uint8_t id, @@ -854,14 +742,14 @@ class ConsensusExtensions_test : public beast::unit_test::suite ExtensionTickHarness harness; auto const localHash = makeHash("sidecar-local"); auto const conflictHash = makeHash("sidecar-conflict"); - harness.position.exportSigSetHash = localHash; - harness.addPeer(1, localHash); - harness.addPeer(2, conflictHash); - harness.addPeer(3, std::nullopt); - harness.addPeer(4, localHash, makeHash("other-tx-set")); + harness.position.entropySetHash = localHash; + harness.addEntropyPeer(1, localHash); + harness.addEntropyPeer(2, conflictHash); + harness.addEntropyPeer(3, std::nullopt); + harness.addEntropyPeer(4, localHash, makeHash("other-tx-set")); - auto const exportHashOf = [](auto const& position) { - return position.exportSigSetHash; + auto const sidecarHashOf = [](auto const& position) { + return position.entropySetHash; }; auto const allMembers = [](auto const&) { return true; }; @@ -870,7 +758,7 @@ class ConsensusExtensions_test : public beast::unit_test::suite harness.peers, harness.position, true, - exportHashOf, + sidecarHashOf, allMembers, [&](auto const& hash) { if (hash) @@ -890,12 +778,12 @@ class ConsensusExtensions_test : public beast::unit_test::suite if (!mismatches.empty()) BEAST_EXPECT(mismatches.front() == conflictHash); - harness.position.exportSigSetHash.reset(); + harness.position.entropySetHash.reset(); auto const unpublishedState = detail::inspectTxConvergedSidecarPeers( harness.peers, harness.position, true, - exportHashOf, + sidecarHashOf, allMembers, [](auto const&) {}); BEAST_EXPECT(!unpublishedState.localCounts); @@ -909,8 +797,9 @@ class ConsensusExtensions_test : public beast::unit_test::suite // proposer (node 5) that tx-converges and aligns on the SAME hash must // NOT inflate alignedParticipants() — otherwise two equivocation // cohorts padded by non-active peers could each clear the gate. - harness.position.exportSigSetHash = localHash; - harness.addPeer(5, localHash); // trusted, but outside the active view + harness.position.entropySetHash = localHash; + harness.addEntropyPeer( + 5, localHash); // trusted, but outside the active view auto const activeOnly = [](auto const& id) { return id != makeNode(5); // nodes 1..4 active; 5 is not }; @@ -919,7 +808,7 @@ class ConsensusExtensions_test : public beast::unit_test::suite harness.peers, harness.position, true, - exportHashOf, + sidecarHashOf, allMembers, [](auto const&) {}); BEAST_EXPECT(padded.aligned == 2); // node 1 + node 5, unfiltered @@ -928,7 +817,7 @@ class ConsensusExtensions_test : public beast::unit_test::suite harness.peers, harness.position, true, - exportHashOf, + sidecarHashOf, activeOnly, [](auto const&) {}); BEAST_EXPECT(filtered.aligned == 1); // node 5 excluded @@ -940,7 +829,7 @@ class ConsensusExtensions_test : public beast::unit_test::suite harness.peers, harness.position, false, - exportHashOf, + sidecarHashOf, activeOnly, [](auto const&) {}); BEAST_EXPECT(!nonActiveLocal.localCounts); @@ -955,8 +844,8 @@ class ConsensusExtensions_test : public beast::unit_test::suite auto const hashA = makeHash("split-brain-sidecar-a"); auto const hashB = makeHash("split-brain-sidecar-b"); - auto const exportHashOf = [](auto const& position) { - return position.exportSigSetHash; + auto const sidecarHashOf = [](auto const& position) { + return position.entropySetHash; }; auto const allMembers = [](auto const&) { return true; }; @@ -965,10 +854,10 @@ class ConsensusExtensions_test : public beast::unit_test::suite std::vector const& hashANodes, std::vector const& hashBNodes) { ExtensionTickHarness harness; - harness.position.exportSigSetHash = localHash; + harness.position.entropySetHash = localHash; auto addPeer = [&](std::uint8_t id, uint256 const& hash) { if (id != localId) - harness.addPeer(id, hash); + harness.addEntropyPeer(id, hash); }; for (auto id : hashANodes) addPeer(id, hashA); @@ -979,7 +868,7 @@ class ConsensusExtensions_test : public beast::unit_test::suite harness.peers, harness.position, true, - exportHashOf, + sidecarHashOf, allMembers, [](auto const&) {}); }; @@ -1195,8 +1084,6 @@ class ConsensusExtensions_test : public beast::unit_test::suite ConsensusExtensions::selectEntropyTierForView( true, 2, cappedView.size(), cappedView.originalViewSize) == entropyTierConsensusFallback); - BEAST_EXPECT( - ConsensusExtensions::exportRootAlignmentThreshold(cappedView) == 6); BEAST_EXPECT( ConsensusExtensions::exportWitnessThreshold(cappedView) == 6); } @@ -1379,20 +1266,15 @@ class ConsensusExtensions_test : public beast::unit_test::suite ConsensusExtensions ce{env.app(), activeNoopJournal()}; BEAST_EXPECT(!ce.testBootstrapFastStartEnabled()); - BEAST_EXPECT(!ce.testSuppressExportSigSetHash()); ConsensusTestConfig cfg; cfg.bootstrapFastStart = true; - cfg.noExportSigHash = true; env.app().getRuntimeConfig().setGlobalConfig(cfg); BEAST_EXPECT(ce.testBootstrapFastStartEnabled()); - BEAST_EXPECT(ce.testSuppressExportSigSetHash()); cfg.bootstrapFastStart = false; - cfg.noExportSigHash = false; env.app().getRuntimeConfig().setGlobalConfig(cfg); BEAST_EXPECT(!ce.testBootstrapFastStartEnabled()); - BEAST_EXPECT(!ce.testSuppressExportSigSetHash()); } void @@ -2252,57 +2134,6 @@ class ConsensusExtensions_test : public beast::unit_test::suite BEAST_EXPECT(ce.pendingRevealCount() == 1); } - void - testExportSidecarBuildsLocalSnapshot() - { - testcase("Export sidecar builds local snapshot"); - - using namespace jtx; - Env env{ - *this, envconfig(validator, ""), supported_amendments(), nullptr}; - auto const ledger = env.app().getLedgerMaster().getClosedLedger(); - auto const& valKeys = env.app().getValidatorKeys(); - BEAST_EXPECT(valKeys.keys); - if (!valKeys.keys) - return; - - auto const& valPK = valKeys.keys->publicKey; - auto const& valSK = valKeys.keys->secretKey; - auto const signerAccount = calcAccountID(valPK); - auto const dst = calcAccountID(randomKeyPair(KeyType::secp256k1).first); - auto const innerObj = makeExportedPayment(signerAccount, dst); - auto const innerTx = makeSTTx(innerObj); - auto const exportTx = makeExportTx(innerObj, signerAccount); - auto const txHash = exportTx->getTransactionID(); - auto const txSet = makeRCLTxSet(env.app(), {exportTx}); - auto const seq = ledger->seq() + 1; - - ConsensusExtensions source{env.app(), activeNoopJournal()}; - source.setExportEnabledThisRound(true); - source.cacheUNLReport(ledger); - source.cacheConsensusTxSet(txSet); - source.cacheConsensusTxSet(txSet); - BEAST_EXPECT(source.hasConsensusExportTxns()); - BEAST_EXPECT(!source.hasPendingExportSigs()); - - auto const sigData = buildMultiSigningData(innerTx, signerAccount); - auto const sig = sign(valPK, valSK, sigData.slice()); - Buffer sigBuf(sig.data(), sig.size()); - source.exportSigCollector().addUnverifiedSignature( - txHash, valPK, sigBuf, seq); - BEAST_EXPECT(source.verifyPendingExportSigs(txSet, seq) == 1); - BEAST_EXPECT( - source.exportSigCollector().hasVerifiedSignature(txHash, valPK)); - BEAST_EXPECT(source.hasPendingExportSigs()); - - auto const exportSigSetHash = source.buildExportSigSet(seq); - auto const exportedSet = - env.app().getInboundTransactions().getSet(exportSigSetHash, false); - BEAST_EXPECT(exportedSet); - if (exportedSet) - BEAST_EXPECT(sidecarLeafCount(*exportedSet) == 1); - } - void testTransactionAcquireRejectsSidecarWireNodes() { @@ -2390,293 +2221,6 @@ class ConsensusExtensions_test : public beast::unit_test::suite //@@end test-acquired-ce-pseudo-reject } - void - testExportSidecarIgnoresCancelOnlyExports() - { - testcase("Export sidecar ignores cancel-only exports"); - - using namespace jtx; - Env env{ - *this, envconfig(validator, ""), supported_amendments(), nullptr}; - auto const ledger = env.app().getLedgerMaster().getClosedLedger(); - auto const& valKeys = env.app().getValidatorKeys(); - BEAST_EXPECT(valKeys.keys); - if (!valKeys.keys) - return; - - auto const& valPK = valKeys.keys->publicKey; - auto const& valSK = valKeys.keys->secretKey; - auto const signerAccount = calcAccountID(valPK); - auto const seq = ledger->seq() + 1; - - auto const dst = calcAccountID(randomKeyPair(KeyType::secp256k1).first); - auto const innerObj = makeExportedPayment(signerAccount, dst); - auto const innerTx = makeSTTx(innerObj); - auto const exportTx = makeExportTx(innerObj, signerAccount); - auto const txHash = exportTx->getTransactionID(); - - std::vector> txs; - for (std::uint32_t sequence = 1; - txs.size() < ExportLimits::maxPendingExports && sequence < 1000; - ++sequence) - { - auto cancelTx = makeCancelExportTx(signerAccount, sequence); - if (cancelTx->getTransactionID() < txHash) - txs.push_back(std::move(cancelTx)); - } - BEAST_EXPECT(txs.size() == ExportLimits::maxPendingExports); - if (txs.size() != ExportLimits::maxPendingExports) - return; - txs.push_back(exportTx); - - auto const txSet = makeRCLTxSet(env.app(), txs); - - ConsensusExtensions source{env.app(), activeNoopJournal()}; - source.setExportEnabledThisRound(true); - source.cacheUNLReport(ledger); - source.cacheConsensusTxSet(txSet); - - auto const sigData = buildMultiSigningData(innerTx, signerAccount); - auto const sig = sign(valPK, valSK, sigData.slice()); - Buffer sigBuf(sig.data(), sig.size()); - source.exportSigCollector().addVerifiedSignature( - txHash, valPK, sigBuf, seq); - - auto const exportSigSetHash = source.buildExportSigSet(seq); - auto const exportedSet = - env.app().getInboundTransactions().getSet(exportSigSetHash, false); - BEAST_EXPECT(exportedSet); - if (exportedSet) - BEAST_EXPECT(sidecarLeafCount(*exportedSet) == 1); - - source.acceptExportSigSet(exportSigSetHash); - BEAST_EXPECT(source.agreedExportSignatures(*exportTx, txHash, 1)); - } - - void - testExportSidecarBuildCapsConsensusCandidates() - { - testcase("Export sidecar build caps consensus candidates"); - - using namespace jtx; - Env env{ - *this, envconfig(validator, ""), supported_amendments(), nullptr}; - auto const ledger = env.app().getLedgerMaster().getClosedLedger(); - auto const& valKeys = env.app().getValidatorKeys(); - BEAST_EXPECT(valKeys.keys); - if (!valKeys.keys) - return; - - auto const& valPK = valKeys.keys->publicKey; - auto const& valSK = valKeys.keys->secretKey; - auto const signerAccount = calcAccountID(valPK); - auto const seq = ledger->seq() + 1; - - std::vector> exportTxs; - std::vector> signatures; - for (std::size_t i = 0; i <= ExportLimits::maxPendingExports; ++i) - { - auto const dst = - calcAccountID(randomKeyPair(KeyType::secp256k1).first); - auto const innerObj = makeExportedPayment(signerAccount, dst); - auto const innerTx = makeSTTx(innerObj); - auto const exportTx = makeExportTx(innerObj, signerAccount); - auto const txHash = exportTx->getTransactionID(); - auto const sigData = buildMultiSigningData(innerTx, signerAccount); - auto const sig = sign(valPK, valSK, sigData.slice()); - - exportTxs.push_back(exportTx); - signatures.emplace_back(txHash, Buffer(sig.data(), sig.size())); - } - - auto const txSet = makeRCLTxSet(env.app(), exportTxs); - - ConsensusExtensions source{env.app(), activeNoopJournal()}; - source.setExportEnabledThisRound(true); - source.cacheUNLReport(ledger); - source.cacheConsensusTxSet(txSet); - for (auto const& [txHash, sig] : signatures) - source.exportSigCollector().addVerifiedSignature( - txHash, valPK, sig, seq); - - auto const exportSigSetHash = source.buildExportSigSet(seq); - auto const exportedSet = - env.app().getInboundTransactions().getSet(exportSigSetHash, false); - BEAST_EXPECT(exportedSet); - if (exportedSet) - BEAST_EXPECT( - sidecarLeafCount(*exportedSet) == - ExportLimits::maxPendingExports); - - source.acceptExportSigSet(exportSigSetHash); - BEAST_EXPECT(!source.agreedExportSignatures( - *exportTxs.back(), - exportTxs.back()->getTransactionID(), - ExportLimits::maxPendingExports + 1)); - } - - void - testExportAgreedSignaturesIgnoreLiveCollectorMutation() - { - testcase("Export apply uses agreed sidecar signatures"); - - using namespace jtx; - Env env{ - *this, envconfig(validator, ""), supported_amendments(), nullptr}; - auto const ledger = env.app().getLedgerMaster().getClosedLedger(); - auto const& valKeys = env.app().getValidatorKeys(); - BEAST_EXPECT(valKeys.keys); - if (!valKeys.keys) - return; - - auto const& valPK = valKeys.keys->publicKey; - auto const& valSK = valKeys.keys->secretKey; - auto const signerAccount = calcAccountID(valPK); - auto const dst = calcAccountID(randomKeyPair(KeyType::secp256k1).first); - auto const innerObj = makeExportedPayment(signerAccount, dst); - auto const innerTx = makeSTTx(innerObj); - auto const exportTx = makeExportTx(innerObj, signerAccount); - auto const txHash = exportTx->getTransactionID(); - auto const txSet = makeRCLTxSet(env.app(), {exportTx}); - auto const seq = ledger->seq() + 1; - - ConsensusExtensions ce{env.app(), activeNoopJournal()}; - ce.setExportEnabledThisRound(true); - ce.cacheUNLReport(ledger); - ce.cacheConsensusTxSet(txSet); - - auto const sigData = buildMultiSigningData(innerTx, signerAccount); - auto const sig = sign(valPK, valSK, sigData.slice()); - Buffer const originalSig(sig.data(), sig.size()); - ce.exportSigCollector().addVerifiedSignature( - txHash, valPK, originalSig, seq); - auto const exportSigSetHash = ce.buildExportSigSet(seq); - BEAST_EXPECT( - env.app().getInboundTransactions().getSet(exportSigSetHash, false)); - 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, 1)); - ce.acceptExportSigSet(makeHash("wrong-export-sigset-root")); - BEAST_EXPECT(!ce.agreedExportSignatures(*exportTx, txHash, 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 - // map. - std::uint8_t const lateBytes[] = {9, 8, 7}; - Buffer const lateSig{lateBytes, sizeof(lateBytes)}; - ce.exportSigCollector().addVerifiedSignature( - txHash, valPK, lateSig, seq); - - auto const live = ce.exportSigCollector().checkQuorumAndSnapshot( - txHash, 1, [&](PublicKey const& pk) { - return ce.isActiveValidator(pk, *view); - }); - BEAST_EXPECT(live); - if (live) - BEAST_EXPECT(live->at(valPK) == lateSig); - - auto const agreed = ce.agreedExportSignatures(*exportTx, txHash, 1); - BEAST_EXPECT(agreed); - if (agreed) - { - BEAST_EXPECT(agreed->size() == 1); - BEAST_EXPECT(agreed->at(valPK) == originalSig); - } - } - - void - testExportAgreedSignaturesTrustAcceptedRootMembership() - { - testcase("Export agreed signatures trust accepted root membership"); - - using namespace jtx; - Env env{ - *this, envconfig(validator, ""), supported_amendments(), nullptr}; - auto const ledger = env.app().getLedgerMaster().getClosedLedger(); - - ConsensusExtensions ce{env.app(), activeNoopJournal()}; - ce.setExportEnabledThisRound(true); - ce.cacheUNLReport(ledger); - auto const view = ce.activeValidatorView(); - - auto const inactive = randomKeyPair(KeyType::secp256k1); - auto const& valPK = inactive.first; - auto const& valSK = inactive.second; - BEAST_EXPECT(!ce.isActiveValidator(valPK, *view)); - - auto const signerAccount = calcAccountID(valPK); - auto const dst = calcAccountID(randomKeyPair(KeyType::secp256k1).first); - auto const innerObj = makeExportedPayment(signerAccount, dst); - auto const innerTx = makeSTTx(innerObj); - auto const exportTx = makeExportTx(innerObj, signerAccount); - auto const txHash = exportTx->getTransactionID(); - - auto const sigData = buildMultiSigningData(innerTx, signerAccount); - auto const sig = sign(valPK, valSK, sigData.slice()); - Buffer const sigBuf(sig.data(), sig.size()); - - STObject sidecar(sfGeneric); - sidecar.setFieldU8(sfSidecarType, sidecarExportSig); - sidecar.setFieldH256(sfTransactionHash, txHash); - sidecar.setFieldVL(sfSigningPubKey, valPK.slice()); - sidecar.setFieldVL(sfTxnSignature, Slice(sigBuf.data(), sigBuf.size())); - - Serializer itemSer; - sidecar.add(itemSer); - auto map = std::make_shared( - SHAMapType::SIDECAR, env.app().getNodeFamily()); - map->setUnbacked(); - map->addItem( - SHAMapNodeType::tnSIDECAR, - make_shamapitem( - sidecar.getHash(HashPrefix::sidecar), itemSer.slice())); - map = map->snapShot(false); - - auto const acceptedHash = map->getHash().as_uint256(); - env.app().getInboundTransactions().giveSet(acceptedHash, map, false); - ce.acceptExportSigSet(acceptedHash); - - auto const agreed = ce.agreedExportSignatures(*exportTx, txHash, 1); - BEAST_EXPECT(agreed); - if (agreed) - { - BEAST_EXPECT(agreed->size() == 1); - BEAST_EXPECT(agreed->at(valPK) == sigBuf); - } - } - - void - testOnPreBuildPreservesExportDecision() - { - testcase("onPreBuild preserves export state through buildLCL"); - - using namespace jtx; - Env env{ - *this, - envconfig(validator, ""), - supported_amendments() | featureConsensusEntropy | featureExport, - nullptr}; - - ConsensusExtensions ce{env.app(), activeNoopJournal()}; - ce.setExportEnabledThisRound(true); - ce.setRngEnabledThisRound(true); - ce.setExportSigConvergenceFailed(); - auto const tx = makeHash("export-prebuild-preserve"); - auto const pk = makeValidatorKeys().front(); - std::uint8_t const sigBytes[] = {1, 2, 3}; - Buffer const sig{sigBytes, sizeof(sigBytes)}; - ce.exportSigCollector().addVerifiedSignature(tx, pk, sig, 10); - - CanonicalTXSet retriableTxs{makeHash("preserve-export-state")}; - ce.onPreBuild(retriableTxs, env.closed()->seq() + 1, makeHash("txset")); - - BEAST_EXPECT(ce.exportSigConvergenceFailed()); - BEAST_EXPECT(ce.exportSigCollector().signatureCount(tx) == 1); - } - void testRngSidecarBuildsLocalSnapshots() { @@ -2865,7 +2409,6 @@ class ConsensusExtensions_test : public beast::unit_test::suite ExtendedPosition pos{makeHash("diagnostic-tx-set")}; pos.commitSetHash = makeHash("diagnostic-commit-set"); pos.entropySetHash = makeHash("diagnostic-entropy-set"); - pos.exportSigSetHash = makeHash("diagnostic-export-set"); pos.exportSignaturesHash = makeHash("diagnostic-export-signatures"); pos.observedParticipantsHash = ce.observedParticipantsHash(); pos.myCommitment = makeHash("diagnostic-commitment"); @@ -2897,34 +2440,6 @@ class ConsensusExtensions_test : public beast::unit_test::suite //@@end test-decorate-position-disabled-legacy } - void - testExportSigGateRequiresQuorumAlignment() - { - testcase("Export sig gate requires quorum alignment"); - - FakeExtensions ext; - ExtensionTickHarness harness; - auto const localHash = ext.exportHash; - - harness.addPeer(1, localHash); - harness.addPeer(2, localHash); - - auto result = harness.tick(ext); - BEAST_EXPECT(!result.readyForAccept); - BEAST_EXPECT(harness.position.exportSigSetHash == localHash); - BEAST_EXPECT(ext.exportSigGateStarted_); - - 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_); - } - void testRngEntropyGateAllowsQuorumDespiteMissingObservation() { @@ -3388,191 +2903,6 @@ class ConsensusExtensions_test : public beast::unit_test::suite BEAST_EXPECT(harness.position.entropySetHash == localHash); } - void - testExportSigGateAllowsAlignedQuorumDespiteMinorityConflict() - { - testcase("Export sig gate ignores minority conflict after quorum"); - - FakeExtensions ext; - ExtensionTickHarness harness; - auto const localHash = ext.exportHash; - auto const conflictHash = makeHash("conflicting-export-sig-set"); - - harness.addPeer(1, localHash); - harness.addPeer(2, localHash); - harness.addPeer(3, localHash); - harness.addPeer(4, conflictHash); - - auto result = harness.tick(ext); - BEAST_EXPECT(!result.readyForAccept); - - result = harness.tick(ext, std::chrono::milliseconds{100}); - BEAST_EXPECT(result.readyForAccept); - BEAST_EXPECT(!ext.exportSigConvergenceFailed_); - } - - void - testExportSigGateAllowsQuorumDespiteMissingObservation() - { - testcase( - "Export sig gate allows quorum despite missing sidecar " - "observation"); - - FakeExtensions ext; - ExtensionTickHarness harness; - auto const localHash = ext.exportHash; - - harness.addPeer(1, localHash); - harness.addPeer(2, localHash); - harness.addPeer(3, localHash); - harness.addPeer(4, std::nullopt); - - auto result = harness.tick(ext); - BEAST_EXPECT(!result.readyForAccept); - BEAST_EXPECT(harness.position.exportSigSetHash == localHash); - BEAST_EXPECT(ext.exportSigGateStarted_); - - // 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_); - } - - void - testExportSigGateObservesAdvertisedPeerSets() - { - testcase("Export sig gate observes advertised peer sets"); - - FakeExtensions ext; - ext.localExportSigs = false; - ext.consensusExportTxns = true; - ExtensionTickHarness harness; - auto const peerHash = makeHash("peer-export-sig-set"); - - harness.addPeer(1, peerHash); - - auto result = harness.tick(ext); - BEAST_EXPECT(!result.readyForAccept); - BEAST_EXPECT(ext.exportSigGateStarted_); - BEAST_EXPECT(!harness.position.exportSigSetHash); - - result = harness.tick( - ext, - harness.parms.rngREVEAL_TIMEOUT * 2 + std::chrono::milliseconds{1}); - BEAST_EXPECT(result.readyForAccept); - BEAST_EXPECT(ext.exportSigConvergenceFailed_); - } - - void - testExportSigGateIgnoresAdvertisedSetsWithoutExportTxns() - { - testcase("Export sig gate ignores advertised sets without export txns"); - - FakeExtensions ext; - ext.localExportSigs = false; - ext.consensusExportTxns = false; - ExtensionTickHarness harness; - auto const peerHash = makeHash("empty-round-export-sig-set"); - - harness.addPeer(1, peerHash); - - auto result = harness.tick(ext); - BEAST_EXPECT(result.readyForAccept); - BEAST_EXPECT(!ext.exportSigGateStarted_); - BEAST_EXPECT(!ext.exportSigConvergenceFailed_); - } - - void - testExportSigGateObservingModeDoesNotPropose() - { - testcase("Export sig gate observing mode does not propose"); - - FakeExtensions ext; - ExtensionTickHarness harness; - harness.mode = ConsensusMode::observing; - - auto result = harness.tick(ext); - BEAST_EXPECT(!result.readyForAccept); - BEAST_EXPECT(harness.position.exportSigSetHash == ext.exportHash); - BEAST_EXPECT(harness.updates == 1); - BEAST_EXPECT(harness.proposes == 0); - } - - void - testExportSigGateRefreshesHashBeforeWaiting() - { - testcase("Export sig gate refreshes hash before waiting"); - - FakeExtensions ext; - auto const staleHash = makeHash("stale-export-sig-set"); - auto const refreshedHash = makeHash("refreshed-export-sig-set"); - auto const conflictHash = makeHash("conflicting-export-sig-set"); - ext.exportHashSequence.push_back(staleHash); - ext.exportHashSequence.push_back(refreshedHash); - - ExtensionTickHarness harness; - harness.start = - std::chrono::steady_clock::time_point{} + std::chrono::seconds{1}; - harness.position.exportSigSetHash = staleHash; - ext.exportSigGateStarted_ = true; - ext.exportSigGateStart_ = harness.start; - harness.addPeer(1, conflictHash); - - auto result = harness.tick(ext, std::chrono::milliseconds{100}); - BEAST_EXPECT(!result.readyForAccept); - BEAST_EXPECT(!ext.exportSigConvergenceFailed_); - BEAST_EXPECT(ext.exportBuilds == 2); - BEAST_EXPECT(harness.position.exportSigSetHash == refreshedHash); - BEAST_EXPECT(harness.updates == 1); - BEAST_EXPECT(harness.proposes == 1); - } - - void - testExportSigGateBoundsCandidateObservationWindow() - { - testcase("Export sig gate bounds candidate observation window"); - - FakeExtensions ext; - ext.localExportSigs = false; - ext.consensusExportTxns = true; - ExtensionTickHarness harness; - - auto result = harness.tick(ext); - BEAST_EXPECT(!result.readyForAccept); - BEAST_EXPECT(ext.exportSigGateStarted_); - BEAST_EXPECT(!harness.position.exportSigSetHash); - BEAST_EXPECT(!ext.exportSigConvergenceFailed_); - - 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_); - } - - void - testExportSigGateSkipsWhenExportDisabled() - { - testcase("Export sig gate skips when Export disabled"); - - FakeExtensions ext; - ext.exportOn = false; - ExtensionTickHarness harness; - - harness.addPeer(1, ext.exportHash); - - auto result = harness.tick(ext); - BEAST_EXPECT(result.readyForAccept); - BEAST_EXPECT(!ext.exportSigGateStarted_); - BEAST_EXPECT(!harness.position.exportSigSetHash); - BEAST_EXPECT(ext.exportBuilds == 0); - } - void testParticipantDiagnosticsOnlyWhenExtensionEnabled() { @@ -3588,7 +2918,6 @@ class ConsensusExtensions_test : public beast::unit_test::suite BEAST_EXPECT(ext.participantDiagnostics == 0); ext.exportOn = true; - ext.localExportSigs = false; result = harness.tick(ext); BEAST_EXPECT(result.readyForAccept); BEAST_EXPECT(ext.participantDiagnostics == 1); @@ -3802,12 +3131,8 @@ class ConsensusExtensions_test : public beast::unit_test::suite auto const ledger = env.app().getLedgerMaster().getClosedLedger(); ce.cacheUNLReport(ledger); - BEAST_EXPECT(ce.exportRootAlignmentThreshold() == 1); BEAST_EXPECT(ce.exportWitnessThreshold() == 1); - ce.setExportSigConvergenceFailed(); - BEAST_EXPECT(ce.exportSigConvergenceFailed()); - ce.setEntropyFailed(); ce.generateEntropySecret(); @@ -3970,20 +3295,13 @@ public: testOnPreBuildTier2WithNegativeUNL(); testProposalProofRoundTrip(); testHarvestRngDataReplacementAndRejection(); - testExportSidecarBuildsLocalSnapshot(); testTransactionAcquireRejectsSidecarWireNodes(); testAcquiredSetsRejectConsensusExtensionPseudos(); - testExportSidecarIgnoresCancelOnlyExports(); - testExportSidecarBuildCapsConsensusCandidates(); - testExportAgreedSignaturesIgnoreLiveCollectorMutation(); - testExportAgreedSignaturesTrustAcceptedRootMembership(); - testOnPreBuildPreservesExportDecision(); testRngSidecarBuildsLocalSnapshots(); testOnPreBuildInjectsStandaloneEntropy(); testOnPreBuildEntropyMismatchKeepsAgreed(); testDiagnosticsJsonAndPositionLogging(); testDecoratePositionSkipsWhenDisabled(); - testExportSigGateRequiresQuorumAlignment(); testRngEntropyGateAllowsQuorumDespiteMissingObservation(); testRngEntropyConflictAllowsQuorumDespiteMissingObservation(); testRngFastPathWaitsAfterEntropyPublish(); @@ -4001,14 +3319,6 @@ public: testRngEntropyConflictTimeoutClearsHash(); testRngEntropyConflictRefreshesHashBeforeWaiting(); testRngEntropyConflictIgnoredWithQuorumAlignment(); - testExportSigGateAllowsAlignedQuorumDespiteMinorityConflict(); - testExportSigGateAllowsQuorumDespiteMissingObservation(); - testExportSigGateObservesAdvertisedPeerSets(); - testExportSigGateIgnoresAdvertisedSetsWithoutExportTxns(); - testExportSigGateObservingModeDoesNotPropose(); - testExportSigGateRefreshesHashBeforeWaiting(); - testExportSigGateBoundsCandidateObservationWindow(); - testExportSigGateSkipsWhenExportDisabled(); testParticipantDiagnosticsOnlyWhenExtensionEnabled(); testExportDisabledRoundClearsCollector(); testValidatorKeylessAuthoringNoops(); diff --git a/src/test/consensus/ConsensusRng_test.cpp b/src/test/consensus/ConsensusRng_test.cpp index c03c393fb..4a5a165e5 100644 --- a/src/test/consensus/ConsensusRng_test.cpp +++ b/src/test/consensus/ConsensusRng_test.cpp @@ -1216,300 +1216,5 @@ public: BEAST_DEFINE_TESTSUITE(ConsensusRng, consensus, ripple); -class ConsensusExport_test : public beast::unit_test::suite -{ - SuiteJournal journal_; - -public: - ConsensusExport_test() : journal_("ConsensusExport_test", *this) - { - } - - void - testExportOnlySteadyStateSucceeds() - { - using namespace csf; - using namespace std::chrono; - - testcase("Export-only sig set converges"); - - ConsensusParms const parms{}; - Sim sim; - PeerGroup peers = sim.createGroup(5); - - for (Peer* peer : peers) - peer->ce().enableExportConsensus_ = true; - - peers.trustAndConnect( - peers, round(0.2 * parms.ledgerGRANULARITY)); - - sim.run(2); - - BEAST_EXPECT(sim.synchronized(peers)); - for (Peer const* peer : peers) - { - BEAST_EXPECT(peer->ce().lastExportSucceeded_); - BEAST_EXPECT(!peer->ce().lastExportRetried_); - } - } - - void - testExportOnlyQuorumIgnoresMinorityConflict() - { - using namespace csf; - using namespace std::chrono; - - testcase("Export-only sig set quorum ignores minority conflict"); - - ConsensusParms const parms{}; - Sim sim; - PeerGroup peers = sim.createGroup(5); - - for (Peer* peer : peers) - peer->ce().enableExportConsensus_ = true; - - peers.trustAndConnect( - peers, round(0.2 * parms.ledgerGRANULARITY)); - - peers[0]->ce().forcedExportSigSetHash_ = - sha512Half(std::string("forced-export-only")); - - sim.run(3); - - PeerGroup honest{ - std::vector{peers[1], peers[2], peers[3], peers[4]}}; - BEAST_EXPECT(sim.branches(honest) == 1); - BEAST_EXPECT(sim.synchronized(honest)); - - for (Peer const* peer : honest) - { - BEAST_EXPECT(peer->ce().lastExportSucceeded_); - BEAST_EXPECT(!peer->ce().lastExportRetried_); - } - BEAST_EXPECT(!peers[0]->ce().lastExportSucceeded_); - } - - void - testExportOnlyMissingProposalSignaturesRetries() - { - //@@start export-missing-signatures-fallback-test - using namespace csf; - using namespace std::chrono; - - testcase("Export-only missing proposal signatures retries"); - - ConsensusParms const parms{}; - Sim sim; - PeerGroup peers = sim.createGroup(5); - - for (Peer* peer : peers) - peer->ce().enableExportConsensus_ = true; - - // Peer 0 remains an active validator/proposer, but drops - // proposal-carried export signatures from peers. Advertised sidecar - // roots do not reconstruct missing signature material; the export must - // retry locally while peers that received quorum signatures can apply. - // - // CSF's Ledger ID is still the base tx-set only. In production the - // quorum peers' ttEXPORT_SIGNATURES witness would make their synthetic - // ledger differ from peer 0's retry ledger until validations pull the - // missing-material peer onto the quorum ledger. Assert the quorum - // cohort's decision rather than full-network synchronization at a - // fixed simulator tick. - peers[0]->ce().suppressOwnExportSig_ = true; - for (std::size_t i = 1; i < peers.size(); ++i) - peers[0]->ce().dropExportSigFrom_.insert(peers[i]->id); - - peers.trustAndConnect( - peers, round(0.2 * parms.ledgerGRANULARITY)); - - sim.run(3); - - PeerGroup honest{ - std::vector{peers[1], peers[2], peers[3], peers[4]}}; - BEAST_EXPECT(sim.branches(honest) == 1); - BEAST_EXPECT(sim.synchronized(honest)); - BEAST_EXPECT(!peers[0]->ce().lastExportSucceeded_); - BEAST_EXPECT(peers[0]->ce().lastExportRetried_); - - for (std::size_t i = 1; i < peers.size(); ++i) - { - BEAST_EXPECT(peers[i]->ce().lastExportSucceeded_); - BEAST_EXPECT(!peers[i]->ce().lastExportRetried_); - } - //@@end export-missing-signatures-fallback-test - } - - void - testExportSigSetQuorumAlignmentIgnoresMinorityConflict() - { - using namespace csf; - using namespace std::chrono; - - testcase("Export sig set quorum ignores minority conflict"); - - ConsensusParms const parms{}; - Sim sim; - PeerGroup peers = sim.createGroup(5); - - for (Peer* peer : peers) - { - peer->ce().enableRngConsensus_ = true; - peer->ce().enableExportConsensus_ = true; - } - - peers.trustAndConnect( - peers, round(0.2 * parms.ledgerGRANULARITY)); - - // Warmup: let peer proposals and close times settle before checking - // the extension tick scenario. - sim.run(1); - BEAST_EXPECT(sim.synchronized(peers)); - - peers[0]->ce().forcedExportSigSetHash_ = - sha512Half(std::string("forced-export-minority")); - - sim.run(3); - - PeerGroup honest{ - std::vector{peers[1], peers[2], peers[3], peers[4]}}; - BEAST_EXPECT(sim.branches(honest) == 1); - BEAST_EXPECT(sim.synchronized(honest)); - - for (Peer const* peer : honest) - { - BEAST_EXPECT(peer->ce().lastExportSucceeded_); - BEAST_EXPECT(!peer->ce().lastExportRetried_); - } - BEAST_EXPECT(!peers[0]->ce().lastExportSucceeded_); - } - - void - testExportSigSetConflictWithoutQuorumRetries() - { - using namespace csf; - using namespace std::chrono; - - testcase("Export sig set conflict without quorum retries"); - - ConsensusParms const parms{}; - Sim sim; - PeerGroup peers = sim.createGroup(5); - - for (Peer* peer : peers) - { - peer->ce().enableRngConsensus_ = true; - peer->ce().enableExportConsensus_ = true; - } - - peers.trustAndConnect( - peers, round(0.2 * parms.ledgerGRANULARITY)); - - sim.run(1); - BEAST_EXPECT(sim.synchronized(peers)); - - peers[0]->ce().forcedExportSigSetHash_ = - sha512Half(std::string("forced-export-conflict-a")); - peers[1]->ce().forcedExportSigSetHash_ = - sha512Half(std::string("forced-export-conflict-b")); - - sim.run(3); - - BEAST_EXPECT(sim.branches(peers) == 1); - for (Peer const* peer : peers) - { - BEAST_EXPECT(!peer->ce().lastExportSucceeded_); - BEAST_EXPECT(peer->ce().lastExportRetried_); - } - } - - void - testExportSigSetRejectsEquivocatedSplitMajorities() - { - using namespace csf; - using namespace std::chrono; - - testcase("Export sig set rejects equivocated split majorities"); - - // Same shape as the entropy equivocation test: 2 honest validators on - // each side, one equivocator advertising a matching sidecar hash to - // each side. Each side sees 3/5 aligned, which must remain below the - // export quorum threshold of 4/5. - ConsensusParms const parms{}; - Sim sim; - - PeerGroup peers = sim.createGroup(5); - PeerGroup left{std::vector{peers[0], peers[1]}}; - Peer* equivocator = peers[2]; - PeerGroup right{std::vector{peers[3], peers[4]}}; - PeerGroup honest = left + right; - - for (Peer* peer : peers) - peer->ce().enableExportConsensus_ = true; - - auto const fast = round(0.2 * parms.ledgerGRANULARITY); - peers.trustAndConnect(peers, fast); - - sim.run(1); - BEAST_EXPECT(sim.synchronized(peers)); - - left.disconnect(right); - - auto const leftHash = sha512Half(std::string("export-equiv-left")); - auto const rightHash = sha512Half(std::string("export-equiv-right")); - for (Peer* peer : left) - { - peer->ce().forcedExportSigSetHash_ = leftHash; - for (Peer const* blocked : right) - peer->ce().dropExportSigFrom_.insert(blocked->id); - } - for (Peer* peer : right) - { - peer->ce().forcedExportSigSetHash_ = rightHash; - for (Peer const* blocked : left) - peer->ce().dropExportSigFrom_.insert(blocked->id); - } - - for (Peer* peer : left) - equivocator->ce().equivocateSidecarsTo_[peer->id].exportSigSetHash = - leftHash; - for (Peer* peer : right) - equivocator->ce().equivocateSidecarsTo_[peer->id].exportSigSetHash = - rightHash; - - sim.run(3); - - for (Peer const* peer : honest) - { - BEAST_EXPECT(!peer->ce().lastExportSucceeded_); - BEAST_EXPECT(peer->ce().lastExportRetried_); - } - } - - void - run() override - { - auto const* filter = std::getenv("XAHAU_EXPORT_TEST"); - std::string f = filter ? filter : ""; - -#define RUN(method) \ - do \ - { \ - if (f.empty() || std::string(#method).find(f) != std::string::npos) \ - method(); \ - } while (false) - - RUN(testExportOnlySteadyStateSucceeds); - RUN(testExportOnlyQuorumIgnoresMinorityConflict); - RUN(testExportOnlyMissingProposalSignaturesRetries); - RUN(testExportSigSetQuorumAlignmentIgnoresMinorityConflict); - RUN(testExportSigSetConflictWithoutQuorumRetries); - RUN(testExportSigSetRejectsEquivocatedSplitMajorities); - -#undef RUN - } -}; - -BEAST_DEFINE_TESTSUITE(ConsensusExport, consensus, ripple); } // namespace test } // namespace ripple diff --git a/src/test/consensus/ExtendedPosition_test.cpp b/src/test/consensus/ExtendedPosition_test.cpp index 7e4881c3b..1f49e4b35 100644 --- a/src/test/consensus/ExtendedPosition_test.cpp +++ b/src/test/consensus/ExtendedPosition_test.cpp @@ -67,7 +67,6 @@ class ExtendedPosition_test : public beast::unit_test::suite BEAST_EXPECT(!deserialized->myReveal); BEAST_EXPECT(!deserialized->commitSetHash); BEAST_EXPECT(!deserialized->entropySetHash); - BEAST_EXPECT(!deserialized->exportSigSetHash); BEAST_EXPECT(!deserialized->exportSignaturesHash); BEAST_EXPECT(!deserialized->observedParticipantsHash); } @@ -132,7 +131,6 @@ class ExtendedPosition_test : public beast::unit_test::suite auto const txSet = makeHash("txset-c"); auto const commitSet = makeHash("commitset-c"); auto const entropySet = makeHash("entropyset-c"); - auto const exportSigSet = makeHash("exportsigset-c"); auto const exportSigs = makeHash("exportsigs-c"); auto const participants = makeHash("participants-c"); auto const commit = makeHash("commit-c"); @@ -141,7 +139,6 @@ class ExtendedPosition_test : public beast::unit_test::suite ExtendedPosition pos{txSet}; pos.commitSetHash = commitSet; pos.entropySetHash = entropySet; - pos.exportSigSetHash = exportSigSet; pos.exportSignaturesHash = exportSigs; pos.observedParticipantsHash = participants; pos.myCommitment = commit; @@ -150,8 +147,8 @@ class ExtendedPosition_test : public beast::unit_test::suite Serializer s; pos.add(s); - // 32 + 1 + 7*32 = 257 - BEAST_EXPECT(s.getDataLength() == 257); + // 32 + 1 + 6*32 = 225 + BEAST_EXPECT(s.getDataLength() == 225); SerialIter sit(s.slice()); auto deserialized = @@ -163,7 +160,6 @@ class ExtendedPosition_test : public beast::unit_test::suite BEAST_EXPECT(deserialized->txSetHash == txSet); BEAST_EXPECT(deserialized->commitSetHash == commitSet); BEAST_EXPECT(deserialized->entropySetHash == entropySet); - BEAST_EXPECT(deserialized->exportSigSetHash == exportSigSet); BEAST_EXPECT(deserialized->exportSignaturesHash == exportSigs); BEAST_EXPECT( deserialized->observedParticipantsHash == participants); @@ -355,7 +351,6 @@ class ExtendedPosition_test : public beast::unit_test::suite ExtendedPosition pos{makeHash("txset-peer")}; pos.commitSetHash = makeHash("commitset-peer"); pos.entropySetHash = makeHash("entropyset-peer"); - pos.exportSigSetHash = makeHash("exportsigset-peer"); pos.exportSignaturesHash = makeHash("exportsigs-peer"); pos.observedParticipantsHash = makeHash("participants-peer"); pos.myCommitment = makeHash("commitment-peer"); @@ -663,14 +658,12 @@ class ExtendedPosition_test : public beast::unit_test::suite auto const txSet = makeHash("txset-json"); auto const commitSet = makeHash("commitset-json"); auto const entropySet = makeHash("entropyset-json"); - auto const exportSigSet = makeHash("exportsigset-json"); auto const exportSigs = makeHash("exportsigs-json"); auto const participants = makeHash("participants-json"); ExtendedPosition pos{txSet}; pos.commitSetHash = commitSet; pos.entropySetHash = entropySet; - pos.exportSigSetHash = exportSigSet; pos.exportSignaturesHash = exportSigs; pos.observedParticipantsHash = participants; @@ -684,8 +677,6 @@ class ExtendedPosition_test : public beast::unit_test::suite BEAST_EXPECT(json["tx_set"].asString() == to_string(txSet)); BEAST_EXPECT(json["commit_set"].asString() == to_string(commitSet)); BEAST_EXPECT(json["entropy_set"].asString() == to_string(entropySet)); - BEAST_EXPECT( - json["export_sig_set"].asString() == to_string(exportSigSet)); BEAST_EXPECT( json["export_signatures"].asString() == to_string(exportSigs)); BEAST_EXPECT( diff --git a/src/test/consensus/ProposalPrecheck_test.cpp b/src/test/consensus/ProposalPrecheck_test.cpp index 1fc4955a1..76907b1c8 100644 --- a/src/test/consensus/ProposalPrecheck_test.cpp +++ b/src/test/consensus/ProposalPrecheck_test.cpp @@ -117,7 +117,7 @@ public: protocol::TMProposeSet exportSet; setPreviousLedger(exportSet); ExtendedPosition exportPos{makeHash("export-lazy-position")}; - exportPos.exportSigSetHash = makeHash("lazy-export-sidecar"); + exportPos.exportSignaturesHash = makeHash("lazy-export-shares"); setPosition(exportSet, exportPos); BEAST_EXPECT( detail::checkProposalExtensions( @@ -184,7 +184,7 @@ public: protocol::TMProposeSet exportSet; setPreviousLedger(exportSet); ExtendedPosition exportPos{makeHash("export-position")}; - exportPos.exportSigSetHash = makeHash("export-sidecar"); + exportPos.exportSignaturesHash = makeHash("export-shares"); setPosition(exportSet, exportPos); BEAST_EXPECT( detail::checkProposalExtensions(exportSet, true, false) diff --git a/src/test/csf/Peer.h b/src/test/csf/Peer.h index fafefd1e2..683ae60fb 100644 --- a/src/test/csf/Peer.h +++ b/src/test/csf/Peer.h @@ -61,7 +61,7 @@ namespace bc = boost::container; /// deterministically, but peers do not merge sidecar roots from each other. struct SidecarStore { - enum class Type { commit, reveal, exportSig }; + enum class Type { commit, reveal }; using EntrySet = hash_map; @@ -325,18 +325,13 @@ struct Peer std::chrono::steady_clock::time_point commitHashConflictStart_{}; bool entropySetPublished_{false}; std::chrono::steady_clock::time_point entropyPublishStart_{}; - bool exportSigGateStarted_{false}; - std::chrono::steady_clock::time_point exportSigGateStart_{}; - bool exportSigConvergenceFailed_{false}; // RNG state bool enableRngConsensus_ = false; - bool enableExportConsensus_ = false; hash_set unlNodes_; hash_set likelyParticipants_; hash_map pendingCommits_; hash_map pendingReveals_; - hash_map pendingExportSigs_; hash_map nodeKeys_; uint256 myEntropySecret_; bool commitSetFrozen_ = false; @@ -346,7 +341,6 @@ struct Peer // that accepted snapshot may feed finalizeRoundEntropy. uint256 lastEntropySetHash_{}; std::optional acceptedEntropySetHash_; - std::optional acceptedExportSigSetHash_; bool entropyFailed_ = false; // Last round summary (for test assertions) @@ -355,15 +349,11 @@ struct Peer std::uint16_t lastEntropyDenominator_ = 0; bool lastEntropyWasFallback_ = true; EntropyTier lastEntropyTier_ = entropyTierNone; - bool lastExportSucceeded_ = false; - bool lastExportRetried_ = false; // Optional test hook: force a specific commit-set hash std::optional forcedCommitSetHash_; // Optional test hook: force a specific entropy-set hash std::optional forcedEntropySetHash_; - // Optional test hook: force a specific export sig-set hash - std::optional forcedExportSigSetHash_; // Optional test hook: remain an active proposer but omit the // entropySetHash advertisement after building the sidecar. This models // a silent sidecar advertiser without shrinking the fixed UNL @@ -374,7 +364,6 @@ struct Peer { std::optional commitSetHash; std::optional entropySetHash; - std::optional exportSigSetHash; }; // Optional test hooks: send recipient-specific sidecar hashes in @@ -385,11 +374,6 @@ struct Peer // Optional test hook: drop reveals from specific peers // (simulates asymmetric reveal delivery / packet loss) hash_set dropRevealFrom_; - // Optional test hook: drop proposal-carried export signatures. - hash_set dropExportSigFrom_; - // Optional test hook: stay an active proposer but do not originate an - // export signature, so tests can force missing local export material. - bool suppressOwnExportSig_ = false; // Optional test hook: exercise generic Consensus bootstrap timing // without making the CSF runtime-config aware. bool testBootstrapFastStartEnabled_ = false; @@ -408,12 +392,6 @@ struct Peer bool exportEnabled() const - { - return enableExportConsensus_; - } - - bool - testSuppressExportSigSetHash() const { return false; } @@ -452,26 +430,6 @@ struct Peer return std::min(quorumThreshold(), tier2Threshold()); } - std::size_t - exportRootAlignmentThreshold() const - { - if (!enableExportConsensus_) - return (std::numeric_limits::max)() / 4; - auto const base = - unlNodes_.empty() ? std::size_t{1} : unlNodes_.size(); - return calculateQuorumThreshold(base); - } - - std::size_t - exportWitnessThreshold() const - { - if (!enableExportConsensus_) - return (std::numeric_limits::max)() / 4; - auto const base = - unlNodes_.empty() ? std::size_t{1} : unlNodes_.size(); - return calculateQuorumThreshold(base); - } - std::size_t pendingCommitCount() const { @@ -548,17 +506,6 @@ struct Peer return hash; } - uint256 - buildExportSigSet(Ledger::Seq seq) - { - if (forcedExportSigSetHash_) - return *forcedExportSigSetHash_; - auto const hash = hashRngSet(pendingExportSigs_, seq, "export-sig"); - peer.sidecarStore.publish( - hash, SidecarStore::Type::exportSig, pendingExportSigs_); - return hash; - } - void generateEntropySecret() { @@ -621,18 +568,6 @@ struct Peer acceptedEntropySetHash_.reset(); } - void - acceptExportSigSet(uint256 const& hash) - { - acceptedExportSigSetHash_ = hash; - } - - void - clearAcceptedExportSigSet() - { - acceptedExportSigSetHash_.reset(); - } - Proposal proposalWithRecipientSidecarHashes( Proposal const& proposal, @@ -647,9 +582,6 @@ struct Peer position.commitSetHash = it->second.commitSetHash; if (it->second.entropySetHash) position.entropySetHash = it->second.entropySetHash; - if (it->second.exportSigSetHash) - position.exportSigSetHash = it->second.exportSigSetHash; - return Proposal{ proposal.prevLedger(), proposal.proposeSeq(), @@ -677,18 +609,13 @@ struct Peer { pendingCommits_.clear(); pendingReveals_.clear(); - pendingExportSigs_.clear(); nodeKeys_.clear(); likelyParticipants_.clear(); myEntropySecret_.zero(); lastEntropySetHash_.zero(); acceptedEntropySetHash_.reset(); - acceptedExportSigSetHash_.reset(); entropyFailed_ = false; commitSetFrozen_ = false; - exportSigGateStarted_ = false; - exportSigGateStart_ = {}; - exportSigConvergenceFailed_ = false; } void @@ -739,7 +666,7 @@ struct Peer Ledger::ID const& prevLedger, std::uint64_t) { - if (!enableRngConsensus_ && !enableExportConsensus_) + if (!enableRngConsensus_) return; if (!isUNLReportMember(nodeId)) return; @@ -758,13 +685,8 @@ struct Peer } } - if (!enableRngConsensus_ || !position.myReveal) - { - if (enableExportConsensus_ && position.myExportSignature && - dropExportSigFrom_.count(nodeId) == 0) - pendingExportSigs_[nodeId] = *position.myExportSignature; + if (!position.myReveal) return; - } // Test hook: drop reveals from specific peers if (dropRevealFrom_.count(nodeId) == 0) @@ -788,10 +710,6 @@ struct Peer } } } - - if (enableExportConsensus_ && position.myExportSignature && - dropExportSigFrom_.count(nodeId) == 0) - pendingExportSigs_[nodeId] = *position.myExportSignature; } bool @@ -923,35 +841,6 @@ struct Peer lastEntropyWasFallback_ = false; } - void - finalizeRoundExport() - { - if (!enableExportConsensus_) - { - lastExportSucceeded_ = false; - lastExportRetried_ = false; - return; - } - - 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 >= exportWitnessThreshold(); - lastExportRetried_ = !lastExportSucceeded_; - } - // --- Lifecycle hooks (matching design doc) --- template @@ -993,8 +882,6 @@ struct Peer Ledger_t const& prevLedger, bool proposing) { - decorateExportPosition(pos, prevLedger, proposing); - if (!enableRngConsensus_ || !proposing || !peer.runAsValidator) return; generateEntropySecret(); @@ -1009,30 +896,6 @@ struct Peer nodeKeys_.insert_or_assign(peer.id, peer.key); } - template - void - decorateExportPosition( - ProposalPosition& pos, - Ledger_t const& prevLedger, - bool proposing) - { - if (!enableExportConsensus_ || !proposing || !peer.runAsValidator) - return; - - auto const seq = static_cast(prevLedger.seq()) + 1; - auto const sig = sha512Half( - std::string("csf-export-sig"), - static_cast(peer.id), - peer.key.second, - seq); - if (!suppressOwnExportSig_) - { - pos.myExportSignature = sig; - pendingExportSigs_[peer.id] = sig; - } - nodeKeys_.insert_or_assign(peer.id, peer.key); - } - void appendJson(Json::Value&) const { @@ -1053,30 +916,11 @@ struct Peer { return testBootstrapFastStartEnabled_; } - bool - hasPendingExportSigs() const - { - return enableExportConsensus_ && !pendingExportSigs_.empty(); - } - bool - hasConsensusExportTxns() const - { - return enableExportConsensus_; - } - void - setExportSigConvergenceFailed() - { - if (enableExportConsensus_) - exportSigConvergenceFailed_ = true; - } - // --- Sub-state accessors --- bool extensionsBusy() const { - return estState_ != EstablishState::ConvergingTx || - (exportEnabled() && - (exportSigGateStarted_ || hasPendingExportSigs())); + return estState_ != EstablishState::ConvergingTx; } EstablishState estState() const @@ -1092,9 +936,6 @@ struct Peer entropySetPublished_ = false; entropyPublishStart_ = {}; commitSetFrozen_ = false; - exportSigGateStarted_ = false; - exportSigGateStart_ = {}; - exportSigConvergenceFailed_ = false; } /// Defined in test/csf/PeerTick.h (keeps xrpld/app dependency @@ -1452,8 +1293,6 @@ struct Peer seq, static_cast(prevLedger.id()), result.txns.id()); - ce().finalizeRoundExport(); - TxSet const acceptedTxs = injectTxs(prevLedger, result.txns); Ledger const newLedger = oracle.accept( prevLedger, diff --git a/src/test/csf/Proposal.h b/src/test/csf/Proposal.h index cd137e937..43cb60f80 100644 --- a/src/test/csf/Proposal.h +++ b/src/test/csf/Proposal.h @@ -43,10 +43,8 @@ struct RngPosition TxSet::ID txSetHash{}; std::optional commitSetHash; std::optional entropySetHash; - std::optional exportSigSetHash; std::optional myCommitment; std::optional myReveal; - std::optional myExportSignature; RngPosition() = default; explicit RngPosition(TxSet::ID txSet) : txSetHash(txSet) @@ -86,10 +84,8 @@ hash_append(Hasher& h, RngPosition const& pos) hash_append(h, pos.txSetHash); appendOpt(pos.commitSetHash); appendOpt(pos.entropySetHash); - appendOpt(pos.exportSigSetHash); appendOpt(pos.myCommitment); appendOpt(pos.myReveal); - appendOpt(pos.myExportSignature); } /** Proposal is a position taken in the consensus process. diff --git a/src/test/rpc/RuntimeConfig_test.cpp b/src/test/rpc/RuntimeConfig_test.cpp index 6d6c4b319..a86568d03 100644 --- a/src/test/rpc/RuntimeConfig_test.cpp +++ b/src/test/rpc/RuntimeConfig_test.cpp @@ -162,7 +162,7 @@ class RuntimeConfig_test : public beast::unit_test::suite R"({"set":{"global":{"rng_claim_drop_pct":3.5,)" R"("rng_reveal_drop_pct":4.5,)" R"("bootstrap_fast_start":false,"rng_poll_ms":5,)" - R"("no_export_sig":true,"no_export_sig_hash":true},)" + R"("no_export_sig":true},)" R"("peer_defaults":{"send_delay_ms":100,)" R"("send_delay_jitter_ms":20,"send_drop_pct":1.25,)" R"("message_types":["proposal"]},)" @@ -180,8 +180,6 @@ class RuntimeConfig_test : public beast::unit_test::suite BEAST_EXPECT(global->rngPollMs == 50); BEAST_EXPECT(global->noExportSig.has_value()); BEAST_EXPECT(*global->noExportSig == true); - BEAST_EXPECT(global->noExportSigHash.has_value()); - BEAST_EXPECT(*global->noExportSigHash == true); auto defaults = rc.getPeerFaultConfig("10.0.0.6:51235"); if (!BEAST_EXPECT(defaults.has_value())) @@ -266,7 +264,6 @@ class RuntimeConfig_test : public beast::unit_test::suite params["set"]["global"] = Json::objectValue; params["set"]["global"]["rng_poll_ms"] = 5; params["set"]["global"]["no_export_sig"] = true; - params["set"]["global"]["no_export_sig_hash"] = true; params["set"]["peer_defaults"] = Json::objectValue; params["set"]["peer_defaults"]["send_delay_ms"] = 100; params["set"]["peer_defaults"]["send_drop_pct"] = 10.0; @@ -280,7 +277,6 @@ class RuntimeConfig_test : public beast::unit_test::suite BEAST_EXPECT(configs.isMember("global")); BEAST_EXPECT(configs["global"]["rng_poll_ms"].asInt() == 50); BEAST_EXPECT(configs["global"]["no_export_sig"].asBool() == true); - BEAST_EXPECT(configs["global"]["no_export_sig_hash"].asBool() == true); BEAST_EXPECT(configs.isMember("peer_defaults")); BEAST_EXPECT(configs.isMember("peer:10.0.0.2:51235")); @@ -290,8 +286,6 @@ class RuntimeConfig_test : public beast::unit_test::suite BEAST_EXPECT(global->rngPollMs == 50); BEAST_EXPECT(global->noExportSig.has_value()); BEAST_EXPECT(*global->noExportSig == true); - BEAST_EXPECT(global->noExportSigHash.has_value()); - BEAST_EXPECT(*global->noExportSigHash == true); auto peerCfg = rc.getPeerFaultConfig("10.0.0.2:51235"); if (!BEAST_EXPECT(peerCfg.has_value())) @@ -447,14 +441,6 @@ class RuntimeConfig_test : public beast::unit_test::suite expectInvalid(params); } - { - Json::Value params; - params["set"] = Json::objectValue; - params["set"]["peer:10.0.0.2:51235"] = Json::objectValue; - params["set"]["peer:10.0.0.2:51235"]["no_export_sig_hash"] = true; - expectInvalid(params); - } - { Json::Value params; params["set"] = Json::objectValue; diff --git a/src/xrpld/app/consensus/ConsensusExtensions.cpp b/src/xrpld/app/consensus/ConsensusExtensions.cpp index bc253960c..fe8f47a58 100644 --- a/src/xrpld/app/consensus/ConsensusExtensions.cpp +++ b/src/xrpld/app/consensus/ConsensusExtensions.cpp @@ -341,25 +341,6 @@ ConsensusExtensions::quorumThreshold() const return safeQuorumThreshold(base); } -std::size_t -ConsensusExtensions::exportRootAlignmentThreshold() const -{ - return exportRootAlignmentThreshold(*activeValidatorView()); -} - -std::size_t -ConsensusExtensions::exportRootAlignmentThreshold( - ActiveValidatorView const& validatorView) -{ - auto const base = validatorView.size(); - - // Export sidecar hashes are signed through ExtendedPosition even when RNG - // is disabled, so a quorum-aligned exportSigSetHash is deterministic - // enough for Export-only mode. Unanimity would let one active validator - // veto an otherwise converged export round. - return safeQuorumThreshold(base); -} - std::size_t ConsensusExtensions::exportWitnessThreshold() const { @@ -1071,13 +1052,6 @@ ConsensusExtensions::exportEnabled() const return exportEnabledThisRound_.load(std::memory_order_relaxed); } -bool -ConsensusExtensions::testSuppressExportSigSetHash() const -{ - auto const cfg = app_.getRuntimeConfig().getConsensusTestConfig(); - return cfg && cfg->noExportSigHash.has_value() && *cfg->noExportSigHash; -} - bool ConsensusExtensions::testBootstrapFastStartEnabled() const { @@ -1220,269 +1194,6 @@ ConsensusExtensions::buildEntropySet(LedgerIndex seq) return hash; } -uint256 -ConsensusExtensions::buildExportSigSet(LedgerIndex seq) -{ - //@@start current-export-global-sigset-build - auto map = - std::make_shared(SHAMapType::SIDECAR, app_.getNodeFamily()); - map->setUnbacked(); - - auto const validatorView = activeValidatorView(); - // Export sidecar convergence should not advertise signatures from trusted - // but inactive validators; those signatures cannot count at apply time. - auto const allSigs = exportSigCollector_.snapshotWithSigs( - activeSignerFilter(*this, validatorView)); - // Only signatures for export txns in the consensus candidate can affect - // this round's sidecar hash; open-ledger-only txns stay cached for later. - std::size_t entryCount = 0; - - for (auto const& [txHash, valSigs] : allSigs) - { - // Candidate membership is the deterministic publication gate. A sig - // may have been verified earlier from the open ledger, but it only - // enters the sidecar hash if the same tx hash is in the converged set. - if (consensusExportTxns_.find(txHash) == consensusExportTxns_.end()) - continue; - - for (auto const& [valPK, sigBuf] : valSigs) - { - STObject sidecar(sfGeneric); - sidecar.setFieldU8(sfSidecarType, sidecarExportSig); - sidecar.setFieldH256(sfTransactionHash, txHash); - sidecar.setFieldVL(sfSigningPubKey, valPK.slice()); - if (sigBuf.size() > 0) - sidecar.setFieldVL( - sfTxnSignature, Slice(sigBuf.data(), sigBuf.size())); - - map->addItem(SHAMapNodeType::tnSIDECAR, makeSidecarItem(sidecar)); - ++entryCount; - } - } - - auto const maxExportSidecarLeaves = validatorView->size() * - std::min(consensusExportTxns_.size(), - static_cast(ExportLimits::maxPendingExports)); - XRPL_ASSERT( - entryCount <= maxExportSidecarLeaves, - "ripple::ConsensusExtensions::buildExportSigSet : " - "export sidecar leaf count must stay within bounded local cap"); - - map = map->snapShot(false); - exportSigSetMap_ = map; - - auto const hash = map->getHash().as_uint256(); - // TODO: move consensus-extension snapshots out of InboundTransactions. - // They are same-process materialization caches only; sidecar roots are no - // longer advertised, fetched, served, or merged from peers. - app_.getInboundTransactions().giveSet(hash, map, false); - - JLOG(j_.debug()) << "Export: built exportSigSet SHAMap" - << " hash=" << hash << " seq=" << seq - << " entries=" << entryCount - << " candidateExportTxns=" << consensusExportTxns_.size() - << " activeValidators=" << validatorView->size(); - //@@end current-export-global-sigset-build - return hash; -} - -bool -ConsensusExtensions::hasPendingExportSigs() const -{ - auto const validatorView = activeValidatorView(); - // The export convergence gate only needs to run for signatures that are - // eligible under the active view used by final quorum evaluation. - auto const allSigs = exportSigCollector_.snapshotWithSigs( - activeSignerFilter(*this, validatorView)); - if (allSigs.empty() || !consensusTxSetMap_) - return false; - - for (auto const& entry : allSigs) - { - if (consensusExportTxns_.find(entry.first) != - consensusExportTxns_.end()) - return true; - } - return false; -} - -bool -ConsensusExtensions::hasConsensusExportTxns() const -{ - return !consensusExportTxns_.empty(); -} - -void -ConsensusExtensions::setExportSigConvergenceFailed() -{ - exportSigConvergenceFailed_ = true; -} - -bool -ConsensusExtensions::exportSigConvergenceFailed() const -{ - return exportSigConvergenceFailed_; -} - -void -ConsensusExtensions::acceptExportSigSet(uint256 const& hash) -{ - acceptedExportSigSetHash_ = hash; -} - -void -ConsensusExtensions::clearAcceptedExportSigSet() -{ - acceptedExportSigSetHash_.reset(); -} - -std::optional -ConsensusExtensions::agreedExportSignatures( - STTx const& exportTx, - uint256 const& txHash, - std::size_t threshold) const -{ - // 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" - << " txHash=" << txHash << " threshold=" << threshold; - return std::nullopt; - } - - auto const acceptedHash = *acceptedExportSigSetHash_; - std::shared_ptr agreedMap; - if (exportSigSetMap_ && - exportSigSetMap_->getHash().as_uint256() == acceptedHash) - { - agreedMap = exportSigSetMap_; - } - else - { - agreedMap = app_.getInboundTransactions().getSet(acceptedHash, false); - } - - if (!agreedMap) - { - JLOG(j_.warn()) << "Export: accepted exportSigSet missing" - << " acceptedHash=" << acceptedHash - << " txHash=" << txHash << " threshold=" << threshold; - return std::nullopt; - } - if (agreedMap->mapType() != SHAMapType::SIDECAR) - { - JLOG(j_.warn()) << "Export: accepted exportSigSet has wrong map type" - << " acceptedHash=" << acceptedHash - << " txHash=" << txHash; - return std::nullopt; - } - - auto const agreedHash = agreedMap->getHash().as_uint256(); - if (agreedHash != acceptedHash) - { - JLOG(j_.warn()) << "Export: accepted exportSigSet hash mismatch" - << " setHash=" << agreedHash - << " acceptedHash=" << acceptedHash - << " txHash=" << txHash; - return std::nullopt; - } - - // The accepted root is the membership decision. Candidate construction - // filters live active signers, but materialization must not re-resolve - // signer keys through mutable manifests or nodes can diverge after a - // rotation. Keep cryptographic verification below; drop only the live - // membership re-filter. - ExportSignatureSnapshot signatures; - bool invalid = false; - agreedMap->visitLeaves( - [&](boost::intrusive_ptr const& item) { - if (invalid) - return; - - try - { - auto admitted = admitSidecarLeaf( - item->key(), - item->slice(), - agreedHash, - j_, - "Export", - "exportSigSet", - "agreed", - ExportLimits::maxExportSignatureSidecarBytes, - &invalid); - if (!admitted || admitted->type != sidecarExportSig) - return; - auto const& sidecar = admitted->sidecar; - - if (!sidecar.isFieldPresent(sfTransactionHash) || - !sidecar.isFieldPresent(sfSigningPubKey) || - !sidecar.isFieldPresent(sfTxnSignature)) - return; - - if (sidecar.getFieldH256(sfTransactionHash) != txHash) - return; - - auto const pk = sidecar.getFieldVL(sfSigningPubKey); - if (!publicKeyType(makeSlice(pk))) - return; - - PublicKey const valPK{makeSlice(pk)}; - - auto const sigVL = sidecar.getFieldVL(sfTxnSignature); - auto const sigSlice = makeSlice(sigVL); - if (!verifyExportSignatureAgainstTx( - exportTx, - valPK, - sigSlice, - txHash, - j_, - "agreed exportSigSet")) - { - invalid = true; - return; - } - - Buffer sigBuf(sigSlice.data(), sigSlice.size()); - if (auto const [_, inserted] = - signatures.emplace(valPK, std::move(sigBuf)); - !inserted) - { - JLOG(j_.warn()) - << "Export: accepted exportSigSet duplicate signer" - << " setHash=" << agreedHash << " txHash=" << txHash - << " signer=" << toBase58(TokenType::NodePublic, valPK); - invalid = true; - } - } - catch (std::exception const& e) - { - JLOG(j_.warn()) - << "Export: agreed exportSigSet parse failed" - << " setHash=" << agreedHash << " txHash=" << txHash - << " error=" << e.what(); - invalid = true; - } - }); - - if (invalid) - return std::nullopt; - - if (signatures.size() < threshold) - { - JLOG(j_.info()) << "Export: accepted exportSigSet below quorum" - << " setHash=" << agreedHash << " txHash=" << txHash - << " signers=" << signatures.size() - << " threshold=" << threshold; - return std::nullopt; - } - - return signatures; -} - void ConsensusExtensions::generateEntropySecret() { @@ -1539,7 +1250,6 @@ ConsensusExtensions::clearRngStatePreservingExport() acceptedEntropySetHash_.reset(); rngRoundSeq_.reset(); roundPrevLedgerHash_ = uint256{}; - consensusTxSetMap_.reset(); consensusExportTxns_.clear(); consensusTxSetHash_.reset(); observedParticipantsHash_.reset(); @@ -1567,12 +1277,7 @@ ConsensusExtensions::clearRngState() // material waiting for a later re-enable. exportSigCollector_.clearAll(); } - exportSigSetMap_.reset(); - acceptedExportSigSetHash_.reset(); consensusExportTxns_.clear(); - exportSigGateStarted_ = false; - exportSigGateStart_ = {}; - exportSigConvergenceFailed_ = false; //@@end round-stop-export-reset clearRngStatePreservingExport(); @@ -1773,7 +1478,6 @@ ConsensusExtensions::cacheConsensusTxSet(RCLTxSet const& txns) if (consensusTxSetHash_ && *consensusTxSetHash_ == txSetHash) return; - consensusTxSetMap_ = txns.map_; consensusExportTxns_ = buildExportTxnLookup(*txns.map_, j_); consensusTxSetHash_ = txSetHash; } @@ -1986,157 +1690,9 @@ ConsensusExtensions::onPreBuild( //@@end rng-inject-pseudotx } - if (exportEnabled()) - { - //@@start export-witness-scrub-stale - auto const validatorView = activeValidatorView(); - for (auto it = retriableTxs.begin(); it != retriableTxs.end();) - { - auto const& tx = it->second; - if (tx && tx->getTxnType() == ttEXPORT_SIGNATURES) - { - // Live witnesses are build-time materializations of the - // accepted sidecar root. Remove stale or externally supplied - // pseudos before reinserting the deterministic witness below. - it = retriableTxs.erase(it); - continue; - } - ++it; - } - //@@end export-witness-scrub-stale - - if (app_.config().standalone()) - { - auto const& valKeys = app_.getValidatorKeys(); - if (valKeys.keys) - { - for (auto const& entry : retriableTxs) - { - auto const& stx = entry.second; - if (!stx || stx->getTxnType() != ttEXPORT || - !stx->isFieldPresent(sfExportedTxn)) - { - continue; - } - - auto const exportTxHash = stx->getTransactionID(); - auto const existing = std::find_if( - retriableTxs.begin(), - retriableTxs.end(), - [&](auto const& candidate) { - auto const& tx = candidate.second; - return tx && - tx->getTxnType() == ttEXPORT_SIGNATURES && - tx->isFieldPresent(sfTransactionHash) && - tx->getFieldH256(sfTransactionHash) == - exportTxHash; - }); - - if (existing != retriableTxs.end()) - continue; - - auto innerTx = ExportLedgerOps::innerExportedTx(*stx); - if (!innerTx) - { - JLOG(j_.warn()) << "Export: standalone witness skipped" - << " exportTxHash=" << exportTxHash - << " reason=inner-tx-parse-failed"; - continue; - } - - ExportResultBuilder::SignatureSnapshot signatures; - signatures.emplace( - valKeys.keys->publicKey, - ExportResultBuilder::signExportedTxn( - *innerTx, - valKeys.keys->publicKey, - valKeys.keys->secretKey)); - - auto witness = ExportResultBuilder::buildSignatureWitness( - exportTxHash, signatures, seq); - - // Standalone uses the same replay witness shape as - // network mode, but the witness is locally synthesized - // from the node's validator key instead of quorum sidecar - // convergence. - retriableTxs.insert( - std::make_shared(std::move(witness))); - } - } - } - //@@start export-witness-from-accepted-root - else if (validatorView->fromUNLReport) - { - auto const threshold = exportWitnessThreshold(*validatorView); - for (auto const& entry : retriableTxs) - { - auto const& stx = entry.second; - if (!stx || stx->getTxnType() != ttEXPORT || - !stx->isFieldPresent(sfExportedTxn)) - continue; - - auto const exportTxHash = stx->getTransactionID(); - auto sigs = - agreedExportSignatures(*stx, exportTxHash, threshold); - if (!sigs) - continue; - - auto witness = ExportResultBuilder::buildSignatureWitness( - exportTxHash, *sigs, seq); - auto const witnessHash = witness.getTransactionID(); - auto const existing = std::find_if( - retriableTxs.begin(), - retriableTxs.end(), - [&](auto const& candidate) { - auto const& tx = candidate.second; - return tx && tx->getTxnType() == ttEXPORT_SIGNATURES && - tx->isFieldPresent(sfTransactionHash) && - tx->getFieldH256(sfTransactionHash) == exportTxHash; - }); - - if (existing != retriableTxs.end()) - { - auto const existingHash = - existing->second->getTransactionID(); - if (existingHash == witnessHash) - continue; - - JLOG(j_.error()) - << "Export: signature witness pseudo-tx mismatch" - << " exportTxHash=" << exportTxHash - << " witnessHash=" << witnessHash - << " existingHash=" << existingHash - << " action=replace-with-agreed"; - // The witness is build-time materialization of the - // accepted sidecar, not a base consensus-set transaction. - // Replacing a mismatch keeps the tx stream tied to the - // accepted root instead of preserving stale local input. - retriableTxs.erase(existing); - } - - // Export signatures determine source quorum success and the - // exported-result witness reference, so they must be tx-stream - // input, not only accepted sidecar memory. The matching - // ttEXPORT consumes this pseudo through the BuildLedger - // pre-scan; the pseudo itself has no ledger effect. - retriableTxs.insert(std::make_shared(std::move(witness))); - } - } - //@@end export-witness-from-accepted-root - else if (!consensusExportTxns_.empty()) - { - JLOG(j_.warn()) - << "Export: not injecting signature witnesses" - << " reason=no-ledger-anchored-validator-view" - << " seq=" << seq - << " candidateExportTxns=" << consensusExportTxns_.size(); - } - } - //@@start accept-time-cleanup-success - // Export's ledger-defining signature witness is now in the tx stream. - // After this point build/replay must use the pre-scanned pseudo, not - // ephemeral sidecar state retained from consensus establish. + // Clear round-local RNG state while preserving proposal-carried Export + // shares for the collector's round-boundary lifecycle. clearRngStatePreservingExport(); //@@end accept-time-cleanup-success } @@ -2509,9 +2065,6 @@ ConsensusExtensions::logPosition( << " entropySetHash=" << (pos.entropySetHash ? to_string(*pos.entropySetHash) : std::string{"none"}) - << " exportSigSetHash=" - << (pos.exportSigSetHash ? to_string(*pos.exportSigSetHash) - : std::string{"none"}) << " exportSignaturesHash=" << (pos.exportSignaturesHash ? to_string(*pos.exportSignaturesHash) @@ -2879,7 +2432,6 @@ ConsensusExtensions::onTick(TickContext const& ctx) } else { - consensusTxSetMap_.reset(); consensusExportTxns_.clear(); consensusTxSetHash_.reset(); } diff --git a/src/xrpld/app/consensus/ConsensusExtensions.h b/src/xrpld/app/consensus/ConsensusExtensions.h index 6b2c2cca8..9569e4e18 100644 --- a/src/xrpld/app/consensus/ConsensusExtensions.h +++ b/src/xrpld/app/consensus/ConsensusExtensions.h @@ -68,7 +68,6 @@ public: using ActiveValidatorView = ripple::ActiveValidatorView; using ActiveValidatorViewPtr = std::shared_ptr; - using ExportSignatureSnapshot = std::map; private: enum class RngContributionKind : uint8_t { commit, reveal }; @@ -92,20 +91,14 @@ private: // Real SHAMaps for the current round (unbacked, ephemeral) std::shared_ptr commitSetMap_; std::shared_ptr entropySetMap_; - std::shared_ptr exportSigSetMap_; // Candidate entropy maps are local snapshots until the gate accepts the // exact root. This hash is set only after the alignment/observation checks // pass. std::optional 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 acceptedExportSigSetHash_; std::optional rngRoundSeq_; // Consensus parent ledger hash, pinned at round start. Input to the // Tier 1 consensus_fallback entropy digest. uint256 roundPrevLedgerHash_; - std::shared_ptr consensusTxSetMap_; hash_map> consensusExportTxns_; std::optional consensusTxSetHash_; @@ -130,9 +123,6 @@ public: std::chrono::steady_clock::time_point commitHashConflictStart_{}; bool entropySetPublished_{false}; std::chrono::steady_clock::time_point entropyPublishStart_{}; - bool exportSigGateStarted_{false}; - std::chrono::steady_clock::time_point exportSigGateStart_{}; - bool exportSigConvergenceFailed_{false}; private: void @@ -188,12 +178,6 @@ public: std::size_t quorumThreshold() const; - std::size_t - exportRootAlignmentThreshold() const; - - static std::size_t - exportRootAlignmentThreshold(ActiveValidatorView const& validatorView); - std::size_t exportWitnessThreshold() const; @@ -308,9 +292,6 @@ public: bool exportEnabled() const; - bool - testSuppressExportSigSetHash() const; - bool testBootstrapFastStartEnabled() const; @@ -320,33 +301,6 @@ public: uint256 buildEntropySet(LedgerIndex seq); - uint256 - buildExportSigSet(LedgerIndex seq); - - bool - hasPendingExportSigs() const; - - bool - hasConsensusExportTxns() const; - - void - setExportSigConvergenceFailed(); - - bool - exportSigConvergenceFailed() const; - - void - acceptExportSigSet(uint256 const& hash); - - void - clearAcceptedExportSigSet(); - - std::optional - agreedExportSignatures( - STTx const& exportTx, - uint256 const& txHash, - std::size_t threshold) const; - ActiveValidatorViewPtr activeValidatorView() const; @@ -561,9 +515,7 @@ public: bool extensionsBusy() const { - return estState_ != EstablishState::ConvergingTx || - (exportEnabled() && - (exportSigGateStarted_ || hasPendingExportSigs())); + return estState_ != EstablishState::ConvergingTx; } EstablishState @@ -580,9 +532,6 @@ public: commitHashConflictStart_ = {}; entropySetPublished_ = false; entropyPublishStart_ = {}; - exportSigGateStarted_ = false; - exportSigGateStart_ = {}; - exportSigConvergenceFailed_ = false; } }; diff --git a/src/xrpld/app/consensus/ConsensusExtensionsDesign.md b/src/xrpld/app/consensus/ConsensusExtensionsDesign.md index d5ff5b1fb..66099b2eb 100644 --- a/src/xrpld/app/consensus/ConsensusExtensionsDesign.md +++ b/src/xrpld/app/consensus/ConsensusExtensionsDesign.md @@ -1,7 +1,7 @@ # Consensus Extension Design Principles This note captures the principles behind the Xahau consensus extensions: -ConsensusEntropy/RNG, proposal sidecars, and export signature convergence. +ConsensusEntropy/RNG, proposal sidecars, and Export signature transport. Read this before changing `ConsensusExtensions`, `ConsensusExtensionsTick`, `ExtendedPosition`, sidecar SHAMap handling, or the related CSF tests. @@ -15,13 +15,11 @@ extension timing must not create divergent closed-ledger effects when a bounded coordination step can avoid it. Fast means those coordination steps stay short and conditional, never becoming an open-ended wait for an extension feature to succeed. Works means missed or late extension material follows that feature's -deterministic fallback, such as Tier 1 consensus_fallback entropy for RNG or normal -Export retry/expiry, rather than blocking core consensus. +deterministic fallback rather than blocking core consensus. ## Fallback Semantics -RNG and Export use similar positive-path sidecar gates, but they do not have -the same safe fallback. RNG closes with a deterministic consensus-bound fallback +RNG closes with a deterministic consensus-bound fallback digest (Tier 1) in either of two cases: (1) when peers cannot establish an accepted participant_aligned or validator_quorum entropy set in time, or (2) whenever the round's active validator view is not UNLReport-backed — no on-ledger @@ -34,9 +32,7 @@ sequence) is already consensus-agreed at injection time, so no second agreement is needed. The result is explicitly labeled (`EntropyTier = consensus_fallback`, `EntropyCount = 0`) — it is user-influenceable via transaction submission and must never be presented -under validator-entropy semantics. Export has no equivalent fallback value: -without quorum-aligned verified export signatures, the export must not be -treated as complete and must retry or expire under transaction rules. +under validator-entropy semantics. The fallback/non-fallback decision is itself ledger-defining. A local node may diagnose that progress looks unlikely from its current peer view, but it must @@ -60,18 +56,16 @@ sidecar gate has had its bounded chance to use proofed/quorum material. `ExtendedPosition` has no whole-position equality or implicit `uint256` conversion. Callers that need the ordinary consensus identity compare `txSetHash` explicitly, or use the generic `positionTxSetID(position)` helper - in templated consensus code. RNG, export sig, commit-set, and entropy-set - hashes are proposal sidecars. They are coordinated during establish, but they - do not define whether peers agree on the ordinary transaction set. + in templated consensus code. RNG commit-set and entropy-set hashes are + proposal sidecars. Export proposal blobs are bound by a payload digest, but + there is no Export signature-set root or establish-phase Export gate. 2. Extension waits are bounded. - RNG and export sidecar convergence may wait briefly inside establish, but - they must not block ledger close indefinitely. If RNG cannot establish + RNG sidecar convergence may wait briefly inside establish, but it must not + block ledger close indefinitely. If RNG cannot establish an accepted entropy set, it injects the deterministic Tier 1 - consensus_fallback digest (labeled `consensus_fallback`, count 0). If export - signatures cannot converge, export retries or expires according to - transaction rules. + consensus_fallback digest (labeled `consensus_fallback`, count 0). The bounded fallback rule is not permission for local shortcuts to decide ledger output. "Cannot establish" means the accepted-hash gate did not @@ -88,20 +82,20 @@ sidecar gate has had its bounded chance to use proofed/quorum material. 4. Align signed inputs, not just derived outputs. - RNG commits, RNG reveals, and export signatures are the verifiable inputs. - The design aligns on signed roots over those input sets using local sidecar - SHAMap snapshots. The final entropy digest and export quorum result are - derived from the accepted local snapshot, not from live collector state. + RNG commits and reveals are the verifiable inputs. The design aligns on + signed roots over those input sets using local sidecar SHAMap snapshots. The + final entropy digest is derived from the accepted local snapshot, not from + live collector state. 5. Sidecars are not transactions. - Commit, reveal, and export signature entries are `STObject(sfGeneric)` - leaves in ephemeral `SHAMapType::SIDECAR` maps. They use `sfSidecarType` + Commit and reveal entries are `STObject(sfGeneric)` leaves in ephemeral + `SHAMapType::SIDECAR` maps. They use `sfSidecarType` to distinguish payloads and `HashPrefix::sidecar` for item hashes. Current same-round consensus does not advertise, fetch, serve, or merge these maps from peers. The maps are local immutable snapshots used to materialize the root a node signed into its proposal and, if accepted, to build the - ledger-visible pseudo/witness. + ledger-visible entropy pseudo. 6. Proposal-visible or validation-visible extension data must be signed. @@ -122,12 +116,11 @@ sidecar gate has had its bounded chance to use proofed/quorum material. A sidecar root proves only byte identity for the local snapshot that produced it. It does not prove that a contribution is well-formed, authorized, or - round-correct. Proposal-carried commits, reveals, and export signatures must + round-correct. Proposal-carried commits, reveals, and Export signatures must pass cheap structural checks, safe key-type checks, active-view membership, - and the relevant cryptographic proof before entering pending RNG/export - state. Cluster trust may affect relay and resource policy, but extension - sidecars become ledger inputs and must be harvested only after the signed - proposal verifies against the claimed validator key. + and the relevant cryptographic proof before entering pending state. Cluster + trust may affect relay and resource policy, but material must be harvested + only after the signed proposal verifies against the claimed validator key. 8. Ledger-defining sidecar material crosses apply as transaction-stream input. @@ -140,7 +133,7 @@ sidecar gate has had its bounded chance to use proofed/quorum material. ## Validator Set And Quorum -The active validator view is the shared denominator for RNG and export: +The active validator view is used by RNG and retained Export witness checks: - Prefer `UNLReport.sfActiveValidators` from the consensus parent ledger. - If no report is available, fall back to configured trusted validators so @@ -155,7 +148,7 @@ The active validator view is the shared denominator for RNG and export: When `featureNegativeUNLActiveViewCap` is enabled, NegativeUNL vote production also uses the parent-ledger `UNLReport.sfActiveValidators` count as the 25-percent disable-cap denominator. This aligns the producer-side nUNL vote -policy with the active-view universe that RNG and Export proofs use. Without +policy with the active-view universe that these checks use. Without that amendment, legacy NegativeUNL voting can still cap against the locally configured trusted UNL size; the consumer-side active-view builder remains defensive and caps any raw ledger NegativeUNL overage against `originalViewSize`. @@ -197,24 +190,19 @@ mechanism in base consensus. Transaction sets are fetchable by hash; proposals are not. A node that joins or falls behind mid-round normally observes for a few ticks/rounds until the relayed proposal stream is coherent enough to participate. -RNG and Export add a stricter requirement on top of that proposal stream: their -sidecar roots are accepted by absolute quorum over a fixed parent-ledger -validator denominator, not by percentages over whichever proposers this node -happens to observe. That fixed denominator is intentional. It gives the -sidecar gates deterministic, intersection-safe semantics: two quorum-aligned -cohorts cannot both make conflicting sidecar roots ledger material under the -same active-view assumptions. The cost is that missed proposal-borne material -does not shrink the target the way observed-proposer percentages do; it leaves -the node short of the fixed quorum. +RNG adds a stricter requirement on top of that proposal stream: its sidecar +roots are accepted by absolute quorum over a fixed parent-ledger validator +denominator, not by percentages over whichever proposers this node happens to +observe. Export proposal blobs remain signed transport only. This branch deliberately does not add same-round sidecar reconciliation for that gap. Sidecar roots are still signed into proposals, but the backing `SHAMapType::SIDECAR` maps are local snapshots only. They are not advertised, served, fetched, or merged from peers, and generic transaction-set acquisition must reject them. A node that missed proposal-carried material may therefore be -unable to materialize the quorum root this round. For RNG it falls back to the -explicit Tier 1 consensus digest or accepts a lower locally materialized tier; for -Export the transaction retries or expires. If a quorum of validators did +unable to materialize the quorum root this round. RNG falls back to the +explicit Tier 1 consensus digest or accepts a lower locally materialized tier. +If a quorum of validators did materialize and validate a richer synthetic ledger, a missing-material validator follows that ledger through the normal validation/LCL path after the round, just as it would after failing to build any other majority ledger. @@ -403,11 +391,12 @@ Sidecar SHAMaps are local immutable snapshots: - Peer-advertised roots are alignment evidence, not payload availability. - Nodes never fetch, advertise, serve, or merge sidecar maps from peers. - If a quorum root cannot be materialized locally before the bounded deadline, - RNG degrades/falls back and Export retries/expires. + RNG degrades or falls back. -Do not use avalanche-style transaction inclusion logic for sidecar inputs. -For RNG and export sidecars, the disagreement to resolve is usually timing or -delivery, not whether a valid contribution should be included. +Do not use avalanche-style transaction inclusion logic for RNG sidecar inputs. +The disagreement to resolve is usually timing or delivery, not whether a valid +contribution should be included. Export witnesses follow ordinary +transaction-set admission after this transition. The entropy sidecar gate always gives peers at least one observation tick after publishing `entropySetHash`. Publishing and accepting in the same tick can hide @@ -418,187 +407,41 @@ that quorum root will not build the richer synthetic ledger in that round. ## Export Principles -`ExportIntent.md` is the normative spine for Export invariants, especially the -replay-witness rule. This section explains the current mechanics and should not -be read as permission to make closed-ledger Export output depend on ephemeral -sidecar memory. +`ExportIntent.md` records the current transition boundary and TODOs. `featureExport` and `featureConsensusEntropy` are independently amendment gated. -Export can run without ConsensusEntropy and still uses the active validator -view's 80% quorum threshold. Verified export signature sidecars converge -through `ExtendedPosition`, and the `exportSigSetHash` is signed by proposals -whether or not RNG is enabled. Do not make Export liveness depend on unanimity: -one active validator with a missing, delayed, or conflicting sidecar must not -veto an otherwise quorum-aligned export round. +### Export transition state -Non-standalone Export completion requires a UNLReport-backed active validator -view. If the parent ledger has no `UNLReport`, Export has no safe deterministic -fallback result, so validators do not publish target-chain signature shares and -the export retries or expires rather than finalizing against local -trusted-configuration thresholds. +The former Export sidecar authority has been removed. `ExtendedPosition` no +longer carries an Export signature-set root, the consensus tick has no Export +root-alignment gate or wait, and `onPreBuild` does not synthesize or replace an +Export witness from process-local collector state. Export therefore cannot +change a closed ledger through a second accept-time decision outside ordinary +transaction-set consensus. -Export's original pre-NegativeUNL validator population must also fit the -32-member target serialization bound. Validators publish no target-chain shares -and Export cannot materialize while it exceeds that bound; temporary NegativeUNL -filtering must not select an implicit bridge committee. +Proposal-carried `exportSignatures` blobs remain as bounded signature transport. +Their ordered digest is bound into the signed proposal through +`exportSignaturesHash`, proposal precheck rejects missing or mismatched payloads, +and the harvester still verifies and stores valid shares. The current attachment +trigger still inspects the open ledger; those shares are not ledger authority +and cannot complete an Export after this excision. -The bounded deployment contract then mirrors the source validator-derived key -universe, weights, and Export/validation threshold in the destination account's -SignerList. A lower destination threshold permits target execution before the -authority needed for source materialization exists. A higher threshold preserves -that safety direction but can strand a successful source latch. The destination -network's ledger-validation quorum is separate: it validates the authorized -transaction's containing ledger, and XPOP proves that finality on return. A -static destination SignerList should remain anchored to the original source -universe during NegativeUNL periods, trading Export liveness for unchanged -destination authority. +`ttEXPORT_SIGNATURES` remains the canonical witness interface for direct apply +and historical replay. Ledger build can pre-scan a witness already present in +the ordered transaction stream, and `Export::doApply` verifies its signatures +and threshold against the parent-ledger active validator view. There is +deliberately no live network producer for that witness in this transition +commit, so a network Export without an explicit canonical witness retries or +expires. The follow-up design must release shares only for validated Export +intents and admit a locally qC-valid witness through ordinary transaction-set +consensus. -The bounded MVP deployment requires a submitter co-signer in the destination -SignerList, held by the target-submission process. Choose weights so destination -quorum requires both the source-validator threshold and that key; for validator -weight total `V` and required validator weight `q`, submitter weight `V` with -target quorum `V + q` is the simple construction. Proposal-carried validator -shares then remain inert until the submitter observes a validated source latch -and signs. The key cannot authorize alone but becomes a liveness and censorship -dependency. It consumes one destination SignerList entry, so this deployment -supports at most 31 source validator identities under a 32-entry limit. - -The current code has no committee selector. A future committee must be explicit, -ledger-anchored, and versioned; a locally configured or sorted UNL subset would -reintroduce nondeterministic or unnamed authority. Full source consensus would -still validate the Export, while committee quorum plus the submitter would be -the honestly stated destination authority. - -The extended proposal machinery is enabled when either feature needs signed -sidecar fields. Do not make Export depend on RNG availability just because RNG -was the first consumer of `ExtendedPosition`. - -Rollout invariant: once a network enables `featureConsensusEntropy`, proposal -messages may use the legacy `currenttxhash` protobuf field to carry a serialized -`ExtendedPosition`, not just a raw 32-byte transaction-set hash. This is a -proposal wire-format change, not a sidecar-reconciliation detail. Disabling -sidecar fetch/reconciliation does not restore compatibility with older binaries -that require `currenttxhash` to be exactly 32 bytes. A network that activates CE -therefore needs every binary expected to process live proposals to understand -the extended position format, or it needs explicit version/capability -negotiation before activation. - -When `featureExport` is disabled, the export sidecar gate is disabled too. Stale -collector entries must not keep a stopped amendment active. - -Only verified export signatures count toward quorum or enter export sidecar -SHAMaps. Proposal-ingress signatures are sender-bound to the trusted proposal -validator and may be stored as unverified until the matching export transaction -is available for cryptographic verification. - -The consensus candidate transaction set is the authority for export signature -verification. The open ledger may be used for early proposal ingestion, but -once a candidate tx set exists, only signatures verified against the `ttEXPORT` -in that candidate set may become quorum material or enter `exportSigSetHash`. - -Export sidecar publication is local-material only. A node may publish only the -verified export signatures it actually has locally, and only for `ttEXPORT` -transactions in the consensus candidate set. Peer-advertised export roots are -used for quorum alignment; they do not reconstruct missing signature material. -The accepted local snapshot root, once quorum-aligned, is the source for the -ledger witness. A node that cannot materialize the accepted quorum witness -locally retries or expires the export and follows the quorum ledger later through -normal validation if other validators built the witness. -`ttEXPORT_SIGNATURES` is the export signature witness interface. Network mode -derives it from the accepted `exportSigSetHash` sidecar snapshot; standalone/dev -helpers may synthesize the same witness from the local validator key. The pseudo -carries the full source-side validator signature witness and binds it to the -matching `ttEXPORT` via `sfTransactionHash`. It has no ledger-state effect by -itself; it exists so apply/replay sees the same signature material through the -transaction stream regardless of which producer supplied it. - -If the consensus candidate contains a `ttEXPORT` but the node has no eligible -local export signatures yet, the export sidecar gate opens only a bounded -safety window for tx-converged peers to advertise `exportSigSetHash`. This is -not a wait-for-Export-success mechanism; it is a short opportunity to avoid -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`, 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 consumes the pre-scanned `ttEXPORT_SIGNATURES` witness, not -the live collector and not ephemeral sidecar state. `Export::doApply` rebuilds -the active validator view from the parent ledger, verifies each witness -signature against the `ttEXPORT` inner transaction, requires source-view quorum, -then canonically assembles the target-chain multisigned transaction. In live -consensus builds, `onPreBuild` first removes any pre-existing export witness and -re-materializes the witness from the accepted `exportSigSetHash` root. That -accepted transaction-stream witness supplies signer membership; apply must not -re-resolve those signing keys through the current manifest cache, because -manifest gossip can differ while the parent-ledger active view is the same. -Historical `LedgerReplay` consumes the same persisted witness after manifests -may have rotated. In both modes, apply still checks signatures and threshold. A -node that times out before accepting a root has no witness and retries/expires; -a node that proceeds uses the same transaction-stream witness during live build -and historical replay. The build-scoped witness map is only an index over that -ordered transaction stream, not hidden consensus state. This avoids -successful-but-different export blobs while preserving the bounded wait model. - -`sfExportResult` metadata stores `sfExportSignatureHash`, a direct reference to -the witness pseudo, rather than duplicating the full signature payload. Clients -assemble the final foreign-chain blob from `ttEXPORT` plus the witness -signatures, or can use a convenience RPC/helper that performs that pure -read-time assembly. That expansion must match `ExportResultBuilder`: canonical -AccountID signer ordering, empty `SigningPubKey`, and the target-chain signer -cap (`STTx::maxMultiSigners()`) before hashing or submitting. The witness can -carry more source-side signatures than the destination transaction may include. -Export currently refuses to sign or materialize when the original UNLReport -validator population exceeds that cap, even if NegativeUNL temporarily shrinks -the effective view below it. The assembly cap remains a defensive serialization -bound, not an implicit committee-selection policy. - -The resulting shadow ticket stores the normalized target transaction's canonical -signing hash in `sfDigest`, not one assembled multisigned transaction ID. Import -therefore accepts any destination-valid signer subset for that exact signing -intent. A missing latch returns `telSHADOW_TICKET_REQUIRED` before consensus or -Hook execution, so an XPOP that races source materialization can be relayed -later. - -Shadow-ticket cancellation is source resource reclamation. It releases account -reserve and an outstanding-ticket slot when a round trip is abandoned, but it -cannot revoke shares already published to peers. Operators should delete a latch -only with external evidence such as target expiry, destination Ticket -consumption, or SignerList invalidation, or with an explicit policy that accepts -later target execution without callback readiness. - -This is intentionally leaner than XPOP. XPOP carries its own UNL and manifest -bundle so it can be independently verified as an external proof. Export witnesses -are not external proof bundles; they are inputs that made it into validated -ledger history. Making them self-contained would require embedding manifest -material or equivalent signing-key history in every witness, which is a separate -protocol/storage design. - -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, -where they can be published in a sidecar set and converged before use. - -Export sig convergence runs in parallel with RNG. An export-side convergence -failure must not change RNG semantics; an RNG fallback must not make export -unsafe. Each feature has its own gate and fallback. - -Accept-time cleanup must preserve Export state through `onPreBuild` whenever -`featureExport` is enabled so the signature witness pseudo can be injected. -After the ordered transaction set contains that witness, replay must not need -the round's export sidecar convergence state. - -CSF consensus tests model the export sidecar gate directly. Testnet scenarios -under `.testnet/scenarios/export/` cover live-node Export+CE behavior and -Export-only quorum behavior. +The target-chain signer cap, normalized intent-hash latch, witness reference, +and historical replay checks remain relevant to that follow-up. They must not +be used to reintroduce an ephemeral accepted root or a process-local apply-time +decision. ## Review Checklist @@ -622,10 +465,10 @@ When changing consensus extension code, check these questions: - Are proposal-visible or validation-visible sidecar fields covered by the relevant signature and duplicate/replay identity? - Are export signatures verified before they count? -- Does export success require `exportSigSetHash` alignment, not just local - collector quorum? +- Are proposal-carried Export shares treated as transport only, never as + independent closed-ledger authority? - Does every ledger-defining export signature enter replay as a `ttEXPORT_SIGNATURES` witness before `ttEXPORT` applies? -- Can one bad validator deny Export to an honest quorum? It must not. -- Can timeout select a largest-but-below-quorum export sidecar set? It must not. +- Does a live witness candidate satisfy local qC before ordinary transaction-set + admission? A peer vote cannot substitute for missing local signatures. - Are CE and Export still independently gated and independently stoppable? diff --git a/src/xrpld/app/consensus/ExportIntent.md b/src/xrpld/app/consensus/ExportIntent.md index aba78f6f9..b4c06d178 100644 --- a/src/xrpld/app/consensus/ExportIntent.md +++ b/src/xrpld/app/consensus/ExportIntent.md @@ -1,193 +1,32 @@ -# Export — Design Intent (canonical spine) +# Export Design Transition -This is the **normative** intent for `featureExport`: the invariants that must -hold regardless of how the implementation is refactored. The verbose mechanics -live in `ConsensusExtensionsDesign.md`; the reviewer-facing walkthrough lives in -the PR description. **Both defer to this file.** +The former same-ledger Export sidecar authority has been removed. This file is +intentionally minimal until its replacement is implemented and reviewed. -How to use it: if code contradicts an invariant below, the *code* is wrong — or -the invariant is being changed and **this file must be consciously edited in the -same change, with the rationale**. In particular, Export is not replay-clean -unless every closed-ledger effect can be rebuilt from the parent ledger and the -closed transaction set alone. +## Current State -## Purpose (one line) +- Proposal `exportSignatures` payloads remain bounded and are signed through + `exportSignaturesHash`. +- Signature harvesting and collection remain available as transport plumbing. +- `ttEXPORT_SIGNATURES` remains readable by direct apply and historical replay. +- No Export signature-set root, establish-phase Export gate, accepted-root + state, or `onPreBuild` witness materializer remains. +- There is currently no live network producer for a canonical Export witness; + network Export retries or expires without one. -Export lets a quorum of active validators produce a foreign-chain-submittable -transaction from an agreed `ttEXPORT`, without letting local sidecar timing, -collector state, or validator silence change the bytes of a successful closed -ledger result. +## TODO -## Invariants +- Persist the validated Export intent/latch and its chosen validator universe. +- Release and relay validator signatures only after the origin ledger validates. +- Require local qC before a witness becomes an ordinary transaction-set + candidate; peer popularity must never substitute for missing evidence. +- Decide and specify the canonical next-ledger pseudo. The leading option is a + self-verifying pseudo containing the fully assembled ordinary XRPL multisigned + transaction, linked to the origin Export transaction and source ledger. +- Keep callback identity bound to the normalized unsigned target intent, not to + one signature-envelope-dependent target transaction ID. +- Specify retry, expiry, bump, subscription, Import, and historical replay + behavior before enabling live completion. -**INV-1 — Quorum, not unanimity.** -Export success is gated by active-validator quorum alignment on the export -signature set. A missing, delayed, or silent minority must not veto an otherwise -quorum-aligned export. -*Anti-pattern:* requiring full observation of every tx-converged validator before -success. - -**INV-2 — Fixed active-view denominator.** -Export thresholds are computed over the parent-ledger active validator view. The -denominator must never be derived from locally observed peers, locally available -signatures, or the subset that happened to advertise sidecar hashes. -On networks that use NegativeUNL, `featureNegativeUNLActiveViewCap` should be -active before or with Export so producer-side nUNL voting caps against the same -UNLReport active-source universe that bounds NegativeUNL shrink. Export quorum -itself is computed over the effective post-NegativeUNL active view. Direct -Export apply still rebuilds and defensively caps the parent active view before -checking the witness threshold. -*Anti-pattern:* letting silence shrink the quorum threshold. - -**INV-3 — Accepted sidecar root, not live collector.** -Any successful Export apply path must use the signature set rooted at the -`exportSigSetHash` accepted by the tick gate. Late local collector arrivals, -timeout flags, or unverified proposal-carried signatures must not change the -signer set selected for the ledger. -*Anti-pattern:* assembling from `ExportSigCollector` at apply time. - -**INV-4 — Replay witness in the transaction stream.** -If export sidecar material changes closed-ledger output, that material must be -represented by canonical ledger input before apply. The closed ledger must be -replayable from `(parent ledger, ordered closed transaction set)` without live -consensus sidecar memory. Export signatures are such a witness: they determine -source quorum success and exported-result metadata, so they must be carried by -a replayable companion pseudo transaction or equivalent transaction-stream -artifact. The shadow-ticket intent hash is signature-independent. -*Anti-pattern:* using ephemeral accepted sidecar state to create a shadow ticket -or result that cannot be reconstructed by ledger delta replay. - -Current shape: `ttEXPORT_SIGNATURES` is the signature witness interface. It -carries the full source-validator signature witness and binds it to the matching -`ttEXPORT` via `sfTransactionHash`. Network consensus produces it from the -accepted sidecar set; standalone/dev helpers may produce the same witness from -the local validator key. Ledger build pre-scans the ordered transaction stream -into a build-local index, and `ttEXPORT` apply consumes that pre-scanned witness. -The index is not an extra consensus input; it is only an efficient lookup over -the canonical transaction set. In live consensus builds, `onPreBuild` first -removes any pre-existing export witness and re-materializes the witness from the -accepted sidecar root. That witness is the signer-membership source for apply; -current manifest-cache state must not re-decide which accepted signing keys -count. Historical `LedgerReplay` uses the same membership rule because current -manifests may no longer map old rotated signing keys. Both paths still verify -each signature against the inner transaction and require the parent-view -threshold. Direct apply paths that did not run `onPreBuild` remain conservative -and filter witness signers through the live active-validator view. - -**INV-5 — Store the witness once.** -The signature witness is canonical input; metadata is output. Metadata may carry -hashes and references for client discovery, but it should not duplicate the full -signature payload merely to avoid a client dereference. A convenience RPC may -expand `ttEXPORT + witness` into the foreign-chain-submittable blob on read. -*Anti-pattern:* storing the same validator signatures once as replay input and -again as a full metadata blob without a separate consensus reason. - -**INV-6 — Bounded retry window.** -An Export that cannot obtain quorum-aligned signatures within its bounded ledger -window retries or expires through normal transaction semantics. It must not wait -unboundedly, pick the largest sub-quorum set, or finalize against local trusted -configuration as a fallback. - -The success-vs-retry decision remains a bounded timing edge, like ordinary -consensus convergence: one node may observe the quorum-aligned witness before -its deadline while another retries. Validation resolves that ledger disagreement. -What must never happen is a "successful" export whose signature bytes come from -live collector state, late proposal arrivals, or a node-local sub-quorum set -instead of the accepted witness in the transaction stream. - -**INV-7 — Shadow tickets are latches, not global tombstones.** -The shadow-ticket object binds the canonical target signing intent, -not one authorization-envelope-dependent target transaction ID. Any -destination-valid execution of that exact intent may complete the callback. -Deletion permits a later re-mint of the same `(account, ticketSequence)` latch, -so replay protection beyond the live latch is a separate protocol decision, not -an implicit property of shadow tickets. - -Cancellation exists to reclaim the account reserve and bounded outstanding- -ticket slot when a round trip will not complete. It deletes callback readiness; -it does not revoke target-chain signatures already published in proposals. Safe -cleanup therefore depends on external evidence that the capability is no longer -executable or the callback is intentionally abandoned, such as target -`LastLedgerSequence` expiry, destination Ticket consumption, or SignerList -invalidation. - -**INV-8 — Export signatures are public capabilities.** -Proposal-carried signature shares may be observed, assembled, and submitted as -soon as destination quorum exists. Source-side witness agreement governs what -Xahau records; it is not a confidentiality or destination-execution gate. -Import therefore waits outside consensus when its shadow ticket does not yet -exist and matches a later XPOP against the signature-independent intent. -The main defense for exposing shares before source finality is authority -equivalence: destination execution must require the same validator-derived -authority that Xahau requires to validate and materialize the Export. -*Anti-pattern:* relying on proposal timing or canonical signer selection to -hide or delay an otherwise valid destination transaction. - -**INV-9 — The active source authority must fit the destination protocol.** -Export does not publish shares without a ledger-anchored `UNLReport`, and does -not publish shares or materialize a result when the source validator population -before NegativeUNL filtering exceeds `STTx::maxMultiSigners()`. Silently -selecting a capped subset would replace source-view authority with an implicit -bridge committee. Any future bounded committee must be an explicit, separately -reviewed policy. - -The current source implementation enforces the 32-signer bound. The bounded MVP -deployment additionally requires one destination submitter co-signer, so its -practical full-view limit is 31 source validator identities. Source code cannot -inspect that remote configuration; activation tooling and monitoring must. - -**INV-10 — Source and destination authorization must be equivalent.** -The bounded deployment contract uses the same validator-derived key universe -and equivalent weighted threshold for Xahau Export/validation and the target -account's SignerList. A lower destination threshold defeats the pre-finality -share-exposure defense. A higher threshold is safety-conservative but can leave -a successful source latch without enough witness authority to execute. - -The target network's ledger-validation quorum is independent: the SignerList -authorizes the account transaction, target consensus validates the containing -ledger, and XPOP later proves that finality. Because an ordinary target -SignerList is static, configure it against the original pre-NegativeUNL source -universe and accept reduced Export liveness during NegativeUNL periods rather -than lowering destination authority. - -The bounded MVP requires a submitter co-signer held by the target-submission -process. If validator weights total `V`, validator threshold is `q`, submitter -weight is `C`, and target quorum is `C + q > V`, validator shares alone cannot -execute and the submitter still needs validator weight `q`. It signs only after -observing the validated source latch, turning the submitter into a -liveness/censorship dependency rather than a sole safety authority. - -A future committee may decouple total UNL size from the destination cap, but it -must be explicit ledger-anchored source state with versioned membership and -rotation. Full source consensus validates the intent; at most 31 committee keys -supply shares; the submitter releases after validation. The target trust claim -then becomes committee quorum plus submitter, not full-UNL destination authority. - -## Replay Witness Shape - -The accepted local export sidecar snapshot is not consumed directly by -`Export::doApply`. -Before ledger build, a producer injects one `ttEXPORT_SIGNATURES` pseudo for -each export that has usable signatures. In network mode that producer is the -consensus extension accept path; in standalone/dev mode it can be a local helper. The -pseudo has no ledger-state effect by itself; it is the ledger's replay witness -for the validator signatures. - -Metadata stores `sfExportSignatureHash`, a direct reference to the witness -pseudo, rather than duplicating the signature payload as an assembled -`sfExportedTxn` blob. Clients assemble the final foreign-chain transaction from -the original `ttEXPORT` inner transaction plus the witness signatures. Assembly -must follow the same deterministic contract as `ExportResultBuilder`: sort -signers canonically by AccountID, use an empty `SigningPubKey`, and cap the -target-chain `Signers` array at `STTx::maxMultiSigners()` before computing or -submitting the blob. The witness may contain extra source-side signatures that -are valid replay input but are not part of the target-chain blob. Source-chain -export does not prove the destination account's SignerList or quorum policy; -that compatibility is an operator/client contract for the chosen target chain. - -This is not an XPOP-style self-contained proof. XPOP embeds its UNL and manifest -bundle because it is imported as external proof material. Export witnesses are -validated-history replay inputs. If we later want trustless historical -re-verification without relying on validated inclusion, the larger design is to -ledger-anchor validator signing-key history (for example via `UNLReport`) or to -embed manifest proof material; that is intentionally out of scope here. +The active design record and evidence live under `.ai-docs/`; source comments +should not recreate superseded designs while these TODOs remain open. diff --git a/src/xrpld/app/consensus/ProposalPrecheck.h b/src/xrpld/app/consensus/ProposalPrecheck.h index 3472190e9..52968db79 100644 --- a/src/xrpld/app/consensus/ProposalPrecheck.h +++ b/src/xrpld/app/consensus/ProposalPrecheck.h @@ -121,7 +121,7 @@ checkProposalExtensions( parsedPosition->myReveal; bool const hasExtensionDiagnostics = parsedPosition->observedParticipantsHash.has_value(); - bool const hasExportMaterial = parsedPosition->exportSigSetHash || + bool const hasExportMaterial = parsedPosition->exportSignaturesHash || set.exportsignatures_size() > 0; if (hasEntropyMaterial && !isEntropyEnabled()) return {ProposalPrecheckResult::entropyDisabled, parsedPosition}; diff --git a/src/xrpld/app/consensus/RCLCxPeerPos.h b/src/xrpld/app/consensus/RCLCxPeerPos.h index cd9e36bbb..0ce65bc98 100644 --- a/src/xrpld/app/consensus/RCLCxPeerPos.h +++ b/src/xrpld/app/consensus/RCLCxPeerPos.h @@ -57,7 +57,6 @@ struct ExtendedPosition // === Set Hashes (sub-state quorum, not core tx-set identity) === std::optional commitSetHash; std::optional entropySetHash; - std::optional exportSigSetHash; std::optional exportSignaturesHash; // Signed diagnostic only: not a quorum input and not part of tx-set // identity. @@ -127,9 +126,8 @@ struct ExtendedPosition // Wire compatibility: if no extensions, emit exactly 32 bytes // so legacy nodes that expect a plain uint256 work unchanged. - if (!commitSetHash && !entropySetHash && !exportSigSetHash && - !exportSignaturesHash && !observedParticipantsHash && - !myCommitment && !myReveal) + if (!commitSetHash && !entropySetHash && !exportSignaturesHash && + !observedParticipantsHash && !myCommitment && !myReveal) return; std::uint8_t flags = 0; @@ -141,8 +139,6 @@ struct ExtendedPosition flags |= 0x04; if (myReveal) flags |= 0x08; - if (exportSigSetHash) - flags |= 0x10; if (exportSignaturesHash) flags |= 0x20; if (observedParticipantsHash) @@ -157,8 +153,6 @@ struct ExtendedPosition s.addBitString(*myCommitment); if (myReveal) s.addBitString(*myReveal); - if (exportSigSetHash) - s.addBitString(*exportSigSetHash); if (exportSignaturesHash) s.addBitString(*exportSignaturesHash); if (observedParticipantsHash) @@ -175,8 +169,6 @@ struct ExtendedPosition ret["commit_set"] = to_string(*commitSetHash); if (entropySetHash) ret["entropy_set"] = to_string(*entropySetHash); - if (exportSigSetHash) - ret["export_sig_set"] = to_string(*exportSigSetHash); if (exportSignaturesHash) ret["export_signatures"] = to_string(*exportSignaturesHash); if (observedParticipantsHash) @@ -213,8 +205,9 @@ struct ExtendedPosition if (flags == 0) return std::nullopt; - // Reject unknown flag bits (reduces wire malleability) - if (flags & 0x80) + // Reject unknown or retired flag bits (reduces wire malleability). + // 0x10 was the removed Export sidecar-root advertisement. + if (flags & 0x90) return std::nullopt; // Validate exact byte count for the flagged fields. @@ -235,8 +228,6 @@ struct ExtendedPosition pos.myCommitment = sit.get256(); if (flags & 0x08) pos.myReveal = sit.get256(); - if (flags & 0x10) - pos.exportSigSetHash = sit.get256(); if (flags & 0x20) pos.exportSignaturesHash = sit.get256(); if (flags & 0x40) diff --git a/src/xrpld/app/misc/RuntimeConfig.h b/src/xrpld/app/misc/RuntimeConfig.h index 3261b8dd9..8fcd50b57 100644 --- a/src/xrpld/app/misc/RuntimeConfig.h +++ b/src/xrpld/app/misc/RuntimeConfig.h @@ -96,9 +96,6 @@ struct ConsensusTestConfig std::optional rngPollMs; // Disable export signature attachment (testing sub-quorum scenarios). std::optional noExportSig; - // Withhold exportSigSetHash publication while still attaching export - // signatures (testing no-veto missing-observation scenarios). - std::optional noExportSigHash; // Standalone-only entropy selection overrides for hook API tests. std::optional standaloneEntropyTier; std::optional standaloneEntropyCount; @@ -110,8 +107,7 @@ struct ConsensusTestConfig return (rngClaimDropPctX100 && *rngClaimDropPctX100 > 0) || (rngRevealDropPctX100 && *rngRevealDropPctX100 > 0) || (bootstrapFastStart && *bootstrapFastStart) || rngPollMs || - (noExportSig && *noExportSig) || - (noExportSigHash && *noExportSigHash) || standaloneEntropyTier || + (noExportSig && *noExportSig) || standaloneEntropyTier || standaloneEntropyCount || standaloneEntropyDenominator; } }; diff --git a/src/xrpld/app/misc/detail/RuntimeConfig.cpp b/src/xrpld/app/misc/detail/RuntimeConfig.cpp index bf4fc8f8e..94ce05b81 100644 --- a/src/xrpld/app/misc/detail/RuntimeConfig.cpp +++ b/src/xrpld/app/misc/detail/RuntimeConfig.cpp @@ -245,7 +245,7 @@ isGlobalField(std::string const& name) { return name == "rng_claim_drop_pct" || name == "bootstrap_fast_start" || name == "rng_reveal_drop_pct" || name == "rng_poll_ms" || - name == "no_export_sig" || name == "no_export_sig_hash"; + name == "no_export_sig"; } bool @@ -445,13 +445,6 @@ parseConsensusTestConfig(Json::Value const& v, std::string& error) return std::nullopt; cfg.noExportSig = parsed; } - else if (name == "no_export_sig_hash") - { - bool parsed = false; - if (!parseBool(v[name], name, parsed, error)) - return std::nullopt; - cfg.noExportSigHash = parsed; - } } return cfg; } @@ -524,8 +517,6 @@ consensusTestConfigJson(ConsensusTestConfig const& cfg) entry["rng_poll_ms"] = *cfg.rngPollMs; if (cfg.noExportSig.has_value()) entry["no_export_sig"] = *cfg.noExportSig; - if (cfg.noExportSigHash.has_value()) - entry["no_export_sig_hash"] = *cfg.noExportSigHash; return entry; } } // namespace diff --git a/src/xrpld/app/tx/detail/Export.cpp b/src/xrpld/app/tx/detail/Export.cpp index 07e5f1f40..dffff201d 100644 --- a/src/xrpld/app/tx/detail/Export.cpp +++ b/src/xrpld/app/tx/detail/Export.cpp @@ -320,10 +320,6 @@ Export::doApply() << " witnessSigs=" << signatures.size() << " collectorSigs=" << sigCount << " threshold=" << threshold << " unlSize=" << unlSize - << " exportSigConvergenceFailed=" - << (consensusExtensions.exportSigConvergenceFailed() - ? "yes" - : "no") << " result=terRETRY_EXPORT"; return terRETRY_EXPORT; } diff --git a/src/xrpld/consensus/ConsensusExtensionsTick.h b/src/xrpld/consensus/ConsensusExtensionsTick.h index 4643d1cfa..d79ba0f7b 100644 --- a/src/xrpld/consensus/ConsensusExtensionsTick.h +++ b/src/xrpld/consensus/ConsensusExtensionsTick.h @@ -259,10 +259,6 @@ extensionsTick(Ext& ext, Ctx const& ctx) << " entropySetHash=" << (ourPos.entropySetHash ? to_string(*ourPos.entropySetHash) : std::string{"none"}) - << " exportSigSetHash=" - << (ourPos.exportSigSetHash - ? to_string(*ourPos.exportSigSetHash) - : std::string{"none"}) << " myCommitment=" << (ourPos.myCommitment ? "yes" : "no") << " myReveal=" << (ourPos.myReveal ? "yes" : "no"); @@ -693,8 +689,7 @@ extensionsTick(Ext& ext, Ctx const& ctx) // 2. Subsequent ticks: check for conflict and rebuild if needed, // bounded by deadline. // - // Same pattern as commitSetHash conflict handling (line ~308) - // and exportSigSetHash convergence gate (line ~674). + // Same pattern as commitSetHash conflict handling (line ~308). { auto const ourPos = ctx.getPosition(); if (ourPos.entropySetHash) @@ -935,308 +930,6 @@ extensionsTick(Ext& ext, Ctx const& ctx) << " mode=" << to_string(ctx.mode); } - // Export sig convergence gate: runs after RNG sub-states when Export has - // verified signatures to publish or when tx-converged peers advertise - // exportSigSetHash roots we can locally materialize. This is a bounded - // safety coordination window, not a wait-for-Export-success mechanism. - if constexpr (requires { ctx.getPosition().exportSigSetHash; }) - { - if (!ext.exportEnabled()) - return {.readyForAccept = true}; - - auto startExportSigGate = [&]() -> bool { - if (ext.exportSigGateStarted_) - return false; - ext.exportSigGateStarted_ = true; - ext.exportSigGateStart_ = ctx.nowSteady; - return true; - }; - - auto observedPeerExportSigSets = [&](auto const& pos) { - std::size_t peerSets = 0; - for (auto const& [_, peerPos] : ctx.peerPositions) - { - auto const& pp = peerPos.proposal().position(); - if (positionTxSetID(pp) != positionTxSetID(pos)) - continue; // not tx-converged - if (!pp.exportSigSetHash) - continue; - - ++peerSets; - } - return peerSets; - }; - - bool hasLocalExportSigs = ext.hasPendingExportSigs(); - //@@start export-sigset-material-wait - if (!hasLocalExportSigs && ext.hasConsensusExportTxns()) - { - auto const peerSets = observedPeerExportSigSets(ctx.getPosition()); - if (peerSets > 0) - { - startExportSigGate(); - hasLocalExportSigs = ext.hasPendingExportSigs(); - if (!hasLocalExportSigs) - { - auto const elapsed = - ctx.nowSteady - ext.exportSigGateStart_; - auto const deadline = - detail::sidecarConvergenceTimeout(ctx.parms); - if (elapsed <= deadline) - { - JLOG(ext.j_.debug()) - << "Export: bounded wait for advertised " - "exportSigSet local material" - << " buildSeq=" << ctx.buildSeq - << " peerSets=" << peerSets - << " elapsedMs=" << toMs(elapsed) - << " deadlineMs=" << toMs(deadline); - return {}; - } - - ext.setExportSigConvergenceFailed(); - ext.clearAcceptedExportSigSet(); - JLOG(ext.j_.warn()) - << "Export: advertised exportSigSet material timeout" - << " buildSeq=" << ctx.buildSeq - << " peerSets=" << peerSets - << " elapsedMs=" << toMs(elapsed) - << " deadlineMs=" << toMs(deadline) - << " action=retry-or-expire"; - } - } - else - { - // A candidate ttEXPORT with no local sig material gets one - // short observation window so proposal-carried signatures can - // arrive before apply. If nothing appears in time, apply takes - // the retry/expire path. - startExportSigGate(); - auto const elapsed = ctx.nowSteady - ext.exportSigGateStart_; - auto const deadline = - detail::sidecarConvergenceTimeout(ctx.parms); - if (elapsed <= deadline) - { - JLOG(ext.j_.debug()) - << "Export: bounded wait for exportSigSet " - "advertisement" - << " buildSeq=" << ctx.buildSeq - << " elapsedMs=" << toMs(elapsed) - << " deadlineMs=" << toMs(deadline) - << " candidateExportTxns=yes"; - return {}; - } - - ext.setExportSigConvergenceFailed(); - ext.clearAcceptedExportSigSet(); - JLOG(ext.j_.warn()) - << "Export: exportSigSet advertisement timeout" - << " buildSeq=" << ctx.buildSeq - << " elapsedMs=" << toMs(elapsed) - << " deadlineMs=" << toMs(deadline) - << " action=retry-or-expire"; - } - } - //@@end export-sigset-material-wait - - if (hasLocalExportSigs) - { - //@@start export-publish-sigset-hash - auto const buildSeqExport = ctx.buildSeq; - auto const exportHash = ext.buildExportSigSet(buildSeqExport); - - auto currentPos = ctx.getPosition(); - bool publishedNewHash = false; - if (ext.testSuppressExportSigSetHash()) - { - if (currentPos.exportSigSetHash) - { - currentPos.exportSigSetHash.reset(); - ctx.updatePosition(currentPos); - - if (ctx.mode == ConsensusMode::proposing) - ctx.propose(); - } - - JLOG(ext.j_.debug()) - << "Export: withholding exportSigSetHash" - << " reason=runtime-config-noExportSigHash" - << " buildSeq=" << buildSeqExport << " hash=" << exportHash; - } - else - { - publishedNewHash = !currentPos.exportSigSetHash || - *currentPos.exportSigSetHash != exportHash; - if (publishedNewHash) - { - currentPos.exportSigSetHash = exportHash; - ctx.updatePosition(currentPos); - - if (ctx.mode == ConsensusMode::proposing) - ctx.propose(); - - JLOG(ext.j_.debug()) << "Export: published exportSigSetHash" - << " buildSeq=" << buildSeqExport - << " hash=" << exportHash; - } - } - //@@end export-publish-sigset-hash - - //@@start export-sigset-conflict-wait - // Check quorum agreement on exportSigSetHash. Like RNG entropy, - // Export success is an accept-time derived effect outside tx-set - // equality. A local-only quorum must not succeed unless enough - // tx-converged peers advertise the same export sig sidecar hash. - { - if (startExportSigGate() || publishedNewHash) - { - JLOG(ext.j_.debug()) << "Export: exportSigSet published" - << " buildSeq=" << buildSeqExport - << " hash=" << exportHash - << " action=wait-for-peer-observation"; - return {}; - } - - auto inspectExportPeers = [&](auto const& pos) { - return detail::inspectTxConvergedSidecarPeers( - ctx.peerPositions, - pos, - ext.localIsActiveValidator(), - [](auto const& position) { - return position.exportSigSetHash; - }, - [&ext](auto const& nodeId) { - return ext.isUNLReportMember(nodeId); - }, - [](auto const&) {}); - }; - - auto exportState = inspectExportPeers(ctx.getPosition()); - auto const exportQuorum = ext.exportRootAlignmentThreshold(); - auto quorumAligned = [&] { - return exportState.quorumAligned(exportQuorum); - }; - bool acceptedExportSigHash = false; - //@@start export-sigset-alignment-check - if (exportState.conflict && !quorumAligned()) - { - auto const refreshedHash = - ext.buildExportSigSet(buildSeqExport); - auto current = ctx.getPosition(); - if (!current.exportSigSetHash || - *current.exportSigSetHash != refreshedHash) - { - auto const oldHash = current.exportSigSetHash; - current.exportSigSetHash = refreshedHash; - ctx.updatePosition(current); - if (ctx.mode == ConsensusMode::proposing) - ctx.propose(); - JLOG(ext.j_.debug()) - << "Export: refreshed exportSigSetHash" - << " reason=local-refresh" - << " buildSeq=" << buildSeqExport << " oldHash=" - << (oldHash ? to_string(*oldHash) - : std::string{"none"}) - << " newHash=" << refreshedHash; - } - - exportState = inspectExportPeers(ctx.getPosition()); - } - //@@end export-sigset-alignment-check - - //@@start export-no-veto-quorum-branches - 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" - << " buildSeq=" << buildSeqExport - << " alignedParticipants=" - << exportState.alignedParticipants() - << " quorum=" << exportQuorum - << " peersSeen=" << exportState.peersSeen - << " txConverged=" << exportState.txConverged; - acceptedExportSigHash = true; - } - else if (quorumAligned() && !exportState.fullObservation()) - { - JLOG(ext.j_.info()) - << "Export: missing exportSigSetHash observation " - "ignored" - << " reason=quorum-aligned" - << " buildSeq=" << buildSeqExport - << " alignedParticipants=" - << exportState.alignedParticipants() - << " quorum=" << exportQuorum - << " peersSeen=" << exportState.peersSeen - << " txConverged=" << exportState.txConverged; - acceptedExportSigHash = true; - } - //@@end export-no-veto-quorum-branches - else if (exportState.conflict || !quorumAligned()) - { - auto const elapsed = - ctx.nowSteady - ext.exportSigGateStart_; - auto const deadline = - detail::sidecarConvergenceTimeout(ctx.parms); - if (elapsed <= deadline) - { - JLOG(ext.j_.debug()) - << "Export: waiting for exportSigSet quorum " - "alignment" - << " buildSeq=" << buildSeqExport - << " alignedParticipants=" - << exportState.alignedParticipants() - << " quorum=" << exportQuorum - << " peersSeen=" << exportState.peersSeen - << " txConverged=" << exportState.txConverged - << " conflict=" - << (exportState.conflict ? "yes" : "no") - << " elapsedMs=" << toMs(elapsed) - << " deadlineMs=" << toMs(deadline); - return {}; - } - - ext.setExportSigConvergenceFailed(); - ext.clearAcceptedExportSigSet(); - JLOG(ext.j_.warn()) - << "Export: exportSigSet quorum alignment timeout" - << " buildSeq=" << buildSeqExport - << " action=retry-or-expire" - << " alignedParticipants=" - << exportState.alignedParticipants() - << " quorum=" << exportQuorum - << " peersSeen=" << exportState.peersSeen - << " txConverged=" << exportState.txConverged - << " conflict=" << (exportState.conflict ? "yes" : "no") - << " 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 - } - } - return {.readyForAccept = true}; }