-
Notifications
You must be signed in to change notification settings - Fork 17
/
Copy pathstorage.go
308 lines (256 loc) · 9.92 KB
/
storage.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
package migration
import (
"encoding/binary"
"encoding/json"
"math/rand/v2"
"path/filepath"
"testing"
"time"
"github.com/nspcc-dev/neo-go/pkg/config"
"github.com/nspcc-dev/neo-go/pkg/core"
"github.com/nspcc-dev/neo-go/pkg/core/dao"
"github.com/nspcc-dev/neo-go/pkg/core/native"
"github.com/nspcc-dev/neo-go/pkg/core/state"
"github.com/nspcc-dev/neo-go/pkg/core/storage"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
"github.com/nspcc-dev/neo-go/pkg/neotest"
"github.com/nspcc-dev/neo-go/pkg/neotest/chain"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/nns"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/nspcc-dev/neo-go/pkg/vm/vmstate"
"github.com/nspcc-dev/neofs-contract/tests"
"github.com/nspcc-dev/neofs-contract/tests/dump"
"github.com/stretchr/testify/require"
)
// Contract provides part of Neo blockchain services primarily related to the
// NeoFS contract being tested. Initial state of the tested contract is
// initialized from the dump of the blockchain in which it has already been
// deployed (mostly with real data from public networks, but custom options are
// also possible). The contract itself is identified by its name registered in
// the NeoFS NNS. After preparing the test shell of the blockchain from the
// input data, the contract can be updated using the appropriate methods.
// Contract also provides data access interfaces that can be used to ensure that
// data is migrated correctly.
//
// Contract instances must be constructed using NewContract.
type Contract struct {
*neotest.ContractInvoker
id int32
exec *neotest.Executor
bNEF []byte
jManifest []byte
}
// ContractOptions groups various options of NewContract.
type ContractOptions struct {
// Path to the directory containing source code of the tested NeoFS contract.
// Defaults to '../name'.
SourceCodeDir string
// Listener of storage dump of the tested contract. Useful for working with raw
// values that can not be accessed by the contract API.
StorageDumpHandler func(key, value []byte)
}
// NewContract constructs Contract from provided dump.Reader for the named NeoFS
// contract.
//
// The Contract is initialized with all contracts (states and data) from the
// dump.Reader. If you need to process storage items of the tested contract
// before the chain is initialized, use ContractOptions.StorageDumpHandler. If
// set, NewContract passes each key-value item into the function.
//
// By default, new version of the contract executable is compiled from '../name'
// directory. The path can be overridden by ContractOptions.SourceCodeDir.
//
// To work with NNS, NewContract compiles NeoFS NNS contract from the source located
// in '../nns' directory and deploys it.
func NewContract(tb testing.TB, d *dump.Reader, name string, opts ContractOptions) *Contract {
lowLevelStore := storage.NewMemoryStore()
cachedStore := storage.NewMemCachedStore(lowLevelStore) // mem-cached store has sweeter interface
_dao := dao.NewSimple(lowLevelStore, false)
var id int32
found := false
nativeContracts := native.NewContracts(config.ProtocolConfiguration{})
err := nativeContracts.Management.InitializeCache(0, _dao)
require.NoError(tb, err)
mNameToID := make(map[string]int32)
err = d.IterateContractStates(func(_name string, _state state.Contract) {
_state.UpdateCounter = 0 // contract could be dumped as already updated
err = native.PutContractState(_dao, &_state)
require.NoError(tb, err)
if !found {
found = _name == name
if found {
id = _state.ID
}
}
mNameToID[_name] = _state.ID
})
require.NoError(tb, err)
require.True(tb, found)
err = d.IterateContractStorages(func(_name string, key, value []byte) {
if opts.StorageDumpHandler != nil && _name == name {
opts.StorageDumpHandler(key, value)
}
id, ok := mNameToID[_name]
require.True(tb, ok)
storageKey := make([]byte, 5+len(key))
storageKey[0] = byte(_dao.Version.StoragePrefix)
binary.LittleEndian.PutUint32(storageKey[1:], uint32(id))
copy(storageKey[5:], key)
cachedStore.Put(storageKey, value)
})
_, err = _dao.PersistSync()
require.NoError(tb, err)
_, err = cachedStore.PersistSync()
require.NoError(tb, err)
// init test blockchain
useDefaultConfig := func(*config.Blockchain) {}
var blockChain *core.Blockchain
{ // FIXME: hack area, track neo-go#2926
// contracts embedded in the blockchain the moment before are not visible unless
// the blockchain is run twice. At the same time, in order not to clear the
// storage, method Close is overridden.
var run bool // otherwise on tb.Cleanup will panic which is not critical, but not pleasant either
blockChain, _ = chain.NewSingleWithCustomConfigAndStore(tb, useDefaultConfig, nopCloseStore{lowLevelStore}, run)
go blockChain.Run()
blockChain.Close()
}
blockChain, alphabetSigner := chain.NewSingleWithCustomConfigAndStore(tb, useDefaultConfig, lowLevelStore, true)
exec := neotest.NewExecutor(tb, blockChain, alphabetSigner, alphabetSigner)
// deal with NNS
const nnsSourceCodeDir = "../nns"
nnsCtr := neotest.CompileFile(tb, exec.CommitteeHash, nnsSourceCodeDir, filepath.Join(nnsSourceCodeDir, "config.yml"))
// Testing NNS.
if name == "nns" {
bNEF, err := nnsCtr.NEF.Bytes()
require.NoError(tb, err)
jManifest, err := json.Marshal(nnsCtr.Manifest)
require.NoError(tb, err)
return &Contract{
ContractInvoker: exec.NewInvoker(exec.ContractHash(tb, 1), alphabetSigner),
id: 1,
exec: exec,
bNEF: bNEF,
jManifest: jManifest,
}
}
// Testing non-NNS. deploy it then.
exec.DeployContract(tb, nnsCtr,
[]any{
[]any{[]any{"neofs", "ops@morphbits.io"}},
},
)
// compile new contract version
if opts.SourceCodeDir == "" {
opts.SourceCodeDir = filepath.Join("..", name)
}
ctr := neotest.CompileFile(tb, exec.CommitteeHash, opts.SourceCodeDir, filepath.Join(opts.SourceCodeDir, "config.yml"))
bNEF, err := ctr.NEF.Bytes()
require.NoError(tb, err)
jManifest, err := json.Marshal(ctr.Manifest)
require.NoError(tb, err)
return &Contract{
ContractInvoker: exec.NewInvoker(exec.ContractHash(tb, id), alphabetSigner),
id: id,
exec: exec,
bNEF: bNEF,
jManifest: jManifest,
}
}
func (x *Contract) checkUpdate(tb testing.TB, faultException string, args ...any) {
const updateMethod = "update"
if faultException != "" {
x.InvokeFail(tb, faultException, updateMethod, x.bNEF, x.jManifest, args)
return
}
var noResult stackitem.Null
x.Invoke(tb, noResult, updateMethod, x.bNEF, x.jManifest, args)
}
// CheckUpdateSuccess tests that contract update with given arguments succeeds.
// Contract executable (NEF and manifest) is compiled from source code (see
// NewContract for details).
func (x *Contract) CheckUpdateSuccess(tb testing.TB, args ...any) {
x.checkUpdate(tb, "", args...)
}
// CheckUpdateFail tests that contract update with given arguments fails with exact fault
// exception.
//
// See also CheckUpdateSuccess.
func (x *Contract) CheckUpdateFail(tb testing.TB, faultException string, args ...any) {
x.checkUpdate(tb, faultException, args...)
}
func makeTestInvoke(tb testing.TB, inv *neotest.ContractInvoker, method string, args ...any) stackitem.Item {
vmStack, err := inv.TestInvoke(tb, method, args...)
require.NoError(tb, err, "method '%s'", method)
// FIXME: temp hack
res, err := unwrap.Item(&result.Invoke{
State: vmstate.Halt.String(),
Stack: vmStack.ToArray(),
}, nil)
require.NoError(tb, err)
return res
}
// Call tests that calling the contract method with optional arguments succeeds
// and result contains single value. The resulting value is returned as
// stackitem.Item.
//
// Note that Call doesn't change the chain state, so only read (aka safe)
// methods should be used.
func (x *Contract) Call(tb testing.TB, method string, args ...any) stackitem.Item {
return makeTestInvoke(tb, x.ContractInvoker, method, args...)
}
// GetStorageItem returns value stored in the tested contract by key.
func (x *Contract) GetStorageItem(key []byte) []byte {
return x.exec.Chain.GetStorageItem(x.id, key)
}
// SeekStorage calls a provided handler against every stored item that starts
// with a provided prefix. On handler's `false` return stops iteration. prefix
// is removed from the resulting pair's key.
func (x *Contract) SeekStorage(prefix []byte, handler func(k, v []byte) bool) {
x.exec.Chain.SeekStorage(x.id, prefix, handler)
}
// RegisterContractInNNS binds given address to the contract referenced by
// provided name via additional record for the 'name.neofs' domain in the NeoFS
// NNS contract. The method is useful when tested contract uses NNS to access
// other contracts.
//
// Record format can be either Neo address or little-endian HEX string. The
// exact format is selected randomly.
//
// See also nns.Register, nns.AddRecord.
func (x *Contract) RegisterContractInNNS(tb testing.TB, name string, addr util.Uint160) {
nnsInvoker := x.exec.CommitteeInvoker(x.exec.ContractHash(tb, 1))
domain := name + ".neofs"
nnsInvoker.InvokeAndCheck(tb, checkSingleTrueInStack, "register",
domain,
x.exec.CommitteeHash,
"ops@morphbits.io",
int64(3600),
int64(600),
int64(10*365*24*time.Hour/time.Second),
int64(3600),
)
var rec string
if rand.IntN(2) == 0 {
rec = address.Uint160ToString(addr)
} else {
rec = addr.StringLE()
}
var noResult stackitem.Null
nnsInvoker.Invoke(tb, noResult,
"addRecord", domain, int64(nns.TXT), rec,
)
}
// SetInnerRing sets Inner Ring composition using RoleManagement contract. The
// list can be read via InnerRing.
func (x *Contract) SetInnerRing(tb testing.TB, _keys keys.PublicKeys) {
tests.SetInnerRing(tb, x.exec, _keys)
}
// InnerRing reads Inner Ring composition using RoleManagement contract. The
// list can be set via SetInnerRing.
func (x *Contract) InnerRing(tb testing.TB) keys.PublicKeys {
return tests.InnerRing(tb, x.exec)
}