Skip to content

Refactor the remote debugger and add support for using the execution data API #6929

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jan 28, 2025
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 80 additions & 9 deletions cmd/util/cmd/debug-script/cmd.go
Original file line number Diff line number Diff line change
@@ -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"
)
Expand All @@ -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{
Expand All @@ -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) {
Expand All @@ -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")
Expand Down
139 changes: 111 additions & 28 deletions cmd/util/cmd/debug-tx/cmd.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
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"

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"
Expand All @@ -17,13 +25,14 @@ 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
flagDumpRegisters bool
)

var Cmd = &cobra.Command{
Expand Down Expand Up @@ -53,9 +62,11 @@ 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")

Cmd.Flags().BoolVar(&flagDumpRegisters, "dump-registers", false, "dump registers")
}

func run(*cobra.Command, []string) {
Expand Down Expand Up @@ -94,16 +105,70 @@ 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 {
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")
}
} 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).
Expand All @@ -128,20 +193,38 @@ 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),
"",
)
}
if txErr != nil {
log.Fatal().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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note (for future expansion): I actually often find it most useful to have the registers sorted in read order for debugging. However currently that ordering is lost because we put the reads in a map.

slices.SortFunc(registerIDs, func(a, b flow.RegisterID) int {
return cmp.Or(
cmp.Compare(a.Owner, b.Owner),
cmp.Compare(a.Key, b.Key),
)
})
}
10 changes: 5 additions & 5 deletions fvm/environment/block_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -146,6 +141,11 @@ func (info *blockInfo) GetBlockAtHeight(
return runtime.Block{}, false, nil
}

if info.blocks == nil {
return runtime.Block{}, false, errors.NewOperationNotSupportedError(
"GetBlockAtHeight")
}

Comment on lines +144 to +148
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move this check after the use of info.blockHeader, which is set in the remote debugger

header, err := info.blocks.ByHeightFrom(height, info.blockHeader)
// TODO (ramtin): remove dependency on storage and move this if condition
// to blockfinder
Expand Down
Loading
Loading