github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/engine/access/rpc/backend/script_comparer.go (about)

     1  package backend
     2  
     3  import (
     4  	"bytes"
     5  	"strings"
     6  	"time"
     7  
     8  	"github.com/rs/zerolog"
     9  	"google.golang.org/grpc/codes"
    10  	"google.golang.org/grpc/status"
    11  
    12  	"github.com/onflow/flow-go/module"
    13  )
    14  
    15  const (
    16  	executeErrorPrefix = "failed to execute script at block"
    17  )
    18  
    19  type scriptResult struct {
    20  	result   []byte
    21  	duration time.Duration
    22  	err      error
    23  }
    24  
    25  func newScriptResult(result []byte, duration time.Duration, err error) *scriptResult {
    26  	return &scriptResult{
    27  		result:   result,
    28  		duration: duration,
    29  		err:      err,
    30  	}
    31  }
    32  
    33  type scriptResultComparison struct {
    34  	log     zerolog.Logger
    35  	metrics module.BackendScriptsMetrics
    36  	request *scriptExecutionRequest
    37  }
    38  
    39  func newScriptResultComparison(
    40  	log zerolog.Logger,
    41  	metrics module.BackendScriptsMetrics,
    42  	request *scriptExecutionRequest,
    43  ) *scriptResultComparison {
    44  	return &scriptResultComparison{
    45  		log:     log,
    46  		metrics: metrics,
    47  		request: request,
    48  	}
    49  }
    50  
    51  func (c *scriptResultComparison) compare(execResult, localResult *scriptResult) bool {
    52  	// record errors caused by missing local data
    53  	if isOutOfRangeError(localResult.err) {
    54  		c.metrics.ScriptExecutionNotIndexed()
    55  		c.logComparison(execResult, localResult,
    56  			"script execution results do not match EN because data is not indexed yet", false)
    57  		return false
    58  	}
    59  
    60  	// check errors first
    61  	if execResult.err != nil {
    62  		if compareErrors(execResult.err, localResult.err) {
    63  			c.metrics.ScriptExecutionErrorMatch()
    64  			return true
    65  		}
    66  
    67  		c.metrics.ScriptExecutionErrorMismatch()
    68  		c.logComparison(execResult, localResult,
    69  			"cadence errors from local execution do not match EN", true)
    70  		return false
    71  	}
    72  
    73  	if bytes.Equal(execResult.result, localResult.result) {
    74  		c.metrics.ScriptExecutionResultMatch()
    75  		return true
    76  	}
    77  
    78  	c.metrics.ScriptExecutionResultMismatch()
    79  	c.logComparison(execResult, localResult,
    80  		"script execution results from local execution do not match EN", true)
    81  	return false
    82  }
    83  
    84  // logScriptExecutionComparison logs the script execution comparison between local execution and execution node
    85  func (c *scriptResultComparison) logComparison(execResult, localResult *scriptResult, msg string, useError bool) {
    86  	args := make([]string, len(c.request.arguments))
    87  	for i, arg := range c.request.arguments {
    88  		args[i] = string(arg)
    89  	}
    90  
    91  	lgCtx := c.log.With().
    92  		Hex("block_id", c.request.blockID[:]).
    93  		Hex("script_hash", c.request.insecureScriptHash[:]).
    94  		Str("script", string(c.request.script)).
    95  		Strs("args", args)
    96  
    97  	if execResult.err != nil {
    98  		lgCtx = lgCtx.AnErr("execution_node_error", execResult.err)
    99  	} else {
   100  		lgCtx = lgCtx.Hex("execution_node_result", execResult.result)
   101  	}
   102  	lgCtx = lgCtx.Dur("execution_node_duration_ms", execResult.duration)
   103  
   104  	if localResult.err != nil {
   105  		lgCtx = lgCtx.AnErr("local_error", localResult.err)
   106  	} else {
   107  		lgCtx = lgCtx.Hex("local_result", localResult.result)
   108  	}
   109  	lgCtx = lgCtx.Dur("local_duration_ms", localResult.duration)
   110  
   111  	lg := lgCtx.Logger()
   112  	if useError {
   113  		lg.Error().Msg(msg)
   114  	} else {
   115  		lg.Debug().Msg(msg)
   116  	}
   117  }
   118  
   119  func isOutOfRangeError(err error) bool {
   120  	return status.Code(err) == codes.OutOfRange
   121  }
   122  
   123  func compareErrors(execErr, localErr error) bool {
   124  	if execErr == localErr {
   125  		return true
   126  	}
   127  
   128  	// if the status code is different, then they definitely don't match
   129  	if status.Code(execErr) != status.Code(localErr) {
   130  		return false
   131  	}
   132  
   133  	// absolute error strings generally won't match since the code paths are slightly different
   134  	// check if the original error is the same by removing unneeded error wrapping.
   135  	return containsError(execErr, localErr)
   136  }
   137  
   138  func containsError(execErr, localErr error) bool {
   139  	// both script execution implementations use the same engine, which adds
   140  	// "failed to execute script at block" to the message before returning. Any characters
   141  	// before this can be ignored. The string that comes after is the original error and
   142  	// should match.
   143  	execErrStr := trimErrorPrefix(execErr)
   144  	localErrStr := trimErrorPrefix(localErr)
   145  
   146  	if execErrStr == localErrStr {
   147  		return true
   148  	}
   149  
   150  	// by default ENs are configured with longer script error size limits, which means that the AN's
   151  	// error may be truncated. check if the non-truncated parts match.
   152  	subParts := strings.Split(localErrStr, " ... ")
   153  
   154  	return len(subParts) == 2 &&
   155  		strings.HasPrefix(execErrStr, subParts[0]) &&
   156  		strings.HasSuffix(execErrStr, subParts[1])
   157  }
   158  
   159  func trimErrorPrefix(err error) string {
   160  	if err == nil {
   161  		return ""
   162  	}
   163  
   164  	parts := strings.Split(err.Error(), executeErrorPrefix)
   165  	if len(parts) != 2 {
   166  		return err.Error()
   167  	}
   168  
   169  	return parts[1]
   170  }