mirror of https://github.com/hyperledger/besu
Layered Transaction Pool (#5290)
* Introduce experimental layered transaction pool Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * new Xlayered-tx-pool flag to enabled the new tx pool Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Move pending transaction sorter tests in the sorter folder Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Unit tests for new and old transaction pool Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Fix: do not decrease size when promoting ready txs Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Fix: remove tx from orderByFee when replaced Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Fix: decrease size when removing confirmed txs Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Fix: always recreate orderByFee for London fee market Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Fix: transaction removal counter when txs added to block Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Fix: update expected nonce when demoting a prioritized transaction Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Fix: correctly remove expected nonce entry when removing the last prioritized transaction for the sender Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Fix NullPointerException when the replaced tx is not prioritized Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Replace postponed with spare transactions Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * WIP Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * WIP Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Fix merge from main Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Fixed most tests Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Fix more tests Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Rename and reorg some classes Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * More renaming and code clean up Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Refactor transaction pool metrics Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Stats log refined Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Cleanup unit tests Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Improve stats log Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Remove unnecessary test parameters Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Fix unit test Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> # Conflicts: # ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/Transaction.java # ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionValidator.java * Cancel older block creation tasks upon receiving a new one Signed-off-by: Simon Dudley <simon.dudley@consensys.net> * Fixes to expected next nonce for sender Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Fix promotion filter and use synchronized methods instead of blocks Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Fix metrics concurrent access issue Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Fixes Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Configuration options for the layered txpool Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Use long instead of Instant for PendingTransaction add time Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Fixes Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Move layered txpool clasess in a dedicated package Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Fixes Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * WIP Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Remove confirmed transaction from sparse set too Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * WIP Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * WIP Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Fill gap on added tx Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Fix eviction on sparse layer Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Fix remove from ready layer Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Fix remove from sparse layer Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Fix for block added and confirmed txs Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Fixes to sparse layer Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Fix the filling of the gap when adding transactions Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Layered pending transactions test and fixes Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Distinguish between layer and comulative space used Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * unit tests for expected next nonce for sender Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Adding test for transaction selection Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Re-enable prioritized transaction tests and more fixes Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * log stats Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * syncronized some methods, metrics update and dump transactions for replay Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Test that replay tx and fix for tx replacement across layers Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Add missing copyright and fix replay test Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Add consistency check asserts Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Fix ready internalRemove Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Metrics tests improvements Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * WIP: Transaction memory size estimation Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Complete pending transaction memory used computation Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Improve metrics Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Rename to specify that the limit is per layer Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Update metric names in tests Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Adjust tx layer max capacity according to new tx memory size calculation Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Fix legacy transaction expiration tests Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Fix IndexOutOfBoundsException in sparse layer Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Unique senders metric Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Ignore ReplayTest by default, fix logging of stats Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Log for replay renamings Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Document howto generate txpool replay Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Reduce max layer capacity Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * exclude transaction replay resource Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Improve compareByFee when effectivePriorityFee is 0 Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * More debug logs during transaction selection for a block Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Use only one thread for building blocks so there is no risk of overlapping Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Improve transaction trace log making wei human readable Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Use List instead of Set when getting all pending transactions Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * More detailed log on adding remote txs Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Execute transaction broadcast aysnc after adding them to the txpool Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Log time taken to add remote txs before their broadcast Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Fix test Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Add missing header Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Fix unit tests Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Add CHANGELOG entry Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Delete unneeded file Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Rename some layered txpool metrics to avoid conflict with existing metrics Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Fix pushing to next layers txs following an invalid one Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * In case of an unexpected error, log more data and do a consistency check Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Fix null check on wrong var Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Fix some codeql alerts Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Fix sparse gap calculation when invalidating Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Apply suggestions from doce review Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Only trigger consistency check if trace log is enable in case of unexpected error Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Fix replay of blocks with no transactions Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Fix for negative gap when there is a reorg Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Implement code review suggestions Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Fix for a case when deleting tx with zero gap in sparse layer Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Delete redoundant tests Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Update CHANGELOG.md Co-authored-by: Sally MacFarlane <macfarla.github@gmail.com> Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Update ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPool.java Co-authored-by: Sally MacFarlane <macfarla.github@gmail.com> Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Update ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPoolMetrics.java Co-authored-by: Sally MacFarlane <macfarla.github@gmail.com> Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Update ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/ReadyTransactions.java Co-authored-by: Sally MacFarlane <macfarla.github@gmail.com> Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Update ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/AbstractPrioritizedTransactionsTestBase.java Co-authored-by: Sally MacFarlane <macfarla.github@gmail.com> Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Update ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/SparseTransactions.java Co-authored-by: Sally MacFarlane <macfarla.github@gmail.com> Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Update besu/src/main/java/org/hyperledger/besu/cli/options/unstable/TransactionPoolOptions.java Co-authored-by: Sally MacFarlane <macfarla.github@gmail.com> Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Update besu/src/main/java/org/hyperledger/besu/cli/options/unstable/TransactionPoolOptions.java Co-authored-by: Sally MacFarlane <macfarla.github@gmail.com> Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Address code review suggestions Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Improve logSender when there are no sparse txs Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Fix off by one error Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Update ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/TransactionsLayer.java Co-authored-by: Simon Dudley <simon.dudley@consensys.net> Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Address code review suggestions Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Rename fix Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Simplify the way reorgs are handled, by detecting a negative gap and deleting and readding all the txs for that sender Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Do not run consistency check on internal error since it is too expensive, instead force a reorg of the sender txs Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Remove invalid txs after the selection is complete to avoid ConcurrentModificationException Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Update txpool defaults Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Tune default Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> * Fix merge Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> --------- Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> Signed-off-by: Simon Dudley <simon.dudley@consensys.net> Co-authored-by: Simon Dudley <simon.dudley@consensys.net> Co-authored-by: Sally MacFarlane <macfarla.github@gmail.com> Co-authored-by: garyschulte <garyschulte@gmail.com>pull/5451/head
parent
7fff05a4cc
commit
423fe1d481
@ -0,0 +1,129 @@ |
|||||||
|
/* |
||||||
|
* Copyright Besu contributors. |
||||||
|
* |
||||||
|
* 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. See the License for the |
||||||
|
* specific language governing permissions and limitations under the License. |
||||||
|
* |
||||||
|
* SPDX-License-Identifier: Apache-2.0 |
||||||
|
*/ |
||||||
|
package org.hyperledger.besu.ethereum.eth.transactions; |
||||||
|
|
||||||
|
import org.hyperledger.besu.ethereum.transaction.TransactionInvalidReason; |
||||||
|
|
||||||
|
import java.util.Objects; |
||||||
|
import java.util.Optional; |
||||||
|
|
||||||
|
public final class TransactionAddedResult { |
||||||
|
private enum Status { |
||||||
|
INVALID, |
||||||
|
REPLACED, |
||||||
|
DROPPED, |
||||||
|
TRY_NEXT_LAYER, |
||||||
|
ADDED, |
||||||
|
REORG_SENDER, |
||||||
|
INTERNAL_ERROR |
||||||
|
} |
||||||
|
|
||||||
|
public static final TransactionAddedResult ALREADY_KNOWN = |
||||||
|
new TransactionAddedResult(TransactionInvalidReason.TRANSACTION_ALREADY_KNOWN); |
||||||
|
public static final TransactionAddedResult REJECTED_UNDERPRICED_REPLACEMENT = |
||||||
|
new TransactionAddedResult(TransactionInvalidReason.TRANSACTION_REPLACEMENT_UNDERPRICED); |
||||||
|
public static final TransactionAddedResult NONCE_TOO_FAR_IN_FUTURE_FOR_SENDER = |
||||||
|
new TransactionAddedResult(TransactionInvalidReason.NONCE_TOO_FAR_IN_FUTURE_FOR_SENDER); |
||||||
|
public static final TransactionAddedResult LOWER_NONCE_INVALID_TRANSACTION_KNOWN = |
||||||
|
new TransactionAddedResult(TransactionInvalidReason.LOWER_NONCE_INVALID_TRANSACTION_EXISTS); |
||||||
|
|
||||||
|
public static final TransactionAddedResult ADDED = new TransactionAddedResult(Status.ADDED); |
||||||
|
public static final TransactionAddedResult TRY_NEXT_LAYER = |
||||||
|
new TransactionAddedResult(Status.TRY_NEXT_LAYER); |
||||||
|
|
||||||
|
public static final TransactionAddedResult REORG_SENDER = |
||||||
|
new TransactionAddedResult(Status.REORG_SENDER); |
||||||
|
|
||||||
|
public static final TransactionAddedResult DROPPED = new TransactionAddedResult(Status.DROPPED); |
||||||
|
|
||||||
|
public static final TransactionAddedResult INTERNAL_ERROR = |
||||||
|
new TransactionAddedResult(Status.INTERNAL_ERROR); |
||||||
|
|
||||||
|
private final Optional<TransactionInvalidReason> rejectReason; |
||||||
|
|
||||||
|
private final Optional<PendingTransaction> replacedTransaction; |
||||||
|
|
||||||
|
private final Status status; |
||||||
|
|
||||||
|
private TransactionAddedResult(final PendingTransaction replacedTransaction) { |
||||||
|
this.replacedTransaction = Optional.of(replacedTransaction); |
||||||
|
this.rejectReason = Optional.empty(); |
||||||
|
this.status = Status.REPLACED; |
||||||
|
} |
||||||
|
|
||||||
|
private TransactionAddedResult(final TransactionInvalidReason rejectReason) { |
||||||
|
this.replacedTransaction = Optional.empty(); |
||||||
|
this.rejectReason = Optional.of(rejectReason); |
||||||
|
this.status = Status.INVALID; |
||||||
|
} |
||||||
|
|
||||||
|
private TransactionAddedResult(final Status status) { |
||||||
|
this.replacedTransaction = Optional.empty(); |
||||||
|
this.rejectReason = Optional.empty(); |
||||||
|
this.status = status; |
||||||
|
} |
||||||
|
|
||||||
|
public boolean isSuccess() { |
||||||
|
return !isRejected() && status != Status.INTERNAL_ERROR; |
||||||
|
} |
||||||
|
|
||||||
|
public boolean isRejected() { |
||||||
|
return status == Status.INVALID; |
||||||
|
} |
||||||
|
|
||||||
|
public boolean isReplacement() { |
||||||
|
return replacedTransaction.isPresent(); |
||||||
|
} |
||||||
|
|
||||||
|
public Optional<TransactionInvalidReason> maybeInvalidReason() { |
||||||
|
return rejectReason; |
||||||
|
} |
||||||
|
|
||||||
|
public Optional<PendingTransaction> maybeReplacedTransaction() { |
||||||
|
return replacedTransaction; |
||||||
|
} |
||||||
|
|
||||||
|
public static TransactionAddedResult createForReplacement( |
||||||
|
final PendingTransaction replacedTransaction) { |
||||||
|
return new TransactionAddedResult(replacedTransaction); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean equals(final Object o) { |
||||||
|
if (this == o) return true; |
||||||
|
if (o == null || getClass() != o.getClass()) return false; |
||||||
|
TransactionAddedResult that = (TransactionAddedResult) o; |
||||||
|
return Objects.equals(rejectReason, that.rejectReason) |
||||||
|
&& Objects.equals(replacedTransaction, that.replacedTransaction) |
||||||
|
&& status == that.status; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public int hashCode() { |
||||||
|
return Objects.hash(rejectReason, replacedTransaction, status); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String toString() { |
||||||
|
return "TransactionAddedResult{" |
||||||
|
+ "rejectReason=" |
||||||
|
+ rejectReason |
||||||
|
+ ", replacedTransaction=" |
||||||
|
+ replacedTransaction |
||||||
|
+ ", status=" |
||||||
|
+ status |
||||||
|
+ '}'; |
||||||
|
} |
||||||
|
} |
@ -1,42 +0,0 @@ |
|||||||
/* |
|
||||||
* Copyright Besu contributors. |
|
||||||
* |
|
||||||
* 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. See the License for the |
|
||||||
* specific language governing permissions and limitations under the License. |
|
||||||
* |
|
||||||
* SPDX-License-Identifier: Apache-2.0 |
|
||||||
*/ |
|
||||||
package org.hyperledger.besu.ethereum.eth.transactions; |
|
||||||
|
|
||||||
import org.hyperledger.besu.ethereum.transaction.TransactionInvalidReason; |
|
||||||
|
|
||||||
import java.util.Optional; |
|
||||||
|
|
||||||
public enum TransactionAddedStatus { |
|
||||||
ALREADY_KNOWN(TransactionInvalidReason.TRANSACTION_ALREADY_KNOWN), |
|
||||||
REJECTED_UNDERPRICED_REPLACEMENT(TransactionInvalidReason.TRANSACTION_REPLACEMENT_UNDERPRICED), |
|
||||||
NONCE_TOO_FAR_IN_FUTURE_FOR_SENDER(TransactionInvalidReason.NONCE_TOO_FAR_IN_FUTURE_FOR_SENDER), |
|
||||||
LOWER_NONCE_INVALID_TRANSACTION_KNOWN( |
|
||||||
TransactionInvalidReason.LOWER_NONCE_INVALID_TRANSACTION_EXISTS), |
|
||||||
ADDED(); |
|
||||||
|
|
||||||
private final Optional<TransactionInvalidReason> invalidReason; |
|
||||||
|
|
||||||
TransactionAddedStatus() { |
|
||||||
this.invalidReason = Optional.empty(); |
|
||||||
} |
|
||||||
|
|
||||||
TransactionAddedStatus(final TransactionInvalidReason invalidReason) { |
|
||||||
this.invalidReason = Optional.of(invalidReason); |
|
||||||
} |
|
||||||
|
|
||||||
public Optional<TransactionInvalidReason> getInvalidReason() { |
|
||||||
return invalidReason; |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,173 @@ |
|||||||
|
/* |
||||||
|
* Copyright Hyperledger Besu Contributors. |
||||||
|
* |
||||||
|
* 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. See the License for the |
||||||
|
* specific language governing permissions and limitations under the License. |
||||||
|
* |
||||||
|
* SPDX-License-Identifier: Apache-2.0 |
||||||
|
*/ |
||||||
|
package org.hyperledger.besu.ethereum.eth.transactions; |
||||||
|
|
||||||
|
import org.hyperledger.besu.ethereum.transaction.TransactionInvalidReason; |
||||||
|
import org.hyperledger.besu.metrics.BesuMetricCategory; |
||||||
|
import org.hyperledger.besu.metrics.RunnableCounter; |
||||||
|
import org.hyperledger.besu.plugin.services.MetricsSystem; |
||||||
|
import org.hyperledger.besu.plugin.services.metrics.Counter; |
||||||
|
import org.hyperledger.besu.plugin.services.metrics.LabelledGauge; |
||||||
|
import org.hyperledger.besu.plugin.services.metrics.LabelledMetric; |
||||||
|
|
||||||
|
import java.util.HashMap; |
||||||
|
import java.util.Map; |
||||||
|
import java.util.function.DoubleSupplier; |
||||||
|
|
||||||
|
import org.slf4j.Logger; |
||||||
|
import org.slf4j.LoggerFactory; |
||||||
|
|
||||||
|
public class TransactionPoolMetrics { |
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(TransactionPoolMetrics.class); |
||||||
|
public static final String ADDED_COUNTER_NAME = "added_total"; |
||||||
|
public static final String REMOVED_COUNTER_NAME = "removed_total"; |
||||||
|
public static final String REJECTED_COUNTER_NAME = "rejected_total"; |
||||||
|
public static final String EXPIRED_MESSAGES_COUNTER_NAME = "messages_expired_total"; |
||||||
|
private static final int SKIPPED_MESSAGES_LOGGING_THRESHOLD = 1000; |
||||||
|
private final MetricsSystem metricsSystem; |
||||||
|
private final LabelledMetric<Counter> addedCounter; |
||||||
|
private final LabelledMetric<Counter> removedCounter; |
||||||
|
private final LabelledMetric<Counter> rejectedCounter; |
||||||
|
private final LabelledGauge spaceUsed; |
||||||
|
private final LabelledGauge transactionCount; |
||||||
|
private final LabelledGauge uniqueSenderCount; |
||||||
|
private final LabelledMetric<Counter> expiredMessagesCounter; |
||||||
|
private final Map<String, RunnableCounter> expiredMessagesRunnableCounters = new HashMap<>(); |
||||||
|
private final LabelledMetric<Counter> alreadySeenTransactionsCounter; |
||||||
|
|
||||||
|
public TransactionPoolMetrics(final MetricsSystem metricsSystem) { |
||||||
|
this.metricsSystem = metricsSystem; |
||||||
|
|
||||||
|
addedCounter = |
||||||
|
metricsSystem.createLabelledCounter( |
||||||
|
BesuMetricCategory.TRANSACTION_POOL, |
||||||
|
ADDED_COUNTER_NAME, |
||||||
|
"Count of transactions added to the transaction pool", |
||||||
|
"source", |
||||||
|
"layer"); |
||||||
|
|
||||||
|
removedCounter = |
||||||
|
metricsSystem.createLabelledCounter( |
||||||
|
BesuMetricCategory.TRANSACTION_POOL, |
||||||
|
REMOVED_COUNTER_NAME, |
||||||
|
"Count of transactions removed from the transaction pool", |
||||||
|
"source", |
||||||
|
"operation", |
||||||
|
"layer"); |
||||||
|
|
||||||
|
rejectedCounter = |
||||||
|
metricsSystem.createLabelledCounter( |
||||||
|
BesuMetricCategory.TRANSACTION_POOL, |
||||||
|
REJECTED_COUNTER_NAME, |
||||||
|
"Count of transactions not accepted to the transaction pool", |
||||||
|
"source", |
||||||
|
"reason", |
||||||
|
"layer"); |
||||||
|
|
||||||
|
spaceUsed = |
||||||
|
metricsSystem.createLabelledGauge( |
||||||
|
BesuMetricCategory.TRANSACTION_POOL, |
||||||
|
"space_used", |
||||||
|
"The amount of space used by the transactions in the layer", |
||||||
|
"layer"); |
||||||
|
|
||||||
|
transactionCount = |
||||||
|
metricsSystem.createLabelledGauge( |
||||||
|
BesuMetricCategory.TRANSACTION_POOL, |
||||||
|
"number_of_transactions", |
||||||
|
"The number of transactions currently present in the layer", |
||||||
|
"layer"); |
||||||
|
|
||||||
|
uniqueSenderCount = |
||||||
|
metricsSystem.createLabelledGauge( |
||||||
|
BesuMetricCategory.TRANSACTION_POOL, |
||||||
|
"unique_senders", |
||||||
|
"The number of senders with at least one transaction currently present in the layer", |
||||||
|
"layer"); |
||||||
|
|
||||||
|
expiredMessagesCounter = |
||||||
|
metricsSystem.createLabelledCounter( |
||||||
|
BesuMetricCategory.TRANSACTION_POOL, |
||||||
|
EXPIRED_MESSAGES_COUNTER_NAME, |
||||||
|
"Total number of received transaction pool messages expired and not processed.", |
||||||
|
"message"); |
||||||
|
|
||||||
|
alreadySeenTransactionsCounter = |
||||||
|
metricsSystem.createLabelledCounter( |
||||||
|
BesuMetricCategory.TRANSACTION_POOL, |
||||||
|
"remote_transactions_already_seen_total", |
||||||
|
"Total number of received transactions already seen", |
||||||
|
"message"); |
||||||
|
} |
||||||
|
|
||||||
|
public MetricsSystem getMetricsSystem() { |
||||||
|
return metricsSystem; |
||||||
|
} |
||||||
|
|
||||||
|
public void initSpaceUsed(final DoubleSupplier spaceUsedSupplier, final String layer) { |
||||||
|
spaceUsed.labels(spaceUsedSupplier, layer); |
||||||
|
} |
||||||
|
|
||||||
|
public void initTransactionCount( |
||||||
|
final DoubleSupplier transactionCountSupplier, final String layer) { |
||||||
|
transactionCount.labels(transactionCountSupplier, layer); |
||||||
|
} |
||||||
|
|
||||||
|
public void initUniqueSenderCount( |
||||||
|
final DoubleSupplier uniqueSenderCountSupplier, final String layer) { |
||||||
|
uniqueSenderCount.labels(uniqueSenderCountSupplier, layer); |
||||||
|
} |
||||||
|
|
||||||
|
public void initExpiredMessagesCounter(final String message) { |
||||||
|
expiredMessagesRunnableCounters.put( |
||||||
|
message, |
||||||
|
new RunnableCounter( |
||||||
|
expiredMessagesCounter.labels(message), |
||||||
|
() -> |
||||||
|
LOG.warn( |
||||||
|
"{} expired {} messages have been skipped.", |
||||||
|
SKIPPED_MESSAGES_LOGGING_THRESHOLD, |
||||||
|
message), |
||||||
|
SKIPPED_MESSAGES_LOGGING_THRESHOLD)); |
||||||
|
} |
||||||
|
|
||||||
|
public void incrementAdded(final boolean receivedFromLocalSource, final String layer) { |
||||||
|
addedCounter.labels(location(receivedFromLocalSource), layer).inc(); |
||||||
|
} |
||||||
|
|
||||||
|
public void incrementRemoved( |
||||||
|
final boolean receivedFromLocalSource, final String operation, final String layer) { |
||||||
|
removedCounter.labels(location(receivedFromLocalSource), operation, layer).inc(); |
||||||
|
} |
||||||
|
|
||||||
|
public void incrementRejected( |
||||||
|
final boolean receivedFromLocalSource, |
||||||
|
final TransactionInvalidReason rejectReason, |
||||||
|
final String layer) { |
||||||
|
rejectedCounter.labels(location(receivedFromLocalSource), rejectReason.name(), layer).inc(); |
||||||
|
} |
||||||
|
|
||||||
|
public void incrementExpiredMessages(final String message) { |
||||||
|
expiredMessagesCounter.labels(message).inc(); |
||||||
|
} |
||||||
|
|
||||||
|
public void incrementAlreadySeenTransactions(final String message, final long count) { |
||||||
|
alreadySeenTransactionsCounter.labels(message).inc(count); |
||||||
|
} |
||||||
|
|
||||||
|
private String location(final boolean receivedFromLocalSource) { |
||||||
|
return receivedFromLocalSource ? "local" : "remote"; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,139 @@ |
|||||||
|
/* |
||||||
|
* Copyright Hyperledger Besu Contributors. |
||||||
|
* |
||||||
|
* 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. See the License for the |
||||||
|
* specific language governing permissions and limitations under the License. |
||||||
|
* |
||||||
|
* SPDX-License-Identifier: Apache-2.0 |
||||||
|
*/ |
||||||
|
package org.hyperledger.besu.ethereum.eth.transactions.layered; |
||||||
|
|
||||||
|
import org.hyperledger.besu.datatypes.Address; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.TransactionAddedResult; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolConfiguration; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolMetrics; |
||||||
|
|
||||||
|
import java.util.Map; |
||||||
|
import java.util.NavigableMap; |
||||||
|
import java.util.TreeMap; |
||||||
|
import java.util.TreeSet; |
||||||
|
import java.util.function.BiFunction; |
||||||
|
import java.util.function.Predicate; |
||||||
|
import java.util.stream.Stream; |
||||||
|
|
||||||
|
public abstract class AbstractPrioritizedTransactions extends AbstractSequentialTransactionsLayer { |
||||||
|
protected final TreeSet<PendingTransaction> orderByFee; |
||||||
|
|
||||||
|
public AbstractPrioritizedTransactions( |
||||||
|
final TransactionPoolConfiguration poolConfig, |
||||||
|
final TransactionsLayer prioritizedTransactions, |
||||||
|
final TransactionPoolMetrics metrics, |
||||||
|
final BiFunction<PendingTransaction, PendingTransaction, Boolean> |
||||||
|
transactionReplacementTester) { |
||||||
|
super(poolConfig, prioritizedTransactions, transactionReplacementTester, metrics); |
||||||
|
this.orderByFee = new TreeSet<>(this::compareByFee); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void reset() { |
||||||
|
super.reset(); |
||||||
|
orderByFee.clear(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String name() { |
||||||
|
return "prioritized"; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected TransactionAddedResult canAdd( |
||||||
|
final PendingTransaction pendingTransaction, final int gap) { |
||||||
|
final var senderTxs = txsBySender.get(pendingTransaction.getSender()); |
||||||
|
|
||||||
|
if (hasExpectedNonce(senderTxs, pendingTransaction, gap) && hasPriority(pendingTransaction)) { |
||||||
|
|
||||||
|
return TransactionAddedResult.ADDED; |
||||||
|
} |
||||||
|
|
||||||
|
return TransactionAddedResult.TRY_NEXT_LAYER; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void internalAdd( |
||||||
|
final NavigableMap<Long, PendingTransaction> senderTxs, final PendingTransaction addedTx) { |
||||||
|
orderByFee.add(addedTx); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void internalReplaced(final PendingTransaction replacedTx) { |
||||||
|
orderByFee.remove(replacedTx); |
||||||
|
} |
||||||
|
|
||||||
|
private boolean hasPriority(final PendingTransaction pendingTransaction) { |
||||||
|
if (orderByFee.size() < poolConfig.getMaxPrioritizedTransactions()) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
return compareByFee(pendingTransaction, orderByFee.first()) > 0; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected int maxTransactionsNumber() { |
||||||
|
return poolConfig.getMaxPrioritizedTransactions(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected PendingTransaction getEvictable() { |
||||||
|
return orderByFee.first(); |
||||||
|
} |
||||||
|
|
||||||
|
protected abstract int compareByFee(final PendingTransaction pt1, final PendingTransaction pt2); |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void internalRemove( |
||||||
|
final NavigableMap<Long, PendingTransaction> senderTxs, |
||||||
|
final PendingTransaction removedTx, |
||||||
|
final RemovalReason removalReason) { |
||||||
|
orderByFee.remove(removedTx); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public PendingTransaction promote(final Predicate<PendingTransaction> promotionFilter) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Stream<PendingTransaction> stream() { |
||||||
|
return orderByFee.descendingSet().stream(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected long cacheFreeSpace() { |
||||||
|
return Integer.MAX_VALUE; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void internalConsistencyCheck( |
||||||
|
final Map<Address, TreeMap<Long, PendingTransaction>> prevLayerTxsBySender) { |
||||||
|
super.internalConsistencyCheck(prevLayerTxsBySender); |
||||||
|
|
||||||
|
final var controlOrderByFee = new TreeSet<>(this::compareByFee); |
||||||
|
controlOrderByFee.addAll(pendingTransactions.values()); |
||||||
|
|
||||||
|
final var itControl = controlOrderByFee.iterator(); |
||||||
|
final var itCurrent = orderByFee.iterator(); |
||||||
|
|
||||||
|
while (itControl.hasNext()) { |
||||||
|
assert itControl.next().equals(itCurrent.next()) |
||||||
|
: "orderByFee does not match pendingTransactions"; |
||||||
|
} |
||||||
|
|
||||||
|
assert itCurrent.hasNext() == false : "orderByFee has more elements that pendingTransactions"; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,160 @@ |
|||||||
|
/* |
||||||
|
* Copyright Hyperledger Besu Contributors. |
||||||
|
* |
||||||
|
* 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. See the License for the |
||||||
|
* specific language governing permissions and limitations under the License. |
||||||
|
* |
||||||
|
* SPDX-License-Identifier: Apache-2.0 |
||||||
|
*/ |
||||||
|
package org.hyperledger.besu.ethereum.eth.transactions.layered; |
||||||
|
|
||||||
|
import static org.hyperledger.besu.ethereum.eth.transactions.layered.TransactionsLayer.RemovalReason.EVICTED; |
||||||
|
import static org.hyperledger.besu.ethereum.eth.transactions.layered.TransactionsLayer.RemovalReason.FOLLOW_INVALIDATED; |
||||||
|
|
||||||
|
import org.hyperledger.besu.datatypes.Address; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolConfiguration; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolMetrics; |
||||||
|
|
||||||
|
import java.util.Map; |
||||||
|
import java.util.NavigableMap; |
||||||
|
import java.util.OptionalLong; |
||||||
|
import java.util.Set; |
||||||
|
import java.util.TreeMap; |
||||||
|
import java.util.function.BiFunction; |
||||||
|
|
||||||
|
public abstract class AbstractSequentialTransactionsLayer extends AbstractTransactionsLayer { |
||||||
|
|
||||||
|
public AbstractSequentialTransactionsLayer( |
||||||
|
final TransactionPoolConfiguration poolConfig, |
||||||
|
final TransactionsLayer nextLayer, |
||||||
|
final BiFunction<PendingTransaction, PendingTransaction, Boolean> |
||||||
|
transactionReplacementTester, |
||||||
|
final TransactionPoolMetrics metrics) { |
||||||
|
super(poolConfig, nextLayer, transactionReplacementTester, metrics); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void remove(final PendingTransaction invalidatedTx, final RemovalReason reason) { |
||||||
|
nextLayer.remove(invalidatedTx, reason); |
||||||
|
|
||||||
|
final var senderTxs = txsBySender.get(invalidatedTx.getSender()); |
||||||
|
final long invalidNonce = invalidatedTx.getNonce(); |
||||||
|
if (senderTxs != null && invalidNonce <= senderTxs.lastKey()) { |
||||||
|
// on sequential layers we need to push to next layer all the txs following the invalid one,
|
||||||
|
// even if it belongs to a previous layer
|
||||||
|
|
||||||
|
if (senderTxs.remove(invalidNonce) != null) { |
||||||
|
// invalid tx removed in this layer
|
||||||
|
processRemove(senderTxs, invalidatedTx.getTransaction(), reason); |
||||||
|
} |
||||||
|
|
||||||
|
// push following to next layer
|
||||||
|
pushDown(senderTxs, invalidNonce, 1); |
||||||
|
|
||||||
|
if (senderTxs.isEmpty()) { |
||||||
|
txsBySender.remove(invalidatedTx.getSender()); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void pushDown( |
||||||
|
final NavigableMap<Long, PendingTransaction> senderTxs, |
||||||
|
final long afterNonce, |
||||||
|
final int gap) { |
||||||
|
senderTxs.tailMap(afterNonce, false).values().stream().toList().stream() |
||||||
|
.peek( |
||||||
|
txToRemove -> { |
||||||
|
senderTxs.remove(txToRemove.getNonce()); |
||||||
|
processRemove(senderTxs, txToRemove.getTransaction(), FOLLOW_INVALIDATED); |
||||||
|
}) |
||||||
|
.forEach(followingTx -> nextLayer.add(followingTx, gap)); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected boolean gapsAllowed() { |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void internalConfirmed( |
||||||
|
final NavigableMap<Long, PendingTransaction> senderTxs, |
||||||
|
final Address sender, |
||||||
|
final long maxConfirmedNonce, |
||||||
|
final PendingTransaction highestNonceRemovedTx) { |
||||||
|
// no -op
|
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void internalEvict( |
||||||
|
final NavigableMap<Long, PendingTransaction> senderTxs, final PendingTransaction evictedTx) { |
||||||
|
internalRemove(senderTxs, evictedTx, EVICTED); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public OptionalLong getNextNonceFor(final Address sender) { |
||||||
|
final OptionalLong nextLayerRes = nextLayer.getNextNonceFor(sender); |
||||||
|
if (nextLayerRes.isEmpty()) { |
||||||
|
final var senderTxs = txsBySender.get(sender); |
||||||
|
if (senderTxs != null) { |
||||||
|
return OptionalLong.of(senderTxs.lastKey() + 1); |
||||||
|
} |
||||||
|
} |
||||||
|
return nextLayerRes; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void internalNotifyAdded( |
||||||
|
final NavigableMap<Long, PendingTransaction> senderTxs, |
||||||
|
final PendingTransaction pendingTransaction) { |
||||||
|
// no-op
|
||||||
|
} |
||||||
|
|
||||||
|
protected boolean hasExpectedNonce( |
||||||
|
final NavigableMap<Long, PendingTransaction> senderTxs, |
||||||
|
final PendingTransaction pendingTransaction, |
||||||
|
final long gap) { |
||||||
|
if (senderTxs == null) { |
||||||
|
return gap == 0; |
||||||
|
} |
||||||
|
|
||||||
|
// true if prepend or append
|
||||||
|
return (senderTxs.lastKey() + 1) == pendingTransaction.getNonce() |
||||||
|
|| (senderTxs.firstKey() - 1) == pendingTransaction.getNonce(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void internalConsistencyCheck( |
||||||
|
final Map<Address, TreeMap<Long, PendingTransaction>> prevLayerTxsBySender) { |
||||||
|
txsBySender.values().stream() |
||||||
|
.filter(senderTxs -> senderTxs.size() > 1) |
||||||
|
.map(NavigableMap::entrySet) |
||||||
|
.map(Set::iterator) |
||||||
|
.forEach( |
||||||
|
itNonce -> { |
||||||
|
PendingTransaction firstTx = itNonce.next().getValue(); |
||||||
|
|
||||||
|
prevLayerTxsBySender.computeIfPresent( |
||||||
|
firstTx.getSender(), |
||||||
|
(sender, txsByNonce) -> { |
||||||
|
assert txsByNonce.lastKey() + 1 == firstTx.getNonce() |
||||||
|
: "first nonce is not sequential with previous layer last nonce"; |
||||||
|
return txsByNonce; |
||||||
|
}); |
||||||
|
|
||||||
|
long prevNonce = firstTx.getNonce(); |
||||||
|
|
||||||
|
while (itNonce.hasNext()) { |
||||||
|
final long currNonce = itNonce.next().getKey(); |
||||||
|
assert prevNonce + 1 == currNonce : "non sequential nonce"; |
||||||
|
prevNonce = currNonce; |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,596 @@ |
|||||||
|
/* |
||||||
|
* Copyright Hyperledger Besu Contributors. |
||||||
|
* |
||||||
|
* 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. See the License for the |
||||||
|
* specific language governing permissions and limitations under the License. |
||||||
|
* |
||||||
|
* SPDX-License-Identifier: Apache-2.0 |
||||||
|
*/ |
||||||
|
package org.hyperledger.besu.ethereum.eth.transactions.layered; |
||||||
|
|
||||||
|
import static org.hyperledger.besu.ethereum.eth.transactions.TransactionAddedResult.ADDED; |
||||||
|
import static org.hyperledger.besu.ethereum.eth.transactions.TransactionAddedResult.ALREADY_KNOWN; |
||||||
|
import static org.hyperledger.besu.ethereum.eth.transactions.TransactionAddedResult.REJECTED_UNDERPRICED_REPLACEMENT; |
||||||
|
import static org.hyperledger.besu.ethereum.eth.transactions.TransactionAddedResult.REORG_SENDER; |
||||||
|
import static org.hyperledger.besu.ethereum.eth.transactions.TransactionAddedResult.TRY_NEXT_LAYER; |
||||||
|
import static org.hyperledger.besu.ethereum.eth.transactions.layered.TransactionsLayer.RemovalReason.CONFIRMED; |
||||||
|
import static org.hyperledger.besu.ethereum.eth.transactions.layered.TransactionsLayer.RemovalReason.CROSS_LAYER_REPLACED; |
||||||
|
import static org.hyperledger.besu.ethereum.eth.transactions.layered.TransactionsLayer.RemovalReason.EVICTED; |
||||||
|
import static org.hyperledger.besu.ethereum.eth.transactions.layered.TransactionsLayer.RemovalReason.PROMOTED; |
||||||
|
import static org.hyperledger.besu.ethereum.eth.transactions.layered.TransactionsLayer.RemovalReason.REPLACED; |
||||||
|
|
||||||
|
import org.hyperledger.besu.datatypes.Address; |
||||||
|
import org.hyperledger.besu.datatypes.Hash; |
||||||
|
import org.hyperledger.besu.ethereum.core.BlockHeader; |
||||||
|
import org.hyperledger.besu.ethereum.core.Transaction; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactionAddedListener; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactionDroppedListener; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.TransactionAddedResult; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolConfiguration; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolMetrics; |
||||||
|
import org.hyperledger.besu.ethereum.mainnet.feemarket.FeeMarket; |
||||||
|
import org.hyperledger.besu.util.Subscribers; |
||||||
|
|
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.HashMap; |
||||||
|
import java.util.List; |
||||||
|
import java.util.Map; |
||||||
|
import java.util.NavigableMap; |
||||||
|
import java.util.Objects; |
||||||
|
import java.util.Optional; |
||||||
|
import java.util.OptionalLong; |
||||||
|
import java.util.TreeMap; |
||||||
|
import java.util.function.BiFunction; |
||||||
|
import java.util.function.BinaryOperator; |
||||||
|
import java.util.function.Function; |
||||||
|
import java.util.stream.Collectors; |
||||||
|
import java.util.stream.Stream; |
||||||
|
|
||||||
|
import org.slf4j.Logger; |
||||||
|
import org.slf4j.LoggerFactory; |
||||||
|
|
||||||
|
public abstract class AbstractTransactionsLayer implements TransactionsLayer { |
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(AbstractTransactionsLayer.class); |
||||||
|
private static final NavigableMap<Long, PendingTransaction> EMPTY_SENDER_TXS = new TreeMap<>(); |
||||||
|
protected final TransactionPoolConfiguration poolConfig; |
||||||
|
protected final TransactionsLayer nextLayer; |
||||||
|
protected final BiFunction<PendingTransaction, PendingTransaction, Boolean> |
||||||
|
transactionReplacementTester; |
||||||
|
protected final TransactionPoolMetrics metrics; |
||||||
|
protected final Map<Hash, PendingTransaction> pendingTransactions = new HashMap<>(); |
||||||
|
protected final Map<Address, NavigableMap<Long, PendingTransaction>> txsBySender = |
||||||
|
new HashMap<>(); |
||||||
|
private final Subscribers<PendingTransactionAddedListener> onAddedListeners = |
||||||
|
Subscribers.create(); |
||||||
|
private final Subscribers<PendingTransactionDroppedListener> onDroppedListeners = |
||||||
|
Subscribers.create(); |
||||||
|
private OptionalLong nextLayerOnAddedListenerId = OptionalLong.empty(); |
||||||
|
private OptionalLong nextLayerOnDroppedListenerId = OptionalLong.empty(); |
||||||
|
protected long spaceUsed = 0; |
||||||
|
|
||||||
|
public AbstractTransactionsLayer( |
||||||
|
final TransactionPoolConfiguration poolConfig, |
||||||
|
final TransactionsLayer nextLayer, |
||||||
|
final BiFunction<PendingTransaction, PendingTransaction, Boolean> |
||||||
|
transactionReplacementTester, |
||||||
|
final TransactionPoolMetrics metrics) { |
||||||
|
this.poolConfig = poolConfig; |
||||||
|
this.nextLayer = nextLayer; |
||||||
|
this.transactionReplacementTester = transactionReplacementTester; |
||||||
|
this.metrics = metrics; |
||||||
|
metrics.initSpaceUsed(this::getLayerSpaceUsed, name()); |
||||||
|
metrics.initTransactionCount(pendingTransactions::size, name()); |
||||||
|
metrics.initUniqueSenderCount(txsBySender::size, name()); |
||||||
|
} |
||||||
|
|
||||||
|
protected abstract boolean gapsAllowed(); |
||||||
|
|
||||||
|
@Override |
||||||
|
public void reset() { |
||||||
|
pendingTransactions.clear(); |
||||||
|
txsBySender.clear(); |
||||||
|
spaceUsed = 0; |
||||||
|
nextLayer.reset(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Optional<Transaction> getByHash(final Hash transactionHash) { |
||||||
|
final var currLayerTx = pendingTransactions.get(transactionHash); |
||||||
|
if (currLayerTx == null) { |
||||||
|
return nextLayer.getByHash(transactionHash); |
||||||
|
} |
||||||
|
return Optional.of(currLayerTx.getTransaction()); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean contains(final Transaction transaction) { |
||||||
|
return pendingTransactions.containsKey(transaction.getHash()) |
||||||
|
|| nextLayer.contains(transaction); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public List<PendingTransaction> getAll() { |
||||||
|
final List<PendingTransaction> allNextLayers = nextLayer.getAll(); |
||||||
|
final List<PendingTransaction> allTxs = |
||||||
|
new ArrayList<>(pendingTransactions.size() + allNextLayers.size()); |
||||||
|
allTxs.addAll(pendingTransactions.values()); |
||||||
|
allTxs.addAll(allNextLayers); |
||||||
|
return allTxs; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public long getCumulativeUsedSpace() { |
||||||
|
return getLayerSpaceUsed() + nextLayer.getCumulativeUsedSpace(); |
||||||
|
} |
||||||
|
|
||||||
|
protected long getLayerSpaceUsed() { |
||||||
|
return spaceUsed; |
||||||
|
} |
||||||
|
|
||||||
|
protected abstract TransactionAddedResult canAdd( |
||||||
|
final PendingTransaction pendingTransaction, final int gap); |
||||||
|
|
||||||
|
@Override |
||||||
|
public TransactionAddedResult add(final PendingTransaction pendingTransaction, final int gap) { |
||||||
|
|
||||||
|
// is replacing an existing one?
|
||||||
|
TransactionAddedResult addStatus = maybeReplaceTransaction(pendingTransaction); |
||||||
|
if (addStatus == null) { |
||||||
|
addStatus = canAdd(pendingTransaction, gap); |
||||||
|
} |
||||||
|
|
||||||
|
if (addStatus.equals(TRY_NEXT_LAYER)) { |
||||||
|
return addToNextLayer(pendingTransaction, gap); |
||||||
|
} |
||||||
|
|
||||||
|
if (addStatus.isSuccess()) { |
||||||
|
processAdded(pendingTransaction); |
||||||
|
addStatus.maybeReplacedTransaction().ifPresent(this::replaced); |
||||||
|
|
||||||
|
nextLayer.notifyAdded(pendingTransaction); |
||||||
|
|
||||||
|
if (!maybeFull()) { |
||||||
|
// if there is space try to see if the added tx filled some gaps
|
||||||
|
tryFillGap(addStatus, pendingTransaction); |
||||||
|
} |
||||||
|
|
||||||
|
notifyTransactionAdded(pendingTransaction); |
||||||
|
} else { |
||||||
|
final var rejectReason = addStatus.maybeInvalidReason().orElseThrow(); |
||||||
|
metrics.incrementRejected(false, rejectReason, name()); |
||||||
|
LOG.atTrace() |
||||||
|
.setMessage("Transaction {} rejected reason {}") |
||||||
|
.addArgument(pendingTransaction::toTraceLog) |
||||||
|
.addArgument(rejectReason) |
||||||
|
.log(); |
||||||
|
} |
||||||
|
|
||||||
|
return addStatus; |
||||||
|
} |
||||||
|
|
||||||
|
private boolean maybeFull() { |
||||||
|
final long cacheFreeSpace = cacheFreeSpace(); |
||||||
|
final int overflowTxsCount = pendingTransactions.size() - maxTransactionsNumber(); |
||||||
|
if (cacheFreeSpace < 0 || overflowTxsCount > 0) { |
||||||
|
LOG.atDebug() |
||||||
|
.setMessage("Layer full: {}") |
||||||
|
.addArgument( |
||||||
|
() -> |
||||||
|
cacheFreeSpace < 0 |
||||||
|
? "need to free " + (-cacheFreeSpace) + " space" |
||||||
|
: "need to evict " + overflowTxsCount + " transaction(s)") |
||||||
|
.log(); |
||||||
|
|
||||||
|
evict(-cacheFreeSpace, overflowTxsCount); |
||||||
|
return true; |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
private void tryFillGap( |
||||||
|
final TransactionAddedResult addStatus, final PendingTransaction pendingTransaction) { |
||||||
|
// it makes sense to fill gaps only if the add is not a replacement and this layer does not
|
||||||
|
// allow gaps
|
||||||
|
if (!addStatus.isReplacement() && !gapsAllowed()) { |
||||||
|
final PendingTransaction promotedTx = |
||||||
|
nextLayer.promoteFor(pendingTransaction.getSender(), pendingTransaction.getNonce()); |
||||||
|
if (promotedTx != null) { |
||||||
|
processAdded(promotedTx); |
||||||
|
if (!maybeFull()) { |
||||||
|
tryFillGap(ADDED, promotedTx); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void notifyAdded(final PendingTransaction pendingTransaction) { |
||||||
|
final Address sender = pendingTransaction.getSender(); |
||||||
|
final var senderTxs = txsBySender.get(sender); |
||||||
|
if (senderTxs != null) { |
||||||
|
if (senderTxs.firstKey() < pendingTransaction.getNonce()) { |
||||||
|
// in the case the world state has been updated but the confirmed txs have not yet been
|
||||||
|
// processed
|
||||||
|
confirmed(sender, pendingTransaction.getNonce()); |
||||||
|
} else if (senderTxs.firstKey() == pendingTransaction.getNonce()) { |
||||||
|
// it is a cross layer replacement, namely added to a previous layer
|
||||||
|
final PendingTransaction replacedTx = senderTxs.pollFirstEntry().getValue(); |
||||||
|
processRemove(senderTxs, replacedTx.getTransaction(), CROSS_LAYER_REPLACED); |
||||||
|
|
||||||
|
if (senderTxs.isEmpty()) { |
||||||
|
txsBySender.remove(sender); |
||||||
|
} |
||||||
|
} else { |
||||||
|
internalNotifyAdded(senderTxs, pendingTransaction); |
||||||
|
} |
||||||
|
} |
||||||
|
nextLayer.notifyAdded(pendingTransaction); |
||||||
|
} |
||||||
|
|
||||||
|
protected abstract void internalNotifyAdded( |
||||||
|
final NavigableMap<Long, PendingTransaction> senderTxs, |
||||||
|
final PendingTransaction pendingTransaction); |
||||||
|
|
||||||
|
@Override |
||||||
|
public PendingTransaction promoteFor(final Address sender, final long nonce) { |
||||||
|
final var senderTxs = txsBySender.get(sender); |
||||||
|
if (senderTxs != null) { |
||||||
|
long expectedNonce = nonce + 1; |
||||||
|
if (senderTxs.firstKey() == expectedNonce) { |
||||||
|
final PendingTransaction promotedTx = senderTxs.pollFirstEntry().getValue(); |
||||||
|
processRemove(senderTxs, promotedTx.getTransaction(), PROMOTED); |
||||||
|
metrics.incrementRemoved(promotedTx.isReceivedFromLocalSource(), "promoted", name()); |
||||||
|
|
||||||
|
if (senderTxs.isEmpty()) { |
||||||
|
txsBySender.remove(sender); |
||||||
|
} |
||||||
|
return promotedTx; |
||||||
|
} |
||||||
|
} |
||||||
|
return nextLayer.promoteFor(sender, nonce); |
||||||
|
} |
||||||
|
|
||||||
|
private TransactionAddedResult addToNextLayer( |
||||||
|
final PendingTransaction pendingTransaction, final int distance) { |
||||||
|
return addToNextLayer( |
||||||
|
txsBySender.getOrDefault(pendingTransaction.getSender(), EMPTY_SENDER_TXS), |
||||||
|
pendingTransaction, |
||||||
|
distance); |
||||||
|
} |
||||||
|
|
||||||
|
private TransactionAddedResult addToNextLayer( |
||||||
|
final NavigableMap<Long, PendingTransaction> senderTxs, |
||||||
|
final PendingTransaction pendingTransaction, |
||||||
|
final int distance) { |
||||||
|
final int nextLayerDistance; |
||||||
|
if (senderTxs.isEmpty()) { |
||||||
|
nextLayerDistance = distance; |
||||||
|
} else { |
||||||
|
nextLayerDistance = (int) (pendingTransaction.getNonce() - (senderTxs.lastKey() + 1)); |
||||||
|
if (nextLayerDistance < 0) { |
||||||
|
return REORG_SENDER; |
||||||
|
} |
||||||
|
} |
||||||
|
return nextLayer.add(pendingTransaction, nextLayerDistance); |
||||||
|
} |
||||||
|
|
||||||
|
private void processAdded(final PendingTransaction addedTx) { |
||||||
|
pendingTransactions.put(addedTx.getHash(), addedTx); |
||||||
|
final var senderTxs = txsBySender.computeIfAbsent(addedTx.getSender(), s -> new TreeMap<>()); |
||||||
|
senderTxs.put(addedTx.getNonce(), addedTx); |
||||||
|
increaseSpaceUsed(addedTx); |
||||||
|
metrics.incrementAdded(addedTx.isReceivedFromLocalSource(), name()); |
||||||
|
internalAdd(senderTxs, addedTx); |
||||||
|
} |
||||||
|
|
||||||
|
protected abstract void internalAdd( |
||||||
|
final NavigableMap<Long, PendingTransaction> senderTxs, final PendingTransaction addedTx); |
||||||
|
|
||||||
|
protected abstract int maxTransactionsNumber(); |
||||||
|
|
||||||
|
private void evict(final long spaceToFree, final int txsToEvict) { |
||||||
|
final var evictableTx = getEvictable(); |
||||||
|
if (evictableTx != null) { |
||||||
|
final var lessReadySender = evictableTx.getSender(); |
||||||
|
final var lessReadySenderTxs = txsBySender.get(lessReadySender); |
||||||
|
|
||||||
|
long evictedSize = 0; |
||||||
|
int evictedCount = 0; |
||||||
|
PendingTransaction lastTx; |
||||||
|
// lastTx must never be null, because the sender have at least the lessReadyTx
|
||||||
|
while ((evictedSize < spaceToFree || txsToEvict > evictedCount) |
||||||
|
&& !lessReadySenderTxs.isEmpty()) { |
||||||
|
lastTx = lessReadySenderTxs.pollLastEntry().getValue(); |
||||||
|
processEvict(lessReadySenderTxs, lastTx); |
||||||
|
++evictedCount; |
||||||
|
evictedSize += lastTx.memorySize(); |
||||||
|
// evicted can always be added to the next layer
|
||||||
|
addToNextLayer(lessReadySenderTxs, lastTx, 0); |
||||||
|
} |
||||||
|
|
||||||
|
if (lessReadySenderTxs.isEmpty()) { |
||||||
|
txsBySender.remove(lessReadySender); |
||||||
|
} |
||||||
|
|
||||||
|
final long newSpaceToFree = spaceToFree - evictedSize; |
||||||
|
final int newTxsToEvict = txsToEvict - evictedCount; |
||||||
|
|
||||||
|
if ((newSpaceToFree > 0 || newTxsToEvict > 0) && !txsBySender.isEmpty()) { |
||||||
|
// try next less valuable sender
|
||||||
|
evict(newSpaceToFree, newTxsToEvict); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
protected void replaced(final PendingTransaction replacedTx) { |
||||||
|
pendingTransactions.remove(replacedTx.getHash()); |
||||||
|
decreaseSpaceUsed(replacedTx); |
||||||
|
metrics.incrementRemoved(replacedTx.isReceivedFromLocalSource(), REPLACED.label(), name()); |
||||||
|
internalReplaced(replacedTx); |
||||||
|
} |
||||||
|
|
||||||
|
protected abstract void internalReplaced(final PendingTransaction replacedTx); |
||||||
|
|
||||||
|
private TransactionAddedResult maybeReplaceTransaction(final PendingTransaction incomingTx) { |
||||||
|
|
||||||
|
final var existingTxs = txsBySender.get(incomingTx.getSender()); |
||||||
|
|
||||||
|
if (existingTxs != null) { |
||||||
|
final var existingReadyTx = existingTxs.get(incomingTx.getNonce()); |
||||||
|
if (existingReadyTx != null) { |
||||||
|
|
||||||
|
if (existingReadyTx.getHash().equals(incomingTx.getHash())) { |
||||||
|
return ALREADY_KNOWN; |
||||||
|
} |
||||||
|
|
||||||
|
if (!transactionReplacementTester.apply(existingReadyTx, incomingTx)) { |
||||||
|
return REJECTED_UNDERPRICED_REPLACEMENT; |
||||||
|
} |
||||||
|
return TransactionAddedResult.createForReplacement(existingReadyTx); |
||||||
|
} |
||||||
|
} |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
protected PendingTransaction processRemove( |
||||||
|
final NavigableMap<Long, PendingTransaction> senderTxs, |
||||||
|
final Transaction transaction, |
||||||
|
final RemovalReason removalReason) { |
||||||
|
final PendingTransaction removedTx = pendingTransactions.remove(transaction.getHash()); |
||||||
|
if (removedTx != null) { |
||||||
|
decreaseSpaceUsed(removedTx); |
||||||
|
metrics.incrementRemoved( |
||||||
|
removedTx.isReceivedFromLocalSource(), removalReason.label(), name()); |
||||||
|
internalRemove(senderTxs, removedTx, removalReason); |
||||||
|
} |
||||||
|
return removedTx; |
||||||
|
} |
||||||
|
|
||||||
|
protected PendingTransaction processEvict( |
||||||
|
final NavigableMap<Long, PendingTransaction> senderTxs, final PendingTransaction evictedTx) { |
||||||
|
final PendingTransaction removedTx = pendingTransactions.remove(evictedTx.getHash()); |
||||||
|
if (removedTx != null) { |
||||||
|
decreaseSpaceUsed(evictedTx); |
||||||
|
metrics.incrementRemoved(evictedTx.isReceivedFromLocalSource(), EVICTED.label(), name()); |
||||||
|
internalEvict(senderTxs, removedTx); |
||||||
|
} |
||||||
|
return removedTx; |
||||||
|
} |
||||||
|
|
||||||
|
protected abstract void internalEvict( |
||||||
|
final NavigableMap<Long, PendingTransaction> lessReadySenderTxs, |
||||||
|
final PendingTransaction evictedTx); |
||||||
|
|
||||||
|
@Override |
||||||
|
public final void blockAdded( |
||||||
|
final FeeMarket feeMarket, |
||||||
|
final BlockHeader blockHeader, |
||||||
|
final Map<Address, Long> maxConfirmedNonceBySender) { |
||||||
|
LOG.atDebug() |
||||||
|
.setMessage("Managing new added block {}") |
||||||
|
.addArgument(blockHeader::toLogString) |
||||||
|
.log(); |
||||||
|
|
||||||
|
nextLayer.blockAdded(feeMarket, blockHeader, maxConfirmedNonceBySender); |
||||||
|
maxConfirmedNonceBySender.forEach(this::confirmed); |
||||||
|
internalBlockAdded(blockHeader, feeMarket); |
||||||
|
} |
||||||
|
|
||||||
|
protected abstract void internalBlockAdded( |
||||||
|
final BlockHeader blockHeader, final FeeMarket feeMarket); |
||||||
|
|
||||||
|
final void promoteTransactions() { |
||||||
|
int freeSlots = maxTransactionsNumber() - pendingTransactions.size(); |
||||||
|
|
||||||
|
while (cacheFreeSpace() > 0 && freeSlots > 0) { |
||||||
|
final var promotedTx = nextLayer.promote(this::promotionFilter); |
||||||
|
if (promotedTx != null) { |
||||||
|
processAdded(promotedTx); |
||||||
|
--freeSlots; |
||||||
|
} else { |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void confirmed(final Address sender, final long maxConfirmedNonce) { |
||||||
|
final var senderTxs = txsBySender.get(sender); |
||||||
|
|
||||||
|
if (senderTxs != null) { |
||||||
|
final var confirmedTxs = senderTxs.headMap(maxConfirmedNonce, true); |
||||||
|
final var highestNonceRemovedTx = |
||||||
|
confirmedTxs.isEmpty() ? null : confirmedTxs.lastEntry().getValue(); |
||||||
|
|
||||||
|
final var itConfirmedTxs = confirmedTxs.values().iterator(); |
||||||
|
while (itConfirmedTxs.hasNext()) { |
||||||
|
final var confirmedTx = itConfirmedTxs.next(); |
||||||
|
itConfirmedTxs.remove(); |
||||||
|
processRemove(senderTxs, confirmedTx.getTransaction(), CONFIRMED); |
||||||
|
|
||||||
|
metrics.incrementRemoved(confirmedTx.isReceivedFromLocalSource(), "confirmed", name()); |
||||||
|
LOG.atTrace() |
||||||
|
.setMessage("Removed confirmed pending transactions {}") |
||||||
|
.addArgument(confirmedTx::toTraceLog) |
||||||
|
.log(); |
||||||
|
} |
||||||
|
|
||||||
|
if (senderTxs.isEmpty()) { |
||||||
|
txsBySender.remove(sender); |
||||||
|
} else { |
||||||
|
internalConfirmed(senderTxs, sender, maxConfirmedNonce, highestNonceRemovedTx); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
promoteTransactions(); |
||||||
|
} |
||||||
|
|
||||||
|
protected abstract void internalConfirmed( |
||||||
|
final NavigableMap<Long, PendingTransaction> senderTxs, |
||||||
|
final Address sender, |
||||||
|
final long maxConfirmedNonce, |
||||||
|
final PendingTransaction highestNonceRemovedTx); |
||||||
|
|
||||||
|
protected abstract void internalRemove( |
||||||
|
final NavigableMap<Long, PendingTransaction> senderTxs, |
||||||
|
final PendingTransaction pendingTransaction, |
||||||
|
final RemovalReason removalReason); |
||||||
|
|
||||||
|
protected abstract PendingTransaction getEvictable(); |
||||||
|
|
||||||
|
protected void increaseSpaceUsed(final PendingTransaction pendingTransaction) { |
||||||
|
spaceUsed += pendingTransaction.memorySize(); |
||||||
|
} |
||||||
|
|
||||||
|
protected void decreaseSpaceUsed(final PendingTransaction pendingTransaction) { |
||||||
|
spaceUsed -= pendingTransaction.memorySize(); |
||||||
|
} |
||||||
|
|
||||||
|
protected abstract long cacheFreeSpace(); |
||||||
|
|
||||||
|
protected abstract boolean promotionFilter(PendingTransaction pendingTransaction); |
||||||
|
|
||||||
|
@Override |
||||||
|
public List<Transaction> getAllLocal() { |
||||||
|
final var localTxs = |
||||||
|
pendingTransactions.values().stream() |
||||||
|
.filter(PendingTransaction::isReceivedFromLocalSource) |
||||||
|
.map(PendingTransaction::getTransaction) |
||||||
|
.collect(Collectors.toCollection(ArrayList::new)); |
||||||
|
localTxs.addAll(nextLayer.getAllLocal()); |
||||||
|
return localTxs; |
||||||
|
} |
||||||
|
|
||||||
|
Stream<PendingTransaction> stream(final Address sender) { |
||||||
|
return txsBySender.getOrDefault(sender, EMPTY_SENDER_TXS).values().stream(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public List<PendingTransaction> getAllFor(final Address sender) { |
||||||
|
return Stream.concat(stream(sender), nextLayer.getAllFor(sender).stream()).toList(); |
||||||
|
} |
||||||
|
|
||||||
|
abstract Stream<PendingTransaction> stream(); |
||||||
|
|
||||||
|
@Override |
||||||
|
public int count() { |
||||||
|
return pendingTransactions.size() + nextLayer.count(); |
||||||
|
} |
||||||
|
|
||||||
|
protected void notifyTransactionAdded(final PendingTransaction pendingTransaction) { |
||||||
|
onAddedListeners.forEach( |
||||||
|
listener -> listener.onTransactionAdded(pendingTransaction.getTransaction())); |
||||||
|
} |
||||||
|
|
||||||
|
protected void notifyTransactionDropped(final PendingTransaction pendingTransaction) { |
||||||
|
onDroppedListeners.forEach( |
||||||
|
listener -> listener.onTransactionDropped(pendingTransaction.getTransaction())); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public long subscribeToAdded(final PendingTransactionAddedListener listener) { |
||||||
|
nextLayerOnAddedListenerId = OptionalLong.of(nextLayer.subscribeToAdded(listener)); |
||||||
|
return onAddedListeners.subscribe(listener); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void unsubscribeFromAdded(final long id) { |
||||||
|
nextLayerOnAddedListenerId.ifPresent(nextLayer::unsubscribeFromAdded); |
||||||
|
onAddedListeners.unsubscribe(id); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public long subscribeToDropped(final PendingTransactionDroppedListener listener) { |
||||||
|
nextLayerOnDroppedListenerId = OptionalLong.of(nextLayer.subscribeToDropped(listener)); |
||||||
|
return onDroppedListeners.subscribe(listener); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void unsubscribeFromDropped(final long id) { |
||||||
|
nextLayerOnDroppedListenerId.ifPresent(nextLayer::unsubscribeFromDropped); |
||||||
|
onDroppedListeners.unsubscribe(id); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String logStats() { |
||||||
|
return internalLogStats() + " | " + nextLayer.logStats(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String logSender(final Address sender) { |
||||||
|
final var senderTxs = txsBySender.get(sender); |
||||||
|
return name() |
||||||
|
+ "[" |
||||||
|
+ (Objects.isNull(senderTxs) ? "Empty" : senderTxs.keySet()) |
||||||
|
+ "] " |
||||||
|
+ nextLayer.logSender(sender); |
||||||
|
} |
||||||
|
|
||||||
|
protected abstract String internalLogStats(); |
||||||
|
|
||||||
|
boolean consistencyCheck( |
||||||
|
final Map<Address, TreeMap<Long, PendingTransaction>> prevLayerTxsBySender) { |
||||||
|
final BinaryOperator<PendingTransaction> noMergeExpected = |
||||||
|
(a, b) -> { |
||||||
|
throw new IllegalArgumentException(); |
||||||
|
}; |
||||||
|
final var controlTxsBySender = |
||||||
|
pendingTransactions.values().stream() |
||||||
|
.collect( |
||||||
|
Collectors.groupingBy( |
||||||
|
PendingTransaction::getSender, |
||||||
|
Collectors.toMap( |
||||||
|
PendingTransaction::getNonce, |
||||||
|
Function.identity(), |
||||||
|
noMergeExpected, |
||||||
|
TreeMap::new))); |
||||||
|
|
||||||
|
assert txsBySender.equals(controlTxsBySender) |
||||||
|
: "pendingTransactions and txsBySender do not contain the same txs"; |
||||||
|
|
||||||
|
assert pendingTransactions.values().stream().mapToInt(PendingTransaction::memorySize).sum() |
||||||
|
== spaceUsed |
||||||
|
: "space used does not match"; |
||||||
|
|
||||||
|
internalConsistencyCheck(prevLayerTxsBySender); |
||||||
|
|
||||||
|
if (nextLayer instanceof AbstractTransactionsLayer) { |
||||||
|
txsBySender.forEach( |
||||||
|
(sender, txsByNonce) -> |
||||||
|
prevLayerTxsBySender |
||||||
|
.computeIfAbsent(sender, s -> new TreeMap<>()) |
||||||
|
.putAll(txsByNonce)); |
||||||
|
return ((AbstractTransactionsLayer) nextLayer).consistencyCheck(prevLayerTxsBySender); |
||||||
|
} |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
protected abstract void internalConsistencyCheck( |
||||||
|
final Map<Address, TreeMap<Long, PendingTransaction>> prevLayerTxsBySender); |
||||||
|
} |
@ -0,0 +1,152 @@ |
|||||||
|
/* |
||||||
|
* Copyright Hyperledger Besu Contributors. |
||||||
|
* |
||||||
|
* 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. See the License for the |
||||||
|
* specific language governing permissions and limitations under the License. |
||||||
|
* |
||||||
|
* SPDX-License-Identifier: Apache-2.0 |
||||||
|
*/ |
||||||
|
package org.hyperledger.besu.ethereum.eth.transactions.layered; |
||||||
|
|
||||||
|
import org.hyperledger.besu.datatypes.Wei; |
||||||
|
import org.hyperledger.besu.ethereum.core.BlockHeader; |
||||||
|
import org.hyperledger.besu.ethereum.core.Transaction; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolConfiguration; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolMetrics; |
||||||
|
import org.hyperledger.besu.ethereum.mainnet.feemarket.BaseFeeMarket; |
||||||
|
import org.hyperledger.besu.ethereum.mainnet.feemarket.FeeMarket; |
||||||
|
|
||||||
|
import java.util.Comparator; |
||||||
|
import java.util.Optional; |
||||||
|
import java.util.function.BiFunction; |
||||||
|
import java.util.function.Supplier; |
||||||
|
import java.util.stream.Collectors; |
||||||
|
|
||||||
|
import org.slf4j.Logger; |
||||||
|
import org.slf4j.LoggerFactory; |
||||||
|
|
||||||
|
/** |
||||||
|
* Holds the current set of pending transactions with the ability to iterate them based on priority |
||||||
|
* for mining or look-up by hash. |
||||||
|
* |
||||||
|
* <p>This class is safe for use across multiple threads. |
||||||
|
*/ |
||||||
|
public class BaseFeePrioritizedTransactions extends AbstractPrioritizedTransactions { |
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(BaseFeePrioritizedTransactions.class); |
||||||
|
private Optional<Wei> nextBlockBaseFee; |
||||||
|
|
||||||
|
public BaseFeePrioritizedTransactions( |
||||||
|
final TransactionPoolConfiguration poolConfig, |
||||||
|
final Supplier<BlockHeader> chainHeadHeaderSupplier, |
||||||
|
final TransactionsLayer nextLayer, |
||||||
|
final TransactionPoolMetrics metrics, |
||||||
|
final BiFunction<PendingTransaction, PendingTransaction, Boolean> |
||||||
|
transactionReplacementTester, |
||||||
|
final BaseFeeMarket baseFeeMarket) { |
||||||
|
super(poolConfig, nextLayer, metrics, transactionReplacementTester); |
||||||
|
this.nextBlockBaseFee = |
||||||
|
Optional.of(calculateNextBlockBaseFee(baseFeeMarket, chainHeadHeaderSupplier.get())); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected int compareByFee(final PendingTransaction pt1, final PendingTransaction pt2) { |
||||||
|
return Comparator.comparing( |
||||||
|
(PendingTransaction pendingTransaction) -> |
||||||
|
pendingTransaction.getTransaction().getEffectivePriorityFeePerGas(nextBlockBaseFee)) |
||||||
|
.thenComparing( |
||||||
|
(PendingTransaction pendingTransaction) -> |
||||||
|
pendingTransaction.getTransaction().getMaxGasPrice()) |
||||||
|
.thenComparing(Comparator.comparing(PendingTransaction::getNonce).reversed()) |
||||||
|
.thenComparing(PendingTransaction::getSequence) |
||||||
|
.compare(pt1, pt2); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void internalBlockAdded(final BlockHeader blockHeader, final FeeMarket feeMarket) { |
||||||
|
final BaseFeeMarket baseFeeMarket = (BaseFeeMarket) feeMarket; |
||||||
|
final Wei newNextBlockBaseFee = calculateNextBlockBaseFee(baseFeeMarket, blockHeader); |
||||||
|
|
||||||
|
LOG.atTrace() |
||||||
|
.setMessage("Updating base fee from {} to {}") |
||||||
|
.addArgument(nextBlockBaseFee.get()::toHumanReadableString) |
||||||
|
.addArgument(newNextBlockBaseFee::toHumanReadableString) |
||||||
|
.log(); |
||||||
|
|
||||||
|
nextBlockBaseFee = Optional.of(newNextBlockBaseFee); |
||||||
|
orderByFee.clear(); |
||||||
|
orderByFee.addAll(pendingTransactions.values()); |
||||||
|
} |
||||||
|
|
||||||
|
private Wei calculateNextBlockBaseFee( |
||||||
|
final BaseFeeMarket baseFeeMarket, final BlockHeader blockHeader) { |
||||||
|
return baseFeeMarket.computeBaseFee( |
||||||
|
blockHeader.getNumber() + 1, |
||||||
|
blockHeader.getBaseFee().orElse(Wei.ZERO), |
||||||
|
blockHeader.getGasUsed(), |
||||||
|
baseFeeMarket.targetGasUsed(blockHeader)); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected boolean promotionFilter(final PendingTransaction pendingTransaction) { |
||||||
|
return nextBlockBaseFee |
||||||
|
.map( |
||||||
|
baseFee -> |
||||||
|
pendingTransaction |
||||||
|
.getTransaction() |
||||||
|
.getEffectiveGasPrice(nextBlockBaseFee) |
||||||
|
.greaterOrEqualThan(baseFee)) |
||||||
|
.orElse(false); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected String internalLogStats() { |
||||||
|
|
||||||
|
if (orderByFee.isEmpty()) { |
||||||
|
return "Basefee Prioritized: Empty"; |
||||||
|
} |
||||||
|
|
||||||
|
final var baseFeePartition = |
||||||
|
stream() |
||||||
|
.map(PendingTransaction::getTransaction) |
||||||
|
.collect( |
||||||
|
Collectors.partitioningBy( |
||||||
|
tx -> tx.getMaxGasPrice().greaterOrEqualThan(nextBlockBaseFee.get()), |
||||||
|
Collectors.counting())); |
||||||
|
final Transaction highest = orderByFee.last().getTransaction(); |
||||||
|
final Transaction lowest = orderByFee.first().getTransaction(); |
||||||
|
|
||||||
|
return "Basefee Prioritized: " |
||||||
|
+ "count: " |
||||||
|
+ pendingTransactions.size() |
||||||
|
+ ", space used: " |
||||||
|
+ spaceUsed |
||||||
|
+ ", unique senders: " |
||||||
|
+ txsBySender.size() |
||||||
|
+ ", highest priority tx: [max fee: " |
||||||
|
+ highest.getMaxGasPrice().toHumanReadableString() |
||||||
|
+ ", curr prio fee: " |
||||||
|
+ highest.getEffectivePriorityFeePerGas(nextBlockBaseFee).toHumanReadableString() |
||||||
|
+ ", hash: " |
||||||
|
+ highest.getHash() |
||||||
|
+ "], lowest priority tx: [max fee: " |
||||||
|
+ lowest.getMaxGasPrice().toHumanReadableString() |
||||||
|
+ ", curr prio fee: " |
||||||
|
+ lowest.getEffectivePriorityFeePerGas(nextBlockBaseFee).toHumanReadableString() |
||||||
|
+ ", hash: " |
||||||
|
+ lowest.getHash() |
||||||
|
+ "], next block base fee: " |
||||||
|
+ nextBlockBaseFee.get().toHumanReadableString() |
||||||
|
+ ", above next base fee: " |
||||||
|
+ baseFeePartition.get(true) |
||||||
|
+ ", below next base fee: " |
||||||
|
+ baseFeePartition.get(false); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,171 @@ |
|||||||
|
/* |
||||||
|
* Copyright Hyperledger Besu Contributors. |
||||||
|
* |
||||||
|
* 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. See the License for the |
||||||
|
* specific language governing permissions and limitations under the License. |
||||||
|
* |
||||||
|
* SPDX-License-Identifier: Apache-2.0 |
||||||
|
*/ |
||||||
|
package org.hyperledger.besu.ethereum.eth.transactions.layered; |
||||||
|
|
||||||
|
import static org.hyperledger.besu.ethereum.eth.transactions.layered.TransactionsLayer.RemovalReason.DROPPED; |
||||||
|
|
||||||
|
import org.hyperledger.besu.datatypes.Address; |
||||||
|
import org.hyperledger.besu.datatypes.Hash; |
||||||
|
import org.hyperledger.besu.ethereum.core.BlockHeader; |
||||||
|
import org.hyperledger.besu.ethereum.core.Transaction; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactionAddedListener; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactionDroppedListener; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.TransactionAddedResult; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolMetrics; |
||||||
|
import org.hyperledger.besu.ethereum.mainnet.feemarket.FeeMarket; |
||||||
|
import org.hyperledger.besu.util.Subscribers; |
||||||
|
|
||||||
|
import java.util.List; |
||||||
|
import java.util.Map; |
||||||
|
import java.util.Optional; |
||||||
|
import java.util.OptionalLong; |
||||||
|
import java.util.function.Predicate; |
||||||
|
|
||||||
|
public class EndLayer implements TransactionsLayer { |
||||||
|
|
||||||
|
private final TransactionPoolMetrics metrics; |
||||||
|
private final Subscribers<PendingTransactionAddedListener> onAddedListeners = |
||||||
|
Subscribers.create(); |
||||||
|
|
||||||
|
private final Subscribers<PendingTransactionDroppedListener> onDroppedListeners = |
||||||
|
Subscribers.create(); |
||||||
|
|
||||||
|
private long droppedCount = 0; |
||||||
|
|
||||||
|
public EndLayer(final TransactionPoolMetrics metrics) { |
||||||
|
this.metrics = metrics; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String name() { |
||||||
|
return "end"; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void reset() { |
||||||
|
droppedCount = 0; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Optional<Transaction> getByHash(final Hash transactionHash) { |
||||||
|
return Optional.empty(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean contains(final Transaction transaction) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public List<PendingTransaction> getAll() { |
||||||
|
return List.of(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public TransactionAddedResult add(final PendingTransaction pendingTransaction, final int gap) { |
||||||
|
notifyTransactionDropped(pendingTransaction); |
||||||
|
metrics.incrementRemoved( |
||||||
|
pendingTransaction.isReceivedFromLocalSource(), DROPPED.label(), name()); |
||||||
|
++droppedCount; |
||||||
|
return TransactionAddedResult.DROPPED; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void remove(final PendingTransaction pendingTransaction, final RemovalReason reason) {} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void blockAdded( |
||||||
|
final FeeMarket feeMarket, |
||||||
|
final BlockHeader blockHeader, |
||||||
|
final Map<Address, Long> maxConfirmedNonceBySender) { |
||||||
|
// no-op
|
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public List<Transaction> getAllLocal() { |
||||||
|
return List.of(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public int count() { |
||||||
|
return 0; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public OptionalLong getNextNonceFor(final Address sender) { |
||||||
|
return OptionalLong.empty(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public PendingTransaction promote(final Predicate<PendingTransaction> promotionFilter) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public long subscribeToAdded(final PendingTransactionAddedListener listener) { |
||||||
|
return onAddedListeners.subscribe(listener); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void unsubscribeFromAdded(final long id) { |
||||||
|
onAddedListeners.unsubscribe(id); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public long subscribeToDropped(final PendingTransactionDroppedListener listener) { |
||||||
|
return onDroppedListeners.subscribe(listener); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void unsubscribeFromDropped(final long id) { |
||||||
|
onDroppedListeners.unsubscribe(id); |
||||||
|
} |
||||||
|
|
||||||
|
protected void notifyTransactionDropped(final PendingTransaction pendingTransaction) { |
||||||
|
onDroppedListeners.forEach( |
||||||
|
listener -> listener.onTransactionDropped(pendingTransaction.getTransaction())); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public PendingTransaction promoteFor(final Address sender, final long nonce) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void notifyAdded(final PendingTransaction pendingTransaction) { |
||||||
|
// no-op
|
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public long getCumulativeUsedSpace() { |
||||||
|
return 0; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String logStats() { |
||||||
|
return "Dropped: " + droppedCount; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String logSender(final Address sender) { |
||||||
|
return ""; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public List<PendingTransaction> getAllFor(final Address sender) { |
||||||
|
return List.of(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,80 @@ |
|||||||
|
/* |
||||||
|
* Copyright Hyperledger Besu Contributors. |
||||||
|
* |
||||||
|
* 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. See the License for the |
||||||
|
* specific language governing permissions and limitations under the License. |
||||||
|
* |
||||||
|
* SPDX-License-Identifier: Apache-2.0 |
||||||
|
*/ |
||||||
|
package org.hyperledger.besu.ethereum.eth.transactions.layered; |
||||||
|
|
||||||
|
import static java.util.Comparator.comparing; |
||||||
|
|
||||||
|
import org.hyperledger.besu.ethereum.core.BlockHeader; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolConfiguration; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolMetrics; |
||||||
|
import org.hyperledger.besu.ethereum.mainnet.feemarket.FeeMarket; |
||||||
|
|
||||||
|
import java.util.function.BiFunction; |
||||||
|
|
||||||
|
/** |
||||||
|
* Holds the current set of pending transactions with the ability to iterate them based on priority |
||||||
|
* for mining or look-up by hash. |
||||||
|
* |
||||||
|
* <p>This class is safe for use across multiple threads. |
||||||
|
*/ |
||||||
|
public class GasPricePrioritizedTransactions extends AbstractPrioritizedTransactions { |
||||||
|
|
||||||
|
public GasPricePrioritizedTransactions( |
||||||
|
final TransactionPoolConfiguration poolConfig, |
||||||
|
final TransactionsLayer nextLayer, |
||||||
|
final TransactionPoolMetrics metrics, |
||||||
|
final BiFunction<PendingTransaction, PendingTransaction, Boolean> |
||||||
|
transactionReplacementTester) { |
||||||
|
super(poolConfig, nextLayer, metrics, transactionReplacementTester); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected int compareByFee(final PendingTransaction pt1, final PendingTransaction pt2) { |
||||||
|
return comparing(PendingTransaction::isReceivedFromLocalSource) |
||||||
|
.thenComparing(PendingTransaction::getGasPrice) |
||||||
|
.thenComparing(PendingTransaction::getSequence) |
||||||
|
.compare(pt1, pt2); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void internalBlockAdded(final BlockHeader blockHeader, final FeeMarket feeMarket) { |
||||||
|
// no-op
|
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected boolean promotionFilter(final PendingTransaction pendingTransaction) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String internalLogStats() { |
||||||
|
if (orderByFee.isEmpty()) { |
||||||
|
return "GasPrice Prioritized: Empty"; |
||||||
|
} |
||||||
|
|
||||||
|
return "GasPrice Prioritized: " |
||||||
|
+ "count: " |
||||||
|
+ pendingTransactions.size() |
||||||
|
+ " space used: " |
||||||
|
+ spaceUsed |
||||||
|
+ " unique senders: " |
||||||
|
+ txsBySender.size() |
||||||
|
+ ", highest fee tx: " |
||||||
|
+ orderByFee.last().getTransaction().getGasPrice().get().toHumanReadableString() |
||||||
|
+ ", lowest fee tx: " |
||||||
|
+ orderByFee.first().getTransaction().getGasPrice().get().toHumanReadableString(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,479 @@ |
|||||||
|
/* |
||||||
|
* Copyright Hyperledger Besu Contributors. |
||||||
|
* |
||||||
|
* 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. See the License for the |
||||||
|
* specific language governing permissions and limitations under the License. |
||||||
|
* |
||||||
|
* SPDX-License-Identifier: Apache-2.0 |
||||||
|
*/ |
||||||
|
package org.hyperledger.besu.ethereum.eth.transactions.layered; |
||||||
|
|
||||||
|
import static java.util.stream.Collectors.groupingBy; |
||||||
|
import static java.util.stream.Collectors.mapping; |
||||||
|
import static java.util.stream.Collectors.reducing; |
||||||
|
import static org.hyperledger.besu.ethereum.eth.transactions.TransactionAddedResult.ALREADY_KNOWN; |
||||||
|
import static org.hyperledger.besu.ethereum.eth.transactions.TransactionAddedResult.INTERNAL_ERROR; |
||||||
|
import static org.hyperledger.besu.ethereum.eth.transactions.TransactionAddedResult.NONCE_TOO_FAR_IN_FUTURE_FOR_SENDER; |
||||||
|
import static org.hyperledger.besu.ethereum.eth.transactions.TransactionAddedResult.REORG_SENDER; |
||||||
|
import static org.hyperledger.besu.ethereum.eth.transactions.layered.TransactionsLayer.RemovalReason.INVALIDATED; |
||||||
|
import static org.hyperledger.besu.ethereum.eth.transactions.layered.TransactionsLayer.RemovalReason.REORG; |
||||||
|
|
||||||
|
import org.hyperledger.besu.datatypes.Address; |
||||||
|
import org.hyperledger.besu.datatypes.Hash; |
||||||
|
import org.hyperledger.besu.ethereum.core.BlockHeader; |
||||||
|
import org.hyperledger.besu.ethereum.core.Transaction; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactionAddedListener; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactionDroppedListener; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactions; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.TransactionAddedResult; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolConfiguration; |
||||||
|
import org.hyperledger.besu.ethereum.mainnet.feemarket.FeeMarket; |
||||||
|
import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput; |
||||||
|
import org.hyperledger.besu.evm.account.Account; |
||||||
|
import org.hyperledger.besu.evm.account.AccountState; |
||||||
|
|
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.HashSet; |
||||||
|
import java.util.List; |
||||||
|
import java.util.Map; |
||||||
|
import java.util.Optional; |
||||||
|
import java.util.OptionalLong; |
||||||
|
import java.util.Set; |
||||||
|
import java.util.concurrent.atomic.AtomicBoolean; |
||||||
|
import java.util.stream.Collector; |
||||||
|
import java.util.stream.Collectors; |
||||||
|
|
||||||
|
import kotlin.ranges.LongRange; |
||||||
|
import org.slf4j.Logger; |
||||||
|
import org.slf4j.LoggerFactory; |
||||||
|
|
||||||
|
public class LayeredPendingTransactions implements PendingTransactions { |
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(LayeredPendingTransactions.class); |
||||||
|
private static final Logger LOG_FOR_REPLAY = LoggerFactory.getLogger("LOG_FOR_REPLAY"); |
||||||
|
private final TransactionPoolConfiguration poolConfig; |
||||||
|
private final Set<Address> localSenders = new HashSet<>(); |
||||||
|
private final AbstractPrioritizedTransactions prioritizedTransactions; |
||||||
|
|
||||||
|
public LayeredPendingTransactions( |
||||||
|
final TransactionPoolConfiguration poolConfig, |
||||||
|
final AbstractPrioritizedTransactions prioritizedTransactions) { |
||||||
|
this.poolConfig = poolConfig; |
||||||
|
this.prioritizedTransactions = prioritizedTransactions; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public synchronized void reset() { |
||||||
|
prioritizedTransactions.reset(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public synchronized TransactionAddedResult addRemoteTransaction( |
||||||
|
final Transaction transaction, final Optional<Account> maybeSenderAccount) { |
||||||
|
|
||||||
|
return addTransaction(new PendingTransaction.Remote(transaction), maybeSenderAccount); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public synchronized TransactionAddedResult addLocalTransaction( |
||||||
|
final Transaction transaction, final Optional<Account> maybeSenderAccount) { |
||||||
|
|
||||||
|
final TransactionAddedResult addedResult = |
||||||
|
addTransaction(new PendingTransaction.Local(transaction), maybeSenderAccount); |
||||||
|
if (addedResult.isSuccess()) { |
||||||
|
localSenders.add(transaction.getSender()); |
||||||
|
} |
||||||
|
return addedResult; |
||||||
|
} |
||||||
|
|
||||||
|
TransactionAddedResult addTransaction( |
||||||
|
final PendingTransaction pendingTransaction, final Optional<Account> maybeSenderAccount) { |
||||||
|
|
||||||
|
final long senderNonce = maybeSenderAccount.map(AccountState::getNonce).orElse(0L); |
||||||
|
|
||||||
|
logTransactionForReplayAdd(pendingTransaction, senderNonce); |
||||||
|
|
||||||
|
final long nonceDistance = pendingTransaction.getNonce() - senderNonce; |
||||||
|
|
||||||
|
final TransactionAddedResult nonceChecksResult = |
||||||
|
nonceChecks(pendingTransaction, senderNonce, nonceDistance); |
||||||
|
if (nonceChecksResult != null) { |
||||||
|
return nonceChecksResult; |
||||||
|
} |
||||||
|
|
||||||
|
try { |
||||||
|
TransactionAddedResult result = |
||||||
|
prioritizedTransactions.add(pendingTransaction, (int) nonceDistance); |
||||||
|
|
||||||
|
if (result.equals(REORG_SENDER)) { |
||||||
|
result = reorgSenderOf(pendingTransaction, (int) nonceDistance); |
||||||
|
} |
||||||
|
|
||||||
|
return result; |
||||||
|
} catch (final Throwable throwable) { |
||||||
|
// in case something unexpected happened, log this sender txs and force a reorg of his txs
|
||||||
|
LOG.warn( |
||||||
|
"Unexpected error {} when adding transaction {}, current sender status {}", |
||||||
|
throwable, |
||||||
|
pendingTransaction.toTraceLog(), |
||||||
|
prioritizedTransactions.logSender(pendingTransaction.getSender())); |
||||||
|
LOG.warn("Stack trace", throwable); |
||||||
|
reorgSenderOf(pendingTransaction, (int) nonceDistance); |
||||||
|
return INTERNAL_ERROR; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private TransactionAddedResult reorgSenderOf( |
||||||
|
final PendingTransaction pendingTransaction, final int nonceDistance) { |
||||||
|
final var existingSenderTxs = prioritizedTransactions.getAllFor(pendingTransaction.getSender()); |
||||||
|
|
||||||
|
// it is more performant to invalidate backward
|
||||||
|
for (int i = existingSenderTxs.size() - 1; i >= 0; --i) { |
||||||
|
prioritizedTransactions.remove(existingSenderTxs.get(i), REORG); |
||||||
|
} |
||||||
|
|
||||||
|
// add the new one and re-add all the previous
|
||||||
|
final var result = prioritizedTransactions.add(pendingTransaction, nonceDistance); |
||||||
|
existingSenderTxs.forEach(ptx -> prioritizedTransactions.add(ptx, nonceDistance)); |
||||||
|
LOG.atTrace() |
||||||
|
.setMessage( |
||||||
|
"Pending transaction {} with nonce distance {} triggered a reorg for sender {} with {} existing transactions: {}") |
||||||
|
.addArgument(pendingTransaction::toTraceLog) |
||||||
|
.addArgument(nonceDistance) |
||||||
|
.addArgument(pendingTransaction::getSender) |
||||||
|
.addArgument(existingSenderTxs::size) |
||||||
|
.addArgument( |
||||||
|
() -> |
||||||
|
existingSenderTxs.stream() |
||||||
|
.map(PendingTransaction::toTraceLog) |
||||||
|
.collect(Collectors.joining("; "))) |
||||||
|
.log(); |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
private void logTransactionForReplayAdd( |
||||||
|
final PendingTransaction pendingTransaction, final long senderNonce) { |
||||||
|
// csv fields: sequence, addedAt, sender, sender_nonce, nonce, type, hash, rlp
|
||||||
|
LOG_FOR_REPLAY |
||||||
|
.atTrace() |
||||||
|
.setMessage("T,{},{},{},{},{},{},{},{}") |
||||||
|
.addArgument(pendingTransaction.getSequence()) |
||||||
|
.addArgument(pendingTransaction.getAddedAt()) |
||||||
|
.addArgument(pendingTransaction.getSender()) |
||||||
|
.addArgument(senderNonce) |
||||||
|
.addArgument(pendingTransaction.getNonce()) |
||||||
|
.addArgument(pendingTransaction.getTransaction().getType()) |
||||||
|
.addArgument(pendingTransaction::getHash) |
||||||
|
.addArgument( |
||||||
|
() -> { |
||||||
|
final BytesValueRLPOutput rlp = new BytesValueRLPOutput(); |
||||||
|
pendingTransaction.getTransaction().writeTo(rlp); |
||||||
|
return rlp.encoded().toHexString(); |
||||||
|
}) |
||||||
|
.log(); |
||||||
|
} |
||||||
|
|
||||||
|
private void logTransactionForReplayDelete(final PendingTransaction pendingTransaction) { |
||||||
|
// csv fields: sequence, addedAt, sender, nonce, type, hash, rlp
|
||||||
|
LOG_FOR_REPLAY |
||||||
|
.atTrace() |
||||||
|
.setMessage("D,{},{},{},{},{},{},{}") |
||||||
|
.addArgument(pendingTransaction.getSequence()) |
||||||
|
.addArgument(pendingTransaction.getAddedAt()) |
||||||
|
.addArgument(pendingTransaction.getSender()) |
||||||
|
.addArgument(pendingTransaction.getNonce()) |
||||||
|
.addArgument(pendingTransaction.getTransaction().getType()) |
||||||
|
.addArgument(pendingTransaction::getHash) |
||||||
|
.addArgument( |
||||||
|
() -> { |
||||||
|
final BytesValueRLPOutput rlp = new BytesValueRLPOutput(); |
||||||
|
pendingTransaction.getTransaction().writeTo(rlp); |
||||||
|
return rlp.encoded().toHexString(); |
||||||
|
}) |
||||||
|
.log(); |
||||||
|
} |
||||||
|
|
||||||
|
private TransactionAddedResult nonceChecks( |
||||||
|
final PendingTransaction pendingTransaction, |
||||||
|
final long senderNonce, |
||||||
|
final long nonceDistance) { |
||||||
|
if (nonceDistance < 0) { |
||||||
|
LOG.atTrace() |
||||||
|
.setMessage("Drop already confirmed transaction {}, since current sender nonce is {}") |
||||||
|
.addArgument(pendingTransaction::toTraceLog) |
||||||
|
.addArgument(senderNonce) |
||||||
|
.log(); |
||||||
|
return ALREADY_KNOWN; |
||||||
|
} else if (nonceDistance >= poolConfig.getMaxFutureBySender()) { |
||||||
|
LOG.atTrace() |
||||||
|
.setMessage( |
||||||
|
"Drop too much in the future transaction {}, since current sender nonce is {}") |
||||||
|
.addArgument(pendingTransaction::toTraceLog) |
||||||
|
.addArgument(senderNonce) |
||||||
|
.log(); |
||||||
|
return NONCE_TOO_FAR_IN_FUTURE_FOR_SENDER; |
||||||
|
} |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void evictOldTransactions() {} |
||||||
|
|
||||||
|
@Override |
||||||
|
public synchronized List<Transaction> getLocalTransactions() { |
||||||
|
return prioritizedTransactions.getAllLocal(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public synchronized boolean isLocalSender(final Address sender) { |
||||||
|
return localSenders.contains(sender); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
// There's a small edge case here we could encounter.
|
||||||
|
// When we pass an upgrade block that has a new transaction type, we start allowing transactions
|
||||||
|
// of that new type into our pool.
|
||||||
|
// If we then reorg to a block lower than the upgrade block height _and_ we create a block, that
|
||||||
|
// block could end up with transactions of the new type.
|
||||||
|
// This seems like it would be very rare but worth it to document that we don't handle that case
|
||||||
|
// right now.
|
||||||
|
public synchronized void selectTransactions( |
||||||
|
final PendingTransactions.TransactionSelector selector) { |
||||||
|
final List<PendingTransaction> invalidTransactions = new ArrayList<>(); |
||||||
|
final Set<Hash> alreadyChecked = new HashSet<>(); |
||||||
|
final AtomicBoolean completed = new AtomicBoolean(false); |
||||||
|
|
||||||
|
prioritizedTransactions.stream() |
||||||
|
.takeWhile(unused -> !completed.get()) |
||||||
|
.peek( |
||||||
|
highPrioPendingTx -> |
||||||
|
LOG.atDebug() |
||||||
|
.setMessage("highPrioPendingTx {}, senderTxs {}") |
||||||
|
.addArgument(highPrioPendingTx::toTraceLog) |
||||||
|
.addArgument( |
||||||
|
() -> |
||||||
|
prioritizedTransactions.stream(highPrioPendingTx.getSender()) |
||||||
|
.map(PendingTransaction::toTraceLog) |
||||||
|
.collect(Collectors.joining(", "))) |
||||||
|
.log()) |
||||||
|
.forEach( |
||||||
|
highPrioPendingTx -> |
||||||
|
prioritizedTransactions.stream(highPrioPendingTx.getSender()) |
||||||
|
.takeWhile(unused -> !completed.get()) |
||||||
|
.filter( |
||||||
|
candidatePendingTx -> |
||||||
|
!alreadyChecked.contains(candidatePendingTx.getHash())) |
||||||
|
.filter( |
||||||
|
candidatePendingTx -> |
||||||
|
candidatePendingTx.getNonce() <= highPrioPendingTx.getNonce()) |
||||||
|
.forEach( |
||||||
|
candidatePendingTx -> { |
||||||
|
alreadyChecked.add(candidatePendingTx.getHash()); |
||||||
|
switch (selector.evaluateTransaction( |
||||||
|
candidatePendingTx.getTransaction())) { |
||||||
|
case CONTINUE: |
||||||
|
LOG.atTrace() |
||||||
|
.setMessage("CONTINUE: Transaction {}") |
||||||
|
.addArgument(candidatePendingTx::toTraceLog) |
||||||
|
.log(); |
||||||
|
break; |
||||||
|
case DELETE_TRANSACTION_AND_CONTINUE: |
||||||
|
invalidTransactions.add(candidatePendingTx); |
||||||
|
LOG.atTrace() |
||||||
|
.setMessage("DELETE_TRANSACTION_AND_CONTINUE: Transaction {}") |
||||||
|
.addArgument(candidatePendingTx::toTraceLog) |
||||||
|
.log(); |
||||||
|
logTransactionForReplayDelete(candidatePendingTx); |
||||||
|
break; |
||||||
|
case COMPLETE_OPERATION: |
||||||
|
completed.set(true); |
||||||
|
LOG.atTrace() |
||||||
|
.setMessage("COMPLETE_OPERATION: Transaction {}") |
||||||
|
.addArgument(candidatePendingTx::toTraceLog) |
||||||
|
.log(); |
||||||
|
break; |
||||||
|
} |
||||||
|
})); |
||||||
|
|
||||||
|
invalidTransactions.forEach( |
||||||
|
invalidTx -> prioritizedTransactions.remove(invalidTx, INVALIDATED)); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public long maxSize() { |
||||||
|
return -1; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public synchronized int size() { |
||||||
|
return prioritizedTransactions.count(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public synchronized boolean containsTransaction(final Transaction transaction) { |
||||||
|
return prioritizedTransactions.contains(transaction); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public synchronized Optional<Transaction> getTransactionByHash(final Hash transactionHash) { |
||||||
|
return prioritizedTransactions.getByHash(transactionHash); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public synchronized List<PendingTransaction> getPendingTransactions() { |
||||||
|
return prioritizedTransactions.getAll(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public long subscribePendingTransactions(final PendingTransactionAddedListener listener) { |
||||||
|
return prioritizedTransactions.subscribeToAdded(listener); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void unsubscribePendingTransactions(final long id) { |
||||||
|
prioritizedTransactions.unsubscribeFromAdded(id); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public long subscribeDroppedTransactions(final PendingTransactionDroppedListener listener) { |
||||||
|
return prioritizedTransactions.subscribeToDropped(listener); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void unsubscribeDroppedTransactions(final long id) { |
||||||
|
prioritizedTransactions.unsubscribeFromDropped(id); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public OptionalLong getNextNonceForSender(final Address sender) { |
||||||
|
return prioritizedTransactions.getNextNonceFor(sender); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public synchronized void manageBlockAdded( |
||||||
|
final BlockHeader blockHeader, |
||||||
|
final List<Transaction> confirmedTransactions, |
||||||
|
final List<Transaction> reorgTransactions, |
||||||
|
final FeeMarket feeMarket) { |
||||||
|
LOG.atDebug() |
||||||
|
.setMessage("Managing new added block {}") |
||||||
|
.addArgument(blockHeader::toLogString) |
||||||
|
.log(); |
||||||
|
|
||||||
|
final var maxConfirmedNonceBySender = maxNonceBySender(confirmedTransactions); |
||||||
|
|
||||||
|
final var reorgNonceRangeBySender = nonceRangeBySender(reorgTransactions); |
||||||
|
|
||||||
|
try { |
||||||
|
prioritizedTransactions.blockAdded(feeMarket, blockHeader, maxConfirmedNonceBySender); |
||||||
|
} catch (final Throwable throwable) { |
||||||
|
LOG.warn( |
||||||
|
"Unexpected error {} when managing added block {}, maxNonceBySender {}, reorgNonceRangeBySender {}", |
||||||
|
throwable, |
||||||
|
blockHeader.toLogString(), |
||||||
|
maxConfirmedNonceBySender, |
||||||
|
reorgTransactions); |
||||||
|
LOG.warn("Stack trace", throwable); |
||||||
|
} |
||||||
|
|
||||||
|
logBlockHeaderForReplay(blockHeader, maxConfirmedNonceBySender, reorgNonceRangeBySender); |
||||||
|
} |
||||||
|
|
||||||
|
private void logBlockHeaderForReplay( |
||||||
|
final BlockHeader blockHeader, |
||||||
|
final Map<Address, Long> maxConfirmedNonceBySender, |
||||||
|
final Map<Address, LongRange> reorgNonceRangeBySender) { |
||||||
|
// block number, block hash, sender, max nonce ..., rlp
|
||||||
|
LOG_FOR_REPLAY |
||||||
|
.atTrace() |
||||||
|
.setMessage("B,{},{},{},R,{},{}") |
||||||
|
.addArgument(blockHeader.getNumber()) |
||||||
|
.addArgument(blockHeader.getBlockHash()) |
||||||
|
.addArgument( |
||||||
|
() -> |
||||||
|
maxConfirmedNonceBySender.entrySet().stream() |
||||||
|
.map(e -> e.getKey().toHexString() + "," + e.getValue()) |
||||||
|
.collect(Collectors.joining(","))) |
||||||
|
.addArgument( |
||||||
|
() -> |
||||||
|
reorgNonceRangeBySender.entrySet().stream() |
||||||
|
.map( |
||||||
|
e -> |
||||||
|
e.getKey().toHexString() |
||||||
|
+ "," |
||||||
|
+ e.getValue().getStart() |
||||||
|
+ "," |
||||||
|
+ e.getValue().getEndInclusive()) |
||||||
|
.collect(Collectors.joining(","))) |
||||||
|
.addArgument( |
||||||
|
() -> { |
||||||
|
final BytesValueRLPOutput rlp = new BytesValueRLPOutput(); |
||||||
|
blockHeader.writeTo(rlp); |
||||||
|
return rlp.encoded().toHexString(); |
||||||
|
}) |
||||||
|
.log(); |
||||||
|
} |
||||||
|
|
||||||
|
private Map<Address, Long> maxNonceBySender(final List<Transaction> confirmedTransactions) { |
||||||
|
return confirmedTransactions.stream() |
||||||
|
.collect( |
||||||
|
groupingBy( |
||||||
|
Transaction::getSender, mapping(Transaction::getNonce, reducing(0L, Math::max)))); |
||||||
|
} |
||||||
|
|
||||||
|
private Map<Address, LongRange> nonceRangeBySender( |
||||||
|
final List<Transaction> confirmedTransactions) { |
||||||
|
|
||||||
|
class MutableLongRange { |
||||||
|
long start = Long.MAX_VALUE; |
||||||
|
long end = 0; |
||||||
|
|
||||||
|
void update(final long nonce) { |
||||||
|
if (nonce < start) { |
||||||
|
start = nonce; |
||||||
|
} |
||||||
|
if (nonce > end) { |
||||||
|
end = nonce; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
MutableLongRange combine(final MutableLongRange other) { |
||||||
|
update(other.start); |
||||||
|
update(other.end); |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
LongRange toImmutable() { |
||||||
|
return new LongRange(start, end); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return confirmedTransactions.stream() |
||||||
|
.collect( |
||||||
|
groupingBy( |
||||||
|
Transaction::getSender, |
||||||
|
mapping( |
||||||
|
Transaction::getNonce, |
||||||
|
Collector.of( |
||||||
|
MutableLongRange::new, |
||||||
|
MutableLongRange::update, |
||||||
|
MutableLongRange::combine, |
||||||
|
MutableLongRange::toImmutable)))); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public synchronized String toTraceLog() { |
||||||
|
return ""; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public synchronized String logStats() { |
||||||
|
return prioritizedTransactions.logStats(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,221 @@ |
|||||||
|
/* |
||||||
|
* Copyright Hyperledger Besu Contributors. |
||||||
|
* |
||||||
|
* 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. See the License for the |
||||||
|
* specific language governing permissions and limitations under the License. |
||||||
|
* |
||||||
|
* SPDX-License-Identifier: Apache-2.0 |
||||||
|
*/ |
||||||
|
package org.hyperledger.besu.ethereum.eth.transactions.layered; |
||||||
|
|
||||||
|
import static org.hyperledger.besu.ethereum.eth.transactions.layered.TransactionsLayer.RemovalReason.PROMOTED; |
||||||
|
|
||||||
|
import org.hyperledger.besu.datatypes.Address; |
||||||
|
import org.hyperledger.besu.ethereum.core.BlockHeader; |
||||||
|
import org.hyperledger.besu.ethereum.core.Transaction; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.TransactionAddedResult; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolConfiguration; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolMetrics; |
||||||
|
import org.hyperledger.besu.ethereum.mainnet.feemarket.FeeMarket; |
||||||
|
|
||||||
|
import java.util.Comparator; |
||||||
|
import java.util.Map; |
||||||
|
import java.util.NavigableMap; |
||||||
|
import java.util.NavigableSet; |
||||||
|
import java.util.Optional; |
||||||
|
import java.util.TreeMap; |
||||||
|
import java.util.TreeSet; |
||||||
|
import java.util.function.BiFunction; |
||||||
|
import java.util.function.Predicate; |
||||||
|
import java.util.stream.Collectors; |
||||||
|
import java.util.stream.Stream; |
||||||
|
|
||||||
|
public class ReadyTransactions extends AbstractSequentialTransactionsLayer { |
||||||
|
|
||||||
|
private final NavigableSet<PendingTransaction> orderByMaxFee = |
||||||
|
new TreeSet<>( |
||||||
|
Comparator.comparing((PendingTransaction pt) -> pt.getTransaction().getMaxGasPrice()) |
||||||
|
.thenComparing(PendingTransaction::getSequence)); |
||||||
|
|
||||||
|
public ReadyTransactions( |
||||||
|
final TransactionPoolConfiguration poolConfig, |
||||||
|
final TransactionsLayer nextLayer, |
||||||
|
final TransactionPoolMetrics metrics, |
||||||
|
final BiFunction<PendingTransaction, PendingTransaction, Boolean> |
||||||
|
transactionReplacementTester) { |
||||||
|
super(poolConfig, nextLayer, transactionReplacementTester, metrics); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String name() { |
||||||
|
return "ready"; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void reset() { |
||||||
|
super.reset(); |
||||||
|
orderByMaxFee.clear(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected long cacheFreeSpace() { |
||||||
|
return poolConfig.getPendingTransactionsLayerMaxCapacityBytes() - getLayerSpaceUsed(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected TransactionAddedResult canAdd( |
||||||
|
final PendingTransaction pendingTransaction, final int gap) { |
||||||
|
final var senderTxs = txsBySender.get(pendingTransaction.getSender()); |
||||||
|
|
||||||
|
if (hasExpectedNonce(senderTxs, pendingTransaction, gap)) { |
||||||
|
return TransactionAddedResult.ADDED; |
||||||
|
} |
||||||
|
|
||||||
|
return TransactionAddedResult.TRY_NEXT_LAYER; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void internalAdd( |
||||||
|
final NavigableMap<Long, PendingTransaction> senderTxs, |
||||||
|
final PendingTransaction pendingTransaction) { |
||||||
|
if (senderTxs.firstKey() == pendingTransaction.getNonce()) { |
||||||
|
// replace previous if exists
|
||||||
|
if (senderTxs.size() > 1) { |
||||||
|
final PendingTransaction secondTx = senderTxs.get(pendingTransaction.getNonce() + 1); |
||||||
|
orderByMaxFee.remove(secondTx); |
||||||
|
} |
||||||
|
orderByMaxFee.add(pendingTransaction); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected int maxTransactionsNumber() { |
||||||
|
return Integer.MAX_VALUE; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void internalRemove( |
||||||
|
final NavigableMap<Long, PendingTransaction> senderTxs, |
||||||
|
final PendingTransaction removedTx, |
||||||
|
final RemovalReason removalReason) { |
||||||
|
orderByMaxFee.remove(removedTx); |
||||||
|
if (!senderTxs.isEmpty()) { |
||||||
|
orderByMaxFee.add(senderTxs.firstEntry().getValue()); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void internalReplaced(final PendingTransaction replacedTx) { |
||||||
|
orderByMaxFee.remove(replacedTx); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void internalBlockAdded(final BlockHeader blockHeader, final FeeMarket feeMarket) { |
||||||
|
// no-op
|
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected PendingTransaction getEvictable() { |
||||||
|
return orderByMaxFee.first(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected boolean promotionFilter(final PendingTransaction pendingTransaction) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Stream<PendingTransaction> stream() { |
||||||
|
return orderByMaxFee.descendingSet().stream() |
||||||
|
.map(PendingTransaction::getSender) |
||||||
|
.flatMap(sender -> txsBySender.get(sender).values().stream()); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public PendingTransaction promote(final Predicate<PendingTransaction> promotionFilter) { |
||||||
|
|
||||||
|
final var maybePromotedTx = |
||||||
|
orderByMaxFee.descendingSet().stream() |
||||||
|
.filter(candidateTx -> promotionFilter.test(candidateTx)) |
||||||
|
.findFirst(); |
||||||
|
|
||||||
|
return maybePromotedTx |
||||||
|
.map( |
||||||
|
promotedTx -> { |
||||||
|
final var senderTxs = txsBySender.get(promotedTx.getSender()); |
||||||
|
// we always promote the first tx of a sender, so remove the first entry
|
||||||
|
senderTxs.pollFirstEntry(); |
||||||
|
processRemove(senderTxs, promotedTx.getTransaction(), PROMOTED); |
||||||
|
|
||||||
|
// now that we have space, promote from the next layer
|
||||||
|
promoteTransactions(); |
||||||
|
|
||||||
|
if (senderTxs.isEmpty()) { |
||||||
|
txsBySender.remove(promotedTx.getSender()); |
||||||
|
} |
||||||
|
return promotedTx; |
||||||
|
}) |
||||||
|
.orElse(null); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String internalLogStats() { |
||||||
|
if (orderByMaxFee.isEmpty()) { |
||||||
|
return "Ready: Empty"; |
||||||
|
} |
||||||
|
|
||||||
|
final Transaction top = orderByMaxFee.last().getTransaction(); |
||||||
|
final Transaction last = orderByMaxFee.first().getTransaction(); |
||||||
|
|
||||||
|
return "Ready: " |
||||||
|
+ "count=" |
||||||
|
+ pendingTransactions.size() |
||||||
|
+ ", space used: " |
||||||
|
+ spaceUsed |
||||||
|
+ ", unique senders: " |
||||||
|
+ txsBySender.size() |
||||||
|
+ ", top by max fee[max fee:" |
||||||
|
+ top.getMaxGasPrice().toHumanReadableString() |
||||||
|
+ ", hash: " |
||||||
|
+ top.getHash() |
||||||
|
+ "], last by max fee [max fee: " |
||||||
|
+ last.getMaxGasPrice().toHumanReadableString() |
||||||
|
+ ", hash: " |
||||||
|
+ last.getHash() |
||||||
|
+ "]"; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void internalConsistencyCheck( |
||||||
|
final Map<Address, TreeMap<Long, PendingTransaction>> prevLayerTxsBySender) { |
||||||
|
super.internalConsistencyCheck(prevLayerTxsBySender); |
||||||
|
|
||||||
|
final var minNonceBySender = |
||||||
|
pendingTransactions.values().stream() |
||||||
|
.collect( |
||||||
|
Collectors.groupingBy( |
||||||
|
PendingTransaction::getSender, |
||||||
|
Collectors.minBy(Comparator.comparingLong(PendingTransaction::getNonce)))); |
||||||
|
|
||||||
|
final var controlOrderByMaxFee = new TreeSet<>(orderByMaxFee.comparator()); |
||||||
|
controlOrderByMaxFee.addAll(minNonceBySender.values().stream().map(Optional::get).toList()); |
||||||
|
|
||||||
|
final var itControl = controlOrderByMaxFee.iterator(); |
||||||
|
final var itCurrent = orderByMaxFee.iterator(); |
||||||
|
|
||||||
|
while (itControl.hasNext()) { |
||||||
|
assert itControl.next().equals(itCurrent.next()) |
||||||
|
: "orderByMaxFee does not match pendingTransactions"; |
||||||
|
} |
||||||
|
|
||||||
|
assert itCurrent.hasNext() == false |
||||||
|
: "orderByMaxFee has more elements than pendingTransactions"; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,374 @@ |
|||||||
|
/* |
||||||
|
* Copyright Hyperledger Besu Contributors. |
||||||
|
* |
||||||
|
* 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. See the License for the |
||||||
|
* specific language governing permissions and limitations under the License. |
||||||
|
* |
||||||
|
* SPDX-License-Identifier: Apache-2.0 |
||||||
|
*/ |
||||||
|
package org.hyperledger.besu.ethereum.eth.transactions.layered; |
||||||
|
|
||||||
|
import static org.hyperledger.besu.ethereum.eth.transactions.layered.TransactionsLayer.RemovalReason.INVALIDATED; |
||||||
|
import static org.hyperledger.besu.ethereum.eth.transactions.layered.TransactionsLayer.RemovalReason.PROMOTED; |
||||||
|
|
||||||
|
import org.hyperledger.besu.datatypes.Address; |
||||||
|
import org.hyperledger.besu.ethereum.core.BlockHeader; |
||||||
|
import org.hyperledger.besu.ethereum.core.Transaction; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.TransactionAddedResult; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolConfiguration; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolMetrics; |
||||||
|
import org.hyperledger.besu.ethereum.mainnet.feemarket.FeeMarket; |
||||||
|
|
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.Collection; |
||||||
|
import java.util.Comparator; |
||||||
|
import java.util.HashMap; |
||||||
|
import java.util.HashSet; |
||||||
|
import java.util.List; |
||||||
|
import java.util.Map; |
||||||
|
import java.util.NavigableMap; |
||||||
|
import java.util.NavigableSet; |
||||||
|
import java.util.Objects; |
||||||
|
import java.util.OptionalLong; |
||||||
|
import java.util.Set; |
||||||
|
import java.util.TreeMap; |
||||||
|
import java.util.TreeSet; |
||||||
|
import java.util.function.BiFunction; |
||||||
|
import java.util.function.Predicate; |
||||||
|
import java.util.stream.IntStream; |
||||||
|
import java.util.stream.Stream; |
||||||
|
|
||||||
|
public class SparseTransactions extends AbstractTransactionsLayer { |
||||||
|
private final NavigableSet<PendingTransaction> sparseEvictionOrder = |
||||||
|
new TreeSet<>(Comparator.comparing(PendingTransaction::getSequence)); |
||||||
|
private final Map<Address, Integer> gapBySender = new HashMap<>(); |
||||||
|
private final List<Set<Address>> orderByGap; |
||||||
|
|
||||||
|
public SparseTransactions( |
||||||
|
final TransactionPoolConfiguration poolConfig, |
||||||
|
final TransactionsLayer nextLayer, |
||||||
|
final TransactionPoolMetrics metrics, |
||||||
|
final BiFunction<PendingTransaction, PendingTransaction, Boolean> |
||||||
|
transactionReplacementTester) { |
||||||
|
super(poolConfig, nextLayer, transactionReplacementTester, metrics); |
||||||
|
orderByGap = new ArrayList<>(poolConfig.getMaxFutureBySender()); |
||||||
|
IntStream.range(0, poolConfig.getMaxFutureBySender()) |
||||||
|
.forEach(i -> orderByGap.add(new HashSet<>())); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String name() { |
||||||
|
return "sparse"; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected long cacheFreeSpace() { |
||||||
|
return poolConfig.getPendingTransactionsLayerMaxCapacityBytes() - getLayerSpaceUsed(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected boolean gapsAllowed() { |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void reset() { |
||||||
|
super.reset(); |
||||||
|
sparseEvictionOrder.clear(); |
||||||
|
gapBySender.clear(); |
||||||
|
orderByGap.forEach(Set::clear); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected TransactionAddedResult canAdd( |
||||||
|
final PendingTransaction pendingTransaction, final int gap) { |
||||||
|
gapBySender.compute( |
||||||
|
pendingTransaction.getSender(), |
||||||
|
(sender, currGap) -> { |
||||||
|
if (currGap == null) { |
||||||
|
orderByGap.get(gap).add(sender); |
||||||
|
return gap; |
||||||
|
} |
||||||
|
if (pendingTransaction.getNonce() < txsBySender.get(sender).firstKey()) { |
||||||
|
orderByGap.get(currGap).remove(sender); |
||||||
|
orderByGap.get(gap).add(sender); |
||||||
|
return gap; |
||||||
|
} |
||||||
|
return currGap; |
||||||
|
}); |
||||||
|
|
||||||
|
return TransactionAddedResult.ADDED; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void internalAdd( |
||||||
|
final NavigableMap<Long, PendingTransaction> senderTxs, final PendingTransaction addedTx) { |
||||||
|
sparseEvictionOrder.add(addedTx); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected int maxTransactionsNumber() { |
||||||
|
return Integer.MAX_VALUE; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void internalReplaced(final PendingTransaction replacedTx) { |
||||||
|
sparseEvictionOrder.remove(replacedTx); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void internalBlockAdded(final BlockHeader blockHeader, final FeeMarket feeMarket) {} |
||||||
|
|
||||||
|
@Override |
||||||
|
public PendingTransaction promote(final Predicate<PendingTransaction> promotionFilter) { |
||||||
|
final PendingTransaction promotedTx = |
||||||
|
orderByGap.get(0).stream() |
||||||
|
.map(txsBySender::get) |
||||||
|
.map(NavigableMap::values) |
||||||
|
.flatMap(Collection::stream) |
||||||
|
.filter(promotionFilter) |
||||||
|
.findFirst() |
||||||
|
.orElse(null); |
||||||
|
|
||||||
|
if (promotedTx != null) { |
||||||
|
final Address sender = promotedTx.getSender(); |
||||||
|
final var senderTxs = txsBySender.get(sender); |
||||||
|
senderTxs.pollFirstEntry(); |
||||||
|
processRemove(senderTxs, promotedTx.getTransaction(), PROMOTED); |
||||||
|
if (senderTxs.isEmpty()) { |
||||||
|
txsBySender.remove(sender); |
||||||
|
orderByGap.get(0).remove(sender); |
||||||
|
gapBySender.remove(sender); |
||||||
|
} else { |
||||||
|
final long firstNonce = senderTxs.firstKey(); |
||||||
|
final int newGap = (int) (firstNonce - (promotedTx.getNonce() + 1)); |
||||||
|
if (newGap != 0) { |
||||||
|
updateGap(sender, 0, newGap); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return promotedTx; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void remove(final PendingTransaction invalidatedTx, final RemovalReason reason) { |
||||||
|
|
||||||
|
final var senderTxs = txsBySender.get(invalidatedTx.getSender()); |
||||||
|
if (senderTxs != null && senderTxs.containsKey(invalidatedTx.getNonce())) { |
||||||
|
// gaps are allowed here then just remove
|
||||||
|
senderTxs.remove(invalidatedTx.getNonce()); |
||||||
|
processRemove(senderTxs, invalidatedTx.getTransaction(), reason); |
||||||
|
if (senderTxs.isEmpty()) { |
||||||
|
txsBySender.remove(invalidatedTx.getSender()); |
||||||
|
} |
||||||
|
} else { |
||||||
|
nextLayer.remove(invalidatedTx, reason); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void internalConfirmed( |
||||||
|
final NavigableMap<Long, PendingTransaction> senderTxs, |
||||||
|
final Address sender, |
||||||
|
final long maxConfirmedNonce, |
||||||
|
final PendingTransaction highestNonceRemovedTx) { |
||||||
|
|
||||||
|
if (highestNonceRemovedTx != null) { |
||||||
|
final int currGap = gapBySender.get(sender); |
||||||
|
final int newGap = (int) (senderTxs.firstKey() - (highestNonceRemovedTx.getNonce() + 1)); |
||||||
|
if (currGap != newGap) { |
||||||
|
updateGap(sender, currGap, newGap); |
||||||
|
} |
||||||
|
} else { |
||||||
|
final int currGap = gapBySender.get(sender); |
||||||
|
final int newGap = (int) (senderTxs.firstKey() - (maxConfirmedNonce + 1)); |
||||||
|
if (newGap < currGap) { |
||||||
|
updateGap(sender, currGap, newGap); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void internalEvict( |
||||||
|
final NavigableMap<Long, PendingTransaction> lessReadySenderTxs, |
||||||
|
final PendingTransaction evictedTx) { |
||||||
|
sparseEvictionOrder.remove(evictedTx); |
||||||
|
|
||||||
|
if (lessReadySenderTxs.isEmpty()) { |
||||||
|
deleteGap(evictedTx.getSender()); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void internalRemove( |
||||||
|
final NavigableMap<Long, PendingTransaction> senderTxs, |
||||||
|
final PendingTransaction removedTx, |
||||||
|
final RemovalReason removalReason) { |
||||||
|
|
||||||
|
sparseEvictionOrder.remove(removedTx); |
||||||
|
|
||||||
|
final Address sender = removedTx.getSender(); |
||||||
|
|
||||||
|
if (senderTxs != null && !senderTxs.isEmpty()) { |
||||||
|
final int deltaGap = (int) (senderTxs.firstKey() - removedTx.getNonce()); |
||||||
|
if (deltaGap > 0) { |
||||||
|
final int currGap = gapBySender.get(sender); |
||||||
|
final int newGap; |
||||||
|
if (removalReason.equals(INVALIDATED)) { |
||||||
|
newGap = currGap + deltaGap; |
||||||
|
} else { |
||||||
|
newGap = deltaGap - 1; |
||||||
|
} |
||||||
|
if (currGap != newGap) { |
||||||
|
updateGap(sender, currGap, newGap); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} else { |
||||||
|
deleteGap(sender); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void deleteGap(final Address sender) { |
||||||
|
orderByGap.get(gapBySender.remove(sender)).remove(sender); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected PendingTransaction getEvictable() { |
||||||
|
return sparseEvictionOrder.first(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected boolean promotionFilter(final PendingTransaction pendingTransaction) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Stream<PendingTransaction> stream() { |
||||||
|
return sparseEvictionOrder.descendingSet().stream(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public OptionalLong getNextNonceFor(final Address sender) { |
||||||
|
final Integer gap = gapBySender.get(sender); |
||||||
|
if (gap != null && gap == 0) { |
||||||
|
final var senderTxs = txsBySender.get(sender); |
||||||
|
var currNonce = senderTxs.firstKey(); |
||||||
|
for (final var nextNonce : senderTxs.keySet()) { |
||||||
|
if (nextNonce > currNonce + 1) { |
||||||
|
break; |
||||||
|
} |
||||||
|
currNonce = nextNonce; |
||||||
|
} |
||||||
|
return OptionalLong.of(currNonce + 1); |
||||||
|
} |
||||||
|
return OptionalLong.empty(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void internalNotifyAdded( |
||||||
|
final NavigableMap<Long, PendingTransaction> senderTxs, |
||||||
|
final PendingTransaction pendingTransaction) { |
||||||
|
final Address sender = pendingTransaction.getSender(); |
||||||
|
final Integer currGap = gapBySender.get(sender); |
||||||
|
if (currGap != null) { |
||||||
|
final int newGap = (int) (senderTxs.firstKey() - (pendingTransaction.getNonce() + 1)); |
||||||
|
if (newGap < currGap) { |
||||||
|
updateGap(sender, currGap, newGap); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String logSender(final Address sender) { |
||||||
|
final var senderTxs = txsBySender.get(sender); |
||||||
|
return name() |
||||||
|
+ "[" |
||||||
|
+ (Objects.isNull(senderTxs) |
||||||
|
? "Empty" |
||||||
|
: "gap(" + gapBySender.get(sender) + ") " + senderTxs.keySet()) |
||||||
|
+ "] " |
||||||
|
+ nextLayer.logSender(sender); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String internalLogStats() { |
||||||
|
if (sparseEvictionOrder.isEmpty()) { |
||||||
|
return "Sparse: Empty"; |
||||||
|
} |
||||||
|
|
||||||
|
final Transaction newest = sparseEvictionOrder.last().getTransaction(); |
||||||
|
final Transaction oldest = sparseEvictionOrder.first().getTransaction(); |
||||||
|
|
||||||
|
return "Sparse: " |
||||||
|
+ "count=" |
||||||
|
+ pendingTransactions.size() |
||||||
|
+ ", space used: " |
||||||
|
+ spaceUsed |
||||||
|
+ ", unique senders: " |
||||||
|
+ txsBySender.size() |
||||||
|
+ ", oldest [gap: " |
||||||
|
+ gapBySender.get(oldest.getSender()) |
||||||
|
+ ", max fee:" |
||||||
|
+ oldest.getMaxGasPrice().toHumanReadableString() |
||||||
|
+ ", hash: " |
||||||
|
+ oldest.getHash() |
||||||
|
+ "], newest [gap: " |
||||||
|
+ gapBySender.get(newest.getSender()) |
||||||
|
+ ", max fee: " |
||||||
|
+ newest.getMaxGasPrice().toHumanReadableString() |
||||||
|
+ ", hash: " |
||||||
|
+ newest.getHash() |
||||||
|
+ "]"; |
||||||
|
} |
||||||
|
|
||||||
|
private void updateGap(final Address sender, final int currGap, final int newGap) { |
||||||
|
orderByGap.get(currGap).remove(sender); |
||||||
|
orderByGap.get(newGap).add(sender); |
||||||
|
gapBySender.put(sender, newGap); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void internalConsistencyCheck( |
||||||
|
final Map<Address, TreeMap<Long, PendingTransaction>> prevLayerTxsBySender) { |
||||||
|
txsBySender.values().stream() |
||||||
|
.filter(senderTxs -> senderTxs.size() > 1) |
||||||
|
.map(NavigableMap::entrySet) |
||||||
|
.map(Set::iterator) |
||||||
|
.forEach( |
||||||
|
itNonce -> { |
||||||
|
PendingTransaction firstTx = itNonce.next().getValue(); |
||||||
|
|
||||||
|
prevLayerTxsBySender.computeIfPresent( |
||||||
|
firstTx.getSender(), |
||||||
|
(sender, txsByNonce) -> { |
||||||
|
final long prevLayerMaxNonce = txsByNonce.lastKey(); |
||||||
|
assert prevLayerMaxNonce < firstTx.getNonce() |
||||||
|
: "first nonce is not greater than previous layer last nonce"; |
||||||
|
|
||||||
|
final int gap = (int) (firstTx.getNonce() - (prevLayerMaxNonce + 1)); |
||||||
|
assert gapBySender.get(firstTx.getSender()).equals(gap) : "gap mismatch"; |
||||||
|
assert orderByGap.get(gap).contains(firstTx.getSender()) |
||||||
|
: "orderByGap sender not found"; |
||||||
|
|
||||||
|
return txsByNonce; |
||||||
|
}); |
||||||
|
|
||||||
|
long prevNonce = firstTx.getNonce(); |
||||||
|
|
||||||
|
while (itNonce.hasNext()) { |
||||||
|
final long currNonce = itNonce.next().getKey(); |
||||||
|
assert prevNonce < currNonce : "non incremental nonce"; |
||||||
|
prevNonce = currNonce; |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,103 @@ |
|||||||
|
/* |
||||||
|
* Copyright Hyperledger Besu Contributors. |
||||||
|
* |
||||||
|
* 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. See the License for the |
||||||
|
* specific language governing permissions and limitations under the License. |
||||||
|
* |
||||||
|
* SPDX-License-Identifier: Apache-2.0 |
||||||
|
*/ |
||||||
|
package org.hyperledger.besu.ethereum.eth.transactions.layered; |
||||||
|
|
||||||
|
import org.hyperledger.besu.datatypes.Address; |
||||||
|
import org.hyperledger.besu.datatypes.Hash; |
||||||
|
import org.hyperledger.besu.ethereum.core.BlockHeader; |
||||||
|
import org.hyperledger.besu.ethereum.core.Transaction; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactionAddedListener; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactionDroppedListener; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.TransactionAddedResult; |
||||||
|
import org.hyperledger.besu.ethereum.mainnet.feemarket.FeeMarket; |
||||||
|
|
||||||
|
import java.util.List; |
||||||
|
import java.util.Map; |
||||||
|
import java.util.Optional; |
||||||
|
import java.util.OptionalLong; |
||||||
|
import java.util.function.Predicate; |
||||||
|
|
||||||
|
public interface TransactionsLayer { |
||||||
|
|
||||||
|
String name(); |
||||||
|
|
||||||
|
void reset(); |
||||||
|
|
||||||
|
Optional<Transaction> getByHash(Hash transactionHash); |
||||||
|
|
||||||
|
boolean contains(Transaction transaction); |
||||||
|
|
||||||
|
List<PendingTransaction> getAll(); |
||||||
|
|
||||||
|
TransactionAddedResult add(PendingTransaction pendingTransaction, int gap); |
||||||
|
|
||||||
|
void remove(PendingTransaction pendingTransaction, RemovalReason reason); |
||||||
|
|
||||||
|
void blockAdded( |
||||||
|
FeeMarket feeMarket, |
||||||
|
BlockHeader blockHeader, |
||||||
|
final Map<Address, Long> maxConfirmedNonceBySender); |
||||||
|
|
||||||
|
List<Transaction> getAllLocal(); |
||||||
|
|
||||||
|
int count(); |
||||||
|
|
||||||
|
OptionalLong getNextNonceFor(Address sender); |
||||||
|
|
||||||
|
PendingTransaction promote(Predicate<PendingTransaction> promotionFilter); |
||||||
|
|
||||||
|
long subscribeToAdded(PendingTransactionAddedListener listener); |
||||||
|
|
||||||
|
void unsubscribeFromAdded(long id); |
||||||
|
|
||||||
|
long subscribeToDropped(PendingTransactionDroppedListener listener); |
||||||
|
|
||||||
|
void unsubscribeFromDropped(long id); |
||||||
|
|
||||||
|
PendingTransaction promoteFor(Address sender, long nonce); |
||||||
|
|
||||||
|
void notifyAdded(PendingTransaction pendingTransaction); |
||||||
|
|
||||||
|
long getCumulativeUsedSpace(); |
||||||
|
|
||||||
|
String logStats(); |
||||||
|
|
||||||
|
String logSender(Address sender); |
||||||
|
|
||||||
|
List<PendingTransaction> getAllFor(Address sender); |
||||||
|
|
||||||
|
enum RemovalReason { |
||||||
|
CONFIRMED, |
||||||
|
CROSS_LAYER_REPLACED, |
||||||
|
EVICTED, |
||||||
|
DROPPED, |
||||||
|
FOLLOW_INVALIDATED, |
||||||
|
INVALIDATED, |
||||||
|
PROMOTED, |
||||||
|
REPLACED, |
||||||
|
REORG; |
||||||
|
|
||||||
|
private final String label; |
||||||
|
|
||||||
|
RemovalReason() { |
||||||
|
this.label = name().toLowerCase(); |
||||||
|
} |
||||||
|
|
||||||
|
public String label() { |
||||||
|
return label; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,76 @@ |
|||||||
|
/* |
||||||
|
* Copyright contributors to Hyperledger Besu. |
||||||
|
* |
||||||
|
* 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. See the License for the |
||||||
|
* specific language governing permissions and limitations under the License. |
||||||
|
* |
||||||
|
* SPDX-License-Identifier: Apache-2.0 |
||||||
|
*/ |
||||||
|
|
||||||
|
/** |
||||||
|
* This implements a new transaction pool (txpool for brevity), with the main goal to better manage |
||||||
|
* nonce gaps, i.e. the possibility that the list of transactions that we see for a sender could not |
||||||
|
* be in order neither contiguous, that could happen just of the way there are broadcast on the p2p |
||||||
|
* network or intentionally to try to spam the txpool with non-executable transactions (transactions |
||||||
|
* that could not be included in a future block), so the goal is to try to keep in the pool |
||||||
|
* transactions that could be selected for a future block proposal, and at the same time, without |
||||||
|
* penalizing legitimate unordered transactions, that are only temporary non-executable. |
||||||
|
* |
||||||
|
* <p>It is disabled by default, to enable use the option {@code Xlayered-tx-pool=true} |
||||||
|
* |
||||||
|
* <p>The main idea is to organize the txpool in an arbitrary number of layers, where each layer has |
||||||
|
* specific rules and constraints that determine if a transaction belong or not to that layer and |
||||||
|
* also the way transactions move across layers. |
||||||
|
* |
||||||
|
* <p>Some design choices that apply to all layers are that a transaction can only be in one layer |
||||||
|
* at any time, and that layers are chained by priority, so the first layer has the transactions |
||||||
|
* that are candidate for a block proposal, and the last layer basically is where transactions are |
||||||
|
* dropped. Layers are meant to be added and removed in case of specific future needs. When adding a |
||||||
|
* new transaction, it is first tried on the first layer, if it is not accepted then the next one is |
||||||
|
* tried and so on. Layers could be limited by transaction number of by space, and when a layer if |
||||||
|
* full, it overflows to the next one and so on, instead when some space is freed, usually when |
||||||
|
* transactions are removed since confirmed in a block, transactions from the next layer are |
||||||
|
* promoted until there is space. |
||||||
|
* |
||||||
|
* <p>The current implementation is based on 3 layers, plus the last one that just drop every |
||||||
|
* transaction when the previous layers are full. The 3 layers are, in order: |
||||||
|
* |
||||||
|
* <ul> |
||||||
|
* <li>Prioritized |
||||||
|
* <li>Ready |
||||||
|
* <li>Sparse |
||||||
|
* </ul> |
||||||
|
* |
||||||
|
* <p>Prioritized: This is where candidate transactions are selected for creating a new block. |
||||||
|
* Transactions ordered by the effective priority fee, and it is limited by size, 2000 by default, |
||||||
|
* to reduce the overhead of the sorting and because that number is enough to fill any block, at the |
||||||
|
* current gas limit. Does not allow nonce gaps, and the first transaction for each sender must be |
||||||
|
* the next one for that sender. Eviction is done removing the transaction with the higher nonce for |
||||||
|
* the sender of the less valuable transaction, to avoid creating nonce gaps, evicted transactions |
||||||
|
* go into the next layer Ready. |
||||||
|
* |
||||||
|
* <p>Ready: Similar to the Prioritized, it does not allow nonce gaps, and the first transaction for |
||||||
|
* each sender must be the next one for that sender, but it is limited by space instead of count, |
||||||
|
* thus allowing many more transactions, think about this layer like a buffer for the Prioritized. |
||||||
|
* Since it is meant to keep ten to hundreds of thousand of transactions, it does not have a full |
||||||
|
* ordering, like the previous, but only the first transaction for each sender is ordered using a |
||||||
|
* stable value that is the max fee per gas. Eviction is the same as the Prioritized, and evicted |
||||||
|
* transaction go into the next layer Sparse. |
||||||
|
* |
||||||
|
* <p>Sparse: This is the first layer where nonce gaps are allowed and where the first transaction |
||||||
|
* for a sender could not be the next expected one for that sender. The main purpose of this layer |
||||||
|
* is to act as a purgatory for temporary unordered and/or non-contiguous transactions, so that they |
||||||
|
* could become ready asap the missing transactions arrive, or they are eventually evicted. It also |
||||||
|
* keeps the less valuable ready transactions, that are evicted from the previous layer. It is |
||||||
|
* limited by space, and eviction select the oldest transaction first, that is sent to the End Layer |
||||||
|
* that just drop it. When promoting to the prev layer Ready, only transactions that will not create |
||||||
|
* nonce gaps are selected, for that we need to keep track of the nonce distance for each sender. So |
||||||
|
* we can say that is ordered by nonce distance for promotion. |
||||||
|
*/ |
||||||
|
package org.hyperledger.besu.ethereum.eth.transactions.layered; |
@ -0,0 +1,697 @@ |
|||||||
|
/* |
||||||
|
* Copyright Hyperledger Besu Contributors. |
||||||
|
* |
||||||
|
* 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. See the License for the |
||||||
|
* specific language governing permissions and limitations under the License. |
||||||
|
* |
||||||
|
* SPDX-License-Identifier: Apache-2.0 |
||||||
|
*/ |
||||||
|
package org.hyperledger.besu.ethereum.eth.transactions; |
||||||
|
|
||||||
|
import static java.util.Arrays.asList; |
||||||
|
import static java.util.Collections.emptyList; |
||||||
|
import static java.util.Collections.singletonList; |
||||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||||
|
import static org.hyperledger.besu.ethereum.mainnet.ValidationResult.valid; |
||||||
|
import static org.hyperledger.besu.ethereum.transaction.TransactionInvalidReason.EXCEEDS_BLOCK_GAS_LIMIT; |
||||||
|
import static org.hyperledger.besu.ethereum.transaction.TransactionInvalidReason.GAS_PRICE_TOO_LOW; |
||||||
|
import static org.hyperledger.besu.ethereum.transaction.TransactionInvalidReason.NONCE_TOO_FAR_IN_FUTURE_FOR_SENDER; |
||||||
|
import static org.hyperledger.besu.ethereum.transaction.TransactionInvalidReason.NONCE_TOO_LOW; |
||||||
|
import static org.hyperledger.besu.ethereum.transaction.TransactionInvalidReason.TRANSACTION_REPLACEMENT_UNDERPRICED; |
||||||
|
import static org.hyperledger.besu.ethereum.transaction.TransactionInvalidReason.TX_FEECAP_EXCEEDED; |
||||||
|
import static org.mockito.ArgumentMatchers.any; |
||||||
|
import static org.mockito.ArgumentMatchers.argThat; |
||||||
|
import static org.mockito.ArgumentMatchers.eq; |
||||||
|
import static org.mockito.ArgumentMatchers.nullable; |
||||||
|
import static org.mockito.Mockito.doNothing; |
||||||
|
import static org.mockito.Mockito.doReturn; |
||||||
|
import static org.mockito.Mockito.mock; |
||||||
|
import static org.mockito.Mockito.never; |
||||||
|
import static org.mockito.Mockito.spy; |
||||||
|
import static org.mockito.Mockito.verify; |
||||||
|
import static org.mockito.Mockito.verifyNoInteractions; |
||||||
|
import static org.mockito.Mockito.verifyNoMoreInteractions; |
||||||
|
import static org.mockito.Mockito.when; |
||||||
|
|
||||||
|
import org.hyperledger.besu.crypto.KeyPair; |
||||||
|
import org.hyperledger.besu.crypto.SignatureAlgorithmFactory; |
||||||
|
import org.hyperledger.besu.datatypes.Wei; |
||||||
|
import org.hyperledger.besu.ethereum.ProtocolContext; |
||||||
|
import org.hyperledger.besu.ethereum.chain.MutableBlockchain; |
||||||
|
import org.hyperledger.besu.ethereum.core.Block; |
||||||
|
import org.hyperledger.besu.ethereum.core.BlockHeader; |
||||||
|
import org.hyperledger.besu.ethereum.core.Difficulty; |
||||||
|
import org.hyperledger.besu.ethereum.core.ExecutionContextTestFixture; |
||||||
|
import org.hyperledger.besu.ethereum.core.MiningParameters; |
||||||
|
import org.hyperledger.besu.ethereum.core.Transaction; |
||||||
|
import org.hyperledger.besu.ethereum.core.TransactionTestFixture; |
||||||
|
import org.hyperledger.besu.ethereum.eth.manager.EthContext; |
||||||
|
import org.hyperledger.besu.ethereum.eth.manager.EthPeer; |
||||||
|
import org.hyperledger.besu.ethereum.eth.manager.EthProtocolManager; |
||||||
|
import org.hyperledger.besu.ethereum.eth.manager.EthProtocolManagerTestUtil; |
||||||
|
import org.hyperledger.besu.ethereum.eth.manager.EthScheduler; |
||||||
|
import org.hyperledger.besu.ethereum.eth.manager.RespondingEthPeer; |
||||||
|
import org.hyperledger.besu.ethereum.eth.messages.EthPV65; |
||||||
|
import org.hyperledger.besu.ethereum.mainnet.MainnetTransactionValidator; |
||||||
|
import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; |
||||||
|
import org.hyperledger.besu.ethereum.mainnet.ProtocolSpec; |
||||||
|
import org.hyperledger.besu.ethereum.mainnet.TransactionValidationParams; |
||||||
|
import org.hyperledger.besu.ethereum.mainnet.ValidationResult; |
||||||
|
import org.hyperledger.besu.ethereum.mainnet.feemarket.FeeMarket; |
||||||
|
import org.hyperledger.besu.ethereum.transaction.TransactionInvalidReason; |
||||||
|
import org.hyperledger.besu.evm.account.Account; |
||||||
|
import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem; |
||||||
|
import org.hyperledger.besu.plugin.services.MetricsSystem; |
||||||
|
|
||||||
|
import java.math.BigInteger; |
||||||
|
import java.util.Collections; |
||||||
|
import java.util.List; |
||||||
|
import java.util.Optional; |
||||||
|
import java.util.Set; |
||||||
|
import java.util.function.BiFunction; |
||||||
|
import java.util.function.Consumer; |
||||||
|
|
||||||
|
import org.junit.Before; |
||||||
|
import org.junit.Test; |
||||||
|
import org.junit.runner.RunWith; |
||||||
|
import org.mockito.ArgumentCaptor; |
||||||
|
import org.mockito.Mock; |
||||||
|
import org.mockito.junit.MockitoJUnitRunner; |
||||||
|
|
||||||
|
@SuppressWarnings("unchecked") |
||||||
|
@RunWith(MockitoJUnitRunner.class) |
||||||
|
public abstract class AbstractTransactionsLayeredPendingTransactionsTest { |
||||||
|
|
||||||
|
protected static final KeyPair KEY_PAIR1 = |
||||||
|
SignatureAlgorithmFactory.getInstance().generateKeyPair(); |
||||||
|
|
||||||
|
private static final KeyPair KEY_PAIR2 = |
||||||
|
SignatureAlgorithmFactory.getInstance().generateKeyPair(); |
||||||
|
@Mock protected MainnetTransactionValidator transactionValidator; |
||||||
|
@Mock protected PendingTransactionAddedListener listener; |
||||||
|
@Mock protected MiningParameters miningParameters; |
||||||
|
@Mock protected TransactionsMessageSender transactionsMessageSender; |
||||||
|
@Mock protected NewPooledTransactionHashesMessageSender newPooledTransactionHashesMessageSender; |
||||||
|
@Mock protected ProtocolSpec protocolSpec; |
||||||
|
|
||||||
|
protected ProtocolSchedule protocolSchedule; |
||||||
|
|
||||||
|
protected final MetricsSystem metricsSystem = new NoOpMetricsSystem(); |
||||||
|
protected MutableBlockchain blockchain; |
||||||
|
private TransactionBroadcaster transactionBroadcaster; |
||||||
|
|
||||||
|
protected PendingTransactions transactions; |
||||||
|
private final Transaction transaction0 = createTransaction(0); |
||||||
|
private final Transaction transaction1 = createTransaction(1); |
||||||
|
|
||||||
|
private final Transaction transactionOtherSender = createTransaction(0, KEY_PAIR2); |
||||||
|
private ExecutionContextTestFixture executionContext; |
||||||
|
protected ProtocolContext protocolContext; |
||||||
|
protected TransactionPool transactionPool; |
||||||
|
protected TransactionPoolConfiguration poolConfig; |
||||||
|
protected long blockGasLimit; |
||||||
|
protected EthProtocolManager ethProtocolManager; |
||||||
|
private EthContext ethContext; |
||||||
|
private PeerTransactionTracker peerTransactionTracker; |
||||||
|
private ArgumentCaptor<Runnable> syncTaskCapture; |
||||||
|
|
||||||
|
protected abstract PendingTransactions createPendingTransactionsSorter( |
||||||
|
final TransactionPoolConfiguration poolConfig, |
||||||
|
BiFunction<PendingTransaction, PendingTransaction, Boolean> transactionReplacementTester); |
||||||
|
|
||||||
|
protected abstract ExecutionContextTestFixture createExecutionContextTestFixture(); |
||||||
|
|
||||||
|
protected abstract FeeMarket getFeeMarket(); |
||||||
|
|
||||||
|
@Before |
||||||
|
public void setUp() { |
||||||
|
executionContext = createExecutionContextTestFixture(); |
||||||
|
protocolContext = executionContext.getProtocolContext(); |
||||||
|
blockchain = executionContext.getBlockchain(); |
||||||
|
|
||||||
|
when(protocolSpec.getTransactionValidator()).thenReturn(transactionValidator); |
||||||
|
when(protocolSpec.getFeeMarket()).thenReturn(getFeeMarket()); |
||||||
|
protocolSchedule = spy(executionContext.getProtocolSchedule()); |
||||||
|
doReturn(protocolSpec).when(protocolSchedule).getByBlockHeader(any()); |
||||||
|
blockGasLimit = blockchain.getChainHeadBlock().getHeader().getGasLimit(); |
||||||
|
ethProtocolManager = EthProtocolManagerTestUtil.create(); |
||||||
|
ethContext = spy(ethProtocolManager.ethContext()); |
||||||
|
|
||||||
|
final EthScheduler ethScheduler = mock(EthScheduler.class); |
||||||
|
syncTaskCapture = ArgumentCaptor.forClass(Runnable.class); |
||||||
|
doNothing().when(ethScheduler).scheduleSyncWorkerTask(syncTaskCapture.capture()); |
||||||
|
doReturn(ethScheduler).when(ethContext).getScheduler(); |
||||||
|
|
||||||
|
peerTransactionTracker = new PeerTransactionTracker(); |
||||||
|
|
||||||
|
transactionPool = createTransactionPool(); |
||||||
|
|
||||||
|
blockchain.observeBlockAdded(transactionPool); |
||||||
|
when(miningParameters.getMinTransactionGasPrice()).thenReturn(Wei.of(2)); |
||||||
|
} |
||||||
|
|
||||||
|
protected TransactionPool createTransactionPool() { |
||||||
|
return createTransactionPool(b -> {}); |
||||||
|
} |
||||||
|
|
||||||
|
protected TransactionPool createTransactionPool( |
||||||
|
final Consumer<ImmutableTransactionPoolConfiguration.Builder> configConsumer) { |
||||||
|
final ImmutableTransactionPoolConfiguration.Builder configBuilder = |
||||||
|
ImmutableTransactionPoolConfiguration.builder(); |
||||||
|
configConsumer.accept(configBuilder); |
||||||
|
poolConfig = configBuilder.build(); |
||||||
|
|
||||||
|
final TransactionPoolReplacementHandler transactionReplacementHandler = |
||||||
|
new TransactionPoolReplacementHandler(poolConfig.getPriceBump()); |
||||||
|
|
||||||
|
final BiFunction<PendingTransaction, PendingTransaction, Boolean> transactionReplacementTester = |
||||||
|
(t1, t2) -> |
||||||
|
transactionReplacementHandler.shouldReplace( |
||||||
|
t1, t2, protocolContext.getBlockchain().getChainHeadHeader()); |
||||||
|
|
||||||
|
transactions = spy(createPendingTransactionsSorter(poolConfig, transactionReplacementTester)); |
||||||
|
|
||||||
|
transactionBroadcaster = |
||||||
|
spy( |
||||||
|
new TransactionBroadcaster( |
||||||
|
ethContext, |
||||||
|
transactions, |
||||||
|
peerTransactionTracker, |
||||||
|
transactionsMessageSender, |
||||||
|
newPooledTransactionHashesMessageSender)); |
||||||
|
|
||||||
|
return new TransactionPool( |
||||||
|
transactions, |
||||||
|
protocolSchedule, |
||||||
|
protocolContext, |
||||||
|
transactionBroadcaster, |
||||||
|
ethContext, |
||||||
|
miningParameters, |
||||||
|
new TransactionPoolMetrics(metricsSystem), |
||||||
|
poolConfig); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void localTransactionHappyPath() { |
||||||
|
final Transaction transaction = createTransaction(0); |
||||||
|
|
||||||
|
givenTransactionIsValid(transaction); |
||||||
|
|
||||||
|
addAndAssertLocalTransactionValid(transaction); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void shouldReturnExclusivelyLocalTransactionsWhenAppropriate() { |
||||||
|
final Transaction localTransaction0 = createTransaction(0, KEY_PAIR2); |
||||||
|
|
||||||
|
givenTransactionIsValid(localTransaction0); |
||||||
|
givenTransactionIsValid(transaction0); |
||||||
|
givenTransactionIsValid(transaction1); |
||||||
|
|
||||||
|
addAndAssertLocalTransactionValid(localTransaction0); |
||||||
|
addAndAssertRemoteTransactionValid(transaction0); |
||||||
|
addAndAssertRemoteTransactionValid(transaction1); |
||||||
|
|
||||||
|
assertThat(transactions.size()).isEqualTo(3); |
||||||
|
List<Transaction> localTransactions = transactions.getLocalTransactions(); |
||||||
|
assertThat(localTransactions.size()).isEqualTo(1); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void shouldRemoveTransactionsFromPendingListWhenIncludedInBlockOnchain() { |
||||||
|
transactions.addRemoteTransaction(transaction0, Optional.empty()); |
||||||
|
assertTransactionPending(transaction0); |
||||||
|
appendBlock(transaction0); |
||||||
|
|
||||||
|
assertTransactionNotPending(transaction0); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void shouldRemoveMultipleTransactionsAddedInOneBlock() { |
||||||
|
transactions.addRemoteTransaction(transaction0, Optional.empty()); |
||||||
|
transactions.addRemoteTransaction(transaction1, Optional.empty()); |
||||||
|
appendBlock(transaction0, transaction1); |
||||||
|
|
||||||
|
assertTransactionNotPending(transaction0); |
||||||
|
assertTransactionNotPending(transaction1); |
||||||
|
assertThat(transactions.size()).isZero(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void shouldIgnoreUnknownTransactionsThatAreAddedInABlock() { |
||||||
|
transactions.addRemoteTransaction(transaction0, Optional.empty()); |
||||||
|
appendBlock(transaction0, transaction1); |
||||||
|
|
||||||
|
assertTransactionNotPending(transaction0); |
||||||
|
assertTransactionNotPending(transaction1); |
||||||
|
assertThat(transactions.size()).isZero(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void shouldNotRemovePendingTransactionsWhenABlockAddedToAFork() { |
||||||
|
transactions.addRemoteTransaction(transaction0, Optional.empty()); |
||||||
|
final BlockHeader commonParent = getHeaderForCurrentChainHead(); |
||||||
|
final Block canonicalHead = appendBlock(Difficulty.of(1000), commonParent); |
||||||
|
appendBlock(Difficulty.ONE, commonParent, transaction0); |
||||||
|
|
||||||
|
verifyChainHeadIs(canonicalHead); |
||||||
|
|
||||||
|
assertTransactionPending(transaction0); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void shouldRemovePendingTransactionsFromAllBlocksOnAForkWhenItBecomesTheCanonicalChain() { |
||||||
|
transactions.addRemoteTransaction(transaction0, Optional.empty()); |
||||||
|
transactions.addRemoteTransaction(transaction1, Optional.empty()); |
||||||
|
final BlockHeader commonParent = getHeaderForCurrentChainHead(); |
||||||
|
final Block originalChainHead = appendBlock(Difficulty.of(1000), commonParent); |
||||||
|
|
||||||
|
final Block forkBlock1 = appendBlock(Difficulty.ONE, commonParent, transaction0); |
||||||
|
verifyChainHeadIs(originalChainHead); |
||||||
|
|
||||||
|
final Block forkBlock2 = appendBlock(Difficulty.of(2000), forkBlock1.getHeader(), transaction1); |
||||||
|
verifyChainHeadIs(forkBlock2); |
||||||
|
|
||||||
|
assertTransactionNotPending(transaction0); |
||||||
|
assertTransactionNotPending(transaction1); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void shouldReAddTransactionsFromThePreviousCanonicalHeadWhenAReorgOccurs() { |
||||||
|
givenTransactionIsValid(transaction0); |
||||||
|
givenTransactionIsValid(transactionOtherSender); |
||||||
|
transactions.addLocalTransaction(transaction0, Optional.empty()); |
||||||
|
transactions.addRemoteTransaction(transactionOtherSender, Optional.empty()); |
||||||
|
final BlockHeader commonParent = getHeaderForCurrentChainHead(); |
||||||
|
final Block originalFork1 = appendBlock(Difficulty.of(1000), commonParent, transaction0); |
||||||
|
final Block originalFork2 = |
||||||
|
appendBlock(Difficulty.ONE, originalFork1.getHeader(), transactionOtherSender); |
||||||
|
assertTransactionNotPending(transaction0); |
||||||
|
assertTransactionNotPending(transactionOtherSender); |
||||||
|
assertThat(transactions.getLocalTransactions()).isEmpty(); |
||||||
|
|
||||||
|
final Block reorgFork1 = appendBlock(Difficulty.ONE, commonParent); |
||||||
|
verifyChainHeadIs(originalFork2); |
||||||
|
|
||||||
|
transactions.subscribePendingTransactions(listener); |
||||||
|
final Block reorgFork2 = appendBlock(Difficulty.of(2000), reorgFork1.getHeader()); |
||||||
|
verifyChainHeadIs(reorgFork2); |
||||||
|
|
||||||
|
assertTransactionPending(transaction0); |
||||||
|
assertTransactionPending(transactionOtherSender); |
||||||
|
assertThat(transactions.getLocalTransactions()).contains(transaction0); |
||||||
|
assertThat(transactions.getLocalTransactions()).doesNotContain(transactionOtherSender); |
||||||
|
verify(listener).onTransactionAdded(transaction0); |
||||||
|
verify(listener).onTransactionAdded(transactionOtherSender); |
||||||
|
verifyNoMoreInteractions(listener); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void shouldNotReAddTransactionsThatAreInBothForksWhenReorgHappens() { |
||||||
|
givenTransactionIsValid(transaction0); |
||||||
|
givenTransactionIsValid(transaction1); |
||||||
|
transactions.addRemoteTransaction(transaction0, Optional.empty()); |
||||||
|
transactions.addRemoteTransaction(transaction1, Optional.empty()); |
||||||
|
final BlockHeader commonParent = getHeaderForCurrentChainHead(); |
||||||
|
final Block originalFork1 = appendBlock(Difficulty.of(1000), commonParent, transaction0); |
||||||
|
final Block originalFork2 = |
||||||
|
appendBlock(Difficulty.ONE, originalFork1.getHeader(), transaction1); |
||||||
|
assertTransactionNotPending(transaction0); |
||||||
|
assertTransactionNotPending(transaction1); |
||||||
|
|
||||||
|
final Block reorgFork1 = appendBlock(Difficulty.ONE, commonParent, transaction1); |
||||||
|
verifyChainHeadIs(originalFork2); |
||||||
|
|
||||||
|
final Block reorgFork2 = appendBlock(Difficulty.of(2000), reorgFork1.getHeader()); |
||||||
|
verifyChainHeadIs(reorgFork2); |
||||||
|
|
||||||
|
assertTransactionPending(transaction0); |
||||||
|
assertTransactionNotPending(transaction1); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void addLocalTransaction_strictReplayProtectionOn_txWithChainId_chainIdIsConfigured() { |
||||||
|
protocolSupportsTxReplayProtection(1337, true); |
||||||
|
transactionPool = createTransactionPool(b -> b.strictTransactionReplayProtectionEnabled(true)); |
||||||
|
final Transaction tx = createTransaction(0); |
||||||
|
givenTransactionIsValid(tx); |
||||||
|
|
||||||
|
addAndAssertLocalTransactionValid(tx); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void addRemoteTransactions_strictReplayProtectionOn_txWithChainId_chainIdIsConfigured() { |
||||||
|
protocolSupportsTxReplayProtection(1337, true); |
||||||
|
transactionPool = createTransactionPool(b -> b.strictTransactionReplayProtectionEnabled(true)); |
||||||
|
final Transaction tx = createTransaction(0); |
||||||
|
givenTransactionIsValid(tx); |
||||||
|
|
||||||
|
addAndAssertRemoteTransactionValid(tx); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void shouldNotAddRemoteTransactionsWhenGasPriceBelowMinimum() { |
||||||
|
final Transaction transaction = createTransaction(1, Wei.ONE); |
||||||
|
transactionPool.addRemoteTransactions(singletonList(transaction)); |
||||||
|
|
||||||
|
assertTransactionNotPending(transaction); |
||||||
|
verifyNoMoreInteractions(transactionValidator); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void shouldNotAddRemoteTransactionsThatAreInvalidAccordingToStateDependentChecks() { |
||||||
|
givenTransactionIsValid(transaction0); |
||||||
|
givenTransactionIsValid(transaction1); |
||||||
|
when(transactionValidator.validateForSender( |
||||||
|
eq(transaction1), eq(null), any(TransactionValidationParams.class))) |
||||||
|
.thenReturn(ValidationResult.invalid(NONCE_TOO_LOW)); |
||||||
|
transactionPool.addRemoteTransactions(asList(transaction0, transaction1)); |
||||||
|
|
||||||
|
assertTransactionPending(transaction0); |
||||||
|
assertTransactionNotPending(transaction1); |
||||||
|
verify(transactionBroadcaster).onTransactionsAdded(singletonList(transaction0)); |
||||||
|
verify(transactionValidator).validate(eq(transaction0), any(Optional.class), any()); |
||||||
|
verify(transactionValidator) |
||||||
|
.validateForSender(eq(transaction0), eq(null), any(TransactionValidationParams.class)); |
||||||
|
verify(transactionValidator).validate(eq(transaction1), any(Optional.class), any()); |
||||||
|
verify(transactionValidator).validateForSender(eq(transaction1), any(), any()); |
||||||
|
verifyNoMoreInteractions(transactionValidator); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void shouldAllowSequenceOfTransactionsWithIncreasingNonceFromSameSender() { |
||||||
|
final Transaction transaction1 = createTransaction(0); |
||||||
|
final Transaction transaction2 = createTransaction(1); |
||||||
|
final Transaction transaction3 = createTransaction(2); |
||||||
|
|
||||||
|
givenTransactionIsValid(transaction1); |
||||||
|
givenTransactionIsValid(transaction2); |
||||||
|
givenTransactionIsValid(transaction3); |
||||||
|
|
||||||
|
addAndAssertLocalTransactionValid(transaction1); |
||||||
|
addAndAssertLocalTransactionValid(transaction2); |
||||||
|
addAndAssertLocalTransactionValid(transaction3); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void |
||||||
|
shouldAllowSequenceOfTransactionsWithIncreasingNonceFromSameSenderWhenSentInBatchOutOfOrder() { |
||||||
|
final Transaction transaction1 = createTransaction(0); |
||||||
|
final Transaction transaction2 = createTransaction(1); |
||||||
|
final Transaction transaction3 = createTransaction(2); |
||||||
|
|
||||||
|
givenTransactionIsValid(transaction1); |
||||||
|
givenTransactionIsValid(transaction2); |
||||||
|
givenTransactionIsValid(transaction3); |
||||||
|
|
||||||
|
transactionPool.addRemoteTransactions(List.of(transaction3, transaction1, transaction2)); |
||||||
|
assertRemoteTransactionValid(transaction3); |
||||||
|
assertRemoteTransactionValid(transaction1); |
||||||
|
assertRemoteTransactionValid(transaction2); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void shouldDiscardRemoteTransactionThatAlreadyExistsBeforeValidation() { |
||||||
|
doReturn(true).when(transactions).containsTransaction(transaction0); |
||||||
|
transactionPool.addRemoteTransactions(singletonList(transaction0)); |
||||||
|
|
||||||
|
verify(transactions).containsTransaction(transaction0); |
||||||
|
verifyNoInteractions(transactionValidator); |
||||||
|
verifyNoMoreInteractions(transactions); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void shouldNotNotifyBatchListenerWhenRemoteTransactionDoesNotReplaceExisting() { |
||||||
|
final Transaction transaction1 = createTransaction(0, Wei.of(100)); |
||||||
|
final Transaction transaction2 = createTransaction(0, Wei.of(50)); |
||||||
|
|
||||||
|
givenTransactionIsValid(transaction1); |
||||||
|
givenTransactionIsValid(transaction2); |
||||||
|
|
||||||
|
addAndAssertRemoteTransactionValid(transaction1); |
||||||
|
addAndAssertRemoteTransactionInvalid(transaction2); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void shouldNotNotifyBatchListenerWhenLocalTransactionDoesNotReplaceExisting() { |
||||||
|
final Transaction transaction1 = createTransaction(0, Wei.of(10)); |
||||||
|
final Transaction transaction2 = createTransaction(0, Wei.of(9)); |
||||||
|
|
||||||
|
givenTransactionIsValid(transaction1); |
||||||
|
givenTransactionIsValid(transaction2); |
||||||
|
|
||||||
|
addAndAssertLocalTransactionValid(transaction1); |
||||||
|
addAndAssertLocalTransactionInvalid(transaction2, TRANSACTION_REPLACEMENT_UNDERPRICED); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void shouldRejectLocalTransactionsWhereGasLimitExceedBlockGasLimit() { |
||||||
|
final Transaction transaction1 = |
||||||
|
createBaseTransaction(0).gasLimit(blockGasLimit + 1).createTransaction(KEY_PAIR1); |
||||||
|
|
||||||
|
givenTransactionIsValid(transaction1); |
||||||
|
|
||||||
|
addAndAssertLocalTransactionInvalid(transaction1, EXCEEDS_BLOCK_GAS_LIMIT); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void shouldRejectRemoteTransactionsWhereGasLimitExceedBlockGasLimit() { |
||||||
|
final Transaction transaction1 = |
||||||
|
createBaseTransaction(0).gasLimit(blockGasLimit + 1).createTransaction(KEY_PAIR1); |
||||||
|
|
||||||
|
givenTransactionIsValid(transaction1); |
||||||
|
|
||||||
|
addAndAssertRemoteTransactionInvalid(transaction1); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void |
||||||
|
shouldAcceptAsPostponedLocalTransactionsEvenIfAnInvalidTransactionWithLowerNonceExists() { |
||||||
|
final Transaction invalidTx = |
||||||
|
createBaseTransaction(0).gasLimit(blockGasLimit + 1).createTransaction(KEY_PAIR1); |
||||||
|
|
||||||
|
final Transaction nextTx = createBaseTransaction(1).gasLimit(1).createTransaction(KEY_PAIR1); |
||||||
|
|
||||||
|
givenTransactionIsValid(invalidTx); |
||||||
|
givenTransactionIsValid(nextTx); |
||||||
|
|
||||||
|
addAndAssertLocalTransactionInvalid(invalidTx, EXCEEDS_BLOCK_GAS_LIMIT); |
||||||
|
final ValidationResult<TransactionInvalidReason> result = |
||||||
|
transactionPool.addLocalTransaction(nextTx); |
||||||
|
|
||||||
|
assertThat(result.isValid()).isTrue(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void shouldRejectLocalTransactionsWhenNonceTooFarInFuture() { |
||||||
|
final Transaction transaction1 = createTransaction(Integer.MAX_VALUE); |
||||||
|
|
||||||
|
givenTransactionIsValid(transaction1); |
||||||
|
|
||||||
|
addAndAssertLocalTransactionInvalid(transaction1, NONCE_TOO_FAR_IN_FUTURE_FOR_SENDER); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void shouldNotNotifyBatchListenerIfNoTransactionsAreAdded() { |
||||||
|
transactionPool.addRemoteTransactions(emptyList()); |
||||||
|
verifyNoInteractions(transactionBroadcaster); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void shouldSendPooledTransactionHashesIfPeerSupportsEth65() { |
||||||
|
EthPeer peer = mock(EthPeer.class); |
||||||
|
when(peer.hasSupportForMessage(EthPV65.NEW_POOLED_TRANSACTION_HASHES)).thenReturn(true); |
||||||
|
|
||||||
|
givenTransactionIsValid(transaction0); |
||||||
|
transactionPool.addLocalTransaction(transaction0); |
||||||
|
transactionPool.handleConnect(peer); |
||||||
|
syncTaskCapture.getValue().run(); |
||||||
|
verify(newPooledTransactionHashesMessageSender).sendTransactionHashesToPeer(peer); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void shouldSendFullTransactionsIfPeerDoesNotSupportEth65() { |
||||||
|
EthPeer peer = mock(EthPeer.class); |
||||||
|
when(peer.hasSupportForMessage(EthPV65.NEW_POOLED_TRANSACTION_HASHES)).thenReturn(false); |
||||||
|
|
||||||
|
givenTransactionIsValid(transaction0); |
||||||
|
transactionPool.addLocalTransaction(transaction0); |
||||||
|
transactionPool.handleConnect(peer); |
||||||
|
syncTaskCapture.getValue().run(); |
||||||
|
verify(transactionsMessageSender).sendTransactionsToPeer(peer); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void shouldSendFullTransactionPoolToNewlyConnectedPeer() { |
||||||
|
final Transaction transactionLocal = createTransaction(0); |
||||||
|
final Transaction transactionRemote = createTransaction(1); |
||||||
|
|
||||||
|
givenTransactionIsValid(transactionLocal); |
||||||
|
givenTransactionIsValid(transactionRemote); |
||||||
|
|
||||||
|
transactionPool.addLocalTransaction(transactionLocal); |
||||||
|
transactionPool.addRemoteTransactions(Collections.singletonList(transactionRemote)); |
||||||
|
|
||||||
|
RespondingEthPeer peer = EthProtocolManagerTestUtil.createPeer(ethProtocolManager); |
||||||
|
|
||||||
|
Set<Transaction> transactionsToSendToPeer = |
||||||
|
peerTransactionTracker.claimTransactionsToSendToPeer(peer.getEthPeer()); |
||||||
|
|
||||||
|
assertThat(transactionsToSendToPeer).contains(transactionLocal, transactionRemote); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void shouldCallValidatorWithExpectedValidationParameters() { |
||||||
|
final ArgumentCaptor<TransactionValidationParams> txValidationParamCaptor = |
||||||
|
ArgumentCaptor.forClass(TransactionValidationParams.class); |
||||||
|
|
||||||
|
when(transactionValidator.validate(eq(transaction0), any(Optional.class), any())) |
||||||
|
.thenReturn(valid()); |
||||||
|
when(transactionValidator.validateForSender(any(), any(), txValidationParamCaptor.capture())) |
||||||
|
.thenReturn(valid()); |
||||||
|
|
||||||
|
final TransactionValidationParams expectedValidationParams = |
||||||
|
TransactionValidationParams.transactionPool(); |
||||||
|
|
||||||
|
transactionPool.addLocalTransaction(transaction0); |
||||||
|
|
||||||
|
assertThat(txValidationParamCaptor.getValue()) |
||||||
|
.usingRecursiveComparison() |
||||||
|
.isEqualTo(expectedValidationParams); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void shouldIgnoreFeeCapIfSetZero() { |
||||||
|
final Wei twoEthers = Wei.fromEth(2); |
||||||
|
transactionPool = createTransactionPool(b -> b.txFeeCap(Wei.ZERO)); |
||||||
|
final Transaction transaction = createTransaction(0, twoEthers.add(Wei.of(1))); |
||||||
|
|
||||||
|
givenTransactionIsValid(transaction); |
||||||
|
|
||||||
|
addAndAssertLocalTransactionValid(transaction); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void shouldRejectLocalTransactionIfFeeCapExceeded() { |
||||||
|
final Wei twoEthers = Wei.fromEth(2); |
||||||
|
transactionPool = createTransactionPool(b -> b.txFeeCap(twoEthers)); |
||||||
|
|
||||||
|
final Transaction transactionLocal = createTransaction(0, twoEthers.add(1)); |
||||||
|
|
||||||
|
givenTransactionIsValid(transactionLocal); |
||||||
|
|
||||||
|
addAndAssertLocalTransactionInvalid(transactionLocal, TX_FEECAP_EXCEEDED); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void shouldRejectZeroGasPriceTransactionWhenNotMining() { |
||||||
|
when(miningParameters.isMiningEnabled()).thenReturn(false); |
||||||
|
|
||||||
|
final Transaction transaction = createTransaction(0, Wei.ZERO); |
||||||
|
|
||||||
|
givenTransactionIsValid(transaction); |
||||||
|
|
||||||
|
addAndAssertLocalTransactionInvalid(transaction, GAS_PRICE_TOO_LOW); |
||||||
|
} |
||||||
|
|
||||||
|
private void assertTransactionPending(final Transaction t) { |
||||||
|
assertThat(transactions.getTransactionByHash(t.getHash())).contains(t); |
||||||
|
} |
||||||
|
|
||||||
|
private void assertTransactionNotPending(final Transaction transaction) { |
||||||
|
assertThat(transactions.getTransactionByHash(transaction.getHash())).isEmpty(); |
||||||
|
} |
||||||
|
|
||||||
|
private void verifyChainHeadIs(final Block forkBlock2) { |
||||||
|
assertThat(blockchain.getChainHeadHash()).isEqualTo(forkBlock2.getHash()); |
||||||
|
} |
||||||
|
|
||||||
|
private void appendBlock(final Transaction... transactionsToAdd) { |
||||||
|
appendBlock(Difficulty.ONE, getHeaderForCurrentChainHead(), transactionsToAdd); |
||||||
|
} |
||||||
|
|
||||||
|
private BlockHeader getHeaderForCurrentChainHead() { |
||||||
|
return blockchain.getBlockHeader(blockchain.getChainHeadHash()).get(); |
||||||
|
} |
||||||
|
|
||||||
|
protected abstract Block appendBlock( |
||||||
|
final Difficulty difficulty, |
||||||
|
final BlockHeader parentBlock, |
||||||
|
final Transaction... transactionsToAdd); |
||||||
|
|
||||||
|
protected abstract Transaction createTransaction( |
||||||
|
final int nonce, final Optional<BigInteger> maybeChainId); |
||||||
|
|
||||||
|
protected abstract Transaction createTransaction(final int nonce, final Wei maxPrice); |
||||||
|
|
||||||
|
protected abstract TransactionTestFixture createBaseTransaction(final int nonce); |
||||||
|
|
||||||
|
private Transaction createTransaction(final int nonce) { |
||||||
|
return createTransaction(nonce, Optional.of(BigInteger.ONE)); |
||||||
|
} |
||||||
|
|
||||||
|
private Transaction createTransaction(final int nonce, final KeyPair keyPair) { |
||||||
|
return createBaseTransaction(nonce).createTransaction(keyPair); |
||||||
|
} |
||||||
|
|
||||||
|
protected void protocolSupportsTxReplayProtection( |
||||||
|
final long chainId, final boolean isSupportedAtCurrentBlock) { |
||||||
|
when(protocolSpec.isReplayProtectionSupported()).thenReturn(isSupportedAtCurrentBlock); |
||||||
|
when(protocolSchedule.getChainId()).thenReturn(Optional.of(BigInteger.valueOf(chainId))); |
||||||
|
} |
||||||
|
|
||||||
|
protected void givenTransactionIsValid(final Transaction transaction) { |
||||||
|
when(transactionValidator.validate(eq(transaction), any(Optional.class), any())) |
||||||
|
.thenReturn(valid()); |
||||||
|
when(transactionValidator.validateForSender( |
||||||
|
eq(transaction), nullable(Account.class), any(TransactionValidationParams.class))) |
||||||
|
.thenReturn(valid()); |
||||||
|
} |
||||||
|
|
||||||
|
protected void addAndAssertLocalTransactionInvalid( |
||||||
|
final Transaction tx, final TransactionInvalidReason invalidReason) { |
||||||
|
final ValidationResult<TransactionInvalidReason> result = |
||||||
|
transactionPool.addLocalTransaction(tx); |
||||||
|
|
||||||
|
assertThat(result.isValid()).isFalse(); |
||||||
|
assertThat(result.getInvalidReason()).isEqualTo(invalidReason); |
||||||
|
assertTransactionNotPending(tx); |
||||||
|
verify(transactionBroadcaster, never()).onTransactionsAdded(singletonList(tx)); |
||||||
|
} |
||||||
|
|
||||||
|
protected void addAndAssertLocalTransactionValid(final Transaction tx) { |
||||||
|
final ValidationResult<TransactionInvalidReason> result = |
||||||
|
transactionPool.addLocalTransaction(tx); |
||||||
|
|
||||||
|
assertThat(result.isValid()).isTrue(); |
||||||
|
assertTransactionPending(tx); |
||||||
|
verify(transactionBroadcaster).onTransactionsAdded(singletonList(tx)); |
||||||
|
assertThat(transactions.getLocalTransactions()).contains(tx); |
||||||
|
} |
||||||
|
|
||||||
|
protected void addAndAssertRemoteTransactionValid(final Transaction tx) { |
||||||
|
transactionPool.addRemoteTransactions(List.of(tx)); |
||||||
|
|
||||||
|
assertRemoteTransactionValid(tx); |
||||||
|
} |
||||||
|
|
||||||
|
protected void assertRemoteTransactionValid(final Transaction tx) { |
||||||
|
verify(transactionBroadcaster) |
||||||
|
.onTransactionsAdded(argThat((List<Transaction> list) -> list.contains(tx))); |
||||||
|
assertTransactionPending(tx); |
||||||
|
assertThat(transactions.getLocalTransactions()).doesNotContain(tx); |
||||||
|
} |
||||||
|
|
||||||
|
protected void addAndAssertRemoteTransactionInvalid(final Transaction tx) { |
||||||
|
transactionPool.addRemoteTransactions(List.of(tx)); |
||||||
|
|
||||||
|
verify(transactionBroadcaster, never()).onTransactionsAdded(singletonList(tx)); |
||||||
|
assertTransactionNotPending(tx); |
||||||
|
} |
||||||
|
} |
@ -1,382 +0,0 @@ |
|||||||
/* |
|
||||||
* Copyright ConsenSys AG. |
|
||||||
* |
|
||||||
* 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. See the License for the |
|
||||||
* specific language governing permissions and limitations under the License. |
|
||||||
* |
|
||||||
* SPDX-License-Identifier: Apache-2.0 |
|
||||||
*/ |
|
||||||
package org.hyperledger.besu.ethereum.eth.transactions; |
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat; |
|
||||||
import static org.hyperledger.besu.ethereum.eth.transactions.TransactionAddedStatus.ALREADY_KNOWN; |
|
||||||
import static org.mockito.Mockito.mock; |
|
||||||
import static org.mockito.Mockito.when; |
|
||||||
|
|
||||||
import org.hyperledger.besu.crypto.KeyPair; |
|
||||||
import org.hyperledger.besu.crypto.SignatureAlgorithm; |
|
||||||
import org.hyperledger.besu.crypto.SignatureAlgorithmFactory; |
|
||||||
import org.hyperledger.besu.datatypes.Wei; |
|
||||||
import org.hyperledger.besu.ethereum.core.BlockHeader; |
|
||||||
import org.hyperledger.besu.ethereum.core.Transaction; |
|
||||||
import org.hyperledger.besu.ethereum.core.TransactionTestFixture; |
|
||||||
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactions.TransactionSelectionResult; |
|
||||||
import org.hyperledger.besu.ethereum.eth.transactions.sorter.BaseFeePendingTransactionsSorter; |
|
||||||
import org.hyperledger.besu.metrics.StubMetricsSystem; |
|
||||||
import org.hyperledger.besu.plugin.data.TransactionType; |
|
||||||
import org.hyperledger.besu.testutil.TestClock; |
|
||||||
|
|
||||||
import java.time.ZoneId; |
|
||||||
import java.util.ArrayList; |
|
||||||
import java.util.List; |
|
||||||
import java.util.Optional; |
|
||||||
import java.util.function.Supplier; |
|
||||||
|
|
||||||
import com.google.common.base.Suppliers; |
|
||||||
import org.junit.Test; |
|
||||||
|
|
||||||
public class PendingMultiTypesTransactionsTest { |
|
||||||
|
|
||||||
private static final int MAX_TRANSACTIONS = 5; |
|
||||||
private static final float MAX_TRANSACTIONS_BY_SENDER_PERCENTAGE = 0.8f; // evaluates to 4
|
|
||||||
private static final Supplier<SignatureAlgorithm> SIGNATURE_ALGORITHM = |
|
||||||
Suppliers.memoize(SignatureAlgorithmFactory::getInstance)::get; |
|
||||||
private static final KeyPair KEYS1 = SIGNATURE_ALGORITHM.get().generateKeyPair(); |
|
||||||
private static final KeyPair KEYS2 = SIGNATURE_ALGORITHM.get().generateKeyPair(); |
|
||||||
private static final KeyPair KEYS3 = SIGNATURE_ALGORITHM.get().generateKeyPair(); |
|
||||||
private static final KeyPair KEYS4 = SIGNATURE_ALGORITHM.get().generateKeyPair(); |
|
||||||
private static final KeyPair KEYS5 = SIGNATURE_ALGORITHM.get().generateKeyPair(); |
|
||||||
private static final KeyPair KEYS6 = SIGNATURE_ALGORITHM.get().generateKeyPair(); |
|
||||||
private static final String ADDED_COUNTER = "transactions_added_total"; |
|
||||||
private static final String REMOTE = "remote"; |
|
||||||
private static final String LOCAL = "local"; |
|
||||||
|
|
||||||
private final BlockHeader blockHeader = mock(BlockHeader.class); |
|
||||||
|
|
||||||
private final StubMetricsSystem metricsSystem = new StubMetricsSystem(); |
|
||||||
private final BaseFeePendingTransactionsSorter transactions = |
|
||||||
new BaseFeePendingTransactionsSorter( |
|
||||||
ImmutableTransactionPoolConfiguration.builder() |
|
||||||
.txPoolMaxSize(MAX_TRANSACTIONS) |
|
||||||
.txPoolLimitByAccountPercentage(MAX_TRANSACTIONS_BY_SENDER_PERCENTAGE) |
|
||||||
.build(), |
|
||||||
TestClock.system(ZoneId.systemDefault()), |
|
||||||
metricsSystem, |
|
||||||
() -> mockBlockHeader(Wei.of(7L))); |
|
||||||
|
|
||||||
@Test |
|
||||||
public void shouldReturnExclusivelyLocal1559TransactionsWhenAppropriate() { |
|
||||||
final Transaction localTransaction0 = create1559Transaction(0, 19, 20, KEYS1); |
|
||||||
transactions.addLocalTransaction(localTransaction0, Optional.empty()); |
|
||||||
assertThat(transactions.size()).isEqualTo(1); |
|
||||||
|
|
||||||
List<Transaction> localTransactions = transactions.getLocalTransactions(); |
|
||||||
assertThat(localTransactions.size()).isEqualTo(1); |
|
||||||
|
|
||||||
final Transaction remoteTransaction1 = create1559Transaction(1, 19, 20, KEYS1); |
|
||||||
transactions.addRemoteTransaction(remoteTransaction1, Optional.empty()); |
|
||||||
assertThat(transactions.size()).isEqualTo(2); |
|
||||||
|
|
||||||
localTransactions = transactions.getLocalTransactions(); |
|
||||||
assertThat(localTransactions.size()).isEqualTo(1); |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
public void shouldReplaceTransactionWithLowestMaxFeePerGas() { |
|
||||||
final Transaction localTransaction0 = create1559Transaction(0, 200, 20, KEYS1); |
|
||||||
final Transaction localTransaction1 = create1559Transaction(0, 190, 20, KEYS2); |
|
||||||
final Transaction localTransaction2 = create1559Transaction(0, 220, 20, KEYS3); |
|
||||||
final Transaction localTransaction3 = create1559Transaction(0, 240, 20, KEYS4); |
|
||||||
final Transaction localTransaction4 = create1559Transaction(0, 260, 20, KEYS5); |
|
||||||
final Transaction localTransaction5 = create1559Transaction(0, 900, 20, KEYS6); |
|
||||||
transactions.addLocalTransaction(localTransaction0, Optional.empty()); |
|
||||||
transactions.addLocalTransaction(localTransaction1, Optional.empty()); |
|
||||||
transactions.addLocalTransaction(localTransaction2, Optional.empty()); |
|
||||||
transactions.addLocalTransaction(localTransaction3, Optional.empty()); |
|
||||||
transactions.addLocalTransaction(localTransaction4, Optional.empty()); |
|
||||||
|
|
||||||
transactions.updateBaseFee(Wei.of(300L)); |
|
||||||
|
|
||||||
transactions.addLocalTransaction(localTransaction5, Optional.empty()); |
|
||||||
assertThat(transactions.size()).isEqualTo(5); |
|
||||||
|
|
||||||
transactions.selectTransactions( |
|
||||||
transaction -> { |
|
||||||
assertThat(transaction.getNonce()).isNotEqualTo(1); |
|
||||||
return TransactionSelectionResult.CONTINUE; |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
public void shouldEvictTransactionWithLowestMaxFeePerGasAndLowestTip() { |
|
||||||
final Transaction localTransaction0 = create1559Transaction(0, 200, 20, KEYS1); |
|
||||||
final Transaction localTransaction1 = create1559Transaction(0, 200, 19, KEYS2); |
|
||||||
final Transaction localTransaction2 = create1559Transaction(0, 200, 18, KEYS3); |
|
||||||
final Transaction localTransaction3 = create1559Transaction(0, 240, 20, KEYS4); |
|
||||||
final Transaction localTransaction4 = create1559Transaction(0, 260, 20, KEYS5); |
|
||||||
final Transaction localTransaction5 = create1559Transaction(0, 900, 20, KEYS6); |
|
||||||
transactions.addLocalTransaction(localTransaction0, Optional.empty()); |
|
||||||
transactions.addLocalTransaction(localTransaction1, Optional.empty()); |
|
||||||
transactions.addLocalTransaction(localTransaction2, Optional.empty()); |
|
||||||
transactions.addLocalTransaction(localTransaction3, Optional.empty()); |
|
||||||
transactions.addLocalTransaction(localTransaction4, Optional.empty()); |
|
||||||
transactions.addLocalTransaction(localTransaction5, Optional.empty()); // causes eviction
|
|
||||||
|
|
||||||
assertThat(transactions.size()).isEqualTo(5); |
|
||||||
|
|
||||||
transactions.selectTransactions( |
|
||||||
transaction -> { |
|
||||||
assertThat(transaction.getNonce()).isNotEqualTo(2); |
|
||||||
return TransactionSelectionResult.CONTINUE; |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
public void shouldEvictLegacyTransactionWithLowestEffectiveMaxPriorityFeePerGas() { |
|
||||||
final Transaction localTransaction0 = create1559Transaction(0, 200, 20, KEYS1); |
|
||||||
final Transaction localTransaction1 = createLegacyTransaction(0, 25, KEYS2); |
|
||||||
final Transaction localTransaction2 = create1559Transaction(0, 200, 18, KEYS3); |
|
||||||
final Transaction localTransaction3 = create1559Transaction(0, 240, 20, KEYS4); |
|
||||||
final Transaction localTransaction4 = create1559Transaction(0, 260, 20, KEYS5); |
|
||||||
final Transaction localTransaction5 = create1559Transaction(0, 900, 20, KEYS6); |
|
||||||
transactions.addLocalTransaction(localTransaction0, Optional.empty()); |
|
||||||
transactions.addLocalTransaction(localTransaction1, Optional.empty()); |
|
||||||
transactions.addLocalTransaction(localTransaction2, Optional.empty()); |
|
||||||
transactions.addLocalTransaction(localTransaction3, Optional.empty()); |
|
||||||
transactions.addLocalTransaction(localTransaction4, Optional.empty()); |
|
||||||
transactions.addLocalTransaction(localTransaction5, Optional.empty()); // causes eviction
|
|
||||||
assertThat(transactions.size()).isEqualTo(5); |
|
||||||
|
|
||||||
transactions.selectTransactions( |
|
||||||
transaction -> { |
|
||||||
assertThat(transaction.getNonce()).isNotEqualTo(1); |
|
||||||
return TransactionSelectionResult.CONTINUE; |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
public void shouldEvictEIP1559TransactionWithLowestEffectiveMaxPriorityFeePerGas() { |
|
||||||
final Transaction localTransaction0 = create1559Transaction(0, 200, 20, KEYS1); |
|
||||||
final Transaction localTransaction1 = createLegacyTransaction(0, 26, KEYS2); |
|
||||||
final Transaction localTransaction2 = create1559Transaction(0, 200, 18, KEYS3); |
|
||||||
final Transaction localTransaction3 = create1559Transaction(0, 240, 20, KEYS4); |
|
||||||
final Transaction localTransaction4 = create1559Transaction(0, 260, 20, KEYS5); |
|
||||||
final Transaction localTransaction5 = create1559Transaction(0, 900, 20, KEYS6); |
|
||||||
transactions.addLocalTransaction(localTransaction0, Optional.empty()); |
|
||||||
transactions.addLocalTransaction(localTransaction1, Optional.empty()); |
|
||||||
transactions.addLocalTransaction(localTransaction2, Optional.empty()); |
|
||||||
transactions.addLocalTransaction(localTransaction3, Optional.empty()); |
|
||||||
transactions.addLocalTransaction(localTransaction4, Optional.empty()); |
|
||||||
transactions.addLocalTransaction(localTransaction5, Optional.empty()); // causes eviction
|
|
||||||
assertThat(transactions.size()).isEqualTo(5); |
|
||||||
|
|
||||||
transactions.selectTransactions( |
|
||||||
transaction -> { |
|
||||||
assertThat(transaction.getNonce()).isNotEqualTo(2); |
|
||||||
return TransactionSelectionResult.CONTINUE; |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
public void shouldChangePriorityWhenBaseFeeIncrease() { |
|
||||||
final Transaction localTransaction0 = create1559Transaction(1, 200, 18, KEYS1); |
|
||||||
final Transaction localTransaction1 = create1559Transaction(1, 100, 20, KEYS2); |
|
||||||
final Transaction localTransaction2 = create1559Transaction(2, 100, 19, KEYS2); |
|
||||||
|
|
||||||
transactions.addLocalTransaction(localTransaction0, Optional.empty()); |
|
||||||
transactions.addLocalTransaction(localTransaction1, Optional.empty()); |
|
||||||
transactions.addLocalTransaction(localTransaction2, Optional.empty()); |
|
||||||
|
|
||||||
final List<Transaction> iterationOrder = new ArrayList<>(); |
|
||||||
transactions.selectTransactions( |
|
||||||
transaction -> { |
|
||||||
iterationOrder.add(transaction); |
|
||||||
return TransactionSelectionResult.CONTINUE; |
|
||||||
}); |
|
||||||
|
|
||||||
assertThat(iterationOrder) |
|
||||||
.containsExactly(localTransaction1, localTransaction2, localTransaction0); |
|
||||||
|
|
||||||
transactions.updateBaseFee(Wei.of(110L)); |
|
||||||
|
|
||||||
final List<Transaction> iterationOrderAfterBaseIncreased = new ArrayList<>(); |
|
||||||
transactions.selectTransactions( |
|
||||||
transaction -> { |
|
||||||
iterationOrderAfterBaseIncreased.add(transaction); |
|
||||||
return TransactionSelectionResult.CONTINUE; |
|
||||||
}); |
|
||||||
|
|
||||||
assertThat(iterationOrderAfterBaseIncreased) |
|
||||||
.containsExactly(localTransaction0, localTransaction1, localTransaction2); |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
public void shouldChangePriorityWhenBaseFeeDecrease() { |
|
||||||
final Transaction localTransaction0 = create1559Transaction(1, 200, 18, KEYS1); |
|
||||||
final Transaction localTransaction1 = create1559Transaction(1, 100, 20, KEYS2); |
|
||||||
final Transaction localTransaction2 = create1559Transaction(2, 100, 19, KEYS2); |
|
||||||
|
|
||||||
transactions.updateBaseFee(Wei.of(110L)); |
|
||||||
|
|
||||||
transactions.addLocalTransaction(localTransaction0, Optional.empty()); |
|
||||||
transactions.addLocalTransaction(localTransaction1, Optional.empty()); |
|
||||||
transactions.addLocalTransaction(localTransaction2, Optional.empty()); |
|
||||||
|
|
||||||
final List<Transaction> iterationOrder = new ArrayList<>(); |
|
||||||
transactions.selectTransactions( |
|
||||||
transaction -> { |
|
||||||
iterationOrder.add(transaction); |
|
||||||
return TransactionSelectionResult.CONTINUE; |
|
||||||
}); |
|
||||||
|
|
||||||
assertThat(iterationOrder) |
|
||||||
.containsExactly(localTransaction0, localTransaction1, localTransaction2); |
|
||||||
|
|
||||||
transactions.updateBaseFee(Wei.of(50L)); |
|
||||||
|
|
||||||
final List<Transaction> iterationOrderAfterBaseIncreased = new ArrayList<>(); |
|
||||||
transactions.selectTransactions( |
|
||||||
transaction -> { |
|
||||||
iterationOrderAfterBaseIncreased.add(transaction); |
|
||||||
return TransactionSelectionResult.CONTINUE; |
|
||||||
}); |
|
||||||
|
|
||||||
assertThat(iterationOrderAfterBaseIncreased) |
|
||||||
.containsExactly(localTransaction1, localTransaction2, localTransaction0); |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
public void shouldCorrectlyPrioritizeMultipleTransactionTypesBasedOnNonce() { |
|
||||||
final Transaction localTransaction0 = create1559Transaction(1, 200, 18, KEYS1); |
|
||||||
final Transaction localTransaction1 = create1559Transaction(1, 100, 20, KEYS2); |
|
||||||
final Transaction localTransaction2 = create1559Transaction(2, 100, 19, KEYS2); |
|
||||||
final Transaction localTransaction3 = createLegacyTransaction(0, 20, KEYS1); |
|
||||||
|
|
||||||
transactions.addLocalTransaction(localTransaction0, Optional.empty()); |
|
||||||
transactions.addLocalTransaction(localTransaction1, Optional.empty()); |
|
||||||
transactions.addLocalTransaction(localTransaction2, Optional.empty()); |
|
||||||
transactions.addLocalTransaction(localTransaction3, Optional.empty()); |
|
||||||
|
|
||||||
final List<Transaction> iterationOrder = new ArrayList<>(); |
|
||||||
transactions.selectTransactions( |
|
||||||
transaction -> { |
|
||||||
iterationOrder.add(transaction); |
|
||||||
return TransactionSelectionResult.CONTINUE; |
|
||||||
}); |
|
||||||
|
|
||||||
assertThat(iterationOrder) |
|
||||||
.containsExactly( |
|
||||||
localTransaction1, localTransaction2, localTransaction3, localTransaction0); |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
public void shouldCorrectlyPrioritizeMultipleTransactionTypesBasedOnGasPayed() { |
|
||||||
final Transaction localTransaction0 = create1559Transaction(0, 100, 19, KEYS2); |
|
||||||
final Transaction localTransaction1 = createLegacyTransaction(0, 2000, KEYS1); |
|
||||||
final Transaction localTransaction2 = createLegacyTransaction(0, 20, KEYS3); |
|
||||||
final Transaction localTransaction3 = createLegacyTransaction(1, 2000, KEYS3); |
|
||||||
|
|
||||||
transactions.addLocalTransaction(localTransaction0, Optional.empty()); |
|
||||||
transactions.addLocalTransaction(localTransaction1, Optional.empty()); |
|
||||||
transactions.addLocalTransaction(localTransaction2, Optional.empty()); |
|
||||||
transactions.addLocalTransaction(localTransaction3, Optional.empty()); |
|
||||||
|
|
||||||
final List<Transaction> iterationOrder = new ArrayList<>(); |
|
||||||
transactions.selectTransactions( |
|
||||||
transaction -> { |
|
||||||
iterationOrder.add(transaction); |
|
||||||
return TransactionSelectionResult.CONTINUE; |
|
||||||
}); |
|
||||||
|
|
||||||
assertThat(iterationOrder) |
|
||||||
.containsExactly( |
|
||||||
localTransaction1, localTransaction0, localTransaction2, localTransaction3); |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
public void shouldSelectNoTransactionsIfPoolEmpty() { |
|
||||||
final List<Transaction> iterationOrder = new ArrayList<>(); |
|
||||||
transactions.selectTransactions( |
|
||||||
transaction -> { |
|
||||||
iterationOrder.add(transaction); |
|
||||||
return TransactionSelectionResult.CONTINUE; |
|
||||||
}); |
|
||||||
|
|
||||||
assertThat(iterationOrder).isEmpty(); |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
public void shouldAdd1559Transaction() { |
|
||||||
final Transaction remoteTransaction0 = create1559Transaction(0, 19, 20, KEYS1); |
|
||||||
transactions.addRemoteTransaction(remoteTransaction0, Optional.empty()); |
|
||||||
assertThat(transactions.size()).isEqualTo(1); |
|
||||||
assertThat(metricsSystem.getCounterValue(ADDED_COUNTER, REMOTE)).isEqualTo(1); |
|
||||||
|
|
||||||
final Transaction remoteTransaction1 = create1559Transaction(1, 19, 20, KEYS1); |
|
||||||
transactions.addRemoteTransaction(remoteTransaction1, Optional.empty()); |
|
||||||
assertThat(transactions.size()).isEqualTo(2); |
|
||||||
assertThat(metricsSystem.getCounterValue(ADDED_COUNTER, REMOTE)).isEqualTo(2); |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
public void shouldNotIncrementAddedCounterWhenRemote1559TransactionAlreadyPresent() { |
|
||||||
final Transaction localTransaction0 = create1559Transaction(0, 19, 20, KEYS1); |
|
||||||
transactions.addLocalTransaction(localTransaction0, Optional.empty()); |
|
||||||
assertThat(transactions.size()).isEqualTo(1); |
|
||||||
assertThat(metricsSystem.getCounterValue(ADDED_COUNTER, LOCAL)).isEqualTo(1); |
|
||||||
assertThat(metricsSystem.getCounterValue(ADDED_COUNTER, REMOTE)).isEqualTo(0); |
|
||||||
|
|
||||||
assertThat(transactions.addRemoteTransaction(localTransaction0, Optional.empty())) |
|
||||||
.isEqualTo(ALREADY_KNOWN); |
|
||||||
assertThat(transactions.size()).isEqualTo(1); |
|
||||||
assertThat(metricsSystem.getCounterValue(ADDED_COUNTER, LOCAL)).isEqualTo(1); |
|
||||||
assertThat(metricsSystem.getCounterValue(ADDED_COUNTER, REMOTE)).isEqualTo(0); |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
public void shouldAddMixedTransactions() { |
|
||||||
final Transaction remoteTransaction0 = create1559Transaction(0, 19, 20, KEYS1); |
|
||||||
transactions.addRemoteTransaction(remoteTransaction0, Optional.empty()); |
|
||||||
assertThat(transactions.size()).isEqualTo(1); |
|
||||||
assertThat(metricsSystem.getCounterValue(ADDED_COUNTER, REMOTE)).isEqualTo(1); |
|
||||||
|
|
||||||
final Transaction remoteTransaction1 = createLegacyTransaction(1, 5000, KEYS1); |
|
||||||
transactions.addRemoteTransaction(remoteTransaction1, Optional.empty()); |
|
||||||
assertThat(transactions.size()).isEqualTo(2); |
|
||||||
assertThat(metricsSystem.getCounterValue(ADDED_COUNTER, REMOTE)).isEqualTo(2); |
|
||||||
} |
|
||||||
|
|
||||||
private Transaction create1559Transaction( |
|
||||||
final long transactionNumber, |
|
||||||
final long maxFeePerGas, |
|
||||||
final long maxPriorityFeePerGas, |
|
||||||
final KeyPair keyPair) { |
|
||||||
return new TransactionTestFixture() |
|
||||||
.type(TransactionType.EIP1559) |
|
||||||
.value(Wei.of(transactionNumber)) |
|
||||||
.nonce(transactionNumber) |
|
||||||
.maxFeePerGas(Optional.of(Wei.of(maxFeePerGas))) |
|
||||||
.maxPriorityFeePerGas(Optional.of(Wei.of(maxPriorityFeePerGas))) |
|
||||||
.createTransaction(keyPair); |
|
||||||
} |
|
||||||
|
|
||||||
private Transaction createLegacyTransaction( |
|
||||||
final long transactionNumber, final long gasPrice, final KeyPair keyPair) { |
|
||||||
return new TransactionTestFixture() |
|
||||||
.value(Wei.of(transactionNumber)) |
|
||||||
.gasPrice(Wei.of(gasPrice)) |
|
||||||
.nonce(transactionNumber) |
|
||||||
.createTransaction(keyPair); |
|
||||||
} |
|
||||||
|
|
||||||
private BlockHeader mockBlockHeader(final Wei baseFee) { |
|
||||||
when(blockHeader.getBaseFee()).thenReturn(Optional.of(baseFee)); |
|
||||||
return blockHeader; |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,414 @@ |
|||||||
|
/* |
||||||
|
* Copyright Besu contributors. |
||||||
|
* |
||||||
|
* 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. See the License for the |
||||||
|
* specific language governing permissions and limitations under the License. |
||||||
|
* |
||||||
|
* SPDX-License-Identifier: Apache-2.0 |
||||||
|
*/ |
||||||
|
package org.hyperledger.besu.ethereum.eth.transactions; |
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||||
|
|
||||||
|
import org.hyperledger.besu.crypto.SignatureAlgorithm; |
||||||
|
import org.hyperledger.besu.datatypes.Address; |
||||||
|
import org.hyperledger.besu.datatypes.Wei; |
||||||
|
import org.hyperledger.besu.ethereum.core.Transaction; |
||||||
|
import org.hyperledger.besu.ethereum.core.TransactionTestFixture; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.layered.BaseTransactionPoolTest; |
||||||
|
import org.hyperledger.besu.ethereum.rlp.BytesValueRLPInput; |
||||||
|
import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput; |
||||||
|
import org.hyperledger.besu.evm.AccessListEntry; |
||||||
|
import org.hyperledger.besu.plugin.data.TransactionType; |
||||||
|
|
||||||
|
import java.util.HashSet; |
||||||
|
import java.util.List; |
||||||
|
import java.util.Optional; |
||||||
|
import java.util.Set; |
||||||
|
import java.util.concurrent.atomic.LongAdder; |
||||||
|
|
||||||
|
import org.apache.tuweni.bytes.Bytes; |
||||||
|
import org.apache.tuweni.bytes.Bytes32; |
||||||
|
import org.junit.jupiter.api.Disabled; |
||||||
|
import org.junit.jupiter.api.Test; |
||||||
|
import org.openjdk.jol.info.ClassLayout; |
||||||
|
import org.openjdk.jol.info.GraphPathRecord; |
||||||
|
import org.openjdk.jol.info.GraphVisitor; |
||||||
|
import org.openjdk.jol.info.GraphWalker; |
||||||
|
|
||||||
|
@Disabled("Need to handle different results on different OS") |
||||||
|
public class PendingTransactionEstimatedMemorySizeTest extends BaseTransactionPoolTest { |
||||||
|
private static final Set<Class<?>> SHARED_CLASSES = |
||||||
|
Set.of(SignatureAlgorithm.class, TransactionType.class); |
||||||
|
private static final Set<String> EIP1559_CONSTANT_FIELD_PATHS = Set.of(".gasPrice"); |
||||||
|
private static final Set<String> EIP1559_VARIABLE_SIZE_PATHS = |
||||||
|
Set.of(".to", ".payload", ".maybeAccessList"); |
||||||
|
|
||||||
|
private static final Set<String> FRONTIER_ACCESS_LIST_CONSTANT_FIELD_PATHS = |
||||||
|
Set.of(".maxFeePerGas", ".maxPriorityFeePerGas"); |
||||||
|
private static final Set<String> FRONTIER_ACCESS_LIST_VARIABLE_SIZE_PATHS = |
||||||
|
Set.of(".to", ".payload", ".maybeAccessList"); |
||||||
|
|
||||||
|
@Test |
||||||
|
public void toSize() { |
||||||
|
TransactionTestFixture preparedTx = |
||||||
|
prepareTransaction(TransactionType.ACCESS_LIST, 10, Wei.of(500), 10); |
||||||
|
Transaction txTo = |
||||||
|
preparedTx.to(Optional.of(Address.extract(Bytes32.random()))).createTransaction(KEYS1); |
||||||
|
BytesValueRLPOutput rlpOut = new BytesValueRLPOutput(); |
||||||
|
txTo.writeTo(rlpOut); |
||||||
|
|
||||||
|
txTo = Transaction.readFrom(new BytesValueRLPInput(rlpOut.encoded(), false)); |
||||||
|
System.out.println(txTo.getSender()); |
||||||
|
System.out.println(txTo.getHash()); |
||||||
|
System.out.println(txTo.getSize()); |
||||||
|
|
||||||
|
Optional<Address> to = txTo.getTo(); |
||||||
|
final ClassLayout cl = ClassLayout.parseInstance(to); |
||||||
|
System.out.println(cl.toPrintable()); |
||||||
|
LongAdder size = new LongAdder(); |
||||||
|
size.add(cl.instanceSize()); |
||||||
|
System.out.println(size); |
||||||
|
|
||||||
|
GraphVisitor gv = |
||||||
|
gpr -> { |
||||||
|
// byte[] is shared so only count the specific part for each field
|
||||||
|
if (gpr.path().endsWith(".bytes")) { |
||||||
|
if (gpr.path().contains("delegate")) { |
||||||
|
size.add(20); |
||||||
|
System.out.println( |
||||||
|
"(" |
||||||
|
+ size |
||||||
|
+ ")[20 = fixed address size; overrides: " |
||||||
|
+ gpr.size() |
||||||
|
+ ", " |
||||||
|
+ gpr.path() |
||||||
|
+ ", " |
||||||
|
+ gpr.klass().toString() |
||||||
|
+ "]"); |
||||||
|
} |
||||||
|
} else { |
||||||
|
size.add(gpr.size()); |
||||||
|
System.out.println( |
||||||
|
"(" |
||||||
|
+ size |
||||||
|
+ ")[" |
||||||
|
+ gpr.size() |
||||||
|
+ ", " |
||||||
|
+ gpr.path() |
||||||
|
+ ", " |
||||||
|
+ gpr.klass().toString() |
||||||
|
+ "]"); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
GraphWalker gw = new GraphWalker(gv); |
||||||
|
|
||||||
|
gw.walk(to); |
||||||
|
|
||||||
|
System.out.println("Optional To size: " + size); |
||||||
|
|
||||||
|
assertThat(size.sum()).isEqualTo(PendingTransaction.OPTIONAL_TO_MEMORY_SIZE); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void payloadSize() { |
||||||
|
|
||||||
|
TransactionTestFixture preparedTx = |
||||||
|
prepareTransaction(TransactionType.ACCESS_LIST, 10, Wei.of(500), 10); |
||||||
|
Transaction txPayload = preparedTx.createTransaction(KEYS1); |
||||||
|
BytesValueRLPOutput rlpOut = new BytesValueRLPOutput(); |
||||||
|
txPayload.writeTo(rlpOut); |
||||||
|
|
||||||
|
txPayload = Transaction.readFrom(new BytesValueRLPInput(rlpOut.encoded(), false)); |
||||||
|
System.out.println(txPayload.getSender()); |
||||||
|
System.out.println(txPayload.getHash()); |
||||||
|
System.out.println(txPayload.getSize()); |
||||||
|
|
||||||
|
final Bytes payload = txPayload.getPayload(); |
||||||
|
final ClassLayout cl = ClassLayout.parseInstance(payload); |
||||||
|
System.out.println(cl.toPrintable()); |
||||||
|
LongAdder size = new LongAdder(); |
||||||
|
size.add(cl.instanceSize()); |
||||||
|
System.out.println("Base payload size: " + size); |
||||||
|
|
||||||
|
assertThat(size.sum()).isEqualTo(PendingTransaction.PAYLOAD_BASE_MEMORY_SIZE); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void pendingTransactionSize() { |
||||||
|
|
||||||
|
TransactionTestFixture preparedTx = |
||||||
|
prepareTransaction(TransactionType.ACCESS_LIST, 10, Wei.of(500), 10); |
||||||
|
Transaction txPayload = preparedTx.createTransaction(KEYS1); |
||||||
|
BytesValueRLPOutput rlpOut = new BytesValueRLPOutput(); |
||||||
|
txPayload.writeTo(rlpOut); |
||||||
|
|
||||||
|
txPayload = Transaction.readFrom(new BytesValueRLPInput(rlpOut.encoded(), false)); |
||||||
|
System.out.println(txPayload.getSender()); |
||||||
|
System.out.println(txPayload.getHash()); |
||||||
|
System.out.println(txPayload.getSize()); |
||||||
|
|
||||||
|
final PendingTransaction pendingTx = new PendingTransaction.Remote(txPayload); |
||||||
|
|
||||||
|
final ClassLayout cl = ClassLayout.parseInstance(pendingTx); |
||||||
|
System.out.println(cl.toPrintable()); |
||||||
|
LongAdder size = new LongAdder(); |
||||||
|
size.add(cl.instanceSize()); |
||||||
|
System.out.println("PendingTransaction size: " + size); |
||||||
|
|
||||||
|
assertThat(size.sum()).isEqualTo(PendingTransaction.PENDING_TRANSACTION_MEMORY_SIZE); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void accessListSize() { |
||||||
|
System.setProperty("jol.magicFieldOffset", "true"); |
||||||
|
|
||||||
|
final AccessListEntry ale1 = |
||||||
|
new AccessListEntry(Address.extract(Bytes32.random()), List.of(Bytes32.random())); |
||||||
|
|
||||||
|
final List<AccessListEntry> ales = List.of(ale1); |
||||||
|
|
||||||
|
TransactionTestFixture preparedTx = |
||||||
|
prepareTransaction(TransactionType.ACCESS_LIST, 0, Wei.of(500), 0); |
||||||
|
Transaction txAccessList = preparedTx.accessList(ales).createTransaction(KEYS1); |
||||||
|
BytesValueRLPOutput rlpOut = new BytesValueRLPOutput(); |
||||||
|
txAccessList.writeTo(rlpOut); |
||||||
|
|
||||||
|
txAccessList = Transaction.readFrom(new BytesValueRLPInput(rlpOut.encoded(), false)); |
||||||
|
System.out.println(txAccessList.getSender()); |
||||||
|
System.out.println(txAccessList.getHash()); |
||||||
|
System.out.println(txAccessList.getSize()); |
||||||
|
|
||||||
|
final var optAL = txAccessList.getAccessList(); |
||||||
|
|
||||||
|
final ClassLayout cl1 = ClassLayout.parseInstance(optAL); |
||||||
|
System.out.println(cl1.toPrintable()); |
||||||
|
System.out.println("Optional size: " + cl1.instanceSize()); |
||||||
|
|
||||||
|
final ClassLayout cl2 = ClassLayout.parseInstance(optAL.get()); |
||||||
|
System.out.println(cl2.toPrintable()); |
||||||
|
System.out.println("Optional + list size: " + cl2.instanceSize()); |
||||||
|
|
||||||
|
assertThat(cl2.instanceSize()).isEqualTo(PendingTransaction.OPTIONAL_ACCESS_LIST_MEMORY_SIZE); |
||||||
|
|
||||||
|
final AccessListEntry ale = optAL.get().get(0); |
||||||
|
|
||||||
|
final ClassLayout cl3 = ClassLayout.parseInstance(ale); |
||||||
|
System.out.println(cl3.toPrintable()); |
||||||
|
System.out.println("AccessListEntry size: " + cl3.instanceSize()); |
||||||
|
|
||||||
|
LongAdder size = new LongAdder(); |
||||||
|
size.add(cl3.instanceSize()); |
||||||
|
|
||||||
|
GraphVisitor gv = |
||||||
|
gpr -> { |
||||||
|
// byte[] is shared so only count the specific part for each field
|
||||||
|
if (gpr.path().endsWith(".bytes")) { |
||||||
|
if (gpr.path().contains("address")) { |
||||||
|
size.add(20); |
||||||
|
System.out.println( |
||||||
|
"(" |
||||||
|
+ size |
||||||
|
+ ")[20 = fixed address size; overrides: " |
||||||
|
+ gpr.size() |
||||||
|
+ ", " |
||||||
|
+ gpr.path() |
||||||
|
+ ", " |
||||||
|
+ gpr.klass().toString() |
||||||
|
+ "]"); |
||||||
|
} |
||||||
|
} else if (!gpr.path() |
||||||
|
.contains( |
||||||
|
"storageKeys.elementData[")) { // exclude elements since we want the container
|
||||||
|
// size
|
||||||
|
size.add(gpr.size()); |
||||||
|
System.out.println( |
||||||
|
"(" |
||||||
|
+ size |
||||||
|
+ ")[" |
||||||
|
+ gpr.size() |
||||||
|
+ ", " |
||||||
|
+ gpr.path() |
||||||
|
+ ", " |
||||||
|
+ gpr.klass().toString() |
||||||
|
+ "]"); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
GraphWalker gw = new GraphWalker(gv); |
||||||
|
|
||||||
|
gw.walk(ale); |
||||||
|
|
||||||
|
System.out.println("AccessListEntry container size: " + size); |
||||||
|
|
||||||
|
assertThat(size.sum()).isEqualTo(PendingTransaction.ACCESS_LIST_ENTRY_BASE_MEMORY_SIZE); |
||||||
|
|
||||||
|
final Bytes32 storageKey = ale.getStorageKeys().get(0); |
||||||
|
final ClassLayout cl4 = ClassLayout.parseInstance(storageKey); |
||||||
|
System.out.println(cl4.toPrintable()); |
||||||
|
System.out.println("Single storage key size: " + cl4.instanceSize()); |
||||||
|
|
||||||
|
assertThat(cl4.instanceSize()) |
||||||
|
.isEqualTo(PendingTransaction.ACCESS_LIST_STORAGE_KEY_MEMORY_SIZE); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void baseEIP1559TransactionMemorySize() { |
||||||
|
System.setProperty("jol.magicFieldOffset", "true"); |
||||||
|
Transaction txEip1559 = createEIP1559Transaction(1, KEYS1, 10); |
||||||
|
BytesValueRLPOutput rlpOut = new BytesValueRLPOutput(); |
||||||
|
txEip1559.writeTo(rlpOut); |
||||||
|
|
||||||
|
txEip1559 = Transaction.readFrom(new BytesValueRLPInput(rlpOut.encoded(), false)); |
||||||
|
System.out.println(txEip1559.getSender()); |
||||||
|
System.out.println(txEip1559.getHash()); |
||||||
|
System.out.println(txEip1559.getSize()); |
||||||
|
|
||||||
|
final ClassLayout cl = ClassLayout.parseInstance(txEip1559); |
||||||
|
System.out.println(cl.toPrintable()); |
||||||
|
LongAdder eip1559size = new LongAdder(); |
||||||
|
eip1559size.add(cl.instanceSize()); |
||||||
|
System.out.println(eip1559size); |
||||||
|
|
||||||
|
final Set<String> skipPrefixes = new HashSet<>(); |
||||||
|
|
||||||
|
GraphVisitor gv = |
||||||
|
gpr -> { |
||||||
|
if (!skipPrefixes.stream().anyMatch(sp -> gpr.path().startsWith(sp))) { |
||||||
|
if (SHARED_CLASSES.stream().anyMatch(scz -> scz.isAssignableFrom(gpr.klass()))) { |
||||||
|
skipPrefixes.add(gpr.path()); |
||||||
|
} else if (!startWithAnyOf(EIP1559_CONSTANT_FIELD_PATHS, gpr) |
||||||
|
&& !startWithAnyOf(EIP1559_VARIABLE_SIZE_PATHS, gpr)) { |
||||||
|
eip1559size.add(gpr.size()); |
||||||
|
System.out.println( |
||||||
|
"(" |
||||||
|
+ eip1559size |
||||||
|
+ ")[" |
||||||
|
+ gpr.size() |
||||||
|
+ ", " |
||||||
|
+ gpr.path() |
||||||
|
+ ", " |
||||||
|
+ gpr.klass().toString() |
||||||
|
+ "]"); |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
GraphWalker gw = new GraphWalker(gv); |
||||||
|
|
||||||
|
gw.walk(txEip1559); |
||||||
|
|
||||||
|
System.out.println("Base EIP1559 size: " + eip1559size); |
||||||
|
assertThat(eip1559size.sum()).isEqualTo(PendingTransaction.EIP1559_BASE_MEMORY_SIZE); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void baseAccessListTransactionMemorySize() { |
||||||
|
System.setProperty("jol.magicFieldOffset", "true"); |
||||||
|
Transaction txAccessList = |
||||||
|
createTransaction(TransactionType.ACCESS_LIST, 1, Wei.of(500), 0, KEYS1); |
||||||
|
BytesValueRLPOutput rlpOut = new BytesValueRLPOutput(); |
||||||
|
txAccessList.writeTo(rlpOut); |
||||||
|
|
||||||
|
txAccessList = Transaction.readFrom(new BytesValueRLPInput(rlpOut.encoded(), false)); |
||||||
|
System.out.println(txAccessList.getSender()); |
||||||
|
System.out.println(txAccessList.getHash()); |
||||||
|
System.out.println(txAccessList.getSize()); |
||||||
|
|
||||||
|
final ClassLayout cl = ClassLayout.parseInstance(txAccessList); |
||||||
|
System.out.println(cl.toPrintable()); |
||||||
|
LongAdder accessListSize = new LongAdder(); |
||||||
|
accessListSize.add(cl.instanceSize()); |
||||||
|
System.out.println(accessListSize); |
||||||
|
|
||||||
|
final Set<String> skipPrefixes = new HashSet<>(); |
||||||
|
|
||||||
|
GraphVisitor gv = |
||||||
|
gpr -> { |
||||||
|
if (!skipPrefixes.stream().anyMatch(sp -> gpr.path().startsWith(sp))) { |
||||||
|
if (SHARED_CLASSES.stream().anyMatch(scz -> scz.isAssignableFrom(gpr.klass()))) { |
||||||
|
skipPrefixes.add(gpr.path()); |
||||||
|
} else if (!startWithAnyOf(FRONTIER_ACCESS_LIST_CONSTANT_FIELD_PATHS, gpr) |
||||||
|
&& !startWithAnyOf(FRONTIER_ACCESS_LIST_VARIABLE_SIZE_PATHS, gpr)) { |
||||||
|
accessListSize.add(gpr.size()); |
||||||
|
System.out.println( |
||||||
|
"(" |
||||||
|
+ accessListSize |
||||||
|
+ ")[" |
||||||
|
+ gpr.size() |
||||||
|
+ ", " |
||||||
|
+ gpr.path() |
||||||
|
+ ", " |
||||||
|
+ gpr.klass().toString() |
||||||
|
+ "]"); |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
GraphWalker gw = new GraphWalker(gv); |
||||||
|
|
||||||
|
gw.walk(txAccessList); |
||||||
|
System.out.println("Base Access List size: " + accessListSize); |
||||||
|
assertThat(accessListSize.sum()).isEqualTo(PendingTransaction.ACCESS_LIST_BASE_MEMORY_SIZE); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void baseFrontierTransactionMemorySize() { |
||||||
|
System.setProperty("jol.magicFieldOffset", "true"); |
||||||
|
Transaction txFrontier = createTransaction(TransactionType.FRONTIER, 1, Wei.of(500), 0, KEYS1); |
||||||
|
BytesValueRLPOutput rlpOut = new BytesValueRLPOutput(); |
||||||
|
txFrontier.writeTo(rlpOut); |
||||||
|
|
||||||
|
txFrontier = Transaction.readFrom(new BytesValueRLPInput(rlpOut.encoded(), false)); |
||||||
|
System.out.println(txFrontier.getSender()); |
||||||
|
System.out.println(txFrontier.getHash()); |
||||||
|
System.out.println(txFrontier.getSize()); |
||||||
|
|
||||||
|
final ClassLayout cl = ClassLayout.parseInstance(txFrontier); |
||||||
|
System.out.println(cl.toPrintable()); |
||||||
|
LongAdder frontierSize = new LongAdder(); |
||||||
|
frontierSize.add(cl.instanceSize()); |
||||||
|
System.out.println(frontierSize); |
||||||
|
|
||||||
|
final Set<String> skipPrefixes = new HashSet<>(); |
||||||
|
|
||||||
|
GraphVisitor gv = |
||||||
|
gpr -> { |
||||||
|
if (!skipPrefixes.stream().anyMatch(sp -> gpr.path().startsWith(sp))) { |
||||||
|
if (SHARED_CLASSES.stream().anyMatch(scz -> scz.isAssignableFrom(gpr.klass()))) { |
||||||
|
skipPrefixes.add(gpr.path()); |
||||||
|
} else if (!startWithAnyOf(FRONTIER_ACCESS_LIST_CONSTANT_FIELD_PATHS, gpr) |
||||||
|
&& !startWithAnyOf(FRONTIER_ACCESS_LIST_VARIABLE_SIZE_PATHS, gpr)) { |
||||||
|
frontierSize.add(gpr.size()); |
||||||
|
System.out.println( |
||||||
|
"(" |
||||||
|
+ frontierSize |
||||||
|
+ ")[" |
||||||
|
+ gpr.size() |
||||||
|
+ ", " |
||||||
|
+ gpr.path() |
||||||
|
+ ", " |
||||||
|
+ gpr.klass().toString() |
||||||
|
+ "]"); |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
GraphWalker gw = new GraphWalker(gv); |
||||||
|
|
||||||
|
gw.walk(txFrontier); |
||||||
|
System.out.println("Base Frontier size: " + frontierSize); |
||||||
|
assertThat(frontierSize.sum()).isEqualTo(PendingTransaction.FRONTIER_BASE_MEMORY_SIZE); |
||||||
|
} |
||||||
|
|
||||||
|
private boolean startWithAnyOf(final Set<String> prefixes, final GraphPathRecord path) { |
||||||
|
return prefixes.stream().anyMatch(prefix -> path.path().startsWith(prefix)); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,185 @@ |
|||||||
|
/* |
||||||
|
* Copyright Besu contributors. |
||||||
|
* |
||||||
|
* 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. See the License for the |
||||||
|
* specific language governing permissions and limitations under the License. |
||||||
|
* |
||||||
|
* SPDX-License-Identifier: Apache-2.0 |
||||||
|
*/ |
||||||
|
package org.hyperledger.besu.ethereum.eth.transactions.layered; |
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||||
|
import static org.hyperledger.besu.ethereum.eth.transactions.TransactionAddedResult.ADDED; |
||||||
|
|
||||||
|
import org.hyperledger.besu.datatypes.Wei; |
||||||
|
import org.hyperledger.besu.ethereum.core.BlockHeader; |
||||||
|
import org.hyperledger.besu.ethereum.core.Transaction; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.ImmutableTransactionPoolConfiguration; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.TransactionAddedResult; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolConfiguration; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolMetrics; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolReplacementHandler; |
||||||
|
|
||||||
|
import java.math.BigInteger; |
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.Iterator; |
||||||
|
import java.util.List; |
||||||
|
import java.util.function.BiFunction; |
||||||
|
import java.util.stream.IntStream; |
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test; |
||||||
|
|
||||||
|
public abstract class AbstractPrioritizedTransactionsTestBase extends BaseTransactionPoolTest { |
||||||
|
protected static final int MAX_TRANSACTIONS = 5; |
||||||
|
protected final TransactionPoolMetrics txPoolMetrics = new TransactionPoolMetrics(metricsSystem); |
||||||
|
protected final EvictCollectorLayer evictCollector = new EvictCollectorLayer(txPoolMetrics); |
||||||
|
protected AbstractPrioritizedTransactions transactions = |
||||||
|
getSorter( |
||||||
|
ImmutableTransactionPoolConfiguration.builder() |
||||||
|
.maxPrioritizedTransactions(MAX_TRANSACTIONS) |
||||||
|
.maxFutureBySender(MAX_TRANSACTIONS) |
||||||
|
.build()); |
||||||
|
|
||||||
|
private AbstractPrioritizedTransactions getSorter(final TransactionPoolConfiguration poolConfig) { |
||||||
|
return getSorter( |
||||||
|
poolConfig, |
||||||
|
evictCollector, |
||||||
|
txPoolMetrics, |
||||||
|
(pt1, pt2) -> transactionReplacementTester(poolConfig, pt1, pt2)); |
||||||
|
} |
||||||
|
|
||||||
|
abstract AbstractPrioritizedTransactions getSorter( |
||||||
|
final TransactionPoolConfiguration poolConfig, |
||||||
|
final TransactionsLayer nextLayer, |
||||||
|
final TransactionPoolMetrics txPoolMetrics, |
||||||
|
final BiFunction<PendingTransaction, PendingTransaction, Boolean> |
||||||
|
transactionReplacementTester); |
||||||
|
|
||||||
|
abstract BlockHeader mockBlockHeader(); |
||||||
|
|
||||||
|
private boolean transactionReplacementTester( |
||||||
|
final TransactionPoolConfiguration poolConfig, |
||||||
|
final PendingTransaction pt1, |
||||||
|
final PendingTransaction pt2) { |
||||||
|
final TransactionPoolReplacementHandler transactionReplacementHandler = |
||||||
|
new TransactionPoolReplacementHandler(poolConfig.getPriceBump()); |
||||||
|
return transactionReplacementHandler.shouldReplace(pt1, pt2, mockBlockHeader()); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void prioritizeLocalTransactionThenValue() { |
||||||
|
final PendingTransaction localTransaction = |
||||||
|
createLocalPendingTransaction(createTransaction(0, KEYS1)); |
||||||
|
assertThat(prioritizeTransaction(localTransaction)).isEqualTo(ADDED); |
||||||
|
|
||||||
|
final List<PendingTransaction> remoteTxs = new ArrayList<>(); |
||||||
|
TransactionAddedResult prioritizeResult = null; |
||||||
|
for (int i = 0; i < MAX_TRANSACTIONS; i++) { |
||||||
|
final PendingTransaction highValueRemoteTx = |
||||||
|
createRemotePendingTransaction( |
||||||
|
createTransaction( |
||||||
|
0, |
||||||
|
Wei.of(BigInteger.valueOf(100).pow(i)), |
||||||
|
SIGNATURE_ALGORITHM.get().generateKeyPair())); |
||||||
|
remoteTxs.add(highValueRemoteTx); |
||||||
|
prioritizeResult = prioritizeTransaction(highValueRemoteTx); |
||||||
|
assertThat(prioritizeResult).isEqualTo(ADDED); |
||||||
|
} |
||||||
|
|
||||||
|
assertEvicted(remoteTxs.get(0)); |
||||||
|
assertTransactionPrioritized(localTransaction); |
||||||
|
remoteTxs.stream().skip(1).forEach(remoteTx -> assertTransactionPrioritized(remoteTx)); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void shouldStartDroppingLocalTransactionsWhenPoolIsFullOfLocalTransactions() { |
||||||
|
final List<PendingTransaction> localTransactions = new ArrayList<>(); |
||||||
|
|
||||||
|
for (int i = 0; i < MAX_TRANSACTIONS; i++) { |
||||||
|
final var localTransaction = createLocalPendingTransaction(createTransaction(i)); |
||||||
|
assertThat(prioritizeTransaction(localTransaction)).isEqualTo(ADDED); |
||||||
|
localTransactions.add(localTransaction); |
||||||
|
} |
||||||
|
|
||||||
|
assertThat(transactions.count()).isEqualTo(MAX_TRANSACTIONS); |
||||||
|
|
||||||
|
// this will be rejected since the prioritized set is full of txs from the same sender with
|
||||||
|
// lower nonce
|
||||||
|
final var lastLocalTransaction = |
||||||
|
createLocalPendingTransaction(createTransaction(MAX_TRANSACTIONS)); |
||||||
|
prioritizeTransaction(lastLocalTransaction); |
||||||
|
assertEvicted(lastLocalTransaction); |
||||||
|
|
||||||
|
assertThat(transactions.count()).isEqualTo(MAX_TRANSACTIONS); |
||||||
|
|
||||||
|
localTransactions.forEach(this::assertTransactionPrioritized); |
||||||
|
assertTransactionNotPrioritized(lastLocalTransaction); |
||||||
|
} |
||||||
|
|
||||||
|
protected void shouldPrioritizeValueThenTimeAddedToPool( |
||||||
|
final Iterator<PendingTransaction> lowValueTxSupplier, |
||||||
|
final PendingTransaction highValueTx, |
||||||
|
final PendingTransaction expectedDroppedTx) { |
||||||
|
|
||||||
|
// Fill the pool with transactions from random senders
|
||||||
|
final List<PendingTransaction> lowGasPriceTransactions = |
||||||
|
IntStream.range(0, MAX_TRANSACTIONS) |
||||||
|
.mapToObj( |
||||||
|
i -> { |
||||||
|
final var lowPriceTx = lowValueTxSupplier.next(); |
||||||
|
final var prioritizeResult = transactions.add(lowPriceTx, 0); |
||||||
|
|
||||||
|
assertThat(prioritizeResult).isEqualTo(ADDED); |
||||||
|
assertThat(evictCollector.getEvictedTransactions()).isEmpty(); |
||||||
|
return lowPriceTx; |
||||||
|
}) |
||||||
|
.toList(); |
||||||
|
|
||||||
|
assertThat(transactions.count()).isEqualTo(MAX_TRANSACTIONS); |
||||||
|
|
||||||
|
// This should kick the oldest tx with the low gas price out, namely the first one we added
|
||||||
|
final var highValuePrioRes = transactions.add(highValueTx, 0); |
||||||
|
assertThat(highValuePrioRes).isEqualTo(ADDED); |
||||||
|
assertEvicted(expectedDroppedTx); |
||||||
|
|
||||||
|
assertTransactionPrioritized(highValueTx); |
||||||
|
lowGasPriceTransactions.stream() |
||||||
|
.filter(tx -> !tx.equals(expectedDroppedTx)) |
||||||
|
.forEach(tx -> assertThat(transactions.getByHash(tx.getHash())).isPresent()); |
||||||
|
} |
||||||
|
|
||||||
|
protected TransactionAddedResult prioritizeTransaction(final Transaction tx) { |
||||||
|
return prioritizeTransaction(createRemotePendingTransaction(tx)); |
||||||
|
} |
||||||
|
|
||||||
|
protected TransactionAddedResult prioritizeTransaction(final PendingTransaction tx) { |
||||||
|
return transactions.add(tx, 0); |
||||||
|
} |
||||||
|
|
||||||
|
protected void assertTransactionPrioritized(final PendingTransaction tx) { |
||||||
|
assertThat(transactions.getByHash(tx.getHash())).isPresent(); |
||||||
|
} |
||||||
|
|
||||||
|
protected void assertTransactionNotPrioritized(final PendingTransaction tx) { |
||||||
|
assertThat(transactions.getByHash(tx.getHash())).isEmpty(); |
||||||
|
} |
||||||
|
|
||||||
|
protected void assertTransactionPrioritized(final Transaction tx) { |
||||||
|
assertThat(transactions.getByHash(tx.getHash())).isPresent(); |
||||||
|
} |
||||||
|
|
||||||
|
protected void assertTransactionNotPrioritized(final Transaction tx) { |
||||||
|
assertThat(transactions.getByHash(tx.getHash())).isEmpty(); |
||||||
|
} |
||||||
|
|
||||||
|
protected void assertEvicted(final PendingTransaction tx) { |
||||||
|
assertThat(evictCollector.getEvictedTransactions()).contains(tx); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,173 @@ |
|||||||
|
/* |
||||||
|
* Copyright Besu contributors. |
||||||
|
* |
||||||
|
* 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. See the License for the |
||||||
|
* specific language governing permissions and limitations under the License. |
||||||
|
* |
||||||
|
* SPDX-License-Identifier: Apache-2.0 |
||||||
|
*/ |
||||||
|
package org.hyperledger.besu.ethereum.eth.transactions.layered; |
||||||
|
|
||||||
|
import static org.hyperledger.besu.plugin.data.TransactionType.EIP1559; |
||||||
|
import static org.hyperledger.besu.plugin.data.TransactionType.FRONTIER; |
||||||
|
import static org.mockito.Mockito.mock; |
||||||
|
import static org.mockito.Mockito.when; |
||||||
|
|
||||||
|
import org.hyperledger.besu.crypto.KeyPair; |
||||||
|
import org.hyperledger.besu.datatypes.Wei; |
||||||
|
import org.hyperledger.besu.ethereum.core.BlockHeader; |
||||||
|
import org.hyperledger.besu.ethereum.core.Transaction; |
||||||
|
import org.hyperledger.besu.ethereum.core.TransactionTestFixture; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolConfiguration; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolMetrics; |
||||||
|
import org.hyperledger.besu.ethereum.mainnet.feemarket.FeeMarket; |
||||||
|
import org.hyperledger.besu.plugin.data.TransactionType; |
||||||
|
|
||||||
|
import java.util.Comparator; |
||||||
|
import java.util.List; |
||||||
|
import java.util.Optional; |
||||||
|
import java.util.Random; |
||||||
|
import java.util.function.BiFunction; |
||||||
|
import java.util.stream.Collectors; |
||||||
|
import java.util.stream.IntStream; |
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test; |
||||||
|
|
||||||
|
public class BaseFeePrioritizedTransactionsTest extends AbstractPrioritizedTransactionsTestBase { |
||||||
|
|
||||||
|
private static final Random randomizeTxType = new Random(); |
||||||
|
|
||||||
|
@Override |
||||||
|
AbstractPrioritizedTransactions getSorter( |
||||||
|
final TransactionPoolConfiguration poolConfig, |
||||||
|
final TransactionsLayer nextLayer, |
||||||
|
final TransactionPoolMetrics txPoolMetrics, |
||||||
|
final BiFunction<PendingTransaction, PendingTransaction, Boolean> |
||||||
|
transactionReplacementTester) { |
||||||
|
|
||||||
|
return new BaseFeePrioritizedTransactions( |
||||||
|
poolConfig, |
||||||
|
this::mockBlockHeader, |
||||||
|
nextLayer, |
||||||
|
txPoolMetrics, |
||||||
|
transactionReplacementTester, |
||||||
|
FeeMarket.london(0L)); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected BlockHeader mockBlockHeader() { |
||||||
|
final BlockHeader blockHeader = mock(BlockHeader.class); |
||||||
|
when(blockHeader.getBaseFee()).thenReturn(Optional.of(Wei.ONE)); |
||||||
|
return blockHeader; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected Transaction createTransaction( |
||||||
|
final long nonce, final Wei maxGasPrice, final KeyPair keys) { |
||||||
|
|
||||||
|
return createTransaction( |
||||||
|
randomizeTxType.nextBoolean() ? EIP1559 : FRONTIER, nonce, maxGasPrice, keys); |
||||||
|
} |
||||||
|
|
||||||
|
protected Transaction createTransaction( |
||||||
|
final TransactionType type, final long nonce, final Wei maxGasPrice, final KeyPair keys) { |
||||||
|
|
||||||
|
var tx = new TransactionTestFixture().value(Wei.of(nonce)).nonce(nonce).type(type); |
||||||
|
if (type.supports1559FeeMarket()) { |
||||||
|
tx.maxFeePerGas(Optional.of(maxGasPrice)) |
||||||
|
.maxPriorityFeePerGas(Optional.of(maxGasPrice.divide(10))); |
||||||
|
} else { |
||||||
|
tx.gasPrice(maxGasPrice); |
||||||
|
} |
||||||
|
return tx.createTransaction(keys); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected Transaction createTransactionReplacement( |
||||||
|
final Transaction originalTransaction, final KeyPair keys) { |
||||||
|
return createTransaction( |
||||||
|
originalTransaction.getType(), |
||||||
|
originalTransaction.getNonce(), |
||||||
|
originalTransaction.getMaxGasPrice().multiply(2), |
||||||
|
keys); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void shouldPrioritizePriorityFeeThenTimeAddedToPoolOnlyEIP1559Txs() { |
||||||
|
shouldPrioritizePriorityFeeThenTimeAddedToPoolSameTypeTxs(EIP1559); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void shouldPrioritizeGasPriceThenTimeAddedToPoolOnlyFrontierTxs() { |
||||||
|
shouldPrioritizePriorityFeeThenTimeAddedToPoolSameTypeTxs(FRONTIER); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void shouldPrioritizeEffectivePriorityFeeThenTimeAddedToPoolOnMixedTypes() { |
||||||
|
final var nextBlockBaseFee = Optional.of(Wei.ONE); |
||||||
|
|
||||||
|
final PendingTransaction highGasPriceTransaction = |
||||||
|
createRemotePendingTransaction(createTransaction(0, Wei.of(100), KEYS1)); |
||||||
|
|
||||||
|
final List<PendingTransaction> lowValueTxs = |
||||||
|
IntStream.range(0, MAX_TRANSACTIONS) |
||||||
|
.mapToObj( |
||||||
|
i -> |
||||||
|
new PendingTransaction.Remote( |
||||||
|
createTransaction( |
||||||
|
0, Wei.of(10), SIGNATURE_ALGORITHM.get().generateKeyPair()))) |
||||||
|
.collect(Collectors.toUnmodifiableList()); |
||||||
|
|
||||||
|
final var lowestPriorityFee = |
||||||
|
lowValueTxs.stream() |
||||||
|
.sorted( |
||||||
|
Comparator.comparing( |
||||||
|
pt -> pt.getTransaction().getEffectivePriorityFeePerGas(nextBlockBaseFee))) |
||||||
|
.findFirst() |
||||||
|
.get() |
||||||
|
.getTransaction() |
||||||
|
.getEffectivePriorityFeePerGas(nextBlockBaseFee); |
||||||
|
|
||||||
|
final var firstLowValueTx = |
||||||
|
lowValueTxs.stream() |
||||||
|
.filter( |
||||||
|
pt -> |
||||||
|
pt.getTransaction() |
||||||
|
.getEffectivePriorityFeePerGas(nextBlockBaseFee) |
||||||
|
.equals(lowestPriorityFee)) |
||||||
|
.findFirst() |
||||||
|
.get(); |
||||||
|
|
||||||
|
shouldPrioritizeValueThenTimeAddedToPool( |
||||||
|
lowValueTxs.iterator(), highGasPriceTransaction, firstLowValueTx); |
||||||
|
} |
||||||
|
|
||||||
|
private void shouldPrioritizePriorityFeeThenTimeAddedToPoolSameTypeTxs( |
||||||
|
final TransactionType transactionType) { |
||||||
|
final PendingTransaction highGasPriceTransaction = |
||||||
|
createRemotePendingTransaction(createTransaction(0, Wei.of(100), KEYS1)); |
||||||
|
|
||||||
|
final var lowValueTxs = |
||||||
|
IntStream.range(0, MAX_TRANSACTIONS) |
||||||
|
.mapToObj( |
||||||
|
i -> |
||||||
|
createRemotePendingTransaction( |
||||||
|
createTransaction( |
||||||
|
transactionType, |
||||||
|
0, |
||||||
|
Wei.of(10), |
||||||
|
0, |
||||||
|
SIGNATURE_ALGORITHM.get().generateKeyPair()))) |
||||||
|
.collect(Collectors.toUnmodifiableList()); |
||||||
|
|
||||||
|
shouldPrioritizeValueThenTimeAddedToPool( |
||||||
|
lowValueTxs.iterator(), highGasPriceTransaction, lowValueTxs.get(0)); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,178 @@ |
|||||||
|
/* |
||||||
|
* Copyright Besu contributors. |
||||||
|
* |
||||||
|
* 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. See the License for the |
||||||
|
* specific language governing permissions and limitations under the License. |
||||||
|
* |
||||||
|
* SPDX-License-Identifier: Apache-2.0 |
||||||
|
*/ |
||||||
|
package org.hyperledger.besu.ethereum.eth.transactions.layered; |
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||||
|
|
||||||
|
import org.hyperledger.besu.crypto.KeyPair; |
||||||
|
import org.hyperledger.besu.crypto.SignatureAlgorithm; |
||||||
|
import org.hyperledger.besu.crypto.SignatureAlgorithmFactory; |
||||||
|
import org.hyperledger.besu.datatypes.Address; |
||||||
|
import org.hyperledger.besu.datatypes.Wei; |
||||||
|
import org.hyperledger.besu.ethereum.core.Transaction; |
||||||
|
import org.hyperledger.besu.ethereum.core.TransactionTestFixture; |
||||||
|
import org.hyperledger.besu.ethereum.core.Util; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactions; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolMetrics; |
||||||
|
import org.hyperledger.besu.evm.account.Account; |
||||||
|
import org.hyperledger.besu.metrics.StubMetricsSystem; |
||||||
|
import org.hyperledger.besu.plugin.data.TransactionType; |
||||||
|
|
||||||
|
import java.util.Optional; |
||||||
|
import java.util.Random; |
||||||
|
|
||||||
|
import com.google.common.base.Supplier; |
||||||
|
import com.google.common.base.Suppliers; |
||||||
|
import org.apache.tuweni.bytes.Bytes; |
||||||
|
|
||||||
|
public class BaseTransactionPoolTest { |
||||||
|
|
||||||
|
protected static final Supplier<SignatureAlgorithm> SIGNATURE_ALGORITHM = |
||||||
|
Suppliers.memoize(SignatureAlgorithmFactory::getInstance); |
||||||
|
protected static final KeyPair KEYS1 = SIGNATURE_ALGORITHM.get().generateKeyPair(); |
||||||
|
protected static final KeyPair KEYS2 = SIGNATURE_ALGORITHM.get().generateKeyPair(); |
||||||
|
protected static final Address SENDER1 = Util.publicKeyToAddress(KEYS1.getPublicKey()); |
||||||
|
protected static final Address SENDER2 = Util.publicKeyToAddress(KEYS2.getPublicKey()); |
||||||
|
|
||||||
|
private static final Random randomizeTxType = new Random(); |
||||||
|
|
||||||
|
protected final Transaction transaction0 = createTransaction(0); |
||||||
|
protected final Transaction transaction1 = createTransaction(1); |
||||||
|
protected final Transaction transaction2 = createTransaction(2); |
||||||
|
|
||||||
|
protected final StubMetricsSystem metricsSystem = new StubMetricsSystem(); |
||||||
|
|
||||||
|
protected Transaction createTransaction(final long nonce) { |
||||||
|
return createTransaction(nonce, Wei.of(5000L), KEYS1); |
||||||
|
} |
||||||
|
|
||||||
|
protected Transaction createTransaction(final long nonce, final KeyPair keys) { |
||||||
|
return createTransaction(nonce, Wei.of(5000L), keys); |
||||||
|
} |
||||||
|
|
||||||
|
protected Transaction createTransaction(final long nonce, final Wei maxGasPrice) { |
||||||
|
return createTransaction(nonce, maxGasPrice, KEYS1); |
||||||
|
} |
||||||
|
|
||||||
|
protected Transaction createTransaction(final long nonce, final int payloadSize) { |
||||||
|
return createTransaction(nonce, Wei.of(5000L), payloadSize, KEYS1); |
||||||
|
} |
||||||
|
|
||||||
|
protected Transaction createTransaction( |
||||||
|
final long nonce, final Wei maxGasPrice, final KeyPair keys) { |
||||||
|
return createTransaction(nonce, maxGasPrice, 0, keys); |
||||||
|
} |
||||||
|
|
||||||
|
protected Transaction createEIP1559Transaction( |
||||||
|
final long nonce, final KeyPair keys, final int gasFeeMultiplier) { |
||||||
|
return createTransaction( |
||||||
|
TransactionType.EIP1559, nonce, Wei.of(5000L).multiply(gasFeeMultiplier), 0, keys); |
||||||
|
} |
||||||
|
|
||||||
|
protected Transaction createTransaction( |
||||||
|
final long nonce, final Wei maxGasPrice, final int payloadSize, final KeyPair keys) { |
||||||
|
|
||||||
|
// ToDo 4844: include BLOB tx here
|
||||||
|
final TransactionType txType = TransactionType.values()[randomizeTxType.nextInt(3)]; |
||||||
|
|
||||||
|
return createTransaction(txType, nonce, maxGasPrice, payloadSize, keys); |
||||||
|
} |
||||||
|
|
||||||
|
protected Transaction createTransaction( |
||||||
|
final TransactionType type, |
||||||
|
final long nonce, |
||||||
|
final Wei maxGasPrice, |
||||||
|
final int payloadSize, |
||||||
|
final KeyPair keys) { |
||||||
|
return prepareTransaction(type, nonce, maxGasPrice, payloadSize).createTransaction(keys); |
||||||
|
} |
||||||
|
|
||||||
|
protected TransactionTestFixture prepareTransaction( |
||||||
|
final TransactionType type, final long nonce, final Wei maxGasPrice, final int payloadSize) { |
||||||
|
|
||||||
|
var tx = |
||||||
|
new TransactionTestFixture() |
||||||
|
.to(Optional.of(Address.fromHexString("0x634316eA0EE79c701c6F67C53A4C54cBAfd2316d"))) |
||||||
|
.value(Wei.of(nonce)) |
||||||
|
.nonce(nonce) |
||||||
|
.type(type); |
||||||
|
if (payloadSize > 0) { |
||||||
|
var payloadBytes = Bytes.repeat((byte) 1, payloadSize); |
||||||
|
tx.payload(payloadBytes); |
||||||
|
} |
||||||
|
if (type.supports1559FeeMarket()) { |
||||||
|
tx.maxFeePerGas(Optional.of(maxGasPrice)) |
||||||
|
.maxPriorityFeePerGas(Optional.of(maxGasPrice.divide(10))); |
||||||
|
} else { |
||||||
|
tx.gasPrice(maxGasPrice); |
||||||
|
} |
||||||
|
return tx; |
||||||
|
} |
||||||
|
|
||||||
|
protected Transaction createTransactionReplacement( |
||||||
|
final Transaction originalTransaction, final KeyPair keys) { |
||||||
|
return createTransaction( |
||||||
|
originalTransaction.getType(), |
||||||
|
originalTransaction.getNonce(), |
||||||
|
originalTransaction.getMaxGasPrice().multiply(2), |
||||||
|
0, |
||||||
|
keys); |
||||||
|
} |
||||||
|
|
||||||
|
protected PendingTransaction createRemotePendingTransaction(final Transaction transaction) { |
||||||
|
return new PendingTransaction.Remote(transaction); |
||||||
|
} |
||||||
|
|
||||||
|
protected PendingTransaction createLocalPendingTransaction(final Transaction transaction) { |
||||||
|
return new PendingTransaction.Local(transaction); |
||||||
|
} |
||||||
|
|
||||||
|
protected void assertTransactionPending( |
||||||
|
final PendingTransactions transactions, final Transaction t) { |
||||||
|
assertThat(transactions.getTransactionByHash(t.getHash())).contains(t); |
||||||
|
} |
||||||
|
|
||||||
|
protected void assertTransactionNotPending( |
||||||
|
final PendingTransactions transactions, final Transaction t) { |
||||||
|
assertThat(transactions.getTransactionByHash(t.getHash())).isEmpty(); |
||||||
|
} |
||||||
|
|
||||||
|
protected void assertNoNextNonceForSender( |
||||||
|
final PendingTransactions pendingTransactions, final Address sender) { |
||||||
|
assertThat(pendingTransactions.getNextNonceForSender(sender)).isEmpty(); |
||||||
|
} |
||||||
|
|
||||||
|
protected void assertNextNonceForSender( |
||||||
|
final PendingTransactions pendingTransactions, final Address sender1, final int i) { |
||||||
|
assertThat(pendingTransactions.getNextNonceForSender(sender1)).isPresent().hasValue(i); |
||||||
|
} |
||||||
|
|
||||||
|
protected void addLocalTransactions( |
||||||
|
final PendingTransactions sorter, final Account sender, final long... nonces) { |
||||||
|
for (final long nonce : nonces) { |
||||||
|
sorter.addLocalTransaction(createTransaction(nonce), Optional.of(sender)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
protected long getAddedCount(final String source, final String layer) { |
||||||
|
return metricsSystem.getCounterValue(TransactionPoolMetrics.ADDED_COUNTER_NAME, source, layer); |
||||||
|
} |
||||||
|
|
||||||
|
protected long getRemovedCount(final String source, final String operation, final String layer) { |
||||||
|
return metricsSystem.getCounterValue( |
||||||
|
TransactionPoolMetrics.REMOVED_COUNTER_NAME, source, operation, layer); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,47 @@ |
|||||||
|
/* |
||||||
|
* Copyright Hyperledger Besu Contributors. |
||||||
|
* |
||||||
|
* 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. See the License for the |
||||||
|
* specific language governing permissions and limitations under the License. |
||||||
|
* |
||||||
|
* SPDX-License-Identifier: Apache-2.0 |
||||||
|
*/ |
||||||
|
package org.hyperledger.besu.ethereum.eth.transactions.layered; |
||||||
|
|
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.TransactionAddedResult; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolMetrics; |
||||||
|
|
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
public class EvictCollectorLayer extends EndLayer { |
||||||
|
static final String LAYER_NAME = "evict-collector"; |
||||||
|
final List<PendingTransaction> evictedTxs = new ArrayList<>(); |
||||||
|
|
||||||
|
public EvictCollectorLayer(final TransactionPoolMetrics metrics) { |
||||||
|
super(metrics); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String name() { |
||||||
|
return LAYER_NAME; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public TransactionAddedResult add(final PendingTransaction pendingTransaction, final int gap) { |
||||||
|
final var res = super.add(pendingTransaction, gap); |
||||||
|
evictedTxs.add(pendingTransaction); |
||||||
|
return res; |
||||||
|
} |
||||||
|
|
||||||
|
public List<PendingTransaction> getEvictedTransactions() { |
||||||
|
return evictedTxs; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,91 @@ |
|||||||
|
/* |
||||||
|
* Copyright Besu contributors. |
||||||
|
* |
||||||
|
* 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. See the License for the |
||||||
|
* specific language governing permissions and limitations under the License. |
||||||
|
* |
||||||
|
* SPDX-License-Identifier: Apache-2.0 |
||||||
|
*/ |
||||||
|
package org.hyperledger.besu.ethereum.eth.transactions.layered; |
||||||
|
|
||||||
|
import static org.mockito.Mockito.mock; |
||||||
|
import static org.mockito.Mockito.when; |
||||||
|
|
||||||
|
import org.hyperledger.besu.crypto.KeyPair; |
||||||
|
import org.hyperledger.besu.datatypes.Wei; |
||||||
|
import org.hyperledger.besu.ethereum.core.BlockHeader; |
||||||
|
import org.hyperledger.besu.ethereum.core.Transaction; |
||||||
|
import org.hyperledger.besu.ethereum.core.TransactionTestFixture; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolConfiguration; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolMetrics; |
||||||
|
|
||||||
|
import java.util.List; |
||||||
|
import java.util.Optional; |
||||||
|
import java.util.function.BiFunction; |
||||||
|
import java.util.stream.IntStream; |
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test; |
||||||
|
|
||||||
|
public class GasPricePrioritizedTransactionsTest extends AbstractPrioritizedTransactionsTestBase { |
||||||
|
|
||||||
|
@Override |
||||||
|
AbstractPrioritizedTransactions getSorter( |
||||||
|
final TransactionPoolConfiguration poolConfig, |
||||||
|
final TransactionsLayer nextLayer, |
||||||
|
final TransactionPoolMetrics txPoolMetrics, |
||||||
|
final BiFunction<PendingTransaction, PendingTransaction, Boolean> |
||||||
|
transactionReplacementTester) { |
||||||
|
|
||||||
|
return new GasPricePrioritizedTransactions( |
||||||
|
poolConfig, nextLayer, txPoolMetrics, transactionReplacementTester); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected BlockHeader mockBlockHeader() { |
||||||
|
final BlockHeader blockHeader = mock(BlockHeader.class); |
||||||
|
when(blockHeader.getBaseFee()).thenReturn(Optional.empty()); |
||||||
|
return blockHeader; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected Transaction createTransaction( |
||||||
|
final long transactionNumber, final Wei maxGasPrice, final KeyPair keys) { |
||||||
|
return new TransactionTestFixture() |
||||||
|
.value(Wei.of(transactionNumber)) |
||||||
|
.nonce(transactionNumber) |
||||||
|
.gasPrice(maxGasPrice) |
||||||
|
.createTransaction(keys); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected Transaction createTransactionReplacement( |
||||||
|
final Transaction originalTransaction, final KeyPair keys) { |
||||||
|
return createTransaction( |
||||||
|
originalTransaction.getNonce(), originalTransaction.getMaxGasPrice().multiply(2), keys); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void shouldPrioritizeGasPriceThenTimeAddedToPool() { |
||||||
|
final List<PendingTransaction> lowValueTxs = |
||||||
|
IntStream.range(0, MAX_TRANSACTIONS) |
||||||
|
.mapToObj( |
||||||
|
i -> |
||||||
|
createRemotePendingTransaction( |
||||||
|
createTransaction( |
||||||
|
0, Wei.of(10), SIGNATURE_ALGORITHM.get().generateKeyPair()))) |
||||||
|
.toList(); |
||||||
|
|
||||||
|
final PendingTransaction highGasPriceTransaction = |
||||||
|
createRemotePendingTransaction(createTransaction(0, Wei.of(100), KEYS1)); |
||||||
|
|
||||||
|
shouldPrioritizeValueThenTimeAddedToPool( |
||||||
|
lowValueTxs.iterator(), highGasPriceTransaction, lowValueTxs.get(0)); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,241 @@ |
|||||||
|
/* |
||||||
|
* Copyright Hyperledger Besu Contributors. |
||||||
|
* |
||||||
|
* 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. See the License for the |
||||||
|
* specific language governing permissions and limitations under the License. |
||||||
|
* |
||||||
|
* SPDX-License-Identifier: Apache-2.0 |
||||||
|
*/ |
||||||
|
package org.hyperledger.besu.ethereum.eth.transactions.layered; |
||||||
|
|
||||||
|
import static java.util.Arrays.asList; |
||||||
|
import static java.util.Collections.emptyList; |
||||||
|
import static java.util.stream.Collectors.toList; |
||||||
|
import static org.hyperledger.besu.ethereum.transaction.TransactionInvalidReason.INVALID_TRANSACTION_FORMAT; |
||||||
|
import static org.hyperledger.besu.ethereum.transaction.TransactionInvalidReason.REPLAY_PROTECTED_SIGNATURE_REQUIRED; |
||||||
|
import static org.mockito.Mockito.when; |
||||||
|
|
||||||
|
import org.hyperledger.besu.datatypes.Wei; |
||||||
|
import org.hyperledger.besu.ethereum.core.Block; |
||||||
|
import org.hyperledger.besu.ethereum.core.BlockBody; |
||||||
|
import org.hyperledger.besu.ethereum.core.BlockHeader; |
||||||
|
import org.hyperledger.besu.ethereum.core.BlockHeaderTestFixture; |
||||||
|
import org.hyperledger.besu.ethereum.core.Difficulty; |
||||||
|
import org.hyperledger.besu.ethereum.core.ExecutionContextTestFixture; |
||||||
|
import org.hyperledger.besu.ethereum.core.Transaction; |
||||||
|
import org.hyperledger.besu.ethereum.core.TransactionReceipt; |
||||||
|
import org.hyperledger.besu.ethereum.core.TransactionTestFixture; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.AbstractTransactionsLayeredPendingTransactionsTest; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactions; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolConfiguration; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolMetrics; |
||||||
|
import org.hyperledger.besu.ethereum.mainnet.feemarket.FeeMarket; |
||||||
|
import org.hyperledger.besu.plugin.data.TransactionType; |
||||||
|
|
||||||
|
import java.math.BigInteger; |
||||||
|
import java.util.List; |
||||||
|
import java.util.Optional; |
||||||
|
import java.util.function.BiFunction; |
||||||
|
|
||||||
|
import org.junit.Test; |
||||||
|
import org.junit.runner.RunWith; |
||||||
|
import org.mockito.junit.MockitoJUnitRunner; |
||||||
|
|
||||||
|
@SuppressWarnings("unchecked") |
||||||
|
@RunWith(MockitoJUnitRunner.class) |
||||||
|
public class LayeredPendingTransactionsLegacyTest |
||||||
|
extends AbstractTransactionsLayeredPendingTransactionsTest { |
||||||
|
|
||||||
|
@Override |
||||||
|
protected PendingTransactions createPendingTransactionsSorter( |
||||||
|
final TransactionPoolConfiguration poolConfig, |
||||||
|
final BiFunction<PendingTransaction, PendingTransaction, Boolean> |
||||||
|
transactionReplacementTester) { |
||||||
|
|
||||||
|
final var txPoolMetrics = new TransactionPoolMetrics(metricsSystem); |
||||||
|
return new LayeredPendingTransactions( |
||||||
|
poolConfig, |
||||||
|
new GasPricePrioritizedTransactions( |
||||||
|
poolConfig, new EndLayer(txPoolMetrics), txPoolMetrics, transactionReplacementTester)); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected Transaction createTransaction( |
||||||
|
final int nonce, final Optional<BigInteger> maybeChainId) { |
||||||
|
return createBaseTransaction(nonce).chainId(maybeChainId).createTransaction(KEY_PAIR1); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected Transaction createTransaction(final int nonce, final Wei maxPrice) { |
||||||
|
return createBaseTransaction(nonce).gasPrice(maxPrice).createTransaction(KEY_PAIR1); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected TransactionTestFixture createBaseTransaction(final int nonce) { |
||||||
|
return new TransactionTestFixture() |
||||||
|
.nonce(nonce) |
||||||
|
.gasLimit(blockGasLimit) |
||||||
|
.type(TransactionType.FRONTIER); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected ExecutionContextTestFixture createExecutionContextTestFixture() { |
||||||
|
return ExecutionContextTestFixture.create(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected FeeMarket getFeeMarket() { |
||||||
|
return FeeMarket.legacy(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected Block appendBlock( |
||||||
|
final Difficulty difficulty, |
||||||
|
final BlockHeader parentBlock, |
||||||
|
final Transaction... transactionsToAdd) { |
||||||
|
final List<Transaction> transactionList = asList(transactionsToAdd); |
||||||
|
final Block block = |
||||||
|
new Block( |
||||||
|
new BlockHeaderTestFixture() |
||||||
|
.difficulty(difficulty) |
||||||
|
.gasLimit(parentBlock.getGasLimit()) |
||||||
|
.parentHash(parentBlock.getHash()) |
||||||
|
.number(parentBlock.getNumber() + 1) |
||||||
|
.buildHeader(), |
||||||
|
new BlockBody(transactionList, emptyList())); |
||||||
|
final List<TransactionReceipt> transactionReceipts = |
||||||
|
transactionList.stream() |
||||||
|
.map(transaction -> new TransactionReceipt(1, 1, emptyList(), Optional.empty())) |
||||||
|
.collect(toList()); |
||||||
|
blockchain.appendBlock(block, transactionReceipts); |
||||||
|
return block; |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void |
||||||
|
addLocalTransaction_strictReplayProtectionOn_txWithoutChainId_chainIdIsConfigured_protectionNotSupportedAtCurrentBlock() { |
||||||
|
protocolSupportsTxReplayProtection(1337, false); |
||||||
|
transactionPool = createTransactionPool(b -> b.strictTransactionReplayProtectionEnabled(true)); |
||||||
|
final Transaction tx = createTransactionWithoutChainId(0); |
||||||
|
givenTransactionIsValid(tx); |
||||||
|
|
||||||
|
addAndAssertLocalTransactionValid(tx); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void |
||||||
|
addRemoteTransactions_strictReplayProtectionOff_txWithoutChainId_chainIdIsConfigured() { |
||||||
|
protocolSupportsTxReplayProtection(1337, true); |
||||||
|
transactionPool = createTransactionPool(b -> b.strictTransactionReplayProtectionEnabled(false)); |
||||||
|
final Transaction tx = createTransactionWithoutChainId(0); |
||||||
|
givenTransactionIsValid(tx); |
||||||
|
|
||||||
|
addAndAssertRemoteTransactionValid(tx); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void addLocalTransaction_strictReplayProtectionOff_txWithoutChainId_chainIdIsConfigured() { |
||||||
|
protocolSupportsTxReplayProtection(1337, true); |
||||||
|
transactionPool = createTransactionPool(b -> b.strictTransactionReplayProtectionEnabled(false)); |
||||||
|
final Transaction tx = createTransactionWithoutChainId(0); |
||||||
|
givenTransactionIsValid(tx); |
||||||
|
|
||||||
|
addAndAssertLocalTransactionValid(tx); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void addLocalTransaction_strictReplayProtectionOn_txWithoutChainId_chainIdIsConfigured() { |
||||||
|
protocolSupportsTxReplayProtection(1337, true); |
||||||
|
transactionPool = createTransactionPool(b -> b.strictTransactionReplayProtectionEnabled(true)); |
||||||
|
final Transaction tx = createTransactionWithoutChainId(0); |
||||||
|
givenTransactionIsValid(tx); |
||||||
|
|
||||||
|
addAndAssertLocalTransactionInvalid(tx, REPLAY_PROTECTED_SIGNATURE_REQUIRED); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void |
||||||
|
addRemoteTransactions_strictReplayProtectionOn_txWithoutChainId_chainIdIsConfigured() { |
||||||
|
protocolSupportsTxReplayProtection(1337, true); |
||||||
|
transactionPool = createTransactionPool(b -> b.strictTransactionReplayProtectionEnabled(true)); |
||||||
|
final Transaction tx = createTransactionWithoutChainId(0); |
||||||
|
givenTransactionIsValid(tx); |
||||||
|
|
||||||
|
addAndAssertRemoteTransactionValid(tx); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void |
||||||
|
addLocalTransaction_strictReplayProtectionOn_txWithoutChainId_chainIdIsNotConfigured() { |
||||||
|
protocolDoesNotSupportTxReplayProtection(); |
||||||
|
transactionPool = createTransactionPool(b -> b.strictTransactionReplayProtectionEnabled(true)); |
||||||
|
final Transaction tx = createTransactionWithoutChainId(0); |
||||||
|
givenTransactionIsValid(tx); |
||||||
|
|
||||||
|
addAndAssertLocalTransactionValid(tx); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void |
||||||
|
addRemoteTransactions_strictReplayProtectionOn_txWithoutChainId_chainIdIsNotConfigured() { |
||||||
|
protocolDoesNotSupportTxReplayProtection(); |
||||||
|
transactionPool = createTransactionPool(b -> b.strictTransactionReplayProtectionEnabled(true)); |
||||||
|
final Transaction tx = createTransactionWithoutChainId(0); |
||||||
|
givenTransactionIsValid(tx); |
||||||
|
|
||||||
|
addAndAssertRemoteTransactionValid(tx); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void shouldIgnoreEIP1559TransactionWhenNotAllowed() { |
||||||
|
final Transaction transaction = |
||||||
|
createBaseTransaction(1) |
||||||
|
.type(TransactionType.EIP1559) |
||||||
|
.maxFeePerGas(Optional.of(Wei.of(100L))) |
||||||
|
.maxPriorityFeePerGas(Optional.of(Wei.of(50L))) |
||||||
|
.gasLimit(10) |
||||||
|
.gasPrice(null) |
||||||
|
.createTransaction(KEY_PAIR1); |
||||||
|
|
||||||
|
givenTransactionIsValid(transaction); |
||||||
|
|
||||||
|
addAndAssertLocalTransactionInvalid(transaction, INVALID_TRANSACTION_FORMAT); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void shouldAcceptZeroGasPriceFrontierTransactionsWhenMining() { |
||||||
|
when(miningParameters.isMiningEnabled()).thenReturn(true); |
||||||
|
|
||||||
|
final Transaction transaction = createTransaction(0, Wei.ZERO); |
||||||
|
|
||||||
|
givenTransactionIsValid(transaction); |
||||||
|
|
||||||
|
addAndAssertLocalTransactionValid(transaction); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void shouldAcceptZeroGasPriceTransactionWhenMinGasPriceIsZero() { |
||||||
|
when(miningParameters.getMinTransactionGasPrice()).thenReturn(Wei.ZERO); |
||||||
|
|
||||||
|
final Transaction transaction = createTransaction(0, Wei.ZERO); |
||||||
|
|
||||||
|
givenTransactionIsValid(transaction); |
||||||
|
|
||||||
|
addAndAssertLocalTransactionValid(transaction); |
||||||
|
} |
||||||
|
|
||||||
|
private Transaction createTransactionWithoutChainId(final int nonce) { |
||||||
|
return createTransaction(nonce, Optional.empty()); |
||||||
|
} |
||||||
|
|
||||||
|
private void protocolDoesNotSupportTxReplayProtection() { |
||||||
|
when(protocolSchedule.getChainId()).thenReturn(Optional.empty()); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,294 @@ |
|||||||
|
/* |
||||||
|
* Copyright Hyperledger Besu Contributors. |
||||||
|
* |
||||||
|
* 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. See the License for the |
||||||
|
* specific language governing permissions and limitations under the License. |
||||||
|
* |
||||||
|
* SPDX-License-Identifier: Apache-2.0 |
||||||
|
*/ |
||||||
|
package org.hyperledger.besu.ethereum.eth.transactions.layered; |
||||||
|
|
||||||
|
import static java.util.Arrays.asList; |
||||||
|
import static java.util.Collections.emptyList; |
||||||
|
import static java.util.stream.Collectors.toList; |
||||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||||
|
import static org.mockito.Mockito.when; |
||||||
|
|
||||||
|
import org.hyperledger.besu.config.StubGenesisConfigOptions; |
||||||
|
import org.hyperledger.besu.datatypes.Wei; |
||||||
|
import org.hyperledger.besu.ethereum.core.Block; |
||||||
|
import org.hyperledger.besu.ethereum.core.BlockBody; |
||||||
|
import org.hyperledger.besu.ethereum.core.BlockHeader; |
||||||
|
import org.hyperledger.besu.ethereum.core.BlockHeaderBuilder; |
||||||
|
import org.hyperledger.besu.ethereum.core.BlockHeaderTestFixture; |
||||||
|
import org.hyperledger.besu.ethereum.core.Difficulty; |
||||||
|
import org.hyperledger.besu.ethereum.core.ExecutionContextTestFixture; |
||||||
|
import org.hyperledger.besu.ethereum.core.PrivacyParameters; |
||||||
|
import org.hyperledger.besu.ethereum.core.Transaction; |
||||||
|
import org.hyperledger.besu.ethereum.core.TransactionReceipt; |
||||||
|
import org.hyperledger.besu.ethereum.core.TransactionTestFixture; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.AbstractTransactionsLayeredPendingTransactionsTest; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactions; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolConfiguration; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolMetrics; |
||||||
|
import org.hyperledger.besu.ethereum.mainnet.MainnetBlockHeaderFunctions; |
||||||
|
import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; |
||||||
|
import org.hyperledger.besu.ethereum.mainnet.ProtocolScheduleBuilder; |
||||||
|
import org.hyperledger.besu.ethereum.mainnet.ProtocolSpecAdapters; |
||||||
|
import org.hyperledger.besu.ethereum.mainnet.feemarket.FeeMarket; |
||||||
|
import org.hyperledger.besu.evm.internal.EvmConfiguration; |
||||||
|
import org.hyperledger.besu.plugin.data.TransactionType; |
||||||
|
|
||||||
|
import java.math.BigInteger; |
||||||
|
import java.util.List; |
||||||
|
import java.util.Optional; |
||||||
|
import java.util.function.BiFunction; |
||||||
|
import java.util.function.Function; |
||||||
|
|
||||||
|
import org.junit.Test; |
||||||
|
|
||||||
|
public class LayeredPendingTransactionsLondonTest |
||||||
|
extends AbstractTransactionsLayeredPendingTransactionsTest { |
||||||
|
|
||||||
|
private static final Wei BASE_FEE_FLOOR = Wei.of(7L); |
||||||
|
|
||||||
|
@Override |
||||||
|
protected PendingTransactions createPendingTransactionsSorter( |
||||||
|
final TransactionPoolConfiguration poolConfig, |
||||||
|
final BiFunction<PendingTransaction, PendingTransaction, Boolean> |
||||||
|
transactionReplacementTester) { |
||||||
|
|
||||||
|
final var txPoolMetrics = new TransactionPoolMetrics(metricsSystem); |
||||||
|
return new LayeredPendingTransactions( |
||||||
|
poolConfig, |
||||||
|
new BaseFeePrioritizedTransactions( |
||||||
|
poolConfig, |
||||||
|
protocolContext.getBlockchain()::getChainHeadHeader, |
||||||
|
new EndLayer(txPoolMetrics), |
||||||
|
txPoolMetrics, |
||||||
|
transactionReplacementTester, |
||||||
|
FeeMarket.london(0L))); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected Transaction createTransaction( |
||||||
|
final int nonce, final Optional<BigInteger> maybeChainId) { |
||||||
|
return createBaseTransaction(nonce).chainId(maybeChainId).createTransaction(KEY_PAIR1); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected Transaction createTransaction(final int nonce, final Wei maxPrice) { |
||||||
|
return createBaseTransaction(nonce) |
||||||
|
.maxFeePerGas(Optional.of(maxPrice)) |
||||||
|
.maxPriorityFeePerGas(Optional.of(maxPrice.divide(5L))) |
||||||
|
.createTransaction(KEY_PAIR1); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected TransactionTestFixture createBaseTransaction(final int nonce) { |
||||||
|
return new TransactionTestFixture() |
||||||
|
.nonce(nonce) |
||||||
|
.gasLimit(blockGasLimit) |
||||||
|
.gasPrice(null) |
||||||
|
.maxFeePerGas(Optional.of(Wei.of(5000L))) |
||||||
|
.maxPriorityFeePerGas(Optional.of(Wei.of(1000L))) |
||||||
|
.type(TransactionType.EIP1559); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected ExecutionContextTestFixture createExecutionContextTestFixture() { |
||||||
|
final ProtocolSchedule protocolSchedule = |
||||||
|
new ProtocolScheduleBuilder( |
||||||
|
new StubGenesisConfigOptions().londonBlock(0L).baseFeePerGas(10L), |
||||||
|
BigInteger.valueOf(1), |
||||||
|
ProtocolSpecAdapters.create(0, Function.identity()), |
||||||
|
new PrivacyParameters(), |
||||||
|
false, |
||||||
|
EvmConfiguration.DEFAULT) |
||||||
|
.createProtocolSchedule(); |
||||||
|
final ExecutionContextTestFixture executionContextTestFixture = |
||||||
|
ExecutionContextTestFixture.builder().protocolSchedule(protocolSchedule).build(); |
||||||
|
|
||||||
|
final Block block = |
||||||
|
new Block( |
||||||
|
new BlockHeaderTestFixture() |
||||||
|
.gasLimit( |
||||||
|
executionContextTestFixture |
||||||
|
.getBlockchain() |
||||||
|
.getChainHeadBlock() |
||||||
|
.getHeader() |
||||||
|
.getGasLimit()) |
||||||
|
.difficulty(Difficulty.ONE) |
||||||
|
.baseFeePerGas(Wei.of(10L)) |
||||||
|
.parentHash(executionContextTestFixture.getBlockchain().getChainHeadHash()) |
||||||
|
.number(executionContextTestFixture.getBlockchain().getChainHeadBlockNumber() + 1) |
||||||
|
.buildHeader(), |
||||||
|
new BlockBody(List.of(), List.of())); |
||||||
|
executionContextTestFixture.getBlockchain().appendBlock(block, List.of()); |
||||||
|
|
||||||
|
return executionContextTestFixture; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected FeeMarket getFeeMarket() { |
||||||
|
return FeeMarket.london(0L, Optional.of(BASE_FEE_FLOOR)); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected Block appendBlock( |
||||||
|
final Difficulty difficulty, |
||||||
|
final BlockHeader parentBlock, |
||||||
|
final Transaction... transactionsToAdd) { |
||||||
|
final List<Transaction> transactionList = asList(transactionsToAdd); |
||||||
|
final Block block = |
||||||
|
new Block( |
||||||
|
new BlockHeaderTestFixture() |
||||||
|
.baseFeePerGas(Wei.of(10L)) |
||||||
|
.gasLimit(parentBlock.getGasLimit()) |
||||||
|
.difficulty(difficulty) |
||||||
|
.parentHash(parentBlock.getHash()) |
||||||
|
.number(parentBlock.getNumber() + 1) |
||||||
|
.buildHeader(), |
||||||
|
new BlockBody(transactionList, emptyList())); |
||||||
|
final List<TransactionReceipt> transactionReceipts = |
||||||
|
transactionList.stream() |
||||||
|
.map(transaction -> new TransactionReceipt(1, 1, emptyList(), Optional.empty())) |
||||||
|
.collect(toList()); |
||||||
|
blockchain.appendBlock(block, transactionReceipts); |
||||||
|
return block; |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void shouldAcceptZeroGasPriceFrontierTxsWhenMinGasPriceIsZeroAndLondonWithZeroBaseFee() { |
||||||
|
when(miningParameters.getMinTransactionGasPrice()).thenReturn(Wei.ZERO); |
||||||
|
when(protocolSpec.getFeeMarket()).thenReturn(FeeMarket.london(0, Optional.of(Wei.ZERO))); |
||||||
|
whenBlockBaseFeeIs(Wei.ZERO); |
||||||
|
|
||||||
|
final Transaction frontierTransaction = createFrontierTransaction(0, Wei.ZERO); |
||||||
|
|
||||||
|
givenTransactionIsValid(frontierTransaction); |
||||||
|
addAndAssertLocalTransactionValid(frontierTransaction); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void shouldAcceptZeroGasPrice1559TxsWhenMinGasPriceIsZeroAndLondonWithZeroBaseFee() { |
||||||
|
when(miningParameters.getMinTransactionGasPrice()).thenReturn(Wei.ZERO); |
||||||
|
when(protocolSpec.getFeeMarket()).thenReturn(FeeMarket.london(0, Optional.of(Wei.ZERO))); |
||||||
|
whenBlockBaseFeeIs(Wei.ZERO); |
||||||
|
|
||||||
|
final Transaction transaction = createTransaction(0, Wei.ZERO); |
||||||
|
|
||||||
|
givenTransactionIsValid(transaction); |
||||||
|
addAndAssertLocalTransactionValid(transaction); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void shouldAcceptBaseFeeFloorGasPriceFrontierTransactionsWhenMining() { |
||||||
|
final Transaction frontierTransaction = createFrontierTransaction(0, BASE_FEE_FLOOR); |
||||||
|
|
||||||
|
givenTransactionIsValid(frontierTransaction); |
||||||
|
|
||||||
|
addAndAssertLocalTransactionValid(frontierTransaction); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void shouldRejectRemote1559TxsWhenMaxFeePerGasBelowMinGasPrice() { |
||||||
|
final Wei genesisBaseFee = Wei.of(100L); |
||||||
|
final Wei minGasPrice = Wei.of(200L); |
||||||
|
final Wei lastBlockBaseFee = minGasPrice.add(50L); |
||||||
|
final Wei txMaxFeePerGas = minGasPrice.subtract(1L); |
||||||
|
|
||||||
|
assertThat( |
||||||
|
add1559TxAndGetPendingTxsCount( |
||||||
|
genesisBaseFee, minGasPrice, lastBlockBaseFee, txMaxFeePerGas, false)) |
||||||
|
.isEqualTo(0); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void shouldAcceptRemote1559TxsWhenMaxFeePerGasIsAtLeastEqualToMinGasPrice() { |
||||||
|
final Wei genesisBaseFee = Wei.of(100L); |
||||||
|
final Wei minGasPrice = Wei.of(200L); |
||||||
|
final Wei lastBlockBaseFee = minGasPrice.add(50L); |
||||||
|
final Wei txMaxFeePerGas = minGasPrice; |
||||||
|
|
||||||
|
assertThat( |
||||||
|
add1559TxAndGetPendingTxsCount( |
||||||
|
genesisBaseFee, minGasPrice, lastBlockBaseFee, txMaxFeePerGas, false)) |
||||||
|
.isEqualTo(1); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void shouldRejectLocal1559TxsWhenMaxFeePerGasBelowMinGasPrice() { |
||||||
|
final Wei genesisBaseFee = Wei.of(100L); |
||||||
|
final Wei minGasPrice = Wei.of(200L); |
||||||
|
final Wei lastBlockBaseFee = minGasPrice.add(50L); |
||||||
|
final Wei txMaxFeePerGas = minGasPrice.subtract(1L); |
||||||
|
|
||||||
|
assertThat( |
||||||
|
add1559TxAndGetPendingTxsCount( |
||||||
|
genesisBaseFee, minGasPrice, lastBlockBaseFee, txMaxFeePerGas, true)) |
||||||
|
.isEqualTo(0); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void shouldAcceptLocal1559TxsWhenMaxFeePerGasIsAtLeastEqualToMinMinGasPrice() { |
||||||
|
final Wei genesisBaseFee = Wei.of(100L); |
||||||
|
final Wei minGasPrice = Wei.of(200L); |
||||||
|
final Wei lastBlockBaseFee = minGasPrice.add(50L); |
||||||
|
final Wei txMaxFeePerGas = minGasPrice; |
||||||
|
|
||||||
|
assertThat( |
||||||
|
add1559TxAndGetPendingTxsCount( |
||||||
|
genesisBaseFee, minGasPrice, lastBlockBaseFee, txMaxFeePerGas, true)) |
||||||
|
.isEqualTo(1); |
||||||
|
} |
||||||
|
|
||||||
|
private int add1559TxAndGetPendingTxsCount( |
||||||
|
final Wei genesisBaseFee, |
||||||
|
final Wei minGasPrice, |
||||||
|
final Wei lastBlockBaseFee, |
||||||
|
final Wei txMaxFeePerGas, |
||||||
|
final boolean isLocal) { |
||||||
|
when(miningParameters.getMinTransactionGasPrice()).thenReturn(minGasPrice); |
||||||
|
when(protocolSpec.getFeeMarket()).thenReturn(FeeMarket.london(0, Optional.of(genesisBaseFee))); |
||||||
|
whenBlockBaseFeeIs(lastBlockBaseFee); |
||||||
|
|
||||||
|
final Transaction transaction = createTransaction(0, txMaxFeePerGas); |
||||||
|
|
||||||
|
givenTransactionIsValid(transaction); |
||||||
|
|
||||||
|
if (isLocal) { |
||||||
|
transactionPool.addTransactionViaApi(transaction); |
||||||
|
} else { |
||||||
|
transactionPool.addRemoteTransactions(List.of(transaction)); |
||||||
|
} |
||||||
|
|
||||||
|
return transactions.size(); |
||||||
|
} |
||||||
|
|
||||||
|
private void whenBlockBaseFeeIs(final Wei baseFee) { |
||||||
|
final BlockHeader header = |
||||||
|
BlockHeaderBuilder.fromHeader(blockchain.getChainHeadHeader()) |
||||||
|
.baseFee(baseFee) |
||||||
|
.blockHeaderFunctions(new MainnetBlockHeaderFunctions()) |
||||||
|
.parentHash(blockchain.getChainHeadHash()) |
||||||
|
.buildBlockHeader(); |
||||||
|
blockchain.appendBlock(new Block(header, BlockBody.empty()), emptyList()); |
||||||
|
} |
||||||
|
|
||||||
|
private Transaction createFrontierTransaction(final int transactionNumber, final Wei gasPrice) { |
||||||
|
return new TransactionTestFixture() |
||||||
|
.nonce(transactionNumber) |
||||||
|
.gasPrice(gasPrice) |
||||||
|
.gasLimit(blockGasLimit) |
||||||
|
.type(TransactionType.FRONTIER) |
||||||
|
.createTransaction(KEY_PAIR1); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,713 @@ |
|||||||
|
/* |
||||||
|
* Copyright Besu contributors. |
||||||
|
* |
||||||
|
* 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. See the License for the |
||||||
|
* specific language governing permissions and limitations under the License. |
||||||
|
* |
||||||
|
* SPDX-License-Identifier: Apache-2.0 |
||||||
|
*/ |
||||||
|
package org.hyperledger.besu.ethereum.eth.transactions.layered; |
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||||
|
import static org.hyperledger.besu.ethereum.eth.transactions.PendingTransactions.TransactionSelectionResult.COMPLETE_OPERATION; |
||||||
|
import static org.hyperledger.besu.ethereum.eth.transactions.PendingTransactions.TransactionSelectionResult.CONTINUE; |
||||||
|
import static org.hyperledger.besu.ethereum.eth.transactions.PendingTransactions.TransactionSelectionResult.DELETE_TRANSACTION_AND_CONTINUE; |
||||||
|
import static org.hyperledger.besu.ethereum.eth.transactions.TransactionAddedResult.ADDED; |
||||||
|
import static org.hyperledger.besu.ethereum.eth.transactions.TransactionAddedResult.ALREADY_KNOWN; |
||||||
|
import static org.hyperledger.besu.ethereum.eth.transactions.TransactionAddedResult.NONCE_TOO_FAR_IN_FUTURE_FOR_SENDER; |
||||||
|
import static org.hyperledger.besu.ethereum.eth.transactions.TransactionAddedResult.REJECTED_UNDERPRICED_REPLACEMENT; |
||||||
|
import static org.hyperledger.besu.ethereum.eth.transactions.layered.TransactionsLayer.RemovalReason.DROPPED; |
||||||
|
import static org.hyperledger.besu.ethereum.eth.transactions.layered.TransactionsLayer.RemovalReason.REPLACED; |
||||||
|
import static org.mockito.Mockito.mock; |
||||||
|
import static org.mockito.Mockito.verify; |
||||||
|
import static org.mockito.Mockito.verifyNoInteractions; |
||||||
|
import static org.mockito.Mockito.verifyNoMoreInteractions; |
||||||
|
import static org.mockito.Mockito.when; |
||||||
|
|
||||||
|
import org.hyperledger.besu.crypto.KeyPair; |
||||||
|
import org.hyperledger.besu.datatypes.Hash; |
||||||
|
import org.hyperledger.besu.datatypes.Wei; |
||||||
|
import org.hyperledger.besu.ethereum.core.BlockHeader; |
||||||
|
import org.hyperledger.besu.ethereum.core.Transaction; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.ImmutableTransactionPoolConfiguration; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactionAddedListener; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactionDroppedListener; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolConfiguration; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolMetrics; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolReplacementHandler; |
||||||
|
import org.hyperledger.besu.ethereum.mainnet.feemarket.FeeMarket; |
||||||
|
import org.hyperledger.besu.evm.account.Account; |
||||||
|
|
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.List; |
||||||
|
import java.util.Optional; |
||||||
|
import java.util.OptionalLong; |
||||||
|
import java.util.function.BiFunction; |
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach; |
||||||
|
import org.junit.jupiter.api.Test; |
||||||
|
|
||||||
|
public class LayeredPendingTransactionsTest extends BaseTransactionPoolTest { |
||||||
|
|
||||||
|
protected static final int MAX_TRANSACTIONS = 5; |
||||||
|
protected static final int MAX_CAPACITY_BYTES = 10_000; |
||||||
|
protected static final int LIMITED_TRANSACTIONS_BY_SENDER = 4; |
||||||
|
protected static final String REMOTE = "remote"; |
||||||
|
protected static final String LOCAL = "local"; |
||||||
|
protected final PendingTransactionAddedListener listener = |
||||||
|
mock(PendingTransactionAddedListener.class); |
||||||
|
protected final PendingTransactionDroppedListener droppedListener = |
||||||
|
mock(PendingTransactionDroppedListener.class); |
||||||
|
|
||||||
|
private final TransactionPoolConfiguration poolConf = |
||||||
|
ImmutableTransactionPoolConfiguration.builder() |
||||||
|
.maxPrioritizedTransactions(MAX_TRANSACTIONS) |
||||||
|
.maxFutureBySender(MAX_TRANSACTIONS) |
||||||
|
.pendingTransactionsLayerMaxCapacityBytes(MAX_CAPACITY_BYTES) |
||||||
|
.build(); |
||||||
|
|
||||||
|
private final TransactionPoolConfiguration senderLimitedConfig = |
||||||
|
ImmutableTransactionPoolConfiguration.builder() |
||||||
|
.maxPrioritizedTransactions(MAX_TRANSACTIONS) |
||||||
|
.maxFutureBySender(LIMITED_TRANSACTIONS_BY_SENDER) |
||||||
|
.pendingTransactionsLayerMaxCapacityBytes(MAX_CAPACITY_BYTES) |
||||||
|
.build(); |
||||||
|
private LayeredPendingTransactions senderLimitedTransactions; |
||||||
|
private LayeredPendingTransactions pendingTransactions; |
||||||
|
private CreatedLayers senderLimitedLayers; |
||||||
|
private CreatedLayers layers; |
||||||
|
private TransactionPoolMetrics txPoolMetrics; |
||||||
|
|
||||||
|
private static BlockHeader mockBlockHeader() { |
||||||
|
final BlockHeader blockHeader = mock(BlockHeader.class); |
||||||
|
when(blockHeader.getBaseFee()).thenReturn(Optional.of(Wei.of(100))); |
||||||
|
return blockHeader; |
||||||
|
} |
||||||
|
|
||||||
|
private CreatedLayers createLayers(final TransactionPoolConfiguration poolConfig) { |
||||||
|
|
||||||
|
final BiFunction<PendingTransaction, PendingTransaction, Boolean> transactionReplacementTester = |
||||||
|
(t1, t2) -> |
||||||
|
new TransactionPoolReplacementHandler(poolConf.getPriceBump()) |
||||||
|
.shouldReplace(t1, t2, mockBlockHeader()); |
||||||
|
|
||||||
|
final EvictCollectorLayer evictCollector = new EvictCollectorLayer(txPoolMetrics); |
||||||
|
|
||||||
|
final SparseTransactions sparseTransactions = |
||||||
|
new SparseTransactions( |
||||||
|
poolConfig, evictCollector, txPoolMetrics, transactionReplacementTester); |
||||||
|
|
||||||
|
final ReadyTransactions readyTransactions = |
||||||
|
new ReadyTransactions( |
||||||
|
poolConfig, sparseTransactions, txPoolMetrics, transactionReplacementTester); |
||||||
|
|
||||||
|
final BaseFeePrioritizedTransactions prioritizedTransactions = |
||||||
|
new BaseFeePrioritizedTransactions( |
||||||
|
poolConfig, |
||||||
|
LayeredPendingTransactionsTest::mockBlockHeader, |
||||||
|
readyTransactions, |
||||||
|
txPoolMetrics, |
||||||
|
transactionReplacementTester, |
||||||
|
FeeMarket.london(0L)); |
||||||
|
return new CreatedLayers( |
||||||
|
prioritizedTransactions, readyTransactions, sparseTransactions, evictCollector); |
||||||
|
} |
||||||
|
|
||||||
|
@BeforeEach |
||||||
|
public void setup() { |
||||||
|
|
||||||
|
txPoolMetrics = new TransactionPoolMetrics(metricsSystem); |
||||||
|
|
||||||
|
layers = createLayers(poolConf); |
||||||
|
senderLimitedLayers = createLayers(senderLimitedConfig); |
||||||
|
|
||||||
|
pendingTransactions = new LayeredPendingTransactions(poolConf, layers.prioritizedTransactions); |
||||||
|
|
||||||
|
senderLimitedTransactions = |
||||||
|
new LayeredPendingTransactions( |
||||||
|
senderLimitedConfig, senderLimitedLayers.prioritizedTransactions); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void returnExclusivelyLocalTransactionsWhenAppropriate() { |
||||||
|
final Transaction localTransaction0 = createTransaction(0, KEYS2); |
||||||
|
pendingTransactions.addLocalTransaction(localTransaction0, Optional.empty()); |
||||||
|
assertThat(pendingTransactions.size()).isEqualTo(1); |
||||||
|
|
||||||
|
pendingTransactions.addRemoteTransaction(transaction0, Optional.empty()); |
||||||
|
assertThat(pendingTransactions.size()).isEqualTo(2); |
||||||
|
|
||||||
|
pendingTransactions.addRemoteTransaction(transaction1, Optional.empty()); |
||||||
|
assertThat(pendingTransactions.size()).isEqualTo(3); |
||||||
|
|
||||||
|
final List<Transaction> localTransactions = pendingTransactions.getLocalTransactions(); |
||||||
|
assertThat(localTransactions.size()).isEqualTo(1); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void addRemoteTransactions() { |
||||||
|
pendingTransactions.addRemoteTransaction(transaction0, Optional.empty()); |
||||||
|
assertThat(pendingTransactions.size()).isEqualTo(1); |
||||||
|
|
||||||
|
assertThat(getAddedCount(REMOTE, layers.prioritizedTransactions.name())).isEqualTo(1); |
||||||
|
|
||||||
|
pendingTransactions.addRemoteTransaction(transaction1, Optional.empty()); |
||||||
|
assertThat(pendingTransactions.size()).isEqualTo(2); |
||||||
|
|
||||||
|
assertThat(getAddedCount(REMOTE, layers.prioritizedTransactions.name())).isEqualTo(2); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void getNotPresentTransaction() { |
||||||
|
assertThat(pendingTransactions.getTransactionByHash(Hash.EMPTY_TRIE_HASH)).isEmpty(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void getTransactionByHash() { |
||||||
|
pendingTransactions.addRemoteTransaction(transaction0, Optional.empty()); |
||||||
|
assertTransactionPending(pendingTransactions, transaction0); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void evictTransactionsWhenSizeLimitExceeded() { |
||||||
|
final List<Transaction> firstTxs = new ArrayList<>(MAX_TRANSACTIONS); |
||||||
|
|
||||||
|
pendingTransactions.subscribeDroppedTransactions(droppedListener); |
||||||
|
|
||||||
|
for (int i = 0; i < MAX_TRANSACTIONS; i++) { |
||||||
|
final Account sender = mock(Account.class); |
||||||
|
when(sender.getNonce()).thenReturn((long) i); |
||||||
|
final var tx = |
||||||
|
createTransaction( |
||||||
|
i, |
||||||
|
Wei.of((i + 1) * 100L), |
||||||
|
(int) poolConf.getPendingTransactionsLayerMaxCapacityBytes() + 1, |
||||||
|
SIGNATURE_ALGORITHM.get().generateKeyPair()); |
||||||
|
pendingTransactions.addRemoteTransaction(tx, Optional.of(sender)); |
||||||
|
firstTxs.add(tx); |
||||||
|
assertTransactionPending(pendingTransactions, tx); |
||||||
|
} |
||||||
|
|
||||||
|
assertThat(pendingTransactions.size()).isEqualTo(MAX_TRANSACTIONS); |
||||||
|
|
||||||
|
final Transaction lastBigTx = |
||||||
|
createTransaction( |
||||||
|
0, |
||||||
|
Wei.of(100_000L), |
||||||
|
(int) poolConf.getPendingTransactionsLayerMaxCapacityBytes(), |
||||||
|
SIGNATURE_ALGORITHM.get().generateKeyPair()); |
||||||
|
final Account lastSender = mock(Account.class); |
||||||
|
when(lastSender.getNonce()).thenReturn(0L); |
||||||
|
pendingTransactions.addRemoteTransaction(lastBigTx, Optional.of(lastSender)); |
||||||
|
assertTransactionPending(pendingTransactions, lastBigTx); |
||||||
|
|
||||||
|
assertTransactionNotPending(pendingTransactions, firstTxs.get(0)); |
||||||
|
assertThat(getRemovedCount(REMOTE, DROPPED.label(), layers.evictedCollector.name())) |
||||||
|
.isEqualTo(1); |
||||||
|
assertThat(layers.evictedCollector.getEvictedTransactions()) |
||||||
|
.map(PendingTransaction::getTransaction) |
||||||
|
.contains(firstTxs.get(0)); |
||||||
|
verify(droppedListener).onTransactionDropped(firstTxs.get(0)); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void addTransactionForMultipleSenders() { |
||||||
|
final var transactionSenderA = createTransaction(0, KEYS1); |
||||||
|
final var transactionSenderB = createTransaction(0, KEYS2); |
||||||
|
assertThat(pendingTransactions.addRemoteTransaction(transactionSenderA, Optional.empty())) |
||||||
|
.isEqualTo(ADDED); |
||||||
|
assertTransactionPending(pendingTransactions, transactionSenderA); |
||||||
|
assertThat(pendingTransactions.addRemoteTransaction(transactionSenderB, Optional.empty())) |
||||||
|
.isEqualTo(ADDED); |
||||||
|
assertTransactionPending(pendingTransactions, transactionSenderB); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void dropIfTransactionTooFarInFutureForTheSender() { |
||||||
|
final var futureTransaction = |
||||||
|
createTransaction(poolConf.getTxPoolMaxFutureTransactionByAccount() + 1); |
||||||
|
assertThat(pendingTransactions.addRemoteTransaction(futureTransaction, Optional.empty())) |
||||||
|
.isEqualTo(NONCE_TOO_FAR_IN_FUTURE_FOR_SENDER); |
||||||
|
assertTransactionNotPending(pendingTransactions, futureTransaction); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void dropAlreadyConfirmedTransaction() { |
||||||
|
final Account sender = mock(Account.class); |
||||||
|
when(sender.getNonce()).thenReturn(5L); |
||||||
|
|
||||||
|
final Transaction oldTransaction = createTransaction(2); |
||||||
|
assertThat(pendingTransactions.addRemoteTransaction(oldTransaction, Optional.of(sender))) |
||||||
|
.isEqualTo(ALREADY_KNOWN); |
||||||
|
assertThat(pendingTransactions.size()).isEqualTo(0); |
||||||
|
assertTransactionNotPending(pendingTransactions, oldTransaction); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void notifyListenerWhenRemoteTransactionAdded() { |
||||||
|
pendingTransactions.subscribePendingTransactions(listener); |
||||||
|
|
||||||
|
pendingTransactions.addRemoteTransaction(transaction0, Optional.empty()); |
||||||
|
|
||||||
|
verify(listener).onTransactionAdded(transaction0); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void notifyListenerWhenLocalTransactionAdded() { |
||||||
|
pendingTransactions.subscribePendingTransactions(listener); |
||||||
|
|
||||||
|
pendingTransactions.addLocalTransaction(transaction0, Optional.empty()); |
||||||
|
|
||||||
|
verify(listener).onTransactionAdded(transaction0); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void notNotifyListenerAfterUnsubscribe() { |
||||||
|
final long id = pendingTransactions.subscribePendingTransactions(listener); |
||||||
|
|
||||||
|
pendingTransactions.addRemoteTransaction(transaction0, Optional.empty()); |
||||||
|
|
||||||
|
verify(listener).onTransactionAdded(transaction0); |
||||||
|
|
||||||
|
pendingTransactions.unsubscribePendingTransactions(id); |
||||||
|
|
||||||
|
pendingTransactions.addRemoteTransaction(transaction1, Optional.empty()); |
||||||
|
|
||||||
|
verifyNoMoreInteractions(listener); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void selectTransactionsUntilSelectorRequestsNoMore() { |
||||||
|
pendingTransactions.addRemoteTransaction(transaction0, Optional.empty()); |
||||||
|
pendingTransactions.addRemoteTransaction(transaction1, Optional.empty()); |
||||||
|
|
||||||
|
final List<Transaction> parsedTransactions = new ArrayList<>(); |
||||||
|
pendingTransactions.selectTransactions( |
||||||
|
transaction -> { |
||||||
|
parsedTransactions.add(transaction); |
||||||
|
return COMPLETE_OPERATION; |
||||||
|
}); |
||||||
|
|
||||||
|
assertThat(parsedTransactions.size()).isEqualTo(1); |
||||||
|
assertThat(parsedTransactions.get(0)).isEqualTo(transaction0); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void selectTransactionsUntilPendingIsEmpty() { |
||||||
|
pendingTransactions.addRemoteTransaction(transaction0, Optional.empty()); |
||||||
|
pendingTransactions.addRemoteTransaction(transaction1, Optional.empty()); |
||||||
|
|
||||||
|
final List<Transaction> parsedTransactions = new ArrayList<>(); |
||||||
|
pendingTransactions.selectTransactions( |
||||||
|
transaction -> { |
||||||
|
parsedTransactions.add(transaction); |
||||||
|
return CONTINUE; |
||||||
|
}); |
||||||
|
|
||||||
|
assertThat(parsedTransactions.size()).isEqualTo(2); |
||||||
|
assertThat(parsedTransactions.get(0)).isEqualTo(transaction0); |
||||||
|
assertThat(parsedTransactions.get(1)).isEqualTo(transaction1); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void notSelectReplacedTransaction() { |
||||||
|
final Transaction transaction1 = createTransaction(0, KEYS1); |
||||||
|
final Transaction transaction1b = createTransactionReplacement(transaction1, KEYS1); |
||||||
|
|
||||||
|
pendingTransactions.addRemoteTransaction(transaction1, Optional.empty()); |
||||||
|
pendingTransactions.addRemoteTransaction(transaction1b, Optional.empty()); |
||||||
|
|
||||||
|
final List<Transaction> parsedTransactions = new ArrayList<>(); |
||||||
|
pendingTransactions.selectTransactions( |
||||||
|
transaction -> { |
||||||
|
parsedTransactions.add(transaction); |
||||||
|
return CONTINUE; |
||||||
|
}); |
||||||
|
|
||||||
|
assertThat(parsedTransactions).containsExactly(transaction1b); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void selectTransactionsFromSameSenderInNonceOrder() { |
||||||
|
final Transaction transaction0 = createTransaction(0, KEYS1); |
||||||
|
final Transaction transaction1 = createTransaction(1, KEYS1); |
||||||
|
final Transaction transaction2 = createTransaction(2, KEYS1); |
||||||
|
|
||||||
|
// add out of order
|
||||||
|
pendingTransactions.addLocalTransaction(transaction2, Optional.empty()); |
||||||
|
pendingTransactions.addLocalTransaction(transaction1, Optional.empty()); |
||||||
|
pendingTransactions.addLocalTransaction(transaction0, Optional.empty()); |
||||||
|
|
||||||
|
final List<Transaction> iterationOrder = new ArrayList<>(3); |
||||||
|
pendingTransactions.selectTransactions( |
||||||
|
transaction -> { |
||||||
|
iterationOrder.add(transaction); |
||||||
|
return CONTINUE; |
||||||
|
}); |
||||||
|
|
||||||
|
assertThat(iterationOrder).containsExactly(transaction0, transaction1, transaction2); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void notForceNonceOrderWhenSendersDiffer() { |
||||||
|
final Account sender2 = mock(Account.class); |
||||||
|
when(sender2.getNonce()).thenReturn(1L); |
||||||
|
|
||||||
|
final Transaction transactionSender1 = createTransaction(0, Wei.of(10), KEYS1); |
||||||
|
final Transaction transactionSender2 = createTransaction(1, Wei.of(200), KEYS2); |
||||||
|
|
||||||
|
pendingTransactions.addLocalTransaction(transactionSender1, Optional.empty()); |
||||||
|
pendingTransactions.addLocalTransaction(transactionSender2, Optional.of(sender2)); |
||||||
|
|
||||||
|
final List<Transaction> iterationOrder = new ArrayList<>(2); |
||||||
|
pendingTransactions.selectTransactions( |
||||||
|
transaction -> { |
||||||
|
iterationOrder.add(transaction); |
||||||
|
return CONTINUE; |
||||||
|
}); |
||||||
|
|
||||||
|
assertThat(iterationOrder).containsExactly(transactionSender2, transactionSender1); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void invalidTransactionIsDeletedFromPendingTransactions() { |
||||||
|
pendingTransactions.addRemoteTransaction(transaction0, Optional.empty()); |
||||||
|
pendingTransactions.addRemoteTransaction(transaction1, Optional.empty()); |
||||||
|
|
||||||
|
final List<Transaction> parsedTransactions = new ArrayList<>(2); |
||||||
|
pendingTransactions.selectTransactions( |
||||||
|
transaction -> { |
||||||
|
parsedTransactions.add(transaction); |
||||||
|
return DELETE_TRANSACTION_AND_CONTINUE; |
||||||
|
}); |
||||||
|
|
||||||
|
assertThat(parsedTransactions.size()).isEqualTo(2); |
||||||
|
assertThat(parsedTransactions.get(0)).isEqualTo(transaction0); |
||||||
|
assertThat(parsedTransactions.get(1)).isEqualTo(transaction1); |
||||||
|
|
||||||
|
assertThat(pendingTransactions.size()).isZero(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void returnEmptyOptionalAsMaximumNonceWhenNoTransactionsPresent() { |
||||||
|
assertThat(pendingTransactions.getNextNonceForSender(SENDER1)).isEmpty(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void replaceTransactionWithSameSenderAndNonce() { |
||||||
|
final Transaction transaction1 = createTransaction(0, Wei.of(20), KEYS1); |
||||||
|
final Transaction transaction1b = createTransactionReplacement(transaction1, KEYS1); |
||||||
|
final Transaction transaction2 = createTransaction(1, Wei.of(10), KEYS1); |
||||||
|
assertThat(pendingTransactions.addRemoteTransaction(transaction1, Optional.empty())) |
||||||
|
.isEqualTo(ADDED); |
||||||
|
assertThat(pendingTransactions.addRemoteTransaction(transaction2, Optional.empty())) |
||||||
|
.isEqualTo(ADDED); |
||||||
|
assertThat( |
||||||
|
pendingTransactions |
||||||
|
.addRemoteTransaction(transaction1b, Optional.empty()) |
||||||
|
.isReplacement()) |
||||||
|
.isTrue(); |
||||||
|
|
||||||
|
assertTransactionNotPending(pendingTransactions, transaction1); |
||||||
|
assertTransactionPending(pendingTransactions, transaction1b); |
||||||
|
assertTransactionPending(pendingTransactions, transaction2); |
||||||
|
assertThat(pendingTransactions.size()).isEqualTo(2); |
||||||
|
assertThat(getAddedCount(REMOTE, layers.prioritizedTransactions.name())).isEqualTo(3); |
||||||
|
assertThat(getRemovedCount(REMOTE, REPLACED.label(), layers.prioritizedTransactions.name())) |
||||||
|
.isEqualTo(1); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void replaceTransactionWithSameSenderAndNonce_multipleReplacements() { |
||||||
|
final int replacedTxCount = 5; |
||||||
|
final List<Transaction> replacedTransactions = new ArrayList<>(replacedTxCount); |
||||||
|
Transaction duplicateTx = createTransaction(0, Wei.of(50), KEYS1); |
||||||
|
for (int i = 0; i < replacedTxCount; i++) { |
||||||
|
replacedTransactions.add(duplicateTx); |
||||||
|
pendingTransactions.addRemoteTransaction(duplicateTx, Optional.empty()); |
||||||
|
duplicateTx = createTransactionReplacement(duplicateTx, KEYS1); |
||||||
|
} |
||||||
|
|
||||||
|
final Transaction independentTx = createTransaction(1, Wei.ONE, KEYS1); |
||||||
|
assertThat(pendingTransactions.addRemoteTransaction(independentTx, Optional.empty())) |
||||||
|
.isEqualTo(ADDED); |
||||||
|
assertThat( |
||||||
|
pendingTransactions.addRemoteTransaction(duplicateTx, Optional.empty()).isReplacement()) |
||||||
|
.isTrue(); |
||||||
|
|
||||||
|
// All txs except the last duplicate should be removed
|
||||||
|
replacedTransactions.forEach(tx -> assertTransactionNotPending(pendingTransactions, tx)); |
||||||
|
assertTransactionPending(pendingTransactions, duplicateTx); |
||||||
|
// Tx with distinct nonce should be maintained
|
||||||
|
assertTransactionPending(pendingTransactions, independentTx); |
||||||
|
|
||||||
|
assertThat(pendingTransactions.size()).isEqualTo(2); |
||||||
|
assertThat(getAddedCount(REMOTE, layers.prioritizedTransactions.name())) |
||||||
|
.isEqualTo(replacedTxCount + 2); |
||||||
|
assertThat(getRemovedCount(REMOTE, REPLACED.label(), layers.prioritizedTransactions.name())) |
||||||
|
.isEqualTo(replacedTxCount); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void |
||||||
|
replaceTransactionWithSameSenderAndNonce_multipleReplacementsAddedLocallyAndRemotely() { |
||||||
|
final int replacedTxCount = 5; |
||||||
|
final List<Transaction> replacedTransactions = new ArrayList<>(replacedTxCount); |
||||||
|
int remoteDuplicateCount = 0; |
||||||
|
Transaction replacingTx = createTransaction(0, KEYS1); |
||||||
|
for (int i = 0; i < replacedTxCount; i++) { |
||||||
|
replacedTransactions.add(replacingTx); |
||||||
|
if (i % 2 == 0) { |
||||||
|
pendingTransactions.addRemoteTransaction(replacingTx, Optional.empty()); |
||||||
|
remoteDuplicateCount++; |
||||||
|
} else { |
||||||
|
pendingTransactions.addLocalTransaction(replacingTx, Optional.empty()); |
||||||
|
} |
||||||
|
replacingTx = createTransactionReplacement(replacingTx, KEYS1); |
||||||
|
} |
||||||
|
|
||||||
|
final Transaction independentTx = createTransaction(1); |
||||||
|
assertThat( |
||||||
|
pendingTransactions.addLocalTransaction(replacingTx, Optional.empty()).isReplacement()) |
||||||
|
.isTrue(); |
||||||
|
assertThat(pendingTransactions.addRemoteTransaction(independentTx, Optional.empty())) |
||||||
|
.isEqualTo(ADDED); |
||||||
|
|
||||||
|
// All txs except the last duplicate should be removed
|
||||||
|
replacedTransactions.forEach(tx -> assertTransactionNotPending(pendingTransactions, tx)); |
||||||
|
assertTransactionPending(pendingTransactions, replacingTx); |
||||||
|
|
||||||
|
// Tx with distinct nonce should be maintained
|
||||||
|
assertTransactionPending(pendingTransactions, independentTx); |
||||||
|
|
||||||
|
final int localDuplicateCount = replacedTxCount - remoteDuplicateCount; |
||||||
|
assertThat(pendingTransactions.size()).isEqualTo(2); |
||||||
|
assertThat(getAddedCount(REMOTE, layers.prioritizedTransactions.name())) |
||||||
|
.isEqualTo(remoteDuplicateCount + 1); |
||||||
|
assertThat(getAddedCount(LOCAL, layers.prioritizedTransactions.name())) |
||||||
|
.isEqualTo(localDuplicateCount + 1); |
||||||
|
assertThat(getRemovedCount(REMOTE, REPLACED.label(), layers.prioritizedTransactions.name())) |
||||||
|
.isEqualTo(remoteDuplicateCount); |
||||||
|
assertThat(getRemovedCount(LOCAL, REPLACED.label(), layers.prioritizedTransactions.name())) |
||||||
|
.isEqualTo(localDuplicateCount); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void notReplaceTransactionWithSameSenderAndNonceWhenGasPriceIsLower() { |
||||||
|
final Transaction transaction1 = createTransaction(0, Wei.of(2)); |
||||||
|
final Transaction transaction1b = createTransaction(0, Wei.ONE); |
||||||
|
assertThat(pendingTransactions.addRemoteTransaction(transaction1, Optional.empty())) |
||||||
|
.isEqualTo(ADDED); |
||||||
|
|
||||||
|
pendingTransactions.subscribePendingTransactions(listener); |
||||||
|
assertThat(pendingTransactions.addRemoteTransaction(transaction1b, Optional.empty())) |
||||||
|
.isEqualTo(REJECTED_UNDERPRICED_REPLACEMENT); |
||||||
|
|
||||||
|
assertTransactionNotPending(pendingTransactions, transaction1b); |
||||||
|
assertTransactionPending(pendingTransactions, transaction1); |
||||||
|
assertThat(pendingTransactions.size()).isEqualTo(1); |
||||||
|
verifyNoInteractions(listener); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void trackNextNonceForEachSender() { |
||||||
|
// first sender consecutive txs: 0->1->2
|
||||||
|
final Account firstSender = mock(Account.class); |
||||||
|
when(firstSender.getNonce()).thenReturn(0L); |
||||||
|
when(firstSender.getAddress()).thenReturn(SENDER1); |
||||||
|
assertNoNextNonceForSender(pendingTransactions, SENDER1); |
||||||
|
pendingTransactions.addRemoteTransaction(createTransaction(0, KEYS1), Optional.of(firstSender)); |
||||||
|
assertNextNonceForSender(pendingTransactions, SENDER1, 1); |
||||||
|
|
||||||
|
pendingTransactions.addRemoteTransaction(createTransaction(1, KEYS1), Optional.of(firstSender)); |
||||||
|
assertNextNonceForSender(pendingTransactions, SENDER1, 2); |
||||||
|
|
||||||
|
pendingTransactions.addRemoteTransaction(createTransaction(2, KEYS1), Optional.of(firstSender)); |
||||||
|
assertNextNonceForSender(pendingTransactions, SENDER1, 3); |
||||||
|
|
||||||
|
// second sender not in orders: 3->0->2->1
|
||||||
|
final Account secondSender = mock(Account.class); |
||||||
|
when(secondSender.getNonce()).thenReturn(0L); |
||||||
|
when(secondSender.getAddress()).thenReturn(SENDER2); |
||||||
|
assertNoNextNonceForSender(pendingTransactions, SENDER2); |
||||||
|
pendingTransactions.addRemoteTransaction( |
||||||
|
createTransaction(3, KEYS2), Optional.of(secondSender)); |
||||||
|
assertNoNextNonceForSender(pendingTransactions, SENDER2); |
||||||
|
|
||||||
|
pendingTransactions.addRemoteTransaction( |
||||||
|
createTransaction(0, KEYS2), Optional.of(secondSender)); |
||||||
|
assertNextNonceForSender(pendingTransactions, SENDER2, 1); |
||||||
|
|
||||||
|
pendingTransactions.addRemoteTransaction( |
||||||
|
createTransaction(2, KEYS2), Optional.of(secondSender)); |
||||||
|
assertNextNonceForSender(pendingTransactions, SENDER2, 1); |
||||||
|
|
||||||
|
// tx 1 will fill the nonce gap and all txs will be ready
|
||||||
|
pendingTransactions.addRemoteTransaction( |
||||||
|
createTransaction(1, KEYS2), Optional.of(secondSender)); |
||||||
|
assertNextNonceForSender(pendingTransactions, SENDER2, 4); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void correctNonceIsReturned() { |
||||||
|
final Account sender = mock(Account.class); |
||||||
|
when(sender.getNonce()).thenReturn(1L); |
||||||
|
assertThat(pendingTransactions.getNextNonceForSender(transaction2.getSender())).isEmpty(); |
||||||
|
// since tx 3 is missing, 4 is sparse,
|
||||||
|
// note that 0 is already known since sender nonce is 1
|
||||||
|
addLocalTransactions(pendingTransactions, sender, 0, 1, 2, 4); |
||||||
|
assertThat(pendingTransactions.size()).isEqualTo(3); |
||||||
|
assertThat(pendingTransactions.getNextNonceForSender(transaction2.getSender())) |
||||||
|
.isPresent() |
||||||
|
.hasValue(3); |
||||||
|
|
||||||
|
// tx 3 arrives and is added, while 4 is moved to ready
|
||||||
|
addLocalTransactions(pendingTransactions, sender, 3); |
||||||
|
assertThat(pendingTransactions.size()).isEqualTo(4); |
||||||
|
assertThat(pendingTransactions.getNextNonceForSender(transaction2.getSender())) |
||||||
|
.isPresent() |
||||||
|
.hasValue(5); |
||||||
|
|
||||||
|
// when 5 is added, the pool is full, and so 6 and 7 are dropped since too far in future
|
||||||
|
addLocalTransactions(pendingTransactions, sender, 5, 6, 7); |
||||||
|
assertThat(pendingTransactions.size()).isEqualTo(5); |
||||||
|
|
||||||
|
// assert that transactions are pruned by account from the latest future nonce first
|
||||||
|
assertThat(pendingTransactions.getNextNonceForSender(transaction2.getSender())) |
||||||
|
.isPresent() |
||||||
|
.hasValue(6); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void correctNonceIsReturnedForSenderLimitedPool() { |
||||||
|
final Account sender = mock(Account.class); |
||||||
|
when(sender.getNonce()).thenReturn(1L); |
||||||
|
|
||||||
|
assertThat(senderLimitedTransactions.getNextNonceForSender(transaction2.getSender())).isEmpty(); |
||||||
|
// since tx 3 is missing, 4 is sparse,
|
||||||
|
// note that 0 is already known since sender nonce is 1
|
||||||
|
addLocalTransactions(senderLimitedTransactions, sender, 0, 1, 2, 4); |
||||||
|
assertThat(senderLimitedTransactions.size()).isEqualTo(3); |
||||||
|
assertThat(senderLimitedTransactions.getNextNonceForSender(transaction2.getSender())) |
||||||
|
.isPresent() |
||||||
|
.hasValue(3); |
||||||
|
|
||||||
|
// tx 3 arrives and is added, while 4 is moved to ready
|
||||||
|
addLocalTransactions(senderLimitedTransactions, sender, 3); |
||||||
|
assertThat(senderLimitedTransactions.size()).isEqualTo(4); |
||||||
|
assertThat(senderLimitedTransactions.getNextNonceForSender(transaction2.getSender())) |
||||||
|
.isPresent() |
||||||
|
.hasValue(5); |
||||||
|
|
||||||
|
// for sender max 4 txs are allowed, so 5, 6 and 7 are dropped since too far in future
|
||||||
|
addLocalTransactions(senderLimitedTransactions, sender, 5, 6, 7); |
||||||
|
assertThat(senderLimitedTransactions.size()).isEqualTo(4); |
||||||
|
|
||||||
|
// assert that we drop txs with future nonce first
|
||||||
|
assertThat(senderLimitedTransactions.getNextNonceForSender(transaction2.getSender())) |
||||||
|
.isPresent() |
||||||
|
.hasValue(5); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void correctNonceIsReturnedWithRepeatedTransactions() { |
||||||
|
assertThat(pendingTransactions.getNextNonceForSender(transaction2.getSender())).isEmpty(); |
||||||
|
final Account sender = mock(Account.class); |
||||||
|
addLocalTransactions(pendingTransactions, sender, 0, 1, 2, 1, 0, 4); |
||||||
|
assertThat(pendingTransactions.getNextNonceForSender(transaction2.getSender())) |
||||||
|
.isPresent() |
||||||
|
.hasValue(3); |
||||||
|
addLocalTransactions(pendingTransactions, sender, 3); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void shouldNotIncrementAddedCounterWhenRemoteTransactionAlreadyPresent() { |
||||||
|
pendingTransactions.addLocalTransaction(transaction0, Optional.empty()); |
||||||
|
assertThat(pendingTransactions.size()).isEqualTo(1); |
||||||
|
assertThat(getAddedCount(LOCAL, layers.prioritizedTransactions.name())).isEqualTo(1); |
||||||
|
assertThat(getAddedCount(REMOTE, layers.prioritizedTransactions.name())).isZero(); |
||||||
|
|
||||||
|
assertThat(pendingTransactions.addRemoteTransaction(transaction0, Optional.empty())) |
||||||
|
.isEqualTo(ALREADY_KNOWN); |
||||||
|
assertThat(pendingTransactions.size()).isEqualTo(1); |
||||||
|
assertThat(getAddedCount(LOCAL, layers.prioritizedTransactions.name())).isEqualTo(1); |
||||||
|
assertThat(getAddedCount(REMOTE, layers.prioritizedTransactions.name())).isZero(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void shouldNotIncrementAddedCounterWhenLocalTransactionAlreadyPresent() { |
||||||
|
pendingTransactions.addRemoteTransaction(transaction0, Optional.empty()); |
||||||
|
assertThat(pendingTransactions.size()).isEqualTo(1); |
||||||
|
assertThat(getAddedCount(LOCAL, layers.prioritizedTransactions.name())).isZero(); |
||||||
|
assertThat(getAddedCount(REMOTE, layers.prioritizedTransactions.name())).isEqualTo(1); |
||||||
|
|
||||||
|
assertThat(pendingTransactions.addLocalTransaction(transaction0, Optional.empty())) |
||||||
|
.isEqualTo(ALREADY_KNOWN); |
||||||
|
assertThat(pendingTransactions.size()).isEqualTo(1); |
||||||
|
assertThat(getAddedCount(LOCAL, layers.prioritizedTransactions.name())).isZero(); |
||||||
|
assertThat(getAddedCount(REMOTE, layers.prioritizedTransactions.name())).isEqualTo(1); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void doNothingIfTransactionAlreadyPending() { |
||||||
|
final var addedTxs = populateCache(1, 0); |
||||||
|
assertThat( |
||||||
|
pendingTransactions.addRemoteTransaction( |
||||||
|
addedTxs[0].transaction, Optional.of(addedTxs[0].account))) |
||||||
|
.isEqualTo(ALREADY_KNOWN); |
||||||
|
assertTransactionPending(pendingTransactions, addedTxs[0].transaction); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void returnsCorrectNextNonceWhenAddedTransactionsHaveGaps() { |
||||||
|
final var addedTxs = populateCache(3, 0, 1); |
||||||
|
assertThat(pendingTransactions.getNextNonceForSender(addedTxs[0].transaction.getSender())) |
||||||
|
.isPresent() |
||||||
|
.hasValue(1); |
||||||
|
} |
||||||
|
|
||||||
|
private TransactionAndAccount[] populateCache(final int numTxs, final long startingNonce) { |
||||||
|
return populateCache(numTxs, KEYS1, startingNonce, OptionalLong.empty()); |
||||||
|
} |
||||||
|
|
||||||
|
private TransactionAndAccount[] populateCache( |
||||||
|
final int numTxs, final long startingNonce, final long missingNonce) { |
||||||
|
return populateCache(numTxs, KEYS1, startingNonce, OptionalLong.of(missingNonce)); |
||||||
|
} |
||||||
|
|
||||||
|
private TransactionAndAccount[] populateCache( |
||||||
|
final int numTxs, |
||||||
|
final KeyPair keys, |
||||||
|
final long startingNonce, |
||||||
|
final OptionalLong maybeGapNonce) { |
||||||
|
final List<TransactionAndAccount> addedTransactions = new ArrayList<>(numTxs); |
||||||
|
for (int i = 0; i < numTxs; i++) { |
||||||
|
final long nonce = startingNonce + i; |
||||||
|
if (maybeGapNonce.isEmpty() || maybeGapNonce.getAsLong() != nonce) { |
||||||
|
final var transaction = createTransaction(nonce, keys); |
||||||
|
final Account sender = mock(Account.class); |
||||||
|
when(sender.getNonce()).thenReturn(startingNonce); |
||||||
|
final var res = pendingTransactions.addRemoteTransaction(transaction, Optional.of(sender)); |
||||||
|
assertTransactionPending(pendingTransactions, transaction); |
||||||
|
assertThat(res).isEqualTo(ADDED); |
||||||
|
addedTransactions.add(new TransactionAndAccount(transaction, sender)); |
||||||
|
} |
||||||
|
} |
||||||
|
return addedTransactions.toArray(TransactionAndAccount[]::new); |
||||||
|
} |
||||||
|
|
||||||
|
record TransactionAndAccount(Transaction transaction, Account account) {} |
||||||
|
|
||||||
|
record CreatedLayers( |
||||||
|
AbstractPrioritizedTransactions prioritizedTransactions, |
||||||
|
ReadyTransactions readyTransactions, |
||||||
|
SparseTransactions sparseTransactions, |
||||||
|
EvictCollectorLayer evictedCollector) {} |
||||||
|
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,297 @@ |
|||||||
|
/* |
||||||
|
* Copyright Hyperledger Besu Contributors. |
||||||
|
* |
||||||
|
* 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. See the License for the |
||||||
|
* specific language governing permissions and limitations under the License. |
||||||
|
* |
||||||
|
* SPDX-License-Identifier: Apache-2.0 |
||||||
|
*/ |
||||||
|
package org.hyperledger.besu.ethereum.eth.transactions.layered; |
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||||
|
import static org.assertj.core.api.Assertions.fail; |
||||||
|
import static org.hyperledger.besu.ethereum.eth.transactions.layered.TransactionsLayer.RemovalReason.INVALIDATED; |
||||||
|
import static org.mockito.Mockito.mock; |
||||||
|
import static org.mockito.Mockito.when; |
||||||
|
|
||||||
|
import org.hyperledger.besu.datatypes.Address; |
||||||
|
import org.hyperledger.besu.datatypes.Wei; |
||||||
|
import org.hyperledger.besu.ethereum.core.BlockHeader; |
||||||
|
import org.hyperledger.besu.ethereum.core.Transaction; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.ImmutableTransactionPoolConfiguration; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.TransactionAddedResult; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolConfiguration; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolMetrics; |
||||||
|
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolReplacementHandler; |
||||||
|
import org.hyperledger.besu.ethereum.mainnet.MainnetBlockHeaderFunctions; |
||||||
|
import org.hyperledger.besu.ethereum.mainnet.feemarket.BaseFeeMarket; |
||||||
|
import org.hyperledger.besu.ethereum.mainnet.feemarket.FeeMarket; |
||||||
|
import org.hyperledger.besu.ethereum.rlp.BytesValueRLPInput; |
||||||
|
import org.hyperledger.besu.ethereum.rlp.RLPInput; |
||||||
|
import org.hyperledger.besu.evm.account.Account; |
||||||
|
import org.hyperledger.besu.metrics.StubMetricsSystem; |
||||||
|
|
||||||
|
import java.io.BufferedReader; |
||||||
|
import java.io.IOException; |
||||||
|
import java.io.InputStreamReader; |
||||||
|
import java.math.BigInteger; |
||||||
|
import java.nio.charset.StandardCharsets; |
||||||
|
import java.time.Instant; |
||||||
|
import java.util.HashMap; |
||||||
|
import java.util.List; |
||||||
|
import java.util.Map; |
||||||
|
import java.util.Optional; |
||||||
|
import java.util.zip.GZIPInputStream; |
||||||
|
|
||||||
|
import com.google.common.base.Splitter; |
||||||
|
import kotlin.ranges.LongRange; |
||||||
|
import org.apache.tuweni.bytes.Bytes; |
||||||
|
import org.junit.jupiter.api.Disabled; |
||||||
|
import org.junit.jupiter.api.Test; |
||||||
|
import org.slf4j.Logger; |
||||||
|
import org.slf4j.LoggerFactory; |
||||||
|
|
||||||
|
public class ReplayTest { |
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(ReplayTest.class); |
||||||
|
private final TransactionPoolConfiguration poolConfig = |
||||||
|
ImmutableTransactionPoolConfiguration.builder().build(); |
||||||
|
|
||||||
|
private final StubMetricsSystem metricsSystem = new StubMetricsSystem(); |
||||||
|
private final TransactionPoolMetrics txPoolMetrics = new TransactionPoolMetrics(metricsSystem); |
||||||
|
|
||||||
|
private final Address senderToLog = |
||||||
|
Address.fromHexString("0x1a8ed0d3ad42c9019cc141aace7e5fb6e576b917"); |
||||||
|
|
||||||
|
private BlockHeader currBlockHeader; |
||||||
|
|
||||||
|
/** |
||||||
|
* Ignored by default since this is useful to debug issues having a dump of txs that could be |
||||||
|
* quite big and so could take many minutes to execute. To generate the input file for the test |
||||||
|
* enable the LOG_FOR_REPLAY logger by adding these parts to the log4j2 configuration: in the |
||||||
|
* Appenders section add |
||||||
|
* |
||||||
|
* <pre>{@code |
||||||
|
* <RollingFile name="txCSV" fileName="/data/besu/tx.csv" filePattern="/data/besu/tx-%d{MM-dd-yyyy}-%i.csv.gz"> |
||||||
|
* <PatternLayout> |
||||||
|
* <Pattern>%m%n</Pattern> |
||||||
|
* </PatternLayout> |
||||||
|
* <Policies> |
||||||
|
* <OnStartupTriggeringPolicy /> |
||||||
|
* </Policies> |
||||||
|
* </RollingFile> |
||||||
|
* }</pre> |
||||||
|
* |
||||||
|
* in the Loggers section add |
||||||
|
* |
||||||
|
* <pre>{@code |
||||||
|
* <Logger name="LOG_FOR_REPLAY" level="TRACE" additivity="false"> |
||||||
|
* <AppenderRef ref="txCSV" /> |
||||||
|
* </Logger> |
||||||
|
* }</pre> |
||||||
|
* |
||||||
|
* restart and let it run until you need it, then copy the CSV in the test resource folder. |
||||||
|
* |
||||||
|
* @throws IOException when fails to read the resource |
||||||
|
*/ |
||||||
|
@Test |
||||||
|
@Disabled("Provide a replay file to run the test on demand") |
||||||
|
public void replay() throws IOException { |
||||||
|
try (BufferedReader br = |
||||||
|
new BufferedReader( |
||||||
|
new InputStreamReader( |
||||||
|
new GZIPInputStream(getClass().getResourceAsStream("/tx.csv.gz")), |
||||||
|
StandardCharsets.UTF_8))) { |
||||||
|
currBlockHeader = mockBlockHeader(br.readLine()); |
||||||
|
final BaseFeeMarket baseFeeMarket = FeeMarket.london(0L); |
||||||
|
|
||||||
|
final AbstractPrioritizedTransactions prioritizedTransactions = |
||||||
|
createLayers(poolConfig, txPoolMetrics, baseFeeMarket); |
||||||
|
final LayeredPendingTransactions pendingTransactions = |
||||||
|
new LayeredPendingTransactions(poolConfig, prioritizedTransactions); |
||||||
|
br.lines() |
||||||
|
.forEach( |
||||||
|
line -> { |
||||||
|
try { |
||||||
|
final String[] commaSplit = line.split(","); |
||||||
|
final String type = commaSplit[0]; |
||||||
|
switch (type) { |
||||||
|
case "T": |
||||||
|
System.out.println( |
||||||
|
"T:" |
||||||
|
+ commaSplit[1] |
||||||
|
+ " @ " |
||||||
|
+ Instant.ofEpochMilli(Long.parseLong(commaSplit[2]))); |
||||||
|
processTransaction(commaSplit, pendingTransactions, prioritizedTransactions); |
||||||
|
break; |
||||||
|
case "B": |
||||||
|
System.out.println("B:" + commaSplit[1]); |
||||||
|
processBlock(commaSplit, prioritizedTransactions, baseFeeMarket); |
||||||
|
break; |
||||||
|
case "S": |
||||||
|
// ToDo: commented since not always working, needs fix
|
||||||
|
// System.out.println("S");
|
||||||
|
// assertStats(line, pendingTransactions);
|
||||||
|
break; |
||||||
|
case "D": |
||||||
|
System.out.println("D:" + commaSplit[1]); |
||||||
|
processInvalid(commaSplit, prioritizedTransactions); |
||||||
|
break; |
||||||
|
default: |
||||||
|
throw new IllegalArgumentException("Unexpected first field value " + type); |
||||||
|
} |
||||||
|
} catch (Throwable throwable) { |
||||||
|
fail(line, throwable); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private BlockHeader mockBlockHeader(final String line) { |
||||||
|
final List<String> commaSplit = Splitter.on(',').splitToList(line); |
||||||
|
final long number = Long.parseLong(commaSplit.get(0)); |
||||||
|
final Wei initBaseFee = Wei.of(new BigInteger(commaSplit.get(1))); |
||||||
|
final long gasUsed = Long.parseLong(commaSplit.get(2)); |
||||||
|
final long gasLimit = Long.parseLong(commaSplit.get(3)); |
||||||
|
|
||||||
|
final BlockHeader mockHeader = mock(BlockHeader.class); |
||||||
|
when(mockHeader.getNumber()).thenReturn(number); |
||||||
|
when(mockHeader.getBaseFee()).thenReturn(Optional.of(initBaseFee)); |
||||||
|
when(mockHeader.getGasUsed()).thenReturn(gasUsed); |
||||||
|
when(mockHeader.getGasLimit()).thenReturn(gasLimit); |
||||||
|
|
||||||
|
return mockHeader; |
||||||
|
} |
||||||
|
|
||||||
|
private BaseFeePrioritizedTransactions createLayers( |
||||||
|
final TransactionPoolConfiguration poolConfig, |
||||||
|
final TransactionPoolMetrics txPoolMetrics, |
||||||
|
final BaseFeeMarket baseFeeMarket) { |
||||||
|
final EvictCollectorLayer evictCollector = new EvictCollectorLayer(txPoolMetrics); |
||||||
|
final SparseTransactions sparseTransactions = |
||||||
|
new SparseTransactions( |
||||||
|
poolConfig, evictCollector, txPoolMetrics, this::transactionReplacementTester); |
||||||
|
|
||||||
|
final ReadyTransactions readyTransactions = |
||||||
|
new ReadyTransactions( |
||||||
|
poolConfig, sparseTransactions, txPoolMetrics, this::transactionReplacementTester); |
||||||
|
|
||||||
|
return new BaseFeePrioritizedTransactions( |
||||||
|
poolConfig, |
||||||
|
() -> currBlockHeader, |
||||||
|
readyTransactions, |
||||||
|
txPoolMetrics, |
||||||
|
this::transactionReplacementTester, |
||||||
|
baseFeeMarket); |
||||||
|
} |
||||||
|
|
||||||
|
// ToDo: commented since not always working, needs fix
|
||||||
|
// private void assertStats(
|
||||||
|
// final String line, final LayeredPendingTransactions pendingTransactions) {
|
||||||
|
// final String statsString = line.substring(2);
|
||||||
|
// assertThat(pendingTransactions.logStats()).as(line).endsWith(statsString);
|
||||||
|
// }
|
||||||
|
|
||||||
|
private void processBlock( |
||||||
|
final String[] commaSplit, |
||||||
|
final AbstractPrioritizedTransactions prioritizedTransactions, |
||||||
|
final FeeMarket feeMarket) { |
||||||
|
final Bytes bytes = Bytes.fromHexString(commaSplit[commaSplit.length - 1]); |
||||||
|
final RLPInput rlpInput = new BytesValueRLPInput(bytes, false); |
||||||
|
final BlockHeader blockHeader = |
||||||
|
BlockHeader.readFrom(rlpInput, new MainnetBlockHeaderFunctions()); |
||||||
|
|
||||||
|
final Map<Address, Long> maxNonceBySender = new HashMap<>(); |
||||||
|
int i = 3; |
||||||
|
if (!commaSplit[i].equals("")) { |
||||||
|
while (!commaSplit[i].equals("R")) { |
||||||
|
final Address sender = Address.fromHexString(commaSplit[i]); |
||||||
|
final long nonce = Long.parseLong(commaSplit[i + 1]); |
||||||
|
maxNonceBySender.put(sender, nonce); |
||||||
|
i += 2; |
||||||
|
} |
||||||
|
} else { |
||||||
|
++i; |
||||||
|
} |
||||||
|
|
||||||
|
++i; |
||||||
|
final Map<Address, LongRange> nonceRangeBySender = new HashMap<>(); |
||||||
|
if (!commaSplit[i].equals("")) { |
||||||
|
for (; i < commaSplit.length - 1; i += 3) { |
||||||
|
final Address sender = Address.fromHexString(commaSplit[i]); |
||||||
|
final long start = Long.parseLong(commaSplit[i + 1]); |
||||||
|
final long end = Long.parseLong(commaSplit[i + 2]); |
||||||
|
nonceRangeBySender.put(sender, new LongRange(start, end)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (maxNonceBySender.containsKey(senderToLog) || nonceRangeBySender.containsKey(senderToLog)) { |
||||||
|
LOG.warn( |
||||||
|
"B {} M {} R {} Before {}", |
||||||
|
blockHeader.getNumber(), |
||||||
|
maxNonceBySender.get(senderToLog), |
||||||
|
nonceRangeBySender.get(senderToLog), |
||||||
|
prioritizedTransactions.logSender(senderToLog)); |
||||||
|
} |
||||||
|
prioritizedTransactions.blockAdded(feeMarket, blockHeader, maxNonceBySender); |
||||||
|
if (maxNonceBySender.containsKey(senderToLog) || nonceRangeBySender.containsKey(senderToLog)) { |
||||||
|
LOG.warn("After {}", prioritizedTransactions.logSender(senderToLog)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void processTransaction( |
||||||
|
final String[] commaSplit, |
||||||
|
final LayeredPendingTransactions pendingTransactions, |
||||||
|
final AbstractPrioritizedTransactions prioritizedTransactions) { |
||||||
|
final Bytes rlp = Bytes.fromHexString(commaSplit[commaSplit.length - 1]); |
||||||
|
final Transaction tx = Transaction.readFrom(rlp); |
||||||
|
final Account mockAccount = mock(Account.class); |
||||||
|
final long nonce = Long.parseLong(commaSplit[4]); |
||||||
|
when(mockAccount.getNonce()).thenReturn(nonce); |
||||||
|
if (tx.getSender().equals(senderToLog)) { |
||||||
|
LOG.warn( |
||||||
|
"N {} T {}, Before {}", |
||||||
|
nonce, |
||||||
|
tx.getNonce(), |
||||||
|
prioritizedTransactions.logSender(senderToLog)); |
||||||
|
} |
||||||
|
assertThat(pendingTransactions.addRemoteTransaction(tx, Optional.of(mockAccount))) |
||||||
|
.isNotEqualTo(TransactionAddedResult.INTERNAL_ERROR); |
||||||
|
if (tx.getSender().equals(senderToLog)) { |
||||||
|
LOG.warn("After {}", prioritizedTransactions.logSender(senderToLog)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void processInvalid( |
||||||
|
final String[] commaSplit, final AbstractPrioritizedTransactions prioritizedTransactions) { |
||||||
|
final Bytes rlp = Bytes.fromHexString(commaSplit[commaSplit.length - 1]); |
||||||
|
final Transaction tx = Transaction.readFrom(rlp); |
||||||
|
if (tx.getSender().equals(senderToLog)) { |
||||||
|
LOG.warn("D {}, Before {}", tx.getNonce(), prioritizedTransactions.logSender(senderToLog)); |
||||||
|
} |
||||||
|
prioritizedTransactions.remove(new PendingTransaction.Remote(tx), INVALIDATED); |
||||||
|
if (tx.getSender().equals(senderToLog)) { |
||||||
|
LOG.warn("After {}", prioritizedTransactions.logSender(senderToLog)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private boolean transactionReplacementTester( |
||||||
|
final PendingTransaction pt1, final PendingTransaction pt2) { |
||||||
|
return transactionReplacementTester(poolConfig, pt1, pt2); |
||||||
|
} |
||||||
|
|
||||||
|
private boolean transactionReplacementTester( |
||||||
|
final TransactionPoolConfiguration poolConfig, |
||||||
|
final PendingTransaction pt1, |
||||||
|
final PendingTransaction pt2) { |
||||||
|
final TransactionPoolReplacementHandler transactionReplacementHandler = |
||||||
|
new TransactionPoolReplacementHandler(poolConfig.getPriceBump()); |
||||||
|
return transactionReplacementHandler.shouldReplace(pt1, pt2, currBlockHeader); |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue