mirror of
https://github.com/XRPLF/rippled.git
synced 2026-09-27 15:28:03 +00:00
test: Address review feedback on Sponsor tests
- Add accountSet() jtx helper and use it (plus existing helpers: sponsor::As, Oracle, MPTSet, xrpLiquid/accountReserve) instead of hand-built JSON where a suitable helper already exists - Add missing coverage: equality at the count() maxU32 boundary, sb2-side assertions in PaymentSandbox owner-count merge tests, sequential fee/reserve sponsorship exhaustion across transactions, reserve-sponsored queue exhaustion in TxQ, and indirect deletion of an expired sponsored credential - Drop the redundant self-compare in OwnerCounts equality test - Move testFeeOnlySponsorshipObjectRouting next to testCosignedTransferConsumesPrefundedBudget (same precedence rule) - Add missing check.h include in TxQ_test.cpp
This commit is contained in:
@@ -45,6 +45,7 @@
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/OpenView.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
@@ -982,6 +983,78 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testSequentialSponsorshipExhaustion()
|
||||
{
|
||||
// A prefunded Sponsorship object's FeeAmount/RemainingOwnerCount is
|
||||
// shared across however many sponsored transactions the sponsee
|
||||
// submits over time, not just a single one. Submit a run of
|
||||
// transactions against a budget that only covers a few of them, and
|
||||
// confirm the early ones succeed while the ones submitted once the
|
||||
// budget is drained fail, without corrupting the object.
|
||||
testcase("Sequential sponsorship exhaustion across transactions");
|
||||
using namespace test::jtx;
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
Account const sponsor("sponsor");
|
||||
|
||||
// Fee exhaustion: enough FeeAmount for 3 of 6 fee-sponsored
|
||||
// transactions; RemainingOwnerCount is generous so reserve is never
|
||||
// the limiting factor.
|
||||
{
|
||||
Env env{*this, testableAmendments()};
|
||||
env.fund(XRP(10000), alice, bob, sponsor);
|
||||
env.close();
|
||||
|
||||
env(sponsor::set(sponsor, 0, 10, XRP(3)), sponsor::SponseeAcc(alice), Ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
for (int i = 1; i <= 6; ++i)
|
||||
{
|
||||
auto const expectedTer = i <= 3 ? tesSUCCESS : TER{terINSUF_FEE_B};
|
||||
env(check::create(alice, bob, XRP(1)),
|
||||
sponsor::As(sponsor, spfSponsorFee),
|
||||
Sig(sfSponsorSignature, sponsor),
|
||||
Fee(XRP(1)),
|
||||
Ter(expectedTer));
|
||||
env.close();
|
||||
}
|
||||
|
||||
auto const sle = env.le(keylet::sponsorship(sponsor, alice));
|
||||
BEAST_EXPECT(sle);
|
||||
BEAST_EXPECT(!sle->isFieldPresent(sfFeeAmount));
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 3);
|
||||
}
|
||||
|
||||
// Reserve exhaustion: enough RemainingOwnerCount for 3 of 6
|
||||
// reserve-sponsored transactions; FeeAmount is generous so fee is
|
||||
// never the limiting factor.
|
||||
{
|
||||
Env env{*this, testableAmendments()};
|
||||
env.fund(XRP(10000), alice, bob, sponsor);
|
||||
env.close();
|
||||
|
||||
env(sponsor::set(sponsor, 0, 3, XRP(100)), sponsor::SponseeAcc(alice), Ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
for (int i = 1; i <= 6; ++i)
|
||||
{
|
||||
auto const expectedTer = i <= 3 ? tesSUCCESS : TER{tecINSUFFICIENT_RESERVE};
|
||||
env(check::create(alice, bob, XRP(1)),
|
||||
sponsor::As(sponsor, spfSponsorReserve),
|
||||
Sig(sfSponsorSignature, sponsor),
|
||||
Fee(XRP(1)),
|
||||
Ter(expectedTer));
|
||||
env.close();
|
||||
}
|
||||
|
||||
auto const sle = env.le(keylet::sponsorship(sponsor, alice));
|
||||
BEAST_EXPECT(sle);
|
||||
BEAST_EXPECT(sle->at(sfRemainingOwnerCount) == 0);
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 3);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testSponsoredFreeTierReserve()
|
||||
{
|
||||
@@ -6030,6 +6103,50 @@ public:
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 2);
|
||||
}
|
||||
|
||||
void
|
||||
testFeeOnlySponsorshipObjectRouting()
|
||||
{
|
||||
// "Object always wins" fee routing: getFeePayer prefers an existing
|
||||
// Sponsorship object over the co-signed sponsor's own balance. A
|
||||
// fee-only-empty object (created by set_reserve, so sfFeeAmount is
|
||||
// absent) therefore makes fee sponsorship fail with terINSUF_FEE_B
|
||||
// even when the sponsor validly co-signs and could easily pay from
|
||||
// its own balance. This is the intended precedence: once a
|
||||
// Sponsorship object exists, all fee sponsorship for that
|
||||
// sponsor/sponsee pair is routed through its pre-funded FeeAmount.
|
||||
testcase("Fee-only Sponsorship object outranks a valid co-sign");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env{*this, testableAmendments()};
|
||||
Account const alice("alice");
|
||||
Account const sponsor("sponsor");
|
||||
env.fund(XRP(10000), alice, sponsor);
|
||||
env.close();
|
||||
|
||||
// Reserve-only sponsorship: no sfFeeAmount on the object.
|
||||
env(sponsor::set_reserve(sponsor, 0, 5), sponsor::SponseeAcc(alice));
|
||||
env.close();
|
||||
|
||||
auto sle = env.le(keylet::sponsorship(sponsor, alice));
|
||||
BEAST_EXPECT(sle && !sle->isFieldPresent(sfFeeAmount));
|
||||
|
||||
auto const aliceBalance = env.balance(alice);
|
||||
auto const sponsorBalance = env.balance(sponsor);
|
||||
|
||||
env(noop(alice),
|
||||
Fee(env.current()->fees().base),
|
||||
sponsor::As(sponsor, spfSponsorFee),
|
||||
Sig(sfSponsorSignature, sponsor),
|
||||
Ter(terINSUF_FEE_B));
|
||||
|
||||
// Nothing was charged anywhere and the object is untouched.
|
||||
BEAST_EXPECT(env.balance(alice) == aliceBalance);
|
||||
BEAST_EXPECT(env.balance(sponsor) == sponsorBalance);
|
||||
sle = env.le(keylet::sponsorship(sponsor, alice));
|
||||
BEAST_EXPECT(sle && !sle->isFieldPresent(sfFeeAmount));
|
||||
BEAST_EXPECT(sle && sle->getFieldU32(sfRemainingOwnerCount) == 5);
|
||||
}
|
||||
|
||||
void
|
||||
testZeroValueUpdateOnAbsentFields()
|
||||
{
|
||||
@@ -6103,56 +6220,14 @@ public:
|
||||
env.fund(XRP(10000), alice, sponsor);
|
||||
env.close();
|
||||
|
||||
auto tx = noop(alice);
|
||||
tx[sfSponsor.jsonName] = sponsor.human();
|
||||
tx[sfSponsorFlags.jsonName] = static_cast<std::uint32_t>(spfSponsorFee);
|
||||
tx[sfSponsorSignature.jsonName][sfSigningPubKey.jsonName] = "";
|
||||
|
||||
env(tx, Fee(XRP(1)), Ter(telENV_RPC_FAILED));
|
||||
}
|
||||
|
||||
void
|
||||
testFeeOnlySponsorshipObjectRouting()
|
||||
{
|
||||
// "Object always wins" fee routing: getFeePayer prefers an existing
|
||||
// Sponsorship object over the co-signed sponsor's own balance. A
|
||||
// fee-only-empty object (created by set_reserve, so sfFeeAmount is
|
||||
// absent) therefore makes fee sponsorship fail with terINSUF_FEE_B
|
||||
// even when the sponsor validly co-signs and could easily pay from
|
||||
// its own balance. This is the intended precedence: once a
|
||||
// Sponsorship object exists, all fee sponsorship for that
|
||||
// sponsor/sponsee pair is routed through its pre-funded FeeAmount.
|
||||
testcase("Fee-only Sponsorship object outranks a valid co-sign");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env{*this, testableAmendments()};
|
||||
Account const alice("alice");
|
||||
Account const sponsor("sponsor");
|
||||
env.fund(XRP(10000), alice, sponsor);
|
||||
env.close();
|
||||
|
||||
// Reserve-only sponsorship: no sfFeeAmount on the object.
|
||||
env(sponsor::set_reserve(sponsor, 0, 5), sponsor::SponseeAcc(alice));
|
||||
env.close();
|
||||
|
||||
auto sle = env.le(keylet::sponsorship(sponsor, alice));
|
||||
BEAST_EXPECT(sle && !sle->isFieldPresent(sfFeeAmount));
|
||||
|
||||
auto const aliceBalance = env.balance(alice);
|
||||
auto const sponsorBalance = env.balance(sponsor);
|
||||
|
||||
env(noop(alice),
|
||||
Fee(env.current()->fees().base),
|
||||
env(
|
||||
noop(alice),
|
||||
sponsor::As(sponsor, spfSponsorFee),
|
||||
Sig(sfSponsorSignature, sponsor),
|
||||
Ter(terINSUF_FEE_B));
|
||||
|
||||
// Nothing was charged anywhere and the object is untouched.
|
||||
BEAST_EXPECT(env.balance(alice) == aliceBalance);
|
||||
BEAST_EXPECT(env.balance(sponsor) == sponsorBalance);
|
||||
sle = env.le(keylet::sponsorship(sponsor, alice));
|
||||
BEAST_EXPECT(sle && !sle->isFieldPresent(sfFeeAmount));
|
||||
BEAST_EXPECT(sle && sle->getFieldU32(sfRemainingOwnerCount) == 5);
|
||||
[](Env&, JTx& jt) {
|
||||
jt.jv[sfSponsorSignature.jsonName][sfSigningPubKey.jsonName] = "";
|
||||
},
|
||||
Fee(XRP(1)),
|
||||
Ter(telENV_RPC_FAILED));
|
||||
}
|
||||
|
||||
void
|
||||
@@ -6273,10 +6348,19 @@ public:
|
||||
|
||||
// Control: for the unsponsored account, xrpLiquid = balance - base
|
||||
// reserve = 0, so OfferCreate's funding check fails.
|
||||
beast::Journal const j{beast::Journal::getNullSink()};
|
||||
BEAST_EXPECT(accountReserve(*env.current(), bob.id(), j) == fees.accountReserve(0, 1));
|
||||
BEAST_EXPECT(xrpLiquid(*env.current(), bob.id(), 0, j) == XRPAmount(0));
|
||||
env(offer(bob, usd(5), XRP(5)), Ter(tecUNFUNDED_OFFER));
|
||||
env.close();
|
||||
BEAST_EXPECT(ownerCount(env, bob) == 0);
|
||||
|
||||
// The sponsored account contributes 0 to its own reserve, so its
|
||||
// reserve is 0 (no owned objects yet), and its full balance is
|
||||
// liquid.
|
||||
BEAST_EXPECT(accountReserve(*env.current(), alice.id(), j) == fees.accountReserve(0, 0));
|
||||
BEAST_EXPECT(xrpLiquid(*env.current(), alice.id(), 0, j) == env.balance(alice));
|
||||
|
||||
// The sponsored account's reserve is just the owner increment, so the
|
||||
// same offer is funded and can even be placed on the book.
|
||||
auto const offerSeq = env.seq(alice);
|
||||
@@ -6436,6 +6520,63 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testExpiredSponsoredCredentialIndirectDeletion()
|
||||
{
|
||||
// CredentialAccept on an already-expired sponsored credential fails
|
||||
// with tecEXPIRED, but still deletes the credential as a side effect
|
||||
// (removeExpired). That indirect deletion must still refund the
|
||||
// sponsor's SponsoringOwnerCount and the issuer's SponsoredOwnerCount,
|
||||
// just like the third-party EscrowCancel and payment-engine
|
||||
// trust-line deletion cases above.
|
||||
testcase("Indirect deletion of an expired sponsored credential");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env{*this, testableAmendments()};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
Account const sponsor("sponsor");
|
||||
env.fund(XRP(10000), alice, bob, sponsor);
|
||||
env.close();
|
||||
|
||||
auto const credType = std::string("test");
|
||||
|
||||
// alice (issuer) creates a credential for bob (subject), with a
|
||||
// short expiration.
|
||||
auto const expiration =
|
||||
env.current()->header().parentCloseTime.time_since_epoch().count() + 20;
|
||||
auto jv = credentials::create(bob, alice, credType);
|
||||
jv[sfExpiration.jsonName] = expiration;
|
||||
env(jv);
|
||||
env.close();
|
||||
|
||||
auto const credKeylet = credentials::keylet(bob, alice, credType);
|
||||
env(sponsor::transfer(alice, tfSponsorshipCreate, credKeylet.key),
|
||||
sponsor::As(sponsor, spfSponsorReserve),
|
||||
Sig(sfSponsorSignature, sponsor));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1);
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1);
|
||||
|
||||
// Advance past expiration.
|
||||
for (; env.current()->header().parentCloseTime.time_since_epoch().count() <=
|
||||
static_cast<std::int64_t>(expiration);
|
||||
env.close())
|
||||
{
|
||||
}
|
||||
|
||||
// Bob's accept fails (expired), but removeExpired deletes the
|
||||
// credential as a side effect.
|
||||
env(credentials::accept(bob, alice, credType), Ter(tecEXPIRED));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(!env.le(credKeylet));
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
||||
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0);
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0);
|
||||
}
|
||||
|
||||
void
|
||||
testReserveSponsorAllowListNoOp()
|
||||
{
|
||||
@@ -6500,10 +6641,7 @@ public:
|
||||
// MPTokenIssuanceSet
|
||||
MPTTester mptt(env, alice, {.fund = false});
|
||||
mptt.create({.flags = tfMPTCanLock});
|
||||
json::Value jvSet;
|
||||
jvSet[sfTransactionType.jsonName] = jss::MPTokenIssuanceSet;
|
||||
jvSet[sfAccount.jsonName] = alice.human();
|
||||
jvSet[sfMPTokenIssuanceID.jsonName] = to_string(mptt.issuanceID());
|
||||
json::Value jvSet = mptt.setJV({.account = alice, .id = mptt.issuanceID()});
|
||||
jvSet[sfFlags.jsonName] = tfMPTLock;
|
||||
env(jvSet,
|
||||
sponsor::As(sponsor, spfSponsorReserve),
|
||||
@@ -6612,6 +6750,7 @@ protected:
|
||||
testSimpleSponsorshipSet();
|
||||
|
||||
testPreFundAndCosign();
|
||||
testSequentialSponsorshipExhaustion();
|
||||
testSponsoredFreeTierReserve();
|
||||
testSponsoredTicketUse();
|
||||
|
||||
@@ -6641,13 +6780,14 @@ protected:
|
||||
|
||||
testAccountSponsorshipTransferPermissions();
|
||||
testCosignedTransferConsumesPrefundedBudget();
|
||||
testFeeOnlySponsorshipObjectRouting();
|
||||
testZeroValueUpdateOnAbsentFields();
|
||||
testEmptySponsorSignatureObject();
|
||||
testFeeOnlySponsorshipObjectRouting();
|
||||
testOuterMultisignedSponsoredTx();
|
||||
testSponsoredAccountBaseReserveExclusion();
|
||||
testThirdPartyEscrowCancelRefundsSponsor();
|
||||
testPaymentEngineTrustLineDeletion();
|
||||
testExpiredSponsoredCredentialIndirectDeletion();
|
||||
testReserveSponsorAllowListNoOp();
|
||||
testPseudoAccountAsSponsor();
|
||||
testCosignedClosedLedgerFeeCap();
|
||||
|
||||
Reference in New Issue
Block a user