Files
rippled/src/test/app/Batch_test.cpp
Denis Angell 86d8b244d6 feat: Add Batch (XLS-56) V1_1 (#6446)
Co-authored-by: Mayukha Vadari <mvadari@ripple.com>
2026-07-01 12:47:14 +00:00

5940 lines
222 KiB
C++

#include <test/jtx/Account.h>
#include <test/jtx/Env.h>
#include <test/jtx/SignerUtils.h>
#include <test/jtx/TestHelpers.h>
#include <test/jtx/acctdelete.h>
#include <test/jtx/amount.h>
#include <test/jtx/balance.h> // IWYU pragma: keep
#include <test/jtx/batch.h>
#include <test/jtx/check.h>
#include <test/jtx/delegate.h>
#include <test/jtx/envconfig.h>
#include <test/jtx/fee.h>
#include <test/jtx/flags.h>
#include <test/jtx/mpt.h>
#include <test/jtx/multisign.h>
#include <test/jtx/noop.h>
#include <test/jtx/offer.h>
#include <test/jtx/pay.h>
#include <test/jtx/regkey.h>
#include <test/jtx/seq.h>
#include <test/jtx/sig.h>
#include <test/jtx/tags.h>
#include <test/jtx/ter.h>
#include <test/jtx/ticket.h>
#include <test/jtx/trust.h>
#include <test/jtx/txflags.h>
#include <test/jtx/utility.h>
#include <test/jtx/vault.h>
#include <xrpld/app/misc/Transaction.h>
#include <xrpld/app/misc/TxQ.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/strHex.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/config/BasicConfig.h>
#include <xrpl/config/Constants.h>
#include <xrpl/core/HashRouter.h>
#include <xrpl/json/json_value.h>
#include <xrpl/json/to_string.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/OpenView.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Batch.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/KeyType.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STParsedJSON.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/Sign.h>
#include <xrpl/protocol/SystemParameters.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/Units.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/server/NetworkOPs.h>
#include <xrpl/tx/Transactor.h>
#include <xrpl/tx/apply.h>
#include <xrpl/tx/transactors/payment/Payment.h>
#include <xrpl/tx/transactors/system/Batch.h>
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <map>
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <vector>
namespace xrpl::test {
class Batch_test : public beast::unit_test::Suite
{
struct TestLedgerData
{
int index;
std::string txType;
std::string result;
std::string txHash;
std::optional<std::string> batchID;
};
struct TestBatchData
{
std::string result;
std::string txHash;
};
static json::Value
getTxByIndex(json::Value const& jrr, int const index)
{
for (auto const& txn : jrr[jss::result][jss::ledger][jss::transactions])
{
if (txn[jss::metaData][sfTransactionIndex.jsonName] == index)
return txn;
}
return {};
}
static json::Value
getLastLedger(jtx::Env& env)
{
json::Value params;
params[jss::ledger_index] = env.closed()->seq();
params[jss::transactions] = true;
params[jss::expand] = true;
return env.rpc("json", "ledger", to_string(params));
}
void
validateInnerTxn(jtx::Env& env, std::string const& batchID, TestLedgerData const& ledgerResult)
{
json::Value const jrr = env.rpc("tx", ledgerResult.txHash)[jss::result];
BEAST_EXPECT(jrr[sfTransactionType.jsonName] == ledgerResult.txType);
BEAST_EXPECT(jrr[jss::meta][sfTransactionResult.jsonName] == ledgerResult.result);
BEAST_EXPECT(jrr[jss::meta][sfParentBatchID.jsonName] == batchID);
}
void
validateClosedLedger(jtx::Env& env, std::vector<TestLedgerData> const& ledgerResults)
{
auto const jrr = getLastLedger(env);
auto const transactions = jrr[jss::result][jss::ledger][jss::transactions];
BEAST_EXPECT(transactions.size() == ledgerResults.size());
for (TestLedgerData const& ledgerResult : ledgerResults)
{
auto const txn = getTxByIndex(jrr, ledgerResult.index);
BEAST_EXPECT(txn[jss::hash].asString() == ledgerResult.txHash);
BEAST_EXPECT(txn.isMember(jss::metaData));
json::Value const meta = txn[jss::metaData];
BEAST_EXPECT(txn[sfTransactionType.jsonName] == ledgerResult.txType);
BEAST_EXPECT(meta[sfTransactionResult.jsonName] == ledgerResult.result);
if (ledgerResult.batchID)
validateInnerTxn(env, *ledgerResult.batchID, ledgerResult);
}
}
template <typename... Args>
std::pair<std::vector<std::string>, std::string>
submitBatch(jtx::Env& env, TER const& result, Args&&... args)
{
auto batchTxn = env.jt(std::forward<Args>(args)...);
env(batchTxn, jtx::Ter(result));
auto const ids = batchTxn.stx->getBatchTransactionIDs();
std::vector<std::string> txIDs;
txIDs.reserve(ids.size());
for (auto const& id : ids)
txIDs.push_back(strHex(id));
TxID const batchID = batchTxn.stx->getTransactionID();
return std::make_pair(txIDs, strHex(batchID));
}
static uint256
getCheckIndex(AccountID const& account, std::uint32_t uSequence)
{
return keylet::check(account, uSequence).key;
}
static std::unique_ptr<Config>
makeSmallQueueConfig(
std::map<std::string, std::string> extraTxQ = {},
std::map<std::string, std::string> extraVoting = {})
{
auto p = test::jtx::envconfig();
auto& section = p->section(Sections::kTransactionQueue);
section.set(Keys::kLedgersInQueue, "2");
section.set(Keys::kMinimumQueueSize, "2");
section.set(Keys::kMinLedgersToComputeSizeLimit, "3");
section.set(Keys::kMaxLedgerCountsToStore, "100");
section.set(Keys::kRetrySequencePercent, "25");
section.set(Keys::kNormalConsensusIncreasePercent, "0");
for (auto const& [k, v] : extraTxQ)
section.set(k, v);
return p;
}
static auto
openLedgerFee(jtx::Env& env, XRPAmount const& batchFee)
{
using namespace jtx;
auto const& view = *env.current();
auto metrics = env.app().getTxQ().getMetrics(view);
return toDrops(metrics.openLedgerFeeLevel, batchFee) + 1;
}
void
testEnable(FeatureBitset features)
{
using namespace test::jtx;
using namespace std::literals;
for (bool const withBatch : {true, false})
{
testcase << "enabled: Batch " << (withBatch ? "enabled" : "disabled");
auto const amend = withBatch ? features : features - featureBatchV1_1;
Env env{*this, amend};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const carol = Account("carol");
env.fund(XRP(10000), alice, bob, carol);
env.close();
// ttBatch
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto const txResult = withBatch ? Ter(tesSUCCESS) : Ter(temDISABLED);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
batch::Inner(pay(alice, bob, XRP(1)), seq + 2),
txResult);
env.close();
}
// tfInnerBatchTxn
// A standalone transaction carrying this flag is never valid, so it
// is rejected early in checkValidity() regardless of the amendment.
{
env(pay(alice, bob, XRP(1)), Txflags(tfInnerBatchTxn), Ter(telENV_RPC_FAILED));
env.close();
}
env.close();
}
}
void
testPreflight(FeatureBitset features)
{
testcase("preflight");
using namespace test::jtx;
using namespace std::literals;
//----------------------------------------------------------------------
// preflight
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const carol = Account("carol");
env.fund(XRP(10000), alice, bob, carol);
env.close();
// temBAD_FEE: preflight1
{
env(batch::outer(alice, env.seq(alice), XRP(-1), tfAllOrNothing), Ter(temBAD_FEE));
env.close();
}
// DEFENSIVE: temINVALID_FLAG: Batch: inner batch flag.
// ACTUAL: telENV_RPC_FAILED: checkValidity()
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 0);
env(batch::outer(alice, seq, batchFee, tfInnerBatchTxn), Ter(telENV_RPC_FAILED));
env.close();
}
// temINVALID_FLAG: Batch: invalid flags.
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 0);
env(batch::outer(alice, seq, batchFee, tfDisallowXRP), Ter(temINVALID_FLAG));
env.close();
}
// temINVALID_FLAG: Batch: too many flags.
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 0);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
Txflags(tfAllOrNothing | tfOnlyOne),
Ter(temINVALID_FLAG));
env.close();
}
// temARRAY_EMPTY: Batch: txns array must have at least 2 entries.
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 0);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing), Ter(temARRAY_EMPTY));
env.close();
}
// temARRAY_EMPTY: Batch: txns array must have at least 2 entries.
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 0);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
Ter(temARRAY_EMPTY));
env.close();
}
// DEFENSIVE: temARRAY_TOO_LARGE: Batch: txns array exceeds 8 entries.
// ACTUAL: telENV_RPC_FAILED: isRawTransactionOkay()
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 9);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
batch::Inner(pay(alice, bob, XRP(1)), seq + 2),
batch::Inner(pay(alice, bob, XRP(1)), seq + 3),
batch::Inner(pay(alice, bob, XRP(1)), seq + 4),
batch::Inner(pay(alice, bob, XRP(1)), seq + 5),
batch::Inner(pay(alice, bob, XRP(1)), seq + 6),
batch::Inner(pay(alice, bob, XRP(1)), seq + 7),
batch::Inner(pay(alice, bob, XRP(1)), seq + 8),
batch::Inner(pay(alice, bob, XRP(1)), seq + 9),
Ter(telENV_RPC_FAILED));
env.close();
}
// temREDUNDANT: Batch: duplicate Txn found.
{
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const seq = env.seq(alice);
auto jt = env.jtnofill(
batch::outer(alice, env.seq(alice), batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
batch::Inner(pay(alice, bob, XRP(10)), seq + 1));
env(jt.jv, batch::Sig(bob), Ter(temREDUNDANT));
env.close();
}
// DEFENSIVE: temINVALID: Batch: batch cannot have inner batch txn.
// ACTUAL: telENV_RPC_FAILED: isRawTransactionOkay()
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::Inner(batch::outer(alice, seq, batchFee, tfAllOrNothing), seq),
batch::Inner(pay(alice, bob, XRP(1)), seq + 2),
Ter(telENV_RPC_FAILED));
env.close();
}
// temINVALID_FLAG: Batch: inner txn must have the
// tfInnerBatchTxn flag.
{
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const seq = env.seq(alice);
auto tx1 = batch::Inner(pay(alice, bob, XRP(10)), seq + 1);
tx1[jss::Flags] = 0;
auto jt = env.jtnofill(
batch::outer(alice, seq, batchFee, tfAllOrNothing),
tx1,
batch::Inner(pay(alice, bob, XRP(10)), seq + 2));
env(jt.jv, batch::Sig(bob), Ter(temINVALID_FLAG));
env.close();
}
// temBAD_SIGNATURE: Batch: inner txn cannot include TxnSignature.
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto jt = env.jt(pay(alice, bob, XRP(1)));
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::Inner(jt.jv, seq + 1),
batch::Inner(pay(alice, bob, XRP(1)), seq + 2),
Ter(temBAD_SIGNATURE));
env.close();
}
// temBAD_SIGNER: Batch: inner txn cannot include Signers.
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto tx1 = pay(alice, bob, XRP(1));
tx1[sfSigners.jsonName] = json::ValueType::Array;
tx1[sfSigners.jsonName][0U][sfSigner.jsonName] = json::ValueType::Object;
tx1[sfSigners.jsonName][0U][sfSigner.jsonName][sfAccount.jsonName] = alice.human();
tx1[sfSigners.jsonName][0U][sfSigner.jsonName][sfSigningPubKey.jsonName] =
strHex(alice.pk());
tx1[sfSigners.jsonName][0U][sfSigner.jsonName][sfTxnSignature.jsonName] = "DEADBEEF";
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::Inner(tx1, seq + 1),
batch::Inner(pay(alice, bob, XRP(1)), seq + 2),
Ter(temBAD_SIGNER));
env.close();
}
// temBAD_REGKEY: Batch: inner txn must include empty
// SigningPubKey.
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto tx1 = batch::Inner(pay(alice, bob, XRP(1)), seq + 1);
tx1[jss::SigningPubKey] = strHex(alice.pk());
auto jt = env.jtnofill(
batch::outer(alice, seq, batchFee, tfAllOrNothing),
tx1,
batch::Inner(pay(alice, bob, XRP(1)), seq + 2));
env(jt.jv, Ter(temBAD_REGKEY));
env.close();
}
// temINVALID_INNER_BATCH: Batch: inner txn preflight failed.
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
// amount can't be negative
batch::Inner(pay(alice, bob, XRP(-1)), seq + 2),
Ter(temINVALID_INNER_BATCH));
env.close();
}
// temINVALID_INNER_BATCH: tfInnerBatchTxn set but no parentBatchId.
{
auto jtx = env.jt(pay(alice, bob, XRP(1)), Txflags(tfInnerBatchTxn));
PreflightContext const pfCtx(
env.app(), *jtx.stx, env.current()->rules(), TapNone, env.journal);
auto const pf = Transactor::invokePreflight<Payment>(pfCtx);
BEAST_EXPECT(pf == temINVALID_INNER_BATCH);
}
// temINVALID_FLAG: tfInnerBatchTxn set but featureBatchV1_1 disabled.
{
Env disabledEnv{*this, features - featureBatchV1_1};
disabledEnv.fund(XRP(10000), alice, bob);
disabledEnv.close();
auto jtx = disabledEnv.jt(pay(alice, bob, XRP(1)), Txflags(tfInnerBatchTxn));
PreflightContext const pfCtx(
disabledEnv.app(),
*jtx.stx,
disabledEnv.current()->rules(),
TapNone,
disabledEnv.journal);
auto const pf = Transactor::invokePreflight<Payment>(pfCtx);
BEAST_EXPECT(pf == temINVALID_FLAG);
}
// temINVALID_INNER_BATCH: parentBatchId set but tfInnerBatchTxn not
// set.
{
auto jtx = env.jt(pay(alice, bob, XRP(1)));
PreflightContext const pfCtx(
env.app(), *jtx.stx, uint256{1}, env.current()->rules(), TapBatch, env.journal);
auto const pf = Transactor::invokePreflight<Payment>(pfCtx);
BEAST_EXPECT(pf == temINVALID_INNER_BATCH);
}
// temBAD_FEE: Batch: inner txn must have a fee of 0.
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto tx1 = batch::Inner(pay(alice, bob, XRP(1)), seq + 1);
tx1[jss::Fee] = to_string(env.current()->fees().base);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
tx1,
batch::Inner(pay(alice, bob, XRP(2)), seq + 2),
Ter(temBAD_FEE));
env.close();
}
// temBAD_FEE: Inner txn with negative fee
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto tx1 = batch::Inner(pay(alice, bob, XRP(1)), seq + 1);
tx1[jss::Fee] = "-1";
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
tx1,
batch::Inner(pay(alice, bob, XRP(2)), seq + 2),
Ter(temBAD_FEE));
env.close();
}
// temBAD_FEE: Inner txn with non-integer fee
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto tx1 = batch::Inner(pay(alice, bob, XRP(1)), seq + 1);
tx1[jss::Fee] = "1.5";
env.setParseFailureExpected(true);
try
{
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
tx1,
batch::Inner(pay(alice, bob, XRP(2)), seq + 2));
fail("Expected parse_error for fractional fee");
}
catch (jtx::ParseError const&)
{
BEAST_EXPECT(true);
}
env.setParseFailureExpected(false);
}
// temSEQ_AND_TICKET: Batch: inner txn cannot have both Sequence
// and TicketSequence.
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto tx1 = batch::Inner(pay(alice, bob, XRP(1)), 0, 1);
tx1[jss::Sequence] = seq + 1;
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
tx1,
batch::Inner(pay(alice, bob, XRP(2)), seq + 2),
Ter(temSEQ_AND_TICKET));
env.close();
}
// temSEQ_AND_TICKET: Batch: inner txn must have either Sequence or
// TicketSequence.
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(1)), 0),
batch::Inner(pay(alice, bob, XRP(2)), seq + 2),
Ter(temSEQ_AND_TICKET));
env.close();
}
// temREDUNDANT: Batch: duplicate sequence found:
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
batch::Inner(pay(alice, bob, XRP(2)), seq + 1),
Ter(temREDUNDANT));
env.close();
}
// temREDUNDANT: Batch: duplicate ticket found:
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(1)), 0, seq + 1),
batch::Inner(pay(alice, bob, XRP(2)), 0, seq + 1),
Ter(temREDUNDANT));
env.close();
}
// temREDUNDANT: Batch: duplicate ticket & sequence found:
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(1)), 0, seq + 1),
batch::Inner(pay(alice, bob, XRP(2)), seq + 1),
Ter(temREDUNDANT));
env.close();
}
// DEFENSIVE: temARRAY_TOO_LARGE: Batch: signers array exceeds
// kMaxBatchSigners entries.
// ACTUAL: telENV_RPC_FAILED: isRawTransactionOkay()
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, kMaxBatchSigners + 1, 2);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
batch::Inner(pay(alice, bob, XRP(5)), seq + 2),
batch::Sig(std::vector<Reg>(kMaxBatchSigners + 1, bob)),
Ter(telENV_RPC_FAILED));
env.close();
}
// temBAD_SIGNER: Batch: signer cannot be the outer account
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 2, 2);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
batch::Sig(alice, bob),
Ter(temBAD_SIGNER));
env.close();
}
// temBAD_SIGNER: Batch: duplicate signer (caught by ascending order check)
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 2, 2);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
batch::Sig(bob, bob),
Ter(temBAD_SIGNER));
env.close();
}
// temBAD_SIGNER: Batch: no account signature for inner txn.
// Note: Extra signature by bob
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 1, 2);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
batch::Inner(pay(alice, bob, XRP(5)), seq + 2),
batch::Sig(bob),
Ter(temBAD_SIGNER));
env.close();
}
// temBAD_SIGNER: Batch: no account signature for inner txn.
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 1, 2);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
batch::Sig(carol),
Ter(temBAD_SIGNER));
env.close();
}
// temBAD_SIGNATURE: Batch: invalid batch txn signature.
{
auto const seq = env.seq(alice);
auto const bobSeq = env.seq(bob);
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto jt = env.jtnofill(
batch::outer(alice, env.seq(alice), batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
batch::Inner(pay(bob, alice, XRP(5)), bobSeq));
Serializer msg;
serializeBatch(
msg,
jt.stx->getAccountID(sfAccount),
jt.stx->getSeqValue(),
tfAllOrNothing,
jt.stx->getBatchTransactionIDs());
finishMultiSigningData(bob.id(), msg);
auto const sig = xrpl::sign(bob.pk(), bob.sk(), msg.slice());
jt.jv[sfBatchSigners.jsonName][0u][sfBatchSigner.jsonName][sfAccount.jsonName] =
bob.human();
jt.jv[sfBatchSigners.jsonName][0u][sfBatchSigner.jsonName][sfSigningPubKey.jsonName] =
strHex(alice.pk());
jt.jv[sfBatchSigners.jsonName][0u][sfBatchSigner.jsonName][sfTxnSignature.jsonName] =
strHex(Slice{sig.data(), sig.size()});
env(jt.jv, Ter(telENV_RPC_FAILED));
env.close();
}
// temBAD_SIGNER: Batch: invalid batch signers.
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 2, 2);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
batch::Inner(pay(carol, alice, XRP(5)), env.seq(carol)),
batch::Sig(bob),
Ter(temBAD_SIGNER));
env.close();
}
}
void
testPreclaim(FeatureBitset features)
{
testcase("preclaim");
using namespace test::jtx;
using namespace std::literals;
//----------------------------------------------------------------------
// preclaim
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const carol = Account("carol");
auto const dave = Account("dave");
auto const elsa = Account("elsa");
auto const frank = Account("frank");
auto const phantom = Account("phantom");
env.memoize(phantom);
env.fund(XRP(10000), alice, bob, carol, dave, elsa, frank);
env.close();
//----------------------------------------------------------------------
// checkSign.checkSingleSign
// tefBAD_AUTH: Bob is not authorized to sign for Alice
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 3, 2);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
batch::Inner(pay(alice, bob, XRP(20)), seq + 2),
Sig(bob),
Ter(tefBAD_AUTH));
env.close();
}
//----------------------------------------------------------------------
// checkBatchSign.checkMultiSign
// tefNOT_MULTI_SIGNING: SignersList not enabled
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 3, 2);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
batch::Msig(bob, {dave, carol}),
Ter(tefNOT_MULTI_SIGNING));
env.close();
}
env(signers(alice, 2, {{bob, 1}, {carol, 1}}));
env.close();
env(signers(bob, 2, {{carol, 1}, {dave, 1}, {elsa, 1}}));
env.close();
// tefBAD_SIGNATURE: Account not in SignersList
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 3, 2);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
batch::Msig(bob, {carol, frank}),
Ter(tefBAD_SIGNATURE));
env.close();
}
// tefBAD_SIGNATURE: Wrong publicKey type
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 3, 2);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
batch::Msig(bob, {carol, Account("dave", KeyType::Ed25519)}),
Ter(tefBAD_SIGNATURE));
env.close();
}
// tefMASTER_DISABLED: Master key disabled
{
env(regkey(elsa, frank));
env(fset(elsa, asfDisableMaster), Sig(elsa));
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 3, 2);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
batch::Msig(bob, {carol, elsa}),
Ter(tefMASTER_DISABLED));
env.close();
}
// tefBAD_SIGNATURE: Signer does not exist
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 3, 2);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
batch::Msig(bob, {carol, phantom}),
Ter(tefBAD_SIGNATURE));
env.close();
}
// tefBAD_SIGNATURE: Signer has not enabled RegularKey
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 3, 2);
Account const davo{"davo", KeyType::Ed25519};
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
batch::Msig(bob, {carol, Reg{dave, davo}}),
Ter(tefBAD_SIGNATURE));
env.close();
}
// tefBAD_SIGNATURE: Wrong RegularKey Set
{
env(regkey(dave, frank));
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 3, 2);
Account const davo{"davo", KeyType::Ed25519};
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
batch::Msig(bob, {carol, Reg{dave, davo}}),
Ter(tefBAD_SIGNATURE));
env.close();
}
// tefBAD_QUORUM
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 2, 2);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
batch::Msig(bob, {carol}),
Ter(tefBAD_QUORUM));
env.close();
}
// tesSUCCESS: BatchSigners.Signers
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 3, 2);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
batch::Msig(bob, {carol, dave}),
Ter(tesSUCCESS));
env.close();
}
// tesSUCCESS: Multisign + BatchSigners.Signers
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 4, 2);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
batch::Msig(bob, {carol, dave}),
Msig(bob, carol),
Ter(tesSUCCESS));
env.close();
}
//----------------------------------------------------------------------
// checkBatchSign.checkSingleSign
// tefBAD_AUTH: Inner Account is not signer
{
auto const ledSeq = env.current()->seq();
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 1, 2);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, phantom, XRP(1000)), seq + 1),
batch::Inner(noop(phantom), ledSeq),
batch::Sig(Reg{phantom, carol}),
Ter(tefBAD_AUTH));
env.close();
}
// tefBAD_AUTH: Account is not signer
{
auto const ledSeq = env.current()->seq();
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 1, 2);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(1000)), seq + 1),
batch::Inner(noop(bob), ledSeq),
batch::Sig(Reg{bob, carol}),
Ter(tefBAD_AUTH));
env.close();
}
// tesSUCCESS: Signed With Regular Key
{
env(regkey(bob, carol));
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 1, 2);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
batch::Inner(pay(bob, alice, XRP(2)), env.seq(bob)),
batch::Sig(Reg{bob, carol}),
Ter(tesSUCCESS));
env.close();
}
// tesSUCCESS: Signed With Master Key
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 1, 2);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
batch::Inner(pay(bob, alice, XRP(2)), env.seq(bob)),
batch::Sig(bob),
Ter(tesSUCCESS));
env.close();
}
// tefMASTER_DISABLED: Signed With Master Key Disabled
{
env(regkey(bob, carol));
env(fset(bob, asfDisableMaster), Sig(bob));
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 1, 2);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
batch::Inner(pay(bob, alice, XRP(2)), env.seq(bob)),
batch::Sig(bob),
Ter(tefMASTER_DISABLED));
env.close();
}
}
void
testBadRawTxn(FeatureBitset features)
{
testcase("bad raw txn");
using namespace test::jtx;
using namespace std::literals;
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
env.fund(XRP(10000), alice, bob);
// Invalid: sfTransactionType
{
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const seq = env.seq(alice);
auto tx1 = batch::Inner(pay(alice, bob, XRP(10)), seq + 1);
tx1.removeMember(jss::TransactionType);
auto jt = env.jtnofill(
batch::outer(alice, seq, batchFee, tfAllOrNothing),
tx1,
batch::Inner(pay(alice, bob, XRP(10)), seq + 2));
env(jt.jv, batch::Sig(bob), Ter(telENV_RPC_FAILED));
env.close();
}
// Invalid: sfAccount
{
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const seq = env.seq(alice);
auto tx1 = batch::Inner(pay(alice, bob, XRP(10)), seq + 1);
tx1.removeMember(jss::Account);
auto jt = env.jtnofill(
batch::outer(alice, seq, batchFee, tfAllOrNothing),
tx1,
batch::Inner(pay(alice, bob, XRP(10)), seq + 2));
env(jt.jv, batch::Sig(bob), Ter(telENV_RPC_FAILED));
env.close();
}
// Invalid: sfSequence
{
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const seq = env.seq(alice);
auto tx1 = batch::Inner(pay(alice, bob, XRP(10)), seq + 1);
tx1.removeMember(jss::Sequence);
auto jt = env.jtnofill(
batch::outer(alice, seq, batchFee, tfAllOrNothing),
tx1,
batch::Inner(pay(alice, bob, XRP(10)), seq + 2));
env(jt.jv, batch::Sig(bob), Ter(telENV_RPC_FAILED));
env.close();
}
// Invalid: sfFee
{
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const seq = env.seq(alice);
auto tx1 = batch::Inner(pay(alice, bob, XRP(10)), seq + 1);
tx1.removeMember(jss::Fee);
auto jt = env.jtnofill(
batch::outer(alice, seq, batchFee, tfAllOrNothing),
tx1,
batch::Inner(pay(alice, bob, XRP(10)), seq + 2));
env(jt.jv, batch::Sig(bob), Ter(telENV_RPC_FAILED));
env.close();
}
// Invalid: sfSigningPubKey
{
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const seq = env.seq(alice);
auto tx1 = batch::Inner(pay(alice, bob, XRP(10)), seq + 1);
tx1.removeMember(jss::SigningPubKey);
auto jt = env.jtnofill(
batch::outer(alice, seq, batchFee, tfAllOrNothing),
tx1,
batch::Inner(pay(alice, bob, XRP(10)), seq + 2));
env(jt.jv, batch::Sig(bob), Ter(telENV_RPC_FAILED));
env.close();
}
// Inner OfferCreate with MPT TakerPays. Valid under featureMPTokensV2.
{
MPTIssue const issue(makeMptID(1, alice));
STAmount const mptAmt{issue, UINT64_C(100)};
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto const seq = env.seq(alice);
json::Value tx1;
tx1[jss::TransactionType] = jss::OfferCreate;
tx1[jss::Account] = alice.human();
tx1[jss::TakerPays] = mptAmt.getJson(JsonOptions::Values::None);
tx1[jss::TakerGets] = XRP(10).value().getJson(JsonOptions::Values::None);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::Inner(tx1, seq + 1),
batch::Inner(pay(alice, bob, XRP(1)), seq + 2),
Ter(tesSUCCESS));
env.close();
}
// Invalid: inner txn with invalid memo (non-URL-safe MemoType)
{
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto const seq = env.seq(alice);
auto tx1 = pay(alice, bob, XRP(10));
auto& ma = tx1["Memos"];
auto& mi = ma[ma.size()];
auto& m = mi["Memo"];
m["MemoType"] = strHex(std::string("\x01\x02\x03", 3));
m["MemoData"] = strHex(std::string("test"));
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::Inner(tx1, seq + 1),
batch::Inner(pay(alice, bob, XRP(1)), seq + 2),
Ter(telENV_RPC_FAILED));
env.close();
}
}
void
testBadSequence(FeatureBitset features)
{
testcase("bad sequence");
using namespace test::jtx;
using namespace std::literals;
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account("gw");
auto const usd = gw["USD"];
env.fund(XRP(10000), alice, bob, gw);
env.close();
env.trust(usd(1000), alice, bob);
env(pay(gw, alice, usd(100)));
env(pay(gw, bob, usd(100)));
env.close();
env(noop(bob), Ter(tesSUCCESS));
env.close();
// Invalid: Alice Sequence is a past sequence
{
auto const preAliceSeq = env.seq(alice);
auto const preAlice = env.balance(alice);
auto const preAliceUSD = env.balance(alice, usd.issue());
auto const preBobSeq = env.seq(bob);
auto const preBob = env.balance(bob);
auto const preBobUSD = env.balance(bob, usd.issue());
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, preAliceSeq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(10)), preAliceSeq - 10),
batch::Inner(pay(bob, alice, XRP(5)), preBobSeq),
batch::Sig(bob));
env.close();
{
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "Batch",
.result = "tesSUCCESS",
.txHash = batchID,
.batchID = std::nullopt},
};
validateClosedLedger(env, testCases);
}
env.close();
{
// next ledger is empty
std::vector<TestLedgerData> const testCases = {};
validateClosedLedger(env, testCases);
}
// Alice pays fee & Bob should not be affected.
BEAST_EXPECT(env.seq(alice) == preAliceSeq + 1);
BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
BEAST_EXPECT(env.balance(alice, usd.issue()) == preAliceUSD);
BEAST_EXPECT(env.seq(bob) == preBobSeq);
BEAST_EXPECT(env.balance(bob) == preBob);
BEAST_EXPECT(env.balance(bob, usd.issue()) == preBobUSD);
}
// Invalid: Alice Sequence is a future sequence
{
auto const preAliceSeq = env.seq(alice);
auto const preAlice = env.balance(alice);
auto const preAliceUSD = env.balance(alice, usd.issue());
auto const preBobSeq = env.seq(bob);
auto const preBob = env.balance(bob);
auto const preBobUSD = env.balance(bob, usd.issue());
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, preAliceSeq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(10)), preAliceSeq + 10),
batch::Inner(pay(bob, alice, XRP(5)), preBobSeq),
batch::Sig(bob));
env.close();
{
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "Batch",
.result = "tesSUCCESS",
.txHash = batchID,
.batchID = std::nullopt},
};
validateClosedLedger(env, testCases);
}
env.close();
{
// next ledger is empty
std::vector<TestLedgerData> const testCases = {};
validateClosedLedger(env, testCases);
}
// Alice pays fee & Bob should not be affected.
BEAST_EXPECT(env.seq(alice) == preAliceSeq + 1);
BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
BEAST_EXPECT(env.balance(alice, usd.issue()) == preAliceUSD);
BEAST_EXPECT(env.seq(bob) == preBobSeq);
BEAST_EXPECT(env.balance(bob) == preBob);
BEAST_EXPECT(env.balance(bob, usd.issue()) == preBobUSD);
}
// Invalid: Bob Sequence is a past sequence
{
auto const preAliceSeq = env.seq(alice);
auto const preAlice = env.balance(alice);
auto const preAliceUSD = env.balance(alice, usd.issue());
auto const preBobSeq = env.seq(bob);
auto const preBob = env.balance(bob);
auto const preBobUSD = env.balance(bob, usd.issue());
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, preAliceSeq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(10)), preAliceSeq + 1),
batch::Inner(pay(bob, alice, XRP(5)), preBobSeq - 10),
batch::Sig(bob));
env.close();
{
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "Batch",
.result = "tesSUCCESS",
.txHash = batchID,
.batchID = std::nullopt},
};
validateClosedLedger(env, testCases);
}
env.close();
{
// next ledger is empty
std::vector<TestLedgerData> const testCases = {};
validateClosedLedger(env, testCases);
}
// Alice pays fee & Bob should not be affected.
BEAST_EXPECT(env.seq(alice) == preAliceSeq + 1);
BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
BEAST_EXPECT(env.balance(alice, usd.issue()) == preAliceUSD);
BEAST_EXPECT(env.seq(bob) == preBobSeq);
BEAST_EXPECT(env.balance(bob) == preBob);
BEAST_EXPECT(env.balance(bob, usd.issue()) == preBobUSD);
}
// Invalid: Bob Sequence is a future sequence
{
auto const preAliceSeq = env.seq(alice);
auto const preAlice = env.balance(alice);
auto const preAliceUSD = env.balance(alice, usd.issue());
auto const preBobSeq = env.seq(bob);
auto const preBob = env.balance(bob);
auto const preBobUSD = env.balance(bob, usd.issue());
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, preAliceSeq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(10)), preAliceSeq + 1),
batch::Inner(pay(bob, alice, XRP(5)), preBobSeq + 10),
batch::Sig(bob));
env.close();
{
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "Batch",
.result = "tesSUCCESS",
.txHash = batchID,
.batchID = std::nullopt},
};
validateClosedLedger(env, testCases);
}
env.close();
{
// next ledger is empty
std::vector<TestLedgerData> const testCases = {};
validateClosedLedger(env, testCases);
}
// Alice pays fee & Bob should not be affected.
BEAST_EXPECT(env.seq(alice) == preAliceSeq + 1);
BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
BEAST_EXPECT(env.balance(alice, usd.issue()) == preAliceUSD);
BEAST_EXPECT(env.seq(bob) == preBobSeq);
BEAST_EXPECT(env.balance(bob) == preBob);
BEAST_EXPECT(env.balance(bob, usd.issue()) == preBobUSD);
}
// Invalid: Outer and Inner Sequence are the same
{
auto const preAliceSeq = env.seq(alice);
auto const preAlice = env.balance(alice);
auto const preAliceUSD = env.balance(alice, usd.issue());
auto const preBobSeq = env.seq(bob);
auto const preBob = env.balance(bob);
auto const preBobUSD = env.balance(bob, usd.issue());
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, preAliceSeq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(10)), preAliceSeq),
batch::Inner(pay(bob, alice, XRP(5)), preBobSeq),
batch::Sig(bob));
env.close();
{
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "Batch",
.result = "tesSUCCESS",
.txHash = batchID,
.batchID = std::nullopt},
};
validateClosedLedger(env, testCases);
}
env.close();
{
// next ledger is empty
std::vector<TestLedgerData> const testCases = {};
validateClosedLedger(env, testCases);
}
// Alice pays fee & Bob should not be affected.
BEAST_EXPECT(env.seq(alice) == preAliceSeq + 1);
BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
BEAST_EXPECT(env.balance(alice, usd.issue()) == preAliceUSD);
BEAST_EXPECT(env.seq(bob) == preBobSeq);
BEAST_EXPECT(env.balance(bob) == preBob);
BEAST_EXPECT(env.balance(bob, usd.issue()) == preBobUSD);
}
}
void
testBadOuterFee(FeatureBitset features)
{
testcase("bad outer fee");
using namespace test::jtx;
using namespace std::literals;
// Bad Fee Without Signer
{
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
env.fund(XRP(10000), alice, bob);
env.close();
env(noop(bob), Ter(tesSUCCESS));
env.close();
// Bad Fee: Should be batch::calcBatchFee(env, 0, 2)
auto const batchFee = batch::calcBatchFee(env, 0, 1);
auto const aliceSeq = env.seq(alice);
env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
batch::Inner(pay(alice, bob, XRP(15)), aliceSeq + 2),
Ter(telINSUF_FEE_P));
env.close();
}
// Bad Fee With MultiSign
{
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const carol = Account("carol");
env.fund(XRP(10000), alice, bob, carol);
env.close();
env(noop(bob), Ter(tesSUCCESS));
env.close();
env(signers(alice, 2, {{bob, 1}, {carol, 1}}));
env.close();
// Bad Fee: Should be batch::calcBatchFee(env, 2, 2)
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const aliceSeq = env.seq(alice);
env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
batch::Inner(pay(alice, bob, XRP(15)), aliceSeq + 2),
Msig(bob, carol),
Ter(telINSUF_FEE_P));
env.close();
}
// Bad Fee With MultiSign + BatchSigners
{
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const carol = Account("carol");
env.fund(XRP(10000), alice, bob, carol);
env.close();
env(noop(bob), Ter(tesSUCCESS));
env.close();
env(signers(alice, 2, {{bob, 1}, {carol, 1}}));
env.close();
// Bad Fee: Should be batch::calcBatchFee(env, 3, 2)
auto const batchFee = batch::calcBatchFee(env, 2, 2);
auto const aliceSeq = env.seq(alice);
auto const bobSeq = env.seq(bob);
env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
batch::Inner(pay(bob, alice, XRP(5)), bobSeq),
batch::Sig(bob),
Msig(bob, carol),
Ter(telINSUF_FEE_P));
env.close();
}
// Bad Fee With MultiSign + BatchSigners.Signers
{
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const carol = Account("carol");
env.fund(XRP(10000), alice, bob, carol);
env.close();
env(noop(bob), Ter(tesSUCCESS));
env.close();
env(signers(alice, 2, {{bob, 1}, {carol, 1}}));
env.close();
env(signers(bob, 2, {{alice, 1}, {carol, 1}}));
env.close();
// Bad Fee: Should be batch::calcBatchFee(env, 4, 2)
auto const batchFee = batch::calcBatchFee(env, 3, 2);
auto const aliceSeq = env.seq(alice);
auto const bobSeq = env.seq(bob);
env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
batch::Inner(pay(bob, alice, XRP(5)), bobSeq),
batch::Msig(bob, {alice, carol}),
Msig(bob, carol),
Ter(telINSUF_FEE_P));
env.close();
}
// Bad Fee With BatchSigners
{
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
env.fund(XRP(10000), alice, bob);
env.close();
env(noop(bob), Ter(tesSUCCESS));
env.close();
// Bad Fee: Should be batch::calcBatchFee(env, 1, 2)
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto const aliceSeq = env.seq(alice);
auto const bobSeq = env.seq(bob);
env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
batch::Inner(pay(bob, alice, XRP(5)), bobSeq),
batch::Sig(bob),
Ter(telINSUF_FEE_P));
env.close();
}
// Bad Fee Dynamic Fee Calculation
{
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account("gw");
auto const usd = gw["USD"];
env.fund(XRP(10000), alice, bob, gw);
env.close();
auto const ammCreate = [&alice](STAmount const& amount, STAmount const& amount2) {
json::Value jv;
jv[jss::Account] = alice.human();
jv[jss::Amount] = amount.getJson(JsonOptions::Values::None);
jv[jss::Amount2] = amount2.getJson(JsonOptions::Values::None);
jv[jss::TradingFee] = 0;
jv[jss::TransactionType] = jss::AMMCreate;
return jv;
};
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto const seq = env.seq(alice);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::Inner(ammCreate(XRP(10), usd(10)), seq + 1),
batch::Inner(pay(alice, bob, XRP(10)), seq + 2),
Ter(telINSUF_FEE_P));
env.close();
}
}
void
testCalculateBaseFee(FeatureBitset features)
{
testcase("calculate base fee");
using namespace test::jtx;
using namespace std::literals;
// telENV_RPC_FAILED: Batch: txns array exceeds 8 entries.
{
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
env.fund(XRP(10000), alice, bob);
env.close();
auto const batchFee = batch::calcBatchFee(env, 0, 9);
auto const aliceSeq = env.seq(alice);
env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
Ter(telENV_RPC_FAILED));
env.close();
}
// temARRAY_TOO_LARGE: Batch: txns array exceeds 8 entries.
{
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
env.fund(XRP(10000), alice, bob);
env.close();
auto const batchFee = batch::calcBatchFee(env, 0, 9);
auto const aliceSeq = env.seq(alice);
auto jt = env.jtnofill(
batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
batch::Inner(pay(alice, bob, XRP(1)), aliceSeq));
env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal j) {
auto const result = xrpl::apply(env.app(), view, *jt.stx, TapNone, j);
BEAST_EXPECT(!result.applied && result.ter == temARRAY_TOO_LARGE);
return result.applied;
});
}
// Regression: the relay-boundary local check (isBatchRawTransactionOkay)
// caps the signers array at kMaxBatchSigners, matching Batch::preflight -
// not kMaxBatchTxCount. A batch with more than kMaxBatchTxCount but at
// most kMaxBatchSigners signers must get past local checks and reach
// signer validation. Before the cap was aligned it was wrongly rejected
// at the boundary (telENV_RPC_FAILED) before preflight ran; now it
// reaches preflightSigValidated and fails there as extra signers
// (temBAD_SIGNER) rather than being dropped pre-engine.
{
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
env.fund(XRP(10000), alice, bob);
env.close();
// Over kMaxBatchTxCount (8) but within kMaxBatchSigners (24).
std::size_t const signerCount = kMaxBatchTxCount + 1;
auto const aliceSeq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, signerCount, 2);
env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
batch::Inner(pay(alice, bob, XRP(5)), aliceSeq + 2),
batch::Sig(std::vector<Reg>(signerCount, bob)),
Ter(temBAD_SIGNER));
env.close();
}
// temARRAY_TOO_LARGE: Batch preflight: signers array exceeds
// kMaxBatchSigners entries.
{
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
env.fund(XRP(10000), alice, bob);
env.close();
auto const batchFee = batch::calcBatchFee(env, kMaxBatchSigners + 1, 2);
auto const aliceSeq = env.seq(alice);
auto jt = env.jtnofill(
batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
batch::Inner(pay(alice, bob, XRP(5)), aliceSeq + 2),
batch::Sig(std::vector<Reg>(kMaxBatchSigners + 1, bob)));
env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal j) {
auto const result = xrpl::apply(env.app(), view, *jt.stx, TapNone, j);
BEAST_EXPECT(!result.applied && result.ter == temARRAY_TOO_LARGE);
return result.applied;
});
}
}
void
testAllOrNothing(FeatureBitset features)
{
testcase("all or nothing");
using namespace test::jtx;
using namespace std::literals;
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account("gw");
auto const usd = gw["USD"];
env.fund(XRP(10000), alice, bob, gw);
env.close();
// all
{
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto const seq = env.seq(alice);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
batch::Inner(pay(alice, bob, XRP(2)), seq + 2));
env.close();
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "Batch",
.result = "tesSUCCESS",
.txHash = batchID,
.batchID = std::nullopt},
{.index = 1,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[0],
.batchID = batchID},
{.index = 2,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[1],
.batchID = batchID},
};
validateClosedLedger(env, testCases);
// Alice consumes sequences (# of txns)
BEAST_EXPECT(env.seq(alice) == seq + 3);
// Alice pays XRP & Fee; Bob receives XRP
BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee);
BEAST_EXPECT(env.balance(bob) == preBob + XRP(3));
}
// tec failure
{
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto const seq = env.seq(alice);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
// tecUNFUNDED_PAYMENT: alice does not have enough XRP
batch::Inner(pay(alice, bob, XRP(9999)), seq + 2));
env.close();
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "Batch",
.result = "tesSUCCESS",
.txHash = batchID,
.batchID = std::nullopt},
};
validateClosedLedger(env, testCases);
// Alice consumes sequence
BEAST_EXPECT(env.seq(alice) == seq + 1);
// Alice pays Fee; Bob should not be affected
BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
BEAST_EXPECT(env.balance(bob) == preBob);
}
// tef failure
{
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto const seq = env.seq(alice);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
// tefNO_AUTH_REQUIRED: trustline auth is not required
batch::Inner(trust(alice, usd(1000), tfSetfAuth), seq + 2));
env.close();
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "Batch",
.result = "tesSUCCESS",
.txHash = batchID,
.batchID = std::nullopt},
};
validateClosedLedger(env, testCases);
// Alice consumes sequence
BEAST_EXPECT(env.seq(alice) == seq + 1);
// Alice pays Fee; Bob should not be affected
BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
BEAST_EXPECT(env.balance(bob) == preBob);
}
// ter failure
{
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto const seq = env.seq(alice);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
// terPRE_TICKET: ticket does not exist
batch::Inner(trust(alice, usd(1000), tfSetfAuth), 0, seq + 2));
env.close();
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "Batch",
.result = "tesSUCCESS",
.txHash = batchID,
.batchID = std::nullopt},
};
validateClosedLedger(env, testCases);
// Alice consumes sequence
BEAST_EXPECT(env.seq(alice) == seq + 1);
// Alice pays Fee; Bob should not be affected
BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
BEAST_EXPECT(env.balance(bob) == preBob);
}
}
void
testOnlyOne(FeatureBitset features)
{
testcase("only one");
using namespace test::jtx;
using namespace std::literals;
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const carol = Account("carol");
auto const dave = Account("dave");
auto const gw = Account("gw");
auto const usd = gw["USD"];
env.fund(XRP(10000), alice, bob, carol, dave, gw);
env.close();
// all transactions fail
{
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const batchFee = batch::calcBatchFee(env, 0, 3);
auto const seq = env.seq(alice);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfOnlyOne),
// tecUNFUNDED_PAYMENT: alice does not have enough XRP
batch::Inner(pay(alice, bob, XRP(9999)), seq + 1),
// tecUNFUNDED_PAYMENT: alice does not have enough XRP
batch::Inner(pay(alice, bob, XRP(9999)), seq + 2),
// tecUNFUNDED_PAYMENT: alice does not have enough XRP
batch::Inner(pay(alice, bob, XRP(9999)), seq + 3));
env.close();
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "Batch",
.result = "tesSUCCESS",
.txHash = batchID,
.batchID = std::nullopt},
{.index = 1,
.txType = "Payment",
.result = "tecUNFUNDED_PAYMENT",
.txHash = txIDs[0],
.batchID = batchID},
{.index = 2,
.txType = "Payment",
.result = "tecUNFUNDED_PAYMENT",
.txHash = txIDs[1],
.batchID = batchID},
{.index = 3,
.txType = "Payment",
.result = "tecUNFUNDED_PAYMENT",
.txHash = txIDs[2],
.batchID = batchID},
};
validateClosedLedger(env, testCases);
// Alice consumes sequences (# of txns)
BEAST_EXPECT(env.seq(alice) == seq + 4);
// Alice pays XRP & Fee; Bob receives XRP
BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
BEAST_EXPECT(env.balance(bob) == preBob);
}
// first transaction fails
{
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const batchFee = batch::calcBatchFee(env, 0, 3);
auto const seq = env.seq(alice);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfOnlyOne),
// tecUNFUNDED_PAYMENT: alice does not have enough XRP
batch::Inner(pay(alice, bob, XRP(9999)), seq + 1),
batch::Inner(pay(alice, bob, XRP(1)), seq + 2),
batch::Inner(pay(alice, bob, XRP(2)), seq + 3));
env.close();
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "Batch",
.result = "tesSUCCESS",
.txHash = batchID,
.batchID = std::nullopt},
{.index = 1,
.txType = "Payment",
.result = "tecUNFUNDED_PAYMENT",
.txHash = txIDs[0],
.batchID = batchID},
{.index = 2,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[1],
.batchID = batchID},
};
validateClosedLedger(env, testCases);
// Alice consumes sequences (# of txns)
BEAST_EXPECT(env.seq(alice) == seq + 3);
// Alice pays XRP & Fee; Bob receives XRP
BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1) - batchFee);
BEAST_EXPECT(env.balance(bob) == preBob + XRP(1));
}
// tec failure
{
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const batchFee = batch::calcBatchFee(env, 0, 3);
auto const seq = env.seq(alice);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfOnlyOne),
batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
// tecUNFUNDED_PAYMENT: alice does not have enough XRP
batch::Inner(pay(alice, bob, XRP(9999)), seq + 2),
batch::Inner(pay(alice, bob, XRP(2)), seq + 3));
env.close();
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "Batch",
.result = "tesSUCCESS",
.txHash = batchID,
.batchID = std::nullopt},
{.index = 1,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[0],
.batchID = batchID},
};
validateClosedLedger(env, testCases);
// Alice consumes sequences (# of txns)
BEAST_EXPECT(env.seq(alice) == seq + 2);
// Alice pays XRP & Fee; Bob receives XRP
BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1) - batchFee);
BEAST_EXPECT(env.balance(bob) == preBob + XRP(1));
}
// tef failure
{
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const batchFee = batch::calcBatchFee(env, 0, 3);
auto const seq = env.seq(alice);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfOnlyOne),
// tefNO_AUTH_REQUIRED: trustline auth is not required
batch::Inner(trust(alice, usd(1000), tfSetfAuth), seq + 1),
batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
batch::Inner(pay(alice, bob, XRP(2)), seq + 3));
env.close();
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "Batch",
.result = "tesSUCCESS",
.txHash = batchID,
.batchID = std::nullopt},
{.index = 1,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[1],
.batchID = batchID},
};
validateClosedLedger(env, testCases);
// Alice consumes sequences (# of txns)
BEAST_EXPECT(env.seq(alice) == seq + 2);
// Alice pays XRP & Fee; Bob receives XRP
BEAST_EXPECT(env.balance(alice) == preAlice - batchFee - XRP(1));
BEAST_EXPECT(env.balance(bob) == preBob + XRP(1));
}
// ter failure
{
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const batchFee = batch::calcBatchFee(env, 0, 3);
auto const seq = env.seq(alice);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfOnlyOne),
// terPRE_TICKET: ticket does not exist
batch::Inner(trust(alice, usd(1000), tfSetfAuth), 0, seq + 1),
batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
batch::Inner(pay(alice, bob, XRP(2)), seq + 3));
env.close();
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "Batch",
.result = "tesSUCCESS",
.txHash = batchID,
.batchID = std::nullopt},
{.index = 1,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[1],
.batchID = batchID},
};
validateClosedLedger(env, testCases);
// Alice consumes sequences (# of txns)
BEAST_EXPECT(env.seq(alice) == seq + 2);
// Alice pays XRP & Fee; Bob receives XRP
BEAST_EXPECT(env.balance(alice) == preAlice - batchFee - XRP(1));
BEAST_EXPECT(env.balance(bob) == preBob + XRP(1));
}
// tec (tecKILLED) error
{
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const preCarol = env.balance(carol);
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 6);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfOnlyOne),
batch::Inner(
offer(alice, alice["USD"](100), XRP(100), tfImmediateOrCancel), seq + 1),
batch::Inner(
offer(alice, alice["USD"](100), XRP(100), tfImmediateOrCancel), seq + 2),
batch::Inner(
offer(alice, alice["USD"](100), XRP(100), tfImmediateOrCancel), seq + 3),
batch::Inner(pay(alice, bob, XRP(100)), seq + 4),
batch::Inner(pay(alice, carol, XRP(100)), seq + 5),
batch::Inner(pay(alice, dave, XRP(100)), seq + 6));
env.close();
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "Batch",
.result = "tesSUCCESS",
.txHash = batchID,
.batchID = std::nullopt},
{.index = 1,
.txType = "OfferCreate",
.result = "tecKILLED",
.txHash = txIDs[0],
.batchID = batchID},
{.index = 2,
.txType = "OfferCreate",
.result = "tecKILLED",
.txHash = txIDs[1],
.batchID = batchID},
{.index = 3,
.txType = "OfferCreate",
.result = "tecKILLED",
.txHash = txIDs[2],
.batchID = batchID},
{.index = 4,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[3],
.batchID = batchID},
};
validateClosedLedger(env, testCases);
BEAST_EXPECT(env.balance(alice) == preAlice - XRP(100) - batchFee);
BEAST_EXPECT(env.balance(bob) == preBob + XRP(100));
BEAST_EXPECT(env.balance(carol) == preCarol);
}
}
void
testUntilFailure(FeatureBitset features)
{
testcase("until failure");
using namespace test::jtx;
using namespace std::literals;
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const carol = Account("carol");
auto const dave = Account("dave");
auto const gw = Account("gw");
auto const usd = gw["USD"];
env.fund(XRP(10000), alice, bob, carol, dave, gw);
env.close();
// first transaction fails
{
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const batchFee = batch::calcBatchFee(env, 0, 4);
auto const seq = env.seq(alice);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfUntilFailure),
// tecUNFUNDED_PAYMENT: alice does not have enough XRP
batch::Inner(pay(alice, bob, XRP(9999)), seq + 1),
batch::Inner(pay(alice, bob, XRP(1)), seq + 2),
batch::Inner(pay(alice, bob, XRP(2)), seq + 3),
batch::Inner(pay(alice, bob, XRP(3)), seq + 4));
env.close();
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "Batch",
.result = "tesSUCCESS",
.txHash = batchID,
.batchID = std::nullopt},
{.index = 1,
.txType = "Payment",
.result = "tecUNFUNDED_PAYMENT",
.txHash = txIDs[0],
.batchID = batchID},
};
validateClosedLedger(env, testCases);
// Alice consumes sequences (# of txns)
BEAST_EXPECT(env.seq(alice) == seq + 2);
// Alice pays XRP & Fee; Bob receives XRP
BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
BEAST_EXPECT(env.balance(bob) == preBob);
}
// all transactions succeed
{
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const batchFee = batch::calcBatchFee(env, 0, 4);
auto const seq = env.seq(alice);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfUntilFailure),
batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
batch::Inner(pay(alice, bob, XRP(2)), seq + 2),
batch::Inner(pay(alice, bob, XRP(3)), seq + 3),
batch::Inner(pay(alice, bob, XRP(4)), seq + 4));
env.close();
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "Batch",
.result = "tesSUCCESS",
.txHash = batchID,
.batchID = std::nullopt},
{.index = 1,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[0],
.batchID = batchID},
{.index = 2,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[1],
.batchID = batchID},
{.index = 3,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[2],
.batchID = batchID},
{.index = 4,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[3],
.batchID = batchID},
};
validateClosedLedger(env, testCases);
// Alice consumes sequences (# of txns)
BEAST_EXPECT(env.seq(alice) == seq + 5);
// Alice pays XRP & Fee; Bob receives XRP
BEAST_EXPECT(env.balance(alice) == preAlice - XRP(10) - batchFee);
BEAST_EXPECT(env.balance(bob) == preBob + XRP(10));
}
// tec error
{
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const batchFee = batch::calcBatchFee(env, 0, 4);
auto const seq = env.seq(alice);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfUntilFailure),
batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
batch::Inner(pay(alice, bob, XRP(2)), seq + 2),
// tecUNFUNDED_PAYMENT: alice does not have enough XRP
batch::Inner(pay(alice, bob, XRP(9999)), seq + 3),
batch::Inner(pay(alice, bob, XRP(3)), seq + 4));
env.close();
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "Batch",
.result = "tesSUCCESS",
.txHash = batchID,
.batchID = std::nullopt},
{.index = 1,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[0],
.batchID = batchID},
{.index = 2,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[1],
.batchID = batchID},
{.index = 3,
.txType = "Payment",
.result = "tecUNFUNDED_PAYMENT",
.txHash = txIDs[2],
.batchID = batchID},
};
validateClosedLedger(env, testCases);
// Alice consumes sequences (# of txns)
BEAST_EXPECT(env.seq(alice) == seq + 4);
// Alice pays XRP & Fee; Bob receives XRP
BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee);
BEAST_EXPECT(env.balance(bob) == preBob + XRP(3));
}
// tef error
{
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const batchFee = batch::calcBatchFee(env, 0, 4);
auto const seq = env.seq(alice);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfUntilFailure),
batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
batch::Inner(pay(alice, bob, XRP(2)), seq + 2),
// tefNO_AUTH_REQUIRED: trustline auth is not required
batch::Inner(trust(alice, usd(1000), tfSetfAuth), seq + 3),
batch::Inner(pay(alice, bob, XRP(3)), seq + 4));
env.close();
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "Batch",
.result = "tesSUCCESS",
.txHash = batchID,
.batchID = std::nullopt},
{.index = 1,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[0],
.batchID = batchID},
{.index = 2,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[1],
.batchID = batchID},
};
validateClosedLedger(env, testCases);
// Alice consumes sequences (# of txns)
BEAST_EXPECT(env.seq(alice) == seq + 3);
// Alice pays XRP & Fee; Bob receives XRP
BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee);
BEAST_EXPECT(env.balance(bob) == preBob + XRP(3));
}
// ter error
{
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const batchFee = batch::calcBatchFee(env, 0, 4);
auto const seq = env.seq(alice);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfUntilFailure),
batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
batch::Inner(pay(alice, bob, XRP(2)), seq + 2),
// terPRE_TICKET: ticket does not exist
batch::Inner(trust(alice, usd(1000), tfSetfAuth), 0, seq + 3),
batch::Inner(pay(alice, bob, XRP(3)), seq + 4));
env.close();
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "Batch",
.result = "tesSUCCESS",
.txHash = batchID,
.batchID = std::nullopt},
{.index = 1,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[0],
.batchID = batchID},
{.index = 2,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[1],
.batchID = batchID},
};
validateClosedLedger(env, testCases);
// Alice consumes sequences (# of txns)
BEAST_EXPECT(env.seq(alice) == seq + 3);
// Alice pays XRP & Fee; Bob receives XRP
BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee);
BEAST_EXPECT(env.balance(bob) == preBob + XRP(3));
}
// tec (tecKILLED) error
{
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const preCarol = env.balance(carol);
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 4);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfUntilFailure),
batch::Inner(pay(alice, bob, XRP(100)), seq + 1),
batch::Inner(pay(alice, carol, XRP(100)), seq + 2),
batch::Inner(
offer(alice, alice["USD"](100), XRP(100), tfImmediateOrCancel), seq + 3),
batch::Inner(pay(alice, dave, XRP(100)), seq + 4));
env.close();
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "Batch",
.result = "tesSUCCESS",
.txHash = batchID,
.batchID = std::nullopt},
{.index = 1,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[0],
.batchID = batchID},
{.index = 2,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[1],
.batchID = batchID},
{.index = 3,
.txType = "OfferCreate",
.result = "tecKILLED",
.txHash = txIDs[2],
.batchID = batchID},
};
validateClosedLedger(env, testCases);
BEAST_EXPECT(env.balance(alice) == preAlice - XRP(200) - batchFee);
BEAST_EXPECT(env.balance(bob) == preBob + XRP(100));
BEAST_EXPECT(env.balance(carol) == preCarol + XRP(100));
}
}
void
testIndependent(FeatureBitset features)
{
testcase("independent");
using namespace test::jtx;
using namespace std::literals;
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const carol = Account("carol");
auto const gw = Account("gw");
auto const usd = gw["USD"];
env.fund(XRP(10000), alice, bob, carol, gw);
env.close();
// multiple transactions fail
{
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const batchFee = batch::calcBatchFee(env, 0, 4);
auto const seq = env.seq(alice);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfIndependent),
batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
// tecUNFUNDED_PAYMENT: alice does not have enough XRP
batch::Inner(pay(alice, bob, XRP(9999)), seq + 2),
// tecUNFUNDED_PAYMENT: alice does not have enough XRP
batch::Inner(pay(alice, bob, XRP(9999)), seq + 3),
batch::Inner(pay(alice, bob, XRP(3)), seq + 4));
env.close();
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "Batch",
.result = "tesSUCCESS",
.txHash = batchID,
.batchID = std::nullopt},
{.index = 1,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[0],
.batchID = batchID},
{.index = 2,
.txType = "Payment",
.result = "tecUNFUNDED_PAYMENT",
.txHash = txIDs[1],
.batchID = batchID},
{.index = 3,
.txType = "Payment",
.result = "tecUNFUNDED_PAYMENT",
.txHash = txIDs[2],
.batchID = batchID},
{.index = 4,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[3],
.batchID = batchID},
};
validateClosedLedger(env, testCases);
// Alice consumes sequences (# of txns)
BEAST_EXPECT(env.seq(alice) == seq + 5);
// Alice pays XRP & Fee; Bob receives XRP
BEAST_EXPECT(env.balance(alice) == preAlice - XRP(4) - batchFee);
BEAST_EXPECT(env.balance(bob) == preBob + XRP(4));
}
// tec error
{
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const batchFee = batch::calcBatchFee(env, 0, 4);
auto const seq = env.seq(alice);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfIndependent),
batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
batch::Inner(pay(alice, bob, XRP(2)), seq + 2),
// tecUNFUNDED_PAYMENT: alice does not have enough XRP
batch::Inner(pay(alice, bob, XRP(9999)), seq + 3),
batch::Inner(pay(alice, bob, XRP(3)), seq + 4));
env.close();
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "Batch",
.result = "tesSUCCESS",
.txHash = batchID,
.batchID = std::nullopt},
{.index = 1,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[0],
.batchID = batchID},
{.index = 2,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[1],
.batchID = batchID},
{.index = 3,
.txType = "Payment",
.result = "tecUNFUNDED_PAYMENT",
.txHash = txIDs[2],
.batchID = batchID},
{.index = 4,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[3],
.batchID = batchID},
};
validateClosedLedger(env, testCases);
// Alice consumes sequences (# of txns)
BEAST_EXPECT(env.seq(alice) == seq + 5);
// Alice pays XRP & Fee; Bob receives XRP
BEAST_EXPECT(env.balance(alice) == preAlice - XRP(6) - batchFee);
BEAST_EXPECT(env.balance(bob) == preBob + XRP(6));
}
// tef error
{
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const batchFee = batch::calcBatchFee(env, 0, 4);
auto const seq = env.seq(alice);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfIndependent),
batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
batch::Inner(pay(alice, bob, XRP(2)), seq + 2),
// tefNO_AUTH_REQUIRED: trustline auth is not required
batch::Inner(trust(alice, usd(1000), tfSetfAuth), seq + 3),
batch::Inner(pay(alice, bob, XRP(3)), seq + 3));
env.close();
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "Batch",
.result = "tesSUCCESS",
.txHash = batchID,
.batchID = std::nullopt},
{.index = 1,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[0],
.batchID = batchID},
{.index = 2,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[1],
.batchID = batchID},
{.index = 3,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[3],
.batchID = batchID},
};
validateClosedLedger(env, testCases);
// Alice consumes sequences (# of txns)
BEAST_EXPECT(env.seq(alice) == seq + 4);
// Alice pays XRP & Fee; Bob receives XRP
BEAST_EXPECT(env.balance(alice) == preAlice - batchFee - XRP(6));
BEAST_EXPECT(env.balance(bob) == preBob + XRP(6));
}
// ter error
{
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const batchFee = batch::calcBatchFee(env, 0, 4);
auto const seq = env.seq(alice);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfIndependent),
batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
batch::Inner(pay(alice, bob, XRP(2)), seq + 2),
// terPRE_TICKET: ticket does not exist
batch::Inner(trust(alice, usd(1000), tfSetfAuth), 0, seq + 3),
batch::Inner(pay(alice, bob, XRP(3)), seq + 3));
env.close();
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "Batch",
.result = "tesSUCCESS",
.txHash = batchID,
.batchID = std::nullopt},
{.index = 1,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[0],
.batchID = batchID},
{.index = 2,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[1],
.batchID = batchID},
{.index = 3,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[3],
.batchID = batchID},
};
validateClosedLedger(env, testCases);
// Alice consumes sequences (# of txns)
BEAST_EXPECT(env.seq(alice) == seq + 4);
// Alice pays XRP & Fee; Bob receives XRP
BEAST_EXPECT(env.balance(alice) == preAlice - batchFee - XRP(6));
BEAST_EXPECT(env.balance(bob) == preBob + XRP(6));
}
// tec (tecKILLED) error
{
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const preCarol = env.balance(carol);
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 3);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfIndependent),
batch::Inner(pay(alice, bob, XRP(100)), seq + 1),
batch::Inner(pay(alice, carol, XRP(100)), seq + 2),
batch::Inner(
offer(alice, alice["USD"](100), XRP(100), tfImmediateOrCancel), seq + 3));
env.close();
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "Batch",
.result = "tesSUCCESS",
.txHash = batchID,
.batchID = std::nullopt},
{.index = 1,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[0],
.batchID = batchID},
{.index = 2,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[1],
.batchID = batchID},
{.index = 3,
.txType = "OfferCreate",
.result = "tecKILLED",
.txHash = txIDs[2],
.batchID = batchID},
};
validateClosedLedger(env, testCases);
BEAST_EXPECT(env.balance(alice) == preAlice - XRP(200) - batchFee);
BEAST_EXPECT(env.balance(bob) == preBob + XRP(100));
BEAST_EXPECT(env.balance(carol) == preCarol + XRP(100));
}
}
void
doTestInnerSubmitRPC(FeatureBitset features, bool withBatch)
{
std::string const testName =
std::string("inner submit rpc: batch ") + (withBatch ? "enabled" : "disabled") + ": ";
auto const amend = withBatch ? features : features - featureBatchV1_1;
using namespace test::jtx;
using namespace std::literals;
Env env{*this, amend};
if (!BEAST_EXPECT(amend[featureBatchV1_1] == withBatch))
return;
auto const alice = Account("alice");
auto const bob = Account("bob");
env.fund(XRP(10000), alice, bob);
env.close();
// Any transaction carrying tfInnerBatchTxn is rejected in checkValidity()
// before it reaches the tx engine, regardless of its signing fields or
// whether the amendment is enabled.
auto submitAndValidate = [&](std::string caseName, Slice const& slice, int line) {
testcase << testName << caseName;
auto const jrr = env.rpc("submit", strHex(slice))[jss::result];
expect(
jrr[jss::status] == "error" && jrr[jss::error] == "invalidTransaction" &&
jrr[jss::error_exception] ==
"fails local checks: Batch inner transactions are never "
"considered validly signed.",
pretty(jrr),
__FILE__,
line);
env.close();
};
// Invalid RPC Submission: TxnSignature
// + has `TxnSignature` field
// - has no `SigningPubKey` field
// - has no `Signers` field
// + has `tfInnerBatchTxn` flag
{
auto txn = batch::Inner(pay(alice, bob, XRP(1)), env.seq(alice));
txn[sfTxnSignature] = "DEADBEEF";
STParsedJSONObject parsed("test", txn.getTxn());
Serializer s;
parsed.object->add(s); // NOLINT(bugprone-unchecked-optional-access)
submitAndValidate("TxnSignature set", s.slice(), __LINE__);
}
// Invalid RPC Submission: SigningPubKey
// - has no `TxnSignature` field
// + has `SigningPubKey` field
// - has no `Signers` field
// + has `tfInnerBatchTxn` flag
{
auto txn = batch::Inner(pay(alice, bob, XRP(1)), env.seq(alice));
txn[sfSigningPubKey] = strHex(alice.pk());
STParsedJSONObject parsed("test", txn.getTxn());
Serializer s;
parsed.object->add(s); // NOLINT(bugprone-unchecked-optional-access)
submitAndValidate("SigningPubKey set", s.slice(), __LINE__);
}
// Invalid RPC Submission: Signers
// - has no `TxnSignature` field
// + has empty `SigningPubKey` field
// + has `Signers` field
// + has `tfInnerBatchTxn` flag
{
auto txn = batch::Inner(pay(alice, bob, XRP(1)), env.seq(alice));
txn[sfSigners] = json::ValueType::Array;
STParsedJSONObject parsed("test", txn.getTxn());
Serializer s;
parsed.object->add(s); // NOLINT(bugprone-unchecked-optional-access)
submitAndValidate("Signers set", s.slice(), __LINE__);
}
{
// Fully signed inner batch transaction
auto const txn = batch::Inner(pay(alice, bob, XRP(1)), env.seq(alice));
auto const jt = env.jt(txn.getTxn());
STParsedJSONObject parsed("test", jt.jv);
Serializer s;
parsed.object->add(s); // NOLINT(bugprone-unchecked-optional-access)
submitAndValidate("Fully signed", s.slice(), __LINE__);
}
// Invalid RPC Submission: tfInnerBatchTxn
// - has no `TxnSignature` field
// + has empty `SigningPubKey` field
// - has no `Signers` field
// + has `tfInnerBatchTxn` flag
{
auto txn = batch::Inner(pay(alice, bob, XRP(1)), env.seq(alice));
STParsedJSONObject parsed("test", txn.getTxn());
Serializer s;
parsed.object->add(s); // NOLINT(bugprone-unchecked-optional-access)
submitAndValidate("No signing fields set", s.slice(), __LINE__);
}
// Invalid RPC Submission: tfInnerBatchTxn pseudo-transaction
// - has no `TxnSignature` field
// + has empty `SigningPubKey` field
// - has no `Signers` field
// + has `tfInnerBatchTxn` flag
{
STTx const amendTx(ttAMENDMENT, [seq = env.closed()->header().seq + 1](auto& obj) {
obj.setAccountID(sfAccount, AccountID());
obj.setFieldH256(sfAmendment, featureBatchV1_1);
obj.setFieldU32(sfLedgerSequence, seq);
obj.setFieldU32(sfFlags, tfInnerBatchTxn);
});
auto txn = batch::Inner(amendTx.getJson(JsonOptions::Values::None), env.seq(alice));
STParsedJSONObject parsed("test", txn.getTxn());
Serializer s;
parsed.object->add(s); // NOLINT(bugprone-unchecked-optional-access)
submitAndValidate("Pseudo-transaction", s.slice(), __LINE__);
}
}
void
testInnerSubmitRPC(FeatureBitset features)
{
for (bool const withBatch : {true, false})
{
doTestInnerSubmitRPC(features, withBatch);
}
}
void
testAccountActivation(FeatureBitset features)
{
testcase("account activation");
using namespace test::jtx;
using namespace std::literals;
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
env.fund(XRP(10000), alice);
env.close();
env.memoize(bob);
auto const preAlice = env.balance(alice);
auto const ledSeq = env.current()->seq();
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(1000)), seq + 1),
batch::Inner(fset(bob, asfAllowTrustLineClawback), ledSeq),
batch::Sig(bob));
env.close();
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "Batch",
.result = "tesSUCCESS",
.txHash = batchID,
.batchID = std::nullopt},
{.index = 1,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[0],
.batchID = batchID},
{.index = 2,
.txType = "AccountSet",
.result = "tesSUCCESS",
.txHash = txIDs[1],
.batchID = batchID},
};
validateClosedLedger(env, testCases);
// Alice consumes sequences (# of txns)
BEAST_EXPECT(env.seq(alice) == seq + 2);
// Bob consumes sequences (# of txns)
BEAST_EXPECT(env.seq(bob) == ledSeq + 1);
// Alice pays XRP & Fee; Bob receives XRP
BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1000) - batchFee);
BEAST_EXPECT(env.balance(bob) == XRP(1000));
// An inner partial payment must never fund a new account. With
// featureBatchV1_1 the partial-payment-to-create check runs on the
// batch's closed view and rejects it (tefNO_DST_PARTIAL). A USD->XRP path is
// set up so that, absent the check, the partial payment would deliver
// XRP and create carol; the check must block exactly that. A
// cross-currency shape (IOU SendMax, XRP Amount) also lets the inner
// pass preflight and reach the preclaim check.
{
Env env{*this, features};
auto const alice = Account("alice");
auto const gw = Account("gw");
auto const carol = Account("carol"); // never funded
auto const usd = gw["USD"];
env.fund(XRP(10000), alice, gw);
env.close();
env.memoize(carol);
env(trust(alice, usd(100000)));
env(pay(gw, alice, usd(10000)));
env(offer(gw, usd(2000), XRP(2000))); // USD -> XRP liquidity
env.close();
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto pp = pay(alice, carol, XRP(1000));
pp[jss::SendMax] = usd(2000).value().getJson(JsonOptions::Values::None);
pp[jss::Flags] = tfPartialPayment;
env(batch::outer(alice, seq, batchFee, tfIndependent),
batch::Inner(pp, seq + 1),
batch::Inner(noop(alice), seq + 2),
Ter(tesSUCCESS));
env.close();
// carol was not created despite the available path.
BEAST_EXPECT(!env.le(carol));
}
}
void
testCheckAllSignatures(FeatureBitset features)
{
testcase("check all signatures");
using namespace test::jtx;
using namespace std::literals;
// Verifies that checkBatchSign validates all signers even when an
// unfunded account (signed with its master key) appears first in the
// sorted signer list. A funded account with an invalid signature must
// still be rejected with tefBAD_AUTH.
Env env{*this, features};
auto const alice = Account("alice");
// "aaa" sorts before other accounts alphabetically, ensuring the
// unfunded account is checked first in the sorted signer list
auto const unfunded = Account("aaa");
auto const carol = Account("carol");
env.fund(XRP(10000), alice, carol);
env.close();
// Verify sort order: unfunded.id() < carol.id()
BEAST_EXPECT(unfunded.id() < carol.id());
auto const seq = env.seq(alice);
auto const ledSeq = env.current()->seq();
auto const batchFee = batch::calcBatchFee(env, 2, 3);
// The batch includes:
// 1. alice pays unfunded (to create unfunded's account)
// 2. unfunded does a noop (signed by unfunded's master key - valid)
// 3. carol pays alice (signed by alice's key - INVALID since alice is
// not carol's regular key)
//
// checkBatchSign must validate all signers regardless of order.
// This must fail with tefBAD_AUTH.
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, unfunded, XRP(100)), seq + 1),
batch::Inner(noop(unfunded), ledSeq),
batch::Inner(pay(carol, alice, XRP(1000)), env.seq(carol)),
batch::Sig(unfunded, Reg{carol, alice}),
Ter(tefBAD_AUTH));
env.close();
}
void
testAccountSet(FeatureBitset features)
{
testcase("account set");
using namespace test::jtx;
using namespace std::literals;
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
env.fund(XRP(10000), alice, bob);
env.close();
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto tx1 = batch::Inner(noop(alice), seq + 1);
std::string domain = "example.com";
tx1[sfDomain] = strHex(domain);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfAllOrNothing),
tx1,
batch::Inner(pay(alice, bob, XRP(1)), seq + 2));
env.close();
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "Batch",
.result = "tesSUCCESS",
.txHash = batchID,
.batchID = std::nullopt},
{.index = 1,
.txType = "AccountSet",
.result = "tesSUCCESS",
.txHash = txIDs[0],
.batchID = batchID},
{.index = 2,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[1],
.batchID = batchID},
};
validateClosedLedger(env, testCases);
auto const sle = env.le(keylet::account(alice));
BEAST_EXPECT(sle);
BEAST_EXPECT(sle->getFieldVL(sfDomain) == Blob(domain.begin(), domain.end()));
// Alice consumes sequences (# of txns)
BEAST_EXPECT(env.seq(alice) == seq + 3);
// Alice pays XRP & Fee; Bob receives XRP
BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1) - batchFee);
BEAST_EXPECT(env.balance(bob) == preBob + XRP(1));
}
void
testAccountDelete(FeatureBitset features)
{
testcase("account delete");
using namespace test::jtx;
using namespace std::literals;
// tfIndependent: account delete success
{
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
env.fund(XRP(10000), alice, bob);
env.close();
incLgrSeqForAccDel(env, alice);
for (int i = 0; i < 5; ++i)
env.close();
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 2) + env.current()->fees().increment;
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfIndependent),
batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
batch::Inner(acctdelete(alice, bob), seq + 2),
// terNO_ACCOUNT: alice does not exist
batch::Inner(pay(alice, bob, XRP(2)), seq + 3));
env.close();
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "Batch",
.result = "tesSUCCESS",
.txHash = batchID,
.batchID = std::nullopt},
{.index = 1,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[0],
.batchID = batchID},
{.index = 2,
.txType = "AccountDelete",
.result = "tesSUCCESS",
.txHash = txIDs[1],
.batchID = batchID},
};
validateClosedLedger(env, testCases);
// Alice does not exist; Bob receives Alice's XRP
BEAST_EXPECT(!env.le(keylet::account(alice)));
BEAST_EXPECT(env.balance(bob) == preBob + (preAlice - batchFee));
}
// tfIndependent: account delete fails
{
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
env.fund(XRP(10000), alice, bob);
env.close();
incLgrSeqForAccDel(env, alice);
for (int i = 0; i < 5; ++i)
env.close();
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
env.trust(bob["USD"](1000), alice);
env.close();
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 2) + env.current()->fees().increment;
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfIndependent),
batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
// tecHAS_OBLIGATIONS: alice has obligations
batch::Inner(acctdelete(alice, bob), seq + 2),
batch::Inner(pay(alice, bob, XRP(2)), seq + 3));
env.close();
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "Batch",
.result = "tesSUCCESS",
.txHash = batchID,
.batchID = std::nullopt},
{.index = 1,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[0],
.batchID = batchID},
{.index = 2,
.txType = "AccountDelete",
.result = "tecHAS_OBLIGATIONS",
.txHash = txIDs[1],
.batchID = batchID},
{.index = 3,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[2],
.batchID = batchID},
};
validateClosedLedger(env, testCases);
// Alice does not exist; Bob receives XRP
BEAST_EXPECT(env.le(keylet::account(alice)));
BEAST_EXPECT(env.balance(bob) == preBob + XRP(3));
}
// tfAllOrNothing: account delete fails
{
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
env.fund(XRP(10000), alice, bob);
env.close();
incLgrSeqForAccDel(env, alice);
for (int i = 0; i < 5; ++i)
env.close();
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 2) + env.current()->fees().increment;
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
batch::Inner(acctdelete(alice, bob), seq + 2),
// terNO_ACCOUNT: alice does not exist
batch::Inner(pay(alice, bob, XRP(2)), seq + 3));
env.close();
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "Batch",
.result = "tesSUCCESS",
.txHash = batchID,
.batchID = std::nullopt},
};
validateClosedLedger(env, testCases);
// Alice still exists; Bob is unchanged
BEAST_EXPECT(env.le(keylet::account(alice)));
BEAST_EXPECT(env.balance(bob) == preBob);
}
}
void
testLoan(FeatureBitset features)
{
testcase("loan");
bool const lendingBatchEnabled = !std::ranges::any_of(
Batch::kDisabledTxTypes,
[](auto const& disabled) { return disabled == ttLOAN_BROKER_SET; });
using namespace test::jtx;
Env env{*this, features};
Account const issuer{"issuer"};
// For simplicity, lender will be the sole actor for the vault &
// brokers.
Account const lender{"lender"};
// Borrower only wants to borrow
Account const borrower{"borrower"};
// Fund the accounts and trust lines with the same amount so that tests
// can use the same values regardless of the asset.
env.fund(XRP(100'000), issuer, noripple(lender, borrower));
env.close();
// Just use an XRP asset
PrettyAsset const asset{xrpIssue(), 1'000'000};
Vault const vault{env};
auto const deposit = asset(50'000);
auto const debtMaximumValue = asset(25'000).value();
auto const coverDepositValue = asset(1000).value();
auto [tx, vaultKeylet] = vault.create({.owner = lender, .asset = asset});
env(tx);
env.close();
BEAST_EXPECT(env.le(vaultKeylet));
env(vault.deposit({.depositor = lender, .id = vaultKeylet.key, .amount = deposit}));
env.close();
auto const brokerKeylet = keylet::loanBroker(lender.id(), env.seq(lender));
{
using namespace loanBroker;
env(set(lender, vaultKeylet.key),
kManagementFeeRate(TenthBips16(100)),
kDebtMaximum(debtMaximumValue),
kCoverRateMinimum(TenthBips32(percentageToTenthBips(10))),
kCoverRateLiquidation(TenthBips32(percentageToTenthBips(25))));
env(coverDeposit(lender, brokerKeylet.key, coverDepositValue));
env.close();
}
{
using namespace loan;
using namespace std::chrono_literals;
auto const lenderSeq = env.seq(lender);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto const loanKeylet = keylet::loan(brokerKeylet.key, 1);
{
auto const [txIDs, batchID] = submitBatch(
env,
lendingBatchEnabled ? temBAD_SIGNATURE : temINVALID_INNER_BATCH,
batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing),
batch::Inner(
env.json(
set(lender, brokerKeylet.key, asset(1000).value()),
// Not allowed to include the counterparty signature
Sig(sfCounterpartySignature, borrower),
Sig(kNone),
Fee(kNone),
Seq(kNone)),
lenderSeq + 1),
batch::Inner(
pay(lender, loanKeylet.key, STAmount{asset, asset(500).value()}),
lenderSeq + 2));
}
{
auto const [txIDs, batchID] = submitBatch(
env,
temINVALID_INNER_BATCH,
batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing),
batch::Inner(
env.json(
set(lender, brokerKeylet.key, asset(1000).value()),
// Counterparty must be set
Sig(kNone),
Fee(kNone),
Seq(kNone)),
lenderSeq + 1),
batch::Inner(
pay(lender, loanKeylet.key, STAmount{asset, asset(500).value()}),
lenderSeq + 2));
}
{
auto const [txIDs, batchID] = submitBatch(
env,
lendingBatchEnabled ? temBAD_SIGNER : temINVALID_INNER_BATCH,
batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing),
batch::Inner(
env.json(
set(lender, brokerKeylet.key, asset(1000).value()),
// Counterparty must sign the outer transaction
kCounterparty(borrower.id()),
Sig(kNone),
Fee(kNone),
Seq(kNone)),
lenderSeq + 1),
batch::Inner(
pay(lender, loanKeylet.key, STAmount{asset, asset(500).value()}),
lenderSeq + 2));
}
{
// LoanSet normally charges at least 2x base fee, but since the
// signature check is done by the batch, it only charges the
// base fee.
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const [txIDs, batchID] = submitBatch(
env,
lendingBatchEnabled ? TER(tesSUCCESS) : TER(temINVALID_INNER_BATCH),
batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing),
batch::Inner(
env.json(
set(lender, brokerKeylet.key, asset(1000).value()),
kCounterparty(borrower.id()),
Sig(kNone),
Fee(kNone),
Seq(kNone)),
lenderSeq + 1),
batch::Inner(
pay(
// However, this inner transaction will fail,
// because the lender is not allowed to draw the
// transaction
lender,
loanKeylet.key,
STAmount{asset, asset(500).value()}),
lenderSeq + 2),
batch::Sig(borrower));
}
env.close();
BEAST_EXPECT(env.le(brokerKeylet));
BEAST_EXPECT(!env.le(loanKeylet));
{
// LoanSet normally charges at least 2x base fee, but since the
// signature check is done by the batch, it only charges the
// base fee.
auto const lenderSeq = env.seq(lender);
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const [txIDs, batchID] = submitBatch(
env,
lendingBatchEnabled ? TER(tesSUCCESS) : TER(temINVALID_INNER_BATCH),
batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing),
batch::Inner(
env.json(
set(lender, brokerKeylet.key, asset(1000).value()),
kCounterparty(borrower.id()),
Sig(kNone),
Fee(kNone),
Seq(kNone)),
lenderSeq + 1),
batch::Inner(manage(lender, loanKeylet.key, tfLoanImpair), lenderSeq + 2),
batch::Sig(borrower));
}
env.close();
BEAST_EXPECT(env.le(brokerKeylet));
if (auto const sleLoan = env.le(loanKeylet);
lendingBatchEnabled ? BEAST_EXPECT(sleLoan) : !BEAST_EXPECT(!sleLoan))
{
BEAST_EXPECT(sleLoan->isFlag(lsfLoanImpaired));
}
}
}
void
testObjectCreateSequence(FeatureBitset features)
{
testcase("object create w/ sequence");
using namespace test::jtx;
using namespace std::literals;
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account("gw");
auto const usd = gw["USD"];
env.fund(XRP(10000), alice, bob, gw);
env.close();
env.trust(usd(1000), alice, bob);
env(pay(gw, alice, usd(100)));
env(pay(gw, bob, usd(100)));
env.close();
// success
{
auto const aliceSeq = env.seq(alice);
auto const bobSeq = env.seq(bob);
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const preAliceUSD = env.balance(alice, usd.issue());
auto const preBobUSD = env.balance(bob, usd.issue());
auto const batchFee = batch::calcBatchFee(env, 1, 2);
uint256 const chkID{getCheckIndex(bob, env.seq(bob))};
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
batch::Inner(check::create(bob, alice, usd(10)), bobSeq),
batch::Inner(check::cash(alice, chkID, usd(10)), aliceSeq + 1),
batch::Sig(bob));
env.close();
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "Batch",
.result = "tesSUCCESS",
.txHash = batchID,
.batchID = std::nullopt},
{.index = 1,
.txType = "CheckCreate",
.result = "tesSUCCESS",
.txHash = txIDs[0],
.batchID = batchID},
{.index = 2,
.txType = "CheckCash",
.result = "tesSUCCESS",
.txHash = txIDs[1],
.batchID = batchID},
};
validateClosedLedger(env, testCases);
// Alice consumes sequences (# of txns)
BEAST_EXPECT(env.seq(alice) == aliceSeq + 2);
// Alice consumes sequences (# of txns)
BEAST_EXPECT(env.seq(bob) == bobSeq + 1);
// Alice pays Fee; Bob XRP Unchanged
BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
BEAST_EXPECT(env.balance(bob) == preBob);
// Alice pays USD & Bob receives USD
BEAST_EXPECT(env.balance(alice, usd.issue()) == preAliceUSD + usd(10));
BEAST_EXPECT(env.balance(bob, usd.issue()) == preBobUSD - usd(10));
}
// failure
{
env(fset(alice, asfRequireDest));
env.close();
auto const aliceSeq = env.seq(alice);
auto const bobSeq = env.seq(bob);
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const preAliceUSD = env.balance(alice, usd.issue());
auto const preBobUSD = env.balance(bob, usd.issue());
auto const batchFee = batch::calcBatchFee(env, 1, 2);
uint256 const chkID{getCheckIndex(bob, env.seq(bob))};
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, aliceSeq, batchFee, tfIndependent),
// tecDST_TAG_NEEDED - alice has enabled asfRequireDest
batch::Inner(check::create(bob, alice, usd(10)), bobSeq),
batch::Inner(check::cash(alice, chkID, usd(10)), aliceSeq + 1),
batch::Sig(bob));
env.close();
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "Batch",
.result = "tesSUCCESS",
.txHash = batchID,
.batchID = std::nullopt},
{.index = 1,
.txType = "CheckCreate",
.result = "tecDST_TAG_NEEDED",
.txHash = txIDs[0],
.batchID = batchID},
{.index = 2,
.txType = "CheckCash",
.result = "tecNO_ENTRY",
.txHash = txIDs[1],
.batchID = batchID},
};
validateClosedLedger(env, testCases);
// Alice consumes sequences (# of txns)
BEAST_EXPECT(env.seq(alice) == aliceSeq + 2);
// Bob consumes sequences (# of txns)
BEAST_EXPECT(env.seq(bob) == bobSeq + 1);
// Alice pays Fee; Bob XRP Unchanged
BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
BEAST_EXPECT(env.balance(bob) == preBob);
// Alice pays USD & Bob receives USD
BEAST_EXPECT(env.balance(alice, usd.issue()) == preAliceUSD);
BEAST_EXPECT(env.balance(bob, usd.issue()) == preBobUSD);
}
}
void
testObjectCreateTicket(FeatureBitset features)
{
testcase("object create w/ ticket");
using namespace test::jtx;
using namespace std::literals;
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account("gw");
auto const usd = gw["USD"];
env.fund(XRP(10000), alice, bob, gw);
env.close();
env.trust(usd(1000), alice, bob);
env(pay(gw, alice, usd(100)));
env(pay(gw, bob, usd(100)));
env.close();
auto const aliceSeq = env.seq(alice);
auto const bobSeq = env.seq(bob);
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const preAliceUSD = env.balance(alice, usd.issue());
auto const preBobUSD = env.balance(bob, usd.issue());
auto const batchFee = batch::calcBatchFee(env, 1, 3);
uint256 const chkID{getCheckIndex(bob, bobSeq + 1)};
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
batch::Inner(ticket::create(bob, 10), bobSeq),
batch::Inner(check::create(bob, alice, usd(10)), 0, bobSeq + 1),
batch::Inner(check::cash(alice, chkID, usd(10)), aliceSeq + 1),
batch::Sig(bob));
env.close();
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "Batch",
.result = "tesSUCCESS",
.txHash = batchID,
.batchID = std::nullopt},
{.index = 1,
.txType = "TicketCreate",
.result = "tesSUCCESS",
.txHash = txIDs[0],
.batchID = batchID},
{.index = 2,
.txType = "CheckCreate",
.result = "tesSUCCESS",
.txHash = txIDs[1],
.batchID = batchID},
{.index = 3,
.txType = "CheckCash",
.result = "tesSUCCESS",
.txHash = txIDs[2],
.batchID = batchID},
};
validateClosedLedger(env, testCases);
BEAST_EXPECT(env.seq(alice) == aliceSeq + 2);
BEAST_EXPECT(env.seq(bob) == bobSeq + 10 + 1);
BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
BEAST_EXPECT(env.balance(bob) == preBob);
BEAST_EXPECT(env.balance(alice, usd.issue()) == preAliceUSD + usd(10));
BEAST_EXPECT(env.balance(bob, usd.issue()) == preBobUSD - usd(10));
}
void
testObjectCreate3rdParty(FeatureBitset features)
{
testcase("object create w/ 3rd party");
using namespace test::jtx;
using namespace std::literals;
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const carol = Account("carol");
auto const gw = Account("gw");
auto const usd = gw["USD"];
env.fund(XRP(10000), alice, bob, carol, gw);
env.close();
env.trust(usd(1000), alice, bob);
env(pay(gw, alice, usd(100)));
env(pay(gw, bob, usd(100)));
env.close();
auto const aliceSeq = env.seq(alice);
auto const bobSeq = env.seq(bob);
auto const carolSeq = env.seq(carol);
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const preCarol = env.balance(carol);
auto const preAliceUSD = env.balance(alice, usd.issue());
auto const preBobUSD = env.balance(bob, usd.issue());
auto const batchFee = batch::calcBatchFee(env, 2, 2);
uint256 const chkID{getCheckIndex(bob, env.seq(bob))};
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(carol, carolSeq, batchFee, tfAllOrNothing),
batch::Inner(check::create(bob, alice, usd(10)), bobSeq),
batch::Inner(check::cash(alice, chkID, usd(10)), aliceSeq),
batch::Sig(alice, bob));
env.close();
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "Batch",
.result = "tesSUCCESS",
.txHash = batchID,
.batchID = std::nullopt},
{.index = 1,
.txType = "CheckCreate",
.result = "tesSUCCESS",
.txHash = txIDs[0],
.batchID = batchID},
{.index = 2,
.txType = "CheckCash",
.result = "tesSUCCESS",
.txHash = txIDs[1],
.batchID = batchID},
};
validateClosedLedger(env, testCases);
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
BEAST_EXPECT(env.seq(bob) == bobSeq + 1);
BEAST_EXPECT(env.seq(carol) == carolSeq + 1);
BEAST_EXPECT(env.balance(alice) == preAlice);
BEAST_EXPECT(env.balance(bob) == preBob);
BEAST_EXPECT(env.balance(carol) == preCarol - batchFee);
BEAST_EXPECT(env.balance(alice, usd.issue()) == preAliceUSD + usd(10));
BEAST_EXPECT(env.balance(bob, usd.issue()) == preBobUSD - usd(10));
}
void
testTickets(FeatureBitset features)
{
{
testcase("tickets outer");
using namespace test::jtx;
using namespace std::literals;
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
env.fund(XRP(10000), alice, bob);
env.close();
std::uint32_t const aliceTicketSeq{env.seq(alice) + 1};
env(ticket::create(alice, 10));
env.close();
auto const aliceSeq = env.seq(alice);
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, 0, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(1)), aliceSeq + 0),
batch::Inner(pay(alice, bob, XRP(2)), aliceSeq + 1),
ticket::Use(aliceTicketSeq));
env.close();
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "Batch",
.result = "tesSUCCESS",
.txHash = batchID,
.batchID = std::nullopt},
{.index = 1,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[0],
.batchID = batchID},
{.index = 2,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[1],
.batchID = batchID},
};
validateClosedLedger(env, testCases);
auto const sle = env.le(keylet::account(alice));
BEAST_EXPECT(sle);
BEAST_EXPECT(sle->getFieldU32(sfOwnerCount) == 9);
BEAST_EXPECT(sle->getFieldU32(sfTicketCount) == 9);
BEAST_EXPECT(env.seq(alice) == aliceSeq + 2);
BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee);
BEAST_EXPECT(env.balance(bob) == preBob + XRP(3));
}
{
testcase("tickets inner");
using namespace test::jtx;
using namespace std::literals;
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
env.fund(XRP(10000), alice, bob);
env.close();
std::uint32_t aliceTicketSeq{env.seq(alice) + 1};
env(ticket::create(alice, 10));
env.close();
auto const aliceSeq = env.seq(alice);
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(1)), 0, aliceTicketSeq),
batch::Inner(pay(alice, bob, XRP(2)), 0, aliceTicketSeq + 1));
env.close();
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "Batch",
.result = "tesSUCCESS",
.txHash = batchID,
.batchID = std::nullopt},
{.index = 1,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[0],
.batchID = batchID},
{.index = 2,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[1],
.batchID = batchID},
};
validateClosedLedger(env, testCases);
auto const sle = env.le(keylet::account(alice));
BEAST_EXPECT(sle);
BEAST_EXPECT(sle->getFieldU32(sfOwnerCount) == 8);
BEAST_EXPECT(sle->getFieldU32(sfTicketCount) == 8);
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee);
BEAST_EXPECT(env.balance(bob) == preBob + XRP(3));
}
{
testcase("tickets outer inner");
using namespace test::jtx;
using namespace std::literals;
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
env.fund(XRP(10000), alice, bob);
env.close();
std::uint32_t const aliceTicketSeq{env.seq(alice) + 1};
env(ticket::create(alice, 10));
env.close();
auto const aliceSeq = env.seq(alice);
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, 0, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(1)), 0, aliceTicketSeq + 1),
batch::Inner(pay(alice, bob, XRP(2)), aliceSeq),
ticket::Use(aliceTicketSeq));
env.close();
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "Batch",
.result = "tesSUCCESS",
.txHash = batchID,
.batchID = std::nullopt},
{.index = 1,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[0],
.batchID = batchID},
{.index = 2,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[1],
.batchID = batchID},
};
validateClosedLedger(env, testCases);
auto const sle = env.le(keylet::account(alice));
BEAST_EXPECT(sle);
BEAST_EXPECT(sle->getFieldU32(sfOwnerCount) == 8);
BEAST_EXPECT(sle->getFieldU32(sfTicketCount) == 8);
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee);
BEAST_EXPECT(env.balance(bob) == preBob + XRP(3));
}
}
void
testSequenceOpenLedger(FeatureBitset features)
{
testcase("sequence open ledger");
using namespace test::jtx;
using namespace std::literals;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const carol = Account("carol");
// Before Batch Txn w/ retry following ledger
{
// IMPORTANT: The batch txn is applied first, then the noop txn.
// Because of this ordering, the noop txn is not applied and is
// overwritten by the payment in the batch transaction. Because the
// terPRE_SEQ is outside of the batch this noop transaction will ge
// reapplied in the following ledger
Env env{*this, features};
env.fund(XRP(10000), alice, bob, carol);
env.close();
auto const aliceSeq = env.seq(alice);
auto const carolSeq = env.seq(carol);
// AccountSet Txn
auto const noopTxn = env.jt(noop(alice), Seq(aliceSeq + 2));
auto const noopTxnID = to_string(noopTxn.stx->getTransactionID());
env(noopTxn, Ter(terPRE_SEQ));
// Batch Txn
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(carol, carolSeq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
batch::Inner(pay(alice, bob, XRP(2)), aliceSeq + 1),
batch::Sig(alice));
env.close();
{
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "Batch",
.result = "tesSUCCESS",
.txHash = batchID,
.batchID = std::nullopt},
{.index = 1,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[0],
.batchID = batchID},
{.index = 2,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[1],
.batchID = batchID},
};
validateClosedLedger(env, testCases);
}
env.close();
{
// next ledger contains noop txn
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "AccountSet",
.result = "tesSUCCESS",
.txHash = noopTxnID,
.batchID = std::nullopt},
};
validateClosedLedger(env, testCases);
}
}
// Before Batch Txn w/ same sequence
{
// IMPORTANT: The batch txn is applied first, then the noop txn.
// Because of this ordering, the noop txn is not applied and is
// overwritten by the payment in the batch transaction.
Env env{*this, features};
env.fund(XRP(10000), alice, bob);
env.close();
auto const aliceSeq = env.seq(alice);
// AccountSet Txn
auto const noopTxn = env.jt(noop(alice), Seq(aliceSeq + 1));
env(noopTxn, Ter(terPRE_SEQ));
// Batch Txn
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(1)), aliceSeq + 1),
batch::Inner(pay(alice, bob, XRP(2)), aliceSeq + 2));
env.close();
{
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "Batch",
.result = "tesSUCCESS",
.txHash = batchID,
.batchID = std::nullopt},
{.index = 1,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[0],
.batchID = batchID},
{.index = 2,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[1],
.batchID = batchID},
};
validateClosedLedger(env, testCases);
}
env.close();
{
// next ledger is empty
std::vector<TestLedgerData> const testCases = {};
validateClosedLedger(env, testCases);
}
}
// After Batch Txn w/ same sequence
{
// IMPORTANT: The batch txn is applied first, then the noop txn.
// Because of this ordering, the noop txn is not applied and is
// overwritten by the payment in the batch transaction.
Env env{*this, features};
env.fund(XRP(10000), alice, bob);
env.close();
auto const aliceSeq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(1)), aliceSeq + 1),
batch::Inner(pay(alice, bob, XRP(2)), aliceSeq + 2));
auto const noopTxn = env.jt(noop(alice), Seq(aliceSeq + 1));
env(noopTxn, Ter(tesSUCCESS));
env.close();
{
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "Batch",
.result = "tesSUCCESS",
.txHash = batchID,
.batchID = std::nullopt},
{.index = 1,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[0],
.batchID = batchID},
{.index = 2,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[1],
.batchID = batchID},
};
validateClosedLedger(env, testCases);
}
env.close();
{
// next ledger is empty
std::vector<TestLedgerData> const testCases = {};
validateClosedLedger(env, testCases);
}
}
// Outer Batch terPRE_SEQ
{
Env env{*this, features};
env.fund(XRP(10000), alice, bob, carol);
env.close();
auto const aliceSeq = env.seq(alice);
auto const carolSeq = env.seq(carol);
// Batch Txn
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const [txIDs, batchID] = submitBatch(
env,
terPRE_SEQ,
batch::outer(carol, carolSeq + 1, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
batch::Inner(pay(alice, bob, XRP(2)), aliceSeq + 1),
batch::Sig(alice));
// AccountSet Txn
auto const noopTxn = env.jt(noop(carol), Seq(carolSeq));
auto const noopTxnID = to_string(noopTxn.stx->getTransactionID());
env(noopTxn, Ter(tesSUCCESS));
env.close();
{
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "AccountSet",
.result = "tesSUCCESS",
.txHash = noopTxnID,
.batchID = std::nullopt},
{.index = 1,
.txType = "Batch",
.result = "tesSUCCESS",
.txHash = batchID,
.batchID = std::nullopt},
{.index = 2,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[0],
.batchID = batchID},
{.index = 3,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[1],
.batchID = batchID},
};
validateClosedLedger(env, testCases);
}
env.close();
{
// next ledger contains no transactions
std::vector<TestLedgerData> const testCases = {};
validateClosedLedger(env, testCases);
}
}
}
void
testTicketsOpenLedger(FeatureBitset features)
{
testcase("tickets open ledger");
using namespace test::jtx;
using namespace std::literals;
auto const alice = Account("alice");
auto const bob = Account("bob");
// Before Batch Txn w/ same ticket
{
// IMPORTANT: The batch txn is applied first, then the noop txn.
// Because of this ordering, the noop txn is not applied and is
// overwritten by the payment in the batch transaction.
Env env{*this, features};
env.fund(XRP(10000), alice, bob);
env.close();
std::uint32_t const aliceTicketSeq{env.seq(alice) + 1};
env(ticket::create(alice, 10));
env.close();
auto const aliceSeq = env.seq(alice);
// AccountSet Txn
auto const noopTxn = env.jt(noop(alice), ticket::Use(aliceTicketSeq + 1));
env(noopTxn, Ter(tesSUCCESS));
// Batch Txn
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, 0, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(1)), 0, aliceTicketSeq + 1),
batch::Inner(pay(alice, bob, XRP(2)), aliceSeq),
ticket::Use(aliceTicketSeq));
env.close();
{
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "Batch",
.result = "tesSUCCESS",
.txHash = batchID,
.batchID = std::nullopt},
{.index = 1,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[0],
.batchID = batchID},
{.index = 2,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[1],
.batchID = batchID},
};
validateClosedLedger(env, testCases);
}
env.close();
{
// next ledger is empty
std::vector<TestLedgerData> const testCases = {};
validateClosedLedger(env, testCases);
}
}
// After Batch Txn w/ same ticket
{
// IMPORTANT: The batch txn is applied first, then the noop txn.
// Because of this ordering, the noop txn is not applied and is
// overwritten by the payment in the batch transaction.
Env env{*this, features};
env.fund(XRP(10000), alice, bob);
env.close();
std::uint32_t const aliceTicketSeq{env.seq(alice) + 1};
env(ticket::create(alice, 10));
env.close();
auto const aliceSeq = env.seq(alice);
// Batch Txn
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, 0, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(1)), 0, aliceTicketSeq + 1),
batch::Inner(pay(alice, bob, XRP(2)), aliceSeq),
ticket::Use(aliceTicketSeq));
// AccountSet Txn
auto const noopTxn = env.jt(noop(alice), ticket::Use(aliceTicketSeq + 1));
env(noopTxn);
env.close();
{
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "Batch",
.result = "tesSUCCESS",
.txHash = batchID,
.batchID = std::nullopt},
{.index = 1,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[0],
.batchID = batchID},
{.index = 2,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[1],
.batchID = batchID},
};
validateClosedLedger(env, testCases);
}
env.close();
{
// next ledger is empty
std::vector<TestLedgerData> const testCases = {};
validateClosedLedger(env, testCases);
}
}
}
void
testObjectsOpenLedger(FeatureBitset features)
{
testcase("objects open ledger");
using namespace test::jtx;
using namespace std::literals;
auto const alice = Account("alice");
auto const bob = Account("bob");
// Consume Object Before Batch Txn
{
// IMPORTANT: The initial result of `CheckCash` is tecNO_ENTRY
// because the create transaction has not been applied because the
// batch will run in the close ledger process. The batch will be
// allied and then retry this transaction in the current ledger.
Env env{*this, features};
env.fund(XRP(10000), alice, bob);
env.close();
std::uint32_t const aliceTicketSeq{env.seq(alice) + 1};
env(ticket::create(alice, 10));
env.close();
auto const aliceSeq = env.seq(alice);
// CheckCash Txn
uint256 const chkID{getCheckIndex(alice, aliceSeq)};
auto const objTxn = env.jt(check::cash(bob, chkID, XRP(10)));
auto const objTxnID = to_string(objTxn.stx->getTransactionID());
env(objTxn, Ter(tecNO_ENTRY));
// Batch Txn
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, 0, batchFee, tfAllOrNothing),
batch::Inner(check::create(alice, bob, XRP(10)), aliceSeq),
batch::Inner(pay(alice, bob, XRP(1)), 0, aliceTicketSeq + 1),
ticket::Use(aliceTicketSeq));
env.close();
{
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "Batch",
.result = "tesSUCCESS",
.txHash = batchID,
.batchID = std::nullopt},
{.index = 1,
.txType = "CheckCreate",
.result = "tesSUCCESS",
.txHash = txIDs[0],
.batchID = batchID},
{.index = 2,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[1],
.batchID = batchID},
{.index = 3,
.txType = "CheckCash",
.result = "tesSUCCESS",
.txHash = objTxnID,
.batchID = std::nullopt},
};
validateClosedLedger(env, testCases);
}
env.close();
{
// next ledger is empty
std::vector<TestLedgerData> const testCases = {};
validateClosedLedger(env, testCases);
}
}
// Create Object Before Batch Txn
{
Env env{*this, features};
env.fund(XRP(10000), alice, bob);
env.close();
std::uint32_t const aliceTicketSeq{env.seq(alice) + 1};
env(ticket::create(alice, 10));
env.close();
auto const aliceSeq = env.seq(alice);
auto const bobSeq = env.seq(bob);
// CheckCreate Txn
uint256 const chkID{getCheckIndex(alice, aliceSeq)};
auto const objTxn = env.jt(check::create(alice, bob, XRP(10)));
auto const objTxnID = to_string(objTxn.stx->getTransactionID());
env(objTxn, Ter(tesSUCCESS));
// Batch Txn
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, 0, batchFee, tfAllOrNothing),
batch::Inner(check::cash(bob, chkID, XRP(10)), bobSeq),
batch::Inner(pay(alice, bob, XRP(1)), 0, aliceTicketSeq + 1),
ticket::Use(aliceTicketSeq),
batch::Sig(bob));
env.close();
{
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "CheckCreate",
.result = "tesSUCCESS",
.txHash = objTxnID,
.batchID = std::nullopt},
{.index = 1,
.txType = "Batch",
.result = "tesSUCCESS",
.txHash = batchID,
.batchID = std::nullopt},
{.index = 2,
.txType = "CheckCash",
.result = "tesSUCCESS",
.txHash = txIDs[0],
.batchID = batchID},
{.index = 3,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[1],
.batchID = batchID},
};
validateClosedLedger(env, testCases);
}
}
// After Batch Txn
{
// IMPORTANT: The initial result of `CheckCash` is tecNO_ENTRY
// because the create transaction has not been applied because the
// batch will run in the close ledger process. The batch will be
// applied and then retry this transaction in the current ledger.
Env env{*this, features};
env.fund(XRP(10000), alice, bob);
env.close();
std::uint32_t const aliceTicketSeq{env.seq(alice) + 1};
env(ticket::create(alice, 10));
env.close();
auto const aliceSeq = env.seq(alice);
// Batch Txn
auto const batchFee = batch::calcBatchFee(env, 0, 2);
uint256 const chkID{getCheckIndex(alice, aliceSeq)};
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, 0, batchFee, tfAllOrNothing),
batch::Inner(check::create(alice, bob, XRP(10)), aliceSeq),
batch::Inner(pay(alice, bob, XRP(1)), 0, aliceTicketSeq + 1),
ticket::Use(aliceTicketSeq));
// CheckCash Txn
auto const objTxn = env.jt(check::cash(bob, chkID, XRP(10)));
auto const objTxnID = to_string(objTxn.stx->getTransactionID());
env(objTxn, Ter(tecNO_ENTRY));
env.close();
{
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "Batch",
.result = "tesSUCCESS",
.txHash = batchID,
.batchID = std::nullopt},
{.index = 1,
.txType = "CheckCreate",
.result = "tesSUCCESS",
.txHash = txIDs[0],
.batchID = batchID},
{.index = 2,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[1],
.batchID = batchID},
{.index = 3,
.txType = "CheckCash",
.result = "tesSUCCESS",
.txHash = objTxnID,
.batchID = std::nullopt},
};
validateClosedLedger(env, testCases);
}
}
}
void
testPseudoTxn(FeatureBitset features)
{
testcase("pseudo txn with tfInnerBatchTxn");
using namespace test::jtx;
using namespace std::literals;
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
env.fund(XRP(10000), alice, bob);
env.close();
STTx const stx = STTx(ttAMENDMENT, [&](auto& obj) {
obj.setAccountID(sfAccount, AccountID());
obj.setFieldH256(sfAmendment, uint256(2));
obj.setFieldU32(sfLedgerSequence, env.seq(alice));
obj.setFieldU32(sfFlags, tfInnerBatchTxn);
});
std::string reason;
BEAST_EXPECT(isPseudoTx(stx));
BEAST_EXPECT(!passesLocalChecks(stx, reason));
BEAST_EXPECT(reason == "Cannot submit pseudo transactions.");
env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal j) {
auto const result = xrpl::apply(env.app(), view, stx, TapNone, j);
BEAST_EXPECT(!result.applied && result.ter == temINVALID_FLAG);
return result.applied;
});
}
void
testOpenLedger(FeatureBitset features)
{
testcase("batch open ledger");
// IMPORTANT: When a transaction is submitted outside of a batch and
// another transaction is part of the batch, the batch might fail
// because the sequence is out of order. This is because the canonical
// order of transactions is determined by the account first. So in this
// case, alice's batch comes after bob's self submitted transaction even
// though the payment was submitted after the batch.
using namespace test::jtx;
using namespace std::literals;
Env env{*this, features};
XRPAmount const baseFee = env.current()->fees().base;
auto const alice = Account("alice");
auto const bob = Account("bob");
env.fund(XRP(10000), alice, bob);
env.close();
env(noop(bob), Ter(tesSUCCESS));
env.close();
auto const aliceSeq = env.seq(alice);
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const bobSeq = env.seq(bob);
// Alice Pays Bob (Open Ledger)
auto const payTxn1 = env.jt(pay(alice, bob, XRP(10)), Seq(aliceSeq));
auto const payTxn1ID = to_string(payTxn1.stx->getTransactionID());
env(payTxn1, Ter(tesSUCCESS));
// Alice & Bob Atomic Batch
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, aliceSeq + 1, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 2),
batch::Inner(pay(bob, alice, XRP(5)), bobSeq),
batch::Sig(bob));
// Bob pays Alice (Open Ledger)
auto const payTxn2 = env.jt(pay(bob, alice, XRP(5)), Seq(bobSeq + 1));
auto const payTxn2ID = to_string(payTxn2.stx->getTransactionID());
env(payTxn2, Ter(terPRE_SEQ));
env.close();
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = payTxn1ID,
.batchID = std::nullopt},
{.index = 1,
.txType = "Batch",
.result = "tesSUCCESS",
.txHash = batchID,
.batchID = std::nullopt},
{.index = 2,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[0],
.batchID = batchID},
{.index = 3,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[1],
.batchID = batchID},
};
validateClosedLedger(env, testCases);
env.close();
{
// next ledger includes the payment txn
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = payTxn2ID,
.batchID = std::nullopt},
};
validateClosedLedger(env, testCases);
}
// Alice consumes sequences (# of txns)
BEAST_EXPECT(env.seq(alice) == aliceSeq + 3);
// Alice consumes sequences (# of txns)
BEAST_EXPECT(env.seq(bob) == bobSeq + 2);
// Alice pays XRP & Fee; Bob receives XRP & pays Fee
BEAST_EXPECT(env.balance(alice) == preAlice - XRP(10) - batchFee - baseFee);
BEAST_EXPECT(env.balance(bob) == preBob + XRP(10) - baseFee);
}
void
testBatchTxQueue(FeatureBitset features)
{
testcase("batch tx queue");
using namespace test::jtx;
using namespace std::literals;
// A Batch is never queued. Under open-ledger congestion a Batch that
// pays only its base fee cannot apply directly and, unlike an ordinary
// transaction, is rejected outright rather than held in the queue. A
// Batch that pays the escalated open-ledger fee applies directly.
{
Env env{
*this,
makeSmallQueueConfig({{Keys::kMinimumTxnInLedgerStandalone, "2"}}),
features,
nullptr,
beast::Severity::Error};
auto alice = Account("alice");
auto bob = Account("bob");
auto carol = Account("carol");
// Fund across several ledgers so the TxQ metrics stay restricted.
env.fund(XRP(10000), noripple(alice, bob));
env.close(env.now() + 5s, 10000ms);
env.fund(XRP(10000), noripple(carol));
env.close(env.now() + 5s, 10000ms);
// Fill the open ledger so escalation is active.
env(noop(alice));
env(noop(alice));
env(noop(alice));
checkMetrics(*this, env, 0, std::nullopt, 3, 2);
env(noop(carol), Ter(terQUEUED));
checkMetrics(*this, env, 1, std::nullopt, 3, 2);
auto const aliceSeq = env.seq(alice);
auto const bobSeq = env.seq(bob);
auto const batchFee = batch::calcBatchFee(env, 1, 2);
// Paying only the base fee, the Batch cannot enter the congested
// open ledger and is rejected rather than queued.
env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
batch::Inner(pay(bob, alice, XRP(5)), bobSeq),
batch::Sig(bob),
Ter(telCAN_NOT_QUEUE));
// The Batch was not queued; only carol's transaction is queued.
checkMetrics(*this, env, 1, std::nullopt, 3, 2);
// Paying the escalated open-ledger fee, the Batch applies directly.
env(batch::outer(alice, aliceSeq, openLedgerFee(env, batchFee), tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
batch::Inner(pay(bob, alice, XRP(5)), bobSeq),
batch::Sig(bob),
Ter(tesSUCCESS));
}
// inner batch transactions are counter towards the ledger tx count
{
Env env{
*this,
makeSmallQueueConfig({{Keys::kMinimumTxnInLedgerStandalone, "2"}}),
features,
nullptr,
beast::Severity::Error};
auto alice = Account("alice");
auto bob = Account("bob");
auto carol = Account("carol");
// Fund across several ledgers so the TxQ metrics stay restricted.
env.fund(XRP(10000), noripple(alice, bob));
env.close(env.now() + 5s, 10000ms);
env.fund(XRP(10000), noripple(carol));
env.close(env.now() + 5s, 10000ms);
// Fill the ledger leaving room for 1 queued transaction
env(noop(alice));
env(noop(alice));
checkMetrics(*this, env, 0, std::nullopt, 2, 2);
auto const aliceSeq = env.seq(alice);
auto const bobSeq = env.seq(bob);
auto const batchFee = batch::calcBatchFee(env, 1, 2);
// Batch Successful
{
env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
batch::Inner(pay(bob, alice, XRP(5)), bobSeq),
batch::Sig(bob),
Ter(tesSUCCESS));
}
checkMetrics(*this, env, 0, std::nullopt, 3, 2);
env(noop(carol), Ter(terQUEUED));
checkMetrics(*this, env, 1, std::nullopt, 3, 2);
}
// A Batch is never queued, so it also cannot sit behind the account's
// already-queued transactions: with a sequence gap it cannot apply
// directly and is rejected rather than queued.
{
Env env{
*this,
makeSmallQueueConfig({{Keys::kMinimumTxnInLedgerStandalone, "2"}}),
features,
nullptr,
beast::Severity::Error};
auto alice = Account("alice");
auto bob = Account("bob");
env.fund(XRP(10000), noripple(alice, bob));
env.close(env.now() + 5s, 10000ms);
// Fill the open ledger so subsequent transactions queue.
env(noop(alice));
env(noop(alice));
env(noop(alice));
checkMetrics(*this, env, 0, std::nullopt, 3, 2);
auto const aliceSeq = env.seq(alice);
// Queue two normal transactions for alice.
env(noop(alice), Seq(aliceSeq + 0), Ter(terQUEUED));
env(noop(alice), Seq(aliceSeq + 1), Ter(terQUEUED));
checkMetrics(*this, env, 2, std::nullopt, 3, 2);
// The Batch's sequence sits behind the queued transactions, so it
// cannot apply directly and is rejected rather than queued.
auto const bobSeq = env.seq(bob);
auto const batchFee = batch::calcBatchFee(env, 1, 2);
env(batch::outer(alice, aliceSeq + 2, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 3),
batch::Inner(pay(bob, alice, XRP(5)), bobSeq),
batch::Sig(bob),
Ter(telCAN_NOT_QUEUE));
// The two queued transactions are untouched.
checkMetrics(*this, env, 2, std::nullopt, 3, 2);
}
}
void
testBatchNetworkOps(FeatureBitset features)
{
testcase("batch network ops");
using namespace test::jtx;
using namespace std::literals;
Env env(*this, envconfig(), features, nullptr, beast::Severity::Disabled);
auto alice = Account("alice");
auto bob = Account("bob");
env.fund(XRP(10000), alice, bob);
env.close();
auto submitTx = [&](std::uint32_t flags) -> uint256 {
auto jt = env.jt(pay(alice, bob, XRP(1)), Txflags(flags));
Serializer s;
jt.stx->add(s);
env.app().getOPs().submitTransaction(jt.stx);
return jt.stx->getTransactionID();
};
auto processTxn = [&](std::uint32_t flags) -> uint256 {
auto jt = env.jt(pay(alice, bob, XRP(1)), Txflags(flags));
Serializer s;
jt.stx->add(s);
std::string reason;
auto transaction = std::make_shared<Transaction>(jt.stx, reason, env.app());
env.app().getOPs().processTransaction(
transaction, false, true, NetworkOPs::FailHard::Yes);
return transaction->getID();
};
// Validate: NetworkOPs::submitTransaction()
{
// Submit a tx with tfInnerBatchTxn
uint256 const txBad = submitTx(tfInnerBatchTxn);
BEAST_EXPECT(env.app().getHashRouter().getFlags(txBad) == HashRouterFlags::UNDEFINED);
}
// Validate: NetworkOPs::processTransaction()
{
uint256 const txid = processTxn(tfInnerBatchTxn);
// HashRouter::getFlags() should return LedgerFlags::BAD
BEAST_EXPECT(env.app().getHashRouter().getFlags(txid) == HashRouterFlags::BAD);
}
}
void
testBatchDelegateConsent(FeatureBitset features)
{
testcase("batch delegate consent");
using namespace test::jtx;
using namespace std::literals;
// Delegate consent bypass.
//
// Alice delegates Payment to Bob. A Batch carries an inner Payment with
// Delegate=Bob, but Bob never signs the batch (and, because the inner
// Account == the outer Account, no BatchSigners are required at all).
// Batch waives the per-inner signature check, and preflightSigValidated
// derives required signers from sfAccount only -- it ignores sfDelegate
// -- so Bob's delegated authority is exercised without Bob's consent.
//
// SECURE EXPECTATION: the delegate (Bob) must authorize the inner txn,
// so the batch must be rejected (temBAD_SIGNER) when Bob does not sign.
{
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const mallory = Account("mallory");
env.fund(XRP(10000), alice, bob, mallory);
env.close();
env(delegate::set(alice, bob, {"Payment"}));
env.close();
auto const preMallory = env.balance(mallory);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto const seq = env.seq(alice);
auto inner = batch::Inner(pay(alice, mallory, XRP(1000)), seq + 1);
inner[jss::Delegate] = bob.human();
// Bob is NOT among the batch signers; he never consents.
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
inner,
batch::Inner(pay(alice, mallory, XRP(1)), seq + 2),
Ter(temBAD_SIGNER));
env.close();
// No funds should have moved to Mallory.
BEAST_EXPECT(env.balance(mallory) == preMallory);
}
// Self-grant attribution forgery.
//
// With no pre-existing delegation, Alice places DelegateSet(authorize
// Bob) and a Delegate=Bob action in the SAME batch. The second inner
// reads the delegation created by the first from the batch's running
// view, and Bob never signs -- manufacturing on-chain attribution of
// the action to Bob without his consent.
//
// SECURE EXPECTATION: rejected (temBAD_SIGNER) -- Bob must consent, and
// a grant cannot be created and exercised within the same atomic batch.
{
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const mallory = Account("mallory");
env.fund(XRP(10000), alice, bob, mallory);
env.close();
auto const preMallory = env.balance(mallory);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto const seq = env.seq(alice);
auto inner = batch::Inner(pay(alice, mallory, XRP(1000)), seq + 2);
inner[jss::Delegate] = bob.human();
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::Inner(delegate::set(alice, bob, {"Payment"}), seq + 1),
inner,
Ter(temBAD_SIGNER));
env.close();
BEAST_EXPECT(env.balance(mallory) == preMallory);
}
// Legitimate counterpart: the same atomic grant-and-use is allowed when
// the delegate co-signs the batch -- consent is present, so it succeeds.
{
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const mallory = Account("mallory");
env.fund(XRP(10000), alice, bob, mallory);
env.close();
auto const preMallory = env.balance(mallory);
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const seq = env.seq(alice);
auto inner = batch::Inner(pay(alice, mallory, XRP(1000)), seq + 2);
inner[jss::Delegate] = bob.human();
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::Inner(delegate::set(alice, bob, {"Payment"}), seq + 1),
inner,
batch::Sig(bob),
Ter(tesSUCCESS));
env.close();
BEAST_EXPECT(env.balance(mallory) == preMallory + XRP(1000));
}
// Multi-account: a delegated inner from a non-outer account also
// requires the delegate's signature (not the account holder's).
{
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const carol = Account("carol");
env.fund(XRP(10000), alice, bob, carol);
env.close();
env(delegate::set(bob, carol, {"Payment"}));
env.close();
auto const preAlice = env.balance(alice);
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const aliceSeq = env.seq(alice);
auto const bobSeq = env.seq(bob);
auto inner = batch::Inner(pay(bob, alice, XRP(1)), bobSeq);
inner[jss::Delegate] = carol.human();
// Carol (the delegate) does not sign.
env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
inner,
batch::Inner(pay(alice, bob, XRP(2)), aliceSeq + 1),
Ter(temBAD_SIGNER));
env.close();
BEAST_EXPECT(env.balance(alice) == preAlice);
}
// Wrong signer: a batch signature from someone other than the named
// delegate does not satisfy the requirement.
{
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const carol = Account("carol");
auto const mallory = Account("mallory");
env.fund(XRP(10000), alice, bob, carol, mallory);
env.close();
env(delegate::set(alice, bob, {"Payment"}));
env.close();
auto const preMallory = env.balance(mallory);
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const seq = env.seq(alice);
auto inner = batch::Inner(pay(alice, mallory, XRP(1000)), seq + 1);
inner[jss::Delegate] = bob.human();
// Carol signs instead of the named delegate Bob.
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
inner,
batch::Inner(pay(alice, mallory, XRP(1)), seq + 2),
batch::Sig(carol),
Ter(temBAD_SIGNER));
env.close();
BEAST_EXPECT(env.balance(mallory) == preMallory);
}
// Delegate is the outer account: the outer signature already provides
// the delegate's consent, so no BatchSigners are required.
{
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
env.fund(XRP(10000), alice, bob);
env.close();
// Bob delegates Payment to Alice, who is also the batch submitter.
env(delegate::set(bob, alice, {"Payment"}));
env.close();
auto const preBob = env.balance(bob);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto const aliceSeq = env.seq(alice);
auto const bobSeq = env.seq(bob);
auto inner = batch::Inner(pay(bob, alice, XRP(1)), bobSeq);
inner[jss::Delegate] = alice.human();
env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
inner,
batch::Inner(pay(alice, bob, XRP(2)), aliceSeq + 1),
Ter(tesSUCCESS));
env.close();
// Net: Bob sends 1 to Alice, receives 2 from Alice.
BEAST_EXPECT(env.balance(bob) == preBob + XRP(1));
}
// Principal signs instead of the delegate. Bob delegates Payment to
// Carol and an inner pays from Bob with Delegate=Carol, so Carol -- not
// Bob -- is the required batch signer. Bob's own signature (the account
// holder) does not satisfy the delegate-consent requirement.
{
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const carol = Account("carol");
env.fund(XRP(10000), alice, bob, carol);
env.close();
env(delegate::set(bob, carol, {"Payment"}));
env.close();
auto const preAlice = env.balance(alice);
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const aliceSeq = env.seq(alice);
auto const bobSeq = env.seq(bob);
auto inner = batch::Inner(pay(bob, alice, XRP(1)), bobSeq);
inner[jss::Delegate] = carol.human();
// Bob (the principal) signs in place of Carol (the delegate).
env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
inner,
batch::Inner(pay(alice, bob, XRP(2)), aliceSeq + 1),
batch::Sig(bob),
Ter(temBAD_SIGNER));
env.close();
BEAST_EXPECT(env.balance(alice) == preAlice);
}
// Control for the revocation case below: identical setup, but inner 1
// is a benign payment instead of a revocation. With Alice's delegation
// intact, the delegated inner 2 succeeds -- proving the failure in the
// revocation case is caused by the revocation, not the setup.
{
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const mallory = Account("mallory");
env.fund(XRP(10000), alice, bob, mallory);
env.close();
env(delegate::set(bob, alice, {"Payment"}));
env.close();
auto const preMallory = env.balance(mallory);
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const seq = env.seq(bob);
auto inner2 = batch::Inner(pay(bob, mallory, XRP(1000)), seq + 2);
inner2[jss::Delegate] = alice.human();
// Inner 1 is Bob's own payment (no revocation); Alice co-signs for
// the delegated inner 2.
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(bob, seq, batchFee, tfIndependent),
batch::Inner(pay(bob, alice, XRP(1)), seq + 1),
inner2,
batch::Sig(alice));
env.close();
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "Batch",
.result = "tesSUCCESS",
.txHash = batchID,
.batchID = std::nullopt},
{.index = 1,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[0],
.batchID = batchID},
{.index = 2,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[1],
.batchID = batchID},
};
validateClosedLedger(env, testCases);
// The delegated payment applied: Mallory received the funds.
BEAST_EXPECT(env.balance(mallory) == preMallory + XRP(1000));
}
// Revocation within the same batch. Bob grants Alice Payment permission
// beforehand; an earlier inner revokes it before a later delegated
// inner tries to use it. Alice must still co-sign (delegate consent is
// derived from the static tx), but the revoked permission makes the
// delegated inner fail at apply time -- a grant cannot be used after it
// is removed earlier in the same batch.
{
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const mallory = Account("mallory");
env.fund(XRP(10000), alice, bob, mallory);
env.close();
env(delegate::set(bob, alice, {"Payment"}));
env.close();
auto const preMallory = env.balance(mallory);
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const seq = env.seq(bob);
// Inner 2: Alice acts as Bob's delegate, so Alice is the required
// batch signer.
auto inner2 = batch::Inner(pay(bob, mallory, XRP(1000)), seq + 2);
inner2[jss::Delegate] = alice.human();
// tfIndependent: inner 1 (the revocation) applies; inner 2 then
// fails for lack of permission without reverting inner 1. An empty
// permission list deletes the Delegate object.
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(bob, seq, batchFee, tfIndependent),
batch::Inner(delegate::set(bob, alice, {}), seq + 1),
inner2,
batch::Sig(alice));
env.close();
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "Batch",
.result = "tesSUCCESS",
.txHash = batchID,
.batchID = std::nullopt},
{.index = 1,
.txType = "DelegateSet",
.result = "tesSUCCESS",
.txHash = txIDs[0],
.batchID = batchID},
// inner 2 fails: Alice's permission was revoked in inner 1.
};
validateClosedLedger(env, testCases);
// The delegated payment never applied.
BEAST_EXPECT(env.balance(mallory) == preMallory);
BEAST_EXPECT(env.rpc("tx", txIDs[1])[jss::result][jss::error] == "txnNotFound");
}
}
void
testBatchDelegate(FeatureBitset features)
{
testcase("batch delegate");
using namespace test::jtx;
using namespace std::literals;
// delegated non atomic inner
{
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account("gw");
auto const usd = gw["USD"];
env.fund(XRP(10000), alice, bob, gw);
env.close();
env(delegate::set(alice, bob, {"Payment"}));
env.close();
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const seq = env.seq(alice);
auto tx = batch::Inner(pay(alice, bob, XRP(1)), seq + 1);
tx[jss::Delegate] = bob.human();
// The delegate (Bob) authorizes the delegated inner, so Bob must
// provide the batch signature.
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfAllOrNothing),
tx,
batch::Inner(pay(alice, bob, XRP(2)), seq + 2),
batch::Sig(bob));
env.close();
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "Batch",
.result = "tesSUCCESS",
.txHash = batchID,
.batchID = std::nullopt},
{.index = 1,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[0],
.batchID = batchID},
{.index = 2,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[1],
.batchID = batchID},
};
validateClosedLedger(env, testCases);
// Alice consumes sequences (# of txns)
BEAST_EXPECT(env.seq(alice) == seq + 3);
// Alice pays XRP & Fee; Bob receives XRP
BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee);
BEAST_EXPECT(env.balance(bob) == preBob + XRP(3));
}
// delegated atomic inner
{
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const carol = Account("carol");
auto const gw = Account("gw");
auto const usd = gw["USD"];
env.fund(XRP(10000), alice, bob, carol, gw);
env.close();
env(delegate::set(bob, carol, {"Payment"}));
env.close();
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const preCarol = env.balance(carol);
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const aliceSeq = env.seq(alice);
auto const bobSeq = env.seq(bob);
auto tx = batch::Inner(pay(bob, alice, XRP(1)), bobSeq);
tx[jss::Delegate] = carol.human();
// Carol is the delegate authorizing the inner txn on Bob's behalf,
// so Carol -- not Bob -- must provide the batch signature.
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
tx,
batch::Inner(pay(alice, bob, XRP(2)), aliceSeq + 1),
batch::Sig(carol));
env.close();
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "Batch",
.result = "tesSUCCESS",
.txHash = batchID,
.batchID = std::nullopt},
{.index = 1,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[0],
.batchID = batchID},
{.index = 2,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[1],
.batchID = batchID},
};
validateClosedLedger(env, testCases);
BEAST_EXPECT(env.seq(alice) == aliceSeq + 2);
BEAST_EXPECT(env.seq(bob) == bobSeq + 1);
BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1) - batchFee);
BEAST_EXPECT(env.balance(bob) == preBob + XRP(1));
// NOTE: Carol would normally pay the fee for delegated txns, but
// because the batch is atomic, the fee is paid by the batch
BEAST_EXPECT(env.balance(carol) == preCarol);
}
// delegated non atomic inner (AccountSet)
// this also makes sure tfInnerBatchTxn won't block delegated AccountSet
// with granular permission
{
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account("gw");
auto const usd = gw["USD"];
env.fund(XRP(10000), alice, bob, gw);
env.close();
env(delegate::set(alice, bob, {"AccountDomainSet"}));
env.close();
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const seq = env.seq(alice);
auto tx = batch::Inner(noop(alice), seq + 1);
std::string const domain = "example.com";
tx[sfDomain.jsonName] = strHex(domain);
tx[jss::Delegate] = bob.human();
// Bob is the delegate authorizing the inner AccountSet, so Bob must
// provide the batch signature.
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfAllOrNothing),
tx,
batch::Inner(pay(alice, bob, XRP(2)), seq + 2),
batch::Sig(bob));
env.close();
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "Batch",
.result = "tesSUCCESS",
.txHash = batchID,
.batchID = std::nullopt},
{.index = 1,
.txType = "AccountSet",
.result = "tesSUCCESS",
.txHash = txIDs[0],
.batchID = batchID},
{.index = 2,
.txType = "Payment",
.result = "tesSUCCESS",
.txHash = txIDs[1],
.batchID = batchID},
};
validateClosedLedger(env, testCases);
// Alice consumes sequences (# of txns)
BEAST_EXPECT(env.seq(alice) == seq + 3);
// Alice pays XRP & Fee; Bob receives XRP
BEAST_EXPECT(env.balance(alice) == preAlice - XRP(2) - batchFee);
BEAST_EXPECT(env.balance(bob) == preBob + XRP(2));
}
// delegated non atomic inner (MPTokenIssuanceSet)
// this also makes sure tfInnerBatchTxn won't block delegated
// MPTokenIssuanceSet with granular permission
{
Env env{*this, features};
Account const alice{"alice"};
Account const bob{"bob"};
env.fund(XRP(100000), alice, bob);
env.close();
auto const mptID = makeMptID(env.seq(alice), alice);
MPTTester mpt(env, alice, {.fund = false});
env.close();
mpt.create({.flags = tfMPTCanLock});
env.close();
// alice gives granular permission to bob of MPTokenIssuanceLock
env(delegate::set(alice, bob, {"MPTokenIssuanceLock", "MPTokenIssuanceUnlock"}));
env.close();
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 1, 2);
json::Value jv1;
jv1[sfTransactionType] = jss::MPTokenIssuanceSet;
jv1[sfAccount] = alice.human();
jv1[sfDelegate] = bob.human();
jv1[sfSequence] = seq + 1;
jv1[sfMPTokenIssuanceID] = to_string(mptID);
jv1[sfFlags] = tfMPTLock;
json::Value jv2;
jv2[sfTransactionType] = jss::MPTokenIssuanceSet;
jv2[sfAccount] = alice.human();
jv2[sfDelegate] = bob.human();
jv2[sfSequence] = seq + 2;
jv2[sfMPTokenIssuanceID] = to_string(mptID);
jv2[sfFlags] = tfMPTUnlock;
// Both inners are delegated to Bob, so Bob must provide the batch
// signature (one signer covers both).
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::Inner(jv1, seq + 1),
batch::Inner(jv2, seq + 2),
batch::Sig(bob));
env.close();
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "Batch",
.result = "tesSUCCESS",
.txHash = batchID,
.batchID = std::nullopt},
{.index = 1,
.txType = "MPTokenIssuanceSet",
.result = "tesSUCCESS",
.txHash = txIDs[0],
.batchID = batchID},
{.index = 2,
.txType = "MPTokenIssuanceSet",
.result = "tesSUCCESS",
.txHash = txIDs[1],
.batchID = batchID},
};
validateClosedLedger(env, testCases);
}
// delegated non atomic inner (TrustSet)
// this also makes sure tfInnerBatchTxn won't block delegated TrustSet
// with granular permission
{
Env env{*this, features};
Account const gw{"gw"};
Account const alice{"alice"};
Account const bob{"bob"};
env.fund(XRP(10000), gw, alice, bob);
env(fset(gw, asfRequireAuth));
env.close();
env(trust(alice, gw["USD"](50)));
env.close();
env(delegate::set(gw, bob, {"TrustlineAuthorize", "TrustlineFreeze"}));
env.close();
auto const seq = env.seq(gw);
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto jv1 = trust(gw, gw["USD"](0), alice, tfSetfAuth);
jv1[sfDelegate] = bob.human();
auto jv2 = trust(gw, gw["USD"](0), alice, tfSetFreeze);
jv2[sfDelegate] = bob.human();
// Both inners are delegated to Bob, so Bob must provide the batch
// signature.
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(gw, seq, batchFee, tfAllOrNothing),
batch::Inner(jv1, seq + 1),
batch::Inner(jv2, seq + 2),
batch::Sig(bob));
env.close();
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "Batch",
.result = "tesSUCCESS",
.txHash = batchID,
.batchID = std::nullopt},
{.index = 1,
.txType = "TrustSet",
.result = "tesSUCCESS",
.txHash = txIDs[0],
.batchID = batchID},
{.index = 2,
.txType = "TrustSet",
.result = "tesSUCCESS",
.txHash = txIDs[1],
.batchID = batchID},
};
validateClosedLedger(env, testCases);
}
// inner transaction not authorized by the delegating account.
{
Env env{*this, features};
Account const gw{"gw"};
Account const alice{"alice"};
Account const bob{"bob"};
env.fund(XRP(10000), gw, alice, bob);
env(fset(gw, asfRequireAuth));
env.close();
env(trust(alice, gw["USD"](50)));
env.close();
env(delegate::set(gw, bob, {"TrustlineAuthorize", "TrustlineFreeze"}));
env.close();
auto const seq = env.seq(gw);
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto jv1 = trust(gw, gw["USD"](0), alice, tfSetFreeze);
jv1[sfDelegate] = bob.human();
auto jv2 = trust(gw, gw["USD"](0), alice, tfClearFreeze);
jv2[sfDelegate] = bob.human();
// Both inners are delegated to Bob, so Bob must provide the batch
// signature; jv2 still fails preclaim for lack of permission.
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(gw, seq, batchFee, tfIndependent),
batch::Inner(jv1, seq + 1),
// terNO_DELEGATE_PERMISSION: not authorized to clear freeze
batch::Inner(jv2, seq + 2),
batch::Sig(bob));
env.close();
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "Batch",
.result = "tesSUCCESS",
.txHash = batchID,
.batchID = std::nullopt},
{.index = 1,
.txType = "TrustSet",
.result = "tesSUCCESS",
.txHash = txIDs[0],
.batchID = batchID},
// jv2 fails with terNO_DELEGATE_PERMISSION.
};
validateClosedLedger(env, testCases);
// verify jv2 is not present in the closed ledger.
BEAST_EXPECT(env.rpc("tx", txIDs[1])[jss::result][jss::error] == "txnNotFound");
}
}
void
testValidateRPCResponse(FeatureBitset features)
{
// Verifying that the RPC response from submit includes
// the account_sequence_available, account_sequence_next,
// open_ledger_cost and validated_ledger_index fields.
testcase("Validate RPC response");
using namespace jtx;
Env env(*this, features);
Account const alice("alice");
Account const bob("bob");
env.fund(XRP(10000), alice, bob);
env.close();
// tes
{
auto const baseFee = env.current()->fees().base;
auto const aliceSeq = env.seq(alice);
auto jtx = env.jt(pay(alice, bob, XRP(1)));
Serializer s;
jtx.stx->add(s);
auto const jr = env.rpc("submit", strHex(s.slice()))[jss::result];
env.close();
BEAST_EXPECT(jr.isMember(jss::account_sequence_available));
BEAST_EXPECT(jr[jss::account_sequence_available].asUInt() == aliceSeq + 1);
BEAST_EXPECT(jr.isMember(jss::account_sequence_next));
BEAST_EXPECT(jr[jss::account_sequence_next].asUInt() == aliceSeq + 1);
BEAST_EXPECT(jr.isMember(jss::open_ledger_cost));
BEAST_EXPECT(jr[jss::open_ledger_cost] == to_string(baseFee));
BEAST_EXPECT(jr.isMember(jss::validated_ledger_index));
}
// tec failure
{
auto const baseFee = env.current()->fees().base;
auto const aliceSeq = env.seq(alice);
env(fset(bob, asfRequireDest));
auto jtx = env.jt(pay(alice, bob, XRP(1)), Seq(aliceSeq));
Serializer s;
jtx.stx->add(s);
auto const jr = env.rpc("submit", strHex(s.slice()))[jss::result];
env.close();
BEAST_EXPECT(jr.isMember(jss::account_sequence_available));
BEAST_EXPECT(jr[jss::account_sequence_available].asUInt() == aliceSeq + 1);
BEAST_EXPECT(jr.isMember(jss::account_sequence_next));
BEAST_EXPECT(jr[jss::account_sequence_next].asUInt() == aliceSeq + 1);
BEAST_EXPECT(jr.isMember(jss::open_ledger_cost));
BEAST_EXPECT(jr[jss::open_ledger_cost] == to_string(baseFee));
BEAST_EXPECT(jr.isMember(jss::validated_ledger_index));
}
// tem failure
{
auto const baseFee = env.current()->fees().base;
auto const aliceSeq = env.seq(alice);
auto jtx = env.jt(pay(alice, bob, XRP(1)), Seq(aliceSeq + 1));
Serializer s;
jtx.stx->add(s);
auto const jr = env.rpc("submit", strHex(s.slice()))[jss::result];
env.close();
BEAST_EXPECT(jr.isMember(jss::account_sequence_available));
BEAST_EXPECT(jr[jss::account_sequence_available].asUInt() == aliceSeq);
BEAST_EXPECT(jr.isMember(jss::account_sequence_next));
BEAST_EXPECT(jr[jss::account_sequence_next].asUInt() == aliceSeq);
BEAST_EXPECT(jr.isMember(jss::open_ledger_cost));
BEAST_EXPECT(jr[jss::open_ledger_cost] == to_string(baseFee));
BEAST_EXPECT(jr.isMember(jss::validated_ledger_index));
}
}
void
testBatchCalculateBaseFee(FeatureBitset features)
{
using namespace jtx;
Env env(*this, features);
Account const alice("alice");
Account const bob("bob");
Account const carol("carol");
env.fund(XRP(10000), alice, bob, carol);
env.close();
auto getBaseFee = [&](JTx const& jtx) -> XRPAmount {
Serializer s;
jtx.stx->add(s);
return Batch::calculateBaseFee(*env.current(), *jtx.stx);
};
// bad: Inner Batch transaction found
{
auto const seq = env.seq(alice);
XRPAmount const batchFee = batch::calcBatchFee(env, 0, 2);
auto jtx = env.jt(
batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::Inner(batch::outer(alice, seq, batchFee, tfAllOrNothing), seq),
batch::Inner(pay(alice, bob, XRP(1)), seq + 2));
XRPAmount const txBaseFee = getBaseFee(jtx);
BEAST_EXPECT(txBaseFee == XRPAmount(kInitialXrp));
}
// bad: Raw Transactions array exceeds max entries.
{
auto const seq = env.seq(alice);
XRPAmount const batchFee = batch::calcBatchFee(env, 0, 2);
auto jtx = env.jt(
batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
batch::Inner(pay(alice, bob, XRP(1)), seq + 2),
batch::Inner(pay(alice, bob, XRP(1)), seq + 3),
batch::Inner(pay(alice, bob, XRP(1)), seq + 4),
batch::Inner(pay(alice, bob, XRP(1)), seq + 5),
batch::Inner(pay(alice, bob, XRP(1)), seq + 6),
batch::Inner(pay(alice, bob, XRP(1)), seq + 7),
batch::Inner(pay(alice, bob, XRP(1)), seq + 8),
batch::Inner(pay(alice, bob, XRP(1)), seq + 9));
XRPAmount const txBaseFee = getBaseFee(jtx);
BEAST_EXPECT(txBaseFee == XRPAmount(kInitialXrp));
}
// bad: Signers array exceeds max entries.
{
auto const seq = env.seq(alice);
XRPAmount const batchFee = batch::calcBatchFee(env, 0, 2);
auto jtx = env.jt(
batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
batch::Inner(pay(alice, bob, XRP(5)), seq + 2),
batch::Sig(std::vector<Reg>(kMaxBatchSigners + 1, bob)));
XRPAmount const txBaseFee = getBaseFee(jtx);
BEAST_EXPECT(txBaseFee == XRPAmount(kInitialXrp));
}
// good:
{
auto const seq = env.seq(alice);
XRPAmount const batchFee = batch::calcBatchFee(env, 0, 2);
auto jtx = env.jt(
batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
batch::Inner(pay(bob, alice, XRP(2)), seq + 2));
XRPAmount const txBaseFee = getBaseFee(jtx);
BEAST_EXPECT(txBaseFee == batchFee);
}
}
void
testStandaloneInnerBatchFlag(FeatureBitset features)
{
testcase("standalone tx with tfInnerBatchTxn rejected");
using namespace test::jtx;
using namespace std::literals;
// A standalone Payment with tfInnerBatchTxn must be rejected.
// Without proper guards this would bypass signature verification
// in preflight2.
{
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
env.fund(XRP(10000), alice, bob);
env.close();
// Submit a normal Payment with tfInnerBatchTxn flag.
// preflight1 must reject with temINVALID_INNER_BATCH because
// the flag is set but no parentBatchId exists.
env(pay(alice, bob, XRP(1)), Txflags(tfInnerBatchTxn), Ter(telENV_RPC_FAILED));
env.close();
// Verify via direct apply path (bypassing RPC layer)
env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal j) {
// Construct a Payment STTx with tfInnerBatchTxn,
// empty signing pub key, and no signature — mimicking
// what an attacker would send to skip sig verification.
STTx const stx = STTx(ttPAYMENT, [&](auto& obj) {
obj.setAccountID(sfAccount, alice.id());
obj.setAccountID(sfDestination, bob.id());
obj.setFieldAmount(sfAmount, XRP(1));
obj.setFieldAmount(sfFee, XRP(0));
obj.setFieldU32(sfSequence, env.seq(alice));
obj.setFieldU32(sfFlags, tfInnerBatchTxn);
});
auto const result = xrpl::apply(env.app(), view, stx, TapNone, j);
// Must NOT be applied — signature was never checked
BEAST_EXPECT(!result.applied);
return false;
});
}
}
void
testOuterBinding(FeatureBitset features)
{
testcase("outer binding");
using namespace test::jtx;
// Signatures captured from one outer account cannot be replayed
// under a different outer account.
{
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const carol = Account("carol");
auto const eve = Account("eve");
env.fund(XRP(10000), alice, bob, carol, eve);
env.close();
auto const preCarol = env.balance(carol);
auto const bobSeq = env.seq(bob);
auto const carolSeq = env.seq(carol);
auto const aliceSeq = env.seq(alice);
auto const batchFee1 = batch::calcBatchFee(env, 2, 2);
auto jt1 = env.jt(
batch::outer(alice, aliceSeq, batchFee1, tfOnlyOne),
batch::Inner(pay(bob, alice, XRP(100)), bobSeq),
batch::Inner(pay(carol, alice, XRP(50)), carolSeq),
batch::Sig(bob, carol));
auto const capturedSigners = jt1.jv[sfBatchSigners.jsonName];
env(jt1, Ter(tesSUCCESS));
env.close();
BEAST_EXPECT(env.seq(bob) == bobSeq + 1);
BEAST_EXPECT(env.seq(carol) == carolSeq);
auto const batchFee2 = batch::calcBatchFee(env, 2, 2);
auto jt2 = env.jtnofill(
batch::outer(eve, env.seq(eve), batchFee2, tfOnlyOne),
batch::Inner(pay(bob, alice, XRP(100)), bobSeq),
batch::Inner(pay(carol, alice, XRP(50)), carolSeq));
jt2.jv[sfBatchSigners.jsonName] = capturedSigners;
env(jt2.jv, Ter(telENV_RPC_FAILED));
env.close();
BEAST_EXPECT(env.seq(carol) == carolSeq);
BEAST_EXPECT(env.balance(carol) == preCarol);
}
// Signatures are bound to the outer sequence; replaying them
// at a higher sequence must fail.
{
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const carol = Account("carol");
env.fund(XRP(10000), alice, bob, carol);
env.close();
auto const preCarol = env.balance(carol);
auto const bobSeq = env.seq(bob);
auto const carolSeq = env.seq(carol);
auto const aliceSeq1 = env.seq(alice);
auto const batchFee1 = batch::calcBatchFee(env, 2, 2);
auto jt1 = env.jt(
batch::outer(alice, aliceSeq1, batchFee1, tfOnlyOne),
batch::Inner(pay(bob, alice, XRP(500)), bobSeq),
batch::Inner(pay(carol, alice, XRP(500)), carolSeq),
batch::Sig(bob, carol));
auto const capturedSigners = jt1.jv[sfBatchSigners.jsonName];
env(jt1, Ter(tesSUCCESS));
env.close();
BEAST_EXPECT(env.seq(bob) == bobSeq + 1);
BEAST_EXPECT(env.seq(carol) == carolSeq);
auto const batchFee2 = batch::calcBatchFee(env, 2, 2);
auto jt2 = env.jtnofill(
batch::outer(alice, env.seq(alice), batchFee2, tfOnlyOne),
batch::Inner(pay(bob, alice, XRP(500)), bobSeq),
batch::Inner(pay(carol, alice, XRP(500)), carolSeq));
jt2.jv[sfBatchSigners.jsonName] = capturedSigners;
env(jt2.jv, Ter(telENV_RPC_FAILED));
env.close();
BEAST_EXPECT(env.balance(carol) == preCarol);
BEAST_EXPECT(env.seq(carol) == carolSeq);
}
// Multi-signed batch signer entries are bound to their account;
// reusing inner signatures under a different batch signer must fail.
{
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const carol = Account("carol");
auto const dave = Account("dave");
auto const elsa = Account("elsa");
env.fund(XRP(10000), alice, bob, carol, dave, elsa);
env.close();
env(signers(bob, 2, {{dave, 1}, {elsa, 1}}));
env.close();
env(signers(carol, 2, {{dave, 1}, {elsa, 1}}));
env.close();
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 3, 2);
auto jt1 = env.jt(
batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
batch::Msig(bob, {dave, elsa}),
Ter(tesSUCCESS));
auto const bobSignerEntry = jt1.jv[sfBatchSigners.jsonName][0u];
env(jt1, Ter(tesSUCCESS));
env.close();
auto const seq2 = env.seq(alice);
auto const batchFee2 = batch::calcBatchFee(env, 3, 2);
auto jt2 = env.jtnofill(
batch::outer(alice, seq2, batchFee2, tfAllOrNothing),
batch::Inner(pay(alice, carol, XRP(10)), seq2 + 1),
batch::Inner(pay(carol, alice, XRP(5)), env.seq(carol)));
json::Value carolSigner;
carolSigner[sfBatchSigner.jsonName][jss::Account] = carol.human();
carolSigner[sfBatchSigner.jsonName][jss::SigningPubKey] = "";
carolSigner[sfBatchSigner.jsonName][sfSigners.jsonName] =
bobSignerEntry[sfBatchSigner.jsonName][sfSigners.jsonName];
jt2.jv[sfBatchSigners.jsonName][0u] = carolSigner;
env(jt2.jv, Ter(telENV_RPC_FAILED));
env.close();
}
}
void
testUnsortedBatchSigners(FeatureBitset features)
{
testcase("unsorted batch signers");
using namespace test::jtx;
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const carol = Account("carol");
env.fund(XRP(10000), alice, bob, carol);
env.close();
auto const seq = env.seq(alice);
auto const bobSeq = env.seq(bob);
auto const carolSeq = env.seq(carol);
auto const batchFee = batch::calcBatchFee(env, 2, 2);
auto jt = env.jt(
batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::Inner(pay(bob, alice, XRP(10)), bobSeq),
batch::Inner(pay(carol, alice, XRP(5)), carolSeq),
batch::Sig(bob, carol));
auto const s0 = jt.jv[sfBatchSigners.jsonName][0u];
auto const s1 = jt.jv[sfBatchSigners.jsonName][1u];
jt.jv[sfBatchSigners.jsonName][0u] = s1;
jt.jv[sfBatchSigners.jsonName][1u] = s0;
env(jt.jv, Ter(temBAD_SIGNER));
env.close();
}
void
testBatchSigCache(FeatureBitset features)
{
testcase("batch signature caching");
using namespace test::jtx;
// Mirrors apply.cpp's file-local kSfSiggood (the standard signature-good
// cache); batch signer sigs are now verified and cached alongside the
// outer signature in checkSign/checkValidity.
constexpr HashRouterFlags kSfSiggood = HashRouterFlags::PRIVATE2;
// Valid batch: alice (outer) + an inner from bob, who co-signs.
auto buildValidBatch = [](Env& env) {
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 1, 2);
return env.jt(
batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
batch::Inner(pay(bob, alice, XRP(2)), env.seq(bob)),
batch::Sig(bob));
};
// WRITE: a valid batch records "good" on its tx id.
{
Env env{*this, features};
env.fund(XRP(10000), Account("alice"), Account("bob"));
env.close();
auto jt = buildValidBatch(env);
auto const txid = jt.stx->getTransactionID();
BEAST_EXPECT(!any(env.app().getHashRouter().getFlags(txid) & kSfSiggood));
env(jt, Ter(tesSUCCESS));
BEAST_EXPECT(any(env.app().getHashRouter().getFlags(txid) & kSfSiggood));
env.close();
}
// READ: corrupt only a signer's signature (outer sig + signer key
// untouched), so just the crypto would fail. Caught when uncached...
{
Env env{*this, features};
env.fund(XRP(10000), Account("alice"), Account("bob"));
env.close();
auto jt = buildValidBatch(env);
jt.jv[sfBatchSigners.jsonName][0u][sfBatchSigner.jsonName][sfTxnSignature.jsonName] =
"00";
env(jt.jv, Ter(telENV_RPC_FAILED));
env.close();
}
{
// ...but a planted "good" skips the crypto, so it applies.
Env env{*this, features};
env.fund(XRP(10000), Account("alice"), Account("bob"));
env.close();
auto jt = buildValidBatch(env);
jt.jv[sfBatchSigners.jsonName][0u][sfBatchSigner.jsonName][sfTxnSignature.jsonName] =
"00";
auto const txid = STTx{parse(jt.jv)}.getTransactionID();
env.app().getHashRouter().setFlags(txid, kSfSiggood);
env(jt.jv, Ter(tesSUCCESS));
env.close();
}
}
void
testWithFeats(FeatureBitset features)
{
testEnable(features);
testPreflight(features);
testPreclaim(features);
testBadRawTxn(features);
testBadSequence(features);
testBadOuterFee(features);
testCalculateBaseFee(features);
testAllOrNothing(features);
testOnlyOne(features);
testUntilFailure(features);
testIndependent(features);
testInnerSubmitRPC(features);
testAccountActivation(features);
testCheckAllSignatures(features);
testAccountSet(features);
testAccountDelete(features);
testLoan(features);
testObjectCreateSequence(features);
testObjectCreateTicket(features);
testObjectCreate3rdParty(features);
testTickets(features);
testSequenceOpenLedger(features);
testTicketsOpenLedger(features);
testObjectsOpenLedger(features);
testPseudoTxn(features);
testOpenLedger(features);
testBatchTxQueue(features);
testBatchNetworkOps(features);
testBatchDelegate(features);
testBatchDelegateConsent(features);
testValidateRPCResponse(features);
testBatchCalculateBaseFee(features);
testStandaloneInnerBatchFlag(features);
testOuterBinding(features);
testUnsortedBatchSigners(features);
testBatchSigCache(features);
}
public:
void
run() override
{
using namespace test::jtx;
auto const sa = testableAmendments();
testWithFeats(sa);
}
};
BEAST_DEFINE_TESTSUITE(Batch, app, xrpl);
} // namespace xrpl::test