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