-
Notifications
You must be signed in to change notification settings - Fork 178
/
programs.go
406 lines (348 loc) · 12.6 KB
/
programs.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
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
package environment
import (
"fmt"
"github.com/hashicorp/go-multierror"
"github.com/onflow/cadence"
jsoncdc "github.com/onflow/cadence/encoding/json"
"github.com/onflow/cadence/runtime/common"
"github.com/onflow/cadence/runtime/interpreter"
"github.com/onflow/flow-go/fvm/errors"
"github.com/onflow/flow-go/fvm/storage"
"github.com/onflow/flow-go/fvm/storage/derived"
"github.com/onflow/flow-go/fvm/storage/state"
"github.com/onflow/flow-go/fvm/tracing"
"github.com/onflow/flow-go/module/trace"
)
// Programs manages operations around cadence program parsing.
//
// Note that cadence guarantees that Get/Set methods are called in a LIFO
// manner. Hence, we create new nested transactions on Get calls and commit
// these nested transactions on Set calls in order to capture the states
// needed for parsing the programs.
type Programs struct {
tracer tracing.TracerSpan
meter Meter
metrics MetricsReporter
txnState storage.TransactionPreparer
accounts Accounts
// NOTE: non-address programs are not reusable across transactions, hence
// they are kept out of the derived data database.
nonAddressPrograms map[common.Location]*interpreter.Program
// dependencyStack tracks programs currently being loaded and their dependencies.
dependencyStack *dependencyStack
}
// NewPrograms constructs a new ProgramHandler
func NewPrograms(
tracer tracing.TracerSpan,
meter Meter,
metrics MetricsReporter,
txnState storage.TransactionPreparer,
accounts Accounts,
) *Programs {
return &Programs{
tracer: tracer,
meter: meter,
metrics: metrics,
txnState: txnState,
accounts: accounts,
nonAddressPrograms: make(map[common.Location]*interpreter.Program),
dependencyStack: newDependencyStack(),
}
}
// Reset resets the program cache.
// this is called if the transactions happy path fails.
func (programs *Programs) Reset() {
programs.nonAddressPrograms = make(map[common.Location]*interpreter.Program)
programs.dependencyStack = newDependencyStack()
}
// GetOrLoadProgram gets the program from the cache,
// or loads it (by calling load) if it is not in the cache.
// When loading a program, this method will be re-entered
// to load the dependencies of the program.
func (programs *Programs) GetOrLoadProgram(
location common.Location,
load func() (*interpreter.Program, error),
) (*interpreter.Program, error) {
defer programs.tracer.StartChildSpan(trace.FVMEnvGetOrLoadProgram).End()
err := programs.meter.MeterComputation(ComputationKindGetOrLoadProgram, 1)
if err != nil {
return nil, fmt.Errorf("get program failed: %w", err)
}
// non-address location program is not reusable across transactions.
switch location := location.(type) {
case common.AddressLocation:
return programs.getOrLoadAddressProgram(location, load)
default:
return programs.getOrLoadNonAddressProgram(location, load)
}
}
func (programs *Programs) getOrLoadAddressProgram(
location common.AddressLocation,
load func() (*interpreter.Program, error),
) (*interpreter.Program, error) {
top, err := programs.dependencyStack.top()
if err != nil {
return nil, err
}
if top.ContainsLocation(location) {
// this dependency has already been seen in the current stack/scope
// this means that it is safe to just fetch it and not reapply
// state/metering changes
program, ok := programs.txnState.GetProgram(location)
if !ok {
// program should be in the cache, if it is not,
// this means there is an implementation error
return nil, errors.NewDerivedDataCacheImplementationFailure(
fmt.Errorf("expected program missing"+
" in cache for location: %s", location))
}
err := programs.dependencyStack.add(program.Dependencies)
if err != nil {
return nil, err
}
programs.cacheHit()
return program.Program, nil
}
loader := newProgramLoader(load, programs.dependencyStack, location)
program, err := programs.txnState.GetOrComputeProgram(
programs.txnState,
location,
loader,
)
if err != nil {
return nil, fmt.Errorf("error getting program %v: %w", location, err)
}
// Add dependencies to the stack.
// This is only really needed if loader was not called,
// but there is no harm in doing it always.
err = programs.dependencyStack.add(program.Dependencies)
if err != nil {
return nil, err
}
if loader.Called() {
programs.cacheMiss()
} else {
programs.cacheHit()
}
return program.Program, nil
}
func (programs *Programs) getOrLoadNonAddressProgram(
location common.Location,
load func() (*interpreter.Program, error),
) (*interpreter.Program, error) {
program, ok := programs.nonAddressPrograms[location]
if ok {
return program, nil
}
program, err := load()
if err != nil {
return nil, err
}
programs.nonAddressPrograms[location] = program
return program, nil
}
func (programs *Programs) DecodeArgument(
bytes []byte,
_ cadence.Type,
) (
cadence.Value,
error,
) {
defer programs.tracer.StartExtensiveTracingChildSpan(
trace.FVMEnvDecodeArgument).End()
v, err := jsoncdc.Decode(programs.meter, bytes)
if err != nil {
return nil, fmt.Errorf(
"decodeing argument failed: %w",
errors.NewInvalidArgumentErrorf(
"argument is not json decodable: %w",
err))
}
return v, err
}
func (programs *Programs) cacheHit() {
programs.metrics.RuntimeTransactionProgramsCacheHit()
}
func (programs *Programs) cacheMiss() {
programs.metrics.RuntimeTransactionProgramsCacheMiss()
}
// programLoader is used to load a program from a location.
type programLoader struct {
loadFunc func() (*interpreter.Program, error)
dependencyStack *dependencyStack
called bool
location common.AddressLocation
}
var _ derived.ValueComputer[common.AddressLocation, *derived.Program] = (*programLoader)(nil)
func newProgramLoader(
loadFunc func() (*interpreter.Program, error),
dependencyStack *dependencyStack,
location common.AddressLocation,
) *programLoader {
return &programLoader{
loadFunc: loadFunc,
dependencyStack: dependencyStack,
// called will be true if the loader was called.
called: false,
location: location,
}
}
func (loader *programLoader) Compute(
txState state.NestedTransactionPreparer,
location common.AddressLocation,
) (
*derived.Program,
error,
) {
if loader.called {
// This should never happen, as the program loader is only called once per
// program. The same loader is never reused. This is only here to make
// this more apparent.
return nil,
errors.NewDerivedDataCacheImplementationFailure(
fmt.Errorf("program loader called twice"))
}
if loader.location != location {
// This should never happen, as the program loader constructed specifically
// to load one location once. This is only a sanity check.
return nil,
errors.NewDerivedDataCacheImplementationFailure(
fmt.Errorf("program loader called with unexpected location"))
}
loader.called = true
interpreterProgram, dependencies, err :=
loader.loadWithDependencyTracking(location, loader.loadFunc)
if err != nil {
return nil, fmt.Errorf("load program failed: %w", err)
}
return &derived.Program{
Program: interpreterProgram,
Dependencies: dependencies,
}, nil
}
func (loader *programLoader) Called() bool {
return loader.called
}
func (loader *programLoader) loadWithDependencyTracking(
address common.AddressLocation,
load func() (*interpreter.Program, error),
) (
*interpreter.Program,
derived.ProgramDependencies,
error,
) {
// this program is not in cache, so we need to load it into the cache.
// tho have proper invalidation, we need to track the dependencies of the program.
// If this program depends on another program,
// that program will be loaded before this one finishes loading (calls set).
// That is why this is a stack.
loader.dependencyStack.push(address)
program, err := load()
// Get collected dependencies of the loaded program.
// Pop the dependencies from the stack even if loading errored.
stackLocation, dependencies, depErr := loader.dependencyStack.pop()
if depErr != nil {
err = multierror.Append(err, depErr).ErrorOrNil()
}
if err != nil {
return nil, derived.NewProgramDependencies(), err
}
if stackLocation != address {
// This should never happen, and indicates an implementation error.
// GetProgram and SetProgram should be always called in pair, this check depends on this assumption.
// Get pushes the stack and set pops the stack.
// Example: if loading B that depends on A (and none of them are in cache yet),
// - get(A): pushes A
// - get(B): pushes B
// - set(B): pops B
// - set(A): pops A
// Note: technically this check is redundant as `CommitParseRestricted` also has a similar check.
return nil, derived.NewProgramDependencies(), fmt.Errorf(
"cannot set program. Popped dependencies are for an unexpeced address"+
" (expected %s, got %s)", address, stackLocation)
}
return program, dependencies, nil
}
// dependencyTracker tracks dependencies for a location
// Or in other words it builds up a list of dependencies for the program being loaded.
// If a program imports another program (A imports B), then B is a dependency of A.
// Assuming that if A imports B which imports C (already in cache), the loading process looks like this:
// - get(A): not in cache, so push A to tracker to start tracking dependencies for A.
// We can be assured that set(A) will eventually be called.
// - get(B): not in cache, push B
// - get(C): in cache, do no push C, just add C's dependencies to the tracker (C's dependencies are also in the cache)
// - set(B): pop B, getting all the collected dependencies for B, and add B's dependencies to the tracker
// (because A also depends on everything B depends on)
// - set(A): pop A, getting all the collected dependencies for A
type dependencyTracker struct {
location common.Location
dependencies derived.ProgramDependencies
}
// dependencyStack is a stack of dependencyTracker
// It is used during loading a program to create a dependency list for each program
type dependencyStack struct {
trackers []dependencyTracker
}
func newDependencyStack() *dependencyStack {
stack := &dependencyStack{
trackers: make([]dependencyTracker, 0),
}
// The root of the stack is the program (script/transaction) that is being executed.
// At the end of the transaction execution, this will hold all the dependencies
// of the script/transaction.
//
// The root of the stack should never be popped.
stack.push(common.StringLocation("^ProgramDependencyStackRoot$"))
return stack
}
// push a new location to track dependencies for.
// it is assumed that the dependencies will be loaded before the program is set and pop is called.
func (s *dependencyStack) push(loc common.Location) {
dependencies := derived.NewProgramDependencies()
// A program is listed as its own dependency.
dependencies.Add(loc)
s.trackers = append(s.trackers, dependencyTracker{
location: loc,
dependencies: dependencies,
})
}
// add adds dependencies to the current dependency tracker
func (s *dependencyStack) add(dependencies derived.ProgramDependencies) error {
l := len(s.trackers)
if l == 0 {
// This cannot happen, as the root of the stack is always present.
return errors.NewDerivedDataCacheImplementationFailure(
fmt.Errorf("dependency stack unexpectedly empty while calling add"))
}
s.trackers[l-1].dependencies.Merge(dependencies)
return nil
}
// pop the last dependencies on the stack and return them.
func (s *dependencyStack) pop() (common.Location, derived.ProgramDependencies, error) {
if len(s.trackers) <= 1 {
return nil,
derived.NewProgramDependencies(),
errors.NewDerivedDataCacheImplementationFailure(
fmt.Errorf("cannot pop the programs" +
" dependency stack, because it is empty"))
}
// pop the last tracker
tracker := s.trackers[len(s.trackers)-1]
s.trackers = s.trackers[:len(s.trackers)-1]
// Add the dependencies of the popped tracker to the parent tracker
// This is an optimisation to avoid having to iterate through the entire stack
// everytime a dependency is pushed or added, instead we add the popped dependencies to the new top of the stack.
// (because if C depends on B which depends on A, A's dependencies include C).
s.trackers[len(s.trackers)-1].dependencies.Merge(tracker.dependencies)
return tracker.location, tracker.dependencies, nil
}
// top returns the last dependencies on the stack without pop-ing them.
func (s *dependencyStack) top() (derived.ProgramDependencies, error) {
l := len(s.trackers)
if l == 0 {
// This cannot happen, as the root of the stack is always present.
return derived.ProgramDependencies{}, errors.NewDerivedDataCacheImplementationFailure(
fmt.Errorf("dependency stack unexpectedly empty while calling top"))
}
return s.trackers[len(s.trackers)-1].dependencies, nil
}