Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions core/state/statedb.libevm.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (

"github.com/ava-labs/libevm/common"
"github.com/ava-labs/libevm/core/state/snapshot"
"github.com/ava-labs/libevm/libevm"
"github.com/ava-labs/libevm/libevm/register"
"github.com/ava-labs/libevm/libevm/stateconf"
)
Expand Down Expand Up @@ -91,8 +92,11 @@ func RegisterExtras(s StateDBHooks) {
// 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)
func WithTempRegisteredExtras(lock libevm.ExtrasLock, s StateDBHooks, fn func() error) error {
if err := lock.Verify(); err != nil {
return err
}
return registeredExtras.TempOverride(s, fn)
}

// TestOnlyClearRegisteredExtras clears the arguments previously passed to
Expand Down
17 changes: 11 additions & 6 deletions core/state/statedb.libevm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"github.com/ava-labs/libevm/core/state/snapshot"
"github.com/ava-labs/libevm/core/types"
"github.com/ava-labs/libevm/ethdb"
"github.com/ava-labs/libevm/libevm"
"github.com/ava-labs/libevm/libevm/stateconf"
"github.com/ava-labs/libevm/trie"
"github.com/ava-labs/libevm/trie/trienode"
Expand Down Expand Up @@ -216,13 +217,17 @@ func TestTransformStateKey(t *testing.T) {
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)
err := libevm.WithTemporaryExtrasLock(func(lock libevm.ExtrasLock) error {
return WithTempRegisteredExtras(lock, noopHooks{}, func() error {
// 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)
return nil
})
})
require.NoError(t, err)
})

updatedVal := common.Hash{'u', 'p', 'd', 'a', 't', 'e', 'd'}
Expand Down
8 changes: 6 additions & 2 deletions core/types/rlp_payload.libevm.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"io"

"github.com/ava-labs/libevm/common"
"github.com/ava-labs/libevm/libevm"
"github.com/ava-labs/libevm/libevm/pseudo"
"github.com/ava-labs/libevm/libevm/register"
"github.com/ava-labs/libevm/libevm/testonly"
Expand Down Expand Up @@ -114,9 +115,12 @@ func WithTempRegisteredExtras[
H, B, SA any,
HPtr HeaderHooksPointer[H],
BPtr BlockBodyHooksPointer[B, BPtr],
](fn func(ExtraPayloads[HPtr, BPtr, SA])) {
](lock libevm.ExtrasLock, fn func(ExtraPayloads[HPtr, BPtr, SA]) error) error {
if err := lock.Verify(); err != nil {
return err
}
payloads, ctors := payloadsAndConstructors[H, HPtr, B, BPtr, SA]()
registeredExtras.TempOverride(ctors, func() { fn(payloads) })
return registeredExtras.TempOverride(ctors, func() error { return fn(payloads) })
}

// A HeaderHooksPointer is a type constraint for an implementation of
Expand Down
25 changes: 15 additions & 10 deletions core/types/tempextras.libevm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/ava-labs/libevm/libevm"
"github.com/ava-labs/libevm/rlp"
)

Expand Down Expand Up @@ -58,19 +59,23 @@ func TestTempRegisteredExtras(t *testing.T) {

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)
err := libevm.WithTemporaryExtrasLock(func(lock libevm.ExtrasLock) error {
return WithTempRegisteredExtras(lock, func(extras ExtraPayloads[*NOOPHeaderHooks, *tempBlockBodyHooks, bool]) error {
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})
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)
assert.Equalf(t, want, got, "rlp.EncodeToBytes(%T) with %T hooks", b, payload)
return nil
})
})
require.NoError(t, err)
})
t.Run("after_temp", testPrimaryExtras)
}
11 changes: 8 additions & 3 deletions core/vm/evm.libevm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/ava-labs/libevm/libevm"
"github.com/ava-labs/libevm/params"
)

Expand Down Expand Up @@ -72,10 +73,14 @@ func TestOverrideNewEVMArgs(t *testing.T) {
assertChainID(t, chainID)

t.Run("WithTempRegisteredHooks", func(t *testing.T) {
override := evmArgOverrider{newEVMchainID: 24680}
WithTempRegisteredHooks(&override, func() {
assertChainID(t, override.newEVMchainID)
err := libevm.WithTemporaryExtrasLock(func(lock libevm.ExtrasLock) error {
override := evmArgOverrider{newEVMchainID: 24680}
return WithTempRegisteredHooks(lock, &override, func() error {
assertChainID(t, override.newEVMchainID)
return nil
})
})
require.NoError(t, err)
t.Run("after", func(t *testing.T) {
assertChainID(t, chainID)
})
Expand Down
8 changes: 6 additions & 2 deletions core/vm/hooks.libevm.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package vm

import (
"github.com/ava-labs/libevm/libevm"
"github.com/ava-labs/libevm/libevm/register"
"github.com/ava-labs/libevm/params"
)
Expand All @@ -36,8 +37,11 @@ func RegisterHooks(h Hooks) {
// 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)
func WithTempRegisteredHooks(lock libevm.ExtrasLock, h Hooks, fn func() error) error {
if err := lock.Verify(); err != nil {
return err
}
return libevmHooks.TempOverride(h, fn)
}

// TestOnlyClearRegisteredHooks clears the [Hooks] previously passed to
Expand Down
63 changes: 63 additions & 0 deletions libevm/extraslock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// 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 libevm

import (
"errors"
"sync"
"sync/atomic"
)

var (
extrasMu sync.Mutex
extrasHandle atomic.Uint64
)

// An ExtrasLock is a handle that proves a current call to
// [WithTemporaryExtrasLock].
type ExtrasLock struct {
handle *uint64
}

// WithTemporaryExtrasLock takes a global lock and calls `fn` with a handle that
// can be used to prove that the lock is held. All package-specific temporary
// overrides require this proof.
//
// WithTemporaryExtrasLock MUST NOT be used on a live chain. It is solely
// intended for off-chain consumers that require access to extras.
func WithTemporaryExtrasLock(fn func(lock ExtrasLock) error) error {
extrasMu.Lock()
defer func() {
extrasHandle.Add(1)
extrasMu.Unlock()
}()

v := extrasHandle.Load()
return fn(ExtrasLock{&v})
}

// ErrExpiredExtrasLock is returned by [ExtrasLock.Verify] if the lock has been
// persisted beyond the call to [WithTemporaryExtrasLock] that created it.
var ErrExpiredExtrasLock = errors.New("libevm.ExtrasLock expired")

// Verify verifies that the lock is valid.
func (l ExtrasLock) Verify() error {
if *l.handle != extrasHandle.Load() {
return ErrExpiredExtrasLock
}
return nil
}
52 changes: 52 additions & 0 deletions libevm/extraslock_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// 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 libevm_test

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

// Testing from outside the package to guarantee usage of the public API only.
. "github.com/ava-labs/libevm/libevm"
)

func TestExtrasLock(t *testing.T) {
var zero ExtrasLock
assert.Panics(t, func() { _ = zero.Verify() }, "Verify() method of zero-value ExtrasLock{}")

testIntegration := func(t *testing.T) {
t.Helper()
require.NoError(t,
WithTemporaryExtrasLock((ExtrasLock).Verify),
"WithTemporaryExtrasLock((ExtrasLock).Verify)",
)
}
t.Run("initial_usage", testIntegration)

t.Run("lock_expiration", func(t *testing.T) {
var persisted ExtrasLock
require.NoError(t, WithTemporaryExtrasLock(func(l ExtrasLock) error {
persisted = l
return l.Verify()
}))
assert.ErrorIs(t, persisted.Verify(), ErrExpiredExtrasLock, "Verify() of persisted ExtrasLock")
})

t.Run("repeat_usage", testIntegration)
}
13 changes: 7 additions & 6 deletions libevm/register/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,22 +72,23 @@ func (o *AtMostOnce[T]) TestOnlyClear() {
//
// 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)
func (o *AtMostOnce[T]) TempOverride(with T, fn func() error) error {
return 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]) TempClear(fn func() error) error {
return o.temp(nil, fn)
}

func (o *AtMostOnce[T]) temp(with *T, fn func()) {
func (o *AtMostOnce[T]) temp(with *T, fn func() error) error {
old := o.v
o.v = with
fn()
err := fn()
o.v = old
return err
}
18 changes: 14 additions & 4 deletions libevm/register/register_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package register

import (
"errors"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -56,9 +57,10 @@ func TestAtMostOnce(t *testing.T) {

t.Run("TempOverride", func(t *testing.T) {
t.Run("during", func(t *testing.T) {
sut.TempOverride(val+1, func() {
require.NoError(t, sut.TempOverride(val+1, func() error {
assertRegistered(t, val+1)
})
return nil
}))
})
t.Run("after", func(t *testing.T) {
assertRegistered(t, val)
Expand All @@ -67,12 +69,20 @@ func TestAtMostOnce(t *testing.T) {

t.Run("TempClear", func(t *testing.T) {
t.Run("during", func(t *testing.T) {
sut.TempClear(func() {
require.NoError(t, sut.TempClear(func() error {
assert.False(t, sut.Registered(), "Registered()")
})
return nil
}))
})
t.Run("after", func(t *testing.T) {
assertRegistered(t, val)
})
})

t.Run("error_propagation", func(t *testing.T) {
errFoo := errors.New("foo")
fn := func() error { return errFoo }
assert.ErrorIs(t, sut.TempOverride(0, fn), errFoo, "TempOverride()") //nolint:testifylint // Blindly using require is an anti-pattern!!!
assert.ErrorIs(t, sut.TempClear(fn), errFoo, "TempClear()")
})
}
Loading
Loading