feat: add XPOP test helper and XPOP_test suite

- src/test/jtx/xpop.h: test utilities for building XPOPs from Env ledgers
  (TestValidator, TestVLPublisher, TestXPOPContext, buildTestXPOP)
- src/test/app/XPOP_test.cpp: 4 tests (173 assertions)
  - LedgerProof construction from payment tx
  - XPOP v1 JSON structure verification
  - Merkle proof verification for multi-tx ledgers
  - Full Import round-trip: source Env payment → XPOP → dest Env Import → tesSUCCESS
This commit is contained in:
Nicholas Dudfield
2026-03-18 11:47:34 +07:00
parent 3ca056a94b
commit cea110f29a
2 changed files with 129 additions and 20 deletions

View File

@@ -18,11 +18,13 @@
//==============================================================================
#include <test/jtx.h>
#include <test/jtx/import.h>
#include <test/jtx/xpop.h>
#include <xrpld/app/ledger/LedgerMaster.h>
#include <xrpld/app/proof/LedgerProof.h>
#include <xrpld/app/proof/ProofBuilder.h>
#include <xrpld/app/proof/XPOPv1.h>
#include <xrpl/protocol/Import.h>
#include <xrpl/protocol/jss.h>
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<SHAMapItem const> 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();
}
};

View File

@@ -100,7 +100,8 @@ struct TestVLPublisher
buildVLData(
std::vector<TestValidator> 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<TestValidator> validators;
TestVLPublisher publisher;
proof::VLData vlData;
static TestXPOPContext
create(int validatorCount = 5)
{
auto pub = TestVLPublisher::create();
std::vector<TestValidator> 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<Config>
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<proof::ValidatorKeys> 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<TestValidator> validators;
std::vector<proof::ValidatorKeys> 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.