Skip to content
13 changes: 13 additions & 0 deletions core/state/statedb.libevm.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
//
Expand Down
16 changes: 16 additions & 0 deletions core/state/statedb.libevm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion core/types/block.libevm.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{} }

Expand Down
80 changes: 59 additions & 21 deletions core/types/rlp_payload.libevm.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand All @@ -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)
Expand All @@ -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
Expand Down
76 changes: 76 additions & 0 deletions core/types/tempextras.libevm_test.go
Original file line number Diff line number Diff line change
@@ -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
// <http://www.gnu.org/licenses/>.

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)
}
20 changes: 17 additions & 3 deletions core/vm/evm.libevm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
13 changes: 13 additions & 0 deletions core/vm/hooks.libevm.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
27 changes: 26 additions & 1 deletion libevm/register/register.go
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
}
Loading
Loading