github.com/onflow/flow-go@v0.33.17/engine/execution/computation/query/executor.go (about)

     1  package query
     2  
     3  import (
     4  	"context"
     5  	"encoding/hex"
     6  	"fmt"
     7  	"strings"
     8  	"sync"
     9  	"time"
    10  
    11  	"github.com/onflow/flow-go/fvm/errors"
    12  
    13  	jsoncdc "github.com/onflow/cadence/encoding/json"
    14  	"github.com/rs/zerolog"
    15  
    16  	"github.com/onflow/flow-go/fvm"
    17  	"github.com/onflow/flow-go/fvm/storage/derived"
    18  	"github.com/onflow/flow-go/fvm/storage/snapshot"
    19  	"github.com/onflow/flow-go/model/flow"
    20  	"github.com/onflow/flow-go/module"
    21  	"github.com/onflow/flow-go/utils/debug"
    22  	"github.com/onflow/flow-go/utils/rand"
    23  )
    24  
    25  const (
    26  	DefaultLogTimeThreshold    = 1 * time.Second
    27  	DefaultExecutionTimeLimit  = 10 * time.Second
    28  	DefaultMaxErrorMessageSize = 1000 // 1000 chars
    29  )
    30  
    31  type Executor interface {
    32  	ExecuteScript(
    33  		ctx context.Context,
    34  		script []byte,
    35  		arguments [][]byte,
    36  		blockHeader *flow.Header,
    37  		snapshot snapshot.StorageSnapshot,
    38  	) (
    39  		[]byte,
    40  		error,
    41  	)
    42  
    43  	GetAccount(
    44  		ctx context.Context,
    45  		addr flow.Address,
    46  		header *flow.Header,
    47  		snapshot snapshot.StorageSnapshot,
    48  	) (
    49  		*flow.Account,
    50  		error,
    51  	)
    52  }
    53  
    54  type QueryConfig struct {
    55  	LogTimeThreshold    time.Duration
    56  	ExecutionTimeLimit  time.Duration
    57  	ComputationLimit    uint64
    58  	MaxErrorMessageSize int
    59  }
    60  
    61  func NewDefaultConfig() QueryConfig {
    62  	return QueryConfig{
    63  		LogTimeThreshold:    DefaultLogTimeThreshold,
    64  		ExecutionTimeLimit:  DefaultExecutionTimeLimit,
    65  		ComputationLimit:    fvm.DefaultComputationLimit,
    66  		MaxErrorMessageSize: DefaultMaxErrorMessageSize,
    67  	}
    68  }
    69  
    70  type QueryExecutor struct {
    71  	config           QueryConfig
    72  	logger           zerolog.Logger
    73  	metrics          module.ExecutionMetrics
    74  	vm               fvm.VM
    75  	vmCtx            fvm.Context
    76  	derivedChainData *derived.DerivedChainData
    77  	rngLock          *sync.Mutex
    78  	entropyPerBlock  EntropyProviderPerBlock
    79  }
    80  
    81  var _ Executor = &QueryExecutor{}
    82  
    83  func NewQueryExecutor(
    84  	config QueryConfig,
    85  	logger zerolog.Logger,
    86  	metrics module.ExecutionMetrics,
    87  	vm fvm.VM,
    88  	vmCtx fvm.Context,
    89  	derivedChainData *derived.DerivedChainData,
    90  	entropyPerBlock EntropyProviderPerBlock,
    91  ) *QueryExecutor {
    92  	if config.ComputationLimit > 0 {
    93  		vmCtx = fvm.NewContextFromParent(vmCtx, fvm.WithComputationLimit(config.ComputationLimit))
    94  	}
    95  	return &QueryExecutor{
    96  		config:           config,
    97  		logger:           logger,
    98  		metrics:          metrics,
    99  		vm:               vm,
   100  		vmCtx:            vmCtx,
   101  		derivedChainData: derivedChainData,
   102  		rngLock:          &sync.Mutex{},
   103  		entropyPerBlock:  entropyPerBlock,
   104  	}
   105  }
   106  
   107  func (e *QueryExecutor) ExecuteScript(
   108  	ctx context.Context,
   109  	script []byte,
   110  	arguments [][]byte,
   111  	blockHeader *flow.Header,
   112  	snapshot snapshot.StorageSnapshot,
   113  ) (
   114  	encodedValue []byte,
   115  	err error,
   116  ) {
   117  
   118  	startedAt := time.Now()
   119  	memAllocBefore := debug.GetHeapAllocsBytes()
   120  
   121  	// allocate a random ID to be able to track this script when its done,
   122  	// scripts might not be unique so we use this extra tracker to follow their logs
   123  	// TODO: this is a temporary measure, we could remove this in the future
   124  	if e.logger.Debug().Enabled() {
   125  		e.rngLock.Lock()
   126  		defer e.rngLock.Unlock()
   127  		trackerID, err := rand.Uint32()
   128  		if err != nil {
   129  			return nil, fmt.Errorf("failed to generate trackerID: %w", err)
   130  		}
   131  
   132  		trackedLogger := e.logger.With().Hex("script_hex", script).Uint32("trackerID", trackerID).Logger()
   133  		trackedLogger.Debug().Msg("script is sent for execution")
   134  		defer func() {
   135  			trackedLogger.Debug().Msg("script execution is complete")
   136  		}()
   137  	}
   138  
   139  	requestCtx, cancel := context.WithTimeout(ctx, e.config.ExecutionTimeLimit)
   140  	defer cancel()
   141  
   142  	defer func() {
   143  		prepareLog := func() *zerolog.Event {
   144  			args := make([]string, 0, len(arguments))
   145  			for _, a := range arguments {
   146  				args = append(args, hex.EncodeToString(a))
   147  			}
   148  			return e.logger.Error().
   149  				Hex("script_hex", script).
   150  				Str("args", strings.Join(args, ","))
   151  		}
   152  
   153  		elapsed := time.Since(startedAt)
   154  
   155  		if r := recover(); r != nil {
   156  			prepareLog().
   157  				Interface("recovered", r).
   158  				Msg("script execution caused runtime panic")
   159  
   160  			err = fmt.Errorf("cadence runtime error: %s", r)
   161  			return
   162  		}
   163  		if elapsed >= e.config.LogTimeThreshold {
   164  			prepareLog().
   165  				Dur("duration", elapsed).
   166  				Msg("script execution exceeded threshold")
   167  		}
   168  	}()
   169  
   170  	var output fvm.ProcedureOutput
   171  	_, output, err = e.vm.Run(
   172  		fvm.NewContextFromParent(
   173  			e.vmCtx,
   174  			fvm.WithBlockHeader(blockHeader),
   175  			fvm.WithEntropyProvider(e.entropyPerBlock.AtBlockID(blockHeader.ID())),
   176  			fvm.WithDerivedBlockData(
   177  				e.derivedChainData.NewDerivedBlockDataForScript(blockHeader.ID()))),
   178  		fvm.NewScriptWithContextAndArgs(script, requestCtx, arguments...),
   179  		snapshot)
   180  	if err != nil {
   181  		return nil, fmt.Errorf("failed to execute script (internal error): %w", err)
   182  	}
   183  
   184  	if output.Err != nil {
   185  		return nil, errors.NewCodedError(
   186  			output.Err.Code(),
   187  			"failed to execute script at block (%s): %s", blockHeader.ID(),
   188  			summarizeLog(output.Err.Error(), e.config.MaxErrorMessageSize),
   189  		)
   190  	}
   191  
   192  	encodedValue, err = jsoncdc.Encode(output.Value)
   193  	if err != nil {
   194  		return nil, fmt.Errorf("failed to encode runtime value: %w", err)
   195  	}
   196  
   197  	memAllocAfter := debug.GetHeapAllocsBytes()
   198  	e.metrics.ExecutionScriptExecuted(
   199  		time.Since(startedAt),
   200  		output.ComputationUsed,
   201  		memAllocAfter-memAllocBefore,
   202  		output.MemoryEstimate)
   203  
   204  	return encodedValue, nil
   205  }
   206  
   207  func summarizeLog(log string, limit int) string {
   208  	if limit > 0 && len(log) > limit {
   209  		split := int(limit/2) - 1
   210  		var sb strings.Builder
   211  		sb.WriteString(log[:split])
   212  		sb.WriteString(" ... ")
   213  		sb.WriteString(log[len(log)-split:])
   214  		return sb.String()
   215  	}
   216  	return log
   217  }
   218  
   219  func (e *QueryExecutor) GetAccount(
   220  	ctx context.Context,
   221  	address flow.Address,
   222  	blockHeader *flow.Header,
   223  	snapshot snapshot.StorageSnapshot,
   224  ) (
   225  	*flow.Account,
   226  	error,
   227  ) {
   228  	// TODO(ramtin): utilize ctx
   229  	blockCtx := fvm.NewContextFromParent(
   230  		e.vmCtx,
   231  		fvm.WithBlockHeader(blockHeader),
   232  		fvm.WithDerivedBlockData(
   233  			e.derivedChainData.NewDerivedBlockDataForScript(blockHeader.ID())))
   234  
   235  	account, err := e.vm.GetAccount(
   236  		blockCtx,
   237  		address,
   238  		snapshot)
   239  	if err != nil {
   240  		return nil, fmt.Errorf(
   241  			"failed to get account (%s) at block (%s): %w",
   242  			address.String(),
   243  			blockHeader.ID(),
   244  			err)
   245  	}
   246  
   247  	return account, nil
   248  }