Add fee cap for transactions submitted via RPC. (#1137)

- Added `--rpc-tx-feecap` command line flag.
    - Maximum transaction fees (in Wei) accepted for transaction submitted through RPC.
    - Defaulted to 1 ether.
- Updated `TransactionPool.addLocalTransaction` method: performs an additional check to verify if transaction fees don't exceed user defined fee cap (if cap is set to 0 then it is ignored and means no cap).

Signed-off-by: Abdelhamid Bakhta <abdelhamid.bakhta@consensys.net>
pull/1142/head
Abdelhamid Bakhta 4 years ago committed by GitHub
parent 9e224ed7f3
commit f5dd1db9b6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java
  2. 3
      besu/src/main/java/org/hyperledger/besu/cli/DefaultCommandValues.java
  3. 18
      besu/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java
  4. 1
      besu/src/test/resources/everything_config.toml
  5. 3
      ethereum/api/src/integration-test/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/EthGetFilterChangesIntegrationTest.java
  6. 3
      ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/graphql/internal/response/GraphQLError.java
  7. 2
      ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcErrorConverter.java
  8. 1
      ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/response/JsonRpcError.java
  9. 6
      ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthSendRawTransactionTest.java
  10. 1
      ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/TransactionValidator.java
  11. 2
      ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/ValidationResult.java
  12. 11
      ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPool.java
  13. 31
      ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPoolConfiguration.java
  14. 3
      ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPoolFactory.java
  15. 14
      ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPoolFactoryTest.java
  16. 81
      ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPoolTest.java

@ -769,6 +769,13 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
arity = "1")
private final Wei minTransactionGasPrice = DEFAULT_MIN_TRANSACTION_GAS_PRICE;
@Option(
names = {"--rpc-tx-feecap"},
description =
"Maximum transaction fees (in Wei) accepted for transaction submitted through RPC (default: ${DEFAULT-VALUE})",
arity = "1")
private final Wei txFeeCap = DEFAULT_RPC_TX_FEE_CAP;
@Option(
names = {"--min-block-occupancy-ratio"},
description = "Minimum occupancy ratio for a mined block (default: ${DEFAULT-VALUE})",
@ -1938,6 +1945,7 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
.pooledTransactionHashesSize(pooledTransactionHashesSize)
.pendingTxRetentionPeriod(pendingTxRetentionPeriod)
.priceBump(priceBump)
.txFeeCap(txFeeCap)
.build();
}

@ -15,6 +15,7 @@
package org.hyperledger.besu.cli;
import org.hyperledger.besu.ethereum.core.Wei;
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolConfiguration;
import org.hyperledger.besu.ethereum.p2p.config.RlpxConfiguration;
import org.hyperledger.besu.nat.NatMethod;
@ -44,6 +45,8 @@ public interface DefaultCommandValues {
String MANDATORY_NETWORK_FORMAT_HELP = "<NETWORK>";
String MANDATORY_NODE_ID_FORMAT_HELP = "<NODEID>";
Wei DEFAULT_MIN_TRANSACTION_GAS_PRICE = Wei.of(1000);
Wei DEFAULT_RPC_TX_FEE_CAP = TransactionPoolConfiguration.DEFAULT_RPC_TX_FEE_CAP;
Double DEFAULT_MIN_BLOCK_OCCUPANCY_RATIO = 0.8;
Bytes DEFAULT_EXTRA_DATA = Bytes.EMPTY;
long DEFAULT_MAX_REFRESH_DELAY = 3600000;

@ -3170,6 +3170,24 @@ public class BesuCommandTest extends CommandTestAbstract {
"should be a number between 0 and 100 inclusive");
}
@Test
public void transactionPoolTxFeeCap() {
final Wei txFeeCap = Wei.fromEth(2);
parseCommand("--rpc-tx-feecap", txFeeCap.toString());
verify(mockControllerBuilder)
.transactionPoolConfiguration(transactionPoolConfigCaptor.capture());
assertThat(transactionPoolConfigCaptor.getValue().getTxFeeCap()).isEqualTo(txFeeCap);
assertThat(commandOutput.toString()).isEmpty();
assertThat(commandErrorOutput.toString()).isEmpty();
}
@Test
public void invalidTansactionPoolTxFeeCapShouldFail() {
parseCommand("--rpc-tx-feecap", "abcd");
assertThat(commandErrorOutput.toString())
.contains("Invalid value for option '--rpc-tx-feecap'", "cannot convert 'abcd' to Wei");
}
@Test
public void txMessageKeepAliveSecondsWithInvalidInputShouldFail() {
parseCommand("--Xincoming-tx-messages-keep-alive-seconds", "acbd");

@ -138,6 +138,7 @@ tx-pool-price-bump=13
tx-pool-max-size=1234
tx-pool-hashes-max-size=10000
Xincoming-tx-messages-keep-alive-seconds=60
rpc-tx-feecap=2000000000000000000
# Revert Reason
revert-reason-enabled=false

@ -126,7 +126,8 @@ public class EthGetFilterChangesIntegrationTest {
Optional.of(peerPendingTransactionTracker),
Wei.ZERO,
metricsSystem,
Optional.empty());
Optional.empty(),
TransactionPoolConfiguration.DEFAULT);
final BlockchainQueries blockchainQueries =
new BlockchainQueries(blockchain, protocolContext.getWorldStateArchive());
filterManager =

@ -38,6 +38,7 @@ public enum GraphQLError {
WRONG_CHAIN_ID(-32000, "Wrong Chain ID in transaction signature"),
REPLAY_PROTECTED_SIGNATURES_NOT_SUPPORTED(
-32000, "Signatures with replay protection are not supported"),
TX_FEECAP_EXCEEDED(-32000, "Transaction fee cap exceeded"),
// Private Transaction Errors
PRIVATE_TRANSACTION_FAILED(-32000, "Private transaction failed"),
@ -91,6 +92,8 @@ public enum GraphQLError {
return PRIVATE_TRANSACTION_FAILED;
case GAS_PRICE_TOO_LOW:
return GAS_PRICE_TOO_LOW;
case TX_FEECAP_EXCEEDED:
return TX_FEECAP_EXCEEDED;
default:
return INTERNAL_ERROR;
}

@ -47,6 +47,8 @@ public class JsonRpcErrorConverter {
return JsonRpcError.CHAIN_HEAD_WORLD_STATE_NOT_AVAILABLE;
case GAS_PRICE_TOO_LOW:
return JsonRpcError.GAS_PRICE_TOO_LOW;
case TX_FEECAP_EXCEEDED:
return JsonRpcError.TX_FEECAP_EXCEEDED;
case OFFCHAIN_PRIVACY_GROUP_DOES_NOT_EXIST:
return JsonRpcError.OFFCHAIN_PRIVACY_GROUP_DOES_NOT_EXIST;
case TRANSACTION_ALREADY_KNOWN:

@ -59,6 +59,7 @@ public enum JsonRpcError {
GAS_PRICE_TOO_LOW(-32009, "Gas price below configured minimum gas price"),
WRONG_CHAIN_ID(-32000, "Wrong chainId"),
REPLAY_PROTECTED_SIGNATURES_NOT_SUPPORTED(-32000, "ChainId not supported"),
TX_FEECAP_EXCEEDED(-32000, "Transaction fee cap exceeded"),
// Miner failures
COINBASE_NOT_SET(-32010, "Coinbase not set. Unable to start mining without a coinbase"),

@ -170,6 +170,12 @@ public class EthSendRawTransactionTest {
TransactionInvalidReason.TX_SENDER_NOT_AUTHORIZED, JsonRpcError.TX_SENDER_NOT_AUTHORIZED);
}
@Test
public void transactionWithFeeCapExceededIsRejected() {
verifyErrorForInvalidTransaction(
TransactionInvalidReason.TX_FEECAP_EXCEEDED, JsonRpcError.TX_FEECAP_EXCEEDED);
}
private void verifyErrorForInvalidTransaction(
final TransactionInvalidReason transactionInvalidReason, final JsonRpcError expectedError) {
when(transactionPool.addLocalTransaction(any(Transaction.class)))

@ -81,6 +81,7 @@ public interface TransactionValidator {
OFFCHAIN_PRIVACY_GROUP_DOES_NOT_EXIST,
INCORRECT_PRIVATE_NONCE,
GAS_PRICE_TOO_LOW,
TX_FEECAP_EXCEEDED,
PRIVATE_VALUE_NOT_ZERO,
PRIVATE_UNIMPLEMENTED_TRANSACTION_TYPE;
}

@ -33,7 +33,7 @@ public final class ValidationResult<T> {
}
public boolean isValid() {
return !invalidReason.isPresent();
return invalidReason.isEmpty();
}
public T getInvalidReason() throws NoSuchElementException {

@ -83,6 +83,7 @@ public class TransactionPool implements BlockAddedObserver {
TransactionPriceCalculator.frontier();
private final TransactionPriceCalculator eip1559PriceCalculator =
TransactionPriceCalculator.eip1559();
private final TransactionPoolConfiguration configuration;
public TransactionPool(
final PendingTransactions pendingTransactions,
@ -96,7 +97,8 @@ public class TransactionPool implements BlockAddedObserver {
final Optional<PeerPendingTransactionTracker> peerPendingTransactionTracker,
final Wei minTransactionGasPrice,
final MetricsSystem metricsSystem,
final Optional<EIP1559> eip1559) {
final Optional<EIP1559> eip1559,
final TransactionPoolConfiguration configuration) {
this.pendingTransactions = pendingTransactions;
this.protocolSchedule = protocolSchedule;
this.protocolContext = protocolContext;
@ -107,6 +109,7 @@ public class TransactionPool implements BlockAddedObserver {
this.peerPendingTransactionTracker = peerPendingTransactionTracker;
this.minTransactionGasPrice = minTransactionGasPrice;
this.eip1559 = eip1559;
this.configuration = configuration;
duplicateTransactionCounter =
metricsSystem.createLabelledCounter(
@ -152,6 +155,12 @@ public class TransactionPool implements BlockAddedObserver {
if (transactionGasPrice.compareTo(minTransactionGasPrice) < 0) {
return ValidationResult.invalid(TransactionInvalidReason.GAS_PRICE_TOO_LOW);
}
if (!configuration.getTxFeeCap().isZero()
&& transactionGasPrice.compareTo(configuration.getTxFeeCap()) > 0) {
return ValidationResult.invalid(TransactionInvalidReason.TX_FEECAP_EXCEEDED);
}
final ValidationResult<TransactionInvalidReason> validationResult =
validateTransaction(transaction);
if (validationResult.isValid()) {

@ -14,6 +14,7 @@
*/
package org.hyperledger.besu.ethereum.eth.transactions;
import org.hyperledger.besu.ethereum.core.Wei;
import org.hyperledger.besu.util.number.Percentage;
import java.util.Objects;
@ -24,25 +25,31 @@ public class TransactionPoolConfiguration {
public static final int MAX_PENDING_TRANSACTIONS_HASHES = 4096;
public static final int DEFAULT_TX_RETENTION_HOURS = 13;
public static final Percentage DEFAULT_PRICE_BUMP = Percentage.fromInt(10);
public static final Wei DEFAULT_RPC_TX_FEE_CAP = Wei.fromEth(1);
public static final TransactionPoolConfiguration DEFAULT =
TransactionPoolConfiguration.builder().build();
private final int txPoolMaxSize;
private final int pooledTransactionHashesSize;
private final int pendingTxRetentionPeriod;
private final int txMessageKeepAliveSeconds;
private final Percentage priceBump;
private final Wei txFeeCap;
public TransactionPoolConfiguration(
final int txPoolMaxSize,
final int pooledTransactionHashesSize,
final int pendingTxRetentionPeriod,
final int txMessageKeepAliveSeconds,
final Percentage priceBump) {
final Percentage priceBump,
final Wei txFeeCap) {
this.txPoolMaxSize = txPoolMaxSize;
this.pooledTransactionHashesSize = pooledTransactionHashesSize;
this.pendingTxRetentionPeriod = pendingTxRetentionPeriod;
this.txMessageKeepAliveSeconds = txMessageKeepAliveSeconds;
this.priceBump = priceBump;
this.txFeeCap = txFeeCap;
}
public int getTxPoolMaxSize() {
@ -65,6 +72,10 @@ public class TransactionPoolConfiguration {
return priceBump;
}
public Wei getTxFeeCap() {
return txFeeCap;
}
@Override
public boolean equals(final Object o) {
if (this == o) {
@ -77,13 +88,14 @@ public class TransactionPoolConfiguration {
return txPoolMaxSize == that.txPoolMaxSize
&& Objects.equals(pendingTxRetentionPeriod, that.pendingTxRetentionPeriod)
&& Objects.equals(txMessageKeepAliveSeconds, that.txMessageKeepAliveSeconds)
&& Objects.equals(priceBump, that.priceBump);
&& Objects.equals(priceBump, that.priceBump)
&& Objects.equals(txFeeCap, that.txFeeCap);
}
@Override
public int hashCode() {
return Objects.hash(
txPoolMaxSize, pendingTxRetentionPeriod, txMessageKeepAliveSeconds, priceBump);
txPoolMaxSize, pendingTxRetentionPeriod, txMessageKeepAliveSeconds, priceBump, txFeeCap);
}
@Override
@ -97,6 +109,8 @@ public class TransactionPoolConfiguration {
+ txMessageKeepAliveSeconds
+ ", priceBump="
+ priceBump
+ ", txFeeCap="
+ txFeeCap
+ '}';
}
@ -110,6 +124,7 @@ public class TransactionPoolConfiguration {
private Integer txMessageKeepAliveSeconds = DEFAULT_TX_MSG_KEEP_ALIVE;
private int pooledTransactionHashesSize = MAX_PENDING_TRANSACTIONS_HASHES;
private Percentage priceBump = DEFAULT_PRICE_BUMP;
private Wei txFeeCap = DEFAULT_RPC_TX_FEE_CAP;
public Builder txPoolMaxSize(final int txPoolMaxSize) {
this.txPoolMaxSize = txPoolMaxSize;
@ -140,13 +155,19 @@ public class TransactionPoolConfiguration {
return priceBump(Percentage.fromInt(priceBump));
}
public Builder txFeeCap(final Wei txFeeCap) {
this.txFeeCap = txFeeCap;
return this;
}
public TransactionPoolConfiguration build() {
return new TransactionPoolConfiguration(
txPoolMaxSize,
pooledTransactionHashesSize,
pendingTxRetentionPeriod,
txMessageKeepAliveSeconds,
priceBump);
priceBump,
txFeeCap);
}
}
}

@ -125,7 +125,8 @@ public class TransactionPoolFactory {
pendingTransactionTracker,
minTransactionGasPrice,
metricsSystem,
eip1559);
eip1559,
transactionPoolConfiguration);
final TransactionsMessageHandler transactionsMessageHandler =
new TransactionsMessageHandler(
ethContext.getScheduler(),

@ -90,7 +90,12 @@ public class TransactionPoolFactoryTest {
state,
Wei.of(1),
new TransactionPoolConfiguration(
1, 1, 1, 1, TransactionPoolConfiguration.DEFAULT_PRICE_BUMP),
1,
1,
1,
1,
TransactionPoolConfiguration.DEFAULT_PRICE_BUMP,
TransactionPoolConfiguration.DEFAULT_RPC_TX_FEE_CAP),
pendingTransactions,
peerTransactionTracker,
transactionsMessageSender,
@ -168,7 +173,12 @@ public class TransactionPoolFactoryTest {
state,
Wei.of(1),
new TransactionPoolConfiguration(
1, 1, 1, 1, TransactionPoolConfiguration.DEFAULT_PRICE_BUMP),
1,
1,
1,
1,
TransactionPoolConfiguration.DEFAULT_PRICE_BUMP,
TransactionPoolConfiguration.DEFAULT_RPC_TX_FEE_CAP),
pendingTransactions,
peerTransactionTracker,
transactionsMessageSender,

@ -148,7 +148,8 @@ public class TransactionPoolTest {
Optional.of(peerPendingTransactionTracker),
Wei.of(2),
metricsSystem,
Optional.empty());
Optional.empty(),
TransactionPoolConfiguration.DEFAULT);
blockchain.observeBlockAdded(transactionPool);
}
@ -404,7 +405,8 @@ public class TransactionPoolTest {
Optional.of(peerPendingTransactionTracker),
Wei.ZERO,
metricsSystem,
Optional.empty());
Optional.empty(),
TransactionPoolConfiguration.DEFAULT);
when(pendingTransactions.containsTransaction(transaction1.getHash())).thenReturn(true);
@ -541,7 +543,8 @@ public class TransactionPoolTest {
Optional.of(peerPendingTransactionTracker),
Wei.ZERO,
metricsSystem,
Optional.empty());
Optional.empty(),
TransactionPoolConfiguration.DEFAULT);
final TransactionTestFixture builder = new TransactionTestFixture();
final Transaction transaction1 = builder.nonce(1).createTransaction(KEY_PAIR1);
@ -610,7 +613,8 @@ public class TransactionPoolTest {
Optional.of(peerPendingTransactionTracker),
Wei.ZERO,
metricsSystem,
Optional.empty());
Optional.empty(),
TransactionPoolConfiguration.DEFAULT);
final TransactionTestFixture builder = new TransactionTestFixture();
final Transaction transactionLocal = builder.nonce(1).createTransaction(KEY_PAIR1);
@ -650,6 +654,75 @@ public class TransactionPoolTest {
.isEqualToComparingFieldByField(expectedValidationParams);
}
@Test
public void shouldIgnoreFeeCapIfSetZero() {
final EthProtocolManager ethProtocolManager = EthProtocolManagerTestUtil.create();
final EthContext ethContext = ethProtocolManager.ethContext();
final PeerTransactionTracker peerTransactionTracker = new PeerTransactionTracker();
final Wei twoEthers = Wei.fromEth(2);
final TransactionPool transactionPool =
new TransactionPool(
transactions,
protocolSchedule,
protocolContext,
batchAddedListener,
Optional.of(pendingBatchAddedListener),
syncState,
ethContext,
peerTransactionTracker,
Optional.of(peerPendingTransactionTracker),
Wei.ZERO,
metricsSystem,
Optional.empty(),
TransactionPoolConfiguration.builder().txFeeCap(Wei.ZERO).build());
when(transactionValidator.validate(any(Transaction.class))).thenReturn(valid());
when(transactionValidator.validateForSender(
any(Transaction.class),
nullable(Account.class),
any(TransactionValidationParams.class)))
.thenReturn(valid());
assertThat(
transactionPool
.addLocalTransaction(
new TransactionTestFixture()
.nonce(1)
.gasPrice(twoEthers.add(Wei.of(1)))
.createTransaction(KEY_PAIR1))
.isValid())
.isTrue();
}
@Test
public void shouldRejectLocalTransactionIfFeeCapExceeded() {
final EthProtocolManager ethProtocolManager = EthProtocolManagerTestUtil.create();
final EthContext ethContext = ethProtocolManager.ethContext();
final PeerTransactionTracker peerTransactionTracker = new PeerTransactionTracker();
final Wei twoEthers = Wei.fromEth(2);
TransactionPool transactionPool =
new TransactionPool(
transactions,
protocolSchedule,
protocolContext,
batchAddedListener,
Optional.of(pendingBatchAddedListener),
syncState,
ethContext,
peerTransactionTracker,
Optional.of(peerPendingTransactionTracker),
Wei.ZERO,
metricsSystem,
Optional.empty(),
TransactionPoolConfiguration.builder().txFeeCap(twoEthers).build());
final TransactionTestFixture builder = new TransactionTestFixture();
final Transaction transactionLocal =
builder.nonce(1).gasPrice(twoEthers.add(Wei.of(1))).createTransaction(KEY_PAIR1);
final ValidationResult<TransactionInvalidReason> result =
transactionPool.addLocalTransaction(transactionLocal);
assertThat(result.getInvalidReason()).isEqualTo(TransactionInvalidReason.TX_FEECAP_EXCEEDED);
}
private void assertTransactionPending(final Transaction t) {
assertThat(transactions.getTransactionByHash(t.getHash())).contains(t);
}

Loading…
Cancel
Save