Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we have this handle as a uint64? Or why do we need the handle? Aren't this implying if the lock is held or not?

}

// 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 {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we can omit taking the lock in the fn

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean? The fn doesn't take it; it waits for it to be taken before being called.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if taking multiple fns (i.e WithTemporaryExtrasLock(fns ...func(lock ExtrasLock) error)` would help cleaning this nested calls.

extrasMu.Lock()
defer func() {
extrasHandle.Add(1)
extrasMu.Unlock()
}()

v := extrasHandle.Load()
return fn(ExtrasLock{&v})
Comment on lines +49 to +50
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to pass the lock here and defer the lock verification to the fn? Can't we verify that here? Are we expecting fn to be called with different locks?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, we're expecting fn to pass the lock to every extras override, each of which does its own verification. This is an example of an fn and this is a test that demonstrates it being used to temporarily emulate all C-Chain behaviour via this package.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1- This pattern could be difficult for a consumer. I wonder if we can move the complexity either to libevm or to coreth/subnet-evm so that the consumer can use something like this

evm.WithTempRegisteredLibEVMExtras(func() error { 
	t.Run("payloads", func(t *testing.T) {
		for pkg, fn := range payloadTests {
			t.Run(pkg, fn)
		}
	})
	return nil
})

TLDR; IMO locks should be abstracted away.

}

// 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