mirror of
https://github.com/XRPLF/rippled.git
synced 2025-11-27 22:45:52 +00:00
Amendments activated for more than 2 years can be retired. This change retires the TicketBatch amendment.
914 lines
35 KiB
C++
914 lines
35 KiB
C++
#include <test/jtx.h>
|
|
|
|
#include <xrpld/app/misc/Transaction.h>
|
|
|
|
#include <xrpl/protocol/Feature.h>
|
|
#include <xrpl/protocol/jss.h>
|
|
|
|
namespace ripple {
|
|
|
|
class Ticket_test : public beast::unit_test::suite
|
|
{
|
|
/// @brief Validate metadata for a successful CreateTicket transaction.
|
|
///
|
|
/// @param env current jtx env (tx and meta are extracted using it)
|
|
void
|
|
checkTicketCreateMeta(test::jtx::Env& env)
|
|
{
|
|
using namespace std::string_literals;
|
|
|
|
Json::Value const& tx{env.tx()->getJson(JsonOptions::none)};
|
|
{
|
|
std::string const txType =
|
|
tx[sfTransactionType.jsonName].asString();
|
|
|
|
if (!BEAST_EXPECTS(
|
|
txType == jss::TicketCreate,
|
|
"Unexpected TransactionType: "s + txType))
|
|
return;
|
|
}
|
|
|
|
std::uint32_t const count = {tx[sfTicketCount.jsonName].asUInt()};
|
|
if (!BEAST_EXPECTS(
|
|
count >= 1,
|
|
"Unexpected ticket count: "s + std::to_string(count)))
|
|
return;
|
|
|
|
std::uint32_t const txSeq = {tx[sfSequence.jsonName].asUInt()};
|
|
std::string const account = tx[sfAccount.jsonName].asString();
|
|
|
|
Json::Value const& metadata = env.meta()->getJson(JsonOptions::none);
|
|
if (!BEAST_EXPECTS(
|
|
metadata.isMember(sfTransactionResult.jsonName) &&
|
|
metadata[sfTransactionResult.jsonName].asString() ==
|
|
"tesSUCCESS",
|
|
"Not metadata for successful TicketCreate."))
|
|
return;
|
|
|
|
BEAST_EXPECT(metadata.isMember(sfAffectedNodes.jsonName));
|
|
BEAST_EXPECT(metadata[sfAffectedNodes.jsonName].isArray());
|
|
|
|
bool directoryChanged = false;
|
|
std::uint32_t acctRootFinalSeq = {0};
|
|
std::vector<std::uint32_t> ticketSeqs;
|
|
ticketSeqs.reserve(count);
|
|
for (Json::Value const& node : metadata[sfAffectedNodes.jsonName])
|
|
{
|
|
if (node.isMember(sfModifiedNode.jsonName))
|
|
{
|
|
Json::Value const& modified = node[sfModifiedNode.jsonName];
|
|
std::string const entryType =
|
|
modified[sfLedgerEntryType.jsonName].asString();
|
|
if (entryType == jss::AccountRoot)
|
|
{
|
|
auto const& previousFields =
|
|
modified[sfPreviousFields.jsonName];
|
|
auto const& finalFields = modified[sfFinalFields.jsonName];
|
|
{
|
|
// Verify the account root Sequence did the right thing.
|
|
std::uint32_t const prevSeq =
|
|
previousFields[sfSequence.jsonName].asUInt();
|
|
|
|
acctRootFinalSeq =
|
|
finalFields[sfSequence.jsonName].asUInt();
|
|
|
|
if (txSeq == 0)
|
|
{
|
|
// Transaction used a TicketSequence.
|
|
BEAST_EXPECT(acctRootFinalSeq == prevSeq + count);
|
|
}
|
|
else
|
|
{
|
|
// Transaction used a (plain) Sequence.
|
|
BEAST_EXPECT(prevSeq == txSeq);
|
|
BEAST_EXPECT(
|
|
acctRootFinalSeq == prevSeq + count + 1);
|
|
}
|
|
}
|
|
|
|
std::uint32_t const consumedTickets = {
|
|
txSeq == 0u ? 1u : 0u};
|
|
|
|
// If...
|
|
// 1. The TicketCount is 1 and
|
|
// 2. A ticket was consumed by the ticket create, then
|
|
// 3. The final TicketCount did not change, so the
|
|
// previous TicketCount is not reported.
|
|
// But, since the count did not change, we know it equals
|
|
// the final Ticket count.
|
|
bool const unreportedPrevTicketCount = {
|
|
count == 1 && txSeq == 0};
|
|
|
|
// Verify the OwnerCount did the right thing
|
|
if (unreportedPrevTicketCount)
|
|
{
|
|
// The number of Tickets should not have changed, so
|
|
// the previous OwnerCount should not be reported.
|
|
BEAST_EXPECT(
|
|
!previousFields.isMember(sfOwnerCount.jsonName));
|
|
}
|
|
else
|
|
{
|
|
// Verify the OwnerCount did the right thing.
|
|
std::uint32_t const prevCount = {
|
|
previousFields[sfOwnerCount.jsonName].asUInt()};
|
|
|
|
std::uint32_t const finalCount = {
|
|
finalFields[sfOwnerCount.jsonName].asUInt()};
|
|
|
|
BEAST_EXPECT(
|
|
prevCount + count - consumedTickets == finalCount);
|
|
}
|
|
|
|
// Verify TicketCount metadata.
|
|
BEAST_EXPECT(finalFields.isMember(sfTicketCount.jsonName));
|
|
|
|
if (unreportedPrevTicketCount)
|
|
{
|
|
// The number of Tickets should not have changed, so
|
|
// the previous TicketCount should not be reported.
|
|
BEAST_EXPECT(
|
|
!previousFields.isMember(sfTicketCount.jsonName));
|
|
}
|
|
else
|
|
{
|
|
// If the TicketCount was previously present it
|
|
// should have been greater than zero.
|
|
std::uint32_t const startCount = {
|
|
previousFields.isMember(sfTicketCount.jsonName)
|
|
? previousFields[sfTicketCount.jsonName]
|
|
.asUInt()
|
|
: 0u};
|
|
|
|
BEAST_EXPECT(
|
|
(startCount == 0u) ^
|
|
previousFields.isMember(sfTicketCount.jsonName));
|
|
|
|
BEAST_EXPECT(
|
|
startCount + count - consumedTickets ==
|
|
finalFields[sfTicketCount.jsonName]);
|
|
}
|
|
}
|
|
else if (entryType == jss::DirectoryNode)
|
|
{
|
|
directoryChanged = true;
|
|
}
|
|
else
|
|
{
|
|
fail(
|
|
"Unexpected modified node: "s + entryType,
|
|
__FILE__,
|
|
__LINE__);
|
|
}
|
|
}
|
|
else if (node.isMember(sfCreatedNode.jsonName))
|
|
{
|
|
Json::Value const& created = node[sfCreatedNode.jsonName];
|
|
std::string const entryType =
|
|
created[sfLedgerEntryType.jsonName].asString();
|
|
if (entryType == jss::Ticket)
|
|
{
|
|
auto const& newFields = created[sfNewFields.jsonName];
|
|
|
|
BEAST_EXPECT(
|
|
newFields[sfAccount.jsonName].asString() == account);
|
|
ticketSeqs.push_back(
|
|
newFields[sfTicketSequence.jsonName].asUInt());
|
|
}
|
|
else if (entryType == jss::DirectoryNode)
|
|
{
|
|
directoryChanged = true;
|
|
}
|
|
else
|
|
{
|
|
fail(
|
|
"Unexpected created node: "s + entryType,
|
|
__FILE__,
|
|
__LINE__);
|
|
}
|
|
}
|
|
else if (node.isMember(sfDeletedNode.jsonName))
|
|
{
|
|
Json::Value const& deleted = node[sfDeletedNode.jsonName];
|
|
std::string const entryType =
|
|
deleted[sfLedgerEntryType.jsonName].asString();
|
|
|
|
if (entryType == jss::Ticket)
|
|
{
|
|
// Verify the transaction's Sequence == 0.
|
|
BEAST_EXPECT(txSeq == 0);
|
|
|
|
// Verify the account of the deleted ticket.
|
|
auto const& finalFields = deleted[sfFinalFields.jsonName];
|
|
BEAST_EXPECT(
|
|
finalFields[sfAccount.jsonName].asString() == account);
|
|
|
|
// Verify the deleted ticket has the right TicketSequence.
|
|
BEAST_EXPECT(
|
|
finalFields[sfTicketSequence.jsonName].asUInt() ==
|
|
tx[sfTicketSequence.jsonName].asUInt());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
fail(
|
|
"Unexpected node type in TicketCreate metadata.",
|
|
__FILE__,
|
|
__LINE__);
|
|
}
|
|
}
|
|
BEAST_EXPECT(directoryChanged);
|
|
|
|
// Verify that all the expected Tickets were created.
|
|
BEAST_EXPECT(ticketSeqs.size() == count);
|
|
std::sort(ticketSeqs.begin(), ticketSeqs.end());
|
|
BEAST_EXPECT(
|
|
std::adjacent_find(ticketSeqs.begin(), ticketSeqs.end()) ==
|
|
ticketSeqs.end());
|
|
BEAST_EXPECT(*ticketSeqs.rbegin() == acctRootFinalSeq - 1);
|
|
}
|
|
|
|
/// @brief Validate metadata for a ticket using transaction.
|
|
///
|
|
/// The transaction may have been successful or failed with a tec.
|
|
///
|
|
/// @param env current jtx env (tx and meta are extracted using it)
|
|
void
|
|
checkTicketConsumeMeta(test::jtx::Env& env)
|
|
{
|
|
Json::Value const& tx{env.tx()->getJson(JsonOptions::none)};
|
|
|
|
// Verify that the transaction includes a TicketSequence.
|
|
|
|
// Capture that TicketSequence.
|
|
// Capture the Account from the transaction
|
|
|
|
// Verify that metadata indicates a tec or a tesSUCCESS.
|
|
|
|
// Walk affected nodes:
|
|
//
|
|
// For each deleted node, see if it is a Ticket node. If it is
|
|
// a Ticket Node being deleted, then assert that the...
|
|
//
|
|
// Account == the transaction Account &&
|
|
// TicketSequence == the transaction TicketSequence
|
|
//
|
|
// If a modified node is an AccountRoot, see if it is the transaction
|
|
// Account. If it is then verify the TicketCount decreased by one.
|
|
// If the old TicketCount was 1, then the TicketCount field should be
|
|
// removed from the final fields of the AccountRoot.
|
|
//
|
|
// After looking at all nodes verify that exactly one Ticket node
|
|
// was deleted.
|
|
BEAST_EXPECT(tx[sfSequence.jsonName].asUInt() == 0);
|
|
std::string const account{tx[sfAccount.jsonName].asString()};
|
|
if (!BEAST_EXPECTS(
|
|
tx.isMember(sfTicketSequence.jsonName),
|
|
"Not metadata for a ticket consuming transaction."))
|
|
return;
|
|
|
|
std::uint32_t const ticketSeq{tx[sfTicketSequence.jsonName].asUInt()};
|
|
|
|
Json::Value const& metadata{env.meta()->getJson(JsonOptions::none)};
|
|
if (!BEAST_EXPECTS(
|
|
metadata.isMember(sfTransactionResult.jsonName),
|
|
"Metadata is missing TransactionResult."))
|
|
return;
|
|
|
|
{
|
|
std::string const transactionResult{
|
|
metadata[sfTransactionResult.jsonName].asString()};
|
|
if (!BEAST_EXPECTS(
|
|
transactionResult == "tesSUCCESS" ||
|
|
transactionResult.compare(0, 3, "tec") == 0,
|
|
transactionResult + " neither tesSUCCESS nor tec"))
|
|
return;
|
|
}
|
|
|
|
BEAST_EXPECT(metadata.isMember(sfAffectedNodes.jsonName));
|
|
BEAST_EXPECT(metadata[sfAffectedNodes.jsonName].isArray());
|
|
|
|
bool acctRootFound{false};
|
|
std::uint32_t acctRootSeq{0};
|
|
int ticketsRemoved{0};
|
|
for (Json::Value const& node : metadata[sfAffectedNodes.jsonName])
|
|
{
|
|
if (node.isMember(sfModifiedNode.jsonName))
|
|
{
|
|
Json::Value const& modified{node[sfModifiedNode.jsonName]};
|
|
std::string const entryType =
|
|
modified[sfLedgerEntryType.jsonName].asString();
|
|
if (entryType == "AccountRoot" &&
|
|
modified[sfFinalFields.jsonName][sfAccount.jsonName]
|
|
.asString() == account)
|
|
{
|
|
acctRootFound = true;
|
|
|
|
auto const& previousFields =
|
|
modified[sfPreviousFields.jsonName];
|
|
auto const& finalFields = modified[sfFinalFields.jsonName];
|
|
|
|
acctRootSeq = finalFields[sfSequence.jsonName].asUInt();
|
|
|
|
// Check that the TicketCount was present and decremented
|
|
// by 1. If it decremented to zero, then the field should
|
|
// be gone.
|
|
if (!BEAST_EXPECTS(
|
|
previousFields.isMember(sfTicketCount.jsonName),
|
|
"AccountRoot previous is missing TicketCount"))
|
|
return;
|
|
|
|
std::uint32_t const prevTicketCount =
|
|
previousFields[sfTicketCount.jsonName].asUInt();
|
|
|
|
BEAST_EXPECT(prevTicketCount > 0);
|
|
if (prevTicketCount == 1)
|
|
BEAST_EXPECT(
|
|
!finalFields.isMember(sfTicketCount.jsonName));
|
|
else
|
|
BEAST_EXPECT(
|
|
finalFields.isMember(sfTicketCount.jsonName) &&
|
|
finalFields[sfTicketCount.jsonName].asUInt() ==
|
|
prevTicketCount - 1);
|
|
}
|
|
}
|
|
else if (node.isMember(sfDeletedNode.jsonName))
|
|
{
|
|
Json::Value const& deleted{node[sfDeletedNode.jsonName]};
|
|
std::string const entryType{
|
|
deleted[sfLedgerEntryType.jsonName].asString()};
|
|
|
|
if (entryType == jss::Ticket)
|
|
{
|
|
// Verify the account of the deleted ticket.
|
|
BEAST_EXPECT(
|
|
deleted[sfFinalFields.jsonName][sfAccount.jsonName]
|
|
.asString() == account);
|
|
|
|
// Verify the deleted ticket has the right TicketSequence.
|
|
BEAST_EXPECT(
|
|
deleted[sfFinalFields.jsonName]
|
|
[sfTicketSequence.jsonName]
|
|
.asUInt() == ticketSeq);
|
|
|
|
++ticketsRemoved;
|
|
}
|
|
}
|
|
}
|
|
BEAST_EXPECT(acctRootFound);
|
|
BEAST_EXPECT(ticketsRemoved == 1);
|
|
BEAST_EXPECT(ticketSeq < acctRootSeq);
|
|
}
|
|
|
|
void
|
|
testTicketCreatePreflightFail()
|
|
{
|
|
testcase("Create Tickets that fail Preflight");
|
|
|
|
using namespace test::jtx;
|
|
Env env{*this};
|
|
|
|
Account const master{env.master};
|
|
|
|
// Exercise boundaries on count.
|
|
env(ticket::create(master, 0), ter(temINVALID_COUNT));
|
|
env(ticket::create(master, 251), ter(temINVALID_COUNT));
|
|
|
|
// Exercise fees.
|
|
std::uint32_t const ticketSeq_A{env.seq(master) + 1};
|
|
env(ticket::create(master, 1), fee(XRP(10)));
|
|
checkTicketCreateMeta(env);
|
|
env.close();
|
|
env.require(owners(master, 1), tickets(master, 1));
|
|
|
|
env(ticket::create(master, 1), fee(XRP(-1)), ter(temBAD_FEE));
|
|
|
|
// Exercise flags.
|
|
std::uint32_t const ticketSeq_B{env.seq(master) + 1};
|
|
env(ticket::create(master, 1), txflags(tfFullyCanonicalSig));
|
|
checkTicketCreateMeta(env);
|
|
env.close();
|
|
env.require(owners(master, 2), tickets(master, 2));
|
|
|
|
env(ticket::create(master, 1), txflags(tfSell), ter(temINVALID_FLAG));
|
|
env.close();
|
|
env.require(owners(master, 2), tickets(master, 2));
|
|
|
|
// We successfully created 1 ticket earlier. Verify that we can
|
|
// create 250 tickets in one shot. We must consume one ticket first.
|
|
env(noop(master), ticket::use(ticketSeq_A));
|
|
checkTicketConsumeMeta(env);
|
|
env.close();
|
|
env.require(owners(master, 1), tickets(master, 1));
|
|
|
|
env(ticket::create(master, 250), ticket::use(ticketSeq_B));
|
|
checkTicketCreateMeta(env);
|
|
env.close();
|
|
env.require(owners(master, 250), tickets(master, 250));
|
|
}
|
|
|
|
void
|
|
testTicketCreatePreclaimFail()
|
|
{
|
|
testcase("Create Tickets that fail Preclaim");
|
|
|
|
using namespace test::jtx;
|
|
{
|
|
// Create tickets on a non-existent account.
|
|
Env env{*this};
|
|
Account alice{"alice"};
|
|
env.memoize(alice);
|
|
|
|
env(ticket::create(alice, 1),
|
|
json(jss::Sequence, 1),
|
|
ter(terNO_ACCOUNT));
|
|
}
|
|
{
|
|
// Exceed the threshold where tickets can no longer be
|
|
// added to an account.
|
|
Env env{*this};
|
|
Account alice{"alice"};
|
|
|
|
env.fund(XRP(100000), alice);
|
|
|
|
std::uint32_t ticketSeq{env.seq(alice) + 1};
|
|
env(ticket::create(alice, 250));
|
|
checkTicketCreateMeta(env);
|
|
env.close();
|
|
env.require(owners(alice, 250), tickets(alice, 250));
|
|
|
|
// Note that we can add one more ticket while consuming a ticket
|
|
// because the final result is still 250 tickets.
|
|
env(ticket::create(alice, 1), ticket::use(ticketSeq + 0));
|
|
checkTicketCreateMeta(env);
|
|
env.close();
|
|
env.require(owners(alice, 250), tickets(alice, 250));
|
|
|
|
// Adding one more ticket will exceed the threshold.
|
|
env(ticket::create(alice, 2),
|
|
ticket::use(ticketSeq + 1),
|
|
ter(tecDIR_FULL));
|
|
env.close();
|
|
env.require(owners(alice, 249), tickets(alice, 249));
|
|
|
|
// Now we can successfully add one more ticket.
|
|
env(ticket::create(alice, 2), ticket::use(ticketSeq + 2));
|
|
checkTicketCreateMeta(env);
|
|
env.close();
|
|
env.require(owners(alice, 250), tickets(alice, 250));
|
|
|
|
// Since we're at 250, we can't add another ticket using a
|
|
// sequence.
|
|
env(ticket::create(alice, 1), ter(tecDIR_FULL));
|
|
env.close();
|
|
env.require(owners(alice, 250), tickets(alice, 250));
|
|
}
|
|
{
|
|
// Explore exceeding the ticket threshold from another angle.
|
|
Env env{*this};
|
|
Account alice{"alice"};
|
|
|
|
env.fund(XRP(100000), alice);
|
|
env.close();
|
|
|
|
std::uint32_t ticketSeq_AB{env.seq(alice) + 1};
|
|
env(ticket::create(alice, 2));
|
|
checkTicketCreateMeta(env);
|
|
env.close();
|
|
env.require(owners(alice, 2), tickets(alice, 2));
|
|
|
|
// Adding 250 tickets (while consuming one) will exceed the
|
|
// threshold.
|
|
env(ticket::create(alice, 250),
|
|
ticket::use(ticketSeq_AB + 0),
|
|
ter(tecDIR_FULL));
|
|
env.close();
|
|
env.require(owners(alice, 1), tickets(alice, 1));
|
|
|
|
// Adding 250 tickets (without consuming one) will exceed the
|
|
// threshold.
|
|
env(ticket::create(alice, 250), ter(tecDIR_FULL));
|
|
env.close();
|
|
env.require(owners(alice, 1), tickets(alice, 1));
|
|
|
|
// Alice can now add 250 tickets while consuming one.
|
|
env(ticket::create(alice, 250), ticket::use(ticketSeq_AB + 1));
|
|
checkTicketCreateMeta(env);
|
|
env.close();
|
|
env.require(owners(alice, 250), tickets(alice, 250));
|
|
}
|
|
}
|
|
|
|
void
|
|
testTicketInsufficientReserve()
|
|
{
|
|
testcase("Create Ticket Insufficient Reserve");
|
|
|
|
using namespace test::jtx;
|
|
Env env{*this};
|
|
Account alice{"alice"};
|
|
|
|
// Fund alice not quite enough to make the reserve for a Ticket.
|
|
env.fund(env.current()->fees().accountReserve(1) - drops(1), alice);
|
|
env.close();
|
|
|
|
env(ticket::create(alice, 1), ter(tecINSUFFICIENT_RESERVE));
|
|
env.close();
|
|
env.require(owners(alice, 0), tickets(alice, 0));
|
|
|
|
// Give alice enough to exactly meet the reserve for one Ticket.
|
|
env(
|
|
pay(env.master,
|
|
alice,
|
|
env.current()->fees().accountReserve(1) - env.balance(alice)));
|
|
env.close();
|
|
|
|
env(ticket::create(alice, 1));
|
|
checkTicketCreateMeta(env);
|
|
env.close();
|
|
env.require(owners(alice, 1), tickets(alice, 1));
|
|
|
|
// Give alice not quite enough to make the reserve for a total of
|
|
// 250 Tickets.
|
|
env(
|
|
pay(env.master,
|
|
alice,
|
|
env.current()->fees().accountReserve(250) - drops(1) -
|
|
env.balance(alice)));
|
|
env.close();
|
|
|
|
// alice doesn't quite have the reserve for a total of 250
|
|
// Tickets, so the transaction fails.
|
|
env(ticket::create(alice, 249), ter(tecINSUFFICIENT_RESERVE));
|
|
env.close();
|
|
env.require(owners(alice, 1), tickets(alice, 1));
|
|
|
|
// Give alice enough so she can make the reserve for all 250
|
|
// Tickets.
|
|
env(pay(
|
|
env.master,
|
|
alice,
|
|
env.current()->fees().accountReserve(250) - env.balance(alice)));
|
|
env.close();
|
|
|
|
std::uint32_t const ticketSeq{env.seq(alice) + 1};
|
|
env(ticket::create(alice, 249));
|
|
checkTicketCreateMeta(env);
|
|
env.close();
|
|
env.require(owners(alice, 250), tickets(alice, 250));
|
|
BEAST_EXPECT(ticketSeq + 249 == env.seq(alice));
|
|
}
|
|
|
|
void
|
|
testUsingTickets()
|
|
{
|
|
testcase("Using Tickets");
|
|
|
|
using namespace test::jtx;
|
|
Env env{*this};
|
|
Account alice{"alice"};
|
|
|
|
env.fund(XRP(10000), alice);
|
|
env.close();
|
|
|
|
// Successfully create tickets (using a sequence)
|
|
std::uint32_t const ticketSeq_AB{env.seq(alice) + 1};
|
|
env(ticket::create(alice, 2));
|
|
checkTicketCreateMeta(env);
|
|
env.close();
|
|
env.require(owners(alice, 2), tickets(alice, 2));
|
|
BEAST_EXPECT(ticketSeq_AB + 2 == env.seq(alice));
|
|
|
|
// You can use a ticket to create one ticket ...
|
|
std::uint32_t const ticketSeq_C{env.seq(alice)};
|
|
env(ticket::create(alice, 1), ticket::use(ticketSeq_AB + 0));
|
|
checkTicketCreateMeta(env);
|
|
env.close();
|
|
env.require(owners(alice, 2), tickets(alice, 2));
|
|
BEAST_EXPECT(ticketSeq_C + 1 == env.seq(alice));
|
|
|
|
// ... you can use a ticket to create multiple tickets ...
|
|
std::uint32_t const ticketSeq_DE{env.seq(alice)};
|
|
env(ticket::create(alice, 2), ticket::use(ticketSeq_AB + 1));
|
|
checkTicketCreateMeta(env);
|
|
env.close();
|
|
env.require(owners(alice, 3), tickets(alice, 3));
|
|
BEAST_EXPECT(ticketSeq_DE + 2 == env.seq(alice));
|
|
|
|
// ... and you can use a ticket for other things.
|
|
env(noop(alice), ticket::use(ticketSeq_DE + 0));
|
|
checkTicketConsumeMeta(env);
|
|
env.close();
|
|
env.require(owners(alice, 2), tickets(alice, 2));
|
|
BEAST_EXPECT(ticketSeq_DE + 2 == env.seq(alice));
|
|
|
|
env(pay(alice, env.master, XRP(20)), ticket::use(ticketSeq_DE + 1));
|
|
checkTicketConsumeMeta(env);
|
|
env.close();
|
|
env.require(owners(alice, 1), tickets(alice, 1));
|
|
BEAST_EXPECT(ticketSeq_DE + 2 == env.seq(alice));
|
|
|
|
env(trust(alice, env.master["USD"](20)), ticket::use(ticketSeq_C));
|
|
checkTicketConsumeMeta(env);
|
|
env.close();
|
|
env.require(owners(alice, 1), tickets(alice, 0));
|
|
BEAST_EXPECT(ticketSeq_DE + 2 == env.seq(alice));
|
|
|
|
// Attempt to use a ticket that has already been used.
|
|
env(noop(alice), ticket::use(ticketSeq_C), ter(tefNO_TICKET));
|
|
env.close();
|
|
|
|
// Attempt to use a ticket from the future.
|
|
std::uint32_t const ticketSeq_F{env.seq(alice) + 1};
|
|
env(noop(alice), ticket::use(ticketSeq_F), ter(terPRE_TICKET));
|
|
env.close();
|
|
|
|
// Now create the ticket. The retry will consume the new ticket.
|
|
env(ticket::create(alice, 1));
|
|
checkTicketCreateMeta(env);
|
|
env.close();
|
|
env.require(owners(alice, 1), tickets(alice, 0));
|
|
BEAST_EXPECT(ticketSeq_F + 1 == env.seq(alice));
|
|
|
|
// Try a transaction that combines consuming a ticket with
|
|
// AccountTxnID.
|
|
std::uint32_t const ticketSeq_G{env.seq(alice) + 1};
|
|
env(ticket::create(alice, 1));
|
|
checkTicketCreateMeta(env);
|
|
env.close();
|
|
|
|
env(noop(alice),
|
|
ticket::use(ticketSeq_G),
|
|
json(R"({"AccountTxnID": "0"})"),
|
|
ter(temINVALID));
|
|
env.close();
|
|
env.require(owners(alice, 2), tickets(alice, 1));
|
|
}
|
|
|
|
void
|
|
testTransactionDatabaseWithTickets()
|
|
{
|
|
// The Transaction database keeps each transaction's sequence number
|
|
// in an entry (called "FromSeq"). Until the introduction of tickets
|
|
// each sequence stored for a given account would always be unique.
|
|
// With the advent of tickets there could be lots of entries
|
|
// with zero.
|
|
//
|
|
// We really don't expect those zeros to cause any problems since
|
|
// there are no indexes that use "FromSeq". But it still seems
|
|
// prudent to exercise this a bit to see if tickets cause any obvious
|
|
// harm.
|
|
testcase("Transaction Database With Tickets");
|
|
|
|
using namespace test::jtx;
|
|
Env env{*this};
|
|
Account alice{"alice"};
|
|
|
|
env.fund(XRP(10000), alice);
|
|
env.close();
|
|
|
|
// Lambda that returns the hash of the most recent transaction.
|
|
auto getTxID = [&env, this]() -> uint256 {
|
|
std::shared_ptr<STTx const> tx{env.tx()};
|
|
if (!BEAST_EXPECTS(tx, "Transaction not found"))
|
|
Throw<std::invalid_argument>("Invalid transaction ID");
|
|
|
|
return tx->getTransactionID();
|
|
};
|
|
|
|
// A note about the metadata created by these transactions.
|
|
//
|
|
// We _could_ check the metadata on these transactions. However
|
|
// checking the metadata has the side effect of advancing the ledger.
|
|
// So if we check the metadata we don't get to look at several
|
|
// transactions in the same ledger. Therefore a specific choice was
|
|
// made to not check the metadata on these transactions.
|
|
|
|
// Successfully create several tickets (using a sequence).
|
|
std::uint32_t ticketSeq{env.seq(alice)};
|
|
static constexpr std::uint32_t ticketCount{10};
|
|
env(ticket::create(alice, ticketCount));
|
|
uint256 const txHash_1{getTxID()};
|
|
|
|
// Just for grins use the tickets in reverse from largest to smallest.
|
|
ticketSeq += ticketCount;
|
|
env(noop(alice), ticket::use(--ticketSeq));
|
|
uint256 const txHash_2{getTxID()};
|
|
|
|
env(pay(alice, env.master, XRP(200)), ticket::use(--ticketSeq));
|
|
uint256 const txHash_3{getTxID()};
|
|
|
|
env(deposit::auth(alice, env.master), ticket::use(--ticketSeq));
|
|
uint256 const txHash_4{getTxID()};
|
|
|
|
// Close the ledger so we look at transactions from a couple of
|
|
// different ledgers.
|
|
env.close();
|
|
|
|
env(pay(alice, env.master, XRP(300)), ticket::use(--ticketSeq));
|
|
uint256 const txHash_5{getTxID()};
|
|
|
|
env(pay(alice, env.master, XRP(400)), ticket::use(--ticketSeq));
|
|
uint256 const txHash_6{getTxID()};
|
|
|
|
env(deposit::unauth(alice, env.master), ticket::use(--ticketSeq));
|
|
uint256 const txHash_7{getTxID()};
|
|
|
|
env(noop(alice), ticket::use(--ticketSeq));
|
|
uint256 const txHash_8{getTxID()};
|
|
|
|
env.close();
|
|
|
|
// Checkout what's in the Transaction database. We go straight
|
|
// to the database. Most of our interfaces cache transactions
|
|
// in memory. So if we use normal interfaces we would get the
|
|
// transactions from memory rather than from the database.
|
|
|
|
// Lambda to verify a transaction pulled from the Transaction database.
|
|
auto checkTxFromDB = [&env, this](
|
|
uint256 const& txID,
|
|
std::uint32_t ledgerSeq,
|
|
std::uint32_t txSeq,
|
|
std::optional<std::uint32_t> ticketSeq,
|
|
TxType txType) {
|
|
error_code_i txErrCode{rpcSUCCESS};
|
|
|
|
using TxPair = std::
|
|
pair<std::shared_ptr<Transaction>, std::shared_ptr<TxMeta>>;
|
|
std::variant<TxPair, TxSearched> maybeTx =
|
|
Transaction::load(txID, env.app(), txErrCode);
|
|
|
|
BEAST_EXPECT(txErrCode == rpcSUCCESS);
|
|
if (auto txPtr = std::get_if<TxPair>(&maybeTx))
|
|
{
|
|
std::shared_ptr<Transaction>& tx = txPtr->first;
|
|
BEAST_EXPECT(tx->getLedger() == ledgerSeq);
|
|
std::shared_ptr<STTx const> const& sttx = tx->getSTransaction();
|
|
BEAST_EXPECT((*sttx)[sfSequence] == txSeq);
|
|
if (ticketSeq)
|
|
BEAST_EXPECT((*sttx)[sfTicketSequence] == *ticketSeq);
|
|
BEAST_EXPECT((*sttx)[sfTransactionType] == txType);
|
|
}
|
|
else
|
|
{
|
|
fail("Expected transaction was not found");
|
|
}
|
|
};
|
|
|
|
// txID ledgerSeq txSeq ticketSeq txType
|
|
checkTxFromDB(txHash_1, 4, 4, {}, ttTICKET_CREATE);
|
|
checkTxFromDB(txHash_2, 4, 0, 13, ttACCOUNT_SET);
|
|
checkTxFromDB(txHash_3, 4, 0, 12, ttPAYMENT);
|
|
checkTxFromDB(txHash_4, 4, 0, 11, ttDEPOSIT_PREAUTH);
|
|
|
|
checkTxFromDB(txHash_5, 5, 0, 10, ttPAYMENT);
|
|
checkTxFromDB(txHash_6, 5, 0, 9, ttPAYMENT);
|
|
checkTxFromDB(txHash_7, 5, 0, 8, ttDEPOSIT_PREAUTH);
|
|
checkTxFromDB(txHash_8, 5, 0, 7, ttACCOUNT_SET);
|
|
}
|
|
|
|
void
|
|
testSignWithTicketSequence()
|
|
{
|
|
// The sign and the submit RPC commands automatically fill in the
|
|
// Sequence field of a transaction if none is provided. If a
|
|
// TicketSequence is provided in the transaction, then the
|
|
// auto-filled Sequence should be zero.
|
|
testcase("Sign with TicketSequence");
|
|
|
|
using namespace test::jtx;
|
|
Env env{*this};
|
|
Account alice{"alice"};
|
|
|
|
env.fund(XRP(10000), alice);
|
|
env.close();
|
|
|
|
// Successfully create tickets (using a sequence)
|
|
std::uint32_t const ticketSeq = env.seq(alice) + 1;
|
|
env(ticket::create(alice, 2));
|
|
checkTicketCreateMeta(env);
|
|
env.close();
|
|
env.require(owners(alice, 2), tickets(alice, 2));
|
|
BEAST_EXPECT(ticketSeq + 2 == env.seq(alice));
|
|
|
|
{
|
|
// Test that the "sign" RPC command fills in a "Sequence": 0 field
|
|
// if none is provided.
|
|
|
|
// Create a noop transaction using a TicketSequence but don't fill
|
|
// in the Sequence field.
|
|
Json::Value tx = Json::objectValue;
|
|
tx[jss::tx_json] = noop(alice);
|
|
tx[jss::tx_json][sfTicketSequence.jsonName] = ticketSeq;
|
|
tx[jss::secret] = toBase58(generateSeed("alice"));
|
|
|
|
// Verify that there is no "Sequence" field.
|
|
BEAST_EXPECT(!tx[jss::tx_json].isMember(sfSequence.jsonName));
|
|
|
|
// Call the "sign" RPC command and see the "Sequence": 0 field
|
|
// filled in.
|
|
Json::Value jr = env.rpc("json", "sign", to_string(tx));
|
|
|
|
// Verify that "sign" inserted a "Sequence": 0 field.
|
|
if (BEAST_EXPECT(jr[jss::result][jss::tx_json].isMember(
|
|
sfSequence.jsonName)))
|
|
{
|
|
BEAST_EXPECT(
|
|
jr[jss::result][jss::tx_json][sfSequence.jsonName] == 0);
|
|
}
|
|
|
|
// "sign" should not have consumed any of alice's tickets.
|
|
env.close();
|
|
env.require(owners(alice, 2), tickets(alice, 2));
|
|
|
|
// "submit" the signed blob and see one of alice's tickets consumed.
|
|
env.rpc("submit", jr[jss::result][jss::tx_blob].asString());
|
|
env.close();
|
|
env.require(owners(alice, 1), tickets(alice, 1));
|
|
}
|
|
{
|
|
// Test that the "submit" RPC command fills in a "Sequence": 0
|
|
// field if none is provided.
|
|
|
|
// Create a noop transaction using a TicketSequence but don't fill
|
|
// in the Sequence field.
|
|
Json::Value tx = Json::objectValue;
|
|
tx[jss::tx_json] = noop(alice);
|
|
tx[jss::tx_json][sfTicketSequence.jsonName] = ticketSeq + 1;
|
|
tx[jss::secret] = toBase58(generateSeed("alice"));
|
|
|
|
// Verify that there is no "Sequence" field.
|
|
BEAST_EXPECT(!tx[jss::tx_json].isMember(sfSequence.jsonName));
|
|
|
|
// Call the "submit" RPC command and see the "Sequence": 0 field
|
|
// filled in.
|
|
Json::Value jr = env.rpc("json", "submit", to_string(tx));
|
|
|
|
// Verify that "submit" inserted a "Sequence": 0 field.
|
|
if (BEAST_EXPECT(jr[jss::result][jss::tx_json].isMember(
|
|
sfSequence.jsonName)))
|
|
{
|
|
BEAST_EXPECT(
|
|
jr[jss::result][jss::tx_json][sfSequence.jsonName] == 0);
|
|
}
|
|
|
|
// "submit" should have consumed the last of alice's tickets.
|
|
env.close();
|
|
env.require(owners(alice, 0), tickets(alice, 0));
|
|
}
|
|
}
|
|
|
|
void
|
|
testFixBothSeqAndTicket()
|
|
{
|
|
using namespace test::jtx;
|
|
|
|
// It is an error if a transaction contains a non-zero Sequence field
|
|
// and a TicketSequence field. Verify that the error is detected.
|
|
testcase("Fix both Seq and Ticket");
|
|
|
|
Env env{*this, testable_amendments()};
|
|
Account alice{"alice"};
|
|
|
|
env.fund(XRP(10000), alice);
|
|
env.close();
|
|
|
|
// Create a ticket.
|
|
std::uint32_t const ticketSeq = env.seq(alice) + 1;
|
|
env(ticket::create(alice, 1));
|
|
env.close();
|
|
env.require(owners(alice, 1), tickets(alice, 1));
|
|
BEAST_EXPECT(ticketSeq + 1 == env.seq(alice));
|
|
|
|
// Create a transaction that includes both a ticket and a non-zero
|
|
// sequence number. The transaction fails with temSEQ_AND_TICKET.
|
|
env(noop(alice),
|
|
ticket::use(ticketSeq),
|
|
seq(env.seq(alice)),
|
|
ter(temSEQ_AND_TICKET));
|
|
env.close();
|
|
|
|
// Verify that the transaction failed by looking at alice's
|
|
// sequence number and tickets.
|
|
env.require(owners(alice, 1), tickets(alice, 1));
|
|
BEAST_EXPECT(ticketSeq + 1 == env.seq(alice));
|
|
}
|
|
|
|
public:
|
|
void
|
|
run() override
|
|
{
|
|
testTicketCreatePreflightFail();
|
|
testTicketCreatePreclaimFail();
|
|
testTicketInsufficientReserve();
|
|
testUsingTickets();
|
|
testTransactionDatabaseWithTickets();
|
|
testSignWithTicketSequence();
|
|
testFixBothSeqAndTicket();
|
|
}
|
|
};
|
|
|
|
BEAST_DEFINE_TESTSUITE(Ticket, app, ripple);
|
|
|
|
} // namespace ripple
|