Skip to content

Commit a76daa6

Browse files
authored
Optimize solidity mapping perf (#1221)
* Optimize solidity mapping perf * Update builtin/solidity/mapping.go * fix nil handling of mapping * dont use named returns
1 parent e9bed70 commit a76daa6

6 files changed

Lines changed: 319 additions & 83 deletions

File tree

builtin/solidity/mapping.go

Lines changed: 73 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66
package solidity
77

88
import (
9-
"reflect"
10-
119
"github.com/ethereum/go-ethereum/rlp"
1210

1311
"github.com/vechain/thor/v2/thor"
@@ -19,44 +17,99 @@ type Key interface {
1917

2018
// Mapping is a key/value storage abstraction for built-in contracts, similar to the mapping in Solidity.
2119
// It DOES NOT (TBD) allow for direct access to values if declared in the same `pos` in the built-in contract.
22-
type Mapping[K Key, V any] struct {
20+
//
21+
// Mapping behavior:
22+
//
23+
// - Setting the default type value means removing the value from storage
24+
//
25+
// - If Mapping[thor.Bytes32, *thor.Address]:
26+
// ex: SET (k:0x123, v:nil) -> stores nil
27+
// ex: SET (k:0x123, v:&thor.Address{}) -> stores nil
28+
//
29+
// - If Mapping[thor.Bytes32, thor.Address]:
30+
// ex: SET (k:0x123, v:thor.Address{}) -> stores nil (default empty value)
31+
//
32+
// - Getting a nil storage value will always return default value of V not an empty pointer of the instance
33+
//
34+
// - If Mapping[thor.Bytes32, *thor.Address]:
35+
// ex: SET (k:0x123, v:nil) GET (k:0x123)-> returns nil
36+
// ex: SET (k:0x123, v:&thor.Address{}) GET (k:0x123)-> returns nil
37+
//
38+
// - If Mapping[thor.Bytes32, thor.Address]:
39+
// ex: SET (k:0x123, v:nil) GET (k:0x123)-> returns thor.Address{} (default empty value)
40+
// ex: SET (k:0x123, v:thor.Address{}) GET (k:0x123)-> returns thor.Address{} (default empty value)
41+
//
42+
// - Getting a non-existing storage will always return the default value of the type defined in the V comparable mapping
43+
type Mapping[K Key, V comparable] struct {
2344
context *Context
2445
basePos thor.Bytes32
2546
}
2647

27-
func NewMapping[K Key, V any](context *Context, pos thor.Bytes32) *Mapping[K, V] {
48+
// NewMapping creates a new persistent mapping at the given storage position.
49+
func NewMapping[K Key, V comparable](context *Context, pos thor.Bytes32) *Mapping[K, V] {
2850
return &Mapping[K, V]{context: context, basePos: pos}
2951
}
3052

31-
func (m *Mapping[K, V]) Get(key K) (value V, err error) {
53+
// Get retrieves the value for the given key, charging SloadGas per 32-byte word.
54+
// If no value is present, returns the zero-value of V.
55+
func (m *Mapping[K, V]) Get(key K) (V, error) {
56+
// compute the storage slot from key + base position
3257
position := thor.Blake2b(key.Bytes(), m.basePos.Bytes())
33-
err = m.context.state.DecodeStorage(m.context.address, position, func(raw []byte) error {
34-
if reflect.ValueOf(value).Kind() == reflect.Ptr {
35-
value = reflect.New(reflect.TypeOf(value).Elem()).Interface().(V)
36-
}
37-
if len(raw) == 0 {
38-
return nil
39-
}
40-
slots := (uint64(len(raw)) + 31) / 32
41-
m.context.UseGas(slots * thor.SloadGas)
42-
return rlp.DecodeBytes(raw, &value)
43-
})
44-
return
58+
59+
// prepare a zero-value container for decoding
60+
var value V
61+
62+
// attempt to decode storage into value
63+
err := m.context.state.DecodeStorage(
64+
m.context.address, position, func(raw []byte) error {
65+
if len(raw) == 0 {
66+
// no data, leave value as zero
67+
return nil
68+
}
69+
70+
// charge gas per 32-byte word
71+
slots := (uint64(len(raw)) + 31) / 32
72+
m.context.UseGas(slots * thor.SloadGas)
73+
74+
// decode RLP in-place
75+
return rlp.DecodeBytes(raw, &value)
76+
})
77+
if err != nil {
78+
// return zero type value
79+
var zero V
80+
return zero, err
81+
}
82+
83+
return value, nil
4584
}
4685

86+
// Set stores the given value at key, charging Sstore gas per 32-byte word.
87+
// Passing the zero-value of V clears the storage slot.
4788
func (m *Mapping[K, V]) Set(key K, value V, newValue bool) error {
4889
position := thor.Blake2b(key.Bytes(), m.basePos.Bytes())
90+
91+
// do not RLP-encode nil values, instead set raw storage to nil
92+
var zero V
93+
if value == zero {
94+
m.context.state.SetRawStorage(m.context.address, position, nil)
95+
return nil
96+
}
97+
98+
// encode and store
4999
return m.context.state.EncodeStorage(m.context.address, position, func() ([]byte, error) {
50-
val, err := rlp.EncodeToBytes(value)
100+
// encode via rlp library's internal pooling
101+
buf, err := rlp.EncodeToBytes(value)
51102
if err != nil {
52103
return nil, err
53104
}
54-
slots := (uint64(len(val)) + 31) / 32
105+
106+
// charge gas per 32-byte word
107+
slots := (uint64(len(buf)) + 31) / 32
55108
if newValue {
56109
m.context.UseGas(slots * thor.SstoreSetGas)
57110
} else {
58111
m.context.UseGas(slots * thor.SstoreResetGas)
59112
}
60-
return rlp.EncodeToBytes(value)
113+
return buf, nil
61114
})
62115
}

0 commit comments

Comments
 (0)