github.com/onflow/flow-go@v0.33.17/fvm/environment/programs.go (about) 1 package environment 2 3 import ( 4 "fmt" 5 6 "github.com/hashicorp/go-multierror" 7 8 "github.com/onflow/cadence" 9 jsoncdc "github.com/onflow/cadence/encoding/json" 10 "github.com/onflow/cadence/runtime/common" 11 "github.com/onflow/cadence/runtime/interpreter" 12 13 "github.com/onflow/flow-go/fvm/errors" 14 "github.com/onflow/flow-go/fvm/storage" 15 "github.com/onflow/flow-go/fvm/storage/derived" 16 "github.com/onflow/flow-go/fvm/storage/state" 17 "github.com/onflow/flow-go/fvm/tracing" 18 "github.com/onflow/flow-go/module/trace" 19 ) 20 21 // Programs manages operations around cadence program parsing. 22 // 23 // Note that cadence guarantees that Get/Set methods are called in a LIFO 24 // manner. Hence, we create new nested transactions on Get calls and commit 25 // these nested transactions on Set calls in order to capture the states 26 // needed for parsing the programs. 27 type Programs struct { 28 tracer tracing.TracerSpan 29 meter Meter 30 metrics MetricsReporter 31 32 txnState storage.TransactionPreparer 33 accounts Accounts 34 35 // NOTE: non-address programs are not reusable across transactions, hence 36 // they are kept out of the derived data database. 37 nonAddressPrograms map[common.Location]*interpreter.Program 38 39 // dependencyStack tracks programs currently being loaded and their dependencies. 40 dependencyStack *dependencyStack 41 } 42 43 // NewPrograms constructs a new ProgramHandler 44 func NewPrograms( 45 tracer tracing.TracerSpan, 46 meter Meter, 47 metrics MetricsReporter, 48 txnState storage.TransactionPreparer, 49 accounts Accounts, 50 ) *Programs { 51 return &Programs{ 52 tracer: tracer, 53 meter: meter, 54 metrics: metrics, 55 txnState: txnState, 56 accounts: accounts, 57 nonAddressPrograms: make(map[common.Location]*interpreter.Program), 58 dependencyStack: newDependencyStack(), 59 } 60 } 61 62 // Reset resets the program cache. 63 // this is called if the transactions happy path fails. 64 func (programs *Programs) Reset() { 65 programs.nonAddressPrograms = make(map[common.Location]*interpreter.Program) 66 programs.dependencyStack = newDependencyStack() 67 } 68 69 // GetOrLoadProgram gets the program from the cache, 70 // or loads it (by calling load) if it is not in the cache. 71 // When loading a program, this method will be re-entered 72 // to load the dependencies of the program. 73 func (programs *Programs) GetOrLoadProgram( 74 location common.Location, 75 load func() (*interpreter.Program, error), 76 ) (*interpreter.Program, error) { 77 defer programs.tracer.StartChildSpan(trace.FVMEnvGetOrLoadProgram).End() 78 err := programs.meter.MeterComputation(ComputationKindGetOrLoadProgram, 1) 79 if err != nil { 80 return nil, fmt.Errorf("get program failed: %w", err) 81 } 82 83 // non-address location program is not reusable across transactions. 84 switch location := location.(type) { 85 case common.AddressLocation: 86 return programs.getOrLoadAddressProgram(location, load) 87 default: 88 return programs.getOrLoadNonAddressProgram(location, load) 89 } 90 } 91 92 func (programs *Programs) getOrLoadAddressProgram( 93 location common.AddressLocation, 94 load func() (*interpreter.Program, error), 95 ) (*interpreter.Program, error) { 96 top, err := programs.dependencyStack.top() 97 if err != nil { 98 return nil, err 99 } 100 101 if top.ContainsLocation(location) { 102 // this dependency has already been seen in the current stack/scope 103 // this means that it is safe to just fetch it and not reapply 104 // state/metering changes 105 program, ok := programs.txnState.GetProgram(location) 106 if !ok { 107 // program should be in the cache, if it is not, 108 // this means there is an implementation error 109 return nil, errors.NewDerivedDataCacheImplementationFailure( 110 fmt.Errorf("expected program missing"+ 111 " in cache for location: %s", location)) 112 } 113 err := programs.dependencyStack.add(program.Dependencies) 114 if err != nil { 115 return nil, err 116 } 117 programs.cacheHit() 118 119 return program.Program, nil 120 } 121 122 loader := newProgramLoader(load, programs.dependencyStack, location) 123 program, err := programs.txnState.GetOrComputeProgram( 124 programs.txnState, 125 location, 126 loader, 127 ) 128 if err != nil { 129 return nil, fmt.Errorf("error getting program %v: %w", location, err) 130 } 131 132 // Add dependencies to the stack. 133 // This is only really needed if loader was not called, 134 // but there is no harm in doing it always. 135 err = programs.dependencyStack.add(program.Dependencies) 136 if err != nil { 137 return nil, err 138 } 139 140 if loader.Called() { 141 programs.cacheMiss() 142 } else { 143 programs.cacheHit() 144 } 145 146 return program.Program, nil 147 } 148 149 func (programs *Programs) getOrLoadNonAddressProgram( 150 location common.Location, 151 load func() (*interpreter.Program, error), 152 ) (*interpreter.Program, error) { 153 program, ok := programs.nonAddressPrograms[location] 154 if ok { 155 return program, nil 156 } 157 158 program, err := load() 159 if err != nil { 160 return nil, err 161 } 162 163 programs.nonAddressPrograms[location] = program 164 return program, nil 165 } 166 167 func (programs *Programs) DecodeArgument( 168 bytes []byte, 169 _ cadence.Type, 170 ) ( 171 cadence.Value, 172 error, 173 ) { 174 defer programs.tracer.StartExtensiveTracingChildSpan( 175 trace.FVMEnvDecodeArgument).End() 176 177 v, err := jsoncdc.Decode(programs.meter, bytes) 178 if err != nil { 179 return nil, fmt.Errorf( 180 "decodeing argument failed: %w", 181 errors.NewInvalidArgumentErrorf( 182 "argument is not json decodable: %w", 183 err)) 184 } 185 186 return v, err 187 } 188 189 func (programs *Programs) cacheHit() { 190 programs.metrics.RuntimeTransactionProgramsCacheHit() 191 } 192 193 func (programs *Programs) cacheMiss() { 194 programs.metrics.RuntimeTransactionProgramsCacheMiss() 195 } 196 197 // programLoader is used to load a program from a location. 198 type programLoader struct { 199 loadFunc func() (*interpreter.Program, error) 200 dependencyStack *dependencyStack 201 called bool 202 location common.AddressLocation 203 } 204 205 var _ derived.ValueComputer[common.AddressLocation, *derived.Program] = (*programLoader)(nil) 206 207 func newProgramLoader( 208 loadFunc func() (*interpreter.Program, error), 209 dependencyStack *dependencyStack, 210 location common.AddressLocation, 211 ) *programLoader { 212 return &programLoader{ 213 loadFunc: loadFunc, 214 dependencyStack: dependencyStack, 215 // called will be true if the loader was called. 216 called: false, 217 location: location, 218 } 219 } 220 221 func (loader *programLoader) Compute( 222 _ state.NestedTransactionPreparer, 223 location common.AddressLocation, 224 ) ( 225 *derived.Program, 226 error, 227 ) { 228 if loader.called { 229 // This should never happen, as the program loader is only called once per 230 // program. The same loader is never reused. This is only here to make 231 // this more apparent. 232 return nil, 233 errors.NewDerivedDataCacheImplementationFailure( 234 fmt.Errorf("program loader called twice")) 235 } 236 237 if loader.location != location { 238 // This should never happen, as the program loader constructed specifically 239 // to load one location once. This is only a sanity check. 240 return nil, 241 errors.NewDerivedDataCacheImplementationFailure( 242 fmt.Errorf("program loader called with unexpected location")) 243 } 244 245 loader.called = true 246 247 interpreterProgram, dependencies, err := 248 loader.loadWithDependencyTracking(location, loader.loadFunc) 249 if err != nil { 250 return nil, fmt.Errorf("load program failed: %w", err) 251 } 252 253 return &derived.Program{ 254 Program: interpreterProgram, 255 Dependencies: dependencies, 256 }, nil 257 } 258 259 func (loader *programLoader) Called() bool { 260 return loader.called 261 } 262 263 func (loader *programLoader) loadWithDependencyTracking( 264 address common.AddressLocation, 265 load func() (*interpreter.Program, error), 266 ) ( 267 *interpreter.Program, 268 derived.ProgramDependencies, 269 error, 270 ) { 271 // this program is not in cache, so we need to load it into the cache. 272 // to have proper invalidation, we need to track the dependencies of the program. 273 // If this program depends on another program, 274 // that program will be loaded before this one finishes loading (calls set). 275 // That is why this is a stack. 276 loader.dependencyStack.push(address) 277 278 program, err := load() 279 280 // Get collected dependencies of the loaded program. 281 // Pop the dependencies from the stack even if loading errored. 282 // 283 // In case of an error, the dependencies of the errored program should not be merged 284 // into the dependencies of the parent program. This is to prevent the parent program 285 // from thinking that this program was already loaded and is in the cache, 286 // if it requests it again. 287 merge := err == nil 288 stackLocation, dependencies, depErr := loader.dependencyStack.pop(merge) 289 if depErr != nil { 290 err = multierror.Append(err, depErr).ErrorOrNil() 291 } 292 293 if err != nil { 294 return nil, derived.NewProgramDependencies(), err 295 } 296 297 if stackLocation != address { 298 // This should never happen, and indicates an implementation error. 299 // GetProgram and SetProgram should be always called in pair, this check depends on this assumption. 300 // Get pushes the stack and set pops the stack. 301 // Example: if loading B that depends on A (and none of them are in cache yet), 302 // - get(A): pushes A 303 // - get(B): pushes B 304 // - set(B): pops B 305 // - set(A): pops A 306 // Note: technically this check is redundant as `CommitParseRestricted` also has a similar check. 307 return nil, derived.NewProgramDependencies(), fmt.Errorf( 308 "cannot set program. Popped dependencies are for an unexpeced address"+ 309 " (expected %s, got %s)", address, stackLocation) 310 } 311 return program, dependencies, nil 312 } 313 314 // dependencyTracker tracks dependencies for a location 315 // Or in other words it builds up a list of dependencies for the program being loaded. 316 // If a program imports another program (A imports B), then B is a dependency of A. 317 // Assuming that if A imports B which imports C (already in cache), the loading process looks like this: 318 // - get(A): not in cache, so push A to tracker to start tracking dependencies for A. 319 // We can be assured that set(A) will eventually be called. 320 // - get(B): not in cache, push B 321 // - get(C): in cache, do no push C, just add C's dependencies to the tracker (C's dependencies are also in the cache) 322 // - set(B): pop B, getting all the collected dependencies for B, and add B's dependencies to the tracker 323 // (because A also depends on everything B depends on) 324 // - set(A): pop A, getting all the collected dependencies for A 325 type dependencyTracker struct { 326 location common.Location 327 dependencies derived.ProgramDependencies 328 } 329 330 // dependencyStack is a stack of dependencyTracker 331 // It is used during loading a program to create a dependency list for each program 332 type dependencyStack struct { 333 trackers []dependencyTracker 334 } 335 336 func newDependencyStack() *dependencyStack { 337 stack := &dependencyStack{ 338 trackers: make([]dependencyTracker, 0), 339 } 340 341 // The root of the stack is the program (script/transaction) that is being executed. 342 // At the end of the transaction execution, this will hold all the dependencies 343 // of the script/transaction. 344 // 345 // The root of the stack should never be popped. 346 stack.push(common.StringLocation("^ProgramDependencyStackRoot$")) 347 348 return stack 349 } 350 351 // push a new location to track dependencies for. 352 // it is assumed that the dependencies will be loaded before the program is set and pop is called. 353 func (s *dependencyStack) push(loc common.Location) { 354 dependencies := derived.NewProgramDependencies() 355 356 // A program is listed as its own dependency. 357 dependencies.Add(loc) 358 359 s.trackers = append(s.trackers, dependencyTracker{ 360 location: loc, 361 dependencies: dependencies, 362 }) 363 } 364 365 // add adds dependencies to the current dependency tracker 366 func (s *dependencyStack) add(dependencies derived.ProgramDependencies) error { 367 l := len(s.trackers) 368 if l == 0 { 369 // This cannot happen, as the root of the stack is always present. 370 return errors.NewDerivedDataCacheImplementationFailure( 371 fmt.Errorf("dependency stack unexpectedly empty while calling add")) 372 } 373 374 s.trackers[l-1].dependencies.Merge(dependencies) 375 return nil 376 } 377 378 // pop the last dependencies on the stack and return them. 379 // if merge is false then the dependencies are not merged into the parent tracker. 380 // this is used to pop the dependencies of a program that errored during loading. 381 func (s *dependencyStack) pop(merge bool) (common.Location, derived.ProgramDependencies, error) { 382 if len(s.trackers) <= 1 { 383 return nil, 384 derived.NewProgramDependencies(), 385 errors.NewDerivedDataCacheImplementationFailure( 386 fmt.Errorf("cannot pop the programs" + 387 " dependency stack, because it is empty")) 388 } 389 390 // pop the last tracker 391 tracker := s.trackers[len(s.trackers)-1] 392 s.trackers = s.trackers[:len(s.trackers)-1] 393 394 if merge { 395 // Add the dependencies of the popped tracker to the parent tracker 396 // This is an optimisation to avoid having to iterate through the entire stack 397 // everytime a dependency is pushed or added, instead we add the popped dependencies to the new top of the stack. 398 // (because if C depends on B which depends on A, A's dependencies include C). 399 s.trackers[len(s.trackers)-1].dependencies.Merge(tracker.dependencies) 400 } 401 402 return tracker.location, tracker.dependencies, nil 403 } 404 405 // top returns the last dependencies on the stack without pop-ing them. 406 func (s *dependencyStack) top() (derived.ProgramDependencies, error) { 407 l := len(s.trackers) 408 if l == 0 { 409 // This cannot happen, as the root of the stack is always present. 410 return derived.ProgramDependencies{}, errors.NewDerivedDataCacheImplementationFailure( 411 fmt.Errorf("dependency stack unexpectedly empty while calling top")) 412 } 413 414 return s.trackers[len(s.trackers)-1].dependencies, nil 415 }