diff --git a/src/test/app/ExportResultBuilder_test.cpp b/src/test/app/ExportResultBuilder_test.cpp index 1a1b9d2af..dc77e22d6 100644 --- a/src/test/app/ExportResultBuilder_test.cpp +++ b/src/test/app/ExportResultBuilder_test.cpp @@ -400,24 +400,33 @@ public: innerTx, signer.first, signer.second)); } + Blob const contributors( + (STTx::maxMultiSigners() + 7) / 8, std::uint8_t{0xFF}); auto witness = ExportResultBuilder::buildSignatureWitness( - exportTxHash, signatures, 654); + exportTxHash, innerTx, signatures, contributors, 654); BEAST_EXPECT(witness.getTxnType() == ttEXPORT_SIGNATURES); BEAST_EXPECT(witness.getFieldU32(sfLedgerSequence) == 654); BEAST_EXPECT(witness.getFieldH256(sfTransactionHash) == exportTxHash); + BEAST_EXPECT(witness.getFieldVL(sfEntropyContributors) == contributors); + BEAST_EXPECT(!witness.isFieldPresent(sfSigners)); + + auto const& exported = + witness.peekAtField(sfExportedTxn).downcast(); + BEAST_EXPECT(exported.getFieldU16(sfTransactionType) == ttPAYMENT); BEAST_EXPECT( - witness.getFieldArray(sfSigners).size() == signatures.size()); + exported.getFieldArray(sfSigners).size() == + STTx::maxMultiSigners()); auto decoded = ExportResultBuilder::signaturesFromWitness(witness); BEAST_EXPECT(decoded); if (decoded) { - BEAST_EXPECT(decoded->size() == signatures.size()); - for (auto const& [pk, sig] : signatures) + BEAST_EXPECT(decoded->size() == STTx::maxMultiSigners()); + for (auto const& [pk, sig] : *decoded) { - auto const it = decoded->find(pk); - BEAST_EXPECT(it != decoded->end()); - if (it != decoded->end()) + auto const it = signatures.find(pk); + BEAST_EXPECT(it != signatures.end()); + if (it != signatures.end()) BEAST_EXPECT(it->second == sig); } } @@ -443,10 +452,11 @@ public: innerTx, signer.first, signer.second)); auto witness = ExportResultBuilder::buildSignatureWitness( - exportTxHash, signatures, 654); - auto signers = witness.getFieldArray(sfSigners); + exportTxHash, innerTx, signatures, Blob{0x01}, 654); + auto& exported = witness.peekFieldObject(sfExportedTxn); + auto signers = exported.getFieldArray(sfSigners); signers[0].setAccountID(sfAccount, calcAccountID(wrongAccount.first)); - witness.setFieldArray(sfSigners, signers); + exported.setFieldArray(sfSigners, signers); BEAST_EXPECT(!ExportResultBuilder::signaturesFromWitness(witness)); } @@ -499,44 +509,40 @@ public: auto const multiSigned = ExportResultBuilder::buildMultiSignedExportedTxn( innerTx, signatures); + Blob const contributors( + (STTx::maxMultiSigners() + 7) / 8, std::uint8_t{0xFF}); auto const witness = ExportResultBuilder::buildSignatureWitness( - makeHash("size-inventory-export"), signatures, 654); - - STObject selfContained{sfGeneric}; - selfContained.setFieldU16(sfTransactionType, ttEXPORT_SIGNATURES); - selfContained.setFieldU32(sfLedgerSequence, 654); - selfContained.setFieldH256( - sfTransactionHash, makeHash("size-inventory-export")); - auto const innerSerializer = innerTx.getSerializer(); - SerialIter innerIter{innerSerializer.slice()}; - selfContained.set(std::make_unique(innerIter, sfExportedTxn)); - selfContained.setFieldArray( - sfSigners, witness.getFieldArray(sfSigners)); + makeHash("size-inventory-export"), + innerTx, + signatures, + contributors, + 654); auto const innerBytes = innerTx.getSerializer().size(); auto const multiSignedBytes = multiSigned.getSerializer().size(); - auto const currentWitnessBytes = witness.getSerializer().size(); - auto const selfContainedWitnessBytes = - selfContained.getSerializer().size(); + auto const selfContainedWitnessBytes = witness.getSerializer().size(); constexpr std::size_t legacyShareBytes = 32 + 33 + 72; log << "Export serialized-size inventory:\n" << " unsigned target + issuance Memo: " << innerBytes << "\n" << " 32-signer target: " << multiSignedBytes << "\n" - << " current 32-signer witness: " << currentWitnessBytes << "\n" - << " self-contained witness baseline: " + << " self-contained 32-signer witness: " << selfContainedWitnessBytes << "\n" << " one legacy share blob: " << legacyShareBytes << "\n" << " 32 legacy share blobs: " << legacyShareBytes * STTx::maxMultiSigners() << std::endl; BEAST_EXPECT(signatures.size() == STTx::maxMultiSigners()); + BEAST_EXPECT(!witness.isFieldPresent(sfSigners)); + auto const& exported = + witness.peekAtField(sfExportedTxn).downcast(); BEAST_EXPECT( - witness.getFieldArray(sfSigners).size() == STTx::maxMultiSigners()); + exported.getFieldArray(sfSigners).size() == + STTx::maxMultiSigners()); + BEAST_EXPECT(witness.getFieldVL(sfEntropyContributors) == contributors); BEAST_EXPECT(innerBytes == 163); BEAST_EXPECT(multiSignedBytes == 4453); - BEAST_EXPECT(currentWitnessBytes == 4369); - BEAST_EXPECT(selfContainedWitnessBytes == 4497); + BEAST_EXPECT(selfContainedWitnessBytes > multiSignedBytes); BEAST_EXPECT(legacyShareBytes * STTx::maxMultiSigners() == 4384); //@@end export-serialized-size-inventory } diff --git a/src/test/app/Export_test.cpp b/src/test/app/Export_test.cpp index 51e929259..9148e0379 100644 --- a/src/test/app/Export_test.cpp +++ b/src/test/app/Export_test.cpp @@ -39,6 +39,7 @@ #include #include #include +#include #include #include #include @@ -83,45 +84,6 @@ struct Export_test : public beast::unit_test::suite return STTx{std::ref(sit)}; } - static RCLTxSet - makeRCLTxSet( - Application& app, - std::vector> const& txns) - { - auto map = std::make_shared( - SHAMapType::TRANSACTION, app.getNodeFamily()); - map->setUnbacked(); - - for (auto const& tx : txns) - { - Serializer s; - tx->add(s); - map->addItem( - SHAMapNodeType::tnTRANSACTION_NM, - make_shamapitem(tx->getTransactionID(), s.slice())); - } - - return RCLTxSet{map->snapShot(false)}; - } - - static ExportResultBuilder::SignatureWitnesses - makeExportSignatureWitnesses( - uint256 const& exportTxHash, - ExportResultBuilder::SignatureSnapshot signatures, - LedgerIndex seq) - { - auto witness = ExportResultBuilder::buildSignatureWitness( - exportTxHash, signatures, seq); - auto const witnessHash = witness.getTransactionID(); - - ExportResultBuilder::SignatureWitnesses witnesses; - witnesses.emplace( - exportTxHash, - ExportResultBuilder::SignatureWitness{ - witnessHash, std::move(signatures)}); - return witnesses; - } - static std::uint32_t importVLSequence(jtx::Env const& env, PublicKey const& pk) { @@ -136,6 +98,7 @@ struct Export_test : public beast::unit_test::suite Json::Value xpopJson; Json::Value exportedTxnJson; std::uint32_t ticketSeq; + uint256 originTxn; std::optional> vlInfo; }; @@ -171,52 +134,43 @@ struct Export_test : public beast::unit_test::suite jvExport[sfExportedTxn.jsonName] = innerObj.getJson(JsonOptions::none); xahau(jvExport, fee(XRP(1)), ter(tesSUCCESS)); + auto const origin = xahau.tx()->getTransactionID(); xahau.close(); - auto const exportMeta = xahau.meta(); - BEAST_EXPECT(exportMeta); + auto const latchKey = keylet::shadowTicket(alice.id(), origin); + BEAST_EXPECT(xahau.current()->exists(latchKey)); Blob multisignedBlob; - if (exportMeta && exportMeta->isFieldPresent(sfExportResult)) + xahau.close(); + for (auto const& [witnessTx, _] : xahau.closed()->txs) { - auto const& result = - exportMeta->peekAtField(sfExportResult).downcast(); - BEAST_EXPECT(result.isFieldPresent(sfExportSignatureHash)); - if (result.isFieldPresent(sfExportSignatureHash)) - { - auto const witnessHash = - result.getFieldH256(sfExportSignatureHash); - auto const witnessRead = xahau.current()->txRead(witnessHash); - auto const& witnessTx = witnessRead.first; - BEAST_EXPECT(witnessTx); - auto signatures = witnessTx - ? ExportResultBuilder::signaturesFromWitness(*witnessTx) - : std::nullopt; - BEAST_EXPECT(signatures); + if (witnessTx->getTxnType() != ttEXPORT_SIGNATURES || + witnessTx->getFieldH256(sfTransactionHash) != origin) + continue; - STTx const innerTx = makeSTTx(innerObj); - auto expTxn = ExportResultBuilder::buildMultiSignedExportedTxn( - innerTx, - signatures ? *signatures - : ExportResultBuilder::SignatureSnapshot{}); + BEAST_EXPECT(!witnessTx->isFieldPresent(sfSigners)); + BEAST_EXPECT( + witnessTx->getFieldVL(sfEntropyContributors) == Blob{0x01}); + auto const& assembled = + witnessTx->peekAtField(sfExportedTxn).downcast(); + Serializer s; + assembled.add(s); + multisignedBlob = s.peekData(); - Serializer s; - expTxn.add(s); - multisignedBlob = s.peekData(); - - log << "Xahau: ExportResult.ExportSignatureHash = " - << witnessHash << std::endl; - log << "Xahau: assembled ExportedTxn = " - << expTxn.getJson(JsonOptions::none).toStyledString() - << std::endl; - } + log << "Xahau: Export witness = " << witnessTx->getTransactionID() + << std::endl; + log << "Xahau: assembled ExportedTxn = " + << assembled.getJson(JsonOptions::none).toStyledString() + << std::endl; } log << "Xahau: multisigned blob size = " << multisignedBlob.size() << std::endl; BEAST_EXPECT(!multisignedBlob.empty()); - BEAST_EXPECT(xahau.current()->exists( - keylet::shadowTicket(alice.id(), ticketSeq))); + auto const releasedLatch = xahau.current()->read(latchKey); + BEAST_EXPECT(releasedLatch); + if (releasedLatch) + BEAST_EXPECT(releasedLatch->isFieldPresent(sfExportSignatureHash)); Env xrpl{*this, xpopCtx.makeEnvConfig(targetNetworkID)}; @@ -324,7 +278,11 @@ struct Export_test : public beast::unit_test::suite BEAST_EXPECT(vlInfo); return CallbackXPOP{ - xpopJson, innerObj.getJson(JsonOptions::none), ticketSeq, vlInfo}; + xpopJson, + innerObj.getJson(JsonOptions::none), + ticketSeq, + origin, + vlInfo}; } void @@ -808,39 +766,52 @@ 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. + std::optional origin; + for (auto const& [stx, _] : env.closed()->txs) { - 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) { - 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; + BEAST_EXPECT(stx->isFieldPresent(sfEmitDetails)); + origin = stx->getTransactionID(); } - BEAST_EXPECT(exportCount == 1); - BEAST_EXPECT(witnessCount == 1); - BEAST_EXPECT(exportHash && witnessExportHash); - if (exportHash && witnessExportHash) - BEAST_EXPECT(*witnessExportHash == *exportHash); + BEAST_EXPECT(stx->getTxnType() != ttEXPORT_SIGNATURES); } + BEAST_EXPECT(origin); + if (!origin) + return; + + auto const latchKey = keylet::shadowTicket(alice.id(), *origin); + auto const pendingLatch = env.closed()->read(latchKey); + BEAST_EXPECT(pendingLatch); + if (pendingLatch) + BEAST_EXPECT(!pendingLatch->isFieldPresent(sfExportSignatureHash)); + + // Validation of the origin ledger releases signatures. The assembled + // witness is therefore materialized in a subsequent ledger. + env.close(); + std::shared_ptr witness; + for (auto const& [stx, _] : env.closed()->txs) + { + if (stx->getTxnType() == ttEXPORT_SIGNATURES && + stx->getFieldH256(sfTransactionHash) == *origin) + witness = stx; + } + BEAST_EXPECT(witness); + if (!witness) + return; + BEAST_EXPECT(!witness->isFieldPresent(sfSigners)); + BEAST_EXPECT(witness->getFieldVL(sfEntropyContributors) == Blob{0x01}); + auto const& assembled = + witness->peekAtField(sfExportedTxn).downcast(); + BEAST_EXPECT(assembled.isFieldPresent(sfSigners)); + BEAST_EXPECT(assembled.getFieldArray(sfSigners).size() == 1); + + auto const releasedLatch = env.closed()->read(latchKey); + BEAST_EXPECT(releasedLatch); + if (releasedLatch) + BEAST_EXPECT( + releasedLatch->getFieldH256(sfExportSignatureHash) == + witness->getTransactionID()); } void @@ -1080,9 +1051,9 @@ struct Export_test : public beast::unit_test::suite } void - testExportNetworkRetryWithoutQuorum(FeatureBitset features) + testExportNetworkAdmitsIntentWithoutQuorum(FeatureBitset features) { - testcase("ttEXPORT network mode retries without quorum"); + testcase("ttEXPORT admits intent before signature quorum"); using namespace jtx; @@ -1093,6 +1064,11 @@ struct Export_test : public beast::unit_test::suite env.fund(XRP(10000), alice, carol); env.close(); + auto const& valKeys = env.app().getValidatorKeys(); + BEAST_EXPECT(valKeys.keys); + if (!valKeys.keys) + return; + seedUNLReportLedger(env, {valKeys.keys->publicKey}); forceNonStandalone(env.app()); BEAST_EXPECT(!env.app().config().standalone()); ConsensusTestConfig cfg; @@ -1112,6 +1088,7 @@ struct Export_test : public beast::unit_test::suite jv[sfExportedTxn.jsonName] = innerObj.getJson(JsonOptions::none); env(jv, fee(XRP(1)), ter(tesSUCCESS)); + auto const origin = env.tx()->getTransactionID(); BEAST_EXPECT(env.close( env.now() + std::chrono::seconds{5}, std::chrono::milliseconds{0})); @@ -1133,248 +1110,11 @@ struct Export_test : public beast::unit_test::suite closedResult = TER::fromInt((*meta)[sfTransactionResult]); } - BEAST_EXPECT(!closedResult); - BEAST_EXPECT(!env.le(keylet::shadowTicket(alice.id(), ticketSeq))); - } - - void - testExportProposalSigningRequiresBoundedUNLReport(FeatureBitset features) - { - testcase("Export proposal signing requires bounded UNLReport"); - - using namespace jtx; - - Env env{*this, exportTestConfig(), features}; - Account const alice{"alice"}; - Account const carol{"carol"}; - env.fund(XRP(10000), alice, carol); - env.close(); - - auto submitOpenExport = [&](std::uint32_t ticketSeq) { - auto const seq = env.current()->seq(); - auto innerObj = buildExportedPayment( - alice.id(), - carol.id(), - seq + 1, - seq + ExportLimits::maxRetryLedgers, - ticketSeq); - Json::Value jv; - jv[jss::TransactionType] = jss::Export; - jv[jss::Account] = alice.human(); - jv[jss::LastLedgerSequence] = seq + ExportLimits::maxRetryLedgers; - jv[sfExportedTxn.jsonName] = innerObj.getJson(JsonOptions::none); - env(jv, fee(XRP(1)), ter(tesSUCCESS)); - }; - - auto attach = [&](ConsensusExtensions& ce) { - protocol::TMProposeSet prop; - auto const closed = env.app().getLedgerMaster().getClosedLedger(); - RCLCxPeerPos::Proposal proposal{ - closed->info().hash, - 0, - ExtendedPosition{closed->info().txHash}, - NetClock::time_point{}, - NetClock::time_point{}, - env.app().getValidatorKeys().nodeID}; - ce.attachExportSignatures(prop, proposal); - return prop.exportsignatures_size(); - }; - - auto& ce = env.app().getConsensusExtensions(); - ce.setExportEnabledThisRound(true); - ce.cacheUNLReport(env.app().getLedgerMaster().getClosedLedger()); - BEAST_EXPECT(!ce.activeValidatorView()->fromUNLReport); - - submitOpenExport(1); - BEAST_EXPECT(attach(ce) == 0); - - auto const localPK = env.app().getValidationPublicKey(); - BEAST_EXPECT(localPK); - if (!localPK) - return; - - seedUNLReportLedger(env, {*localPK}); - ce.cacheUNLReport(env.app().getLedgerMaster().getClosedLedger()); - BEAST_EXPECT(ce.activeValidatorView()->fromUNLReport); - BEAST_EXPECT(ce.activeValidatorView()->size() == 1); - - submitOpenExport(2); - BEAST_EXPECT(attach(ce) == 1); - - std::vector activeKeys{*localPK}; - while (activeKeys.size() <= STTx::maxMultiSigners()) - activeKeys.push_back(randomKeyPair(KeyType::secp256k1).first); - - seedUNLReportLedger(env, activeKeys); - ce.cacheUNLReport(env.app().getLedgerMaster().getClosedLedger()); - BEAST_EXPECT(ce.activeValidatorView()->fromUNLReport); - BEAST_EXPECT( - ce.activeValidatorView()->size() > STTx::maxMultiSigners()); - - submitOpenExport(3); - BEAST_EXPECT(attach(ce) == 0); - } - - void - testExportNetworkApplyUsesAgreedSidecar(FeatureBitset features) - { - testcase("ttEXPORT network apply uses agreed export sidecar"); - - using namespace jtx; - - Env env{*this, exportTestConfig(), features}; - - Account const alice{"alice"}; - Account const carol{"carol"}; - - env.fund(XRP(10000), alice, carol); - env.close(); - - 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; - seedUNLReportLedger(env, {valPK}); - forceNonStandalone(env.app()); - BEAST_EXPECT(!env.app().config().standalone()); - - 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 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); - auto const exportSignatureWitnesses = - makeExportSignatureWitnesses(txHash, expectedSigs, applySeq); - ApplyOptions const applyOptions{ - &exportSignatureWitnesses, - ApplyOptions::ExportWitnessMembership::TrustConsensusMaterialized, - false, - nullptr}; - auto const expectedIntentHash = - ExportResultBuilder::exportIntentHash(innerTx); - - auto const parent = env.app().getLedgerMaster().getClosedLedger(); - auto next = std::make_shared( - *parent, env.app().timeKeeper().closeTime()); - OpenView accum(&*next); - auto const result = ripple::apply( - env.app(), accum, *exportTx, tapNONE, env.journal, applyOptions); - BEAST_EXPECT(result.ter == tesSUCCESS); - BEAST_EXPECT(result.applied); - accum.apply(*next); - - auto const st = next->read(keylet::shadowTicket(alice.id(), ticketSeq)); - BEAST_EXPECT(st); - if (st) - BEAST_EXPECT(st->getFieldH256(sfDigest) == expectedIntentHash); - } - - void - testExportNetworkRetriesOversizedActiveView(FeatureBitset features) - { - testcase("ttEXPORT retries above target signer cap"); - - using namespace jtx; - - Env env{*this, exportTestConfig(), features}; - - Account const alice{"alice"}; - Account const carol{"carol"}; - - env.fund(XRP(10000), alice, carol); - env.close(); - - std::vector> validators; - std::vector activeKeys; - validators.reserve(STTx::maxMultiSigners() + 5); - activeKeys.reserve(STTx::maxMultiSigners() + 5); - while (validators.size() < STTx::maxMultiSigners() + 5) - { - auto keys = randomKeyPair(KeyType::secp256k1); - activeKeys.push_back(keys.first); - validators.push_back(std::move(keys)); - } - - seedUNLReportLedger(env, activeKeys); - forceNonStandalone(env.app()); - BEAST_EXPECT(!env.app().config().standalone()); - - auto const parent = env.app().getLedgerMaster().getClosedLedger(); - auto const applySeq = parent->seq() + 1; - auto const ticketSeq = std::uint32_t{1}; - auto innerObj = buildExportedPayment( - alice.id(), carol.id(), applySeq, applySeq + 5, ticketSeq); - auto const innerTx = makeSTTx(innerObj); - auto jt = makeExportJTx(env, alice, innerObj, applySeq + 5); - auto const exportTx = jt.stx; - BEAST_EXPECT(exportTx); - if (!exportTx) - return; - auto const txHash = exportTx->getTransactionID(); - - ExportResultBuilder::SignatureSnapshot signatures; - for (auto const& [pk, sk] : validators) - signatures.emplace( - pk, ExportResultBuilder::signExportedTxn(innerTx, pk, sk)); - - BEAST_EXPECT(signatures.size() > STTx::maxMultiSigners()); - auto const exportSignatureWitnesses = - makeExportSignatureWitnesses(txHash, signatures, applySeq); - ApplyOptions const applyOptions{ - &exportSignatureWitnesses, - ApplyOptions::ExportWitnessMembership::TrustConsensusMaterialized, - false, - nullptr}; - - auto next = std::make_shared( - *parent, env.app().timeKeeper().closeTime()); - OpenView accum(&*next); - auto const result = ripple::apply( - env.app(), accum, *exportTx, tapNONE, env.journal, applyOptions); - BEAST_EXPECT(result.ter == terRETRY_EXPORT); - BEAST_EXPECT(!result.applied); - - auto const st = next->read(keylet::shadowTicket(alice.id(), ticketSeq)); - BEAST_EXPECT(!st); + BEAST_EXPECT(closedResult == tesSUCCESS); + auto const latch = env.le(keylet::shadowTicket(alice.id(), origin)); + BEAST_EXPECT(latch); + if (latch) + BEAST_EXPECT(!latch->isFieldPresent(sfExportSignatureHash)); } void @@ -1403,7 +1143,6 @@ struct Export_test : public beast::unit_test::suite return; auto const& valPK = valKeys.keys->publicKey; - auto const& valSK = valKeys.keys->secretKey; seedUNLReportLedger(env, {valPK}); forceNonStandalone(env.app()); @@ -1412,7 +1151,6 @@ struct Export_test : public beast::unit_test::suite auto const ticketSeq = std::uint32_t{1}; auto innerObj = buildExportedPayment( alice.id(), carol.id(), applySeq, applySeq + 5, ticketSeq); - auto const innerTx = makeSTTx(innerObj); auto jt = makeExportJTx(env, alice, innerObj, applySeq + 5); auto const exportTx = jt.stx; BEAST_EXPECT(exportTx); @@ -1420,18 +1158,6 @@ struct Export_test : public beast::unit_test::suite return; auto const txHash = exportTx->getTransactionID(); - auto const sig = - ExportResultBuilder::signExportedTxn(innerTx, valPK, valSK); - ExportResultBuilder::SignatureSnapshot signatures; - signatures.emplace(valPK, sig); - auto const exportSignatureWitnesses = - makeExportSignatureWitnesses(txHash, signatures, applySeq); - ApplyOptions const applyOptions{ - &exportSignatureWitnesses, - ApplyOptions::ExportWitnessMembership::TrustConsensusMaterialized, - false, - nullptr}; - auto next = std::make_shared( *parent, env.app().timeKeeper().closeTime()); auto const account = next->read(keylet::account(alice.id())); @@ -1447,18 +1173,17 @@ struct Export_test : public beast::unit_test::suite next->rawReplace(replacement); OpenView accum(&*next); - auto const result = ripple::apply( - env.app(), accum, *exportTx, tapNONE, env.journal, applyOptions); + auto const result = + ripple::apply(env.app(), accum, *exportTx, tapNONE, env.journal); BEAST_EXPECT(result.ter == tecINSUFFICIENT_RESERVE); - BEAST_EXPECT(!accum.read(keylet::shadowTicket(alice.id(), ticketSeq))); + BEAST_EXPECT(!accum.read(keylet::shadowTicket(alice.id(), txHash))); } void - testExportHistoricalReplayIgnoresCurrentManifestMap(FeatureBitset features) + testLaterLedgerWitnessTransitionAndReplay(FeatureBitset features) { - testcase( - "ttEXPORT historical replay does not require current manifest"); + testcase("later ledger witness records assembled Export result"); using namespace jtx; @@ -1480,179 +1205,106 @@ struct Export_test : public beast::unit_test::suite BEAST_EXPECT(!env.app().config().standalone()); auto const parent = env.app().getLedgerMaster().getClosedLedger(); - auto const applySeq = parent->seq() + 1; + auto const countExportWork = [](std::shared_ptr const& sle) { + return sle && sle->isFieldPresent(sfExportCount) + ? sle->getFieldU16(sfExportCount) + : std::uint16_t{0}; + }; + auto const parentPendingCount = + countExportWork(parent->read(keylet::pendingExports())); + auto const parentAccount = parent->read(keylet::account(alice.id())); + BEAST_EXPECT(parentAccount); + if (!parentAccount) + return; + auto const parentAccountExportCount = countExportWork(parentAccount); + auto const parentOwnerCount = parentAccount->getFieldU32(sfOwnerCount); + auto const originSeq = parent->seq() + 1; auto const ticketSeq = std::uint32_t{1}; auto innerObj = buildExportedPayment( - alice.id(), carol.id(), applySeq, applySeq + 5, ticketSeq); + alice.id(), carol.id(), originSeq, originSeq + 5, ticketSeq); auto const innerTx = makeSTTx(innerObj); - auto jt = makeExportJTx(env, alice, innerObj, applySeq + 5); + auto jt = makeExportJTx(env, alice, innerObj, originSeq + 5); auto const exportTx = jt.stx; BEAST_EXPECT(exportTx); if (!exportTx) return; - auto const txHash = exportTx->getTransactionID(); + auto const origin = exportTx->getTransactionID(); - auto const oldSigningKey = randomKeyPair(KeyType::secp256k1); - auto const sig = ExportResultBuilder::signExportedTxn( - innerTx, oldSigningKey.first, oldSigningKey.second); - - ExportResultBuilder::SignatureSnapshot signatures; - signatures.emplace(oldSigningKey.first, sig); - auto const exportSignatureWitnesses = - makeExportSignatureWitnesses(txHash, signatures, applySeq); - - ApplyOptions const liveOptions{ - &exportSignatureWitnesses, - ApplyOptions::ExportWitnessMembership::FilterLiveManifest, - false, - nullptr}; - { - auto next = std::make_shared( - *parent, env.app().timeKeeper().closeTime()); - OpenView accum(&*next); - auto const result = ripple::apply( - env.app(), accum, *exportTx, tapNONE, env.journal, liveOptions); - BEAST_EXPECT(result.ter == terRETRY_EXPORT); - BEAST_EXPECT(!result.applied); - } - - auto const expectedIntentHash = - ExportResultBuilder::exportIntentHash(innerTx); - - // A live consensus build only reaches Export::doApply after onPreBuild - // has replaced witnesses with material from the accepted sidecar root. - // That tx-stream witness is the membership source; the current - // ManifestCache is not. - ApplyOptions const consensusOptions{ - &exportSignatureWitnesses, - ApplyOptions::ExportWitnessMembership::TrustConsensusMaterialized, - false, - nullptr}; - { - auto next = std::make_shared( - *parent, env.app().timeKeeper().closeTime()); - OpenView accum(&*next); - auto const result = ripple::apply( - env.app(), - accum, - *exportTx, - tapNONE, - env.journal, - consensusOptions); - BEAST_EXPECT(result.ter == tesSUCCESS); - BEAST_EXPECT(result.applied); - accum.apply(*next); - - auto const st = - next->read(keylet::shadowTicket(alice.id(), ticketSeq)); - BEAST_EXPECT(st); - if (st) - BEAST_EXPECT(st->getFieldH256(sfDigest) == expectedIntentHash); - } - - ApplyOptions const missingParentReplayOptions{ - &exportSignatureWitnesses, - ApplyOptions::ExportWitnessMembership::TrustHistoricalReplay, + CanonicalTXSet originTxs{parent->info().hash}; + originTxs.insert(exportTx); + std::set originFailed; + auto const originLedger = buildLedger( + parent, + env.app().timeKeeper().closeTime(), true, - nullptr}; - { - auto next = std::make_shared( - *parent, env.app().timeKeeper().closeTime()); - OpenView accum(&*next); - auto const result = ripple::apply( - env.app(), - accum, - *exportTx, - tapNONE, - env.journal, - missingParentReplayOptions); - BEAST_EXPECT(result.ter == terRETRY_EXPORT); - BEAST_EXPECT(!result.applied); - } + parent->info().closeTimeResolution, + env.app(), + originTxs, + originFailed, + env.journal); + BEAST_EXPECT(originTxs.empty()); + BEAST_EXPECT(originFailed.empty()); - // Historical replay reconstructs an already-validated ledger. The - // persisted witness supplies the historical signing-key membership; - // current ManifestCache may no longer know a rotated signing key. The - // replay parent must be threaded explicitly so apply never consults - // process-global ledger state for the validator view. - ApplyOptions const replayOptions{ - &exportSignatureWitnesses, - ApplyOptions::ExportWitnessMembership::TrustHistoricalReplay, - true, - parent}; - - auto replayed = std::make_shared( - *parent, env.app().timeKeeper().closeTime()); - OpenView accum(&*replayed); - auto const replayResult = ripple::apply( - env.app(), accum, *exportTx, tapNONE, env.journal, replayOptions); - BEAST_EXPECT(replayResult.ter == tesSUCCESS); - BEAST_EXPECT(replayResult.applied); - accum.apply(*replayed); - - auto const st = - replayed->read(keylet::shadowTicket(alice.id(), ticketSeq)); - BEAST_EXPECT(st); - if (st) - BEAST_EXPECT(st->getFieldH256(sfDigest) == expectedIntentHash); - } - - void - testBuildLedgerReplayPrescansExportWitness(FeatureBitset features) - { - testcase("BuildLedger replay pre-scans persisted export witness"); - - using namespace jtx; - - Env env{*this, exportTestConfig(), features}; - - Account const alice{"alice"}; - Account const carol{"carol"}; - - env.fund(XRP(10000), alice, carol); - env.close(); - - auto const& valKeys = env.app().getValidatorKeys(); - BEAST_EXPECT(valKeys.keys); - if (!valKeys.keys) + auto const latchKey = keylet::shadowTicket(alice.id(), origin); + auto const pendingLatch = originLedger->read(latchKey); + BEAST_EXPECT(pendingLatch); + if (!pendingLatch) return; - - seedUNLReportLedger(env, {valKeys.keys->publicKey}); - forceNonStandalone(env.app()); - BEAST_EXPECT(!env.app().config().standalone()); - - auto const parent = env.app().getLedgerMaster().getClosedLedger(); - auto const applySeq = parent->seq() + 1; - auto const ticketSeq = std::uint32_t{1}; - auto innerObj = buildExportedPayment( - alice.id(), carol.id(), applySeq, applySeq + 5, ticketSeq); - auto const innerTx = makeSTTx(innerObj); - auto jt = makeExportJTx(env, alice, innerObj, applySeq + 5); - auto const exportTx = jt.stx; - BEAST_EXPECT(exportTx); - if (!exportTx) + BEAST_EXPECT(!pendingLatch->isFieldPresent(sfExportSignatureHash)); + BEAST_EXPECT(pendingLatch->isFieldPresent(sfExportNode)); + auto const originPendingRoot = + originLedger->read(keylet::pendingExports()); + auto const originAccount = + originLedger->read(keylet::account(alice.id())); + BEAST_EXPECT(originPendingRoot); + BEAST_EXPECT(originAccount); + if (!originPendingRoot || !originAccount) + return; + BEAST_EXPECT( + countExportWork(originPendingRoot) == parentPendingCount + 1); + BEAST_EXPECT( + countExportWork(originAccount) == parentAccountExportCount + 1); + BEAST_EXPECT( + originAccount->getFieldU32(sfOwnerCount) == parentOwnerCount + 1); + + auto release = ExportOriginMemo::releaseForm( + innerTx, + ExportOriginMemo::Origin{env.app().config().NETWORK_ID, 0, origin}, + ExportOriginMemo::Anchor{ + originLedger->info().seq, originLedger->info().hash}); + BEAST_EXPECT(release); + if (!release) return; - auto const txHash = exportTx->getTransactionID(); ExportResultBuilder::SignatureSnapshot signatures; signatures.emplace( valKeys.keys->publicKey, ExportResultBuilder::signExportedTxn( - innerTx, valKeys.keys->publicKey, valKeys.keys->secretKey)); + release.value(), + valKeys.keys->publicKey, + valKeys.keys->secretKey)); + auto const witnessSeq = originLedger->seq() + 1; auto const witnessTx = std::make_shared( ExportResultBuilder::buildSignatureWitness( - txHash, signatures, applySeq)); + origin, release.value(), signatures, Blob{0x01}, witnessSeq)); - CanonicalTXSet txns{parent->info().hash}; - txns.insert(exportTx); + BEAST_EXPECT(!witnessTx->isFieldPresent(sfSigners)); + BEAST_EXPECT( + witnessTx->getFieldVL(sfEntropyContributors) == Blob{0x01}); + auto const& assembled = + witnessTx->peekAtField(sfExportedTxn).downcast(); + BEAST_EXPECT(assembled.isFieldPresent(sfSigners)); + BEAST_EXPECT(assembled.getFieldArray(sfSigners).size() == 1); + + CanonicalTXSet txns{originLedger->info().hash}; txns.insert(witnessTx); std::set failed; auto const built = buildLedger( - parent, + originLedger, env.app().timeKeeper().closeTime(), true, - parent->info().closeTimeResolution, + originLedger->info().closeTimeResolution, env.app(), txns, failed, @@ -1660,17 +1312,34 @@ struct Export_test : public beast::unit_test::suite BEAST_EXPECT(txns.empty()); BEAST_EXPECT(failed.empty()); - auto const expectedIntentHash = - ExportResultBuilder::exportIntentHash(innerTx); - auto const builtTicket = - built->read(keylet::shadowTicket(alice.id(), ticketSeq)); - BEAST_EXPECT(builtTicket); - if (builtTicket) + auto const releasedLatch = built->read(latchKey); + BEAST_EXPECT(releasedLatch); + if (releasedLatch) + { BEAST_EXPECT( - builtTicket->getFieldH256(sfDigest) == expectedIntentHash); + releasedLatch->getFieldH256(sfExportSignatureHash) == + witnessTx->getTransactionID()); + BEAST_EXPECT(!releasedLatch->isFieldPresent(sfExportNode)); + } + + auto const releasedPendingRoot = built->read(keylet::pendingExports()); + auto const releasedAccount = built->read(keylet::account(alice.id())); + BEAST_EXPECT(releasedPendingRoot); + BEAST_EXPECT(releasedAccount); + if (releasedPendingRoot && releasedAccount) + { + BEAST_EXPECT( + countExportWork(releasedPendingRoot) == parentPendingCount); + BEAST_EXPECT( + countExportWork(releasedAccount) == + parentAccountExportCount + 1); + BEAST_EXPECT( + releasedAccount->getFieldU32(sfOwnerCount) == + parentOwnerCount + 1); + } auto const replayed = buildLedger( - LedgerReplay(parent, built), tapNONE, env.app(), env.journal); + LedgerReplay(originLedger, built), tapNONE, env.app(), env.journal); BEAST_EXPECT(replayed->info().hash == built->info().hash); } @@ -1691,19 +1360,11 @@ 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); @@ -1716,17 +1377,6 @@ 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 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( @@ -1737,119 +1387,49 @@ struct Export_test : public beast::unit_test::suite BEAST_EXPECT(result.ter == terRETRY_EXPORT); BEAST_EXPECT(!result.applied); - BEAST_EXPECT(!next->read(keylet::shadowTicket(alice.id(), ticketSeq))); + BEAST_EXPECT(!next->read(keylet::shadowTicket(alice.id(), txHash))); } void testExportNetworkLastLedgerSequenceBoundary(FeatureBitset features) { - testcase("ttEXPORT network mode LastLedgerSequence boundary"); + testcase("ttEXPORT admits intent at LastLedgerSequence boundary"); using namespace jtx; - auto applyAtLastLedger = [&](bool withQuorum) { - Env env{*this, exportTestConfig(), features}; + Env env{*this, exportTestConfig(), features}; + Account const alice{"alice"}; + Account const carol{"carol"}; + env.fund(XRP(10000), alice, carol); + env.close(); - Account const alice{"alice"}; - Account const carol{"carol"}; + auto const& valKeys = env.app().getValidatorKeys(); + BEAST_EXPECT(valKeys.keys); + if (!valKeys.keys) + return; + seedUNLReportLedger(env, {valKeys.keys->publicKey}); + forceNonStandalone(env.app()); - env.fund(XRP(10000), alice, carol); - env.close(); + auto const parent = env.app().getLedgerMaster().getClosedLedger(); + auto const applySeq = parent->seq() + 1; + auto const ticketSeq = std::uint32_t{1}; + auto innerObj = buildExportedPayment( + alice.id(), carol.id(), applySeq, applySeq, ticketSeq); + auto const exportTx = makeExportJTx(env, alice, innerObj, applySeq).stx; + BEAST_EXPECT(exportTx); + if (!exportTx) + return; - 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; - seedUNLReportLedger(env, {valPK}); - forceNonStandalone(env.app()); - BEAST_EXPECT(!env.app().config().standalone()); - - auto const parent = env.app().getLedgerMaster().getClosedLedger(); - auto const applySeq = parent->seq() + 1; - auto const ticketSeq = - withQuorum ? std::uint32_t{1} : std::uint32_t{2}; - auto innerObj = buildExportedPayment( - alice.id(), carol.id(), applySeq, applySeq, ticketSeq); - auto const innerTx = makeSTTx(innerObj); - auto jt = makeExportJTx(env, alice, innerObj, applySeq); - 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(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 = - makeExportSignatureWitnesses(txHash, signatures, applySeq); - } - ApplyOptions const applyOptions{ - &exportSignatureWitnesses, - ApplyOptions::ExportWitnessMembership:: - TrustConsensusMaterialized, - false, - nullptr}; - - auto next = std::make_shared( - *parent, env.app().timeKeeper().closeTime()); - BEAST_EXPECT(next->seq() == applySeq); - OpenView accum(&*next); - auto const result = ripple::apply( - env.app(), - accum, - *exportTx, - tapNONE, - env.journal, - applyOptions); - - if (withQuorum) - { - BEAST_EXPECT(result.ter == tesSUCCESS); - BEAST_EXPECT(result.applied); - accum.apply(*next); - BEAST_EXPECT( - next->read(keylet::shadowTicket(alice.id(), ticketSeq))); - } - else - { - BEAST_EXPECT(result.ter == tecEXPORT_EXPIRED); - // tecEXPORT_EXPIRED is a terminal tec result: it applies to - // consume the sequence/fee, but must not create export state. - BEAST_EXPECT(result.applied); - accum.apply(*next); - BEAST_EXPECT( - !next->read(keylet::shadowTicket(alice.id(), ticketSeq))); - } - }; - - // Quorum at ledger == LastLedgerSequence still succeeds; no quorum at - // the same boundary expires cleanly instead of falling through to a - // later tefMAX_LEDGER preclaim. - applyAtLastLedger(true); - applyAtLastLedger(false); + auto next = std::make_shared( + *parent, env.app().timeKeeper().closeTime()); + OpenView accum(&*next); + auto const result = + ripple::apply(env.app(), accum, *exportTx, tapNONE, env.journal); + BEAST_EXPECT(result.ter == tesSUCCESS); + BEAST_EXPECT(result.applied); + accum.apply(*next); + BEAST_EXPECT(next->read( + keylet::shadowTicket(alice.id(), exportTx->getTransactionID()))); } void @@ -1906,6 +1486,8 @@ struct Export_test : public beast::unit_test::suite env.fund(XRP(10000), alice, carol); env.close(); + std::map origins; + auto submitClosedExport = [&](std::uint32_t ticketSeq, TER expected, bool expectShadow) { @@ -1924,14 +1506,19 @@ struct Export_test : public beast::unit_test::suite jv[sfExportedTxn.jsonName] = innerObj.getJson(JsonOptions::none); env(jv, fee(XRP(1)), ter(tesSUCCESS)); + auto const submittedOrigin = env.tx()->getTransactionID(); auto const meta = env.meta(); BEAST_EXPECT(meta); BEAST_EXPECT( (*meta)[sfTransactionResult] == static_cast(TERtoInt(expected))); - auto const shadow = - env.le(keylet::shadowTicket(alice.id(), ticketSeq)); + if (expected == tesSUCCESS) + origins.emplace(ticketSeq, submittedOrigin); + auto const found = origins.find(ticketSeq); + auto const shadow = found == origins.end() + ? nullptr + : env.le(keylet::shadowTicket(alice.id(), found->second)); BEAST_EXPECT(expectShadow == static_cast(shadow)); env.close(); }; @@ -1976,19 +1563,23 @@ struct Export_test : public beast::unit_test::suite jv[sfExportedTxn.jsonName] = innerObj.getJson(JsonOptions::none); env(jv, fee(XRP(1)), ter(tesSUCCESS)); + auto const origin = env.tx()->getTransactionID(); auto const meta = env.meta(); BEAST_EXPECT(meta); BEAST_EXPECT( (*meta)[sfTransactionResult] == static_cast(TERtoInt(tesSUCCESS))); - auto const shadow = env.le(keylet::shadowTicket(alice.id(), ticketSeq)); + auto const shadow = env.le(keylet::shadowTicket(alice.id(), origin)); BEAST_EXPECT(shadow); if (shadow) { BEAST_EXPECT(shadow->getAccountID(sfAccount) == alice.id()); BEAST_EXPECT(shadow->getFieldU32(sfTicketSequence) == ticketSeq); + BEAST_EXPECT(shadow->getFieldH256(sfTransactionHash) == origin); BEAST_EXPECT(shadow->isFieldPresent(sfDigest)); + BEAST_EXPECT(shadow->isFieldPresent(sfExportNode)); + BEAST_EXPECT(!shadow->isFieldPresent(sfExportSignatureHash)); BEAST_EXPECT( shadow->getFieldH256(sfDigest) == ExportResultBuilder::exportIntentHash(makeSTTx(innerObj))); @@ -2036,12 +1627,13 @@ struct Export_test : public beast::unit_test::suite jvExport[sfExportedTxn.jsonName] = innerObj.getJson(JsonOptions::none); env(jvExport, fee(XRP(1)), ter(tesSUCCESS)); + auto const origin = env.tx()->getTransactionID(); auto const exportMeta = env.meta(); BEAST_EXPECT(exportMeta); BEAST_EXPECT( (*exportMeta)[sfTransactionResult] == static_cast(TERtoInt(tesSUCCESS))); - BEAST_EXPECT(env.le(keylet::shadowTicket(alice.id(), ticketSeq))); + BEAST_EXPECT(env.le(keylet::shadowTicket(alice.id(), origin))); Json::Value jvCancelExisting; jvCancelExisting[jss::TransactionType] = jss::Export; @@ -2054,7 +1646,7 @@ struct Export_test : public beast::unit_test::suite BEAST_EXPECT( (*cancelMeta)[sfTransactionResult] == static_cast(TERtoInt(tesSUCCESS))); - BEAST_EXPECT(!env.le(keylet::shadowTicket(alice.id(), ticketSeq))); + BEAST_EXPECT(!env.le(keylet::shadowTicket(alice.id(), origin))); env.close(); } @@ -2261,7 +1853,7 @@ struct Export_test : public beast::unit_test::suite // Shadow ticket should be consumed after import. BEAST_EXPECT(!xahau.current()->exists( - keylet::shadowTicket(alice.id(), callback.ticketSeq))); + keylet::shadowTicket(alice.id(), callback.originTxn))); if (callback.vlInfo) { BEAST_EXPECT( @@ -2297,7 +1889,7 @@ struct Export_test : public beast::unit_test::suite xahau(cancel, fee(XRP(1)), ter(tesSUCCESS)); xahau.close(); BEAST_EXPECT(!xahau.current()->exists( - keylet::shadowTicket(alice.id(), callback.ticketSeq))); + keylet::shadowTicket(alice.id(), callback.originTxn))); auto const importFee = xahau.current()->fees().base * 10; xahau( @@ -2312,17 +1904,18 @@ struct Export_test : public beast::unit_test::suite xahau.current()->seq() + ExportLimits::maxRetryLedgers; rearm[sfExportedTxn.jsonName] = callback.exportedTxnJson; xahau(rearm, fee(XRP(1)), ter(tesSUCCESS)); + auto const rearmedOrigin = xahau.tx()->getTransactionID(); xahau.close(); BEAST_EXPECT(xahau.current()->exists( - keylet::shadowTicket(alice.id(), callback.ticketSeq))); + keylet::shadowTicket(alice.id(), rearmedOrigin))); xahau( import::import(alice, callback.xpopJson), fee(importFee), - ter(tesSUCCESS)); + ter(telSHADOW_TICKET_REQUIRED)); xahau.close(); - BEAST_EXPECT(!xahau.current()->exists( - keylet::shadowTicket(alice.id(), callback.ticketSeq))); + BEAST_EXPECT(xahau.current()->exists( + keylet::shadowTicket(alice.id(), rearmedOrigin))); } void @@ -2371,7 +1964,7 @@ struct Export_test : public beast::unit_test::suite xahau.close(); BEAST_EXPECT(xahau.current()->exists( - keylet::shadowTicket(alice.id(), callback.ticketSeq))); + keylet::shadowTicket(alice.id(), callback.originTxn))); BEAST_EXPECT( importVLSequence(xahau, callback.vlInfo->second) == freshSeq); } @@ -2407,13 +2000,9 @@ struct Export_test : public beast::unit_test::suite // ttEXPORT transactor tests testExportTxnOpenLedger(allWithExport); - testExportNetworkRetryWithoutQuorum(allWithExport); - testExportProposalSigningRequiresBoundedUNLReport(allWithExport); - testExportNetworkApplyUsesAgreedSidecar(allWithExport); - testExportNetworkRetriesOversizedActiveView(allWithExport); + testExportNetworkAdmitsIntentWithoutQuorum(allWithExport); testExportShadowTicketInsufficientReserve(allWithExport); - testExportHistoricalReplayIgnoresCurrentManifestMap(allWithExport); - testBuildLedgerReplayPrescansExportWitness(allWithExport); + testLaterLedgerWitnessTransitionAndReplay(allWithExport); testExportNetworkRetryWithoutUNLReport(allWithExport); testExportNetworkLastLedgerSequenceBoundary(allWithExport); testOpenLedgerExportLimit(allWithExport); diff --git a/src/test/app/PseudoTx_test.cpp b/src/test/app/PseudoTx_test.cpp index 1d2b95681..79099e5d4 100644 --- a/src/test/app/PseudoTx_test.cpp +++ b/src/test/app/PseudoTx_test.cpp @@ -89,8 +89,15 @@ struct PseudoTx_test : public beast::unit_test::suite std::uint8_t const signatureBytes[] = {1, 2, 3}; signatures.emplace( publicKey, Buffer{signatureBytes, sizeof(signatureBytes)}); + auto const releaseTarget = STTx(ttPAYMENT, [&](auto& obj) { + obj.setAccountID(sfAccount, AccountID(1)); + obj.setAccountID(sfDestination, AccountID(2)); + obj.setFieldU32(sfSequence, 0); + obj.setFieldAmount(sfFee, STAmount{}); + obj.setFieldVL(sfSigningPubKey, Blob{}); + }); res.emplace_back(ExportResultBuilder::buildSignatureWitness( - uint256(4), signatures, seq)); + uint256(4), releaseTarget, signatures, Blob{0x01}, seq)); return res; } diff --git a/src/test/consensus/ConsensusExtensions_test.cpp b/src/test/consensus/ConsensusExtensions_test.cpp index d762e28f0..f905981ab 100644 --- a/src/test/consensus/ConsensusExtensions_test.cpp +++ b/src/test/consensus/ConsensusExtensions_test.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -36,6 +37,7 @@ #include #include #include +#include #include #include #include @@ -152,14 +154,6 @@ makeExportSigBlob(uint256 const& txHash, PublicKey const& publicKey) return blob; } -void -setWirePosition(protocol::TMProposeSet& wire, ExtendedPosition const& position) -{ - Serializer s; - position.add(s); - wire.set_currenttxhash(s.data(), s.size()); -} - STTx makeSTTx(STObject const& obj) { @@ -190,36 +184,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, @@ -257,7 +221,7 @@ makeExportSignaturesTx(std::uint32_t ledgerSeq, uint256 const& exportTxHash) return std::make_shared( ExportResultBuilder::buildSignatureWitness( - exportTxHash, signatures, ledgerSeq)); + exportTxHash, innerTx, signatures, Blob{0x01}, ledgerSeq)); } RCLTxSet @@ -279,15 +243,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) { @@ -385,7 +340,7 @@ struct FakeExtensions bool exportSigConvergenceFailed_{false}; bool rngOn{false}; bool localExportSigs{true}; - bool consensusExportTxns{false}; + bool livePendingExportLatches{false}; bool exportOn{true}; bool entropyFailed{false}; bool commitFrozen{false}; @@ -573,7 +528,7 @@ struct FakeExtensions bool hasConsensusExportTxns() const { - return consensusExportTxns; + return livePendingExportLatches; } uint256 @@ -1419,7 +1374,8 @@ class ConsensusExtensions_test : public beast::unit_test::suite disabledEnv.app().getLedgerMaster().getClosedLedger(); ConsensusExtensions ce{enabledEnv.app(), activeNoopJournal()}; - auto const tx = makeHash("on-round-start-export-latch"); + auto const origin = makeHash("on-round-start-export-latch"); + auto const trigger = makeHash("on-round-start-export-trigger"); auto const pk = makeValidatorKeys().front(); std::uint8_t const sigBytes[] = {1, 2, 3}; Buffer const sig{sigBytes, sizeof(sigBytes)}; @@ -1430,17 +1386,34 @@ class ConsensusExtensions_test : public beast::unit_test::suite BEAST_EXPECT(ce.rngEnabled()); BEAST_EXPECT(ce.exportEnabled()); - ce.exportSigCollector().addVerifiedSignature(tx, pk, sig, 10); - BEAST_EXPECT(ce.exportSigCollector().signatureCount(tx) == 1); + BEAST_EXPECT(ce.postValidationExportSigCollector().reopenPublication( + origin, trigger, 10)); + auto admission = + ce.postValidationExportSigCollector().beginAttributedAdmission( + origin, ExportSigCollectorV2::Contribution{0, pk, sig}, 10); + BEAST_EXPECT( + admission.result == ExportSigCollectorV2::BeginResult::verify); + BEAST_EXPECT(admission.ticket); + if (admission.ticket) + BEAST_EXPECT( + ce.postValidationExportSigCollector() + .admitContribution(std::move(*admission.ticket), true, 10) + .result == ExportSigCollectorV2::AdmitResult::accepted); + BEAST_EXPECT( + ce.postValidationExportSigCollector().fullUnionSnapshot().size() == + 1); ce.onRoundStart(RCLCxLedger{enabledLedger}, {}); - BEAST_EXPECT(ce.exportSigCollector().signatureCount(tx) == 1); + BEAST_EXPECT( + ce.postValidationExportSigCollector().fullUnionSnapshot().size() == + 1); ce.setRngEnabledThisRound(true); ce.setExportEnabledThisRound(true); ce.onRoundStart(RCLCxLedger{disabledLedger}, {}); BEAST_EXPECT(!ce.rngEnabled()); BEAST_EXPECT(!ce.exportEnabled()); - BEAST_EXPECT(ce.exportSigCollector().signatureCount(tx) == 0); + BEAST_EXPECT( + ce.postValidationExportSigCollector().fullUnionSnapshot().empty()); //@@end test-round-extension-feature-latches } @@ -2140,6 +2113,79 @@ class ConsensusExtensions_test : public beast::unit_test::suite malformed, publicKey, *position.myCommitment, true)); } + void + testProposalPrecheckUsesExportShareRelayLimits() + { + testcase("proposal precheck uses serialized ExportShare limits"); + + using enum detail::ProposalPrecheckResult; + + auto const signer = randomKeyPair(KeyType::secp256k1); + auto const owner = calcAccountID(signer.first); + auto const destination = + calcAccountID(randomKeyPair(KeyType::secp256k1).first); + auto const releaseTarget = + makeSTTx(makeExportedPayment(owner, destination)); + auto signature = ExportResultBuilder::signExportedTxn( + releaseTarget, signer.first, signer.second); + ExportShare const share{ + ExportShare::currentVersion, + owner, + makeHash("precheck-export-origin"), + 10, + makeHash("precheck-export-origin-ledger"), + makeHash("precheck-export-trigger"), + 3, + signer.first, + std::move(signature)}; + auto const serialized = share.serialize(); + std::string const frame{ + reinterpret_cast(serialized.data()), + serialized.size()}; + + BEAST_EXPECT(frame.size() > 137); + BEAST_EXPECT( + frame.size() <= ExportLimits::maxSerializedExportShareBytes); + BEAST_EXPECT(ExportShare::parse(makeSlice(frame))); + + auto const makeProposal = [&](std::vector const& frames) { + protocol::TMProposeSet set; + auto const previous = makeHash("precheck-previous-ledger"); + set.set_previousledger(previous.data(), previous.size()); + for (auto const& bytes : frames) + set.add_exportsignatures(bytes); + + ExtendedPosition position{makeHash("precheck-position")}; + position.exportSignaturesHash = + proposalExportSignaturesHash(frames); + Serializer positionData; + position.add(positionData); + set.set_currenttxhash(positionData.data(), positionData.size()); + return set; + }; + + std::vector atLimit( + ExportLimits::maxExportSharesPerRelay, frame); + auto const accepted = makeProposal(atLimit); + BEAST_EXPECT( + detail::checkProposalExtensions(accepted, true, true).result == ok); + + auto overLimit = atLimit; + overLimit.push_back(frame); + auto const rejectedCount = makeProposal(overLimit); + BEAST_EXPECT( + detail::checkProposalExtensions(rejectedCount, true, true).result == + tooManyExportSignatures); + + std::string oversized = frame; + oversized.resize(ExportLimits::maxSerializedExportShareBytes + 1, '\0'); + auto const rejectedSize = + makeProposal(std::vector{std::move(oversized)}); + BEAST_EXPECT( + detail::checkProposalExtensions(rejectedSize, true, true).result == + oversizedExportSignature); + } + void testHarvestRngDataReplacementAndRejection() { @@ -2253,54 +2299,58 @@ class ConsensusExtensions_test : public beast::unit_test::suite } void - testExportSidecarBuildsLocalSnapshot() + testExportV2CollectorBuildsAttributedUnion() { - testcase("Export sidecar builds local snapshot"); + testcase("Export V2 collector builds attributed union"); 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; + ConsensusExtensions ce{env.app(), activeNoopJournal()}; + auto& collector = ce.postValidationExportSigCollector(); + auto const origin = makeHash("v2-attributed-origin"); + auto const trigger = makeHash("v2-attributed-trigger"); + auto const signerA = randomKeyPair(KeyType::secp256k1).first; + auto const signerB = randomKeyPair(KeyType::secp256k1).first; + std::uint8_t const signatureABytes[] = {1, 2}; + std::uint8_t const signatureBBytes[] = {3, 4}; + Buffer const signatureA{signatureABytes, sizeof(signatureABytes)}; + Buffer const signatureB{signatureBBytes, sizeof(signatureBBytes)}; - 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; + BEAST_EXPECT(collector.reopenPublication(origin, trigger, 10)); + auto admit = [&](ExportSigCollectorV2::Position position, + PublicKey const& key, + Buffer const& signature) { + auto admission = collector.beginAttributedAdmission( + origin, + ExportSigCollectorV2::Contribution{position, key, signature}, + 10); + BEAST_EXPECT( + admission.result == ExportSigCollectorV2::BeginResult::verify); + BEAST_EXPECT(admission.ticket); + if (!admission.ticket) + return; + BEAST_EXPECT( + collector + .admitContribution(std::move(*admission.ticket), true, 10) + .result == ExportSigCollectorV2::AdmitResult::accepted); + }; + admit(0, signerA, signatureA); + admit(2, signerB, signatureB); - 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); + auto const snapshot = collector.fullUnionSnapshot(); + auto const found = snapshot.find(origin); + BEAST_EXPECT(found != snapshot.end()); + if (found != snapshot.end()) + { + BEAST_EXPECT(found->second.size() == 2); + BEAST_EXPECT(found->second[0].position == 0); + BEAST_EXPECT(found->second[0].signingKey == signerA); + BEAST_EXPECT(found->second[0].signature == signatureA); + BEAST_EXPECT(found->second[1].position == 2); + BEAST_EXPECT(found->second[1].signingKey == signerB); + BEAST_EXPECT(found->second[1].signature == signatureB); + } } void @@ -2391,290 +2441,77 @@ class ConsensusExtensions_test : public beast::unit_test::suite } void - testExportSidecarIgnoresCancelOnlyExports() + testAgreedExportWitnessBuildsContributorBitmap() { - testcase("Export sidecar ignores cancel-only exports"); + testcase("agreed Export witness assembles target and contributors"); 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 signerA = randomKeyPair(KeyType::secp256k1); + auto const signerB = randomKeyPair(KeyType::secp256k1); 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 releaseTarget = + makeSTTx(makeExportedPayment(calcAccountID(signerA.first), dst)); + auto const origin = makeHash("agreed-export-v2-origin"); + Blob const committee{0x05}; - 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())); + auto addContribution = [&](std::uint32_t position, + PublicKey const& publicKey, + SecretKey const& secretKey) { + auto const signature = ExportResultBuilder::signExportedTxn( + releaseTarget, publicKey, secretKey); + STObject sidecar(sfGeneric); + sidecar.setFieldU8(sfSidecarType, sidecarExportSig); + sidecar.setFieldH256(sfTransactionHash, origin); + sidecar.setFieldU32(sfTransactionIndex, position); + sidecar.setFieldVL(sfSigningPubKey, publicKey.slice()); + sidecar.setFieldVL( + sfTxnSignature, Slice{signature.data(), signature.size()}); + Serializer serialized; + sidecar.add(serialized); + map->addItem( + SHAMapNodeType::tnSIDECAR, + make_shamapitem( + sidecar.getHash(HashPrefix::sidecar), serialized.slice())); + }; + addContribution(0, signerA.first, signerA.second); + addContribution(2, signerB.first, signerB.second); map = map->snapShot(false); auto const acceptedHash = map->getHash().as_uint256(); env.app().getInboundTransactions().giveSet(acceptedHash, map, false); + auto const threshold = ExportLimits::committeeQuorumThreshold(2); + BEAST_EXPECT(threshold == 2); + BEAST_EXPECT(!ce.agreedExportWitness( + releaseTarget, origin, committee, 3, threshold)); 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); - } - } + auto const material = ce.agreedExportWitness( + releaseTarget, origin, committee, 3, threshold); + BEAST_EXPECT(material); + if (!material) + return; + BEAST_EXPECT(material->signatures.size() == 2); + BEAST_EXPECT(material->contributors == Blob{0x05}); - 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); + auto const witness = ExportResultBuilder::buildSignatureWitness( + origin, + releaseTarget, + material->signatures, + material->contributors, + 20); + BEAST_EXPECT(!witness.isFieldPresent(sfSigners)); + BEAST_EXPECT(witness.getFieldVL(sfEntropyContributors) == Blob{0x05}); + auto const& assembled = + witness.peekAtField(sfExportedTxn).downcast(); + BEAST_EXPECT(assembled.isFieldPresent(sfSigners)); + BEAST_EXPECT(assembled.getFieldArray(sfSigners).size() == 2); } void @@ -3446,7 +3283,7 @@ class ConsensusExtensions_test : public beast::unit_test::suite FakeExtensions ext; ext.localExportSigs = false; - ext.consensusExportTxns = true; + ext.livePendingExportLatches = true; ExtensionTickHarness harness; auto const peerHash = makeHash("peer-export-sig-set"); @@ -3471,7 +3308,7 @@ class ConsensusExtensions_test : public beast::unit_test::suite FakeExtensions ext; ext.localExportSigs = false; - ext.consensusExportTxns = false; + ext.livePendingExportLatches = false; ExtensionTickHarness harness; auto const peerHash = makeHash("empty-round-export-sig-set"); @@ -3535,7 +3372,7 @@ class ConsensusExtensions_test : public beast::unit_test::suite FakeExtensions ext; ext.localExportSigs = false; - ext.consensusExportTxns = true; + ext.livePendingExportLatches = true; ExtensionTickHarness harness; auto result = harness.tick(ext); @@ -3602,19 +3439,31 @@ class ConsensusExtensions_test : public beast::unit_test::suite using namespace jtx; Env env{*this, envconfig(), supported_amendments(), nullptr}; ConsensusExtensions ce{env.app(), env.journal}; - auto const tx = makeHash("export-disabled-clears-collector"); + auto const origin = makeHash("export-disabled-clears-collector"); + auto const trigger = makeHash("export-disabled-trigger"); auto const pk = makeValidatorKeys().front(); std::uint8_t const sigBytes[] = {1, 2, 3}; Buffer const sig{sigBytes, sizeof(sigBytes)}; ce.setExportEnabledThisRound(true); - ce.exportSigCollector().addVerifiedSignature(tx, pk, sig, 10); + BEAST_EXPECT(ce.postValidationExportSigCollector().reopenPublication( + origin, trigger, 10)); + auto admission = + ce.postValidationExportSigCollector().beginAttributedAdmission( + origin, ExportSigCollectorV2::Contribution{0, pk, sig}, 10); + BEAST_EXPECT(admission.ticket); + if (admission.ticket) + ce.postValidationExportSigCollector().admitContribution( + std::move(*admission.ticket), true, 10); ce.clearRngState(); - BEAST_EXPECT(ce.exportSigCollector().signatureCount(tx) == 1); + BEAST_EXPECT( + ce.postValidationExportSigCollector().fullUnionSnapshot().size() == + 1); ce.setExportEnabledThisRound(false); ce.clearRngState(); - BEAST_EXPECT(ce.exportSigCollector().signatureCount(tx) == 0); + BEAST_EXPECT( + ce.postValidationExportSigCollector().fullUnionSnapshot().empty()); } void @@ -3662,134 +3511,6 @@ class ConsensusExtensions_test : public beast::unit_test::suite BEAST_EXPECT(ce.pendingCommitCount() == 0); } - void - testReplayedProposalHarvestsExportSigs() - { - testcase("Replayed proposal harvests export signatures"); - - using namespace jtx; - Env env{ - *this, envconfig(validator, ""), supported_amendments(), nullptr}; - auto const& valKeys = env.app().getValidatorKeys(); - BEAST_EXPECT(valKeys.keys); - if (!valKeys.keys) - return; - - ConsensusExtensions ce{env.app(), env.journal}; - ce.setExportEnabledThisRound(true); - ce.cacheUNLReport(); - - auto const activeView = ce.activeValidatorView(); - BEAST_EXPECT(activeView->sourceLedgerHash); - if (!activeView->sourceLedgerHash) - return; - - auto const senderPK = valKeys.keys->publicKey; - auto const senderSK = valKeys.keys->secretKey; - BEAST_EXPECT(ce.isActiveValidator(senderPK, *activeView)); - if (!ce.isActiveValidator(senderPK, *activeView)) - return; - - auto const tx = makeHash("replayed-export-sig-tx"); - auto const blob = makeExportSigBlob(tx, senderPK); - ExtendedPosition position{makeHash("replayed-position")}; - position.myCommitment = makeHash("replayed-position-commitment"); - position.exportSignaturesHash = - proposalExportSignaturesHash(std::vector{blob}); - auto const prevLedger = *activeView->sourceLedgerHash; - auto const closeTime = NetClock::time_point{NetClock::duration{77}}; - - // Stored proposals can arrive from cluster relay paths. Extension - // sidecars are ledger inputs, so an invalid proposal signature must - // harvest neither RNG claims nor export signature blobs. - ce.onTrustedPeerProposal( - calcNodeID(senderPK), - senderPK, - position, - 0, - closeTime, - prevLedger, - Slice{}, - std::vector{blob}); - BEAST_EXPECT(!ce.exportSigCollector().hasUnverifiedSignatures()); - BEAST_EXPECT(ce.pendingCommitCount() == 0); - - auto const sig = signPosition( - senderPK, senderSK, position, 0, closeTime, prevLedger); - - ce.onTrustedPeerProposal( - calcNodeID(senderPK), - senderPK, - position, - 0, - closeTime, - prevLedger, - Slice(sig.data(), sig.size()), - std::vector{blob}); - - BEAST_EXPECT(ce.exportSigCollector().hasUnverifiedSignatures()); - BEAST_EXPECT(ce.pendingCommitCount() == 1); - } - - void - testWireProposalHarvestsExportSigs() - { - testcase("wire proposal harvests export signatures"); - - using namespace jtx; - Env env{ - *this, envconfig(validator, ""), supported_amendments(), nullptr}; - auto const& valKeys = env.app().getValidatorKeys(); - BEAST_EXPECT(valKeys.keys); - if (!valKeys.keys) - return; - - ConsensusExtensions ce{env.app(), activeNoopJournal()}; - ce.setExportEnabledThisRound(true); - ce.cacheUNLReport(); - - protocol::TMProposeSet wire; - auto const& senderPK = valKeys.keys->publicKey; - wire.set_nodepubkey(senderPK.data(), senderPK.size()); - auto const prevLedger = *ce.activeValidatorView()->sourceLedgerHash; - wire.set_previousledger(prevLedger.data(), prevLedger.size()); - auto const tx = makeHash("wire-export-sig-tx"); - auto const blob = makeExportSigBlob(tx, senderPK); - wire.add_exportsignatures(blob); - ExtendedPosition position{makeHash("wire-position")}; - position.exportSignaturesHash = - proposalExportSignaturesHash(wire.exportsignatures()); - setWirePosition(wire, position); - - ce.onTrustedPeerMessage(wire); - BEAST_EXPECT(ce.exportSigCollector().hasUnverifiedSignatures()); - auto const beforeMalformed = - ce.exportSigCollector().unverifiedSignatures(tx); - BEAST_EXPECT(beforeMalformed.size() == 1); - - protocol::TMProposeSet mismatch; - mismatch.set_nodepubkey(senderPK.data(), senderPK.size()); - mismatch.set_previousledger(prevLedger.data(), prevLedger.size()); - mismatch.add_exportsignatures(blob); - ExtendedPosition mismatchPosition{makeHash("wire-mismatch-position")}; - mismatchPosition.exportSignaturesHash = - proposalExportSignaturesHash(std::vector{"other"}); - setWirePosition(mismatch, mismatchPosition); - ce.onTrustedPeerMessage(mismatch); - BEAST_EXPECT( - ce.exportSigCollector().unverifiedSignatures(tx) == - beforeMalformed); - - protocol::TMProposeSet malformed; - malformed.add_exportsignatures(blob); - malformed.set_nodepubkey("bad", 3); - setWirePosition(malformed, position); - ce.onTrustedPeerMessage(malformed); - auto const afterMalformed = - ce.exportSigCollector().unverifiedSignatures(tx); - BEAST_EXPECT(afterMalformed == beforeMalformed); - } - void testPublicHookNoopAndFailureBranches() { @@ -3969,15 +3690,12 @@ public: testTier2ThresholdAnchorsToOriginalView(); testOnPreBuildTier2WithNegativeUNL(); testProposalProofRoundTrip(); + testProposalPrecheckUsesExportShareRelayLimits(); testHarvestRngDataReplacementAndRejection(); - testExportSidecarBuildsLocalSnapshot(); + testExportV2CollectorBuildsAttributedUnion(); testTransactionAcquireRejectsSidecarWireNodes(); testAcquiredSetsRejectConsensusExtensionPseudos(); - testExportSidecarIgnoresCancelOnlyExports(); - testExportSidecarBuildCapsConsensusCandidates(); - testExportAgreedSignaturesIgnoreLiveCollectorMutation(); - testExportAgreedSignaturesTrustAcceptedRootMembership(); - testOnPreBuildPreservesExportDecision(); + testAgreedExportWitnessBuildsContributorBitmap(); testRngSidecarBuildsLocalSnapshots(); testOnPreBuildInjectsStandaloneEntropy(); testOnPreBuildEntropyMismatchKeepsAgreed(); @@ -4012,8 +3730,6 @@ public: testParticipantDiagnosticsOnlyWhenExtensionEnabled(); testExportDisabledRoundClearsCollector(); testValidatorKeylessAuthoringNoops(); - testReplayedProposalHarvestsExportSigs(); - testWireProposalHarvestsExportSigs(); testPublicHookNoopAndFailureBranches(); testDecorateMessageStoresSelfProofs(); }