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
Fabio Di Fabio 2 years ago committed by GitHub
parent 7fff05a4cc
commit 423fe1d481
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      .gitignore
  2. 1
      CHANGELOG.md
  3. 3
      besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java
  4. 79
      besu/src/main/java/org/hyperledger/besu/cli/options/unstable/TransactionPoolOptions.java
  5. 19
      besu/src/test/java/org/hyperledger/besu/cli/options/TransactionPoolOptionsTest.java
  6. 6
      ethereum/api/src/integration-test/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/fork/frontier/EthGetFilterChangesIntegrationTest.java
  7. 6
      ethereum/api/src/integration-test/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/fork/london/EthGetFilterChangesIntegrationTest.java
  8. 4
      ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/TxPoolBesuPendingTransactions.java
  9. 5
      ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/TxPoolBesuStatistics.java
  10. 2
      ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/PendingTransactionResult.java
  11. 14
      ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/PendingTransactionsResult.java
  12. 9
      ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/transaction/pool/PendingTransactionFilter.java
  13. 4
      ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/pending/PendingTransactionSubscriptionService.java
  14. 12
      ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/graphql/AbstractEthGraphQLHttpServiceTest.java
  15. 5
      ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/AbstractJsonRpcHttpServiceTest.java
  16. 39
      ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/LatestNonceProviderTest.java
  17. 9
      ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetTransactionByHashTest.java
  18. 56
      ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetTransactionCountTest.java
  19. 9
      ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/TxPoolBesuPendingTransactionsTest.java
  20. 4
      ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/TxPoolBesuStatisticsTest.java
  21. 10
      ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/TxPoolBesuTransactionsTest.java
  22. 6
      ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/transaction/pool/PendingTransactionFilterTest.java
  23. 7
      ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/BlockTransactionSelector.java
  24. 30
      ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/Transaction.java
  25. 38
      ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bonsai/AbstractIsolationTests.java
  26. 1
      ethereum/eth/build.gradle
  27. 34
      ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/task/BufferedGetPooledTransactionsFromPeerFetcher.java
  28. 32
      ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/NewPooledTransactionHashesMessageProcessor.java
  29. 2
      ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/PeerTransactionTracker.java
  30. 156
      ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/PendingTransaction.java
  31. 2
      ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/PendingTransactionAddedListener.java
  32. 60
      ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/PendingTransactions.java
  33. 129
      ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionAddedResult.java
  34. 42
      ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionAddedStatus.java
  35. 9
      ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionBroadcaster.java
  36. 202
      ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPool.java
  37. 32
      ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPoolConfiguration.java
  38. 109
      ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPoolFactory.java
  39. 173
      ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPoolMetrics.java
  40. 42
      ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionsMessageProcessor.java
  41. 139
      ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/AbstractPrioritizedTransactions.java
  42. 160
      ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/AbstractSequentialTransactionsLayer.java
  43. 596
      ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/AbstractTransactionsLayer.java
  44. 152
      ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/BaseFeePrioritizedTransactions.java
  45. 171
      ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/EndLayer.java
  46. 80
      ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/GasPricePrioritizedTransactions.java
  47. 479
      ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/LayeredPendingTransactions.java
  48. 221
      ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/ReadyTransactions.java
  49. 374
      ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/SparseTransactions.java
  50. 103
      ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/TransactionsLayer.java
  51. 76
      ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/package-info.java
  52. 129
      ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/sorter/AbstractPendingTransactionsSorter.java
  53. 9
      ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/sorter/BaseFeePendingTransactionsSorter.java
  54. 5
      ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/sorter/GasPricePendingTransactionsSorter.java
  55. 14
      ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/task/BufferedGetPooledTransactionsFromPeerFetcherTest.java
  56. 4
      ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/task/GetPooledTransactionsFromPeerTaskTest.java
  57. 8
      ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/AbstractTransactionPoolTest.java
  58. 697
      ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/AbstractTransactionsLayeredPendingTransactionsTest.java
  59. 10
      ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/NewPooledTransactionHashesMessageProcessorTest.java
  60. 18
      ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/NewPooledTransactionHashesMessageSenderTest.java
  61. 382
      ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/PendingMultiTypesTransactionsTest.java
  62. 414
      ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/PendingTransactionEstimatedMemorySizeTest.java
  63. 5
      ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionBroadcasterTest.java
  64. 4
      ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPoolFactoryTest.java
  65. 15
      ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionsMessageProcessorTest.java
  66. 185
      ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/AbstractPrioritizedTransactionsTestBase.java
  67. 173
      ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/BaseFeePrioritizedTransactionsTest.java
  68. 178
      ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/BaseTransactionPoolTest.java
  69. 47
      ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/EvictCollectorLayer.java
  70. 91
      ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/GasPricePrioritizedTransactionsTest.java
  71. 241
      ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/LayeredPendingTransactionsLegacyTest.java
  72. 294
      ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/LayeredPendingTransactionsLondonTest.java
  73. 713
      ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/LayeredPendingTransactionsTest.java
  74. 1331
      ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/LayersTest.java
  75. 297
      ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/ReplayTest.java
  76. 18
      ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/sorter/AbstractPendingTransactionsTestBase.java
  77. 3
      ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/sorter/BaseFeePendingTransactionsTest.java
  78. 3
      ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/sorter/GasPricePendingTransactionsTest.java
  79. 2
      ethereum/rlp/src/main/java/org/hyperledger/besu/ethereum/rlp/RLPInput.java
  80. 13
      gradle/verification-metadata.xml
  81. 1
      gradle/versions.gradle

4
.gitignore vendored

@ -4,7 +4,6 @@
*~.nib
*.iml
*.launch
*.swp
*.log
.classpath
.DS_Store
@ -29,4 +28,5 @@ site/
/kubernetes/reports/
/kubernetes/besu-*.tar.gz
**/src/*/generated
jitpack.yml
jitpack.yml
/ethereum/eth/src/test/resources/tx.csv.gz

@ -9,6 +9,7 @@
- EIP-4844: Zero blob transactions are invalid [#5425](https://github.com/hyperledger/besu/pull/5425)
- Transaction pool flag to disable specific behaviors for locally submitted transactions [#5418](https://github.com/hyperledger/besu/pull/5418)
- New optional feature to save the txpool content to file on shutdown and reloading it on startup [#5434](https://github.com/hyperledger/besu/pull/5434)
- Early access - layered transaction pool implementation [#5290](https://github.com/hyperledger/besu/pull/5290)
### Bug Fixes
- Fix eth_feeHistory response for the case in which blockCount is higher than highestBlock requested. [#5397](https://github.com/hyperledger/besu/pull/5397)

@ -1202,7 +1202,8 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
description =
"Maximum number of pending transactions that will be kept in the transaction pool (default: ${DEFAULT-VALUE})",
arity = "1")
private final Integer txPoolMaxSize = TransactionPoolConfiguration.MAX_PENDING_TRANSACTIONS;
private final Integer txPoolMaxSize =
TransactionPoolConfiguration.DEFAULT_MAX_PENDING_TRANSACTIONS;
@Option(
names = {"--tx-pool-retention-hours"},

@ -25,11 +25,15 @@ import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import picocli.CommandLine;
/** The Transaction pool Cli options. */
public class TransactionPoolOptions
implements CLIOptions<ImmutableTransactionPoolConfiguration.Builder> {
private static final Logger LOG = LoggerFactory.getLogger(TransactionPoolOptions.class);
private static final String TX_MESSAGE_KEEP_ALIVE_SEC_FLAG =
"--Xincoming-tx-messages-keep-alive-seconds";
@ -47,6 +51,14 @@ public class TransactionPoolOptions
private static final String SAVE_RESTORE_FLAG = "--tx-pool-enable-save-restore";
private static final String SAVE_FILE = "--tx-pool-save-file";
private static final String LAYERED_TX_POOL_ENABLED_FLAG = "--Xlayered-tx-pool";
private static final String LAYERED_TX_POOL_LAYER_MAX_CAPACITY =
"--Xlayered-tx-pool-layer-max-capacity";
private static final String LAYERED_TX_POOL_MAX_PRIORITIZED =
"--Xlayered-tx-pool-max-prioritized";
private static final String LAYERED_TX_POOL_MAX_FUTURE_BY_SENDER =
"--Xlayered-tx-pool-max-future-by-sender";
@CommandLine.Option(
names = {STRICT_TX_REPLAY_PROTECTION_ENABLED_FLAG},
paramLabel = "<Boolean>",
@ -84,7 +96,46 @@ public class TransactionPoolOptions
"Maximum portion of the transaction pool which a single account may occupy with future transactions (default: ${DEFAULT-VALUE})",
arity = "1")
private Float txPoolLimitByAccountPercentage =
TransactionPoolConfiguration.LIMIT_TXPOOL_BY_ACCOUNT_PERCENTAGE;
TransactionPoolConfiguration.DEFAULT_LIMIT_TX_POOL_BY_ACCOUNT_PERCENTAGE;
@CommandLine.Option(
names = {LAYERED_TX_POOL_ENABLED_FLAG},
paramLabel = "<Boolean>",
hidden = true,
description = "Enable the Layered Transaction Pool (default: ${DEFAULT-VALUE})",
arity = "0..1")
private Boolean layeredTxPoolEnabled =
TransactionPoolConfiguration.DEFAULT_LAYERED_TX_POOL_ENABLED;
@CommandLine.Option(
names = {LAYERED_TX_POOL_LAYER_MAX_CAPACITY},
paramLabel = "<Long>",
hidden = true,
description =
"Max amount of memory space, in bytes, that any layer within the transaction pool could occupy (default: ${DEFAULT-VALUE})",
arity = "1")
private long layeredTxPoolLayerMaxCapacity =
TransactionPoolConfiguration.DEFAULT_PENDING_TRANSACTIONS_LAYER_MAX_CAPACITY_BYTES;
@CommandLine.Option(
names = {LAYERED_TX_POOL_MAX_PRIORITIZED},
paramLabel = "<Int>",
hidden = true,
description =
"Max number of pending transactions that are prioritized and thus kept sorted (default: ${DEFAULT-VALUE})",
arity = "1")
private int layeredTxPoolMaxPrioritized =
TransactionPoolConfiguration.DEFAULT_MAX_PRIORITIZED_TRANSACTIONS;
@CommandLine.Option(
names = {LAYERED_TX_POOL_MAX_FUTURE_BY_SENDER},
paramLabel = "<Int>",
hidden = true,
description =
"Max number of future pending transactions allowed for a single sender (default: ${DEFAULT-VALUE})",
arity = "1")
private int layeredTxPoolMaxFutureBySender =
TransactionPoolConfiguration.DEFAULT_MAX_FUTURE_BY_SENDER;
@CommandLine.Option(
names = {DISABLE_LOCAL_TXS_FLAG},
@ -139,11 +190,21 @@ public class TransactionPoolOptions
options.disableLocalTxs = config.getDisableLocalTransactions();
options.saveRestoreEnabled = config.getEnableSaveRestore();
options.saveFile = config.getSaveFile();
options.layeredTxPoolEnabled = config.getLayeredTxPoolEnabled();
options.layeredTxPoolLayerMaxCapacity = config.getPendingTransactionsLayerMaxCapacityBytes();
options.layeredTxPoolMaxPrioritized = config.getMaxPrioritizedTransactions();
options.layeredTxPoolMaxFutureBySender = config.getMaxFutureBySender();
return options;
}
@Override
public ImmutableTransactionPoolConfiguration.Builder toDomainObject() {
if (layeredTxPoolEnabled) {
LOG.warn(
"Layered transaction pool enabled, ignoring settings for "
+ "--tx-pool-max-size and --tx-pool-limit-by-account-percentage");
}
return ImmutableTransactionPoolConfiguration.builder()
.strictTransactionReplayProtectionEnabled(strictTxReplayProtectionEnabled)
.txMessageKeepAliveSeconds(txMessageKeepAliveSeconds)
@ -151,7 +212,12 @@ public class TransactionPoolOptions
.txPoolLimitByAccountPercentage(txPoolLimitByAccountPercentage)
.disableLocalTransactions(disableLocalTxs)
.enableSaveRestore(saveRestoreEnabled)
.saveFile(saveFile);
.saveFile(saveFile)
.txPoolLimitByAccountPercentage(txPoolLimitByAccountPercentage)
.layeredTxPoolEnabled(layeredTxPoolEnabled)
.pendingTransactionsLayerMaxCapacityBytes(layeredTxPoolLayerMaxCapacity)
.maxPrioritizedTransactions(layeredTxPoolMaxPrioritized)
.maxFutureBySender(layeredTxPoolMaxFutureBySender);
}
@Override
@ -166,7 +232,14 @@ public class TransactionPoolOptions
TX_MESSAGE_KEEP_ALIVE_SEC_FLAG,
OptionParser.format(txMessageKeepAliveSeconds),
ETH65_TX_ANNOUNCED_BUFFERING_PERIOD_FLAG,
OptionParser.format(eth65TrxAnnouncedBufferingPeriod));
OptionParser.format(eth65TrxAnnouncedBufferingPeriod),
LAYERED_TX_POOL_ENABLED_FLAG + "=" + layeredTxPoolEnabled,
LAYERED_TX_POOL_LAYER_MAX_CAPACITY,
OptionParser.format(layeredTxPoolLayerMaxCapacity),
LAYERED_TX_POOL_MAX_PRIORITIZED,
OptionParser.format(layeredTxPoolMaxPrioritized),
LAYERED_TX_POOL_MAX_FUTURE_BY_SENDER,
OptionParser.format(layeredTxPoolMaxFutureBySender));
}
/**

@ -16,7 +16,7 @@ package org.hyperledger.besu.cli.options;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolConfiguration.LIMIT_TXPOOL_BY_ACCOUNT_PERCENTAGE;
import static org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolConfiguration.DEFAULT_LIMIT_TX_POOL_BY_ACCOUNT_PERCENTAGE;
import org.hyperledger.besu.cli.options.unstable.TransactionPoolOptions;
import org.hyperledger.besu.ethereum.eth.transactions.ImmutableTransactionPoolConfiguration;
@ -101,7 +101,7 @@ public class TransactionPoolOptionsTest
final TransactionPoolOptions options = getOptionsFromBesuCommand(cmd);
final TransactionPoolConfiguration config = options.toDomainObject().build();
assertThat(config.getTxPoolLimitByAccountPercentage())
.isEqualTo(LIMIT_TXPOOL_BY_ACCOUNT_PERCENTAGE);
.isEqualTo(DEFAULT_LIMIT_TX_POOL_BY_ACCOUNT_PERCENTAGE);
assertThat(commandOutput.toString(UTF_8)).isEmpty();
assertThat(commandErrorOutput.toString(UTF_8))
@ -239,7 +239,13 @@ public class TransactionPoolOptionsTest
.txPoolLimitByAccountPercentage(defaultValue.getTxPoolLimitByAccountPercentage())
.disableLocalTransactions(defaultValue.getDisableLocalTransactions())
.enableSaveRestore(defaultValue.getEnableSaveRestore())
.saveFile(defaultValue.getSaveFile());
.saveFile(defaultValue.getSaveFile())
.txPoolLimitByAccountPercentage(defaultValue.getTxPoolLimitByAccountPercentage())
.layeredTxPoolEnabled(defaultValue.getLayeredTxPoolEnabled())
.pendingTransactionsLayerMaxCapacityBytes(
defaultValue.getPendingTransactionsLayerMaxCapacityBytes())
.maxPrioritizedTransactions(defaultValue.getMaxPrioritizedTransactions())
.maxFutureBySender(defaultValue.getMaxFutureBySender());
}
@Override
@ -253,7 +259,12 @@ public class TransactionPoolOptionsTest
.txPoolLimitByAccountPercentage(0.5f)
.disableLocalTransactions(true)
.enableSaveRestore(true)
.saveFile(new File("abc.xyz"));
.saveFile(new File("abc.xyz"))
.txPoolLimitByAccountPercentage(0.5f)
.layeredTxPoolEnabled(true)
.pendingTransactionsLayerMaxCapacityBytes(1_000_000L)
.maxPrioritizedTransactions(1000)
.maxFutureBySender(10);
}
@Override

@ -49,9 +49,11 @@ import org.hyperledger.besu.ethereum.core.TransactionReceipt;
import org.hyperledger.besu.ethereum.eth.manager.EthContext;
import org.hyperledger.besu.ethereum.eth.manager.EthPeers;
import org.hyperledger.besu.ethereum.eth.transactions.ImmutableTransactionPoolConfiguration;
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactions;
import org.hyperledger.besu.ethereum.eth.transactions.TransactionBroadcaster;
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPool;
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolConfiguration;
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolMetrics;
import org.hyperledger.besu.ethereum.eth.transactions.sorter.GasPricePendingTransactionsSorter;
import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem;
import org.hyperledger.besu.plugin.data.TransactionType;
@ -84,7 +86,7 @@ public class EthGetFilterChangesIntegrationTest {
private TransactionPool transactionPool;
private final MetricsSystem metricsSystem = new NoOpMetricsSystem();
private GasPricePendingTransactionsSorter transactions;
private PendingTransactions transactions;
private static final int MAX_TRANSACTIONS = 5;
private static final KeyPair keyPair = SignatureAlgorithmFactory.getInstance().generateKeyPair();
@ -116,7 +118,7 @@ public class EthGetFilterChangesIntegrationTest {
batchAddedListener,
ethContext,
new MiningParameters.Builder().minTransactionGasPrice(Wei.ZERO).build(),
metricsSystem,
new TransactionPoolMetrics(metricsSystem),
TransactionPoolConfiguration.DEFAULT);
final BlockchainQueries blockchainQueries =
new BlockchainQueries(blockchain, protocolContext.getWorldStateArchive());

@ -49,9 +49,11 @@ import org.hyperledger.besu.ethereum.core.TransactionReceipt;
import org.hyperledger.besu.ethereum.eth.manager.EthContext;
import org.hyperledger.besu.ethereum.eth.manager.EthPeers;
import org.hyperledger.besu.ethereum.eth.transactions.ImmutableTransactionPoolConfiguration;
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactions;
import org.hyperledger.besu.ethereum.eth.transactions.TransactionBroadcaster;
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPool;
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolConfiguration;
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolMetrics;
import org.hyperledger.besu.ethereum.eth.transactions.sorter.BaseFeePendingTransactionsSorter;
import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem;
import org.hyperledger.besu.plugin.data.TransactionType;
@ -84,7 +86,7 @@ public class EthGetFilterChangesIntegrationTest {
private TransactionPool transactionPool;
private final MetricsSystem metricsSystem = new NoOpMetricsSystem();
private BaseFeePendingTransactionsSorter transactions;
private PendingTransactions transactions;
private static final int MAX_TRANSACTIONS = 5;
private static final KeyPair keyPair = SignatureAlgorithmFactory.getInstance().generateKeyPair();
@ -116,7 +118,7 @@ public class EthGetFilterChangesIntegrationTest {
batchAddedListener,
ethContext,
new MiningParameters.Builder().minTransactionGasPrice(Wei.ZERO).build(),
metricsSystem,
new TransactionPoolMetrics(metricsSystem),
TransactionPoolConfiguration.DEFAULT);
final BlockchainQueries blockchainQueries =
new BlockchainQueries(blockchain, protocolContext.getWorldStateArchive());

@ -25,9 +25,9 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.transaction.po
import org.hyperledger.besu.ethereum.core.Transaction;
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactions;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
public class TxPoolBesuPendingTransactions implements JsonRpcMethod {
@ -56,7 +56,7 @@ public class TxPoolBesuPendingTransactions implements JsonRpcMethod {
.map(PendingTransactionsParams::filters)
.orElse(Collections.emptyList());
final Set<Transaction> pendingTransactionsFiltered =
final Collection<Transaction> pendingTransactionsFiltered =
pendingTransactionFilter.reduce(
pendingTransactions.getPendingTransactions(), filters, limit);

@ -22,7 +22,7 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.PendingTransac
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction;
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactions;
import java.util.Set;
import java.util.Collection;
public class TxPoolBesuStatistics implements JsonRpcMethod {
@ -43,7 +43,8 @@ public class TxPoolBesuStatistics implements JsonRpcMethod {
}
private PendingTransactionsStatisticsResult statistics() {
final Set<PendingTransaction> pendingTransaction = pendingTransactions.getPendingTransactions();
final Collection<PendingTransaction> pendingTransaction =
pendingTransactions.getPendingTransactions();
final long localCount =
pendingTransaction.stream().filter(PendingTransaction::isReceivedFromLocalSource).count();
final long remoteCount = pendingTransaction.size() - localCount;

@ -31,7 +31,7 @@ public class PendingTransactionResult implements TransactionResult {
public PendingTransactionResult(final PendingTransaction pendingTransaction) {
hash = pendingTransaction.getHash().toString();
isReceivedFromLocalSource = pendingTransaction.isReceivedFromLocalSource();
addedToPoolAt = pendingTransaction.getAddedToPoolAt();
addedToPoolAt = Instant.ofEpochMilli(pendingTransaction.getAddedAt());
}
@JsonGetter(value = "hash")

@ -16,24 +16,22 @@ package org.hyperledger.besu.ethereum.api.jsonrpc.internal.results;
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.Collection;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonValue;
public class PendingTransactionsResult implements TransactionResult {
private final Set<PendingTransactionResult> pendingTransactionResults;
private final List<PendingTransactionResult> pendingTransactionResults;
public PendingTransactionsResult(final Set<PendingTransaction> pendingTransactionSet) {
public PendingTransactionsResult(final Collection<PendingTransaction> pendingTransactionSet) {
pendingTransactionResults =
pendingTransactionSet.stream()
.map(PendingTransactionResult::new)
.collect(Collectors.toSet());
pendingTransactionSet.stream().map(PendingTransactionResult::new).toList();
}
@JsonValue
public Set<PendingTransactionResult> getResults() {
public List<PendingTransactionResult> getResults() {
return pendingTransactionResults;
}
}

@ -23,10 +23,9 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.internal.exception.InvalidJsonR
import org.hyperledger.besu.ethereum.core.Transaction;
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
/**
* This class allows to filter a list of pending transactions
@ -43,8 +42,8 @@ public class PendingTransactionFilter {
public static final String VALUE_FIELD = "value";
public static final String NONCE_FIELD = "nonce";
public Set<Transaction> reduce(
final Set<PendingTransaction> pendingTransactions,
public Collection<Transaction> reduce(
final Collection<PendingTransaction> pendingTransactions,
final List<Filter> filters,
final int limit)
throws InvalidJsonRpcParameters {
@ -52,7 +51,7 @@ public class PendingTransactionFilter {
.filter(pendingTx -> applyFilters(pendingTx, filters))
.limit(limit)
.map(PendingTransaction::getTransaction)
.collect(Collectors.toSet());
.toList();
}
private boolean applyFilters(

@ -18,11 +18,11 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.Subscrip
import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.SubscriptionManager;
import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.request.SubscriptionType;
import org.hyperledger.besu.ethereum.core.Transaction;
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactionListener;
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactionAddedListener;
import java.util.List;
public class PendingTransactionSubscriptionService implements PendingTransactionListener {
public class PendingTransactionSubscriptionService implements PendingTransactionAddedListener {
private final SubscriptionManager subscriptionManager;

@ -34,8 +34,8 @@ import org.hyperledger.besu.ethereum.core.Transaction;
import org.hyperledger.besu.ethereum.eth.EthProtocol;
import org.hyperledger.besu.ethereum.eth.manager.EthScheduler;
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction;
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactions;
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPool;
import org.hyperledger.besu.ethereum.eth.transactions.sorter.GasPricePendingTransactionsSorter;
import org.hyperledger.besu.ethereum.mainnet.HeaderValidationMode;
import org.hyperledger.besu.ethereum.mainnet.MainnetBlockHeaderFunctions;
import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule;
@ -51,7 +51,6 @@ import org.hyperledger.besu.testutil.BlockTestUtil;
import java.net.URL;
import java.nio.file.Paths;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
@ -139,21 +138,18 @@ public abstract class AbstractEthGraphQLHttpServiceTest {
transactionPoolMock.addTransactionViaApi(
ArgumentMatchers.argThat(tx -> tx.getNonce() == 16)))
.thenReturn(ValidationResult.invalid(TransactionInvalidReason.NONCE_TOO_LOW));
final GasPricePendingTransactionsSorter pendingTransactionsMock =
Mockito.mock(GasPricePendingTransactionsSorter.class);
final PendingTransactions pendingTransactionsMock = Mockito.mock(PendingTransactions.class);
Mockito.when(transactionPoolMock.getPendingTransactions()).thenReturn(pendingTransactionsMock);
Mockito.when(pendingTransactionsMock.getPendingTransactions())
.thenReturn(
Collections.singleton(
new PendingTransaction(
new PendingTransaction.Local(
Transaction.builder()
.type(TransactionType.FRONTIER)
.nonce(42)
.gasLimit(654321)
.gasPrice(Wei.ONE)
.build(),
true,
Instant.ofEpochSecond(Integer.MAX_VALUE))));
.build())));
final WorldStateArchive stateArchive = createInMemoryWorldStateArchive();
GENESIS_CONFIG.writeStateTo(stateArchive.getMutable());

@ -38,8 +38,8 @@ import org.hyperledger.besu.ethereum.core.Synchronizer;
import org.hyperledger.besu.ethereum.core.Transaction;
import org.hyperledger.besu.ethereum.eth.EthProtocol;
import org.hyperledger.besu.ethereum.eth.manager.EthPeers;
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactions;
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPool;
import org.hyperledger.besu.ethereum.eth.transactions.sorter.GasPricePendingTransactionsSorter;
import org.hyperledger.besu.ethereum.mainnet.ValidationResult;
import org.hyperledger.besu.ethereum.p2p.network.P2PNetwork;
import org.hyperledger.besu.ethereum.p2p.rlpx.wire.Capability;
@ -140,8 +140,7 @@ public abstract class AbstractJsonRpcHttpServiceTest {
// nonce too low tests uses a tx with nonce=16
when(transactionPoolMock.addTransactionViaApi(argThat(tx -> tx.getNonce() == 16)))
.thenReturn(ValidationResult.invalid(TransactionInvalidReason.NONCE_TOO_LOW));
final GasPricePendingTransactionsSorter pendingTransactionsMock =
mock(GasPricePendingTransactionsSorter.class);
final PendingTransactions pendingTransactionsMock = mock(PendingTransactions.class);
when(transactionPoolMock.getPendingTransactions()).thenReturn(pendingTransactionsMock);
final PrivacyParameters privacyParameters = mock(PrivacyParameters.class);

@ -15,42 +15,29 @@
package org.hyperledger.besu.ethereum.api.jsonrpc;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.ethereum.api.query.BlockchainQueries;
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactions;
import org.hyperledger.besu.ethereum.eth.transactions.sorter.BaseFeePendingTransactionsSorter;
import org.hyperledger.besu.ethereum.eth.transactions.sorter.GasPricePendingTransactionsSorter;
import java.util.Arrays;
import java.util.Collection;
import java.util.OptionalLong;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
@RunWith(Parameterized.class)
@RunWith(MockitoJUnitRunner.class)
public class LatestNonceProviderTest {
private final Address senderAdress = Address.fromHexString("1");
private final Address senderAddress = Address.fromHexString("1");
private final BlockchainQueries blockchainQueries = mock(BlockchainQueries.class);
@Mock private BlockchainQueries blockchainQueries;
private LatestNonceProvider nonceProvider;
@Parameterized.Parameter public PendingTransactions pendingTransactions;
@Parameterized.Parameters
public static Collection<Object[]> data() {
return Arrays.asList(
new Object[][] {
{mock(GasPricePendingTransactionsSorter.class)},
{mock(BaseFeePendingTransactionsSorter.class)}
});
}
@Mock private PendingTransactions pendingTransactions;
@Before
public void setUp() {
@ -60,19 +47,19 @@ public class LatestNonceProviderTest {
@Test
public void nextNonceUsesTxPool() {
final long highestNonceInPendingTransactions = 123;
when(pendingTransactions.getNextNonceForSender(senderAdress))
when(pendingTransactions.getNextNonceForSender(senderAddress))
.thenReturn(OptionalLong.of(highestNonceInPendingTransactions));
assertThat(nonceProvider.getNonce(senderAdress)).isEqualTo(highestNonceInPendingTransactions);
assertThat(nonceProvider.getNonce(senderAddress)).isEqualTo(highestNonceInPendingTransactions);
}
@Test
public void nextNonceIsTakenFromBlockchainIfNoPendingTransactionResponse() {
final long headBlockNumber = 8;
final long nonceInBLockchain = 56;
when(pendingTransactions.getNextNonceForSender(senderAdress)).thenReturn(OptionalLong.empty());
final long nonceInBlockchain = 56;
when(pendingTransactions.getNextNonceForSender(senderAddress)).thenReturn(OptionalLong.empty());
when(blockchainQueries.headBlockNumber()).thenReturn(headBlockNumber);
when(blockchainQueries.getTransactionCount(senderAdress, headBlockNumber))
.thenReturn(nonceInBLockchain);
assertThat(nonceProvider.getNonce(senderAdress)).isEqualTo(nonceInBLockchain);
when(blockchainQueries.getTransactionCount(senderAddress, headBlockNumber))
.thenReturn(nonceInBlockchain);
assertThat(nonceProvider.getNonce(senderAddress)).isEqualTo(nonceInBlockchain);
}
}

@ -33,10 +33,9 @@ import org.hyperledger.besu.ethereum.api.query.BlockchainQueries;
import org.hyperledger.besu.ethereum.api.query.TransactionWithMetadata;
import org.hyperledger.besu.ethereum.core.BlockDataGenerator;
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction;
import org.hyperledger.besu.ethereum.eth.transactions.sorter.GasPricePendingTransactionsSorter;
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactions;
import org.hyperledger.besu.plugin.data.Transaction;
import java.time.Instant;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
@ -59,7 +58,7 @@ public class EthGetTransactionByHashTest {
private final String JSON_RPC_VERSION = "2.0";
private final String ETH_METHOD = "eth_getTransactionByHash";
@Mock private GasPricePendingTransactionsSorter pendingTransactions;
@Mock private PendingTransactions pendingTransactions;
@Before
public void setUp() {
@ -195,9 +194,7 @@ public class EthGetTransactionByHashTest {
Transaction pendingTransaction = gen.transaction();
System.out.println(pendingTransaction.getHash());
return gen.transactionsWithAllTypes(4).stream()
.map(
transaction ->
new PendingTransaction(transaction, true, Instant.ofEpochSecond(Integer.MAX_VALUE)))
.map(transaction -> new PendingTransaction.Local(transaction))
.collect(Collectors.toUnmodifiableSet());
}
}

@ -28,17 +28,13 @@ import org.hyperledger.besu.ethereum.api.query.BlockchainQueries;
import org.hyperledger.besu.ethereum.chain.Blockchain;
import org.hyperledger.besu.ethereum.chain.ChainHead;
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactions;
import org.hyperledger.besu.ethereum.eth.transactions.sorter.BaseFeePendingTransactionsSorter;
import org.hyperledger.besu.ethereum.eth.transactions.sorter.GasPricePendingTransactionsSorter;
import java.util.Arrays;
import java.util.Collection;
import java.util.OptionalLong;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
class EthGetTransactionCountTest {
public class EthGetTransactionCountTest {
private final Blockchain blockchain = mock(Blockchain.class);
private final BlockchainQueries blockchainQueries = mock(BlockchainQueries.class);
private final ChainHead chainHead = mock(ChainHead.class);
@ -46,19 +42,16 @@ class EthGetTransactionCountTest {
private EthGetTransactionCount ethGetTransactionCount;
private final String pendingTransactionString = "0x00000000000000000000000000000000000000AA";
private final Object[] pendingParams = new Object[] {pendingTransactionString, "pending"};
private PendingTransactions pendingTransactions;
public static Collection<Object[]> data() {
return Arrays.asList(
new Object[][] {
{mock(GasPricePendingTransactionsSorter.class)},
{mock(BaseFeePendingTransactionsSorter.class)}
});
@BeforeEach
public void setup() {
pendingTransactions = mock(PendingTransactions.class);
ethGetTransactionCount = new EthGetTransactionCount(blockchainQueries, pendingTransactions);
}
@ParameterizedTest
@MethodSource("data")
void shouldUsePendingTransactionsWhenToldTo(final PendingTransactions pendingTransactions) {
setup(pendingTransactions);
@Test
public void shouldUsePendingTransactionsWhenToldTo() {
final Address address = Address.fromHexString(pendingTransactionString);
when(pendingTransactions.getNextNonceForSender(address)).thenReturn(OptionalLong.of(12));
@ -71,11 +64,8 @@ class EthGetTransactionCountTest {
assertThat(response.getResult()).isEqualTo("0xc");
}
@ParameterizedTest
@MethodSource("data")
void shouldUseLatestTransactionsWhenNoPendingTransactions(
final PendingTransactions pendingTransactions) {
setup(pendingTransactions);
@Test
public void shouldUseLatestTransactionsWhenNoPendingTransactions() {
final Address address = Address.fromHexString(pendingTransactionString);
when(pendingTransactions.getNextNonceForSender(address)).thenReturn(OptionalLong.empty());
@ -88,10 +78,8 @@ class EthGetTransactionCountTest {
assertThat(response.getResult()).isEqualTo("0x7");
}
@ParameterizedTest
@MethodSource("data")
void shouldUseLatestWhenItIsBiggerThanPending(final PendingTransactions pendingTransactions) {
setup(pendingTransactions);
@Test
public void shouldUseLatestWhenItIsBiggerThanPending() {
final Address address = Address.fromHexString(pendingTransactionString);
mockGetTransactionCount(address, 8);
@ -105,10 +93,8 @@ class EthGetTransactionCountTest {
assertThat(response.getResult()).isEqualTo("0x8");
}
@ParameterizedTest
@MethodSource("data")
void shouldReturnPendingWithHighNonce(final PendingTransactions pendingTransactions) {
setup(pendingTransactions);
@Test
public void shouldReturnPendingWithHighNonce() {
final Address address = Address.fromHexString(pendingTransactionString);
when(pendingTransactions.getNextNonceForSender(address))
@ -122,10 +108,8 @@ class EthGetTransactionCountTest {
assertThat(response.getResult()).isEqualTo("0xfffffffffffffffe");
}
@ParameterizedTest
@MethodSource("data")
void shouldReturnLatestWithHighNonce(final PendingTransactions pendingTransactions) {
setup(pendingTransactions);
@Test
public void shouldReturnLatestWithHighNonce() {
final Address address = Address.fromHexString(pendingTransactionString);
when(pendingTransactions.getNextNonceForSender(address))
@ -139,10 +123,6 @@ class EthGetTransactionCountTest {
assertThat(response.getResult()).isEqualTo("0xfffffffffffffffe");
}
private void setup(final PendingTransactions pendingTransactions) {
ethGetTransactionCount = new EthGetTransactionCount(blockchainQueries, pendingTransactions);
}
private void mockGetTransactionCount(final Address address, final long transactionCount) {
when(blockchainQueries.getBlockchain()).thenReturn(blockchain);
when(blockchain.getChainHead()).thenReturn(chainHead);

@ -26,9 +26,8 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcSucces
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.TransactionPendingResult;
import org.hyperledger.besu.ethereum.core.BlockDataGenerator;
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction;
import org.hyperledger.besu.ethereum.eth.transactions.sorter.GasPricePendingTransactionsSorter;
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactions;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
@ -45,7 +44,7 @@ import org.mockito.junit.MockitoJUnitRunner;
@RunWith(MockitoJUnitRunner.class)
public class TxPoolBesuPendingTransactionsTest {
@Mock private GasPricePendingTransactionsSorter pendingTransactions;
@Mock private PendingTransactions pendingTransactions;
private TxPoolBesuPendingTransactions method;
private final String JSON_RPC_VERSION = "2.0";
private final String TXPOOL_PENDING_TRANSACTIONS_METHOD = "txpool_besuPendingTransactions";
@ -258,9 +257,7 @@ public class TxPoolBesuPendingTransactionsTest {
final BlockDataGenerator gen = new BlockDataGenerator();
return gen.transactionsWithAllTypes(4).stream()
.map(
transaction ->
new PendingTransaction(transaction, true, Instant.ofEpochSecond(Integer.MAX_VALUE)))
.map(transaction -> new PendingTransaction.Local(transaction))
.collect(Collectors.toUnmodifiableSet());
}
}

@ -23,7 +23,7 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequestContext;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcSuccessResponse;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.PendingTransactionsStatisticsResult;
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction;
import org.hyperledger.besu.ethereum.eth.transactions.sorter.GasPricePendingTransactionsSorter;
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactions;
import com.google.common.collect.Sets;
import org.junit.Before;
@ -35,7 +35,7 @@ import org.mockito.junit.MockitoJUnitRunner;
@RunWith(MockitoJUnitRunner.class)
public class TxPoolBesuStatisticsTest {
@Mock private GasPricePendingTransactionsSorter pendingTransactions;
@Mock private PendingTransactions pendingTransactions;
private TxPoolBesuStatistics method;
private final String JSON_RPC_VERSION = "2.0";
private final String TXPOOL_PENDING_TRANSACTIONS_METHOD = "txpool_besuStatistics";

@ -25,7 +25,7 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcSucces
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.PendingTransactionResult;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.PendingTransactionsResult;
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction;
import org.hyperledger.besu.ethereum.eth.transactions.sorter.GasPricePendingTransactionsSorter;
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactions;
import java.time.Instant;
@ -39,7 +39,7 @@ import org.mockito.junit.MockitoJUnitRunner;
@RunWith(MockitoJUnitRunner.class)
public class TxPoolBesuTransactionsTest {
@Mock private GasPricePendingTransactionsSorter pendingTransactions;
@Mock private PendingTransactions pendingTransactions;
private TxPoolBesuTransactions method;
private final String JSON_RPC_VERSION = "2.0";
private final String TXPOOL_PENDING_TRANSACTIONS_METHOD = "txpool_besuTransactions";
@ -58,7 +58,7 @@ public class TxPoolBesuTransactionsTest {
@Test
public void shouldReturnPendingTransactions() {
Instant addedAt = Instant.ofEpochMilli(10_000_000);
long addedAt = 10_000_000;
final JsonRpcRequestContext request =
new JsonRpcRequestContext(
new JsonRpcRequest(
@ -67,7 +67,7 @@ public class TxPoolBesuTransactionsTest {
PendingTransaction pendingTransaction = mock(PendingTransaction.class);
when(pendingTransaction.getHash()).thenReturn(Hash.fromHexString(TRANSACTION_HASH));
when(pendingTransaction.isReceivedFromLocalSource()).thenReturn(true);
when(pendingTransaction.getAddedToPoolAt()).thenReturn(addedAt);
when(pendingTransaction.getAddedAt()).thenReturn(addedAt);
when(pendingTransactions.getPendingTransactions())
.thenReturn(Sets.newHashSet(pendingTransaction));
@ -78,6 +78,6 @@ public class TxPoolBesuTransactionsTest {
assertThat(actualResult.getHash()).isEqualTo(TRANSACTION_HASH);
assertThat(actualResult.isReceivedFromLocalSource()).isTrue();
assertThat(actualResult.getAddedToPoolAt()).isEqualTo(addedAt.toString());
assertThat(actualResult.getAddedToPoolAt()).isEqualTo(Instant.ofEpochMilli(addedAt).toString());
}
}

@ -32,7 +32,6 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.transaction.po
import org.hyperledger.besu.ethereum.core.Transaction;
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
@ -114,7 +113,7 @@ public class PendingTransactionFilterTest {
@Test
public void localAndRemoteAddressShouldNotStartWithForwardSlash() {
final Set<Transaction> filteredList =
final Collection<Transaction> filteredList =
pendingTransactionFilter.reduce(getPendingTransactions(), filters, limit);
assertThat(filteredList.size()).isEqualTo(expectedListOfTransactionHash.size());
@ -139,8 +138,7 @@ public class PendingTransactionFilterTest {
if (i == numberTrx - 1) {
when(transaction.isContractCreation()).thenReturn(true);
}
pendingTransactionList.add(
new PendingTransaction(transaction, true, Instant.ofEpochSecond(Integer.MAX_VALUE)));
pendingTransactionList.add(new PendingTransaction.Local(transaction));
}
return new LinkedHashSet<>(pendingTransactionList);
}

@ -260,10 +260,9 @@ public class BlockTransactionSelector {
in this throwing an CancellationException).
*/
public TransactionSelectionResults buildTransactionListForBlock() {
LOG.debug("Transaction pool size {}", pendingTransactions.size());
LOG.atTrace()
.setMessage("Transaction pool content {}")
.addArgument(() -> pendingTransactions.toTraceLog(false, false))
LOG.atDebug()
.setMessage("Transaction pool stats {}")
.addArgument(pendingTransactions.logStats())
.log();
pendingTransactions.selectTransactions(
pendingTransaction -> evaluateTransaction(pendingTransaction, false));

@ -1033,19 +1033,27 @@ public class Transaction
public String toTraceLog() {
final StringBuilder sb = new StringBuilder();
sb.append(getHash()).append("={");
sb.append(isContractCreation() ? "ContractCreation" : "MessageCall").append(", ");
sb.append(getNonce()).append(", ");
sb.append(getSender()).append(", ");
sb.append(getType()).append(", ");
sb.append(getNonce()).append(", ");
getGasPrice().ifPresent(gasPrice -> sb.append(gasPrice.toBigInteger()).append(", "));
getGasPrice()
.ifPresent(
gasPrice -> sb.append("gp: ").append(gasPrice.toHumanReadableString()).append(", "));
if (getMaxPriorityFeePerGas().isPresent() && getMaxFeePerGas().isPresent()) {
sb.append(getMaxPriorityFeePerGas().map(Wei::toBigInteger).get()).append(", ");
sb.append(getMaxFeePerGas().map(Wei::toBigInteger).get()).append(", ");
getMaxFeePerDataGas().ifPresent(wei -> sb.append(wei.toShortHexString()).append(", "));
sb.append("mf: ")
.append(getMaxFeePerGas().map(Wei::toHumanReadableString).get())
.append(", ");
sb.append("pf: ")
.append(getMaxPriorityFeePerGas().map(Wei::toHumanReadableString).get())
.append(", ");
getMaxFeePerDataGas()
.ifPresent(wei -> sb.append("df: ").append(wei.toHumanReadableString()).append(", "));
}
sb.append(getGasLimit()).append(", ");
sb.append(getValue().toBigInteger()).append(", ");
if (getTo().isPresent()) sb.append(getTo().get()).append(", ");
sb.append("gl: ").append(getGasLimit()).append(", ");
sb.append("v: ").append(getValue().toHumanReadableString()).append(", ");
getTo().ifPresent(to -> sb.append(to));
return sb.append("}").toString();
}
@ -1057,6 +1065,7 @@ public class Transaction
}
public static class Builder {
private static final Optional<List<AccessListEntry>> EMPTY_ACCESS_LIST = Optional.of(List.of());
protected TransactionType transactionType;
@ -1149,7 +1158,10 @@ public class Transaction
}
public Builder accessList(final List<AccessListEntry> accessList) {
this.accessList = Optional.ofNullable(accessList);
this.accessList =
accessList == null
? Optional.empty()
: accessList.isEmpty() ? EMPTY_ACCESS_LIST : Optional.of(accessList);
return this;
}

@ -42,8 +42,14 @@ import org.hyperledger.besu.ethereum.core.SealableBlockHeader;
import org.hyperledger.besu.ethereum.core.Transaction;
import org.hyperledger.besu.ethereum.core.TransactionTestFixture;
import org.hyperledger.besu.ethereum.eth.transactions.ImmutableTransactionPoolConfiguration;
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction;
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactions;
import org.hyperledger.besu.ethereum.eth.transactions.sorter.GasPricePendingTransactionsSorter;
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.eth.transactions.layered.EndLayer;
import org.hyperledger.besu.ethereum.eth.transactions.layered.GasPricePrioritizedTransactions;
import org.hyperledger.besu.ethereum.eth.transactions.layered.LayeredPendingTransactions;
import org.hyperledger.besu.ethereum.mainnet.MainnetProtocolSchedule;
import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule;
import org.hyperledger.besu.ethereum.storage.StorageProvider;
@ -59,11 +65,11 @@ import org.hyperledger.besu.plugin.services.storage.rocksdb.configuration.RocksD
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.time.Clock;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@ -87,12 +93,30 @@ public abstract class AbstractIsolationTests {
protected final GenesisState genesisState =
GenesisState.fromConfig(GenesisConfigFile.development(), protocolSchedule);
protected final MutableBlockchain blockchain = createInMemoryBlockchain(genesisState.getBlock());
protected final TransactionPoolConfiguration poolConfiguration =
ImmutableTransactionPoolConfiguration.builder().txPoolMaxSize(100).build();
protected final TransactionPoolReplacementHandler transactionReplacementHandler =
new TransactionPoolReplacementHandler(poolConfiguration.getPriceBump());
protected final BiFunction<PendingTransaction, PendingTransaction, Boolean>
transactionReplacementTester =
(t1, t2) ->
transactionReplacementHandler.shouldReplace(
t1, t2, protocolContext.getBlockchain().getChainHeadHeader());
protected TransactionPoolMetrics txPoolMetrics =
new TransactionPoolMetrics(new NoOpMetricsSystem());
protected final PendingTransactions sorter =
new GasPricePendingTransactionsSorter(
ImmutableTransactionPoolConfiguration.builder().txPoolMaxSize(100).build(),
Clock.systemUTC(),
new NoOpMetricsSystem(),
blockchain::getChainHeadHeader);
new LayeredPendingTransactions(
poolConfiguration,
new GasPricePrioritizedTransactions(
poolConfiguration,
new EndLayer(txPoolMetrics),
txPoolMetrics,
transactionReplacementTester));
protected final List<GenesisAllocation> accounts =
GenesisConfigFile.development()

@ -80,6 +80,7 @@ dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter'
testImplementation 'org.mockito:mockito-core'
testImplementation 'org.mockito:mockito-junit-jupiter'
testImplementation 'org.openjdk.jol:jol-core'
testRuntimeOnly 'org.junit.vintage:junit-vintage-engine'

@ -14,7 +14,7 @@
*/
package org.hyperledger.besu.ethereum.eth.manager.task;
import static org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolConfiguration.MAX_PENDING_TRANSACTIONS;
import static org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolConfiguration.DEFAULT_MAX_PENDING_TRANSACTIONS;
import org.hyperledger.besu.datatypes.Hash;
import org.hyperledger.besu.ethereum.core.Transaction;
@ -22,9 +22,7 @@ import org.hyperledger.besu.ethereum.eth.manager.EthContext;
import org.hyperledger.besu.ethereum.eth.manager.EthPeer;
import org.hyperledger.besu.ethereum.eth.transactions.PeerTransactionTracker;
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPool;
import org.hyperledger.besu.metrics.BesuMetricCategory;
import org.hyperledger.besu.plugin.services.MetricsSystem;
import org.hyperledger.besu.plugin.services.metrics.Counter;
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolMetrics;
import java.util.ArrayList;
import java.util.Collection;
@ -42,16 +40,15 @@ public class BufferedGetPooledTransactionsFromPeerFetcher {
private static final Logger LOG =
LoggerFactory.getLogger(BufferedGetPooledTransactionsFromPeerFetcher.class);
private static final int MAX_HASHES = 256;
private static final String HASHES = "hashes";
private final TransactionPool transactionPool;
private final PeerTransactionTracker transactionTracker;
private final EthContext ethContext;
private final MetricsSystem metricsSystem;
private final TransactionPoolMetrics metrics;
private final String metricLabel;
private final ScheduledFuture<?> scheduledFuture;
private final EthPeer peer;
private final Queue<Hash> txAnnounces;
private final Counter alreadySeenTransactionsCounter;
public BufferedGetPooledTransactionsFromPeerFetcher(
final EthContext ethContext,
@ -59,23 +56,17 @@ public class BufferedGetPooledTransactionsFromPeerFetcher {
final EthPeer peer,
final TransactionPool transactionPool,
final PeerTransactionTracker transactionTracker,
final MetricsSystem metricsSystem) {
final TransactionPoolMetrics metrics,
final String metricLabel) {
this.ethContext = ethContext;
this.scheduledFuture = scheduledFuture;
this.peer = peer;
this.transactionPool = transactionPool;
this.transactionTracker = transactionTracker;
this.metricsSystem = metricsSystem;
this.txAnnounces = Queues.synchronizedQueue(EvictingQueue.create(MAX_PENDING_TRANSACTIONS));
this.alreadySeenTransactionsCounter =
metricsSystem
.createLabelledCounter(
BesuMetricCategory.TRANSACTION_POOL,
"remote_already_seen_total",
"Total number of received transactions already seen",
"source")
.labels(HASHES);
this.metrics = metrics;
this.metricLabel = metricLabel;
this.txAnnounces =
Queues.synchronizedQueue(EvictingQueue.create(DEFAULT_MAX_PENDING_TRANSACTIONS));
}
public ScheduledFuture<?> getScheduledFuture() {
@ -86,7 +77,8 @@ public class BufferedGetPooledTransactionsFromPeerFetcher {
List<Hash> txHashesAnnounced;
while (!(txHashesAnnounced = getTxHashesAnnounced()).isEmpty()) {
final GetPooledTransactionsFromPeerTask task =
GetPooledTransactionsFromPeerTask.forHashes(ethContext, txHashesAnnounced, metricsSystem);
GetPooledTransactionsFromPeerTask.forHashes(
ethContext, txHashesAnnounced, metrics.getMetricsSystem());
task.assignPeer(peer);
ethContext
.getScheduler()
@ -125,7 +117,7 @@ public class BufferedGetPooledTransactionsFromPeerFetcher {
}
final int alreadySeenCount = discarded;
alreadySeenTransactionsCounter.inc(alreadySeenCount);
metrics.incrementAlreadySeenTransactions(metricLabel, alreadySeenCount);
LOG.atTrace()
.setMessage(
"Transaction hashes to request from peer {}, fresh count {}, already seen count {}")

@ -23,10 +23,6 @@ import org.hyperledger.besu.ethereum.eth.manager.task.BufferedGetPooledTransacti
import org.hyperledger.besu.ethereum.eth.messages.NewPooledTransactionHashesMessage;
import org.hyperledger.besu.ethereum.p2p.rlpx.wire.messages.DisconnectMessage.DisconnectReason;
import org.hyperledger.besu.ethereum.rlp.RLPException;
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 java.time.Duration;
import java.time.Instant;
@ -40,43 +36,32 @@ import org.slf4j.LoggerFactory;
public class NewPooledTransactionHashesMessageProcessor {
private static final int SKIPPED_MESSAGES_LOGGING_THRESHOLD = 1000;
private static final Logger LOG =
LoggerFactory.getLogger(NewPooledTransactionHashesMessageProcessor.class);
static final String METRIC_LABEL = "new_pooled_transaction_hashes";
private final ConcurrentHashMap<EthPeer, BufferedGetPooledTransactionsFromPeerFetcher>
scheduledTasks;
private final PeerTransactionTracker transactionTracker;
private final Counter totalSkippedNewPooledTransactionHashesMessageCounter;
private final TransactionPool transactionPool;
private final TransactionPoolConfiguration transactionPoolConfiguration;
private final EthContext ethContext;
private final MetricsSystem metricsSystem;
private final TransactionPoolMetrics metrics;
public NewPooledTransactionHashesMessageProcessor(
final PeerTransactionTracker transactionTracker,
final TransactionPool transactionPool,
final TransactionPoolConfiguration transactionPoolConfiguration,
final EthContext ethContext,
final MetricsSystem metricsSystem) {
final TransactionPoolMetrics metrics) {
this.transactionTracker = transactionTracker;
this.transactionPool = transactionPool;
this.transactionPoolConfiguration = transactionPoolConfiguration;
this.ethContext = ethContext;
this.metricsSystem = metricsSystem;
this.totalSkippedNewPooledTransactionHashesMessageCounter =
new RunnableCounter(
metricsSystem.createCounter(
BesuMetricCategory.TRANSACTION_POOL,
"new_pooled_transaction_hashes_messages_skipped_total",
"Total number of new pooled transaction hashes messages skipped by the processor."),
() ->
LOG.warn(
"{} expired new pooled transaction hashes messages have been skipped.",
SKIPPED_MESSAGES_LOGGING_THRESHOLD),
SKIPPED_MESSAGES_LOGGING_THRESHOLD);
this.metrics = metrics;
metrics.initExpiredMessagesCounter(METRIC_LABEL);
this.scheduledTasks = new ConcurrentHashMap<>();
}
@ -89,7 +74,7 @@ public class NewPooledTransactionHashesMessageProcessor {
if (startedAt.plus(keepAlive).isAfter(now())) {
this.processNewPooledTransactionHashesMessage(peer, transactionsMessage);
} else {
totalSkippedNewPooledTransactionHashesMessageCounter.inc();
metrics.incrementExpiredMessages(METRIC_LABEL);
}
}
@ -125,7 +110,8 @@ public class NewPooledTransactionHashesMessageProcessor {
peer,
transactionPool,
transactionTracker,
metricsSystem);
metrics,
METRIC_LABEL);
});
bufferedTask.addHashes(

@ -88,7 +88,7 @@ public class PeerTransactionTracker implements EthPeer.DisconnectCallback {
private <T> Set<T> createTransactionsSet() {
return Collections.newSetFromMap(
new LinkedHashMap<T, Boolean>(1 << 4, 0.75f, true) {
new LinkedHashMap<>(1 << 4, 0.75f, true) {
@Override
protected boolean removeEldestEntry(final Map.Entry<T, Boolean> eldest) {
return size() > MAX_TRACKED_SEEN_TRANSACTIONS;

@ -18,32 +18,37 @@ import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.datatypes.Hash;
import org.hyperledger.besu.datatypes.Wei;
import org.hyperledger.besu.ethereum.core.Transaction;
import org.hyperledger.besu.evm.AccessListEntry;
import java.time.Instant;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors;
/**
* Tracks the additional metadata associated with transactions to enable prioritization for mining
* and deciding which transactions to drop when the transaction pool reaches its size limit.
*/
public class PendingTransaction {
public abstract class PendingTransaction {
static final int NOT_INITIALIZED = -1;
static final int FRONTIER_BASE_MEMORY_SIZE = 944;
static final int ACCESS_LIST_BASE_MEMORY_SIZE = 944;
static final int EIP1559_BASE_MEMORY_SIZE = 1056;
static final int OPTIONAL_TO_MEMORY_SIZE = 92;
static final int PAYLOAD_BASE_MEMORY_SIZE = 32;
static final int ACCESS_LIST_STORAGE_KEY_MEMORY_SIZE = 32;
static final int ACCESS_LIST_ENTRY_BASE_MEMORY_SIZE = 128;
static final int OPTIONAL_ACCESS_LIST_MEMORY_SIZE = 24;
static final int PENDING_TRANSACTION_MEMORY_SIZE = 40;
private static final AtomicLong TRANSACTIONS_ADDED = new AtomicLong();
private final Transaction transaction;
private final boolean receivedFromLocalSource;
private final Instant addedToPoolAt;
private final long addedAt;
private final long sequence; // Allows prioritization based on order transactions are added
public PendingTransaction(
final Transaction transaction,
final boolean receivedFromLocalSource,
final Instant addedToPoolAt) {
private int memorySize = NOT_INITIALIZED;
protected PendingTransaction(final Transaction transaction, final long addedAt) {
this.transaction = transaction;
this.receivedFromLocalSource = receivedFromLocalSource;
this.addedToPoolAt = addedToPoolAt;
this.addedAt = addedAt;
this.sequence = TRANSACTIONS_ADDED.getAndIncrement();
}
@ -67,23 +72,85 @@ public class PendingTransaction {
return transaction.getSender();
}
public boolean isReceivedFromLocalSource() {
return receivedFromLocalSource;
}
public abstract boolean isReceivedFromLocalSource();
public Hash getHash() {
return transaction.getHash();
}
public Instant getAddedToPoolAt() {
return addedToPoolAt;
public long getAddedAt() {
return addedAt;
}
public int memorySize() {
if (memorySize == NOT_INITIALIZED) {
memorySize = computeMemorySize();
}
return memorySize;
}
private int computeMemorySize() {
return switch (transaction.getType()) {
case FRONTIER -> computeFrontierMemorySize();
case ACCESS_LIST -> computeAccessListMemorySize();
case EIP1559 -> computeEIP1559MemorySize();
case BLOB -> computeBlobMemorySize();
}
+ PENDING_TRANSACTION_MEMORY_SIZE;
}
private int computeFrontierMemorySize() {
return FRONTIER_BASE_MEMORY_SIZE + computePayloadMemorySize() + computeToMemorySize();
}
private int computeAccessListMemorySize() {
return ACCESS_LIST_BASE_MEMORY_SIZE
+ computePayloadMemorySize()
+ computeToMemorySize()
+ computeAccessListEntriesMemorySize();
}
private int computeEIP1559MemorySize() {
return EIP1559_BASE_MEMORY_SIZE
+ computePayloadMemorySize()
+ computeToMemorySize()
+ computeAccessListEntriesMemorySize();
}
private int computeBlobMemorySize() {
// ToDo 4844: adapt for blobs
return computeEIP1559MemorySize();
}
private int computePayloadMemorySize() {
return PAYLOAD_BASE_MEMORY_SIZE + transaction.getPayload().size();
}
private int computeToMemorySize() {
if (transaction.getTo().isPresent()) {
return OPTIONAL_TO_MEMORY_SIZE;
}
return 0;
}
private int computeAccessListEntriesMemorySize() {
return transaction
.getAccessList()
.map(
al -> {
int totalSize = OPTIONAL_ACCESS_LIST_MEMORY_SIZE;
totalSize += al.size() * ACCESS_LIST_ENTRY_BASE_MEMORY_SIZE;
totalSize +=
al.stream().map(AccessListEntry::getStorageKeys).mapToInt(List::size).sum()
* ACCESS_LIST_STORAGE_KEY_MEMORY_SIZE;
return totalSize;
})
.orElse(0);
}
public static List<Transaction> toTransactionList(
final Collection<PendingTransaction> transactionsInfo) {
return transactionsInfo.stream()
.map(PendingTransaction::getTransaction)
.collect(Collectors.toUnmodifiableList());
return transactionsInfo.stream().map(PendingTransaction::getTransaction).toList();
}
@Override
@ -105,13 +172,60 @@ public class PendingTransaction {
return 31 * (int) (sequence ^ (sequence >>> 32));
}
@Override
public String toString() {
return "Hash="
+ transaction.getHash().toShortHexString()
+ ", nonce="
+ transaction.getNonce()
+ ", sender="
+ transaction.getSender().toShortHexString()
+ ", addedAt="
+ addedAt
+ ", sequence="
+ sequence
+ '}';
}
public String toTraceLog() {
return "{sequence: "
+ sequence
+ ", addedAt: "
+ addedToPoolAt
+ addedAt
+ ", "
+ transaction.toTraceLog()
+ "}";
}
public static class Local extends PendingTransaction {
public Local(final Transaction transaction, final long addedAt) {
super(transaction, addedAt);
}
public Local(final Transaction transaction) {
this(transaction, System.currentTimeMillis());
}
@Override
public boolean isReceivedFromLocalSource() {
return true;
}
}
public static class Remote extends PendingTransaction {
public Remote(final Transaction transaction, final long addedAt) {
super(transaction, addedAt);
}
public Remote(final Transaction transaction) {
this(transaction, System.currentTimeMillis());
}
@Override
public boolean isReceivedFromLocalSource() {
return false;
}
}
}

@ -17,7 +17,7 @@ package org.hyperledger.besu.ethereum.eth.transactions;
import org.hyperledger.besu.ethereum.core.Transaction;
@FunctionalInterface
public interface PendingTransactionListener {
public interface PendingTransactionAddedListener {
void onTransactionAdded(Transaction transaction);
}

@ -16,14 +16,15 @@ package org.hyperledger.besu.ethereum.eth.transactions;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.datatypes.Hash;
import org.hyperledger.besu.ethereum.core.Block;
import org.hyperledger.besu.ethereum.core.BlockHeader;
import org.hyperledger.besu.ethereum.core.Transaction;
import org.hyperledger.besu.ethereum.mainnet.feemarket.FeeMarket;
import org.hyperledger.besu.evm.account.Account;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.OptionalLong;
import java.util.Set;
public interface PendingTransactions {
@ -33,45 +34,56 @@ public interface PendingTransactions {
List<Transaction> getLocalTransactions();
TransactionAddedStatus addRemoteTransaction(
final Transaction transaction, final Optional<Account> maybeSenderAccount);
TransactionAddedResult addRemoteTransaction(
Transaction transaction, Optional<Account> maybeSenderAccount);
TransactionAddedStatus addLocalTransaction(
final Transaction transaction, final Optional<Account> maybeSenderAccount);
TransactionAddedResult addLocalTransaction(
Transaction transaction, Optional<Account> maybeSenderAccount);
void removeTransaction(final Transaction transaction);
void transactionAddedToBlock(final Transaction transaction);
void selectTransactions(final TransactionSelector selector);
void selectTransactions(TransactionSelector selector);
long maxSize();
int size();
boolean containsTransaction(final Hash transactionHash);
boolean containsTransaction(Transaction transaction);
Optional<Transaction> getTransactionByHash(Hash transactionHash);
Optional<Transaction> getTransactionByHash(final Hash transactionHash);
Collection<PendingTransaction> getPendingTransactions();
Set<PendingTransaction> getPendingTransactions();
long subscribePendingTransactions(PendingTransactionAddedListener listener);
long subscribePendingTransactions(final PendingTransactionListener listener);
void unsubscribePendingTransactions(long id);
void unsubscribePendingTransactions(final long id);
long subscribeDroppedTransactions(PendingTransactionDroppedListener listener);
long subscribeDroppedTransactions(final PendingTransactionDroppedListener listener);
void unsubscribeDroppedTransactions(long id);
void unsubscribeDroppedTransactions(final long id);
OptionalLong getNextNonceForSender(Address sender);
OptionalLong getNextNonceForSender(final Address sender);
void manageBlockAdded(
BlockHeader blockHeader,
List<Transaction> confirmedTransactions,
final List<Transaction> reorgTransactions,
FeeMarket feeMarket);
void manageBlockAdded(final Block block);
String toTraceLog();
String toTraceLog(final boolean withTransactionsBySender, final boolean withLowestInvalidNonce);
String logStats();
List<Transaction> signalInvalidAndGetDependentTransactions(final Transaction transaction);
default List<Transaction> signalInvalidAndGetDependentTransactions(
final Transaction transaction) {
// ToDo: remove when the legacy tx pool is removed
return List.of();
}
default void signalInvalidAndRemoveDependentTransactions(final Transaction transaction) {
// ToDo: remove when the legacy tx pool is removed
// no-op
}
boolean isLocalSender(final Address sender);
boolean isLocalSender(Address sender);
enum TransactionSelectionResult {
DELETE_TRANSACTION_AND_CONTINUE,
@ -81,6 +93,6 @@ public interface PendingTransactions {
@FunctionalInterface
interface TransactionSelector {
TransactionSelectionResult evaluateTransaction(final Transaction transaction);
TransactionSelectionResult evaluateTransaction(Transaction transaction);
}
}

@ -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;
}
}

@ -30,7 +30,6 @@ import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.slf4j.Logger;
@ -64,13 +63,13 @@ public class TransactionBroadcaster implements TransactionBatchAddedListener {
}
public void relayTransactionPoolTo(final EthPeer peer) {
Set<PendingTransaction> pendingPendingTransaction =
final Collection<PendingTransaction> allPendingTransactions =
pendingTransactions.getPendingTransactions();
if (!pendingPendingTransaction.isEmpty()) {
if (!allPendingTransactions.isEmpty()) {
if (peer.hasSupportForMessage(EthPV65.NEW_POOLED_TRANSACTION_HASHES)) {
sendTransactionHashes(toTransactionList(pendingPendingTransaction), List.of(peer));
sendTransactionHashes(toTransactionList(allPendingTransactions), List.of(peer));
} else {
sendFullTransactions(toTransactionList(pendingPendingTransaction), List.of(peer));
sendFullTransactions(toTransactionList(allPendingTransactions), List.of(peer));
}
}
}

@ -14,9 +14,6 @@
*/
package org.hyperledger.besu.ethereum.eth.transactions;
import static java.util.Collections.singletonList;
import static org.hyperledger.besu.ethereum.eth.transactions.TransactionAddedStatus.ADDED;
import static org.hyperledger.besu.ethereum.eth.transactions.TransactionAddedStatus.ALREADY_KNOWN;
import static org.hyperledger.besu.ethereum.transaction.TransactionInvalidReason.CHAIN_HEAD_NOT_AVAILABLE;
import static org.hyperledger.besu.ethereum.transaction.TransactionInvalidReason.CHAIN_HEAD_WORLD_STATE_NOT_AVAILABLE;
import static org.hyperledger.besu.ethereum.transaction.TransactionInvalidReason.INTERNAL_ERROR;
@ -43,11 +40,7 @@ import org.hyperledger.besu.ethereum.transaction.TransactionInvalidReason;
import org.hyperledger.besu.ethereum.trie.MerkleTrieException;
import org.hyperledger.besu.evm.account.Account;
import org.hyperledger.besu.evm.fluent.SimpleAccount;
import org.hyperledger.besu.metrics.BesuMetricCategory;
import org.hyperledger.besu.plugin.data.TransactionType;
import org.hyperledger.besu.plugin.services.MetricsSystem;
import org.hyperledger.besu.plugin.services.metrics.Counter;
import org.hyperledger.besu.plugin.services.metrics.LabelledMetric;
import java.io.BufferedReader;
import java.io.BufferedWriter;
@ -55,15 +48,18 @@ import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.IntSummaryStatistics;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.tuweni.bytes.Bytes;
import org.slf4j.Logger;
@ -77,17 +73,14 @@ import org.slf4j.LoggerFactory;
* <p>This class is safe for use across multiple threads.
*/
public class TransactionPool implements BlockAddedObserver {
private static final Logger LOG = LoggerFactory.getLogger(TransactionPool.class);
private static final String REMOTE = "remote";
private static final String LOCAL = "local";
private static final Logger LOG_FOR_REPLAY = LoggerFactory.getLogger("LOG_FOR_REPLAY");
private final PendingTransactions pendingTransactions;
private final ProtocolSchedule protocolSchedule;
private final ProtocolContext protocolContext;
private final TransactionBroadcaster transactionBroadcaster;
private final MiningParameters miningParameters;
private final LabelledMetric<Counter> duplicateTransactionCounter;
private final TransactionPoolMetrics metrics;
private final TransactionPoolConfiguration configuration;
private final AtomicBoolean isPoolEnabled = new AtomicBoolean(true);
@ -98,27 +91,36 @@ public class TransactionPool implements BlockAddedObserver {
final TransactionBroadcaster transactionBroadcaster,
final EthContext ethContext,
final MiningParameters miningParameters,
final MetricsSystem metricsSystem,
final TransactionPoolMetrics metrics,
final TransactionPoolConfiguration configuration) {
this.pendingTransactions = pendingTransactions;
this.protocolSchedule = protocolSchedule;
this.protocolContext = protocolContext;
this.transactionBroadcaster = transactionBroadcaster;
this.miningParameters = miningParameters;
this.metrics = metrics;
this.configuration = configuration;
duplicateTransactionCounter =
metricsSystem.createLabelledCounter(
BesuMetricCategory.TRANSACTION_POOL,
"transactions_duplicates_total",
"Total number of duplicate transactions received",
"source");
ethContext.getEthPeers().subscribeConnect(this::handleConnect);
initLogForReplay();
CompletableFuture.runAsync(this::loadFromDisk);
}
private void initLogForReplay() {
LOG_FOR_REPLAY
.atTrace()
.setMessage("{},{},{},{}")
.addArgument(() -> getChainHeadBlockHeader().map(BlockHeader::getNumber).orElse(0L))
.addArgument(
() ->
getChainHeadBlockHeader()
.flatMap(BlockHeader::getBaseFee)
.map(Wei::getAsBigInteger)
.orElse(BigInteger.ZERO))
.addArgument(() -> getChainHeadBlockHeader().map(BlockHeader::getGasUsed).orElse(0L))
.addArgument(() -> getChainHeadBlockHeader().map(BlockHeader::getGasLimit).orElse(0L))
.log();
}
public void saveToDisk() {
if (configuration.getEnableSaveRestore()) {
final File saveFile = configuration.getSaveFile();
@ -217,25 +219,24 @@ public class TransactionPool implements BlockAddedObserver {
if (validationResult.result.isValid()) {
final TransactionAddedStatus transactionAddedStatus =
final TransactionAddedResult transactionAddedResult =
pendingTransactions.addLocalTransaction(transaction, validationResult.maybeAccount);
if (!transactionAddedStatus.equals(ADDED)) {
if (transactionAddedStatus.equals(ALREADY_KNOWN)) {
duplicateTransactionCounter.labels(LOCAL).inc();
}
return ValidationResult.invalid(
transactionAddedStatus
.getInvalidReason()
if (transactionAddedResult.isRejected()) {
final var rejectReason =
transactionAddedResult
.maybeInvalidReason()
.orElseGet(
() -> {
LOG.warn("Missing invalid reason for status {}", transactionAddedStatus);
LOG.warn("Missing invalid reason for status {}", transactionAddedResult);
return INTERNAL_ERROR;
}));
});
return ValidationResult.invalid(rejectReason);
}
final Collection<Transaction> txs = singletonList(transaction);
transactionBroadcaster.onTransactionsAdded(txs);
transactionBroadcaster.onTransactionsAdded(List.of(transaction));
} else {
metrics.incrementRejected(true, validationResult.result.getInvalidReason(), "txpool");
}
return validationResult.result;
@ -251,80 +252,99 @@ public class TransactionPool implements BlockAddedObserver {
.orElse(true);
}
public void addRemoteTransactions(final Collection<Transaction> transactions) {
final List<Transaction> addedTransactions = new ArrayList<>(transactions.size());
LOG.trace("Adding {} remote transactions", transactions.size());
for (final Transaction transaction : transactions) {
private Stream<Transaction> sortedBySenderAndNonce(final Collection<Transaction> transactions) {
return transactions.stream()
.sorted(Comparator.comparing(Transaction::getSender).thenComparing(Transaction::getNonce));
}
final var result = addRemoteTransaction(transaction);
if (result.isValid()) {
addedTransactions.add(transaction);
}
}
public void addRemoteTransactions(final Collection<Transaction> transactions) {
final long started = System.currentTimeMillis();
final int initialCount = transactions.size();
final List<Transaction> addedTransactions = new ArrayList<>(initialCount);
LOG.debug("Adding {} remote transactions", initialCount);
sortedBySenderAndNonce(transactions)
.forEach(
transaction -> {
final var result = addRemoteTransaction(transaction);
if (result.isValid()) {
addedTransactions.add(transaction);
}
});
LOG_FOR_REPLAY
.atTrace()
.setMessage("S,{}")
.addArgument(() -> pendingTransactions.logStats())
.log();
LOG.atDebug()
.setMessage(
"Added {} transactions to the pool in {}ms, {} not added, current pool stats {}")
.addArgument(addedTransactions::size)
.addArgument(() -> System.currentTimeMillis() - started)
.addArgument(() -> initialCount - addedTransactions.size())
.addArgument(pendingTransactions::logStats)
.log();
if (!addedTransactions.isEmpty()) {
transactionBroadcaster.onTransactionsAdded(addedTransactions);
LOG.atTrace()
.setMessage("Added {} transactions to the pool, current pool size {}, content {}")
.addArgument(addedTransactions::size)
.addArgument(pendingTransactions::size)
.addArgument(() -> pendingTransactions.toTraceLog(true, true))
.log();
}
}
private ValidationResult<TransactionInvalidReason> addRemoteTransaction(
final Transaction transaction) {
if (pendingTransactions.containsTransaction(transaction.getHash())) {
if (pendingTransactions.containsTransaction(transaction)) {
LOG.atTrace()
.setMessage("Discard already present transaction {}")
.addArgument(transaction::toTraceLog)
.log();
// We already have this transaction, don't even validate it.
duplicateTransactionCounter.labels(REMOTE).inc();
metrics.incrementRejected(false, TRANSACTION_ALREADY_KNOWN, "txpool");
return ValidationResult.invalid(TRANSACTION_ALREADY_KNOWN);
}
final ValidationResultAndAccount validationResult = validateRemoteTransaction(transaction);
if (validationResult.result.isValid()) {
final var status =
final TransactionAddedResult status =
pendingTransactions.addRemoteTransaction(transaction, validationResult.maybeAccount);
switch (status) {
case ADDED:
LOG.atTrace()
.setMessage("Added remote transaction {}")
.addArgument(transaction::toTraceLog)
.log();
break;
case ALREADY_KNOWN:
LOG.atTrace()
.setMessage("Duplicate remote transaction {}")
.addArgument(transaction::toTraceLog)
.log();
duplicateTransactionCounter.labels(REMOTE).inc();
return ValidationResult.invalid(TRANSACTION_ALREADY_KNOWN);
default:
LOG.atTrace().setMessage("Transaction added status {}").addArgument(status::name).log();
return ValidationResult.invalid(status.getInvalidReason().get());
if (status.isSuccess()) {
LOG.atTrace()
.setMessage("Added remote transaction {}")
.addArgument(transaction::toTraceLog)
.log();
} else {
final var rejectReason =
status
.maybeInvalidReason()
.orElseGet(
() -> {
LOG.warn("Missing invalid reason for status {}", status);
return INTERNAL_ERROR;
});
LOG.atTrace()
.setMessage("Transaction {} rejected reason {}")
.addArgument(transaction::toTraceLog)
.addArgument(rejectReason)
.log();
metrics.incrementRejected(false, rejectReason, "txpool");
return ValidationResult.invalid(rejectReason);
}
} else {
LOG.atTrace()
.setMessage("Discard invalid transaction {}, reason {}")
.addArgument(transaction::toTraceLog)
.addArgument(validationResult.result::getInvalidReason)
.log();
pendingTransactions
.signalInvalidAndGetDependentTransactions(transaction)
.forEach(pendingTransactions::removeTransaction);
metrics.incrementRejected(false, validationResult.result.getInvalidReason(), "txpool");
pendingTransactions.signalInvalidAndRemoveDependentTransactions(transaction);
}
return validationResult.result;
}
public long subscribePendingTransactions(final PendingTransactionListener listener) {
public long subscribePendingTransactions(final PendingTransactionAddedListener listener) {
return pendingTransactions.subscribePendingTransactions(listener);
}
@ -343,10 +363,16 @@ public class TransactionPool implements BlockAddedObserver {
@Override
public void onBlockAdded(final BlockAddedEvent event) {
LOG.trace("Block added event {}", event);
if (isPoolEnabled.get()) {
event.getAddedTransactions().forEach(pendingTransactions::transactionAddedToBlock);
pendingTransactions.manageBlockAdded(event.getBlock());
reAddTransactions(event.getRemovedTransactions());
if (event.getEventType().equals(BlockAddedEvent.EventType.HEAD_ADVANCED)
|| event.getEventType().equals(BlockAddedEvent.EventType.CHAIN_REORG)) {
if (isPoolEnabled.get()) {
pendingTransactions.manageBlockAdded(
event.getBlock().getHeader(),
event.getAddedTransactions(),
event.getRemovedTransactions(),
protocolSchedule.getByBlockHeader(event.getBlock().getHeader()).getFeeMarket());
reAddTransactions(event.getRemovedTransactions());
}
}
}
@ -363,16 +389,28 @@ public class TransactionPool implements BlockAddedObserver {
var reAddLocalTxs = txsByOrigin.get(true);
var reAddRemoteTxs = txsByOrigin.get(false);
if (!reAddLocalTxs.isEmpty()) {
LOG.trace("Re-adding {} local transactions from a block event", reAddLocalTxs.size());
reAddLocalTxs.forEach(this::addLocalTransaction);
logReAddedTransactions(reAddLocalTxs, "local");
sortedBySenderAndNonce(reAddLocalTxs).forEach(this::addLocalTransaction);
}
if (!reAddRemoteTxs.isEmpty()) {
LOG.trace("Re-adding {} remote transactions from a block event", reAddRemoteTxs.size());
logReAddedTransactions(reAddRemoteTxs, "remote");
addRemoteTransactions(reAddRemoteTxs);
}
}
}
private static void logReAddedTransactions(
final List<Transaction> reAddedTxs, final String source) {
LOG.atTrace()
.setMessage("Re-adding {} {} transactions from a block event: {}")
.addArgument(reAddedTxs::size)
.addArgument(source)
.addArgument(
() ->
reAddedTxs.stream().map(Transaction::toTraceLog).collect(Collectors.joining("; ")))
.log();
}
private MainnetTransactionValidator getTransactionValidator() {
return protocolSchedule
.getByBlockHeader(protocolContext.getBlockchain().getChainHeadHeader())

@ -27,8 +27,8 @@ import org.immutables.value.Value;
public interface TransactionPoolConfiguration {
String DEFAULT_SAVE_FILE_NAME = "txpool.dump";
int DEFAULT_TX_MSG_KEEP_ALIVE = 60;
int MAX_PENDING_TRANSACTIONS = 4096;
float LIMIT_TXPOOL_BY_ACCOUNT_PERCENTAGE = 0.001f; // 0.1%
int DEFAULT_MAX_PENDING_TRANSACTIONS = 4096;
float DEFAULT_LIMIT_TX_POOL_BY_ACCOUNT_PERCENTAGE = 0.001f; // 0.1%
int DEFAULT_TX_RETENTION_HOURS = 13;
boolean DEFAULT_STRICT_TX_REPLAY_PROTECTION_ENABLED = false;
Percentage DEFAULT_PRICE_BUMP = Percentage.fromInt(10);
@ -38,17 +38,21 @@ public interface TransactionPoolConfiguration {
boolean DEFAULT_ENABLE_SAVE_RESTORE = false;
File DEFAULT_SAVE_FILE = new File(DEFAULT_SAVE_FILE_NAME);
long DEFAULT_PENDING_TRANSACTIONS_LAYER_MAX_CAPACITY_BYTES = 50_000_000L;
int DEFAULT_MAX_PRIORITIZED_TRANSACTIONS = 2000;
int DEFAULT_MAX_FUTURE_BY_SENDER = 200;
boolean DEFAULT_LAYERED_TX_POOL_ENABLED = false;
TransactionPoolConfiguration DEFAULT = ImmutableTransactionPoolConfiguration.builder().build();
@Value.Default
default int getTxPoolMaxSize() {
return MAX_PENDING_TRANSACTIONS;
return DEFAULT_MAX_PENDING_TRANSACTIONS;
}
@Value.Default
default float getTxPoolLimitByAccountPercentage() {
return LIMIT_TXPOOL_BY_ACCOUNT_PERCENTAGE;
return DEFAULT_LIMIT_TX_POOL_BY_ACCOUNT_PERCENTAGE;
}
@Value.Derived
@ -100,4 +104,24 @@ public interface TransactionPoolConfiguration {
default File getSaveFile() {
return DEFAULT_SAVE_FILE;
}
@Value.Default
default Boolean getLayeredTxPoolEnabled() {
return DEFAULT_LAYERED_TX_POOL_ENABLED;
}
@Value.Default
default long getPendingTransactionsLayerMaxCapacityBytes() {
return DEFAULT_PENDING_TRANSACTIONS_LAYER_MAX_CAPACITY_BYTES;
}
@Value.Default
default int getMaxPrioritizedTransactions() {
return DEFAULT_MAX_PRIORITIZED_TRANSACTIONS;
}
@Value.Default
default int getMaxFutureBySender() {
return DEFAULT_MAX_FUTURE_BY_SENDER;
}
}

@ -20,13 +20,23 @@ import org.hyperledger.besu.ethereum.eth.manager.EthContext;
import org.hyperledger.besu.ethereum.eth.messages.EthPV62;
import org.hyperledger.besu.ethereum.eth.messages.EthPV65;
import org.hyperledger.besu.ethereum.eth.sync.state.SyncState;
import org.hyperledger.besu.ethereum.eth.transactions.layered.AbstractPrioritizedTransactions;
import org.hyperledger.besu.ethereum.eth.transactions.layered.BaseFeePrioritizedTransactions;
import org.hyperledger.besu.ethereum.eth.transactions.layered.EndLayer;
import org.hyperledger.besu.ethereum.eth.transactions.layered.GasPricePrioritizedTransactions;
import org.hyperledger.besu.ethereum.eth.transactions.layered.LayeredPendingTransactions;
import org.hyperledger.besu.ethereum.eth.transactions.layered.ReadyTransactions;
import org.hyperledger.besu.ethereum.eth.transactions.layered.SparseTransactions;
import org.hyperledger.besu.ethereum.eth.transactions.sorter.AbstractPendingTransactionsSorter;
import org.hyperledger.besu.ethereum.eth.transactions.sorter.BaseFeePendingTransactionsSorter;
import org.hyperledger.besu.ethereum.eth.transactions.sorter.GasPricePendingTransactionsSorter;
import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule;
import org.hyperledger.besu.ethereum.mainnet.feemarket.BaseFeeMarket;
import org.hyperledger.besu.plugin.services.BesuEvents;
import org.hyperledger.besu.plugin.services.MetricsSystem;
import java.time.Clock;
import java.util.function.BiFunction;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -44,9 +54,11 @@ public class TransactionPoolFactory {
final MiningParameters miningParameters,
final TransactionPoolConfiguration transactionPoolConfiguration) {
final TransactionPoolMetrics metrics = new TransactionPoolMetrics(metricsSystem);
final PendingTransactions pendingTransactions =
createPendingTransactions(
protocolSchedule, protocolContext, clock, metricsSystem, transactionPoolConfiguration);
protocolSchedule, protocolContext, clock, metrics, transactionPoolConfiguration);
final PeerTransactionTracker transactionTracker = new PeerTransactionTracker();
final TransactionsMessageSender transactionsMessageSender =
@ -59,7 +71,7 @@ public class TransactionPoolFactory {
protocolSchedule,
protocolContext,
ethContext,
metricsSystem,
metrics,
syncState,
miningParameters,
transactionPoolConfiguration,
@ -73,7 +85,7 @@ public class TransactionPoolFactory {
final ProtocolSchedule protocolSchedule,
final ProtocolContext protocolContext,
final EthContext ethContext,
final MetricsSystem metricsSystem,
final TransactionPoolMetrics metrics,
final SyncState syncState,
final MiningParameters miningParameters,
final TransactionPoolConfiguration transactionPoolConfiguration,
@ -81,6 +93,7 @@ public class TransactionPoolFactory {
final PeerTransactionTracker transactionTracker,
final TransactionsMessageSender transactionsMessageSender,
final NewPooledTransactionHashesMessageSender newPooledTransactionHashesMessageSender) {
final TransactionPool transactionPool =
new TransactionPool(
pendingTransactions,
@ -94,13 +107,13 @@ public class TransactionPoolFactory {
newPooledTransactionHashesMessageSender),
ethContext,
miningParameters,
metricsSystem,
metrics,
transactionPoolConfiguration);
final TransactionsMessageHandler transactionsMessageHandler =
new TransactionsMessageHandler(
ethContext.getScheduler(),
new TransactionsMessageProcessor(transactionTracker, transactionPool, metricsSystem),
new TransactionsMessageProcessor(transactionTracker, transactionPool, metrics),
transactionPoolConfiguration.getTxMessageKeepAliveSeconds());
final NewPooledTransactionHashesMessageHandler pooledTransactionsMessageHandler =
@ -111,7 +124,7 @@ public class TransactionPoolFactory {
transactionPool,
transactionPoolConfiguration,
ethContext,
metricsSystem),
metrics),
transactionPoolConfiguration.getTxMessageKeepAliveSeconds());
subscribeTransactionHandlers(
@ -173,11 +186,37 @@ public class TransactionPoolFactory {
final ProtocolSchedule protocolSchedule,
final ProtocolContext protocolContext,
final Clock clock,
final MetricsSystem metricsSystem,
final TransactionPoolMetrics metrics,
final TransactionPoolConfiguration transactionPoolConfiguration) {
boolean isFeeMarketImplementBaseFee =
protocolSchedule.anyMatch(
scheduledSpec -> scheduledSpec.spec().getFeeMarket().implementsBaseFee());
if (transactionPoolConfiguration.getLayeredTxPoolEnabled()) {
LOG.info("Using layered transaction pool");
return createLayeredPendingTransactions(
protocolSchedule,
protocolContext,
metrics,
transactionPoolConfiguration,
isFeeMarketImplementBaseFee);
} else {
return createPendingTransactionSorter(
protocolContext,
clock,
metrics.getMetricsSystem(),
transactionPoolConfiguration,
isFeeMarketImplementBaseFee);
}
}
private static AbstractPendingTransactionsSorter createPendingTransactionSorter(
final ProtocolContext protocolContext,
final Clock clock,
final MetricsSystem metricsSystem,
final TransactionPoolConfiguration transactionPoolConfiguration,
final boolean isFeeMarketImplementBaseFee) {
if (isFeeMarketImplementBaseFee) {
return new BaseFeePendingTransactionsSorter(
transactionPoolConfiguration,
@ -192,4 +231,60 @@ public class TransactionPoolFactory {
protocolContext.getBlockchain()::getChainHeadHeader);
}
}
private static PendingTransactions createLayeredPendingTransactions(
final ProtocolSchedule protocolSchedule,
final ProtocolContext protocolContext,
final TransactionPoolMetrics metrics,
final TransactionPoolConfiguration transactionPoolConfiguration,
final boolean isFeeMarketImplementBaseFee) {
final TransactionPoolReplacementHandler transactionReplacementHandler =
new TransactionPoolReplacementHandler(transactionPoolConfiguration.getPriceBump());
final BiFunction<PendingTransaction, PendingTransaction, Boolean> transactionReplacementTester =
(t1, t2) ->
transactionReplacementHandler.shouldReplace(
t1, t2, protocolContext.getBlockchain().getChainHeadHeader());
final EndLayer endLayer = new EndLayer(metrics);
final SparseTransactions sparseTransactions =
new SparseTransactions(
transactionPoolConfiguration, endLayer, metrics, transactionReplacementTester);
final ReadyTransactions readyTransactions =
new ReadyTransactions(
transactionPoolConfiguration,
sparseTransactions,
metrics,
transactionReplacementTester);
final AbstractPrioritizedTransactions pendingTransactionsSorter;
if (isFeeMarketImplementBaseFee) {
final BaseFeeMarket baseFeeMarket =
(BaseFeeMarket)
protocolSchedule
.getByBlockHeader(protocolContext.getBlockchain().getChainHeadHeader())
.getFeeMarket();
pendingTransactionsSorter =
new BaseFeePrioritizedTransactions(
transactionPoolConfiguration,
protocolContext.getBlockchain()::getChainHeadHeader,
readyTransactions,
metrics,
transactionReplacementTester,
baseFeeMarket);
} else {
pendingTransactionsSorter =
new GasPricePrioritizedTransactions(
transactionPoolConfiguration,
readyTransactions,
metrics,
transactionReplacementTester);
}
return new LayeredPendingTransactions(transactionPoolConfiguration, pendingTransactionsSorter);
}
}

@ -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";
}
}

@ -22,10 +22,6 @@ import org.hyperledger.besu.ethereum.eth.manager.EthPeer;
import org.hyperledger.besu.ethereum.eth.messages.TransactionsMessage;
import org.hyperledger.besu.ethereum.p2p.rlpx.wire.messages.DisconnectMessage.DisconnectReason;
import org.hyperledger.besu.ethereum.rlp.RLPException;
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 java.time.Duration;
import java.time.Instant;
@ -38,40 +34,20 @@ import org.slf4j.LoggerFactory;
class TransactionsMessageProcessor {
private static final Logger LOG = LoggerFactory.getLogger(TransactionsMessageProcessor.class);
private static final int SKIPPED_MESSAGES_LOGGING_THRESHOLD = 1000;
private static final String TRANSACTIONS = "transactions";
static final String METRIC_LABEL = "transactions";
private final PeerTransactionTracker transactionTracker;
private final TransactionPool transactionPool;
private final Counter totalSkippedTransactionsMessageCounter;
private final Counter alreadySeenTransactionsCounter;
private final TransactionPoolMetrics metrics;
public TransactionsMessageProcessor(
final PeerTransactionTracker transactionTracker,
final TransactionPool transactionPool,
final MetricsSystem metricsSystem) {
final TransactionPoolMetrics metrics) {
this.transactionTracker = transactionTracker;
this.transactionPool = transactionPool;
this.totalSkippedTransactionsMessageCounter =
new RunnableCounter(
metricsSystem.createCounter(
BesuMetricCategory.TRANSACTION_POOL,
"transactions_messages_skipped_total",
"Total number of transactions messages skipped by the processor."),
() ->
LOG.warn(
"{} expired transaction messages have been skipped.",
SKIPPED_MESSAGES_LOGGING_THRESHOLD),
SKIPPED_MESSAGES_LOGGING_THRESHOLD);
alreadySeenTransactionsCounter =
metricsSystem
.createLabelledCounter(
BesuMetricCategory.TRANSACTION_POOL,
"remote_already_seen_total",
"Total number of received transactions already seen",
"source")
.labels(TRANSACTIONS);
this.metrics = metrics;
metrics.initExpiredMessagesCounter(METRIC_LABEL);
}
void processTransactionsMessage(
@ -83,7 +59,7 @@ class TransactionsMessageProcessor {
if (startedAt.plus(keepAlive).isAfter(now())) {
this.processTransactionsMessage(peer, transactionsMessage);
} else {
totalSkippedTransactionsMessageCounter.inc();
metrics.incrementExpiredMessages(METRIC_LABEL);
}
}
@ -95,8 +71,8 @@ class TransactionsMessageProcessor {
transactionTracker.markTransactionsAsSeen(peer, incomingTransactions);
alreadySeenTransactionsCounter.inc(
(long) incomingTransactions.size() - freshTransactions.size());
metrics.incrementAlreadySeenTransactions(
METRIC_LABEL, incomingTransactions.size() - freshTransactions.size());
LOG.atTrace()
.setMessage(
"Received transactions message from {}, incoming transactions {}, incoming list {}"

@ -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;

@ -14,24 +14,25 @@
*/
package org.hyperledger.besu.ethereum.eth.transactions.sorter;
import static org.hyperledger.besu.ethereum.eth.transactions.TransactionAddedStatus.ADDED;
import static org.hyperledger.besu.ethereum.eth.transactions.TransactionAddedStatus.LOWER_NONCE_INVALID_TRANSACTION_KNOWN;
import static org.hyperledger.besu.ethereum.eth.transactions.TransactionAddedStatus.NONCE_TOO_FAR_IN_FUTURE_FOR_SENDER;
import static org.hyperledger.besu.ethereum.eth.transactions.TransactionAddedStatus.REJECTED_UNDERPRICED_REPLACEMENT;
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.LOWER_NONCE_INVALID_TRANSACTION_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 org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.datatypes.Hash;
import org.hyperledger.besu.ethereum.core.AccountTransactionOrder;
import org.hyperledger.besu.ethereum.core.Block;
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.PendingTransactionListener;
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactions;
import org.hyperledger.besu.ethereum.eth.transactions.TransactionAddedStatus;
import org.hyperledger.besu.ethereum.eth.transactions.TransactionAddedResult;
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolConfiguration;
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolReplacementHandler;
import org.hyperledger.besu.ethereum.mainnet.feemarket.FeeMarket;
import org.hyperledger.besu.evm.account.Account;
import org.hyperledger.besu.evm.account.AccountState;
import org.hyperledger.besu.metrics.BesuMetricCategory;
@ -41,8 +42,8 @@ import org.hyperledger.besu.plugin.services.metrics.LabelledMetric;
import org.hyperledger.besu.util.Subscribers;
import java.time.Clock;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
@ -83,7 +84,7 @@ public abstract class AbstractPendingTransactionsSorter implements PendingTransa
protected final LowestInvalidNonceCache lowestInvalidKnownNonceCache =
new LowestInvalidNonceCache(DEFAULT_LOWEST_INVALID_KNOWN_NONCE_CACHE);
protected final Subscribers<PendingTransactionListener> pendingTransactionSubscribers =
protected final Subscribers<PendingTransactionAddedListener> pendingTransactionSubscribers =
Subscribers.create();
protected final Subscribers<PendingTransactionDroppedListener> transactionDroppedListeners =
@ -144,11 +145,14 @@ public abstract class AbstractPendingTransactionsSorter implements PendingTransa
@Override
public void evictOldTransactions() {
final Instant removeTransactionsBefore =
clock.instant().minus(poolConfig.getPendingTxRetentionPeriod(), ChronoUnit.HOURS);
final long removeTransactionsBefore =
clock
.instant()
.minus(poolConfig.getPendingTxRetentionPeriod(), ChronoUnit.HOURS)
.toEpochMilli();
pendingTransactions.values().stream()
.filter(transaction -> transaction.getAddedToPoolAt().isBefore(removeTransactionsBefore))
.filter(transaction -> transaction.getAddedAt() < removeTransactionsBefore)
.forEach(
transactionInfo -> {
LOG.atTrace()
@ -168,7 +172,7 @@ public abstract class AbstractPendingTransactionsSorter implements PendingTransa
}
@Override
public TransactionAddedStatus addRemoteTransaction(
public TransactionAddedResult addRemoteTransaction(
final Transaction transaction, final Optional<Account> maybeSenderAccount) {
if (lowestInvalidKnownNonceCache.hasInvalidLowerNonce(transaction)) {
@ -181,8 +185,8 @@ public abstract class AbstractPendingTransactionsSorter implements PendingTransa
}
final PendingTransaction pendingTransaction =
new PendingTransaction(transaction, false, clock.instant());
final TransactionAddedStatus transactionAddedStatus =
new PendingTransaction.Remote(transaction, clock.millis());
final TransactionAddedResult transactionAddedStatus =
addTransaction(pendingTransaction, maybeSenderAccount);
if (transactionAddedStatus.equals(ADDED)) {
lowestInvalidKnownNonceCache.registerValidTransaction(transaction);
@ -192,11 +196,11 @@ public abstract class AbstractPendingTransactionsSorter implements PendingTransa
}
@Override
public TransactionAddedStatus addLocalTransaction(
public TransactionAddedResult addLocalTransaction(
final Transaction transaction, final Optional<Account> maybeSenderAccount) {
final TransactionAddedStatus transactionAdded =
final TransactionAddedResult transactionAdded =
addTransaction(
new PendingTransaction(transaction, true, clock.instant()), maybeSenderAccount);
new PendingTransaction.Local(transaction, clock.millis()), maybeSenderAccount);
if (transactionAdded.equals(ADDED)) {
localSenders.add(transaction.getSender());
localTransactionAddedCounter.inc();
@ -204,13 +208,25 @@ public abstract class AbstractPendingTransactionsSorter implements PendingTransa
return transactionAdded;
}
@Override
public void removeTransaction(final Transaction transaction) {
void removeTransaction(final Transaction transaction) {
removeTransaction(transaction, false);
notifyTransactionDropped(transaction);
}
@Override
public void manageBlockAdded(
final BlockHeader blockHeader,
final List<Transaction> confirmedTransactions,
final List<Transaction> reorgTransactions,
final FeeMarket feeMarket) {
synchronized (lock) {
confirmedTransactions.forEach(this::transactionAddedToBlock);
manageBlockAdded(blockHeader);
}
}
protected abstract void manageBlockAdded(final BlockHeader blockHeader);
public void transactionAddedToBlock(final Transaction transaction) {
removeTransaction(transaction, true);
lowestInvalidKnownNonceCache.registerValidTransaction(transaction);
@ -250,8 +266,8 @@ public abstract class AbstractPendingTransactionsSorter implements PendingTransa
switch (result) {
case DELETE_TRANSACTION_AND_CONTINUE:
transactionsToRemove.add(transactionToProcess);
signalInvalidAndGetDependentTransactions(transactionToProcess)
.forEach(transactionsToRemove::add);
transactionsToRemove.addAll(
signalInvalidAndGetDependentTransactions(transactionToProcess));
break;
case CONTINUE:
break;
@ -275,7 +291,7 @@ public abstract class AbstractPendingTransactionsSorter implements PendingTransa
.map(PendingTransaction::getTransaction));
}
private TransactionAddedStatus addTransactionForSenderAndNonce(
private TransactionAddedResult addTransactionForSenderAndNonce(
final PendingTransaction pendingTransaction, final Optional<Account> maybeSenderAccount) {
PendingTransactionsForSender pendingTxsForSender =
@ -357,8 +373,8 @@ public abstract class AbstractPendingTransactionsSorter implements PendingTransa
}
@Override
public boolean containsTransaction(final Hash transactionHash) {
return pendingTransactions.containsKey(transactionHash);
public boolean containsTransaction(final Transaction transaction) {
return pendingTransactions.containsKey(transaction.getHash());
}
@Override
@ -368,12 +384,12 @@ public abstract class AbstractPendingTransactionsSorter implements PendingTransa
}
@Override
public Set<PendingTransaction> getPendingTransactions() {
return new HashSet<>(pendingTransactions.values());
public List<PendingTransaction> getPendingTransactions() {
return new ArrayList<>(pendingTransactions.values());
}
@Override
public long subscribePendingTransactions(final PendingTransactionListener listener) {
public long subscribePendingTransactions(final PendingTransactionAddedListener listener) {
return pendingTransactionSubscribers.subscribe(listener);
}
@ -401,9 +417,6 @@ public abstract class AbstractPendingTransactionsSorter implements PendingTransa
: pendingTransactionsForSender.maybeNextNonce();
}
@Override
public abstract void manageBlockAdded(final Block block);
private void removeTransaction(final Transaction transaction, final boolean addedToBlock) {
synchronized (lock) {
final PendingTransaction removedPendingTx = pendingTransactions.remove(transaction.getHash());
@ -422,7 +435,7 @@ public abstract class AbstractPendingTransactionsSorter implements PendingTransa
protected abstract void prioritizeTransaction(final PendingTransaction pendingTransaction);
private TransactionAddedStatus addTransaction(
private TransactionAddedResult addTransaction(
final PendingTransaction pendingTransaction, final Optional<Account> maybeSenderAccount) {
final Transaction transaction = pendingTransaction.getTransaction();
synchronized (lock) {
@ -431,7 +444,7 @@ public abstract class AbstractPendingTransactionsSorter implements PendingTransa
.setMessage("Already known transaction {}")
.addArgument(pendingTransaction::toTraceLog)
.log();
return TransactionAddedStatus.ALREADY_KNOWN;
return ALREADY_KNOWN;
}
if (transaction.getNonce() - maybeSenderAccount.map(AccountState::getNonce).orElse(0L)
@ -450,10 +463,10 @@ public abstract class AbstractPendingTransactionsSorter implements PendingTransa
return NONCE_TOO_FAR_IN_FUTURE_FOR_SENDER;
}
final TransactionAddedStatus transactionAddedStatus =
final TransactionAddedResult transactionAddedStatus =
addTransactionForSenderAndNonce(pendingTransaction, maybeSenderAccount);
if (!transactionAddedStatus.equals(TransactionAddedStatus.ADDED)) {
if (!transactionAddedStatus.equals(ADDED)) {
return transactionAddedStatus;
}
@ -465,7 +478,7 @@ public abstract class AbstractPendingTransactionsSorter implements PendingTransa
}
}
notifyTransactionAdded(pendingTransaction.getTransaction());
return TransactionAddedStatus.ADDED;
return ADDED;
}
protected abstract PendingTransaction getLeastPriorityTransaction();
@ -483,46 +496,24 @@ public abstract class AbstractPendingTransactionsSorter implements PendingTransa
}
@Override
public String toTraceLog(
final boolean withTransactionsBySender, final boolean withLowestInvalidNonce) {
public String logStats() {
return "Pending " + pendingTransactions.size();
}
@Override
public String toTraceLog() {
synchronized (lock) {
StringBuilder sb =
new StringBuilder(
"Transactions in order { "
"Prioritized transactions { "
+ StreamSupport.stream(
Spliterators.spliteratorUnknownSize(
prioritizedTransactions(), Spliterator.ORDERED),
false)
.map(
pendingTx -> {
PendingTransactionsForSender pendingTxsForSender =
transactionsBySender.get(pendingTx.getSender());
long nonceDistance =
pendingTx.getNonce() - pendingTxsForSender.getSenderAccountNonce();
return "nonceDistance: "
+ nonceDistance
+ ", senderAccount: "
+ pendingTxsForSender.getSenderAccount()
+ ", "
+ pendingTx.toTraceLog();
})
.map(PendingTransaction::toTraceLog)
.collect(Collectors.joining("; "))
+ " }");
if (withTransactionsBySender) {
sb.append(
", Transactions by sender { "
+ transactionsBySender.entrySet().stream()
.map(e -> "(" + e.getKey() + ") " + e.getValue().toTraceLog())
.collect(Collectors.joining("; "))
+ " }");
}
if (withLowestInvalidNonce) {
sb.append(
", Lowest invalid nonce by sender cache {"
+ lowestInvalidKnownNonceCache.toTraceLog()
+ "}");
}
return sb.toString();
}
}
@ -547,10 +538,14 @@ public abstract class AbstractPendingTransactionsSorter implements PendingTransa
.map(PendingTransaction::getTransaction)
.collect(Collectors.toList());
}
return List.of();
}
@Override
public void signalInvalidAndRemoveDependentTransactions(final Transaction transaction) {
signalInvalidAndGetDependentTransactions(transaction).forEach(this::removeTransaction);
}
@Override
public boolean isLocalSender(final Address sender) {
return poolConfig.getDisableLocalTransactions() ? false : localSenders.contains(sender);

@ -18,7 +18,6 @@ import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.toUnmodifiableList;
import org.hyperledger.besu.datatypes.Wei;
import org.hyperledger.besu.ethereum.core.Block;
import org.hyperledger.besu.ethereum.core.BlockHeader;
import org.hyperledger.besu.ethereum.core.Transaction;
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction;
@ -74,7 +73,7 @@ public class BaseFeePendingTransactionsSorter extends AbstractPendingTransaction
.orElse(Wei.ZERO)
.getAsBigInteger()
.longValue())
.thenComparing(PendingTransaction::getAddedToPoolAt)
.thenComparing(PendingTransaction::getAddedAt)
.thenComparing(PendingTransaction::getSequence)
.reversed());
@ -88,7 +87,7 @@ public class BaseFeePendingTransactionsSorter extends AbstractPendingTransaction
.getMaxFeePerGas()
.map(maxFeePerGas -> maxFeePerGas.getAsBigInteger().longValue())
.orElse(pendingTx.getGasPrice().toLong()))
.thenComparing(PendingTransaction::getAddedToPoolAt)
.thenComparing(PendingTransaction::getAddedAt)
.thenComparing(PendingTransaction::getSequence)
.reversed());
@ -100,8 +99,8 @@ public class BaseFeePendingTransactionsSorter extends AbstractPendingTransaction
}
@Override
public void manageBlockAdded(final Block block) {
block.getHeader().getBaseFee().ifPresent(this::updateBaseFee);
public void manageBlockAdded(final BlockHeader blockHeader) {
blockHeader.getBaseFee().ifPresent(this::updateBaseFee);
}
@Override

@ -16,7 +16,6 @@ package org.hyperledger.besu.ethereum.eth.transactions.sorter;
import static java.util.Comparator.comparing;
import org.hyperledger.besu.ethereum.core.Block;
import org.hyperledger.besu.ethereum.core.BlockHeader;
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction;
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolConfiguration;
@ -40,7 +39,7 @@ public class GasPricePendingTransactionsSorter extends AbstractPendingTransactio
new TreeSet<>(
comparing(PendingTransaction::isReceivedFromLocalSource)
.thenComparing(PendingTransaction::getGasPrice)
.thenComparing(PendingTransaction::getAddedToPoolAt)
.thenComparing(PendingTransaction::getAddedAt)
.thenComparing(PendingTransaction::getSequence)
.reversed());
@ -59,7 +58,7 @@ public class GasPricePendingTransactionsSorter extends AbstractPendingTransactio
}
@Override
public void manageBlockAdded(final Block block) {
public void manageBlockAdded(final BlockHeader blockHeader) {
// nothing to do
}

@ -32,6 +32,7 @@ import org.hyperledger.besu.ethereum.eth.manager.EthPeer;
import org.hyperledger.besu.ethereum.eth.manager.EthScheduler;
import org.hyperledger.besu.ethereum.eth.transactions.PeerTransactionTracker;
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPool;
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolMetrics;
import org.hyperledger.besu.metrics.StubMetricsSystem;
import java.util.List;
@ -68,7 +69,13 @@ public class BufferedGetPooledTransactionsFromPeerFetcherTest {
ScheduledFuture<?> mock = mock(ScheduledFuture.class);
fetcher =
new BufferedGetPooledTransactionsFromPeerFetcher(
ethContext, mock, ethPeer, transactionPool, transactionTracker, metricsSystem);
ethContext,
mock,
ethPeer,
transactionPool,
transactionTracker,
new TransactionPoolMetrics(metricsSystem),
"new_pooled_transaction_hashes");
}
@Test
@ -123,6 +130,9 @@ public class BufferedGetPooledTransactionsFromPeerFetcherTest {
verifyNoInteractions(ethScheduler);
verify(transactionPool, never()).addRemoteTransactions(List.of(transaction));
assertThat(metricsSystem.getCounterValue("remote_already_seen_total", "hashes")).isEqualTo(1);
assertThat(
metricsSystem.getCounterValue(
"remote_transactions_already_seen_total", "new_pooled_transaction_hashes"))
.isEqualTo(1);
}
}

@ -23,7 +23,7 @@ import org.hyperledger.besu.ethereum.core.Transaction;
import org.hyperledger.besu.ethereum.core.TransactionTestFixture;
import org.hyperledger.besu.ethereum.eth.manager.EthPeer;
import org.hyperledger.besu.ethereum.eth.manager.ethtaskutils.PeerMessageTaskTest;
import org.hyperledger.besu.ethereum.eth.transactions.TransactionAddedStatus;
import org.hyperledger.besu.ethereum.eth.transactions.TransactionAddedResult;
import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem;
import org.hyperledger.besu.plugin.services.MetricsSystem;
@ -51,7 +51,7 @@ public class GetPooledTransactionsFromPeerTaskTest extends PeerMessageTaskTest<L
.chainId(Optional.empty())
.createTransaction(keyPair);
assertThat(transactionPool.getPendingTransactions().addLocalTransaction(tx, Optional.empty()))
.isEqualTo(TransactionAddedStatus.ADDED);
.isEqualTo(TransactionAddedResult.ADDED);
requestedData.add(tx);
}
return requestedData;

@ -98,7 +98,7 @@ public abstract class AbstractTransactionPoolTest {
private static final KeyPair KEY_PAIR2 =
SignatureAlgorithmFactory.getInstance().generateKeyPair();
@Mock protected MainnetTransactionValidator transactionValidator;
@Mock protected PendingTransactionListener listener;
@Mock protected PendingTransactionAddedListener listener;
@Mock protected MiningParameters miningParameters;
@Mock protected TransactionsMessageSender transactionsMessageSender;
@Mock protected NewPooledTransactionHashesMessageSender newPooledTransactionHashesMessageSender;
@ -182,7 +182,7 @@ public abstract class AbstractTransactionPoolTest {
transactionBroadcaster,
ethContext,
miningParameters,
metricsSystem,
new TransactionPoolMetrics(metricsSystem),
config);
}
@ -433,10 +433,10 @@ public abstract class AbstractTransactionPoolTest {
@Test
public void shouldDiscardRemoteTransactionThatAlreadyExistsBeforeValidation() {
doReturn(true).when(transactions).containsTransaction(transaction1.getHash());
doReturn(true).when(transactions).containsTransaction(transaction1);
transactionPool.addRemoteTransactions(singletonList(transaction1));
verify(transactions).containsTransaction(transaction1.getHash());
verify(transactions).containsTransaction(transaction1);
verifyNoInteractions(transactionValidator);
verifyNoMoreInteractions(transactions);
}

@ -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);
}
}

@ -93,7 +93,7 @@ public class NewPooledTransactionHashesMessageProcessorTest {
transactionPool,
transactionPoolConfiguration,
ethContext,
metricsSystem);
new TransactionPoolMetrics(metricsSystem));
when(ethContext.getScheduler()).thenReturn(ethScheduler);
}
@ -150,7 +150,9 @@ public class NewPooledTransactionHashesMessageProcessorTest {
ofMillis(1));
verifyNoInteractions(transactionTracker);
assertThat(
metricsSystem.getCounterValue("new_pooled_transaction_hashes_messages_skipped_total"))
metricsSystem.getCounterValue(
TransactionPoolMetrics.EXPIRED_MESSAGES_COUNTER_NAME,
NewPooledTransactionHashesMessageProcessor.METRIC_LABEL))
.isEqualTo(1);
}
@ -163,7 +165,9 @@ public class NewPooledTransactionHashesMessageProcessorTest {
ofMillis(1));
verifyNoInteractions(transactionPool);
assertThat(
metricsSystem.getCounterValue("new_pooled_transaction_hashes_messages_skipped_total"))
metricsSystem.getCounterValue(
TransactionPoolMetrics.EXPIRED_MESSAGES_COUNTER_NAME,
NewPooledTransactionHashesMessageProcessor.METRIC_LABEL))
.isEqualTo(1);
}

@ -33,12 +33,9 @@ import org.hyperledger.besu.ethereum.eth.manager.EthPeer;
import org.hyperledger.besu.ethereum.eth.manager.MockPeerConnection;
import org.hyperledger.besu.ethereum.eth.messages.EthPV65;
import org.hyperledger.besu.ethereum.eth.messages.NewPooledTransactionHashesMessage;
import org.hyperledger.besu.ethereum.eth.transactions.sorter.BaseFeePendingTransactionsSorter;
import org.hyperledger.besu.ethereum.eth.transactions.sorter.GasPricePendingTransactionsSorter;
import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MessageData;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.Set;
@ -47,11 +44,8 @@ import java.util.stream.Collectors;
import com.google.common.collect.Sets;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.mockito.ArgumentCaptor;
@RunWith(Parameterized.class)
public class NewPooledTransactionHashesMessageSenderTest {
private final EthPeer peer1 = mock(EthPeer.class);
@ -62,25 +56,17 @@ public class NewPooledTransactionHashesMessageSenderTest {
private final Transaction transaction2 = generator.transaction();
private final Transaction transaction3 = generator.transaction();
@Parameterized.Parameter public PendingTransactions pendingTransactions;
public PendingTransactions pendingTransactions;
private PeerTransactionTracker transactionTracker;
private NewPooledTransactionHashesMessageSender messageSender;
@Parameterized.Parameters
public static Collection<Object[]> data() {
return Arrays.asList(
new Object[][] {
{mock(GasPricePendingTransactionsSorter.class)},
{mock(BaseFeePendingTransactionsSorter.class)}
});
}
@Before
public void setUp() {
transactionTracker = new PeerTransactionTracker();
messageSender = new NewPooledTransactionHashesMessageSender(transactionTracker);
final Transaction tx = mock(Transaction.class);
pendingTransactions = mock(PendingTransactions.class);
when(pendingTransactions.getTransactionByHash(any())).thenReturn(Optional.of(tx));
when(peer1.getConnection())

@ -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));
}
}

@ -35,7 +35,6 @@ import org.hyperledger.besu.ethereum.eth.manager.EthScheduler;
import org.hyperledger.besu.ethereum.eth.messages.EthPV65;
import org.hyperledger.besu.plugin.data.TransactionType;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@ -328,7 +327,7 @@ public class TransactionBroadcasterTest {
private Set<PendingTransaction> createPendingTransactionList(final int num, final boolean local) {
return IntStream.range(0, num)
.mapToObj(unused -> generator.transaction())
.map(tx -> new PendingTransaction(tx, local, Instant.now()))
.map(tx -> local ? new PendingTransaction.Local(tx) : new PendingTransaction.Remote(tx))
.collect(Collectors.toSet());
}
@ -336,7 +335,7 @@ public class TransactionBroadcasterTest {
final TransactionType type, final int num, final boolean local) {
return IntStream.range(0, num)
.mapToObj(unused -> generator.transaction(type))
.map(tx -> new PendingTransaction(tx, local, Instant.now()))
.map(tx -> local ? new PendingTransaction.Local(tx) : new PendingTransaction.Remote(tx))
.collect(Collectors.toSet());
}

@ -79,7 +79,7 @@ public class TransactionPoolFactoryTest {
@Mock EthMessages ethMessages;
@Mock EthScheduler ethScheduler;
@Mock GasPricePendingTransactionsSorter pendingTransactions;
@Mock PendingTransactions pendingTransactions;
@Mock PeerTransactionTracker peerTransactionTracker;
@Mock TransactionsMessageSender transactionsMessageSender;
@ -243,7 +243,7 @@ public class TransactionPoolFactoryTest {
schedule,
context,
ethContext,
new NoOpMetricsSystem(),
new TransactionPoolMetrics(new NoOpMetricsSystem()),
syncState,
new MiningParameters.Builder().minTransactionGasPrice(Wei.ONE).build(),
ImmutableTransactionPoolConfiguration.builder()

@ -54,7 +54,8 @@ public class TransactionsMessageProcessorTest {
metricsSystem = new StubMetricsSystem();
messageHandler =
new TransactionsMessageProcessor(transactionTracker, transactionPool, metricsSystem);
new TransactionsMessageProcessor(
transactionTracker, transactionPool, new TransactionPoolMetrics(metricsSystem));
}
@Test
@ -87,7 +88,11 @@ public class TransactionsMessageProcessorTest {
now().minus(ofMinutes(1)),
ofMillis(1));
verifyNoInteractions(transactionTracker);
assertThat(metricsSystem.getCounterValue("transactions_messages_skipped_total")).isEqualTo(1);
assertThat(
metricsSystem.getCounterValue(
TransactionPoolMetrics.EXPIRED_MESSAGES_COUNTER_NAME,
TransactionsMessageProcessor.METRIC_LABEL))
.isEqualTo(1);
}
@Test
@ -98,6 +103,10 @@ public class TransactionsMessageProcessorTest {
now().minus(ofMinutes(1)),
ofMillis(1));
verifyNoInteractions(transactionPool);
assertThat(metricsSystem.getCounterValue("transactions_messages_skipped_total")).isEqualTo(1);
assertThat(
metricsSystem.getCounterValue(
TransactionPoolMetrics.EXPIRED_MESSAGES_COUNTER_NAME,
TransactionsMessageProcessor.METRIC_LABEL))
.isEqualTo(1);
}
}

@ -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) {}
}

@ -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);
}
}

@ -15,12 +15,11 @@
package org.hyperledger.besu.ethereum.eth.transactions.sorter;
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.TransactionAddedStatus.ADDED;
import static org.hyperledger.besu.ethereum.eth.transactions.TransactionAddedStatus.ALREADY_KNOWN;
import static org.hyperledger.besu.ethereum.eth.transactions.TransactionAddedStatus.REJECTED_UNDERPRICED_REPLACEMENT;
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.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
@ -38,8 +37,8 @@ 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.ImmutableTransactionPoolConfiguration;
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactionAddedListener;
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactionDroppedListener;
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactionListener;
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactions;
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolConfiguration;
import org.hyperledger.besu.evm.account.Account;
@ -76,7 +75,7 @@ public abstract class AbstractPendingTransactionsTestBase {
protected final TestClock clock = new TestClock();
protected final StubMetricsSystem metricsSystem = new StubMetricsSystem();
protected PendingTransactions transactions =
protected AbstractPendingTransactionsSorter transactions =
getPendingTransactions(
ImmutableTransactionPoolConfiguration.builder()
.txPoolMaxSize(MAX_TRANSACTIONS)
@ -94,13 +93,14 @@ public abstract class AbstractPendingTransactionsTestBase {
protected final Transaction transaction1 = createTransaction(2);
protected final Transaction transaction2 = createTransaction(1);
protected final PendingTransactionListener listener = mock(PendingTransactionListener.class);
protected final PendingTransactionAddedListener listener =
mock(PendingTransactionAddedListener.class);
protected final PendingTransactionDroppedListener droppedListener =
mock(PendingTransactionDroppedListener.class);
protected static final Address SENDER1 = Util.publicKeyToAddress(KEYS1.getPublicKey());
protected static final Address SENDER2 = Util.publicKeyToAddress(KEYS2.getPublicKey());
abstract PendingTransactions getPendingTransactions(
abstract AbstractPendingTransactionsSorter getPendingTransactions(
final TransactionPoolConfiguration poolConfig, Optional<Clock> clock);
@Test
@ -314,7 +314,7 @@ public abstract class AbstractPendingTransactionsTestBase {
transactions.selectTransactions(
transaction -> {
parsedTransactions.add(transaction);
return COMPLETE_OPERATION;
return PendingTransactions.TransactionSelectionResult.COMPLETE_OPERATION;
});
assertThat(parsedTransactions.size()).isEqualTo(1);

@ -17,7 +17,6 @@ package org.hyperledger.besu.ethereum.eth.transactions.sorter;
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.PendingTransactions;
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolConfiguration;
import org.hyperledger.besu.plugin.data.TransactionType;
import org.hyperledger.besu.testutil.TestClock;
@ -30,7 +29,7 @@ import java.util.Random;
public class BaseFeePendingTransactionsTest extends AbstractPendingTransactionsTestBase {
@Override
PendingTransactions getPendingTransactions(
AbstractPendingTransactionsSorter getPendingTransactions(
final TransactionPoolConfiguration poolConfig, final Optional<Clock> clock) {
return new BaseFeePendingTransactionsSorter(
poolConfig,

@ -14,7 +14,6 @@
*/
package org.hyperledger.besu.ethereum.eth.transactions.sorter;
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactions;
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolConfiguration;
import org.hyperledger.besu.testutil.TestClock;
@ -25,7 +24,7 @@ import java.util.Optional;
public class GasPricePendingTransactionsTest extends AbstractPendingTransactionsTestBase {
@Override
PendingTransactions getPendingTransactions(
AbstractPendingTransactionsSorter getPendingTransactions(
final TransactionPoolConfiguration poolConfig, final Optional<Clock> clock) {
return new BaseFeePendingTransactionsSorter(
poolConfig,

@ -350,7 +350,7 @@ public interface RLPInput {
*/
default <T> List<T> readList(final Function<RLPInput, T> valueReader) {
final int size = enterList();
final List<T> res = new ArrayList<>(size);
final List<T> res = size == 0 ? List.of() : new ArrayList<>(size);
for (int i = 0; i < size; i++) {
try {
res.add(valueReader.apply(this));

@ -4983,6 +4983,19 @@
<sha256 value="3b63ce6e8fefacb320376e05e9fbb3bae86a889239008759189a0b0d5ca5c5d6" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.openjdk.jol" name="jol-core" version="0.17">
<artifact name="jol-core-0.17.jar">
<sha256 value="bd73d9ad265d8478ccde06130200877f3a4f0a6e2d44e7fb8cbb2e6f4104cbdc" origin="Generated by Gradle"/>
</artifact>
<artifact name="jol-core-0.17.pom">
<sha256 value="1abac2bcdd59c65bde95aa254bfc7da2e030080a6ace0a9deeb1a396bd38ecfe" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.openjdk.jol" name="jol-parent" version="0.17">
<artifact name="jol-parent-0.17.pom">
<sha256 value="e13ef86564da581de9383f9d4d1bf8c132b7c8689713308fc9f3c3e5e2e786c0" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.opentest4j" name="opentest4j" version="1.2.0">
<artifact name="opentest4j-1.2.0.jar">
<sha256 value="58812de60898d976fb81ef3b62da05c6604c18fd4a249f5044282479fc286af2" origin="Generated by Gradle"/>

@ -157,6 +157,7 @@ dependencyManagement {
}
dependency 'org.fusesource.jansi:jansi:2.4.0'
dependency 'org.openjdk.jol:jol-core:0.17'
dependency 'tech.pegasys:jc-kzg-4844:0.4.0'
dependencySet(group: 'org.hyperledger.besu', version: '0.7.1') {

Loading…
Cancel
Save