xrpld
Loading...
Searching...
No Matches
proposal.cpp
1#include <test/jtx/proposal.h>
2
3#include <test/jtx/Account.h>
4#include <test/jtx/Env.h>
5#include <test/jtx/JTx.h>
6#include <test/jtx/batch.h>
7#include <test/jtx/multisign.h>
8#include <test/jtx/ticket.h>
9#include <test/jtx/utility.h>
10
11#include <xrpl/basics/base_uint.h>
12#include <xrpl/basics/chrono.h>
13#include <xrpl/basics/contract.h>
14#include <xrpl/json/json_value.h>
15#include <xrpl/ledger/ReadView.h>
16#include <xrpl/ledger/helpers/ProposalHelpers.h>
17#include <xrpl/protocol/AccountID.h>
18#include <xrpl/protocol/Indexes.h>
19#include <xrpl/protocol/SField.h>
20#include <xrpl/protocol/STAmount.h>
21#include <xrpl/protocol/STLedgerEntry.h>
22#include <xrpl/protocol/STObject.h>
23#include <xrpl/protocol/SeqProxy.h>
24#include <xrpl/protocol/TER.h>
25#include <xrpl/protocol/TxFlags.h>
26#include <xrpl/protocol/jss.h>
27
28#include <cstdint>
29#include <map>
30#include <optional>
31#include <set>
32#include <stdexcept>
33#include <string>
34#include <utility>
35#include <vector>
36
38
40create(Account const& proposer, json::Value const& proposedTx, std::uint32_t expiration)
41{
42 json::Value jv;
43 jv[jss::TransactionType] = jss::TransactionProposalCreate;
44 jv[jss::Account] = proposer.human();
45 jv[sfProposedTransaction.jsonName] = proposedTx;
46 jv[sfExpiration.jsonName] = expiration;
47 return jv;
48}
49
51unsignedPayload(Env const& env, json::Value tx, std::uint32_t ticketSeq)
52{
53 // Unsigned canonical form: an empty SigningPubKey and no signature fields
54 // at all. Signatures may only ever arrive through TransactionProposalSign.
55 tx[jss::SigningPubKey] = "";
56
57 // Ticket-based rather than sequence-based. Sequence is a required common
58 // field, so "no Sequence" is expressed as a Sequence of 0.
59 tx[jss::Sequence] = 0;
60 tx[sfTicketSequence.jsonName] = ticketSeq;
61
62 // The target account pays this fee when the completed transaction is
63 // submitted, so it is fixed now. A fee already chosen by the caller stands.
64 fillFee(tx, *env.current());
65
66 return tx;
67}
68
71{
72 return batch::Inner{std::move(tx), seq}.getTxn();
73}
74
77 Env const& env,
78 Account const& target,
79 std::uint32_t ticketSeq,
80 std::uint32_t flags,
81 std::vector<json::Value> const& inners,
83{
84 // Each inner account other than the outer one will contribute one
85 // BatchSigners entry once the signatures are collected, and the outer fee
86 // has to cover them from the start (Batch::calculateBaseFee).
87 std::uint32_t const signers = numSigners ? *numSigners : [&]() {
88 std::set<std::string> participants;
89 for (auto const& inner : inners)
90 {
91 if (auto const account = inner[jss::Account].asString(); account != target.human())
92 participants.insert(account);
93 }
94 return static_cast<std::uint32_t>(participants.size());
95 }();
96
98 target,
99 0,
100 batch::calcBatchFee(env, signers, static_cast<std::uint32_t>(inners.size())),
101 flags);
102
103 json::Value& rawTransactions = jv[jss::RawTransactions];
104 for (auto const& inner : inners)
105 rawTransactions[rawTransactions.size()][jss::RawTransaction] = inner;
106
107 return unsignedPayload(env, std::move(jv), ticketSeq);
108}
109
110void
111authorizeProposer(Env& env, Account const& target, Account const& proposer)
112{
113 env(signers(target, 1, {{proposer, 1}}));
114 env.close();
115}
116
118createTicket(Env& env, Account const& account, std::uint32_t count)
119{
120 // The tickets a TicketCreate makes are numbered from the sequence that
121 // follows the one it consumes.
122 std::uint32_t const firstTicketSeq = env.seq(account) + 1;
123 env(ticket::create(account, count));
124 env.close();
125 return firstTicketSeq;
126}
127
130{
131 return (env.now() + delta).time_since_epoch().count();
132}
133
135entry(Env const& env, AccountID const& target, std::uint32_t ticketSeq)
136{
137 return env.le(keylet::txProposal(target, ticketSeq));
138}
139
141entry(Env const& env, Account const& target, std::uint32_t ticketSeq)
142{
143 return entry(env, target.id(), ticketSeq);
144}
145
146namespace {
147
148// The keys an account's owner directory lists, each with the page it sits on.
149// The pages are read directly, so a key with nothing behind it is still seen.
151ownerDirKeys(ReadView const& view, AccountID const& account)
152{
154
155 auto const root = keylet::ownerDir(account);
156 std::uint64_t page = 0;
157 for (auto sle = view.read(root); sle;)
158 {
159 for (auto const& key : sle->getFieldV256(sfIndexes))
160 keys.emplace(key, page);
161
162 page = sle->getFieldU64(sfIndexNext);
163 if (page == 0)
164 break;
165 sle = view.read(keylet::page(root, page));
166 }
167
168 return keys;
169}
170
171// Whether two reads of a ledger entry found it unchanged, or found nothing both
172// times. Entries that are there are compared whole.
173bool
174unchanged(SLE::const_pointer const& before, SLE::const_pointer const& after)
175{
176 if (!before || !after)
177 return !before && !after;
178
179 return before->key() == after->key() &&
180 static_cast<STObject const&>(*before) == static_cast<STObject const&>(*after);
181}
182
183// Whether a TransactionProposal entry may carry the field when it is created.
184// A fresh proposal has gathered no signatures, so it carries nothing else —
185// except a Sponsor, which a reserve-sponsored creation stamps on the entry from
186// the start.
187bool
188isCreationField(SField const& field)
189{
190 return field == sfLedgerEntryType || field == sfFlags || field == sfOwner ||
191 field == sfProposedTransaction || field == sfExpiration || field == sfOwnerNode ||
192 field == sfPreviousTxnID || field == sfPreviousTxnLgrSeq || field == sfSponsor;
193}
194
195} // namespace
196
197void
199{
200 // Only a TransactionProposalCreate carries the fields read below, and a
201 // condition that quietly checks nothing is worse than none at all.
202 if (jt.jv[jss::TransactionType].asString() != jss::TransactionProposalCreate.cStr())
203 Throw<std::logic_error>("proposal::verify::create: not a TransactionProposalCreate");
204
205 // Funclets run before the transaction is applied, so everything read here
206 // is the state the effects are measured against.
207 auto const& proposedTx = jt.jv[sfProposedTransaction.jsonName];
208 auto const parsedTarget = parseBase58<AccountID>(proposedTx[jss::Account].asString());
209
210 // A payload naming no usable target is a malformed case a test is making
211 // on purpose, and has no ledger effect to measure.
212 if (!parsedTarget)
213 return;
214
215 auto const target = *parsedTarget;
216 auto const proposer = env.lookup(jt.jv[jss::Account].asString());
217 auto const ticketSeq = proposedTx[sfTicketSequence.jsonName].asUInt();
218 auto const expiration = jt.jv[sfExpiration.jsonName].asUInt();
219 auto const cost = proposedTx[jss::TransactionType].asString() == jss::Batch.cStr()
222
223 std::optional<Account> reserveSponsor;
224 if (jt.jv.isMember(sfSponsor.jsonName) &&
225 (jt.jv[sfSponsorFlags.jsonName].asUInt() & spfSponsorReserve) != 0)
226 reserveSponsor.emplace(env.lookup(jt.jv[sfSponsor.jsonName].asString()));
227
228 // Every entry the transaction could touch, read whole, so what follows can
229 // say that nothing moved rather than that the fields we named did not.
230 auto const& view = *env.current();
231 auto const proposalKeylet = keylet::txProposal(target, ticketSeq);
232 auto const ownerCountBefore = env.ownerCount(proposer);
233 auto const sponsoredOwnerCountBefore = env.sponsoredOwnerCount(proposer);
234 auto const sponsoringOwnerCountBefore =
235 reserveSponsor ? std::optional{env.sponsoringOwnerCount(*reserveSponsor)} : std::nullopt;
236 auto const proposalBefore = view.read(proposalKeylet);
237 auto const targetBefore = view.read(keylet::account(target));
238 auto const ticketBefore = view.read(keylet::ticket(target, SeqProxy::rawTicket(ticketSeq)));
239 auto const proposerDirBefore = ownerDirKeys(view, proposer.id());
240 auto const targetDirBefore = ownerDirKeys(view, target);
241
242 jt.require.emplace_back([=](Env& applied) {
243 auto& test = applied.test;
244 auto const& view = *applied.current();
245
246 auto const created = isTesSuccess(applied.ter());
247
248 // The proposer owns the proposal even when another account covers its
249 // reserve. A proposed Batch costs more owner-count increments.
250 test.expect(
251 applied.ownerCount(proposer) == ownerCountBefore + (created ? cost : 0),
252 "proposal reserve");
253 test.expect(
254 applied.sponsoredOwnerCount(proposer) ==
255 sponsoredOwnerCountBefore + (created && reserveSponsor ? cost : 0),
256 "proposal sponsored owner count");
257 if (reserveSponsor)
258 {
259 test.expect(
260 applied.sponsoringOwnerCount(*reserveSponsor) ==
261 *sponsoringOwnerCountBefore + (created ? cost : 0),
262 "proposal sponsoring owner count");
263 }
264
265 // The target's ticket is left for the proposed transaction, including
266 // when the target is also the proposer.
267 test.expect(
268 unchanged(
269 ticketBefore, view.read(keylet::ticket(target, SeqProxy::rawTicket(ticketSeq)))),
270 "proposal target ticket");
271
272 // Nothing else of a distinct target's moves: the proposal belongs in
273 // the proposer's account and owner directory.
274 if (target != proposer.id())
275 {
276 test.expect(
277 unchanged(targetBefore, view.read(keylet::account(target))),
278 "proposal target account");
279 test.expect(ownerDirKeys(view, target) == targetDirBefore, "proposal target directory");
280 }
281
282 auto const sleProposal = view.read(proposalKeylet);
283
284 if (!created)
285 {
286 // A create that did not succeed leaves the proposal as it found
287 // it, down to the last field.
288 test.expect(unchanged(proposalBefore, sleProposal), "proposal unchanged");
289
290 // Nor did the directory gain a listing for an entry that does not
291 // exist.
292 test.expect(
293 ownerDirKeys(view, proposer.id()) == proposerDirBefore, "proposal owner directory");
294 return;
295 }
296
297 // A successful create must have created the entry, not overwritten one
298 // that was already there. The checks below read the entry after the
299 // write, so they pass either way; this is what rules an overwrite out.
300 if (!test.expect(!proposalBefore, "proposal is new") ||
301 !test.expect(sleProposal, "proposal entry"))
302 return;
303
304 // What is on the ledger is what was submitted: a proposal is only worth
305 // collecting signatures against if the transaction it stores is the one
306 // proposed, so the payload is compared whole.
307 test.expect(sleProposal->getAccountID(sfOwner) == proposer.id(), "proposal owner");
308 test.expect(sleProposal->getFieldU32(sfExpiration) == expiration, "proposal expiration");
309 test.expect(
310 reserveSponsor ? sleProposal->isFieldPresent(sfSponsor) &&
311 sleProposal->getAccountID(sfSponsor) == reserveSponsor->id()
312 : !sleProposal->isFieldPresent(sfSponsor),
313 "proposal sponsor");
314
315 auto const& stored = sleProposal->getFieldObject(sfProposedTransaction);
316 test.expect(stored == parse(proposedTx), "proposal payload");
317
318 // Unsigned canonical form, keyed by the target and ticket the payload
319 // names (On-Chain Cosigner spec §6.1).
320 test.expect(
323 "proposal payload unsigned");
324 test.expect(
325 stored.getAccountID(sfAccount) == target &&
326 stored.getFieldU32(sfTicketSequence) == ticketSeq &&
327 stored.getFieldU32(sfSequence) == 0,
328 "proposal payload target");
329
330 // Nothing beyond the fields a fresh proposal is created with. An
331 // STObject carries a placeholder for each optional field its format
332 // allows, so each field is asked whether it is really present.
333 test.expect(sleProposal->getFieldU32(sfFlags) == 0, "proposal flags");
334 for (auto const& field : *sleProposal)
335 {
336 if (field.getSType() != STI_NOTPRESENT)
337 {
338 test.expect(
339 isCreationField(field.getFName()),
340 "proposal field " + field.getFName().getName());
341 }
342 }
343
344 // The proposal is listed in the proposer's directory on the page its
345 // OwnerNode names, and nothing else listed moved.
346 auto expectedDir = proposerDirBefore;
347 expectedDir.emplace(sleProposal->key(), sleProposal->getFieldU64(sfOwnerNode));
348 test.expect(ownerDirKeys(view, proposer.id()) == expectedDir, "proposal owner directory");
349 });
350}
351
352} // namespace xrpl::test::jtx::proposal
Represents a JSON value.
Definition json_value.h:117
UInt size() const
Number of values in array or object.
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.
std::chrono::duration< rep, period > duration
Definition chrono.h:47
A view into a ledger.
Definition ReadView.h:41
virtual SLE::const_pointer read(Keylet const &k) const =0
Return the state item associated with a key.
std::shared_ptr< STLedgerEntry const > const_pointer
static constexpr SeqProxy rawTicket(std::uint32_t v)
Factory function to return a ticket-based SeqProxy.
Definition SeqProxy.h:74
Immutable cryptographic account descriptor.
Definition jtx/Account.h:21
std::string const & human() const
Returns the human readable public key.
AccountID id() const
Returns the Account ID.
A transaction testing environment.
Definition Env.h:161
bool close(NetClock::time_point closeTime, std::optional< std::chrono::milliseconds > consensusDelay=std::nullopt)
Close and advance the ledger.
Definition Env.cpp:133
TER ter() const
Return the TER for the last JTx.
Definition Env.h:844
std::uint32_t sponsoringOwnerCount(Account const &account) const
Return the number of sponsoring objects owned by an account.
Definition Env.cpp:284
SLE::const_pointer le(Account const &account) const
Return an account root.
Definition Env.cpp:311
std::uint32_t ownerCount(Account const &account) const
Return the number of objects owned by an account.
Definition Env.cpp:266
Account const & lookup(AccountID const &id) const
Returns the Account given the AccountID.
Definition Env.cpp:180
std::uint32_t seq(Account const &account) const
Returns the next sequence number on account.
Definition Env.cpp:302
std::uint32_t sponsoredOwnerCount(Account const &account) const
Return the number of sponsored objects owned by an account.
Definition Env.cpp:275
beast::unit_test::Suite & test
Definition Env.h:163
std::shared_ptr< OpenView const > current() const
Returns the current ledger.
Definition Env.h:377
NetClock::time_point now()
Returns the current network time.
Definition Env.h:326
Adds an inner Batch transaction to a JTx and autofills it.
Definition batch.h:91
json::Value const & getTxn() const
Definition batch.h:132
void operator()(Env &, JTx &) const
Definition proposal.cpp:198
T emplace_back(T... args)
T emplace(T... args)
T insert(T... args)
Keylet ownerDir(AccountID const &id) noexcept
The root page of an account's directory.
Definition Indexes.cpp:403
Keylet txProposal(AccountID const &target, std::uint32_t ticketSequence) noexcept
A TransactionProposal.
Definition Indexes.cpp:366
Keylet ticket(AccountID const &id, SeqProxy const &ticketSeq)
A ticket belonging to an account.
Definition Indexes.cpp:332
Keylet page(UInt256 const &root, std::uint64_t const index=0) noexcept
A page in a directory.
Definition Indexes.cpp:409
Keylet account(AccountID const &id) noexcept
AccountID root.
Definition Indexes.cpp:220
bool hasSignatureField(STObject const &proposedTx)
Whether the proposed transaction carries any signature field.
bool hasEmptySigningPubKey(STObject const &proposedTx)
Whether the proposed transaction's SigningPubKey is present and empty, as unsigned canonical form req...
json::Value outer(jtx::Account const &account, uint32_t seq, STAmount const &fee, std::uint32_t flags)
Build an outer Batch transaction JSON object.
Definition batch.cpp:55
XRPAmount calcBatchFee(jtx::Env const &env, uint32_t const &numSigners, uint32_t const &txns=0)
Calculate the expected outer Batch transaction fee.
Definition batch.cpp:37
Helpers for constructing TransactionProposal test transactions.
Definition proposal.cpp:37
json::Value innerTx(json::Value tx, std::uint32_t seq)
Put a transaction into the form an inner transaction of a proposed Batch takes, as batch::Inner does ...
Definition proposal.cpp:70
std::uint32_t createTicket(Env &env, Account const &account, std::uint32_t count)
Create tickets for a proposal to be built against, and close the ledger.
Definition proposal.cpp:118
json::Value unsignedPayload(Env const &env, json::Value tx, std::uint32_t ticketSeq)
Put a transaction of any type into the form a proposal stores it in: unsigned and ticket-based,...
Definition proposal.cpp:51
constexpr std::uint32_t kBatchProposalOwnerCount
Owner-reserve increments held by a proposal of a Batch transaction.
constexpr std::uint32_t kProposalOwnerCount
Owner-reserve increments held by a proposal of an ordinary transaction.
SLE::const_pointer entry(Env const &env, AccountID const &target, std::uint32_t ticketSeq)
The proposal stored against a target account's ticket.
Definition proposal.cpp:135
json::Value create(Account const &proposer, json::Value const &proposedTx, std::uint32_t expiration)
Build a TransactionProposalCreate carrying an unsigned proposed transaction.
Definition proposal.cpp:40
void authorizeProposer(Env &env, Account const &target, Account const &proposer)
Give proposer a place on target's SignerList, so it may create proposals against target.
Definition proposal.cpp:111
json::Value unsignedBatch(Env const &env, Account const &target, std::uint32_t ticketSeq, std::uint32_t flags, std::vector< json::Value > const &inners, std::optional< std::uint32_t > numSigners)
An unsigned outer Batch payload holding inners.
Definition proposal.cpp:76
std::uint32_t expiration(Env &env, NetClock::duration delta)
An absolute expiration delta past the environment's current time.
Definition proposal.cpp:129
json::Value create(Account const &account, std::uint32_t count)
Create one of more tickets.
Definition ticket.cpp:16
void fillFee(json::Value &jv, ReadView const &view)
Set the fee automatically.
Definition utility.cpp:69
STObject parse(json::Value const &jv)
Convert JSON to STObject.
Definition utility.cpp:33
json::Value signers(Account const &account, std::uint32_t quorum, std::vector< Signer > const &v)
Definition multisign.cpp:31
std::optional< AccountID > parseBase58(std::string const &s)
Parse AccountID from checked, base58 string.
Number root(Number f, unsigned d)
bool after(NetClock::time_point now, std::uint32_t mark)
Has the specified time passed?
Definition View.cpp:644
BaseUInt< 160, detail::AccountIDTag > AccountID
A 160-bit unsigned that uniquely identifies an account.
Definition AccountID.h:34
bool isTesSuccess(TER x) noexcept
Definition TER.h:683
constexpr FlagValue spfSponsorReserve
Definition TxFlags.h:466
XRPL_NO_SANITIZE_ADDRESS void Throw(Args &&... args)
Definition contract.h:52
T size(T... args)
Execution context for applying a JSON transaction.
Definition JTx.h:27
RequiresT require
Definition JTx.h:29
json::Value jv
Definition JTx.h:28