diff --git a/src/test/app/XPOP_test.cpp b/src/test/app/XPOP_test.cpp index 62134c30c..5ee99008a 100644 --- a/src/test/app/XPOP_test.cpp +++ b/src/test/app/XPOP_test.cpp @@ -18,11 +18,13 @@ //============================================================================== #include +#include #include #include #include #include #include +#include #include namespace ripple { @@ -220,12 +222,76 @@ struct XPOP_test : public beast::unit_test::suite BEAST_EXPECT(proofCount >= 3); } + void + testImportWithGeneratedXPOP() + { + testcase("Import accepts dynamically generated XPOP"); + + using namespace jtx; + + // Create XPOP context (VL publisher + validators). + auto const xpopCtx = xpop::TestXPOPContext::create(3); + + // --- Source "network": generate a payment and build XPOP --- + Env srcEnv{*this}; + Account const alice{"alice"}; + Account const bob{"bob"}; + + srcEnv.fund(XRP(10000), alice, bob); + srcEnv.close(); + + // Import requires: no sfNetworkID + sfOperationLimit = dest NETWORK_ID. + Json::Value payTx; + payTx[jss::TransactionType] = jss::Payment; + payTx[jss::Account] = alice.human(); + payTx[jss::Destination] = bob.human(); + payTx[jss::Amount] = "100000000"; + payTx[sfOperationLimit.jsonName] = 21337; + srcEnv(payTx, fee(XRP(1))); + srcEnv.close(); + + // Find the tx hash and build the XPOP. + auto const srcLcl = srcEnv.app().getLedgerMaster().getClosedLedger(); + BEAST_EXPECT(srcLcl); + + uint256 paymentHash; + srcLcl->txMap().visitLeaves( + [&](boost::intrusive_ptr const& item) { + paymentHash = item->key(); + }); + + auto const xpopJson = xpopCtx.buildXPOP(*srcLcl, paymentHash); + BEAST_EXPECT(!xpopJson.isNull()); + + // --- Destination "network": import the XPOP --- + Env dstEnv{*this, xpopCtx.makeEnvConfig(21337)}; + + // Burn some XRP so B2M can credit. + auto const master = Account("masterpassphrase"); + dstEnv(noop(master), fee(10'000'000'000), ter(tesSUCCESS)); + dstEnv.close(); + + Account const importAlice{"alice"}; + dstEnv.fund(XRP(1000), importAlice); + dstEnv.close(); + + auto const feeDrops = dstEnv.current()->fees().base; + + // Submit the import — should succeed (B2M path). + dstEnv( + import::import(importAlice, xpopJson), + fee(feeDrops * 10), + ter(tesSUCCESS)); + dstEnv.close(); + } + void run() override { testBuildLedgerProof(); testBuildXPOPv1(); testMerkleProofVerification(); + testImportWithGeneratedXPOP(); } }; diff --git a/src/test/jtx/xpop.h b/src/test/jtx/xpop.h index 106484faf..37d74e35c 100644 --- a/src/test/jtx/xpop.h +++ b/src/test/jtx/xpop.h @@ -100,7 +100,8 @@ struct TestVLPublisher buildVLData( std::vector const& validators, std::uint32_t sequence = 1, - std::uint32_t expiration = 0xFFFFFFFF) const + std::uint32_t expiration = + 767784645) const // ~2024, matches Import_test { // Build the JSON blob std::string data = "{\"sequence\":" + std::to_string(sequence) + @@ -126,30 +127,72 @@ struct TestVLPublisher } }; +/// Everything needed to build and import XPOPs in tests. +struct TestXPOPContext +{ + std::vector validators; + TestVLPublisher publisher; + proof::VLData vlData; + + static TestXPOPContext + create(int validatorCount = 5) + { + auto pub = TestVLPublisher::create(); + std::vector vals; + for (int i = 0; i < validatorCount; ++i) + vals.push_back(TestValidator::create()); + auto vl = pub.buildVLData(vals); + return {std::move(vals), std::move(pub), std::move(vl)}; + } + + /// Get the VL master public key hex for IMPORT_VL_KEYS config. + std::string + vlKeyHex() const + { + return strHex(publisher.masterPublic); + } + + /// Build an Env config with NETWORK_ID and IMPORT_VL_KEYS set. + std::unique_ptr + makeEnvConfig(std::uint32_t networkID = 21337) const + { + auto cfg = envconfig(jtx::validator, ""); + cfg->NETWORK_ID = networkID; + auto const keyHex = vlKeyHex(); + auto const pkHex = strUnHex(keyHex); + if (pkHex) + cfg->IMPORT_VL_KEYS.emplace(keyHex, makeSlice(*pkHex)); + return cfg; + } + + /// Build XPOP from a closed ledger for a specific tx. + Json::Value + buildXPOP(Ledger const& ledger, uint256 const& txHash) const + { + std::vector valKeys; + for (auto const& v : validators) + valKeys.push_back(v.toValidatorKeys()); + return proof::buildXPOPv1(ledger, txHash, valKeys, vlData); + } + + /// Build XPOP from an Env's last closed ledger. + Json::Value + buildXPOP(Env& env, uint256 const& txHash) const + { + auto const lcl = env.app().getLedgerMaster().getClosedLedger(); + if (!lcl) + return {}; + return buildXPOP(*lcl, txHash); + } +}; + /// Build a complete XPOP v1 JSON from an Env's last closed ledger. /// Creates fresh validator keys and VL publisher for each call. inline Json::Value buildTestXPOP(Env& env, uint256 const& txHash, int validatorCount = 5) { - // Create validators - std::vector validators; - std::vector valKeys; - for (int i = 0; i < validatorCount; ++i) - { - validators.push_back(TestValidator::create()); - valKeys.push_back(validators.back().toValidatorKeys()); - } - - // Create VL publisher - auto const publisher = TestVLPublisher::create(); - auto const vlData = publisher.buildVLData(validators); - - // Build XPOP from the last closed ledger - auto const lcl = env.app().getLedgerMaster().getClosedLedger(); - if (!lcl) - return {}; - - return proof::buildXPOPv1(*lcl, txHash, valKeys, vlData); + auto ctx = TestXPOPContext::create(validatorCount); + return ctx.buildXPOP(env, txHash); } /// Get the hex-encoded XPOP blob suitable for sfBlob in ttIMPORT.