mirror of
https://github.com/XRPLF/rippled.git
synced 2025-12-06 17:27:55 +00:00
Implement enhanced Ticket support:
Tickets are a mechanism to allow for the "out-of-order" execution of transactions on the XRP Ledger. This commit, if merged, reworks the existing support for tickets and introduces support for 'ticket batching', completing the feature set needed for tickets. The code is gated under the newly-introduced `TicketBatch` amendment and the `Tickets` amendment, which is not presently active on the network, is being removed. The specification for this change can be found at: https://github.com/xrp-community/standards-drafts/issues/16
This commit is contained in:
committed by
Nik Bougalis
parent
01bd5a2646
commit
7724cca384
@@ -349,8 +349,7 @@ public:
|
||||
Account const gw{"gateway"};
|
||||
auto const USD = gw["USD"];
|
||||
|
||||
// Test for ticket account objects when they are supported.
|
||||
Env env(*this, supported_amendments().set(featureTickets));
|
||||
Env env(*this, supported_amendments() | featureTicketBatch);
|
||||
|
||||
// Make a lambda we can use to get "account_objects" easily.
|
||||
auto acct_objs = [&env](Account const& acct, char const* type) {
|
||||
@@ -504,7 +503,7 @@ public:
|
||||
BEAST_EXPECT(entry[sfSignerWeight.jsonName].asUInt() == 7);
|
||||
}
|
||||
// Create a Ticket for gw.
|
||||
env(ticket::create(gw, gw));
|
||||
env(ticket::create(gw, 1));
|
||||
env.close();
|
||||
{
|
||||
// Find the ticket.
|
||||
@@ -514,7 +513,7 @@ public:
|
||||
auto const& ticket = resp[jss::result][jss::account_objects][0u];
|
||||
BEAST_EXPECT(ticket[sfAccount.jsonName] == gw.human());
|
||||
BEAST_EXPECT(ticket[sfLedgerEntryType.jsonName] == jss::Ticket);
|
||||
BEAST_EXPECT(ticket[sfSequence.jsonName].asUInt() == 11);
|
||||
BEAST_EXPECT(ticket[sfTicketSequence.jsonName].asUInt() == 12);
|
||||
}
|
||||
{
|
||||
// See how "deletion_blockers_only" handles gw's directory.
|
||||
|
||||
@@ -495,6 +495,46 @@ public:
|
||||
BEAST_EXPECT(!dirIsEmpty(*env.closed(), keylet::ownerDir(alice)));
|
||||
|
||||
env(fset(alice, asfRequireAuth), ter(tecOWNERS));
|
||||
|
||||
// Remove the signer list. After that asfRequireAuth should succeed.
|
||||
env(signers(alice, test::jtx::none));
|
||||
env.close();
|
||||
BEAST_EXPECT(dirIsEmpty(*env.closed(), keylet::ownerDir(alice)));
|
||||
|
||||
env(fset(alice, asfRequireAuth));
|
||||
}
|
||||
|
||||
void
|
||||
testTicket()
|
||||
{
|
||||
using namespace test::jtx;
|
||||
Env env(*this, supported_amendments() | featureTicketBatch);
|
||||
Account const alice("alice");
|
||||
|
||||
env.fund(XRP(10000), alice);
|
||||
env.close();
|
||||
|
||||
std::uint32_t const ticketSeq{env.seq(alice) + 1};
|
||||
env(ticket::create(alice, 1));
|
||||
env.close();
|
||||
env.require(owners(alice, 1), tickets(alice, 1));
|
||||
|
||||
// Try using a ticket that alice doesn't have.
|
||||
env(noop(alice), ticket::use(ticketSeq + 1), ter(terPRE_TICKET));
|
||||
env.close();
|
||||
env.require(owners(alice, 1), tickets(alice, 1));
|
||||
|
||||
// Actually use alice's ticket. Note that if a transaction consumes
|
||||
// a ticket then the account's sequence number does not advance.
|
||||
std::uint32_t const aliceSeq{env.seq(alice)};
|
||||
env(noop(alice), ticket::use(ticketSeq));
|
||||
env.close();
|
||||
env.require(owners(alice, 0), tickets(alice, 0));
|
||||
BEAST_EXPECT(aliceSeq == env.seq(alice));
|
||||
|
||||
// Try re-using a ticket that alice already used.
|
||||
env(noop(alice), ticket::use(ticketSeq), ter(tefNO_TICKET));
|
||||
env.close();
|
||||
}
|
||||
|
||||
void
|
||||
@@ -512,6 +552,7 @@ public:
|
||||
testBadInputs();
|
||||
testRequireAuthWithDir();
|
||||
testTransferRate();
|
||||
testTicket();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -252,7 +252,7 @@ class AccountTx_test : public beast::unit_test::suite
|
||||
using namespace test::jtx;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
Env env(*this);
|
||||
Env env(*this, supported_amendments() | featureTicketBatch);
|
||||
Account const alice{"alice"};
|
||||
Account const alie{"alie"};
|
||||
Account const gw{"gw"};
|
||||
@@ -399,10 +399,15 @@ class AccountTx_test : public beast::unit_test::suite
|
||||
env(check::cancel(alice, aliceCheckId), sig(alie));
|
||||
env.close();
|
||||
}
|
||||
{
|
||||
// Deposit preauthorization with a Ticket.
|
||||
std::uint32_t const tktSeq{env.seq(alice) + 1};
|
||||
env(ticket::create(alice, 1), sig(alie));
|
||||
env.close();
|
||||
|
||||
// Deposit preauthorization.
|
||||
env(deposit::auth(alice, gw), sig(alie));
|
||||
env.close();
|
||||
env(deposit::auth(alice, gw), ticket::use(tktSeq), sig(alie));
|
||||
env.close();
|
||||
}
|
||||
|
||||
// Setup is done. Look at the transactions returned by account_tx.
|
||||
Json::Value params;
|
||||
@@ -423,27 +428,28 @@ class AccountTx_test : public beast::unit_test::suite
|
||||
// be returned in the reverse order of application to the ledger.
|
||||
static const NodeSanity sanity[]{
|
||||
// txType, created, deleted, modified
|
||||
{ 0, jss::DepositPreauth, {jss::DepositPreauth}, {}, {jss::AccountRoot, jss::DirectoryNode}},
|
||||
{ 1, jss::CheckCancel, {}, {jss::Check}, {jss::AccountRoot, jss::AccountRoot, jss::DirectoryNode, jss::DirectoryNode}},
|
||||
{ 2, jss::CheckCash, {}, {jss::Check}, {jss::AccountRoot, jss::AccountRoot, jss::DirectoryNode, jss::DirectoryNode}},
|
||||
{ 3, jss::CheckCreate, {jss::Check}, {}, {jss::AccountRoot, jss::AccountRoot, jss::DirectoryNode, jss::DirectoryNode}},
|
||||
{ 4, jss::CheckCreate, {jss::Check}, {}, {jss::AccountRoot, jss::AccountRoot, jss::DirectoryNode, jss::DirectoryNode}},
|
||||
{ 5, jss::PaymentChannelClaim, {}, {jss::PayChannel}, {jss::AccountRoot, jss::AccountRoot, jss::DirectoryNode, jss::DirectoryNode}},
|
||||
{ 6, jss::PaymentChannelFund, {}, {}, {jss::AccountRoot, jss::PayChannel }},
|
||||
{ 7, jss::PaymentChannelCreate, {jss::PayChannel}, {}, {jss::AccountRoot, jss::AccountRoot, jss::DirectoryNode, jss::DirectoryNode}},
|
||||
{ 8, jss::EscrowCancel, {}, {jss::Escrow}, {jss::AccountRoot, jss::DirectoryNode}},
|
||||
{ 9, jss::EscrowFinish, {}, {jss::Escrow}, {jss::AccountRoot, jss::DirectoryNode}},
|
||||
{ 10, jss::EscrowCreate, {jss::Escrow}, {}, {jss::AccountRoot, jss::DirectoryNode}},
|
||||
{ 11, jss::EscrowCreate, {jss::Escrow}, {}, {jss::AccountRoot, jss::DirectoryNode}},
|
||||
{ 12, jss::SignerListSet, {jss::SignerList}, {}, {jss::AccountRoot, jss::DirectoryNode}},
|
||||
{ 13, jss::OfferCancel, {}, {jss::Offer, jss::DirectoryNode}, {jss::AccountRoot, jss::DirectoryNode}},
|
||||
{ 14, jss::OfferCreate, {jss::Offer, jss::DirectoryNode}, {}, {jss::AccountRoot, jss::DirectoryNode}},
|
||||
{ 15, jss::TrustSet, {jss::RippleState, jss::DirectoryNode, jss::DirectoryNode}, {}, {jss::AccountRoot, jss::AccountRoot}},
|
||||
{ 16, jss::SetRegularKey, {}, {}, {jss::AccountRoot}},
|
||||
{ 17, jss::Payment, {}, {}, {jss::AccountRoot, jss::AccountRoot}},
|
||||
{ 18, jss::AccountSet, {}, {}, {jss::AccountRoot}},
|
||||
{ 19, jss::AccountSet, {}, {}, {jss::AccountRoot}},
|
||||
{ 20, jss::Payment, {jss::AccountRoot}, {}, {jss::AccountRoot}},
|
||||
{0, jss::DepositPreauth, {jss::DepositPreauth}, {jss::Ticket}, {jss::AccountRoot, jss::DirectoryNode}},
|
||||
{1, jss::TicketCreate, {jss::Ticket}, {}, {jss::AccountRoot, jss::DirectoryNode}},
|
||||
{2, jss::CheckCancel, {}, {jss::Check}, {jss::AccountRoot, jss::AccountRoot, jss::DirectoryNode, jss::DirectoryNode}},
|
||||
{3, jss::CheckCash, {}, {jss::Check}, {jss::AccountRoot, jss::AccountRoot, jss::DirectoryNode, jss::DirectoryNode}},
|
||||
{4, jss::CheckCreate, {jss::Check}, {}, {jss::AccountRoot, jss::AccountRoot, jss::DirectoryNode, jss::DirectoryNode}},
|
||||
{5, jss::CheckCreate, {jss::Check}, {}, {jss::AccountRoot, jss::AccountRoot, jss::DirectoryNode, jss::DirectoryNode}},
|
||||
{6, jss::PaymentChannelClaim, {}, {jss::PayChannel}, {jss::AccountRoot, jss::AccountRoot, jss::DirectoryNode, jss::DirectoryNode}},
|
||||
{7, jss::PaymentChannelFund, {}, {}, {jss::AccountRoot, jss::PayChannel}},
|
||||
{8, jss::PaymentChannelCreate, {jss::PayChannel}, {}, {jss::AccountRoot, jss::AccountRoot, jss::DirectoryNode, jss::DirectoryNode}},
|
||||
{9, jss::EscrowCancel, {}, {jss::Escrow}, {jss::AccountRoot, jss::DirectoryNode}},
|
||||
{10, jss::EscrowFinish, {}, {jss::Escrow}, {jss::AccountRoot, jss::DirectoryNode}},
|
||||
{11, jss::EscrowCreate, {jss::Escrow}, {}, {jss::AccountRoot, jss::DirectoryNode}},
|
||||
{12, jss::EscrowCreate, {jss::Escrow}, {}, {jss::AccountRoot, jss::DirectoryNode}},
|
||||
{13, jss::SignerListSet, {jss::SignerList}, {}, {jss::AccountRoot, jss::DirectoryNode}},
|
||||
{14, jss::OfferCancel, {}, {jss::Offer, jss::DirectoryNode}, {jss::AccountRoot, jss::DirectoryNode}},
|
||||
{15, jss::OfferCreate, {jss::Offer, jss::DirectoryNode}, {}, {jss::AccountRoot, jss::DirectoryNode}},
|
||||
{16, jss::TrustSet, {jss::RippleState, jss::DirectoryNode, jss::DirectoryNode}, {}, {jss::AccountRoot, jss::AccountRoot}},
|
||||
{17, jss::SetRegularKey, {}, {}, {jss::AccountRoot}},
|
||||
{18, jss::Payment, {}, {}, {jss::AccountRoot, jss::AccountRoot}},
|
||||
{19, jss::AccountSet, {}, {}, {jss::AccountRoot}},
|
||||
{20, jss::AccountSet, {}, {}, {jss::AccountRoot}},
|
||||
{21, jss::Payment, {jss::AccountRoot}, {}, {jss::AccountRoot}},
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
|
||||
@@ -114,17 +114,14 @@ class Fee_test : public beast::unit_test::suite
|
||||
auto const baseFee = view->fees().base;
|
||||
BEAST_EXPECT(
|
||||
fee.base_fee().drops() ==
|
||||
toDrops(metrics.referenceFeeLevel, baseFee).second);
|
||||
toDrops(metrics.referenceFeeLevel, baseFee));
|
||||
BEAST_EXPECT(
|
||||
fee.minimum_fee().drops() ==
|
||||
toDrops(metrics.minProcessingFeeLevel, baseFee).second);
|
||||
toDrops(metrics.minProcessingFeeLevel, baseFee));
|
||||
BEAST_EXPECT(
|
||||
fee.median_fee().drops() ==
|
||||
toDrops(metrics.medFeeLevel, baseFee).second);
|
||||
fee.median_fee().drops() == toDrops(metrics.medFeeLevel, baseFee));
|
||||
auto openLedgerFee =
|
||||
toDrops(metrics.openLedgerFeeLevel - FeeLevel64{1}, baseFee)
|
||||
.second +
|
||||
1;
|
||||
toDrops(metrics.openLedgerFeeLevel - FeeLevel64{1}, baseFee) + 1;
|
||||
BEAST_EXPECT(fee.open_ledger_fee().drops() == openLedgerFee.drops());
|
||||
}
|
||||
|
||||
|
||||
@@ -312,7 +312,7 @@ public:
|
||||
Env env{
|
||||
*this,
|
||||
envconfig(validator, ""),
|
||||
supported_amendments().set(featureTickets)};
|
||||
supported_amendments() | featureTicketBatch};
|
||||
|
||||
Account const gw{"gateway"};
|
||||
auto const USD = gw["USD"];
|
||||
@@ -338,7 +338,7 @@ public:
|
||||
}
|
||||
env(signers(
|
||||
Account{"bob0"}, 1, {{Account{"bob1"}, 1}, {Account{"bob2"}, 1}}));
|
||||
env(ticket::create(env.master));
|
||||
env(ticket::create(env.master, 1));
|
||||
|
||||
{
|
||||
Json::Value jv;
|
||||
|
||||
@@ -393,7 +393,7 @@ class LedgerRPC_test : public beast::unit_test::suite
|
||||
void
|
||||
testLedgerEntryDepositPreauth()
|
||||
{
|
||||
testcase("ledger_entry Request Directory");
|
||||
testcase("ledger_entry Request DepositPreauth");
|
||||
using namespace test::jtx;
|
||||
Env env{*this};
|
||||
Account const alice{"alice"};
|
||||
@@ -1074,6 +1074,124 @@ class LedgerRPC_test : public beast::unit_test::suite
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testLedgerEntryTicket()
|
||||
{
|
||||
testcase("ledger_entry Request Ticket");
|
||||
using namespace test::jtx;
|
||||
Env env{*this, supported_amendments() | featureTicketBatch};
|
||||
env.close();
|
||||
|
||||
// Create two tickets.
|
||||
std::uint32_t const tkt1{env.seq(env.master) + 1};
|
||||
env(ticket::create(env.master, 2));
|
||||
env.close();
|
||||
|
||||
std::string const ledgerHash{to_string(env.closed()->info().hash)};
|
||||
// Request four tickets: one before the first one we created, the
|
||||
// two created tickets, and the ticket that would come after the
|
||||
// last created ticket.
|
||||
{
|
||||
// Not a valid ticket requested by index.
|
||||
Json::Value jvParams;
|
||||
jvParams[jss::ticket] =
|
||||
to_string(getTicketIndex(env.master, tkt1 - 1));
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
Json::Value const jrr = env.rpc(
|
||||
"json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
checkErrorValue(jrr, "entryNotFound", "");
|
||||
}
|
||||
{
|
||||
// First real ticket requested by index.
|
||||
Json::Value jvParams;
|
||||
jvParams[jss::ticket] = to_string(getTicketIndex(env.master, tkt1));
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
Json::Value const jrr = env.rpc(
|
||||
"json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
BEAST_EXPECT(
|
||||
jrr[jss::node][sfLedgerEntryType.jsonName] == jss::Ticket);
|
||||
BEAST_EXPECT(jrr[jss::node][sfTicketSequence.jsonName] == tkt1);
|
||||
}
|
||||
{
|
||||
// Second real ticket requested by account and sequence.
|
||||
Json::Value jvParams;
|
||||
jvParams[jss::ticket] = Json::objectValue;
|
||||
jvParams[jss::ticket][jss::account] = env.master.human();
|
||||
jvParams[jss::ticket][jss::ticket_seq] = tkt1 + 1;
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
Json::Value const jrr = env.rpc(
|
||||
"json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
BEAST_EXPECT(
|
||||
jrr[jss::node][jss::index] ==
|
||||
to_string(getTicketIndex(env.master, tkt1 + 1)));
|
||||
}
|
||||
{
|
||||
// Not a valid ticket requested by account and sequence.
|
||||
Json::Value jvParams;
|
||||
jvParams[jss::ticket] = Json::objectValue;
|
||||
jvParams[jss::ticket][jss::account] = env.master.human();
|
||||
jvParams[jss::ticket][jss::ticket_seq] = tkt1 + 2;
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
Json::Value const jrr = env.rpc(
|
||||
"json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
checkErrorValue(jrr, "entryNotFound", "");
|
||||
}
|
||||
{
|
||||
// Request a ticket using an account root entry.
|
||||
Json::Value jvParams;
|
||||
jvParams[jss::ticket] = to_string(keylet::account(env.master).key);
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
Json::Value const jrr = env.rpc(
|
||||
"json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
checkErrorValue(jrr, "malformedRequest", "");
|
||||
}
|
||||
{
|
||||
// Malformed account entry.
|
||||
Json::Value jvParams;
|
||||
jvParams[jss::ticket] = Json::objectValue;
|
||||
|
||||
std::string const badAddress = makeBadAddress(env.master.human());
|
||||
jvParams[jss::ticket][jss::account] = badAddress;
|
||||
jvParams[jss::ticket][jss::ticket_seq] = env.seq(env.master) - 1;
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
Json::Value const jrr = env.rpc(
|
||||
"json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
checkErrorValue(jrr, "malformedAddress", "");
|
||||
}
|
||||
{
|
||||
// Malformed ticket object. Missing account member.
|
||||
Json::Value jvParams;
|
||||
jvParams[jss::ticket] = Json::objectValue;
|
||||
jvParams[jss::ticket][jss::ticket_seq] = env.seq(env.master) - 1;
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
Json::Value const jrr = env.rpc(
|
||||
"json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
checkErrorValue(jrr, "malformedRequest", "");
|
||||
}
|
||||
{
|
||||
// Malformed ticket object. Missing seq member.
|
||||
Json::Value jvParams;
|
||||
jvParams[jss::ticket] = Json::objectValue;
|
||||
jvParams[jss::ticket][jss::account] = env.master.human();
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
Json::Value const jrr = env.rpc(
|
||||
"json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
checkErrorValue(jrr, "malformedRequest", "");
|
||||
}
|
||||
{
|
||||
// Malformed ticket object. Non-integral seq member.
|
||||
Json::Value jvParams;
|
||||
jvParams[jss::ticket] = Json::objectValue;
|
||||
jvParams[jss::ticket][jss::account] = env.master.human();
|
||||
jvParams[jss::ticket][jss::ticket_seq] =
|
||||
std::to_string(env.seq(env.master) - 1);
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
Json::Value const jrr = env.rpc(
|
||||
"json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
checkErrorValue(jrr, "malformedRequest", "");
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testLedgerEntryUnknownOption()
|
||||
{
|
||||
@@ -1560,6 +1678,7 @@ public:
|
||||
testLedgerEntryOffer();
|
||||
testLedgerEntryPayChan();
|
||||
testLedgerEntryRippleState();
|
||||
testLedgerEntryTicket();
|
||||
testLedgerEntryUnknownOption();
|
||||
testLookupLedger();
|
||||
testNoQueue();
|
||||
|
||||
@@ -297,23 +297,20 @@ class NoRippleCheckLimits_test : public beast::unit_test::suite
|
||||
seq(autofill),
|
||||
fee(toDrops(
|
||||
txq.getMetrics(*env.current()).openLedgerFeeLevel,
|
||||
baseFee)
|
||||
.second +
|
||||
baseFee) +
|
||||
1),
|
||||
sig(autofill));
|
||||
env(fset(gw, asfDefaultRipple),
|
||||
seq(autofill),
|
||||
fee(toDrops(
|
||||
txq.getMetrics(*env.current()).openLedgerFeeLevel,
|
||||
baseFee)
|
||||
.second +
|
||||
baseFee) +
|
||||
1),
|
||||
sig(autofill));
|
||||
env(trust(alice, gw["USD"](10)),
|
||||
fee(toDrops(
|
||||
txq.getMetrics(*env.current()).openLedgerFeeLevel,
|
||||
baseFee)
|
||||
.second +
|
||||
baseFee) +
|
||||
1));
|
||||
env.close();
|
||||
}
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2012, 2013 Ripple Labs Inc.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
|
||||
@@ -185,8 +185,9 @@ public:
|
||||
{
|
||||
return;
|
||||
}
|
||||
BEAST_EXPECT(client.reply.engine_result().result() == "tefALREADY");
|
||||
BEAST_EXPECT(client.reply.engine_result_code() == -198);
|
||||
BEAST_EXPECT(
|
||||
client.reply.engine_result().result() == "tefPAST_SEQ");
|
||||
BEAST_EXPECT(client.reply.engine_result_code() == -190);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user