Skip to content

Commit 464de82

Browse files
authored
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.
1 parent 99f0d0b commit 464de82

File tree

7 files changed

+173
-12
lines changed

7 files changed

+173
-12
lines changed

core/state/statedb.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,16 @@ import (
2828
"github.com/ava-labs/libevm/core/state/snapshot"
2929
"github.com/ava-labs/libevm/core/types"
3030
"github.com/ava-labs/libevm/crypto"
31-
"github.com/ava-labs/libevm/libevm/stateconf"
3231
"github.com/ava-labs/libevm/log"
3332
"github.com/ava-labs/libevm/metrics"
3433
"github.com/ava-labs/libevm/params"
3534
"github.com/ava-labs/libevm/trie"
3635
"github.com/ava-labs/libevm/trie/trienode"
3736
"github.com/ava-labs/libevm/trie/triestate"
3837
"github.com/holiman/uint256"
38+
39+
// libevm extra imports
40+
"github.com/ava-labs/libevm/libevm/stateconf"
3941
)
4042

4143
const (
@@ -341,18 +343,20 @@ func (s *StateDB) GetCodeHash(addr common.Address) common.Hash {
341343
}
342344

343345
// GetState retrieves a value from the given account's storage trie.
344-
func (s *StateDB) GetState(addr common.Address, hash common.Hash) common.Hash {
346+
func (s *StateDB) GetState(addr common.Address, hash common.Hash, opts ...stateconf.StateDBStateOption) common.Hash {
345347
stateObject := s.getStateObject(addr)
346348
if stateObject != nil {
349+
hash = transformStateKey(addr, hash, opts...)
347350
return stateObject.GetState(hash)
348351
}
349352
return common.Hash{}
350353
}
351354

352355
// GetCommittedState retrieves a value from the given account's committed storage trie.
353-
func (s *StateDB) GetCommittedState(addr common.Address, hash common.Hash) common.Hash {
356+
func (s *StateDB) GetCommittedState(addr common.Address, hash common.Hash, opts ...stateconf.StateDBStateOption) common.Hash {
354357
stateObject := s.getStateObject(addr)
355358
if stateObject != nil {
359+
hash = transformStateKey(addr, hash, opts...)
356360
return stateObject.GetCommittedState(hash)
357361
}
358362
return common.Hash{}
@@ -412,9 +416,10 @@ func (s *StateDB) SetCode(addr common.Address, code []byte) {
412416
}
413417
}
414418

415-
func (s *StateDB) SetState(addr common.Address, key, value common.Hash) {
419+
func (s *StateDB) SetState(addr common.Address, key, value common.Hash, opts ...stateconf.StateDBStateOption) {
416420
stateObject := s.getOrNewStateObject(addr)
417421
if stateObject != nil {
422+
key = transformStateKey(addr, key, opts...)
418423
stateObject.SetState(key, value)
419424
}
420425
}

core/state/statedb.libevm.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121

2222
"github.com/ava-labs/libevm/common"
2323
"github.com/ava-labs/libevm/core/state/snapshot"
24+
"github.com/ava-labs/libevm/libevm/register"
2425
"github.com/ava-labs/libevm/libevm/stateconf"
2526
)
2627

@@ -57,3 +58,46 @@ func clearTypedNilPointer(snaps SnapshotTree) SnapshotTree {
5758
}
5859
return snaps
5960
}
61+
62+
// StateDBHooks modify the behaviour of [StateDB] instances.
63+
type StateDBHooks interface {
64+
// TransformStateKey receives the arguments passed to [StateDB.GetState],
65+
// [StateDB.GetCommittedState] or [StateDB.SetState], and returns the key
66+
// that each of those methods will use for accessing state. This method will
67+
// not, however, be called if any of the aforementioned [StateDB] methods
68+
// receives a [stateconf.SkipStateKeyTransformation] option.
69+
//
70+
// This method SHOULD NOT be used for anything other than achieving
71+
// backwards compatibility with an existing chain. In the event that other
72+
// methods are added to the [StateDBHooks] interface and no key
73+
// transformation is required, it is acceptable for this method to echo the
74+
// [common.Hash], unchanged.
75+
TransformStateKey(_ common.Address, key common.Hash) (newKey common.Hash)
76+
}
77+
78+
// RegisterExtras registers the [StateDBHooks] such that they modify the
79+
// behaviour of all [StateDB] instances. It is expected to be called in an
80+
// `init()` function and MUST NOT be called more than once.
81+
func RegisterExtras(s StateDBHooks) {
82+
registeredExtras.MustRegister(s)
83+
}
84+
85+
// TestOnlyClearRegisteredExtras clears the arguments previously passed to
86+
// [RegisterExtras]. It panics if called from a non-testing call stack.
87+
//
88+
// In tests it SHOULD be called before every call to [RegisterExtras] and then
89+
// defer-called afterwards, either directly or via testing.TB.Cleanup(). This is
90+
// a workaround for the single-call limitation on [RegisterExtras].
91+
func TestOnlyClearRegisteredExtras() {
92+
registeredExtras.TestOnlyClear()
93+
}
94+
95+
var registeredExtras register.AtMostOnce[StateDBHooks]
96+
97+
func transformStateKey(addr common.Address, key common.Hash, opts ...stateconf.StateDBStateOption) common.Hash {
98+
r := &registeredExtras
99+
if !r.Registered() || !stateconf.ShouldTransformStateKey(opts...) {
100+
return key
101+
}
102+
return r.Get().TransformStateKey(addr, key)
103+
}

core/state/statedb.libevm_test.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,3 +138,81 @@ func (r *triedbRecorder) Update(
138138
func (r *triedbRecorder) Reader(_ common.Hash) (database.Reader, error) {
139139
return r.Database.Reader(common.Hash{})
140140
}
141+
142+
type highByteFlipper struct{}
143+
144+
func flipHighByte(h common.Hash) common.Hash {
145+
h[0] = ^h[0]
146+
return h
147+
}
148+
149+
func (highByteFlipper) TransformStateKey(_ common.Address, key common.Hash) common.Hash {
150+
return flipHighByte(key)
151+
}
152+
153+
func TestTransformStateKey(t *testing.T) {
154+
rawdb := rawdb.NewMemoryDatabase()
155+
trie := triedb.NewDatabase(rawdb, nil)
156+
db := NewDatabaseWithNodeDB(rawdb, trie)
157+
sdb, err := New(types.EmptyRootHash, db, nil)
158+
require.NoErrorf(t, err, "New()")
159+
160+
addr := common.Address{1}
161+
regularKey := common.Hash{0, 'k', 'e', 'y'}
162+
flippedKey := flipHighByte(regularKey)
163+
regularVal := common.Hash{'r', 'e', 'g', 'u', 'l', 'a', 'r'}
164+
flippedVal := common.Hash{'f', 'l', 'i', 'p', 'p', 'e', 'd'}
165+
166+
sdb.SetState(addr, regularKey, regularVal)
167+
sdb.SetState(addr, flippedKey, flippedVal)
168+
169+
assertEq := func(t *testing.T, key, want common.Hash, opts ...stateconf.StateDBStateOption) {
170+
t.Helper()
171+
assert.Equal(t, want, sdb.GetState(addr, key, opts...))
172+
}
173+
174+
assertEq(t, regularKey, regularVal)
175+
assertEq(t, flippedKey, flippedVal)
176+
177+
root, err := sdb.Commit(0, false)
178+
require.NoErrorf(t, err, "state.Commit()")
179+
180+
err = trie.Commit(root, false)
181+
require.NoErrorf(t, err, "trie.Commit()")
182+
183+
sdb, err = New(root, db, nil)
184+
require.NoErrorf(t, err, "New()")
185+
186+
assertCommittedEq := func(t *testing.T, key, want common.Hash, opts ...stateconf.StateDBStateOption) {
187+
t.Helper()
188+
assert.Equal(t, want, sdb.GetCommittedState(addr, key, opts...))
189+
}
190+
191+
assertEq(t, regularKey, regularVal)
192+
assertEq(t, flippedKey, flippedVal)
193+
assertCommittedEq(t, regularKey, regularVal)
194+
assertCommittedEq(t, flippedKey, flippedVal)
195+
196+
// Typically the hook would be registered before any state access or
197+
// setting, but doing it here aids testing by showing the before-and-after
198+
// effects.
199+
RegisterExtras(highByteFlipper{})
200+
t.Cleanup(TestOnlyClearRegisteredExtras)
201+
202+
noTransform := stateconf.SkipStateKeyTransformation()
203+
assertEq(t, regularKey, flippedVal)
204+
assertEq(t, regularKey, regularVal, noTransform)
205+
assertEq(t, flippedKey, regularVal)
206+
assertEq(t, flippedKey, flippedVal, noTransform)
207+
assertCommittedEq(t, regularKey, flippedVal)
208+
assertCommittedEq(t, regularKey, regularVal, noTransform)
209+
assertCommittedEq(t, flippedKey, regularVal)
210+
assertCommittedEq(t, flippedKey, flippedVal, noTransform)
211+
212+
updatedVal := common.Hash{'u', 'p', 'd', 'a', 't', 'e', 'd'}
213+
sdb.SetState(addr, regularKey, updatedVal)
214+
assertEq(t, regularKey, updatedVal)
215+
assertEq(t, flippedKey, updatedVal, noTransform)
216+
assertCommittedEq(t, regularKey, flippedVal)
217+
assertCommittedEq(t, flippedKey, flippedVal, noTransform)
218+
}

core/vm/interface.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ import (
2323
"github.com/ava-labs/libevm/core/types"
2424
"github.com/ava-labs/libevm/params"
2525
"github.com/holiman/uint256"
26+
27+
// libevm extra imports
28+
"github.com/ava-labs/libevm/libevm/stateconf"
2629
)
2730

2831
// StateDB is an EVM database for full state querying.
@@ -45,9 +48,9 @@ type StateDB interface {
4548
SubRefund(uint64)
4649
GetRefund() uint64
4750

48-
GetCommittedState(common.Address, common.Hash) common.Hash
49-
GetState(common.Address, common.Hash) common.Hash
50-
SetState(common.Address, common.Hash, common.Hash)
51+
GetCommittedState(common.Address, common.Hash, ...stateconf.StateDBStateOption) common.Hash
52+
GetState(common.Address, common.Hash, ...stateconf.StateDBStateOption) common.Hash
53+
SetState(common.Address, common.Hash, common.Hash, ...stateconf.StateDBStateOption)
5154

5255
GetTransientState(addr common.Address, key common.Hash) common.Hash
5356
SetTransientState(addr common.Address, key, value common.Hash)

eth/tracers/logger/logger_test.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ import (
2727
"github.com/ava-labs/libevm/core/vm"
2828
"github.com/ava-labs/libevm/params"
2929
"github.com/holiman/uint256"
30+
31+
// libevm extra imports
32+
"github.com/ava-labs/libevm/libevm/stateconf"
3033
)
3134

3235
type dummyContractRef struct {
@@ -49,9 +52,12 @@ type dummyStatedb struct {
4952
state.StateDB
5053
}
5154

52-
func (*dummyStatedb) GetRefund() uint64 { return 1337 }
53-
func (*dummyStatedb) GetState(_ common.Address, _ common.Hash) common.Hash { return common.Hash{} }
54-
func (*dummyStatedb) SetState(_ common.Address, _ common.Hash, _ common.Hash) {}
55+
func (*dummyStatedb) GetRefund() uint64 { return 1337 }
56+
func (*dummyStatedb) GetState(_ common.Address, _ common.Hash, _ ...stateconf.StateDBStateOption) common.Hash {
57+
return common.Hash{}
58+
}
59+
func (*dummyStatedb) SetState(_ common.Address, _ common.Hash, _ common.Hash, _ ...stateconf.StateDBStateOption) {
60+
}
5561

5662
func TestStoreCapture(t *testing.T) {
5763
var (

libevm/libevm.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"github.com/holiman/uint256"
2121

2222
"github.com/ava-labs/libevm/common"
23+
"github.com/ava-labs/libevm/libevm/stateconf"
2324
)
2425

2526
// PrecompiledContract is an exact copy of vm.PrecompiledContract, mirrored here
@@ -43,8 +44,8 @@ type StateReader interface {
4344

4445
GetRefund() uint64
4546

46-
GetCommittedState(common.Address, common.Hash) common.Hash
47-
GetState(common.Address, common.Hash) common.Hash
47+
GetCommittedState(common.Address, common.Hash, ...stateconf.StateDBStateOption) common.Hash
48+
GetState(common.Address, common.Hash, ...stateconf.StateDBStateOption) common.Hash
4849

4950
GetTransientState(addr common.Address, key common.Hash) common.Hash
5051

libevm/stateconf/conf.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,3 +113,27 @@ func ExtractTrieDBUpdatePayload(opts ...TrieDBUpdateOption) (common.Hash, common
113113
}
114114
return *conf.parentBlockHash, *conf.currentBlockHash, true
115115
}
116+
117+
// A StateDBStateOption configures the behaviour of state.StateDB methods for
118+
// getting and setting state: GetState(), GetCommittedState(), and SetState().
119+
type StateDBStateOption = options.Option[stateDBStateConfig]
120+
121+
type stateDBStateConfig struct {
122+
skipKeyTransformation bool
123+
}
124+
125+
// SkipStateKeyTransformation causes any registered state-key transformation
126+
// hook to be ignored. See state.RegisterExtras() for details.
127+
func SkipStateKeyTransformation() StateDBStateOption {
128+
return options.Func[stateDBStateConfig](func(c *stateDBStateConfig) {
129+
c.skipKeyTransformation = true
130+
})
131+
}
132+
133+
// ShouldTransformStateKey parses the options, returning whether or not any
134+
// registered state-key transformation hook should be used; i.e. it returns
135+
// `true` i.f.f. there are no [SkipStateKeyTransformation] options in the
136+
// arguments.
137+
func ShouldTransformStateKey(opts ...StateDBStateOption) bool {
138+
return !options.As(opts...).skipKeyTransformation
139+
}

0 commit comments

Comments
 (0)