rippled
Loading...
Searching...
No Matches
Batch.cpp
1//------------------------------------------------------------------------------
2/*
3 This file is part of rippled: https://github.com/ripple/rippled
4 Copyright (c) 2024 Ripple Labs Inc.
5
6 Permission to use, copy, modify, and/or distribute this software for any
7 purpose with or without fee is hereby granted, provided that the above
8 copyright notice and this permission notice appear in all copies.
9
10 THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11 WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12 MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13 ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14 WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15 ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16 OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17*/
18//==============================================================================
19
20#include <xrpld/app/tx/apply.h>
21#include <xrpld/app/tx/detail/Batch.h>
22
23#include <xrpl/basics/Log.h>
24#include <xrpl/ledger/Sandbox.h>
25#include <xrpl/ledger/View.h>
26#include <xrpl/protocol/Feature.h>
27#include <xrpl/protocol/Indexes.h>
28#include <xrpl/protocol/TER.h>
29#include <xrpl/protocol/TxFlags.h>
30
31namespace ripple {
32
53XRPAmount
54Batch::calculateBaseFee(ReadView const& view, STTx const& tx)
55{
56 XRPAmount const maxAmount{
58
59 // batchBase: view.fees().base for batch processing + default base fee
60 XRPAmount const baseFee = Transactor::calculateBaseFee(view, tx);
61
62 // LCOV_EXCL_START
63 if (baseFee > maxAmount - view.fees().base)
64 {
65 JLOG(debugLog().error()) << "BatchTrace: Base fee overflow detected.";
66 return XRPAmount{INITIAL_XRP};
67 }
68 // LCOV_EXCL_STOP
69
70 XRPAmount const batchBase = view.fees().base + baseFee;
71
72 // Calculate the Inner Txn Fees
73 XRPAmount txnFees{0};
74 if (tx.isFieldPresent(sfRawTransactions))
75 {
76 auto const& txns = tx.getFieldArray(sfRawTransactions);
77
78 // LCOV_EXCL_START
79 if (txns.size() > maxBatchTxCount)
80 {
81 JLOG(debugLog().error())
82 << "BatchTrace: Raw Transactions array exceeds max entries.";
83 return XRPAmount{INITIAL_XRP};
84 }
85 // LCOV_EXCL_STOP
86
87 for (STObject txn : txns)
88 {
89 STTx const stx = STTx{std::move(txn)};
90
91 // LCOV_EXCL_START
92 if (stx.getTxnType() == ttBATCH)
93 {
94 JLOG(debugLog().error())
95 << "BatchTrace: Inner Batch transaction found.";
96 return XRPAmount{INITIAL_XRP};
97 }
98 // LCOV_EXCL_STOP
99
100 auto const fee = ripple::calculateBaseFee(view, stx);
101 // LCOV_EXCL_START
102 if (txnFees > maxAmount - fee)
103 {
104 JLOG(debugLog().error())
105 << "BatchTrace: XRPAmount overflow in txnFees calculation.";
106 return XRPAmount{INITIAL_XRP};
107 }
108 // LCOV_EXCL_STOP
109 txnFees += fee;
110 }
111 }
112
113 // Calculate the Signers/BatchSigners Fees
114 std::int32_t signerCount = 0;
115 if (tx.isFieldPresent(sfBatchSigners))
116 {
117 auto const& signers = tx.getFieldArray(sfBatchSigners);
118
119 // LCOV_EXCL_START
120 if (signers.size() > maxBatchTxCount)
121 {
122 JLOG(debugLog().error())
123 << "BatchTrace: Batch Signers array exceeds max entries.";
124 return XRPAmount{INITIAL_XRP};
125 }
126 // LCOV_EXCL_STOP
127
128 for (STObject const& signer : signers)
129 {
130 if (signer.isFieldPresent(sfTxnSignature))
131 signerCount += 1;
132 else if (signer.isFieldPresent(sfSigners))
133 signerCount += signer.getFieldArray(sfSigners).size();
134 }
135 }
136
137 // LCOV_EXCL_START
138 if (signerCount > 0 && view.fees().base > maxAmount / signerCount)
139 {
140 JLOG(debugLog().error())
141 << "BatchTrace: XRPAmount overflow in signerCount calculation.";
142 return XRPAmount{INITIAL_XRP};
143 }
144 // LCOV_EXCL_STOP
145
146 XRPAmount signerFees = signerCount * view.fees().base;
147
148 // LCOV_EXCL_START
149 if (signerFees > maxAmount - txnFees)
150 {
151 JLOG(debugLog().error())
152 << "BatchTrace: XRPAmount overflow in signerFees calculation.";
153 return XRPAmount{INITIAL_XRP};
154 }
155 if (txnFees + signerFees > maxAmount - batchBase)
156 {
157 JLOG(debugLog().error())
158 << "BatchTrace: XRPAmount overflow in total fee calculation.";
159 return XRPAmount{INITIAL_XRP};
160 }
161 // LCOV_EXCL_STOP
162
163 // 10 drops per batch signature + sum of inner tx fees + batchBase
164 return signerFees + txnFees + batchBase;
165}
166
169{
170 return tfBatchMask;
171}
172
206NotTEC
208{
209 auto const parentBatchId = ctx.tx.getTransactionID();
210 auto const flags = ctx.tx.getFlags();
211
212 if (std::popcount(
213 flags &
215 {
216 JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]:"
217 << "too many flags.";
218 return temINVALID_FLAG;
219 }
220
221 auto const& rawTxns = ctx.tx.getFieldArray(sfRawTransactions);
222 if (rawTxns.size() <= 1)
223 {
224 JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]:"
225 << "txns array must have at least 2 entries.";
226 return temARRAY_EMPTY;
227 }
228
229 if (rawTxns.size() > maxBatchTxCount)
230 {
231 JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]:"
232 << "txns array exceeds 8 entries.";
233 return temARRAY_TOO_LARGE;
234 }
235
236 // Validation Inner Batch Txns
237 std::unordered_set<uint256> uniqueHashes;
239 accountSeqTicket;
240 for (STObject rb : rawTxns)
241 {
242 STTx const stx = STTx{std::move(rb)};
243 auto const hash = stx.getTransactionID();
244 if (!uniqueHashes.emplace(hash).second)
245 {
246 JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: "
247 << "duplicate Txn found. "
248 << "txID: " << hash;
249 return temREDUNDANT;
250 }
251
252 if (stx.getFieldU16(sfTransactionType) == ttBATCH)
253 {
254 JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: "
255 << "batch cannot have an inner batch txn. "
256 << "txID: " << hash;
257 return temINVALID;
258 }
259
260 if (!(stx.getFlags() & tfInnerBatchTxn))
261 {
262 JLOG(ctx.j.debug())
263 << "BatchTrace[" << parentBatchId << "]: "
264 << "inner txn must have the tfInnerBatchTxn flag. "
265 << "txID: " << hash;
266 return temINVALID_FLAG;
267 }
268
269 if (stx.isFieldPresent(sfTxnSignature))
270 {
271 JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: "
272 << "inner txn cannot include TxnSignature. "
273 << "txID: " << hash;
274 return temBAD_SIGNATURE;
275 }
276
277 if (stx.isFieldPresent(sfSigners))
278 {
279 JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: "
280 << "inner txn cannot include Signers. "
281 << "txID: " << hash;
282 return temBAD_SIGNER;
283 }
284
285 if (!stx.getSigningPubKey().empty())
286 {
287 JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: "
288 << "inner txn SigningPubKey must be empty. "
289 << "txID: " << hash;
290 return temBAD_REGKEY;
291 }
292
293 auto const innerAccount = stx.getAccountID(sfAccount);
294 if (auto const preflightResult = ripple::preflight(
295 ctx.app, ctx.rules, parentBatchId, stx, tapBATCH, ctx.j);
296 preflightResult.ter != tesSUCCESS)
297 {
298 JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: "
299 << "inner txn preflight failed: "
300 << transHuman(preflightResult.ter) << " "
301 << "txID: " << hash;
303 }
304
305 // Check that the fee is zero
306 if (auto const fee = stx.getFieldAmount(sfFee);
307 !fee.native() || fee.xrp() != beast::zero)
308 {
309 JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: "
310 << "inner txn must have a fee of 0. "
311 << "txID: " << hash;
312 return temBAD_FEE;
313 }
314
315 // Check that Sequence and TicketSequence are not both present
316 if (stx.isFieldPresent(sfTicketSequence) &&
317 stx.getFieldU32(sfSequence) != 0)
318 {
319 JLOG(ctx.j.debug())
320 << "BatchTrace[" << parentBatchId << "]: "
321 << "inner txn must have exactly one of Sequence and "
322 "TicketSequence. "
323 << "txID: " << hash;
324 return temSEQ_AND_TICKET;
325 }
326
327 // Verify that either Sequence or TicketSequence is present
328 if (!stx.isFieldPresent(sfTicketSequence) &&
329 stx.getFieldU32(sfSequence) == 0)
330 {
331 JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: "
332 << "inner txn must have either Sequence or "
333 "TicketSequence. "
334 << "txID: " << hash;
335 return temSEQ_AND_TICKET;
336 }
337
338 // Duplicate sequence and ticket checks
339 if (flags & (tfAllOrNothing | tfUntilFailure))
340 {
341 if (auto const seq = stx.getFieldU32(sfSequence); seq != 0)
342 {
343 if (!accountSeqTicket[innerAccount].insert(seq).second)
344 {
345 JLOG(ctx.j.debug())
346 << "BatchTrace[" << parentBatchId << "]: "
347 << "duplicate sequence found: "
348 << "txID: " << hash;
349 return temREDUNDANT;
350 }
351 }
352
353 if (stx.isFieldPresent(sfTicketSequence))
354 {
355 if (auto const ticket = stx.getFieldU32(sfTicketSequence);
356 !accountSeqTicket[innerAccount].insert(ticket).second)
357 {
358 JLOG(ctx.j.debug())
359 << "BatchTrace[" << parentBatchId << "]: "
360 << "duplicate ticket found: "
361 << "txID: " << hash;
362 return temREDUNDANT;
363 }
364 }
365 }
366 }
367
368 return tesSUCCESS;
369}
370
371NotTEC
373{
374 auto const parentBatchId = ctx.tx.getTransactionID();
375 auto const outerAccount = ctx.tx.getAccountID(sfAccount);
376 auto const& rawTxns = ctx.tx.getFieldArray(sfRawTransactions);
377
378 // Build the signers list
379 std::unordered_set<AccountID> requiredSigners;
380 for (STObject const& rb : rawTxns)
381 {
382 auto const innerAccount = rb.getAccountID(sfAccount);
383
384 // If the inner account is the same as the outer account, do not add the
385 // inner account to the required signers set.
386 if (innerAccount != outerAccount)
387 requiredSigners.insert(innerAccount);
388 }
389
390 // Validation Batch Signers
392 if (ctx.tx.isFieldPresent(sfBatchSigners))
393 {
394 STArray const& signers = ctx.tx.getFieldArray(sfBatchSigners);
395
396 // Check that the batch signers array is not too large.
397 if (signers.size() > maxBatchTxCount)
398 {
399 JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: "
400 << "signers array exceeds 8 entries.";
401 return temARRAY_TOO_LARGE;
402 }
403
404 // Add batch signers to the set to ensure all signer accounts are
405 // unique. Meanwhile, remove signer accounts from the set of inner
406 // transaction accounts (`requiredSigners`). By the end of the loop,
407 // `requiredSigners` should be empty, indicating that all inner
408 // accounts are matched with signers.
409 for (auto const& signer : signers)
410 {
411 AccountID const signerAccount = signer.getAccountID(sfAccount);
412 if (signerAccount == outerAccount)
413 {
414 JLOG(ctx.j.debug())
415 << "BatchTrace[" << parentBatchId << "]: "
416 << "signer cannot be the outer account: " << signerAccount;
417 return temBAD_SIGNER;
418 }
419
420 if (!batchSigners.insert(signerAccount).second)
421 {
422 JLOG(ctx.j.debug())
423 << "BatchTrace[" << parentBatchId << "]: "
424 << "duplicate signer found: " << signerAccount;
425 return temREDUNDANT;
426 }
427
428 // Check that the batch signer is in the required signers set.
429 // Remove it if it does, as it can be crossed off the list.
430 if (requiredSigners.erase(signerAccount) == 0)
431 {
432 JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: "
433 << "no account signature for inner txn.";
434 return temBAD_SIGNER;
435 }
436 }
437
438 // Check the batch signers signatures.
439 auto const sigResult = ctx.tx.checkBatchSign(
441
442 if (!sigResult)
443 {
444 JLOG(ctx.j.debug())
445 << "BatchTrace[" << parentBatchId << "]: "
446 << "invalid batch txn signature: " << sigResult.error();
447 return temBAD_SIGNATURE;
448 }
449 }
450
451 if (!requiredSigners.empty())
452 {
453 JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: "
454 << "invalid batch signers.";
455 return temBAD_SIGNER;
456 }
457 return tesSUCCESS;
458}
459
477NotTEC
479{
480 if (auto ret = Transactor::checkSign(ctx); !isTesSuccess(ret))
481 return ret;
482
483 if (auto ret = Transactor::checkBatchSign(ctx); !isTesSuccess(ret))
484 return ret;
485
486 return tesSUCCESS;
487}
488
499TER
501{
502 return tesSUCCESS;
503}
504
505} // namespace ripple
Stream debug() const
Definition Journal.h:328
static std::uint32_t getFlagsMask(PreflightContext const &ctx)
Definition Batch.cpp:168
TER doApply() override
Applies the outer batch transaction.
Definition Batch.cpp:500
static NotTEC preflight(PreflightContext const &ctx)
Performs preflight validation checks for a Batch transaction.
Definition Batch.cpp:207
static NotTEC preflightSigValidated(PreflightContext const &ctx)
Definition Batch.cpp:372
static NotTEC checkSign(PreclaimContext const &ctx)
Checks the validity of signatures for a batch transaction.
Definition Batch.cpp:478
static XRPAmount calculateBaseFee(ReadView const &view, STTx const &tx)
Calculates the total base fee for a batch transaction.
Definition Batch.cpp:54
A view into a ledger.
Definition ReadView.h:51
virtual Fees const & fees() const =0
Returns the fees for the base ledger.
AccountID getAccountID(SField const &field) const
Definition STObject.cpp:657
STArray const & getFieldArray(SField const &field) const
Definition STObject.cpp:692
std::uint16_t getFieldU16(SField const &field) const
Definition STObject.cpp:609
std::uint32_t getFieldU32(SField const &field) const
Definition STObject.cpp:615
STAmount const & getFieldAmount(SField const &field) const
Definition STObject.cpp:671
bool isFieldPresent(SField const &field) const
Definition STObject.cpp:484
std::uint32_t getFlags() const
Definition STObject.cpp:537
Blob getSigningPubKey() const
Definition STTx.h:213
TxType getTxnType() const
Definition STTx.h:207
uint256 getTransactionID() const
Definition STTx.h:219
Expected< void, std::string > checkBatchSign(RequireFullyCanonicalSig requireCanonicalSig, Rules const &rules) const
Definition STTx.cpp:269
static XRPAmount calculateBaseFee(ReadView const &view, STTx const &tx)
static NotTEC checkSign(PreclaimContext const &ctx)
ApplyView & view()
Definition Transactor.h:163
static NotTEC checkBatchSign(PreclaimContext const &ctx)
T emplace(T... args)
T empty(T... args)
T erase(T... args)
T insert(T... args)
T max(T... args)
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:25
std::string transHuman(TER code)
Definition TER.cpp:273
constexpr std::uint32_t tfAllOrNothing
Definition TxFlags.h:276
constexpr std::uint32_t const tfBatchMask
Definition TxFlags.h:285
PreflightResult preflight(Application &app, Rules const &rules, STTx const &tx, ApplyFlags flags, beast::Journal j)
Gate a transaction based on static information.
constexpr std::uint32_t tfOnlyOne
Definition TxFlags.h:277
constexpr std::uint32_t tfIndependent
Definition TxFlags.h:279
XRPAmount calculateBaseFee(ReadView const &view, STTx const &tx)
Compute only the expected base fee for a transaction.
constexpr XRPAmount INITIAL_XRP
Configure the native currency.
constexpr std::uint32_t tfUntilFailure
Definition TxFlags.h:278
beast::Journal debugLog()
Returns a debug journal.
Definition Log.cpp:468
@ tesSUCCESS
Definition TER.h:244
bool isTesSuccess(TER x) noexcept
Definition TER.h:674
@ tapBATCH
Definition ApplyView.h:45
std::size_t constexpr maxBatchTxCount
The maximum number of transactions that can be in a batch.
Definition Protocol.h:179
constexpr std::uint32_t tfInnerBatchTxn
Definition TxFlags.h:61
@ temREDUNDANT
Definition TER.h:112
@ temBAD_FEE
Definition TER.h:92
@ temBAD_SIGNER
Definition TER.h:115
@ temSEQ_AND_TICKET
Definition TER.h:126
@ temINVALID_INNER_BATCH
Definition TER.h:143
@ temBAD_REGKEY
Definition TER.h:98
@ temINVALID
Definition TER.h:110
@ temINVALID_FLAG
Definition TER.h:111
@ temARRAY_EMPTY
Definition TER.h:140
@ temARRAY_TOO_LARGE
Definition TER.h:141
@ temBAD_SIGNATURE
Definition TER.h:105
T popcount(T... args)
XRPAmount base
State information when determining if a tx is likely to claim a fee.
Definition Transactor.h:80
State information when preflighting a tx.
Definition Transactor.h:35
beast::Journal const j
Definition Transactor.h:42