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