From 7841149c57b30ea0282cad6c46b9ab57ba433270 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Sat, 27 Sep 2025 11:39:34 +0100 Subject: [PATCH 01/10] feat: `register.AtMostOnce.Temp{Override,Clear}()` methods --- libevm/register/register.go | 27 ++++++++++- libevm/register/register_test.go | 78 ++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 libevm/register/register_test.go diff --git a/libevm/register/register.go b/libevm/register/register.go index 0cf3333d413..03c6c8cbdd6 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 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 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) + }) + }) +} From c54d88d04ebb662348e46ae0e8c868d7a0588be6 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Sat, 27 Sep 2025 12:18:02 +0100 Subject: [PATCH 02/10] feat: `params.WithTempRegisteredExtras()` --- params/config.libevm.go | 37 ++++++++++++---- params/config.libevm_test.go | 85 ++++++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+), 8 deletions(-) diff --git a/params/config.libevm.go b/params/config.libevm.go index 4abfdda16e1..011181c6ce0 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,33 @@ 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 `C` and `R` as if calling +// [RegisterExtras] with `e`. The [ExtraPayloads] are passed to `fn` instead of +// being returned. 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 in a live chain. It is solely intended for off-chain +// consumers that require access to extras. +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) +} From 3cf1438508c21aa0adce8fae0dc3e22728cefd0b Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Sat, 27 Sep 2025 20:15:50 +0100 Subject: [PATCH 03/10] feat: `types.WithTempRegisteredExtras()` --- core/types/block.libevm.go | 2 +- core/types/rlp_payload.libevm.go | 73 ++++++++++++++++++++-------- core/types/tempextras.libevm_test.go | 61 +++++++++++++++++++++++ 3 files changed, 116 insertions(+), 20 deletions(-) create mode 100644 core/types/tempextras.libevm_test.go 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..341dd618585 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,15 +94,40 @@ 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. 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 in a live chain. It is solely intended for off-chain +// consumers that require access to extras. +func WithTempRegisteredExtras[ + H any, B any, 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 diff --git a/core/types/tempextras.libevm_test.go b/core/types/tempextras.libevm_test.go new file mode 100644 index 00000000000..0b168a62ca4 --- /dev/null +++ b/core/types/tempextras.libevm_test.go @@ -0,0 +1,61 @@ +// 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" +) + +func TestTempRegisteredExtras(t *testing.T) { + TestOnlyClearRegisteredExtras() + t.Cleanup(TestOnlyClearRegisteredExtras) + + type ( + primary struct { + NOOPHeaderHooks + } + override struct { + NOOPHeaderHooks + } + ) + + RegisterExtras[primary, *primary, NOOPBlockBodyHooks, *NOOPBlockBodyHooks, bool]() + testPrimaryExtras := func(t *testing.T) { + t.Helper() + assertHeaderHooksConcreteType[*primary](t) + } + + t.Run("before_temp", testPrimaryExtras) + t.Run("WithTempRegisteredExtras", func(t *testing.T) { + WithTempRegisteredExtras(func(ExtraPayloads[*override, *NOOPBlockBodyHooks, bool]) { + assertHeaderHooksConcreteType[*override](t) + }) + }) + t.Run("after_temp", testPrimaryExtras) +} + +func assertHeaderHooksConcreteType[WantT any](t *testing.T) { + t.Helper() + + hdr := new(Header) + switch got := hdr.hooks().(type) { + case WantT: + default: + var want WantT + t.Errorf("%T.hooks() got concrete type %T; want %T", hdr, got, want) + } +} From 4bc33838861a8c4f7754624ebb73ffd678474d4e Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Sun, 28 Sep 2025 08:46:00 +0100 Subject: [PATCH 04/10] test: `types.WithTempRegisteredExtras()` hooks and payloads --- core/types/tempextras.libevm_test.go | 62 +++++++++++++++++----------- 1 file changed, 38 insertions(+), 24 deletions(-) diff --git a/core/types/tempextras.libevm_test.go b/core/types/tempextras.libevm_test.go index 0b168a62ca4..e6f3ea6fab0 100644 --- a/core/types/tempextras.libevm_test.go +++ b/core/types/tempextras.libevm_test.go @@ -18,44 +18,58 @@ package types import ( "testing" + + "github.com/ava-labs/libevm/rlp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) +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) - type ( - primary struct { - NOOPHeaderHooks - } - override struct { - NOOPHeaderHooks - } - ) + rlpWithoutHooks, err := rlp.EncodeToBytes(&Block{}) + require.NoErrorf(t, err, "rlp.EncodeToBytes(%T) without hooks", &Block{}) - RegisterExtras[primary, *primary, NOOPBlockBodyHooks, *NOOPBlockBodyHooks, bool]() + extras := RegisterExtras[NOOPHeaderHooks, *NOOPHeaderHooks, NOOPBlockBodyHooks, *NOOPBlockBodyHooks, bool]() testPrimaryExtras := func(t *testing.T) { t.Helper() - assertHeaderHooksConcreteType[*primary](t) + 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(ExtraPayloads[*override, *NOOPBlockBodyHooks, bool]) { - assertHeaderHooksConcreteType[*override](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) } - -func assertHeaderHooksConcreteType[WantT any](t *testing.T) { - t.Helper() - - hdr := new(Header) - switch got := hdr.hooks().(type) { - case WantT: - default: - var want WantT - t.Errorf("%T.hooks() got concrete type %T; want %T", hdr, got, want) - } -} From fb7ed1d12045156b2b94774083db410389b374e3 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Sun, 28 Sep 2025 08:50:46 +0100 Subject: [PATCH 05/10] chore: placate the linter --- core/types/tempextras.libevm_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/types/tempextras.libevm_test.go b/core/types/tempextras.libevm_test.go index e6f3ea6fab0..eb5bfb4bdf6 100644 --- a/core/types/tempextras.libevm_test.go +++ b/core/types/tempextras.libevm_test.go @@ -19,9 +19,10 @@ package types import ( "testing" - "github.com/ava-labs/libevm/rlp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/ava-labs/libevm/rlp" ) type tempBlockBodyHooks struct { From d764662e07222417b6251ef6affbd8e814e87a28 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Mon, 29 Sep 2025 09:59:33 +0100 Subject: [PATCH 06/10] doc: allowed life of `extras` arg + self-documenting type parameter --- core/types/rlp_payload.libevm.go | 11 ++++++----- params/config.libevm.go | 9 +++++---- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/core/types/rlp_payload.libevm.go b/core/types/rlp_payload.libevm.go index 341dd618585..eaf354451e6 100644 --- a/core/types/rlp_payload.libevm.go +++ b/core/types/rlp_payload.libevm.go @@ -101,9 +101,10 @@ func payloadsAndConstructors[ // 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. After `fn` returns, the -// registration is returned to its former state, be that none or the types -// originally passed to [RegisterExtras]. +// 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 in a live chain. It is solely intended for off-chain // consumers that require access to extras. @@ -133,9 +134,9 @@ type BlockBodyHooksPointer[B any, Self any] interface { // 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/params/config.libevm.go b/params/config.libevm.go index 011181c6ce0..55b57a19f95 100644 --- a/params/config.libevm.go +++ b/params/config.libevm.go @@ -94,10 +94,11 @@ func payloadsAndConstructors[C ChainConfigHooks, R RulesHooks](e Extras[C, R]) ( } } -// WithTempRegisteredExtras temporarily registers `C` and `R` as if calling -// [RegisterExtras] with `e`. The [ExtraPayloads] are passed to `fn` instead of -// being returned. After `fn` returns, the registration is returned to its -// former state, be that none or the types originally passed to +// 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 in a live chain. It is solely intended for off-chain From f35c484afd6d89b14bb0f635eef63493a4270bb5 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Wed, 1 Oct 2025 10:05:44 +0100 Subject: [PATCH 07/10] doc: lack of thread safety --- core/types/rlp_payload.libevm.go | 2 +- libevm/register/register.go | 4 ++-- params/config.libevm.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/types/rlp_payload.libevm.go b/core/types/rlp_payload.libevm.go index eaf354451e6..837e691f802 100644 --- a/core/types/rlp_payload.libevm.go +++ b/core/types/rlp_payload.libevm.go @@ -107,7 +107,7 @@ func payloadsAndConstructors[ // [RegisterExtras]. // // This MUST NOT be used in a live chain. It is solely intended for off-chain -// consumers that require access to extras. +// consumers that require access to extras, and is also not threadsafe. func WithTempRegisteredExtras[ H any, B any, SA any, HPtr HeaderHooksPointer[H], diff --git a/libevm/register/register.go b/libevm/register/register.go index 03c6c8cbdd6..b8bb246e86f 100644 --- a/libevm/register/register.go +++ b/libevm/register/register.go @@ -68,7 +68,7 @@ func (o *AtMostOnce[T]) TestOnlyClear() { } // TempOverride calls `fn`, overriding any registered `T`, but only for the life -// of the call. +// of the call. It is not threadsafe. // // It is valid to call this method with or without a prior call to // [AtMostOnce.Register]. @@ -77,7 +77,7 @@ func (o *AtMostOnce[T]) TempOverride(with T, fn func()) { } // TempClear calls `fn`, clearing any registered `T`, but only for the life of -// the call. +// the call. It is not threadsafe. // // It is valid to call this method with or without a prior call to // [AtMostOnce.Register]. diff --git a/params/config.libevm.go b/params/config.libevm.go index 55b57a19f95..9cf5db98285 100644 --- a/params/config.libevm.go +++ b/params/config.libevm.go @@ -102,7 +102,7 @@ func payloadsAndConstructors[C ChainConfigHooks, R RulesHooks](e Extras[C, R]) ( // [RegisterExtras]. // // This MUST NOT be used in a live chain. It is solely intended for off-chain -// consumers that require access to extras. +// consumers that require access to extras, and is also not threadsafe. func WithTempRegisteredExtras[C ChainConfigHooks, R RulesHooks]( e Extras[C, R], fn func(ExtraPayloads[C, R]), From 296603b00486eba3e3d0279787c8b6b3c2fc699d Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Wed, 1 Oct 2025 10:17:55 +0100 Subject: [PATCH 08/10] feat: `vm.WithTempRegisteredHooks()` --- core/vm/evm.libevm_test.go | 20 +++++++++++++++++--- core/vm/hooks.libevm.go | 11 +++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) 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..6e6c05f1924 100644 --- a/core/vm/hooks.libevm.go +++ b/core/vm/hooks.libevm.go @@ -27,6 +27,17 @@ 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 in a live chain. It is solely intended for off-chain +// consumers that require access to extras, and is also not threadsafe. +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() { From 5d22eea050ba3fe0f485612eed19367f0bc3682a Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Wed, 1 Oct 2025 10:23:26 +0100 Subject: [PATCH 09/10] feat: `state.WithTempRegisteredExtras()` --- core/state/statedb.libevm.go | 11 +++++++++++ core/state/statedb.libevm_test.go | 16 ++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/core/state/statedb.libevm.go b/core/state/statedb.libevm.go index 57346c736d9..d81f7d27eaa 100644 --- a/core/state/statedb.libevm.go +++ b/core/state/statedb.libevm.go @@ -82,6 +82,17 @@ 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 in a live chain. It is solely intended for off-chain +// consumers that require access to extras, and is also not threadsafe. +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) From 3827f63aef99aec95230267909d31d21519e514e Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Wed, 1 Oct 2025 13:36:51 +0100 Subject: [PATCH 10/10] feat: `temporary` package for thread-safe atomic overrides --- core/state/statedb.libevm.go | 6 ++- core/types/rlp_payload.libevm.go | 8 ++-- core/vm/hooks.libevm.go | 6 ++- libevm/temporary/temporary.go | 65 ++++++++++++++++++++++++++++++++ params/config.libevm.go | 6 ++- 5 files changed, 82 insertions(+), 9 deletions(-) create mode 100644 libevm/temporary/temporary.go diff --git a/core/state/statedb.libevm.go b/core/state/statedb.libevm.go index d81f7d27eaa..5b9d7d10420 100644 --- a/core/state/statedb.libevm.go +++ b/core/state/statedb.libevm.go @@ -87,8 +87,10 @@ func RegisterExtras(s StateDBHooks) { // registration is returned to its former state, be that none or the types // originally passed to [RegisterExtras]. // -// This MUST NOT be used in a live chain. It is solely intended for off-chain -// consumers that require access to extras, and is also not threadsafe. +// 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) } diff --git a/core/types/rlp_payload.libevm.go b/core/types/rlp_payload.libevm.go index 837e691f802..82f3da30a81 100644 --- a/core/types/rlp_payload.libevm.go +++ b/core/types/rlp_payload.libevm.go @@ -106,10 +106,12 @@ func payloadsAndConstructors[ // its former state, be that none or the types originally passed to // [RegisterExtras]. // -// This MUST NOT be used in a live chain. It is solely intended for off-chain -// consumers that require access to extras, and is also not threadsafe. +// 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 any, B any, SA any, + H, B, SA any, HPtr HeaderHooksPointer[H], BPtr BlockBodyHooksPointer[B, BPtr], ](fn func(ExtraPayloads[HPtr, BPtr, SA])) { diff --git a/core/vm/hooks.libevm.go b/core/vm/hooks.libevm.go index 6e6c05f1924..16e49f3bdcb 100644 --- a/core/vm/hooks.libevm.go +++ b/core/vm/hooks.libevm.go @@ -32,8 +32,10 @@ func RegisterHooks(h Hooks) { // is returned to its former state, be that none or the types originally passed // to [RegisterHooks]. // -// This MUST NOT be used in a live chain. It is solely intended for off-chain -// consumers that require access to extras, and is also not threadsafe. +// 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) } 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 9cf5db98285..7d05893bb1b 100644 --- a/params/config.libevm.go +++ b/params/config.libevm.go @@ -101,8 +101,10 @@ func payloadsAndConstructors[C ChainConfigHooks, R RulesHooks](e Extras[C, R]) ( // its former state, be that none or the types originally passed to // [RegisterExtras]. // -// This MUST NOT be used in a live chain. It is solely intended for off-chain -// consumers that require access to extras, and is also not threadsafe. +// 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]),