(Specifications) A bug in specifications with no direct impact on client implementations
Description
Brief/Intro
The intrinsic gas amount is the minimal amount of gas required for a transaction calculated solely based on the transaction structure before the actual transaction execution. Both transaction validation in txpool and evm execution enforce the gas limit provided in the transaction larger than the intrinsic gas to be included into a block and execution.
In Erigon (https://github.com/erigontech/erigon ), the intrinsic gas amount calculated in txpool is less than the transaction execution for transaction type AccessListTxType (they should be the same), which could possibly lead to DoS attack with many spam transactions entered into the txpool or user accidentally provides gas limit larger than the intrinsic gas amount in txpool but less than the execution, but the transaction ultimately will not be included in a block for execution.
func IntrinsicGas(data []byte, accessList types.AccessList, isContractCreation bool, isHomestead, isEIP2028, isEIP3860 bool, authorizationsLen uint64) (uint64, error) {
// Zero and non-zero bytes are priced differently
dataLen := uint64(len(data))
dataNonZeroLen := uint64(0)
for _, byt := range data {
if byt != 0 {
dataNonZeroLen++
}
}
gas, status := txpoolcfg.CalcIntrinsicGas(dataLen, dataNonZeroLen, authorizationsLen, accessList, isContractCreation, isHomestead, isEIP2028, isEIP3860)
if status != txpoolcfg.Success {
return 0, ErrGasUintOverflow
}
return gas, nil
}
In this case, the access list of the transaction is provided for the intrinsic gas. Consequently, the intrinsic gas of a transaction with access list calculated in the txpool (mempool) is less than that in the execution
// Check clauses 4-5, subtract intrinsic gas if everything is correct
gas, err := IntrinsicGas(st.data, accessTuples, contractCreation, rules.IsHomestead, rules.IsIstanbul, isEIP3860, uint64(len(auths)))
if err != nil {
return nil, err
}
if st.gasRemaining < gas {
return nil, fmt.Errorf("%w: have %d, want %d", ErrIntrinsicGas, st.gasRemaining, gas)
}
This would allow spam transactions to flood into the txpool (mempool) with a gas limit larger than the intrinsic gas in mempool but less than the execution, which could potentially lead to DoS attack to the txpool.
Impact Details
The difference in the intrinsic gas computation in the txpool (mempool) and execution would allow spam transactions to flood into the mempool, which could potentially lead to DoS attack to the txpool (mempool).
We create the following unit test to show that the intrinsic gas computed in txpool is less than the intrinsic gas in execution. Moreover, the gas limit lies in between them.
Create a transaction of type AccessListTxType of non-empty access list and compute the intrinsic gas in txpool and execution:
package core
import (
"bytes"
"fmt"
libcommon "github.com/erigontech/erigon-lib/common"
"github.com/erigontech/erigon-lib/crypto"
"github.com/erigontech/erigon/core/types"
"github.com/erigontech/erigon/txnprovider/txpool/txpoolcfg"
"github.com/holiman/uint256"
"testing"
)
func TestIntrinsicGasCalculation(t *testing.T) {
var (
signer = types.LatestSignerForChainID(libcommon.Big1)
addr = libcommon.HexToAddress("0x0000000000000000000000000000000000000001")
recipient = libcommon.HexToAddress("095e7baea6a6c7c4c2dfeb977efac326af552d87")
accesses = types.AccessList{{Address: addr, StorageKeys: []libcommon.Hash{{0}}}}
)
key, err := crypto.GenerateKey()
if err != nil {
t.Fatalf("could not generate key: %v", err)
}
txdata := &types.AccessListTx{
ChainID: uint256.NewInt(1),
LegacyTx: types.LegacyTx{
CommonTx: types.CommonTx{
To: &recipient,
Nonce: 1,
Gas: 25000,
},
GasPrice: uint256.NewInt(10),
},
AccessList: accesses,
}
tx, err := types.SignNewTx(key, *signer, txdata)
if err != nil {
t.Fatalf("could not sign transaction: %v", err)
}
// RLP
parsedTx, err := encodeDecodeBinary(tx)
data := parsedTx.GetData()
dataLen := uint64(len(data))
dataNonZeroLen := uint64(0)
for _, byt := range data {
if byt != 0 {
dataNonZeroLen++
}
}
// access list is nil in txpool
txpoolIntrinsicGas, _ := txpoolcfg.CalcIntrinsicGas(dataLen, dataNonZeroLen, 0, nil, false, true, true, true)
// access list is not nil in execution
executionIntrinsicGas, _ := IntrinsicGas(parsedTx.GetData(), parsedTx.GetAccessList(), false, true, true, true, 0)
fmt.Printf("txpoolIntrinsicGas %d is less than executionIntrinsicGas %d: %t\n", txpoolIntrinsicGas, executionIntrinsicGas, txpoolIntrinsicGas < executionIntrinsicGas)
fmt.Printf("txpoolIntrinsicGas %d is less than gas limit %d: %t\n", txpoolIntrinsicGas, parsedTx.GetGas(), txpoolIntrinsicGas < parsedTx.GetGas())
fmt.Printf("gas limit %d is less than executionIntrinsicGas %d: %t\n", parsedTx.GetGas(), executionIntrinsicGas, parsedTx.GetGas() < executionIntrinsicGas)
}
func encodeDecodeBinary(tx types.Transaction) (types.Transaction, error) {
var buf bytes.Buffer
var err error
if err = tx.MarshalBinary(&buf); err != nil {
return nil, fmt.Errorf("rlp encoding failed: %w", err)
}
var parsedTx types.Transaction
if parsedTx, err = types.UnmarshalTransactionFromBinary(buf.Bytes(), false /* blobTxnsAreWrappedWithBlobs */); err != nil {
return nil, fmt.Errorf("rlp decoding failed: %w", err)
}
return parsedTx, nil
}
The test result shows that the intrinsic gas in txpool is less than that in execution.
=== RUN TestIntrinsicGasCalculation
txpoolIntrinsicGas 21000 is less than executionIntrinsicGas 25300: true
txpoolIntrinsicGas 21000 is less than gas limit 25000: true
gas limit 25000 is less than executionIntrinsicGas 25300: true
--- PASS: TestIntrinsicGasCalculation (0.00s)
PASS
Process finished with the exit code 0