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  }