11import Heap from 'qheap'
22import {
3+ AccessListEIP2930Transaction ,
34 Capability ,
45 FeeMarketEIP1559Transaction ,
56 Transaction ,
67 TypedTransaction ,
78} from '@ethereumjs/tx'
8- import { Address , BN } from 'ethereumjs-util'
9+ import { Address , BN , bufferToHex } from 'ethereumjs-util'
910import { Config } from '../config'
1011import { Peer } from '../net/peer'
1112import type VM from '@ethereumjs/vm'
1213import type { FullEthereumService } from './fullethereumservice'
1314import type { PeerPool } from '../net/peerpool'
1415import type { Block } from '@ethereumjs/block'
1516
17+ // Configuration constants
18+ const MIN_GAS_PRICE_BUMP_PERCENT = 10
19+ const MIN_GAS_PRICE = new BN ( 1000000000 ) // 1 GWei
20+ const TX_MAX_DATA_SIZE = 128 * 1024 // 128KB
21+ const MAX_POOL_SIZE = 5000
22+ const MAX_TXS_PER_ACCOUNT = 100
23+
1624export interface TxPoolOptions {
1725 /* Config */
1826 config : Config
@@ -41,6 +49,11 @@ type UnprefixedAddress = string
4149type UnprefixedHash = string
4250type PeerId = string
4351
52+ type GasPrice = {
53+ tip : BN
54+ maxFee : BN
55+ }
56+
4457/**
4558 * @module service
4659 */
@@ -73,6 +86,11 @@ export class TxPool {
7386 */
7487 public pool : Map < UnprefixedAddress , TxPoolObject [ ] >
7588
89+ /**
90+ * The number of txs currently in the pool
91+ */
92+ public txsInPool : number
93+
7694 /**
7795 * Map for handled tx hashes
7896 * (have been added to the pool at some point)
@@ -114,7 +132,7 @@ export class TxPool {
114132 /**
115133 * Rebroadcast full txs and new blocks to a fraction
116134 * of peers by doing
117- * `min (1, floor(NUM_PEERS/NUM_PEERS_REBROADCAST_QUOTIENT))`
135+ * `max (1, floor(NUM_PEERS/NUM_PEERS_REBROADCAST_QUOTIENT))`
118136 */
119137 public NUM_PEERS_REBROADCAST_QUOTIENT = 4
120138
@@ -133,6 +151,7 @@ export class TxPool {
133151 this . vm = this . service . execution . vm
134152
135153 this . pool = new Map < UnprefixedAddress , TxPoolObject [ ] > ( )
154+ this . txsInPool = 0
136155 this . handled = new Map < UnprefixedHash , HandledObject > ( )
137156 this . knownByPeer = new Map < PeerId , SentObject [ ] > ( )
138157
@@ -178,27 +197,140 @@ export class TxPool {
178197 }
179198 }
180199
200+ /**
201+ * Returns the GasPrice object to provide information of the tx' gas prices
202+ * @param tx Tx to use
203+ * @returns Gas price (both tip and max fee)
204+ */
205+ private txGasPrice ( tx : TypedTransaction ) : GasPrice {
206+ switch ( tx . type ) {
207+ case 0 :
208+ return {
209+ maxFee : ( tx as Transaction ) . gasPrice ,
210+ tip : ( tx as Transaction ) . gasPrice ,
211+ }
212+ case 1 :
213+ return {
214+ maxFee : ( tx as AccessListEIP2930Transaction ) . gasPrice ,
215+ tip : ( tx as AccessListEIP2930Transaction ) . gasPrice ,
216+ }
217+ case 2 :
218+ return {
219+ maxFee : ( tx as FeeMarketEIP1559Transaction ) . maxFeePerGas ,
220+ tip : ( tx as FeeMarketEIP1559Transaction ) . maxPriorityFeePerGas ,
221+ }
222+ default :
223+ throw new Error ( `tx of type ${ tx . type } unknown` )
224+ }
225+ }
226+
227+ private validateTxGasBump ( existingTx : TypedTransaction , addedTx : TypedTransaction ) {
228+ const existingTxGasPrice = this . txGasPrice ( existingTx )
229+ const newGasPrice = this . txGasPrice ( addedTx )
230+ const minTipCap = existingTxGasPrice . tip . add (
231+ existingTxGasPrice . tip . muln ( MIN_GAS_PRICE_BUMP_PERCENT ) . divn ( 100 )
232+ )
233+ const minFeeCap = existingTxGasPrice . maxFee . add (
234+ existingTxGasPrice . maxFee . muln ( MIN_GAS_PRICE_BUMP_PERCENT ) . divn ( 100 )
235+ )
236+ if ( newGasPrice . tip . lt ( minTipCap ) || newGasPrice . maxFee . lt ( minFeeCap ) ) {
237+ throw new Error ( 'replacement gas too low' )
238+ }
239+ }
240+
241+ /**
242+ * Validates a transaction against the pool and other constraints
243+ * @param tx The tx to validate
244+ */
245+ private async validate ( tx : TypedTransaction , isLocalTransaction : boolean = false ) {
246+ if ( ! tx . isSigned ( ) ) {
247+ throw new Error ( 'Attempting to add tx to txpool which is not signed' )
248+ }
249+ if ( tx . data . length > TX_MAX_DATA_SIZE ) {
250+ throw new Error (
251+ `Tx is too large (${ tx . data . length } bytes) and exceeds the max data size of ${ TX_MAX_DATA_SIZE } bytes`
252+ )
253+ }
254+ const currentGasPrice = this . txGasPrice ( tx )
255+ // This is the tip which the miner receives: miner does not want
256+ // to mine underpriced txs where miner gets almost no fees
257+ const currentTip = currentGasPrice . tip
258+ if ( ! isLocalTransaction ) {
259+ const txsInPool = this . txsInPool
260+ if ( txsInPool >= MAX_POOL_SIZE ) {
261+ throw new Error ( 'Cannot add tx: pool is full' )
262+ }
263+ // Local txs are not checked against MIN_GAS_PRICE
264+ if ( currentTip . lt ( MIN_GAS_PRICE ) ) {
265+ throw new Error ( `Tx does not pay the minimum gas price of ${ MIN_GAS_PRICE } ` )
266+ }
267+ }
268+ const senderAddress = tx . getSenderAddress ( )
269+ const sender : UnprefixedAddress = senderAddress . toString ( ) . slice ( 2 )
270+ const inPool = this . pool . get ( sender )
271+ if ( inPool ) {
272+ if ( ! isLocalTransaction && inPool . length >= MAX_TXS_PER_ACCOUNT ) {
273+ throw new Error (
274+ `Cannot add tx for ${ senderAddress } : already have max amount of txs for this account`
275+ )
276+ }
277+ // Replace pooled txs with the same nonce
278+ const existingTxn = inPool . find ( ( poolObj ) => poolObj . tx . nonce . eq ( tx . nonce ) )
279+ if ( existingTxn ) {
280+ if ( existingTxn . tx . hash ( ) . equals ( tx . hash ( ) ) ) {
281+ throw new Error ( `${ bufferToHex ( tx . hash ( ) ) } : this transaction is already in the TxPool` )
282+ }
283+ this . validateTxGasBump ( existingTxn . tx , tx )
284+ }
285+ }
286+ const block = await this . service . chain . getLatestHeader ( )
287+ if ( block . baseFeePerGas ) {
288+ if ( currentGasPrice . maxFee . lt ( block . baseFeePerGas ) ) {
289+ throw new Error (
290+ `Tx cannot pay basefee of ${ block . baseFeePerGas } , have ${ currentGasPrice . maxFee } .`
291+ )
292+ }
293+ }
294+ if ( tx . gasLimit . gt ( block . gasLimit ) ) {
295+ throw new Error ( `Tx gaslimit of ${ tx . gasLimit } exceeds block gas limit of ${ block . gasLimit } ` )
296+ }
297+ const account = await this . vm . stateManager . getAccount ( senderAddress )
298+ if ( account . nonce . gt ( tx . nonce ) ) {
299+ throw new Error (
300+ `0x${ sender } tries to send a tx with nonce ${ tx . nonce } , but account has nonce ${ account . nonce } (tx nonce too low)`
301+ )
302+ }
303+ const minimumBalance = tx . value . add ( currentGasPrice . maxFee . mul ( tx . gasLimit ) )
304+ if ( account . balance . lt ( minimumBalance ) ) {
305+ throw new Error (
306+ `0x${ sender } does not have enough balance to cover transaction costs, need ${ minimumBalance } , but have ${ account . balance } `
307+ )
308+ }
309+ }
310+
181311 /**
182312 * Adds a tx to the pool.
183313 *
184314 * If there is a tx in the pool with the same address and
185- * nonce it will be replaced by the new tx.
315+ * nonce it will be replaced by the new tx, if it has a sufficent gas bump.
316+ * This also verifies certain constraints, if these are not met, tx will not be added to the pool.
186317 * @param tx Transaction
318+ * @param isLocalTransaction Boolean which is true if this is a local transaction (this drops some constraint checks)
187319 */
188- add ( tx : TypedTransaction ) {
189- const sender : UnprefixedAddress = tx . getSenderAddress ( ) . toString ( ) . slice ( 2 )
190- const inPool = this . pool . get ( sender )
191- let add : TxPoolObject [ ] = [ ]
320+ async add ( tx : TypedTransaction , isLocalTransaction : boolean = false ) {
321+ await this . validate ( tx , isLocalTransaction )
322+ const address : UnprefixedAddress = tx . getSenderAddress ( ) . toString ( ) . slice ( 2 )
323+ let add : TxPoolObject [ ] = this . pool . get ( address ) ?? [ ]
324+ const inPool = this . pool . get ( address )
192325 if ( inPool ) {
193326 // Replace pooled txs with the same nonce
194327 add = inPool . filter ( ( poolObj ) => ! poolObj . tx . nonce . eq ( tx . nonce ) )
195328 }
196- const address : UnprefixedAddress = tx . getSenderAddress ( ) . toString ( ) . slice ( 2 )
197329 const hash : UnprefixedHash = tx . hash ( ) . toString ( 'hex' )
198330 const added = Date . now ( )
199331 add . push ( { tx, added, hash } )
200332 this . pool . set ( address , add )
201-
333+ this . txsInPool ++
202334 this . handled . set ( hash , { address, added } )
203335 }
204336
@@ -236,6 +368,7 @@ export class TxPool {
236368 return
237369 }
238370 const newPoolObjects = this . pool . get ( address ) ! . filter ( ( poolObj ) => poolObj . hash !== txHash )
371+ this . txsInPool --
239372 if ( newPoolObjects . length === 0 ) {
240373 // List of txs for address is now empty, can delete
241374 this . pool . delete ( address )
@@ -342,12 +475,18 @@ export class TxPool {
342475
343476 const newTxHashes = [ ]
344477 for ( const tx of txs ) {
345- this . add ( tx )
346- newTxHashes . push ( tx . hash ( ) )
478+ try {
479+ await this . add ( tx )
480+ newTxHashes . push ( tx . hash ( ) )
481+ } catch ( error : any ) {
482+ this . config . logger . debug (
483+ `Error adding tx to TxPool: ${ error . message } (tx hash: ${ bufferToHex ( tx . hash ( ) ) } )`
484+ )
485+ }
347486 }
348487 const peers = peerPool . peers
349488 const numPeers = peers . length
350- const sendFull = Math . min ( 1 , Math . floor ( numPeers / this . NUM_PEERS_REBROADCAST_QUOTIENT ) )
489+ const sendFull = Math . max ( 1 , Math . floor ( numPeers / this . NUM_PEERS_REBROADCAST_QUOTIENT ) )
351490 this . sendTransactions ( txs , peers . slice ( 0 , sendFull ) )
352491 await this . sendNewTxHashes ( newTxHashes , peers . slice ( sendFull ) )
353492 }
@@ -398,7 +537,13 @@ export class TxPool {
398537
399538 const newTxHashes = [ ]
400539 for ( const tx of txs ) {
401- this . add ( tx )
540+ try {
541+ await this . add ( tx )
542+ } catch ( error : any ) {
543+ this . config . logger . debug (
544+ `Error adding tx to TxPool: ${ error . message } (tx hash: ${ bufferToHex ( tx . hash ( ) ) } )`
545+ )
546+ }
402547 newTxHashes . push ( tx . hash ( ) )
403548 }
404549 await this . sendNewTxHashes ( newTxHashes , peerPool . peers )
@@ -456,7 +601,7 @@ export class TxPool {
456601 * @param baseFee Provide a baseFee to subtract from the legacy
457602 * gasPrice to determine the leftover priority tip.
458603 */
459- private txGasPrice ( tx : TypedTransaction , baseFee ?: BN ) {
604+ private normalizedGasPrice ( tx : TypedTransaction , baseFee ?: BN ) {
460605 const supports1559 = tx . supports ( Capability . EIP1559FeeMarket )
461606 if ( baseFee ) {
462607 if ( supports1559 ) {
@@ -509,7 +654,7 @@ export class TxPool {
509654 if ( baseFee ) {
510655 // If any tx has an insiffucient gasPrice,
511656 // remove all txs after that since they cannot be executed
512- const found = txsSortedByNonce . findIndex ( ( tx ) => this . txGasPrice ( tx ) . lt ( baseFee ) )
657+ const found = txsSortedByNonce . findIndex ( ( tx ) => this . normalizedGasPrice ( tx ) . lt ( baseFee ) )
513658 if ( found > - 1 ) {
514659 txsSortedByNonce = txsSortedByNonce . slice ( 0 , found )
515660 }
@@ -519,7 +664,7 @@ export class TxPool {
519664 // Initialize a price based heap with the head transactions
520665 const byPrice = new Heap < TypedTransaction > ( {
521666 comparBefore : ( a : TypedTransaction , b : TypedTransaction ) =>
522- this . txGasPrice ( b , baseFee ) . sub ( this . txGasPrice ( a , baseFee ) ) . ltn ( 0 ) ,
667+ this . normalizedGasPrice ( b , baseFee ) . sub ( this . normalizedGasPrice ( a , baseFee ) ) . ltn ( 0 ) ,
523668 } )
524669 byNonce . forEach ( ( txs , address ) => {
525670 byPrice . insert ( txs [ 0 ] )
@@ -566,10 +711,7 @@ export class TxPool {
566711 }
567712
568713 _logPoolStats ( ) {
569- let count = 0
570- this . pool . forEach ( ( poolObjects ) => {
571- count += poolObjects . length
572- } )
714+ const count = this . txsInPool
573715 this . config . logger . info (
574716 `TxPool Statistics txs=${ count } senders=${ this . pool . size } peers=${ this . service . pool . peers . length } `
575717 )
0 commit comments