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  }