-
Notifications
You must be signed in to change notification settings - Fork 179
/
chunkVerifier.go
395 lines (345 loc) · 13.9 KB
/
chunkVerifier.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
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
package chunks
import (
"errors"
"fmt"
"github.com/rs/zerolog"
"github.com/onflow/flow-go/engine/execution/computation/computer"
executionState "github.com/onflow/flow-go/engine/execution/state"
"github.com/onflow/flow-go/fvm"
"github.com/onflow/flow-go/fvm/blueprints"
"github.com/onflow/flow-go/fvm/storage/derived"
"github.com/onflow/flow-go/fvm/storage/logical"
"github.com/onflow/flow-go/fvm/storage/snapshot"
fvmState "github.com/onflow/flow-go/fvm/storage/state"
"github.com/onflow/flow-go/ledger"
"github.com/onflow/flow-go/ledger/partial"
chmodels "github.com/onflow/flow-go/model/chunks"
"github.com/onflow/flow-go/model/flow"
"github.com/onflow/flow-go/model/verification"
"github.com/onflow/flow-go/module/executiondatasync/execution_data"
"github.com/onflow/flow-go/module/executiondatasync/provider"
"github.com/onflow/flow-go/module/metrics"
)
// ChunkVerifier is a verifier based on the current definitions of the flow network
type ChunkVerifier struct {
vm fvm.VM
vmCtx fvm.Context
systemChunkCtx fvm.Context
logger zerolog.Logger
}
// NewChunkVerifier creates a chunk verifier containing a flow virtual machine
func NewChunkVerifier(vm fvm.VM, vmCtx fvm.Context, logger zerolog.Logger) *ChunkVerifier {
return &ChunkVerifier{
vm: vm,
vmCtx: vmCtx,
systemChunkCtx: computer.SystemChunkContext(vmCtx, metrics.NewNoopCollector()),
logger: logger.With().Str("component", "chunk_verifier").Logger(),
}
}
// Verify verifies a given VerifiableChunk by executing it and checking the
// final state commitment.
// It returns a Spock Secret as a byte array, verification fault of the chunk,
// and an error.
func (fcv *ChunkVerifier) Verify(
vc *verification.VerifiableChunkData,
) (
[]byte,
error,
) {
var ctx fvm.Context
var transactions []*fvm.TransactionProcedure
if vc.IsSystemChunk {
ctx = fvm.NewContextFromParent(
fcv.systemChunkCtx,
fvm.WithBlockHeader(vc.Header),
// `protocol.Snapshot` implements `EntropyProvider` interface
// Note that `Snapshot` possible errors for RandomSource() are:
// - storage.ErrNotFound if the QC is unknown.
// - state.ErrUnknownSnapshotReference if the snapshot reference block is unknown
// However, at this stage, snapshot reference block should be known and the QC should also be known,
// so no error is expected in normal operations, as required by `EntropyProvider`.
fvm.WithEntropyProvider(vc.Snapshot),
)
txBody, err := blueprints.SystemChunkTransaction(fcv.vmCtx.Chain)
if err != nil {
return nil, fmt.Errorf("could not get system chunk transaction: %w", err)
}
transactions = []*fvm.TransactionProcedure{
fvm.Transaction(txBody, vc.TransactionOffset+uint32(0)),
}
} else {
ctx = fvm.NewContextFromParent(
fcv.vmCtx,
fvm.WithBlockHeader(vc.Header),
// `protocol.Snapshot` implements `EntropyProvider` interface
// Note that `Snapshot` possible errors for RandomSource() are:
// - storage.ErrNotFound if the QC is unknown.
// - state.ErrUnknownSnapshotReference if the snapshot reference block is unknown
// However, at this stage, snapshot reference block should be known and the QC should also be known,
// so no error is expected in normal operations, as required by `EntropyProvider`.
fvm.WithEntropyProvider(vc.Snapshot),
)
transactions = make(
[]*fvm.TransactionProcedure,
0,
len(vc.ChunkDataPack.Collection.Transactions))
for i, txBody := range vc.ChunkDataPack.Collection.Transactions {
tx := fvm.Transaction(txBody, vc.TransactionOffset+uint32(i))
transactions = append(transactions, tx)
}
}
return fcv.verifyTransactionsInContext(
ctx,
vc.TransactionOffset,
vc.Chunk,
vc.ChunkDataPack,
vc.Result,
transactions,
vc.EndState,
vc.IsSystemChunk)
}
type partialLedgerStorageSnapshot struct {
snapshot snapshot.StorageSnapshot
unknownRegTouch map[flow.RegisterID]struct{}
}
func (storage *partialLedgerStorageSnapshot) Get(
id flow.RegisterID,
) (
flow.RegisterValue,
error,
) {
value, err := storage.snapshot.Get(id)
if err != nil && errors.Is(err, ledger.ErrMissingKeys{}) {
storage.unknownRegTouch[id] = struct{}{}
// don't send error just return empty byte slice
// we always assume empty value for missing registers (which might
// cause the transaction to fail)
// but after execution we check unknownRegTouch and if any
// register is inside it, code won't generate approvals and
// it activates a challenge
return flow.RegisterValue{}, nil
}
return value, err
}
func (fcv *ChunkVerifier) verifyTransactionsInContext(
context fvm.Context,
transactionOffset uint32,
chunk *flow.Chunk,
chunkDataPack *flow.ChunkDataPack,
result *flow.ExecutionResult,
transactions []*fvm.TransactionProcedure,
endState flow.StateCommitment,
systemChunk bool,
) (
[]byte,
error,
) {
// TODO check collection hash to match
// TODO check datapack hash to match
// TODO check the number of transactions and computation used
chIndex := chunk.Index
execResID := result.ID()
if chunkDataPack == nil {
return nil, fmt.Errorf("missing chunk data pack")
}
// Execution nodes must not include a collection for system chunks.
if systemChunk && chunkDataPack.Collection != nil {
return nil, chmodels.NewCFSystemChunkIncludedCollection(chIndex, execResID)
}
// Consensus nodes already enforce some fundamental properties of ExecutionResults:
// 1. The result contains the correct number of chunks (compared to the block it pertains to).
// 2. The result contains chunks with strictly monotonically increasing `Chunk.Index` starting with index 0
// 3. for each chunk, the consistency requirement `Chunk.Index == Chunk.CollectionIndex` holds
// See `module/validation/receiptValidator` for implementation, which is used by the consensus nodes.
// And issue https://github.com/dapperlabs/flow-go/issues/6864 for implementing 3.
// Hence, the following is a consistency check. Failing it means we have either encountered a critical bug,
// or a super majority of byzantine nodes. In their case, continuing operations is impossible.
if int(chIndex) >= len(result.Chunks) {
return nil, chmodels.NewCFInvalidVerifiableChunk("error constructing partial trie: ",
fmt.Errorf("chunk index out of bounds of ExecutionResult's chunk list"), chIndex, execResID)
}
var events flow.EventsList = nil
serviceEvents := make(flow.ServiceEventList, 0)
// constructing a partial trie given chunk data package
psmt, err := partial.NewLedger(chunkDataPack.Proof, ledger.State(chunkDataPack.StartState), partial.DefaultPathFinderVersion)
if err != nil {
// TODO provide more details based on the error type
return nil, chmodels.NewCFInvalidVerifiableChunk(
"error constructing partial trie: ",
err,
chIndex,
execResID)
}
context = fvm.NewContextFromParent(
context,
fvm.WithDerivedBlockData(
derived.NewEmptyDerivedBlockData(logical.Time(transactionOffset))))
// chunk view construction
// unknown register tracks access to parts of the partial trie which
// are not expanded and values are unknown.
unknownRegTouch := make(map[flow.RegisterID]struct{})
snapshotTree := snapshot.NewSnapshotTree(
&partialLedgerStorageSnapshot{
snapshot: executionState.NewLedgerStorageSnapshot(
psmt,
chunkDataPack.StartState),
unknownRegTouch: unknownRegTouch,
})
chunkState := fvmState.NewExecutionState(nil, fvmState.DefaultParameters())
var problematicTx flow.Identifier
// collect execution data formatted transaction results
var txResults []flow.LightTransactionResult
if len(transactions) > 0 {
txResults = make([]flow.LightTransactionResult, len(transactions))
}
// executes all transactions in this chunk
for i, tx := range transactions {
executionSnapshot, output, err := fcv.vm.Run(
context,
tx,
snapshotTree)
if err != nil {
// this covers unexpected and very rare cases (e.g. system memory issues...),
// so we shouldn't be here even if transaction naturally fails (e.g. permission, runtime ... )
return nil, fmt.Errorf("failed to execute transaction: %d (%w)", i, err)
}
if len(unknownRegTouch) > 0 {
problematicTx = tx.ID
}
events = append(events, output.Events...)
serviceEvents = append(serviceEvents, output.ConvertedServiceEvents...)
snapshotTree = snapshotTree.Append(executionSnapshot)
err = chunkState.Merge(executionSnapshot)
if err != nil {
return nil, fmt.Errorf("failed to merge: %d (%w)", i, err)
}
txResults[i] = flow.LightTransactionResult{
TransactionID: tx.ID,
ComputationUsed: output.ComputationUsed,
Failed: output.Err != nil,
}
}
// check read access to unknown registers
if len(unknownRegTouch) > 0 {
var missingRegs []string
for id := range unknownRegTouch {
missingRegs = append(missingRegs, id.String())
}
return nil, chmodels.NewCFMissingRegisterTouch(missingRegs, chIndex, execResID, problematicTx)
}
eventsHash, err := flow.EventsMerkleRootHash(events)
if err != nil {
return nil, fmt.Errorf("cannot calculate events collection hash: %w", err)
}
if chunk.EventCollection != eventsHash {
collectionID := ""
if chunkDataPack.Collection != nil {
collectionID = chunkDataPack.Collection.ID().String()
}
for i, event := range events {
fcv.logger.Warn().Int("list_index", i).
Str("event_id", event.ID().String()).
Hex("event_fingerptint", event.Fingerprint()).
Str("event_type", string(event.Type)).
Str("event_tx_id", event.TransactionID.String()).
Uint32("event_tx_index", event.TransactionIndex).
Uint32("event_index", event.EventIndex).
Bytes("event_payload", event.Payload).
Str("block_id", chunk.BlockID.String()).
Str("collection_id", collectionID).
Str("result_id", result.ID().String()).
Uint64("chunk_index", chunk.Index).
Msg("not matching events debug")
}
return nil, chmodels.NewCFInvalidEventsCollection(chunk.EventCollection, eventsHash, chIndex, execResID, events)
}
if systemChunk {
equal, err := result.ServiceEvents.EqualTo(serviceEvents)
if err != nil {
return nil, fmt.Errorf("error while comparing service events: %w", err)
}
if !equal {
return nil, chmodels.CFInvalidServiceSystemEventsEmitted(result.ServiceEvents, serviceEvents, chIndex, execResID)
}
}
// Applying chunk updates to the partial trie. This returns the expected
// end state commitment after updates and the list of register keys that
// was not provided by the chunk data package (err).
chunkExecutionSnapshot := chunkState.Finalize()
keys, values := executionState.RegisterEntriesToKeysValues(
chunkExecutionSnapshot.UpdatedRegisters())
update, err := ledger.NewUpdate(
ledger.State(chunkDataPack.StartState),
keys,
values)
if err != nil {
return nil, fmt.Errorf("cannot create ledger update: %w", err)
}
expEndStateComm, trieUpdate, err := psmt.Set(update)
if err != nil {
if errors.Is(err, ledger.ErrMissingKeys{}) {
keys := err.(*ledger.ErrMissingKeys).Keys
stringKeys := make([]string, len(keys))
for i, key := range keys {
stringKeys[i] = key.String()
}
return nil, chmodels.NewCFMissingRegisterTouch(stringKeys, chIndex, execResID, problematicTx)
}
return nil, chmodels.NewCFMissingRegisterTouch(nil, chIndex, execResID, problematicTx)
}
// TODO check if exec node provided register touches that was not used (no read and no update)
// check if the end state commitment mentioned in the chunk matches
// what the partial trie is providing.
if flow.StateCommitment(expEndStateComm) != endState {
return nil, chmodels.NewCFNonMatchingFinalState(flow.StateCommitment(expEndStateComm), endState, chIndex, execResID)
}
// verify the execution data ID included in the ExecutionResult
// 1. check basic execution data root fields
if chunk.BlockID != chunkDataPack.ExecutionDataRoot.BlockID {
return nil, chmodels.NewCFExecutionDataBlockIDMismatch(chunkDataPack.ExecutionDataRoot.BlockID, chunk.BlockID, chIndex, execResID)
}
if len(chunkDataPack.ExecutionDataRoot.ChunkExecutionDataIDs) != len(result.Chunks) {
return nil, chmodels.NewCFExecutionDataChunksLengthMismatch(len(chunkDataPack.ExecutionDataRoot.ChunkExecutionDataIDs), len(result.Chunks), chIndex, execResID)
}
cedCollection := chunkDataPack.Collection
// the system chunk collection is not included in the chunkDataPack, but is included in the
// ChunkExecutionData. Create the collection here using the transaction body from the
// transactions list
if systemChunk {
cedCollection = &flow.Collection{
Transactions: []*flow.TransactionBody{transactions[0].Transaction},
}
}
// 2. build our chunk's chunk execution data using the locally calculated values, and calculate
// its CID
chunkExecutionData := execution_data.ChunkExecutionData{
Collection: cedCollection,
Events: events,
TrieUpdate: trieUpdate,
TransactionResults: txResults,
}
cidProvider := provider.NewExecutionDataCIDProvider(execution_data.DefaultSerializer)
cedCID, err := cidProvider.CalculateChunkExecutionDataID(chunkExecutionData)
if err != nil {
return nil, fmt.Errorf("failed to calculate CID of ChunkExecutionData: %w", err)
}
// 3. check that with the chunk execution results that we created locally,
// we can reproduce the ChunkExecutionData's ID, which the execution node is stating in its ChunkDataPack
if cedCID != chunkDataPack.ExecutionDataRoot.ChunkExecutionDataIDs[chIndex] {
return nil, chmodels.NewCFExecutionDataInvalidChunkCID(
chunkDataPack.ExecutionDataRoot.ChunkExecutionDataIDs[chIndex],
cedCID,
chIndex,
execResID,
)
}
// 4. check the execution data root ID by calculating it using the provided execution data root
executionDataID, err := cidProvider.CalculateExecutionDataRootID(chunkDataPack.ExecutionDataRoot)
if err != nil {
return nil, fmt.Errorf("failed to calculate ID of ExecutionDataRoot: %w", err)
}
if executionDataID != result.ExecutionDataID {
return nil, chmodels.NewCFInvalidExecutionDataID(result.ExecutionDataID, executionDataID, chIndex, execResID)
}
return chunkExecutionSnapshot.SpockSecret, nil
}