xrpld
Loading...
Searching...
No Matches
Batch_test.cpp
1#include <test/jtx/Account.h>
2#include <test/jtx/Env.h>
3#include <test/jtx/SignerUtils.h>
4#include <test/jtx/TestHelpers.h>
5#include <test/jtx/acctdelete.h>
6#include <test/jtx/amount.h>
7#include <test/jtx/balance.h> // IWYU pragma: keep
8#include <test/jtx/batch.h>
9#include <test/jtx/check.h>
10#include <test/jtx/delegate.h>
11#include <test/jtx/envconfig.h>
12#include <test/jtx/fee.h>
13#include <test/jtx/flags.h>
14#include <test/jtx/mpt.h>
15#include <test/jtx/multisign.h>
16#include <test/jtx/noop.h>
17#include <test/jtx/offer.h>
18#include <test/jtx/pay.h>
19#include <test/jtx/regkey.h>
20#include <test/jtx/seq.h>
21#include <test/jtx/sig.h>
22#include <test/jtx/tags.h>
23#include <test/jtx/ter.h>
24#include <test/jtx/ticket.h>
25#include <test/jtx/trust.h>
26#include <test/jtx/txflags.h>
27#include <test/jtx/utility.h>
28#include <test/jtx/vault.h>
29
30#include <xrpld/app/misc/Transaction.h>
31#include <xrpld/app/misc/TxQ.h>
32
33#include <xrpl/basics/base_uint.h>
34#include <xrpl/basics/strHex.h>
35#include <xrpl/beast/unit_test/suite.h>
36#include <xrpl/beast/utility/Journal.h>
37#include <xrpl/config/BasicConfig.h>
38#include <xrpl/config/Constants.h>
39#include <xrpl/core/HashRouter.h>
40#include <xrpl/json/json_value.h>
41#include <xrpl/json/to_string.h>
42#include <xrpl/ledger/ApplyView.h>
43#include <xrpl/ledger/OpenView.h>
44#include <xrpl/protocol/AccountID.h>
45#include <xrpl/protocol/Batch.h>
46#include <xrpl/protocol/Feature.h>
47#include <xrpl/protocol/Indexes.h>
48#include <xrpl/protocol/Issue.h>
49#include <xrpl/protocol/KeyType.h>
50#include <xrpl/protocol/LedgerFormats.h>
51#include <xrpl/protocol/MPTIssue.h>
52#include <xrpl/protocol/Protocol.h>
53#include <xrpl/protocol/SField.h>
54#include <xrpl/protocol/STParsedJSON.h>
55#include <xrpl/protocol/STTx.h>
56#include <xrpl/protocol/SecretKey.h>
57#include <xrpl/protocol/SeqProxy.h>
58#include <xrpl/protocol/Serializer.h>
59#include <xrpl/protocol/Sign.h>
60#include <xrpl/protocol/TER.h>
61#include <xrpl/protocol/TxFlags.h>
62#include <xrpl/protocol/TxFormats.h>
63#include <xrpl/protocol/Units.h>
64#include <xrpl/protocol/XRPAmount.h>
65#include <xrpl/protocol/jss.h>
66#include <xrpl/server/NetworkOPs.h>
67#include <xrpl/tx/Transactor.h>
68#include <xrpl/tx/apply.h>
69#include <xrpl/tx/transactors/payment/Payment.h>
70#include <xrpl/tx/transactors/system/Batch.h>
71
72#include <algorithm>
73#include <cstddef>
74#include <cstdint>
75#include <exception>
76#include <map>
77#include <memory>
78#include <optional>
79#include <string>
80#include <utility>
81#include <vector>
82
83namespace xrpl::test {
84
86{
95
101
102 static json::Value
103 getTxByIndex(json::Value const& jrr, int const index)
104 {
105 for (auto const& txn : jrr[jss::result][jss::ledger][jss::transactions])
106 {
107 if (txn[jss::metaData][sfTransactionIndex.jsonName] == index)
108 return txn;
109 }
110 return {};
111 }
112
113 static json::Value
115 {
116 json::Value params;
117 params[jss::ledger_index] = env.closed()->seq();
118 params[jss::transactions] = true;
119 params[jss::expand] = true;
120 return env.rpc("json", "ledger", to_string(params));
121 }
122
123 void
124 validateInnerTxn(jtx::Env& env, std::string const& batchID, TestLedgerData const& ledgerResult)
125 {
126 json::Value const jrr = env.rpc("tx", ledgerResult.txHash)[jss::result];
127 BEAST_EXPECT(jrr[sfTransactionType.jsonName] == ledgerResult.txType);
128 BEAST_EXPECT(jrr[jss::meta][sfTransactionResult.jsonName] == ledgerResult.result);
129 BEAST_EXPECT(jrr[jss::meta][sfParentBatchID.jsonName] == batchID);
130 }
131
132 void
134 {
135 auto const jrr = getLastLedger(env);
136 auto const transactions = jrr[jss::result][jss::ledger][jss::transactions];
137 BEAST_EXPECT(transactions.size() == ledgerResults.size());
138 for (TestLedgerData const& ledgerResult : ledgerResults)
139 {
140 auto const txn = getTxByIndex(jrr, ledgerResult.index);
141 BEAST_EXPECT(txn[jss::hash].asString() == ledgerResult.txHash);
142 BEAST_EXPECT(txn.isMember(jss::metaData));
143 json::Value const meta = txn[jss::metaData];
144 BEAST_EXPECT(txn[sfTransactionType.jsonName] == ledgerResult.txType);
145 BEAST_EXPECT(meta[sfTransactionResult.jsonName] == ledgerResult.result);
146 if (ledgerResult.batchID)
147 validateInnerTxn(env, *ledgerResult.batchID, ledgerResult);
148 }
149 }
150
151 template <typename... Args>
153 submitBatch(jtx::Env& env, TER const& result, Args&&... args)
154 {
155 auto batchTxn = env.jt(std::forward<Args>(args)...);
156 env(batchTxn, jtx::Ter(result));
157
158 auto const ids = batchTxn.stx->getBatchTransactionIDs();
160 txIDs.reserve(ids.size());
161 for (auto const& id : ids)
162 txIDs.push_back(strHex(id));
163 TxID const batchID = batchTxn.stx->getTransactionID();
164 return std::make_pair(txIDs, strHex(batchID));
165 }
166
167 static uint256
168 getCheckIndex(AccountID const& account, std::uint32_t uSequence)
169 {
170 return keylet::check(account, SeqProxy::rawSequence(uSequence)).key;
171 }
172
176 std::map<std::string, std::string> extraVoting = {})
177 {
178 auto p = test::jtx::envconfig();
179 auto& section = p->section(Sections::kTransactionQueue);
180 section.set(Keys::kLedgersInQueue, "2");
181 section.set(Keys::kMinimumQueueSize, "2");
182 section.set(Keys::kMinLedgersToComputeSizeLimit, "3");
183 section.set(Keys::kMaxLedgerCountsToStore, "100");
184 section.set(Keys::kRetrySequencePercent, "25");
186
187 for (auto const& [k, v] : extraTxQ)
188 section.set(k, v);
189
190 return p;
191 }
192
193 static auto
194 openLedgerFee(jtx::Env& env, XRPAmount const& batchFee)
195 {
196 using namespace jtx;
197
198 auto const& view = *env.current();
199 auto metrics = env.app().getTxQ().getMetrics(view);
200 return toDrops(metrics.openLedgerFeeLevel, batchFee) + 1;
201 }
202
203 void
205 {
206 using namespace test::jtx;
207 using namespace std::literals;
208
209 for (bool const withBatch : {true, false})
210 {
211 testcase << "enabled: Batch " << (withBatch ? "enabled" : "disabled");
212
213 auto const amend = withBatch ? features : features - featureBatchV1_1;
214
215 Env env{*this, amend};
216
217 auto const alice = Account("alice");
218 auto const bob = Account("bob");
219 auto const carol = Account("carol");
220 env.fund(XRP(10000), alice, bob, carol);
221 env.close();
222
223 // ttBatch
224 {
225 auto const seq = env.seq(alice);
226 auto const batchFee = batch::calcBatchFee(env, 0, 2);
227 auto const txResult = withBatch ? Ter(tesSUCCESS) : Ter(temDISABLED);
228 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
229 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
230 batch::Inner(pay(alice, bob, XRP(1)), seq + 2),
231 txResult);
232 env.close();
233 }
234
235 // tfInnerBatchTxn
236 // A standalone transaction carrying this flag is never valid, so it
237 // is rejected early in checkValidity() regardless of the amendment.
238 {
239 env(pay(alice, bob, XRP(1)), Txflags(tfInnerBatchTxn), Ter(telENV_RPC_FAILED));
240 env.close();
241 }
242
243 env.close();
244 }
245 }
246
247 void
249 {
250 testcase("preflight");
251
252 using namespace test::jtx;
253 using namespace std::literals;
254
255 //----------------------------------------------------------------------
256 // preflight
257
258 Env env{*this, features};
259
260 auto const alice = Account("alice");
261 auto const bob = Account("bob");
262 auto const carol = Account("carol");
263 env.fund(XRP(10000), alice, bob, carol);
264 env.close();
265
266 // temBAD_FEE: preflight1
267 {
268 env(batch::outer(alice, env.seq(alice), XRP(-1), tfAllOrNothing), Ter(temBAD_FEE));
269 env.close();
270 }
271
272 // DEFENSIVE: temINVALID_FLAG: Batch: inner batch flag.
273 // ACTUAL: telENV_RPC_FAILED: checkValidity()
274 {
275 auto const seq = env.seq(alice);
276 auto const batchFee = batch::calcBatchFee(env, 0, 0);
277 env(batch::outer(alice, seq, batchFee, tfInnerBatchTxn), Ter(telENV_RPC_FAILED));
278 env.close();
279 }
280
281 // temINVALID_FLAG: Batch: invalid flags.
282 {
283 auto const seq = env.seq(alice);
284 auto const batchFee = batch::calcBatchFee(env, 0, 0);
285 env(batch::outer(alice, seq, batchFee, tfDisallowXRP), Ter(temINVALID_FLAG));
286 env.close();
287 }
288
289 // temINVALID_FLAG: Batch: too many flags.
290 {
291 auto const seq = env.seq(alice);
292 auto const batchFee = batch::calcBatchFee(env, 0, 0);
293 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
294 Txflags(tfAllOrNothing | tfOnlyOne),
296 env.close();
297 }
298
299 // temARRAY_EMPTY: Batch: txns array must have at least 2 entries.
300 {
301 auto const seq = env.seq(alice);
302 auto const batchFee = batch::calcBatchFee(env, 0, 0);
303 env(batch::outer(alice, seq, batchFee, tfAllOrNothing), Ter(temARRAY_EMPTY));
304 env.close();
305 }
306
307 // temARRAY_EMPTY: Batch: txns array must have at least 2 entries.
308 {
309 auto const seq = env.seq(alice);
310 auto const batchFee = batch::calcBatchFee(env, 0, 0);
311 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
312 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
314 env.close();
315 }
316
317 // An oversized batch (more than kMaxBatchTxCount inners) fails STTx
318 // construction, so the transaction cannot be built.
319 {
320 auto const seq = env.seq(alice);
321 auto const batchFee = batch::calcBatchFee(env, 0, 9);
322 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
323 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
324 batch::Inner(pay(alice, bob, XRP(1)), seq + 2),
325 batch::Inner(pay(alice, bob, XRP(1)), seq + 3),
326 batch::Inner(pay(alice, bob, XRP(1)), seq + 4),
327 batch::Inner(pay(alice, bob, XRP(1)), seq + 5),
328 batch::Inner(pay(alice, bob, XRP(1)), seq + 6),
329 batch::Inner(pay(alice, bob, XRP(1)), seq + 7),
330 batch::Inner(pay(alice, bob, XRP(1)), seq + 8),
331 batch::Inner(pay(alice, bob, XRP(1)), seq + 9),
333 env.close();
334 }
335
336 // temREDUNDANT: Batch: duplicate Txn found.
337 {
338 auto const batchFee = batch::calcBatchFee(env, 1, 2);
339 auto const seq = env.seq(alice);
340 auto jt = env.jtnofill(
341 batch::outer(alice, env.seq(alice), batchFee, tfAllOrNothing),
342 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
343 batch::Inner(pay(alice, bob, XRP(10)), seq + 1));
344
345 env(jt.jv, batch::Sig(bob), Ter(temREDUNDANT));
346 env.close();
347 }
348
349 // A batch may not contain a batch: the nested inner fails STTx
350 // construction, so the transaction cannot be built.
351 {
352 auto const seq = env.seq(alice);
353 auto const batchFee = batch::calcBatchFee(env, 0, 2);
354 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
355 batch::Inner(batch::outer(alice, seq, batchFee, tfAllOrNothing), seq),
356 batch::Inner(pay(alice, bob, XRP(1)), seq + 2),
358 env.close();
359 }
360
361 // temINVALID_FLAG: Batch: inner txn must have the
362 // tfInnerBatchTxn flag.
363 {
364 auto const batchFee = batch::calcBatchFee(env, 1, 2);
365 auto const seq = env.seq(alice);
366 auto tx1 = batch::Inner(pay(alice, bob, XRP(10)), seq + 1);
367 tx1[jss::Flags] = 0;
368 auto jt = env.jtnofill(
369 batch::outer(alice, seq, batchFee, tfAllOrNothing),
370 tx1,
371 batch::Inner(pay(alice, bob, XRP(10)), seq + 2));
372
373 env(jt.jv, batch::Sig(bob), Ter(temINVALID_FLAG));
374 env.close();
375 }
376
377 // temBAD_SIGNATURE: Batch: inner txn cannot include TxnSignature.
378 {
379 auto const seq = env.seq(alice);
380 auto const batchFee = batch::calcBatchFee(env, 0, 2);
381 auto jt = env.jt(pay(alice, bob, XRP(1)));
382 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
383 batch::Inner(jt.jv, seq + 1),
384 batch::Inner(pay(alice, bob, XRP(1)), seq + 2),
386 env.close();
387 }
388
389 // temBAD_SIGNER: Batch: inner txn cannot include Signers.
390 {
391 auto const seq = env.seq(alice);
392 auto const batchFee = batch::calcBatchFee(env, 0, 2);
393 auto tx1 = pay(alice, bob, XRP(1));
394 tx1[sfSigners.jsonName] = json::ValueType::Array;
395 tx1[sfSigners.jsonName][0U][sfSigner.jsonName] = json::ValueType::Object;
396 tx1[sfSigners.jsonName][0U][sfSigner.jsonName][sfAccount.jsonName] = alice.human();
397 tx1[sfSigners.jsonName][0U][sfSigner.jsonName][sfSigningPubKey.jsonName] =
398 strHex(alice.pk());
399 tx1[sfSigners.jsonName][0U][sfSigner.jsonName][sfTxnSignature.jsonName] = "DEADBEEF";
400 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
401 batch::Inner(tx1, seq + 1),
402 batch::Inner(pay(alice, bob, XRP(1)), seq + 2),
404 env.close();
405 }
406
407 // temBAD_REGKEY: Batch: inner txn must include empty
408 // SigningPubKey.
409 {
410 auto const seq = env.seq(alice);
411 auto const batchFee = batch::calcBatchFee(env, 0, 2);
412 auto tx1 = batch::Inner(pay(alice, bob, XRP(1)), seq + 1);
413 tx1[jss::SigningPubKey] = strHex(alice.pk());
414 auto jt = env.jtnofill(
415 batch::outer(alice, seq, batchFee, tfAllOrNothing),
416 tx1,
417 batch::Inner(pay(alice, bob, XRP(1)), seq + 2));
418
419 env(jt.jv, Ter(temBAD_REGKEY));
420 env.close();
421 }
422
423 // temINVALID_INNER_BATCH: Batch: inner txn preflight failed.
424 {
425 auto const seq = env.seq(alice);
426 auto const batchFee = batch::calcBatchFee(env, 0, 2);
427 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
428 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
429 // amount can't be negative
430 batch::Inner(pay(alice, bob, XRP(-1)), seq + 2),
432 env.close();
433 }
434
435 // temINVALID_INNER_BATCH: tfInnerBatchTxn set but no parentBatchId.
436 {
437 auto jtx = env.jt(pay(alice, bob, XRP(1)), Txflags(tfInnerBatchTxn));
438 PreflightContext const pfCtx(
439 env.app(), *jtx.stx, env.current()->rules(), TapNone, env.journal);
440 auto const pf = Transactor::invokePreflight<Payment>(pfCtx);
441 BEAST_EXPECT(pf == temINVALID_INNER_BATCH);
442 }
443
444 // temINVALID_FLAG: tfInnerBatchTxn set but featureBatchV1_1 disabled.
445 {
446 Env disabledEnv{*this, features - featureBatchV1_1};
447 disabledEnv.fund(XRP(10000), alice, bob);
448 disabledEnv.close();
449 auto jtx = disabledEnv.jt(pay(alice, bob, XRP(1)), Txflags(tfInnerBatchTxn));
450 PreflightContext const pfCtx(
451 disabledEnv.app(),
452 *jtx.stx,
453 disabledEnv.current()->rules(),
454 TapNone,
455 disabledEnv.journal);
456 auto const pf = Transactor::invokePreflight<Payment>(pfCtx);
457 BEAST_EXPECT(pf == temINVALID_FLAG);
458 }
459
460 // temINVALID_INNER_BATCH: parentBatchId set but tfInnerBatchTxn not
461 // set.
462 {
463 auto jtx = env.jt(pay(alice, bob, XRP(1)));
464 PreflightContext const pfCtx(
465 env.app(), *jtx.stx, uint256{1}, env.current()->rules(), TapBatch, env.journal);
466 auto const pf = Transactor::invokePreflight<Payment>(pfCtx);
467 BEAST_EXPECT(pf == temINVALID_INNER_BATCH);
468 }
469
470 // temBAD_FEE: Batch: inner txn must have a fee of 0.
471 {
472 auto const seq = env.seq(alice);
473 auto const batchFee = batch::calcBatchFee(env, 0, 2);
474 auto tx1 = batch::Inner(pay(alice, bob, XRP(1)), seq + 1);
475 tx1[jss::Fee] = to_string(env.current()->fees().base);
476 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
477 tx1,
478 batch::Inner(pay(alice, bob, XRP(2)), seq + 2),
479 Ter(temBAD_FEE));
480 env.close();
481 }
482
483 // temBAD_FEE: Inner txn with negative fee
484 {
485 auto const seq = env.seq(alice);
486 auto const batchFee = batch::calcBatchFee(env, 0, 2);
487 auto tx1 = batch::Inner(pay(alice, bob, XRP(1)), seq + 1);
488 tx1[jss::Fee] = "-1";
489 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
490 tx1,
491 batch::Inner(pay(alice, bob, XRP(2)), seq + 2),
492 Ter(temBAD_FEE));
493 env.close();
494 }
495
496 // temBAD_FEE: Inner txn with non-integer fee
497 {
498 auto const seq = env.seq(alice);
499 auto const batchFee = batch::calcBatchFee(env, 0, 2);
500 auto tx1 = batch::Inner(pay(alice, bob, XRP(1)), seq + 1);
501 tx1[jss::Fee] = "1.5";
502 auto const g = env.getParseFailureGuard(true);
503 try
504 {
505 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
506 tx1,
507 batch::Inner(pay(alice, bob, XRP(2)), seq + 2));
508 fail("Expected parse_error for fractional fee");
509 }
510 catch (jtx::ParseError const&)
511 {
512 BEAST_EXPECT(true);
513 }
514 }
515
516 // temSEQ_AND_TICKET: Batch: inner txn cannot have both Sequence
517 // and TicketSequence.
518 {
519 auto const seq = env.seq(alice);
520 auto const batchFee = batch::calcBatchFee(env, 0, 2);
521 auto tx1 = batch::Inner(pay(alice, bob, XRP(1)), 0, 1);
522 tx1[jss::Sequence] = seq + 1;
523 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
524 tx1,
525 batch::Inner(pay(alice, bob, XRP(2)), seq + 2),
527 env.close();
528 }
529
530 // temSEQ_AND_TICKET: Batch: inner txn must have either Sequence or
531 // TicketSequence.
532 {
533 auto const seq = env.seq(alice);
534 auto const batchFee = batch::calcBatchFee(env, 0, 2);
535 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
536 batch::Inner(pay(alice, bob, XRP(1)), 0),
537 batch::Inner(pay(alice, bob, XRP(2)), seq + 2),
539 env.close();
540 }
541
542 // temREDUNDANT: Batch: duplicate sequence found:
543 {
544 auto const seq = env.seq(alice);
545 auto const batchFee = batch::calcBatchFee(env, 0, 2);
546 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
547 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
548 batch::Inner(pay(alice, bob, XRP(2)), seq + 1),
550 env.close();
551 }
552
553 // temREDUNDANT: Batch: duplicate ticket found:
554 {
555 auto const seq = env.seq(alice);
556 auto const batchFee = batch::calcBatchFee(env, 0, 2);
557 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
558 batch::Inner(pay(alice, bob, XRP(1)), 0, seq + 1),
559 batch::Inner(pay(alice, bob, XRP(2)), 0, seq + 1),
561 env.close();
562 }
563
564 // temREDUNDANT: Batch: duplicate ticket & sequence found:
565 {
566 auto const seq = env.seq(alice);
567 auto const batchFee = batch::calcBatchFee(env, 0, 2);
568 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
569 batch::Inner(pay(alice, bob, XRP(1)), 0, seq + 1),
570 batch::Inner(pay(alice, bob, XRP(2)), seq + 1),
572 env.close();
573 }
574
575 // DEFENSIVE: temARRAY_TOO_LARGE: Batch: signers array exceeds
576 // kMaxBatchSigners entries.
577 // ACTUAL: telENV_RPC_FAILED: isRawTransactionOkay()
578 {
579 auto const seq = env.seq(alice);
580 auto const batchFee = batch::calcBatchFee(env, kMaxBatchSigners + 1, 2);
581 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
582 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
583 batch::Inner(pay(alice, bob, XRP(5)), seq + 2),
586 env.close();
587 }
588
589 // temBAD_SIGNER: Batch: signer cannot be the outer account
590 {
591 auto const seq = env.seq(alice);
592 auto const batchFee = batch::calcBatchFee(env, 2, 2);
593 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
594 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
595 batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
596 batch::Sig(alice, bob),
598 env.close();
599 }
600
601 // temBAD_SIGNER: Batch: duplicate signer (caught by ascending order check)
602 {
603 auto const seq = env.seq(alice);
604 auto const batchFee = batch::calcBatchFee(env, 2, 2);
605 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
606 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
607 batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
608 batch::Sig(bob, bob),
610 env.close();
611 }
612
613 // temBAD_SIGNER: Batch: no account signature for inner txn.
614 // Note: Extra signature by bob
615 {
616 auto const seq = env.seq(alice);
617 auto const batchFee = batch::calcBatchFee(env, 1, 2);
618 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
619 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
620 batch::Inner(pay(alice, bob, XRP(5)), seq + 2),
621 batch::Sig(bob),
623 env.close();
624 }
625
626 // temBAD_SIGNER: Batch: no account signature for inner txn.
627 {
628 auto const seq = env.seq(alice);
629 auto const batchFee = batch::calcBatchFee(env, 1, 2);
630 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
631 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
632 batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
633 batch::Sig(carol),
635 env.close();
636 }
637
638 // temBAD_SIGNATURE: Batch: invalid batch txn signature.
639 {
640 auto const seq = env.seq(alice);
641 auto const bobSeq = env.seq(bob);
642 auto const batchFee = batch::calcBatchFee(env, 1, 2);
643 auto jt = env.jtnofill(
644 batch::outer(alice, env.seq(alice), batchFee, tfAllOrNothing),
645 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
646 batch::Inner(pay(bob, alice, XRP(5)), bobSeq));
647
648 Serializer msg;
650 msg,
651 jt.stx->getAccountID(sfAccount),
652 jt.stx->getSeqProxy().value(),
653 tfAllOrNothing,
654 jt.stx->getBatchTransactionIDs());
655 finishMultiSigningData(bob.id(), msg);
656 auto const sig = xrpl::sign(bob.pk(), bob.sk(), msg.slice());
657 jt.jv[sfBatchSigners.jsonName][0u][sfBatchSigner.jsonName][sfAccount.jsonName] =
658 bob.human();
659 jt.jv[sfBatchSigners.jsonName][0u][sfBatchSigner.jsonName][sfSigningPubKey.jsonName] =
660 strHex(alice.pk());
661 jt.jv[sfBatchSigners.jsonName][0u][sfBatchSigner.jsonName][sfTxnSignature.jsonName] =
662 strHex(Slice{sig.data(), sig.size()});
663
664 env(jt.jv, Ter(telENV_RPC_FAILED));
665 env.close();
666 }
667
668 // temBAD_SIGNER: Batch: invalid batch signers.
669 {
670 auto const seq = env.seq(alice);
671 auto const batchFee = batch::calcBatchFee(env, 2, 2);
672 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
673 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
674 batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
675 batch::Inner(pay(carol, alice, XRP(5)), env.seq(carol)),
676 batch::Sig(bob),
678 env.close();
679 }
680 }
681
682 void
684 {
685 testcase("preclaim");
686
687 using namespace test::jtx;
688 using namespace std::literals;
689
690 //----------------------------------------------------------------------
691 // preclaim
692
693 Env env{*this, features};
694
695 auto const alice = Account("alice");
696 auto const bob = Account("bob");
697 auto const carol = Account("carol");
698 auto const dave = Account("dave");
699 auto const elsa = Account("elsa");
700 auto const frank = Account("frank");
701 auto const phantom = Account("phantom");
702 env.memoize(phantom);
703
704 env.fund(XRP(10000), alice, bob, carol, dave, elsa, frank);
705 env.close();
706
707 //----------------------------------------------------------------------
708 // checkSign.checkSingleSign
709
710 // tefBAD_AUTH: Bob is not authorized to sign for Alice
711 {
712 auto const seq = env.seq(alice);
713 auto const batchFee = batch::calcBatchFee(env, 3, 2);
714 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
715 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
716 batch::Inner(pay(alice, bob, XRP(20)), seq + 2),
717 Sig(bob),
719 env.close();
720 }
721
722 //----------------------------------------------------------------------
723 // checkBatchSign.checkMultiSign
724
725 // tefNOT_MULTI_SIGNING: SignersList not enabled
726 {
727 auto const seq = env.seq(alice);
728 auto const batchFee = batch::calcBatchFee(env, 3, 2);
729 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
730 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
731 batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
732 batch::Msig(bob, {dave, carol}),
734 env.close();
735 }
736
737 env(signers(alice, 2, {{bob, 1}, {carol, 1}}));
738 env.close();
739
740 env(signers(bob, 2, {{carol, 1}, {dave, 1}, {elsa, 1}}));
741 env.close();
742
743 // tefBAD_SIGNATURE: Account not in SignersList
744 {
745 auto const seq = env.seq(alice);
746 auto const batchFee = batch::calcBatchFee(env, 3, 2);
747 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
748 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
749 batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
750 batch::Msig(bob, {carol, frank}),
752 env.close();
753 }
754
755 // tefBAD_SIGNATURE: Wrong publicKey type
756 {
757 auto const seq = env.seq(alice);
758 auto const batchFee = batch::calcBatchFee(env, 3, 2);
759 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
760 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
761 batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
762 batch::Msig(bob, {carol, Account("dave", KeyType::Ed25519)}),
764 env.close();
765 }
766
767 // tefMASTER_DISABLED: Master key disabled
768 {
769 env(regkey(elsa, frank));
770 env(fset(elsa, asfDisableMaster), Sig(elsa));
771 auto const seq = env.seq(alice);
772 auto const batchFee = batch::calcBatchFee(env, 3, 2);
773 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
774 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
775 batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
776 batch::Msig(bob, {carol, elsa}),
778 env.close();
779 }
780
781 // tefBAD_SIGNATURE: Signer does not exist
782 {
783 auto const seq = env.seq(alice);
784 auto const batchFee = batch::calcBatchFee(env, 3, 2);
785 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
786 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
787 batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
788 batch::Msig(bob, {carol, phantom}),
790 env.close();
791 }
792
793 // tefBAD_SIGNATURE: Signer has not enabled RegularKey
794 {
795 auto const seq = env.seq(alice);
796 auto const batchFee = batch::calcBatchFee(env, 3, 2);
797 Account const davo{"davo", KeyType::Ed25519};
798 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
799 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
800 batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
801 batch::Msig(bob, {carol, Reg{dave, davo}}),
803 env.close();
804 }
805
806 // tefBAD_SIGNATURE: Wrong RegularKey Set
807 {
808 env(regkey(dave, frank));
809 auto const seq = env.seq(alice);
810 auto const batchFee = batch::calcBatchFee(env, 3, 2);
811 Account const davo{"davo", KeyType::Ed25519};
812 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
813 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
814 batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
815 batch::Msig(bob, {carol, Reg{dave, davo}}),
816 Ter(tefBAD_SIGNATURE));
817 env.close();
818 }
819
820 // tefBAD_QUORUM
821 {
822 auto const seq = env.seq(alice);
823 auto const batchFee = batch::calcBatchFee(env, 2, 2);
824 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
825 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
826 batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
827 batch::Msig(bob, {carol}),
829 env.close();
830 }
831
832 // tesSUCCESS: BatchSigners.Signers
833 {
834 auto const seq = env.seq(alice);
835 auto const batchFee = batch::calcBatchFee(env, 3, 2);
836 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
837 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
838 batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
839 batch::Msig(bob, {carol, dave}),
840 Ter(tesSUCCESS));
841 env.close();
842 }
843
844 // tesSUCCESS: Multisign + BatchSigners.Signers
845 {
846 auto const seq = env.seq(alice);
847 auto const batchFee = batch::calcBatchFee(env, 4, 2);
848 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
849 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
850 batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
851 batch::Msig(bob, {carol, dave}),
852 Msig(bob, carol),
853 Ter(tesSUCCESS));
854 env.close();
855 }
856
857 //----------------------------------------------------------------------
858 // checkBatchSign.checkSingleSign
859
860 // tefBAD_AUTH: Inner Account is not signer
861 {
862 auto const ledSeq = env.current()->seq();
863 auto const seq = env.seq(alice);
864 auto const batchFee = batch::calcBatchFee(env, 1, 2);
865 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
866 batch::Inner(pay(alice, phantom, XRP(1000)), seq + 1),
867 batch::Inner(noop(phantom), ledSeq),
868 batch::Sig(Reg{phantom, carol}),
870 env.close();
871 }
872
873 // tefBAD_AUTH: Account is not signer
874 {
875 auto const ledSeq = env.current()->seq();
876 auto const seq = env.seq(alice);
877 auto const batchFee = batch::calcBatchFee(env, 1, 2);
878 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
879 batch::Inner(pay(alice, bob, XRP(1000)), seq + 1),
880 batch::Inner(noop(bob), ledSeq),
881 batch::Sig(Reg{bob, carol}),
883 env.close();
884 }
885
886 // tesSUCCESS: Signed With Regular Key
887 {
888 env(regkey(bob, carol));
889 auto const seq = env.seq(alice);
890 auto const batchFee = batch::calcBatchFee(env, 1, 2);
891 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
892 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
893 batch::Inner(pay(bob, alice, XRP(2)), env.seq(bob)),
894 batch::Sig(Reg{bob, carol}),
895 Ter(tesSUCCESS));
896 env.close();
897 }
898
899 // tesSUCCESS: Signed With Master Key
900 {
901 auto const seq = env.seq(alice);
902 auto const batchFee = batch::calcBatchFee(env, 1, 2);
903 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
904 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
905 batch::Inner(pay(bob, alice, XRP(2)), env.seq(bob)),
906 batch::Sig(bob),
907 Ter(tesSUCCESS));
908 env.close();
909 }
910
911 // tefMASTER_DISABLED: Signed With Master Key Disabled
912 {
913 env(regkey(bob, carol));
914 env(fset(bob, asfDisableMaster), Sig(bob));
915 auto const seq = env.seq(alice);
916 auto const batchFee = batch::calcBatchFee(env, 1, 2);
917 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
918 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
919 batch::Inner(pay(bob, alice, XRP(2)), env.seq(bob)),
920 batch::Sig(bob),
922 env.close();
923 }
924 }
925
926 void
928 {
929 testcase("bad raw txn");
930
931 using namespace test::jtx;
932 using namespace std::literals;
933
934 Env env{*this, features};
935
936 auto const alice = Account("alice");
937 auto const bob = Account("bob");
938
939 env.fund(XRP(10000), alice, bob);
940
941 // An inner missing a required field can no longer be submitted: the
942 // outer STTx builds and validates each inner at construction, so
943 // building the batch (as signing does) throws. Returns true if the
944 // build fails.
945 auto batchCtorFails = [&](json::StaticString const& field) -> bool {
946 auto const batchFee = batch::calcBatchFee(env, 1, 2);
947 auto const seq = env.seq(alice);
948 auto tx1 = batch::Inner(pay(alice, bob, XRP(10)), seq + 1);
949 tx1.removeMember(field);
950 try
951 {
952 // Env::st swallows the construction failure and yields a null
953 // stx, so a malformed inner shows up as no transaction built.
954 auto const jt = env.jtnofill(
955 batch::outer(alice, seq, batchFee, tfAllOrNothing),
956 tx1,
957 batch::Inner(pay(alice, bob, XRP(10)), seq + 2));
958 return jt.stx == nullptr;
959 }
960 catch (std::exception const&)
961 {
962 return true;
963 }
964 };
965
966 // Invalid: sfTransactionType
967 BEAST_EXPECT(batchCtorFails(jss::TransactionType));
968 // Invalid: sfAccount
969 BEAST_EXPECT(batchCtorFails(jss::Account));
970 // Invalid: sfSequence
971 BEAST_EXPECT(batchCtorFails(jss::Sequence));
972 // Invalid: sfFee
973 BEAST_EXPECT(batchCtorFails(jss::Fee));
974 // Invalid: sfSigningPubKey
975 BEAST_EXPECT(batchCtorFails(jss::SigningPubKey));
976
977 // Inner OfferCreate with MPT TakerPays. Valid under featureMPTokensV2.
978 {
979 MPTIssue const issue(makeMptID(1, alice));
980 STAmount const mptAmt{issue, UINT64_C(100)};
981
982 auto const batchFee = batch::calcBatchFee(env, 0, 2);
983 auto const seq = env.seq(alice);
984
985 json::Value tx1;
986 tx1[jss::TransactionType] = jss::OfferCreate;
987 tx1[jss::Account] = alice.human();
988 tx1[jss::TakerPays] = mptAmt.getJson(JsonOptions::Values::None);
989 tx1[jss::TakerGets] = XRP(10).value().getJson(JsonOptions::Values::None);
990
991 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
992 batch::Inner(tx1, seq + 1),
993 batch::Inner(pay(alice, bob, XRP(1)), seq + 2),
994 Ter(tesSUCCESS));
995 env.close();
996 }
997
998 // Invalid: inner txn with invalid memo (non-URL-safe MemoType)
999 {
1000 auto const batchFee = batch::calcBatchFee(env, 0, 2);
1001 auto const seq = env.seq(alice);
1002
1003 auto tx1 = pay(alice, bob, XRP(10));
1004 auto& ma = tx1["Memos"];
1005 auto& mi = ma[ma.size()];
1006 auto& m = mi["Memo"];
1007 m["MemoType"] = strHex(std::string("\x01\x02\x03", 3));
1008 m["MemoData"] = strHex(std::string("test"));
1009
1010 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
1011 batch::Inner(tx1, seq + 1),
1012 batch::Inner(pay(alice, bob, XRP(1)), seq + 2),
1014 env.close();
1015 }
1016 }
1017
1018 void
1020 {
1021 testcase("bad sequence");
1022
1023 using namespace test::jtx;
1024 using namespace std::literals;
1025
1026 Env env{*this, features};
1027
1028 auto const alice = Account("alice");
1029 auto const bob = Account("bob");
1030 auto const gw = Account("gw");
1031 auto const usd = gw["USD"];
1032
1033 env.fund(XRP(10000), alice, bob, gw);
1034 env.close();
1035 env.trust(usd(1000), alice, bob);
1036 env(pay(gw, alice, usd(100)));
1037 env(pay(gw, bob, usd(100)));
1038 env.close();
1039
1040 env(noop(bob), Ter(tesSUCCESS));
1041 env.close();
1042
1043 // Invalid: Alice Sequence is a past sequence
1044 {
1045 auto const preAliceSeq = env.seq(alice);
1046 auto const preAlice = env.balance(alice);
1047 auto const preAliceUSD = env.balance(alice, usd.issue());
1048 auto const preBobSeq = env.seq(bob);
1049 auto const preBob = env.balance(bob);
1050 auto const preBobUSD = env.balance(bob, usd.issue());
1051
1052 auto const batchFee = batch::calcBatchFee(env, 1, 2);
1053 auto const [txIDs, batchID] = submitBatch(
1054 env,
1055 tesSUCCESS,
1056 batch::outer(alice, preAliceSeq, batchFee, tfAllOrNothing),
1057 batch::Inner(pay(alice, bob, XRP(10)), preAliceSeq - 10),
1058 batch::Inner(pay(bob, alice, XRP(5)), preBobSeq),
1059 batch::Sig(bob));
1060
1061 env.close();
1062 {
1063 std::vector<TestLedgerData> const testCases = {
1064 {.index = 0,
1065 .txType = "Batch",
1066 .result = "tesSUCCESS",
1067 .txHash = batchID,
1068 .batchID = std::nullopt},
1069 };
1070 validateClosedLedger(env, testCases);
1071 }
1072
1073 env.close();
1074 {
1075 // next ledger is empty
1076 std::vector<TestLedgerData> const testCases = {};
1077 validateClosedLedger(env, testCases);
1078 }
1079
1080 // Alice pays fee & Bob should not be affected.
1081 BEAST_EXPECT(env.seq(alice) == preAliceSeq + 1);
1082 BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
1083 BEAST_EXPECT(env.balance(alice, usd.issue()) == preAliceUSD);
1084 BEAST_EXPECT(env.seq(bob) == preBobSeq);
1085 BEAST_EXPECT(env.balance(bob) == preBob);
1086 BEAST_EXPECT(env.balance(bob, usd.issue()) == preBobUSD);
1087 }
1088
1089 // Invalid: Alice Sequence is a future sequence
1090 {
1091 auto const preAliceSeq = env.seq(alice);
1092 auto const preAlice = env.balance(alice);
1093 auto const preAliceUSD = env.balance(alice, usd.issue());
1094 auto const preBobSeq = env.seq(bob);
1095 auto const preBob = env.balance(bob);
1096 auto const preBobUSD = env.balance(bob, usd.issue());
1097
1098 auto const batchFee = batch::calcBatchFee(env, 1, 2);
1099 auto const [txIDs, batchID] = submitBatch(
1100 env,
1101 tesSUCCESS,
1102 batch::outer(alice, preAliceSeq, batchFee, tfAllOrNothing),
1103 batch::Inner(pay(alice, bob, XRP(10)), preAliceSeq + 10),
1104 batch::Inner(pay(bob, alice, XRP(5)), preBobSeq),
1105 batch::Sig(bob));
1106
1107 env.close();
1108 {
1109 std::vector<TestLedgerData> const testCases = {
1110 {.index = 0,
1111 .txType = "Batch",
1112 .result = "tesSUCCESS",
1113 .txHash = batchID,
1114 .batchID = std::nullopt},
1115 };
1116 validateClosedLedger(env, testCases);
1117 }
1118
1119 env.close();
1120 {
1121 // next ledger is empty
1122 std::vector<TestLedgerData> const testCases = {};
1123 validateClosedLedger(env, testCases);
1124 }
1125
1126 // Alice pays fee & Bob should not be affected.
1127 BEAST_EXPECT(env.seq(alice) == preAliceSeq + 1);
1128 BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
1129 BEAST_EXPECT(env.balance(alice, usd.issue()) == preAliceUSD);
1130 BEAST_EXPECT(env.seq(bob) == preBobSeq);
1131 BEAST_EXPECT(env.balance(bob) == preBob);
1132 BEAST_EXPECT(env.balance(bob, usd.issue()) == preBobUSD);
1133 }
1134
1135 // Invalid: Bob Sequence is a past sequence
1136 {
1137 auto const preAliceSeq = env.seq(alice);
1138 auto const preAlice = env.balance(alice);
1139 auto const preAliceUSD = env.balance(alice, usd.issue());
1140 auto const preBobSeq = env.seq(bob);
1141 auto const preBob = env.balance(bob);
1142 auto const preBobUSD = env.balance(bob, usd.issue());
1143
1144 auto const batchFee = batch::calcBatchFee(env, 1, 2);
1145 auto const [txIDs, batchID] = submitBatch(
1146 env,
1147 tesSUCCESS,
1148 batch::outer(alice, preAliceSeq, batchFee, tfAllOrNothing),
1149 batch::Inner(pay(alice, bob, XRP(10)), preAliceSeq + 1),
1150 batch::Inner(pay(bob, alice, XRP(5)), preBobSeq - 10),
1151 batch::Sig(bob));
1152
1153 env.close();
1154 {
1155 std::vector<TestLedgerData> const testCases = {
1156 {.index = 0,
1157 .txType = "Batch",
1158 .result = "tesSUCCESS",
1159 .txHash = batchID,
1160 .batchID = std::nullopt},
1161 };
1162 validateClosedLedger(env, testCases);
1163 }
1164
1165 env.close();
1166 {
1167 // next ledger is empty
1168 std::vector<TestLedgerData> const testCases = {};
1169 validateClosedLedger(env, testCases);
1170 }
1171
1172 // Alice pays fee & Bob should not be affected.
1173 BEAST_EXPECT(env.seq(alice) == preAliceSeq + 1);
1174 BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
1175 BEAST_EXPECT(env.balance(alice, usd.issue()) == preAliceUSD);
1176 BEAST_EXPECT(env.seq(bob) == preBobSeq);
1177 BEAST_EXPECT(env.balance(bob) == preBob);
1178 BEAST_EXPECT(env.balance(bob, usd.issue()) == preBobUSD);
1179 }
1180
1181 // Invalid: Bob Sequence is a future sequence
1182 {
1183 auto const preAliceSeq = env.seq(alice);
1184 auto const preAlice = env.balance(alice);
1185 auto const preAliceUSD = env.balance(alice, usd.issue());
1186 auto const preBobSeq = env.seq(bob);
1187 auto const preBob = env.balance(bob);
1188 auto const preBobUSD = env.balance(bob, usd.issue());
1189
1190 auto const batchFee = batch::calcBatchFee(env, 1, 2);
1191 auto const [txIDs, batchID] = submitBatch(
1192 env,
1193 tesSUCCESS,
1194 batch::outer(alice, preAliceSeq, batchFee, tfAllOrNothing),
1195 batch::Inner(pay(alice, bob, XRP(10)), preAliceSeq + 1),
1196 batch::Inner(pay(bob, alice, XRP(5)), preBobSeq + 10),
1197 batch::Sig(bob));
1198
1199 env.close();
1200 {
1201 std::vector<TestLedgerData> const testCases = {
1202 {.index = 0,
1203 .txType = "Batch",
1204 .result = "tesSUCCESS",
1205 .txHash = batchID,
1206 .batchID = std::nullopt},
1207 };
1208 validateClosedLedger(env, testCases);
1209 }
1210
1211 env.close();
1212 {
1213 // next ledger is empty
1214 std::vector<TestLedgerData> const testCases = {};
1215 validateClosedLedger(env, testCases);
1216 }
1217
1218 // Alice pays fee & Bob should not be affected.
1219 BEAST_EXPECT(env.seq(alice) == preAliceSeq + 1);
1220 BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
1221 BEAST_EXPECT(env.balance(alice, usd.issue()) == preAliceUSD);
1222 BEAST_EXPECT(env.seq(bob) == preBobSeq);
1223 BEAST_EXPECT(env.balance(bob) == preBob);
1224 BEAST_EXPECT(env.balance(bob, usd.issue()) == preBobUSD);
1225 }
1226
1227 // Invalid: Outer and Inner Sequence are the same
1228 {
1229 auto const preAliceSeq = env.seq(alice);
1230 auto const preAlice = env.balance(alice);
1231 auto const preAliceUSD = env.balance(alice, usd.issue());
1232 auto const preBobSeq = env.seq(bob);
1233 auto const preBob = env.balance(bob);
1234 auto const preBobUSD = env.balance(bob, usd.issue());
1235
1236 auto const batchFee = batch::calcBatchFee(env, 1, 2);
1237 auto const [txIDs, batchID] = submitBatch(
1238 env,
1239 tesSUCCESS,
1240 batch::outer(alice, preAliceSeq, batchFee, tfAllOrNothing),
1241 batch::Inner(pay(alice, bob, XRP(10)), preAliceSeq),
1242 batch::Inner(pay(bob, alice, XRP(5)), preBobSeq),
1243 batch::Sig(bob));
1244
1245 env.close();
1246 {
1247 std::vector<TestLedgerData> const testCases = {
1248 {.index = 0,
1249 .txType = "Batch",
1250 .result = "tesSUCCESS",
1251 .txHash = batchID,
1252 .batchID = std::nullopt},
1253 };
1254 validateClosedLedger(env, testCases);
1255 }
1256
1257 env.close();
1258 {
1259 // next ledger is empty
1260 std::vector<TestLedgerData> const testCases = {};
1261 validateClosedLedger(env, testCases);
1262 }
1263
1264 // Alice pays fee & Bob should not be affected.
1265 BEAST_EXPECT(env.seq(alice) == preAliceSeq + 1);
1266 BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
1267 BEAST_EXPECT(env.balance(alice, usd.issue()) == preAliceUSD);
1268 BEAST_EXPECT(env.seq(bob) == preBobSeq);
1269 BEAST_EXPECT(env.balance(bob) == preBob);
1270 BEAST_EXPECT(env.balance(bob, usd.issue()) == preBobUSD);
1271 }
1272 }
1273
1274 void
1276 {
1277 testcase("bad outer fee");
1278
1279 using namespace test::jtx;
1280 using namespace std::literals;
1281
1282 // Bad Fee Without Signer
1283 {
1284 Env env{*this, features};
1285
1286 auto const alice = Account("alice");
1287 auto const bob = Account("bob");
1288 env.fund(XRP(10000), alice, bob);
1289 env.close();
1290
1291 env(noop(bob), Ter(tesSUCCESS));
1292 env.close();
1293
1294 // Bad Fee: Should be batch::calcBatchFee(env, 0, 2)
1295 auto const batchFee = batch::calcBatchFee(env, 0, 1);
1296 auto const aliceSeq = env.seq(alice);
1297 env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
1298 batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
1299 batch::Inner(pay(alice, bob, XRP(15)), aliceSeq + 2),
1301 env.close();
1302 }
1303
1304 // Bad Fee With MultiSign
1305 {
1306 Env env{*this, features};
1307
1308 auto const alice = Account("alice");
1309 auto const bob = Account("bob");
1310 auto const carol = Account("carol");
1311 env.fund(XRP(10000), alice, bob, carol);
1312 env.close();
1313
1314 env(noop(bob), Ter(tesSUCCESS));
1315 env.close();
1316
1317 env(signers(alice, 2, {{bob, 1}, {carol, 1}}));
1318 env.close();
1319
1320 // Bad Fee: Should be batch::calcBatchFee(env, 2, 2)
1321 auto const batchFee = batch::calcBatchFee(env, 1, 2);
1322 auto const aliceSeq = env.seq(alice);
1323 env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
1324 batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
1325 batch::Inner(pay(alice, bob, XRP(15)), aliceSeq + 2),
1326 Msig(bob, carol),
1328 env.close();
1329 }
1330
1331 // Bad Fee With MultiSign + BatchSigners
1332 {
1333 Env env{*this, features};
1334
1335 auto const alice = Account("alice");
1336 auto const bob = Account("bob");
1337 auto const carol = Account("carol");
1338 env.fund(XRP(10000), alice, bob, carol);
1339 env.close();
1340
1341 env(noop(bob), Ter(tesSUCCESS));
1342 env.close();
1343
1344 env(signers(alice, 2, {{bob, 1}, {carol, 1}}));
1345 env.close();
1346
1347 // Bad Fee: Should be batch::calcBatchFee(env, 3, 2)
1348 auto const batchFee = batch::calcBatchFee(env, 2, 2);
1349 auto const aliceSeq = env.seq(alice);
1350 auto const bobSeq = env.seq(bob);
1351 env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
1352 batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
1353 batch::Inner(pay(bob, alice, XRP(5)), bobSeq),
1354 batch::Sig(bob),
1355 Msig(bob, carol),
1357 env.close();
1358 }
1359
1360 // Bad Fee With MultiSign + BatchSigners.Signers
1361 {
1362 Env env{*this, features};
1363
1364 auto const alice = Account("alice");
1365 auto const bob = Account("bob");
1366 auto const carol = Account("carol");
1367 env.fund(XRP(10000), alice, bob, carol);
1368 env.close();
1369
1370 env(noop(bob), Ter(tesSUCCESS));
1371 env.close();
1372
1373 env(signers(alice, 2, {{bob, 1}, {carol, 1}}));
1374 env.close();
1375
1376 env(signers(bob, 2, {{alice, 1}, {carol, 1}}));
1377 env.close();
1378
1379 // Bad Fee: Should be batch::calcBatchFee(env, 4, 2)
1380 auto const batchFee = batch::calcBatchFee(env, 3, 2);
1381 auto const aliceSeq = env.seq(alice);
1382 auto const bobSeq = env.seq(bob);
1383 env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
1384 batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
1385 batch::Inner(pay(bob, alice, XRP(5)), bobSeq),
1386 batch::Msig(bob, {alice, carol}),
1387 Msig(bob, carol),
1389 env.close();
1390 }
1391
1392 // Bad Fee With BatchSigners
1393 {
1394 Env env{*this, features};
1395
1396 auto const alice = Account("alice");
1397 auto const bob = Account("bob");
1398 env.fund(XRP(10000), alice, bob);
1399 env.close();
1400
1401 env(noop(bob), Ter(tesSUCCESS));
1402 env.close();
1403
1404 // Bad Fee: Should be batch::calcBatchFee(env, 1, 2)
1405 auto const batchFee = batch::calcBatchFee(env, 0, 2);
1406 auto const aliceSeq = env.seq(alice);
1407 auto const bobSeq = env.seq(bob);
1408 env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
1409 batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
1410 batch::Inner(pay(bob, alice, XRP(5)), bobSeq),
1411 batch::Sig(bob),
1413 env.close();
1414 }
1415
1416 // Bad Fee Dynamic Fee Calculation
1417 {
1418 Env env{*this, features};
1419
1420 auto const alice = Account("alice");
1421 auto const bob = Account("bob");
1422 auto const gw = Account("gw");
1423 auto const usd = gw["USD"];
1424
1425 env.fund(XRP(10000), alice, bob, gw);
1426 env.close();
1427 auto const ammCreate = [&alice](STAmount const& amount, STAmount const& amount2) {
1428 json::Value jv;
1429 jv[jss::Account] = alice.human();
1430 jv[jss::Amount] = amount.getJson(JsonOptions::Values::None);
1431 jv[jss::Amount2] = amount2.getJson(JsonOptions::Values::None);
1432 jv[jss::TradingFee] = 0;
1433 jv[jss::TransactionType] = jss::AMMCreate;
1434 return jv;
1435 };
1436
1437 auto const batchFee = batch::calcBatchFee(env, 0, 2);
1438 auto const seq = env.seq(alice);
1439 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
1440 batch::Inner(ammCreate(XRP(10), usd(10)), seq + 1),
1441 batch::Inner(pay(alice, bob, XRP(10)), seq + 2),
1443 env.close();
1444 }
1445 }
1446
1447 void
1449 {
1450 testcase("calculate base fee");
1451
1452 using namespace test::jtx;
1453 using namespace std::literals;
1454
1455 // telENV_RPC_FAILED: Batch: txns array exceeds 8 entries.
1456 {
1457 Env env{*this, features};
1458
1459 auto const alice = Account("alice");
1460 auto const bob = Account("bob");
1461 env.fund(XRP(10000), alice, bob);
1462 env.close();
1463
1464 auto const batchFee = batch::calcBatchFee(env, 0, 9);
1465 auto const aliceSeq = env.seq(alice);
1466 env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
1467 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1468 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1469 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1470 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1471 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1472 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1473 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1474 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1475 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1476 Ter(temMALFORMED));
1477 env.close();
1478 }
1479
1480 // An oversized batch (more than kMaxBatchTxCount inners) fails STTx
1481 // construction, so it never reaches apply or checkValidity - Env::st
1482 // swallows the failure and yields a null stx.
1483 {
1484 Env env{*this, features};
1485
1486 auto const alice = Account("alice");
1487 auto const bob = Account("bob");
1488 env.fund(XRP(10000), alice, bob);
1489 env.close();
1490
1491 auto const batchFee = batch::calcBatchFee(env, 0, 9);
1492 auto const aliceSeq = env.seq(alice);
1493 auto jt = env.jtnofill(
1494 batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
1495 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1496 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1497 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1498 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1499 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1500 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1501 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1502 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1503 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq));
1504
1505 BEAST_EXPECT(jt.stx == nullptr);
1506 }
1507
1508 // An oversized batch cannot slip through as validly signed even when it
1509 // carries a BatchSigners array: the oversized inner array fails STTx
1510 // construction before any signature is checked, so the batch can't be
1511 // built at all (building it - as signing does - throws).
1512 {
1513 Env env{*this, features};
1514
1515 auto const alice = Account("alice");
1516 auto const bob = Account("bob");
1517 env.fund(XRP(10000), alice, bob);
1518 env.close();
1519
1520 auto const aliceSeq = env.seq(alice);
1521 auto const batchFee = batch::calcBatchFee(env, kMaxBatchSigners + 1, 9);
1522 bool threw = false;
1523 try
1524 {
1525 env.jtnofill(
1526 batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
1527 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1528 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1529 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1530 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1531 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1532 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1533 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1534 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1535 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
1537 }
1538 catch (std::exception const&)
1539 {
1540 threw = true;
1541 }
1542 BEAST_EXPECT(threw);
1543 }
1544
1545 // Regression: the relay-boundary local check (isBatchRawTransactionOkay)
1546 // caps the signers array at kMaxBatchSigners, matching Batch::preflight -
1547 // not kMaxBatchTxCount. A batch with more than kMaxBatchTxCount but at
1548 // most kMaxBatchSigners signers must get past local checks and reach
1549 // signer validation. Before the cap was aligned it was wrongly rejected
1550 // at the boundary (telENV_RPC_FAILED) before preflight ran; now it
1551 // reaches preflightSigValidated and fails there as extra signers
1552 // (temBAD_SIGNER) rather than being dropped pre-engine.
1553 {
1554 Env env{*this, features};
1555
1556 auto const alice = Account("alice");
1557 auto const bob = Account("bob");
1558 env.fund(XRP(10000), alice, bob);
1559 env.close();
1560
1561 // Over kMaxBatchTxCount (8) but within kMaxBatchSigners (24).
1562 std::size_t const signerCount = kMaxBatchTxCount + 1;
1563
1564 auto const aliceSeq = env.seq(alice);
1565 auto const batchFee = batch::calcBatchFee(env, signerCount, 2);
1566 env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
1567 batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
1568 batch::Inner(pay(alice, bob, XRP(5)), aliceSeq + 2),
1569 batch::Sig(std::vector<Reg>(signerCount, bob)),
1571 env.close();
1572 }
1573
1574 // temARRAY_TOO_LARGE: Batch preflight: signers array exceeds
1575 // kMaxBatchSigners entries.
1576 {
1577 Env env{*this, features};
1578
1579 auto const alice = Account("alice");
1580 auto const bob = Account("bob");
1581 env.fund(XRP(10000), alice, bob);
1582 env.close();
1583
1584 auto const batchFee = batch::calcBatchFee(env, kMaxBatchSigners + 1, 2);
1585 auto const aliceSeq = env.seq(alice);
1586 auto jt = env.jtnofill(
1587 batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
1588 batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
1589 batch::Inner(pay(alice, bob, XRP(5)), aliceSeq + 2),
1591
1592 env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal j) {
1593 auto const result = xrpl::apply(env.app(), view, *jt.stx, TapNone, j);
1594 BEAST_EXPECT(!result.applied && result.ter == temARRAY_TOO_LARGE);
1595 return result.applied;
1596 });
1597
1598 // Regression (uncapped batch-signer verification): the relay
1599 // boundary (checkValidity) rejects the oversized signers array via
1600 // the checkBatchSign guard, BEFORE verifying a single signature.
1601 auto const [valid, reason] =
1602 xrpl::checkValidity(env.app().getHashRouter(), *jt.stx, env.current()->rules());
1603 BEAST_EXPECT(valid == xrpl::Validity::SigBad);
1604 BEAST_EXPECT(reason == "BatchSigners array exceeds max entries.");
1605 }
1606 }
1607
1608 void
1610 {
1611 testcase("all or nothing");
1612
1613 using namespace test::jtx;
1614 using namespace std::literals;
1615
1616 Env env{*this, features};
1617
1618 auto const alice = Account("alice");
1619 auto const bob = Account("bob");
1620 auto const gw = Account("gw");
1621 auto const usd = gw["USD"];
1622 env.fund(XRP(10000), alice, bob, gw);
1623 env.close();
1624
1625 // all
1626 {
1627 auto const preAlice = env.balance(alice);
1628 auto const preBob = env.balance(bob);
1629
1630 auto const batchFee = batch::calcBatchFee(env, 0, 2);
1631 auto const seq = env.seq(alice);
1632 auto const [txIDs, batchID] = submitBatch(
1633 env,
1634 tesSUCCESS,
1635 batch::outer(alice, seq, batchFee, tfAllOrNothing),
1636 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
1637 batch::Inner(pay(alice, bob, XRP(2)), seq + 2));
1638 env.close();
1639
1640 std::vector<TestLedgerData> const testCases = {
1641 {.index = 0,
1642 .txType = "Batch",
1643 .result = "tesSUCCESS",
1644 .txHash = batchID,
1645 .batchID = std::nullopt},
1646 {.index = 1,
1647 .txType = "Payment",
1648 .result = "tesSUCCESS",
1649 .txHash = txIDs[0],
1650 .batchID = batchID},
1651 {.index = 2,
1652 .txType = "Payment",
1653 .result = "tesSUCCESS",
1654 .txHash = txIDs[1],
1655 .batchID = batchID},
1656 };
1657 validateClosedLedger(env, testCases);
1658
1659 // Alice consumes sequences (# of txns)
1660 BEAST_EXPECT(env.seq(alice) == seq + 3);
1661
1662 // Alice pays XRP & Fee; Bob receives XRP
1663 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee);
1664 BEAST_EXPECT(env.balance(bob) == preBob + XRP(3));
1665 }
1666
1667 // tec failure
1668 {
1669 auto const preAlice = env.balance(alice);
1670 auto const preBob = env.balance(bob);
1671
1672 auto const batchFee = batch::calcBatchFee(env, 0, 2);
1673 auto const seq = env.seq(alice);
1674
1675 auto const [txIDs, batchID] = submitBatch(
1676 env,
1677 tesSUCCESS,
1678 batch::outer(alice, seq, batchFee, tfAllOrNothing),
1679 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
1680 // tecUNFUNDED_PAYMENT: alice does not have enough XRP
1681 batch::Inner(pay(alice, bob, XRP(9999)), seq + 2));
1682 env.close();
1683
1684 std::vector<TestLedgerData> const testCases = {
1685 {.index = 0,
1686 .txType = "Batch",
1687 .result = "tesSUCCESS",
1688 .txHash = batchID,
1689 .batchID = std::nullopt},
1690 };
1691 validateClosedLedger(env, testCases);
1692
1693 // Alice consumes sequence
1694 BEAST_EXPECT(env.seq(alice) == seq + 1);
1695
1696 // Alice pays Fee; Bob should not be affected
1697 BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
1698 BEAST_EXPECT(env.balance(bob) == preBob);
1699 }
1700
1701 // tef failure
1702 {
1703 auto const preAlice = env.balance(alice);
1704 auto const preBob = env.balance(bob);
1705
1706 auto const batchFee = batch::calcBatchFee(env, 0, 2);
1707 auto const seq = env.seq(alice);
1708 auto const [txIDs, batchID] = submitBatch(
1709 env,
1710 tesSUCCESS,
1711 batch::outer(alice, seq, batchFee, tfAllOrNothing),
1712 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
1713 // tefNO_AUTH_REQUIRED: trustline auth is not required
1714 batch::Inner(trust(alice, usd(1000), tfSetfAuth), seq + 2));
1715 env.close();
1716
1717 std::vector<TestLedgerData> const testCases = {
1718 {.index = 0,
1719 .txType = "Batch",
1720 .result = "tesSUCCESS",
1721 .txHash = batchID,
1722 .batchID = std::nullopt},
1723 };
1724 validateClosedLedger(env, testCases);
1725
1726 // Alice consumes sequence
1727 BEAST_EXPECT(env.seq(alice) == seq + 1);
1728
1729 // Alice pays Fee; Bob should not be affected
1730 BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
1731 BEAST_EXPECT(env.balance(bob) == preBob);
1732 }
1733
1734 // ter failure
1735 {
1736 auto const preAlice = env.balance(alice);
1737 auto const preBob = env.balance(bob);
1738
1739 auto const batchFee = batch::calcBatchFee(env, 0, 2);
1740 auto const seq = env.seq(alice);
1741 auto const [txIDs, batchID] = submitBatch(
1742 env,
1743 tesSUCCESS,
1744 batch::outer(alice, seq, batchFee, tfAllOrNothing),
1745 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
1746 // terPRE_TICKET: ticket does not exist
1747 batch::Inner(trust(alice, usd(1000), tfSetfAuth), 0, seq + 2));
1748 env.close();
1749
1750 std::vector<TestLedgerData> const testCases = {
1751 {.index = 0,
1752 .txType = "Batch",
1753 .result = "tesSUCCESS",
1754 .txHash = batchID,
1755 .batchID = std::nullopt},
1756 };
1757 validateClosedLedger(env, testCases);
1758
1759 // Alice consumes sequence
1760 BEAST_EXPECT(env.seq(alice) == seq + 1);
1761
1762 // Alice pays Fee; Bob should not be affected
1763 BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
1764 BEAST_EXPECT(env.balance(bob) == preBob);
1765 }
1766 }
1767
1768 void
1770 {
1771 testcase("only one");
1772
1773 using namespace test::jtx;
1774 using namespace std::literals;
1775
1776 Env env{*this, features};
1777
1778 auto const alice = Account("alice");
1779 auto const bob = Account("bob");
1780 auto const carol = Account("carol");
1781 auto const dave = Account("dave");
1782 auto const gw = Account("gw");
1783 auto const usd = gw["USD"];
1784 env.fund(XRP(10000), alice, bob, carol, dave, gw);
1785 env.close();
1786
1787 // all transactions fail
1788 {
1789 auto const preAlice = env.balance(alice);
1790 auto const preBob = env.balance(bob);
1791
1792 auto const batchFee = batch::calcBatchFee(env, 0, 3);
1793 auto const seq = env.seq(alice);
1794 auto const [txIDs, batchID] = submitBatch(
1795 env,
1796 tesSUCCESS,
1797 batch::outer(alice, seq, batchFee, tfOnlyOne),
1798 // tecUNFUNDED_PAYMENT: alice does not have enough XRP
1799 batch::Inner(pay(alice, bob, XRP(9999)), seq + 1),
1800 // tecUNFUNDED_PAYMENT: alice does not have enough XRP
1801 batch::Inner(pay(alice, bob, XRP(9999)), seq + 2),
1802 // tecUNFUNDED_PAYMENT: alice does not have enough XRP
1803 batch::Inner(pay(alice, bob, XRP(9999)), seq + 3));
1804 env.close();
1805
1806 std::vector<TestLedgerData> const testCases = {
1807 {.index = 0,
1808 .txType = "Batch",
1809 .result = "tesSUCCESS",
1810 .txHash = batchID,
1811 .batchID = std::nullopt},
1812 {.index = 1,
1813 .txType = "Payment",
1814 .result = "tecUNFUNDED_PAYMENT",
1815 .txHash = txIDs[0],
1816 .batchID = batchID},
1817 {.index = 2,
1818 .txType = "Payment",
1819 .result = "tecUNFUNDED_PAYMENT",
1820 .txHash = txIDs[1],
1821 .batchID = batchID},
1822 {.index = 3,
1823 .txType = "Payment",
1824 .result = "tecUNFUNDED_PAYMENT",
1825 .txHash = txIDs[2],
1826 .batchID = batchID},
1827 };
1828 validateClosedLedger(env, testCases);
1829
1830 // Alice consumes sequences (# of txns)
1831 BEAST_EXPECT(env.seq(alice) == seq + 4);
1832
1833 // Alice pays XRP & Fee; Bob receives XRP
1834 BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
1835 BEAST_EXPECT(env.balance(bob) == preBob);
1836 }
1837
1838 // first transaction fails
1839 {
1840 auto const preAlice = env.balance(alice);
1841 auto const preBob = env.balance(bob);
1842
1843 auto const batchFee = batch::calcBatchFee(env, 0, 3);
1844 auto const seq = env.seq(alice);
1845 auto const [txIDs, batchID] = submitBatch(
1846 env,
1847 tesSUCCESS,
1848 batch::outer(alice, seq, batchFee, tfOnlyOne),
1849 // tecUNFUNDED_PAYMENT: alice does not have enough XRP
1850 batch::Inner(pay(alice, bob, XRP(9999)), seq + 1),
1851 batch::Inner(pay(alice, bob, XRP(1)), seq + 2),
1852 batch::Inner(pay(alice, bob, XRP(2)), seq + 3));
1853 env.close();
1854
1855 std::vector<TestLedgerData> const testCases = {
1856 {.index = 0,
1857 .txType = "Batch",
1858 .result = "tesSUCCESS",
1859 .txHash = batchID,
1860 .batchID = std::nullopt},
1861 {.index = 1,
1862 .txType = "Payment",
1863 .result = "tecUNFUNDED_PAYMENT",
1864 .txHash = txIDs[0],
1865 .batchID = batchID},
1866 {.index = 2,
1867 .txType = "Payment",
1868 .result = "tesSUCCESS",
1869 .txHash = txIDs[1],
1870 .batchID = batchID},
1871 };
1872 validateClosedLedger(env, testCases);
1873
1874 // Alice consumes sequences (# of txns)
1875 BEAST_EXPECT(env.seq(alice) == seq + 3);
1876
1877 // Alice pays XRP & Fee; Bob receives XRP
1878 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1) - batchFee);
1879 BEAST_EXPECT(env.balance(bob) == preBob + XRP(1));
1880 }
1881
1882 // tec failure
1883 {
1884 auto const preAlice = env.balance(alice);
1885 auto const preBob = env.balance(bob);
1886
1887 auto const batchFee = batch::calcBatchFee(env, 0, 3);
1888 auto const seq = env.seq(alice);
1889 auto const [txIDs, batchID] = submitBatch(
1890 env,
1891 tesSUCCESS,
1892 batch::outer(alice, seq, batchFee, tfOnlyOne),
1893 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
1894 // tecUNFUNDED_PAYMENT: alice does not have enough XRP
1895 batch::Inner(pay(alice, bob, XRP(9999)), seq + 2),
1896 batch::Inner(pay(alice, bob, XRP(2)), seq + 3));
1897 env.close();
1898
1899 std::vector<TestLedgerData> const testCases = {
1900 {.index = 0,
1901 .txType = "Batch",
1902 .result = "tesSUCCESS",
1903 .txHash = batchID,
1904 .batchID = std::nullopt},
1905 {.index = 1,
1906 .txType = "Payment",
1907 .result = "tesSUCCESS",
1908 .txHash = txIDs[0],
1909 .batchID = batchID},
1910 };
1911 validateClosedLedger(env, testCases);
1912
1913 // Alice consumes sequences (# of txns)
1914 BEAST_EXPECT(env.seq(alice) == seq + 2);
1915
1916 // Alice pays XRP & Fee; Bob receives XRP
1917 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1) - batchFee);
1918 BEAST_EXPECT(env.balance(bob) == preBob + XRP(1));
1919 }
1920
1921 // tef failure
1922 {
1923 auto const preAlice = env.balance(alice);
1924 auto const preBob = env.balance(bob);
1925
1926 auto const batchFee = batch::calcBatchFee(env, 0, 3);
1927 auto const seq = env.seq(alice);
1928 auto const [txIDs, batchID] = submitBatch(
1929 env,
1930 tesSUCCESS,
1931 batch::outer(alice, seq, batchFee, tfOnlyOne),
1932 // tefNO_AUTH_REQUIRED: trustline auth is not required
1933 batch::Inner(trust(alice, usd(1000), tfSetfAuth), seq + 1),
1934 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
1935 batch::Inner(pay(alice, bob, XRP(2)), seq + 3));
1936 env.close();
1937
1938 std::vector<TestLedgerData> const testCases = {
1939 {.index = 0,
1940 .txType = "Batch",
1941 .result = "tesSUCCESS",
1942 .txHash = batchID,
1943 .batchID = std::nullopt},
1944 {.index = 1,
1945 .txType = "Payment",
1946 .result = "tesSUCCESS",
1947 .txHash = txIDs[1],
1948 .batchID = batchID},
1949 };
1950 validateClosedLedger(env, testCases);
1951
1952 // Alice consumes sequences (# of txns)
1953 BEAST_EXPECT(env.seq(alice) == seq + 2);
1954
1955 // Alice pays XRP & Fee; Bob receives XRP
1956 BEAST_EXPECT(env.balance(alice) == preAlice - batchFee - XRP(1));
1957 BEAST_EXPECT(env.balance(bob) == preBob + XRP(1));
1958 }
1959
1960 // ter failure
1961 {
1962 auto const preAlice = env.balance(alice);
1963 auto const preBob = env.balance(bob);
1964
1965 auto const batchFee = batch::calcBatchFee(env, 0, 3);
1966 auto const seq = env.seq(alice);
1967 auto const [txIDs, batchID] = submitBatch(
1968 env,
1969 tesSUCCESS,
1970 batch::outer(alice, seq, batchFee, tfOnlyOne),
1971 // terPRE_TICKET: ticket does not exist
1972 batch::Inner(trust(alice, usd(1000), tfSetfAuth), 0, seq + 1),
1973 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
1974 batch::Inner(pay(alice, bob, XRP(2)), seq + 3));
1975 env.close();
1976
1977 std::vector<TestLedgerData> const testCases = {
1978 {.index = 0,
1979 .txType = "Batch",
1980 .result = "tesSUCCESS",
1981 .txHash = batchID,
1982 .batchID = std::nullopt},
1983 {.index = 1,
1984 .txType = "Payment",
1985 .result = "tesSUCCESS",
1986 .txHash = txIDs[1],
1987 .batchID = batchID},
1988 };
1989 validateClosedLedger(env, testCases);
1990
1991 // Alice consumes sequences (# of txns)
1992 BEAST_EXPECT(env.seq(alice) == seq + 2);
1993
1994 // Alice pays XRP & Fee; Bob receives XRP
1995 BEAST_EXPECT(env.balance(alice) == preAlice - batchFee - XRP(1));
1996 BEAST_EXPECT(env.balance(bob) == preBob + XRP(1));
1997 }
1998
1999 // tec (tecKILLED) error
2000 {
2001 auto const preAlice = env.balance(alice);
2002 auto const preBob = env.balance(bob);
2003 auto const preCarol = env.balance(carol);
2004 auto const seq = env.seq(alice);
2005 auto const batchFee = batch::calcBatchFee(env, 0, 6);
2006
2007 auto const [txIDs, batchID] = submitBatch(
2008 env,
2009 tesSUCCESS,
2010 batch::outer(alice, seq, batchFee, tfOnlyOne),
2012 offer(alice, alice["USD"](100), XRP(100), tfImmediateOrCancel), seq + 1),
2014 offer(alice, alice["USD"](100), XRP(100), tfImmediateOrCancel), seq + 2),
2016 offer(alice, alice["USD"](100), XRP(100), tfImmediateOrCancel), seq + 3),
2017 batch::Inner(pay(alice, bob, XRP(100)), seq + 4),
2018 batch::Inner(pay(alice, carol, XRP(100)), seq + 5),
2019 batch::Inner(pay(alice, dave, XRP(100)), seq + 6));
2020 env.close();
2021
2022 std::vector<TestLedgerData> const testCases = {
2023 {.index = 0,
2024 .txType = "Batch",
2025 .result = "tesSUCCESS",
2026 .txHash = batchID,
2027 .batchID = std::nullopt},
2028 {.index = 1,
2029 .txType = "OfferCreate",
2030 .result = "tecKILLED",
2031 .txHash = txIDs[0],
2032 .batchID = batchID},
2033 {.index = 2,
2034 .txType = "OfferCreate",
2035 .result = "tecKILLED",
2036 .txHash = txIDs[1],
2037 .batchID = batchID},
2038 {.index = 3,
2039 .txType = "OfferCreate",
2040 .result = "tecKILLED",
2041 .txHash = txIDs[2],
2042 .batchID = batchID},
2043 {.index = 4,
2044 .txType = "Payment",
2045 .result = "tesSUCCESS",
2046 .txHash = txIDs[3],
2047 .batchID = batchID},
2048 };
2049 validateClosedLedger(env, testCases);
2050
2051 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(100) - batchFee);
2052 BEAST_EXPECT(env.balance(bob) == preBob + XRP(100));
2053 BEAST_EXPECT(env.balance(carol) == preCarol);
2054 }
2055 }
2056
2057 void
2059 {
2060 testcase("until failure");
2061
2062 using namespace test::jtx;
2063 using namespace std::literals;
2064
2065 Env env{*this, features};
2066
2067 auto const alice = Account("alice");
2068 auto const bob = Account("bob");
2069 auto const carol = Account("carol");
2070 auto const dave = Account("dave");
2071 auto const gw = Account("gw");
2072 auto const usd = gw["USD"];
2073 env.fund(XRP(10000), alice, bob, carol, dave, gw);
2074 env.close();
2075
2076 // first transaction fails
2077 {
2078 auto const preAlice = env.balance(alice);
2079 auto const preBob = env.balance(bob);
2080
2081 auto const batchFee = batch::calcBatchFee(env, 0, 4);
2082 auto const seq = env.seq(alice);
2083 auto const [txIDs, batchID] = submitBatch(
2084 env,
2085 tesSUCCESS,
2086 batch::outer(alice, seq, batchFee, tfUntilFailure),
2087 // tecUNFUNDED_PAYMENT: alice does not have enough XRP
2088 batch::Inner(pay(alice, bob, XRP(9999)), seq + 1),
2089 batch::Inner(pay(alice, bob, XRP(1)), seq + 2),
2090 batch::Inner(pay(alice, bob, XRP(2)), seq + 3),
2091 batch::Inner(pay(alice, bob, XRP(3)), seq + 4));
2092 env.close();
2093
2094 std::vector<TestLedgerData> const testCases = {
2095 {.index = 0,
2096 .txType = "Batch",
2097 .result = "tesSUCCESS",
2098 .txHash = batchID,
2099 .batchID = std::nullopt},
2100 {.index = 1,
2101 .txType = "Payment",
2102 .result = "tecUNFUNDED_PAYMENT",
2103 .txHash = txIDs[0],
2104 .batchID = batchID},
2105 };
2106 validateClosedLedger(env, testCases);
2107
2108 // Alice consumes sequences (# of txns)
2109 BEAST_EXPECT(env.seq(alice) == seq + 2);
2110
2111 // Alice pays XRP & Fee; Bob receives XRP
2112 BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
2113 BEAST_EXPECT(env.balance(bob) == preBob);
2114 }
2115
2116 // all transactions succeed
2117 {
2118 auto const preAlice = env.balance(alice);
2119 auto const preBob = env.balance(bob);
2120
2121 auto const batchFee = batch::calcBatchFee(env, 0, 4);
2122 auto const seq = env.seq(alice);
2123 auto const [txIDs, batchID] = submitBatch(
2124 env,
2125 tesSUCCESS,
2126 batch::outer(alice, seq, batchFee, tfUntilFailure),
2127 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
2128 batch::Inner(pay(alice, bob, XRP(2)), seq + 2),
2129 batch::Inner(pay(alice, bob, XRP(3)), seq + 3),
2130 batch::Inner(pay(alice, bob, XRP(4)), seq + 4));
2131 env.close();
2132
2133 std::vector<TestLedgerData> const testCases = {
2134 {.index = 0,
2135 .txType = "Batch",
2136 .result = "tesSUCCESS",
2137 .txHash = batchID,
2138 .batchID = std::nullopt},
2139 {.index = 1,
2140 .txType = "Payment",
2141 .result = "tesSUCCESS",
2142 .txHash = txIDs[0],
2143 .batchID = batchID},
2144 {.index = 2,
2145 .txType = "Payment",
2146 .result = "tesSUCCESS",
2147 .txHash = txIDs[1],
2148 .batchID = batchID},
2149 {.index = 3,
2150 .txType = "Payment",
2151 .result = "tesSUCCESS",
2152 .txHash = txIDs[2],
2153 .batchID = batchID},
2154 {.index = 4,
2155 .txType = "Payment",
2156 .result = "tesSUCCESS",
2157 .txHash = txIDs[3],
2158 .batchID = batchID},
2159 };
2160 validateClosedLedger(env, testCases);
2161
2162 // Alice consumes sequences (# of txns)
2163 BEAST_EXPECT(env.seq(alice) == seq + 5);
2164
2165 // Alice pays XRP & Fee; Bob receives XRP
2166 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(10) - batchFee);
2167 BEAST_EXPECT(env.balance(bob) == preBob + XRP(10));
2168 }
2169
2170 // tec error
2171 {
2172 auto const preAlice = env.balance(alice);
2173 auto const preBob = env.balance(bob);
2174
2175 auto const batchFee = batch::calcBatchFee(env, 0, 4);
2176 auto const seq = env.seq(alice);
2177 auto const [txIDs, batchID] = submitBatch(
2178 env,
2179 tesSUCCESS,
2180 batch::outer(alice, seq, batchFee, tfUntilFailure),
2181 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
2182 batch::Inner(pay(alice, bob, XRP(2)), seq + 2),
2183 // tecUNFUNDED_PAYMENT: alice does not have enough XRP
2184 batch::Inner(pay(alice, bob, XRP(9999)), seq + 3),
2185 batch::Inner(pay(alice, bob, XRP(3)), seq + 4));
2186 env.close();
2187
2188 std::vector<TestLedgerData> const testCases = {
2189 {.index = 0,
2190 .txType = "Batch",
2191 .result = "tesSUCCESS",
2192 .txHash = batchID,
2193 .batchID = std::nullopt},
2194 {.index = 1,
2195 .txType = "Payment",
2196 .result = "tesSUCCESS",
2197 .txHash = txIDs[0],
2198 .batchID = batchID},
2199 {.index = 2,
2200 .txType = "Payment",
2201 .result = "tesSUCCESS",
2202 .txHash = txIDs[1],
2203 .batchID = batchID},
2204 {.index = 3,
2205 .txType = "Payment",
2206 .result = "tecUNFUNDED_PAYMENT",
2207 .txHash = txIDs[2],
2208 .batchID = batchID},
2209 };
2210 validateClosedLedger(env, testCases);
2211
2212 // Alice consumes sequences (# of txns)
2213 BEAST_EXPECT(env.seq(alice) == seq + 4);
2214
2215 // Alice pays XRP & Fee; Bob receives XRP
2216 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee);
2217 BEAST_EXPECT(env.balance(bob) == preBob + XRP(3));
2218 }
2219
2220 // tef error
2221 {
2222 auto const preAlice = env.balance(alice);
2223 auto const preBob = env.balance(bob);
2224
2225 auto const batchFee = batch::calcBatchFee(env, 0, 4);
2226 auto const seq = env.seq(alice);
2227 auto const [txIDs, batchID] = submitBatch(
2228 env,
2229 tesSUCCESS,
2230 batch::outer(alice, seq, batchFee, tfUntilFailure),
2231 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
2232 batch::Inner(pay(alice, bob, XRP(2)), seq + 2),
2233 // tefNO_AUTH_REQUIRED: trustline auth is not required
2234 batch::Inner(trust(alice, usd(1000), tfSetfAuth), seq + 3),
2235 batch::Inner(pay(alice, bob, XRP(3)), seq + 4));
2236 env.close();
2237
2238 std::vector<TestLedgerData> const testCases = {
2239 {.index = 0,
2240 .txType = "Batch",
2241 .result = "tesSUCCESS",
2242 .txHash = batchID,
2243 .batchID = std::nullopt},
2244 {.index = 1,
2245 .txType = "Payment",
2246 .result = "tesSUCCESS",
2247 .txHash = txIDs[0],
2248 .batchID = batchID},
2249 {.index = 2,
2250 .txType = "Payment",
2251 .result = "tesSUCCESS",
2252 .txHash = txIDs[1],
2253 .batchID = batchID},
2254 };
2255 validateClosedLedger(env, testCases);
2256
2257 // Alice consumes sequences (# of txns)
2258 BEAST_EXPECT(env.seq(alice) == seq + 3);
2259
2260 // Alice pays XRP & Fee; Bob receives XRP
2261 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee);
2262 BEAST_EXPECT(env.balance(bob) == preBob + XRP(3));
2263 }
2264
2265 // ter error
2266 {
2267 auto const preAlice = env.balance(alice);
2268 auto const preBob = env.balance(bob);
2269
2270 auto const batchFee = batch::calcBatchFee(env, 0, 4);
2271 auto const seq = env.seq(alice);
2272 auto const [txIDs, batchID] = submitBatch(
2273 env,
2274 tesSUCCESS,
2275 batch::outer(alice, seq, batchFee, tfUntilFailure),
2276 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
2277 batch::Inner(pay(alice, bob, XRP(2)), seq + 2),
2278 // terPRE_TICKET: ticket does not exist
2279 batch::Inner(trust(alice, usd(1000), tfSetfAuth), 0, seq + 3),
2280 batch::Inner(pay(alice, bob, XRP(3)), seq + 4));
2281 env.close();
2282
2283 std::vector<TestLedgerData> const testCases = {
2284 {.index = 0,
2285 .txType = "Batch",
2286 .result = "tesSUCCESS",
2287 .txHash = batchID,
2288 .batchID = std::nullopt},
2289 {.index = 1,
2290 .txType = "Payment",
2291 .result = "tesSUCCESS",
2292 .txHash = txIDs[0],
2293 .batchID = batchID},
2294 {.index = 2,
2295 .txType = "Payment",
2296 .result = "tesSUCCESS",
2297 .txHash = txIDs[1],
2298 .batchID = batchID},
2299 };
2300 validateClosedLedger(env, testCases);
2301
2302 // Alice consumes sequences (# of txns)
2303 BEAST_EXPECT(env.seq(alice) == seq + 3);
2304
2305 // Alice pays XRP & Fee; Bob receives XRP
2306 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee);
2307 BEAST_EXPECT(env.balance(bob) == preBob + XRP(3));
2308 }
2309
2310 // tec (tecKILLED) error
2311 {
2312 auto const preAlice = env.balance(alice);
2313 auto const preBob = env.balance(bob);
2314 auto const preCarol = env.balance(carol);
2315 auto const seq = env.seq(alice);
2316 auto const batchFee = batch::calcBatchFee(env, 0, 4);
2317 auto const [txIDs, batchID] = submitBatch(
2318 env,
2319 tesSUCCESS,
2320 batch::outer(alice, seq, batchFee, tfUntilFailure),
2321 batch::Inner(pay(alice, bob, XRP(100)), seq + 1),
2322 batch::Inner(pay(alice, carol, XRP(100)), seq + 2),
2324 offer(alice, alice["USD"](100), XRP(100), tfImmediateOrCancel), seq + 3),
2325 batch::Inner(pay(alice, dave, XRP(100)), seq + 4));
2326 env.close();
2327
2328 std::vector<TestLedgerData> const testCases = {
2329 {.index = 0,
2330 .txType = "Batch",
2331 .result = "tesSUCCESS",
2332 .txHash = batchID,
2333 .batchID = std::nullopt},
2334 {.index = 1,
2335 .txType = "Payment",
2336 .result = "tesSUCCESS",
2337 .txHash = txIDs[0],
2338 .batchID = batchID},
2339 {.index = 2,
2340 .txType = "Payment",
2341 .result = "tesSUCCESS",
2342 .txHash = txIDs[1],
2343 .batchID = batchID},
2344 {.index = 3,
2345 .txType = "OfferCreate",
2346 .result = "tecKILLED",
2347 .txHash = txIDs[2],
2348 .batchID = batchID},
2349 };
2350 validateClosedLedger(env, testCases);
2351
2352 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(200) - batchFee);
2353 BEAST_EXPECT(env.balance(bob) == preBob + XRP(100));
2354 BEAST_EXPECT(env.balance(carol) == preCarol + XRP(100));
2355 }
2356 }
2357
2358 void
2360 {
2361 testcase("independent");
2362
2363 using namespace test::jtx;
2364 using namespace std::literals;
2365
2366 Env env{*this, features};
2367
2368 auto const alice = Account("alice");
2369 auto const bob = Account("bob");
2370 auto const carol = Account("carol");
2371 auto const gw = Account("gw");
2372 auto const usd = gw["USD"];
2373 env.fund(XRP(10000), alice, bob, carol, gw);
2374 env.close();
2375
2376 // multiple transactions fail
2377 {
2378 auto const preAlice = env.balance(alice);
2379 auto const preBob = env.balance(bob);
2380
2381 auto const batchFee = batch::calcBatchFee(env, 0, 4);
2382 auto const seq = env.seq(alice);
2383 auto const [txIDs, batchID] = submitBatch(
2384 env,
2385 tesSUCCESS,
2386 batch::outer(alice, seq, batchFee, tfIndependent),
2387 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
2388 // tecUNFUNDED_PAYMENT: alice does not have enough XRP
2389 batch::Inner(pay(alice, bob, XRP(9999)), seq + 2),
2390 // tecUNFUNDED_PAYMENT: alice does not have enough XRP
2391 batch::Inner(pay(alice, bob, XRP(9999)), seq + 3),
2392 batch::Inner(pay(alice, bob, XRP(3)), seq + 4));
2393 env.close();
2394
2395 std::vector<TestLedgerData> const testCases = {
2396 {.index = 0,
2397 .txType = "Batch",
2398 .result = "tesSUCCESS",
2399 .txHash = batchID,
2400 .batchID = std::nullopt},
2401 {.index = 1,
2402 .txType = "Payment",
2403 .result = "tesSUCCESS",
2404 .txHash = txIDs[0],
2405 .batchID = batchID},
2406 {.index = 2,
2407 .txType = "Payment",
2408 .result = "tecUNFUNDED_PAYMENT",
2409 .txHash = txIDs[1],
2410 .batchID = batchID},
2411 {.index = 3,
2412 .txType = "Payment",
2413 .result = "tecUNFUNDED_PAYMENT",
2414 .txHash = txIDs[2],
2415 .batchID = batchID},
2416 {.index = 4,
2417 .txType = "Payment",
2418 .result = "tesSUCCESS",
2419 .txHash = txIDs[3],
2420 .batchID = batchID},
2421 };
2422 validateClosedLedger(env, testCases);
2423
2424 // Alice consumes sequences (# of txns)
2425 BEAST_EXPECT(env.seq(alice) == seq + 5);
2426
2427 // Alice pays XRP & Fee; Bob receives XRP
2428 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(4) - batchFee);
2429 BEAST_EXPECT(env.balance(bob) == preBob + XRP(4));
2430 }
2431
2432 // tec error
2433 {
2434 auto const preAlice = env.balance(alice);
2435 auto const preBob = env.balance(bob);
2436
2437 auto const batchFee = batch::calcBatchFee(env, 0, 4);
2438 auto const seq = env.seq(alice);
2439 auto const [txIDs, batchID] = submitBatch(
2440 env,
2441 tesSUCCESS,
2442 batch::outer(alice, seq, batchFee, tfIndependent),
2443 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
2444 batch::Inner(pay(alice, bob, XRP(2)), seq + 2),
2445 // tecUNFUNDED_PAYMENT: alice does not have enough XRP
2446 batch::Inner(pay(alice, bob, XRP(9999)), seq + 3),
2447 batch::Inner(pay(alice, bob, XRP(3)), seq + 4));
2448 env.close();
2449
2450 std::vector<TestLedgerData> const testCases = {
2451 {.index = 0,
2452 .txType = "Batch",
2453 .result = "tesSUCCESS",
2454 .txHash = batchID,
2455 .batchID = std::nullopt},
2456 {.index = 1,
2457 .txType = "Payment",
2458 .result = "tesSUCCESS",
2459 .txHash = txIDs[0],
2460 .batchID = batchID},
2461 {.index = 2,
2462 .txType = "Payment",
2463 .result = "tesSUCCESS",
2464 .txHash = txIDs[1],
2465 .batchID = batchID},
2466 {.index = 3,
2467 .txType = "Payment",
2468 .result = "tecUNFUNDED_PAYMENT",
2469 .txHash = txIDs[2],
2470 .batchID = batchID},
2471 {.index = 4,
2472 .txType = "Payment",
2473 .result = "tesSUCCESS",
2474 .txHash = txIDs[3],
2475 .batchID = batchID},
2476 };
2477 validateClosedLedger(env, testCases);
2478
2479 // Alice consumes sequences (# of txns)
2480 BEAST_EXPECT(env.seq(alice) == seq + 5);
2481
2482 // Alice pays XRP & Fee; Bob receives XRP
2483 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(6) - batchFee);
2484 BEAST_EXPECT(env.balance(bob) == preBob + XRP(6));
2485 }
2486
2487 // tef error
2488 {
2489 auto const preAlice = env.balance(alice);
2490 auto const preBob = env.balance(bob);
2491
2492 auto const batchFee = batch::calcBatchFee(env, 0, 4);
2493 auto const seq = env.seq(alice);
2494 auto const [txIDs, batchID] = submitBatch(
2495 env,
2496 tesSUCCESS,
2497 batch::outer(alice, seq, batchFee, tfIndependent),
2498 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
2499 batch::Inner(pay(alice, bob, XRP(2)), seq + 2),
2500 // tefNO_AUTH_REQUIRED: trustline auth is not required
2501 batch::Inner(trust(alice, usd(1000), tfSetfAuth), seq + 3),
2502 batch::Inner(pay(alice, bob, XRP(3)), seq + 3));
2503 env.close();
2504
2505 std::vector<TestLedgerData> const testCases = {
2506 {.index = 0,
2507 .txType = "Batch",
2508 .result = "tesSUCCESS",
2509 .txHash = batchID,
2510 .batchID = std::nullopt},
2511 {.index = 1,
2512 .txType = "Payment",
2513 .result = "tesSUCCESS",
2514 .txHash = txIDs[0],
2515 .batchID = batchID},
2516 {.index = 2,
2517 .txType = "Payment",
2518 .result = "tesSUCCESS",
2519 .txHash = txIDs[1],
2520 .batchID = batchID},
2521 {.index = 3,
2522 .txType = "Payment",
2523 .result = "tesSUCCESS",
2524 .txHash = txIDs[3],
2525 .batchID = batchID},
2526 };
2527 validateClosedLedger(env, testCases);
2528
2529 // Alice consumes sequences (# of txns)
2530 BEAST_EXPECT(env.seq(alice) == seq + 4);
2531
2532 // Alice pays XRP & Fee; Bob receives XRP
2533 BEAST_EXPECT(env.balance(alice) == preAlice - batchFee - XRP(6));
2534 BEAST_EXPECT(env.balance(bob) == preBob + XRP(6));
2535 }
2536
2537 // ter error
2538 {
2539 auto const preAlice = env.balance(alice);
2540 auto const preBob = env.balance(bob);
2541
2542 auto const batchFee = batch::calcBatchFee(env, 0, 4);
2543 auto const seq = env.seq(alice);
2544 auto const [txIDs, batchID] = submitBatch(
2545 env,
2546 tesSUCCESS,
2547 batch::outer(alice, seq, batchFee, tfIndependent),
2548 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
2549 batch::Inner(pay(alice, bob, XRP(2)), seq + 2),
2550 // terPRE_TICKET: ticket does not exist
2551 batch::Inner(trust(alice, usd(1000), tfSetfAuth), 0, seq + 3),
2552 batch::Inner(pay(alice, bob, XRP(3)), seq + 3));
2553 env.close();
2554
2555 std::vector<TestLedgerData> const testCases = {
2556 {.index = 0,
2557 .txType = "Batch",
2558 .result = "tesSUCCESS",
2559 .txHash = batchID,
2560 .batchID = std::nullopt},
2561 {.index = 1,
2562 .txType = "Payment",
2563 .result = "tesSUCCESS",
2564 .txHash = txIDs[0],
2565 .batchID = batchID},
2566 {.index = 2,
2567 .txType = "Payment",
2568 .result = "tesSUCCESS",
2569 .txHash = txIDs[1],
2570 .batchID = batchID},
2571 {.index = 3,
2572 .txType = "Payment",
2573 .result = "tesSUCCESS",
2574 .txHash = txIDs[3],
2575 .batchID = batchID},
2576 };
2577 validateClosedLedger(env, testCases);
2578
2579 // Alice consumes sequences (# of txns)
2580 BEAST_EXPECT(env.seq(alice) == seq + 4);
2581
2582 // Alice pays XRP & Fee; Bob receives XRP
2583 BEAST_EXPECT(env.balance(alice) == preAlice - batchFee - XRP(6));
2584 BEAST_EXPECT(env.balance(bob) == preBob + XRP(6));
2585 }
2586
2587 // tec (tecKILLED) error
2588 {
2589 auto const preAlice = env.balance(alice);
2590 auto const preBob = env.balance(bob);
2591 auto const preCarol = env.balance(carol);
2592 auto const seq = env.seq(alice);
2593 auto const batchFee = batch::calcBatchFee(env, 0, 3);
2594 auto const [txIDs, batchID] = submitBatch(
2595 env,
2596 tesSUCCESS,
2597 batch::outer(alice, seq, batchFee, tfIndependent),
2598 batch::Inner(pay(alice, bob, XRP(100)), seq + 1),
2599 batch::Inner(pay(alice, carol, XRP(100)), seq + 2),
2601 offer(alice, alice["USD"](100), XRP(100), tfImmediateOrCancel), seq + 3));
2602 env.close();
2603
2604 std::vector<TestLedgerData> const testCases = {
2605 {.index = 0,
2606 .txType = "Batch",
2607 .result = "tesSUCCESS",
2608 .txHash = batchID,
2609 .batchID = std::nullopt},
2610 {.index = 1,
2611 .txType = "Payment",
2612 .result = "tesSUCCESS",
2613 .txHash = txIDs[0],
2614 .batchID = batchID},
2615 {.index = 2,
2616 .txType = "Payment",
2617 .result = "tesSUCCESS",
2618 .txHash = txIDs[1],
2619 .batchID = batchID},
2620 {.index = 3,
2621 .txType = "OfferCreate",
2622 .result = "tecKILLED",
2623 .txHash = txIDs[2],
2624 .batchID = batchID},
2625 };
2626 validateClosedLedger(env, testCases);
2627
2628 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(200) - batchFee);
2629 BEAST_EXPECT(env.balance(bob) == preBob + XRP(100));
2630 BEAST_EXPECT(env.balance(carol) == preCarol + XRP(100));
2631 }
2632 }
2633
2634 void
2635 doTestInnerSubmitRPC(FeatureBitset features, bool withBatch)
2636 {
2637 std::string const testName =
2638 std::string("inner submit rpc: batch ") + (withBatch ? "enabled" : "disabled") + ": ";
2639
2640 auto const amend = withBatch ? features : features - featureBatchV1_1;
2641
2642 using namespace test::jtx;
2643 using namespace std::literals;
2644
2645 Env env{*this, amend};
2646 if (!BEAST_EXPECT(amend[featureBatchV1_1] == withBatch))
2647 return;
2648
2649 auto const alice = Account("alice");
2650 auto const bob = Account("bob");
2651
2652 env.fund(XRP(10000), alice, bob);
2653 env.close();
2654
2655 // Any transaction carrying tfInnerBatchTxn is rejected in checkValidity()
2656 // before it reaches the tx engine, regardless of its signing fields or
2657 // whether the amendment is enabled.
2658 auto submitAndValidate = [&](std::string caseName, Slice const& slice, int line) {
2659 testcase << testName << caseName;
2660 auto const jrr = env.rpc("submit", strHex(slice))[jss::result];
2661 expect(
2662 jrr[jss::status] == "error" && jrr[jss::error] == "invalidTransaction" &&
2663 jrr[jss::error_exception] ==
2664 "fails local checks: Batch inner transactions are never "
2665 "considered validly signed.",
2666 pretty(jrr),
2667 __FILE__,
2668 line);
2669 env.close();
2670 };
2671
2672 // Invalid RPC Submission: TxnSignature
2673 // + has `TxnSignature` field
2674 // - has no `SigningPubKey` field
2675 // - has no `Signers` field
2676 // + has `tfInnerBatchTxn` flag
2677 {
2678 auto txn = batch::Inner(pay(alice, bob, XRP(1)), env.seq(alice));
2679 txn[sfTxnSignature] = "DEADBEEF";
2680 STParsedJSONObject parsed("test", txn.getTxn());
2681 Serializer s;
2682 parsed.object->add(s); // NOLINT(bugprone-unchecked-optional-access)
2683 submitAndValidate("TxnSignature set", s.slice(), __LINE__);
2684 }
2685
2686 // Invalid RPC Submission: SigningPubKey
2687 // - has no `TxnSignature` field
2688 // + has `SigningPubKey` field
2689 // - has no `Signers` field
2690 // + has `tfInnerBatchTxn` flag
2691 {
2692 auto txn = batch::Inner(pay(alice, bob, XRP(1)), env.seq(alice));
2693 txn[sfSigningPubKey] = strHex(alice.pk());
2694 STParsedJSONObject parsed("test", txn.getTxn());
2695 Serializer s;
2696 parsed.object->add(s); // NOLINT(bugprone-unchecked-optional-access)
2697 submitAndValidate("SigningPubKey set", s.slice(), __LINE__);
2698 }
2699
2700 // Invalid RPC Submission: Signers
2701 // - has no `TxnSignature` field
2702 // + has empty `SigningPubKey` field
2703 // + has `Signers` field
2704 // + has `tfInnerBatchTxn` flag
2705 {
2706 auto txn = batch::Inner(pay(alice, bob, XRP(1)), env.seq(alice));
2707 txn[sfSigners] = json::ValueType::Array;
2708 STParsedJSONObject parsed("test", txn.getTxn());
2709 Serializer s;
2710 parsed.object->add(s); // NOLINT(bugprone-unchecked-optional-access)
2711 submitAndValidate("Signers set", s.slice(), __LINE__);
2712 }
2713
2714 {
2715 // Fully signed inner batch transaction
2716 auto const txn = batch::Inner(pay(alice, bob, XRP(1)), env.seq(alice));
2717 auto const jt = env.jt(txn.getTxn());
2718
2719 STParsedJSONObject parsed("test", jt.jv);
2720 Serializer s;
2721 parsed.object->add(s); // NOLINT(bugprone-unchecked-optional-access)
2722 submitAndValidate("Fully signed", s.slice(), __LINE__);
2723 }
2724
2725 // Invalid RPC Submission: tfInnerBatchTxn
2726 // - has no `TxnSignature` field
2727 // + has empty `SigningPubKey` field
2728 // - has no `Signers` field
2729 // + has `tfInnerBatchTxn` flag
2730 {
2731 auto txn = batch::Inner(pay(alice, bob, XRP(1)), env.seq(alice));
2732 STParsedJSONObject parsed("test", txn.getTxn());
2733 Serializer s;
2734 parsed.object->add(s); // NOLINT(bugprone-unchecked-optional-access)
2735 submitAndValidate("No signing fields set", s.slice(), __LINE__);
2736 }
2737
2738 // Invalid RPC Submission: tfInnerBatchTxn pseudo-transaction
2739 // - has no `TxnSignature` field
2740 // + has empty `SigningPubKey` field
2741 // - has no `Signers` field
2742 // + has `tfInnerBatchTxn` flag
2743 {
2744 STTx const amendTx(ttAMENDMENT, [seq = env.closed()->header().seq + 1](auto& obj) {
2745 obj.setAccountID(sfAccount, AccountID());
2746 obj.setFieldH256(sfAmendment, featureBatchV1_1);
2747 obj.setFieldU32(sfLedgerSequence, seq);
2748 obj.setFieldU32(sfFlags, tfInnerBatchTxn);
2749 });
2750 auto txn = batch::Inner(amendTx.getJson(JsonOptions::Values::None), env.seq(alice));
2751 STParsedJSONObject parsed("test", txn.getTxn());
2752 Serializer s;
2753 parsed.object->add(s); // NOLINT(bugprone-unchecked-optional-access)
2754 submitAndValidate("Pseudo-transaction", s.slice(), __LINE__);
2755 }
2756 }
2757
2758 void
2760 {
2761 for (bool const withBatch : {true, false})
2762 {
2763 doTestInnerSubmitRPC(features, withBatch);
2764 }
2765 }
2766
2767 void
2769 {
2770 testcase("account activation");
2771
2772 using namespace test::jtx;
2773 using namespace std::literals;
2774
2775 Env env{*this, features};
2776
2777 auto const alice = Account("alice");
2778 auto const bob = Account("bob");
2779 env.fund(XRP(10000), alice);
2780 env.close();
2781 env.memoize(bob);
2782
2783 auto const preAlice = env.balance(alice);
2784 auto const ledSeq = env.current()->seq();
2785 auto const seq = env.seq(alice);
2786 auto const batchFee = batch::calcBatchFee(env, 1, 2);
2787 auto const [txIDs, batchID] = submitBatch(
2788 env,
2789 tesSUCCESS,
2790 batch::outer(alice, seq, batchFee, tfAllOrNothing),
2791 batch::Inner(pay(alice, bob, XRP(1000)), seq + 1),
2792 batch::Inner(fset(bob, asfAllowTrustLineClawback), ledSeq),
2793 batch::Sig(bob));
2794 env.close();
2795
2796 std::vector<TestLedgerData> const testCases = {
2797 {.index = 0,
2798 .txType = "Batch",
2799 .result = "tesSUCCESS",
2800 .txHash = batchID,
2801 .batchID = std::nullopt},
2802 {.index = 1,
2803 .txType = "Payment",
2804 .result = "tesSUCCESS",
2805 .txHash = txIDs[0],
2806 .batchID = batchID},
2807 {.index = 2,
2808 .txType = "AccountSet",
2809 .result = "tesSUCCESS",
2810 .txHash = txIDs[1],
2811 .batchID = batchID},
2812 };
2813 validateClosedLedger(env, testCases);
2814
2815 // Alice consumes sequences (# of txns)
2816 BEAST_EXPECT(env.seq(alice) == seq + 2);
2817
2818 // Bob consumes sequences (# of txns)
2819 BEAST_EXPECT(env.seq(bob) == ledSeq + 1);
2820
2821 // Alice pays XRP & Fee; Bob receives XRP
2822 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1000) - batchFee);
2823 BEAST_EXPECT(env.balance(bob) == XRP(1000));
2824
2825 // An inner partial payment must never fund a new account. With
2826 // featureBatchV1_1 the partial-payment-to-create check runs on the
2827 // batch's closed view and rejects it (tefNO_DST_PARTIAL). A USD->XRP path is
2828 // set up so that, absent the check, the partial payment would deliver
2829 // XRP and create carol; the check must block exactly that. A
2830 // cross-currency shape (IOU SendMax, XRP Amount) also lets the inner
2831 // pass preflight and reach the preclaim check.
2832 {
2833 Env env{*this, features};
2834
2835 auto const alice = Account("alice");
2836 auto const gw = Account("gw");
2837 auto const carol = Account("carol"); // never funded
2838 auto const usd = gw["USD"];
2839 env.fund(XRP(10000), alice, gw);
2840 env.close();
2841 env.memoize(carol);
2842
2843 env(trust(alice, usd(100000)));
2844 env(pay(gw, alice, usd(10000)));
2845 env(offer(gw, usd(2000), XRP(2000))); // USD -> XRP liquidity
2846 env.close();
2847
2848 auto const seq = env.seq(alice);
2849 auto const batchFee = batch::calcBatchFee(env, 0, 2);
2850
2851 auto pp = pay(alice, carol, XRP(1000));
2852 pp[jss::SendMax] = usd(2000).value().getJson(JsonOptions::Values::None);
2853 pp[jss::Flags] = tfPartialPayment;
2854
2855 env(batch::outer(alice, seq, batchFee, tfIndependent),
2856 batch::Inner(pp, seq + 1),
2857 batch::Inner(noop(alice), seq + 2),
2858 Ter(tesSUCCESS));
2859 env.close();
2860
2861 // carol was not created despite the available path.
2862 BEAST_EXPECT(!env.le(carol));
2863 }
2864 }
2865
2866 void
2868 {
2869 testcase("check all signatures");
2870
2871 using namespace test::jtx;
2872 using namespace std::literals;
2873
2874 // Verifies that checkBatchSign validates all signers even when an
2875 // unfunded account (signed with its master key) appears first in the
2876 // sorted signer list. A funded account with an invalid signature must
2877 // still be rejected with tefBAD_AUTH.
2878
2879 Env env{*this, features};
2880
2881 auto const alice = Account("alice");
2882 // "aaa" sorts before other accounts alphabetically, ensuring the
2883 // unfunded account is checked first in the sorted signer list
2884 auto const unfunded = Account("aaa");
2885 auto const carol = Account("carol");
2886 env.fund(XRP(10000), alice, carol);
2887 env.close();
2888
2889 // Verify sort order: unfunded.id() < carol.id()
2890 BEAST_EXPECT(unfunded.id() < carol.id());
2891
2892 auto const seq = env.seq(alice);
2893 auto const ledSeq = env.current()->seq();
2894 auto const batchFee = batch::calcBatchFee(env, 2, 3);
2895
2896 // The batch includes:
2897 // 1. alice pays unfunded (to create unfunded's account)
2898 // 2. unfunded does a noop (signed by unfunded's master key - valid)
2899 // 3. carol pays alice (signed by alice's key - INVALID since alice is
2900 // not carol's regular key)
2901 //
2902 // checkBatchSign must validate all signers regardless of order.
2903 // This must fail with tefBAD_AUTH.
2904 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
2905 batch::Inner(pay(alice, unfunded, XRP(100)), seq + 1),
2906 batch::Inner(noop(unfunded), ledSeq),
2907 batch::Inner(pay(carol, alice, XRP(1000)), env.seq(carol)),
2908 batch::Sig(unfunded, Reg{carol, alice}),
2909 Ter(tefBAD_AUTH));
2910 env.close();
2911 }
2912
2913 void
2915 {
2916 testcase("account set");
2917
2918 using namespace test::jtx;
2919 using namespace std::literals;
2920
2921 Env env{*this, features};
2922
2923 auto const alice = Account("alice");
2924 auto const bob = Account("bob");
2925 env.fund(XRP(10000), alice, bob);
2926 env.close();
2927
2928 auto const preAlice = env.balance(alice);
2929 auto const preBob = env.balance(bob);
2930
2931 auto const seq = env.seq(alice);
2932 auto const batchFee = batch::calcBatchFee(env, 0, 2);
2933 auto tx1 = batch::Inner(noop(alice), seq + 1);
2934 std::string domain = "example.com";
2935 tx1[sfDomain] = strHex(domain);
2936 auto const [txIDs, batchID] = submitBatch(
2937 env,
2938 tesSUCCESS,
2939 batch::outer(alice, seq, batchFee, tfAllOrNothing),
2940 tx1,
2941 batch::Inner(pay(alice, bob, XRP(1)), seq + 2));
2942 env.close();
2943
2944 std::vector<TestLedgerData> const testCases = {
2945 {.index = 0,
2946 .txType = "Batch",
2947 .result = "tesSUCCESS",
2948 .txHash = batchID,
2949 .batchID = std::nullopt},
2950 {.index = 1,
2951 .txType = "AccountSet",
2952 .result = "tesSUCCESS",
2953 .txHash = txIDs[0],
2954 .batchID = batchID},
2955 {.index = 2,
2956 .txType = "Payment",
2957 .result = "tesSUCCESS",
2958 .txHash = txIDs[1],
2959 .batchID = batchID},
2960 };
2961 validateClosedLedger(env, testCases);
2962
2963 auto const sle = env.le(keylet::account(alice));
2964 BEAST_EXPECT(sle);
2965 BEAST_EXPECT(sle->getFieldVL(sfDomain) == Blob(domain.begin(), domain.end()));
2966
2967 // Alice consumes sequences (# of txns)
2968 BEAST_EXPECT(env.seq(alice) == seq + 3);
2969
2970 // Alice pays XRP & Fee; Bob receives XRP
2971 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1) - batchFee);
2972 BEAST_EXPECT(env.balance(bob) == preBob + XRP(1));
2973 }
2974
2975 void
2977 {
2978 testcase("account delete");
2979
2980 using namespace test::jtx;
2981 using namespace std::literals;
2982
2983 // tfIndependent: account delete success
2984 {
2985 Env env{*this, features};
2986
2987 auto const alice = Account("alice");
2988 auto const bob = Account("bob");
2989 env.fund(XRP(10000), alice, bob);
2990 env.close();
2991
2992 incLgrSeqForAccDel(env, alice);
2993 for (int i = 0; i < 5; ++i)
2994 env.close();
2995
2996 auto const preAlice = env.balance(alice);
2997 auto const preBob = env.balance(bob);
2998
2999 auto const seq = env.seq(alice);
3000 auto const batchFee = batch::calcBatchFee(env, 0, 2) + env.current()->fees().increment;
3001 auto const [txIDs, batchID] = submitBatch(
3002 env,
3003 tesSUCCESS,
3004 batch::outer(alice, seq, batchFee, tfIndependent),
3005 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
3006 batch::Inner(acctdelete(alice, bob), seq + 2),
3007 // terNO_ACCOUNT: alice does not exist
3008 batch::Inner(pay(alice, bob, XRP(2)), seq + 3));
3009 env.close();
3010
3011 std::vector<TestLedgerData> const testCases = {
3012 {.index = 0,
3013 .txType = "Batch",
3014 .result = "tesSUCCESS",
3015 .txHash = batchID,
3016 .batchID = std::nullopt},
3017 {.index = 1,
3018 .txType = "Payment",
3019 .result = "tesSUCCESS",
3020 .txHash = txIDs[0],
3021 .batchID = batchID},
3022 {.index = 2,
3023 .txType = "AccountDelete",
3024 .result = "tesSUCCESS",
3025 .txHash = txIDs[1],
3026 .batchID = batchID},
3027 };
3028 validateClosedLedger(env, testCases);
3029
3030 // Alice does not exist; Bob receives Alice's XRP
3031 BEAST_EXPECT(!env.le(keylet::account(alice)));
3032 BEAST_EXPECT(env.balance(bob) == preBob + (preAlice - batchFee));
3033 }
3034
3035 // tfIndependent: account delete fails
3036 {
3037 Env env{*this, features};
3038
3039 auto const alice = Account("alice");
3040 auto const bob = Account("bob");
3041 env.fund(XRP(10000), alice, bob);
3042 env.close();
3043
3044 incLgrSeqForAccDel(env, alice);
3045 for (int i = 0; i < 5; ++i)
3046 env.close();
3047
3048 auto const preAlice = env.balance(alice);
3049 auto const preBob = env.balance(bob);
3050
3051 env.trust(bob["USD"](1000), alice);
3052 env.close();
3053
3054 auto const seq = env.seq(alice);
3055 auto const batchFee = batch::calcBatchFee(env, 0, 2) + env.current()->fees().increment;
3056 auto const [txIDs, batchID] = submitBatch(
3057 env,
3058 tesSUCCESS,
3059 batch::outer(alice, seq, batchFee, tfIndependent),
3060 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
3061 // tecHAS_OBLIGATIONS: alice has obligations
3062 batch::Inner(acctdelete(alice, bob), seq + 2),
3063 batch::Inner(pay(alice, bob, XRP(2)), seq + 3));
3064 env.close();
3065
3066 std::vector<TestLedgerData> const testCases = {
3067 {.index = 0,
3068 .txType = "Batch",
3069 .result = "tesSUCCESS",
3070 .txHash = batchID,
3071 .batchID = std::nullopt},
3072 {.index = 1,
3073 .txType = "Payment",
3074 .result = "tesSUCCESS",
3075 .txHash = txIDs[0],
3076 .batchID = batchID},
3077 {.index = 2,
3078 .txType = "AccountDelete",
3079 .result = "tecHAS_OBLIGATIONS",
3080 .txHash = txIDs[1],
3081 .batchID = batchID},
3082 {.index = 3,
3083 .txType = "Payment",
3084 .result = "tesSUCCESS",
3085 .txHash = txIDs[2],
3086 .batchID = batchID},
3087 };
3088 validateClosedLedger(env, testCases);
3089
3090 // Alice does not exist; Bob receives XRP
3091 BEAST_EXPECT(env.le(keylet::account(alice)));
3092 BEAST_EXPECT(env.balance(bob) == preBob + XRP(3));
3093 }
3094
3095 // tfAllOrNothing: account delete fails
3096 {
3097 Env env{*this, features};
3098
3099 auto const alice = Account("alice");
3100 auto const bob = Account("bob");
3101 env.fund(XRP(10000), alice, bob);
3102 env.close();
3103
3104 incLgrSeqForAccDel(env, alice);
3105 for (int i = 0; i < 5; ++i)
3106 env.close();
3107
3108 auto const preAlice = env.balance(alice);
3109 auto const preBob = env.balance(bob);
3110
3111 auto const seq = env.seq(alice);
3112 auto const batchFee = batch::calcBatchFee(env, 0, 2) + env.current()->fees().increment;
3113 auto const [txIDs, batchID] = submitBatch(
3114 env,
3115 tesSUCCESS,
3116 batch::outer(alice, seq, batchFee, tfAllOrNothing),
3117 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
3118 batch::Inner(acctdelete(alice, bob), seq + 2),
3119 // terNO_ACCOUNT: alice does not exist
3120 batch::Inner(pay(alice, bob, XRP(2)), seq + 3));
3121 env.close();
3122
3123 std::vector<TestLedgerData> const testCases = {
3124 {.index = 0,
3125 .txType = "Batch",
3126 .result = "tesSUCCESS",
3127 .txHash = batchID,
3128 .batchID = std::nullopt},
3129 };
3130 validateClosedLedger(env, testCases);
3131
3132 // Alice still exists; Bob is unchanged
3133 BEAST_EXPECT(env.le(keylet::account(alice)));
3134 BEAST_EXPECT(env.balance(bob) == preBob);
3135 }
3136 }
3137
3138 void
3140 {
3141 testcase("loan");
3142
3143 bool const lendingBatchEnabled = !std::ranges::any_of(
3145 [](auto const& disabled) { return disabled == ttLOAN_BROKER_SET; });
3146
3147 using namespace test::jtx;
3148
3149 Env env{*this, features};
3150
3151 Account const issuer{"issuer"};
3152 // For simplicity, lender will be the sole actor for the vault &
3153 // brokers.
3154 Account const lender{"lender"};
3155 // Borrower only wants to borrow
3156 Account const borrower{"borrower"};
3157
3158 // Fund the accounts and trust lines with the same amount so that tests
3159 // can use the same values regardless of the asset.
3160 env.fund(XRP(100'000), issuer, noripple(lender, borrower));
3161 env.close();
3162
3163 // Just use an XRP asset
3164 PrettyAsset const asset{xrpIssue(), 1'000'000};
3165
3166 Vault const vault{env};
3167
3168 auto const deposit = asset(50'000);
3169 auto const debtMaximumValue = asset(25'000).value();
3170 auto const coverDepositValue = asset(1000).value();
3171
3172 auto [tx, vaultKeylet] = vault.create({.owner = lender, .asset = asset});
3173 env(tx);
3174 env.close();
3175 BEAST_EXPECT(env.le(vaultKeylet));
3176
3177 env(vault.deposit({.depositor = lender, .id = vaultKeylet.key, .amount = deposit}));
3178 env.close();
3179
3180 auto const brokerKeylet =
3181 keylet::loanBroker(lender.id(), SeqProxy::rawSequence(env.seq(lender)));
3182
3183 {
3184 using namespace loan_broker;
3185 env(set(lender, vaultKeylet.key),
3186 kManagementFeeRate(TenthBips16(100)),
3187 kDebtMaximum(debtMaximumValue),
3188 kCoverRateMinimum(TenthBips32(percentageToTenthBips(10))),
3189 kCoverRateLiquidation(TenthBips32(percentageToTenthBips(25))));
3190
3191 env(coverDeposit(lender, brokerKeylet.key, coverDepositValue));
3192
3193 env.close();
3194 }
3195
3196 {
3197 using namespace loan;
3198 using namespace std::chrono_literals;
3199
3200 auto const lenderSeq = env.seq(lender);
3201 auto const batchFee = batch::calcBatchFee(env, 0, 2);
3202
3203 auto const loanKeylet = keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(1));
3204 {
3205 auto const [txIDs, batchID] = submitBatch(
3206 env,
3207 lendingBatchEnabled ? temBAD_SIGNATURE : temINVALID_INNER_BATCH,
3208 batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing),
3210 env.json(
3211 set(lender, brokerKeylet.key, asset(1000).value()),
3212 // Not allowed to include the counterparty signature
3213 Sig(sfCounterpartySignature, borrower),
3214 Sig(kNone),
3215 Fee(kNone),
3216 Seq(kNone)),
3217 lenderSeq + 1),
3219 pay(lender, loanKeylet.key, STAmount{asset, asset(500).value()}),
3220 lenderSeq + 2));
3221 }
3222 {
3223 auto const [txIDs, batchID] = submitBatch(
3224 env,
3226 batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing),
3228 env.json(
3229 set(lender, brokerKeylet.key, asset(1000).value()),
3230 // Counterparty must be set
3231 Sig(kNone),
3232 Fee(kNone),
3233 Seq(kNone)),
3234 lenderSeq + 1),
3236 pay(lender, loanKeylet.key, STAmount{asset, asset(500).value()}),
3237 lenderSeq + 2));
3238 }
3239 {
3240 auto const [txIDs, batchID] = submitBatch(
3241 env,
3242 lendingBatchEnabled ? temBAD_SIGNER : temINVALID_INNER_BATCH,
3243 batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing),
3245 env.json(
3246 set(lender, brokerKeylet.key, asset(1000).value()),
3247 // Counterparty must sign the outer transaction
3248 kCounterparty(borrower.id()),
3249 Sig(kNone),
3250 Fee(kNone),
3251 Seq(kNone)),
3252 lenderSeq + 1),
3254 pay(lender, loanKeylet.key, STAmount{asset, asset(500).value()}),
3255 lenderSeq + 2));
3256 }
3257 {
3258 // LoanSet normally charges at least 2x base fee, but since the
3259 // signature check is done by the batch, it only charges the
3260 // base fee.
3261 auto const batchFee = batch::calcBatchFee(env, 1, 2);
3262 auto const [txIDs, batchID] = submitBatch(
3263 env,
3264 lendingBatchEnabled ? TER(tesSUCCESS) : TER(temINVALID_INNER_BATCH),
3265 batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing),
3267 env.json(
3268 set(lender, brokerKeylet.key, asset(1000).value()),
3269 kCounterparty(borrower.id()),
3270 Sig(kNone),
3271 Fee(kNone),
3272 Seq(kNone)),
3273 lenderSeq + 1),
3275 pay(
3276 // However, this inner transaction will fail,
3277 // because the lender is not allowed to draw the
3278 // transaction
3279 lender,
3280 loanKeylet.key,
3281 STAmount{asset, asset(500).value()}),
3282 lenderSeq + 2),
3283 batch::Sig(borrower));
3284 }
3285 env.close();
3286 BEAST_EXPECT(env.le(brokerKeylet));
3287 BEAST_EXPECT(!env.le(loanKeylet));
3288 {
3289 // LoanSet normally charges at least 2x base fee, but since the
3290 // signature check is done by the batch, it only charges the
3291 // base fee.
3292 auto const lenderSeq = env.seq(lender);
3293 auto const batchFee = batch::calcBatchFee(env, 1, 2);
3294 auto const [txIDs, batchID] = submitBatch(
3295 env,
3296 lendingBatchEnabled ? TER(tesSUCCESS) : TER(temINVALID_INNER_BATCH),
3297 batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing),
3299 env.json(
3300 set(lender, brokerKeylet.key, asset(1000).value()),
3301 kCounterparty(borrower.id()),
3302 Sig(kNone),
3303 Fee(kNone),
3304 Seq(kNone)),
3305 lenderSeq + 1),
3306 batch::Inner(manage(lender, loanKeylet.key, tfLoanImpair), lenderSeq + 2),
3307 batch::Sig(borrower));
3308 }
3309 env.close();
3310 BEAST_EXPECT(env.le(brokerKeylet));
3311 if (auto const sleLoan = env.le(loanKeylet);
3312 lendingBatchEnabled ? BEAST_EXPECT(sleLoan) : !BEAST_EXPECT(!sleLoan))
3313 {
3314 BEAST_EXPECT(sleLoan->isFlag(lsfLoanImpaired));
3315 }
3316 }
3317 }
3318
3319 void
3321 {
3322 testcase("object create w/ sequence");
3323
3324 using namespace test::jtx;
3325 using namespace std::literals;
3326
3327 Env env{*this, features};
3328
3329 auto const alice = Account("alice");
3330 auto const bob = Account("bob");
3331 auto const gw = Account("gw");
3332 auto const usd = gw["USD"];
3333
3334 env.fund(XRP(10000), alice, bob, gw);
3335 env.close();
3336
3337 env.trust(usd(1000), alice, bob);
3338 env(pay(gw, alice, usd(100)));
3339 env(pay(gw, bob, usd(100)));
3340 env.close();
3341
3342 // success
3343 {
3344 auto const aliceSeq = env.seq(alice);
3345 auto const bobSeq = env.seq(bob);
3346 auto const preAlice = env.balance(alice);
3347 auto const preBob = env.balance(bob);
3348 auto const preAliceUSD = env.balance(alice, usd.issue());
3349 auto const preBobUSD = env.balance(bob, usd.issue());
3350
3351 auto const batchFee = batch::calcBatchFee(env, 1, 2);
3352 uint256 const chkID{getCheckIndex(bob, env.seq(bob))};
3353 auto const [txIDs, batchID] = submitBatch(
3354 env,
3355 tesSUCCESS,
3356 batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
3357 batch::Inner(check::create(bob, alice, usd(10)), bobSeq),
3358 batch::Inner(check::cash(alice, chkID, usd(10)), aliceSeq + 1),
3359 batch::Sig(bob));
3360 env.close();
3361
3362 std::vector<TestLedgerData> const testCases = {
3363 {.index = 0,
3364 .txType = "Batch",
3365 .result = "tesSUCCESS",
3366 .txHash = batchID,
3367 .batchID = std::nullopt},
3368 {.index = 1,
3369 .txType = "CheckCreate",
3370 .result = "tesSUCCESS",
3371 .txHash = txIDs[0],
3372 .batchID = batchID},
3373 {.index = 2,
3374 .txType = "CheckCash",
3375 .result = "tesSUCCESS",
3376 .txHash = txIDs[1],
3377 .batchID = batchID},
3378 };
3379 validateClosedLedger(env, testCases);
3380
3381 // Alice consumes sequences (# of txns)
3382 BEAST_EXPECT(env.seq(alice) == aliceSeq + 2);
3383
3384 // Alice consumes sequences (# of txns)
3385 BEAST_EXPECT(env.seq(bob) == bobSeq + 1);
3386
3387 // Alice pays Fee; Bob XRP Unchanged
3388 BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
3389 BEAST_EXPECT(env.balance(bob) == preBob);
3390
3391 // Alice pays USD & Bob receives USD
3392 BEAST_EXPECT(env.balance(alice, usd.issue()) == preAliceUSD + usd(10));
3393 BEAST_EXPECT(env.balance(bob, usd.issue()) == preBobUSD - usd(10));
3394 }
3395
3396 // failure
3397 {
3398 env(fset(alice, asfRequireDest));
3399 env.close();
3400
3401 auto const aliceSeq = env.seq(alice);
3402 auto const bobSeq = env.seq(bob);
3403 auto const preAlice = env.balance(alice);
3404 auto const preBob = env.balance(bob);
3405 auto const preAliceUSD = env.balance(alice, usd.issue());
3406 auto const preBobUSD = env.balance(bob, usd.issue());
3407
3408 auto const batchFee = batch::calcBatchFee(env, 1, 2);
3409 uint256 const chkID{getCheckIndex(bob, env.seq(bob))};
3410 auto const [txIDs, batchID] = submitBatch(
3411 env,
3412 tesSUCCESS,
3413 batch::outer(alice, aliceSeq, batchFee, tfIndependent),
3414 // tecDST_TAG_NEEDED - alice has enabled asfRequireDest
3415 batch::Inner(check::create(bob, alice, usd(10)), bobSeq),
3416 batch::Inner(check::cash(alice, chkID, usd(10)), aliceSeq + 1),
3417 batch::Sig(bob));
3418 env.close();
3419
3420 std::vector<TestLedgerData> const testCases = {
3421 {.index = 0,
3422 .txType = "Batch",
3423 .result = "tesSUCCESS",
3424 .txHash = batchID,
3425 .batchID = std::nullopt},
3426 {.index = 1,
3427 .txType = "CheckCreate",
3428 .result = "tecDST_TAG_NEEDED",
3429 .txHash = txIDs[0],
3430 .batchID = batchID},
3431 {.index = 2,
3432 .txType = "CheckCash",
3433 .result = "tecNO_ENTRY",
3434 .txHash = txIDs[1],
3435 .batchID = batchID},
3436 };
3437 validateClosedLedger(env, testCases);
3438
3439 // Alice consumes sequences (# of txns)
3440 BEAST_EXPECT(env.seq(alice) == aliceSeq + 2);
3441
3442 // Bob consumes sequences (# of txns)
3443 BEAST_EXPECT(env.seq(bob) == bobSeq + 1);
3444
3445 // Alice pays Fee; Bob XRP Unchanged
3446 BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
3447 BEAST_EXPECT(env.balance(bob) == preBob);
3448
3449 // Alice pays USD & Bob receives USD
3450 BEAST_EXPECT(env.balance(alice, usd.issue()) == preAliceUSD);
3451 BEAST_EXPECT(env.balance(bob, usd.issue()) == preBobUSD);
3452 }
3453 }
3454
3455 void
3457 {
3458 testcase("object create w/ ticket");
3459
3460 using namespace test::jtx;
3461 using namespace std::literals;
3462
3463 Env env{*this, features};
3464
3465 auto const alice = Account("alice");
3466 auto const bob = Account("bob");
3467 auto const gw = Account("gw");
3468 auto const usd = gw["USD"];
3469
3470 env.fund(XRP(10000), alice, bob, gw);
3471 env.close();
3472
3473 env.trust(usd(1000), alice, bob);
3474 env(pay(gw, alice, usd(100)));
3475 env(pay(gw, bob, usd(100)));
3476 env.close();
3477
3478 auto const aliceSeq = env.seq(alice);
3479 auto const bobSeq = env.seq(bob);
3480 auto const preAlice = env.balance(alice);
3481 auto const preBob = env.balance(bob);
3482 auto const preAliceUSD = env.balance(alice, usd.issue());
3483 auto const preBobUSD = env.balance(bob, usd.issue());
3484
3485 auto const batchFee = batch::calcBatchFee(env, 1, 3);
3486 uint256 const chkID{getCheckIndex(bob, bobSeq + 1)};
3487 auto const [txIDs, batchID] = submitBatch(
3488 env,
3489 tesSUCCESS,
3490 batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
3491 batch::Inner(ticket::create(bob, 10), bobSeq),
3492 batch::Inner(check::create(bob, alice, usd(10)), 0, bobSeq + 1),
3493 batch::Inner(check::cash(alice, chkID, usd(10)), aliceSeq + 1),
3494 batch::Sig(bob));
3495 env.close();
3496
3497 std::vector<TestLedgerData> const testCases = {
3498 {.index = 0,
3499 .txType = "Batch",
3500 .result = "tesSUCCESS",
3501 .txHash = batchID,
3502 .batchID = std::nullopt},
3503 {.index = 1,
3504 .txType = "TicketCreate",
3505 .result = "tesSUCCESS",
3506 .txHash = txIDs[0],
3507 .batchID = batchID},
3508 {.index = 2,
3509 .txType = "CheckCreate",
3510 .result = "tesSUCCESS",
3511 .txHash = txIDs[1],
3512 .batchID = batchID},
3513 {.index = 3,
3514 .txType = "CheckCash",
3515 .result = "tesSUCCESS",
3516 .txHash = txIDs[2],
3517 .batchID = batchID},
3518 };
3519 validateClosedLedger(env, testCases);
3520
3521 BEAST_EXPECT(env.seq(alice) == aliceSeq + 2);
3522 BEAST_EXPECT(env.seq(bob) == bobSeq + 10 + 1);
3523 BEAST_EXPECT(env.balance(alice) == preAlice - batchFee);
3524 BEAST_EXPECT(env.balance(bob) == preBob);
3525 BEAST_EXPECT(env.balance(alice, usd.issue()) == preAliceUSD + usd(10));
3526 BEAST_EXPECT(env.balance(bob, usd.issue()) == preBobUSD - usd(10));
3527 }
3528
3529 void
3531 {
3532 testcase("object create w/ 3rd party");
3533
3534 using namespace test::jtx;
3535 using namespace std::literals;
3536
3537 Env env{*this, features};
3538
3539 auto const alice = Account("alice");
3540 auto const bob = Account("bob");
3541 auto const carol = Account("carol");
3542 auto const gw = Account("gw");
3543 auto const usd = gw["USD"];
3544
3545 env.fund(XRP(10000), alice, bob, carol, gw);
3546 env.close();
3547
3548 env.trust(usd(1000), alice, bob);
3549 env(pay(gw, alice, usd(100)));
3550 env(pay(gw, bob, usd(100)));
3551 env.close();
3552
3553 auto const aliceSeq = env.seq(alice);
3554 auto const bobSeq = env.seq(bob);
3555 auto const carolSeq = env.seq(carol);
3556 auto const preAlice = env.balance(alice);
3557 auto const preBob = env.balance(bob);
3558 auto const preCarol = env.balance(carol);
3559 auto const preAliceUSD = env.balance(alice, usd.issue());
3560 auto const preBobUSD = env.balance(bob, usd.issue());
3561
3562 auto const batchFee = batch::calcBatchFee(env, 2, 2);
3563 uint256 const chkID{getCheckIndex(bob, env.seq(bob))};
3564 auto const [txIDs, batchID] = submitBatch(
3565 env,
3566 tesSUCCESS,
3567 batch::outer(carol, carolSeq, batchFee, tfAllOrNothing),
3568 batch::Inner(check::create(bob, alice, usd(10)), bobSeq),
3569 batch::Inner(check::cash(alice, chkID, usd(10)), aliceSeq),
3570 batch::Sig(alice, bob));
3571 env.close();
3572
3573 std::vector<TestLedgerData> const testCases = {
3574 {.index = 0,
3575 .txType = "Batch",
3576 .result = "tesSUCCESS",
3577 .txHash = batchID,
3578 .batchID = std::nullopt},
3579 {.index = 1,
3580 .txType = "CheckCreate",
3581 .result = "tesSUCCESS",
3582 .txHash = txIDs[0],
3583 .batchID = batchID},
3584 {.index = 2,
3585 .txType = "CheckCash",
3586 .result = "tesSUCCESS",
3587 .txHash = txIDs[1],
3588 .batchID = batchID},
3589 };
3590 validateClosedLedger(env, testCases);
3591
3592 BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
3593 BEAST_EXPECT(env.seq(bob) == bobSeq + 1);
3594 BEAST_EXPECT(env.seq(carol) == carolSeq + 1);
3595 BEAST_EXPECT(env.balance(alice) == preAlice);
3596 BEAST_EXPECT(env.balance(bob) == preBob);
3597 BEAST_EXPECT(env.balance(carol) == preCarol - batchFee);
3598 BEAST_EXPECT(env.balance(alice, usd.issue()) == preAliceUSD + usd(10));
3599 BEAST_EXPECT(env.balance(bob, usd.issue()) == preBobUSD - usd(10));
3600 }
3601
3602 void
3604 {
3605 {
3606 testcase("tickets outer");
3607
3608 using namespace test::jtx;
3609 using namespace std::literals;
3610
3611 Env env{*this, features};
3612
3613 auto const alice = Account("alice");
3614 auto const bob = Account("bob");
3615
3616 env.fund(XRP(10000), alice, bob);
3617 env.close();
3618
3619 std::uint32_t const aliceTicketSeq{env.seq(alice) + 1};
3620 env(ticket::create(alice, 10));
3621 env.close();
3622
3623 auto const aliceSeq = env.seq(alice);
3624 auto const preAlice = env.balance(alice);
3625 auto const preBob = env.balance(bob);
3626
3627 auto const batchFee = batch::calcBatchFee(env, 0, 2);
3628 auto const [txIDs, batchID] = submitBatch(
3629 env,
3630 tesSUCCESS,
3631 batch::outer(alice, 0, batchFee, tfAllOrNothing),
3632 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq + 0),
3633 batch::Inner(pay(alice, bob, XRP(2)), aliceSeq + 1),
3634 ticket::Use(aliceTicketSeq));
3635 env.close();
3636
3637 std::vector<TestLedgerData> const testCases = {
3638 {.index = 0,
3639 .txType = "Batch",
3640 .result = "tesSUCCESS",
3641 .txHash = batchID,
3642 .batchID = std::nullopt},
3643 {.index = 1,
3644 .txType = "Payment",
3645 .result = "tesSUCCESS",
3646 .txHash = txIDs[0],
3647 .batchID = batchID},
3648 {.index = 2,
3649 .txType = "Payment",
3650 .result = "tesSUCCESS",
3651 .txHash = txIDs[1],
3652 .batchID = batchID},
3653 };
3654 validateClosedLedger(env, testCases);
3655
3656 auto const sle = env.le(keylet::account(alice));
3657 BEAST_EXPECT(sle);
3658 BEAST_EXPECT(sle->getFieldU32(sfOwnerCount) == 9);
3659 BEAST_EXPECT(sle->getFieldU32(sfTicketCount) == 9);
3660
3661 BEAST_EXPECT(env.seq(alice) == aliceSeq + 2);
3662 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee);
3663 BEAST_EXPECT(env.balance(bob) == preBob + XRP(3));
3664 }
3665
3666 {
3667 testcase("tickets inner");
3668
3669 using namespace test::jtx;
3670 using namespace std::literals;
3671
3672 Env env{*this, features};
3673
3674 auto const alice = Account("alice");
3675 auto const bob = Account("bob");
3676
3677 env.fund(XRP(10000), alice, bob);
3678 env.close();
3679
3680 std::uint32_t aliceTicketSeq{env.seq(alice) + 1};
3681 env(ticket::create(alice, 10));
3682 env.close();
3683
3684 auto const aliceSeq = env.seq(alice);
3685 auto const preAlice = env.balance(alice);
3686 auto const preBob = env.balance(bob);
3687
3688 auto const batchFee = batch::calcBatchFee(env, 0, 2);
3689 auto const [txIDs, batchID] = submitBatch(
3690 env,
3691 tesSUCCESS,
3692 batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
3693 batch::Inner(pay(alice, bob, XRP(1)), 0, aliceTicketSeq),
3694 batch::Inner(pay(alice, bob, XRP(2)), 0, aliceTicketSeq + 1));
3695 env.close();
3696
3697 std::vector<TestLedgerData> const testCases = {
3698 {.index = 0,
3699 .txType = "Batch",
3700 .result = "tesSUCCESS",
3701 .txHash = batchID,
3702 .batchID = std::nullopt},
3703 {.index = 1,
3704 .txType = "Payment",
3705 .result = "tesSUCCESS",
3706 .txHash = txIDs[0],
3707 .batchID = batchID},
3708 {.index = 2,
3709 .txType = "Payment",
3710 .result = "tesSUCCESS",
3711 .txHash = txIDs[1],
3712 .batchID = batchID},
3713 };
3714 validateClosedLedger(env, testCases);
3715
3716 auto const sle = env.le(keylet::account(alice));
3717 BEAST_EXPECT(sle);
3718 BEAST_EXPECT(sle->getFieldU32(sfOwnerCount) == 8);
3719 BEAST_EXPECT(sle->getFieldU32(sfTicketCount) == 8);
3720
3721 BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
3722 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee);
3723 BEAST_EXPECT(env.balance(bob) == preBob + XRP(3));
3724 }
3725
3726 {
3727 testcase("tickets outer inner");
3728
3729 using namespace test::jtx;
3730 using namespace std::literals;
3731
3732 Env env{*this, features};
3733
3734 auto const alice = Account("alice");
3735 auto const bob = Account("bob");
3736
3737 env.fund(XRP(10000), alice, bob);
3738 env.close();
3739
3740 std::uint32_t const aliceTicketSeq{env.seq(alice) + 1};
3741 env(ticket::create(alice, 10));
3742 env.close();
3743
3744 auto const aliceSeq = env.seq(alice);
3745 auto const preAlice = env.balance(alice);
3746 auto const preBob = env.balance(bob);
3747
3748 auto const batchFee = batch::calcBatchFee(env, 0, 2);
3749 auto const [txIDs, batchID] = submitBatch(
3750 env,
3751 tesSUCCESS,
3752 batch::outer(alice, 0, batchFee, tfAllOrNothing),
3753 batch::Inner(pay(alice, bob, XRP(1)), 0, aliceTicketSeq + 1),
3754 batch::Inner(pay(alice, bob, XRP(2)), aliceSeq),
3755 ticket::Use(aliceTicketSeq));
3756 env.close();
3757
3758 std::vector<TestLedgerData> const testCases = {
3759 {.index = 0,
3760 .txType = "Batch",
3761 .result = "tesSUCCESS",
3762 .txHash = batchID,
3763 .batchID = std::nullopt},
3764 {.index = 1,
3765 .txType = "Payment",
3766 .result = "tesSUCCESS",
3767 .txHash = txIDs[0],
3768 .batchID = batchID},
3769 {.index = 2,
3770 .txType = "Payment",
3771 .result = "tesSUCCESS",
3772 .txHash = txIDs[1],
3773 .batchID = batchID},
3774 };
3775 validateClosedLedger(env, testCases);
3776
3777 auto const sle = env.le(keylet::account(alice));
3778 BEAST_EXPECT(sle);
3779 BEAST_EXPECT(sle->getFieldU32(sfOwnerCount) == 8);
3780 BEAST_EXPECT(sle->getFieldU32(sfTicketCount) == 8);
3781
3782 BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
3783 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee);
3784 BEAST_EXPECT(env.balance(bob) == preBob + XRP(3));
3785 }
3786 }
3787
3788 void
3790 {
3791 testcase("sequence open ledger");
3792
3793 using namespace test::jtx;
3794 using namespace std::literals;
3795
3796 auto const alice = Account("alice");
3797 auto const bob = Account("bob");
3798 auto const carol = Account("carol");
3799
3800 // Before Batch Txn w/ retry following ledger
3801 {
3802 // IMPORTANT: The batch txn is applied first, then the noop txn.
3803 // Because of this ordering, the noop txn is not applied and is
3804 // overwritten by the payment in the batch transaction. Because the
3805 // terPRE_SEQ is outside of the batch this noop transaction will ge
3806 // reapplied in the following ledger
3807 Env env{*this, features};
3808 env.fund(XRP(10000), alice, bob, carol);
3809 env.close();
3810
3811 auto const aliceSeq = env.seq(alice);
3812 auto const carolSeq = env.seq(carol);
3813
3814 // AccountSet Txn
3815 auto const noopTxn = env.jt(noop(alice), Seq(aliceSeq + 2));
3816 auto const noopTxnID = to_string(noopTxn.stx->getTransactionID());
3817 env(noopTxn, Ter(terPRE_SEQ));
3818
3819 // Batch Txn
3820 auto const batchFee = batch::calcBatchFee(env, 1, 2);
3821 auto const [txIDs, batchID] = submitBatch(
3822 env,
3823 tesSUCCESS,
3824 batch::outer(carol, carolSeq, batchFee, tfAllOrNothing),
3825 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
3826 batch::Inner(pay(alice, bob, XRP(2)), aliceSeq + 1),
3827 batch::Sig(alice));
3828 env.close();
3829
3830 {
3831 std::vector<TestLedgerData> const testCases = {
3832 {.index = 0,
3833 .txType = "Batch",
3834 .result = "tesSUCCESS",
3835 .txHash = batchID,
3836 .batchID = std::nullopt},
3837 {.index = 1,
3838 .txType = "Payment",
3839 .result = "tesSUCCESS",
3840 .txHash = txIDs[0],
3841 .batchID = batchID},
3842 {.index = 2,
3843 .txType = "Payment",
3844 .result = "tesSUCCESS",
3845 .txHash = txIDs[1],
3846 .batchID = batchID},
3847 };
3848 validateClosedLedger(env, testCases);
3849 }
3850
3851 env.close();
3852 {
3853 // next ledger contains noop txn
3854 std::vector<TestLedgerData> const testCases = {
3855 {.index = 0,
3856 .txType = "AccountSet",
3857 .result = "tesSUCCESS",
3858 .txHash = noopTxnID,
3859 .batchID = std::nullopt},
3860 };
3861 validateClosedLedger(env, testCases);
3862 }
3863 }
3864
3865 // Before Batch Txn w/ same sequence
3866 {
3867 // IMPORTANT: The batch txn is applied first, then the noop txn.
3868 // Because of this ordering, the noop txn is not applied and is
3869 // overwritten by the payment in the batch transaction.
3870 Env env{*this, features};
3871 env.fund(XRP(10000), alice, bob);
3872 env.close();
3873
3874 auto const aliceSeq = env.seq(alice);
3875
3876 // AccountSet Txn
3877 auto const noopTxn = env.jt(noop(alice), Seq(aliceSeq + 1));
3878 env(noopTxn, Ter(terPRE_SEQ));
3879
3880 // Batch Txn
3881 auto const batchFee = batch::calcBatchFee(env, 0, 2);
3882 auto const [txIDs, batchID] = submitBatch(
3883 env,
3884 tesSUCCESS,
3885 batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
3886 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq + 1),
3887 batch::Inner(pay(alice, bob, XRP(2)), aliceSeq + 2));
3888 env.close();
3889
3890 {
3891 std::vector<TestLedgerData> const testCases = {
3892 {.index = 0,
3893 .txType = "Batch",
3894 .result = "tesSUCCESS",
3895 .txHash = batchID,
3896 .batchID = std::nullopt},
3897 {.index = 1,
3898 .txType = "Payment",
3899 .result = "tesSUCCESS",
3900 .txHash = txIDs[0],
3901 .batchID = batchID},
3902 {.index = 2,
3903 .txType = "Payment",
3904 .result = "tesSUCCESS",
3905 .txHash = txIDs[1],
3906 .batchID = batchID},
3907 };
3908 validateClosedLedger(env, testCases);
3909 }
3910
3911 env.close();
3912 {
3913 // next ledger is empty
3914 std::vector<TestLedgerData> const testCases = {};
3915 validateClosedLedger(env, testCases);
3916 }
3917 }
3918
3919 // After Batch Txn w/ same sequence
3920 {
3921 // IMPORTANT: The batch txn is applied first, then the noop txn.
3922 // Because of this ordering, the noop txn is not applied and is
3923 // overwritten by the payment in the batch transaction.
3924 Env env{*this, features};
3925 env.fund(XRP(10000), alice, bob);
3926 env.close();
3927
3928 auto const aliceSeq = env.seq(alice);
3929 auto const batchFee = batch::calcBatchFee(env, 0, 2);
3930 auto const [txIDs, batchID] = submitBatch(
3931 env,
3932 tesSUCCESS,
3933 batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
3934 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq + 1),
3935 batch::Inner(pay(alice, bob, XRP(2)), aliceSeq + 2));
3936
3937 auto const noopTxn = env.jt(noop(alice), Seq(aliceSeq + 1));
3938 env(noopTxn, Ter(tesSUCCESS));
3939 env.close();
3940
3941 {
3942 std::vector<TestLedgerData> const testCases = {
3943 {.index = 0,
3944 .txType = "Batch",
3945 .result = "tesSUCCESS",
3946 .txHash = batchID,
3947 .batchID = std::nullopt},
3948 {.index = 1,
3949 .txType = "Payment",
3950 .result = "tesSUCCESS",
3951 .txHash = txIDs[0],
3952 .batchID = batchID},
3953 {.index = 2,
3954 .txType = "Payment",
3955 .result = "tesSUCCESS",
3956 .txHash = txIDs[1],
3957 .batchID = batchID},
3958 };
3959 validateClosedLedger(env, testCases);
3960 }
3961
3962 env.close();
3963 {
3964 // next ledger is empty
3965 std::vector<TestLedgerData> const testCases = {};
3966 validateClosedLedger(env, testCases);
3967 }
3968 }
3969
3970 // Outer Batch terPRE_SEQ
3971 {
3972 Env env{*this, features};
3973 env.fund(XRP(10000), alice, bob, carol);
3974 env.close();
3975
3976 auto const aliceSeq = env.seq(alice);
3977 auto const carolSeq = env.seq(carol);
3978
3979 // Batch Txn
3980 auto const batchFee = batch::calcBatchFee(env, 1, 2);
3981 auto const [txIDs, batchID] = submitBatch(
3982 env,
3983 terPRE_SEQ,
3984 batch::outer(carol, carolSeq + 1, batchFee, tfAllOrNothing),
3985 batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
3986 batch::Inner(pay(alice, bob, XRP(2)), aliceSeq + 1),
3987 batch::Sig(alice));
3988
3989 // AccountSet Txn
3990 auto const noopTxn = env.jt(noop(carol), Seq(carolSeq));
3991 auto const noopTxnID = to_string(noopTxn.stx->getTransactionID());
3992 env(noopTxn, Ter(tesSUCCESS));
3993 env.close();
3994
3995 {
3996 std::vector<TestLedgerData> const testCases = {
3997 {.index = 0,
3998 .txType = "AccountSet",
3999 .result = "tesSUCCESS",
4000 .txHash = noopTxnID,
4001 .batchID = std::nullopt},
4002 {.index = 1,
4003 .txType = "Batch",
4004 .result = "tesSUCCESS",
4005 .txHash = batchID,
4006 .batchID = std::nullopt},
4007 {.index = 2,
4008 .txType = "Payment",
4009 .result = "tesSUCCESS",
4010 .txHash = txIDs[0],
4011 .batchID = batchID},
4012 {.index = 3,
4013 .txType = "Payment",
4014 .result = "tesSUCCESS",
4015 .txHash = txIDs[1],
4016 .batchID = batchID},
4017 };
4018 validateClosedLedger(env, testCases);
4019 }
4020
4021 env.close();
4022 {
4023 // next ledger contains no transactions
4024 std::vector<TestLedgerData> const testCases = {};
4025 validateClosedLedger(env, testCases);
4026 }
4027 }
4028 }
4029
4030 void
4032 {
4033 testcase("tickets open ledger");
4034
4035 using namespace test::jtx;
4036 using namespace std::literals;
4037
4038 auto const alice = Account("alice");
4039 auto const bob = Account("bob");
4040
4041 // Before Batch Txn w/ same ticket
4042 {
4043 // IMPORTANT: The batch txn is applied first, then the noop txn.
4044 // Because of this ordering, the noop txn is not applied and is
4045 // overwritten by the payment in the batch transaction.
4046 Env env{*this, features};
4047 env.fund(XRP(10000), alice, bob);
4048 env.close();
4049
4050 std::uint32_t const aliceTicketSeq{env.seq(alice) + 1};
4051 env(ticket::create(alice, 10));
4052 env.close();
4053
4054 auto const aliceSeq = env.seq(alice);
4055
4056 // AccountSet Txn
4057 auto const noopTxn = env.jt(noop(alice), ticket::Use(aliceTicketSeq + 1));
4058 env(noopTxn, Ter(tesSUCCESS));
4059
4060 // Batch Txn
4061 auto const batchFee = batch::calcBatchFee(env, 0, 2);
4062 auto const [txIDs, batchID] = submitBatch(
4063 env,
4064 tesSUCCESS,
4065 batch::outer(alice, 0, batchFee, tfAllOrNothing),
4066 batch::Inner(pay(alice, bob, XRP(1)), 0, aliceTicketSeq + 1),
4067 batch::Inner(pay(alice, bob, XRP(2)), aliceSeq),
4068 ticket::Use(aliceTicketSeq));
4069 env.close();
4070
4071 {
4072 std::vector<TestLedgerData> const testCases = {
4073 {.index = 0,
4074 .txType = "Batch",
4075 .result = "tesSUCCESS",
4076 .txHash = batchID,
4077 .batchID = std::nullopt},
4078 {.index = 1,
4079 .txType = "Payment",
4080 .result = "tesSUCCESS",
4081 .txHash = txIDs[0],
4082 .batchID = batchID},
4083 {.index = 2,
4084 .txType = "Payment",
4085 .result = "tesSUCCESS",
4086 .txHash = txIDs[1],
4087 .batchID = batchID},
4088 };
4089 validateClosedLedger(env, testCases);
4090 }
4091
4092 env.close();
4093 {
4094 // next ledger is empty
4095 std::vector<TestLedgerData> const testCases = {};
4096 validateClosedLedger(env, testCases);
4097 }
4098 }
4099
4100 // After Batch Txn w/ same ticket
4101 {
4102 // IMPORTANT: The batch txn is applied first, then the noop txn.
4103 // Because of this ordering, the noop txn is not applied and is
4104 // overwritten by the payment in the batch transaction.
4105 Env env{*this, features};
4106 env.fund(XRP(10000), alice, bob);
4107 env.close();
4108
4109 std::uint32_t const aliceTicketSeq{env.seq(alice) + 1};
4110 env(ticket::create(alice, 10));
4111 env.close();
4112
4113 auto const aliceSeq = env.seq(alice);
4114
4115 // Batch Txn
4116 auto const batchFee = batch::calcBatchFee(env, 0, 2);
4117 auto const [txIDs, batchID] = submitBatch(
4118 env,
4119 tesSUCCESS,
4120 batch::outer(alice, 0, batchFee, tfAllOrNothing),
4121 batch::Inner(pay(alice, bob, XRP(1)), 0, aliceTicketSeq + 1),
4122 batch::Inner(pay(alice, bob, XRP(2)), aliceSeq),
4123 ticket::Use(aliceTicketSeq));
4124
4125 // AccountSet Txn
4126 auto const noopTxn = env.jt(noop(alice), ticket::Use(aliceTicketSeq + 1));
4127 env(noopTxn);
4128
4129 env.close();
4130 {
4131 std::vector<TestLedgerData> const testCases = {
4132 {.index = 0,
4133 .txType = "Batch",
4134 .result = "tesSUCCESS",
4135 .txHash = batchID,
4136 .batchID = std::nullopt},
4137 {.index = 1,
4138 .txType = "Payment",
4139 .result = "tesSUCCESS",
4140 .txHash = txIDs[0],
4141 .batchID = batchID},
4142 {.index = 2,
4143 .txType = "Payment",
4144 .result = "tesSUCCESS",
4145 .txHash = txIDs[1],
4146 .batchID = batchID},
4147 };
4148 validateClosedLedger(env, testCases);
4149 }
4150
4151 env.close();
4152 {
4153 // next ledger is empty
4154 std::vector<TestLedgerData> const testCases = {};
4155 validateClosedLedger(env, testCases);
4156 }
4157 }
4158 }
4159
4160 void
4162 {
4163 testcase("objects open ledger");
4164
4165 using namespace test::jtx;
4166 using namespace std::literals;
4167
4168 auto const alice = Account("alice");
4169 auto const bob = Account("bob");
4170
4171 // Consume Object Before Batch Txn
4172 {
4173 // IMPORTANT: The initial result of `CheckCash` is tecNO_ENTRY
4174 // because the create transaction has not been applied because the
4175 // batch will run in the close ledger process. The batch will be
4176 // allied and then retry this transaction in the current ledger.
4177
4178 Env env{*this, features};
4179 env.fund(XRP(10000), alice, bob);
4180 env.close();
4181
4182 std::uint32_t const aliceTicketSeq{env.seq(alice) + 1};
4183 env(ticket::create(alice, 10));
4184 env.close();
4185
4186 auto const aliceSeq = env.seq(alice);
4187
4188 // CheckCash Txn
4189 uint256 const chkID{getCheckIndex(alice, aliceSeq)};
4190 auto const objTxn = env.jt(check::cash(bob, chkID, XRP(10)));
4191 auto const objTxnID = to_string(objTxn.stx->getTransactionID());
4192 env(objTxn, Ter(tecNO_ENTRY));
4193
4194 // Batch Txn
4195 auto const batchFee = batch::calcBatchFee(env, 0, 2);
4196 auto const [txIDs, batchID] = submitBatch(
4197 env,
4198 tesSUCCESS,
4199 batch::outer(alice, 0, batchFee, tfAllOrNothing),
4200 batch::Inner(check::create(alice, bob, XRP(10)), aliceSeq),
4201 batch::Inner(pay(alice, bob, XRP(1)), 0, aliceTicketSeq + 1),
4202 ticket::Use(aliceTicketSeq));
4203
4204 env.close();
4205 {
4206 std::vector<TestLedgerData> const testCases = {
4207 {.index = 0,
4208 .txType = "Batch",
4209 .result = "tesSUCCESS",
4210 .txHash = batchID,
4211 .batchID = std::nullopt},
4212 {.index = 1,
4213 .txType = "CheckCreate",
4214 .result = "tesSUCCESS",
4215 .txHash = txIDs[0],
4216 .batchID = batchID},
4217 {.index = 2,
4218 .txType = "Payment",
4219 .result = "tesSUCCESS",
4220 .txHash = txIDs[1],
4221 .batchID = batchID},
4222 {.index = 3,
4223 .txType = "CheckCash",
4224 .result = "tesSUCCESS",
4225 .txHash = objTxnID,
4226 .batchID = std::nullopt},
4227 };
4228 validateClosedLedger(env, testCases);
4229 }
4230
4231 env.close();
4232 {
4233 // next ledger is empty
4234 std::vector<TestLedgerData> const testCases = {};
4235 validateClosedLedger(env, testCases);
4236 }
4237 }
4238
4239 // Create Object Before Batch Txn
4240 {
4241 Env env{*this, features};
4242 env.fund(XRP(10000), alice, bob);
4243 env.close();
4244
4245 std::uint32_t const aliceTicketSeq{env.seq(alice) + 1};
4246 env(ticket::create(alice, 10));
4247 env.close();
4248
4249 auto const aliceSeq = env.seq(alice);
4250 auto const bobSeq = env.seq(bob);
4251
4252 // CheckCreate Txn
4253 uint256 const chkID{getCheckIndex(alice, aliceSeq)};
4254 auto const objTxn = env.jt(check::create(alice, bob, XRP(10)));
4255 auto const objTxnID = to_string(objTxn.stx->getTransactionID());
4256 env(objTxn, Ter(tesSUCCESS));
4257
4258 // Batch Txn
4259 auto const batchFee = batch::calcBatchFee(env, 1, 2);
4260 auto const [txIDs, batchID] = submitBatch(
4261 env,
4262 tesSUCCESS,
4263 batch::outer(alice, 0, batchFee, tfAllOrNothing),
4264 batch::Inner(check::cash(bob, chkID, XRP(10)), bobSeq),
4265 batch::Inner(pay(alice, bob, XRP(1)), 0, aliceTicketSeq + 1),
4266 ticket::Use(aliceTicketSeq),
4267 batch::Sig(bob));
4268
4269 env.close();
4270 {
4271 std::vector<TestLedgerData> const testCases = {
4272 {.index = 0,
4273 .txType = "CheckCreate",
4274 .result = "tesSUCCESS",
4275 .txHash = objTxnID,
4276 .batchID = std::nullopt},
4277 {.index = 1,
4278 .txType = "Batch",
4279 .result = "tesSUCCESS",
4280 .txHash = batchID,
4281 .batchID = std::nullopt},
4282 {.index = 2,
4283 .txType = "CheckCash",
4284 .result = "tesSUCCESS",
4285 .txHash = txIDs[0],
4286 .batchID = batchID},
4287 {.index = 3,
4288 .txType = "Payment",
4289 .result = "tesSUCCESS",
4290 .txHash = txIDs[1],
4291 .batchID = batchID},
4292 };
4293 validateClosedLedger(env, testCases);
4294 }
4295 }
4296
4297 // After Batch Txn
4298 {
4299 // IMPORTANT: The initial result of `CheckCash` is tecNO_ENTRY
4300 // because the create transaction has not been applied because the
4301 // batch will run in the close ledger process. The batch will be
4302 // applied and then retry this transaction in the current ledger.
4303
4304 Env env{*this, features};
4305 env.fund(XRP(10000), alice, bob);
4306 env.close();
4307
4308 std::uint32_t const aliceTicketSeq{env.seq(alice) + 1};
4309 env(ticket::create(alice, 10));
4310 env.close();
4311
4312 auto const aliceSeq = env.seq(alice);
4313
4314 // Batch Txn
4315 auto const batchFee = batch::calcBatchFee(env, 0, 2);
4316 uint256 const chkID{getCheckIndex(alice, aliceSeq)};
4317 auto const [txIDs, batchID] = submitBatch(
4318 env,
4319 tesSUCCESS,
4320 batch::outer(alice, 0, batchFee, tfAllOrNothing),
4321 batch::Inner(check::create(alice, bob, XRP(10)), aliceSeq),
4322 batch::Inner(pay(alice, bob, XRP(1)), 0, aliceTicketSeq + 1),
4323 ticket::Use(aliceTicketSeq));
4324
4325 // CheckCash Txn
4326 auto const objTxn = env.jt(check::cash(bob, chkID, XRP(10)));
4327 auto const objTxnID = to_string(objTxn.stx->getTransactionID());
4328 env(objTxn, Ter(tecNO_ENTRY));
4329
4330 env.close();
4331 {
4332 std::vector<TestLedgerData> const testCases = {
4333 {.index = 0,
4334 .txType = "Batch",
4335 .result = "tesSUCCESS",
4336 .txHash = batchID,
4337 .batchID = std::nullopt},
4338 {.index = 1,
4339 .txType = "CheckCreate",
4340 .result = "tesSUCCESS",
4341 .txHash = txIDs[0],
4342 .batchID = batchID},
4343 {.index = 2,
4344 .txType = "Payment",
4345 .result = "tesSUCCESS",
4346 .txHash = txIDs[1],
4347 .batchID = batchID},
4348 {.index = 3,
4349 .txType = "CheckCash",
4350 .result = "tesSUCCESS",
4351 .txHash = objTxnID,
4352 .batchID = std::nullopt},
4353 };
4354 validateClosedLedger(env, testCases);
4355 }
4356 }
4357 }
4358
4359 void
4361 {
4362 testcase("pseudo txn with tfInnerBatchTxn");
4363
4364 using namespace test::jtx;
4365 using namespace std::literals;
4366
4367 Env env{*this, features};
4368
4369 auto const alice = Account("alice");
4370 auto const bob = Account("bob");
4371 env.fund(XRP(10000), alice, bob);
4372 env.close();
4373
4374 STTx const stx = STTx(ttAMENDMENT, [&](auto& obj) {
4375 obj.setAccountID(sfAccount, AccountID());
4376 obj.setFieldH256(sfAmendment, uint256(2));
4377 obj.setFieldU32(sfLedgerSequence, env.seq(alice));
4378 obj.setFieldU32(sfFlags, tfInnerBatchTxn);
4379 });
4380
4381 std::string reason;
4382 BEAST_EXPECT(isPseudoTx(stx));
4383 BEAST_EXPECT(!passesLocalChecks(stx, reason));
4384 BEAST_EXPECT(reason == "Cannot submit pseudo transactions.");
4385 env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal j) {
4386 auto const result = xrpl::apply(env.app(), view, stx, TapNone, j);
4387 BEAST_EXPECT(!result.applied && result.ter == temINVALID_FLAG);
4388 return result.applied;
4389 });
4390 }
4391
4392 void
4394 {
4395 testcase("batch open ledger");
4396 // IMPORTANT: When a transaction is submitted outside of a batch and
4397 // another transaction is part of the batch, the batch might fail
4398 // because the sequence is out of order. This is because the canonical
4399 // order of transactions is determined by the account first. So in this
4400 // case, alice's batch comes after bob's self submitted transaction even
4401 // though the payment was submitted after the batch.
4402
4403 using namespace test::jtx;
4404 using namespace std::literals;
4405
4406 Env env{*this, features};
4407 XRPAmount const baseFee = env.current()->fees().base;
4408
4409 auto const alice = Account("alice");
4410 auto const bob = Account("bob");
4411
4412 env.fund(XRP(10000), alice, bob);
4413 env.close();
4414
4415 env(noop(bob), Ter(tesSUCCESS));
4416 env.close();
4417
4418 auto const aliceSeq = env.seq(alice);
4419 auto const preAlice = env.balance(alice);
4420 auto const preBob = env.balance(bob);
4421 auto const bobSeq = env.seq(bob);
4422
4423 // Alice Pays Bob (Open Ledger)
4424 auto const payTxn1 = env.jt(pay(alice, bob, XRP(10)), Seq(aliceSeq));
4425 auto const payTxn1ID = to_string(payTxn1.stx->getTransactionID());
4426 env(payTxn1, Ter(tesSUCCESS));
4427
4428 // Alice & Bob Atomic Batch
4429 auto const batchFee = batch::calcBatchFee(env, 1, 2);
4430 auto const [txIDs, batchID] = submitBatch(
4431 env,
4432 tesSUCCESS,
4433 batch::outer(alice, aliceSeq + 1, batchFee, tfAllOrNothing),
4434 batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 2),
4435 batch::Inner(pay(bob, alice, XRP(5)), bobSeq),
4436 batch::Sig(bob));
4437
4438 // Bob pays Alice (Open Ledger)
4439 auto const payTxn2 = env.jt(pay(bob, alice, XRP(5)), Seq(bobSeq + 1));
4440 auto const payTxn2ID = to_string(payTxn2.stx->getTransactionID());
4441 env(payTxn2, Ter(terPRE_SEQ));
4442 env.close();
4443
4444 std::vector<TestLedgerData> const testCases = {
4445 {.index = 0,
4446 .txType = "Payment",
4447 .result = "tesSUCCESS",
4448 .txHash = payTxn1ID,
4449 .batchID = std::nullopt},
4450 {.index = 1,
4451 .txType = "Batch",
4452 .result = "tesSUCCESS",
4453 .txHash = batchID,
4454 .batchID = std::nullopt},
4455 {.index = 2,
4456 .txType = "Payment",
4457 .result = "tesSUCCESS",
4458 .txHash = txIDs[0],
4459 .batchID = batchID},
4460 {.index = 3,
4461 .txType = "Payment",
4462 .result = "tesSUCCESS",
4463 .txHash = txIDs[1],
4464 .batchID = batchID},
4465 };
4466 validateClosedLedger(env, testCases);
4467
4468 env.close();
4469 {
4470 // next ledger includes the payment txn
4471 std::vector<TestLedgerData> const testCases = {
4472 {.index = 0,
4473 .txType = "Payment",
4474 .result = "tesSUCCESS",
4475 .txHash = payTxn2ID,
4476 .batchID = std::nullopt},
4477 };
4478 validateClosedLedger(env, testCases);
4479 }
4480
4481 // Alice consumes sequences (# of txns)
4482 BEAST_EXPECT(env.seq(alice) == aliceSeq + 3);
4483
4484 // Alice consumes sequences (# of txns)
4485 BEAST_EXPECT(env.seq(bob) == bobSeq + 2);
4486
4487 // Alice pays XRP & Fee; Bob receives XRP & pays Fee
4488 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(10) - batchFee - baseFee);
4489 BEAST_EXPECT(env.balance(bob) == preBob + XRP(10) - baseFee);
4490 }
4491
4492 void
4494 {
4495 testcase("batch tx queue");
4496
4497 using namespace test::jtx;
4498 using namespace std::literals;
4499
4500 // A Batch is never queued. Under open-ledger congestion a Batch that
4501 // pays only its base fee cannot apply directly and, unlike an ordinary
4502 // transaction, is rejected outright rather than held in the queue. A
4503 // Batch that pays the escalated open-ledger fee applies directly.
4504 {
4505 Env env{
4506 *this,
4508 features,
4509 nullptr,
4511
4512 auto alice = Account("alice");
4513 auto bob = Account("bob");
4514 auto carol = Account("carol");
4515
4516 // Fund across several ledgers so the TxQ metrics stay restricted.
4517 env.fund(XRP(10000), noripple(alice, bob));
4518 env.close(env.now() + 5s, 10000ms);
4519 env.fund(XRP(10000), noripple(carol));
4520 env.close(env.now() + 5s, 10000ms);
4521
4522 // Fill the open ledger so escalation is active.
4523 env(noop(alice));
4524 env(noop(alice));
4525 env(noop(alice));
4526 checkMetrics(*this, env, 0, std::nullopt, 3, 2);
4527
4528 env(noop(carol), Ter(terQUEUED));
4529 checkMetrics(*this, env, 1, std::nullopt, 3, 2);
4530
4531 auto const aliceSeq = env.seq(alice);
4532 auto const bobSeq = env.seq(bob);
4533 auto const batchFee = batch::calcBatchFee(env, 1, 2);
4534
4535 // Paying only the base fee, the Batch cannot enter the congested
4536 // open ledger and is rejected rather than queued.
4537 env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
4538 batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
4539 batch::Inner(pay(bob, alice, XRP(5)), bobSeq),
4540 batch::Sig(bob),
4542
4543 // The Batch was not queued; only carol's transaction is queued.
4544 checkMetrics(*this, env, 1, std::nullopt, 3, 2);
4545
4546 // Paying the escalated open-ledger fee, the Batch applies directly.
4547 env(batch::outer(alice, aliceSeq, openLedgerFee(env, batchFee), tfAllOrNothing),
4548 batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
4549 batch::Inner(pay(bob, alice, XRP(5)), bobSeq),
4550 batch::Sig(bob),
4551 Ter(tesSUCCESS));
4552 }
4553
4554 // inner batch transactions are counter towards the ledger tx count
4555 {
4556 Env env{
4557 *this,
4559 features,
4560 nullptr,
4562
4563 auto alice = Account("alice");
4564 auto bob = Account("bob");
4565 auto carol = Account("carol");
4566
4567 // Fund across several ledgers so the TxQ metrics stay restricted.
4568 env.fund(XRP(10000), noripple(alice, bob));
4569 env.close(env.now() + 5s, 10000ms);
4570 env.fund(XRP(10000), noripple(carol));
4571 env.close(env.now() + 5s, 10000ms);
4572
4573 // Fill the ledger leaving room for 1 queued transaction
4574 env(noop(alice));
4575 env(noop(alice));
4576 checkMetrics(*this, env, 0, std::nullopt, 2, 2);
4577
4578 auto const aliceSeq = env.seq(alice);
4579 auto const bobSeq = env.seq(bob);
4580 auto const batchFee = batch::calcBatchFee(env, 1, 2);
4581
4582 // Batch Successful
4583 {
4584 env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
4585 batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
4586 batch::Inner(pay(bob, alice, XRP(5)), bobSeq),
4587 batch::Sig(bob),
4588 Ter(tesSUCCESS));
4589 }
4590
4591 checkMetrics(*this, env, 0, std::nullopt, 3, 2);
4592
4593 env(noop(carol), Ter(terQUEUED));
4594 checkMetrics(*this, env, 1, std::nullopt, 3, 2);
4595 }
4596
4597 // A Batch is never queued, so it also cannot sit behind the account's
4598 // already-queued transactions: with a sequence gap it cannot apply
4599 // directly and is rejected rather than queued.
4600 {
4601 Env env{
4602 *this,
4604 features,
4605 nullptr,
4607
4608 auto alice = Account("alice");
4609 auto bob = Account("bob");
4610
4611 env.fund(XRP(10000), noripple(alice, bob));
4612 env.close(env.now() + 5s, 10000ms);
4613
4614 // Fill the open ledger so subsequent transactions queue.
4615 env(noop(alice));
4616 env(noop(alice));
4617 env(noop(alice));
4618 checkMetrics(*this, env, 0, std::nullopt, 3, 2);
4619
4620 auto const aliceSeq = env.seq(alice);
4621
4622 // Queue two normal transactions for alice.
4623 env(noop(alice), Seq(aliceSeq + 0), Ter(terQUEUED));
4624 env(noop(alice), Seq(aliceSeq + 1), Ter(terQUEUED));
4625 checkMetrics(*this, env, 2, std::nullopt, 3, 2);
4626
4627 // The Batch's sequence sits behind the queued transactions, so it
4628 // cannot apply directly and is rejected rather than queued.
4629 auto const bobSeq = env.seq(bob);
4630 auto const batchFee = batch::calcBatchFee(env, 1, 2);
4631 env(batch::outer(alice, aliceSeq + 2, batchFee, tfAllOrNothing),
4632 batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 3),
4633 batch::Inner(pay(bob, alice, XRP(5)), bobSeq),
4634 batch::Sig(bob),
4636
4637 // The two queued transactions are untouched.
4638 checkMetrics(*this, env, 2, std::nullopt, 3, 2);
4639 }
4640 }
4641
4642 void
4644 {
4645 testcase("batch network ops");
4646
4647 using namespace test::jtx;
4648 using namespace std::literals;
4649
4650 Env env(*this, envconfig(), features, nullptr, beast::Severity::Disabled);
4651
4652 auto alice = Account("alice");
4653 auto bob = Account("bob");
4654 env.fund(XRP(10000), alice, bob);
4655 env.close();
4656
4657 auto submitTx = [&](std::uint32_t flags) -> uint256 {
4658 auto jt = env.jt(pay(alice, bob, XRP(1)), Txflags(flags));
4659 Serializer s;
4660 jt.stx->add(s);
4661 env.app().getOPs().submitTransaction(jt.stx);
4662 return jt.stx->getTransactionID();
4663 };
4664
4665 auto processTxn = [&](std::uint32_t flags) -> uint256 {
4666 auto jt = env.jt(pay(alice, bob, XRP(1)), Txflags(flags));
4667 Serializer s;
4668 jt.stx->add(s);
4669 std::string reason;
4670 auto transaction = std::make_shared<Transaction>(jt.stx, reason, env.app());
4672 transaction, false, true, NetworkOPs::FailHard::Yes);
4673 return transaction->getID();
4674 };
4675
4676 // Validate: NetworkOPs::submitTransaction()
4677 {
4678 // Submit a tx with tfInnerBatchTxn
4679 uint256 const txBad = submitTx(tfInnerBatchTxn);
4680 BEAST_EXPECT(env.app().getHashRouter().getFlags(txBad) == HashRouterFlags::UNDEFINED);
4681 }
4682
4683 // Validate: NetworkOPs::processTransaction()
4684 {
4685 uint256 const txid = processTxn(tfInnerBatchTxn);
4686 // HashRouter::getFlags() should return LedgerFlags::BAD
4687 BEAST_EXPECT(env.app().getHashRouter().getFlags(txid) == HashRouterFlags::BAD);
4688 }
4689 }
4690
4691 void
4693 {
4694 testcase("batch delegate consent");
4695
4696 using namespace test::jtx;
4697 using namespace std::literals;
4698
4699 // Delegate consent bypass.
4700 //
4701 // Alice delegates Payment to Bob. A Batch carries an inner Payment with
4702 // Delegate=Bob, but Bob never signs the batch (and, because the inner
4703 // Account == the outer Account, no BatchSigners are required at all).
4704 // Batch waives the per-inner signature check, and preflightSigValidated
4705 // derives required signers from sfAccount only -- it ignores sfDelegate
4706 // -- so Bob's delegated authority is exercised without Bob's consent.
4707 //
4708 // SECURE EXPECTATION: the delegate (Bob) must authorize the inner txn,
4709 // so the batch must be rejected (temBAD_SIGNER) when Bob does not sign.
4710 {
4711 Env env{*this, features};
4712
4713 auto const alice = Account("alice");
4714 auto const bob = Account("bob");
4715 auto const mallory = Account("mallory");
4716 env.fund(XRP(10000), alice, bob, mallory);
4717 env.close();
4718
4719 env(delegate::set(alice, bob, {"Payment"}));
4720 env.close();
4721
4722 auto const preMallory = env.balance(mallory);
4723
4724 auto const batchFee = batch::calcBatchFee(env, 0, 2);
4725 auto const seq = env.seq(alice);
4726
4727 auto inner = batch::Inner(pay(alice, mallory, XRP(1000)), seq + 1);
4728 inner[jss::Delegate] = bob.human();
4729
4730 // Bob is NOT among the batch signers; he never consents.
4731 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
4732 inner,
4733 batch::Inner(pay(alice, mallory, XRP(1)), seq + 2),
4735 env.close();
4736
4737 // No funds should have moved to Mallory.
4738 BEAST_EXPECT(env.balance(mallory) == preMallory);
4739 }
4740
4741 // Self-grant attribution forgery.
4742 //
4743 // With no pre-existing delegation, Alice places DelegateSet(authorize
4744 // Bob) and a Delegate=Bob action in the SAME batch. The second inner
4745 // reads the delegation created by the first from the batch's running
4746 // view, and Bob never signs -- manufacturing on-chain attribution of
4747 // the action to Bob without his consent.
4748 //
4749 // SECURE EXPECTATION: rejected (temBAD_SIGNER) -- Bob must consent, and
4750 // a grant cannot be created and exercised within the same atomic batch.
4751 {
4752 Env env{*this, features};
4753
4754 auto const alice = Account("alice");
4755 auto const bob = Account("bob");
4756 auto const mallory = Account("mallory");
4757 env.fund(XRP(10000), alice, bob, mallory);
4758 env.close();
4759
4760 auto const preMallory = env.balance(mallory);
4761
4762 auto const batchFee = batch::calcBatchFee(env, 0, 2);
4763 auto const seq = env.seq(alice);
4764
4765 auto inner = batch::Inner(pay(alice, mallory, XRP(1000)), seq + 2);
4766 inner[jss::Delegate] = bob.human();
4767
4768 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
4769 batch::Inner(delegate::set(alice, bob, {"Payment"}), seq + 1),
4770 inner,
4772 env.close();
4773
4774 BEAST_EXPECT(env.balance(mallory) == preMallory);
4775 }
4776
4777 // Legitimate counterpart: the same atomic grant-and-use is allowed when
4778 // the delegate co-signs the batch -- consent is present, so it succeeds.
4779 {
4780 Env env{*this, features};
4781
4782 auto const alice = Account("alice");
4783 auto const bob = Account("bob");
4784 auto const mallory = Account("mallory");
4785 env.fund(XRP(10000), alice, bob, mallory);
4786 env.close();
4787
4788 auto const preMallory = env.balance(mallory);
4789
4790 auto const batchFee = batch::calcBatchFee(env, 1, 2);
4791 auto const seq = env.seq(alice);
4792
4793 auto inner = batch::Inner(pay(alice, mallory, XRP(1000)), seq + 2);
4794 inner[jss::Delegate] = bob.human();
4795
4796 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
4797 batch::Inner(delegate::set(alice, bob, {"Payment"}), seq + 1),
4798 inner,
4799 batch::Sig(bob),
4800 Ter(tesSUCCESS));
4801 env.close();
4802
4803 BEAST_EXPECT(env.balance(mallory) == preMallory + XRP(1000));
4804 }
4805
4806 // Multi-account: a delegated inner from a non-outer account also
4807 // requires the delegate's signature (not the account holder's).
4808 {
4809 Env env{*this, features};
4810
4811 auto const alice = Account("alice");
4812 auto const bob = Account("bob");
4813 auto const carol = Account("carol");
4814 env.fund(XRP(10000), alice, bob, carol);
4815 env.close();
4816
4817 env(delegate::set(bob, carol, {"Payment"}));
4818 env.close();
4819
4820 auto const preAlice = env.balance(alice);
4821
4822 auto const batchFee = batch::calcBatchFee(env, 1, 2);
4823 auto const aliceSeq = env.seq(alice);
4824 auto const bobSeq = env.seq(bob);
4825
4826 auto inner = batch::Inner(pay(bob, alice, XRP(1)), bobSeq);
4827 inner[jss::Delegate] = carol.human();
4828
4829 // Carol (the delegate) does not sign.
4830 env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
4831 inner,
4832 batch::Inner(pay(alice, bob, XRP(2)), aliceSeq + 1),
4834 env.close();
4835
4836 BEAST_EXPECT(env.balance(alice) == preAlice);
4837 }
4838
4839 // Wrong signer: a batch signature from someone other than the named
4840 // delegate does not satisfy the requirement.
4841 {
4842 Env env{*this, features};
4843
4844 auto const alice = Account("alice");
4845 auto const bob = Account("bob");
4846 auto const carol = Account("carol");
4847 auto const mallory = Account("mallory");
4848 env.fund(XRP(10000), alice, bob, carol, mallory);
4849 env.close();
4850
4851 env(delegate::set(alice, bob, {"Payment"}));
4852 env.close();
4853
4854 auto const preMallory = env.balance(mallory);
4855
4856 auto const batchFee = batch::calcBatchFee(env, 1, 2);
4857 auto const seq = env.seq(alice);
4858
4859 auto inner = batch::Inner(pay(alice, mallory, XRP(1000)), seq + 1);
4860 inner[jss::Delegate] = bob.human();
4861
4862 // Carol signs instead of the named delegate Bob.
4863 env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
4864 inner,
4865 batch::Inner(pay(alice, mallory, XRP(1)), seq + 2),
4866 batch::Sig(carol),
4868 env.close();
4869
4870 BEAST_EXPECT(env.balance(mallory) == preMallory);
4871 }
4872
4873 // Delegate is the outer account: the outer signature already provides
4874 // the delegate's consent, so no BatchSigners are required.
4875 {
4876 Env env{*this, features};
4877
4878 auto const alice = Account("alice");
4879 auto const bob = Account("bob");
4880 env.fund(XRP(10000), alice, bob);
4881 env.close();
4882
4883 // Bob delegates Payment to Alice, who is also the batch submitter.
4884 env(delegate::set(bob, alice, {"Payment"}));
4885 env.close();
4886
4887 auto const preBob = env.balance(bob);
4888
4889 auto const batchFee = batch::calcBatchFee(env, 0, 2);
4890 auto const aliceSeq = env.seq(alice);
4891 auto const bobSeq = env.seq(bob);
4892
4893 auto inner = batch::Inner(pay(bob, alice, XRP(1)), bobSeq);
4894 inner[jss::Delegate] = alice.human();
4895
4896 env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
4897 inner,
4898 batch::Inner(pay(alice, bob, XRP(2)), aliceSeq + 1),
4899 Ter(tesSUCCESS));
4900 env.close();
4901
4902 // Net: Bob sends 1 to Alice, receives 2 from Alice.
4903 BEAST_EXPECT(env.balance(bob) == preBob + XRP(1));
4904 }
4905
4906 // Principal signs instead of the delegate. Bob delegates Payment to
4907 // Carol and an inner pays from Bob with Delegate=Carol, so Carol -- not
4908 // Bob -- is the required batch signer. Bob's own signature (the account
4909 // holder) does not satisfy the delegate-consent requirement.
4910 {
4911 Env env{*this, features};
4912
4913 auto const alice = Account("alice");
4914 auto const bob = Account("bob");
4915 auto const carol = Account("carol");
4916 env.fund(XRP(10000), alice, bob, carol);
4917 env.close();
4918
4919 env(delegate::set(bob, carol, {"Payment"}));
4920 env.close();
4921
4922 auto const preAlice = env.balance(alice);
4923
4924 auto const batchFee = batch::calcBatchFee(env, 1, 2);
4925 auto const aliceSeq = env.seq(alice);
4926 auto const bobSeq = env.seq(bob);
4927
4928 auto inner = batch::Inner(pay(bob, alice, XRP(1)), bobSeq);
4929 inner[jss::Delegate] = carol.human();
4930
4931 // Bob (the principal) signs in place of Carol (the delegate).
4932 env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
4933 inner,
4934 batch::Inner(pay(alice, bob, XRP(2)), aliceSeq + 1),
4935 batch::Sig(bob),
4937 env.close();
4938
4939 BEAST_EXPECT(env.balance(alice) == preAlice);
4940 }
4941
4942 // Control for the revocation case below: identical setup, but inner 1
4943 // is a benign payment instead of a revocation. With Alice's delegation
4944 // intact, the delegated inner 2 succeeds -- proving the failure in the
4945 // revocation case is caused by the revocation, not the setup.
4946 {
4947 Env env{*this, features};
4948
4949 auto const alice = Account("alice");
4950 auto const bob = Account("bob");
4951 auto const mallory = Account("mallory");
4952 env.fund(XRP(10000), alice, bob, mallory);
4953 env.close();
4954
4955 env(delegate::set(bob, alice, {"Payment"}));
4956 env.close();
4957
4958 auto const preMallory = env.balance(mallory);
4959
4960 auto const batchFee = batch::calcBatchFee(env, 1, 2);
4961 auto const seq = env.seq(bob);
4962
4963 auto inner2 = batch::Inner(pay(bob, mallory, XRP(1000)), seq + 2);
4964 inner2[jss::Delegate] = alice.human();
4965
4966 // Inner 1 is Bob's own payment (no revocation); Alice co-signs for
4967 // the delegated inner 2.
4968 auto const [txIDs, batchID] = submitBatch(
4969 env,
4970 tesSUCCESS,
4971 batch::outer(bob, seq, batchFee, tfIndependent),
4972 batch::Inner(pay(bob, alice, XRP(1)), seq + 1),
4973 inner2,
4974 batch::Sig(alice));
4975 env.close();
4976
4977 std::vector<TestLedgerData> const testCases = {
4978 {.index = 0,
4979 .txType = "Batch",
4980 .result = "tesSUCCESS",
4981 .txHash = batchID,
4982 .batchID = std::nullopt},
4983 {.index = 1,
4984 .txType = "Payment",
4985 .result = "tesSUCCESS",
4986 .txHash = txIDs[0],
4987 .batchID = batchID},
4988 {.index = 2,
4989 .txType = "Payment",
4990 .result = "tesSUCCESS",
4991 .txHash = txIDs[1],
4992 .batchID = batchID},
4993 };
4994 validateClosedLedger(env, testCases);
4995
4996 // The delegated payment applied: Mallory received the funds.
4997 BEAST_EXPECT(env.balance(mallory) == preMallory + XRP(1000));
4998 }
4999
5000 // Revocation within the same batch. Bob grants Alice Payment permission
5001 // beforehand; an earlier inner revokes it before a later delegated
5002 // inner tries to use it. Alice must still co-sign (delegate consent is
5003 // derived from the static tx), but the revoked permission makes the
5004 // delegated inner fail at apply time -- a grant cannot be used after it
5005 // is removed earlier in the same batch.
5006 {
5007 Env env{*this, features};
5008
5009 auto const alice = Account("alice");
5010 auto const bob = Account("bob");
5011 auto const mallory = Account("mallory");
5012 env.fund(XRP(10000), alice, bob, mallory);
5013 env.close();
5014
5015 env(delegate::set(bob, alice, {"Payment"}));
5016 env.close();
5017
5018 auto const preMallory = env.balance(mallory);
5019
5020 auto const batchFee = batch::calcBatchFee(env, 1, 2);
5021 auto const seq = env.seq(bob);
5022
5023 // Inner 2: Alice acts as Bob's delegate, so Alice is the required
5024 // batch signer.
5025 auto inner2 = batch::Inner(pay(bob, mallory, XRP(1000)), seq + 2);
5026 inner2[jss::Delegate] = alice.human();
5027
5028 // tfIndependent: inner 1 (the revocation) applies; inner 2 then
5029 // fails for lack of permission without reverting inner 1. An empty
5030 // permission list deletes the Delegate object.
5031 auto const [txIDs, batchID] = submitBatch(
5032 env,
5033 tesSUCCESS,
5034 batch::outer(bob, seq, batchFee, tfIndependent),
5035 batch::Inner(delegate::set(bob, alice, {}), seq + 1),
5036 inner2,
5037 batch::Sig(alice));
5038 env.close();
5039
5040 std::vector<TestLedgerData> const testCases = {
5041 {.index = 0,
5042 .txType = "Batch",
5043 .result = "tesSUCCESS",
5044 .txHash = batchID,
5045 .batchID = std::nullopt},
5046 {.index = 1,
5047 .txType = "DelegateSet",
5048 .result = "tesSUCCESS",
5049 .txHash = txIDs[0],
5050 .batchID = batchID},
5051 // inner 2 fails: Alice's permission was revoked in inner 1.
5052 };
5053 validateClosedLedger(env, testCases);
5054
5055 // The delegated payment never applied.
5056 BEAST_EXPECT(env.balance(mallory) == preMallory);
5057 BEAST_EXPECT(env.rpc("tx", txIDs[1])[jss::result][jss::error] == "txnNotFound");
5058 }
5059 }
5060
5061 void
5063 {
5064 testcase("batch delegate");
5065
5066 using namespace test::jtx;
5067 using namespace std::literals;
5068
5069 // delegated non atomic inner
5070 {
5071 Env env{*this, features};
5072
5073 auto const alice = Account("alice");
5074 auto const bob = Account("bob");
5075 auto const gw = Account("gw");
5076 auto const usd = gw["USD"];
5077 env.fund(XRP(10000), alice, bob, gw);
5078 env.close();
5079
5080 env(delegate::set(alice, bob, {"Payment"}));
5081 env.close();
5082
5083 auto const preAlice = env.balance(alice);
5084 auto const preBob = env.balance(bob);
5085
5086 auto const batchFee = batch::calcBatchFee(env, 1, 2);
5087 auto const seq = env.seq(alice);
5088
5089 auto tx = batch::Inner(pay(alice, bob, XRP(1)), seq + 1);
5090 tx[jss::Delegate] = bob.human();
5091 // The delegate (Bob) authorizes the delegated inner, so Bob must
5092 // provide the batch signature.
5093 auto const [txIDs, batchID] = submitBatch(
5094 env,
5095 tesSUCCESS,
5096 batch::outer(alice, seq, batchFee, tfAllOrNothing),
5097 tx,
5098 batch::Inner(pay(alice, bob, XRP(2)), seq + 2),
5099 batch::Sig(bob));
5100 env.close();
5101
5102 std::vector<TestLedgerData> const testCases = {
5103 {.index = 0,
5104 .txType = "Batch",
5105 .result = "tesSUCCESS",
5106 .txHash = batchID,
5107 .batchID = std::nullopt},
5108 {.index = 1,
5109 .txType = "Payment",
5110 .result = "tesSUCCESS",
5111 .txHash = txIDs[0],
5112 .batchID = batchID},
5113 {.index = 2,
5114 .txType = "Payment",
5115 .result = "tesSUCCESS",
5116 .txHash = txIDs[1],
5117 .batchID = batchID},
5118 };
5119 validateClosedLedger(env, testCases);
5120
5121 // Alice consumes sequences (# of txns)
5122 BEAST_EXPECT(env.seq(alice) == seq + 3);
5123
5124 // Alice pays XRP & Fee; Bob receives XRP
5125 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee);
5126 BEAST_EXPECT(env.balance(bob) == preBob + XRP(3));
5127 }
5128
5129 // delegated atomic inner
5130 {
5131 Env env{*this, features};
5132
5133 auto const alice = Account("alice");
5134 auto const bob = Account("bob");
5135 auto const carol = Account("carol");
5136 auto const gw = Account("gw");
5137 auto const usd = gw["USD"];
5138 env.fund(XRP(10000), alice, bob, carol, gw);
5139 env.close();
5140
5141 env(delegate::set(bob, carol, {"Payment"}));
5142 env.close();
5143
5144 auto const preAlice = env.balance(alice);
5145 auto const preBob = env.balance(bob);
5146 auto const preCarol = env.balance(carol);
5147
5148 auto const batchFee = batch::calcBatchFee(env, 1, 2);
5149 auto const aliceSeq = env.seq(alice);
5150 auto const bobSeq = env.seq(bob);
5151
5152 auto tx = batch::Inner(pay(bob, alice, XRP(1)), bobSeq);
5153 tx[jss::Delegate] = carol.human();
5154 // Carol is the delegate authorizing the inner txn on Bob's behalf,
5155 // so Carol -- not Bob -- must provide the batch signature.
5156 auto const [txIDs, batchID] = submitBatch(
5157 env,
5158 tesSUCCESS,
5159 batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
5160 tx,
5161 batch::Inner(pay(alice, bob, XRP(2)), aliceSeq + 1),
5162 batch::Sig(carol));
5163 env.close();
5164
5165 std::vector<TestLedgerData> const testCases = {
5166 {.index = 0,
5167 .txType = "Batch",
5168 .result = "tesSUCCESS",
5169 .txHash = batchID,
5170 .batchID = std::nullopt},
5171 {.index = 1,
5172 .txType = "Payment",
5173 .result = "tesSUCCESS",
5174 .txHash = txIDs[0],
5175 .batchID = batchID},
5176 {.index = 2,
5177 .txType = "Payment",
5178 .result = "tesSUCCESS",
5179 .txHash = txIDs[1],
5180 .batchID = batchID},
5181 };
5182 validateClosedLedger(env, testCases);
5183
5184 BEAST_EXPECT(env.seq(alice) == aliceSeq + 2);
5185 BEAST_EXPECT(env.seq(bob) == bobSeq + 1);
5186 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1) - batchFee);
5187 BEAST_EXPECT(env.balance(bob) == preBob + XRP(1));
5188 // NOTE: Carol would normally pay the fee for delegated txns, but
5189 // because the batch is atomic, the fee is paid by the batch
5190 BEAST_EXPECT(env.balance(carol) == preCarol);
5191 }
5192
5193 // delegated non atomic inner (AccountSet)
5194 // this also makes sure tfInnerBatchTxn won't block delegated AccountSet
5195 // with granular permission
5196 {
5197 Env env{*this, features};
5198
5199 auto const alice = Account("alice");
5200 auto const bob = Account("bob");
5201 auto const gw = Account("gw");
5202 auto const usd = gw["USD"];
5203 env.fund(XRP(10000), alice, bob, gw);
5204 env.close();
5205
5206 env(delegate::set(alice, bob, {"AccountDomainSet"}));
5207 env.close();
5208
5209 auto const preAlice = env.balance(alice);
5210 auto const preBob = env.balance(bob);
5211
5212 auto const batchFee = batch::calcBatchFee(env, 1, 2);
5213 auto const seq = env.seq(alice);
5214
5215 auto tx = batch::Inner(noop(alice), seq + 1);
5216 std::string const domain = "example.com";
5217 tx[sfDomain.jsonName] = strHex(domain);
5218 tx[jss::Delegate] = bob.human();
5219 // Bob is the delegate authorizing the inner AccountSet, so Bob must
5220 // provide the batch signature.
5221 auto const [txIDs, batchID] = submitBatch(
5222 env,
5223 tesSUCCESS,
5224 batch::outer(alice, seq, batchFee, tfAllOrNothing),
5225 tx,
5226 batch::Inner(pay(alice, bob, XRP(2)), seq + 2),
5227 batch::Sig(bob));
5228 env.close();
5229
5230 std::vector<TestLedgerData> const testCases = {
5231 {.index = 0,
5232 .txType = "Batch",
5233 .result = "tesSUCCESS",
5234 .txHash = batchID,
5235 .batchID = std::nullopt},
5236 {.index = 1,
5237 .txType = "AccountSet",
5238 .result = "tesSUCCESS",
5239 .txHash = txIDs[0],
5240 .batchID = batchID},
5241 {.index = 2,
5242 .txType = "Payment",
5243 .result = "tesSUCCESS",
5244 .txHash = txIDs[1],
5245 .batchID = batchID},
5246 };
5247 validateClosedLedger(env, testCases);
5248
5249 // Alice consumes sequences (# of txns)
5250 BEAST_EXPECT(env.seq(alice) == seq + 3);
5251
5252 // Alice pays XRP & Fee; Bob receives XRP
5253 BEAST_EXPECT(env.balance(alice) == preAlice - XRP(2) - batchFee);
5254 BEAST_EXPECT(env.balance(bob) == preBob + XRP(2));
5255 }
5256
5257 // delegated non atomic inner (MPTokenIssuanceSet)
5258 // this also makes sure tfInnerBatchTxn won't block delegated
5259 // MPTokenIssuanceSet with granular permission
5260 {
5261 Env env{*this, features};
5262 Account const alice{"alice"};
5263 Account const bob{"bob"};
5264 env.fund(XRP(100000), alice, bob);
5265 env.close();
5266
5267 auto const mptID = makeMptID(env.seq(alice), alice);
5268 MPTTester mpt(env, alice, {.fund = false});
5269 env.close();
5270 mpt.create({.flags = tfMPTCanLock});
5271 env.close();
5272
5273 // alice gives granular permission to bob of MPTokenIssuanceLock
5274 env(delegate::set(alice, bob, {"MPTokenIssuanceLock", "MPTokenIssuanceUnlock"}));
5275 env.close();
5276
5277 auto const seq = env.seq(alice);
5278 auto const batchFee = batch::calcBatchFee(env, 1, 2);
5279
5280 json::Value jv1;
5281 jv1[sfTransactionType] = jss::MPTokenIssuanceSet;
5282 jv1[sfAccount] = alice.human();
5283 jv1[sfDelegate] = bob.human();
5284 jv1[sfSequence] = seq + 1;
5285 jv1[sfMPTokenIssuanceID] = to_string(mptID);
5286 jv1[sfFlags] = tfMPTLock;
5287
5288 json::Value jv2;
5289 jv2[sfTransactionType] = jss::MPTokenIssuanceSet;
5290 jv2[sfAccount] = alice.human();
5291 jv2[sfDelegate] = bob.human();
5292 jv2[sfSequence] = seq + 2;
5293 jv2[sfMPTokenIssuanceID] = to_string(mptID);
5294 jv2[sfFlags] = tfMPTUnlock;
5295
5296 // Both inners are delegated to Bob, so Bob must provide the batch
5297 // signature (one signer covers both).
5298 auto const [txIDs, batchID] = submitBatch(
5299 env,
5300 tesSUCCESS,
5301 batch::outer(alice, seq, batchFee, tfAllOrNothing),
5302 batch::Inner(jv1, seq + 1),
5303 batch::Inner(jv2, seq + 2),
5304 batch::Sig(bob));
5305 env.close();
5306
5307 std::vector<TestLedgerData> const testCases = {
5308 {.index = 0,
5309 .txType = "Batch",
5310 .result = "tesSUCCESS",
5311 .txHash = batchID,
5312 .batchID = std::nullopt},
5313 {.index = 1,
5314 .txType = "MPTokenIssuanceSet",
5315 .result = "tesSUCCESS",
5316 .txHash = txIDs[0],
5317 .batchID = batchID},
5318 {.index = 2,
5319 .txType = "MPTokenIssuanceSet",
5320 .result = "tesSUCCESS",
5321 .txHash = txIDs[1],
5322 .batchID = batchID},
5323 };
5324 validateClosedLedger(env, testCases);
5325 }
5326
5327 // delegated non atomic inner (TrustSet)
5328 // this also makes sure tfInnerBatchTxn won't block delegated TrustSet
5329 // with granular permission
5330 {
5331 Env env{*this, features};
5332 Account const gw{"gw"};
5333 Account const alice{"alice"};
5334 Account const bob{"bob"};
5335 env.fund(XRP(10000), gw, alice, bob);
5336 env(fset(gw, asfRequireAuth));
5337 env.close();
5338 env(trust(alice, gw["USD"](50)));
5339 env.close();
5340
5341 env(delegate::set(gw, bob, {"TrustlineAuthorize", "TrustlineFreeze"}));
5342 env.close();
5343
5344 auto const seq = env.seq(gw);
5345 auto const batchFee = batch::calcBatchFee(env, 1, 2);
5346
5347 auto jv1 = trust(gw, gw["USD"](0), alice, tfSetfAuth);
5348 jv1[sfDelegate] = bob.human();
5349 auto jv2 = trust(gw, gw["USD"](0), alice, tfSetFreeze);
5350 jv2[sfDelegate] = bob.human();
5351
5352 // Both inners are delegated to Bob, so Bob must provide the batch
5353 // signature.
5354 auto const [txIDs, batchID] = submitBatch(
5355 env,
5356 tesSUCCESS,
5357 batch::outer(gw, seq, batchFee, tfAllOrNothing),
5358 batch::Inner(jv1, seq + 1),
5359 batch::Inner(jv2, seq + 2),
5360 batch::Sig(bob));
5361 env.close();
5362
5363 std::vector<TestLedgerData> const testCases = {
5364 {.index = 0,
5365 .txType = "Batch",
5366 .result = "tesSUCCESS",
5367 .txHash = batchID,
5368 .batchID = std::nullopt},
5369 {.index = 1,
5370 .txType = "TrustSet",
5371 .result = "tesSUCCESS",
5372 .txHash = txIDs[0],
5373 .batchID = batchID},
5374 {.index = 2,
5375 .txType = "TrustSet",
5376 .result = "tesSUCCESS",
5377 .txHash = txIDs[1],
5378 .batchID = batchID},
5379 };
5380 validateClosedLedger(env, testCases);
5381 }
5382
5383 // inner transaction not authorized by the delegating account.
5384 {
5385 Env env{*this, features};
5386 Account const gw{"gw"};
5387 Account const alice{"alice"};
5388 Account const bob{"bob"};
5389 env.fund(XRP(10000), gw, alice, bob);
5390 env(fset(gw, asfRequireAuth));
5391 env.close();
5392 env(trust(alice, gw["USD"](50)));
5393 env.close();
5394
5395 env(delegate::set(gw, bob, {"TrustlineAuthorize", "TrustlineFreeze"}));
5396 env.close();
5397
5398 auto const seq = env.seq(gw);
5399 auto const batchFee = batch::calcBatchFee(env, 1, 2);
5400
5401 auto jv1 = trust(gw, gw["USD"](0), alice, tfSetFreeze);
5402 jv1[sfDelegate] = bob.human();
5403 auto jv2 = trust(gw, gw["USD"](0), alice, tfClearFreeze);
5404 jv2[sfDelegate] = bob.human();
5405
5406 // Both inners are delegated to Bob, so Bob must provide the batch
5407 // signature; jv2 still fails preclaim for lack of permission.
5408 auto const [txIDs, batchID] = submitBatch(
5409 env,
5410 tesSUCCESS,
5411 batch::outer(gw, seq, batchFee, tfIndependent),
5412 batch::Inner(jv1, seq + 1),
5413 // terNO_DELEGATE_PERMISSION: not authorized to clear freeze
5414 batch::Inner(jv2, seq + 2),
5415 batch::Sig(bob));
5416 env.close();
5417
5418 std::vector<TestLedgerData> const testCases = {
5419 {.index = 0,
5420 .txType = "Batch",
5421 .result = "tesSUCCESS",
5422 .txHash = batchID,
5423 .batchID = std::nullopt},
5424 {.index = 1,
5425 .txType = "TrustSet",
5426 .result = "tesSUCCESS",
5427 .txHash = txIDs[0],
5428 .batchID = batchID},
5429 // jv2 fails with terNO_DELEGATE_PERMISSION.
5430 };
5431 validateClosedLedger(env, testCases);
5432
5433 // verify jv2 is not present in the closed ledger.
5434 BEAST_EXPECT(env.rpc("tx", txIDs[1])[jss::result][jss::error] == "txnNotFound");
5435 }
5436 }
5437
5438 void
5440 {
5441 // Verifying that the RPC response from submit includes
5442 // the account_sequence_available, account_sequence_next,
5443 // open_ledger_cost and validated_ledger_index fields.
5444 testcase("Validate RPC response");
5445
5446 using namespace jtx;
5447 Env env(*this, features);
5448 Account const alice("alice");
5449 Account const bob("bob");
5450 env.fund(XRP(10000), alice, bob);
5451 env.close();
5452
5453 // tes
5454 {
5455 auto const baseFee = env.current()->fees().base;
5456 auto const aliceSeq = env.seq(alice);
5457 auto jtx = env.jt(pay(alice, bob, XRP(1)));
5458
5459 Serializer s;
5460 jtx.stx->add(s);
5461 auto const jr = env.rpc("submit", strHex(s.slice()))[jss::result];
5462 env.close();
5463
5464 BEAST_EXPECT(jr.isMember(jss::account_sequence_available));
5465 BEAST_EXPECT(jr[jss::account_sequence_available].asUInt() == aliceSeq + 1);
5466 BEAST_EXPECT(jr.isMember(jss::account_sequence_next));
5467 BEAST_EXPECT(jr[jss::account_sequence_next].asUInt() == aliceSeq + 1);
5468 BEAST_EXPECT(jr.isMember(jss::open_ledger_cost));
5469 BEAST_EXPECT(jr[jss::open_ledger_cost] == to_string(baseFee));
5470 BEAST_EXPECT(jr.isMember(jss::validated_ledger_index));
5471 }
5472
5473 // tec failure
5474 {
5475 auto const baseFee = env.current()->fees().base;
5476 auto const aliceSeq = env.seq(alice);
5477 env(fset(bob, asfRequireDest));
5478 auto jtx = env.jt(pay(alice, bob, XRP(1)), Seq(aliceSeq));
5479
5480 Serializer s;
5481 jtx.stx->add(s);
5482 auto const jr = env.rpc("submit", strHex(s.slice()))[jss::result];
5483 env.close();
5484
5485 BEAST_EXPECT(jr.isMember(jss::account_sequence_available));
5486 BEAST_EXPECT(jr[jss::account_sequence_available].asUInt() == aliceSeq + 1);
5487 BEAST_EXPECT(jr.isMember(jss::account_sequence_next));
5488 BEAST_EXPECT(jr[jss::account_sequence_next].asUInt() == aliceSeq + 1);
5489 BEAST_EXPECT(jr.isMember(jss::open_ledger_cost));
5490 BEAST_EXPECT(jr[jss::open_ledger_cost] == to_string(baseFee));
5491 BEAST_EXPECT(jr.isMember(jss::validated_ledger_index));
5492 }
5493
5494 // tem failure
5495 {
5496 auto const baseFee = env.current()->fees().base;
5497 auto const aliceSeq = env.seq(alice);
5498 auto jtx = env.jt(pay(alice, bob, XRP(1)), Seq(aliceSeq + 1));
5499
5500 Serializer s;
5501 jtx.stx->add(s);
5502 auto const jr = env.rpc("submit", strHex(s.slice()))[jss::result];
5503 env.close();
5504
5505 BEAST_EXPECT(jr.isMember(jss::account_sequence_available));
5506 BEAST_EXPECT(jr[jss::account_sequence_available].asUInt() == aliceSeq);
5507 BEAST_EXPECT(jr.isMember(jss::account_sequence_next));
5508 BEAST_EXPECT(jr[jss::account_sequence_next].asUInt() == aliceSeq);
5509 BEAST_EXPECT(jr.isMember(jss::open_ledger_cost));
5510 BEAST_EXPECT(jr[jss::open_ledger_cost] == to_string(baseFee));
5511 BEAST_EXPECT(jr.isMember(jss::validated_ledger_index));
5512 }
5513 }
5514
5515 void
5517 {
5518 using namespace jtx;
5519 Env env(*this, features);
5520 Account const alice("alice");
5521 Account const bob("bob");
5522 Account const carol("carol");
5523 env.fund(XRP(10000), alice, bob, carol);
5524 env.close();
5525
5526 auto getBaseFee = [&](JTx const& jtx) -> XRPAmount {
5527 Serializer s;
5528 jtx.stx->add(s);
5529 return Batch::calculateBaseFee(*env.current(), *jtx.stx);
5530 };
5531
5532 // bad: a batch may not contain a batch - the nested inner fails STTx
5533 // construction, so the transaction cannot be built.
5534 {
5535 auto const seq = env.seq(alice);
5536 XRPAmount const batchFee = batch::calcBatchFee(env, 0, 2);
5537 auto jtx = env.jt(
5538 batch::outer(alice, seq, batchFee, tfAllOrNothing),
5539 batch::Inner(batch::outer(alice, seq, batchFee, tfAllOrNothing), seq),
5540 batch::Inner(pay(alice, bob, XRP(1)), seq + 2));
5541 BEAST_EXPECT(jtx.stx == nullptr);
5542 }
5543
5544 // bad: an oversized batch (more than kMaxBatchTxCount inners) fails
5545 // STTx construction, so it cannot be built.
5546 {
5547 auto const seq = env.seq(alice);
5548 XRPAmount const batchFee = batch::calcBatchFee(env, 0, 2);
5549
5550 auto jtx = env.jt(
5551 batch::outer(alice, seq, batchFee, tfAllOrNothing),
5552 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
5553 batch::Inner(pay(alice, bob, XRP(1)), seq + 2),
5554 batch::Inner(pay(alice, bob, XRP(1)), seq + 3),
5555 batch::Inner(pay(alice, bob, XRP(1)), seq + 4),
5556 batch::Inner(pay(alice, bob, XRP(1)), seq + 5),
5557 batch::Inner(pay(alice, bob, XRP(1)), seq + 6),
5558 batch::Inner(pay(alice, bob, XRP(1)), seq + 7),
5559 batch::Inner(pay(alice, bob, XRP(1)), seq + 8),
5560 batch::Inner(pay(alice, bob, XRP(1)), seq + 9));
5561
5562 BEAST_EXPECT(jtx.stx == nullptr);
5563 }
5564
5565 // bad: Signers array exceeds max entries.
5566 {
5567 auto const seq = env.seq(alice);
5568 XRPAmount const batchFee = batch::calcBatchFee(env, 0, 2);
5569
5570 auto jtx = env.jt(
5571 batch::outer(alice, seq, batchFee, tfAllOrNothing),
5572 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
5573 batch::Inner(pay(alice, bob, XRP(5)), seq + 2),
5575 // Failure paths fall back to the ledger base fee.
5576 XRPAmount const txBaseFee = getBaseFee(jtx);
5577 BEAST_EXPECT(txBaseFee == env.current()->fees().base);
5578 }
5579
5580 // good:
5581 {
5582 auto const seq = env.seq(alice);
5583 XRPAmount const batchFee = batch::calcBatchFee(env, 0, 2);
5584 auto jtx = env.jt(
5585 batch::outer(alice, seq, batchFee, tfAllOrNothing),
5586 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
5587 batch::Inner(pay(bob, alice, XRP(2)), seq + 2));
5588 XRPAmount const txBaseFee = getBaseFee(jtx);
5589 BEAST_EXPECT(txBaseFee == batchFee);
5590 }
5591 }
5592
5593 void
5595 {
5596 testcase("standalone tx with tfInnerBatchTxn rejected");
5597
5598 using namespace test::jtx;
5599 using namespace std::literals;
5600
5601 // A standalone Payment with tfInnerBatchTxn must be rejected.
5602 // Without proper guards this would bypass signature verification
5603 // in preflight2.
5604 {
5605 Env env{*this, features};
5606
5607 auto const alice = Account("alice");
5608 auto const bob = Account("bob");
5609 env.fund(XRP(10000), alice, bob);
5610 env.close();
5611
5612 // Submit a normal Payment with tfInnerBatchTxn flag.
5613 // preflight1 must reject with temINVALID_INNER_BATCH because
5614 // the flag is set but no parentBatchId exists.
5615 env(pay(alice, bob, XRP(1)), Txflags(tfInnerBatchTxn), Ter(telENV_RPC_FAILED));
5616 env.close();
5617
5618 // Verify via direct apply path (bypassing RPC layer)
5619 env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal j) {
5620 // Construct a Payment STTx with tfInnerBatchTxn,
5621 // empty signing pub key, and no signature — mimicking
5622 // what an attacker would send to skip sig verification.
5623 STTx const stx = STTx(ttPAYMENT, [&](auto& obj) {
5624 obj.setAccountID(sfAccount, alice.id());
5625 obj.setAccountID(sfDestination, bob.id());
5626 obj.setFieldAmount(sfAmount, XRP(1));
5627 obj.setFieldAmount(sfFee, XRP(0));
5628 obj.setFieldU32(sfSequence, env.seq(alice));
5629 obj.setFieldU32(sfFlags, tfInnerBatchTxn);
5630 });
5631
5632 auto const result = xrpl::apply(env.app(), view, stx, TapNone, j);
5633 // Must NOT be applied — signature was never checked
5634 BEAST_EXPECT(!result.applied);
5635 return false;
5636 });
5637 }
5638 }
5639
5640 void
5642 {
5643 testcase("outer binding");
5644
5645 using namespace test::jtx;
5646
5647 // Signatures captured from one outer account cannot be replayed
5648 // under a different outer account.
5649 {
5650 Env env{*this, features};
5651
5652 auto const alice = Account("alice");
5653 auto const bob = Account("bob");
5654 auto const carol = Account("carol");
5655 auto const eve = Account("eve");
5656 env.fund(XRP(10000), alice, bob, carol, eve);
5657 env.close();
5658
5659 auto const preCarol = env.balance(carol);
5660 auto const bobSeq = env.seq(bob);
5661 auto const carolSeq = env.seq(carol);
5662
5663 auto const aliceSeq = env.seq(alice);
5664 auto const batchFee1 = batch::calcBatchFee(env, 2, 2);
5665 auto jt1 = env.jt(
5666 batch::outer(alice, aliceSeq, batchFee1, tfOnlyOne),
5667 batch::Inner(pay(bob, alice, XRP(100)), bobSeq),
5668 batch::Inner(pay(carol, alice, XRP(50)), carolSeq),
5669 batch::Sig(bob, carol));
5670
5671 auto const capturedSigners = jt1.jv[sfBatchSigners.jsonName];
5672
5673 env(jt1, Ter(tesSUCCESS));
5674 env.close();
5675 BEAST_EXPECT(env.seq(bob) == bobSeq + 1);
5676 BEAST_EXPECT(env.seq(carol) == carolSeq);
5677
5678 auto const batchFee2 = batch::calcBatchFee(env, 2, 2);
5679 auto jt2 = env.jtnofill(
5680 batch::outer(eve, env.seq(eve), batchFee2, tfOnlyOne),
5681 batch::Inner(pay(bob, alice, XRP(100)), bobSeq),
5682 batch::Inner(pay(carol, alice, XRP(50)), carolSeq));
5683
5684 jt2.jv[sfBatchSigners.jsonName] = capturedSigners;
5685 env(jt2.jv, Ter(telENV_RPC_FAILED));
5686 env.close();
5687
5688 BEAST_EXPECT(env.seq(carol) == carolSeq);
5689 BEAST_EXPECT(env.balance(carol) == preCarol);
5690 }
5691
5692 // Signatures are bound to the outer sequence; replaying them
5693 // at a higher sequence must fail.
5694 {
5695 Env env{*this, features};
5696
5697 auto const alice = Account("alice");
5698 auto const bob = Account("bob");
5699 auto const carol = Account("carol");
5700 env.fund(XRP(10000), alice, bob, carol);
5701 env.close();
5702
5703 auto const preCarol = env.balance(carol);
5704 auto const bobSeq = env.seq(bob);
5705 auto const carolSeq = env.seq(carol);
5706
5707 auto const aliceSeq1 = env.seq(alice);
5708 auto const batchFee1 = batch::calcBatchFee(env, 2, 2);
5709 auto jt1 = env.jt(
5710 batch::outer(alice, aliceSeq1, batchFee1, tfOnlyOne),
5711 batch::Inner(pay(bob, alice, XRP(500)), bobSeq),
5712 batch::Inner(pay(carol, alice, XRP(500)), carolSeq),
5713 batch::Sig(bob, carol));
5714
5715 auto const capturedSigners = jt1.jv[sfBatchSigners.jsonName];
5716
5717 env(jt1, Ter(tesSUCCESS));
5718 env.close();
5719 BEAST_EXPECT(env.seq(bob) == bobSeq + 1);
5720 BEAST_EXPECT(env.seq(carol) == carolSeq);
5721
5722 auto const batchFee2 = batch::calcBatchFee(env, 2, 2);
5723 auto jt2 = env.jtnofill(
5724 batch::outer(alice, env.seq(alice), batchFee2, tfOnlyOne),
5725 batch::Inner(pay(bob, alice, XRP(500)), bobSeq),
5726 batch::Inner(pay(carol, alice, XRP(500)), carolSeq));
5727
5728 jt2.jv[sfBatchSigners.jsonName] = capturedSigners;
5729 env(jt2.jv, Ter(telENV_RPC_FAILED));
5730 env.close();
5731
5732 BEAST_EXPECT(env.balance(carol) == preCarol);
5733 BEAST_EXPECT(env.seq(carol) == carolSeq);
5734 }
5735
5736 // Multi-signed batch signer entries are bound to their account;
5737 // reusing inner signatures under a different batch signer must fail.
5738 {
5739 Env env{*this, features};
5740
5741 auto const alice = Account("alice");
5742 auto const bob = Account("bob");
5743 auto const carol = Account("carol");
5744 auto const dave = Account("dave");
5745 auto const elsa = Account("elsa");
5746 env.fund(XRP(10000), alice, bob, carol, dave, elsa);
5747 env.close();
5748
5749 env(signers(bob, 2, {{dave, 1}, {elsa, 1}}));
5750 env.close();
5751 env(signers(carol, 2, {{dave, 1}, {elsa, 1}}));
5752 env.close();
5753
5754 auto const seq = env.seq(alice);
5755 auto const batchFee = batch::calcBatchFee(env, 3, 2);
5756 auto jt1 = env.jt(
5757 batch::outer(alice, seq, batchFee, tfAllOrNothing),
5758 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
5759 batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
5760 batch::Msig(bob, {dave, elsa}),
5761 Ter(tesSUCCESS));
5762
5763 auto const bobSignerEntry = jt1.jv[sfBatchSigners.jsonName][0u];
5764
5765 env(jt1, Ter(tesSUCCESS));
5766 env.close();
5767
5768 auto const seq2 = env.seq(alice);
5769 auto const batchFee2 = batch::calcBatchFee(env, 3, 2);
5770 auto jt2 = env.jtnofill(
5771 batch::outer(alice, seq2, batchFee2, tfAllOrNothing),
5772 batch::Inner(pay(alice, carol, XRP(10)), seq2 + 1),
5773 batch::Inner(pay(carol, alice, XRP(5)), env.seq(carol)));
5774
5775 json::Value carolSigner;
5776 carolSigner[sfBatchSigner.jsonName][jss::Account] = carol.human();
5777 carolSigner[sfBatchSigner.jsonName][jss::SigningPubKey] = "";
5778 carolSigner[sfBatchSigner.jsonName][sfSigners.jsonName] =
5779 bobSignerEntry[sfBatchSigner.jsonName][sfSigners.jsonName];
5780
5781 jt2.jv[sfBatchSigners.jsonName][0u] = carolSigner;
5782 env(jt2.jv, Ter(telENV_RPC_FAILED));
5783 env.close();
5784 }
5785 }
5786
5787 void
5789 {
5790 testcase("unsorted batch signers");
5791
5792 using namespace test::jtx;
5793
5794 Env env{*this, features};
5795
5796 auto const alice = Account("alice");
5797 auto const bob = Account("bob");
5798 auto const carol = Account("carol");
5799 env.fund(XRP(10000), alice, bob, carol);
5800 env.close();
5801
5802 auto const seq = env.seq(alice);
5803 auto const bobSeq = env.seq(bob);
5804 auto const carolSeq = env.seq(carol);
5805 auto const batchFee = batch::calcBatchFee(env, 2, 2);
5806
5807 auto jt = env.jt(
5808 batch::outer(alice, seq, batchFee, tfAllOrNothing),
5809 batch::Inner(pay(bob, alice, XRP(10)), bobSeq),
5810 batch::Inner(pay(carol, alice, XRP(5)), carolSeq),
5811 batch::Sig(bob, carol));
5812
5813 auto const s0 = jt.jv[sfBatchSigners.jsonName][0u];
5814 auto const s1 = jt.jv[sfBatchSigners.jsonName][1u];
5815 jt.jv[sfBatchSigners.jsonName][0u] = s1;
5816 jt.jv[sfBatchSigners.jsonName][1u] = s0;
5817
5818 env(jt.jv, Ter(temBAD_SIGNER));
5819 env.close();
5820 }
5821
5822 void
5824 {
5825 testcase("batch signature caching");
5826
5827 using namespace test::jtx;
5828
5829 // Mirrors apply.cpp's file-local kSfSiggood (the standard signature-good
5830 // cache); batch signer sigs are now verified and cached alongside the
5831 // outer signature in checkSign/checkValidity.
5833
5834 // Valid batch: alice (outer) + an inner from bob, who co-signs.
5835 auto buildValidBatch = [](Env& env) {
5836 auto const alice = Account("alice");
5837 auto const bob = Account("bob");
5838 auto const seq = env.seq(alice);
5839 auto const batchFee = batch::calcBatchFee(env, 1, 2);
5840 return env.jt(
5841 batch::outer(alice, seq, batchFee, tfAllOrNothing),
5842 batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
5843 batch::Inner(pay(bob, alice, XRP(2)), env.seq(bob)),
5844 batch::Sig(bob));
5845 };
5846
5847 // WRITE: a valid batch records "good" on its tx id.
5848 {
5849 Env env{*this, features};
5850 env.fund(XRP(10000), Account("alice"), Account("bob"));
5851 env.close();
5852
5853 auto jt = buildValidBatch(env);
5854 auto const txid = jt.stx->getTransactionID();
5855
5856 BEAST_EXPECT(!any(env.app().getHashRouter().getFlags(txid) & kSfSiggood));
5857 env(jt, Ter(tesSUCCESS));
5858 BEAST_EXPECT(any(env.app().getHashRouter().getFlags(txid) & kSfSiggood));
5859 env.close();
5860 }
5861
5862 // READ: corrupt only a signer's signature (outer sig + signer key
5863 // untouched), so just the crypto would fail. Caught when uncached...
5864 {
5865 Env env{*this, features};
5866 env.fund(XRP(10000), Account("alice"), Account("bob"));
5867 env.close();
5868
5869 auto jt = buildValidBatch(env);
5870 jt.jv[sfBatchSigners.jsonName][0u][sfBatchSigner.jsonName][sfTxnSignature.jsonName] =
5871 "00";
5872 env(jt.jv, Ter(telENV_RPC_FAILED));
5873 env.close();
5874 }
5875 {
5876 // ...but a planted "good" skips the crypto, so it applies.
5877 Env env{*this, features};
5878 env.fund(XRP(10000), Account("alice"), Account("bob"));
5879 env.close();
5880
5881 auto jt = buildValidBatch(env);
5882 jt.jv[sfBatchSigners.jsonName][0u][sfBatchSigner.jsonName][sfTxnSignature.jsonName] =
5883 "00";
5884 auto const txid = STTx{parse(jt.jv)}.getTransactionID();
5885 env.app().getHashRouter().setFlags(txid, kSfSiggood);
5886 env(jt.jv, Ter(tesSUCCESS));
5887 env.close();
5888 }
5889 }
5890
5891 void
5893 {
5894 testEnable(features);
5895 testPreflight(features);
5896 testPreclaim(features);
5897 testBadRawTxn(features);
5898 testBadSequence(features);
5899 testBadOuterFee(features);
5900 testCalculateBaseFee(features);
5901 testAllOrNothing(features);
5902 testOnlyOne(features);
5903 testUntilFailure(features);
5904 testIndependent(features);
5905 testInnerSubmitRPC(features);
5906 testAccountActivation(features);
5907 testCheckAllSignatures(features);
5908 testAccountSet(features);
5909 testAccountDelete(features);
5910 testLoan(features);
5911 testObjectCreateSequence(features);
5912 testObjectCreateTicket(features);
5913 testObjectCreate3rdParty(features);
5914 testTickets(features);
5915 testSequenceOpenLedger(features);
5916 testTicketsOpenLedger(features);
5917 testObjectsOpenLedger(features);
5918 testPseudoTxn(features);
5919 testOpenLedger(features);
5920 testBatchTxQueue(features);
5921 testBatchNetworkOps(features);
5922 testBatchDelegate(features);
5923 testBatchDelegateConsent(features);
5924 testValidateRPCResponse(features);
5925 testBatchCalculateBaseFee(features);
5927 testOuterBinding(features);
5928 testUnsortedBatchSigners(features);
5929 testBatchSigCache(features);
5930 }
5931
5932public:
5933 void
5934 run() override
5935 {
5936 using namespace test::jtx;
5937
5938 auto const sa = testableAmendments();
5939 testWithFeats(sa);
5940 }
5941};
5942
5944
5945} // namespace xrpl::test
T any_of(T... args)
T begin(T... args)
A generic endpoint for log messages.
Definition Journal.h:44
A testsuite class.
Definition suite.h:52
bool expect(Condition const &shouldBeTrue)
Evaluate a test condition.
Definition suite.h:235
void fail(String const &reason, char const *file, int line)
Record a failure.
Definition suite.h:554
TestcaseT testcase
Memberspace for declaring test cases.
Definition suite.h:155
Lightweight wrapper to tag static string.
Definition json_value.h:48
Represents a JSON value.
Definition json_value.h:117
static XRPAmount calculateBaseFee(ReadView const &view, STTx const &tx)
Definition Batch.cpp:151
static constexpr auto kDisabledTxTypes
HashRouterFlags getFlags(uint256 const &key)
bool setFlags(uint256 const &key, HashRouterFlags flags)
Set the flags on a hash.
virtual void submitTransaction(std::shared_ptr< STTx const > const &)=0
virtual void processTransaction(std::shared_ptr< Transaction > &transaction, bool bUnlimited, bool bLocal, FailHard failType)=0
Process transactions as they arrive from the network or which are submitted by clients.
bool modify(modify_type const &f)
Modify the open ledger.
Writable ledger view that accumulates state and tx changes.
Definition OpenView.h:59
json::Value getJson(JsonOptions=JsonOptions::Values::None) const override
Definition STAmount.cpp:734
Holds the serialized result of parsing an input JSON object.
std::optional< STObject > object
The STObject if the parse was successful.
json::Value getJson(JsonOptions options) const override
Definition STTx.cpp:338
uint256 getTransactionID() const
Definition STTx.h:238
static constexpr SeqProxy rawSequence(std::uint32_t v)
Factory function to return a sequence-based SeqProxy.
Definition SeqProxy.h:62
Slice slice() const noexcept
Definition Serializer.h:45
virtual TxQ & getTxQ()=0
virtual NetworkOPs & getOPs()=0
virtual OpenLedger & getOpenLedger()=0
virtual HashRouter & getHashRouter()=0
An immutable linear range of bytes.
Definition Slice.h:28
static NotTEC invokePreflight(PreflightContext const &ctx)
Definition Transactor.h:595
Metrics getMetrics(OpenView const &view) const
Returns fee metrics in reference fee level units.
Definition TxQ.cpp:1750
void testOpenLedger(FeatureBitset features)
void testBatchDelegateConsent(FeatureBitset features)
void testTicketsOpenLedger(FeatureBitset features)
static json::Value getLastLedger(jtx::Env &env)
void run() override
Runs the suite.
void testBadRawTxn(FeatureBitset features)
void testObjectsOpenLedger(FeatureBitset features)
void testStandaloneInnerBatchFlag(FeatureBitset features)
void testSequenceOpenLedger(FeatureBitset features)
void testEnable(FeatureBitset features)
void testPreflight(FeatureBitset features)
void testObjectCreate3rdParty(FeatureBitset features)
void testIndependent(FeatureBitset features)
void doTestInnerSubmitRPC(FeatureBitset features, bool withBatch)
void testBatchDelegate(FeatureBitset features)
void testBadSequence(FeatureBitset features)
void testTickets(FeatureBitset features)
void testAccountSet(FeatureBitset features)
std::pair< std::vector< std::string >, std::string > submitBatch(jtx::Env &env, TER const &result, Args &&... args)
void testPreclaim(FeatureBitset features)
void testBatchTxQueue(FeatureBitset features)
static auto openLedgerFee(jtx::Env &env, XRPAmount const &batchFee)
void testAllOrNothing(FeatureBitset features)
void testCheckAllSignatures(FeatureBitset features)
void testValidateRPCResponse(FeatureBitset features)
void testWithFeats(FeatureBitset features)
void testBatchCalculateBaseFee(FeatureBitset features)
void testLoan(FeatureBitset features)
void testOuterBinding(FeatureBitset features)
void testBatchNetworkOps(FeatureBitset features)
static json::Value getTxByIndex(json::Value const &jrr, int const index)
void testObjectCreateTicket(FeatureBitset features)
void testInnerSubmitRPC(FeatureBitset features)
void validateClosedLedger(jtx::Env &env, std::vector< TestLedgerData > const &ledgerResults)
void testCalculateBaseFee(FeatureBitset features)
void testAccountActivation(FeatureBitset features)
void testUntilFailure(FeatureBitset features)
static uint256 getCheckIndex(AccountID const &account, std::uint32_t uSequence)
void validateInnerTxn(jtx::Env &env, std::string const &batchID, TestLedgerData const &ledgerResult)
void testObjectCreateSequence(FeatureBitset features)
void testOnlyOne(FeatureBitset features)
static std::unique_ptr< Config > makeSmallQueueConfig(std::map< std::string, std::string > extraTxQ={}, std::map< std::string, std::string > extraVoting={})
void testAccountDelete(FeatureBitset features)
void testUnsortedBatchSigners(FeatureBitset features)
void testPseudoTxn(FeatureBitset features)
void testBadOuterFee(FeatureBitset features)
void testBatchSigCache(FeatureBitset features)
Immutable cryptographic account descriptor.
Definition jtx/Account.h:21
std::string const & human() const
Returns the human readable public key.
AccountID id() const
Returns the Account ID.
A transaction testing environment.
Definition Env.h:161
Application & app()
Definition Env.h:300
bool close(NetClock::time_point closeTime, std::optional< std::chrono::milliseconds > consensusDelay=std::nullopt)
Close and advance the ledger.
Definition Env.cpp:133
json::Value json(JsonValue &&jv, FN const &... fN)
Create JSON from parameters.
Definition Env.h:750
SLE::const_pointer le(Account const &account) const
Return an account root.
Definition Env.cpp:311
std::shared_ptr< ReadView const > closed()
Returns the last closed ledger.
Definition Env.cpp:127
void fund(bool setDefaultRipple, STAmount const &amount, Account const &account)
Definition Env.cpp:323
ParseFailureGuard getParseFailureGuard(bool b)
Gets an RAII guard to set and restore the parse failure flag.
Definition Env.h:596
std::uint32_t seq(Account const &account) const
Returns the next sequence number on account.
Definition Env.cpp:302
json::Value rpc(unsigned apiVersion, std::unordered_map< std::string, std::string > const &headers, std::string const &cmd, Args &&... args)
Execute an RPC command.
Definition Env.h:1056
JTx jt(JsonValue &&jv, FN const &... fN)
Create a JTx from parameters.
Definition Env.h:721
PrettyAmount balance(Account const &account) const
Returns the XRP balance on an account.
Definition Env.cpp:201
void trust(STAmount const &amount, Account const &account)
Establish trust lines.
Definition Env.cpp:354
JTx jtnofill(JsonValue &&jv, FN const &... fN)
Create a JTx from parameters.
Definition Env.h:735
void memoize(Account const &account)
Associate AccountID with account.
Definition Env.cpp:174
beast::Journal const journal
Definition Env.h:204
std::shared_ptr< OpenView const > current() const
Returns the current ledger.
Definition Env.h:377
NetClock::time_point now()
Returns the current network time.
Definition Env.h:326
Set the fee on a JTx.
Definition fee.h:20
Test helper for creating, mutating, and asserting MPT and confidential MPT ledger state.
Definition mpt.h:447
Set a multisignature on a JTx.
Definition multisign.h:53
Set the regular signature on a JTx.
Definition sig.h:19
Set the expected result code for a JTx The test will fail if the code doesn't match.
Definition ter.h:18
Set the flags on a JTx.
Definition txflags.h:14
Adds an inner Batch transaction to a JTx and autofills it.
Definition batch.h:66
Sets a nested multi-signature for a Batch transaction on a JTx.
Definition batch.h:142
Sets the Batch transaction signers on a JTx.
Definition batch.h:117
Set a ticket sequence on a JTx.
Definition ticket.h:36
T end(T... args)
T forward(T... args)
T make_pair(T... args)
T make_shared(T... args)
@ Array
array value (ordered list)
Definition json_value.h:28
@ Object
object value (collection of name/value pairs).
Definition json_value.h:29
TER valid(STTx const &tx, ReadView const &view, AccountID const &src, beast::Journal j)
Keylet loan(uint256 const &loanBrokerID, SeqProxy const &loanSeq) noexcept
Definition Indexes.cpp:573
Keylet check(AccountID const &id, SeqProxy const &seq) noexcept
A Check.
Definition Indexes.cpp:338
Keylet loanBroker(AccountID const &owner, SeqProxy const &seq) noexcept
Definition Indexes.cpp:567
Keylet account(AccountID const &id) noexcept
AccountID root.
Definition Indexes.cpp:198
json::Value outer(jtx::Account const &account, uint32_t seq, STAmount const &fee, std::uint32_t flags)
Build an outer Batch transaction JSON object.
Definition batch.cpp:53
XRPAmount calcBatchFee(jtx::Env const &env, uint32_t const &numSigners, uint32_t const &txns=0)
Calculate the expected outer Batch transaction fee.
Definition batch.cpp:35
json::Value create(A const &account, A const &dest, STAmount const &sendMax)
Create a check.
json::Value cash(jtx::Account const &dest, uint256 const &checkId, STAmount const &amount)
Cash a check requiring that a specific amount be delivered.
Definition check.cpp:15
json::Value set(jtx::Account const &account, jtx::Account const &authorize, std::vector< std::string > const &permissions)
Definition delegate.cpp:17
Deposit preauthorize operations.
Definition deposit.h:16
json::Value create(Account const &account, std::uint32_t count)
Create one of more tickets.
Definition ticket.cpp:16
static NoneT const kNone
Definition tags.h:9
json::Value pay(AccountID const &account, AccountID const &to, AnyAmount amount)
Create a payment.
Definition pay.cpp:14
json::Value regkey(Account const &account, DisabledT)
Disable the regular key.
Definition regkey.cpp:13
XrpT const XRP
Converts to XRP Issue or STAmount.
Definition amount.cpp:92
json::Value noop(Account const &account)
The null transaction.
Definition noop.h:14
void checkMetrics(Suite &test, jtx::Env &env, std::size_t expectedCount, std::optional< std::size_t > expectedMaxCount, std::size_t expectedInLedger, std::size_t expectedPerLedger, std::uint64_t expectedMinFeeLevel=kBaseFeeLevel.fee(), std::uint64_t expectedMedFeeLevel=kMinEscalationFeeLevel.fee(), std::source_location const location=std::source_location::current())
FeatureBitset testableAmendments()
Definition Env.h:92
json::Value acctdelete(Account const &account, Account const &dest)
Delete account.
void incLgrSeqForAccDel(jtx::Env &env, jtx::Account const &acc, std::uint32_t margin=0)
std::array< Account, 1+sizeof...(Args)> noripple(Account const &account, Args const &... args)
Designate accounts as no-ripple in Env::fund.
Definition Env.h:86
json::Value offer(Account const &account, STAmount const &takerPays, STAmount const &takerGets, std::uint32_t flags)
Create an offer.
Definition offer.cpp:14
std::unique_ptr< Config > envconfig()
creates and initializes a default configuration for jtx::Env
Definition envconfig.h:37
json::Value trust(Account const &account, STAmount const &amount, std::uint32_t flags)
Modify a trust line.
Definition trust.cpp:18
json::Value signers(Account const &account, std::uint32_t quorum, std::vector< Signer > const &v)
Definition multisign.cpp:31
json::Value fset(Account const &account, std::uint32_t on, std::uint32_t off=0)
Add and/or remove flag.
Definition flags.cpp:15
BEAST_DEFINE_TESTSUITE(AMMClawback, app, xrpl)
constexpr XRPAmount
Convert XRP to drops (integral types).
Definition TxTest.h:54
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
@ telINSUF_FEE_P
Definition TER.h:43
@ telENV_RPC_FAILED
Definition TER.h:54
@ telCAN_NOT_QUEUE
Definition TER.h:45
@ terPRE_SEQ
Definition TER.h:217
@ terQUEUED
Definition TER.h:221
bool set(T &target, std::string const &name, Section const &section)
Set a value from a configuration Section If the named value is not found or doesn't parse as a T,...
Issue const & xrpIssue()
Returns an asset specifier that represents XRP.
Definition Issue.h:108
constexpr FlagValue tfInnerBatchTxn
Definition TxFlags.h:44
constexpr std::size_t kMaxBatchSigners
The maximum number of batch signers.
Definition Protocol.h:448
@ SigBad
Signature is bad.
Definition apply.h:28
ApplyResult apply(ServiceRegistry &registry, OpenView &view, STTx const &tx, ApplyFlags flags, beast::Journal journal)
Apply a transaction to an OpenView.
Definition apply.cpp:122
std::string strHex(FwdIt begin, FwdIt end)
Definition strHex.h:13
std::pair< Validity, std::string > checkValidity(HashRouter &router, STTx const &tx, Rules const &rules)
Checks transaction signature and local checks.
Definition apply.cpp:36
constexpr TenthBips32 percentageToTenthBips(std::uint32_t percentage)
Definition Protocol.h:126
constexpr std::size_t kMaxBatchTxCount
The maximum number of transactions that can be in a batch.
Definition Protocol.h:443
constexpr HashRouterFlags kSfSiggood
Definition apply.cpp:29
@ tefBAD_QUORUM
Definition TER.h:172
@ tefMASTER_DISABLED
Definition TER.h:169
@ tefBAD_SIGNATURE
Definition TER.h:171
@ tefBAD_AUTH
Definition TER.h:161
@ tefNOT_MULTI_SIGNING
Definition TER.h:173
XRPAmount toDrops(FeeLevel< T > const &level, XRPAmount baseFee)
Definition TxQ.h:1003
TenthBips< std::uint32_t > TenthBips32
Definition Units.h:454
TenthBips< std::uint16_t > TenthBips16
Definition Units.h:453
std::string to_string(BaseUInt< Bits, Tag > const &a)
Definition base_uint.h:651
bool passesLocalChecks(STTx const &tx, std::string &)
Definition STTx.cpp:847
void finishMultiSigningData(AccountID const &signingID, Serializer &s)
Definition Sign.h:73
HashRouterFlags
Definition HashRouter.h:20
MPTID makeMptID(std::uint32_t const sequence, AccountID const &account)
Definition Indexes.cpp:184
@ TapNone
Definition ApplyView.h:28
@ 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
@ temMALFORMED
Definition TER.h:75
@ temARRAY_EMPTY
Definition TER.h:128
@ temSEQ_AND_TICKET
Definition TER.h:114
@ temDISABLED
Definition TER.h:102
@ temBAD_SIGNATURE
Definition TER.h:93
@ temINVALID_INNER_BATCH
Definition TER.h:131
@ temREDUNDANT
Definition TER.h:100
@ temBAD_SIGNER
Definition TER.h:103
Buffer sign(PublicKey const &pk, SecretKey const &sk, Slice const &message)
Generate a signature for a message.
TERSubset< CanCvtToTER > TER
Definition TER.h:647
@ tecNO_ENTRY
Definition TER.h:309
std::vector< unsigned char > Blob
Storage for linear binary data.
Definition Blob.h:11
void serializeBatch(Serializer &msg, AccountID const &outerAccount, std::uint32_t outerSeqValue, std::uint32_t const &flags, std::vector< uint256 > const &txids)
bool isPseudoTx(STObject const &tx)
Check whether a transaction is a pseudo-transaction.
Definition STTx.cpp:886
BaseUInt< 256 > uint256
Definition base_uint.h:580
uint256 TxID
A transaction identifier.
Definition Protocol.h:391
@ tesSUCCESS
Definition TER.h:245
constexpr bool any(HashRouterFlags flags)
Definition HashRouter.h:71
T parse(T... args)
T push_back(T... args)
T reserve(T... args)
T size(T... args)
uint256 key
Definition Keylet.h:21
static constexpr auto kMinLedgersToComputeSizeLimit
Definition Constants.h:128
static constexpr auto kRetrySequencePercent
Definition Constants.h:151
static constexpr auto kMaxLedgerCountsToStore
Definition Constants.h:121
static constexpr auto kMinimumQueueSize
Definition Constants.h:131
static constexpr auto kNormalConsensusIncreasePercent
Definition Constants.h:134
static constexpr auto kLedgersInQueue
Definition Constants.h:117
static constexpr auto kMinimumTxnInLedgerStandalone
Definition Constants.h:133
State information when preflighting a tx.
Definition Transactor.h:38
static constexpr auto kTransactionQueue
Definition Constants.h:66
std::optional< std::string > batchID
Execution context for applying a JSON transaction.
Definition JTx.h:27
std::shared_ptr< STTx const > stx
Definition JTx.h:37
json::Value jv
Definition JTx.h:28
Thrown when parse fails.
Definition utility.h:20
Set the sequence number on a JTx.
Definition seq.h:16