From 028e9a0213ce194bbc88ccda89325024a2321fde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Thu, 23 Jan 2025 15:30:04 -0800 Subject: [PATCH 1/6] refactor the remote debugger and add support for using the execution data API --- cmd/util/cmd/debug-script/cmd.go | 89 ++++++++++++-- cmd/util/cmd/debug-tx/cmd.go | 86 +++++++++---- utils/debug/README.md | 120 +++++++++++++++---- utils/debug/api.go | 60 ++++++++++ utils/debug/registerCache.go | 30 +++-- utils/debug/remoteDebugger.go | 93 ++++---------- utils/debug/remoteView.go | 200 +++++++++++++++---------------- 7 files changed, 430 insertions(+), 248 deletions(-) create mode 100644 utils/debug/api.go diff --git a/cmd/util/cmd/debug-script/cmd.go b/cmd/util/cmd/debug-script/cmd.go index 3c0a2718f7f..a1ce10361f6 100644 --- a/cmd/util/cmd/debug-script/cmd.go +++ b/cmd/util/cmd/debug-script/cmd.go @@ -1,11 +1,18 @@ package debug_tx import ( + "context" "os" + "github.com/onflow/flow/protobuf/go/flow/access" + "github.com/onflow/flow/protobuf/go/flow/execution" + "github.com/onflow/flow/protobuf/go/flow/executiondata" "github.com/rs/zerolog/log" "github.com/spf13/cobra" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "github.com/onflow/flow-go/fvm/storage/snapshot" "github.com/onflow/flow-go/model/flow" "github.com/onflow/flow-go/utils/debug" ) @@ -14,9 +21,12 @@ import ( // `gcloud compute ssh '--ssh-flag=-A' --no-user-output-enabled --tunnel-through-iap migrationmainnet1-execution-001 --project flow-multi-region -- -NL 9001:localhost:9000` var ( - flagExecutionAddress string - flagChain string - flagScript string + flagAccessAddress string + flagExecutionAddress string + flagBlockID string + flagChain string + flagScript string + flagUseExecutionDataAPI bool ) var Cmd = &cobra.Command{ @@ -33,13 +43,22 @@ func init() { "", "Chain name", ) - _ = Cmd.MarkFlagRequired("chain") + + Cmd.Flags().StringVar(&flagAccessAddress, "access-address", "", "address of the access node") + _ = Cmd.MarkFlagRequired("access-address") Cmd.Flags().StringVar(&flagExecutionAddress, "execution-address", "", "address of the execution node") _ = Cmd.MarkFlagRequired("execution-address") + Cmd.Flags().StringVar(&flagBlockID, "block-id", "", "block ID") + _ = Cmd.MarkFlagRequired("block-id") + + _ = Cmd.MarkFlagRequired("chain") + Cmd.Flags().StringVar(&flagScript, "script", "", "path to script") _ = Cmd.MarkFlagRequired("script") + + Cmd.Flags().BoolVar(&flagUseExecutionDataAPI, "use-execution-data-api", false, "use the execution data API") } func run(*cobra.Command, []string) { @@ -52,16 +71,68 @@ func run(*cobra.Command, []string) { log.Fatal().Err(err).Msgf("failed to read script from file %s", flagScript) } - debugger := debug.NewRemoteDebugger( - flagExecutionAddress, - chain, - log.Logger, + log.Info().Msg("Fetching block header ...") + + accessConn, err := grpc.NewClient( + flagAccessAddress, + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) + if err != nil { + log.Fatal().Err(err).Msg("failed to create access connection") + } + defer accessConn.Close() + + accessClient := access.NewAccessAPIClient(accessConn) + + blockID, err := flow.HexStringToIdentifier(flagBlockID) + if err != nil { + log.Fatal().Err(err).Msg("failed to parse block ID") + } + + header, err := debug.GetAccessAPIBlockHeader(accessClient, context.Background(), blockID) + if err != nil { + log.Fatal().Err(err).Msg("failed to fetch block header") + } + + blockHeight := header.Height + + log.Info().Msgf( + "Fetched block header: %s (height %d)", + header.ID(), + blockHeight, ) + var snap snapshot.StorageSnapshot + + if flagUseExecutionDataAPI { + executionDataClient := executiondata.NewExecutionDataAPIClient(accessConn) + snap, err = debug.NewExecutionDataStorageSnapshot(executionDataClient, nil, blockHeight) + if err != nil { + log.Fatal().Err(err).Msg("failed to create storage snapshot") + } + } else { + executionConn, err := grpc.NewClient( + flagExecutionAddress, + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) + if err != nil { + log.Fatal().Err(err).Msg("failed to create execution connection") + } + defer executionConn.Close() + + executionClient := execution.NewExecutionAPIClient(executionConn) + snap, err = debug.NewExecutionNodeStorageSnapshot(executionClient, nil, blockID) + if err != nil { + log.Fatal().Err(err).Msg("failed to create storage snapshot") + } + } + + debugger := debug.NewRemoteDebugger(chain, log.Logger) + // TODO: add support for arguments var arguments [][]byte - result, scriptErr, processErr := debugger.RunScript(code, arguments) + result, scriptErr, processErr := debugger.RunScript(code, arguments, snap, header) if scriptErr != nil { log.Fatal().Err(scriptErr).Msg("transaction error") diff --git a/cmd/util/cmd/debug-tx/cmd.go b/cmd/util/cmd/debug-tx/cmd.go index a7f955549e5..24d1750c325 100644 --- a/cmd/util/cmd/debug-tx/cmd.go +++ b/cmd/util/cmd/debug-tx/cmd.go @@ -3,11 +3,15 @@ package debug_tx import ( "context" + "github.com/onflow/flow/protobuf/go/flow/execution" "github.com/rs/zerolog/log" "github.com/spf13/cobra" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" sdk "github.com/onflow/flow-go-sdk" + "github.com/onflow/flow-go/fvm/storage/snapshot" "github.com/onflow/flow-go/model/flow" "github.com/onflow/flow-go/module/grpcclient" "github.com/onflow/flow-go/utils/debug" @@ -17,13 +21,13 @@ import ( // `gcloud compute ssh '--ssh-flag=-A' --no-user-output-enabled --tunnel-through-iap migrationmainnet1-execution-001 --project flow-multi-region -- -NL 9001:localhost:9000` var ( - flagAccessAddress string - flagExecutionAddress string - flagChain string - flagTx string - flagComputeLimit uint64 - flagAtLatestBlock bool - flagProposalKeySeq uint64 + flagAccessAddress string + flagExecutionAddress string + flagChain string + flagTx string + flagComputeLimit uint64 + flagProposalKeySeq uint64 + flagUseExecutionDataAPI bool ) var Cmd = &cobra.Command{ @@ -53,9 +57,9 @@ func init() { Cmd.Flags().Uint64Var(&flagComputeLimit, "compute-limit", 9999, "transaction compute limit") - Cmd.Flags().BoolVar(&flagAtLatestBlock, "at-latest-block", false, "run at latest block") - Cmd.Flags().Uint64Var(&flagProposalKeySeq, "proposal-key-seq", 0, "proposal key sequence number") + + Cmd.Flags().BoolVar(&flagUseExecutionDataAPI, "use-execution-data-api", false, "use the execution data API") } func run(*cobra.Command, []string) { @@ -94,16 +98,57 @@ func run(*cobra.Command, []string) { log.Fatal().Err(err).Msg("failed to fetch transaction result") } - log.Info().Msgf("Fetched transaction result: %s at block %s", txResult.Status, txResult.BlockID) + blockID := flow.Identifier(txResult.BlockID) + blockHeight := txResult.BlockHeight - log.Info().Msg("Debugging transaction ...") + log.Info().Msgf( + "Fetched transaction result: %s at block %s (height %d)", + txResult.Status, + blockID, + blockHeight, + ) + + log.Info().Msg("Fetching block header ...") - debugger := debug.NewRemoteDebugger( - flagExecutionAddress, - chain, - log.Logger, + header, err := debug.GetAccessAPIBlockHeader(flowClient.RPCClient(), context.Background(), blockID) + if err != nil { + log.Fatal().Err(err).Msg("failed to fetch block header") + } + + log.Info().Msgf( + "Fetched block header: %s (height %d)", + header.ID(), + header.Height, ) + var snap snapshot.StorageSnapshot + + if flagUseExecutionDataAPI { + snap, err = debug.NewExecutionDataStorageSnapshot(flowClient.ExecutionDataRPCClient(), nil, blockHeight) + if err != nil { + log.Fatal().Err(err).Msg("failed to create storage snapshot") + } + } else { + executionConn, err := grpc.NewClient( + flagExecutionAddress, + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) + if err != nil { + log.Fatal().Err(err).Msg("failed to create execution connection") + } + defer executionConn.Close() + + executionClient := execution.NewExecutionAPIClient(executionConn) + snap, err = debug.NewExecutionNodeStorageSnapshot(executionClient, nil, blockID) + if err != nil { + log.Fatal().Err(err).Msg("failed to create storage snapshot") + } + } + + log.Info().Msg("Debugging transaction ...") + + debugger := debug.NewRemoteDebugger(chain, log.Logger) + txBody := flow.NewTransactionBody(). SetScript(tx.Script). SetComputeLimit(flagComputeLimit). @@ -128,16 +173,7 @@ func run(*cobra.Command, []string) { proposalKeySequenceNumber, ) - var txErr, processErr error - if flagAtLatestBlock { - txErr, processErr = debugger.RunTransaction(txBody) - } else { - txErr, processErr = debugger.RunTransactionAtBlockID( - txBody, - flow.Identifier(txResult.BlockID), - "", - ) - } + txErr, processErr := debugger.RunTransaction(txBody, snap, header) if txErr != nil { log.Fatal().Err(txErr).Msg("transaction error") } diff --git a/utils/debug/README.md b/utils/debug/README.md index 87b4e0775a3..6d85a94a6c1 100644 --- a/utils/debug/README.md +++ b/utils/debug/README.md @@ -1,32 +1,44 @@ - ## Remote Debugger -Remote debugger provides utils needed to run transactions and scripts against live network data. It uses GRPC endpoints on an execution nodes to fetch registers and block info when running a transaction. This is mostly provided for debugging purpose and should not be used for production level operations. -If you use the caching method you can run the transaction once and use the cached values to run transaction in debugging mode. +The remote debugger allows running transactions and scripts against existing network data. + +It uses APIs to fetch registers and block info, for example the register value API of the execution nodes, +or the execution data API of the access nodes. + +This is mostly provided for debugging purpose and should not be used for production level operations. + +Optionally, the debugger allows the fetched registers that are necessary for the execution to be written to +and read from a cache. + +Use the `ExecutionNodeStorageSnapshot` to fetch the registers and block info from the execution node (live/recent data). -### sample code +Use the `ExecutionDataStorageSnapshot` to fetch the execution data from the access node (recent/historic data). + +### Sample Code ```GO package debug_test import ( + "context" "fmt" "os" "testing" + "github.com/onflow/flow/protobuf/go/flow/access" + "github.com/onflow/flow/protobuf/go/flow/execution" + "github.com/onflow/flow/protobuf/go/flow/executiondata" "github.com/rs/zerolog" "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" "github.com/onflow/flow-go/model/flow" "github.com/onflow/flow-go/utils/debug" ) -func TestDebugger_RunTransaction(t *testing.T) { - - grpcAddress := "localhost:3600" - chain := flow.Emulator.Chain() - debugger := debug.NewRemoteDebugger(grpcAddress, chain, zerolog.New(os.Stdout).With().Logger()) +func getTransaction(chain flow.Chain) *flow.TransactionBody { const scriptTemplate = ` import FlowServiceAccount from 0x%s @@ -40,37 +52,99 @@ func TestDebugger_RunTransaction(t *testing.T) { script := []byte(fmt.Sprintf(scriptTemplate, chain.ServiceAddress())) txBody := flow.NewTransactionBody(). SetComputeLimit(9999). - SetScript([]byte(script)). + SetScript(script). SetPayer(chain.ServiceAddress()). SetProposalKey(chain.ServiceAddress(), 0, 0) txBody.Authorizers = []flow.Address{chain.ServiceAddress()} - // Run at the latest blockID - txErr, err := debugger.RunTransaction(txBody) - require.NoError(t, txErr) + return txBody +} + +func TestDebugger_RunTransactionAgainstExecutionNodeAtBlockID(t *testing.T) { + + host := "execution-001.mainnet26.nodes.onflow.org:9000" + + conn, err := grpc.NewClient( + host, + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) require.NoError(t, err) + defer conn.Close() + + executionClient := execution.NewExecutionAPIClient(conn) - // Run with blockID (use the file cache) - blockId, err := flow.HexStringToIdentifier("3a8281395e2c1aaa3b8643d148594b19e2acb477611a8e0cab8a55c46c40b563") + blockID, err := flow.HexStringToIdentifier("e68a9a1fe849d1be80e4c5e414f53e3b59a170b88785e0b22be077ae9c3bbd29") require.NoError(t, err) - txErr, err = debugger.RunTransactionAtBlockID(txBody, blockId, "") + + header, err := debug.GetExecutionAPIBlockHeader(executionClient, context.Background(), blockID) + + snapshot, err := debug.NewExecutionNodeStorageSnapshot(executionClient, nil, blockID) + require.NoError(t, err) + + defer snapshot.Close() + + chain := flow.Mainnet.Chain() + logger := zerolog.New(os.Stdout).With().Logger() + debugger := debug.NewRemoteDebugger(chain, logger) + + txBody := getTransaction(chain) + + txErr, err := debugger.RunTransaction(txBody, snapshot, header) require.NoError(t, txErr) require.NoError(t, err) +} + +func TestDebugger_RunTransactionAgainstAccessNodeAtBlockIDWithFileCache(t *testing.T) { + + host := "access.mainnet.nodes.onflow.org:9000" + + conn, err := grpc.NewClient( + host, + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) + require.NoError(t, err) + defer conn.Close() + + executionDataClient := executiondata.NewExecutionDataAPIClient(conn) + + var blockHeight uint64 = 100_000_000 + + blockID, err := flow.HexStringToIdentifier("e68a9a1fe849d1be80e4c5e414f53e3b59a170b88785e0b22be077ae9c3bbd29") + require.NoError(t, err) + + accessClient := access.NewAccessAPIClient(conn) + header, err := debug.GetAccessAPIBlockHeader( + accessClient, + context.Background(), + blockID, + ) + require.NoError(t, err) testCacheFile := "test.cache" defer os.Remove(testCacheFile) - // the first run would cache the results - txErr, err = debugger.RunTransactionAtBlockID(txBody, blockId, testCacheFile) + + cache := debug.NewFileRegisterCache(testCacheFile) + + snapshot, err := debug.NewExecutionDataStorageSnapshot(executionDataClient, cache, blockHeight) + require.NoError(t, err) + + defer snapshot.Close() + + chain := flow.Mainnet.Chain() + logger := zerolog.New(os.Stdout).With().Logger() + debugger := debug.NewRemoteDebugger(chain, logger) + + txBody := getTransaction(chain) + + // the first run will cache the results + txErr, err := debugger.RunTransaction(txBody, snapshot, header) require.NoError(t, txErr) require.NoError(t, err) - // second one should only use the cache - // make blockId invalid so if it endsup looking up by id it should fail - blockId = flow.Identifier{} - txErr, err = debugger.RunTransactionAtBlockID(txBody, blockId, testCacheFile) + // the second run should only use the cache. + txErr, err = debugger.RunTransaction(txBody, snapshot, header) require.NoError(t, txErr) require.NoError(t, err) } - ``` diff --git a/utils/debug/api.go b/utils/debug/api.go new file mode 100644 index 00000000000..8f9d126618d --- /dev/null +++ b/utils/debug/api.go @@ -0,0 +1,60 @@ +package debug + +import ( + "context" + + "github.com/onflow/flow/protobuf/go/flow/access" + "github.com/onflow/flow/protobuf/go/flow/execution" + + "github.com/onflow/flow-go/model/flow" +) + +func GetExecutionAPIBlockHeader( + client execution.ExecutionAPIClient, + ctx context.Context, + blockID flow.Identifier, +) ( + *flow.Header, + error, +) { + req := &execution.GetBlockHeaderByIDRequest{ + Id: blockID[:], + } + + resp, err := client.GetBlockHeaderByID(ctx, req) + if err != nil { + return nil, err + } + + return &flow.Header{ + ChainID: flow.ChainID(resp.Block.ChainId), + ParentID: flow.Identifier(resp.Block.ParentId), + Height: resp.Block.Height, + Timestamp: resp.Block.Timestamp.AsTime(), + }, nil +} + +func GetAccessAPIBlockHeader( + client access.AccessAPIClient, + ctx context.Context, + blockID flow.Identifier, +) ( + *flow.Header, + error, +) { + req := &access.GetBlockHeaderByIDRequest{ + Id: blockID[:], + } + + resp, err := client.GetBlockHeaderByID(ctx, req) + if err != nil { + return nil, err + } + + return &flow.Header{ + ChainID: flow.ChainID(resp.Block.ChainId), + ParentID: flow.Identifier(resp.Block.ParentId), + Height: resp.Block.Height, + Timestamp: resp.Block.Timestamp.AsTime(), + }, nil +} diff --git a/utils/debug/registerCache.go b/utils/debug/registerCache.go index 11e0c801ca7..29befd1a7fc 100644 --- a/utils/debug/registerCache.go +++ b/utils/debug/registerCache.go @@ -11,40 +11,44 @@ import ( "github.com/onflow/flow-go/model/flow" ) -type registerCache interface { +type RegisterCache interface { Get(owner, key string) (value []byte, found bool) Set(owner, key string, value []byte) Persist() error } -type memRegisterCache struct { +type InMemoryRegisterCache struct { data map[string]flow.RegisterValue } -func newMemRegisterCache() *memRegisterCache { - return &memRegisterCache{data: make(map[string]flow.RegisterValue)} +var _ RegisterCache = &InMemoryRegisterCache{} + +func NewInMemoryRegisterCache() *InMemoryRegisterCache { + return &InMemoryRegisterCache{data: make(map[string]flow.RegisterValue)} } -func (c *memRegisterCache) Get(owner, key string) ([]byte, bool) { +func (c *InMemoryRegisterCache) Get(owner, key string) ([]byte, bool) { v, found := c.data[owner+"~"+key] return v, found } -func (c *memRegisterCache) Set(owner, key string, value []byte) { +func (c *InMemoryRegisterCache) Set(owner, key string, value []byte) { c.data[owner+"~"+key] = value } -func (c *memRegisterCache) Persist() error { +func (c *InMemoryRegisterCache) Persist() error { // No-op return nil } -type fileRegisterCache struct { +type FileRegisterCache struct { filePath string data map[string]flow.RegisterEntry } -func newFileRegisterCache(filePath string) *fileRegisterCache { - cache := &fileRegisterCache{filePath: filePath} +var _ RegisterCache = &FileRegisterCache{} + +func NewFileRegisterCache(filePath string) *FileRegisterCache { + cache := &FileRegisterCache{filePath: filePath} data := make(map[string]flow.RegisterEntry) if _, err := os.Stat(filePath); err == nil { @@ -86,7 +90,7 @@ func newFileRegisterCache(filePath string) *fileRegisterCache { return cache } -func (f *fileRegisterCache) Get(owner, key string) ([]byte, bool) { +func (f *FileRegisterCache) Get(owner, key string) ([]byte, bool) { v, found := f.data[owner+"~"+key] if found { return v.Value, found @@ -94,7 +98,7 @@ func (f *fileRegisterCache) Get(owner, key string) ([]byte, bool) { return nil, found } -func (f *fileRegisterCache) Set(owner, key string, value []byte) { +func (f *FileRegisterCache) Set(owner, key string, value []byte) { valueCopy := make([]byte, len(value)) copy(valueCopy, value) ownerAddr := flow.BytesToAddress([]byte(owner)) @@ -105,7 +109,7 @@ func (f *fileRegisterCache) Set(owner, key string, value []byte) { } } -func (c *fileRegisterCache) Persist() error { +func (c *FileRegisterCache) Persist() error { f, err := os.OpenFile(c.filePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) if err != nil { return err diff --git a/utils/debug/remoteDebugger.go b/utils/debug/remoteDebugger.go index bb088d3b2fd..4bb4d35408c 100644 --- a/utils/debug/remoteDebugger.go +++ b/utils/debug/remoteDebugger.go @@ -9,19 +9,18 @@ import ( ) type RemoteDebugger struct { - vm fvm.VM - ctx fvm.Context - grpcAddress string + vm fvm.VM + ctx fvm.Context } -// Warning : make sure you use the proper flow-go version, same version as the network you are collecting registers -// from, otherwise the execution might differ from the way runs on the network +// NewRemoteDebugger creates a new remote debugger. +// NOTE: Make sure to use the same version of flow-go as the network +// you are collecting registers from, otherwise the execution might differ +// from the way it runs on the network func NewRemoteDebugger( - grpcAddress string, chain flow.Chain, logger zerolog.Logger, ) *RemoteDebugger { - vm := fvm.NewVirtualMachine() // no signature processor here @@ -33,107 +32,55 @@ func NewRemoteDebugger( ) return &RemoteDebugger{ - ctx: ctx, - vm: vm, - grpcAddress: grpcAddress, + ctx: ctx, + vm: vm, } } -// RunTransaction runs the transaction given the latest sealed block data +// RunTransaction runs the transaction using the given storage snapshot. func (d *RemoteDebugger) RunTransaction( txBody *flow.TransactionBody, + snapshot StorageSnapshot, + blockHeader *flow.Header, ) ( txErr error, processError error, ) { - snapshot := NewRemoteStorageSnapshot(d.grpcAddress) - defer snapshot.Close() - blockCtx := fvm.NewContextFromParent( d.ctx, - fvm.WithBlockHeader(d.ctx.BlockHeader)) - tx := fvm.Transaction(txBody, 0) - _, output, err := d.vm.Run(blockCtx, tx, snapshot) - if err != nil { - return nil, err - } - return output.Err, nil -} - -// RunTransactionAtBlockID runs the transaction and tries to collect the registers at -// the given blockID note that it would be very likely that block is far in the -// past and you can't find the trie to read the registers from. -// if regCachePath is empty, the register values won't be cached -func (d *RemoteDebugger) RunTransactionAtBlockID( - txBody *flow.TransactionBody, - blockID flow.Identifier, - regCachePath string, -) ( - txErr error, - processError error, -) { - snapshot := NewRemoteStorageSnapshot(d.grpcAddress, WithBlockID(blockID)) - defer snapshot.Close() + fvm.WithBlockHeader(blockHeader)) - blockCtx := fvm.NewContextFromParent( - d.ctx, - fvm.WithBlockHeader(d.ctx.BlockHeader)) - if len(regCachePath) > 0 { - snapshot.Cache = newFileRegisterCache(regCachePath) - } tx := fvm.Transaction(txBody, 0) + _, output, err := d.vm.Run(blockCtx, tx, snapshot) if err != nil { return nil, err } - err = snapshot.Cache.Persist() - if err != nil { - return nil, err - } return output.Err, nil } +// RunScript runs the script using the given storage snapshot. func (d *RemoteDebugger) RunScript( code []byte, arguments [][]byte, + snapshot StorageSnapshot, + blockHeader *flow.Header, ) ( value cadence.Value, scriptError error, processError error, ) { - snapshot := NewRemoteStorageSnapshot(d.grpcAddress) - defer snapshot.Close() - scriptCtx := fvm.NewContextFromParent( d.ctx, - fvm.WithBlockHeader(d.ctx.BlockHeader)) - script := fvm.Script(code).WithArguments(arguments...) - _, output, err := d.vm.Run(scriptCtx, script, snapshot) - if err != nil { - return nil, nil, err - } - return output.Value, output.Err, nil -} - -func (d *RemoteDebugger) RunScriptAtBlockID( - code []byte, - arguments [][]byte, - blockID flow.Identifier, -) ( - value cadence.Value, - scriptError error, - processError error, -) { - snapshot := NewRemoteStorageSnapshot(d.grpcAddress, WithBlockID(blockID)) - defer snapshot.Close() + fvm.WithBlockHeader(blockHeader), + ) - scriptCtx := fvm.NewContextFromParent( - d.ctx, - fvm.WithBlockHeader(d.ctx.BlockHeader)) script := fvm.Script(code).WithArguments(arguments...) + _, output, err := d.vm.Run(scriptCtx, script, snapshot) if err != nil { return nil, nil, err } + return output.Value, output.Err, nil } diff --git a/utils/debug/remoteView.go b/utils/debug/remoteView.go index 5951a555ac0..fba6dccf46a 100644 --- a/utils/debug/remoteView.go +++ b/utils/debug/remoteView.go @@ -3,164 +3,154 @@ package debug import ( "context" + "github.com/onflow/flow/protobuf/go/flow/entities" "github.com/onflow/flow/protobuf/go/flow/execution" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" + "github.com/onflow/flow/protobuf/go/flow/executiondata" + "github.com/onflow/flow-go/fvm/storage/snapshot" "github.com/onflow/flow-go/model/flow" ) -// RemoteStorageSnapshot provides a storage snapshot connected to a live -// execution node to read the registers. -type RemoteStorageSnapshot struct { - Cache registerCache - BlockID []byte - BlockHeader *flow.Header - connection *grpc.ClientConn - executionAPIclient execution.ExecutionAPIClient +type StorageSnapshot interface { + snapshot.StorageSnapshot } -// A RemoteStorageSnapshotOption sets a configuration parameter for the remote -// snapshot -type RemoteStorageSnapshotOption func(*RemoteStorageSnapshot) *RemoteStorageSnapshot - -// WithFileCache sets the output path to store -// register values so can be fetched from a file cache -// it loads the values from the cache upon object construction -func WithCache(cache registerCache) RemoteStorageSnapshotOption { - return func(snapshot *RemoteStorageSnapshot) *RemoteStorageSnapshot { - snapshot.Cache = cache - return snapshot - } -} - -// WithBlockID sets the blockID for the remote snapshot, if not used -// remote snapshot will use the latest sealed block -func WithBlockID(blockID flow.Identifier) RemoteStorageSnapshotOption { - return func(snapshot *RemoteStorageSnapshot) *RemoteStorageSnapshot { - snapshot.BlockID = blockID[:] - var err error - snapshot.BlockHeader, err = snapshot.getBlockHeader(blockID) - if err != nil { - panic(err) - } - return snapshot - } +// ExecutionNodeStorageSnapshot provides a storage snapshot connected +// to an execution node to read the registers. +type ExecutionNodeStorageSnapshot struct { + Client execution.ExecutionAPIClient + Cache RegisterCache + BlockID flow.Identifier } -func NewRemoteStorageSnapshot( - grpcAddress string, - opts ...RemoteStorageSnapshotOption, -) *RemoteStorageSnapshot { - conn, err := grpc.Dial( - grpcAddress, - grpc.WithTransportCredentials(insecure.NewCredentials())) - if err != nil { - panic(err) - } - - snapshot := &RemoteStorageSnapshot{ - connection: conn, - executionAPIclient: execution.NewExecutionAPIClient(conn), - Cache: newMemRegisterCache(), - } +var _ StorageSnapshot = &ExecutionNodeStorageSnapshot{} - snapshot.BlockID, snapshot.BlockHeader, err = snapshot.getLatestBlockID() - if err != nil { - panic(err) +func NewExecutionNodeStorageSnapshot( + client execution.ExecutionAPIClient, + cache RegisterCache, + blockID flow.Identifier, +) ( + *ExecutionNodeStorageSnapshot, + error, +) { + if cache == nil { + cache = NewInMemoryRegisterCache() } - for _, applyOption := range opts { - snapshot = applyOption(snapshot) - } - return snapshot + return &ExecutionNodeStorageSnapshot{ + Client: client, + Cache: cache, + BlockID: blockID, + }, nil } -func (snapshot *RemoteStorageSnapshot) Close() error { - return snapshot.connection.Close() +func (snapshot *ExecutionNodeStorageSnapshot) Close() error { + return snapshot.Cache.Persist() } -func (snapshot *RemoteStorageSnapshot) getLatestBlockID() ( - []byte, - *flow.Header, +func (snapshot *ExecutionNodeStorageSnapshot) Get( + id flow.RegisterID, +) ( + flow.RegisterValue, error, ) { - req := &execution.GetLatestBlockHeaderRequest{ - IsSealed: true, + // first, check the cache + value, found := snapshot.Cache.Get(id.Owner, id.Key) + if found { + return value, nil + } + + // if the register is not cached, fetch it from the execution node + req := &execution.GetRegisterAtBlockIDRequest{ + BlockId: snapshot.BlockID[:], + RegisterOwner: []byte(id.Owner), + RegisterKey: []byte(id.Key), } - resp, err := snapshot.executionAPIclient.GetLatestBlockHeader( + // TODO use a proper context for timeouts + resp, err := snapshot.Client.GetRegisterAtBlockID( context.Background(), - req) + req, + ) if err != nil { - return nil, nil, err + return nil, err } - // TODO set chainID and parentID - header := &flow.Header{ - Height: resp.Block.Height, - Timestamp: resp.Block.Timestamp.AsTime(), - } + // append register to the cache + snapshot.Cache.Set(id.Owner, id.Key, resp.Value) - return resp.Block.Id, header, nil + return resp.Value, nil } -func (snapshot *RemoteStorageSnapshot) getBlockHeader( - blockID flow.Identifier, +// ExecutionDataStorageSnapshot provides a storage snapshot connected +// to an access node to read the registers (via its execution data API). +type ExecutionDataStorageSnapshot struct { + Client executiondata.ExecutionDataAPIClient + Cache RegisterCache + BlockHeight uint64 +} + +var _ StorageSnapshot = &ExecutionDataStorageSnapshot{} + +func NewExecutionDataStorageSnapshot( + client executiondata.ExecutionDataAPIClient, + cache RegisterCache, + blockHeight uint64, ) ( - *flow.Header, + *ExecutionDataStorageSnapshot, error, ) { - req := &execution.GetBlockHeaderByIDRequest{ - Id: blockID[:], + if cache == nil { + cache = NewInMemoryRegisterCache() } - resp, err := snapshot.executionAPIclient.GetBlockHeaderByID( - context.Background(), - req) - if err != nil { - return nil, err - } - - // TODO set chainID and parentID - header := &flow.Header{ - Height: resp.Block.Height, - Timestamp: resp.Block.Timestamp.AsTime(), - } + return &ExecutionDataStorageSnapshot{ + Client: client, + Cache: cache, + BlockHeight: blockHeight, + }, nil +} - return header, nil +func (snapshot *ExecutionDataStorageSnapshot) Close() error { + return snapshot.Cache.Persist() } -func (snapshot *RemoteStorageSnapshot) Get( +func (snapshot *ExecutionDataStorageSnapshot) Get( id flow.RegisterID, ) ( flow.RegisterValue, error, ) { - // then check the read cache + // first, check the cache value, found := snapshot.Cache.Get(id.Owner, id.Key) if found { return value, nil } - // last use the grpc api the - req := &execution.GetRegisterAtBlockIDRequest{ - BlockId: []byte(snapshot.BlockID), - RegisterOwner: []byte(id.Owner), - RegisterKey: []byte(id.Key), + // if the register is not cached, fetch it from the execution data API + req := &executiondata.GetRegisterValuesRequest{ + BlockHeight: snapshot.BlockHeight, + RegisterIds: []*entities.RegisterID{ + { + Owner: []byte(id.Owner), + Key: []byte(id.Key), + }, + }, } // TODO use a proper context for timeouts - resp, err := snapshot.executionAPIclient.GetRegisterAtBlockID( + resp, err := snapshot.Client.GetRegisterValues( context.Background(), - req) + req, + ) if err != nil { return nil, err } - snapshot.Cache.Set(id.Owner, id.Key, resp.Value) + value = resp.Values[0] - // append value to the file cache + // append register to the cache + snapshot.Cache.Set(id.Owner, id.Key, value) - return resp.Value, nil + return value, nil } From 283f5533269d7c69ba2b3db31fb418724eff6e10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Fri, 24 Jan 2025 15:25:56 -0800 Subject: [PATCH 2/6] add support for getCurrentBlock() in remote debugger though the blocks are unavailable, the block header is provided. this allows us to at least to provide the current block info --- fvm/environment/block_info.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/fvm/environment/block_info.go b/fvm/environment/block_info.go index ce803d22479..663e03b4620 100644 --- a/fvm/environment/block_info.go +++ b/fvm/environment/block_info.go @@ -133,11 +133,6 @@ func (info *blockInfo) GetBlockAtHeight( "get block at height failed: %w", err) } - if info.blocks == nil { - return runtime.Block{}, false, errors.NewOperationNotSupportedError( - "GetBlockAtHeight") - } - if info.blockHeader != nil && height == info.blockHeader.Height { return runtimeBlockFromHeader(info.blockHeader), true, nil } @@ -146,6 +141,11 @@ func (info *blockInfo) GetBlockAtHeight( return runtime.Block{}, false, nil } + if info.blocks == nil { + return runtime.Block{}, false, errors.NewOperationNotSupportedError( + "GetBlockAtHeight") + } + header, err := info.blocks.ByHeightFrom(height, info.blockHeader) // TODO (ramtin): remove dependency on storage and move this if condition // to blockfinder From f5ce99f1f5dacf693100eeef6c81e551d6598552 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Fri, 24 Jan 2025 15:26:38 -0800 Subject: [PATCH 3/6] execution data API returns resulting data, so create snapshot for parent block --- cmd/util/cmd/debug-tx/cmd.go | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/cmd/util/cmd/debug-tx/cmd.go b/cmd/util/cmd/debug-tx/cmd.go index 24d1750c325..a303cc188f6 100644 --- a/cmd/util/cmd/debug-tx/cmd.go +++ b/cmd/util/cmd/debug-tx/cmd.go @@ -4,6 +4,7 @@ import ( "context" "github.com/onflow/flow/protobuf/go/flow/execution" + "github.com/onflow/flow/protobuf/go/flow/executiondata" "github.com/rs/zerolog/log" "github.com/spf13/cobra" "google.golang.org/grpc" @@ -124,7 +125,20 @@ func run(*cobra.Command, []string) { var snap snapshot.StorageSnapshot if flagUseExecutionDataAPI { - snap, err = debug.NewExecutionDataStorageSnapshot(flowClient.ExecutionDataRPCClient(), nil, blockHeight) + accessConn, err := grpc.NewClient( + flagAccessAddress, + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) + if err != nil { + log.Fatal().Err(err).Msg("failed to create access connection") + } + defer accessConn.Close() + + executionDataClient := executiondata.NewExecutionDataAPIClient(accessConn) + + // The execution data API provides the *resulting* data, + // so fetch the data for the parent block for the *initial* data. + snap, err = debug.NewExecutionDataStorageSnapshot(executionDataClient, nil, blockHeight-1) if err != nil { log.Fatal().Err(err).Msg("failed to create storage snapshot") } From 7d305dabc613656fc86027aa583076ae9aedeb9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Fri, 24 Jan 2025 15:42:12 -0800 Subject: [PATCH 4/6] don't exit on transaction error --- cmd/util/cmd/debug-tx/cmd.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/util/cmd/debug-tx/cmd.go b/cmd/util/cmd/debug-tx/cmd.go index a303cc188f6..c4e0393b9c3 100644 --- a/cmd/util/cmd/debug-tx/cmd.go +++ b/cmd/util/cmd/debug-tx/cmd.go @@ -189,7 +189,7 @@ func run(*cobra.Command, []string) { txErr, processErr := debugger.RunTransaction(txBody, snap, header) if txErr != nil { - log.Fatal().Err(txErr).Msg("transaction error") + log.Err(txErr).Msg("transaction error") } if processErr != nil { log.Fatal().Err(processErr).Msg("process error") From 0c0326bacf1ad4329d049ef5b88c48dfebb47df4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Fri, 24 Jan 2025 13:51:16 -0800 Subject: [PATCH 5/6] optionally dump all read and written registers --- cmd/util/cmd/debug-tx/cmd.go | 41 +++++++++++++++++++++++++++++++---- utils/debug/README.md | 6 ++--- utils/debug/remoteDebugger.go | 12 +++++++--- 3 files changed, 49 insertions(+), 10 deletions(-) diff --git a/cmd/util/cmd/debug-tx/cmd.go b/cmd/util/cmd/debug-tx/cmd.go index c4e0393b9c3..c940218de04 100644 --- a/cmd/util/cmd/debug-tx/cmd.go +++ b/cmd/util/cmd/debug-tx/cmd.go @@ -1,12 +1,15 @@ package debug_tx import ( + "cmp" "context" + "encoding/hex" "github.com/onflow/flow/protobuf/go/flow/execution" "github.com/onflow/flow/protobuf/go/flow/executiondata" "github.com/rs/zerolog/log" "github.com/spf13/cobra" + "golang.org/x/exp/slices" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" @@ -29,6 +32,7 @@ var ( flagComputeLimit uint64 flagProposalKeySeq uint64 flagUseExecutionDataAPI bool + flagDumpRegisters bool ) var Cmd = &cobra.Command{ @@ -61,6 +65,8 @@ func init() { Cmd.Flags().Uint64Var(&flagProposalKeySeq, "proposal-key-seq", 0, "proposal key sequence number") Cmd.Flags().BoolVar(&flagUseExecutionDataAPI, "use-execution-data-api", false, "use the execution data API") + + Cmd.Flags().BoolVar(&flagDumpRegisters, "dump-registers", false, "dump registers") } func run(*cobra.Command, []string) { @@ -187,11 +193,38 @@ func run(*cobra.Command, []string) { proposalKeySequenceNumber, ) - txErr, processErr := debugger.RunTransaction(txBody, snap, header) - if txErr != nil { - log.Err(txErr).Msg("transaction error") - } + resultSnapshot, txErr, processErr := debugger.RunTransaction(txBody, snap, header) if processErr != nil { log.Fatal().Err(processErr).Msg("process error") } + + if flagDumpRegisters { + log.Info().Msg("Read registers:") + readRegisterIDs := resultSnapshot.ReadRegisterIDs() + sortRegisters(readRegisterIDs) + for _, registerID := range readRegisterIDs { + log.Info().Msgf("\t%s", registerID) + } + + log.Info().Msg("Written registers:") + for _, updatedRegister := range resultSnapshot.UpdatedRegisters() { + log.Info().Msgf( + "\t%s, %s", + updatedRegister.Key, + hex.EncodeToString(updatedRegister.Value), + ) + } + } + if txErr != nil { + log.Err(txErr).Msg("transaction error") + } +} + +func sortRegisters(registerIDs []flow.RegisterID) { + slices.SortFunc(registerIDs, func(a, b flow.RegisterID) int { + return cmp.Or( + cmp.Compare(a.Owner, b.Owner), + cmp.Compare(a.Key, b.Key), + ) + }) } diff --git a/utils/debug/README.md b/utils/debug/README.md index 6d85a94a6c1..6bdc182bba0 100644 --- a/utils/debug/README.md +++ b/utils/debug/README.md @@ -89,7 +89,7 @@ func TestDebugger_RunTransactionAgainstExecutionNodeAtBlockID(t *testing.T) { txBody := getTransaction(chain) - txErr, err := debugger.RunTransaction(txBody, snapshot, header) + _, txErr, err := debugger.RunTransaction(txBody, snapshot, header) require.NoError(t, txErr) require.NoError(t, err) } @@ -137,12 +137,12 @@ func TestDebugger_RunTransactionAgainstAccessNodeAtBlockIDWithFileCache(t *testi txBody := getTransaction(chain) // the first run will cache the results - txErr, err := debugger.RunTransaction(txBody, snapshot, header) + _, txErr, err := debugger.RunTransaction(txBody, snapshot, header) require.NoError(t, txErr) require.NoError(t, err) // the second run should only use the cache. - txErr, err = debugger.RunTransaction(txBody, snapshot, header) + _, txErr, err = debugger.RunTransaction(txBody, snapshot, header) require.NoError(t, txErr) require.NoError(t, err) } diff --git a/utils/debug/remoteDebugger.go b/utils/debug/remoteDebugger.go index 4bb4d35408c..524f7ed5dc3 100644 --- a/utils/debug/remoteDebugger.go +++ b/utils/debug/remoteDebugger.go @@ -5,6 +5,7 @@ import ( "github.com/rs/zerolog" "github.com/onflow/flow-go/fvm" + "github.com/onflow/flow-go/fvm/storage/snapshot" "github.com/onflow/flow-go/model/flow" ) @@ -43,6 +44,7 @@ func (d *RemoteDebugger) RunTransaction( snapshot StorageSnapshot, blockHeader *flow.Header, ) ( + resultSnapshot *snapshot.ExecutionSnapshot, txErr error, processError error, ) { @@ -52,11 +54,15 @@ func (d *RemoteDebugger) RunTransaction( tx := fvm.Transaction(txBody, 0) - _, output, err := d.vm.Run(blockCtx, tx, snapshot) + var ( + output fvm.ProcedureOutput + err error + ) + resultSnapshot, output, err = d.vm.Run(blockCtx, tx, snapshot) if err != nil { - return nil, err + return resultSnapshot, nil, err } - return output.Err, nil + return resultSnapshot, output.Err, nil } // RunScript runs the script using the given storage snapshot. From 15f26d2d68d0401f49149eb14989ff2dd9bc2119 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Tue, 28 Jan 2025 11:37:10 -0800 Subject: [PATCH 6/6] also run preceeding transactions --- cmd/util/cmd/debug-tx/cmd.go | 128 +++++++++++++++++++++++++++++------ 1 file changed, 108 insertions(+), 20 deletions(-) diff --git a/cmd/util/cmd/debug-tx/cmd.go b/cmd/util/cmd/debug-tx/cmd.go index c940218de04..1c302748e82 100644 --- a/cmd/util/cmd/debug-tx/cmd.go +++ b/cmd/util/cmd/debug-tx/cmd.go @@ -5,6 +5,7 @@ import ( "context" "encoding/hex" + client "github.com/onflow/flow-go-sdk/access/grpc" "github.com/onflow/flow/protobuf/go/flow/execution" "github.com/onflow/flow/protobuf/go/flow/executiondata" "github.com/rs/zerolog/log" @@ -86,18 +87,9 @@ func run(*cobra.Command, []string) { flowClient, err := grpcclient.FlowClient(config) if err != nil { - log.Fatal().Err(err).Msg("failed to create flow client") + log.Fatal().Err(err).Msg("failed to create client") } - log.Info().Msg("Fetching transaction ...") - - tx, err := flowClient.GetTransaction(context.Background(), sdk.Identifier(txID)) - if err != nil { - log.Fatal().Err(err).Msg("failed to fetch transaction") - } - - log.Info().Msgf("Fetched transaction: %s", tx.ID()) - log.Info().Msg("Fetching transaction result ...") txResult, err := flowClient.GetTransactionResult(context.Background(), sdk.Identifier(txID)) @@ -115,6 +107,17 @@ func run(*cobra.Command, []string) { blockHeight, ) + log.Info().Msg("Fetching transactions of block ...") + + txsResult, err := flowClient.GetTransactionsByBlockID(context.Background(), sdk.Identifier(blockID)) + if err != nil { + log.Fatal().Err(err).Msg("failed to fetch transactions of block") + } + + for _, blockTx := range txsResult { + log.Info().Msgf("Block transaction: %s", blockTx.ID()) + } + log.Info().Msg("Fetching block header ...") header, err := debug.GetAccessAPIBlockHeader(flowClient.RPCClient(), context.Background(), blockID) @@ -128,7 +131,7 @@ func run(*cobra.Command, []string) { header.Height, ) - var snap snapshot.StorageSnapshot + var remoteSnapshot snapshot.StorageSnapshot if flagUseExecutionDataAPI { accessConn, err := grpc.NewClient( @@ -144,7 +147,7 @@ func run(*cobra.Command, []string) { // The execution data API provides the *resulting* data, // so fetch the data for the parent block for the *initial* data. - snap, err = debug.NewExecutionDataStorageSnapshot(executionDataClient, nil, blockHeight-1) + remoteSnapshot, err = debug.NewExecutionDataStorageSnapshot(executionDataClient, nil, blockHeight-1) if err != nil { log.Fatal().Err(err).Msg("failed to create storage snapshot") } @@ -159,16 +162,59 @@ func run(*cobra.Command, []string) { defer executionConn.Close() executionClient := execution.NewExecutionAPIClient(executionConn) - snap, err = debug.NewExecutionNodeStorageSnapshot(executionClient, nil, blockID) + + remoteSnapshot, err = debug.NewExecutionNodeStorageSnapshot(executionClient, nil, blockID) if err != nil { log.Fatal().Err(err).Msg("failed to create storage snapshot") } } - log.Info().Msg("Debugging transaction ...") + blockSnapshot := newBlockSnapshot(remoteSnapshot) debugger := debug.NewRemoteDebugger(chain, log.Logger) + for _, blockTx := range txsResult { + blockTxID := flow.Identifier(blockTx.ID()) + + isDebuggedTx := blockTxID == txID + + dumpRegisters := flagDumpRegisters && isDebuggedTx + + runTransaction( + debugger, + blockTxID, + flowClient, + blockSnapshot, + header, + dumpRegisters, + ) + + if isDebuggedTx { + break + } + } +} + +func runTransaction( + debugger *debug.RemoteDebugger, + txID flow.Identifier, + flowClient *client.Client, + blockSnapshot *blockSnapshot, + header *flow.Header, + dumpRegisters bool, +) { + + log.Info().Msgf("Fetching transaction %s ...", txID) + + tx, err := flowClient.GetTransaction(context.Background(), sdk.Identifier(txID)) + if err != nil { + log.Fatal().Err(err).Msg("Failed to fetch transaction") + } + + log.Info().Msgf("Fetched transaction: %s", tx.ID()) + + log.Info().Msgf("Debugging transaction %s ...", tx.ID()) + txBody := flow.NewTransactionBody(). SetScript(tx.Script). SetComputeLimit(flagComputeLimit). @@ -193,12 +239,29 @@ func run(*cobra.Command, []string) { proposalKeySequenceNumber, ) - resultSnapshot, txErr, processErr := debugger.RunTransaction(txBody, snap, header) + resultSnapshot, txErr, processErr := debugger.RunTransaction( + txBody, + blockSnapshot, + header, + ) if processErr != nil { - log.Fatal().Err(processErr).Msg("process error") + log.Fatal().Err(processErr).Msg("Failed to process transaction") + } + + if txErr != nil { + log.Err(txErr).Msg("Transaction failed") + } else { + log.Info().Msg("Transaction succeeded") } - if flagDumpRegisters { + for _, updatedRegister := range resultSnapshot.UpdatedRegisters() { + blockSnapshot.Set( + updatedRegister.Key, + updatedRegister.Value, + ) + } + + if dumpRegisters { log.Info().Msg("Read registers:") readRegisterIDs := resultSnapshot.ReadRegisterIDs() sortRegisters(readRegisterIDs) @@ -215,9 +278,6 @@ func run(*cobra.Command, []string) { ) } } - if txErr != nil { - log.Err(txErr).Msg("transaction error") - } } func sortRegisters(registerIDs []flow.RegisterID) { @@ -228,3 +288,31 @@ func sortRegisters(registerIDs []flow.RegisterID) { ) }) } + +type blockSnapshot struct { + cache *debug.InMemoryRegisterCache + backing snapshot.StorageSnapshot +} + +var _ snapshot.StorageSnapshot = (*blockSnapshot)(nil) + +func newBlockSnapshot(backing snapshot.StorageSnapshot) *blockSnapshot { + cache := debug.NewInMemoryRegisterCache() + return &blockSnapshot{ + cache: cache, + backing: backing, + } +} + +func (s *blockSnapshot) Get(id flow.RegisterID) (flow.RegisterValue, error) { + data, found := s.cache.Get(id.Key, id.Owner) + if found { + return data, nil + } + + return s.backing.Get(id) +} + +func (s *blockSnapshot) Set(id flow.RegisterID, value flow.RegisterValue) { + s.cache.Set(id.Key, id.Owner, value) +}