Skip to content

Xls 68 sponsored fees reserves updated#720

Open
cybele-ripple wants to merge 86 commits into
mainfrom
XLS-68-sponsored-fees-reservesupdated
Open

Xls 68 sponsored fees reserves updated#720
cybele-ripple wants to merge 86 commits into
mainfrom
XLS-68-sponsored-fees-reservesupdated

Conversation

@cybele-ripple

Copy link
Copy Markdown
Collaborator

This PR supports the new amendment for sponsored fees and reserves for the java library.

Changes here use XRPLF/rippled#5887

Per XLS-0068, the Sponsorship ledger object carries a SponseeNode
field (UInt64, nth=32). SponsorshipObject.java already serializes it
via @JsonProperty("SponseeNode"), but the field was absent from
definitions.json, so any binary-codec round-trip of a Sponsorship
object emitted by rippled would fail to decode.

Matches rippled source: sfields.macro line 155
  TYPED_SFIELD(sfSponseeNode, UINT64, 32)
Base automatically changed from rp/lending-protocol to main May 20, 2026 18:21
Merges main (Dynamic MPT XLS-94D, XLS-65 Vault, XLS-66 Lending Protocol,
Issue polymorphism refactor, Q2 dep upgrades, bug fixes) while preserving
XLS-68 sponsored fees changes (sponsorSign/sponsorMultiSign, SponsorshipObject,
SponsorshipSet/Transfer, SponsorSignature, SponsorshipValidations).

Resolved conflicts:
- Vault/Loan/LoanBroker objects: took main's AssetAmount→Amount refactor
- Transaction.java/TransactionType/LedgerObject: merged both sides' additions
- AbstractTransactionSigner: removed duplicate counterpartySign methods,
  fixed signingHelper→signatureHelper, moved overloads adjacent for checkstyle
- MetaMpTokenIssuanceObject: removed duplicate domainId() method
- Deleted orphaned AssetAmountDeserializer/Serializer and AssetAmountTest
- RippledContainer: kept legleux/xrpld:sponsor image for CI
…y check

Replaced explicit (Consumer<CreateContainerCmd>) cast with an inferred
lambda so that the docker-java-api transitive dependency is no longer
directly referenced in source, fixing the dependency:analyze-only failure.
…devnet/testnet

- Add docker-java-api as explicit test dependency in integration-tests pom.xml
  to satisfy dependency:analyze-only (RippledContainer directly uses CreateContainerCmd
  bytecode regardless of the source-level cast)
- Annotate SponsorshipIT with @DisabledIf to skip on devnet/testnet/clio environments
  where the featureSponsorship amendment is not yet enabled, matching the pattern
  used by BatchTransactionIT, LendingProtocolIT, SingleAssetVaultIT, and XChainIT
@JsonProperty("SponsorSignature")
Optional<SponsorSignature> sponsorSignature();

// TODO: Add Granular Permission fields (SponsorFee, SponsorReserve) for sponsorship once #689 is merged.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO nees to be addressed.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed this TODO since the merged sponsorfee/sponsorreserve no longer uses granular permissions.

Objects.requireNonNull(transaction);

// Validate sponsorship fields per XLS-0068
SponsorshipValidations.validateSponsorFields(transaction);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't inject statically like this. If we have a dependency like SponsorshipValidations then we need to inject via the constructor. This will allow proper mocking for testing.

That said, I'm not convinced that this check should occur here in SignatureUtils. This function is merely serializing bytes. The correctness or non-correctness checks seem like they should be in the transaction itself, or possibly in the signing function itself (as opposed to here).

If we want to consider adding an @Check default function in Transaction.java that might be a good candidate. Transaction.java is not itself an immutable, but we do have some prior art here where we annotation transactionType() function with @Value.Default. The immutables codegen picks this up and applies it to all Transactions.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved this into a @Value.Check in Transaction.java. Also removed the SponsorshipValidations calls from SignatureUtils.java

* @return An {@link UnsignedByteArray}.
*/
@Beta
public UnsignedByteArray toSponsorSignableBytes(final Transaction transaction) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method doesn't seem any different from toSignableBytes(final Transaction transaction). Is there any reason we have it like this (i.e., with a different name)?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.

Objects.requireNonNull(privateKeyable);
Objects.requireNonNull(transaction);

final UnsignedByteArray signableTransactionBytes = this.signatureUtils.toSponsorSignableBytes(transaction);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might be missing something, but if privateKeyable is the Sponsor's private key, then couldn't this function simply call this.signatureUtils.toSignableBytes(transaction) instead?

If that answer is "yes", then do we need sponsorSign at all?

I see that we need a special function for sponsorMultiSign, so even if this is a redudant function, I kind of like the API part of it. In that case, could this just be implemented like this?

@Override
  public <T extends Transaction> Signature sponsorSign(final P privateKeyable, final T transaction) {
    Objects.requireNonNull(privateKeyable);
    Objects.requireNonNull(transaction);

    return this.sign(privateKeyable, transaction).signature();
  }

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Went with your implementation here.

}

@Override
public <T extends Transaction> Signature sponsorSign(final P privateKeyable, final T transaction) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this return a SingleSignedTransaction<T> instead of a `Signature?

I'm inclined to say "no" because sign that returns a SingleSignedTransaction<T> appears to be an anomaly rather than a pattern, but this is still probably worth discussing.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left it as if for now. Looks like only 1 other function returns SingleSignedTransaction<T> while all other functions (like multiSign, counterpartySign, sponsorMultiSIgn) return Signature

* Its API is subject to change.</p>
*/
@Beta
protected static final PaymentFlags SPONSOR_CREATED_ACCOUNT = new PaymentFlags(0x00080000L);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see this change reflected in PaymentFlagsTests.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added coverage for tfSponsorCreatedAccount in PaymentFlagsTests.java

*/
@Beta
@JsonProperty("Sponsor")
Optional<Address> sponsor();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's be sure to update unit tests for this. In particular, we'll want to make sure serialization works with and without this field being present.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added testJsonWithSponsor to PayChannelObjectJsonTests.java

*/
@Beta
@JsonProperty("Sponsor")
Optional<Address> sponsor();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's be sure to update unit tests for this. In particular, we'll want to make sure serialization works with and without this field being present.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sponsor field has since been removed from OfferObject

*/
@Beta
@JsonProperty("Sponsor")
Optional<Address> sponsor();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's be sure to update unit tests for this (e.g., EscrowObjectJsonTests). In particular, we'll want to make sure serialization works with and without this field being present.

We should do this for any ledger object that gets a sponsor.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added testJsonWithSponsor in EscrowObjectJsonTests.java

*/
@Beta
@JsonProperty("LowSponsor")
Optional<Address> lowSponsor();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's be sure to update unit tests for this. In particular, we'll want to make sure serialization works with and without this field being present.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added testJsonWithSponsors in RippleStateObjectJsonTests.java

@Value.Immutable
@JsonSerialize(as = ImmutableSponsorSignature.class)
@JsonDeserialize(as = ImmutableSponsorSignature.class)
public interface SponsorSignature {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this class needs to move to package org.xrpl.xrpl4j.crypto.signing;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now moved to org.xrpl.xrpl4j.crypto.signing and moved the associated test to xrpl4j-core/src/test/java/org/xrpl/xrpl4j/crypto/signing

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we move this to package org.xrpl.xrpl4j.model.transactions similar to BatchSignerWrapper and CounterpartySignature?

rippled's Sponsor implementation moved on significantly since this PR
was written against #5887. Realigns the Java model with the current
xrplf/sponsor branch:

- SponsorshipSet/SponsorshipTransfer tx type codes: 86/85 -> 91/90
- ReserveCount renamed to RemainingOwnerCount and renumbered (72->73);
  SponsorFlags nth 73->74; ObjectID nth 39->41
- SponsorshipTransfer gains a Sponsee field for ending a third party's
  sponsorship, validated to only be set with tfSponsorshipEnd
- SponsorshipSetFlags gains the missing Clear variants
  (tfSponsorshipClearRequireSignForFee/Reserve)
- Sponsor field removed from OfferObject/NfTokenOfferObject/
  TicketObject (excluded from rippled's current scope) and added to
  MpTokenObject/MpTokenIssuanceObject/SignerListObject (now in scope)
- SponsorshipTransfer requires a SponsorSignature for account-level
  tfSponsorshipReassign, matching rippled's fix for the
  forced-transfer-without-consent vulnerability
The short {@link Beta} reference doesn't resolve in the Immutables-generated
classes, which don't import com.google.common.annotations.Beta, breaking the
javadoc:jar build (attach-javadocs).
private XrplAdminClient xrplAdminClient;
private boolean started;

private static final String RIPPLED_DOCKER_IMAGE = "legleux/xrpld:sponsor";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Severity: MEDIUM

The Docker image was changed from the official rippleci/rippled:develop to a personal Docker Hub account (legleux/xrpld:sponsor). Combined with PullPolicy.alwaysPull(), this exposes CI environments to supply-chain compromise if the personal account is hijacked or the image is tampered with.
Helpful? Add 👍 / 👎

💡 Fix Suggestion

Suggestion: Pin the Docker image by its content digest (SHA256 hash) instead of using a mutable tag. This prevents supply-chain attacks where the image content at the sponsor tag could be changed after initial verification. Replace the tag-based reference with a digest-based one:

  1. Determine the current image digest: docker inspect --format='{{index .RepoDigests 0}}' legleux/xrpld:sponsor
  2. Use the digest in the image reference, e.g.: "legleux/xrpld@sha256:<digest>"
  3. Add a comment explaining this is a temporary image pending merge of rippled PR #5887 into the official rippleci/rippled image, and should be reverted once available.

Additionally, consider switching back to the official rippleci/rippled image once the sponsored fees feature is included in the official build.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docker image has been changed to rippleci/rippled:develop

The CI-pinned rippled test image (legleux/xrpld:sponsor) is a snapshot
that predates rippled's transaction-type renumbering on the
xrplf/sponsor branch. It still expects SponsorshipSet=86 and
SponsorshipTransfer=85, and rejects the current upstream values
(91/90) with "Invalid transaction type". Reverting just the type
codes restores compatibility with what CI can actually exercise today;
the rest of the rebase (field renames, new flags, ledger-object scope,
consent check) is unaffected since none of it is gated by tx type.
- Move SponsorSignature to crypto.signing package (sappenin)
- Move sponsorship field validation from SignatureUtils into a
  Transaction @Value.Check, and remove the now-redundant
  toSponsorSignableBytes/sponsorSign special-casing (sappenin)
- Add missing @OverRide annotations on SponsorshipSetFlags (code quality bot)
- Add missing unit test coverage for PaymentFlags.tfSponsorCreatedAccount
  and the new sponsor fields on PayChannelObject/EscrowObject/
  RippleStateObject (sappenin)
- Fix SponsorshipSet.flags() to return SponsorshipSetFlags instead of
  the generic TransactionFlags, and add field-level temMALFORMED-style
  validation to SponsorshipSet and SponsorshipTransfer matching rippled's
  merged preflight logic (Patel-Raj11)
- Replace the personal legleux/xrpld:sponsor Docker image in
  RippledContainer with the official rippleci/xrpld:develop image, now
  that rippled#7350 has merged sponsor support into develop (sappenin,
  depthfirst-app)
The direct dependency was only needed for withCreateContainerCmdModifier's
CreateContainerCmd type, which was removed along with the personal
legleux/xrpld:sponsor image in the prior commit.
Pulls in main's Permission Delegation feature (#689), Issue/Vault
refactor, and other independent changes. Resolved conflicts:

- SignatureUtils.java: kept our toSponsorMultiSignableBytes addition
- MpTokenIssuanceObject.java: kept both our sponsor() and main's
  referenceHolding() additions
- Transaction.java: kept both our sponsorship @Value.Check
  (checkSponsorshipFields) and main's delegate field/check
  (validateDelegateNotSameAsAccount)
- definitions.json: resolved an actual nth collision between our
  ObjectID field and main's new ReferenceHolding field (both wrongly
  claimed nth 39 for Hash256). Verified against rippled's authoritative
  sfields.macro and corrected ObjectID to nth 41 (its real assignment).
- BatchTest.java: the anonymous Transaction impl now overrides both
  sponsor()/sponsorFlags()/sponsorSignature() and delegate()
- PaymentJsonTests.java: split what was actually two independent new
  test methods (testPaymentWithSponsorCreatedAccountFlag and
  testJsonWithDelegate) that the diff had conflated
Cross-checked every Sponsor-related SField against rippled's
authoritative sfields.macro (XRPLF/rippled develop). Found and fixed:

- ObjectID (SponsorshipTransfer): was nth 39, collided with main's new
  ReferenceHolding field during the recent merge; real value is 41
- SponsoredOwnerCount: 69 -> 70
- SponsoringOwnerCount: 70 -> 71
- SponsoringAccountCount: 71 -> 72
- ReserveCount: 72 -> 73, and renamed to RemainingOwnerCount (its real
  wire name) on both the SponsorshipSet transaction and the
  Sponsorship ledger object
- SponsorFlags: 73 -> 74

These five UInt32 fields were each off by one because rippled has an
unrelated sfConfidentialBalanceVersion field at nth 69 (from a
separate, unimplemented-in-xrpl4j feature) that our numbering didn't
account for. Without this fix, any binary-encoded transaction using
these fields would carry the wrong field code and be rejected or
misinterpreted by a real rippled node.
Rippled removed its SponsorFee/SponsorReserve GranularPermission
implementation before merging Sponsor support into develop, so
sponsorship no longer uses granular permissions.
The generated ImmutableSponsorSignature.java doesn't inherit
SponsorSignature's imports, so the unqualified {@link Signer}
reference (valid in the source file via its own import) failed to
resolve in the generated class, breaking the javadoc build in CI.
Fully-qualifying the reference fixes it regardless of imports.
The rippleci/xrpld:develop image renamed the Batch amendment to
BatchV1_1, so rippled aborted on startup with 'Unknown feature: Batch',
causing every integration test to fail with ExceptionInInitializerError
in AbstractIT. Update xrpld.cfg to the current amendment name.
The develop image assigns SponsorshipSet=91 and SponsorshipTransfer=90,
but xrpl4j's definitions.json used 86/85 (which now map to
ConfidentialMPTMergeInbox/ConfidentialMPTConvert in develop). This caused
rippled to reject SponsorshipIT transactions with 'Field MPTokenIssuanceID
is required but missing'. Also fix SponseeNode UInt64 nth (32->33) to match.
rippled develop (commit 530e09d) defines the SponsorshipTransfer tf flags
in the 0x0001xxxx range, but xrpl4j used 0x00000001/2/4. This caused
develop to reject SponsorshipTransfer transactions with temINVALID_FLAG,
failing 4 SponsorshipIT tests. Align the values:
  tfSponsorshipEnd      0x00000001 -> 0x00010000
  tfSponsorshipCreate   0x00000002 -> 0x00020000
  tfSponsorshipReassign 0x00000004 -> 0x00040000
Update the corresponding unit-test and JSON-fixture expectations.
*/
@Beta
@JsonProperty("Sponsor")
Optional<Address> sponsor();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on current rippled source of truth these ledger objects should have the Sponsor field (expect RippleState):

AccountRoot , Check , Escrow , PayChannel , MPToken , MPTokenIssuance , SignerList , DepositPreauth , Credential , Delegate , Sponsorship , and RippleState ( HighSponsor / LowSponsor ).

Can we add the missing ones both in ledger objects and Meta* objects?

Comment thread xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/ledger/SponsorshipObject.java Outdated
Comment thread xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/ledger/SponsorshipObject.java Outdated
@@ -1 +1 @@
package org.xrpl.xrpl4j.model.client.accounts;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commenting here -- LedgerEntryRequestParams is missing sponsorship. We can add that by referencing other ledger entry lookups and have integration test exercise it.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added SponsorshipLedgerEntryParams and sponsorship() to LedgerEntryRequestParams

@Value.Immutable
@JsonSerialize(as = ImmutableSponsorSignature.class)
@JsonDeserialize(as = ImmutableSponsorSignature.class)
public interface SponsorSignature {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we move this to package org.xrpl.xrpl4j.model.transactions similar to BatchSignerWrapper and CounterpartySignature?

* @throws IllegalStateException if validation fails.
*/
@Value.Check
default SponsorSignature checkAndNormalize() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if interaction of Sponsorship and Batch were taken into account, but some of the checks might needs to be relaxed here since when a transaction appears in a Batch transaction, it shouldn't have signature fields to be empty.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Relaxed SponsorSignature.checkAndNormalize() here.

…ons, regenerate definitions.json from server_definitions, add sponsored filter and ledger_entry lookup, relax SponsorSignature for inner-Batch txns
Comment on lines +918 to +926
LedgerEntryResult<SponsorshipObject> sponsorshipEntry = xrplClient.ledgerEntry(
LedgerEntryRequestParams.sponsorship(
SponsorshipLedgerEntryParams.builder()
.owner(sponsorAddress)
.sponsee(sponseeAddress)
.build(),
LedgerSpecifier.VALIDATED
)
);
Comment on lines +934 to +936
LedgerEntryResult<SponsorshipObject> sponsorshipByIndex = xrplClient.ledgerEntry(
LedgerEntryRequestParams.index(expectedSponsorship.index(), SponsorshipObject.class, LedgerSpecifier.VALIDATED)
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants