Skip to content

Commit aa90384

Browse files
committed
Base the EscrowFinish inner fee on the encoded fulfillment blob size rather than the preimage size
1 parent 32ad5de commit aa90384

2 files changed

Lines changed: 28 additions & 21 deletions

File tree

xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/fees/FeeUtils.java

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
import com.google.common.collect.ImmutableSet;
2828
import com.google.common.primitives.UnsignedInteger;
2929
import com.google.common.primitives.UnsignedLong;
30-
import com.ripple.cryptoconditions.PreimageSha256Fulfillment;
3130
import org.immutables.value.Value.Derived;
3231
import org.immutables.value.Value.Immutable;
3332
import org.xrpl.xrpl4j.model.immutables.FluentCompareTo;
@@ -46,7 +45,6 @@
4645
import java.math.MathContext;
4746
import java.math.RoundingMode;
4847
import java.util.Arrays;
49-
import java.util.Base64;
5048
import java.util.Map;
5149
import java.util.Objects;
5250
import java.util.Optional;
@@ -313,18 +311,14 @@ private static long surchargeUnits(final FeeParams feeParams, final Transaction
313311
* @return A number of extra base fees.
314312
*/
315313
private static long fulfillmentUnits(final EscrowFinish escrowFinish) {
316-
return escrowFinish.fulfillment()
317-
.map(fulfillment -> {
318-
Preconditions.checkArgument(
319-
PreimageSha256Fulfillment.class.isAssignableFrom(fulfillment.getClass()),
320-
"Only PreimageSha256Fulfillment is supported, but the fulfillment was a %s.",
321-
fulfillment.getClass().getSimpleName()
322-
);
323-
final long fulfillmentByteSize = Base64.getUrlDecoder()
324-
.decode(((PreimageSha256Fulfillment) fulfillment).getEncodedPreimage()).length;
325-
// See rippled's EscrowFinish::calculateBaseFee: extraFee = base * (32 + (fulfillment size / 16)).
326-
return 32L + (fulfillmentByteSize / 16L);
327-
})
314+
// rippled charges by the on-wire size of the sfFulfillment blob:
315+
// extraFee = base * (32 + fulfillmentBytes / 16) (EscrowFinish::calculateBaseFee)
316+
// fulfillmentRawValue() is that exact blob, hex-encoded — the DER-encoded crypto-condition fulfillment that
317+
// EscrowFinish#normalizeFulfillment always populates. It is NOT the decoded preimage: for a PREIMAGE-SHA-256
318+
// fulfillment the DER wrapper adds a few bytes, which can change the /16 term near a boundary. Measuring the
319+
// blob (not the preimage) is what matches rippled.
320+
return escrowFinish.fulfillmentRawValue()
321+
.map(fulfillmentHex -> 32L + ((fulfillmentHex.length() / 2L) / 16L))
328322
.orElse(0L);
329323
}
330324

xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/fees/FeeUtilsTest.java

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -656,20 +656,29 @@ void allThreeFeeLevelsAreScaled() {
656656

657657
@Test
658658
void escrowFinishWithoutFulfillmentCostsOneBaseFee() {
659-
assertFeeUnits(paramsFor(escrowFinish(false)), 1);
659+
assertFeeUnits(paramsFor(escrowFinish(0)), 1);
660660
}
661661

662662
@Test
663663
void escrowFinishWithFulfillmentAddsTheSizeSurcharge() {
664-
// 1 + (32 + 32/16) = 35
665-
assertFeeUnits(paramsFor(escrowFinish(true)), 35);
664+
// rippled charges by the on-wire fulfillment blob, not the preimage. A 32-byte preimage DER-encodes to a
665+
// 36-byte blob, so the surcharge is 32 + 36/16 = 34: total 1 + 34 = 35.
666+
assertFeeUnits(paramsFor(escrowFinish(32)), 35);
667+
}
668+
669+
@Test
670+
void escrowFinishSurchargeUsesTheEncodedBlobSizeNotThePreimageSize() {
671+
// Boundary case exposing the difference: a 28-byte preimage DER-encodes to a 32-byte blob. Measuring the blob
672+
// (correct, matches rippled) gives 32 + 32/16 = 34 -> total 35. Measuring the preimage (the old, buggy
673+
// behavior) would give 32 + 28/16 = 33 -> total 34, underpaying by one base fee.
674+
assertFeeUnits(paramsFor(escrowFinish(28)), 35);
666675
}
667676

668677
@Test
669678
void escrowFinishSurchargeIsAddedToTheSignatureTerms() {
670679
// (1 + 2) + (32 + 32/16) = 37 — the signer terms EscrowFinish.computeFee omits.
671680
assertFeeUnits(
672-
FeeParams.builder().feeResult(feeResultBuilder().build()).transaction(escrowFinish(true))
681+
FeeParams.builder().feeResult(feeResultBuilder().build()).transaction(escrowFinish(32))
673682
.signersCount(UnsignedInteger.valueOf(2)),
674683
37
675684
);
@@ -1052,16 +1061,20 @@ private AccountDelete accountDelete() {
10521061
.build();
10531062
}
10541063

1055-
private EscrowFinish escrowFinish(final boolean withFulfillment) {
1064+
/**
1065+
* An {@link EscrowFinish} carrying a PREIMAGE-SHA-256 fulfillment built from a {@code preimageBytes}-byte preimage,
1066+
* or no fulfillment when {@code preimageBytes} is 0.
1067+
*/
1068+
private EscrowFinish escrowFinish(final int preimageBytes) {
10561069
ImmutableEscrowFinish.Builder builder = EscrowFinish.builder()
10571070
.account(ALICE)
10581071
.owner(BOB)
10591072
.offerSequence(UnsignedInteger.ONE)
10601073
.fee(XrpCurrencyAmount.ofDrops(0))
10611074
.sequence(UnsignedInteger.ONE)
10621075
.signingPublicKey(PUBLIC_KEY);
1063-
return withFulfillment ?
1064-
builder.fulfillment(PreimageSha256Fulfillment.from(new byte[32])).build() : builder.build();
1076+
return preimageBytes == 0 ?
1077+
builder.build() : builder.fulfillment(PreimageSha256Fulfillment.from(new byte[preimageBytes])).build();
10651078
}
10661079

10671080
private Batch batch(final Address outerAccount, final Transaction... inners) {

0 commit comments

Comments
 (0)