diff --git a/api/api_full.go b/api/api_full.go index adb9a192f..80edf385b 100644 --- a/api/api_full.go +++ b/api/api_full.go @@ -95,7 +95,7 @@ type FullNode interface { //ClientListAsks() []Ask // if tipset is nil, we'll use heaviest - StateCall(context.Context, *types.Message, *types.TipSet) (*types.MessageReceipt, error) + StateCall(context.Context, *types.Message, *types.TipSet) (*MethodCall, error) StateReplay(context.Context, *types.TipSet, cid.Cid) (*ReplayResults, error) StateGetActor(ctx context.Context, actor address.Address, ts *types.TipSet) (*types.Actor, error) StateReadState(ctx context.Context, act *types.Actor, ts *types.TipSet) (*ActorState, error) @@ -271,6 +271,11 @@ type ReplayResults struct { Error string } +type MethodCall struct { + types.MessageReceipt + Error string +} + type ActiveSync struct { Base *types.TipSet Target *types.TipSet diff --git a/api/apistruct/struct.go b/api/apistruct/struct.go index 2824127dc..1a6e09421 100644 --- a/api/apistruct/struct.go +++ b/api/apistruct/struct.go @@ -99,7 +99,7 @@ type FullNodeStruct struct { StateMinerPeerID func(ctx context.Context, m address.Address, ts *types.TipSet) (peer.ID, error) `perm:"read"` StateMinerElectionPeriodStart func(ctx context.Context, actor address.Address, ts *types.TipSet) (uint64, error) `perm:"read"` StateMinerSectorSize func(context.Context, address.Address, *types.TipSet) (uint64, error) `perm:"read"` - StateCall func(context.Context, *types.Message, *types.TipSet) (*types.MessageReceipt, error) `perm:"read"` + StateCall func(context.Context, *types.Message, *types.TipSet) (*api.MethodCall, error) `perm:"read"` StateReplay func(context.Context, *types.TipSet, cid.Cid) (*api.ReplayResults, error) `perm:"read"` StateGetActor func(context.Context, address.Address, *types.TipSet) (*types.Actor, error) `perm:"read"` StateReadState func(context.Context, *types.Actor, *types.TipSet) (*api.ActorState, error) `perm:"read"` @@ -410,7 +410,7 @@ func (c *FullNodeStruct) StateMinerSectorSize(ctx context.Context, actor address return c.Internal.StateMinerSectorSize(ctx, actor, ts) } -func (c *FullNodeStruct) StateCall(ctx context.Context, msg *types.Message, ts *types.TipSet) (*types.MessageReceipt, error) { +func (c *FullNodeStruct) StateCall(ctx context.Context, msg *types.Message, ts *types.TipSet) (*api.MethodCall, error) { return c.Internal.StateCall(ctx, msg, ts) } diff --git a/chain/stmgr/call.go b/chain/stmgr/call.go index bc4a2f9fc..d8d46212a 100644 --- a/chain/stmgr/call.go +++ b/chain/stmgr/call.go @@ -8,13 +8,14 @@ import ( "go.opencensus.io/trace" "golang.org/x/xerrors" + "github.com/filecoin-project/lotus/api" "github.com/filecoin-project/lotus/chain/actors" "github.com/filecoin-project/lotus/chain/store" "github.com/filecoin-project/lotus/chain/types" "github.com/filecoin-project/lotus/chain/vm" ) -func (sm *StateManager) CallRaw(ctx context.Context, msg *types.Message, bstate cid.Cid, r vm.Rand, bheight uint64) (*types.MessageReceipt, error) { +func (sm *StateManager) CallRaw(ctx context.Context, msg *types.Message, bstate cid.Cid, r vm.Rand, bheight uint64) (*api.MethodCall, error) { ctx, span := trace.StartSpan(ctx, "statemanager.CallRaw") defer span.End() @@ -54,14 +55,19 @@ func (sm *StateManager) CallRaw(ctx context.Context, msg *types.Message, bstate return nil, xerrors.Errorf("apply message failed: %w", err) } + var errs string if ret.ActorErr != nil { + errs = ret.ActorErr.Error() log.Warnf("chain call failed: %s", ret.ActorErr) } - return &ret.MessageReceipt, nil + return &api.MethodCall{ + MessageReceipt: ret.MessageReceipt, + Error: errs, + }, nil } -func (sm *StateManager) Call(ctx context.Context, msg *types.Message, ts *types.TipSet) (*types.MessageReceipt, error) { +func (sm *StateManager) Call(ctx context.Context, msg *types.Message, ts *types.TipSet) (*api.MethodCall, error) { if ts == nil { ts = sm.cs.GetHeaviestTipSet() } diff --git a/cli/chain.go b/cli/chain.go index 69b1afbc7..48854d217 100644 --- a/cli/chain.go +++ b/cli/chain.go @@ -174,6 +174,10 @@ var chainGetMsgCmd = &cli.Command{ Name: "getmessage", Usage: "Get and print a message by its cid", Action: func(cctx *cli.Context) error { + if !cctx.Args().Present() { + return fmt.Errorf("must pass a cid of a message to get") + } + api, closer, err := GetFullNodeAPI(cctx) if err != nil { return err diff --git a/cli/state.go b/cli/state.go index 12e438510..2454c06ee 100644 --- a/cli/state.go +++ b/cli/state.go @@ -1,18 +1,24 @@ package cli import ( + "bytes" "context" "encoding/json" "fmt" + "reflect" + "strconv" "strings" "github.com/filecoin-project/go-address" "github.com/filecoin-project/lotus/api" + actors "github.com/filecoin-project/lotus/chain/actors" "github.com/filecoin-project/lotus/chain/types" "github.com/filecoin-project/lotus/miner" + "github.com/libp2p/go-libp2p-core/peer" "golang.org/x/xerrors" "github.com/ipfs/go-cid" + cbg "github.com/whyrusleeping/cbor-gen" "gopkg.in/urfave/cli.v2" ) @@ -39,6 +45,7 @@ var stateCmd = &cli.Command{ stateReadStateCmd, stateListMessagesCmd, stateComputeStateCmd, + stateCallCmd, }, } @@ -648,3 +655,209 @@ var stateComputeStateCmd = &cli.Command{ return nil }, } + +var stateCallCmd = &cli.Command{ + Name: "call", + Usage: "Invoke a method on an actor locally", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "from", + Usage: "", + Value: actors.NetworkAddress.String(), + }, + &cli.StringFlag{ + Name: "value", + Usage: "specify value field for invocation", + Value: "0", + }, + &cli.StringFlag{ + Name: "ret", + Usage: "specify how to parse output (auto, raw, addr, big)", + Value: "auto", + }, + }, + Action: func(cctx *cli.Context) error { + if cctx.Args().Len() < 2 { + return fmt.Errorf("must specify at least actor and method to invoke") + } + + api, closer, err := GetFullNodeAPI(cctx) + if err != nil { + return err + } + defer closer() + + ctx := ReqContext(cctx) + + toa, err := address.NewFromString(cctx.Args().First()) + if err != nil { + return fmt.Errorf("given 'to' address %q was invalid: %w", cctx.Args().First(), err) + } + + froma, err := address.NewFromString(cctx.String("from")) + if err != nil { + return fmt.Errorf("given 'from' address %q was invalid: %w", cctx.String("from"), err) + } + + ts, err := loadTipSet(ctx, cctx, api) + if err != nil { + return err + } + + method, err := strconv.ParseUint(cctx.Args().Get(1), 10, 64) + if err != nil { + return fmt.Errorf("must pass method as a number") + } + + value, err := types.ParseFIL(cctx.String("value")) + if err != nil { + return fmt.Errorf("failed to parse 'value': %s", err) + } + + act, err := api.StateGetActor(ctx, toa, ts) + if err != nil { + return fmt.Errorf("failed to lookup target actor: %s", err) + } + + params, err := parseParamsForMethod(act.Code, method, cctx.Args().Slice()[2:]) + if err != nil { + return fmt.Errorf("failed to parse params: %s", err) + } + + ret, err := api.StateCall(ctx, &types.Message{ + From: froma, + To: toa, + Value: types.BigInt(value), + GasLimit: types.NewInt(10000000000), + GasPrice: types.NewInt(0), + Method: method, + Params: params, + }, ts) + if err != nil { + return fmt.Errorf("state call failed: %s", err) + } + + if ret.ExitCode != 0 { + return fmt.Errorf("invocation failed (exit: %d): %s", ret.ExitCode, ret.Error) + } + + s, err := formatOutput(cctx.String("ret"), ret.Return) + if err != nil { + return fmt.Errorf("failed to format output: %s", err) + } + + fmt.Printf("return: %s\n", s) + + return nil + }, +} + +func formatOutput(t string, val []byte) (string, error) { + switch t { + case "raw", "hex": + return fmt.Sprintf("%x", val), nil + case "address", "addr", "a": + a, err := address.NewFromBytes(val) + if err != nil { + return "", err + } + return a.String(), nil + case "big", "int", "bigint": + bi := types.BigFromBytes(val) + return bi.String(), nil + case "fil": + bi := types.FIL(types.BigFromBytes(val)) + return bi.String(), nil + case "pid", "peerid", "peer": + pid, err := peer.IDFromBytes(val) + if err != nil { + return "", err + } + + return pid.Pretty(), nil + case "auto": + if len(val) == 0 { + return "", nil + } + + a, err := address.NewFromBytes(val) + if err == nil { + return "address: " + a.String(), nil + } + + pid, err := peer.IDFromBytes(val) + if err == nil { + return "peerID: " + pid.Pretty(), nil + } + + bi := types.BigFromBytes(val) + return "bigint: " + bi.String(), nil + default: + return "", fmt.Errorf("unrecognized output type: %q", t) + } +} + +func parseParamsForMethod(act cid.Cid, method uint64, args []string) ([]byte, error) { + if len(args) == 0 { + return nil, nil + } + + var f interface{} + switch act { + case actors.StorageMarketCodeCid: + f = actors.StorageMarketActor{}.Exports()[method] + case actors.StorageMinerCodeCid: + f = actors.StorageMinerActor{}.Exports()[method] + case actors.StoragePowerCodeCid: + f = actors.StoragePowerActor{}.Exports()[method] + case actors.MultisigCodeCid: + f = actors.MultiSigActor{}.Exports()[method] + case actors.PaymentChannelCodeCid: + f = actors.PaymentChannelActor{}.Exports()[method] + default: + return nil, fmt.Errorf("the lazy devs didnt add support for that actor to this call yet") + } + + rf := reflect.TypeOf(f) + if rf.NumIn() != 3 { + return nil, fmt.Errorf("expected referenced method to have three arguments") + } + + paramObj := rf.In(2).Elem() + if paramObj.NumField() != len(args) { + return nil, fmt.Errorf("not enough arguments given to call that method (expecting %d)", paramObj.NumField()) + } + + p := reflect.New(paramObj) + for i := 0; i < len(args); i++ { + switch paramObj.Field(i).Type { + case reflect.TypeOf(address.Address{}): + a, err := address.NewFromString(args[i]) + if err != nil { + return nil, fmt.Errorf("failed to parse address: %s", err) + } + p.Elem().Field(i).Set(reflect.ValueOf(a)) + case reflect.TypeOf(uint64(0)): + val, err := strconv.ParseUint(args[i], 10, 64) + if err != nil { + return nil, err + } + p.Elem().Field(i).Set(reflect.ValueOf(val)) + case reflect.TypeOf(peer.ID("")): + pid, err := peer.IDB58Decode(args[i]) + if err != nil { + return nil, fmt.Errorf("failed to parse peer ID: %s", err) + } + p.Elem().Field(i).Set(reflect.ValueOf(pid)) + default: + return nil, fmt.Errorf("unsupported type for call (TODO): %s", paramObj.Field(i).Type) + } + } + + m := p.Interface().(cbg.CBORMarshaler) + buf := new(bytes.Buffer) + if err := m.MarshalCBOR(buf); err != nil { + return nil, fmt.Errorf("failed to marshal param object: %s", err) + } + return buf.Bytes(), nil +} diff --git a/node/impl/full/state.go b/node/impl/full/state.go index c195b63f0..3ba0dfbf8 100644 --- a/node/impl/full/state.go +++ b/node/impl/full/state.go @@ -108,7 +108,7 @@ func (a *StateAPI) StatePledgeCollateral(ctx context.Context, ts *types.TipSet) return types.BigFromBytes(ret.Return), nil } -func (a *StateAPI) StateCall(ctx context.Context, msg *types.Message, ts *types.TipSet) (*types.MessageReceipt, error) { +func (a *StateAPI) StateCall(ctx context.Context, msg *types.Message, ts *types.TipSet) (*api.MethodCall, error) { return a.StateManager.Call(ctx, msg, ts) } diff --git a/storage/miner.go b/storage/miner.go index d4c122f03..c06036dfe 100644 --- a/storage/miner.go +++ b/storage/miner.go @@ -42,7 +42,7 @@ type Miner struct { type storageMinerApi interface { // Call a read only method on actors (no interaction with the chain required) - StateCall(ctx context.Context, msg *types.Message, ts *types.TipSet) (*types.MessageReceipt, error) + StateCall(ctx context.Context, msg *types.Message, ts *types.TipSet) (*api.MethodCall, error) StateMinerWorker(context.Context, address.Address, *types.TipSet) (address.Address, error) StateMinerElectionPeriodStart(ctx context.Context, actor address.Address, ts *types.TipSet) (uint64, error) StateMinerSectors(context.Context, address.Address, *types.TipSet) ([]*api.ChainSectorInfo, error)