diff --git a/cmd/util/cmd/debug-tx/cmd.go b/cmd/util/cmd/debug-tx/cmd.go new file mode 100644 index 00000000000..a7f955549e5 --- /dev/null +++ b/cmd/util/cmd/debug-tx/cmd.go @@ -0,0 +1,147 @@ +package debug_tx + +import ( + "context" + + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" + + sdk "github.com/onflow/flow-go-sdk" + + "github.com/onflow/flow-go/model/flow" + "github.com/onflow/flow-go/module/grpcclient" + "github.com/onflow/flow-go/utils/debug" +) + +// use the following command to forward port 9000 from the EN to localhost:9001 +// `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 +) + +var Cmd = &cobra.Command{ + Use: "debug-tx", + Short: "debug a transaction", + Run: run, +} + +func init() { + + Cmd.Flags().StringVar( + &flagChain, + "chain", + "", + "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(&flagTx, "tx", "", "transaction ID") + _ = Cmd.MarkFlagRequired("tx") + + 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") +} + +func run(*cobra.Command, []string) { + + chainID := flow.ChainID(flagChain) + chain := chainID.Chain() + + txID, err := flow.HexStringToIdentifier(flagTx) + if err != nil { + log.Fatal().Err(err).Msg("failed to parse transaction ID") + } + + config, err := grpcclient.NewFlowClientConfig(flagAccessAddress, "", flow.ZeroID, true) + if err != nil { + log.Fatal().Err(err).Msg("failed to create flow client config") + } + + flowClient, err := grpcclient.FlowClient(config) + if err != nil { + log.Fatal().Err(err).Msg("failed to create flow 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)) + if err != nil { + log.Fatal().Err(err).Msg("failed to fetch transaction result") + } + + log.Info().Msgf("Fetched transaction result: %s at block %s", txResult.Status, txResult.BlockID) + + log.Info().Msg("Debugging transaction ...") + + debugger := debug.NewRemoteDebugger( + flagExecutionAddress, + chain, + log.Logger, + ) + + txBody := flow.NewTransactionBody(). + SetScript(tx.Script). + SetComputeLimit(flagComputeLimit). + SetPayer(flow.Address(tx.Payer)) + + for _, argument := range tx.Arguments { + txBody.AddArgument(argument) + } + + for _, authorizer := range tx.Authorizers { + txBody.AddAuthorizer(flow.Address(authorizer)) + } + + proposalKeySequenceNumber := tx.ProposalKey.SequenceNumber + if flagProposalKeySeq != 0 { + proposalKeySequenceNumber = flagProposalKeySeq + } + + txBody.SetProposalKey( + flow.Address(tx.ProposalKey.Address), + tx.ProposalKey.KeyIndex, + 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") + } + if processErr != nil { + log.Fatal().Err(processErr).Msg("process error") + } +} diff --git a/cmd/util/cmd/root.go b/cmd/util/cmd/root.go index 12e50909d2c..c4464fba648 100644 --- a/cmd/util/cmd/root.go +++ b/cmd/util/cmd/root.go @@ -17,6 +17,7 @@ import ( checkpoint_collect_stats "github.com/onflow/flow-go/cmd/util/cmd/checkpoint-collect-stats" checkpoint_list_tries "github.com/onflow/flow-go/cmd/util/cmd/checkpoint-list-tries" checkpoint_trie_stats "github.com/onflow/flow-go/cmd/util/cmd/checkpoint-trie-stats" + debug_tx "github.com/onflow/flow-go/cmd/util/cmd/debug-tx" diff_states "github.com/onflow/flow-go/cmd/util/cmd/diff-states" epochs "github.com/onflow/flow-go/cmd/util/cmd/epochs/cmd" export "github.com/onflow/flow-go/cmd/util/cmd/exec-data-json-export" @@ -118,6 +119,7 @@ func addCommands() { rootCmd.AddCommand(run_script.Cmd) rootCmd.AddCommand(system_addresses.Cmd) rootCmd.AddCommand(check_storage.Cmd) + rootCmd.AddCommand(debug_tx.Cmd) } func initConfig() { diff --git a/utils/debug/remoteDebugger.go b/utils/debug/remoteDebugger.go index 86c8292588a..bb088d3b2fd 100644 --- a/utils/debug/remoteDebugger.go +++ b/utils/debug/remoteDebugger.go @@ -16,9 +16,12 @@ type RemoteDebugger struct { // 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 -func NewRemoteDebugger(grpcAddress string, +func NewRemoteDebugger( + grpcAddress string, chain flow.Chain, - logger zerolog.Logger) *RemoteDebugger { + logger zerolog.Logger, +) *RemoteDebugger { + vm := fvm.NewVirtualMachine() // no signature processor here @@ -57,7 +60,7 @@ func (d *RemoteDebugger) RunTransaction( return output.Err, nil } -// RunTransaction runs the transaction and tries to collect the registers at +// 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