diff --git a/api/api_full.go b/api/api_full.go index 9eaca53f9..29688a5e6 100644 --- a/api/api_full.go +++ b/api/api_full.go @@ -315,13 +315,15 @@ type FullNode interface { // MethodGroup: State // The State methods are used to query, inspect, and interact with chain state. - // All methods take a TipSetKey as a parameter. The state looked up is the state at that tipset. + // Most methods take a TipSetKey as a parameter. The state looked up is the state at that tipset. // A nil TipSetKey can be provided as a param, this will cause the heaviest tipset in the chain to be used. // StateCall runs the given message and returns its result without any persisted changes. StateCall(context.Context, *types.Message, types.TipSetKey) (*InvocResult, error) // StateTransplant returns the result of executing the indicated message, assuming it was executed in the indicated tipset. StateTransplant(context.Context, types.TipSetKey, cid.Cid) (*InvocResult, error) + // StateReplay searches for where the given message was executed, and replays it in that tipset. + StateReplay(context.Context, cid.Cid) (*InvocResult, error) // StateGetActor returns the indicated actor's nonce and balance. StateGetActor(ctx context.Context, actor address.Address, tsk types.TipSetKey) (*types.Actor, error) // StateReadState returns the indicated actor's state. diff --git a/api/apistruct/struct.go b/api/apistruct/struct.go index 3e474f596..33670c276 100644 --- a/api/apistruct/struct.go +++ b/api/apistruct/struct.go @@ -189,6 +189,7 @@ type FullNodeStruct struct { StateSectorPartition func(context.Context, address.Address, abi.SectorNumber, types.TipSetKey) (*miner.SectorLocation, error) `perm:"read"` StateCall func(context.Context, *types.Message, types.TipSetKey) (*api.InvocResult, error) `perm:"read"` StateTransplant func(context.Context, types.TipSetKey, cid.Cid) (*api.InvocResult, error) `perm:"read"` + StateReplay func(context.Context, cid.Cid) (*api.InvocResult, error) `perm:"read"` StateGetActor func(context.Context, address.Address, types.TipSetKey) (*types.Actor, error) `perm:"read"` StateReadState func(context.Context, address.Address, types.TipSetKey) (*api.ActorState, error) `perm:"read"` StateMsgGasCost func(context.Context, cid.Cid, types.TipSetKey) (*api.MsgGasCost, error) `perm:"read"` @@ -884,6 +885,10 @@ func (c *FullNodeStruct) StateTransplant(ctx context.Context, tsk types.TipSetKe return c.Internal.StateTransplant(ctx, tsk, mc) } +func (c *FullNodeStruct) StateReplay(ctx context.Context, mc cid.Cid) (*api.InvocResult, error) { + return c.Internal.StateReplay(ctx, mc) +} + func (c *FullNodeStruct) StateGetActor(ctx context.Context, actor address.Address, tsk types.TipSetKey) (*types.Actor, error) { return c.Internal.StateGetActor(ctx, actor, tsk) } diff --git a/cli/state.go b/cli/state.go index 362e29636..bff41e268 100644 --- a/cli/state.go +++ b/cli/state.go @@ -61,6 +61,7 @@ var stateCmd = &cli.Command{ stateGetActorCmd, stateLookupIDCmd, stateTransplantCmd, + stateReplayCmd, stateSectorSizeCmd, stateReadStateCmd, stateListMessagesCmd, @@ -388,6 +389,16 @@ var stateTransplantCmd = &cli.Command{ Name: "transplant", Usage: "Play a particular message within a tipset", ArgsUsage: "[tipsetKey messageCid]", + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "show-trace", + Usage: "print out full execution trace for given message", + }, + &cli.BoolFlag{ + Name: "detailed-gas", + Usage: "print out detailed gas costs for given message", + }, + }, Action: func(cctx *cli.Context) error { if cctx.Args().Len() < 1 { fmt.Println("usage: [tipset] ") @@ -453,10 +464,90 @@ var stateTransplantCmd = &cli.Command{ fmt.Printf("Exit code: %d\n", res.MsgRct.ExitCode) fmt.Printf("Return: %x\n", res.MsgRct.Return) fmt.Printf("Gas Used: %d\n", res.MsgRct.GasUsed) + + if cctx.Bool("detailed-gas") { + fmt.Printf("Base Fee Burn: %d\n", res.GasCost.BaseFeeBurn) + fmt.Printf("Overestimaton Burn: %d\n", res.GasCost.OverEstimationBurn) + fmt.Printf("Miner Penalty: %d\n", res.GasCost.MinerPenalty) + fmt.Printf("Miner Tip: %d\n", res.GasCost.MinerTip) + fmt.Printf("Refund: %d\n", res.GasCost.Refund) + } + fmt.Printf("Total Message Cost: %d\n", res.GasCost.TotalCost) + if res.MsgRct.ExitCode != 0 { fmt.Printf("Error message: %q\n", res.Error) } + if cctx.Bool("show-trace") { + fmt.Printf("%s\t%s\t%s\t%d\t%x\t%d\t%x\n", res.Msg.From, res.Msg.To, res.Msg.Value, res.Msg.Method, res.Msg.Params, res.MsgRct.ExitCode, res.MsgRct.Return) + printInternalExecutions("\t", res.ExecutionTrace.Subcalls) + } + + return nil + }, +} + +var stateReplayCmd = &cli.Command{ + Name: "replay", + Usage: "Replay a particular message", + ArgsUsage: "", + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "show-trace", + Usage: "print out full execution trace for given message", + }, + &cli.BoolFlag{ + Name: "detailed-gas", + Usage: "print out detailed gas costs for given message", + }, + }, + Action: func(cctx *cli.Context) error { + if cctx.Args().Len() != 1 { + fmt.Println("must provide cid of message to replay") + return nil + } + + mcid, err := cid.Decode(cctx.Args().First()) + if err != nil { + return fmt.Errorf("message cid was invalid: %s", err) + } + + fapi, closer, err := GetFullNodeAPI(cctx) + if err != nil { + return err + } + defer closer() + + ctx := ReqContext(cctx) + + res, err := fapi.StateReplay(ctx, mcid) + if err != nil { + return xerrors.Errorf("replay call failed: %w", err) + } + + fmt.Println("Replay receipt:") + fmt.Printf("Exit code: %d\n", res.MsgRct.ExitCode) + fmt.Printf("Return: %x\n", res.MsgRct.Return) + fmt.Printf("Gas Used: %d\n", res.MsgRct.GasUsed) + + if cctx.Bool("detailed-gas") { + fmt.Printf("Base Fee Burn: %d\n", res.GasCost.BaseFeeBurn) + fmt.Printf("Overestimaton Burn: %d\n", res.GasCost.OverEstimationBurn) + fmt.Printf("Miner Penalty: %d\n", res.GasCost.MinerPenalty) + fmt.Printf("Miner Tip: %d\n", res.GasCost.MinerTip) + fmt.Printf("Refund: %d\n", res.GasCost.Refund) + } + fmt.Printf("Total Message Cost: %d\n", res.GasCost.TotalCost) + + if res.MsgRct.ExitCode != 0 { + fmt.Printf("Error message: %q\n", res.Error) + } + + if cctx.Bool("show-trace") { + fmt.Printf("%s\t%s\t%s\t%d\t%x\t%d\t%x\n", res.Msg.From, res.Msg.To, res.Msg.Value, res.Msg.Method, res.Msg.Params, res.MsgRct.ExitCode, res.MsgRct.Return) + printInternalExecutions("\t", res.ExecutionTrace.Subcalls) + } + return nil }, } diff --git a/node/impl/full/state.go b/node/impl/full/state.go index 349a4918b..959660085 100644 --- a/node/impl/full/state.go +++ b/node/impl/full/state.go @@ -372,6 +372,46 @@ func (a *StateAPI) StateTransplant(ctx context.Context, tsk types.TipSetKey, mc }, nil } +func (a *StateAPI) StateReplay(ctx context.Context, mc cid.Cid) (*api.InvocResult, error) { + mlkp, err := a.StateSearchMsg(ctx, mc) + if err != nil { + return nil, xerrors.Errorf("searching for msg %s: %w", mc, err) + } + if mlkp == nil { + return nil, xerrors.Errorf("didn't find msg %s", mc) + } + + executionTs, err := a.Chain.GetTipSetFromKey(mlkp.TipSet) + if err != nil { + return nil, xerrors.Errorf("loading tipset %s: %w", mlkp.TipSet, err) + } + + ts, err := a.Chain.LoadTipSet(executionTs.Parents()) + if err != nil { + return nil, xerrors.Errorf("loading parent tipset %s: %w", mlkp.TipSet, err) + } + + m, r, err := a.StateManager.Replay(ctx, ts, mlkp.Message) + if err != nil { + return nil, err + } + + var errstr string + if r.ActorErr != nil { + errstr = r.ActorErr.Error() + } + + return &api.InvocResult{ + MsgCid: mlkp.Message, + Msg: m, + MsgRct: &r.MessageReceipt, + GasCost: stmgr.MakeMsgGasCost(m, r), + ExecutionTrace: r.ExecutionTrace, + Error: errstr, + Duration: r.Duration, + }, nil +} + func stateForTs(ctx context.Context, ts *types.TipSet, cstore *store.ChainStore, smgr *stmgr.StateManager) (*state.StateTree, error) { if ts == nil { ts = cstore.GetHeaviestTipSet()