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