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  }