mirror of
https://github.com/XRPLF/rippled.git
synced 2026-10-22 03:27:08 +00:00
feat: Add TransactionProposalCancel & Cosign Ticket reservation (#8538)
Co-authored-by: Shawn Xie <[email protected]> Co-authored-by: Kassaking7 <[email protected]> Co-authored-by: Shawn Xie <[email protected]> Co-authored-by: Zhiyuan Wang <[email protected]> Co-authored-by: Claude Fable 5 <[email protected]>
This commit is contained in:
18 files changed
+3987
-42
No files matched your search
@@ -1,10 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
|
||||
namespace xrpl::proposal {
|
||||
|
||||
@@ -30,31 +37,22 @@ proposalOwnerCount(STObject const& proposedTx)
|
||||
: kProposalOwnerCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the proposed transaction is itself a proposal transaction, which
|
||||
* would nest one proposal inside another.
|
||||
*
|
||||
* TODO: cover ttTRANSACTION_PROPOSAL_SIGN and ttTRANSACTION_PROPOSAL_CANCEL
|
||||
* once those transactions exist.
|
||||
*/
|
||||
inline bool
|
||||
isProposalTx(STObject const& proposedTx)
|
||||
{
|
||||
return proposedTx.getFieldU16(sfTransactionType) == ttTRANSACTION_PROPOSAL_CREATE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the proposed transaction is independently submittable through the
|
||||
* ordinary multi-sign path: not a nested proposal, not a pseudo-transaction,
|
||||
* not itself flagged as someone else's inner batch transaction, and — if it
|
||||
* is a Batch — none of its own inner transactions is a nested proposal or a
|
||||
* pseudo-transaction either. A Batch inner transaction cannot itself be
|
||||
* pseudo (preflight0 rejects the pseudo/tfInnerBatchTxn combination
|
||||
* generically), but that guard lives outside this feature, so it is checked
|
||||
* again here rather than relied upon.
|
||||
* ordinary multi-sign path: not a proposal transaction itself, not a
|
||||
* pseudo-transaction, not itself flagged as someone else's inner batch
|
||||
* transaction, and — if it is a Batch — every one of its own inner
|
||||
* transactions carries a TransactionType and is neither a proposal, a
|
||||
* pseudo-transaction, nor a nested Batch. A Batch inner transaction cannot
|
||||
* lack a TransactionType, be pseudo, or be a nested Batch (STTx construction
|
||||
* rejects a typeless inner and an inner Batch, and preflight0 rejects the
|
||||
* pseudo/tfInnerBatchTxn combination), but those guards live outside this
|
||||
* feature, so they are checked again here rather than relied upon.
|
||||
*
|
||||
* TODO: cover ttTRANSACTION_PROPOSAL_SIGN once that transaction exists.
|
||||
*/
|
||||
bool
|
||||
isValidProposal(STObject const& proposedTx);
|
||||
isValidProposalTxnType(STObject const& proposedTx);
|
||||
|
||||
/**
|
||||
* Whether the proposed transaction carries any signature field.
|
||||
@@ -84,4 +82,93 @@ hasEmptySigningPubKey(STObject const& proposedTx)
|
||||
proposedTx.getFieldVL(sfSigningPubKey).empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the proposal is terminal (On-Chain Cosigner spec §4.5). A terminal proposal
|
||||
* stops accepting signatures and anyone may delete it. An already signed
|
||||
* payload can still execute after Expiration, because the Ticket reservation
|
||||
* compares payloads only; a passed LastLedgerSequence does stop it.
|
||||
*
|
||||
* A proposal is terminal when either:
|
||||
* - its Expiration has passed (the parent ledger closed at or after it), or
|
||||
* - the proposed transaction carries a LastLedgerSequence and the current
|
||||
* ledger sequence is strictly greater than it. This mirrors the ordinary
|
||||
* transactor's tefMAX_LEDGER rule: at view.seq() == LastLedgerSequence the
|
||||
* payload can still be submitted in the current ledger, so the proposal is
|
||||
* still live.
|
||||
*
|
||||
* @param view The ledger the deciding transaction is being applied to.
|
||||
* @param expiration The proposal's Expiration field.
|
||||
* @param proposedTx The proposal's ProposedTransaction field.
|
||||
*/
|
||||
bool
|
||||
isTerminal(
|
||||
ReadView const& view,
|
||||
std::optional<std::uint32_t> expiration,
|
||||
STObject const& proposedTx);
|
||||
|
||||
/**
|
||||
* Delete a TransactionProposal ledger entry.
|
||||
*
|
||||
* Removes the entry from its Owner's directory, releases the reserve the
|
||||
* proposal holds against the Owner, and erases the entry. Shared by every
|
||||
* deletion path the spec defines (On-Chain Cosigner spec §4.5): automatic cleanup when the
|
||||
* proposed transaction's TicketSequence is consumed, and the
|
||||
* TransactionProposalCancel / TransactionProposalSign cleanup paths once
|
||||
* those transactions exist.
|
||||
*
|
||||
* The release goes through decreaseOwnerCountForObject, so it follows the
|
||||
* sfSponsor an entry created with reserve sponsorship carries: the reserve is
|
||||
* returned to whoever was holding it, the Owner or that sponsor.
|
||||
*
|
||||
* @param view The apply view for making changes
|
||||
* @param sleProposal The TransactionProposal ledger entry to delete
|
||||
* @param j Journal for logging
|
||||
* @return tesSUCCESS, tecINTERNAL for an invalid entry, or tefBAD_LEDGER if
|
||||
* the ledger contradicts the entry
|
||||
*/
|
||||
TER
|
||||
deleteProposal(ApplyView& view, SLE::pointer const& sleProposal, beast::Journal j);
|
||||
|
||||
/**
|
||||
* Whether tx carries the same payload as a proposal's stored
|
||||
* ProposedTransaction (On-Chain Cosigner spec §4.2.1).
|
||||
*
|
||||
* The payload is the signing payload: the fields the collected signatures
|
||||
* commit to. Compared is every field except those flagged kNotSigning
|
||||
* (TxnSignature, Signers, BatchSigners, CounterpartySignature,
|
||||
* SponsorSignature, Signature, MasterSignature) and SigningPubKey, which is
|
||||
* stored empty and filled only if the target signs with its own key. So the
|
||||
* completed transaction matches no matter which mix of collected or
|
||||
* off-ledger signatures it carries, and any change to a non-signature field
|
||||
* results in a false-return value.
|
||||
*
|
||||
* Signature (PaymentChannelClaim and the XChain attestation transactions)
|
||||
* is deliberately not compared: it is outside the account's own TxnSignature
|
||||
* even for an ordinary submission, so a proposal cannot bind it more tightly
|
||||
* than the signatures it collects do, and those transactors still validate a
|
||||
* substituted value exactly as they would for an ordinary submission.
|
||||
* MasterSignature appears in no transaction format.
|
||||
*
|
||||
* @param proposedTx The proposal's ProposedTransaction field.
|
||||
* @param tx The transaction to compare, typically an STTx.
|
||||
*/
|
||||
bool
|
||||
payloadMatches(STObject const& proposedTx, STObject const& tx);
|
||||
|
||||
/**
|
||||
* Whether tx may consume its Ticket.
|
||||
*
|
||||
* While a proposal keyed to the Ticket exists, the target's Ticket is
|
||||
* reserved: only the proposal's own proposed transaction may spend it
|
||||
* (On-Chain Cosigner spec §4.2.1), so that unrelated target-account activity cannot
|
||||
* invalidate the proposal while signatures are being collected.
|
||||
* TransactionProposalCancel is not allowed through: it deletes the
|
||||
* reservation without consuming the ticket.
|
||||
*
|
||||
* @param view The ledger containing the Ticket and any reservation.
|
||||
* @param tx The transaction attempting to consume that ticket.
|
||||
*/
|
||||
bool
|
||||
canConsumeTicket(ReadView const& view, STTx const& tx);
|
||||
|
||||
} // namespace xrpl::proposal
|
||||
@@ -231,6 +231,8 @@ enum TERcodes : TERUnderlyingType {
|
||||
terNO_DELEGATE_PERMISSION, // Delegate does not have permission
|
||||
terLOCKED, // MPT is locked
|
||||
terNO_PERMISSION, // No permission but retry
|
||||
terTICKET_RESERVED, // Ticket is reserved and cannot be consumed
|
||||
// until the reservation is released
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
@@ -1175,6 +1175,20 @@ TRANSACTION(ttTRANSACTION_PROPOSAL_CREATE, 95, TransactionProposalCreate,
|
||||
{sfExpiration, SoeRequired},
|
||||
}))
|
||||
|
||||
// ttTRANSACTION_PROPOSAL_SIGN arrives in a later PR and takes the next unassigned type.
|
||||
|
||||
/** This transaction deletes a TransactionProposal, releasing its owner's
|
||||
reserve. While the proposal is live only its Owner or its target account
|
||||
may cancel; once it is terminal anyone may (On-Chain Cosigner spec §7). */
|
||||
#if TRANSACTION_INCLUDE
|
||||
# include <xrpl/tx/transactors/proposal/TransactionProposalCancel.h>
|
||||
#endif
|
||||
TRANSACTION(ttTRANSACTION_PROPOSAL_CANCEL, 94, TransactionProposalCancel,
|
||||
({.amendment = featureCosign}),
|
||||
({
|
||||
{sfProposalID, SoeRequired},
|
||||
}))
|
||||
|
||||
/** This system-generated transaction type is used to update the status of the various amendments.
|
||||
|
||||
For details, see: https://xrpl.org/amendments.html
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
// This file is auto-generated. Do not edit.
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/STParsedJSON.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <xrpl/protocol_autogen/TransactionBase.h>
|
||||
#include <xrpl/protocol_autogen/TransactionBuilderBase.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
|
||||
#include <stdexcept>
|
||||
#include <optional>
|
||||
|
||||
namespace xrpl::transactions {
|
||||
|
||||
class TransactionProposalCancelBuilder;
|
||||
|
||||
/**
|
||||
* @brief Transaction: TransactionProposalCancel
|
||||
*
|
||||
* Type: ttTRANSACTION_PROPOSAL_CANCEL (94)
|
||||
* Delegable: Delegation::NotDelegable
|
||||
* Amendment: featureCosign
|
||||
* Privileges: Privilege::NoPriv
|
||||
*
|
||||
* Immutable wrapper around STTx providing type-safe field access.
|
||||
* Use TransactionProposalCancelBuilder to construct new transactions.
|
||||
*/
|
||||
class TransactionProposalCancel : public TransactionBase
|
||||
{
|
||||
public:
|
||||
static constexpr xrpl::TxType txType = ttTRANSACTION_PROPOSAL_CANCEL;
|
||||
|
||||
/**
|
||||
* @brief Construct a TransactionProposalCancel transaction wrapper from an existing STTx object.
|
||||
* @throws std::runtime_error if the transaction type doesn't match.
|
||||
*/
|
||||
explicit TransactionProposalCancel(std::shared_ptr<STTx const> tx)
|
||||
: TransactionBase(std::move(tx))
|
||||
{
|
||||
// Verify transaction type
|
||||
if (tx_->getTxnType() != txType)
|
||||
{
|
||||
throw std::runtime_error("Invalid transaction type for TransactionProposalCancel");
|
||||
}
|
||||
}
|
||||
|
||||
// Transaction-specific field getters
|
||||
|
||||
/**
|
||||
* @brief Get sfProposalID (SoeRequired)
|
||||
* @return The field value.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
SF_UINT256::type::value_type
|
||||
getProposalID() const
|
||||
{
|
||||
return this->tx_->at(sfProposalID);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Builder for TransactionProposalCancel transactions.
|
||||
*
|
||||
* Provides a fluent interface for constructing transactions with method chaining.
|
||||
* Uses STObject internally for flexible transaction construction.
|
||||
* Inherits common field setters from TransactionBuilderBase.
|
||||
*/
|
||||
class TransactionProposalCancelBuilder : public TransactionBuilderBase<TransactionProposalCancelBuilder>
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a new TransactionProposalCancelBuilder with required fields.
|
||||
* @param account The account initiating the transaction.
|
||||
* @param proposalID The sfProposalID field value.
|
||||
* @param sequence Optional sequence number for the transaction.
|
||||
* @param fee Optional fee for the transaction.
|
||||
*/
|
||||
TransactionProposalCancelBuilder(SF_ACCOUNT::type::value_type account,
|
||||
std::decay_t<typename SF_UINT256::type::value_type> const& proposalID, std::optional<SF_UINT32::type::value_type> sequence = std::nullopt,
|
||||
std::optional<SF_AMOUNT::type::value_type> fee = std::nullopt
|
||||
)
|
||||
: TransactionBuilderBase<TransactionProposalCancelBuilder>(ttTRANSACTION_PROPOSAL_CANCEL, account, sequence, fee)
|
||||
{
|
||||
setProposalID(proposalID);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Construct a TransactionProposalCancelBuilder from an existing STTx object.
|
||||
* @param tx The existing transaction to copy from.
|
||||
* @throws std::runtime_error if the transaction type doesn't match.
|
||||
*/
|
||||
TransactionProposalCancelBuilder(std::shared_ptr<STTx const> tx)
|
||||
{
|
||||
if (tx->getTxnType() != ttTRANSACTION_PROPOSAL_CANCEL)
|
||||
{
|
||||
throw std::runtime_error("Invalid transaction type for TransactionProposalCancelBuilder");
|
||||
}
|
||||
object_ = *tx;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Transaction-specific field setters
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Set sfProposalID (SoeRequired)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
TransactionProposalCancelBuilder&
|
||||
setProposalID(std::decay_t<typename SF_UINT256::type::value_type> const& value)
|
||||
{
|
||||
object_[sfProposalID] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Build and return the TransactionProposalCancel wrapper.
|
||||
* @param publicKey The public key for signing.
|
||||
* @param secretKey The secret key for signing.
|
||||
* @return The constructed transaction wrapper.
|
||||
*/
|
||||
TransactionProposalCancel
|
||||
build(PublicKey const& publicKey, SecretKey const& secretKey)
|
||||
{
|
||||
sign(publicKey, secretKey);
|
||||
return TransactionProposalCancel{std::make_shared<STTx>(std::move(object_))};
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace xrpl::transactions
|
||||
@@ -375,6 +375,22 @@ public:
|
||||
/////////////////////////////////////////////////////
|
||||
|
||||
// Interface used by AccountDelete
|
||||
/**
|
||||
* This method removes one Ticket ledger-object from the ledger. AccountDelete uses it to
|
||||
* clean up Ticket objects en masse.
|
||||
*
|
||||
* This method reduces the TicketCount and the OwnerCount of the Owner AccountRoot object. If
|
||||
* a TransactionProposal reserved this Ticket, that proposal is deleted as well: once the
|
||||
* Ticket is gone, its proposed transaction can never be applied (tefNO_TICKET).
|
||||
*
|
||||
* @param view the apply view for making changes.
|
||||
* @param account the account which owns the Ticket object.
|
||||
* @param ticketIndex ticketIndex is used to construct the ledger-entry ID of Ticket object.
|
||||
* @param j the journal reference used to log messages.
|
||||
*
|
||||
* @return the final TER after all invariant checks. `tefBAD_LEDGER` is returned if Ticket,
|
||||
* Owner AccountRoot is not found in the apply-view ledger.
|
||||
*/
|
||||
static TER
|
||||
ticketDelete(
|
||||
ApplyView& view,
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/tx/ApplyContext.h>
|
||||
#include <xrpl/tx/Transactor.h>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
class TransactionProposalCancel : public Transactor
|
||||
{
|
||||
public:
|
||||
static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal;
|
||||
|
||||
explicit TransactionProposalCancel(ApplyContext& ctx) : Transactor(ctx)
|
||||
{
|
||||
}
|
||||
|
||||
static NotTEC
|
||||
preflight(PreflightContext const& ctx);
|
||||
|
||||
static TER
|
||||
preclaim(PreclaimContext const& ctx);
|
||||
|
||||
TER
|
||||
doApply() override;
|
||||
|
||||
void
|
||||
visitInvariantEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef after) override;
|
||||
|
||||
[[nodiscard]] bool
|
||||
finalizeInvariants(
|
||||
STTx const& tx,
|
||||
TER result,
|
||||
XRPAmount fee,
|
||||
ReadView const& view,
|
||||
beast::Journal const& j) override;
|
||||
};
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -1,18 +1,51 @@
|
||||
#include <xrpl/ledger/helpers/ProposalHelpers.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Keylet.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STArray.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
|
||||
namespace xrpl::proposal {
|
||||
|
||||
namespace {
|
||||
|
||||
// Proposals do not nest, so a proposed transaction may carry none of these
|
||||
// types, neither as its own payload nor as a Batch inner transaction.
|
||||
constexpr auto kProposalTxTypes = std::to_array<TxType>({
|
||||
ttTRANSACTION_PROPOSAL_CREATE,
|
||||
ttTRANSACTION_PROPOSAL_CANCEL,
|
||||
});
|
||||
|
||||
} // namespace
|
||||
|
||||
bool
|
||||
isValidProposal(STObject const& proposedTx)
|
||||
isValidProposalTxnType(STObject const& proposedTx)
|
||||
{
|
||||
if (isProposalTx(proposedTx))
|
||||
auto const type = proposedTx.getFieldU16(sfTransactionType);
|
||||
|
||||
if (std::ranges::find(kProposalTxTypes, type) != kProposalTxTypes.end())
|
||||
return false;
|
||||
|
||||
if (isPseudoTx(proposedTx))
|
||||
@@ -22,13 +55,24 @@ isValidProposal(STObject const& proposedTx)
|
||||
(proposedTx.getFieldU32(sfFlags) & tfInnerBatchTxn) != 0u)
|
||||
return false;
|
||||
|
||||
if (proposedTx.getFieldU16(sfTransactionType) == ttBATCH &&
|
||||
proposedTx.isFieldPresent(sfRawTransactions))
|
||||
if (type == ttBATCH && proposedTx.isFieldPresent(sfRawTransactions))
|
||||
{
|
||||
STArray const& innerTxns = proposedTx.getFieldArray(sfRawTransactions);
|
||||
for (STObject const& inner : innerTxns)
|
||||
{
|
||||
if (isProposalTx(inner) || isPseudoTx(inner))
|
||||
// The only production caller reaches this walk through STTx
|
||||
// construction, which rejects a typeless inner first, so this is
|
||||
// re-checked rather than relied upon.
|
||||
if (!inner.isFieldPresent(sfTransactionType))
|
||||
return false;
|
||||
|
||||
auto const innerType = inner.getFieldU16(sfTransactionType);
|
||||
|
||||
// A nested Batch is rejected alongside nested proposals and
|
||||
// pseudo-transactions: STTx construction forbids inner Batches, so
|
||||
// a proposal must not be able to store one either.
|
||||
if (std::ranges::find(kProposalTxTypes, innerType) != kProposalTxTypes.end() ||
|
||||
innerType == ttBATCH || isPseudoTx(inner))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -36,4 +80,112 @@ isValidProposal(STObject const& proposedTx)
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
isTerminal(
|
||||
ReadView const& view,
|
||||
std::optional<std::uint32_t> expiration,
|
||||
STObject const& proposedTx)
|
||||
{
|
||||
if (hasExpired(view, expiration))
|
||||
return true;
|
||||
|
||||
// Mirror the ordinary transactor's tefMAX_LEDGER rule: view.seq() > bound.
|
||||
// At view.seq() == bound the payload can still be submitted, so the
|
||||
// proposal is still live (On-Chain Cosigner spec §4.5).
|
||||
return proposedTx.isFieldPresent(sfLastLedgerSequence) &&
|
||||
view.seq() > proposedTx.getFieldU32(sfLastLedgerSequence);
|
||||
}
|
||||
|
||||
TER
|
||||
deleteProposal(ApplyView& view, SLE::pointer const& sleProposal, beast::Journal j)
|
||||
{
|
||||
if (!sleProposal || sleProposal->getType() != ltTRANSACTION_PROPOSAL ||
|
||||
!view.exists(Keylet{ltTRANSACTION_PROPOSAL, sleProposal->key()}))
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j.fatal()) << "Invalid TransactionProposal deletion.";
|
||||
return tecINTERNAL;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
AccountID const owner = sleProposal->getAccountID(sfOwner);
|
||||
|
||||
std::uint64_t const page{(*sleProposal)[sfOwnerNode]};
|
||||
if (!view.dirRemove(keylet::ownerDir(owner), page, sleProposal->key(), true))
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j.fatal()) << "Unable to delete TransactionProposal from owner.";
|
||||
return tefBAD_LEDGER;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
auto const sleOwner = view.peek(keylet::account(owner));
|
||||
if (!sleOwner)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j.fatal()) << "Could not find TransactionProposal owner account root.";
|
||||
return tefBAD_LEDGER;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
// Release the reserve against the Owner or, if the entry carries a
|
||||
// reserve sponsor, against that sponsor.
|
||||
decreaseOwnerCountForObject(
|
||||
view,
|
||||
sleOwner,
|
||||
sleProposal,
|
||||
proposalOwnerCount(sleProposal->getFieldObject(sfProposedTransaction)),
|
||||
j);
|
||||
|
||||
view.erase(sleProposal);
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
bool
|
||||
payloadMatches(STObject const& proposedTx, STObject const& tx)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
proposedTx.isFieldPresent(sfTransactionType) && tx.isFieldPresent(sfTransactionType),
|
||||
"xrpl::proposal::payloadMatches : transaction-shaped inputs");
|
||||
|
||||
// The signing-payload serialization omits every kNotSigning field: the
|
||||
// signature containers a proposal lets evolve (TxnSignature, Signers,
|
||||
// BatchSigners, CounterpartySignature, SponsorSignature) plus Signature
|
||||
// and MasterSignature, which the account's own signature does not cover
|
||||
// either (see the header). What remains is the field set fixed at
|
||||
// creation — except SigningPubKey, which signing payloads include but
|
||||
// which is also mutable here (stored empty, filled if the target signs
|
||||
// with its own key). Neutralize it on both sides before comparing.
|
||||
auto const payloadBytes = [](STObject const& obj) {
|
||||
STObject copy{obj};
|
||||
copy.setFieldVL(sfSigningPubKey, Slice{});
|
||||
Serializer s;
|
||||
copy.addWithoutSigningFields(s);
|
||||
return s.getData();
|
||||
};
|
||||
|
||||
return payloadBytes(proposedTx) == payloadBytes(tx);
|
||||
}
|
||||
|
||||
bool
|
||||
canConsumeTicket(ReadView const& view, STTx const& tx)
|
||||
{
|
||||
auto const seqProx = tx.getSeqProxy();
|
||||
if (!seqProx.isTicket())
|
||||
{
|
||||
// The caller only reaches here for a Ticket-based transaction.
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::proposal::canConsumeTicket : tx spends a Sequence");
|
||||
return false;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
auto const sleProposal =
|
||||
view.read(keylet::txProposal(tx.getAccountID(sfAccount), seqProx.value()));
|
||||
if (!sleProposal)
|
||||
return true;
|
||||
|
||||
return payloadMatches(sleProposal->getFieldObject(sfProposedTransaction), tx);
|
||||
}
|
||||
|
||||
} // namespace xrpl::proposal
|
||||
@@ -228,6 +228,7 @@ transResults()
|
||||
MAKE_ERROR(terNO_DELEGATE_PERMISSION, "Delegated account lacks permission to perform this transaction."),
|
||||
MAKE_ERROR(terLOCKED, "Fund is locked."),
|
||||
MAKE_ERROR(terNO_PERMISSION, "No permission to perform requested operation."),
|
||||
MAKE_ERROR(terTICKET_RESERVED, "Ticket is reserved."),
|
||||
|
||||
MAKE_ERROR(tesSUCCESS, "The transaction was applied. Only final in a validated ledger."),
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include <xrpl/ledger/helpers/DelegateHelpers.h>
|
||||
#include <xrpl/ledger/helpers/NFTokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/OfferHelpers.h>
|
||||
#include <xrpl/ledger/helpers/ProposalHelpers.h>
|
||||
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
|
||||
#include <xrpl/ledger/helpers/SponsorHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
@@ -327,10 +328,9 @@ Transactor::preflight2(PreflightContext const& ctx)
|
||||
|
||||
auto const sigValid = checkValidity(ctx.registry.get().getHashRouter(), ctx.tx, ctx.rules);
|
||||
if (sigValid.first == Validity::SigBad)
|
||||
{ // LCOV_EXCL_START
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "preflight2: bad signature. " << sigValid.second;
|
||||
return temINVALID;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
// Do not add any checks after this point that are relevant for
|
||||
@@ -755,6 +755,19 @@ Transactor::checkSeqProxy(ReadView const& view, STTx const& tx, beast::Journal j
|
||||
<< "a_seq=" << aSeq << " t_seq=" << tSeqProx;
|
||||
return tefNO_TICKET;
|
||||
}
|
||||
|
||||
// While a TransactionProposal keyed to this Ticket exists, the
|
||||
// Ticket is reserved: only the proposal's own proposed transaction
|
||||
// may consume it, so unrelated activity cannot invalidate the
|
||||
// proposal while signatures are being collected (On-Chain Cosigner spec §4.2.1).
|
||||
// Cancelling the proposal frees the Ticket, so this is a retry, not
|
||||
// a final failure. No proposal can exist before the Cosign amendment,
|
||||
// so the gate only makes that dependency explicit.
|
||||
if (view.rules().enabled(featureCosign) && !proposal::canConsumeTicket(view, tx))
|
||||
{
|
||||
JLOG(j.trace()) << "applyTransaction: ticket " << tSeqProx << " is reserved";
|
||||
return terTICKET_RESERVED;
|
||||
}
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
@@ -866,8 +879,28 @@ Transactor::ticketDelete(
|
||||
// Update the Ticket owner's reserve.
|
||||
decreaseOwnerCountForObject(view, sleAccount, sleTicket, 1, j);
|
||||
|
||||
std::uint32_t const ticketSeq{(*sleTicket)[sfTicketSequence]};
|
||||
|
||||
// Remove Ticket from ledger.
|
||||
view.erase(sleTicket);
|
||||
|
||||
// Once the Ticket is gone, a TransactionProposal keyed to it can never
|
||||
// execute: its proposed transaction would fail with tefNO_TICKET. Clean
|
||||
// up the stale proposal and release its Owner's reserve (On-Chain Cosigner spec §4.5).
|
||||
// The reservation check in checkSeqProxy means this is reached only by
|
||||
// the proposal's own proposed transaction executing (or failing with a
|
||||
// claimed-fee tec), or by AccountDelete sweeping the target account's
|
||||
// Tickets. No proposal can exist before the Cosign amendment, so the gate
|
||||
// only makes that dependency explicit.
|
||||
if (view.rules().enabled(featureCosign))
|
||||
{
|
||||
if (auto const sleProposal = view.peek(keylet::txProposal(account, ticketSeq)))
|
||||
{
|
||||
if (TER const ter = proposal::deleteProposal(view, sleProposal, j); !isTesSuccess(ter))
|
||||
return ter; // LCOV_EXCL_LINE
|
||||
}
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
#include <xrpl/tx/transactors/proposal/TransactionProposalCancel.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/helpers/ProposalHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/tx/Transactor.h>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
NotTEC
|
||||
TransactionProposalCancel::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
if (ctx.tx[sfProposalID] == beast::kZero)
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "TransactionProposalCancel: zero ProposalID.";
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
TransactionProposalCancel::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
uint256 const proposalID = ctx.tx[sfProposalID];
|
||||
|
||||
// Never read the zero keylet. Preflight already rejected a zero
|
||||
// ProposalID, and no real proposal can have one (every ProposalID is a
|
||||
// hash), so reaching here with zero is an internal error. (and a mathematical rarity. )
|
||||
if (proposalID == beast::kZero)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const sleProposal = ctx.view.read(keylet::txProposal(proposalID));
|
||||
if (!sleProposal)
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "TransactionProposalCancel: proposal does not exist.";
|
||||
return tecNO_ENTRY;
|
||||
}
|
||||
|
||||
STObject const proposedTx = sleProposal->getFieldObject(sfProposedTransaction);
|
||||
|
||||
// A terminal proposal is closed to new signatures, so anyone may delete
|
||||
// it and release the Owner's reserve (On-Chain Cosigner spec §7.2). Being
|
||||
// terminal does not stop an already signed payload from executing; see
|
||||
// proposal::isTerminal.
|
||||
bool const terminal = proposal::isTerminal(ctx.view, (*sleProposal)[~sfExpiration], proposedTx);
|
||||
|
||||
// A live proposal may only be cancelled by its Owner (the proposer) or
|
||||
// by its target. The target is the account the proposed transaction
|
||||
// would execute against (its Account field) or the Delegate named in it.
|
||||
// Targets may always refuse a proposal, because anyone can create one
|
||||
// against any account and doing so ties up one of that account's
|
||||
// tickets (On-Chain Cosigner spec §7.2).
|
||||
//
|
||||
// The Delegate is read from the stored payload as-is; whether that
|
||||
// delegation exists on-ledger is not checked. The proposal asks that
|
||||
// account to sign, so that account may refuse — and refusing only
|
||||
// deletes the proposer's own entry and returns the proposer's own
|
||||
// reserve.
|
||||
AccountID const account = ctx.tx[sfAccount];
|
||||
bool const ownerOrTarget = account == sleProposal->getAccountID(sfOwner) ||
|
||||
account == proposedTx.getAccountID(sfAccount) || account == proposedTx[~sfDelegate];
|
||||
|
||||
if (!terminal && !ownerOrTarget)
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "TransactionProposalCancel: proposal is live and "
|
||||
"canceller is neither its Owner nor its target.";
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
TransactionProposalCancel::doApply()
|
||||
{
|
||||
uint256 const proposalID = ctx_.tx[sfProposalID];
|
||||
|
||||
// Never read the zero keylet; see the matching guard in preclaim.
|
||||
if (proposalID == beast::kZero)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const sleProposal = view().peek(keylet::txProposal(proposalID));
|
||||
if (!sleProposal)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
return proposal::deleteProposal(view(), sleProposal, ctx_.registry.get().getJournal("View"));
|
||||
}
|
||||
|
||||
void
|
||||
TransactionProposalCancel::visitInvariantEntry(bool, SLE::ConstRef, SLE::ConstRef)
|
||||
{
|
||||
// No transaction-specific invariants yet (future work).
|
||||
}
|
||||
|
||||
bool
|
||||
TransactionProposalCancel::finalizeInvariants(
|
||||
STTx const&,
|
||||
TER,
|
||||
XRPAmount,
|
||||
ReadView const&,
|
||||
beast::Journal const&)
|
||||
{
|
||||
// No transaction-specific invariants yet (future work).
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -84,7 +84,7 @@ TransactionProposalCreate::preflight(PreflightContext const& ctx)
|
||||
// no batch inner transactions — and, if it is a Batch, none of its own
|
||||
// inner transactions may be a nested proposal or a pseudo-transaction
|
||||
// either.
|
||||
if (!proposal::isValidProposal(proposedTx))
|
||||
if (!proposal::isValidProposalTxnType(proposedTx))
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "TransactionProposalCreate: proposed txn is not "
|
||||
"independently submittable.";
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -19,11 +19,13 @@
|
||||
#include <test/jtx/token.h>
|
||||
#include <test/jtx/trust.h>
|
||||
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/basics/strHex.h>
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/ledger/OpenView.h>
|
||||
#include <xrpl/ledger/helpers/ProposalHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
@@ -36,6 +38,7 @@
|
||||
#include <xrpl/protocol/SeqProxy.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
#include <chrono> // IWYU pragma: keep
|
||||
@@ -44,6 +47,7 @@
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl::test {
|
||||
@@ -163,7 +167,7 @@ struct TransactionProposalCreate_test : public beast::unit_test::Suite
|
||||
// Proposals do not nest. This payload also isn't a valid instance of
|
||||
// TransactionProposalCreate's own template (it lacks Expiration and
|
||||
// ProposedTransaction), so it fails STTx construction before ever
|
||||
// reaching our own isProposalTx check.
|
||||
// reaching our own nesting check.
|
||||
{
|
||||
json::Value tx = payload();
|
||||
tx[jss::TransactionType] = "TransactionProposalCreate";
|
||||
@@ -189,6 +193,49 @@ struct TransactionProposalCreate_test : public beast::unit_test::Suite
|
||||
reject(tx, temINVALID);
|
||||
}
|
||||
|
||||
// A Batch inner lacking TransactionType cannot be constructed as an
|
||||
// STTx, so Create rejects the payload as temMALFORMED before the
|
||||
// nesting walk runs at all. That inner would otherwise make the walk
|
||||
// report temINVALID, so this pins the ordering.
|
||||
{
|
||||
json::Value missingType;
|
||||
missingType[jss::Account] = target.human();
|
||||
missingType[jss::Flags] = tfInnerBatchTxn;
|
||||
missingType[jss::Sequence] = env.seq(target);
|
||||
missingType[jss::Fee] = "10";
|
||||
missingType[jss::SigningPubKey] = "";
|
||||
|
||||
reject(
|
||||
proposal::unsignedBatch(
|
||||
env,
|
||||
target,
|
||||
targetTicketSeq,
|
||||
tfAllOrNothing,
|
||||
{
|
||||
proposal::innerTx(pay(target, bob, XRP(1)), env.seq(target) + 1),
|
||||
missingType,
|
||||
}),
|
||||
temMALFORMED);
|
||||
}
|
||||
|
||||
// The walk rejects such an inner in its own right. Nothing submitted
|
||||
// through Create can reach that guard — the case above shows STTx
|
||||
// construction throwing first — so it is checked directly here, which
|
||||
// also pins it for a caller that does not build an STTx beforehand.
|
||||
{
|
||||
STObject inner(sfRawTransaction);
|
||||
inner.setAccountID(sfAccount, target.id());
|
||||
|
||||
STArray rawTxns(sfRawTransactions);
|
||||
rawTxns.push_back(std::move(inner));
|
||||
|
||||
STObject batch(sfGeneric);
|
||||
batch.setFieldU16(sfTransactionType, ttBATCH);
|
||||
batch.setFieldArray(sfRawTransactions, rawTxns);
|
||||
|
||||
BEAST_EXPECT(!xrpl::proposal::isValidProposalTxnType(batch));
|
||||
}
|
||||
|
||||
// The proposed transaction must be ticket-based: a missing
|
||||
// TicketSequence, or a live Sequence alongside it, is rejected.
|
||||
{
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -47,6 +47,16 @@ create(Account const& proposer, json::Value const& proposedTx, std::uint32_t exp
|
||||
return jv;
|
||||
}
|
||||
|
||||
json::Value
|
||||
cancel(Account const& account, uint256 const& proposalID)
|
||||
{
|
||||
json::Value jv;
|
||||
jv[jss::TransactionType] = jss::TransactionProposalCancel;
|
||||
jv[jss::Account] = account.human();
|
||||
jv[sfProposalID.jsonName] = to_string(proposalID);
|
||||
return jv;
|
||||
}
|
||||
|
||||
json::Value
|
||||
unsignedPayload(Env const& env, json::Value tx, std::uint32_t ticketSeq)
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/JTx.h>
|
||||
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/basics/chrono.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/ledger/helpers/ProposalHelpers.h>
|
||||
@@ -40,6 +41,16 @@ using xrpl::proposal::kProposalOwnerCount;
|
||||
json::Value
|
||||
create(Account const& proposer, json::Value const& proposedTx, std::uint32_t expiration);
|
||||
|
||||
/**
|
||||
* @brief Build a TransactionProposalCancel naming the proposal to delete.
|
||||
*
|
||||
* @param account The account requesting the cancellation.
|
||||
* @param proposalID The ID of the TransactionProposal to cancel.
|
||||
* @return The TransactionProposalCancel JSON object.
|
||||
*/
|
||||
json::Value
|
||||
cancel(Account const& account, uint256 const& proposalID);
|
||||
|
||||
/**
|
||||
* @brief Conditions that check what a proposal transaction did to the ledger.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
// Auto-generated unit tests for transaction TransactionProposalCancel
|
||||
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <protocol_autogen/TestHelpers.h>
|
||||
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
#include <xrpl/protocol/Seed.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol_autogen/transactions/TransactionProposalCancel.h>
|
||||
#include <xrpl/protocol_autogen/transactions/AccountSet.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace xrpl::transactions {
|
||||
|
||||
// 1 & 4) Set fields via builder setters, build, then read them back via
|
||||
// wrapper getters. After build(), validate() should succeed.
|
||||
TEST(TransactionsTransactionProposalCancelTests, BuilderSettersRoundTrip)
|
||||
{
|
||||
// Generate a deterministic keypair for signing
|
||||
auto const [publicKey, secretKey] =
|
||||
generateKeyPair(KeyType::Secp256k1, generateSeed("testTransactionProposalCancel"));
|
||||
|
||||
// Common transaction fields
|
||||
auto const accountValue = calcAccountID(publicKey);
|
||||
std::uint32_t const sequenceValue = 1;
|
||||
auto const feeValue = canonical_AMOUNT();
|
||||
|
||||
// Transaction-specific field values
|
||||
auto const proposalIDValue = canonical_UINT256();
|
||||
|
||||
TransactionProposalCancelBuilder builder{
|
||||
accountValue,
|
||||
proposalIDValue,
|
||||
sequenceValue,
|
||||
feeValue
|
||||
};
|
||||
|
||||
// Set optional fields
|
||||
|
||||
auto tx = builder.build(publicKey, secretKey);
|
||||
|
||||
std::string reason;
|
||||
EXPECT_TRUE(tx.validate(reason)) << reason;
|
||||
|
||||
// Verify signing was applied
|
||||
EXPECT_FALSE(tx.getSigningPubKey().empty());
|
||||
EXPECT_TRUE(tx.hasTxnSignature());
|
||||
|
||||
// Verify common fields
|
||||
EXPECT_EQ(tx.getAccount(), accountValue);
|
||||
EXPECT_EQ(tx.getSequence(), sequenceValue);
|
||||
EXPECT_EQ(tx.getFee(), feeValue);
|
||||
|
||||
// Verify required fields
|
||||
{
|
||||
auto const& expected = proposalIDValue;
|
||||
auto const actual = tx.getProposalID();
|
||||
expectEqualField(expected, actual, "sfProposalID");
|
||||
}
|
||||
|
||||
// Verify optional fields
|
||||
}
|
||||
|
||||
// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper,
|
||||
// and verify all fields match.
|
||||
TEST(TransactionsTransactionProposalCancelTests, BuilderFromStTxRoundTrip)
|
||||
{
|
||||
// Generate a deterministic keypair for signing
|
||||
auto const [publicKey, secretKey] =
|
||||
generateKeyPair(KeyType::Secp256k1, generateSeed("testTransactionProposalCancelFromTx"));
|
||||
|
||||
// Common transaction fields
|
||||
auto const accountValue = calcAccountID(publicKey);
|
||||
std::uint32_t const sequenceValue = 2;
|
||||
auto const feeValue = canonical_AMOUNT();
|
||||
|
||||
// Transaction-specific field values
|
||||
auto const proposalIDValue = canonical_UINT256();
|
||||
|
||||
// Build an initial transaction
|
||||
TransactionProposalCancelBuilder initialBuilder{
|
||||
accountValue,
|
||||
proposalIDValue,
|
||||
sequenceValue,
|
||||
feeValue
|
||||
};
|
||||
|
||||
|
||||
auto initialTx = initialBuilder.build(publicKey, secretKey);
|
||||
|
||||
// Create builder from existing STTx
|
||||
TransactionProposalCancelBuilder builderFromTx{initialTx.getSTTx()};
|
||||
|
||||
auto rebuiltTx = builderFromTx.build(publicKey, secretKey);
|
||||
|
||||
std::string reason;
|
||||
EXPECT_TRUE(rebuiltTx.validate(reason)) << reason;
|
||||
|
||||
// Verify common fields
|
||||
EXPECT_EQ(rebuiltTx.getAccount(), accountValue);
|
||||
EXPECT_EQ(rebuiltTx.getSequence(), sequenceValue);
|
||||
EXPECT_EQ(rebuiltTx.getFee(), feeValue);
|
||||
|
||||
// Verify required fields
|
||||
{
|
||||
auto const& expected = proposalIDValue;
|
||||
auto const actual = rebuiltTx.getProposalID();
|
||||
expectEqualField(expected, actual, "sfProposalID");
|
||||
}
|
||||
|
||||
// Verify optional fields
|
||||
}
|
||||
|
||||
// 3) Verify wrapper throws when constructed from wrong transaction type.
|
||||
TEST(TransactionsTransactionProposalCancelTests, WrapperThrowsOnWrongTxType)
|
||||
{
|
||||
// Build a valid transaction of a different type
|
||||
auto const [pk, sk] =
|
||||
generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongType"));
|
||||
auto const account = calcAccountID(pk);
|
||||
|
||||
AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()};
|
||||
auto wrongTx = wrongBuilder.build(pk, sk);
|
||||
|
||||
EXPECT_THROW(TransactionProposalCancel{wrongTx.getSTTx()}, std::runtime_error);
|
||||
}
|
||||
|
||||
// 4) Verify builder throws when constructed from wrong transaction type.
|
||||
TEST(TransactionsTransactionProposalCancelTests, BuilderThrowsOnWrongTxType)
|
||||
{
|
||||
// Build a valid transaction of a different type
|
||||
auto const [pk, sk] =
|
||||
generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongTypeBuilder"));
|
||||
auto const account = calcAccountID(pk);
|
||||
|
||||
AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()};
|
||||
auto wrongTx = wrongBuilder.build(pk, sk);
|
||||
|
||||
EXPECT_THROW(TransactionProposalCancelBuilder{wrongTx.getSTTx()}, std::runtime_error);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -15,7 +15,7 @@ namespace xrpl::test {
|
||||
|
||||
namespace {
|
||||
|
||||
// A bare STObject carrying only sfTransactionType. isValidProposal accepts an
|
||||
// A bare STObject carrying only sfTransactionType. isValidProposalTxnType accepts an
|
||||
// STObject (not an STTx) precisely so that this file's defense-in-depth
|
||||
// checks can be tested without the surrounding STTx format validation.
|
||||
inline STObject
|
||||
@@ -45,7 +45,7 @@ batchWrapping(STObject inner)
|
||||
// The happy path — an ordinary Payment is independently submittable.
|
||||
TEST(ProposalHelpers, plain_payment_is_valid)
|
||||
{
|
||||
EXPECT_TRUE(proposal::isValidProposal(txOfType(ttPAYMENT)));
|
||||
EXPECT_TRUE(proposal::isValidProposalTxnType(txOfType(ttPAYMENT)));
|
||||
}
|
||||
|
||||
// A nested TransactionProposalCreate. In practice STTx construction rejects
|
||||
@@ -54,16 +54,16 @@ TEST(ProposalHelpers, plain_payment_is_valid)
|
||||
// drift apart.
|
||||
TEST(ProposalHelpers, nested_proposal_is_rejected)
|
||||
{
|
||||
EXPECT_FALSE(proposal::isValidProposal(txOfType(ttTRANSACTION_PROPOSAL_CREATE)));
|
||||
EXPECT_FALSE(proposal::isValidProposalTxnType(txOfType(ttTRANSACTION_PROPOSAL_CREATE)));
|
||||
}
|
||||
|
||||
// Any pseudo-transaction — see STTx::isPseudoTx. Also normally caught earlier
|
||||
// by STTx construction / preflight0.
|
||||
TEST(ProposalHelpers, pseudo_tx_is_rejected)
|
||||
{
|
||||
EXPECT_FALSE(proposal::isValidProposal(txOfType(ttAMENDMENT)));
|
||||
EXPECT_FALSE(proposal::isValidProposal(txOfType(ttFEE)));
|
||||
EXPECT_FALSE(proposal::isValidProposal(txOfType(ttUNL_MODIFY)));
|
||||
EXPECT_FALSE(proposal::isValidProposalTxnType(txOfType(ttAMENDMENT)));
|
||||
EXPECT_FALSE(proposal::isValidProposalTxnType(txOfType(ttFEE)));
|
||||
EXPECT_FALSE(proposal::isValidProposalTxnType(txOfType(ttUNL_MODIFY)));
|
||||
}
|
||||
|
||||
// tfInnerBatchTxn marks a transaction as an inner leg of an enclosing Batch,
|
||||
@@ -74,7 +74,7 @@ TEST(ProposalHelpers, inner_batch_flag_is_rejected)
|
||||
{
|
||||
STObject tx = txOfType(ttPAYMENT);
|
||||
tx.setFieldU32(sfFlags, tfInnerBatchTxn);
|
||||
EXPECT_FALSE(proposal::isValidProposal(tx));
|
||||
EXPECT_FALSE(proposal::isValidProposalTxnType(tx));
|
||||
}
|
||||
|
||||
// A Flags value that is present but does not include tfInnerBatchTxn must
|
||||
@@ -83,26 +83,27 @@ TEST(ProposalHelpers, other_flags_are_accepted)
|
||||
{
|
||||
STObject tx = txOfType(ttPAYMENT);
|
||||
tx.setFieldU32(sfFlags, tfFullyCanonicalSig);
|
||||
EXPECT_TRUE(proposal::isValidProposal(tx));
|
||||
EXPECT_TRUE(proposal::isValidProposalTxnType(tx));
|
||||
}
|
||||
|
||||
// A Batch wrapping a plain inner is fine — the loop is only there to catch
|
||||
// specifically forbidden inner types.
|
||||
TEST(ProposalHelpers, batch_with_plain_inner_is_valid)
|
||||
{
|
||||
EXPECT_TRUE(proposal::isValidProposal(batchWrapping(txOfType(ttPAYMENT))));
|
||||
EXPECT_TRUE(proposal::isValidProposalTxnType(batchWrapping(txOfType(ttPAYMENT))));
|
||||
}
|
||||
|
||||
// A Batch whose inner is itself a proposal must be rejected.
|
||||
TEST(ProposalHelpers, batch_with_nested_proposal_inner_is_rejected)
|
||||
{
|
||||
EXPECT_FALSE(proposal::isValidProposal(batchWrapping(txOfType(ttTRANSACTION_PROPOSAL_CREATE))));
|
||||
EXPECT_FALSE(
|
||||
proposal::isValidProposalTxnType(batchWrapping(txOfType(ttTRANSACTION_PROPOSAL_CREATE))));
|
||||
}
|
||||
|
||||
// A Batch whose inner is a pseudo-transaction must be rejected.
|
||||
TEST(ProposalHelpers, batch_with_pseudo_inner_is_rejected)
|
||||
{
|
||||
EXPECT_FALSE(proposal::isValidProposal(batchWrapping(txOfType(ttAMENDMENT))));
|
||||
EXPECT_FALSE(proposal::isValidProposalTxnType(batchWrapping(txOfType(ttAMENDMENT))));
|
||||
}
|
||||
|
||||
// A Batch with no sfRawTransactions field skips the inner-loop entirely.
|
||||
@@ -110,7 +111,7 @@ TEST(ProposalHelpers, batch_with_pseudo_inner_is_rejected)
|
||||
// helper (the field is optional at the STObject level) and should hold.
|
||||
TEST(ProposalHelpers, batch_without_raw_transactions_is_valid)
|
||||
{
|
||||
EXPECT_TRUE(proposal::isValidProposal(txOfType(ttBATCH)));
|
||||
EXPECT_TRUE(proposal::isValidProposalTxnType(txOfType(ttBATCH)));
|
||||
}
|
||||
|
||||
} // namespace xrpl::test
|
||||
Reference in new issue
Block a user