mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-18 19:00:46 +00:00
feat: state-key transformation w/ override (#205)
## Why this should be merged
`ava-labs/coreth` has a partitioned state-address space, achieved by
setting or clearing a specific bit in the hash used to key the space.
This change allows such behaviour to be achieved with pure `libevm`
instead of the `StateDB` wrapping that `coreth` currently uses.
## How this works
Introduction of `state.StateDBHooks` interface, including a
`TransformStateKey()` method that allows for arbitrary change of state
key. If registered, this hook will be honoured by
`StateDB.{Get,GetCommitted,State}Key()` methods unless they receive a
`stateconf.SkipStateKeyTransformation` option.
## How this was tested
Unit test of `SetState() -> GetState() + GetCommittedState()` round trip
with and without options to skip.
This commit is contained in:
parent
99f0d0b1de
commit
464de82910
7 changed files with 173 additions and 12 deletions
|
|
@ -28,7 +28,6 @@ import (
|
||||||
"github.com/ava-labs/libevm/core/state/snapshot"
|
"github.com/ava-labs/libevm/core/state/snapshot"
|
||||||
"github.com/ava-labs/libevm/core/types"
|
"github.com/ava-labs/libevm/core/types"
|
||||||
"github.com/ava-labs/libevm/crypto"
|
"github.com/ava-labs/libevm/crypto"
|
||||||
"github.com/ava-labs/libevm/libevm/stateconf"
|
|
||||||
"github.com/ava-labs/libevm/log"
|
"github.com/ava-labs/libevm/log"
|
||||||
"github.com/ava-labs/libevm/metrics"
|
"github.com/ava-labs/libevm/metrics"
|
||||||
"github.com/ava-labs/libevm/params"
|
"github.com/ava-labs/libevm/params"
|
||||||
|
|
@ -36,6 +35,9 @@ import (
|
||||||
"github.com/ava-labs/libevm/trie/trienode"
|
"github.com/ava-labs/libevm/trie/trienode"
|
||||||
"github.com/ava-labs/libevm/trie/triestate"
|
"github.com/ava-labs/libevm/trie/triestate"
|
||||||
"github.com/holiman/uint256"
|
"github.com/holiman/uint256"
|
||||||
|
|
||||||
|
// libevm extra imports
|
||||||
|
"github.com/ava-labs/libevm/libevm/stateconf"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -341,18 +343,20 @@ func (s *StateDB) GetCodeHash(addr common.Address) common.Hash {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetState retrieves a value from the given account's storage trie.
|
// GetState retrieves a value from the given account's storage trie.
|
||||||
func (s *StateDB) GetState(addr common.Address, hash common.Hash) common.Hash {
|
func (s *StateDB) GetState(addr common.Address, hash common.Hash, opts ...stateconf.StateDBStateOption) common.Hash {
|
||||||
stateObject := s.getStateObject(addr)
|
stateObject := s.getStateObject(addr)
|
||||||
if stateObject != nil {
|
if stateObject != nil {
|
||||||
|
hash = transformStateKey(addr, hash, opts...)
|
||||||
return stateObject.GetState(hash)
|
return stateObject.GetState(hash)
|
||||||
}
|
}
|
||||||
return common.Hash{}
|
return common.Hash{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCommittedState retrieves a value from the given account's committed storage trie.
|
// GetCommittedState retrieves a value from the given account's committed storage trie.
|
||||||
func (s *StateDB) GetCommittedState(addr common.Address, hash common.Hash) common.Hash {
|
func (s *StateDB) GetCommittedState(addr common.Address, hash common.Hash, opts ...stateconf.StateDBStateOption) common.Hash {
|
||||||
stateObject := s.getStateObject(addr)
|
stateObject := s.getStateObject(addr)
|
||||||
if stateObject != nil {
|
if stateObject != nil {
|
||||||
|
hash = transformStateKey(addr, hash, opts...)
|
||||||
return stateObject.GetCommittedState(hash)
|
return stateObject.GetCommittedState(hash)
|
||||||
}
|
}
|
||||||
return common.Hash{}
|
return common.Hash{}
|
||||||
|
|
@ -412,9 +416,10 @@ func (s *StateDB) SetCode(addr common.Address, code []byte) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *StateDB) SetState(addr common.Address, key, value common.Hash) {
|
func (s *StateDB) SetState(addr common.Address, key, value common.Hash, opts ...stateconf.StateDBStateOption) {
|
||||||
stateObject := s.getOrNewStateObject(addr)
|
stateObject := s.getOrNewStateObject(addr)
|
||||||
if stateObject != nil {
|
if stateObject != nil {
|
||||||
|
key = transformStateKey(addr, key, opts...)
|
||||||
stateObject.SetState(key, value)
|
stateObject.SetState(key, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import (
|
||||||
|
|
||||||
"github.com/ava-labs/libevm/common"
|
"github.com/ava-labs/libevm/common"
|
||||||
"github.com/ava-labs/libevm/core/state/snapshot"
|
"github.com/ava-labs/libevm/core/state/snapshot"
|
||||||
|
"github.com/ava-labs/libevm/libevm/register"
|
||||||
"github.com/ava-labs/libevm/libevm/stateconf"
|
"github.com/ava-labs/libevm/libevm/stateconf"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -57,3 +58,46 @@ func clearTypedNilPointer(snaps SnapshotTree) SnapshotTree {
|
||||||
}
|
}
|
||||||
return snaps
|
return snaps
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StateDBHooks modify the behaviour of [StateDB] instances.
|
||||||
|
type StateDBHooks interface {
|
||||||
|
// TransformStateKey receives the arguments passed to [StateDB.GetState],
|
||||||
|
// [StateDB.GetCommittedState] or [StateDB.SetState], and returns the key
|
||||||
|
// that each of those methods will use for accessing state. This method will
|
||||||
|
// not, however, be called if any of the aforementioned [StateDB] methods
|
||||||
|
// receives a [stateconf.SkipStateKeyTransformation] option.
|
||||||
|
//
|
||||||
|
// This method SHOULD NOT be used for anything other than achieving
|
||||||
|
// backwards compatibility with an existing chain. In the event that other
|
||||||
|
// methods are added to the [StateDBHooks] interface and no key
|
||||||
|
// transformation is required, it is acceptable for this method to echo the
|
||||||
|
// [common.Hash], unchanged.
|
||||||
|
TransformStateKey(_ common.Address, key common.Hash) (newKey common.Hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterExtras registers the [StateDBHooks] such that they modify the
|
||||||
|
// behaviour of all [StateDB] instances. It is expected to be called in an
|
||||||
|
// `init()` function and MUST NOT be called more than once.
|
||||||
|
func RegisterExtras(s StateDBHooks) {
|
||||||
|
registeredExtras.MustRegister(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestOnlyClearRegisteredExtras clears the arguments previously passed to
|
||||||
|
// [RegisterExtras]. It panics if called from a non-testing call stack.
|
||||||
|
//
|
||||||
|
// In tests it SHOULD be called before every call to [RegisterExtras] and then
|
||||||
|
// defer-called afterwards, either directly or via testing.TB.Cleanup(). This is
|
||||||
|
// a workaround for the single-call limitation on [RegisterExtras].
|
||||||
|
func TestOnlyClearRegisteredExtras() {
|
||||||
|
registeredExtras.TestOnlyClear()
|
||||||
|
}
|
||||||
|
|
||||||
|
var registeredExtras register.AtMostOnce[StateDBHooks]
|
||||||
|
|
||||||
|
func transformStateKey(addr common.Address, key common.Hash, opts ...stateconf.StateDBStateOption) common.Hash {
|
||||||
|
r := ®isteredExtras
|
||||||
|
if !r.Registered() || !stateconf.ShouldTransformStateKey(opts...) {
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
return r.Get().TransformStateKey(addr, key)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -138,3 +138,81 @@ func (r *triedbRecorder) Update(
|
||||||
func (r *triedbRecorder) Reader(_ common.Hash) (database.Reader, error) {
|
func (r *triedbRecorder) Reader(_ common.Hash) (database.Reader, error) {
|
||||||
return r.Database.Reader(common.Hash{})
|
return r.Database.Reader(common.Hash{})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type highByteFlipper struct{}
|
||||||
|
|
||||||
|
func flipHighByte(h common.Hash) common.Hash {
|
||||||
|
h[0] = ^h[0]
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
func (highByteFlipper) TransformStateKey(_ common.Address, key common.Hash) common.Hash {
|
||||||
|
return flipHighByte(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTransformStateKey(t *testing.T) {
|
||||||
|
rawdb := rawdb.NewMemoryDatabase()
|
||||||
|
trie := triedb.NewDatabase(rawdb, nil)
|
||||||
|
db := NewDatabaseWithNodeDB(rawdb, trie)
|
||||||
|
sdb, err := New(types.EmptyRootHash, db, nil)
|
||||||
|
require.NoErrorf(t, err, "New()")
|
||||||
|
|
||||||
|
addr := common.Address{1}
|
||||||
|
regularKey := common.Hash{0, 'k', 'e', 'y'}
|
||||||
|
flippedKey := flipHighByte(regularKey)
|
||||||
|
regularVal := common.Hash{'r', 'e', 'g', 'u', 'l', 'a', 'r'}
|
||||||
|
flippedVal := common.Hash{'f', 'l', 'i', 'p', 'p', 'e', 'd'}
|
||||||
|
|
||||||
|
sdb.SetState(addr, regularKey, regularVal)
|
||||||
|
sdb.SetState(addr, flippedKey, flippedVal)
|
||||||
|
|
||||||
|
assertEq := func(t *testing.T, key, want common.Hash, opts ...stateconf.StateDBStateOption) {
|
||||||
|
t.Helper()
|
||||||
|
assert.Equal(t, want, sdb.GetState(addr, key, opts...))
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEq(t, regularKey, regularVal)
|
||||||
|
assertEq(t, flippedKey, flippedVal)
|
||||||
|
|
||||||
|
root, err := sdb.Commit(0, false)
|
||||||
|
require.NoErrorf(t, err, "state.Commit()")
|
||||||
|
|
||||||
|
err = trie.Commit(root, false)
|
||||||
|
require.NoErrorf(t, err, "trie.Commit()")
|
||||||
|
|
||||||
|
sdb, err = New(root, db, nil)
|
||||||
|
require.NoErrorf(t, err, "New()")
|
||||||
|
|
||||||
|
assertCommittedEq := func(t *testing.T, key, want common.Hash, opts ...stateconf.StateDBStateOption) {
|
||||||
|
t.Helper()
|
||||||
|
assert.Equal(t, want, sdb.GetCommittedState(addr, key, opts...))
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEq(t, regularKey, regularVal)
|
||||||
|
assertEq(t, flippedKey, flippedVal)
|
||||||
|
assertCommittedEq(t, regularKey, regularVal)
|
||||||
|
assertCommittedEq(t, flippedKey, flippedVal)
|
||||||
|
|
||||||
|
// Typically the hook would be registered before any state access or
|
||||||
|
// setting, but doing it here aids testing by showing the before-and-after
|
||||||
|
// effects.
|
||||||
|
RegisterExtras(highByteFlipper{})
|
||||||
|
t.Cleanup(TestOnlyClearRegisteredExtras)
|
||||||
|
|
||||||
|
noTransform := stateconf.SkipStateKeyTransformation()
|
||||||
|
assertEq(t, regularKey, flippedVal)
|
||||||
|
assertEq(t, regularKey, regularVal, noTransform)
|
||||||
|
assertEq(t, flippedKey, regularVal)
|
||||||
|
assertEq(t, flippedKey, flippedVal, noTransform)
|
||||||
|
assertCommittedEq(t, regularKey, flippedVal)
|
||||||
|
assertCommittedEq(t, regularKey, regularVal, noTransform)
|
||||||
|
assertCommittedEq(t, flippedKey, regularVal)
|
||||||
|
assertCommittedEq(t, flippedKey, flippedVal, noTransform)
|
||||||
|
|
||||||
|
updatedVal := common.Hash{'u', 'p', 'd', 'a', 't', 'e', 'd'}
|
||||||
|
sdb.SetState(addr, regularKey, updatedVal)
|
||||||
|
assertEq(t, regularKey, updatedVal)
|
||||||
|
assertEq(t, flippedKey, updatedVal, noTransform)
|
||||||
|
assertCommittedEq(t, regularKey, flippedVal)
|
||||||
|
assertCommittedEq(t, flippedKey, flippedVal, noTransform)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,9 @@ import (
|
||||||
"github.com/ava-labs/libevm/core/types"
|
"github.com/ava-labs/libevm/core/types"
|
||||||
"github.com/ava-labs/libevm/params"
|
"github.com/ava-labs/libevm/params"
|
||||||
"github.com/holiman/uint256"
|
"github.com/holiman/uint256"
|
||||||
|
|
||||||
|
// libevm extra imports
|
||||||
|
"github.com/ava-labs/libevm/libevm/stateconf"
|
||||||
)
|
)
|
||||||
|
|
||||||
// StateDB is an EVM database for full state querying.
|
// StateDB is an EVM database for full state querying.
|
||||||
|
|
@ -45,9 +48,9 @@ type StateDB interface {
|
||||||
SubRefund(uint64)
|
SubRefund(uint64)
|
||||||
GetRefund() uint64
|
GetRefund() uint64
|
||||||
|
|
||||||
GetCommittedState(common.Address, common.Hash) common.Hash
|
GetCommittedState(common.Address, common.Hash, ...stateconf.StateDBStateOption) common.Hash
|
||||||
GetState(common.Address, common.Hash) common.Hash
|
GetState(common.Address, common.Hash, ...stateconf.StateDBStateOption) common.Hash
|
||||||
SetState(common.Address, common.Hash, common.Hash)
|
SetState(common.Address, common.Hash, common.Hash, ...stateconf.StateDBStateOption)
|
||||||
|
|
||||||
GetTransientState(addr common.Address, key common.Hash) common.Hash
|
GetTransientState(addr common.Address, key common.Hash) common.Hash
|
||||||
SetTransientState(addr common.Address, key, value common.Hash)
|
SetTransientState(addr common.Address, key, value common.Hash)
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,9 @@ import (
|
||||||
"github.com/ava-labs/libevm/core/vm"
|
"github.com/ava-labs/libevm/core/vm"
|
||||||
"github.com/ava-labs/libevm/params"
|
"github.com/ava-labs/libevm/params"
|
||||||
"github.com/holiman/uint256"
|
"github.com/holiman/uint256"
|
||||||
|
|
||||||
|
// libevm extra imports
|
||||||
|
"github.com/ava-labs/libevm/libevm/stateconf"
|
||||||
)
|
)
|
||||||
|
|
||||||
type dummyContractRef struct {
|
type dummyContractRef struct {
|
||||||
|
|
@ -50,8 +53,11 @@ type dummyStatedb struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*dummyStatedb) GetRefund() uint64 { return 1337 }
|
func (*dummyStatedb) GetRefund() uint64 { return 1337 }
|
||||||
func (*dummyStatedb) GetState(_ common.Address, _ common.Hash) common.Hash { return common.Hash{} }
|
func (*dummyStatedb) GetState(_ common.Address, _ common.Hash, _ ...stateconf.StateDBStateOption) common.Hash {
|
||||||
func (*dummyStatedb) SetState(_ common.Address, _ common.Hash, _ common.Hash) {}
|
return common.Hash{}
|
||||||
|
}
|
||||||
|
func (*dummyStatedb) SetState(_ common.Address, _ common.Hash, _ common.Hash, _ ...stateconf.StateDBStateOption) {
|
||||||
|
}
|
||||||
|
|
||||||
func TestStoreCapture(t *testing.T) {
|
func TestStoreCapture(t *testing.T) {
|
||||||
var (
|
var (
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import (
|
||||||
"github.com/holiman/uint256"
|
"github.com/holiman/uint256"
|
||||||
|
|
||||||
"github.com/ava-labs/libevm/common"
|
"github.com/ava-labs/libevm/common"
|
||||||
|
"github.com/ava-labs/libevm/libevm/stateconf"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PrecompiledContract is an exact copy of vm.PrecompiledContract, mirrored here
|
// PrecompiledContract is an exact copy of vm.PrecompiledContract, mirrored here
|
||||||
|
|
@ -43,8 +44,8 @@ type StateReader interface {
|
||||||
|
|
||||||
GetRefund() uint64
|
GetRefund() uint64
|
||||||
|
|
||||||
GetCommittedState(common.Address, common.Hash) common.Hash
|
GetCommittedState(common.Address, common.Hash, ...stateconf.StateDBStateOption) common.Hash
|
||||||
GetState(common.Address, common.Hash) common.Hash
|
GetState(common.Address, common.Hash, ...stateconf.StateDBStateOption) common.Hash
|
||||||
|
|
||||||
GetTransientState(addr common.Address, key common.Hash) common.Hash
|
GetTransientState(addr common.Address, key common.Hash) common.Hash
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -113,3 +113,27 @@ func ExtractTrieDBUpdatePayload(opts ...TrieDBUpdateOption) (common.Hash, common
|
||||||
}
|
}
|
||||||
return *conf.parentBlockHash, *conf.currentBlockHash, true
|
return *conf.parentBlockHash, *conf.currentBlockHash, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A StateDBStateOption configures the behaviour of state.StateDB methods for
|
||||||
|
// getting and setting state: GetState(), GetCommittedState(), and SetState().
|
||||||
|
type StateDBStateOption = options.Option[stateDBStateConfig]
|
||||||
|
|
||||||
|
type stateDBStateConfig struct {
|
||||||
|
skipKeyTransformation bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// SkipStateKeyTransformation causes any registered state-key transformation
|
||||||
|
// hook to be ignored. See state.RegisterExtras() for details.
|
||||||
|
func SkipStateKeyTransformation() StateDBStateOption {
|
||||||
|
return options.Func[stateDBStateConfig](func(c *stateDBStateConfig) {
|
||||||
|
c.skipKeyTransformation = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShouldTransformStateKey parses the options, returning whether or not any
|
||||||
|
// registered state-key transformation hook should be used; i.e. it returns
|
||||||
|
// `true` i.f.f. there are no [SkipStateKeyTransformation] options in the
|
||||||
|
// arguments.
|
||||||
|
func ShouldTransformStateKey(opts ...StateDBStateOption) bool {
|
||||||
|
return !options.As(opts...).skipKeyTransformation
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue