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