diff --git a/src/test/consensus/ConsensusExtensions_test.cpp b/src/test/consensus/ConsensusExtensions_test.cpp index b7e0521dd..c09d8792d 100644 --- a/src/test/consensus/ConsensusExtensions_test.cpp +++ b/src/test/consensus/ConsensusExtensions_test.cpp @@ -331,7 +331,6 @@ struct FakeExtensions EstablishState estState_{EstablishState::ConvergingTx}; std::chrono::steady_clock::time_point revealPhaseStart_{}; std::chrono::steady_clock::time_point commitHashConflictStart_{}; - bool explicitFinalProposalSent_{false}; bool entropySetPublished_{false}; std::chrono::steady_clock::time_point entropyPublishStart_{}; bool exportSigGateStarted_{false}; @@ -348,14 +347,12 @@ struct FakeExtensions bool commitQuorum{true}; bool minimumReveals{true}; bool anyReveals{true}; - bool sendExplicitFinal{false}; 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; - std::optional explicitFinalTxSet; std::vector fetchedExportSets; std::vector fetchedEntropySets; std::vector fetchedCommitSets; @@ -504,18 +501,6 @@ struct FakeExtensions fetchedExportSets.push_back(*hash); } - bool - shouldSendExplicitFinalProposal() const - { - return sendExplicitFinal; - } - - std::optional - buildExplicitFinalProposalTxSet(FakeTxSet const&, LedgerIndex) - { - return explicitFinalTxSet; - } - bool hasPendingExportSigs() const { @@ -1030,140 +1015,6 @@ class ConsensusExtensions_test : public beast::unit_test::suite } } - void - testExplicitFinalProposalTxSetBuildsEntropyTxn() - { - testcase("explicit final proposal tx set builds entropy txn"); - - auto extractSingleEntropyTx = - [](RCLTxSet const& set) -> std::shared_ptr { - std::vector> txs; - set.map_->visitLeaves( - [&](boost::intrusive_ptr const& item) { - SerialIter sit(item->slice()); - txs.push_back(std::make_shared(sit)); - }); - if (txs.size() != 1 || - txs.front()->getTxnType() != ttCONSENSUS_ENTROPY) - return {}; - return txs.front(); - }; - - using namespace jtx; - Env env{ - *this, envconfig(validator, ""), supported_amendments(), nullptr}; - ConsensusExtensions ce{env.app(), activeNoopJournal()}; - auto const base = makeRCLTxSet(env.app(), {}); - auto const seq = env.closed()->seq() + 1; - - auto synthetic = ce.buildExplicitFinalProposalTxSet(base, seq); - BEAST_EXPECT(synthetic); - if (!synthetic) - return; - - auto const txPtr = extractSingleEntropyTx(*synthetic); - BEAST_EXPECT(txPtr); - if (!txPtr) - return; - auto const& tx = *txPtr; - BEAST_EXPECT(tx.getFieldU32(sfLedgerSequence) == seq); - BEAST_EXPECT( - tx.getFieldH256(sfDigest) == - sha512Half(std::string("standalone-entropy"), seq)); - BEAST_EXPECT(tx.getFieldU16(sfEntropyCount) == 20); - BEAST_EXPECT( - tx.getFieldU8(sfEntropyTier) == entropyTierValidatorQuorum); - - auto duplicate = ce.buildExplicitFinalProposalTxSet(*synthetic, seq); - BEAST_EXPECT(duplicate); - if (duplicate) - BEAST_EXPECT(duplicate->id() == synthetic->id()); - - Env nonStandaloneEnv{ - *this, - envconfig(validator, ""), - supported_amendments() | featureConsensusEntropy, - nullptr}; - forceNonStandalone(nonStandaloneEnv.app()); - auto const ledger = - nonStandaloneEnv.app().getLedgerMaster().getClosedLedger(); - auto const nonStandaloneBase = makeRCLTxSet(nonStandaloneEnv.app(), {}); - auto const nonStandaloneSeq = ledger->seq() + 1; - - ConsensusExtensions zeroCe{nonStandaloneEnv.app(), activeNoopJournal()}; - zeroCe.onRoundStart(RCLCxLedger{ledger}, {}); - zeroCe.setEntropyFailed(); - auto zeroSynthetic = zeroCe.buildExplicitFinalProposalTxSet( - nonStandaloneBase, nonStandaloneSeq); - BEAST_EXPECT(zeroSynthetic); - auto const zeroTx = - zeroSynthetic ? extractSingleEntropyTx(*zeroSynthetic) : nullptr; - BEAST_EXPECT(zeroTx); - if (zeroTx) - { - // Tier 1 consensus_fallback digest over (prevLedgerHash, base set, - // seq). - auto const expectedFallback = sha512Half( - HashPrefix::entropyFallback, - ledger->info().hash, - nonStandaloneBase.id(), - nonStandaloneSeq); - BEAST_EXPECT(zeroTx->getFieldH256(sfDigest) == expectedFallback); - BEAST_EXPECT(zeroTx->getFieldH256(sfDigest) != uint256{}); - BEAST_EXPECT(zeroTx->getFieldU16(sfEntropyCount) == 0); - BEAST_EXPECT( - zeroTx->getFieldU8(sfEntropyTier) == - entropyTierConsensusFallback); - } - - auto const& valKeys = nonStandaloneEnv.app().getValidatorKeys(); - BEAST_EXPECT(valKeys.keys); - if (!valKeys.keys) - return; - - auto const& publicKey = valKeys.keys->publicKey; - auto const& secretKey = valKeys.keys->secretKey; - auto const nodeId = valKeys.nodeID; - auto const prevLedger = ledger->info().hash; - auto const closeTime = NetClock::time_point{NetClock::duration{654}}; - auto const txSetHash = makeHash("explicit-final-nonzero-txset"); - auto const reveal = makeHash("explicit-final-nonzero-reveal"); - auto const viewLedger = makeUNLReportLedger( - nonStandaloneEnv, std::vector{publicKey}); - ConsensusExtensions revealCe{ - nonStandaloneEnv.app(), activeNoopJournal()}; - revealCe.cacheUNLReport(viewLedger); - harvestCommitReveal( - revealCe, - nodeId, - publicKey, - secretKey, - txSetHash, - nonStandaloneSeq, - closeTime, - prevLedger, - reveal); - revealCe.buildEntropySet(nonStandaloneSeq); - - auto revealSynthetic = revealCe.buildExplicitFinalProposalTxSet( - nonStandaloneBase, nonStandaloneSeq); - BEAST_EXPECT(revealSynthetic); - auto const revealTx = revealSynthetic - ? extractSingleEntropyTx(*revealSynthetic) - : nullptr; - BEAST_EXPECT(revealTx); - if (revealTx) - { - BEAST_EXPECT( - revealTx->getFieldH256(sfDigest) == - expectedEntropy(publicKey, reveal)); - BEAST_EXPECT(revealTx->getFieldU16(sfEntropyCount) == 1); - BEAST_EXPECT( - revealTx->getFieldU8(sfEntropyTier) == - entropyTierValidatorQuorum); - } - } - void testRuntimeConfigPolicyAccessors() { @@ -1175,20 +1026,15 @@ class ConsensusExtensions_test : public beast::unit_test::suite ConsensusExtensions ce{env.app(), activeNoopJournal()}; BEAST_EXPECT(!ce.bootstrapFastStartEnabled()); - BEAST_EXPECT(!ce.shouldSendExplicitFinalProposal()); ConfigVals cfg; cfg.bootstrapFastStart = true; - cfg.explicitFinalProposal = true; env.app().getRuntimeConfig().setConfig("*", cfg); BEAST_EXPECT(ce.bootstrapFastStartEnabled()); - BEAST_EXPECT(ce.shouldSendExplicitFinalProposal()); cfg.bootstrapFastStart = false; - cfg.explicitFinalProposal = false; env.app().getRuntimeConfig().setConfig("*", cfg); BEAST_EXPECT(!ce.bootstrapFastStartEnabled()); - BEAST_EXPECT(!ce.shouldSendExplicitFinalProposal()); } void @@ -2684,38 +2530,6 @@ class ConsensusExtensions_test : public beast::unit_test::suite BEAST_EXPECT(ext.fetchedEntropySets.size() == 1); } - void - testRngExplicitFinalProposalPublishesSyntheticTxSet() - { - testcase("RNG explicit final proposal publishes synthetic tx set"); - - FakeExtensions ext; - ext.rngOn = true; - ext.exportOn = false; - ext.estState_ = EstablishState::ConvergingReveal; - ext.sendExplicitFinal = true; - ext.explicitFinalTxSet = FakeTxSet{makeHash("explicit-final-tx-set")}; - - ExportTickHarness harness; - auto const localHash = ext.entropyHash; - harness.position.entropySetHash = localHash; - ext.entropySetPublished_ = true; - ext.entropyPublishStart_ = harness.start; - harness.addEntropyPeer(1, localHash); - harness.addEntropyPeer(2, localHash); - harness.addEntropyPeer(3, localHash); - harness.addEntropyPeer(4, localHash); - - auto result = harness.tick(ext, std::chrono::milliseconds{100}); - BEAST_EXPECT(result.readyForAccept); - BEAST_EXPECT(ext.explicitFinalProposalSent_); - BEAST_EXPECT( - harness.position.txSetHash == ext.explicitFinalTxSet->hash); - BEAST_EXPECT(harness.position.entropySetHash == localHash); - BEAST_EXPECT(harness.updates == 1); - BEAST_EXPECT(harness.proposes == 1); - } - void testExportSigGateAllowsAlignedQuorumDespiteMinorityConflict() { @@ -3177,7 +2991,6 @@ public: testActiveValidatorViewAppliesNegativeUNL(); testActiveValidatorViewNullSourceAndExpectedProposers(); testParticipantThreshold(); - testExplicitFinalProposalTxSetBuildsEntropyTxn(); testRuntimeConfigPolicyAccessors(); testDecoratePositionGeneratesCommitment(); testOnPreBuildInjectsZeroEntropyFallback(); @@ -3210,7 +3023,6 @@ public: testRngEntropyConflictTimeoutClearsHash(); testRngEntropyConflictRefreshesHashBeforeWaiting(); testRngEntropyConflictIgnoredWithQuorumAlignment(); - testRngExplicitFinalProposalPublishesSyntheticTxSet(); testExportSigGateAllowsAlignedQuorumDespiteMinorityConflict(); testExportSigGateAllowsQuorumDespiteMissingObservation(); testExportSigGateFetchesAdvertisedPeerSets(); diff --git a/src/test/csf/Peer.h b/src/test/csf/Peer.h index 1cdfbecd6..22bbf3979 100644 --- a/src/test/csf/Peer.h +++ b/src/test/csf/Peer.h @@ -325,7 +325,6 @@ struct Peer EstablishState estState_{EstablishState::ConvergingTx}; std::chrono::steady_clock::time_point revealPhaseStart_{}; std::chrono::steady_clock::time_point commitHashConflictStart_{}; - bool explicitFinalProposalSent_{false}; bool entropySetPublished_{false}; std::chrono::steady_clock::time_point entropyPublishStart_{}; bool exportSigGateStarted_{false}; @@ -965,16 +964,6 @@ struct Peer return bootstrapFastStartEnabled_; } bool - shouldSendExplicitFinalProposal() const - { - return false; - } - std::optional - buildExplicitFinalProposalTxSet(TxSet const&, Ledger::Seq) - { - return std::nullopt; - } - bool hasPendingExportSigs() const { return enableExportConsensus_ && !pendingExportSigs_.empty(); @@ -1010,7 +999,6 @@ struct Peer estState_ = EstablishState::ConvergingTx; revealPhaseStart_ = {}; commitHashConflictStart_ = {}; - explicitFinalProposalSent_ = false; entropySetPublished_ = false; entropyPublishStart_ = {}; exportSigGateStarted_ = false; diff --git a/src/test/rpc/RuntimeConfig_test.cpp b/src/test/rpc/RuntimeConfig_test.cpp index 07f97f29d..5b03fd5c7 100644 --- a/src/test/rpc/RuntimeConfig_test.cpp +++ b/src/test/rpc/RuntimeConfig_test.cpp @@ -110,7 +110,6 @@ class RuntimeConfig_test : public beast::unit_test::suite ConfigVals peer; peer.sendDelayMs = 500; peer.sendDelayJitterMs = 25; - peer.explicitFinalProposal = true; peer.rngPollMs = 75; peer.noExportSig = true; peer.messageCategories = std::set{}; @@ -122,8 +121,6 @@ class RuntimeConfig_test : public beast::unit_test::suite BEAST_EXPECT(merged.rngClaimDropPctX100 == 750); BEAST_EXPECT(merged.bootstrapFastStart.has_value()); BEAST_EXPECT(*merged.bootstrapFastStart == false); - BEAST_EXPECT(merged.explicitFinalProposal.has_value()); - BEAST_EXPECT(*merged.explicitFinalProposal == true); BEAST_EXPECT(merged.rngPollMs == 75); BEAST_EXPECT(merged.noExportSig.has_value()); BEAST_EXPECT(*merged.noExportSig == true); @@ -138,7 +135,7 @@ class RuntimeConfig_test : public beast::unit_test::suite inactive.sendDropPctX100 = 0; inactive.rngClaimDropPctX100 = 0; BEAST_EXPECT(!inactive.active()); - inactive.explicitFinalProposal = false; + inactive.bootstrapFastStart = false; BEAST_EXPECT(inactive.active()); } @@ -304,7 +301,6 @@ class RuntimeConfig_test : public beast::unit_test::suite EnvVarGuard jitter{"XAHAU_SEND_DELAY_JITTER_MS", "3"}; EnvVarGuard drop{"XAHAU_SEND_DROP_PCT", "4.5"}; EnvVarGuard rngDrop{"XAHAU_RNG_CLAIM_DROP_PCT", "6.25"}; - EnvVarGuard explicitFinal{"XAHAUD_EXPLICIT_FINAL_PROPOSAL", "off"}; EnvVarGuard bootstrap{"XAHAUD_BOOTSTRAP_FAST_START", "yes"}; EnvVarGuard rngPoll{"XAHAU_RNG_POLL_MS", "5"}; EnvVarGuard noExportSig{"XAHAUD_NO_EXPORT_SIG", "0"}; @@ -319,8 +315,6 @@ class RuntimeConfig_test : public beast::unit_test::suite BEAST_EXPECT(cfg->sendDelayJitterMs == 3); BEAST_EXPECT(cfg->sendDropPctX100 == 450); BEAST_EXPECT(cfg->rngClaimDropPctX100 == 625); - BEAST_EXPECT(cfg->explicitFinalProposal.has_value()); - BEAST_EXPECT(*cfg->explicitFinalProposal == false); BEAST_EXPECT(cfg->bootstrapFastStart.has_value()); BEAST_EXPECT(*cfg->bootstrapFastStart == true); BEAST_EXPECT(cfg->rngPollMs == 50); @@ -337,12 +331,12 @@ class RuntimeConfig_test : public beast::unit_test::suite "XAHAU_RUNTIME_CONFIG", R"({"*":{"send_delay_ms":100,"send_delay_jitter_ms":20,)" R"("send_drop_pct":1.25,"rng_claim_drop_pct":3.5,)" - R"("explicit_final_proposal":true,"bootstrap_fast_start":false,)" + R"("bootstrap_fast_start":false,)" R"("rng_poll_ms":5,"no_export_sig":true,)" R"("message_types":["proposal"]},)" R"("10.0.0.5:51235":{"send_delay_ms":200,)" R"("send_drop_pct":2.5,"rng_claim_drop_pct":4.5,)" - R"("explicit_final_proposal":false,"bootstrap_fast_start":true,)" + R"("bootstrap_fast_start":true,)" R"("rng_poll_ms":125,"no_export_sig":false,)" R"("message_types":[]}})"}; @@ -354,8 +348,6 @@ class RuntimeConfig_test : public beast::unit_test::suite BEAST_EXPECT(global->sendDelayJitterMs == 20); BEAST_EXPECT(global->sendDropPctX100 == 125); BEAST_EXPECT(global->rngClaimDropPctX100 == 350); - BEAST_EXPECT(global->explicitFinalProposal.has_value()); - BEAST_EXPECT(*global->explicitFinalProposal == true); BEAST_EXPECT(global->bootstrapFastStart.has_value()); BEAST_EXPECT(*global->bootstrapFastStart == false); BEAST_EXPECT(global->rngPollMs == 50); @@ -371,8 +363,6 @@ class RuntimeConfig_test : public beast::unit_test::suite BEAST_EXPECT(peer->sendDelayJitterMs == 20); BEAST_EXPECT(peer->sendDropPctX100 == 250); BEAST_EXPECT(peer->rngClaimDropPctX100 == 450); - BEAST_EXPECT(peer->explicitFinalProposal.has_value()); - BEAST_EXPECT(*peer->explicitFinalProposal == false); BEAST_EXPECT(peer->bootstrapFastStart.has_value()); BEAST_EXPECT(*peer->bootstrapFastStart == true); BEAST_EXPECT(peer->rngPollMs == 125); @@ -882,54 +872,6 @@ class RuntimeConfig_test : public beast::unit_test::suite } } - void - testExplicitFinalProposalToggle() - { - testcase("explicit_final_proposal round-trips and merges"); - using namespace test::jtx; - Env env{*this}; - - // Global default for this node: skip explicit final proposal. - { - Json::Value params; - params["set"] = Json::objectValue; - params["set"]["*"] = Json::objectValue; - params["set"]["*"]["explicit_final_proposal"] = false; - auto result = runtimeConfig(env, params); - - auto const& global = result["configs"]["*"]; - BEAST_EXPECT(global["explicit_final_proposal"].asBool() == false); - } - - auto& rc = env.app().getRuntimeConfig(); - BEAST_EXPECT(rc.active()); - - // Global view is false. - auto globalCfg = rc.getConfig("*"); - BEAST_EXPECT(globalCfg.has_value()); - BEAST_EXPECT(globalCfg->explicitFinalProposal.has_value()); - BEAST_EXPECT(*globalCfg->explicitFinalProposal == false); - - // Per-peer override can re-enable. - { - Json::Value params; - params["set"] = Json::objectValue; - params["set"]["10.0.0.2:51235"] = Json::objectValue; - params["set"]["10.0.0.2:51235"]["explicit_final_proposal"] = true; - runtimeConfig(env, params); - } - - auto peerCfg = rc.getConfig("10.0.0.2:51235"); - BEAST_EXPECT(peerCfg.has_value()); - BEAST_EXPECT(peerCfg->explicitFinalProposal.has_value()); - BEAST_EXPECT(*peerCfg->explicitFinalProposal == true); - - auto otherCfg = rc.getConfig("10.0.0.3:51235"); - BEAST_EXPECT(otherCfg.has_value()); - BEAST_EXPECT(otherCfg->explicitFinalProposal.has_value()); - BEAST_EXPECT(*otherCfg->explicitFinalProposal == false); - } - void testPerPeerClearInheritedFilter() { @@ -1001,7 +943,6 @@ public: testRngClaimDropPct(); testRngClaimDropPctClamping(); testRngAndExportRuntimeToggles(); - testExplicitFinalProposalToggle(); testPerPeerClearInheritedFilter(); } }; diff --git a/src/xrpld/app/consensus/ConsensusExtensions.cpp b/src/xrpld/app/consensus/ConsensusExtensions.cpp index 717ab20cb..dc1dbdcc8 100644 --- a/src/xrpld/app/consensus/ConsensusExtensions.cpp +++ b/src/xrpld/app/consensus/ConsensusExtensions.cpp @@ -554,122 +554,6 @@ ConsensusExtensions::bootstrapFastStartEnabled() const return false; } -bool -ConsensusExtensions::shouldSendExplicitFinalProposal() const -{ - // Explicit-final-proposal policy is node-local and experimental. - // - // Default behavior is implicit finalization (no extra seq=4 proposal): - // entropy pseudo-tx is injected in onAccept/buildLCL. - // - // We only enable explicit-final when operators intentionally opt in via - // runtime config/env for measurement/diagnostics. - // - // TODO: remove the explicit-final proposal path. The implicit accept-time - // injection path is the consensus path; explicit-final never found a robust - // timing model and still carries separate experimental-only alignment - // hazards. Do not promote this by just widening the gates. - auto const cfg = app_.getRuntimeConfig().getConfig("*"); - if (cfg && cfg->explicitFinalProposal.has_value()) - return *cfg->explicitFinalProposal; - return false; -} - -std::optional -ConsensusExtensions::buildExplicitFinalProposalTxSet( - RCLTxSet const& txns, - LedgerIndex seq) -{ - JLOG(j_.debug()) << "RNGFINAL: build synthetic txSet" - << " baseTxSet=" << txns.id() << " seq=" << seq - << " commits=" << pendingCommits_.size() - << " reveals=" << pendingReveals_.size() - << " entropyFailed=" << (entropyFailed_ ? "yes" : "no"); - - // Shared deterministic selector over the AGREED entropySetMap_ — the same - // one onPreBuild uses — NOT local pendingReveals_, which can diverge from - // the agreed set at timeout boundaries. Routing explicit-final - // (experimental, default-off) through it keeps this path byte-identical to - // the implicit one. txns.id() is the BASE tx set hash for the fallback. - // - // TODO: delete this with the explicit-final proposal path; keep this helper - // only while the runtime-config experiment still exists. - auto const selection = selectEntropy(txns.id(), seq); - uint256 const finalEntropy = selection.digest; - std::uint8_t const entropyTier = selection.tier; - std::uint16_t const entropyCount = selection.count; - - JLOG(j_.debug()) << "RNGFINAL: entropy selected" - << " seq=" << seq - << " tier=" << static_cast(entropyTier) - << " count=" << entropyCount << " digest=" << finalEntropy - << " baseTxSet=" << txns.id(); - - STTx tx(ttCONSENSUS_ENTROPY, [&](auto& obj) { - obj.setFieldU32(sfLedgerSequence, seq); - obj.setAccountID(sfAccount, AccountID{}); - obj.setFieldU32(sfSequence, 0); - obj.setFieldAmount(sfFee, STAmount{}); - obj.setFieldH256(sfDigest, finalEntropy); - obj.setFieldU16(sfEntropyCount, entropyCount); - obj.setFieldU8(sfEntropyTier, entropyTier); - }); - - auto const txID = tx.getTransactionID(); - // Value-based dedup (mirrors onPreBuild): there must never be two entropy - // pseudo-txs. If one is already in the base set it must be the EXACT - // pseudo-tx we would have produced (injection is deterministic, so the same - // agreed inputs yield an identical txID); a present-but-different one is a - // determinism violation to surface, not silently accept. Either way return - // the base unchanged — explicit-final is best-effort and must not rewrite - // an already-committed set. - std::optional presentID; - txns.map_->visitLeaves( - [&](boost::intrusive_ptr const& item) { - if (presentID) - return; - try - { - SerialIter sit(item->slice()); - STTx const parsed{sit}; - if (parsed.getTxnType() == ttCONSENSUS_ENTROPY) - presentID = parsed.getTransactionID(); - } - catch (...) - { - } - }); - if (presentID) - { - if (*presentID == txID) - JLOG(j_.debug()) - << "RNGFINAL: entropy pseudo-tx already in base txSet" - << " txHash=" << txID << " baseTxSet=" << txns.id() - << " action=skip-duplicate-verified"; - else - JLOG(j_.error()) - << "RNGFINAL: entropy pseudo-tx MISMATCH in base txSet" - << " reason=determinism-violation action=keep-base" - << " ourTxHash=" << txID << " presentTxHash=" << *presentID - << " baseTxSet=" << txns.id(); - return txns; - } - - RCLTxSet::MutableTxSet mutableTxSet{txns}; - Serializer ser(512); - tx.add(ser); - mutableTxSet.insert(RCLCxTx{make_shamapitem(txID, ser.slice())}); - auto syntheticSet = RCLTxSet{mutableTxSet}; - auto const hash = syntheticSet.id(); - app_.getInboundTransactions().giveSet(hash, syntheticSet.map_, false); - - JLOG(j_.debug()) << "RNGFINAL: built synthetic txSet" - << " syntheticTxSet=" << hash << " baseTxSet=" << txns.id() - << " txHash=" << txID << " entropyCount=" << entropyCount; - - return syntheticSet; -} - uint256 ConsensusExtensions::buildCommitSet(LedgerIndex seq) { @@ -1866,11 +1750,9 @@ ConsensusExtensions::onPreBuild( //@@start rng-inject-entropy-selection // One deterministic selector over the AGREED entropySetMap_ chooses the - // digest and its tier/count. onPreBuild and buildExplicitFinalProposalTxSet - // share it, so neither the implicit vs explicit-final paths on one node nor - // two different nodes can derive different entropy for the same agreed - // round inputs. txSetHash is the BASE (pre-injection) consensus tx set - // hash. + // digest and its tier/count. Every node derives the same entropy for the + // same agreed round inputs. txSetHash is the BASE (pre-injection) + // consensus tx set hash. auto const selection = selectEntropy(txSetHash, seq); uint256 const finalEntropy = selection.digest; std::uint8_t const entropyTier = selection.tier; @@ -1898,11 +1780,6 @@ ConsensusExtensions::onPreBuild( // agree on the base transaction set first, then deterministically // derive/apply the entropy pseudo-tx for ledger construction. // - // Explicit-final (seq=4 synthetic proposal) remains an optional - // experiment for observability/perf testing and is default-off. - // TBD (2026-03-03): revisit only with stronger evidence that explicit - // publication can be made stable under tx-bearing, lossy networks. - //@@start rng-inject-pseudotx-core // Account Zero convention for pseudo-transactions (same as ttFEE, etc) STTx tx(ttCONSENSUS_ENTROPY, [&](auto& obj) { @@ -1917,13 +1794,13 @@ ConsensusExtensions::onPreBuild( auto const txID = tx.getTransactionID(); // Value-based dedup. There must never be two entropy pseudo-txs, but - // when one is already present (explicit-final, or a peer's agreed - // set) it must be VALIDATED as the exact pseudo-tx we would have - // produced — not merely "same type". Injection is deterministic, so - // every honest node derives the identical pseudo-tx (identical txID) - // for the same agreed inputs. A present-but-different entropy pseudo-tx - // is therefore a determinism violation (version skew or a divergent/ - // malicious peer) and must be surfaced, not silently trusted. + // when one is already present in the agreed set it must be VALIDATED as + // the exact pseudo-tx we would have produced — not merely "same type". + // Injection is deterministic, so every honest node derives the + // identical pseudo-tx (identical txID) for the same agreed inputs. A + // present-but-different entropy pseudo-tx is therefore a determinism + // violation (version skew or a divergent/malicious peer) and must be + // surfaced, not silently trusted. auto const existing = std::find_if( retriableTxs.begin(), retriableTxs.end(), [](auto const& entry) { return entry.second->getTxnType() == ttCONSENSUS_ENTROPY; diff --git a/src/xrpld/app/consensus/ConsensusExtensions.h b/src/xrpld/app/consensus/ConsensusExtensions.h index a77fdd5ce..1e2ad2fb7 100644 --- a/src/xrpld/app/consensus/ConsensusExtensions.h +++ b/src/xrpld/app/consensus/ConsensusExtensions.h @@ -101,7 +101,6 @@ public: EstablishState estState_{EstablishState::ConvergingTx}; std::chrono::steady_clock::time_point revealPhaseStart_{}; std::chrono::steady_clock::time_point commitHashConflictStart_{}; - bool explicitFinalProposalSent_{false}; bool entropySetPublished_{false}; std::chrono::steady_clock::time_point entropyPublishStart_{}; bool exportSigGateStarted_{false}; @@ -226,12 +225,6 @@ public: bool bootstrapFastStartEnabled() const; - bool - shouldSendExplicitFinalProposal() const; - - std::optional - buildExplicitFinalProposalTxSet(RCLTxSet const& txns, LedgerIndex seq); - uint256 buildCommitSet(LedgerIndex seq); @@ -502,7 +495,6 @@ public: estState_ = EstablishState::ConvergingTx; revealPhaseStart_ = {}; commitHashConflictStart_ = {}; - explicitFinalProposalSent_ = false; entropySetPublished_ = false; entropyPublishStart_ = {}; exportSigGateStarted_ = false; diff --git a/src/xrpld/app/consensus/ConsensusExtensionsDesign.md b/src/xrpld/app/consensus/ConsensusExtensionsDesign.md index bf6e311f3..1f70644d6 100644 --- a/src/xrpld/app/consensus/ConsensusExtensionsDesign.md +++ b/src/xrpld/app/consensus/ConsensusExtensionsDesign.md @@ -210,12 +210,6 @@ If no entropy hash reaches the entropy gate threshold before the bounded deadline, the round must fall back to the Tier 1 consensus-bound digest. This is the safe degradation path, not a consensus failure. -> Known exception: the experimental, default-off explicit-final proposal path -> counts alignment over the unfiltered proposer set (not the active view). It is -> flagged in-code as an outstanding F1 gap, but the intended disposition is -> removal rather than repair/promotion; it must not be enabled as a production -> consensus path. - Examples with six active validators on a UNLReport-anchored view (validator_quorum threshold five, participant_aligned threshold four; six is the smallest view with a non-empty Tier 2 band and non-zero tolerated Byzantine count; at five validators @@ -253,10 +247,10 @@ after it updates the SLE. Hooks that need final entropy must treat open-ledger RNG results as previews. The fallback digest derives from the BASE (pre-injection) tx set hash to avoid -circularity, and entropy pseudo-tx deduplication is value-based: if an -explicit-final synthetic set already contains the exact pseudo-tx, injection -skips it; a present-but-different pseudo-tx is logged as a determinism -violation and left in the agreed set. +circularity, and entropy pseudo-tx deduplication is value-based: if the agreed +set already contains the exact pseudo-tx, injection skips it; a +present-but-different pseudo-tx is logged as a determinism violation and left in +the agreed set. ## Sidecar Convergence Rules diff --git a/src/xrpld/app/misc/RuntimeConfig.h b/src/xrpld/app/misc/RuntimeConfig.h index 9543aa4d5..64eca9324 100644 --- a/src/xrpld/app/misc/RuntimeConfig.h +++ b/src/xrpld/app/misc/RuntimeConfig.h @@ -41,13 +41,6 @@ struct ConfigVals std::optional sendDelayJitterMs; std::optional sendDropPctX100; // 0-10000 (pct * 100, avoids float) std::optional rngClaimDropPctX100; // 0-10000 (pct * 100) - // Controls explicit final proposal broadcast in the RNG reveal phase. - // true = attempt explicit-final proposal (experimental) - // false = keep implicit mode (recommended default for production) - // - // NOTE: This knob is intentionally explicit opt-in. The consensus system - // is fully functional without it via accept-time pseudo-tx injection. - std::optional explicitFinalProposal; // Bootstrap fast start: seed prevRoundTime_ to 3s instead of 15s on first // round, auto-disables after stable quorum is observed. std::optional bootstrapFastStart; @@ -76,7 +69,6 @@ struct ConfigVals (sendDelayJitterMs && *sendDelayJitterMs > 0) || (sendDropPctX100 && *sendDropPctX100 > 0) || (rngClaimDropPctX100 && *rngClaimDropPctX100 > 0) || - explicitFinalProposal.has_value() || bootstrapFastStart.has_value() || rngPollMs.has_value() || noExportSig.has_value(); } @@ -94,8 +86,6 @@ struct ConfigVals result.sendDropPctX100 = other.sendDropPctX100; if (other.rngClaimDropPctX100) result.rngClaimDropPctX100 = other.rngClaimDropPctX100; - if (other.explicitFinalProposal.has_value()) - result.explicitFinalProposal = other.explicitFinalProposal; if (other.bootstrapFastStart.has_value()) result.bootstrapFastStart = other.bootstrapFastStart; if (other.rngPollMs) diff --git a/src/xrpld/app/misc/detail/RuntimeConfig.cpp b/src/xrpld/app/misc/detail/RuntimeConfig.cpp index 60b0db8a7..309bdb165 100644 --- a/src/xrpld/app/misc/detail/RuntimeConfig.cpp +++ b/src/xrpld/app/misc/detail/RuntimeConfig.cpp @@ -257,8 +257,6 @@ parseConfigVals(Json::Value const& v) if (v.isMember("rng_claim_drop_pct")) cfg.rngClaimDropPctX100 = static_cast(v["rng_claim_drop_pct"].asDouble() * 100); - if (v.isMember("explicit_final_proposal")) - cfg.explicitFinalProposal = v["explicit_final_proposal"].asBool(); if (v.isMember("bootstrap_fast_start")) cfg.bootstrapFastStart = v["bootstrap_fast_start"].asBool(); if (v.isMember("rng_poll_ms")) @@ -346,11 +344,6 @@ RuntimeConfig::RuntimeConfig() global.sendDropPctX100 = static_cast(std::atof(env) * 100); if (auto const* env = std::getenv("XAHAU_RNG_CLAIM_DROP_PCT")) global.rngClaimDropPctX100 = static_cast(std::atof(env) * 100); - // Explicit-final proposal is intentionally opt-in and defaults to - // implicit behavior when unset. - if (auto parsed = - parseBoolEnv(std::getenv("XAHAUD_EXPLICIT_FINAL_PROPOSAL"))) - global.explicitFinalProposal = *parsed; if (auto parsed = parseBoolEnv(std::getenv("XAHAUD_BOOTSTRAP_FAST_START"))) global.bootstrapFastStart = *parsed; if (auto const* env = std::getenv("XAHAU_RNG_POLL_MS")) diff --git a/src/xrpld/consensus/ConsensusExtensionsTick.h b/src/xrpld/consensus/ConsensusExtensionsTick.h index 0a9faefb6..2c784190b 100644 --- a/src/xrpld/consensus/ConsensusExtensionsTick.h +++ b/src/xrpld/consensus/ConsensusExtensionsTick.h @@ -188,8 +188,6 @@ extensionsTick(Ext& ext, Ctx const& ctx) << " participants=" << participants << " peerPositions=" << ctx.peerPositions.size() << " prevProposers=" << ctx.prevProposers - << " explicitFinalSent=" - << (ext.explicitFinalProposalSent_ ? "yes" : "no") << " closeTimeConsensus=" << (ctx.haveCloseTimeConsensus ? "yes" : "no") << " txSet=" << ourPos; @@ -906,193 +904,6 @@ extensionsTick(Ext& ext, Ctx const& ctx) << (entropyState.conflict ? "yes" : "no"); } } - - // Optional explicit final proposal (seq=4 style): - // publish a synthetic tx-set hash that includes the - // consensus-entropy pseudo-tx just before accept. - // - // IMPORTANT DESIGN NOTE (read before editing this block): - // - // This path is intentionally OPTIONAL and default-off. It - // exists for diagnostics/perf experiments (for example, making - // monitor visibility of the final pseudo-tx set more direct), - // NOT as a required step for consensus correctness. - // - // Why so conservative? - // - The main consensus engine still keys agreement on tx-set - // hash. - // - Updating our tx-set hash here creates a "late identity - // change" in establish. - // - Under lossy/reordered networks, peers can be slightly out - // of - // phase: some nodes may have switched to the synthetic hash - // while others are still on the base hash. - // - That can fragment agreement during a critical window (two - // hashes in flight for one ledger), increase proposal - // chatter, and trigger sync churn. - // - // Therefore this logic must remain best-effort only: - // - Never required for liveness/safety. - // - No extra wait tick is introduced. - // - If gates are not met, we skip and continue to accept via - // the - // normal implicit path (accept-time pseudo-tx injection). - // - // TODO: remove this explicit-final proposal path. We did not find - // a robust timing model that folds it into a guaranteed-safe - // explicit final proposal across lossy/reordered links without - // increasing churn. It remains opt-in only until deleted. - { - bool fullParticipantCoverage = false; - bool entropyAligned = false; - { - // Guard against "early switch" churn: - // require at least as many participants as the previous - // round before attempting the explicit-final mutation. - // - // This is a heuristic to reduce risk, not a proof of - // safety. We still keep the feature - // optional/default-off. - // - // OUTSTANDING (F1): unlike the main entropy gate - // (inspectTxConvergedSidecarPeers), this explicit-final - // participant/alignment count is over the UNFILTERED - // trusted-proposer set (ctx.peerPositions) plus an - // unconditional local +1 -- NOT the active validator view. - // A trusted-but-non-active proposer can pad both - // `participants` and `alignedPeers`, inflating the counting - // universe above originalViewSize and eroding the same - // Tier-2 intersection margin that the F1 fix closed for the - // main gate. Before this path is EVER enabled it must - // filter peers through activeValidatorView()->containsNode - // and gate the local +1 on local active-view membership - // (mirror inspectTxConvergedSidecarPeers) if this code - // survives long enough to be touched. Preferred disposition - // is deletion, not promotion. - auto const participants = ctx.peerPositions.size() + 1; - auto const expectedParticipants = ctx.prevProposers + 1; - fullParticipantCoverage = - participants >= expectedParticipants; - // Require a majority aligned on entropySetHash before - // mutating tx-set hash. If this threshold is loosened, - // the probability of hash fragmentation rises quickly. - auto const requiredEntropyAligned = - (expectedParticipants / 2) + 1; - auto const ourPos = ctx.getPosition(); - if (ourPos.entropySetHash) - { - auto const expectedEntropy = *ourPos.entropySetHash; - std::size_t alignedPeers = 0; - bool conflict = false; - for (auto const& [_, peerPos] : ctx.peerPositions) - { - auto const& peerPosition = - peerPos.proposal().position(); - if (!peerPosition.entropySetHash) - continue; - if (*peerPosition.entropySetHash == expectedEntropy) - { - ++alignedPeers; - continue; - } - conflict = true; - break; - } - - auto const alignedParticipants = alignedPeers + 1; - entropyAligned = !conflict && - alignedParticipants >= requiredEntropyAligned; - if (!entropyAligned) - { - JLOG(ext.j_.debug()) - << "RNG: explicit-final entropy alignment " - "insufficient" - << " buildSeq=" << buildSeq - << " alignedParticipants=" - << alignedParticipants - << " required=" << requiredEntropyAligned - << " conflict=" << (conflict ? "yes" : "no"); - } - } - else - { - JLOG(ext.j_.debug()) - << "RNG: explicit-final waiting" - << " reason=missing-local-entropySetHash" - << " buildSeq=" << buildSeq; - } - } - - if (ctx.mode == ConsensusMode::proposing && - !ext.explicitFinalProposalSent_ && - ext.hasQuorumOfCommits() && revealConsensus && - fullParticipantCoverage && entropyAligned && - ext.shouldSendExplicitFinalProposal()) - { - // One-shot per round. This avoids repeated mutations/ - // broadcasts from timer ticks, which can amplify - // network chatter in the exact conditions - // (loss/reordering) where this path is already fragile. - auto const synthSet = ext.buildExplicitFinalProposalTxSet( - ctx.getTxns(), buildSeq); - ext.explicitFinalProposalSent_ = true; - - if (synthSet) - { - auto const synthHash = synthSet->id(); - auto currentPos = ctx.getPosition(); - auto newPos = currentPos; - newPos.updateTxSet(synthHash); - - if (!(newPos == currentPos)) - { - // WARNING: - // This changes proposal tx-set identity late in - // establish. Keep this path tightly gated and - // optional. The canonical ledger path remains - // the implicit accept-time injection logic. - - // Maintain the invariant that our active - // position's tx-set hash is present in - // acquired_, otherwise gotTxSet can assert if - // this set arrives back from the network. - ctx.cacheAndShareTxSet(*synthSet); - JLOG(ext.j_.debug()) - << "RNG: cached explicit-final txSet" - << " buildSeq=" << buildSeq - << " txSet=" << synthHash; - ctx.updatePosition(newPos); - ctx.propose(); - JLOG(ext.j_.debug()) - << "RNG: explicit final proposal" - << " buildSeq=" << buildSeq - << " txSet=" << synthHash; - logRngDiag("rng-explicit-final-proposed"); - } - } - } - else - { - char const* reason = "disabled"; - if (ctx.mode != ConsensusMode::proposing) - reason = "not-proposing"; - else if (ext.explicitFinalProposalSent_) - reason = "already-sent"; - else if (!ext.hasQuorumOfCommits()) - reason = "no-commit-quorum"; - else if (!revealConsensus) - reason = "reveal-timeout"; - else if (!fullParticipantCoverage) - reason = "participant-gap"; - else if (!entropyAligned) - reason = "entropy-not-aligned"; - JLOG(ext.j_.debug()) - << "STALLDIAG: rng-explicit-final-skipped" - << " reason=" << reason << " buildSeq=" << buildSeq - << " mode=" << to_string(ctx.mode) << " sent=" - << (ext.explicitFinalProposalSent_ ? "yes" : "no"); - } - } } } else diff --git a/src/xrpld/rpc/handlers/RuntimeConfig.cpp b/src/xrpld/rpc/handlers/RuntimeConfig.cpp index 54123451e..ec0787ace 100644 --- a/src/xrpld/rpc/handlers/RuntimeConfig.cpp +++ b/src/xrpld/rpc/handlers/RuntimeConfig.cpp @@ -62,9 +62,6 @@ doRuntimeConfig(RPC::JsonContext& context) pct = 100.0; cfg.rngClaimDropPctX100 = static_cast(pct * 100); } - if (v.isMember("explicit_final_proposal")) - cfg.explicitFinalProposal = - v["explicit_final_proposal"].asBool(); if (v.isMember("bootstrap_fast_start")) cfg.bootstrapFastStart = v["bootstrap_fast_start"].asBool(); if (v.isMember("rng_poll_ms")) @@ -131,8 +128,6 @@ doRuntimeConfig(RPC::JsonContext& context) entry["send_drop_pct"] = *cfg.sendDropPctX100 / 100.0; if (cfg.rngClaimDropPctX100) entry["rng_claim_drop_pct"] = *cfg.rngClaimDropPctX100 / 100.0; - if (cfg.explicitFinalProposal.has_value()) - entry["explicit_final_proposal"] = *cfg.explicitFinalProposal; if (cfg.bootstrapFastStart.has_value()) entry["bootstrap_fast_start"] = *cfg.bootstrapFastStart; if (cfg.rngPollMs)