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