github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/module/chunks/chunkVerifier.go (about) 1 package chunks 2 3 import ( 4 "errors" 5 "fmt" 6 7 "github.com/rs/zerolog" 8 9 "github.com/onflow/flow-go/engine/execution/computation/computer" 10 executionState "github.com/onflow/flow-go/engine/execution/state" 11 "github.com/onflow/flow-go/fvm" 12 "github.com/onflow/flow-go/fvm/blueprints" 13 "github.com/onflow/flow-go/fvm/storage/derived" 14 "github.com/onflow/flow-go/fvm/storage/logical" 15 "github.com/onflow/flow-go/fvm/storage/snapshot" 16 fvmState "github.com/onflow/flow-go/fvm/storage/state" 17 "github.com/onflow/flow-go/ledger" 18 "github.com/onflow/flow-go/ledger/partial" 19 chmodels "github.com/onflow/flow-go/model/chunks" 20 "github.com/onflow/flow-go/model/flow" 21 "github.com/onflow/flow-go/model/verification" 22 "github.com/onflow/flow-go/module/executiondatasync/execution_data" 23 "github.com/onflow/flow-go/module/executiondatasync/provider" 24 ) 25 26 // ChunkVerifier is a verifier based on the current definitions of the flow network 27 type ChunkVerifier struct { 28 vm fvm.VM 29 vmCtx fvm.Context 30 systemChunkCtx fvm.Context 31 logger zerolog.Logger 32 } 33 34 // NewChunkVerifier creates a chunk verifier containing a flow virtual machine 35 func NewChunkVerifier(vm fvm.VM, vmCtx fvm.Context, logger zerolog.Logger) *ChunkVerifier { 36 return &ChunkVerifier{ 37 vm: vm, 38 vmCtx: vmCtx, 39 systemChunkCtx: computer.SystemChunkContext(vmCtx), 40 logger: logger.With().Str("component", "chunk_verifier").Logger(), 41 } 42 } 43 44 // Verify verifies a given VerifiableChunk by executing it and checking the 45 // final state commitment. 46 // It returns a Spock Secret as a byte array, verification fault of the chunk, 47 // and an error. 48 func (fcv *ChunkVerifier) Verify( 49 vc *verification.VerifiableChunkData, 50 ) ( 51 []byte, 52 error, 53 ) { 54 55 var ctx fvm.Context 56 var transactions []*fvm.TransactionProcedure 57 if vc.IsSystemChunk { 58 ctx = fvm.NewContextFromParent( 59 fcv.systemChunkCtx, 60 fvm.WithBlockHeader(vc.Header), 61 // `protocol.Snapshot` implements `EntropyProvider` interface 62 // Note that `Snapshot` possible errors for RandomSource() are: 63 // - storage.ErrNotFound if the QC is unknown. 64 // - state.ErrUnknownSnapshotReference if the snapshot reference block is unknown 65 // However, at this stage, snapshot reference block should be known and the QC should also be known, 66 // so no error is expected in normal operations, as required by `EntropyProvider`. 67 fvm.WithEntropyProvider(vc.Snapshot), 68 ) 69 70 txBody, err := blueprints.SystemChunkTransaction(fcv.vmCtx.Chain) 71 if err != nil { 72 return nil, fmt.Errorf("could not get system chunk transaction: %w", err) 73 } 74 75 transactions = []*fvm.TransactionProcedure{ 76 fvm.Transaction(txBody, vc.TransactionOffset+uint32(0)), 77 } 78 } else { 79 ctx = fvm.NewContextFromParent( 80 fcv.vmCtx, 81 fvm.WithBlockHeader(vc.Header), 82 // `protocol.Snapshot` implements `EntropyProvider` interface 83 // Note that `Snapshot` possible errors for RandomSource() are: 84 // - storage.ErrNotFound if the QC is unknown. 85 // - state.ErrUnknownSnapshotReference if the snapshot reference block is unknown 86 // However, at this stage, snapshot reference block should be known and the QC should also be known, 87 // so no error is expected in normal operations, as required by `EntropyProvider`. 88 fvm.WithEntropyProvider(vc.Snapshot), 89 ) 90 91 transactions = make( 92 []*fvm.TransactionProcedure, 93 0, 94 len(vc.ChunkDataPack.Collection.Transactions)) 95 for i, txBody := range vc.ChunkDataPack.Collection.Transactions { 96 tx := fvm.Transaction(txBody, vc.TransactionOffset+uint32(i)) 97 transactions = append(transactions, tx) 98 } 99 } 100 101 return fcv.verifyTransactionsInContext( 102 ctx, 103 vc.TransactionOffset, 104 vc.Chunk, 105 vc.ChunkDataPack, 106 vc.Result, 107 transactions, 108 vc.EndState, 109 vc.IsSystemChunk) 110 } 111 112 type partialLedgerStorageSnapshot struct { 113 snapshot snapshot.StorageSnapshot 114 115 unknownRegTouch map[flow.RegisterID]struct{} 116 } 117 118 func (storage *partialLedgerStorageSnapshot) Get( 119 id flow.RegisterID, 120 ) ( 121 flow.RegisterValue, 122 error, 123 ) { 124 value, err := storage.snapshot.Get(id) 125 if err != nil && errors.Is(err, ledger.ErrMissingKeys{}) { 126 storage.unknownRegTouch[id] = struct{}{} 127 128 // don't send error just return empty byte slice 129 // we always assume empty value for missing registers (which might 130 // cause the transaction to fail) 131 // but after execution we check unknownRegTouch and if any 132 // register is inside it, code won't generate approvals and 133 // it activates a challenge 134 return flow.RegisterValue{}, nil 135 } 136 137 return value, err 138 } 139 140 func (fcv *ChunkVerifier) verifyTransactionsInContext( 141 context fvm.Context, 142 transactionOffset uint32, 143 chunk *flow.Chunk, 144 chunkDataPack *flow.ChunkDataPack, 145 result *flow.ExecutionResult, 146 transactions []*fvm.TransactionProcedure, 147 endState flow.StateCommitment, 148 systemChunk bool, 149 ) ( 150 []byte, 151 error, 152 ) { 153 154 // TODO check collection hash to match 155 // TODO check datapack hash to match 156 // TODO check the number of transactions and computation used 157 158 chIndex := chunk.Index 159 execResID := result.ID() 160 161 if chunkDataPack == nil { 162 return nil, fmt.Errorf("missing chunk data pack") 163 } 164 165 // Execution nodes must not include a collection for system chunks. 166 if systemChunk && chunkDataPack.Collection != nil { 167 return nil, chmodels.NewCFSystemChunkIncludedCollection(chIndex, execResID) 168 } 169 170 // Consensus nodes already enforce some fundamental properties of ExecutionResults: 171 // 1. The result contains the correct number of chunks (compared to the block it pertains to). 172 // 2. The result contains chunks with strictly monotonically increasing `Chunk.Index` starting with index 0 173 // 3. for each chunk, the consistency requirement `Chunk.Index == Chunk.CollectionIndex` holds 174 // See `module/validation/receiptValidator` for implementation, which is used by the consensus nodes. 175 // And issue https://github.com/dapperlabs/flow-go/issues/6864 for implementing 3. 176 // Hence, the following is a consistency check. Failing it means we have either encountered a critical bug, 177 // or a super majority of byzantine nodes. In their case, continuing operations is impossible. 178 if int(chIndex) >= len(result.Chunks) { 179 return nil, chmodels.NewCFInvalidVerifiableChunk("error constructing partial trie: ", 180 fmt.Errorf("chunk index out of bounds of ExecutionResult's chunk list"), chIndex, execResID) 181 } 182 183 var events flow.EventsList = nil 184 serviceEvents := make(flow.ServiceEventList, 0) 185 186 // constructing a partial trie given chunk data package 187 psmt, err := partial.NewLedger(chunkDataPack.Proof, ledger.State(chunkDataPack.StartState), partial.DefaultPathFinderVersion) 188 189 if err != nil { 190 // TODO provide more details based on the error type 191 return nil, chmodels.NewCFInvalidVerifiableChunk( 192 "error constructing partial trie: ", 193 err, 194 chIndex, 195 execResID) 196 } 197 198 context = fvm.NewContextFromParent( 199 context, 200 fvm.WithDerivedBlockData( 201 derived.NewEmptyDerivedBlockData(logical.Time(transactionOffset)))) 202 203 // chunk view construction 204 // unknown register tracks access to parts of the partial trie which 205 // are not expanded and values are unknown. 206 unknownRegTouch := make(map[flow.RegisterID]struct{}) 207 snapshotTree := snapshot.NewSnapshotTree( 208 &partialLedgerStorageSnapshot{ 209 snapshot: executionState.NewLedgerStorageSnapshot( 210 psmt, 211 chunkDataPack.StartState), 212 unknownRegTouch: unknownRegTouch, 213 }) 214 chunkState := fvmState.NewExecutionState(nil, fvmState.DefaultParameters()) 215 216 var problematicTx flow.Identifier 217 218 // collect execution data formatted transaction results 219 var txResults []flow.LightTransactionResult 220 if len(transactions) > 0 { 221 txResults = make([]flow.LightTransactionResult, len(transactions)) 222 } 223 224 // executes all transactions in this chunk 225 for i, tx := range transactions { 226 executionSnapshot, output, err := fcv.vm.Run( 227 context, 228 tx, 229 snapshotTree) 230 if err != nil { 231 // this covers unexpected and very rare cases (e.g. system memory issues...), 232 // so we shouldn't be here even if transaction naturally fails (e.g. permission, runtime ... ) 233 return nil, fmt.Errorf("failed to execute transaction: %d (%w)", i, err) 234 } 235 236 if len(unknownRegTouch) > 0 { 237 problematicTx = tx.ID 238 } 239 240 events = append(events, output.Events...) 241 serviceEvents = append(serviceEvents, output.ConvertedServiceEvents...) 242 243 snapshotTree = snapshotTree.Append(executionSnapshot) 244 err = chunkState.Merge(executionSnapshot) 245 if err != nil { 246 return nil, fmt.Errorf("failed to merge: %d (%w)", i, err) 247 } 248 249 txResults[i] = flow.LightTransactionResult{ 250 TransactionID: tx.ID, 251 ComputationUsed: output.ComputationUsed, 252 Failed: output.Err != nil, 253 } 254 } 255 256 // check read access to unknown registers 257 if len(unknownRegTouch) > 0 { 258 var missingRegs []string 259 for id := range unknownRegTouch { 260 missingRegs = append(missingRegs, id.String()) 261 } 262 return nil, chmodels.NewCFMissingRegisterTouch(missingRegs, chIndex, execResID, problematicTx) 263 } 264 265 eventsHash, err := flow.EventsMerkleRootHash(events) 266 if err != nil { 267 return nil, fmt.Errorf("cannot calculate events collection hash: %w", err) 268 } 269 if chunk.EventCollection != eventsHash { 270 collectionID := "" 271 if chunkDataPack.Collection != nil { 272 collectionID = chunkDataPack.Collection.ID().String() 273 } 274 for i, event := range events { 275 fcv.logger.Warn().Int("list_index", i). 276 Str("event_id", event.ID().String()). 277 Hex("event_fingerptint", event.Fingerprint()). 278 Str("event_type", string(event.Type)). 279 Str("event_tx_id", event.TransactionID.String()). 280 Uint32("event_tx_index", event.TransactionIndex). 281 Uint32("event_index", event.EventIndex). 282 Bytes("event_payload", event.Payload). 283 Str("block_id", chunk.BlockID.String()). 284 Str("collection_id", collectionID). 285 Str("result_id", result.ID().String()). 286 Uint64("chunk_index", chunk.Index). 287 Msg("not matching events debug") 288 } 289 290 return nil, chmodels.NewCFInvalidEventsCollection(chunk.EventCollection, eventsHash, chIndex, execResID, events) 291 } 292 293 if systemChunk { 294 equal, err := result.ServiceEvents.EqualTo(serviceEvents) 295 if err != nil { 296 return nil, fmt.Errorf("error while comparing service events: %w", err) 297 } 298 if !equal { 299 return nil, chmodels.CFInvalidServiceSystemEventsEmitted(result.ServiceEvents, serviceEvents, chIndex, execResID) 300 } 301 } 302 303 // Applying chunk updates to the partial trie. This returns the expected 304 // end state commitment after updates and the list of register keys that 305 // was not provided by the chunk data package (err). 306 chunkExecutionSnapshot := chunkState.Finalize() 307 keys, values := executionState.RegisterEntriesToKeysValues( 308 chunkExecutionSnapshot.UpdatedRegisters()) 309 310 update, err := ledger.NewUpdate( 311 ledger.State(chunkDataPack.StartState), 312 keys, 313 values) 314 if err != nil { 315 return nil, fmt.Errorf("cannot create ledger update: %w", err) 316 } 317 318 expEndStateComm, trieUpdate, err := psmt.Set(update) 319 if err != nil { 320 if errors.Is(err, ledger.ErrMissingKeys{}) { 321 keys := err.(*ledger.ErrMissingKeys).Keys 322 stringKeys := make([]string, len(keys)) 323 for i, key := range keys { 324 stringKeys[i] = key.String() 325 } 326 return nil, chmodels.NewCFMissingRegisterTouch(stringKeys, chIndex, execResID, problematicTx) 327 } 328 return nil, chmodels.NewCFMissingRegisterTouch(nil, chIndex, execResID, problematicTx) 329 } 330 331 // TODO check if exec node provided register touches that was not used (no read and no update) 332 // check if the end state commitment mentioned in the chunk matches 333 // what the partial trie is providing. 334 if flow.StateCommitment(expEndStateComm) != endState { 335 return nil, chmodels.NewCFNonMatchingFinalState(flow.StateCommitment(expEndStateComm), endState, chIndex, execResID) 336 } 337 338 // verify the execution data ID included in the ExecutionResult 339 // 1. check basic execution data root fields 340 if chunk.BlockID != chunkDataPack.ExecutionDataRoot.BlockID { 341 return nil, chmodels.NewCFExecutionDataBlockIDMismatch(chunkDataPack.ExecutionDataRoot.BlockID, chunk.BlockID, chIndex, execResID) 342 } 343 344 if len(chunkDataPack.ExecutionDataRoot.ChunkExecutionDataIDs) != len(result.Chunks) { 345 return nil, chmodels.NewCFExecutionDataChunksLengthMismatch(len(chunkDataPack.ExecutionDataRoot.ChunkExecutionDataIDs), len(result.Chunks), chIndex, execResID) 346 } 347 348 cedCollection := chunkDataPack.Collection 349 // the system chunk collection is not included in the chunkDataPack, but is included in the 350 // ChunkExecutionData. Create the collection here using the transaction body from the 351 // transactions list 352 if systemChunk { 353 cedCollection = &flow.Collection{ 354 Transactions: []*flow.TransactionBody{transactions[0].Transaction}, 355 } 356 } 357 358 // 2. build our chunk's chunk execution data using the locally calculated values, and calculate 359 // its CID 360 chunkExecutionData := execution_data.ChunkExecutionData{ 361 Collection: cedCollection, 362 Events: events, 363 TrieUpdate: trieUpdate, 364 TransactionResults: txResults, 365 } 366 367 cidProvider := provider.NewExecutionDataCIDProvider(execution_data.DefaultSerializer) 368 cedCID, err := cidProvider.CalculateChunkExecutionDataID(chunkExecutionData) 369 if err != nil { 370 return nil, fmt.Errorf("failed to calculate CID of ChunkExecutionData: %w", err) 371 } 372 373 // 3. check that with the chunk execution results that we created locally, 374 // we can reproduce the ChunkExecutionData's ID, which the execution node is stating in its ChunkDataPack 375 if cedCID != chunkDataPack.ExecutionDataRoot.ChunkExecutionDataIDs[chIndex] { 376 return nil, chmodels.NewCFExecutionDataInvalidChunkCID( 377 chunkDataPack.ExecutionDataRoot.ChunkExecutionDataIDs[chIndex], 378 cedCID, 379 chIndex, 380 execResID, 381 ) 382 } 383 384 // 4. check the execution data root ID by calculating it using the provided execution data root 385 executionDataID, err := cidProvider.CalculateExecutionDataRootID(chunkDataPack.ExecutionDataRoot) 386 if err != nil { 387 return nil, fmt.Errorf("failed to calculate ID of ExecutionDataRoot: %w", err) 388 } 389 if executionDataID != result.ExecutionDataID { 390 return nil, chmodels.NewCFInvalidExecutionDataID(result.ExecutionDataID, executionDataID, chIndex, execResID) 391 } 392 393 return chunkExecutionSnapshot.SpockSecret, nil 394 }