xrpld
Loading...
Searching...
No Matches
Batch.cpp
1#include <xrpl/tx/transactors/system/Batch.h>
2
3#include <xrpl/basics/Log.h>
4#include <xrpl/basics/base_uint.h>
5#include <xrpl/beast/utility/Zero.h>
6#include <xrpl/beast/utility/instrumentation.h>
7#include <xrpl/core/ServiceRegistry.h>
8#include <xrpl/ledger/ApplyView.h>
9#include <xrpl/ledger/ReadView.h>
10#include <xrpl/ledger/helpers/SponsorHelpers.h>
11#include <xrpl/protocol/AccountID.h>
12#include <xrpl/protocol/Protocol.h>
13#include <xrpl/protocol/SField.h>
14#include <xrpl/protocol/STAccount.h> // IWYU pragma: keep
15#include <xrpl/protocol/STLedgerEntry.h>
16#include <xrpl/protocol/STObject.h>
17#include <xrpl/protocol/STTx.h>
18#include <xrpl/protocol/TER.h>
19#include <xrpl/protocol/TxFlags.h>
20#include <xrpl/protocol/TxFormats.h>
21#include <xrpl/protocol/XRPAmount.h>
22#include <xrpl/tx/Transactor.h>
23#include <xrpl/tx/applySteps.h>
24
25#include <algorithm>
26#include <bit>
27#include <cstddef>
28#include <cstdint>
29#include <limits>
30#include <optional>
31#include <unordered_map>
32#include <unordered_set>
33#include <utility>
34#include <vector>
35
36namespace xrpl {
37
52std::optional<XRPAmount>
54{
56
57 // batchBase: view.fees().base for batch processing + default base fee
58 XRPAmount const baseFee = Transactor::calculateBaseFee(view, tx);
59
60 // LCOV_EXCL_START
61 if (baseFee > maxAmount - view.fees().base)
62 {
63 JLOG(debugLog().error()) << "BatchTrace: Base fee overflow detected.";
64 return std::nullopt;
65 }
66 // LCOV_EXCL_STOP
67
68 XRPAmount const batchBase = view.fees().base + baseFee;
69
70 // Calculate the Inner Txn Fees. Inners are built and validated (count,
71 // no nesting) at construction, so they are reused here directly.
72 XRPAmount txnFees{0};
73 for (auto const& stx : tx.getBatchTransactions())
74 {
75 auto const fee = xrpl::calculateBaseFee(view, *stx);
76 // LCOV_EXCL_START
77 if (txnFees > maxAmount - fee)
78 {
79 JLOG(debugLog().error()) << "BatchTrace: XRPAmount overflow in txnFees calculation.";
80 return std::nullopt;
81 }
82 // LCOV_EXCL_STOP
83 txnFees += fee;
84 }
85
86 // Calculate the Signers/BatchSigners Fees
87 std::uint32_t signerCount = 0;
88 if (tx.isFieldPresent(sfBatchSigners))
89 {
90 auto const& signers = tx.getFieldArray(sfBatchSigners);
91
92 // LCOV_EXCL_START
93 if (signers.size() > kMaxBatchSigners)
94 {
95 JLOG(debugLog().error()) << "BatchTrace: Batch Signers array exceeds max entries.";
96 return std::nullopt;
97 }
98 // LCOV_EXCL_STOP
99
100 for (STObject const& signer : signers)
101 {
102 if (signer.isFieldPresent(sfTxnSignature))
103 {
104 signerCount += 1;
105 }
106 else if (signer.isFieldPresent(sfSigners))
107 {
108 auto const& nestedSigners = signer.getFieldArray(sfSigners);
109 // LCOV_EXCL_START
110 if (nestedSigners.size() > STTx::kMaxMultiSigners)
111 {
112 JLOG(debugLog().error())
113 << "BatchTrace: Nested Signers array exceeds max entries.";
114 return std::nullopt;
115 }
116 // LCOV_EXCL_STOP
117 signerCount += nestedSigners.size();
118 }
119 }
120 }
121
122 // LCOV_EXCL_START
123 if (signerCount > 0 && view.fees().base > maxAmount / signerCount)
124 {
125 JLOG(debugLog().error()) << "BatchTrace: XRPAmount overflow in signerCount calculation.";
126 return std::nullopt;
127 }
128 // LCOV_EXCL_STOP
129
130 XRPAmount const signerFees = signerCount * view.fees().base;
131
132 // LCOV_EXCL_START
133 if (signerFees > maxAmount - txnFees)
134 {
135 JLOG(debugLog().error()) << "BatchTrace: XRPAmount overflow in signerFees calculation.";
136 return std::nullopt;
137 }
138 XRPAmount const innerFees = txnFees + signerFees;
139 if (innerFees > maxAmount - batchBase)
140 {
141 JLOG(debugLog().error()) << "BatchTrace: XRPAmount overflow in total fee calculation.";
142 return std::nullopt;
143 }
144 // LCOV_EXCL_STOP
145
146 // 10 drops per batch signature + sum of inner tx fees + batchBase
147 return innerFees + batchBase;
148}
149
152{
153 if (auto const fee = calculateBaseFeeImpl(view, tx))
154 return *fee;
155 // The fee could not be computed, so return a placeholder the account can
156 // pay; preclaim rejects the transaction with tecINSUFF_FEE.
157 return view.fees().base; // LCOV_EXCL_LINE
158}
159
160TER
162{
163 if (!calculateBaseFeeImpl(ctx.view, ctx.tx))
164 return tecINSUFF_FEE; // LCOV_EXCL_LINE
165 return tesSUCCESS;
166}
167
170{
171 return tfBatchMask;
172}
173
207NotTEC
209{
210 auto const parentBatchId = ctx.tx.getTransactionID();
211 auto const flags = ctx.tx.getFlags();
212
213 if (std::popcount(flags & (tfAllOrNothing | tfOnlyOne | tfUntilFailure | tfIndependent)) != 1)
214 {
215 JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]:"
216 << "too many flags.";
217 return temINVALID_FLAG;
218 }
219
220 if (ctx.tx.isFieldPresent(sfSponsorFlags))
221 {
222 if (isReserveSponsored(ctx.tx))
223 {
224 JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]:"
225 << "spfSponsorReserve is not allowed on outer Batch.";
226 return temINVALID_FLAG;
227 }
228 }
229
230 auto const& rawTxns = ctx.tx.getFieldArray(sfRawTransactions);
231 if (rawTxns.size() <= 1)
232 {
233 JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]:"
234 << "txns array must have at least 2 entries.";
235 return temARRAY_EMPTY;
236 }
237
238 if (ctx.tx.isFieldPresent(sfBatchSigners) &&
239 ctx.tx.getFieldArray(sfBatchSigners).size() > kMaxBatchSigners)
240 {
241 JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]:"
242 << "signers array exceeds " << kMaxBatchSigners << " entries.";
243 return temARRAY_TOO_LARGE;
244 }
245
246 // Validation Inner Batch Txns
247 std::unordered_set<uint256> uniqueHashes;
249 auto checkSignatureFields =
250 [&parentBatchId, &j = ctx.j](
251 STObject const& sig, uint256 const& hash, char const* label = "") -> NotTEC {
252 if (sig.isFieldPresent(sfTxnSignature))
253 {
254 JLOG(j.debug()) << "BatchTrace[" << parentBatchId << "]: "
255 << "inner txn " << label << "cannot include TxnSignature. "
256 << "txID: " << hash;
257 return temBAD_SIGNATURE;
258 }
259
260 if (sig.isFieldPresent(sfSigners))
261 {
262 JLOG(j.debug()) << "BatchTrace[" << parentBatchId << "]: "
263 << "inner txn " << label << " cannot include Signers. "
264 << "txID: " << hash;
265 return temBAD_SIGNER;
266 }
267
268 if (!sig.getFieldVL(sfSigningPubKey).empty())
269 {
270 JLOG(j.debug()) << "BatchTrace[" << parentBatchId << "]: "
271 << "inner txn " << label << " SigningPubKey must be empty. "
272 << "txID: " << hash;
273 return temBAD_REGKEY;
274 }
275
276 return tesSUCCESS;
277 };
278 for (auto const& stxPtr : ctx.tx.getBatchTransactions())
279 {
280 STTx const& stx = *stxPtr;
281 auto const hash = stx.getTransactionID();
282 if (!uniqueHashes.emplace(hash).second)
283 {
284 JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: "
285 << "duplicate Txn found. "
286 << "txID: " << hash;
287 return temREDUNDANT;
288 }
289
290 auto const txType = stx.getFieldU16(sfTransactionType);
292 kDisabledTxTypes, [txType](auto const& disabled) { return txType == disabled; }))
293 {
295 }
296
297 if (!stx.isFlag(tfInnerBatchTxn))
298 {
299 JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: "
300 << "inner txn must have the tfInnerBatchTxn flag. "
301 << "txID: " << hash;
302 return temINVALID_FLAG;
303 }
304
305 if (auto const ret = checkSignatureFields(stx, hash))
306 return ret;
307
308 // Note that the CounterpartySignature is optional, and should not be
309 // included, but if it is, ensure it doesn't contain a signature.
310 if (stx.isFieldPresent(sfCounterpartySignature))
311 {
312 auto const counterpartySignature = stx.getFieldObject(sfCounterpartySignature);
313 if (auto const ret =
314 checkSignatureFields(counterpartySignature, hash, "counterparty signature "))
315 {
316 return ret;
317 }
318 }
319 if (stx.isFieldPresent(sfSponsorSignature))
320 {
321 auto const sponsorSignature = stx.getFieldObject(sfSponsorSignature);
322 if (auto const ret = checkSignatureFields(sponsorSignature, hash, "sponsor signature "))
323 {
324 return ret;
325 }
326 }
327
328 // Check that the Fee is native asset (XRP) and zero
329 if (auto const fee = stx.getFieldAmount(sfFee); !fee.native() || fee.xrp() != beast::kZero)
330 {
331 JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: "
332 << "inner txn must have a fee of 0. "
333 << "txID: " << hash;
334 return temBAD_FEE;
335 }
336
337 // Disallow fee sponsorship on Batch inner txs
338 if (stx.isFieldPresent(sfSponsor) && isFeeSponsored(stx))
339 return temINVALID_FLAG;
340
341 auto const innerAccount = stx.getAccountID(sfAccount);
342 if (auto const preflightResult =
343 xrpl::preflight(ctx.registry, ctx.rules, parentBatchId, stx, TapBatch, ctx.j);
344 !isTesSuccess(preflightResult.ter))
345 {
346 JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: "
347 << "inner txn preflight failed: " << transHuman(preflightResult.ter)
348 << " "
349 << "txID: " << hash;
351 }
352
353 // Check that Sequence and TicketSequence are not both present
354 if (stx.isFieldPresent(sfTicketSequence) && stx.getFieldU32(sfSequence) != 0)
355 {
356 JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: "
357 << "inner txn must have exactly one of Sequence and "
358 "TicketSequence. "
359 << "txID: " << hash;
360 return temSEQ_AND_TICKET;
361 }
362
363 // Verify that either Sequence or TicketSequence is present
364 if (!stx.isFieldPresent(sfTicketSequence) && stx.getFieldU32(sfSequence) == 0)
365 {
366 JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: "
367 << "inner txn must have either Sequence or "
368 "TicketSequence. "
369 << "txID: " << hash;
370 return temSEQ_AND_TICKET;
371 }
372
373 // Duplicate sequence and ticket checks
374 if ((flags & (tfAllOrNothing | tfUntilFailure)) != 0u)
375 {
376 if (auto const seq = stx.getFieldU32(sfSequence); seq != 0)
377 {
378 if (!accountSeqTicket[innerAccount].insert(seq).second)
379 {
380 JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: "
381 << "duplicate sequence found: "
382 << "txID: " << hash;
383 return temREDUNDANT;
384 }
385 }
386
387 if (stx.isFieldPresent(sfTicketSequence))
388 {
389 if (auto const ticket = stx.getFieldU32(sfTicketSequence);
390 !accountSeqTicket[innerAccount].insert(ticket).second)
391 {
392 JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: "
393 << "duplicate ticket found: "
394 << "txID: " << hash;
395 return temREDUNDANT;
396 }
397 }
398 }
399 }
400
401 return tesSUCCESS;
402}
403
404NotTEC
406{
407 XRPL_ASSERT(
408 ctx.tx.getTxnType() == ttBATCH, "xrpl::Batch::preflightSigValidated : batch transaction");
409 auto const parentBatchId = ctx.tx.getTransactionID();
410 auto const outerAccount = ctx.tx.getAccountID(sfAccount);
411 // Accounts that must sign the batch: each inner authorizer and counterparty
412 // (excluding the outer account), sorted and de-duplicated to match against
413 // the ascending, unique batch signers.
414 std::vector<AccountID> requiredSigners;
415 requiredSigners.reserve(kMaxBatchSigners);
416 for (auto const& stxPtr : ctx.tx.getBatchTransactions())
417 {
418 STTx const& rb = *stxPtr;
419 // A delegated inner is signed by the delegate, not the account holder,
420 // so the delegate is the required signer when present.
421 AccountID const authorizer = rb.getInitiator();
422
423 // The outer account signs the batch itself, so it is never added to the
424 // required signers.
425 if (authorizer != outerAccount)
426 requiredSigners.push_back(authorizer);
427 // Some transactions have a Counterparty, who must also sign the
428 // transaction if they are not the outer account
429 if (auto const counterparty = rb[~sfCounterparty];
430 counterparty && counterparty != outerAccount)
431 requiredSigners.push_back(*counterparty);
432
433 if (auto const sponsor = rb.at(~sfSponsor);
434 sponsor && rb.isFieldPresent(sfSponsorSignature) && sponsor != outerAccount)
435 requiredSigners.push_back(*sponsor);
436 }
437 std::ranges::sort(requiredSigners);
438 auto const dupes = std::ranges::unique(requiredSigners);
439 requiredSigners.erase(dupes.begin(), dupes.end());
440
441 std::size_t numReqSignersMatched = 0;
442
443 // Validation Batch Signers
444 if (ctx.tx.isFieldPresent(sfBatchSigners))
445 {
446 STArray const& signers = ctx.tx.getFieldArray(sfBatchSigners);
447
448 // Batch signers must be strictly ascending and match the required
449 // signers exactly; since both are sorted, each must be the next
450 // required signer.
451 AccountID lastBatchSigner{beast::kZero};
452 for (auto const& signer : signers)
453 {
454 AccountID const signerAccount = signer.getAccountID(sfAccount);
455 if (signerAccount == outerAccount)
456 {
457 JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: "
458 << "signer cannot be the outer account: " << signerAccount;
459 return temBAD_SIGNER;
460 }
461
462 if (lastBatchSigner == signerAccount)
463 {
464 JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: "
465 << "duplicate signer found: " << signerAccount;
466 return temBAD_SIGNER;
467 }
468
469 if (lastBatchSigner > signerAccount)
470 {
471 JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: "
472 << "unsorted signers array: " << signerAccount;
473 return temBAD_SIGNER;
474 }
475 lastBatchSigner = signerAccount;
476
477 if (numReqSignersMatched >= requiredSigners.size() ||
478 requiredSigners[numReqSignersMatched] != signerAccount)
479 {
480 JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: "
481 << "missing signer or extra signer provided: " << signerAccount;
482 return temBAD_SIGNER;
483 }
484 ++numReqSignersMatched;
485 }
486 }
487
488 // Every required signer must be matched. Also covers sfBatchSigners being
489 // absent while inner txns require signers (numReqSignersMatched stays 0).
490 if (numReqSignersMatched != requiredSigners.size())
491 {
492 JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: "
493 << "invalid batch signers.";
494 return temBAD_SIGNER;
495 }
496
497 return tesSUCCESS;
498}
499
500NotTEC
502{
503 STArray const& signers{ctx.tx.getFieldArray(sfBatchSigners)};
504 for (auto const& signer : signers)
505 {
506 // Reuse the standard signer-authorization rules so the batch and
507 // non-batch paths cannot drift. permitUncreatedAccount allows an inner
508 // from an account that an earlier inner creates, which must be
509 // authorized by its own master key.
510 auto const idAccount = signer.getAccountID(sfAccount);
511 if (auto const ret = Transactor::checkSign(
512 ctx.view,
513 ctx.flags,
514 ctx.parentBatchId,
515 idAccount,
516 signer,
517 ctx.j,
518 /*permitUncreatedAccount=*/true);
519 !isTesSuccess(ret))
520 return ret;
521 }
522 return tesSUCCESS;
523}
524
542NotTEC
544{
545 if (auto ret = Transactor::checkSign(ctx); !isTesSuccess(ret))
546 return ret;
547
548 if (ctx.tx.isFieldPresent(sfBatchSigners))
549 {
550 if (auto ret = checkBatchSign(ctx); !isTesSuccess(ret))
551 return ret;
552 }
553
554 return tesSUCCESS;
555}
556
567TER
569{
570 return tesSUCCESS;
571}
572
573void
575{
576 // No transaction-specific invariants yet (future work).
577}
578
579bool
581{
582 // No transaction-specific invariants yet (future work).
583 return true;
584}
585
586} // namespace xrpl
T any_of(T... args)
A generic endpoint for log messages.
Definition Journal.h:44
Stream debug() const
Definition Journal.h:344
UInt size() const
Number of values in array or object.
static TER preclaim(PreclaimContext const &ctx)
Definition Batch.cpp:161
void visitInvariantEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after) override
Inspect a single ledger entry modified by this transaction.
Definition Batch.cpp:574
static std::optional< XRPAmount > calculateBaseFeeImpl(ReadView const &view, STTx const &tx)
Calculates the total base fee for a batch transaction.
Definition Batch.cpp:53
static XRPAmount calculateBaseFee(ReadView const &view, STTx const &tx)
Definition Batch.cpp:151
static NotTEC preflight(PreflightContext const &ctx)
Performs preflight validation checks for a Batch transaction.
Definition Batch.cpp:208
static NotTEC checkBatchSign(PreclaimContext const &ctx)
Definition Batch.cpp:501
static NotTEC checkSign(PreclaimContext const &ctx)
Checks the validity of signatures for a batch transaction.
Definition Batch.cpp:543
TER doApply() override
Applies the outer batch transaction.
Definition Batch.cpp:568
bool finalizeInvariants(STTx const &tx, TER result, XRPAmount fee, ReadView const &view, beast::Journal const &j) override
Check transaction-specific post-conditions after all entries have been visited.
Definition Batch.cpp:580
static std::uint32_t getFlagsMask(PreflightContext const &ctx)
Definition Batch.cpp:169
static constexpr auto kDisabledTxTypes
static NotTEC preflightSigValidated(PreflightContext const &ctx)
Definition Batch.cpp:405
A view into a ledger.
Definition ReadView.h:41
bool native() const noexcept
Definition STAmount.h:471
size_type size() const
Definition STArray.h:248
std::shared_ptr< STLedgerEntry const > const & const_ref
T::value_type at(TypedField< T > const &f) const
Get the value of a field.
Definition STObject.h:1078
std::uint32_t getFieldU32(SField const &field) const
Definition STObject.cpp:601
STArray const & getFieldArray(SField const &field) const
Definition STObject.cpp:688
bool isFlag(std::uint32_t) const
Definition STObject.cpp:511
bool isFieldPresent(SField const &field) const
Definition STObject.cpp:464
STObject getFieldObject(SField const &field) const
Definition STObject.cpp:678
AccountID getAccountID(SField const &field) const
Definition STObject.cpp:643
std::uint16_t getFieldU16(SField const &field) const
Definition STObject.cpp:595
STAmount const & getFieldAmount(SField const &field) const
Definition STObject.cpp:657
std::uint32_t getFlags() const
Definition STObject.cpp:517
static constexpr std::size_t kMaxMultiSigners
Definition STTx.h:47
TxType getTxnType() const
Definition STTx.h:226
AccountID getInitiator() const
The account responsible for the authorization: the delegate when sfDelegate is present,...
Definition STTx.cpp:656
uint256 getTransactionID() const
Definition STTx.h:238
std::vector< std::shared_ptr< STTx const > > const & getBatchTransactions() const
The inner transactions of a Batch, built and validated at construction.
Definition STTx.cpp:645
static NotTEC checkSign(PreclaimContext const &ctx)
ApplyView & view()
Definition Transactor.h:175
static XRPAmount calculateBaseFee(ReadView const &view, STTx const &tx)
T emplace(T... args)
T erase(T... args)
T insert(T... args)
T max(T... args)
constexpr Zero kZero
Definition Zero.h:30
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
constexpr FlagValue tfInnerBatchTxn
Definition TxFlags.h:44
constexpr std::size_t kMaxBatchSigners
The maximum number of batch signers.
Definition Protocol.h:448
PreflightResult preflight(ServiceRegistry &registry, Rules const &rules, STTx const &tx, ApplyFlags flags, beast::Journal j)
Gate a transaction based on static information.
beast::Journal debugLog()
Returns a debug journal.
Definition Log.cpp:399
std::string transHuman(TER code)
Definition TER.cpp:260
bool isReserveSponsored(STTx const &tx)
Whether the transaction's reserve is sponsored (sfSponsor present + spfSponsorReserve set).
TERSubset< CanCvtToNotTEC > NotTEC
Definition TER.h:607
bool isFeeSponsored(STTx const &tx)
Whether the transaction's fee is sponsored (sfSponsor present + spfSponsorFee set).
@ TapBatch
Definition ApplyView.h:42
BaseUInt< 160, detail::AccountIDTag > AccountID
A 160-bit unsigned that uniquely identifies an account.
Definition AccountID.h:34
@ temBAD_REGKEY
Definition TER.h:86
@ temARRAY_TOO_LARGE
Definition TER.h:129
@ temBAD_FEE
Definition TER.h:80
@ temINVALID_FLAG
Definition TER.h:99
@ temARRAY_EMPTY
Definition TER.h:128
@ temSEQ_AND_TICKET
Definition TER.h:114
@ temBAD_SIGNATURE
Definition TER.h:93
@ temINVALID_INNER_BATCH
Definition TER.h:131
@ temREDUNDANT
Definition TER.h:100
@ temBAD_SIGNER
Definition TER.h:103
bool isTesSuccess(TER x) noexcept
Definition TER.h:676
TERSubset< CanCvtToTER > TER
Definition TER.h:647
@ tecINSUFF_FEE
Definition TER.h:305
XRPAmount calculateBaseFee(ReadView const &view, STTx const &tx)
Compute only the expected base fee for a transaction.
BaseUInt< 256 > uint256
Definition base_uint.h:580
@ tesSUCCESS
Definition TER.h:245
T popcount(T... args)
T push_back(T... args)
T reserve(T... args)
T size(T... args)
T sort(T... args)
State information when determining if a tx is likely to claim a fee.
Definition Transactor.h:83
ReadView const & view
Definition Transactor.h:86
beast::Journal const j
Definition Transactor.h:91
std::optional< uint256 const > const parentBatchId
Definition Transactor.h:90
State information when preflighting a tx.
Definition Transactor.h:38
beast::Journal const j
Definition Transactor.h:45
std::reference_wrapper< ServiceRegistry > registry
Definition Transactor.h:40
T unique(T... args)