I am building a Django service with Celery that sends multiple transactions to a contract and I am finding that the error {'code': -32000, 'message': 'replacement transaction underpriced'}
from geth
is raised even when I have not sent any prior transaction with that nonce.
I believe this is specially strange because I understand that this error is only raised when another transaction in the pool exists with the same nonce and, when the same transaction is built again and sent to the node with equal nonce and gas price, it is sent successfully.
For more context, this is a simplification of the function called:
def send_transaction(self):
contract = self.w3.eth.contract(address=CONTRACT_ADDRESS, abi=CONTRACT_ABI)
// This function gets next nonce from cache and node's transaction count
// returning whichever is higher
nonce = self.get_next_nonce()
// For logging purposes only
logger.info("Trying to send transaction with nonce " + str(nonce))
logger.info(
"Transaction count from node: "
+ str(self.w3.eth.get_transaction_count(self.w3.eth.default_account))
)
gas_price = self.w3.eth.generate_gas_price()
logger.info("Gas: " + str(gas_price))
// Function consumes ~77,000 gas
tx = contract.functions.contractFunction().transact(
{"gas": 1000000, "gasPrice": gas_price}
)
logger.info("Sent transaction with nonce " + str(nonce))
// Caches used nonce so another transaction can be sent
self.cache_next_nonce(nonce=nonce)
From which the following logs are caught:
[2022-10-24 10:50:13,802: INFO/MainProcess] Trying to send transaction with nonce 2276
[2022-10-24 10:50:13,863: INFO/MainProcess] Transaction count from node: 2276
[2022-10-24 10:50:13,924: INFO/MainProcess] Gas: 1500000057
[2022-10-24 10:50:14,857: INFO/MainProcess] {'code': -32000, 'message': 'replacement transaction underpriced'}
[2022-10-24 10:50:14,963: INFO/MainProcess] Trying to send transaction with nonce 2276
[2022-10-24 10:50:15,197: INFO/MainProcess] Transaction count from node: 2276
[2022-10-24 10:50:15,430: INFO/MainProcess] Gas: 1500000057
[2022-10-24 10:50:16,221: INFO/MainProcess] Sent transaction with nonce 2276
Digging into geth
code I find that this error corresponds to ErrReplaceUnderpriced
is raised in two use cases:
// add validates a transaction and inserts it into the non-executable queue for later
// pending promotion and execution. If the transaction is a replacement for an already
// pending or queued one, it overwrites the previous transaction if its price is higher.
//
// If a newly added transaction is marked as local, its sending account will be
// be added to the allowlist, preventing any associated transaction from being dropped
// out of the pool due to pricing constraints.
func (pool *TxPool) add(tx *types.Transaction, local bool) (replaced bool, err error) {
[...]
if list := pool.pending[from]; list != nil && list.Overlaps(tx) {
// Nonce already pending, check if required price bump is met
inserted, old := list.Add(tx, pool.config.PriceBump)
if !inserted {
pendingDiscardMeter.Mark(1)
return false, ErrReplaceUnderpriced
}
And then:
// enqueueTx inserts a new transaction into the non-executable transaction queue.
//
// Note, this method assumes the pool lock is held!
func (pool *TxPool) enqueueTx(hash common.Hash, tx *types.Transaction, local bool, addAll bool) (bool, error) {
// Try to insert the transaction into the future queue
from, _ := types.Sender(pool.signer, tx) // already validated
if pool.queue[from] == nil {
pool.queue[from] = newTxList(false)
}
inserted, old := pool.queue[from].Add(tx, pool.config.PriceBump)
if !inserted {
// An older transaction was better, discard this
queuedDiscardMeter.Mark(1)
return false, ErrReplaceUnderpriced
}
So, as I understand it, the node is rejecting the transaction because another one already exists with the same nonce, but then I send a transaction with the same nonce and gas price and it is accepted. What can be triggering this? Is there any other use case for this error to be raised that I do not know about? Can this be caused because of a custom implementation of geth
(I do not know if this is possible)?
I am using a public RPC node endpoint for the Polygon Mumbai testnet.
Edit: solved, it was the RPC node service. I switched to a private one and everything is working as expected. So now the question is: is it because of a custom implementation the geth
protocol that raises ErrReplaceUnderpriced
for another hidden reason, maybe node saturation as it was a public endpoint?