Compare commits

...

4 Commits

2 changed files with 193 additions and 1 deletions

View File

@@ -34,6 +34,7 @@
#include <xrpl/protocol/SystemParameters.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/TxMeta.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/server/LoadFeeTrack.h>
@@ -905,7 +906,12 @@ Transactor::apply()
if (!isTesSuccess(result))
return result;
if (sle->isFieldPresent(sfAccountTxnID))
// With fixCleanup3_4_0, a Batch wrapper does not update
// sfAccountTxnID: its ID hashes over sfRawTransactions, so a
// same-account inner could never carry a matching prior-txn ID. The
// inners update the field as they apply.
if (sle->isFieldPresent(sfAccountTxnID) &&
(ctx_.tx.getTxnType() != ttBATCH || !view().rules().enabled(fixCleanup3_4_0)))
sle->setFieldH256(sfAccountTxnID, ctx_.tx.getTransactionID());
view().update(sle);

View File

@@ -2,6 +2,7 @@
#include <test/jtx/Env.h>
#include <test/jtx/SignerUtils.h>
#include <test/jtx/TestHelpers.h>
#include <test/jtx/WSClient.h>
#include <test/jtx/acctdelete.h>
#include <test/jtx/amount.h>
#include <test/jtx/balance.h> // IWYU pragma: keep
@@ -2972,6 +2973,189 @@ class Batch_test : public beast::unit_test::Suite
BEAST_EXPECT(env.balance(bob) == preBob + XRP(1));
}
void
testAccountTxnID(FeatureBitset features)
{
testcase("account txn id");
using namespace test::jtx;
using namespace std::literals;
// With fixCleanup3_4_0 the Batch wrapper does not stamp
// sfAccountTxnID, so an inner can reference the last pre-batch
// transaction. Without the fix the wrapper stamps its own ID, which
// no inner can carry: the outer's ID hashes over sfRawTransactions.
for (bool const withFix : {true, false})
{
auto const amend = withFix ? features : features - fixCleanup3_4_0;
Env env{*this, amend};
auto const alice = Account("alice");
auto const bob = Account("bob");
env.fund(XRP(10000), alice, bob);
env.close();
env(fset(alice, asfAccountTxnID));
env.close();
// Arm the tracking field: fset leaves it zero, and the first
// transaction after it stamps the first usable prior-txn ID.
env(noop(alice));
env.close();
uint256 const priorID = env.tx()->getTransactionID();
auto const preBob = env.balance(bob);
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[sfAccountTxnID.jsonName] = strHex(priorID);
auto const [txIDs, batchID] = submitBatch(
env,
tesSUCCESS,
batch::outer(alice, seq, batchFee, tfAllOrNothing),
tx1,
batch::Inner(pay(alice, bob, XRP(2)), seq + 2));
env.close();
auto const sle = env.le(keylet::account(alice));
BEAST_EXPECT(sle && sle->isFieldPresent(sfAccountTxnID));
if (withFix)
{
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) == seq + 3);
BEAST_EXPECT(env.balance(bob) == preBob + XRP(3));
// The chain ends at the last applied inner, not the wrapper.
BEAST_EXPECT(strHex(sle->getFieldH256(sfAccountTxnID)) == txIDs[1]);
// A post-batch transaction chains off the last inner.
auto jv = pay(alice, bob, XRP(1));
jv[sfAccountTxnID.jsonName] = txIDs[1];
env(jv);
env.close();
}
else
{
// tefWRONG_PRIOR on the inner: the wrapper already stamped
// its own ID, and tfAllOrNothing reverts every inner.
std::vector<TestLedgerData> const testCases = {
{.index = 0,
.txType = "Batch",
.result = "tesSUCCESS",
.txHash = batchID,
.batchID = std::nullopt},
};
validateClosedLedger(env, testCases);
BEAST_EXPECT(env.seq(alice) == seq + 1);
BEAST_EXPECT(env.balance(bob) == preBob);
// The wrapper stamped its own ID.
BEAST_EXPECT(strHex(sle->getFieldH256(sfAccountTxnID)) == batchID);
}
}
}
void
testSubscriptions(FeatureBitset features)
{
testcase("subscriptions");
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 wsc = makeWSClient(env.app().config());
json::Value stream;
stream[jss::streams] = json::ValueType::Array;
stream[jss::streams].append("transactions");
stream[jss::streams].append("transactions_proposed");
BEAST_EXPECT(wsc->invoke("subscribe", stream)[jss::status] == "success");
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
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();
// The hash sits under jss::transaction (API v1), jss::tx_json
// (API v2), or at the top level, depending on message shape.
auto const txHash = [](json::Value const& msg) -> std::string {
if (msg.isMember(jss::hash))
{
return msg[jss::hash].asString();
}
for (auto const& field : {jss::transaction, jss::tx_json})
{
if (msg.isMember(field) && msg[field].isMember(jss::hash))
{
return msg[field][jss::hash].asString();
}
}
return {};
};
auto const isValidated = [](json::Value const& msg) {
return msg.isMember(jss::validated) && msg[jss::validated].asBool();
};
std::vector<json::Value> msgs;
while (auto msg = wsc->getMsg(2s))
{
msgs.push_back(*msg);
}
// Proposed stream: the outer Batch only. pubProposedTransaction
// drops tfInnerBatchTxn, so an inner must never appear unvalidated.
std::size_t proposed = 0;
for (auto const& msg : msgs)
{
if (!isValidated(msg))
{
++proposed;
BEAST_EXPECT(txHash(msg) == batchID);
}
}
BEAST_EXPECT(proposed == 1);
// Validated stream: the outer and both inners publish, each with
// metadata.
for (std::string const& hash : {batchID, txIDs[0], txIDs[1]})
{
BEAST_EXPECT(std::ranges::any_of(msgs, [&](json::Value const& msg) {
return isValidated(msg) && txHash(msg) == hash && msg.isMember(jss::meta);
}));
}
BEAST_EXPECT(wsc->invoke("unsubscribe", stream)[jss::status] == "success");
}
void
testAccountDelete(FeatureBitset features)
{
@@ -5914,6 +6098,8 @@ class Batch_test : public beast::unit_test::Suite
testAccountActivation(features);
testCheckAllSignatures(features);
testAccountSet(features);
testAccountTxnID(features);
testSubscriptions(features);
testAccountDelete(features);
testLoan(features);
testObjectCreateSequence(features);