-
Notifications
You must be signed in to change notification settings - Fork 175
/
backend_scripts.go
183 lines (160 loc) · 5.91 KB
/
backend_scripts.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
171
172
173
174
175
176
177
178
179
180
181
182
183
package backend
import (
"context"
"crypto/md5" //nolint:gosec
"time"
lru "github.com/hashicorp/golang-lru"
"github.com/hashicorp/go-multierror"
execproto "github.com/onflow/flow/protobuf/go/flow/execution"
"github.com/rs/zerolog"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/onflow/flow-go/engine/common/rpc"
"github.com/onflow/flow-go/model/flow"
"github.com/onflow/flow-go/module"
"github.com/onflow/flow-go/state/protocol"
"github.com/onflow/flow-go/storage"
)
// uniqueScriptLoggingTimeWindow is the duration for checking the uniqueness of scripts sent for execution
const uniqueScriptLoggingTimeWindow = 10 * time.Minute
type backendScripts struct {
headers storage.Headers
executionReceipts storage.ExecutionReceipts
state protocol.State
connFactory ConnectionFactory
log zerolog.Logger
metrics module.BackendScriptsMetrics
loggedScripts *lru.Cache
}
func (b *backendScripts) ExecuteScriptAtLatestBlock(
ctx context.Context,
script []byte,
arguments [][]byte,
) ([]byte, error) {
// get the latest sealed header
latestHeader, err := b.state.Sealed().Head()
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get latest sealed header: %v", err)
}
// get the block id of the latest sealed header
latestBlockID := latestHeader.ID()
// execute script on the execution node at that block id
return b.executeScriptOnExecutionNode(ctx, latestBlockID, script, arguments)
}
func (b *backendScripts) ExecuteScriptAtBlockID(
ctx context.Context,
blockID flow.Identifier,
script []byte,
arguments [][]byte,
) ([]byte, error) {
// execute script on the execution node at that block id
return b.executeScriptOnExecutionNode(ctx, blockID, script, arguments)
}
func (b *backendScripts) ExecuteScriptAtBlockHeight(
ctx context.Context,
blockHeight uint64,
script []byte,
arguments [][]byte,
) ([]byte, error) {
// get header at given height
header, err := b.headers.ByHeight(blockHeight)
if err != nil {
err = rpc.ConvertStorageError(err)
return nil, err
}
blockID := header.ID()
// execute script on the execution node at that block id
return b.executeScriptOnExecutionNode(ctx, blockID, script, arguments)
}
// executeScriptOnExecutionNode forwards the request to the execution node using the execution node
// grpc client and converts the response back to the access node api response format
func (b *backendScripts) executeScriptOnExecutionNode(
ctx context.Context,
blockID flow.Identifier,
script []byte,
arguments [][]byte,
) ([]byte, error) {
execReq := execproto.ExecuteScriptAtBlockIDRequest{
BlockId: blockID[:],
Script: script,
Arguments: arguments,
}
// find few execution nodes which have executed the block earlier and provided an execution receipt for it
execNodes, err := executionNodesForBlockID(ctx, blockID, b.executionReceipts, b.state, b.log)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to find execution nodes at blockId %v: %v", blockID.String(), err)
}
// encode to MD5 as low compute/memory lookup key
// CAUTION: cryptographically insecure md5 is used here, but only to de-duplicate logs.
// *DO NOT* use this hash for any protocol-related or cryptographic functions.
insecureScriptHash := md5.Sum(script) //nolint:gosec
// try each of the execution nodes found
var errors *multierror.Error
// try to execute the script on one of the execution nodes
for _, execNode := range execNodes {
execStartTime := time.Now() // record start time
result, err := b.tryExecuteScript(ctx, execNode, execReq)
if err == nil {
if b.log.GetLevel() == zerolog.DebugLevel {
executionTime := time.Now()
if b.shouldLogScript(executionTime, insecureScriptHash) {
b.log.Debug().
Str("execution_node", execNode.String()).
Hex("block_id", blockID[:]).
Hex("script_hash", insecureScriptHash[:]).
Str("script", string(script)).
Msg("Successfully executed script")
b.loggedScripts.Add(insecureScriptHash, executionTime)
}
}
// log execution time
b.metrics.ScriptExecuted(
time.Since(execStartTime),
len(script),
)
return result, nil
}
// return if it's just a script failure as opposed to an EN failure and skip trying other ENs
if status.Code(err) == codes.InvalidArgument {
b.log.Debug().Err(err).
Str("execution_node", execNode.String()).
Hex("block_id", blockID[:]).
Hex("script_hash", insecureScriptHash[:]).
Str("script", string(script)).
Msg("script failed to execute on the execution node")
return nil, err
}
errors = multierror.Append(errors, err)
}
errToReturn := errors.ErrorOrNil()
if errToReturn != nil {
b.log.Error().Err(err).Msg("script execution failed for execution node internal reasons")
}
return nil, errToReturn
}
// shouldLogScript checks if the script hash is unique in the time window
func (b *backendScripts) shouldLogScript(execTime time.Time, scriptHash [16]byte) bool {
rawTimestamp, seen := b.loggedScripts.Get(scriptHash)
if !seen || rawTimestamp == nil {
return true
} else {
// safe cast
timestamp := rawTimestamp.(time.Time)
return execTime.Sub(timestamp) >= uniqueScriptLoggingTimeWindow
}
}
func (b *backendScripts) tryExecuteScript(ctx context.Context, execNode *flow.Identity, req execproto.ExecuteScriptAtBlockIDRequest) ([]byte, error) {
execRPCClient, closer, err := b.connFactory.GetExecutionAPIClient(execNode.Address)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to create client for execution node %s: %v", execNode.String(), err)
}
defer closer.Close()
execResp, err := execRPCClient.ExecuteScriptAtBlockID(ctx, &req)
if err != nil {
if status.Code(err) == codes.Unavailable {
b.connFactory.InvalidateExecutionAPIClient(execNode.Address)
}
return nil, status.Errorf(status.Code(err), "failed to execute the script on the execution node %s: %v", execNode.String(), err)
}
return execResp.GetValue(), nil
}