Skip to content

Commit

Permalink
Fix Escrow Fee calculations
Browse files Browse the repository at this point in the history
  • Loading branch information
sappenin committed Dec 17, 2024
1 parent f61750f commit 08882c5
Show file tree
Hide file tree
Showing 2 changed files with 107 additions and 137 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
import com.ripple.cryptoconditions.CryptoConditionReader;
import com.ripple.cryptoconditions.CryptoConditionWriter;
import com.ripple.cryptoconditions.Fulfillment;
import com.ripple.cryptoconditions.PreimageSha256Fulfillment;
import com.ripple.cryptoconditions.PreimageSha256Fulfillment.AbstractPreimageSha256Fulfillment;
import com.ripple.cryptoconditions.der.DerEncodingException;
import org.immutables.value.Value;
import org.slf4j.Logger;
Expand All @@ -41,6 +43,7 @@
import org.xrpl.xrpl4j.model.transactions.AccountSet.AccountSetFlag;

import java.util.Arrays;
import java.util.Base64;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
Expand Down Expand Up @@ -69,25 +72,35 @@ static ImmutableEscrowFinish.Builder builder() {
* transaction increases if it contains a fulfillment. If the transaction contains a fulfillment, the transaction cost
* is 330 drops of XRP plus another 10 drops for every 16 bytes in size of the preimage.
*
* @param currentLedgerFeeDrops The number of drops that the ledger demands at present.
* @param fulfillment The {@link Fulfillment} that is being presented to the ledger for computation
* purposes.
* @param currentLedgerBaseFeeDrops The number of drops that the ledger demands at present.
* @param fulfillment The {@link Fulfillment} that is being presented to the ledger for computation
* purposes.
*
* @return An {@link XrpCurrencyAmount} representing the computed fee.
*
* @see "https://xrpl.org/escrowfinish.html"
*/
static XrpCurrencyAmount computeFee(final XrpCurrencyAmount currentLedgerFeeDrops, final Fulfillment fulfillment) {
Objects.requireNonNull(currentLedgerFeeDrops);
static XrpCurrencyAmount computeFee(
final XrpCurrencyAmount currentLedgerBaseFeeDrops,
final Fulfillment<?> fulfillment
) {
Objects.requireNonNull(currentLedgerBaseFeeDrops);
Objects.requireNonNull(fulfillment);

UnsignedLong newFee =
currentLedgerFeeDrops.value() // <-- usually 10 drops, per the docs.
// <-- https://github.com/ripple/rippled/blob/develop/src/ripple/app/tx/impl/Escrow.cpp#L362
.plus(UnsignedLong.valueOf(320))
// <-- 10 drops for each additional 16 bytes.
.plus(UnsignedLong.valueOf(10 * (fulfillment.getDerivedCondition().getCost() / 16)));
return XrpCurrencyAmount.of(newFee);
final int fulfillmentByteSize = Base64.getUrlDecoder().decode(
((PreimageSha256Fulfillment) fulfillment).getEncodedPreimage()
).length;
final int baseFee = currentLedgerBaseFeeDrops.value().intValue();

// See https://xrpl.org/docs/references/protocol/transactions/types/escrowfinish#escrowfinish-fields for '
// computing the additional fee for Escrows.
// In particular: `extraFee = view.fees().base * (32 + (fb->size() / 16))`
// See https://github.com/XRPLF/rippled/blob/master/src/xrpld/app/tx/detail/Escrow.cpp#L368
final int extraFeeDrops = baseFee * (32 + (fulfillmentByteSize / 16));
final int totalFeeDrops = baseFee + extraFeeDrops; // <-- Add an extra base fee
return XrpCurrencyAmount.of(
UnsignedLong.valueOf(totalFeeDrops)
);
}

/**
Expand Down Expand Up @@ -144,11 +157,11 @@ default TransactionFlags flags() {
*
* <p>Note that a similar field does not exist on {@link EscrowCreate},
* {@link org.xrpl.xrpl4j.model.ledger.EscrowObject}, or
* {@link org.xrpl.xrpl4j.model.transactions.metadata.MetaEscrowObject} because {@link EscrowCreate}s with
* malformed conditions will never be included in a ledger by the XRPL. Because of this fact, an
* {@link org.xrpl.xrpl4j.model.transactions.metadata.MetaEscrowObject} because {@link EscrowCreate}s with malformed
* conditions will never be included in a ledger by the XRPL. Because of this fact, an
* {@link org.xrpl.xrpl4j.model.ledger.EscrowObject} and
* {@link org.xrpl.xrpl4j.model.transactions.metadata.MetaEscrowObject} will also never contain a malformed
* crypto condition.</p>
* {@link org.xrpl.xrpl4j.model.transactions.metadata.MetaEscrowObject} will also never contain a malformed crypto
* condition.</p>
*
* @return An {@link Optional} {@link String} containing the hex-encoded PREIMAGE-SHA-256 condition.
*/
Expand Down Expand Up @@ -191,8 +204,8 @@ default TransactionFlags flags() {
* <p>If {@link #condition()} is present but {@link #conditionRawValue()} is empty, we set
* {@link #conditionRawValue()} to the underlying value of {@link #condition()}.</p>
* <p>If {@link #condition()} is empty and {@link #conditionRawValue()} is present, we will set
* {@link #condition()} to the {@link Condition} representing the raw condition value, or leave
* {@link #condition()} empty if {@link #conditionRawValue()} is a malformed {@link Condition}.</p>
* {@link #condition()} to the {@link Condition} representing the raw condition value, or leave {@link #condition()}
* empty if {@link #conditionRawValue()} is a malformed {@link Condition}.</p>
*
* @return A normalized {@link EscrowFinish}.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
Expand All @@ -36,6 +36,9 @@
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.xrpl.xrpl4j.model.jackson.ObjectMapperFactory;
import org.xrpl.xrpl4j.model.transactions.ImmutableEscrowFinish.Builder;

Expand Down Expand Up @@ -86,7 +89,8 @@ public void constructWithNoFulfillmentNoCondition() {

////////////////////////////////
// normalizeCondition tests
////////////////////////////////

/// /////////////////////////////

@Test
void normalizeWithNoConditionNoRawValue() {
Expand Down Expand Up @@ -131,7 +135,7 @@ void normalizeWithConditionAndRawValueNonMatching() {
.conditionRawValue("A0258020E3B0C44298FC1C149ABCD4C8996FB92427AE41E4649B934CA495991B7852B855810100")
.build()
).isInstanceOf(IllegalStateException.class)
.hasMessage("condition and conditionRawValue should be equivalent if both are present.");
.hasMessage("condition and conditionRawValue should be equivalent if both are present.");
}

@Test
Expand Down Expand Up @@ -178,10 +182,9 @@ void normalizeWithNoConditionAndRawValueForMalformedCondition() {
}

/**
* This tests the case where conditionRawValue is present and is parseable to a Condition but when the
* parsed Condition is written to a byte array, the value differs from the conditionRawValue bytes. This
* can occur if the condition raw value contains a valid condition in the first 32 bytes, but also includes
* extra bytes afterward.
* This tests the case where conditionRawValue is present and is parseable to a Condition but when the parsed
* Condition is written to a byte array, the value differs from the conditionRawValue bytes. This can occur if the
* condition raw value contains a valid condition in the first 32 bytes, but also includes extra bytes afterward.
*/
@Test
void normalizeConditionWithRawValueThatIsParseableButNotValid() {
Expand Down Expand Up @@ -217,7 +220,8 @@ void normalizeWithNoConditionAndRawValueForBadHexLengthCondition() {

////////////////////////////////
// normalizeFulfillment tests
////////////////////////////////

/// /////////////////////////////

@Test
void normalizeWithNoFulfillmentNoRawValue() {
Expand Down Expand Up @@ -309,10 +313,10 @@ void normalizeWithNoFulfillmentAndRawValueForMalformedFulfillment() {
}

/**
* This tests the case where fulfillmentRawValue is present and is parseable to a Fulfillment<?> but when the
* parsed Fulfillment is written to a byte array, the value differs from the fulfillmentRawValue bytes. This
* can occur if the fulfillment raw value contains a valid fulfillment in the first 32 bytes, but also includes
* extra bytes afterward, such as in transaction 138543329687544CDAFCD3AB0DCBFE9C4F8E710397747BA7155F19426F493C8D.
* This tests the case where fulfillmentRawValue is present and is parseable to a Fulfillment<?> but when the parsed
* Fulfillment is written to a byte array, the value differs from the fulfillmentRawValue bytes. This can occur if the
* fulfillment raw value contains a valid fulfillment in the first 32 bytes, but also includes extra bytes afterward,
* such as in transaction 138543329687544CDAFCD3AB0DCBFE9C4F8E710397747BA7155F19426F493C8D.
*/
@Test
void normalizeFulfillmentWithRawValueThatIsParseableButNotValid() {
Expand Down Expand Up @@ -346,122 +350,75 @@ void normalizeWithNoFulfillmentAndRawValueForBadHexLengthFulfillment() {
assertThat(actual.fulfillmentRawValue()).isNotEmpty().get().isEqualTo("123");
}

@Test
public void testNormalizeWithVariousFulfillmentSizes() {
@ParameterizedTest
@CsvSource( {
// Simulate 10 drop base fee
"0,10,330", // Standard 10 drop fee, plus 320 drops
"1,10,330", // Standard 10 drop fee, plus 320 drops
"2,10,330", // Standard 10 drop fee, plus 320 drops
"15,10,330", // Standard 10 drop fee, plus 320 drops
"16,10,340", // Standard 10 drop fee, plus 320 drops + 10 drops for 1 chunk of 16 bytes
"17,10,340", // Standard 10 drop fee, plus 320 drops + 10 drops for 1 chunk of 16 bytes
"31,10,340", // Standard 10 drop fee, plus 320 drops + 10 drops for 1 chunk of 16 bytes
"32,10,350", // Standard 10 drop fee, plus 320 drops + 20 drops for 2 chunks of 16 bytes
"33,10,350", // Standard 10 drop fee, plus 320 drops + 20 drops for 2 chunks of 16 bytes
// Simulate 100 drop base fee
"0,100,3300", // Standard 100 drop fee, plus 3200 drops
"1,100,3300", // Standard 100 drop fee, plus 3200 drops
"2,100,3300", // Standard 100 drop fee, plus 3200 drops
"15,100,3300", // Standard 100 drop fee, plus 3200 drops
"16,100,3400", // Standard 100 drop fee, plus 3200 drops + 100 drops for 1 chunk of 16 bytes
"17,100,3400", // Standard 100 drop fee, plus 3200 drops + 100 drops for 1 chunk of 16 bytes
"31,100,3400", // Standard 100 drop fee, plus 3200 drops + 100 drops for 1 chunk of 16 bytes
"32,100,3500", // Standard 100 drop fee, plus 3200 drops + 200 drops for 2 chunks of 16 bytes
"33,100,3500", // Standard 100 drop fee, plus 3200 drops + 200 drops for 2 chunks of 16 bytes
// Simulate 200 drop base fee
"0,200,6600", // Standard 200 drop fee, plus 6400 drops
"1,200,6600", // Standard 200 drop fee, plus 6400 drops
"2,200,6600", // Standard 200 drop fee, plus 6400 drops
"15,200,6600",// Standard 200 drop fee, plus 6400 drops
"16,200,6800",// Standard 200 drop fee, plus 6400 drops + 200 drops for 1 chunk of 16 bytes
"17,200,6800",// Standard 200 drop fee, plus 6400 drops + 200 drops for 1 chunk of 16 bytes
"31,200,6800",// Standard 200 drop fee, plus 6400 drops + 200 drops for 1 chunk of 16 bytes
"32,200,7000",// Standard 200 drop fee, plus 6400 drops + 400 drops for 2 chunks of 16 bytes
"33,200,7000",// Standard 200 drop fee, plus 6400 drops + 400 drops for 2 chunks of 16 bytes
// Simulate 5000 drop base fee
"0,5000,165000", // Standard 5000 drop fee, plus 160000 drops
"1,5000,165000", // Standard 5000 drop fee, plus 160000 drops
"2,5000,165000", // Standard 5000 drop fee, plus 160000 drops
"15,5000,165000",// Standard 5000 drop fee, plus 160000 drops
"16,5000,170000",// Standard 5000 drop fee, plus 160000 drops + 5000 drops for 1 chunk of 16 bytes
"17,5000,170000",// Standard 5000 drop fee, plus 160000 drops + 5000 drops for 1 chunk of 16 bytes
"31,5000,170000",// Standard 5000 drop fee, plus 160000 drops + 5000 drops for 1 chunk of 16 bytes
"32,5000,175000",// Standard 5000 drop fee, plus 160000 drops + 10000 drops for 2 chunks of 16 bytes
"33,5000,175000",// Standard 5000 drop fee, plus 160000 drops + 10000 drops for 2 chunks of 16 bytes
})
public void testNormalizeWithVariousFulfillmentSizes(int numBytes, int baseFee, int expectedDrops) {
/////////////////
// Test Normalize
/////////////////

Builder builder = EscrowFinish.builder()
.fee(XrpCurrencyAmount.ofDrops(1))
.account(Address.of("rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59Ba"))
.sequence(UnsignedInteger.ONE)
.owner(Address.of("rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH"))
.offerSequence(UnsignedInteger.ZERO);

// 0 bytes
Fulfillment fulfillment = PreimageSha256Fulfillment.from(new byte[0]);
builder.fulfillment(fulfillment);
builder.fee(EscrowFinish.computeFee(XrpCurrencyAmount.ofDrops(10), fulfillment));
builder.condition(fulfillment.getDerivedCondition());
// Standard 10 drop fee, plus 320 drops
Assertions.assertThat(builder.build().fee()).isEqualTo(XrpCurrencyAmount.ofDrops(330));

// 1 byte
fulfillment = PreimageSha256Fulfillment.from(new byte[1]);
builder.fulfillment(fulfillment);
builder.fee(EscrowFinish.computeFee(XrpCurrencyAmount.ofDrops(10), fulfillment));
builder.condition(fulfillment.getDerivedCondition());
// Standard 10 drop fee, plus 320 drops
Assertions.assertThat(builder.build().fee()).isEqualTo(XrpCurrencyAmount.ofDrops(330));

// 2 byte2
fulfillment = PreimageSha256Fulfillment.from(new byte[2]);
builder.fulfillment(fulfillment);
builder.fee(EscrowFinish.computeFee(XrpCurrencyAmount.ofDrops(10), fulfillment));
builder.condition(fulfillment.getDerivedCondition());
// Standard 10 drop fee, plus 320 drops
Assertions.assertThat(builder.build().fee()).isEqualTo(XrpCurrencyAmount.ofDrops(330));

// 15 bytes
fulfillment = PreimageSha256Fulfillment.from(new byte[15]);
builder.fulfillment(fulfillment);
builder.fee(EscrowFinish.computeFee(XrpCurrencyAmount.ofDrops(10), fulfillment));
builder.condition(fulfillment.getDerivedCondition());
// Standard 10 drop fee, plus 320 drops
Assertions.assertThat(builder.build().fee()).isEqualTo(XrpCurrencyAmount.ofDrops(330));

// 16 bytes
fulfillment = PreimageSha256Fulfillment.from(new byte[16]);
Fulfillment<?> fulfillment = PreimageSha256Fulfillment.from(new byte[numBytes]);
builder.fulfillment(fulfillment);
builder.fee(EscrowFinish.computeFee(XrpCurrencyAmount.ofDrops(10), fulfillment));
builder.fee(EscrowFinish.computeFee(XrpCurrencyAmount.ofDrops(baseFee), fulfillment));
builder.condition(fulfillment.getDerivedCondition());
// Standard 10 drop fee, plus 320 drops + 10 drops for 16 bytes
Assertions.assertThat(builder.build().fee()).isEqualTo(XrpCurrencyAmount.ofDrops(340));

// 17 bytes
fulfillment = PreimageSha256Fulfillment.from(new byte[17]);
builder.fulfillment(fulfillment);
builder.fee(EscrowFinish.computeFee(XrpCurrencyAmount.ofDrops(10), fulfillment));
builder.condition(fulfillment.getDerivedCondition());
// Standard 10 drop fee, plus 320 drops + 10 drops for 16 bytes
Assertions.assertThat(builder.build().fee()).isEqualTo(XrpCurrencyAmount.ofDrops(340));

// 31 bytes
fulfillment = PreimageSha256Fulfillment.from(new byte[31]);
builder.fulfillment(fulfillment);
builder.fee(EscrowFinish.computeFee(XrpCurrencyAmount.ofDrops(10), fulfillment));
builder.condition(fulfillment.getDerivedCondition());
// Standard 10 drop fee, plus 320 drops + 10 drops for 16 bytes
Assertions.assertThat(builder.build().fee()).isEqualTo(XrpCurrencyAmount.ofDrops(340));

// 32 bytes
fulfillment = PreimageSha256Fulfillment.from(new byte[32]);
builder.fulfillment(fulfillment);
builder.fee(EscrowFinish.computeFee(XrpCurrencyAmount.ofDrops(10), fulfillment));
builder.condition(fulfillment.getDerivedCondition());
// Standard 10 drop fee, plus 320 drops + 20 drops for 32 bytes
// Standard fee, plus (32 * base-fee) drops, plus one base-fee for every 16 fulfillment bytes.
// (see https://xrpl.org/transaction-cost.html#fee-levels)
Assertions.assertThat(builder.build().fee()).isEqualTo(XrpCurrencyAmount.ofDrops(350));

// 33 bytes
fulfillment = PreimageSha256Fulfillment.from(new byte[33]);
builder.fulfillment(fulfillment);
builder.fee(EscrowFinish.computeFee(XrpCurrencyAmount.ofDrops(10), fulfillment));
builder.condition(fulfillment.getDerivedCondition());
// Standard 10 drop fee, plus 320 drops + 20 drops for 32 bytes
Assertions.assertThat(builder.build().fee()).isEqualTo(XrpCurrencyAmount.ofDrops(350));
}
Assertions.assertThat(builder.build().fee()).isEqualTo(XrpCurrencyAmount.ofDrops(expectedDrops));

@Test
public void testComputeFee() {
// 0 bytes
assertThat(
EscrowFinish.computeFee(XrpCurrencyAmount.ofDrops(10), PreimageSha256Fulfillment.from(new byte[0])))
.isEqualTo(XrpCurrencyAmount.ofDrops(330));
assertThat(
EscrowFinish.computeFee(XrpCurrencyAmount.ofDrops(10), PreimageSha256Fulfillment.from(new byte[1])))
.isEqualTo(XrpCurrencyAmount.ofDrops(330));
assertThat(
EscrowFinish.computeFee(XrpCurrencyAmount.ofDrops(10), PreimageSha256Fulfillment.from(new byte[2])))
.isEqualTo(XrpCurrencyAmount.ofDrops(330));
assertThat(
EscrowFinish.computeFee(XrpCurrencyAmount.ofDrops(10), PreimageSha256Fulfillment.from(new byte[15])))
.isEqualTo(XrpCurrencyAmount.ofDrops(330));
assertThat(
EscrowFinish.computeFee(XrpCurrencyAmount.ofDrops(10), PreimageSha256Fulfillment.from(new byte[16])))
.isEqualTo(XrpCurrencyAmount.ofDrops(340));
assertThat(
EscrowFinish.computeFee(XrpCurrencyAmount.ofDrops(10), PreimageSha256Fulfillment.from(new byte[17])))
.isEqualTo(XrpCurrencyAmount.ofDrops(340));
assertThat(
EscrowFinish.computeFee(XrpCurrencyAmount.ofDrops(10), PreimageSha256Fulfillment.from(new byte[31])))
.isEqualTo(XrpCurrencyAmount.ofDrops(340));
assertThat(
EscrowFinish.computeFee(XrpCurrencyAmount.ofDrops(10), PreimageSha256Fulfillment.from(new byte[32])))
.isEqualTo(XrpCurrencyAmount.ofDrops(350));
assertThat(
EscrowFinish.computeFee(XrpCurrencyAmount.ofDrops(10), PreimageSha256Fulfillment.from(new byte[33])))
.isEqualTo(XrpCurrencyAmount.ofDrops(350));
assertThat(
EscrowFinish.computeFee(XrpCurrencyAmount.ofDrops(10), PreimageSha256Fulfillment.from(new byte[64])))
.isEqualTo(XrpCurrencyAmount.ofDrops(370));
///////////////////
// Test Compute Fee
///////////////////
final XrpCurrencyAmount computedFee = EscrowFinish.computeFee(
XrpCurrencyAmount.ofDrops(baseFee), PreimageSha256Fulfillment.from(new byte[numBytes])
);
assertThat(computedFee).isEqualTo(XrpCurrencyAmount.ofDrops(expectedDrops));
}

}

0 comments on commit 08882c5

Please sign in to comment.