1

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?

3
  • 1
    Does this answer your question? error: replacement transaction underpriced Commented Oct 24, 2022 at 11:58
  • 1
    It does not, the key difference is that I had not sent any unmined transaction previously for it to appear. But thanks!
    – tinom9
    Commented Oct 24, 2022 at 12:07
  • 1
    It would be nice if you could add the solution as answer.
    – Ismael
    Commented Oct 26, 2022 at 16:34

1 Answer 1

2

Solved, it was the RPC node service. I switched to a private one and everything is working as expected.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.