Proposal create bug fixes (#8139)

This commit is contained in:
Kassaking7
2026-09-08 15:27:38 -04:00
committed by GitHub
parent 2d80b9f832
commit d51aea5d79
8 changed files with 431 additions and 16 deletions

View File

@@ -6,6 +6,7 @@
#include <test/jtx/delegate.h>
#include <test/jtx/deposit.h>
#include <test/jtx/fee.h>
#include <test/jtx/flags.h>
#include <test/jtx/multisign.h>
#include <test/jtx/noop.h>
#include <test/jtx/offer.h>
@@ -20,14 +21,19 @@
#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/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Keylet.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STArray.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/SeqProxy.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/jss.h>
@@ -36,6 +42,7 @@
#include <cstddef>
#include <cstdint>
#include <functional>
#include <memory>
#include <string>
#include <vector>
@@ -698,6 +705,119 @@ struct TransactionProposalCreate_test : public beast::unit_test::Suite
}
}
// An on-ledger ltSIGNER_LIST that cannot be read as signer entries is
// unexpected ledger state, not a malformed transaction. preclaim must
// surface tefBAD_LEDGER (not temMALFORMED, not tefINTERNAL which is
// reserved for truly unreachable paths, and not the tefEXCEPTION that
// applySteps would wrap an uncaught throw with).
//
// SignerEntries::deserialize returns unexpected(temMALFORMED) when
// sfSignerEntries is missing or an element is not named sfSignerEntry.
// It still throws from STObject accessors when an sfSignerEntry is
// missing required fields (getAccountID → "Field not found: Account").
// Do not close() after the synthetic corruption: a closed ledger would
// drop the overlay and restore a well-formed list.
void
testCorruptSignerList(FeatureBitset features)
{
testcase("unparseable on-ledger SignerList is tefBAD_LEDGER");
using namespace jtx;
using namespace std::chrono_literals;
auto setup = [&](Env& env, Account const& target, Account const& signer) {
env.fund(XRP(10000), target, signer);
env.close();
env(signers(target, 1, {{signer, 1}}));
env.close();
// Ticket first: createTicket closes, which would drop a later
// open-ledger overlay and restore a well-formed SignerList.
return proposal::createTicket(env, target);
};
auto proposeAsSigner =
[&](Env& env, Account const& target, Account const& signer, std::uint32_t ticketSeq) {
env(proposal::create(
signer,
proposal::unsignedPayload(env, pay(target, signer, XRP(1)), ticketSeq),
proposal::expiration(env, 100s)),
Ter(tefBAD_LEDGER),
proposal::verify::create());
BEAST_EXPECT(!proposal::entry(env, target, ticketSeq));
};
{
Env env{*this, features};
Account const target{"targetMissing"};
Account const signer{"signerMissing"};
std::uint32_t const ticketSeq = setup(env, target, signer);
auto const signerListKeylet = keylet::signerList(target.id());
BEAST_EXPECT(env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal) {
auto const sle = view.read(signerListKeylet);
if (!sle)
return false;
auto replacement = std::make_shared<SLE>(*sle);
if (!replacement->delField(sfSignerEntries))
return false;
view.rawReplace(replacement);
return true;
}));
BEAST_EXPECT(env.le(signerListKeylet));
proposeAsSigner(env, target, signer, ticketSeq);
}
{
Env env{*this, features};
Account const target{"targetBadEntry"};
Account const signer{"signerBadEntry"};
std::uint32_t const ticketSeq = setup(env, target, signer);
auto const signerListKeylet = keylet::signerList(target.id());
BEAST_EXPECT(env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal) {
auto const sle = view.read(signerListKeylet);
if (!sle)
return false;
auto replacement = std::make_shared<SLE>(*sle);
STArray badEntries;
badEntries.pushBack(STObject{sfSigner});
replacement->setFieldArray(sfSignerEntries, badEntries);
view.rawReplace(replacement);
return true;
}));
BEAST_EXPECT(env.le(signerListKeylet));
proposeAsSigner(env, target, signer, ticketSeq);
}
{
Env env{*this, features};
Account const target{"targetMissingAccount"};
Account const signer{"signerMissingAccount"};
std::uint32_t const ticketSeq = setup(env, target, signer);
auto const signerListKeylet = keylet::signerList(target.id());
BEAST_EXPECT(env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal) {
auto const sle = view.read(signerListKeylet);
if (!sle)
return false;
auto replacement = std::make_shared<SLE>(*sle);
STArray badEntries;
// Right inner name, but no sfAccount: deserialize calls
// getAccountID and throws (Field not found), which the
// catch maps to tefBAD_LEDGER.
badEntries.pushBack(STObject{sfSignerEntry});
replacement->setFieldArray(sfSignerEntries, badEntries);
view.rawReplace(replacement);
return true;
}));
BEAST_EXPECT(env.le(signerListKeylet));
proposeAsSigner(env, target, signer, ticketSeq);
}
}
// The target account may delegate authority over the proposed
// transaction's own type to another account (Permission Delegation,
// XLS-75); if it does, that delegate — or an account on the delegate's
@@ -781,6 +901,81 @@ struct TransactionProposalCreate_test : public beast::unit_test::Suite
}
}
// A delegate holding only a granular permission (XLS-75) that would
// authorize submitting the proposed transaction may also create a
// proposal for it. A granular grant that fails checkGranularSandbox
// still cannot.
void
testDelegatedGranularProposedTx(FeatureBitset features)
{
testcase("proposer authorized through granular delegate permission");
using namespace jtx;
using namespace std::chrono_literals;
Env env{*this, features};
Account const gw{"gw"}; // issuer / proposed-tx Account
Account const alice{"alice"}; // holder of the trust line being authorized
Account const bob{"bob"}; // delegate with TrustlineAuthorize only
env.fund(XRP(10000), gw, alice, bob);
env(fset(gw, asfRequireAuth));
env.close();
env(trust(alice, gw["USD"](50)));
env.close();
env(delegate::set(gw, bob, {"TrustlineAuthorize"}));
env.close();
auto delegatedTrustSet = [&](std::uint32_t ticketSeq, std::uint32_t flags) {
json::Value tx = trust(gw, gw["USD"](0), alice, flags);
tx[sfDelegate.jsonName] = bob.human();
return proposal::unsignedPayload(env, tx, ticketSeq);
};
// TrustlineAuthorize is sufficient for a tfSetfAuth TrustSet against
// an existing line whose limit is unchanged — the same shape that
// submits successfully under invokeCheckPermission.
{
std::uint32_t const ticketSeq = proposal::createTicket(env, gw);
env(proposal::create(
bob, delegatedTrustSet(ticketSeq, tfSetfAuth), proposal::expiration(env, 100s)),
proposal::verify::create());
env.close();
BEAST_EXPECT(proposal::entry(env, gw, ticketSeq));
}
// tfSetFreeze is not in TrustlineAuthorize's sandbox.
{
std::uint32_t const ticketSeq = proposal::createTicket(env, gw);
env(proposal::create(
bob,
delegatedTrustSet(ticketSeq, tfSetFreeze),
proposal::expiration(env, 100s)),
Ter(tecNO_PERMISSION),
proposal::verify::create());
env.close();
BEAST_EXPECT(!proposal::entry(env, gw, ticketSeq));
}
// sfQualityOut is a valid TrustSet field but not in the granular
// template, so checkGranularSandbox rejects it.
{
std::uint32_t const ticketSeq = proposal::createTicket(env, gw);
json::Value tx = trust(gw, gw["USD"](0), alice, tfSetfAuth);
tx[sfDelegate.jsonName] = bob.human();
tx[sfQualityOut.jsonName] = 100;
env(proposal::create(
bob,
proposal::unsignedPayload(env, tx, ticketSeq),
proposal::expiration(env, 100s)),
Ter(tecNO_PERMISSION),
proposal::verify::create());
env.close();
BEAST_EXPECT(!proposal::entry(env, gw, ticketSeq));
}
}
// The target account must be able to authorize a transaction through a
// SignerList, so a pseudo-account (here an AMM's) cannot be a target even
// though it exists on-ledger (On-Chain Cosigner spec §5.3.2.5).
@@ -1282,6 +1477,75 @@ struct TransactionProposalCreate_test : public beast::unit_test::Suite
BEAST_EXPECT(ownerCount(env, target) == 1 + proposal::kBatchProposalOwnerCount);
}
// A proposed Batch's inner preflight must receive TapProposal, or an
// unsigned account-reserve SponsorshipTransfer is rejected at Create
// (On-Chain Cosigner spec §6.1.1: an inner Sponsor is a collectable
// signature slot). Other inner types that do not key on TapProposal keep
// their existing preflight result.
void
testProposedBatchInnerSponsorship(FeatureBitset features)
{
testcase("proposed batch inner account-reserve SponsorshipTransfer");
using namespace jtx;
using namespace std::chrono_literals;
Env env{*this, features};
Account const target{"target"};
Account const bob{"bob"}; // named as Sponsor; signature collected later
env.fund(XRP(10000), target, bob);
env.close();
auto unsignedInnerSponsorship = [&](Account const& account) {
json::Value tx = sponsor::transfer(account, tfSponsorshipCreate);
tx[sfSponsor.getJsonName()] = bob.human();
tx[sfSponsorFlags.getJsonName()] = spfSponsorReserve;
return tx;
};
// Payment (unaffected by TapProposal) plus an unsigned inner
// account-reserve SponsorshipTransfer. Create succeeds only if
// TapProposal reaches the inner.
{
std::uint32_t const ticketSeq = proposal::createTicket(env, target);
auto const seq = env.seq(target);
json::Value const proposedTx = proposal::unsignedBatch(
env,
target,
ticketSeq,
tfAllOrNothing,
{proposal::innerTx(pay(target, bob, XRP(1)), seq),
proposal::innerTx(unsignedInnerSponsorship(target), seq + 1)});
env(proposal::create(target, proposedTx, proposal::expiration(env, 100s)),
proposal::verify::create());
env.close();
BEAST_EXPECT(proposal::entry(env, target, ticketSeq));
}
// Structural inner failures are unchanged: missing sfSponsor is still
// temMALFORMED in SponsorshipTransfer::preflight, collapsed by Batch
// to temINVALID_INNER_BATCH. TapProposal does not skip that.
{
std::uint32_t const ticketSeq = proposal::createTicket(env, target);
auto const seq = env.seq(target);
json::Value const proposedTx = proposal::unsignedBatch(
env,
target,
ticketSeq,
tfAllOrNothing,
{proposal::innerTx(pay(target, bob, XRP(1)), seq),
proposal::innerTx(sponsor::transfer(target, tfSponsorshipCreate), seq + 1)});
env(proposal::create(target, proposedTx, proposal::expiration(env, 100s)),
Ter(temINVALID_INNER_BATCH),
proposal::verify::create());
env.close();
BEAST_EXPECT(!proposal::entry(env, target, ticketSeq));
}
}
void
run() override
{
@@ -1299,7 +1563,9 @@ struct TransactionProposalCreate_test : public beast::unit_test::Suite
// Preclaim
testPreclaim(all);
testProposerAuthorization(all);
testCorruptSignerList(all);
testDelegatedProposedTx(all);
testDelegatedGranularProposedTx(all);
testPseudoTarget(all);
// Apply
@@ -1312,6 +1578,7 @@ struct TransactionProposalCreate_test : public beast::unit_test::Suite
testFeeSponsored(all);
testBatchReserve(all);
testMultiAccountBatch(all);
testProposedBatchInnerSponsorship(all);
}
};