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
200NotTEC
202{
203 if (!ctx.rules.enabled(featureBatch))
204 return temDISABLED;
205
206 if (auto const ret = preflight1(ctx); !isTesSuccess(ret))
207 return ret;
208
209 auto const parentBatchId = ctx.tx.getTransactionID();
210 auto const outerAccount = ctx.tx.getAccountID(sfAccount);
211 auto const flags = ctx.tx.getFlags();
212
213 if (flags & tfBatchMask)
214 {
215 JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]:"
216 << "invalid flags.";
217 return temINVALID_FLAG;
218 }
219
220 if (std::popcount(
221 flags &
223 {
224 JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]:"
225 << "too many flags.";
226 return temINVALID_FLAG;
227 }
228
229 auto const& rawTxns = ctx.tx.getFieldArray(sfRawTransactions);
230 if (rawTxns.size() <= 1)
231 {
232 JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]:"
233 << "txns array must have at least 2 entries.";
234 return temARRAY_EMPTY;
235 }
236
237 if (rawTxns.size() > maxBatchTxCount)
238 {
239 JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]:"
240 << "txns array exceeds 8 entries.";
241 return temARRAY_TOO_LARGE;
242 }
243
244 // Validation Inner Batch Txns
245 std::unordered_set<AccountID> requiredSigners;
246 std::unordered_set<uint256> uniqueHashes;
248 accountSeqTicket;
249 for (STObject rb : rawTxns)
250 {
251 STTx const stx = STTx{std::move(rb)};
252 auto const hash = stx.getTransactionID();
253 if (!uniqueHashes.emplace(hash).second)
254 {
255 JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: "
256 << "duplicate Txn found. "
257 << "txID: " << hash;
258 return temREDUNDANT;
259 }
260
261 if (stx.getFieldU16(sfTransactionType) == ttBATCH)
262 {
263 JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: "
264 << "batch cannot have an inner batch txn. "
265 << "txID: " << hash;
266 return temINVALID;
267 }
268
269 if (!(stx.getFlags() & tfInnerBatchTxn))
270 {
271 JLOG(ctx.j.debug())
272 << "BatchTrace[" << parentBatchId << "]: "
273 << "inner txn must have the tfInnerBatchTxn flag. "
274 << "txID: " << hash;
275 return temINVALID_FLAG;
276 }
277
278 if (stx.isFieldPresent(sfTxnSignature))
279 {
280 JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: "
281 << "inner txn cannot include TxnSignature. "
282 << "txID: " << hash;
283 return temBAD_SIGNATURE;
284 }
285
286 if (stx.isFieldPresent(sfSigners))
287 {
288 JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: "
289 << "inner txn cannot include Signers. "
290 << "txID: " << hash;
291 return temBAD_SIGNER;
292 }
293
294 if (!stx.getSigningPubKey().empty())
295 {
296 JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: "
297 << "inner txn SigningPubKey must be empty. "
298 << "txID: " << hash;
299 return temBAD_REGKEY;
300 }
301
302 auto const innerAccount = stx.getAccountID(sfAccount);
303 if (auto const preflightResult = ripple::preflight(
304 ctx.app, ctx.rules, parentBatchId, stx, tapBATCH, ctx.j);
305 preflightResult.ter != tesSUCCESS)
306 {
307 JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: "
308 << "inner txn preflight failed: "
309 << transHuman(preflightResult.ter) << " "
310 << "txID: " << hash;
312 }
313
314 // Check that the fee is zero
315 if (auto const fee = stx.getFieldAmount(sfFee);
316 !fee.native() || fee.xrp() != beast::zero)
317 {
318 JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: "
319 << "inner txn must have a fee of 0. "
320 << "txID: " << hash;
321 return temBAD_FEE;
322 }
323
324 // Check that Sequence and TicketSequence are not both present
325 if (stx.isFieldPresent(sfTicketSequence) &&
326 stx.getFieldU32(sfSequence) != 0)
327 {
328 JLOG(ctx.j.debug())
329 << "BatchTrace[" << parentBatchId << "]: "
330 << "inner txn must have exactly one of Sequence and "
331 "TicketSequence. "
332 << "txID: " << hash;
333 return temSEQ_AND_TICKET;
334 }
335
336 // Verify that either Sequence or TicketSequence is present
337 if (!stx.isFieldPresent(sfTicketSequence) &&
338 stx.getFieldU32(sfSequence) == 0)
339 {
340 JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: "
341 << "inner txn must have either Sequence or "
342 "TicketSequence. "
343 << "txID: " << hash;
344 return temSEQ_AND_TICKET;
345 }
346
347 // Duplicate sequence and ticket checks
348 if (flags & (tfAllOrNothing | tfUntilFailure))
349 {
350 if (auto const seq = stx.getFieldU32(sfSequence); seq != 0)
351 {
352 if (!accountSeqTicket[innerAccount].insert(seq).second)
353 {
354 JLOG(ctx.j.debug())
355 << "BatchTrace[" << parentBatchId << "]: "
356 << "duplicate sequence found: "
357 << "txID: " << hash;
358 return temREDUNDANT;
359 }
360 }
361
362 if (stx.isFieldPresent(sfTicketSequence))
363 {
364 if (auto const ticket = stx.getFieldU32(sfTicketSequence);
365 !accountSeqTicket[innerAccount].insert(ticket).second)
366 {
367 JLOG(ctx.j.debug())
368 << "BatchTrace[" << parentBatchId << "]: "
369 << "duplicate ticket found: "
370 << "txID: " << hash;
371 return temREDUNDANT;
372 }
373 }
374 }
375
376 // If the inner account is the same as the outer account, do not add the
377 // inner account to the required signers set.
378 if (innerAccount != outerAccount)
379 requiredSigners.insert(innerAccount);
380 }
381
382 // LCOV_EXCL_START
383 if (auto const ret = preflight2(ctx); !isTesSuccess(ret))
384 return ret;
385 // LCOV_EXCL_STOP
386
387 // Validation Batch Signers
389 if (ctx.tx.isFieldPresent(sfBatchSigners))
390 {
391 STArray const& signers = ctx.tx.getFieldArray(sfBatchSigners);
392
393 // Check that the batch signers array is not too large.
394 if (signers.size() > maxBatchTxCount)
395 {
396 JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: "
397 << "signers array exceeds 8 entries.";
398 return temARRAY_TOO_LARGE;
399 }
400
401 // Add batch signers to the set to ensure all signer accounts are
402 // unique. Meanwhile, remove signer accounts from the set of inner
403 // transaction accounts (`requiredSigners`). By the end of the loop,
404 // `requiredSigners` should be empty, indicating that all inner
405 // accounts are matched with signers.
406 for (auto const& signer : signers)
407 {
408 AccountID const signerAccount = signer.getAccountID(sfAccount);
409 if (signerAccount == outerAccount)
410 {
411 JLOG(ctx.j.debug())
412 << "BatchTrace[" << parentBatchId << "]: "
413 << "signer cannot be the outer account: " << signerAccount;
414 return temBAD_SIGNER;
415 }
416
417 if (!batchSigners.insert(signerAccount).second)
418 {
419 JLOG(ctx.j.debug())
420 << "BatchTrace[" << parentBatchId << "]: "
421 << "duplicate signer found: " << signerAccount;
422 return temREDUNDANT;
423 }
424
425 // Check that the batch signer is in the required signers set.
426 // Remove it if it does, as it can be crossed off the list.
427 if (requiredSigners.erase(signerAccount) == 0)
428 {
429 JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: "
430 << "no account signature for inner txn.";
431 return temBAD_SIGNER;
432 }
433 }
434
435 // Check the batch signers signatures.
436 auto const sigResult = ctx.tx.checkBatchSign(
438
439 if (!sigResult)
440 {
441 JLOG(ctx.j.debug())
442 << "BatchTrace[" << parentBatchId << "]: "
443 << "invalid batch txn signature: " << sigResult.error();
444 return temBAD_SIGNATURE;
445 }
446 }
447
448 if (!requiredSigners.empty())
449 {
450 JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: "
451 << "invalid batch signers.";
452 return temBAD_SIGNER;
453 }
454 return tesSUCCESS;
455}
456
474NotTEC
476{
477 if (auto ret = Transactor::checkSign(ctx); !isTesSuccess(ret))
478 return ret;
479
480 if (auto ret = Transactor::checkBatchSign(ctx); !isTesSuccess(ret))
481 return ret;
482
483 return tesSUCCESS;
484}
485
496TER
498{
499 return tesSUCCESS;
500}
501
502} // namespace ripple
Stream debug() const
Definition Journal.h:328
TER doApply() override
Applies the outer batch transaction.
Definition Batch.cpp:497
static NotTEC preflight(PreflightContext const &ctx)
Performs preflight validation checks for a Batch transaction.
Definition Batch.cpp:201
static NotTEC checkSign(PreclaimContext const &ctx)
Checks the validity of signatures for a batch transaction.
Definition Batch.cpp:475
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.
bool enabled(uint256 const &feature) const
Returns true if a feature is enabled.
Definition Rules.cpp:130
AccountID getAccountID(SField const &field) const
Definition STObject.cpp:651
STArray const & getFieldArray(SField const &field) const
Definition STObject.cpp:686
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:665
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:161
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.
NotTEC preflight1(PreflightContext const &ctx)
Performs early sanity checks on the account and fee fields.
constexpr std::uint32_t tfUntilFailure
Definition TxFlags.h:278
NotTEC preflight2(PreflightContext const &ctx)
Checks whether the signature appears valid.
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
@ temDISABLED
Definition TER.h:114
@ 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