Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions graft/coreth/core/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,11 @@ graft_go_test(
"rlp_test.go",
"state_manager_test.go",
"state_processor_test.go",
"state_transition_extra_test.go",
"state_transition_test.go",
"txindexer_test.go",
],
data = glob(["testdata/**"]),
embed = [":core"],
deps = [
"//graft/coreth/consensus",
Expand Down
110 changes: 110 additions & 0 deletions graft/coreth/core/state_transition.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,11 @@ package core

import (
"fmt"
"math"
"math/big"

"github.com/ava-labs/avalanchego/graft/coreth/params"
"github.com/ava-labs/avalanchego/vms/evm/predicate"
"github.com/ava-labs/libevm/common"
cmath "github.com/ava-labs/libevm/common/math"
libevmcore "github.com/ava-labs/libevm/core"
Expand Down Expand Up @@ -79,6 +81,114 @@ func (result *ExecutionResult) Revert() []byte {
return common.CopyBytes(result.ReturnData)
}

// legacyIntrinsicGas is the original Coreth implementation of IntrinsicGas.
// It is preserved here (unexported) to verify that libevm/core.IntrinsicGas
// with hooks produces identical results. This function should not be used
// in production code - use libevmcore.IntrinsicGas instead.
func legacyIntrinsicGas(data []byte, accessList types.AccessList, isContractCreation bool, rules params.Rules) (uint64, error) {
var gas uint64
if isContractCreation && rules.IsHomestead {
gas = ethparams.TxGasContractCreation
} else {
gas = ethparams.TxGas
}
dataLen := uint64(len(data))
// Bump the required gas by the amount of transactional data
if dataLen > 0 {
// Zero and non-zero bytes are priced differently
var nz uint64
for _, byt := range data {
if byt != 0 {
nz++
}
}
// Make sure we don't exceed uint64 for all data combinations
nonZeroGas := ethparams.TxDataNonZeroGasFrontier
if rules.IsIstanbul {
nonZeroGas = ethparams.TxDataNonZeroGasEIP2028
}
if (math.MaxUint64-gas)/nonZeroGas < nz {
return 0, ErrGasUintOverflow
}
gas += nz * nonZeroGas

z := dataLen - nz
if (math.MaxUint64-gas)/ethparams.TxDataZeroGas < z {
return 0, ErrGasUintOverflow
}
gas += z * ethparams.TxDataZeroGas

if isContractCreation && params.GetRulesExtra(rules).IsDurango {
lenWords := toWordSize(dataLen)
if (math.MaxUint64-gas)/ethparams.InitCodeWordGas < lenWords {
return 0, ErrGasUintOverflow
}
gas += lenWords * ethparams.InitCodeWordGas
}
}
if accessList != nil {
accessListGas, err := accessListGas(rules, accessList)
if err != nil {
return 0, err
}
totalGas, overflow := cmath.SafeAdd(gas, accessListGas)
if overflow {
return 0, ErrGasUintOverflow
}
gas = totalGas
}

return gas, nil
}

func accessListGas(rules params.Rules, accessList types.AccessList) (uint64, error) {
var gas uint64
rulesExtra := params.GetRulesExtra(rules)
if !rulesExtra.PredicatersExist() {
gas += uint64(len(accessList)) * ethparams.TxAccessListAddressGas
gas += uint64(accessList.StorageKeys()) * ethparams.TxAccessListStorageKeyGas
return gas, nil
}

for _, accessTuple := range accessList {
address := accessTuple.Address
predicaterContract, ok := rulesExtra.Predicaters[address]
if !ok {
// Previous access list gas calculation does not use safemath because an overflow would not be possible with
// the size of access lists that could be included in a block and standard access list gas costs.
// Therefore, we only check for overflow when adding to [totalGas], which could include the sum of values
// returned by a predicate.
accessTupleGas := ethparams.TxAccessListAddressGas + uint64(len(accessTuple.StorageKeys))*ethparams.TxAccessListStorageKeyGas
totalGas, overflow := cmath.SafeAdd(gas, accessTupleGas)
if overflow {
return 0, ErrGasUintOverflow
}
gas = totalGas
} else {
predicateGas, err := predicaterContract.PredicateGas(predicate.Predicate(accessTuple.StorageKeys), rulesExtra)
if err != nil {
return 0, err
}
totalGas, overflow := cmath.SafeAdd(gas, predicateGas)
if overflow {
return 0, ErrGasUintOverflow
}
gas = totalGas
}
}

return gas, nil
}

// toWordSize returns the ceiled word size required for init code payment calculation.
func toWordSize(size uint64) uint64 {
if size > math.MaxUint64-31 {
return math.MaxUint64/32 + 1
}

return (size + 31) / 32
}

// A Message contains the data derived from a single transaction that is relevant to state
// processing.
type Message struct {
Expand Down
260 changes: 260 additions & 0 deletions graft/coreth/core/state_transition_extra_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
// Copyright (C) 2019, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package core

import (
"encoding/binary"
"errors"
"fmt"
"math"
"testing"

"github.com/ava-labs/libevm/common"
"github.com/ava-labs/libevm/core/types"
"github.com/stretchr/testify/require"

"github.com/ava-labs/avalanchego/graft/coreth/params"
"github.com/ava-labs/avalanchego/graft/coreth/precompile/contracts/warp"
"github.com/ava-labs/avalanchego/graft/coreth/precompile/precompileconfig"
"github.com/ava-labs/avalanchego/vms/evm/predicate"

cmath "github.com/ava-labs/libevm/common/math"
libevmcore "github.com/ava-labs/libevm/core"
)

// TestIntrinsicGasEquivalenceErrors verifies that libevm/core.IntrinsicGas with
// hooks produces identical error results to the legacy Coreth implementation.
// Happy-path equivalence is covered by [FuzzAccessListIntrinsicGasEquivalence].
func TestIntrinsicGasEquivalenceErrors(t *testing.T) {
predicateAddr := common.Address{0x01}
errPredicateGas := errors.New("predicate gas error")

tests := []struct {
name string
accessList types.AccessList
predicater *testPredicater
}{
{
name: "predicate_gas_error",
accessList: types.AccessList{
{Address: predicateAddr, StorageKeys: []common.Hash{{1}}},
},
predicater: newStaticPredicater(0, errPredicateGas),
},
{
name: "predicate_gas_overflow",
accessList: types.AccessList{
{Address: predicateAddr, StorageKeys: []common.Hash{{1}}},
{Address: common.Address{0x03}, StorageKeys: []common.Hash{{2}}},
},
predicater: newStaticPredicater(math.MaxUint64, nil),
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require := require.New(t)

rules := params.TestChainConfig.Rules(common.Big0, params.IsMergeTODO, 0)
rulesExtra := params.GetRulesExtra(rules)

rulesExtra.Predicaters = make(map[common.Address]precompileconfig.Predicater)
for _, tuple := range tt.accessList {
rulesExtra.Predicaters[tuple.Address] = tt.predicater
}

_, legacyErr := legacyIntrinsicGas(nil, tt.accessList, false, rules)
_, libevmErr := libevmcore.IntrinsicGas(nil, tt.accessList, false, rules)

require.ErrorIs(legacyErr, libevmErr)
})
}
}

// Isolated addresses for mocks: [params.TestChainConfig.Rules] pre-populates
// Predicaters from registered modules; low addresses like 0x01 may collide.
var (
addrTestPredicater0 = common.HexToAddress("0x00000000000000000000000000000000a11c0de0")
addrTestNormal0 = common.HexToAddress("0x00000000000000000000000000000000b00b0b00")
)

// fuzzChainConfigs covers the fork boundaries that affect intrinsic gas:
// - TestCortinaChainConfig: pre-Durango (no init-code word gas)
// - TestDurangoChainConfig: Durango + Shanghai
// - TestChainConfig: latest (Cancun)
var fuzzChainConfigs = []*params.ChainConfig{
params.TestCortinaChainConfig,
params.TestDurangoChainConfig,
params.TestChainConfig,
}

// intrinsicGasFuzzSeed is a readable corpus entry; use [encodeIntrinsicGasFuzzInput] for f.Add.
type intrinsicGasFuzzSeed struct {
configIdx uint8
isPredicater []bool
storageKeyLens []uint16
signerLen uint16
data []byte
isContractCreation bool
}

// encodeIntrinsicGasFuzzInput converts a seed to the byte form expected by [FuzzAccessListIntrinsicGasEquivalence]:
// - isPredicater: one byte per bool (0 or 1), index i selects tuple i (with wrap via i%len in the fuzz body).
// - storageKeyLens: little-endian uint16s, two bytes per tuple length.
func encodeIntrinsicGasFuzzInput(c intrinsicGasFuzzSeed) (configIdx uint8, isPred []byte, storageKeyByteLens []byte, signerLen uint16, data []byte, isContractCreation bool) {
isPred = make([]byte, len(c.isPredicater))
for i, b := range c.isPredicater {
if b {
isPred[i] = 1
}
}
storageKeyByteLens = make([]byte, 2*len(c.storageKeyLens))
for i, v := range c.storageKeyLens {
binary.LittleEndian.PutUint16(storageKeyByteLens[2*i:], v)
}
return c.configIdx, isPred, storageKeyByteLens, c.signerLen, c.data, c.isContractCreation
}

func FuzzAccessListIntrinsicGasEquivalence(f *testing.F) {
seeds := []intrinsicGasFuzzSeed{
// Predicater with storage keys
{isPredicater: []bool{true}, storageKeyLens: []uint16{90}, signerLen: 5},
// No access list tuples
{signerLen: 1},
// Single regular address with keys
{isPredicater: []bool{false}, storageKeyLens: []uint16{3}, signerLen: 1},
// Multiple regular addresses
{isPredicater: []bool{false, false}, storageKeyLens: []uint16{1, 2}, signerLen: 1},
// Mixed predicater and regular
{isPredicater: []bool{true, false}, storageKeyLens: []uint16{1, 3}, signerLen: 5},
// Large access list
{isPredicater: []bool{false, false, false, false}, storageKeyLens: []uint16{5, 3, 2, 0}, signerLen: 1},
// Zero-byte data
{isPredicater: []bool{false}, storageKeyLens: []uint16{1}, signerLen: 1, data: []byte{0, 0, 0, 0}},
// Non-zero-byte data
{isPredicater: []bool{false}, storageKeyLens: []uint16{1}, signerLen: 1, data: []byte{1, 2, 3, 4}},
// Mixed data
{isPredicater: []bool{false}, storageKeyLens: []uint16{1}, signerLen: 1, data: []byte{0, 1, 0, 2, 3}},
// Contract creation with access list
{isPredicater: []bool{false}, storageKeyLens: []uint16{1}, signerLen: 1, isContractCreation: true},
// Contract creation with data (exercises Durango init-code word gas)
{isPredicater: []bool{false}, storageKeyLens: []uint16{1}, signerLen: 1, data: make([]byte, 100), isContractCreation: true},
// Data with predicater
{isPredicater: []bool{true}, storageKeyLens: []uint16{2}, signerLen: 3, data: []byte{0, 0xff, 0, 0xff}},
}
for ci := range fuzzChainConfigs {
for _, s := range seeds {
s.configIdx = uint8(ci)
cfgIdx, a, b, sl, d, cc := encodeIntrinsicGasFuzzInput(s)
f.Add(cfgIdx, a, b, sl, d, cc)
}
}

f.Fuzz(func(t *testing.T, configIdx uint8, isPredicater []byte, storageKeyLens []byte, signerLen uint16, data []byte, isContractCreation bool) {
if signerLen == 0 {
t.Skip()
}
cfg := fuzzChainConfigs[int(configIdx)%len(fuzzChainConfigs)]
predicater := newWarpPredicater(signerLen)

rules := cfg.Rules(common.Big0, params.IsMergeTODO, 0)
rulesExtra := params.GetRulesExtra(rules)
rulesExtra.Predicaters[addrTestPredicater0] = predicater

keyLens := decodeUint16s(storageKeyLens)
al := make(types.AccessList, 0, len(keyLens))
for i, kl := range keyLens {
addr := addrTestNormal0
if len(isPredicater) > 0 && isPredicater[i%len(isPredicater)]&1 == 1 {
addr = addrTestPredicater0
}
al = append(al, types.AccessTuple{
Address: addr,
StorageKeys: hashRepeat(int(kl)),
})
t.Logf("(isPredicater[%d] == %v", i, addr == addrTestPredicater0)
t.Logf("(storageKeyLens[%d] == %d)", i, kl)
}

t.Logf("config: %d (IsDurango=%v)", configIdx%uint8(len(fuzzChainConfigs)), rulesExtra.IsDurango)
t.Logf("access list length: %d", len(al))
t.Logf("isPredicater: %v", isPredicater)
t.Logf("storageKeyLenByteInput: %v", storageKeyLens)
t.Logf("signerLen: %d", signerLen)
t.Logf("data length: %d", len(data))
t.Logf("isContractCreation: %v", isContractCreation)

libevmTotal, libevmErr := libevmcore.IntrinsicGas(data, al, isContractCreation, rules)
legacyTotal, legacyErr := legacyIntrinsicGas(data, al, isContractCreation, rules)
t.Logf("libevmTotal: %d", libevmTotal)
t.Logf("legacyTotal: %d", legacyTotal)
require.ErrorIs(t, libevmErr, legacyErr)
require.Equal(t, legacyTotal, libevmTotal)
})
}

// testPredicater implements [precompileconfig.Predicater] for tests by
// delegating PredicateGas to a caller-supplied function.
type testPredicater struct {
predicateGasF func(predicate.Predicate, precompileconfig.Rules) (uint64, error)
}

func (p *testPredicater) PredicateGas(pred predicate.Predicate, rules precompileconfig.Rules) (uint64, error) {
return p.predicateGasF(pred, rules)
}

func (*testPredicater) VerifyPredicate(*precompileconfig.PredicateContext, predicate.Predicate) error {
return nil
}

func newWarpPredicater(signerLen uint16) *testPredicater {
return &testPredicater{
predicateGasF: func(pred predicate.Predicate, rules precompileconfig.Rules) (uint64, error) {
config := warp.CurrentGasConfig(rules)
totalGas := config.VerifyPredicateBase
bytesGasCost, overflow := cmath.SafeMul(config.PerWarpMessageChunk, uint64(len(pred)))
if overflow {
return 0, fmt.Errorf("overflow calculating gas cost for %d warp message chunks", len(pred))
}
totalGas, overflow = cmath.SafeAdd(totalGas, bytesGasCost)
if overflow {
return 0, fmt.Errorf("overflow adding gas cost for %d warp message chunks", len(pred))
}
signerGas, overflow := cmath.SafeMul(uint64(signerLen), config.PerWarpSigner)
if overflow {
return 0, fmt.Errorf("overflow calculating gas cost for %d signers", signerLen)
}
totalGas, overflow = cmath.SafeAdd(totalGas, signerGas)
if overflow {
return 0, fmt.Errorf("overflow adding gas cost for %d signers", signerLen)
}
return totalGas, nil
},
}
}

func newStaticPredicater(gas uint64, err error) *testPredicater {
return &testPredicater{
predicateGasF: func(predicate.Predicate, precompileconfig.Rules) (uint64, error) {
return gas, err
},
}
}

func decodeUint16s(b []byte) []uint16 {
n := len(b) / 2
out := make([]uint16, n)
for i := 0; i < n; i++ {
out[i] = binary.LittleEndian.Uint16(b[i*2:])
}
return out
}

func hashRepeat(n int) []common.Hash {
keys := make([]common.Hash, n)
for i := range keys {
keys[i][0] = byte(i + 1)
}
return keys
}
Loading
Loading