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