mirror of
https://github.com/Xahau/xahaud.git
synced 2026-04-29 15:37:46 +00:00
- 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
302 lines
9.6 KiB
C++
302 lines
9.6 KiB
C++
//------------------------------------------------------------------------------
|
|
/*
|
|
This file is part of rippled: https://github.com/ripple/rippled
|
|
Copyright (c) 2026 XRPL Labs
|
|
|
|
Permission to use, copy, modify, and/or distribute this software for any
|
|
purpose with or without fee is hereby granted, provided that the above
|
|
copyright notice and this permission notice appear in all copies.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
*/
|
|
//==============================================================================
|
|
|
|
#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 {
|
|
namespace test {
|
|
|
|
struct XPOP_test : public beast::unit_test::suite
|
|
{
|
|
void
|
|
testBuildLedgerProof()
|
|
{
|
|
testcase("Build LedgerProof from a payment");
|
|
|
|
using namespace jtx;
|
|
|
|
Env env{*this};
|
|
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
|
|
env.fund(XRP(10000), alice, bob);
|
|
env.close();
|
|
|
|
// Submit a payment and close the ledger.
|
|
env(pay(alice, bob, XRP(100)));
|
|
env.close();
|
|
|
|
// Get the tx hash from the last closed ledger.
|
|
auto const lcl = env.app().getLedgerMaster().getClosedLedger();
|
|
BEAST_EXPECT(lcl);
|
|
|
|
// Find a payment tx in the ledger.
|
|
uint256 paymentHash;
|
|
bool found = false;
|
|
lcl->txMap().visitLeaves(
|
|
[&](boost::intrusive_ptr<SHAMapItem const> const& item) {
|
|
if (!found)
|
|
{
|
|
paymentHash = item->key();
|
|
found = true;
|
|
}
|
|
});
|
|
BEAST_EXPECT(found);
|
|
|
|
// Build the proof.
|
|
auto const lp = proof::buildLedgerProof(*lcl, paymentHash);
|
|
BEAST_EXPECT(lp.has_value());
|
|
|
|
if (lp)
|
|
{
|
|
// Verify header fields are populated.
|
|
BEAST_EXPECT(lp->ledgerIndex > 0);
|
|
BEAST_EXPECT(lp->totalCoins > 0);
|
|
BEAST_EXPECT(lp->parentHash != uint256{});
|
|
BEAST_EXPECT(lp->txRoot != uint256{});
|
|
BEAST_EXPECT(lp->accountRoot != uint256{});
|
|
|
|
// Verify tx blob is non-empty.
|
|
BEAST_EXPECT(!lp->txBlob.empty());
|
|
BEAST_EXPECT(!lp->metaBlob.empty());
|
|
|
|
// Verify merkle proof exists and is valid.
|
|
BEAST_EXPECT(lp->txProof.has_value());
|
|
if (lp->txProof)
|
|
{
|
|
auto const computedRoot = lp->txProof->computeRoot();
|
|
BEAST_EXPECT(computedRoot.has_value());
|
|
if (computedRoot)
|
|
BEAST_EXPECT(*computedRoot == lp->txRoot);
|
|
}
|
|
|
|
// Verify ledger hash reconstruction.
|
|
auto const computedHash = lp->computeLedgerHash();
|
|
BEAST_EXPECT(computedHash == lcl->info().hash);
|
|
}
|
|
}
|
|
|
|
void
|
|
testBuildXPOPv1()
|
|
{
|
|
testcase("Build XPOP v1 JSON from a payment");
|
|
|
|
using namespace jtx;
|
|
|
|
Env env{*this};
|
|
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
|
|
env.fund(XRP(10000), alice, bob);
|
|
env.close();
|
|
|
|
env(pay(alice, bob, XRP(100)));
|
|
env.close();
|
|
|
|
auto const lcl = env.app().getLedgerMaster().getClosedLedger();
|
|
BEAST_EXPECT(lcl);
|
|
|
|
// Find a tx.
|
|
uint256 txHash;
|
|
lcl->txMap().visitLeaves(
|
|
[&](boost::intrusive_ptr<SHAMapItem const> const& item) {
|
|
txHash = item->key();
|
|
});
|
|
|
|
// Build XPOP using the test helper.
|
|
auto const xpop = xpop::buildTestXPOP(env, txHash, 3);
|
|
BEAST_EXPECT(!xpop.isNull());
|
|
|
|
// Verify structure.
|
|
BEAST_EXPECT(xpop.isMember(jss::ledger));
|
|
BEAST_EXPECT(xpop.isMember(jss::transaction));
|
|
BEAST_EXPECT(xpop.isMember(jss::validation));
|
|
|
|
// Ledger section.
|
|
auto const& lgr = xpop[jss::ledger];
|
|
BEAST_EXPECT(lgr.isMember(jss::index));
|
|
BEAST_EXPECT(lgr.isMember(jss::coins));
|
|
BEAST_EXPECT(lgr.isMember(jss::phash));
|
|
BEAST_EXPECT(lgr.isMember(jss::txroot));
|
|
BEAST_EXPECT(lgr.isMember(jss::acroot));
|
|
BEAST_EXPECT(lgr.isMember(jss::close));
|
|
BEAST_EXPECT(lgr.isMember(jss::pclose));
|
|
BEAST_EXPECT(lgr.isMember(jss::cres));
|
|
BEAST_EXPECT(lgr.isMember(jss::flags));
|
|
|
|
// Transaction section.
|
|
auto const& txn = xpop[jss::transaction];
|
|
BEAST_EXPECT(txn.isMember(jss::blob));
|
|
BEAST_EXPECT(txn.isMember(jss::meta));
|
|
BEAST_EXPECT(txn.isMember(jss::proof));
|
|
BEAST_EXPECT(txn[jss::blob].asString().size() > 0);
|
|
BEAST_EXPECT(txn[jss::meta].asString().size() > 0);
|
|
|
|
// Validation section.
|
|
auto const& val = xpop[jss::validation];
|
|
BEAST_EXPECT(val.isMember(jss::data));
|
|
BEAST_EXPECT(val.isMember(jss::unl));
|
|
BEAST_EXPECT(val[jss::data].size() == 3); // 3 validators
|
|
|
|
auto const& unl = val[jss::unl];
|
|
BEAST_EXPECT(unl.isMember(jss::public_key));
|
|
BEAST_EXPECT(unl.isMember(jss::manifest));
|
|
BEAST_EXPECT(unl.isMember(jss::blob));
|
|
BEAST_EXPECT(unl.isMember(jss::signature));
|
|
BEAST_EXPECT(unl.isMember(jss::version));
|
|
}
|
|
|
|
void
|
|
testMerkleProofVerification()
|
|
{
|
|
testcase("Merkle proof verifies against tx root");
|
|
|
|
using namespace jtx;
|
|
|
|
Env env{*this};
|
|
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
Account const carol{"carol"};
|
|
|
|
env.fund(XRP(10000), alice, bob, carol);
|
|
env.close();
|
|
|
|
// Multiple transactions to create a deeper trie.
|
|
env(pay(alice, bob, XRP(10)));
|
|
env(pay(bob, carol, XRP(5)));
|
|
env(pay(carol, alice, XRP(1)));
|
|
env.close();
|
|
|
|
auto const lcl = env.app().getLedgerMaster().getClosedLedger();
|
|
BEAST_EXPECT(lcl);
|
|
|
|
// Verify proof for each transaction in the ledger.
|
|
int proofCount = 0;
|
|
lcl->txMap().visitLeaves(
|
|
[&](boost::intrusive_ptr<SHAMapItem const> const& item) {
|
|
auto const lp = proof::buildLedgerProof(*lcl, item->key());
|
|
BEAST_EXPECT(lp.has_value());
|
|
|
|
if (lp && lp->txProof)
|
|
{
|
|
// Proof must verify against the ledger's tx root.
|
|
BEAST_EXPECT(lp->txProof->verify(lp->txRoot));
|
|
|
|
// JSON v1 serialization must round-trip.
|
|
auto const json = lp->txProof->toJsonV1();
|
|
BEAST_EXPECT(!json.isNull());
|
|
BEAST_EXPECT(json.isArray());
|
|
|
|
++proofCount;
|
|
}
|
|
});
|
|
|
|
// We should have proven at least 3 transactions.
|
|
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();
|
|
}
|
|
};
|
|
|
|
BEAST_DEFINE_TESTSUITE(XPOP, app, ripple);
|
|
|
|
} // namespace test
|
|
} // namespace ripple
|