chore: logging (#151)

## Why this should be merged

Adds logging of `libevm` modification of default behaviour.

## How this works

1. Introduces `log.Lazy` functions to allow expensive logging operations
to be computed i.f.f. required by the logging level.
2. Adds `Info` logging for registration of types and `Debug` logging for
all else.
3. Only paths that change behaviour in a potentially unpredictable
manner are logged; of note, RLP / JSON encoding is _not_ considered
unpredictable given that registered extras are logged.
4. The minimal viable package, `set`, was necessary because we don't
want to depend on `avalanchego` and the `hashicorp/go-set` latest
version requires a later version of Go. #153 tracks a swap to the latter
when possible.

The `eth/tracers/internal/tracetest` test flaked at least twice
(unrelated to these changes) so I've marked it as such since it's not
worth a separate PR.

## How this was tested

New unit test on `log.Lazy` + `set` methods. Existing CI for the rest as
it's a refactor.
This commit is contained in:
Arran Schlosberg 2025-02-24 16:53:21 +00:00 committed by GitHub
parent d32c7e0090
commit 02110d3f34
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 331 additions and 9 deletions

View file

@ -22,7 +22,7 @@ jobs:
go_test_short:
env:
FLAKY_REGEX: "ava-labs/libevm/(triedb/pathdb|eth|eth/tracers/js|eth/tracers/logger|accounts/abi/bind|accounts/keystore|eth/downloader|miner|ethclient|ethclient/gethclient|eth/catalyst)$"
FLAKY_REGEX: "ava-labs/libevm/(triedb/pathdb|eth|eth/tracers/js|eth/tracers/logger|eth/tracers/internal/tracetest|accounts/abi/bind|accounts/keystore|eth/downloader|miner|ethclient|ethclient/gethclient|eth/catalyst)$"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

View file

@ -16,10 +16,24 @@
package core
import (
"github.com/ava-labs/libevm/log"
)
// canExecuteTransaction is a convenience wrapper for calling the
// [params.RulesHooks.CanExecuteTransaction] hook.
func (st *StateTransition) canExecuteTransaction() error {
bCtx := st.evm.Context
rules := st.evm.ChainConfig().Rules(bCtx.BlockNumber, bCtx.Random != nil, bCtx.Time)
return rules.Hooks().CanExecuteTransaction(st.msg.From, st.msg.To, st.state)
if err := rules.Hooks().CanExecuteTransaction(st.msg.From, st.msg.To, st.state); err != nil {
log.Debug(
"Transaction execution blocked by libevm hook",
"from", st.msg.From,
"to", st.msg.To,
"hooks", log.TypeOf(rules.Hooks()),
"reason", err,
)
return err
}
return nil
}

View file

@ -23,6 +23,7 @@ import (
"github.com/ava-labs/libevm/libevm/pseudo"
"github.com/ava-labs/libevm/libevm/register"
"github.com/ava-labs/libevm/libevm/testonly"
"github.com/ava-labs/libevm/log"
"github.com/ava-labs/libevm/rlp"
)
@ -84,6 +85,12 @@ func RegisterExtras[
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
}

View file

@ -148,10 +148,7 @@ func init() {
}
// ActivePrecompiles returns the precompiles enabled with the current configuration.
func ActivePrecompiles(rules params.Rules) (active []common.Address) {
defer func() {
active = rules.Hooks().ActivePrecompiles(append([]common.Address{}, active...))
}()
func activePrecompiles(rules params.Rules) []common.Address {
switch {
case rules.IsCancun:
return PrecompiledAddressesCancun

View file

@ -21,13 +21,42 @@ import (
"math/big"
"github.com/holiman/uint256"
"golang.org/x/exp/slog"
"github.com/ava-labs/libevm/common"
"github.com/ava-labs/libevm/core/types"
"github.com/ava-labs/libevm/libevm"
"github.com/ava-labs/libevm/libevm/set"
"github.com/ava-labs/libevm/log"
"github.com/ava-labs/libevm/params"
)
// ActivePrecompiles returns the precompiles enabled with the current configuration.
func ActivePrecompiles(rules params.Rules) []common.Address {
orig := activePrecompiles(rules) // original, upstream implementation
active := rules.Hooks().ActivePrecompiles(append([]common.Address{}, orig...))
// As all set computation is done lazily and only when debugging, there is
// some duplication in favour of simplified code.
log.Debug(
"Overriding active precompiles",
"added", log.Lazy(func() slog.Value {
diff := set.From(active...).Sub(set.From(orig...))
return slog.AnyValue(diff.Slice())
}),
"removed", log.Lazy(func() slog.Value {
diff := set.From(orig...).Sub(set.From(active...))
return slog.AnyValue(diff.Slice())
}),
"unchanged", log.Lazy(func() slog.Value {
both := set.From(active...).Intersect(set.From(orig...))
return slog.AnyValue(both.Slice())
}),
)
return active
}
// evmCallArgs mirrors the parameters of the [EVM] methods Call(), CallCode(),
// DelegateCall() and StaticCall(). Its fields are identical to those of the
// parameters, prepended with the receiver name and call type. As

View file

@ -23,7 +23,7 @@ import (
"github.com/ava-labs/libevm/common"
"github.com/ava-labs/libevm/core/types"
"github.com/ava-labs/libevm/crypto"
"github.com/ava-labs/libevm/libevm"
"github.com/ava-labs/libevm/log"
"github.com/ava-labs/libevm/params"
"github.com/holiman/uint256"
)
@ -40,6 +40,7 @@ type (
func (evm *EVM) precompile(addr common.Address) (PrecompiledContract, bool) {
if p, override := evm.chainRules.Hooks().PrecompileOverride(addr); override {
log.Debug("Overriding precompile", "address", addr, "implementation", log.TypeOf(p))
return p, p != nil
}
var precompiles map[common.Address]PrecompiledContract
@ -459,8 +460,7 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64,
// This check MUST be placed after the caller's nonce is incremented but
// before all other state-modifying behaviour, even if changes may be
// reverted to the snapshot.
addrs := &libevm.AddressContext{Origin: evm.Origin, Caller: caller.Address(), Self: address}
gas, err := evm.chainRules.Hooks().CanCreateContract(addrs, gas, evm.StateDB)
gas, err := evm.canCreateContract(caller, address, gas)
if err != nil {
return nil, common.Address{}, gas, err
}

45
core/vm/evm.libevm.go Normal file
View file

@ -0,0 +1,45 @@
// 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 vm
import (
"github.com/ava-labs/libevm/common"
"github.com/ava-labs/libevm/libevm"
"github.com/ava-labs/libevm/log"
)
// canCreateContract is a convenience wrapper for calling the
// [params.RulesHooks.CanCreateContract] hook.
func (evm *EVM) canCreateContract(caller ContractRef, contractToCreate common.Address, gas uint64) (remainingGas uint64, _ error) {
addrs := &libevm.AddressContext{Origin: evm.Origin, Caller: caller.Address(), Self: contractToCreate}
gas, err := evm.chainRules.Hooks().CanCreateContract(addrs, gas, evm.StateDB)
// NOTE that this block only performs logging and that all paths propagate
// `(gas, err)` unmodified.
if err != nil {
log.Debug(
"Contract creation blocked by libevm hook",
"origin", addrs.Origin,
"caller", addrs.Caller,
"contract", addrs.Self,
"hooks", log.TypeOf(evm.chainRules.Hooks()),
"reason", err,
)
}
return gas, err
}

59
libevm/set/set.go Normal file
View file

@ -0,0 +1,59 @@
// 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 set provides a generic implementation of a set.
package set
// A Set is a generic set implementation.
type Set[T comparable] map[T]struct{}
// From returns a Set containing the elements.
func From[T comparable](elements ...T) Set[T] {
s := make(Set[T], len(elements))
for _, e := range elements {
s[e] = struct{}{}
}
return s
}
// Sub returns the elements in `s` that aren't in `t`.
func (s Set[T]) Sub(t Set[T]) Set[T] {
return s.alsoIn(t, false)
}
// Intersect returns the intersection of `s` and `t`.
func (s Set[T]) Intersect(t Set[T]) Set[T] {
return s.alsoIn(t, true)
}
func (s Set[T]) alsoIn(t Set[T], inBoth bool) Set[T] {
res := make(Set[T])
for el := range s {
if _, ok := t[el]; ok == inBoth {
res[el] = struct{}{}
}
}
return res
}
// Slice returns the elements of `s` as a slice.
func (s Set[T]) Slice() []T {
sl := make([]T, 0, len(s))
for el := range s {
sl = append(sl, el)
}
return sl
}

56
libevm/set/set_test.go Normal file
View file

@ -0,0 +1,56 @@
// 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 set
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestSub(t *testing.T) {
for _, tt := range [][3][]int{ // start, sub, want
{{}, {}, {}},
{{0}, {}, {0}},
{{}, {0}, {}},
{{0, 1}, {0}, {1}},
{{0, 1}, {1}, {0}},
} {
in, sub := tt[0], tt[1]
want := tt[2]
got := From(in...).Sub(From(sub...)).Slice()
assert.Equalf(t, want, got, "Set(%v).Sub(%v)", in, sub)
}
}
func TestIntersect(t *testing.T) {
for _, tt := range [][3][]int{ // L, R, intersection
{{}, {}, {}},
{{0}, {}, {}},
{{0}, {0}, {0}},
{{0, 1}, {0}, {0}},
{{0, 1}, {1}, {1}},
} {
want := tt[2]
for i := 0; i <= 1; i++ { // commutativity
lhs, rhs := tt[i], tt[1-i]
got := From(lhs...).Intersect(From(rhs...)).Slice()
assert.Equalf(t, want, got, "Set(%v).Intersect(%v)", lhs, rhs)
}
}
}

41
log/value.libevm.go Normal file
View file

@ -0,0 +1,41 @@
// 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 log
import (
"fmt"
"golang.org/x/exp/slog"
)
// A Lazy function defers its execution until logging is performed.
type Lazy func() slog.Value
var _ slog.LogValuer = Lazy(nil)
// LogValue implements the [slog.LogValuer] interface.
func (l Lazy) LogValue() slog.Value {
return l()
}
// TypeOf returns a Lazy function that reports the concrete type of `v` as
// determined with the `%T` [fmt] verb.
func TypeOf(v any) Lazy {
return Lazy(func() slog.Value {
return slog.StringValue(fmt.Sprintf("%T", v))
})
}

67
log/value.libevm_test.go Normal file
View file

@ -0,0 +1,67 @@
// 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 log
import (
"bytes"
"testing"
"github.com/stretchr/testify/assert"
"golang.org/x/exp/slog"
)
func TestTypeOf(t *testing.T) {
type foo struct{}
tests := map[any]string{
nil: "<nil>",
int(0): "int",
int(1): "int",
uint(0): "uint",
foo{}: "log.foo",
(*foo)(nil): "*log.foo",
}
for in, want := range tests {
got := TypeOf(in).LogValue()
assert.Equalf(t, want, got.String(), "TypeOf(%T(%[1]v))", in, in)
}
}
func TestLazy(t *testing.T) {
const (
key = "theKey"
val = "theVal"
wantLogged = key + "=" + val
)
var gotNumEvaluations int
fn := Lazy(func() slog.Value {
gotNumEvaluations++
return slog.StringValue(val)
})
var out bytes.Buffer
log := slog.New(slog.NewTextHandler(&out, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
log.Info("", key, fn)
log.Debug("", "not evaluated", fn)
assert.Containsf(t, out.String(), wantLogged, "evaluation of %T function is logged", fn)
assert.Equalf(t, 1, gotNumEvaluations, "number of evaluations of %T function", fn)
}

View file

@ -23,6 +23,7 @@ import (
"github.com/ava-labs/libevm/libevm/pseudo"
"github.com/ava-labs/libevm/libevm/register"
"github.com/ava-labs/libevm/log"
)
// Extras are arbitrary payloads to be added as extra fields in [ChainConfig]
@ -79,6 +80,12 @@ func RegisterExtras[C ChainConfigHooks, R RulesHooks](e Extras[C, R]) ExtraPaylo
newForRules: e.newForRules,
payloads: payloads,
})
log.Info(
"Registered params extras",
"ChainConfig", log.TypeOf(pseudo.Zero[C]().Value.Get()),
"Rules", log.TypeOf(pseudo.Zero[R]().Value.Get()),
"ReuseJSONRoot", e.ReuseJSONRoot,
)
return payloads
}