Files
xahaud/src/test/app/Export_test.cpp
Nicholas Dudfield 034010716e feat: add signature verification cache for export signatures
Add cryptographic verification of export signatures as they arrive:
- stashTxnData() caches serialized txn for verification
- verifyAndAddSignature() verifies against cached data, rejects invalid
- isSignatureVerified() / verifySignature() for Transactor fallback
- Cleanup methods updated to clear verification cache

Also removes leftover debug std::cerr from OpenView, STObject, and tests.
2026-01-27 18:03:55 +07:00

581 lines
20 KiB
C++

//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2012, 2013 Ripple Labs Inc.
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 <ripple/protocol/Feature.h>
#include <ripple/protocol/Indexes.h>
#include <ripple/protocol/jss.h>
#include <test/app/Export_test_hooks.h>
#include <test/jtx.h>
#include <test/jtx/hook.h>
#include <map>
namespace ripple {
namespace test {
using TestHook = std::vector<uint8_t> const&;
// Large fee for hook operations
#define HSFEE fee(100'000'000)
//------------------------------------------------------------------------------
// Per-partition debug logging for tests
// Usage:
// auto logs = std::make_unique<DebugLogs>(*this, DebugLogs::Levels{
// {"View", kTrace}, // Hook operations
// {"TxQ", kDebug}, // Transaction queue
// });
// Env env{*this, envconfig(), features, std::move(logs), kError};
//------------------------------------------------------------------------------
class DebugLogs : public Logs
{
public:
using Levels = std::map<std::string, beast::severities::Severity>;
private:
beast::unit_test::suite& suite_;
Levels levels_;
public:
DebugLogs(beast::unit_test::suite& suite, Levels levels = {})
: Logs(beast::severities::kError)
, suite_(suite)
, levels_(std::move(levels))
{
}
std::unique_ptr<beast::Journal::Sink>
makeSink(
std::string const& partition,
beast::severities::Severity defaultThresh) override
{
auto thresh = defaultThresh;
if (auto it = levels_.find(partition); it != levels_.end())
thresh = it->second;
return std::make_unique<SuiteJournalSink>(partition, thresh, suite_);
}
};
struct Export_test : public beast::unit_test::suite
{
// Bare bones hook that just accepts
TestHook accept_wasm = export_test_wasm[R"[test.hook](
#include <stdint.h>
extern int32_t _g(uint32_t id, uint32_t maxiter);
extern int64_t accept(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
int64_t hook(uint32_t reserved) {
_g(1, 1);
accept(0, 0, 0);
return 0;
}
)[test.hook]"];
// Hook that emits a payment to bob (passed via otxn_param)
TestHook emit_wasm = export_test_wasm[R"[test.hook](
#include <stdint.h>
extern int32_t _g(uint32_t id, uint32_t maxiter);
extern int64_t accept(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t rollback(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t emit(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len);
extern int64_t etxn_reserve(uint32_t count);
extern int64_t etxn_details(uint32_t write_ptr, uint32_t write_len);
extern int64_t etxn_fee_base(uint32_t read_ptr, uint32_t read_len);
extern int64_t hook_account(uint32_t write_ptr, uint32_t write_len);
extern int64_t otxn_param(uint32_t write_ptr, uint32_t write_len, uint32_t name_ptr, uint32_t name_len);
extern int64_t ledger_seq(void);
#define SBUF(x) (uint32_t)(x), sizeof(x)
#define ASSERT(x) if (!(x)) rollback((uint32_t)#x, sizeof(#x), __LINE__)
#define ttPAYMENT 0
#define tfCANONICAL 0x80000000UL
#define amAMOUNT 1
#define amFEE 8
#define atACCOUNT 1
#define atDESTINATION 3
#define ENCODE_TT(buf_out, tt) \
buf_out[0] = 0x12U; \
buf_out[1] = (tt >> 8) & 0xFFU; \
buf_out[2] = tt & 0xFFU; \
buf_out += 3;
#define ENCODE_FLAGS(buf_out, flags) \
buf_out[0] = 0x22U; \
buf_out[1] = (flags >> 24) & 0xFFU; \
buf_out[2] = (flags >> 16) & 0xFFU; \
buf_out[3] = (flags >> 8) & 0xFFU; \
buf_out[4] = flags & 0xFFU; \
buf_out += 5;
#define ENCODE_SEQUENCE(buf_out, seq) \
buf_out[0] = 0x24U; \
buf_out[1] = (seq >> 24) & 0xFFU; \
buf_out[2] = (seq >> 16) & 0xFFU; \
buf_out[3] = (seq >> 8) & 0xFFU; \
buf_out[4] = seq & 0xFFU; \
buf_out += 5;
#define ENCODE_FLS(buf_out, fls) \
buf_out[0] = 0x20U; \
buf_out[1] = 0x1AU; \
buf_out[2] = (fls >> 24) & 0xFFU; \
buf_out[3] = (fls >> 16) & 0xFFU; \
buf_out[4] = (fls >> 8) & 0xFFU; \
buf_out[5] = fls & 0xFFU; \
buf_out += 6;
#define ENCODE_LLS(buf_out, lls) \
buf_out[0] = 0x20U; \
buf_out[1] = 0x1BU; \
buf_out[2] = (lls >> 24) & 0xFFU; \
buf_out[3] = (lls >> 16) & 0xFFU; \
buf_out[4] = (lls >> 8) & 0xFFU; \
buf_out[5] = lls & 0xFFU; \
buf_out += 6;
#define ENCODE_DROPS(buf_out, drops, amt_type) \
buf_out[0] = 0x60U + amt_type; \
buf_out[1] = 0x40U + ((drops >> 56) & 0x3FU); \
buf_out[2] = (drops >> 48) & 0xFFU; \
buf_out[3] = (drops >> 40) & 0xFFU; \
buf_out[4] = (drops >> 32) & 0xFFU; \
buf_out[5] = (drops >> 24) & 0xFFU; \
buf_out[6] = (drops >> 16) & 0xFFU; \
buf_out[7] = (drops >> 8) & 0xFFU; \
buf_out[8] = drops & 0xFFU; \
buf_out += 9;
#define ENCODE_SIGNING_PUBKEY_NULL(buf_out) \
buf_out[0] = 0x73U; \
buf_out[1] = 0x21U; \
for (int i = 2; i < 35; ++i) buf_out[i] = 0; \
buf_out += 35;
#define ENCODE_ACCOUNT(buf_out, acc, acc_type) \
buf_out[0] = 0x80U + acc_type; \
buf_out[1] = 0x14U; \
for (int i = 0; i < 20; ++i) buf_out[2+i] = acc[i]; \
buf_out += 22;
#define PREPARE_PAYMENT_SIMPLE_SIZE 270U
int64_t hook(uint32_t reserved) {
_g(1, 1);
// Reserve 1 emit slot
ASSERT(etxn_reserve(1) == 1);
// Get destination from parameter "DST"
uint8_t dst[20];
int64_t dst_len = otxn_param(SBUF(dst), "DST", 3);
ASSERT(dst_len == 20);
// Get hook account (source)
uint8_t acc[20];
ASSERT(hook_account(SBUF(acc)) == 20);
// Get ledger seq for FLS/LLS
uint32_t cls = (uint32_t)ledger_seq();
// Build payment transaction
uint8_t tx[PREPARE_PAYMENT_SIMPLE_SIZE];
uint8_t* buf = tx;
ENCODE_TT(buf, ttPAYMENT);
ENCODE_FLAGS(buf, tfCANONICAL);
ENCODE_SEQUENCE(buf, 0);
ENCODE_FLS(buf, cls + 1);
ENCODE_LLS(buf, cls + 5);
uint64_t drops = 1000000; // 1 XRP
ENCODE_DROPS(buf, drops, amAMOUNT);
uint8_t* fee_ptr = buf;
ENCODE_DROPS(buf, 0, amFEE); // placeholder
ENCODE_SIGNING_PUBKEY_NULL(buf);
ENCODE_ACCOUNT(buf, acc, atACCOUNT);
ENCODE_ACCOUNT(buf, dst, atDESTINATION);
// Add emit details
int64_t details_len = etxn_details((uint32_t)buf, PREPARE_PAYMENT_SIMPLE_SIZE - (buf - tx));
ASSERT(details_len > 0);
buf += details_len;
// Calculate and set fee
int64_t fee = etxn_fee_base((uint32_t)tx, buf - tx);
ASSERT(fee > 0);
ENCODE_DROPS(fee_ptr, fee, amFEE);
// Emit!
uint8_t hash[32];
int64_t emit_result = emit(SBUF(hash), (uint32_t)tx, buf - tx);
ASSERT(emit_result == 32);
return accept(0, 0, 0);
}
)[test.hook]"];
// Hook that exports a payment using xport (for cross-chain export)
// xport APIs are gated by featureExport amendment, not sfHookApiVersion
TestHook xport_wasm = export_test_wasm[R"[test.hook](
#include <stdint.h>
extern int32_t _g(uint32_t id, uint32_t maxiter);
extern int64_t accept(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t rollback(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t xport(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len);
extern int64_t xport_reserve(uint32_t count);
extern int64_t hook_account(uint32_t write_ptr, uint32_t write_len);
extern int64_t otxn_param(uint32_t write_ptr, uint32_t write_len, uint32_t name_ptr, uint32_t name_len);
extern int64_t otxn_type(void);
extern int64_t ledger_seq(void);
#define SBUF(x) (uint32_t)(x), sizeof(x)
#define ASSERT(x) if (!(x)) rollback((uint32_t)#x, sizeof(#x), __LINE__)
#define ttPAYMENT 0
#define tfCANONICAL 0x80000000UL
#define amAMOUNT 1
#define amFEE 8
#define atACCOUNT 1
#define atDESTINATION 3
#define ENCODE_TT(buf_out, tt) \
buf_out[0] = 0x12U; \
buf_out[1] = (tt >> 8) & 0xFFU; \
buf_out[2] = tt & 0xFFU; \
buf_out += 3;
#define ENCODE_FLAGS(buf_out, flags) \
buf_out[0] = 0x22U; \
buf_out[1] = (flags >> 24) & 0xFFU; \
buf_out[2] = (flags >> 16) & 0xFFU; \
buf_out[3] = (flags >> 8) & 0xFFU; \
buf_out[4] = flags & 0xFFU; \
buf_out += 5;
#define ENCODE_SEQUENCE(buf_out, seq) \
buf_out[0] = 0x24U; \
buf_out[1] = (seq >> 24) & 0xFFU; \
buf_out[2] = (seq >> 16) & 0xFFU; \
buf_out[3] = (seq >> 8) & 0xFFU; \
buf_out[4] = seq & 0xFFU; \
buf_out += 5;
#define ENCODE_FLS(buf_out, fls) \
buf_out[0] = 0x20U; \
buf_out[1] = 0x1AU; \
buf_out[2] = (fls >> 24) & 0xFFU; \
buf_out[3] = (fls >> 16) & 0xFFU; \
buf_out[4] = (fls >> 8) & 0xFFU; \
buf_out[5] = fls & 0xFFU; \
buf_out += 6;
#define ENCODE_LLS(buf_out, lls) \
buf_out[0] = 0x20U; \
buf_out[1] = 0x1BU; \
buf_out[2] = (lls >> 24) & 0xFFU; \
buf_out[3] = (lls >> 16) & 0xFFU; \
buf_out[4] = (lls >> 8) & 0xFFU; \
buf_out[5] = lls & 0xFFU; \
buf_out += 6;
#define ENCODE_DROPS(buf_out, drops, amt_type) \
buf_out[0] = 0x60U + amt_type; \
buf_out[1] = 0x40U + ((drops >> 56) & 0x3FU); \
buf_out[2] = (drops >> 48) & 0xFFU; \
buf_out[3] = (drops >> 40) & 0xFFU; \
buf_out[4] = (drops >> 32) & 0xFFU; \
buf_out[5] = (drops >> 24) & 0xFFU; \
buf_out[6] = (drops >> 16) & 0xFFU; \
buf_out[7] = (drops >> 8) & 0xFFU; \
buf_out[8] = drops & 0xFFU; \
buf_out += 9;
#define ENCODE_SIGNING_PUBKEY_NULL(buf_out) \
buf_out[0] = 0x73U; \
buf_out[1] = 0x21U; \
for (int i = 2; i < 35; ++i) buf_out[i] = 0; \
buf_out += 35;
#define ENCODE_ACCOUNT(buf_out, acc, acc_type) \
buf_out[0] = 0x80U + acc_type; \
buf_out[1] = 0x14U; \
for (int i = 0; i < 20; ++i) buf_out[2+i] = acc[i]; \
buf_out += 22;
#define PREPARE_PAYMENT_SIMPLE_SIZE 270U
int64_t hook(uint32_t reserved) {
_g(1, 1);
// Only trigger on Payment transactions
if (otxn_type() != ttPAYMENT)
return accept(0, 0, 0);
// Reserve 1 xport slot
ASSERT(xport_reserve(1) == 1);
// Get destination from parameter "DST"
uint8_t dst[20];
int64_t dst_len = otxn_param(SBUF(dst), "DST", 3);
ASSERT(dst_len == 20);
// Get hook account (source) - xport requires sfAccount to match hook account
uint8_t acc[20];
ASSERT(hook_account(SBUF(acc)) == 20);
// Get ledger seq for FLS/LLS
uint32_t cls = (uint32_t)ledger_seq();
// Build payment transaction for export
uint8_t tx[PREPARE_PAYMENT_SIMPLE_SIZE];
uint8_t* buf = tx;
ENCODE_TT(buf, ttPAYMENT);
ENCODE_FLAGS(buf, tfCANONICAL);
ENCODE_SEQUENCE(buf, 0);
ENCODE_FLS(buf, cls + 1);
ENCODE_LLS(buf, cls + 5);
uint64_t drops = 1000000; // 1 XRP
ENCODE_DROPS(buf, drops, amAMOUNT);
ENCODE_DROPS(buf, 10, amFEE); // minimal fee for exported txn
ENCODE_SIGNING_PUBKEY_NULL(buf);
ENCODE_ACCOUNT(buf, acc, atACCOUNT);
ENCODE_ACCOUNT(buf, dst, atDESTINATION);
// Export!
uint8_t hash[32];
int64_t xport_result = xport(SBUF(hash), (uint32_t)tx, buf - tx);
ASSERT(xport_result == 32);
return accept(0, 0, 0);
}
)[test.hook]"];
void
testBasicSetup(FeatureBitset features)
{
testcase("Basic Setup");
using namespace jtx;
Env env{*this, features};
Account const alice{"alice"};
Account const bob{"bob"};
env.fund(XRP(10000), alice, bob);
env.close();
BEAST_EXPECT(env.current()->seq() < 256);
env(ripple::test::jtx::hook(alice, {{hso(accept_wasm)}}, 0),
HSFEE,
ter(tesSUCCESS));
env.close();
auto const alicePreBal = env.balance(alice);
env(pay(bob, alice, XRP(100)), fee(XRP(1)), ter(tesSUCCESS));
env.close();
BEAST_EXPECT(env.balance(alice) == alicePreBal + XRP(100));
}
void
testEmitPayment(FeatureBitset features)
{
testcase("Emit Payment");
using namespace jtx;
using namespace beast::severities;
// Per-partition logging: trace for View (hooks), error for everything
// else
auto logs = std::make_unique<DebugLogs>(
*this,
DebugLogs::Levels{
{"View", kTrace}, // Hook emit/xport operations
});
Env env{*this, envconfig(), features, std::move(logs), kError};
Account const alice{"alice"};
Account const bob{"bob"};
Account const carol{"carol"};
env.fund(XRP(10000), alice, bob, carol);
env.close();
// Install emit hook on alice
env(ripple::test::jtx::hook(alice, {{hso(emit_wasm)}}, 0),
HSFEE,
ter(tesSUCCESS));
env.close();
// Get carol's balance before
auto const carolPreBal = env.balance(carol);
// Bob sends payment to alice, hook emits payment to carol
// Pass carol's account ID as "DST" parameter
Json::Value params(Json::arrayValue);
Json::Value param;
param[jss::HookParameter] = Json::Value(Json::objectValue);
param[jss::HookParameter][jss::HookParameterName] =
strHex(std::string("DST"));
param[jss::HookParameter][jss::HookParameterValue] = strHex(carol.id());
params.append(param);
env(pay(bob, alice, XRP(100)),
fee(XRP(1)),
json(jss::HookParameters, params),
ter(tesSUCCESS));
env.close();
// Emitted transactions are processed in subsequent ledgers
env.close();
// Verify carol received the emitted payment (1 XRP)
BEAST_EXPECT(env.balance(carol) == carolPreBal + XRP(1));
}
// Helper: run xport test with given config
// Returns true if the exported directory is empty after the flow
// (meaning ttEXPORT cleaned up the entry)
void
runXportTest(
FeatureBitset features,
std::function<std::unique_ptr<Config>()> makeConfig,
bool expectCleanup)
{
using namespace jtx;
using namespace beast::severities;
auto logs = std::make_unique<DebugLogs>(
*this,
DebugLogs::Levels{
{"View", kTrace},
{"TxQ", kTrace},
});
Env env{*this, makeConfig(), features, std::move(logs), kError};
Account const alice{"alice"};
Account const bob{"bob"};
Account const carol{"carol"};
env.fund(XRP(10000), alice, bob, carol);
env.close();
// Install xport hook on alice
env(ripple::test::jtx::hook(alice, {{hso(xport_wasm)}}, 0),
HSFEE,
ter(tesSUCCESS));
env.close();
auto const xportLedgerSeq = env.current()->seq();
// Trigger hook with payment containing DST parameter
Json::Value params(Json::arrayValue);
Json::Value param;
param[jss::HookParameter] = Json::Value(Json::objectValue);
param[jss::HookParameter][jss::HookParameterName] =
strHex(std::string("DST"));
param[jss::HookParameter][jss::HookParameterValue] = strHex(carol.id());
params.append(param);
env(pay(bob, alice, XRP(100)),
fee(XRP(1)),
json(jss::HookParameters, params),
ter(tesSUCCESS));
env.close(); // Ledger N: xport() creates ltEXPORTED_TXN
// Verify ltEXPORTED_TXN was created
{
auto const exportedDirKey = keylet::exportedDir();
BEAST_EXPECT(env.current()->read(exportedDirKey));
BEAST_EXPECT(!dirIsEmpty(*env.current(), exportedDirKey));
}
// Close additional ledgers for signing flow
env.close(); // N+1: validators submit ttEXPORT_SIGN
env.close(); // N+2: ttEXPORT created (rawTxInsert)
env.close(); // N+3: does ttEXPORT get applied here?
// Check if cleanup happened
{
auto const exportedDirKey = keylet::exportedDir();
bool dirEmpty = dirIsEmpty(*env.current(), exportedDirKey);
BEAST_EXPECT(dirEmpty == expectCleanup);
}
BEAST_EXPECT(env.current()->seq() == xportLedgerSeq + 4);
}
void
testXportPayment(FeatureBitset features)
{
testcase("Xport Payment (no validator)");
// Without validator config, ltEXPORTED_TXN stays in directory
// (no one to sign it)
runXportTest(
features, []() { return jtx::envconfig(); }, false);
}
void
testXportPaymentWithValidator(FeatureBitset features)
{
testcase("Xport Payment (with validator)");
// With validator config, full flow should work:
// N: xport creates entry
// N+1: validator signs
// N+2: ttEXPORT cleans up
runXportTest(
features,
[]() { return jtx::envconfig(jtx::validator, ""); },
true);
}
void
run() override
{
using namespace test::jtx;
FeatureBitset const all{supported_amendments()};
FeatureBitset const allWithExport{all | featureExport};
// testBasicSetup(all);
// testEmitPayment(all);
// testXportPayment(allWithExport);
testXportPaymentWithValidator(
allWithExport); // with push_back disabled
}
};
BEAST_DEFINE_TESTSUITE(Export, app, ripple);
} // namespace test
} // namespace ripple