xrpld
Loading...
Searching...
No Matches
TransactionProposalCreate.cpp
1#include <xrpl/tx/transactors/proposal/TransactionProposalCreate.h>
2
3#include <xrpl/basics/Log.h>
4#include <xrpl/core/ServiceRegistry.h>
5#include <xrpl/ledger/ApplyView.h>
6#include <xrpl/ledger/View.h>
7#include <xrpl/ledger/helpers/AccountRootHelpers.h>
8#include <xrpl/ledger/helpers/DirectoryHelpers.h>
9#include <xrpl/ledger/helpers/ProposalHelpers.h>
10#include <xrpl/ledger/helpers/SponsorHelpers.h>
11#include <xrpl/protocol/AccountID.h>
12#include <xrpl/protocol/Indexes.h>
13#include <xrpl/protocol/Keylet.h>
14#include <xrpl/protocol/SField.h>
15#include <xrpl/protocol/STLedgerEntry.h>
16#include <xrpl/protocol/STObject.h>
17#include <xrpl/protocol/STTx.h>
18#include <xrpl/protocol/SeqProxy.h>
19#include <xrpl/protocol/TER.h>
20#include <xrpl/protocol/XRPAmount.h>
21#include <xrpl/tx/SignerEntries.h>
22#include <xrpl/tx/Transactor.h>
23#include <xrpl/tx/applySteps.h>
24
25#include <algorithm>
26#include <cstdint>
27#include <exception>
28#include <expected>
29#include <memory>
30#include <string>
31
32namespace xrpl {
33
36{
37 if (ctx.tx[sfExpiration] == 0)
38 {
39 JLOG(ctx.j.debug()) << "TransactionProposalCreate: zero expiration.";
40 return temBAD_EXPIRATION;
41 }
42
43 STObject const proposedTx = ctx.tx.getFieldObject(sfProposedTransaction);
44
45 // The proposed transaction must pass "the same [stateless format]
46 // checks it would receive if submitted directly" (On-Chain Cosigner
47 // spec §5.3.1 rule 2), so no statically-dead proposal can be stored.
48 // That is exactly the pair checkValidity runs on a direct submission:
49 // xrpl::preflight (the transactor's own preflight chain) and
50 // passesLocalChecks. TapDryRun accepts the unsigned canonical form;
51 // TapProposal skips signature-presence checks (§5.3.1.2). A failure
52 // surfaces the proposed type's own code, or temMALFORMED if the
53 // payload is not even a valid instance of that type.
54 try
55 {
56 STTx const stx{STObject{proposedTx}};
57 auto const inner =
58 xrpl::preflight(ctx.registry, ctx.rules, stx, TapDryRun | TapProposal, ctx.j);
59 if (!isTesSuccess(inner.ter))
60 {
61 JLOG(ctx.j.debug()) << "TransactionProposalCreate: proposed txn "
62 "failed preflight: "
63 << transHuman(inner.ter);
64 return inner.ter;
65 }
66 if (std::string reason; !passesLocalChecks(stx, reason))
67 {
68 JLOG(ctx.j.debug()) << "TransactionProposalCreate: proposed txn "
69 "fails local checks: "
70 << reason;
71 return temMALFORMED;
72 }
73 }
74 catch (std::exception const& e)
75 {
76 JLOG(ctx.j.debug()) << "TransactionProposalCreate: proposed txn is "
77 "malformed: "
78 << e.what();
79 return temMALFORMED;
80 }
81
82 // The proposed transaction must be independently submittable through the
83 // ordinary multi-sign path: no nested proposals, no pseudo-transactions,
84 // no batch inner transactions — and, if it is a Batch, none of its own
85 // inner transactions may be a nested proposal or a pseudo-transaction
86 // either.
87 if (!proposal::isValidProposal(proposedTx))
88 {
89 JLOG(ctx.j.debug()) << "TransactionProposalCreate: proposed txn is not "
90 "independently submittable.";
91 return temINVALID;
92 }
93
94 // The proposed transaction is stored in its unsigned canonical form; the
95 // ledger populates its signature fields as contributions arrive.
96 if (proposal::hasSignatureField(proposedTx))
97 {
98 JLOG(ctx.j.debug()) << "TransactionProposalCreate: proposed txn "
99 "carries signature fields.";
100 return temBAD_SIGNER;
101 }
102
103 if (!proposal::hasEmptySigningPubKey(proposedTx))
104 {
105 JLOG(ctx.j.debug()) << "TransactionProposalCreate: proposed txn "
106 "SigningPubKey must be present and empty.";
107 return temBAD_SIGNER;
108 }
109
110 // The proposed transaction must be ticket-based: it must carry a
111 // TicketSequence and must not use a live Sequence. Sequence is a required
112 // common field, so "no Sequence" is expressed as a Sequence of 0 rather
113 // than an absent field. A ticket decouples the proposal from the target
114 // account's live sequence, so unrelated target-account activity cannot
115 // invalidate it while signatures are collected (On-Chain Cosigner spec
116 // §4.2.1).
117 if (!proposedTx.isFieldPresent(sfTicketSequence) || proposedTx.getFieldU32(sfSequence) != 0)
118 return temSEQ_AND_TICKET;
119
120 // If this transaction itself is paying with a Ticket, and the proposed
121 // transaction is targeting that same account and Ticket, then applying
122 // this transaction consumes the very Ticket the proposal depends on
123 // before the proposal is even stored: the proposal would be dead on
124 // arrival, and its only recourse would be TransactionProposalCancel.
125 if (ctx.tx.getSeqProxy().isTicket() &&
126 proposedTx.getAccountID(sfAccount) == ctx.tx.getAccountID(sfAccount) &&
127 proposedTx.getFieldU32(sfTicketSequence) == ctx.tx.getSeqProxy().value())
128 {
129 JLOG(ctx.j.debug()) << "TransactionProposalCreate: proposed txn "
130 "reuses the Ticket this transaction itself consumes.";
131 return temMALFORMED;
132 }
133
134 return tesSUCCESS;
135}
136
137TER
139{
140 if (hasExpired(ctx.view, ctx.tx[~sfExpiration]))
141 {
142 JLOG(ctx.j.debug()) << "TransactionProposalCreate: already expired.";
143 return tecEXPIRED;
144 }
145
146 auto const proposedTx = ctx.tx.getFieldObject(sfProposedTransaction);
147
148 // Once the proposed transaction's own ledger bound has passed it can never
149 // be applied, so the proposal is dead on arrival. The bound is the one the
150 // ordinary path uses for tefMAX_LEDGER: the last ledger in which the
151 // proposed transaction may still be submitted (On-Chain Cosigner spec
152 // §4.5).
153 if (proposedTx.isFieldPresent(sfLastLedgerSequence) &&
154 proposedTx.getFieldU32(sfLastLedgerSequence) <= ctx.view.seq())
155 {
156 JLOG(ctx.j.debug()) << "TransactionProposalCreate: proposed txn "
157 "LastLedgerSequence has passed.";
158 return tecEXPIRED;
159 }
160
161 AccountID const target = proposedTx.getAccountID(sfAccount);
162 auto const sleTarget = ctx.view.read(keylet::account(target));
163 if (!sleTarget)
164 {
165 JLOG(ctx.j.debug()) << "TransactionProposalCreate: target account "
166 "does not exist.";
167 return tecNO_TARGET;
168 }
169
170 // A pseudo-account cannot authorize a transaction through a SignerList.
171 if (isPseudoAccount(sleTarget))
172 return tecNO_PERMISSION;
173
174 // Only the target account itself, an account on its SignerList, or (if
175 // the proposed transaction's own type has been delegated by the target,
176 // Permission Delegation / XLS-75) that delegate or an account on the
177 // delegate's own SignerList, may create a proposal against it. Otherwise
178 // any account could spam or squat the target's Tickets with unwanted
179 // proposals (On-Chain Cosigner V1 scope).
180 if (AccountID const proposer = ctx.tx.getAccountID(sfAccount); proposer != target)
181 {
182 // Whether `proposer` is `account` itself or an entry on `account`'s
183 // applicable SignerList.
184 auto isAuthorizedFor = [&](AccountID const& account) -> std::expected<bool, TER> {
185 if (proposer == account)
186 return true;
187
188 auto const sleSigners = ctx.view.read(keylet::signerList(account));
189 if (!sleSigners)
190 return false;
191
192 // deserialize itself returns unexpected(temMALFORMED) when
193 // sfSignerEntries is missing or an element is not an sfSignerEntry.
194 // Those are the right codes for a transaction object. Here the object
195 // is an on-ledger ltSIGNER_LIST (sfSignerEntries is SoeRequired;
196 // each element is an sfSignerEntry). A corrupt SLE can still throw
197 // from the STObject accessors deserialize calls: getFieldArray
198 // ("Wrong field type") or getAccountID/getFieldU16 ("Field not
199 // found") when an sfSignerEntry is missing required fields. Either
200 // the expected<> error or a throw is unexpected ledger state, not a
201 // malformed TransactionProposalCreate, so tefBAD_LEDGER (rather
202 // than tefINTERNAL, which is reserved for truly unreachable code
203 // paths) is the right code.
204 try
205 {
206 auto const accountSigners =
207 SignerEntries::deserialize(*sleSigners, ctx.j, "ledger");
208 if (!accountSigners)
209 {
210 // Only reachable if the on-ledger SignerList is corrupt
211 // (SignerListSet re-runs the same deserialize on write).
212 // Exercised by testCorruptSignerList via an OpenLedger
213 // overlay that produces the same failure modes.
214 JLOG(ctx.j.fatal()) << "TransactionProposalCreate: unparseable SignerList: "
215 << transToken(accountSigners.error());
217 }
218
219 return std::ranges::any_of(
220 *accountSigners, [&](auto const& entry) { return entry.account == proposer; });
221 }
222 catch (std::exception const& e)
223 {
224 // Same as above: only reachable via ledger corruption that
225 // makes an STObject accessor throw. Exercised by
226 // testCorruptSignerList.
227 JLOG(ctx.j.fatal())
228 << "TransactionProposalCreate: unparseable SignerList: " << e.what();
230 }
231 };
232
233 auto isSigner = isAuthorizedFor(target);
234 if (!isSigner)
235 return isSigner.error();
236
237 // A delegate that the target has granted permission over the
238 // proposed transaction — or one of that delegate's own signers — is
239 // equally authorized: it will need to help complete the proposed
240 // transaction's own authorization anyway once the proposal is
241 // submitted. xrpl::invokeCheckPermission is the type-erased
242 // submission hierarchy (not checkTxPermission alone, which would
243 // reject a matching granular grant). Qualify xrpl:: so the inherited
244 // Transactor template is not chosen; it cannot deduce T here. A
245 // failed grant is still "not authorized" and becomes
246 // tecNO_PERMISSION below — Create is already signed, so do not leak
247 // the pre-sign terNO_DELEGATE_PERMISSION.
248 if (!*isSigner && proposedTx.isFieldPresent(sfDelegate))
249 {
250 AccountID const delegateAccount = proposedTx.getAccountID(sfDelegate);
251 STTx const proposedStTx{STObject{proposedTx}};
252 if (isTesSuccess(xrpl::invokeCheckPermission(ctx.view, proposedStTx)))
253 {
254 // A grant cannot exist without a funded authorize (DelegateSet
255 // uses tecNO_TARGET; AccountDelete of the delegatee removes the
256 // Delegate SLE). Do not treat a missing account as a Create-time
257 // user error — that would extra-validate the proposed tx. If
258 // permission passed anyway, the ledger is corrupt.
259 if (!ctx.view.exists(keylet::account(delegateAccount)))
260 return tefINTERNAL; // LCOV_EXCL_LINE
261 isSigner = isAuthorizedFor(delegateAccount);
262 if (!isSigner)
263 return isSigner.error();
264 }
265 }
266
267 if (!*isSigner)
268 {
269 JLOG(ctx.j.debug()) << "TransactionProposalCreate: proposer is "
270 "not the target account, one of its "
271 "signers, or an authorized delegate.";
272 return tecNO_PERMISSION;
273 }
274 }
275
276 std::uint32_t const ticketSequence = proposedTx.getFieldU32(sfTicketSequence);
277
278 // The proposal reserves the ticket for as long as it exists (On-Chain
279 // Cosigner spec §4.2.1, §5.3.2): a ticket that doesn't exist yet can't be
280 // reserved.
281 if (!ctx.view.exists(keylet::ticket(target, SeqProxy::rawTicket(ticketSequence))))
282 {
283 JLOG(ctx.j.debug()) << "TransactionProposalCreate: target ticket "
284 "does not exist.";
285 return tefNO_TICKET;
286 }
287
288 if (ctx.view.exists(keylet::txProposal(target, ticketSequence)))
289 {
290 JLOG(ctx.j.debug()) << "TransactionProposalCreate: duplicate proposal.";
291 return tecDUPLICATE;
292 }
293
294 return tesSUCCESS;
295}
296
297TER
299{
300 auto const sle = view().peek(keylet::account(accountID_));
301 if (!sle)
302 return tefINTERNAL; // LCOV_EXCL_LINE
303
304 auto const proposedTx = ctx_.tx.getFieldObject(sfProposedTransaction);
306
307 // The proposal holds a full transaction plus its collected signatures, so
308 // it reserves more than a typical ledger entry (5 increments; 10 for a
309 // proposed Batch).
310 if (auto const ret = checkReserve(
311 ctx_.getApplyViewContext(),
312 sle,
314 {.ownerCountDelta = static_cast<int>(ownerCount)},
315 ctx_.journal);
316 !isTesSuccess(ret))
317 return ret;
318
319 AccountID const target = proposedTx.getAccountID(sfAccount);
320 std::uint32_t const ticketSequence = proposedTx.getFieldU32(sfTicketSequence);
321
322 Keylet const proposalKeylet = keylet::txProposal(target, ticketSequence);
323 auto sleProposal = std::make_shared<SLE>(proposalKeylet);
324 sleProposal->setAccountID(sfOwner, accountID_);
325 sleProposal->setFieldObject(sfProposedTransaction, proposedTx);
326 sleProposal->setFieldU32(sfExpiration, ctx_.tx[sfExpiration]);
327
328 view().insert(sleProposal);
329
330 auto viewJ = ctx_.registry.get().getJournal("View");
331 {
332 auto const page = view().dirInsert(
334 if (!page)
335 return tecDIR_FULL; // LCOV_EXCL_LINE
336 sleProposal->setFieldU64(sfOwnerNode, *page);
337 }
338
339 increaseOwnerCount(ctx_.getApplyViewContext(), sle, ownerCount, viewJ);
340 addSponsorToLedgerEntry(ctx_.getApplyViewContext(), sleProposal);
341 return tesSUCCESS;
342}
343
344void
346{
347 // No transaction-specific invariants yet (future work). Object-level
348 // invariants for the TransactionProposal ledger entry (unsigned canonical
349 // form, non-zero Expiration, correct ProposalID key, sorted/unique signer
350 // arrays) belong in a protocol-level ValidTransactionProposal check.
351}
352
353bool
355 STTx const&,
356 TER,
357 XRPAmount,
358 ReadView const&,
359 beast::Journal const&)
360{
361 // No transaction-specific invariants yet (future work).
362 return true;
363}
364
365} // namespace xrpl
T any_of(T... args)
A generic endpoint for log messages.
Definition Journal.h:44
Stream fatal() const
Definition Journal.h:368
Stream debug() const
Definition Journal.h:344
virtual SLE::pointer peek(Keylet const &k)=0
Prepare to modify the SLE associated with key.
virtual void insert(SLE::Ref sle)=0
Insert a new state SLE.
std::optional< std::uint64_t > dirInsert(Keylet const &directory, UInt256 const &key, std::function< void(SLE::Ref)> const &describe)
Insert an entry to a directory.
Definition ApplyView.h:373
A view into a ledger.
Definition ReadView.h:41
virtual bool exists(Keylet const &k) const =0
Determine if a state item exists.
virtual SLE::const_pointer read(Keylet const &k) const =0
Return the state item associated with a key.
LedgerIndex seq() const
Returns the sequence number of the base ledger.
Definition ReadView.h:115
std::shared_ptr< STLedgerEntry const > const & ConstRef
std::uint32_t getFieldU32(SField const &field) const
Definition STObject.cpp:601
bool isFieldPresent(SField const &field) const
Definition STObject.cpp:464
STObject getFieldObject(SField const &field) const
Definition STObject.cpp:678
AccountID getAccountID(SField const &field) const
Definition STObject.cpp:643
SeqProxy getSeqProxy() const
Definition STTx.cpp:198
constexpr bool isTicket() const
Definition SeqProxy.h:92
static constexpr SeqProxy rawTicket(std::uint32_t v)
Factory function to return a ticket-based SeqProxy.
Definition SeqProxy.h:74
constexpr std::uint32_t value() const
Definition SeqProxy.h:80
static std::expected< std::vector< SignerEntry >, NotTEC > deserialize(STObject const &obj, beast::Journal journal, std::string_view annotation)
void visitInvariantEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef after) override
Inspect a single ledger entry modified by this transaction.
bool finalizeInvariants(STTx const &tx, TER result, XRPAmount fee, ReadView const &view, beast::Journal const &j) override
Check transaction-specific post-conditions after all entries have been visited.
static TER preclaim(PreclaimContext const &ctx)
static NotTEC preflight(PreflightContext const &ctx)
ApplyView & view()
Definition Transactor.h:184
AccountID const accountID_
Definition Transactor.h:166
XRPAmount preFeeBalance_
Definition Transactor.h:167
ApplyContext & ctx_
Definition Transactor.h:162
T make_shared(T... args)
Keylet signerList(AccountID const &account) noexcept
A SignerList.
Definition Indexes.cpp:348
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 account(AccountID const &id) noexcept
AccountID root.
Definition Indexes.cpp:220
std::uint32_t proposalOwnerCount(STObject const &proposedTx)
Owner-reserve increments held by a proposal of the given transaction.
bool hasSignatureField(STObject const &proposedTx)
Whether the proposed transaction carries any signature field.
bool isValidProposal(STObject const &proposedTx)
Whether the proposed transaction is independently submittable through the ordinary multi-sign path: n...
bool hasEmptySigningPubKey(STObject const &proposedTx)
Whether the proposed transaction's SigningPubKey is present and empty, as unsigned canonical form req...
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
bool hasExpired(ReadView const &view, std::optional< std::uint32_t > const &exp, ExpiryComparison comparison=ExpiryComparison::Inclusive)
Determines whether the given expiration time has passed.
Definition View.cpp:51
PreflightResult preflight(ServiceRegistry &registry, Rules const &rules, STTx const &tx, ApplyFlags flags, beast::Journal j)
Gate a transaction based on static information.
std::uint32_t ownerCount(SLE::ConstRef sle, beast::Journal j, std::int32_t ownerCountAdj=0)
Return number of the objects which reserve is covered by the account(sle) (so called "ownercount").
@ tefBAD_LEDGER
Definition TER.h:165
@ tefNO_TICKET
Definition TER.h:180
@ tefINTERNAL
Definition TER.h:168
std::string transHuman(TER code)
Definition TER.cpp:266
TER checkReserve(ApplyViewContext ctx, SLE::ConstRef accSle, XRPAmount accBalance, SLE::ConstRef sponsorSle, Adjustment adj, beast::Journal j, TER insufReserveCode=tecINSUFFICIENT_RESERVE)
Check if an account has sufficient reserve.
void increaseOwnerCount(ApplyView &view, SLE::Ref accountSle, SLE::Ref sponsorSle, std::uint32_t count, beast::Journal j)
Increase owner-count fields when the caller supplies the sponsor.
std::string transToken(TER code)
Definition TER.cpp:257
void addSponsorToLedgerEntry(SLE::Ref sle, SLE::ConstRef sponsorSle, SF_ACCOUNT const &field=sfSponsor)
Stamp a reserve sponsor onto a ledger entry using an explicit sponsor SLE.
bool passesLocalChecks(STTx const &tx, std::string &)
Definition STTx.cpp:850
TERSubset< CanCvtToNotTEC > NotTEC
Definition TER.h:614
NotTEC invokeCheckPermission(ReadView const &view, STTx const &tx)
Type-erased overload of Transactor::invokeCheckPermission.
@ TapDryRun
Definition ApplyView.h:46
@ TapProposal
Definition ApplyView.h:53
bool isPseudoAccount(SLE::const_pointer sleAcct)
Returns true if and only if sleAcct is a pseudo-account of any kind (i.e.
BaseUInt< 160, detail::AccountIDTag > AccountID
A 160-bit unsigned that uniquely identifies an account.
Definition AccountID.h:34
@ temBAD_EXPIRATION
Definition TER.h:79
@ temINVALID
Definition TER.h:98
@ temMALFORMED
Definition TER.h:75
@ temSEQ_AND_TICKET
Definition TER.h:114
@ temBAD_SIGNER
Definition TER.h:103
bool isTesSuccess(TER x) noexcept
Definition TER.h:683
TERSubset< CanCvtToTER > TER
Definition TER.h:654
@ tecDIR_FULL
Definition TER.h:295
@ tecNO_TARGET
Definition TER.h:312
@ tecEXPIRED
Definition TER.h:322
@ tecNO_PERMISSION
Definition TER.h:313
@ tecDUPLICATE
Definition TER.h:323
std::function< void(SLE::Ref)> describeOwnerDir(AccountID const &account)
Returns a function that sets the owner on a directory SLE.
@ tesSUCCESS
Definition TER.h:250
A pair of SHAMap key and LedgerEntryType.
Definition Keylet.h:20
State information when determining if a tx is likely to claim a fee.
Definition Transactor.h:92
ReadView const & view
Definition Transactor.h:95
beast::Journal const j
Definition Transactor.h:100
State information when preflighting a tx.
Definition Transactor.h:39
beast::Journal const j
Definition Transactor.h:46
std::reference_wrapper< ServiceRegistry > registry
Definition Transactor.h:41
T unexpected(T... args)
T what(T... args)