rippled
Loading...
Searching...
No Matches
Ticket_test.cpp
1#include <test/jtx.h>
2
3#include <xrpld/app/misc/Transaction.h>
4
5#include <xrpl/protocol/Feature.h>
6#include <xrpl/protocol/jss.h>
7
8namespace xrpl {
9
11{
15 void
17 {
18 using namespace std::string_literals;
19
20 Json::Value const& tx{env.tx()->getJson(JsonOptions::none)};
21 {
22 std::string const txType = tx[sfTransactionType.jsonName].asString();
23
24 if (!BEAST_EXPECTS(
25 txType == jss::TicketCreate, "Unexpected TransactionType: "s + txType))
26 return;
27 }
28
29 std::uint32_t const count = {tx[sfTicketCount.jsonName].asUInt()};
30 if (!BEAST_EXPECTS(count >= 1, "Unexpected ticket count: "s + std::to_string(count)))
31 return;
32
33 std::uint32_t const txSeq = {tx[sfSequence.jsonName].asUInt()};
34 std::string const account = tx[sfAccount.jsonName].asString();
35
36 Json::Value const& metadata = env.meta()->getJson(JsonOptions::none);
37 if (!BEAST_EXPECTS(
38 metadata.isMember(sfTransactionResult.jsonName) &&
39 metadata[sfTransactionResult.jsonName].asString() == "tesSUCCESS",
40 "Not metadata for successful TicketCreate."))
41 return;
42
43 BEAST_EXPECT(metadata.isMember(sfAffectedNodes.jsonName));
44 BEAST_EXPECT(metadata[sfAffectedNodes.jsonName].isArray());
45
46 bool directoryChanged = false;
47 std::uint32_t acctRootFinalSeq = {0};
49 ticketSeqs.reserve(count);
50 for (Json::Value const& node : metadata[sfAffectedNodes.jsonName])
51 {
52 if (node.isMember(sfModifiedNode.jsonName))
53 {
54 Json::Value const& modified = node[sfModifiedNode.jsonName];
55 std::string const entryType = modified[sfLedgerEntryType.jsonName].asString();
56 if (entryType == jss::AccountRoot)
57 {
58 auto const& previousFields = modified[sfPreviousFields.jsonName];
59 auto const& finalFields = modified[sfFinalFields.jsonName];
60 {
61 // Verify the account root Sequence did the right thing.
62 std::uint32_t const prevSeq = previousFields[sfSequence.jsonName].asUInt();
63
64 acctRootFinalSeq = finalFields[sfSequence.jsonName].asUInt();
65
66 if (txSeq == 0)
67 {
68 // Transaction used a TicketSequence.
69 BEAST_EXPECT(acctRootFinalSeq == prevSeq + count);
70 }
71 else
72 {
73 // Transaction used a (plain) Sequence.
74 BEAST_EXPECT(prevSeq == txSeq);
75 BEAST_EXPECT(acctRootFinalSeq == prevSeq + count + 1);
76 }
77 }
78
79 std::uint32_t const consumedTickets = {txSeq == 0u ? 1u : 0u};
80
81 // If...
82 // 1. The TicketCount is 1 and
83 // 2. A ticket was consumed by the ticket create, then
84 // 3. The final TicketCount did not change, so the
85 // previous TicketCount is not reported.
86 // But, since the count did not change, we know it equals
87 // the final Ticket count.
88 bool const unreportedPrevTicketCount = {count == 1 && txSeq == 0};
89
90 // Verify the OwnerCount did the right thing
91 if (unreportedPrevTicketCount)
92 {
93 // The number of Tickets should not have changed, so
94 // the previous OwnerCount should not be reported.
95 BEAST_EXPECT(!previousFields.isMember(sfOwnerCount.jsonName));
96 }
97 else
98 {
99 // Verify the OwnerCount did the right thing.
100 std::uint32_t const prevCount = {
101 previousFields[sfOwnerCount.jsonName].asUInt()};
102
103 std::uint32_t const finalCount = {
104 finalFields[sfOwnerCount.jsonName].asUInt()};
105
106 BEAST_EXPECT(prevCount + count - consumedTickets == finalCount);
107 }
108
109 // Verify TicketCount metadata.
110 BEAST_EXPECT(finalFields.isMember(sfTicketCount.jsonName));
111
112 if (unreportedPrevTicketCount)
113 {
114 // The number of Tickets should not have changed, so
115 // the previous TicketCount should not be reported.
116 BEAST_EXPECT(!previousFields.isMember(sfTicketCount.jsonName));
117 }
118 else
119 {
120 // If the TicketCount was previously present it
121 // should have been greater than zero.
122 std::uint32_t const startCount = {
123 previousFields.isMember(sfTicketCount.jsonName)
124 ? previousFields[sfTicketCount.jsonName].asUInt()
125 : 0u};
126
127 BEAST_EXPECT(
128 (startCount == 0u) ^ previousFields.isMember(sfTicketCount.jsonName));
129
130 BEAST_EXPECT(
131 startCount + count - consumedTickets ==
132 finalFields[sfTicketCount.jsonName]);
133 }
134 }
135 else if (entryType == jss::DirectoryNode)
136 {
137 directoryChanged = true;
138 }
139 else
140 {
141 fail("Unexpected modified node: "s + entryType, __FILE__, __LINE__);
142 }
143 }
144 else if (node.isMember(sfCreatedNode.jsonName))
145 {
146 Json::Value const& created = node[sfCreatedNode.jsonName];
147 std::string const entryType = created[sfLedgerEntryType.jsonName].asString();
148 if (entryType == jss::Ticket)
149 {
150 auto const& newFields = created[sfNewFields.jsonName];
151
152 BEAST_EXPECT(newFields[sfAccount.jsonName].asString() == account);
153 ticketSeqs.push_back(newFields[sfTicketSequence.jsonName].asUInt());
154 }
155 else if (entryType == jss::DirectoryNode)
156 {
157 directoryChanged = true;
158 }
159 else
160 {
161 fail("Unexpected created node: "s + entryType, __FILE__, __LINE__);
162 }
163 }
164 else if (node.isMember(sfDeletedNode.jsonName))
165 {
166 Json::Value const& deleted = node[sfDeletedNode.jsonName];
167 std::string const entryType = deleted[sfLedgerEntryType.jsonName].asString();
168
169 if (entryType == jss::Ticket)
170 {
171 // Verify the transaction's Sequence == 0.
172 BEAST_EXPECT(txSeq == 0);
173
174 // Verify the account of the deleted ticket.
175 auto const& finalFields = deleted[sfFinalFields.jsonName];
176 BEAST_EXPECT(finalFields[sfAccount.jsonName].asString() == account);
177
178 // Verify the deleted ticket has the right TicketSequence.
179 BEAST_EXPECT(
180 finalFields[sfTicketSequence.jsonName].asUInt() ==
181 tx[sfTicketSequence.jsonName].asUInt());
182 }
183 }
184 else
185 {
186 fail("Unexpected node type in TicketCreate metadata.", __FILE__, __LINE__);
187 }
188 }
189 BEAST_EXPECT(directoryChanged);
190
191 // Verify that all the expected Tickets were created.
192 BEAST_EXPECT(ticketSeqs.size() == count);
193 std::sort(ticketSeqs.begin(), ticketSeqs.end());
194 BEAST_EXPECT(std::adjacent_find(ticketSeqs.begin(), ticketSeqs.end()) == ticketSeqs.end());
195 BEAST_EXPECT(*ticketSeqs.rbegin() == acctRootFinalSeq - 1);
196 }
197
203 void
205 {
206 Json::Value const& tx{env.tx()->getJson(JsonOptions::none)};
207
208 // Verify that the transaction includes a TicketSequence.
209
210 // Capture that TicketSequence.
211 // Capture the Account from the transaction
212
213 // Verify that metadata indicates a tec or a tesSUCCESS.
214
215 // Walk affected nodes:
216 //
217 // For each deleted node, see if it is a Ticket node. If it is
218 // a Ticket Node being deleted, then assert that the...
219 //
220 // Account == the transaction Account &&
221 // TicketSequence == the transaction TicketSequence
222 //
223 // If a modified node is an AccountRoot, see if it is the transaction
224 // Account. If it is then verify the TicketCount decreased by one.
225 // If the old TicketCount was 1, then the TicketCount field should be
226 // removed from the final fields of the AccountRoot.
227 //
228 // After looking at all nodes verify that exactly one Ticket node
229 // was deleted.
230 BEAST_EXPECT(tx[sfSequence.jsonName].asUInt() == 0);
231 std::string const account{tx[sfAccount.jsonName].asString()};
232 if (!BEAST_EXPECTS(
233 tx.isMember(sfTicketSequence.jsonName),
234 "Not metadata for a ticket consuming transaction."))
235 return;
236
237 std::uint32_t const ticketSeq{tx[sfTicketSequence.jsonName].asUInt()};
238
239 Json::Value const& metadata{env.meta()->getJson(JsonOptions::none)};
240 if (!BEAST_EXPECTS(
241 metadata.isMember(sfTransactionResult.jsonName),
242 "Metadata is missing TransactionResult."))
243 return;
244
245 {
246 std::string const transactionResult{metadata[sfTransactionResult.jsonName].asString()};
247 if (!BEAST_EXPECTS(
248 transactionResult == "tesSUCCESS" ||
249 transactionResult.compare(0, 3, "tec") == 0,
250 transactionResult + " neither tesSUCCESS nor tec"))
251 return;
252 }
253
254 BEAST_EXPECT(metadata.isMember(sfAffectedNodes.jsonName));
255 BEAST_EXPECT(metadata[sfAffectedNodes.jsonName].isArray());
256
257 bool acctRootFound{false};
258 std::uint32_t acctRootSeq{0};
259 int ticketsRemoved{0};
260 for (Json::Value const& node : metadata[sfAffectedNodes.jsonName])
261 {
262 if (node.isMember(sfModifiedNode.jsonName))
263 {
264 Json::Value const& modified{node[sfModifiedNode.jsonName]};
265 std::string const entryType = modified[sfLedgerEntryType.jsonName].asString();
266 if (entryType == "AccountRoot" &&
267 modified[sfFinalFields.jsonName][sfAccount.jsonName].asString() == account)
268 {
269 acctRootFound = true;
270
271 auto const& previousFields = modified[sfPreviousFields.jsonName];
272 auto const& finalFields = modified[sfFinalFields.jsonName];
273
274 acctRootSeq = finalFields[sfSequence.jsonName].asUInt();
275
276 // Check that the TicketCount was present and decremented
277 // by 1. If it decremented to zero, then the field should
278 // be gone.
279 if (!BEAST_EXPECTS(
280 previousFields.isMember(sfTicketCount.jsonName),
281 "AccountRoot previous is missing TicketCount"))
282 return;
283
284 std::uint32_t const prevTicketCount =
285 previousFields[sfTicketCount.jsonName].asUInt();
286
287 BEAST_EXPECT(prevTicketCount > 0);
288 if (prevTicketCount == 1)
289 {
290 BEAST_EXPECT(!finalFields.isMember(sfTicketCount.jsonName));
291 }
292 else
293 {
294 BEAST_EXPECT(
295 finalFields.isMember(sfTicketCount.jsonName) &&
296 finalFields[sfTicketCount.jsonName].asUInt() == prevTicketCount - 1);
297 }
298 }
299 }
300 else if (node.isMember(sfDeletedNode.jsonName))
301 {
302 Json::Value const& deleted{node[sfDeletedNode.jsonName]};
303 std::string const entryType{deleted[sfLedgerEntryType.jsonName].asString()};
304
305 if (entryType == jss::Ticket)
306 {
307 // Verify the account of the deleted ticket.
308 BEAST_EXPECT(
309 deleted[sfFinalFields.jsonName][sfAccount.jsonName].asString() == account);
310
311 // Verify the deleted ticket has the right TicketSequence.
312 BEAST_EXPECT(
313 deleted[sfFinalFields.jsonName][sfTicketSequence.jsonName].asUInt() ==
314 ticketSeq);
315
316 ++ticketsRemoved;
317 }
318 }
319 }
320 BEAST_EXPECT(acctRootFound);
321 BEAST_EXPECT(ticketsRemoved == 1);
322 BEAST_EXPECT(ticketSeq < acctRootSeq);
323 }
324
325 void
327 {
328 testcase("Create Tickets that fail Preflight");
329
330 using namespace test::jtx;
331 Env env{*this};
332
333 Account const master{env.master};
334
335 // Exercise boundaries on count.
336 env(ticket::create(master, 0), ter(temINVALID_COUNT));
337 env(ticket::create(master, 251), ter(temINVALID_COUNT));
338
339 // Exercise fees.
340 std::uint32_t const ticketSeq_A{env.seq(master) + 1};
341 env(ticket::create(master, 1), fee(XRP(10)));
343 env.close();
344 env.require(owners(master, 1), tickets(master, 1));
345
346 env(ticket::create(master, 1), fee(XRP(-1)), ter(temBAD_FEE));
347
348 // Exercise flags.
349 std::uint32_t const ticketSeq_B{env.seq(master) + 1};
350 env(ticket::create(master, 1), txflags(tfFullyCanonicalSig));
352 env.close();
353 env.require(owners(master, 2), tickets(master, 2));
354
355 env(ticket::create(master, 1), txflags(tfSell), ter(temINVALID_FLAG));
356 env.close();
357 env.require(owners(master, 2), tickets(master, 2));
358
359 // We successfully created 1 ticket earlier. Verify that we can
360 // create 250 tickets in one shot. We must consume one ticket first.
361 env(noop(master), ticket::use(ticketSeq_A));
363 env.close();
364 env.require(owners(master, 1), tickets(master, 1));
365
366 env(ticket::create(master, 250), ticket::use(ticketSeq_B));
368 env.close();
369 env.require(owners(master, 250), tickets(master, 250));
370 }
371
372 void
374 {
375 testcase("Create Tickets that fail Preclaim");
376
377 using namespace test::jtx;
378 {
379 // Create tickets on a non-existent account.
380 Env env{*this};
381 Account const alice{"alice"};
382 env.memoize(alice);
383
384 env(ticket::create(alice, 1), json(jss::Sequence, 1), ter(terNO_ACCOUNT));
385 }
386 {
387 // Exceed the threshold where tickets can no longer be
388 // added to an account.
389 Env env{*this};
390 Account const alice{"alice"};
391
392 env.fund(XRP(100000), alice);
393
394 std::uint32_t const ticketSeq{env.seq(alice) + 1};
395 env(ticket::create(alice, 250));
397 env.close();
398 env.require(owners(alice, 250), tickets(alice, 250));
399
400 // Note that we can add one more ticket while consuming a ticket
401 // because the final result is still 250 tickets.
402 env(ticket::create(alice, 1), ticket::use(ticketSeq + 0));
404 env.close();
405 env.require(owners(alice, 250), tickets(alice, 250));
406
407 // Adding one more ticket will exceed the threshold.
408 env(ticket::create(alice, 2), ticket::use(ticketSeq + 1), ter(tecDIR_FULL));
409 env.close();
410 env.require(owners(alice, 249), tickets(alice, 249));
411
412 // Now we can successfully add one more ticket.
413 env(ticket::create(alice, 2), ticket::use(ticketSeq + 2));
415 env.close();
416 env.require(owners(alice, 250), tickets(alice, 250));
417
418 // Since we're at 250, we can't add another ticket using a
419 // sequence.
420 env(ticket::create(alice, 1), ter(tecDIR_FULL));
421 env.close();
422 env.require(owners(alice, 250), tickets(alice, 250));
423 }
424 {
425 // Explore exceeding the ticket threshold from another angle.
426 Env env{*this};
427 Account const alice{"alice"};
428
429 env.fund(XRP(100000), alice);
430 env.close();
431
432 std::uint32_t const ticketSeq_AB{env.seq(alice) + 1};
433 env(ticket::create(alice, 2));
435 env.close();
436 env.require(owners(alice, 2), tickets(alice, 2));
437
438 // Adding 250 tickets (while consuming one) will exceed the
439 // threshold.
440 env(ticket::create(alice, 250), ticket::use(ticketSeq_AB + 0), ter(tecDIR_FULL));
441 env.close();
442 env.require(owners(alice, 1), tickets(alice, 1));
443
444 // Adding 250 tickets (without consuming one) will exceed the
445 // threshold.
446 env(ticket::create(alice, 250), ter(tecDIR_FULL));
447 env.close();
448 env.require(owners(alice, 1), tickets(alice, 1));
449
450 // Alice can now add 250 tickets while consuming one.
451 env(ticket::create(alice, 250), ticket::use(ticketSeq_AB + 1));
453 env.close();
454 env.require(owners(alice, 250), tickets(alice, 250));
455 }
456 }
457
458 void
460 {
461 testcase("Create Ticket Insufficient Reserve");
462
463 using namespace test::jtx;
464 Env env{*this};
465 Account const alice{"alice"};
466
467 // Fund alice not quite enough to make the reserve for a Ticket.
468 env.fund(env.current()->fees().accountReserve(1) - drops(1), alice);
469 env.close();
470
471 env(ticket::create(alice, 1), ter(tecINSUFFICIENT_RESERVE));
472 env.close();
473 env.require(owners(alice, 0), tickets(alice, 0));
474
475 // Give alice enough to exactly meet the reserve for one Ticket.
476 env(pay(env.master, alice, env.current()->fees().accountReserve(1) - env.balance(alice)));
477 env.close();
478
479 env(ticket::create(alice, 1));
481 env.close();
482 env.require(owners(alice, 1), tickets(alice, 1));
483
484 // Give alice not quite enough to make the reserve for a total of
485 // 250 Tickets.
486 env(
487 pay(env.master,
488 alice,
489 env.current()->fees().accountReserve(250) - drops(1) - env.balance(alice)));
490 env.close();
491
492 // alice doesn't quite have the reserve for a total of 250
493 // Tickets, so the transaction fails.
494 env(ticket::create(alice, 249), ter(tecINSUFFICIENT_RESERVE));
495 env.close();
496 env.require(owners(alice, 1), tickets(alice, 1));
497
498 // Give alice enough so she can make the reserve for all 250
499 // Tickets.
500 env(pay(env.master, alice, env.current()->fees().accountReserve(250) - env.balance(alice)));
501 env.close();
502
503 std::uint32_t const ticketSeq{env.seq(alice) + 1};
504 env(ticket::create(alice, 249));
506 env.close();
507 env.require(owners(alice, 250), tickets(alice, 250));
508 BEAST_EXPECT(ticketSeq + 249 == env.seq(alice));
509 }
510
511 void
513 {
514 testcase("Using Tickets");
515
516 using namespace test::jtx;
517 Env env{*this};
518 Account const alice{"alice"};
519
520 env.fund(XRP(10000), alice);
521 env.close();
522
523 // Successfully create tickets (using a sequence)
524 std::uint32_t const ticketSeq_AB{env.seq(alice) + 1};
525 env(ticket::create(alice, 2));
527 env.close();
528 env.require(owners(alice, 2), tickets(alice, 2));
529 BEAST_EXPECT(ticketSeq_AB + 2 == env.seq(alice));
530
531 // You can use a ticket to create one ticket ...
532 std::uint32_t const ticketSeq_C{env.seq(alice)};
533 env(ticket::create(alice, 1), ticket::use(ticketSeq_AB + 0));
535 env.close();
536 env.require(owners(alice, 2), tickets(alice, 2));
537 BEAST_EXPECT(ticketSeq_C + 1 == env.seq(alice));
538
539 // ... you can use a ticket to create multiple tickets ...
540 std::uint32_t const ticketSeq_DE{env.seq(alice)};
541 env(ticket::create(alice, 2), ticket::use(ticketSeq_AB + 1));
543 env.close();
544 env.require(owners(alice, 3), tickets(alice, 3));
545 BEAST_EXPECT(ticketSeq_DE + 2 == env.seq(alice));
546
547 // ... and you can use a ticket for other things.
548 env(noop(alice), ticket::use(ticketSeq_DE + 0));
550 env.close();
551 env.require(owners(alice, 2), tickets(alice, 2));
552 BEAST_EXPECT(ticketSeq_DE + 2 == env.seq(alice));
553
554 env(pay(alice, env.master, XRP(20)), ticket::use(ticketSeq_DE + 1));
556 env.close();
557 env.require(owners(alice, 1), tickets(alice, 1));
558 BEAST_EXPECT(ticketSeq_DE + 2 == env.seq(alice));
559
560 env(trust(alice, env.master["USD"](20)), ticket::use(ticketSeq_C));
562 env.close();
563 env.require(owners(alice, 1), tickets(alice, 0));
564 BEAST_EXPECT(ticketSeq_DE + 2 == env.seq(alice));
565
566 // Attempt to use a ticket that has already been used.
567 env(noop(alice), ticket::use(ticketSeq_C), ter(tefNO_TICKET));
568 env.close();
569
570 // Attempt to use a ticket from the future.
571 std::uint32_t const ticketSeq_F{env.seq(alice) + 1};
572 env(noop(alice), ticket::use(ticketSeq_F), ter(terPRE_TICKET));
573 env.close();
574
575 // Now create the ticket. The retry will consume the new ticket.
576 env(ticket::create(alice, 1));
578 env.close();
579 env.require(owners(alice, 1), tickets(alice, 0));
580 BEAST_EXPECT(ticketSeq_F + 1 == env.seq(alice));
581
582 // Try a transaction that combines consuming a ticket with
583 // AccountTxnID.
584 std::uint32_t const ticketSeq_G{env.seq(alice) + 1};
585 env(ticket::create(alice, 1));
587 env.close();
588
589 env(noop(alice),
590 ticket::use(ticketSeq_G),
591 json(R"({"AccountTxnID": "0"})"),
592 ter(temINVALID));
593 env.close();
594 env.require(owners(alice, 2), tickets(alice, 1));
595 }
596
597 void
599 {
600 // The Transaction database keeps each transaction's sequence number
601 // in an entry (called "FromSeq"). Until the introduction of tickets
602 // each sequence stored for a given account would always be unique.
603 // With the advent of tickets there could be lots of entries
604 // with zero.
605 //
606 // We really don't expect those zeros to cause any problems since
607 // there are no indexes that use "FromSeq". But it still seems
608 // prudent to exercise this a bit to see if tickets cause any obvious
609 // harm.
610 testcase("Transaction Database With Tickets");
611
612 using namespace test::jtx;
613 Env env{*this};
614 Account const alice{"alice"};
615
616 env.fund(XRP(10000), alice);
617 env.close();
618
619 // Lambda that returns the hash of the most recent transaction.
620 auto getTxID = [&env, this]() -> uint256 {
621 std::shared_ptr<STTx const> const tx{env.tx()};
622 if (!BEAST_EXPECTS(tx, "Transaction not found"))
623 Throw<std::invalid_argument>("Invalid transaction ID");
624
625 return tx->getTransactionID();
626 };
627
628 // A note about the metadata created by these transactions.
629 //
630 // We _could_ check the metadata on these transactions. However
631 // checking the metadata has the side effect of advancing the ledger.
632 // So if we check the metadata we don't get to look at several
633 // transactions in the same ledger. Therefore a specific choice was
634 // made to not check the metadata on these transactions.
635
636 // Successfully create several tickets (using a sequence).
637 std::uint32_t ticketSeq{env.seq(alice)};
638 static constexpr std::uint32_t ticketCount{10};
639 env(ticket::create(alice, ticketCount));
640 uint256 const txHash_1{getTxID()};
641
642 // Just for grins use the tickets in reverse from largest to smallest.
643 ticketSeq += ticketCount;
644 env(noop(alice), ticket::use(--ticketSeq));
645 uint256 const txHash_2{getTxID()};
646
647 env(pay(alice, env.master, XRP(200)), ticket::use(--ticketSeq));
648 uint256 const txHash_3{getTxID()};
649
650 env(deposit::auth(alice, env.master), ticket::use(--ticketSeq));
651 uint256 const txHash_4{getTxID()};
652
653 // Close the ledger so we look at transactions from a couple of
654 // different ledgers.
655 env.close();
656
657 env(pay(alice, env.master, XRP(300)), ticket::use(--ticketSeq));
658 uint256 const txHash_5{getTxID()};
659
660 env(pay(alice, env.master, XRP(400)), ticket::use(--ticketSeq));
661 uint256 const txHash_6{getTxID()};
662
663 env(deposit::unauth(alice, env.master), ticket::use(--ticketSeq));
664 uint256 const txHash_7{getTxID()};
665
666 env(noop(alice), ticket::use(--ticketSeq));
667 uint256 const txHash_8{getTxID()};
668
669 env.close();
670
671 // Checkout what's in the Transaction database. We go straight
672 // to the database. Most of our interfaces cache transactions
673 // in memory. So if we use normal interfaces we would get the
674 // transactions from memory rather than from the database.
675
676 // Lambda to verify a transaction pulled from the Transaction database.
677 auto checkTxFromDB = [&env, this](
678 uint256 const& txID,
679 std::uint32_t ledgerSeq,
680 std::uint32_t txSeq,
682 TxType txType) {
683 error_code_i txErrCode{rpcSUCCESS};
684
687 Transaction::load(txID, env.app(), txErrCode);
688
689 BEAST_EXPECT(txErrCode == rpcSUCCESS);
690 if (auto txPtr = std::get_if<TxPair>(&maybeTx))
691 {
692 std::shared_ptr<Transaction> const& tx = txPtr->first;
693 BEAST_EXPECT(tx->getLedger() == ledgerSeq);
694 std::shared_ptr<STTx const> const& sttx = tx->getSTransaction();
695 BEAST_EXPECT((*sttx)[sfSequence] == txSeq);
696 if (ticketSeq)
697 BEAST_EXPECT((*sttx)[sfTicketSequence] == *ticketSeq);
698 BEAST_EXPECT((*sttx)[sfTransactionType] == txType);
699 }
700 else
701 {
702 fail("Expected transaction was not found");
703 }
704 };
705
706 // txID ledgerSeq txSeq ticketSeq txType
707 checkTxFromDB(txHash_1, 4, 4, {}, ttTICKET_CREATE);
708 checkTxFromDB(txHash_2, 4, 0, 13, ttACCOUNT_SET);
709 checkTxFromDB(txHash_3, 4, 0, 12, ttPAYMENT);
710 checkTxFromDB(txHash_4, 4, 0, 11, ttDEPOSIT_PREAUTH);
711
712 checkTxFromDB(txHash_5, 5, 0, 10, ttPAYMENT);
713 checkTxFromDB(txHash_6, 5, 0, 9, ttPAYMENT);
714 checkTxFromDB(txHash_7, 5, 0, 8, ttDEPOSIT_PREAUTH);
715 checkTxFromDB(txHash_8, 5, 0, 7, ttACCOUNT_SET);
716 }
717
718 void
720 {
721 // The sign and the submit RPC commands automatically fill in the
722 // Sequence field of a transaction if none is provided. If a
723 // TicketSequence is provided in the transaction, then the
724 // auto-filled Sequence should be zero.
725 testcase("Sign with TicketSequence");
726
727 using namespace test::jtx;
728 Env env{*this};
729 Account const alice{"alice"};
730
731 env.fund(XRP(10000), alice);
732 env.close();
733
734 // Successfully create tickets (using a sequence)
735 std::uint32_t const ticketSeq = env.seq(alice) + 1;
736 env(ticket::create(alice, 2));
738 env.close();
739 env.require(owners(alice, 2), tickets(alice, 2));
740 BEAST_EXPECT(ticketSeq + 2 == env.seq(alice));
741
742 {
743 // Test that the "sign" RPC command fills in a "Sequence": 0 field
744 // if none is provided.
745
746 // Create a noop transaction using a TicketSequence but don't fill
747 // in the Sequence field.
749 tx[jss::tx_json] = noop(alice);
750 tx[jss::tx_json][sfTicketSequence.jsonName] = ticketSeq;
751 tx[jss::secret] = toBase58(generateSeed("alice"));
752
753 // Verify that there is no "Sequence" field.
754 BEAST_EXPECT(!tx[jss::tx_json].isMember(sfSequence.jsonName));
755
756 // Call the "sign" RPC command and see the "Sequence": 0 field
757 // filled in.
758 Json::Value jr = env.rpc("json", "sign", to_string(tx));
759
760 // Verify that "sign" inserted a "Sequence": 0 field.
761 if (BEAST_EXPECT(jr[jss::result][jss::tx_json].isMember(sfSequence.jsonName)))
762 {
763 BEAST_EXPECT(jr[jss::result][jss::tx_json][sfSequence.jsonName] == 0);
764 }
765
766 // "sign" should not have consumed any of alice's tickets.
767 env.close();
768 env.require(owners(alice, 2), tickets(alice, 2));
769
770 // "submit" the signed blob and see one of alice's tickets consumed.
771 env.rpc("submit", jr[jss::result][jss::tx_blob].asString());
772 env.close();
773 env.require(owners(alice, 1), tickets(alice, 1));
774 }
775 {
776 // Test that the "submit" RPC command fills in a "Sequence": 0
777 // field if none is provided.
778
779 // Create a noop transaction using a TicketSequence but don't fill
780 // in the Sequence field.
782 tx[jss::tx_json] = noop(alice);
783 tx[jss::tx_json][sfTicketSequence.jsonName] = ticketSeq + 1;
784 tx[jss::secret] = toBase58(generateSeed("alice"));
785
786 // Verify that there is no "Sequence" field.
787 BEAST_EXPECT(!tx[jss::tx_json].isMember(sfSequence.jsonName));
788
789 // Call the "submit" RPC command and see the "Sequence": 0 field
790 // filled in.
791 Json::Value jr = env.rpc("json", "submit", to_string(tx));
792
793 // Verify that "submit" inserted a "Sequence": 0 field.
794 if (BEAST_EXPECT(jr[jss::result][jss::tx_json].isMember(sfSequence.jsonName)))
795 {
796 BEAST_EXPECT(jr[jss::result][jss::tx_json][sfSequence.jsonName] == 0);
797 }
798
799 // "submit" should have consumed the last of alice's tickets.
800 env.close();
801 env.require(owners(alice, 0), tickets(alice, 0));
802 }
803 }
804
805 void
807 {
808 using namespace test::jtx;
809
810 // It is an error if a transaction contains a non-zero Sequence field
811 // and a TicketSequence field. Verify that the error is detected.
812 testcase("Fix both Seq and Ticket");
813
814 Env env{*this, testable_amendments()};
815 Account const alice{"alice"};
816
817 env.fund(XRP(10000), alice);
818 env.close();
819
820 // Create a ticket.
821 std::uint32_t const ticketSeq = env.seq(alice) + 1;
822 env(ticket::create(alice, 1));
823 env.close();
824 env.require(owners(alice, 1), tickets(alice, 1));
825 BEAST_EXPECT(ticketSeq + 1 == env.seq(alice));
826
827 // Create a transaction that includes both a ticket and a non-zero
828 // sequence number. The transaction fails with temSEQ_AND_TICKET.
829 env(noop(alice), ticket::use(ticketSeq), seq(env.seq(alice)), ter(temSEQ_AND_TICKET));
830 env.close();
831
832 // Verify that the transaction failed by looking at alice's
833 // sequence number and tickets.
834 env.require(owners(alice, 1), tickets(alice, 1));
835 BEAST_EXPECT(ticketSeq + 1 == env.seq(alice));
836 }
837
838public:
839 void
850};
851
852BEAST_DEFINE_TESTSUITE(Ticket, app, xrpl);
853
854} // namespace xrpl
T adjacent_find(T... args)
T begin(T... args)
Represents a JSON value.
Definition json_value.h:130
bool isArray() const
std::string asString() const
Returns the unquoted string value.
bool isMember(char const *key) const
Return true if the object has a member named key.
A testsuite class.
Definition suite.h:51
testcase_t testcase
Memberspace for declaring test cases.
Definition suite.h:150
void fail(String const &reason, char const *file, int line)
Record a failure.
Definition suite.h:519
void testTicketCreatePreflightFail()
void testTicketInsufficientReserve()
void run() override
Runs the suite.
void checkTicketConsumeMeta(test::jtx::Env &env)
Validate metadata for a ticket using transaction.
void checkTicketCreateMeta(test::jtx::Env &env)
Validate metadata for a successful TicketCreate transaction.
void testTransactionDatabaseWithTickets()
void testFixBothSeqAndTicket()
void testSignWithTicketSequence()
void testTicketCreatePreclaimFail()
static std::variant< std::pair< std::shared_ptr< Transaction >, std::shared_ptr< TxMeta > >, TxSearched > load(uint256 const &id, Application &app, error_code_i &ec)
A transaction testing environment.
Definition Env.h:122
std::shared_ptr< STObject const > meta()
Return metadata for the last JTx.
Definition Env.cpp:483
std::shared_ptr< STTx const > tx() const
Return the tx data for the last JTx.
Definition Env.cpp:505
T end(T... args)
T is_same_v
@ objectValue
object value (collection of name/value pairs).
Definition json_value.h:26
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
@ terNO_ACCOUNT
Definition TER.h:197
@ terPRE_TICKET
Definition TER.h:206
TxType
Transaction type identifiers.
Definition TxFormats.h:39
std::string to_string(base_uint< Bits, Tag > const &a)
Definition base_uint.h:602
@ tefNO_TICKET
Definition TER.h:165
std::string toBase58(AccountID const &v)
Convert AccountID to base58 checked string.
Definition AccountID.cpp:92
Seed generateSeed(std::string const &passPhrase)
Generate a seed deterministically.
Definition Seed.cpp:57
@ temBAD_FEE
Definition TER.h:72
@ temINVALID
Definition TER.h:90
@ temINVALID_FLAG
Definition TER.h:91
@ temINVALID_COUNT
Definition TER.h:101
@ temSEQ_AND_TICKET
Definition TER.h:106
@ tecDIR_FULL
Definition TER.h:268
@ tecINSUFFICIENT_RESERVE
Definition TER.h:288
error_code_i
Definition ErrorCodes.h:20
@ rpcSUCCESS
Definition ErrorCodes.h:24
constexpr FlagValue tfFullyCanonicalSig
Definition TxFlags.h:40
T push_back(T... args)
T rbegin(T... args)
T reserve(T... args)
T size(T... args)
T sort(T... args)
T to_string(T... args)