diff --git a/core/state/statedb.libevm.go b/core/state/statedb.libevm.go
index 57346c736d9..5b9d7d10420 100644
--- a/core/state/statedb.libevm.go
+++ b/core/state/statedb.libevm.go
@@ -82,6 +82,19 @@ func RegisterExtras(s StateDBHooks) {
registeredExtras.MustRegister(s)
}
+// WithTempRegisteredExtras temporarily registers `s` as if calling
+// [RegisterExtras] the same type parameter. After `fn` returns, the
+// registration is returned to its former state, be that none or the types
+// originally passed to [RegisterExtras].
+//
+// This MUST NOT be used on a live chain. It is solely intended for off-chain
+// consumers that require access to extras. Said consumers SHOULD NOT, however
+// call this function directly. Use the libevm/temporary.WithRegisteredExtras()
+// function instead as it atomically overrides all possible packages.
+func WithTempRegisteredExtras(s StateDBHooks, fn func()) {
+ registeredExtras.TempOverride(s, fn)
+}
+
// TestOnlyClearRegisteredExtras clears the arguments previously passed to
// [RegisterExtras]. It panics if called from a non-testing call stack.
//
diff --git a/core/state/statedb.libevm_test.go b/core/state/statedb.libevm_test.go
index 5491646974b..5cb3c8d5acb 100644
--- a/core/state/statedb.libevm_test.go
+++ b/core/state/statedb.libevm_test.go
@@ -150,6 +150,12 @@ func (highByteFlipper) TransformStateKey(_ common.Address, key common.Hash) comm
return flipHighByte(key)
}
+type noopHooks struct{}
+
+func (noopHooks) TransformStateKey(_ common.Address, key common.Hash) common.Hash {
+ return key
+}
+
func TestTransformStateKey(t *testing.T) {
rawdb := rawdb.NewMemoryDatabase()
trie := triedb.NewDatabase(rawdb, nil)
@@ -209,6 +215,16 @@ func TestTransformStateKey(t *testing.T) {
assertCommittedEq(t, flippedKey, regularVal)
assertCommittedEq(t, flippedKey, flippedVal, noTransform)
+ t.Run("WithTempRegisteredExtras", func(t *testing.T) {
+ WithTempRegisteredExtras(noopHooks{}, func() {
+ // No-op hooks are equivalent to using the `noTransform` option.
+ // NOTE this is NOT the intended usage of [WithTempRegisteredExtras]
+ // and is simply an easy way to test the temporary registration.
+ assertEq(t, regularKey, regularVal)
+ assertEq(t, flippedKey, flippedVal)
+ })
+ })
+
updatedVal := common.Hash{'u', 'p', 'd', 'a', 't', 'e', 'd'}
sdb.SetState(addr, regularKey, updatedVal)
assertEq(t, regularKey, updatedVal)
diff --git a/core/types/block.libevm.go b/core/types/block.libevm.go
index a512118dd41..4b83dac8c25 100644
--- a/core/types/block.libevm.go
+++ b/core/types/block.libevm.go
@@ -130,7 +130,7 @@ type BlockBodyHooks interface {
// to no type having been registered.
type NOOPBlockBodyHooks struct{}
-var _ BlockBodyPayload[*NOOPBlockBodyHooks] = NOOPBlockBodyHooks{}
+var _ BlockBodyPayload[*NOOPBlockBodyHooks] = (*NOOPBlockBodyHooks)(nil)
func (NOOPBlockBodyHooks) Copy() *NOOPBlockBodyHooks { return &NOOPBlockBodyHooks{} }
diff --git a/core/types/rlp_payload.libevm.go b/core/types/rlp_payload.libevm.go
index 3225136dd90..82f3da30a81 100644
--- a/core/types/rlp_payload.libevm.go
+++ b/core/types/rlp_payload.libevm.go
@@ -44,17 +44,27 @@ import (
// [Header] or [Block] / [Body] is a non-nil `HPtr` or `BPtr` respectively. The
// latter guarantee ensures that hooks won't be called on nil-pointer receivers.
func RegisterExtras[
- H any, HPtr interface {
- HeaderHooks
- *H
- },
- B any, BPtr interface {
- BlockBodyPayload[BPtr]
- *B
- },
+ H any, HPtr HeaderHooksPointer[H],
+ B any, BPtr BlockBodyHooksPointer[B, BPtr],
SA any,
]() ExtraPayloads[HPtr, BPtr, SA] {
- extra := ExtraPayloads[HPtr, BPtr, SA]{
+ payloads, ctors := payloadsAndConstructors[H, HPtr, B, BPtr, SA]()
+ registeredExtras.MustRegister(ctors)
+ log.Info(
+ "Registered core/types extras",
+ "Header", log.TypeOf(pseudo.Zero[HPtr]().Value.Get()),
+ "Block/Body", log.TypeOf(pseudo.Zero[BPtr]().Value.Get()),
+ "StateAccount", log.TypeOf(pseudo.Zero[SA]().Value.Get()),
+ )
+ return payloads
+}
+
+func payloadsAndConstructors[
+ H any, HPtr HeaderHooksPointer[H],
+ B any, BPtr BlockBodyHooksPointer[B, BPtr],
+ SA any,
+]() (ExtraPayloads[HPtr, BPtr, SA], *extraConstructors) {
+ payloads := ExtraPayloads[HPtr, BPtr, SA]{
Header: pseudo.NewAccessor[*Header, HPtr](
(*Header).extraPayload,
func(h *Header, t *pseudo.Type) { h.extra = t },
@@ -72,7 +82,7 @@ func RegisterExtras[
func(a StateOrSlimAccount, t *pseudo.Type) { a.extra().t = t },
),
}
- registeredExtras.MustRegister(&extraConstructors{
+ ctors := &extraConstructors{
stateAccountType: func() string {
var x SA
return fmt.Sprintf("%T", x)
@@ -84,23 +94,51 @@ func RegisterExtras[
newHeader: pseudo.NewConstructor[H]().NewPointer, // i.e. non-nil HPtr
newBlockOrBody: pseudo.NewConstructor[B]().NewPointer, // i.e. non-nil BPtr
newStateAccount: pseudo.NewConstructor[SA]().Zero,
- hooks: extra,
- })
- log.Info(
- "Registered core/types extras",
- "Header", log.TypeOf(pseudo.Zero[HPtr]().Value.Get()),
- "Block/Body", log.TypeOf(pseudo.Zero[BPtr]().Value.Get()),
- "StateAccount", log.TypeOf(pseudo.Zero[SA]().Value.Get()),
- )
- return extra
+ hooks: payloads,
+ }
+ return payloads, ctors
+}
+
+// WithTempRegisteredExtras temporarily registers `HPtr`, `BPtr`, and `SA` as if
+// calling [RegisterExtras] the same type parameters. The [ExtraPayloads] are
+// passed to `fn` instead of being returned; the argument MUST NOT be persisted
+// beyond the life of `fn`. After `fn` returns, the registration is returned to
+// its former state, be that none or the types originally passed to
+// [RegisterExtras].
+//
+// This MUST NOT be used on a live chain. It is solely intended for off-chain
+// consumers that require access to extras. Said consumers SHOULD NOT, however
+// call this function directly. Use the libevm/temporary.WithRegisteredExtras()
+// function instead as it atomically overrides all possible packages.
+func WithTempRegisteredExtras[
+ H, B, SA any,
+ HPtr HeaderHooksPointer[H],
+ BPtr BlockBodyHooksPointer[B, BPtr],
+](fn func(ExtraPayloads[HPtr, BPtr, SA])) {
+ payloads, ctors := payloadsAndConstructors[H, HPtr, B, BPtr, SA]()
+ registeredExtras.TempOverride(ctors, func() { fn(payloads) })
+}
+
+// A HeaderHooksPointer is a type constraint for an implementation of
+// [HeaderHooks] with a pointer receiver.
+type HeaderHooksPointer[H any] interface {
+ HeaderHooks
+ *H
+}
+
+// A BlockBodyHooksPointer is a type constraint for an implementation of
+// [BlockBodyPayload] with a pointer receiver.
+type BlockBodyHooksPointer[B any, Self any] interface {
+ BlockBodyPayload[Self]
+ *B
}
// A BlockBodyPayload is an implementation of [BlockBodyHooks] that is also able
// to clone itself. Both [Block.Body] and [Block.WithBody] require this
// functionality to copy the payload between the types.
-type BlockBodyPayload[BPtr any] interface {
+type BlockBodyPayload[Self any] interface {
BlockBodyHooks
- Copy() BPtr
+ Copy() Self
}
// TestOnlyClearRegisteredExtras clears the [Extras] previously passed to
diff --git a/core/types/tempextras.libevm_test.go b/core/types/tempextras.libevm_test.go
new file mode 100644
index 00000000000..eb5bfb4bdf6
--- /dev/null
+++ b/core/types/tempextras.libevm_test.go
@@ -0,0 +1,76 @@
+// Copyright 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
+// .
+
+package types
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/ava-labs/libevm/rlp"
+)
+
+type tempBlockBodyHooks struct {
+ X string
+ NOOPBlockBodyHooks
+}
+
+func (b *tempBlockBodyHooks) Copy() *tempBlockBodyHooks {
+ return &tempBlockBodyHooks{X: b.X}
+}
+
+func (b *tempBlockBodyHooks) BlockRLPFieldsForEncoding(*BlockRLPProxy) *rlp.Fields {
+ return &rlp.Fields{
+ Required: []any{b.X},
+ }
+}
+
+func TestTempRegisteredExtras(t *testing.T) {
+ TestOnlyClearRegisteredExtras()
+ t.Cleanup(TestOnlyClearRegisteredExtras)
+
+ rlpWithoutHooks, err := rlp.EncodeToBytes(&Block{})
+ require.NoErrorf(t, err, "rlp.EncodeToBytes(%T) without hooks", &Block{})
+
+ extras := RegisterExtras[NOOPHeaderHooks, *NOOPHeaderHooks, NOOPBlockBodyHooks, *NOOPBlockBodyHooks, bool]()
+ testPrimaryExtras := func(t *testing.T) {
+ t.Helper()
+ b := new(Block)
+ got, err := rlp.EncodeToBytes(b)
+ require.NoErrorf(t, err, "rlp.EncodeToBytes(%T) with %T hooks", b, extras.Block.Get(b))
+ assert.Equalf(t, rlpWithoutHooks, got, "rlp.EncodeToBytes(%T) with noop hooks; expect same as without hooks", b)
+ }
+
+ t.Run("before_temp", testPrimaryExtras)
+ t.Run("WithTempRegisteredExtras", func(t *testing.T) {
+ WithTempRegisteredExtras(func(extras ExtraPayloads[*NOOPHeaderHooks, *tempBlockBodyHooks, bool]) {
+ const val = "Hello, world"
+ b := new(Block)
+ payload := &tempBlockBodyHooks{X: val}
+ extras.Block.Set(b, payload)
+
+ got, err := rlp.EncodeToBytes(b)
+ require.NoErrorf(t, err, "rlp.EncodeToBytes(%T) with %T hooks", b, extras.Block.Get(b))
+ want, err := rlp.EncodeToBytes([]string{val})
+ require.NoErrorf(t, err, "rlp.EncodeToBytes(%T{%[1]v})", []string{val})
+
+ assert.Equalf(t, want, got, "rlp.EncodeToBytes(%T) with %T hooks", b, payload)
+ })
+ })
+ t.Run("after_temp", testPrimaryExtras)
+}
diff --git a/core/vm/evm.libevm_test.go b/core/vm/evm.libevm_test.go
index c0a33718e3d..cc98d4bc1ac 100644
--- a/core/vm/evm.libevm_test.go
+++ b/core/vm/evm.libevm_test.go
@@ -63,9 +63,23 @@ func TestOverrideNewEVMArgs(t *testing.T) {
hooks := evmArgOverrider{newEVMchainID: chainID}
hooks.register(t)
- evm := NewEVM(BlockContext{}, TxContext{}, nil, nil, Config{})
- got := evm.ChainConfig().ChainID
- require.Equalf(t, big.NewInt(chainID), got, "%T.ChainConfig().ChainID set by NewEVM() hook", evm)
+ assertChainID := func(t *testing.T, want int64) {
+ t.Helper()
+ evm := NewEVM(BlockContext{}, TxContext{}, nil, nil, Config{})
+ got := evm.ChainConfig().ChainID
+ require.Equalf(t, big.NewInt(want), got, "%T.ChainConfig().ChainID set by NewEVM() hook", evm)
+ }
+ assertChainID(t, chainID)
+
+ t.Run("WithTempRegisteredHooks", func(t *testing.T) {
+ override := evmArgOverrider{newEVMchainID: 24680}
+ WithTempRegisteredHooks(&override, func() {
+ assertChainID(t, override.newEVMchainID)
+ })
+ t.Run("after", func(t *testing.T) {
+ assertChainID(t, chainID)
+ })
+ })
}
func TestOverrideEVMResetArgs(t *testing.T) {
diff --git a/core/vm/hooks.libevm.go b/core/vm/hooks.libevm.go
index 1e5acd49db9..16e49f3bdcb 100644
--- a/core/vm/hooks.libevm.go
+++ b/core/vm/hooks.libevm.go
@@ -27,6 +27,19 @@ func RegisterHooks(h Hooks) {
libevmHooks.MustRegister(h)
}
+// WithTempRegisteredHooks temporarily registers `h` as if calling
+// [RegisterHooks] the same type parameter. After `fn` returns, the registration
+// is returned to its former state, be that none or the types originally passed
+// to [RegisterHooks].
+//
+// This MUST NOT be used on a live chain. It is solely intended for off-chain
+// consumers that require access to extras. Said consumers SHOULD NOT, however
+// call this function directly. Use the libevm/temporary.WithRegisteredExtras()
+// function instead as it atomically overrides all possible packages.
+func WithTempRegisteredHooks(h Hooks, fn func()) {
+ libevmHooks.TempOverride(h, fn)
+}
+
// TestOnlyClearRegisteredHooks clears the [Hooks] previously passed to
// [RegisterHooks]. It panics if called from a non-testing call stack.
func TestOnlyClearRegisteredHooks() {
diff --git a/libevm/register/register.go b/libevm/register/register.go
index 0cf3333d413..b8bb246e86f 100644
--- a/libevm/register/register.go
+++ b/libevm/register/register.go
@@ -1,4 +1,4 @@
-// Copyright 2024 the libevm authors.
+// 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
@@ -66,3 +66,28 @@ func (o *AtMostOnce[T]) TestOnlyClear() {
o.v = nil
})
}
+
+// TempOverride calls `fn`, overriding any registered `T`, but only for the life
+// of the call. It is not threadsafe.
+//
+// It is valid to call this method with or without a prior call to
+// [AtMostOnce.Register].
+func (o *AtMostOnce[T]) TempOverride(with T, fn func()) {
+ o.temp(&with, fn)
+}
+
+// TempClear calls `fn`, clearing any registered `T`, but only for the life of
+// the call. It is not threadsafe.
+//
+// It is valid to call this method with or without a prior call to
+// [AtMostOnce.Register].
+func (o *AtMostOnce[T]) TempClear(fn func()) {
+ o.temp(nil, fn)
+}
+
+func (o *AtMostOnce[T]) temp(with *T, fn func()) {
+ old := o.v
+ o.v = with
+ fn()
+ o.v = old
+}
diff --git a/libevm/register/register_test.go b/libevm/register/register_test.go
new file mode 100644
index 00000000000..fa8e1f70714
--- /dev/null
+++ b/libevm/register/register_test.go
@@ -0,0 +1,78 @@
+// Copyright 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
+// .
+
+package register
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestAtMostOnce(t *testing.T) {
+ var sut AtMostOnce[int]
+ assertRegistered := func(t *testing.T, want int) {
+ t.Helper()
+ require.True(t, sut.Registered(), "Registered()")
+ assert.Equal(t, want, sut.Get(), "Get()")
+ }
+
+ const val int = 42
+ require.NoError(t, sut.Register(val), "Register()")
+ assertRegistered(t, val)
+
+ assert.PanicsWithValue(
+ t, ErrReRegistration,
+ func() { sut.MustRegister(0) },
+ "MustRegister() after Register()",
+ )
+
+ t.Run("TestOnlyClear", func(t *testing.T) {
+ sut.TestOnlyClear()
+ require.False(t, sut.Registered(), "Registered()")
+
+ t.Run("re-registration", func(t *testing.T) {
+ sut.MustRegister(val)
+ assertRegistered(t, val)
+ })
+ })
+ if t.Failed() {
+ return
+ }
+
+ t.Run("TempOverride", func(t *testing.T) {
+ t.Run("during", func(t *testing.T) {
+ sut.TempOverride(val+1, func() {
+ assertRegistered(t, val+1)
+ })
+ })
+ t.Run("after", func(t *testing.T) {
+ assertRegistered(t, val)
+ })
+ })
+
+ t.Run("TempClear", func(t *testing.T) {
+ t.Run("during", func(t *testing.T) {
+ sut.TempClear(func() {
+ assert.False(t, sut.Registered(), "Registered()")
+ })
+ })
+ t.Run("after", func(t *testing.T) {
+ assertRegistered(t, val)
+ })
+ })
+}
diff --git a/libevm/temporary/temporary.go b/libevm/temporary/temporary.go
new file mode 100644
index 00000000000..dae3a5b772f
--- /dev/null
+++ b/libevm/temporary/temporary.go
@@ -0,0 +1,65 @@
+// Copyright 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
+// .
+
+// Package temporary provides thread-safe, temporary registration of all libevm
+// hooks and payloads.
+package temporary
+
+import (
+ "sync"
+
+ "github.com/ava-labs/libevm/core/state"
+ "github.com/ava-labs/libevm/core/types"
+ "github.com/ava-labs/libevm/core/vm"
+ "github.com/ava-labs/libevm/params"
+)
+
+var mu sync.Mutex
+
+// WithRegisteredExtras takes a global lock and temporarily registers [params],
+// [state], [types], and [vm] extras before calling the provided function. It
+// can be thought of as an atomic call to all functions equivalent to
+// [params.WithTempRegisteredExtras].
+//
+// This is the *only* safe way to override libevm functionality. Direct calls to
+// the package-specific temporary registration functions are not advised.
+//
+// WithRegisteredExtras MUST NOT be used on a live chain. It is solely intended
+// for off-chain consumers that require access to extras.
+func WithRegisteredExtras[
+ C params.ChainConfigHooks, R params.RulesHooks,
+ H, B, SA any,
+ HPtr types.HeaderHooksPointer[H],
+ BPtr types.BlockBodyHooksPointer[B, BPtr],
+](
+ paramsExtras params.Extras[C, R],
+ sdbHooks state.StateDBHooks,
+ vmHooks vm.Hooks,
+ fn func(params.ExtraPayloads[C, R], types.ExtraPayloads[HPtr, BPtr, SA]),
+) {
+ mu.Lock()
+ defer mu.Unlock()
+
+ params.WithTempRegisteredExtras(paramsExtras, func(paramsPayloads params.ExtraPayloads[C, R]) {
+ types.WithTempRegisteredExtras(func(typesPayloads types.ExtraPayloads[HPtr, BPtr, SA]) {
+ state.WithTempRegisteredExtras(sdbHooks, func() {
+ vm.WithTempRegisteredHooks(vmHooks, func() {
+ fn(paramsPayloads, typesPayloads)
+ })
+ })
+ })
+ })
+}
diff --git a/params/config.libevm.go b/params/config.libevm.go
index 4abfdda16e1..7d05893bb1b 100644
--- a/params/config.libevm.go
+++ b/params/config.libevm.go
@@ -72,14 +72,8 @@ func RegisterExtras[C ChainConfigHooks, R RulesHooks](e Extras[C, R]) ExtraPaylo
mustBeStructOrPointerToOne[C]()
mustBeStructOrPointerToOne[R]()
- payloads := e.payloads()
- registeredExtras.MustRegister(&extraConstructors{
- newChainConfig: pseudo.NewConstructor[C]().Zero,
- newRules: pseudo.NewConstructor[R]().Zero,
- reuseJSONRoot: e.ReuseJSONRoot,
- newForRules: e.newForRules,
- payloads: payloads,
- })
+ payloads, ctors := payloadsAndConstructors(e)
+ registeredExtras.MustRegister(ctors)
log.Info(
"Registered params extras",
"ChainConfig", log.TypeOf(pseudo.Zero[C]().Value.Get()),
@@ -89,6 +83,36 @@ func RegisterExtras[C ChainConfigHooks, R RulesHooks](e Extras[C, R]) ExtraPaylo
return payloads
}
+func payloadsAndConstructors[C ChainConfigHooks, R RulesHooks](e Extras[C, R]) (ExtraPayloads[C, R], *extraConstructors) {
+ payloads := e.payloads()
+ return payloads, &extraConstructors{
+ newChainConfig: pseudo.NewConstructor[C]().Zero,
+ newRules: pseudo.NewConstructor[R]().Zero,
+ reuseJSONRoot: e.ReuseJSONRoot,
+ newForRules: e.newForRules,
+ payloads: payloads,
+ }
+}
+
+// WithTempRegisteredExtras temporarily registers `HPtr`, `BPtr`, and `SA` as if
+// calling [RegisterExtras] the same type parameters. The [ExtraPayloads] are
+// passed to `fn` instead of being returned; the argument MUST NOT be persisted
+// beyond the life of `fn`. After `fn` returns, the registration is returned to
+// its former state, be that none or the types originally passed to
+// [RegisterExtras].
+//
+// This MUST NOT be used on a live chain. It is solely intended for off-chain
+// consumers that require access to extras. Said consumers SHOULD NOT, however
+// call this function directly. Use the libevm/temporary.WithRegisteredExtras()
+// function instead as it atomically overrides all possible packages.
+func WithTempRegisteredExtras[C ChainConfigHooks, R RulesHooks](
+ e Extras[C, R],
+ fn func(ExtraPayloads[C, R]),
+) {
+ payloads, ctors := payloadsAndConstructors(e)
+ registeredExtras.TempOverride(ctors, func() { fn(payloads) })
+}
+
// TestOnlyClearRegisteredExtras clears the [Extras] previously passed to
// [RegisterExtras]. It panics if called from a non-testing call stack.
//
diff --git a/params/config.libevm_test.go b/params/config.libevm_test.go
index 3464df4fbdf..da6f04ce4ef 100644
--- a/params/config.libevm_test.go
+++ b/params/config.libevm_test.go
@@ -277,3 +277,88 @@ func assertPanics(t *testing.T, fn func(), wantContains string) {
}()
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) {
+ WithTempRegisteredExtras(
+ override,
+ func(extras ExtraPayloads[overrideCC, overrideRules]) { // deliberately shadow `extras`
+ assertRulesCopiedFromChainConfig(
+ t, extras, "hello, world",
+ func(cc *overrideCC, x string) { cc.X = x },
+ func(r *overrideRules) string { return r.X },
+ )
+ },
+ )
+ })
+ 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)
+}