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) +}