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 }