go-ethereum/params/config.libevm_test.go
Arran Schlosberg 1bccf4f2dd
refactor!: temporary extras require proof of global lock (#238)
## Why this should be merged

The `temporary.WithTempRegisteredExtras()` global lock introduced in
#234 wasn't fit for purpose when used in `coreth` as it required central
coordination of registration, types, and usage of the payload accessor.

## How this works

Instead of a central registration point, the new
`libevm.WithTemporaryExtrasLock()` function takes out a global lock and
provides the caller with a handle that proves the lock is held. All of
the override functions, e.g. `params.WithTempRegisteredExtras()` now
require a current lock, which will be propagated by the respective
`coreth` functions.

See https://github.com/ava-labs/coreth/pull/1328 for intended usage in
`coreth` and `subnet-evm`. A consumer of both of these can then safely
do the following:

```go
import (
    "github.com/ava-labs/libevm/libevm"

    coreth "github.com/ava-labs/coreth/plugin/evm"
    subnet "github.com/ava-labs/subnet-evm/plugin/evm"
)

// asCChain calls `fn` while emulating `coreth`. It is safe for concurrent usage with [asSubnetEVM].
func asCChain(fn func() error) error {
    return libevm.WithTemporaryExtrasLock(func(l libevm.ExtrasLock) error {
        return coreth.WithTempRegisteredLibEVMExtras(l, fn)
    })
}

// asSubnetEVM calls `fn` while emulating `subnet-evm`. It is safe for concurrent usage with [asCChain].
func asSubnetEVM(fn func() error) error {
    return libevm.WithTemporaryExtrasLock(func(l libevm.ExtrasLock) error {
        return subnet.WithTempRegisteredLibEVMExtras(l, fn)
    })
}
```

## How this was tested

Unit test of the new function plus existing integration tests of all
modified code.
2025-10-16 14:27:15 +00:00

370 lines
9.2 KiB
Go

// Copyright 2024-2025 the libevm authors.
//
// The libevm additions to go-ethereum are free software: you can redistribute
// them and/or modify them under the terms of the GNU Lesser General Public License
// as published by the Free Software Foundation, either version 3 of the License,
// or (at your option) any later version.
//
// The libevm additions are distributed in the hope that they will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
// General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see
// <http://www.gnu.org/licenses/>.
package params
import (
"encoding/json"
"math/big"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ava-labs/libevm/libevm"
"github.com/ava-labs/libevm/libevm/pseudo"
"github.com/ava-labs/libevm/libevm/register"
)
type rawJSON struct {
json.RawMessage
NOOPHooks
}
var _ interface {
json.Marshaler
json.Unmarshaler
} = (*rawJSON)(nil)
func TestRegisterExtras(t *testing.T) {
type (
ccExtraA struct {
A string `json:"a"`
ChainConfigHooks
}
rulesExtraA struct {
A string
RulesHooks
}
ccExtraB struct {
B string `json:"b"`
ChainConfigHooks
}
rulesExtraB struct {
B string
RulesHooks
}
)
tests := []struct {
name string
register func()
ccExtra *pseudo.Type
wantRulesExtra any
}{
{
name: "Rules payload copied from ChainConfig payload",
register: func() {
RegisterExtras(Extras[ccExtraA, rulesExtraA]{
NewRules: func(cc *ChainConfig, r *Rules, ex ccExtraA, _ *big.Int, _ bool, _ uint64) rulesExtraA {
return rulesExtraA{
A: ex.A,
}
},
})
},
ccExtra: pseudo.From(ccExtraA{
A: "hello",
}).Type,
wantRulesExtra: rulesExtraA{
A: "hello",
},
},
{
name: "no NewForRules() function results in zero value",
register: func() {
RegisterExtras(Extras[ccExtraB, rulesExtraB]{})
},
ccExtra: pseudo.From(ccExtraB{
B: "world",
}).Type,
wantRulesExtra: rulesExtraB{},
},
{
name: "no NewForRules() function results in nil pointer",
register: func() {
RegisterExtras(Extras[ccExtraB, *rulesExtraB]{})
},
ccExtra: pseudo.From(ccExtraB{
B: "world",
}).Type,
wantRulesExtra: (*rulesExtraB)(nil),
},
{
name: "custom JSON handling honoured",
register: func() {
RegisterExtras(Extras[rawJSON, struct{ RulesHooks }]{})
},
ccExtra: pseudo.From(rawJSON{
RawMessage: []byte(`"hello, world"`),
}).Type,
wantRulesExtra: struct{ RulesHooks }{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
TestOnlyClearRegisteredExtras()
tt.register()
defer TestOnlyClearRegisteredExtras()
input := &ChainConfig{
ChainID: big.NewInt(142857),
extra: tt.ccExtra,
}
buf, err := json.Marshal(input)
require.NoError(t, err)
got := new(ChainConfig)
require.NoError(t, json.Unmarshal(buf, got))
assert.Equal(t, tt.ccExtra.Interface(), got.extraPayload().Interface())
assert.Equal(t, input, got)
gotRules := got.Rules(nil, false, 0)
assert.Equal(t, tt.wantRulesExtra, gotRules.extraPayload().Interface())
})
}
}
func TestModificationOfZeroExtras(t *testing.T) {
type (
ccExtra struct {
X int
NOOPHooks
}
rulesExtra struct {
X int
NOOPHooks
}
)
TestOnlyClearRegisteredExtras()
t.Cleanup(TestOnlyClearRegisteredExtras)
extras := RegisterExtras(Extras[ccExtra, rulesExtra]{})
config := new(ChainConfig)
rules := new(Rules)
// These assertion helpers are defined before any modifications so that the
// closure is demonstrably over the original zero values.
assertChainConfigExtra := func(t *testing.T, want ccExtra, msg string) {
t.Helper()
assert.Equalf(t, want, extras.ChainConfig.Get(config), "%T: "+msg, &config)
}
assertRulesExtra := func(t *testing.T, want rulesExtra, msg string) {
t.Helper()
assert.Equalf(t, want, extras.Rules.Get(rules), "%T: "+msg, &rules)
}
assertChainConfigExtra(t, ccExtra{}, "zero value")
assertRulesExtra(t, rulesExtra{}, "zero value")
const answer = 42
extras.ChainConfig.GetPointer(config).X = answer
assertChainConfigExtra(t, ccExtra{X: answer}, "after setting via pointer field")
const pi = 314159
extras.Rules.GetPointer(rules).X = pi
assertRulesExtra(t, rulesExtra{X: pi}, "after setting via pointer field")
ccReplace := ccExtra{X: 142857}
extras.ChainConfig.Set(config, ccReplace)
assertChainConfigExtra(t, ccReplace, "after replacement of entire extra via `*pointer = x`")
rulesReplace := rulesExtra{X: 18101986}
extras.Rules.Set(rules, rulesReplace)
assertRulesExtra(t, rulesReplace, "after replacement of entire extra via `*pointer = x`")
if t.Failed() {
// The test of shallow copying is now guaranteed to fail.
return
}
t.Run("copy", func(t *testing.T) {
const (
// Arbitrary test values.
seqUp = 123456789
seqDown = 987654321
)
ccCopy := *config
t.Run("ChainConfig", func(t *testing.T) {
assert.Equal(t, extras.ChainConfig.Get(&ccCopy), ccReplace, "extras copied")
extras.ChainConfig.GetPointer(&ccCopy).X = seqUp
assertChainConfigExtra(t, ccExtra{X: seqUp}, "original changed via copied.PointerFromChainConfig because copy only shallow")
ccReplace = ccExtra{X: seqDown}
extras.ChainConfig.Set(&ccCopy, ccReplace)
assert.Equal(t, extras.ChainConfig.Get(&ccCopy), ccReplace, "SetOnChainConfig effect")
assertChainConfigExtra(t, ccExtra{X: seqUp}, "original unchanged after copied.SetOnChainConfig")
})
rCopy := *rules
t.Run("Rules", func(t *testing.T) {
assert.Equal(t, extras.Rules.Get(&rCopy), rulesReplace, "extras copied")
extras.Rules.GetPointer(&rCopy).X = seqUp
assertRulesExtra(t, rulesExtra{X: seqUp}, "original changed via copied.PointerFromRuels because copy only shallow")
rulesReplace = rulesExtra{X: seqDown}
extras.Rules.Set(&rCopy, rulesReplace)
assert.Equal(t, extras.Rules.Get(&rCopy), rulesReplace, "SetOnRules effect")
assertRulesExtra(t, rulesExtra{X: seqUp}, "original unchanged after copied.SetOnRules")
})
})
}
func TestExtrasPanic(t *testing.T) {
TestOnlyClearRegisteredExtras()
defer TestOnlyClearRegisteredExtras()
assertPanics(
t, func() {
new(ChainConfig).extraPayload()
},
"before RegisterExtras",
)
assertPanics(
t, func() {
new(Rules).extraPayload()
},
"before RegisterExtras",
)
assertPanics(
t, func() {
mustBeStructOrPointerToOne[int]()
},
notStructMessage[int](),
)
RegisterExtras(Extras[struct{ ChainConfigHooks }, struct{ RulesHooks }]{})
assertPanics(
t, func() {
RegisterExtras(Extras[struct{ ChainConfigHooks }, struct{ RulesHooks }]{})
},
register.ErrReRegistration.Error(),
)
}
func assertPanics(t *testing.T, fn func(), wantContains string) {
t.Helper()
defer func() {
t.Helper()
switch r := recover().(type) {
case nil:
t.Error("function did not panic when panic expected")
case string:
assert.Contains(t, r, wantContains)
case error:
assert.Contains(t, r.Error(), wantContains)
default:
t.Fatalf("BAD TEST SETUP: recover() got unsupported type %T", r)
}
}()
fn()
}
func TestTempRegisteredExtras(t *testing.T) {
TestOnlyClearRegisteredExtras()
t.Cleanup(TestOnlyClearRegisteredExtras)
type (
primaryCC struct {
X int
NOOPHooks
}
primaryRules struct {
X int
NOOPHooks
}
overrideCC struct {
X string
NOOPHooks
}
overrideRules struct {
X string
NOOPHooks
}
)
primary := Extras[primaryCC, primaryRules]{
NewRules: func(_ *ChainConfig, _ *Rules, cc primaryCC, _ *big.Int, _ bool, _ uint64) primaryRules {
return primaryRules{
X: cc.X,
}
},
}
override := Extras[overrideCC, overrideRules]{
NewRules: func(_ *ChainConfig, _ *Rules, cc overrideCC, _ *big.Int, _ bool, _ uint64) overrideRules {
return overrideRules{
X: cc.X,
}
},
}
extras := RegisterExtras(primary)
testPrimaryExtras := func(t *testing.T) {
t.Helper()
assertRulesCopiedFromChainConfig(
t, extras, 42,
func(cc *primaryCC, x int) { cc.X = x },
func(r *primaryRules) int { return r.X },
)
}
t.Run("before_temp", testPrimaryExtras)
t.Run("WithTempRegisteredExtras", func(t *testing.T) {
err := libevm.WithTemporaryExtrasLock(func(lock libevm.ExtrasLock) error {
return WithTempRegisteredExtras(
lock, override,
func(extras ExtraPayloads[overrideCC, overrideRules]) error { // deliberately shadow `extras`
assertRulesCopiedFromChainConfig(
t, extras, "hello, world",
func(cc *overrideCC, x string) { cc.X = x },
func(r *overrideRules) string { return r.X },
)
return nil
},
)
})
require.NoError(t, err)
})
t.Run("after_temp", testPrimaryExtras)
}
func assertRulesCopiedFromChainConfig[C ChainConfigHooks, R RulesHooks, Payload any](
t *testing.T,
extras ExtraPayloads[C, R],
val Payload,
setX func(*C, Payload),
getX func(*R) Payload,
) {
t.Helper()
cc := new(ChainConfig)
var ccExtra C
setX(&ccExtra, val)
extras.ChainConfig.Set(cc, ccExtra)
rules := cc.Rules(nil, false, 0)
rulesExtra := extras.Rules.Get(&rules)
assert.Equalf(t, val, getX(&rulesExtra), "%T.X copied from %T.X", rulesExtra, ccExtra)
}