mirror of
https://github.com/Xahau/xahaud.git
synced 2026-04-29 15:37:46 +00:00
feat: add XPOP test helper and XPOP_test suite
- src/test/jtx/xpop.h: test utilities for building XPOPs from Env ledgers (TestValidator, TestVLPublisher, buildTestXPOP) - src/test/app/XPOP_test.cpp: 3 tests (133 assertions) - LedgerProof construction from payment tx - XPOP v1 JSON structure verification - Merkle proof verification for multi-tx ledgers
This commit is contained in:
235
src/test/app/XPOP_test.cpp
Normal file
235
src/test/app/XPOP_test.cpp
Normal file
@@ -0,0 +1,235 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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/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/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
|
||||
run() override
|
||||
{
|
||||
testBuildLedgerProof();
|
||||
testBuildXPOPv1();
|
||||
testMerkleProofVerification();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(XPOP, app, ripple);
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
170
src/test/jtx/xpop.h
Normal file
170
src/test/jtx/xpop.h
Normal file
@@ -0,0 +1,170 @@
|
||||
#ifndef RIPPLE_TEST_JTX_XPOP_H_INCLUDED
|
||||
#define RIPPLE_TEST_JTX_XPOP_H_INCLUDED
|
||||
|
||||
#include <test/jtx/Env.h>
|
||||
#include <xrpld/app/ledger/LedgerMaster.h>
|
||||
#include <xrpld/app/proof/LedgerProof.h>
|
||||
#include <xrpld/app/proof/XPOPv1.h>
|
||||
#include <xrpl/basics/StringUtilities.h>
|
||||
#include <xrpl/basics/base64.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
#include <xrpl/protocol/Sign.h>
|
||||
#include <xrpl/protocol/digest.h>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
namespace jtx {
|
||||
namespace xpop {
|
||||
|
||||
/// Build a manifest string (binary, not base64).
|
||||
inline std::string
|
||||
makeManifestRaw(
|
||||
PublicKey const& masterPub,
|
||||
SecretKey const& masterSec,
|
||||
PublicKey const& signingPub,
|
||||
SecretKey const& signingSec,
|
||||
int seq = 1)
|
||||
{
|
||||
STObject st(sfGeneric);
|
||||
st[sfSequence] = seq;
|
||||
st[sfPublicKey] = masterPub;
|
||||
st[sfSigningPubKey] = signingPub;
|
||||
|
||||
sign(st, HashPrefix::manifest, *publicKeyType(signingPub), signingSec);
|
||||
sign(
|
||||
st,
|
||||
HashPrefix::manifest,
|
||||
*publicKeyType(masterPub),
|
||||
masterSec,
|
||||
sfMasterSignature);
|
||||
|
||||
Serializer s;
|
||||
st.add(s);
|
||||
return std::string(static_cast<char const*>(s.data()), s.size());
|
||||
}
|
||||
|
||||
/// A complete test validator with all keys and manifest.
|
||||
struct TestValidator
|
||||
{
|
||||
PublicKey masterPublic;
|
||||
SecretKey masterSecret;
|
||||
PublicKey signingPublic;
|
||||
SecretKey signingSecret;
|
||||
std::string manifestRaw;
|
||||
std::string manifestBase64;
|
||||
|
||||
static TestValidator
|
||||
create()
|
||||
{
|
||||
auto const ms = randomSecretKey();
|
||||
auto const mp = derivePublicKey(KeyType::ed25519, ms);
|
||||
auto const [sp, ss] = randomKeyPair(KeyType::secp256k1);
|
||||
auto raw = makeManifestRaw(mp, ms, sp, ss, 1);
|
||||
return {mp, ms, sp, ss, raw, base64_encode(raw)};
|
||||
}
|
||||
|
||||
proof::ValidatorKeys
|
||||
toValidatorKeys() const
|
||||
{
|
||||
return {
|
||||
masterPublic,
|
||||
masterSecret,
|
||||
signingPublic,
|
||||
signingSecret,
|
||||
manifestBase64};
|
||||
}
|
||||
};
|
||||
|
||||
/// A complete test VL publisher with keys and manifest.
|
||||
struct TestVLPublisher
|
||||
{
|
||||
PublicKey masterPublic;
|
||||
SecretKey masterSecret;
|
||||
PublicKey signingPublic;
|
||||
SecretKey signingSecret;
|
||||
std::string manifestBase64;
|
||||
|
||||
static TestVLPublisher
|
||||
create()
|
||||
{
|
||||
auto const ms = randomSecretKey();
|
||||
auto const mp = derivePublicKey(KeyType::ed25519, ms);
|
||||
auto const [sp, ss] = randomKeyPair(KeyType::secp256k1);
|
||||
return {
|
||||
mp, ms, sp, ss, base64_encode(makeManifestRaw(mp, ms, sp, ss, 1))};
|
||||
}
|
||||
|
||||
/// Build VL data for these validators.
|
||||
proof::VLData
|
||||
buildVLData(
|
||||
std::vector<TestValidator> const& validators,
|
||||
std::uint32_t sequence = 1,
|
||||
std::uint32_t expiration = 0xFFFFFFFF) const
|
||||
{
|
||||
// Build the JSON blob
|
||||
std::string data = "{\"sequence\":" + std::to_string(sequence) +
|
||||
",\"expiration\":" + std::to_string(expiration) +
|
||||
",\"validators\":[";
|
||||
|
||||
for (std::size_t i = 0; i < validators.size(); ++i)
|
||||
{
|
||||
if (i > 0)
|
||||
data += ",";
|
||||
data += "{\"validation_public_key\":\"" +
|
||||
strHex(validators[i].masterPublic) + "\",\"manifest\":\"" +
|
||||
validators[i].manifestBase64 + "\"}";
|
||||
}
|
||||
data += "]}";
|
||||
|
||||
auto const blob = base64_encode(data);
|
||||
auto const sig =
|
||||
strHex(sign(signingPublic, signingSecret, makeSlice(data)));
|
||||
|
||||
return proof::VLData{
|
||||
masterPublic, masterSecret, manifestBase64, blob, sig, 1};
|
||||
}
|
||||
};
|
||||
|
||||
/// 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);
|
||||
}
|
||||
|
||||
/// Get the hex-encoded XPOP blob suitable for sfBlob in ttIMPORT.
|
||||
inline std::string
|
||||
buildTestXPOPHex(Env& env, uint256 const& txHash, int validatorCount = 5)
|
||||
{
|
||||
auto const xpop = buildTestXPOP(env, txHash, validatorCount);
|
||||
if (xpop.isNull())
|
||||
return {};
|
||||
return proof::xpopToHex(xpop);
|
||||
}
|
||||
|
||||
} // namespace xpop
|
||||
} // namespace jtx
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
@@ -52,11 +52,11 @@ buildXPOPv1(
|
||||
std::vector<ValidatorKeys> const& validators,
|
||||
VLData const& vl);
|
||||
|
||||
/// Convenience: build XPOP from a ReadView + tx hash + validator keys.
|
||||
/// Convenience: build XPOP from a Ledger + tx hash + validator keys.
|
||||
/// Combines buildLedgerProof + buildXPOPv1.
|
||||
Json::Value
|
||||
buildXPOPv1(
|
||||
ReadView const& ledger,
|
||||
Ledger const& ledger,
|
||||
uint256 const& txHash,
|
||||
std::vector<ValidatorKeys> const& validators,
|
||||
VLData const& vl);
|
||||
|
||||
Reference in New Issue
Block a user