mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-18 19:00:46 +00:00
feat: temporary extras (#234)
## Why this should be merged Registration of extras requires runtime enforcement of payload types. In production, only a single set of types can be registered and any attempts at re-registration will panic (by design). Although this makes production usage safe, it doesn't allow downstream consumers (e.g. data indexers) to use extras from different chains (e.g. `coreth` _and_ `subnet-evm`) at the same time. ## How this works 1. The `libevm/register.AtMostOnce` type can now be overridden temporarily. 2. `params`, `core/types`, `core/vm`, and `state` packages introduce `WithTempRegisteredExtras()` functions. 3. `libevm/temporary.WithRegisteredExtras()` provides "atomic" override of all extras. In all cases, the scope of the override is limited to the life of a single function call. ## How this was tested Relative to numbered list above: 1. Unit test of new and existing functionality. 2. Integration tests of both packages, demonstrating both payload and behavioural override.
This commit is contained in:
parent
414b1f5dff
commit
35926db4d6
12 changed files with 481 additions and 34 deletions
|
|
@ -82,6 +82,19 @@ func RegisterExtras(s StateDBHooks) {
|
||||||
registeredExtras.MustRegister(s)
|
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
|
// TestOnlyClearRegisteredExtras clears the arguments previously passed to
|
||||||
// [RegisterExtras]. It panics if called from a non-testing call stack.
|
// [RegisterExtras]. It panics if called from a non-testing call stack.
|
||||||
//
|
//
|
||||||
|
|
|
||||||
|
|
@ -150,6 +150,12 @@ func (highByteFlipper) TransformStateKey(_ common.Address, key common.Hash) comm
|
||||||
return flipHighByte(key)
|
return flipHighByte(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type noopHooks struct{}
|
||||||
|
|
||||||
|
func (noopHooks) TransformStateKey(_ common.Address, key common.Hash) common.Hash {
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
func TestTransformStateKey(t *testing.T) {
|
func TestTransformStateKey(t *testing.T) {
|
||||||
rawdb := rawdb.NewMemoryDatabase()
|
rawdb := rawdb.NewMemoryDatabase()
|
||||||
trie := triedb.NewDatabase(rawdb, nil)
|
trie := triedb.NewDatabase(rawdb, nil)
|
||||||
|
|
@ -209,6 +215,16 @@ func TestTransformStateKey(t *testing.T) {
|
||||||
assertCommittedEq(t, flippedKey, regularVal)
|
assertCommittedEq(t, flippedKey, regularVal)
|
||||||
assertCommittedEq(t, flippedKey, flippedVal, noTransform)
|
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'}
|
updatedVal := common.Hash{'u', 'p', 'd', 'a', 't', 'e', 'd'}
|
||||||
sdb.SetState(addr, regularKey, updatedVal)
|
sdb.SetState(addr, regularKey, updatedVal)
|
||||||
assertEq(t, regularKey, updatedVal)
|
assertEq(t, regularKey, updatedVal)
|
||||||
|
|
|
||||||
|
|
@ -130,7 +130,7 @@ type BlockBodyHooks interface {
|
||||||
// to no type having been registered.
|
// to no type having been registered.
|
||||||
type NOOPBlockBodyHooks struct{}
|
type NOOPBlockBodyHooks struct{}
|
||||||
|
|
||||||
var _ BlockBodyPayload[*NOOPBlockBodyHooks] = NOOPBlockBodyHooks{}
|
var _ BlockBodyPayload[*NOOPBlockBodyHooks] = (*NOOPBlockBodyHooks)(nil)
|
||||||
|
|
||||||
func (NOOPBlockBodyHooks) Copy() *NOOPBlockBodyHooks { return &NOOPBlockBodyHooks{} }
|
func (NOOPBlockBodyHooks) Copy() *NOOPBlockBodyHooks { return &NOOPBlockBodyHooks{} }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,17 +44,27 @@ import (
|
||||||
// [Header] or [Block] / [Body] is a non-nil `HPtr` or `BPtr` respectively. The
|
// [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.
|
// latter guarantee ensures that hooks won't be called on nil-pointer receivers.
|
||||||
func RegisterExtras[
|
func RegisterExtras[
|
||||||
H any, HPtr interface {
|
H any, HPtr HeaderHooksPointer[H],
|
||||||
HeaderHooks
|
B any, BPtr BlockBodyHooksPointer[B, BPtr],
|
||||||
*H
|
|
||||||
},
|
|
||||||
B any, BPtr interface {
|
|
||||||
BlockBodyPayload[BPtr]
|
|
||||||
*B
|
|
||||||
},
|
|
||||||
SA any,
|
SA any,
|
||||||
]() ExtraPayloads[HPtr, BPtr, SA] {
|
]() 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: pseudo.NewAccessor[*Header, HPtr](
|
||||||
(*Header).extraPayload,
|
(*Header).extraPayload,
|
||||||
func(h *Header, t *pseudo.Type) { h.extra = t },
|
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 },
|
func(a StateOrSlimAccount, t *pseudo.Type) { a.extra().t = t },
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
registeredExtras.MustRegister(&extraConstructors{
|
ctors := &extraConstructors{
|
||||||
stateAccountType: func() string {
|
stateAccountType: func() string {
|
||||||
var x SA
|
var x SA
|
||||||
return fmt.Sprintf("%T", x)
|
return fmt.Sprintf("%T", x)
|
||||||
|
|
@ -84,23 +94,51 @@ func RegisterExtras[
|
||||||
newHeader: pseudo.NewConstructor[H]().NewPointer, // i.e. non-nil HPtr
|
newHeader: pseudo.NewConstructor[H]().NewPointer, // i.e. non-nil HPtr
|
||||||
newBlockOrBody: pseudo.NewConstructor[B]().NewPointer, // i.e. non-nil BPtr
|
newBlockOrBody: pseudo.NewConstructor[B]().NewPointer, // i.e. non-nil BPtr
|
||||||
newStateAccount: pseudo.NewConstructor[SA]().Zero,
|
newStateAccount: pseudo.NewConstructor[SA]().Zero,
|
||||||
hooks: extra,
|
hooks: payloads,
|
||||||
})
|
}
|
||||||
log.Info(
|
return payloads, ctors
|
||||||
"Registered core/types extras",
|
}
|
||||||
"Header", log.TypeOf(pseudo.Zero[HPtr]().Value.Get()),
|
|
||||||
"Block/Body", log.TypeOf(pseudo.Zero[BPtr]().Value.Get()),
|
// WithTempRegisteredExtras temporarily registers `HPtr`, `BPtr`, and `SA` as if
|
||||||
"StateAccount", log.TypeOf(pseudo.Zero[SA]().Value.Get()),
|
// calling [RegisterExtras] the same type parameters. The [ExtraPayloads] are
|
||||||
)
|
// passed to `fn` instead of being returned; the argument MUST NOT be persisted
|
||||||
return extra
|
// 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
|
// A BlockBodyPayload is an implementation of [BlockBodyHooks] that is also able
|
||||||
// to clone itself. Both [Block.Body] and [Block.WithBody] require this
|
// to clone itself. Both [Block.Body] and [Block.WithBody] require this
|
||||||
// functionality to copy the payload between the types.
|
// functionality to copy the payload between the types.
|
||||||
type BlockBodyPayload[BPtr any] interface {
|
type BlockBodyPayload[Self any] interface {
|
||||||
BlockBodyHooks
|
BlockBodyHooks
|
||||||
Copy() BPtr
|
Copy() Self
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestOnlyClearRegisteredExtras clears the [Extras] previously passed to
|
// TestOnlyClearRegisteredExtras clears the [Extras] previously passed to
|
||||||
|
|
|
||||||
76
core/types/tempextras.libevm_test.go
Normal file
76
core/types/tempextras.libevm_test.go
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -63,9 +63,23 @@ func TestOverrideNewEVMArgs(t *testing.T) {
|
||||||
hooks := evmArgOverrider{newEVMchainID: chainID}
|
hooks := evmArgOverrider{newEVMchainID: chainID}
|
||||||
hooks.register(t)
|
hooks.register(t)
|
||||||
|
|
||||||
evm := NewEVM(BlockContext{}, TxContext{}, nil, nil, Config{})
|
assertChainID := func(t *testing.T, want int64) {
|
||||||
got := evm.ChainConfig().ChainID
|
t.Helper()
|
||||||
require.Equalf(t, big.NewInt(chainID), got, "%T.ChainConfig().ChainID set by NewEVM() hook", evm)
|
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) {
|
func TestOverrideEVMResetArgs(t *testing.T) {
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,19 @@ func RegisterHooks(h Hooks) {
|
||||||
libevmHooks.MustRegister(h)
|
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
|
// TestOnlyClearRegisteredHooks clears the [Hooks] previously passed to
|
||||||
// [RegisterHooks]. It panics if called from a non-testing call stack.
|
// [RegisterHooks]. It panics if called from a non-testing call stack.
|
||||||
func TestOnlyClearRegisteredHooks() {
|
func TestOnlyClearRegisteredHooks() {
|
||||||
|
|
|
||||||
|
|
@ -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
|
// 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
|
// 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
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
78
libevm/register/register_test.go
Normal file
78
libevm/register/register_test.go
Normal file
|
|
@ -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
|
||||||
|
// <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
65
libevm/temporary/temporary.go
Normal file
65
libevm/temporary/temporary.go
Normal file
|
|
@ -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
|
||||||
|
// <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -72,14 +72,8 @@ func RegisterExtras[C ChainConfigHooks, R RulesHooks](e Extras[C, R]) ExtraPaylo
|
||||||
mustBeStructOrPointerToOne[C]()
|
mustBeStructOrPointerToOne[C]()
|
||||||
mustBeStructOrPointerToOne[R]()
|
mustBeStructOrPointerToOne[R]()
|
||||||
|
|
||||||
payloads := e.payloads()
|
payloads, ctors := payloadsAndConstructors(e)
|
||||||
registeredExtras.MustRegister(&extraConstructors{
|
registeredExtras.MustRegister(ctors)
|
||||||
newChainConfig: pseudo.NewConstructor[C]().Zero,
|
|
||||||
newRules: pseudo.NewConstructor[R]().Zero,
|
|
||||||
reuseJSONRoot: e.ReuseJSONRoot,
|
|
||||||
newForRules: e.newForRules,
|
|
||||||
payloads: payloads,
|
|
||||||
})
|
|
||||||
log.Info(
|
log.Info(
|
||||||
"Registered params extras",
|
"Registered params extras",
|
||||||
"ChainConfig", log.TypeOf(pseudo.Zero[C]().Value.Get()),
|
"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
|
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
|
// TestOnlyClearRegisteredExtras clears the [Extras] previously passed to
|
||||||
// [RegisterExtras]. It panics if called from a non-testing call stack.
|
// [RegisterExtras]. It panics if called from a non-testing call stack.
|
||||||
//
|
//
|
||||||
|
|
|
||||||
|
|
@ -277,3 +277,88 @@ func assertPanics(t *testing.T, fn func(), wantContains string) {
|
||||||
}()
|
}()
|
||||||
fn()
|
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)
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue