package main import ( "fmt" "os" "github.com/fatih/color" "github.com/urfave/cli/v2" "golang.org/x/xerrors" "github.com/filecoin-project/go-address" "github.com/filecoin-project/go-state-types/abi" "github.com/filecoin-project/go-state-types/big" "github.com/filecoin-project/lotus/api" miner2 "github.com/filecoin-project/specs-actors/v2/actors/builtin/miner" "github.com/filecoin-project/lotus/build" "github.com/filecoin-project/lotus/chain/actors" "github.com/filecoin-project/lotus/chain/actors/builtin/miner" "github.com/filecoin-project/lotus/chain/types" lcli "github.com/filecoin-project/lotus/cli" cliutil "github.com/filecoin-project/lotus/cli/util" "github.com/filecoin-project/lotus/lib/tablewriter" ) var actorCmd = &cli.Command{ Name: "actor", Usage: "manipulate the miner actor", Subcommands: []*cli.Command{ actorWithdrawCmd, actorSetOwnerCmd, actorControl, actorProposeChangeWorker, actorConfirmChangeWorker, }, } var actorWithdrawCmd = &cli.Command{ Name: "withdraw", Usage: "withdraw available balance", ArgsUsage: "[amount (FIL)]", Flags: []cli.Flag{ &cli.StringFlag{ Name: "actor", Usage: "specify the address of miner actor", }, }, Action: func(cctx *cli.Context) error { var maddr address.Address if act := cctx.String("actor"); act != "" { var err error maddr, err = address.NewFromString(act) if err != nil { return fmt.Errorf("parsing address %s: %w", act, err) } } nodeAPI, acloser, err := lcli.GetFullNodeAPI(cctx) if err != nil { return err } defer acloser() ctx := lcli.ReqContext(cctx) if maddr.Empty() { minerAPI, closer, err := lcli.GetStorageMinerAPI(cctx) if err != nil { return err } defer closer() maddr, err = minerAPI.ActorAddress(ctx) if err != nil { return err } } mi, err := nodeAPI.StateMinerInfo(ctx, maddr, types.EmptyTSK) if err != nil { return err } available, err := nodeAPI.StateMinerAvailableBalance(ctx, maddr, types.EmptyTSK) if err != nil { return err } amount := available if cctx.Args().Present() { f, err := types.ParseFIL(cctx.Args().First()) if err != nil { return xerrors.Errorf("parsing 'amount' argument: %w", err) } amount = abi.TokenAmount(f) if amount.GreaterThan(available) { return xerrors.Errorf("can't withdraw more funds than available; requested: %s; available: %s", amount, available) } } params, err := actors.SerializeParams(&miner2.WithdrawBalanceParams{ AmountRequested: amount, // Default to attempting to withdraw all the extra funds in the miner actor }) if err != nil { return err } smsg, err := nodeAPI.MpoolPushMessage(ctx, &types.Message{ To: maddr, From: mi.Owner, Value: types.NewInt(0), Method: miner.Methods.WithdrawBalance, Params: params, }, &api.MessageSendSpec{MaxFee: abi.TokenAmount(types.MustParseFIL("0.1"))}) if err != nil { return err } fmt.Printf("Requested rewards withdrawal in message %s\n", smsg.Cid()) return nil }, } var actorSetOwnerCmd = &cli.Command{ Name: "set-owner", Usage: "Set owner address (this command should be invoked twice, first with the old owner as the senderAddress, and then with the new owner)", ArgsUsage: "[newOwnerAddress senderAddress]", Flags: []cli.Flag{ &cli.StringFlag{ Name: "actor", Usage: "specify the address of miner actor", }, &cli.BoolFlag{ Name: "really-do-it", Usage: "Actually send transaction performing the action", Value: false, }, }, Action: func(cctx *cli.Context) error { if !cctx.Bool("really-do-it") { fmt.Println("Pass --really-do-it to actually execute this action") return nil } if cctx.NArg() != 2 { return fmt.Errorf("must pass new owner address and sender address") } var maddr address.Address if act := cctx.String("actor"); act != "" { var err error maddr, err = address.NewFromString(act) if err != nil { return fmt.Errorf("parsing address %s: %w", act, err) } } nodeAPI, acloser, err := lcli.GetFullNodeAPI(cctx) if err != nil { return err } defer acloser() ctx := lcli.ReqContext(cctx) na, err := address.NewFromString(cctx.Args().First()) if err != nil { return err } newAddrId, err := nodeAPI.StateLookupID(ctx, na, types.EmptyTSK) if err != nil { return err } fa, err := address.NewFromString(cctx.Args().Get(1)) if err != nil { return err } fromAddrId, err := nodeAPI.StateLookupID(ctx, fa, types.EmptyTSK) if err != nil { return err } if maddr.Empty() { minerAPI, closer, err := lcli.GetStorageMinerAPI(cctx) if err != nil { return err } defer closer() maddr, err = minerAPI.ActorAddress(ctx) if err != nil { return err } } mi, err := nodeAPI.StateMinerInfo(ctx, maddr, types.EmptyTSK) if err != nil { return err } if fromAddrId != mi.Owner && fromAddrId != newAddrId { return xerrors.New("from address must either be the old owner or the new owner") } sp, err := actors.SerializeParams(&newAddrId) if err != nil { return xerrors.Errorf("serializing params: %w", err) } smsg, err := nodeAPI.MpoolPushMessage(ctx, &types.Message{ From: fromAddrId, To: maddr, Method: miner.Methods.ChangeOwnerAddress, Value: big.Zero(), Params: sp, }, nil) if err != nil { return xerrors.Errorf("mpool push: %w", err) } fmt.Println("Message CID:", smsg.Cid()) // wait for it to get mined into a block wait, err := nodeAPI.StateWaitMsg(ctx, smsg.Cid(), build.MessageConfidence) if err != nil { return err } // check it executed successfully if wait.Receipt.ExitCode != 0 { fmt.Println("owner change failed!") return err } fmt.Println("message succeeded!") return nil }, } var actorControl = &cli.Command{ Name: "control", Usage: "Manage control addresses", Subcommands: []*cli.Command{ actorControlList, actorControlSet, }, } var actorControlList = &cli.Command{ Name: "list", Usage: "Get currently set control addresses", Flags: []cli.Flag{ &cli.StringFlag{ Name: "actor", Usage: "specify the address of miner actor", }, &cli.BoolFlag{ Name: "verbose", }, &cli.BoolFlag{ Name: "color", Value: cliutil.DefaultColorUse, DefaultText: "depends on output being a TTY", }, }, Action: func(cctx *cli.Context) error { color.NoColor = !cctx.Bool("color") var maddr address.Address if act := cctx.String("actor"); act != "" { var err error maddr, err = address.NewFromString(act) if err != nil { return fmt.Errorf("parsing address %s: %w", act, err) } } nodeAPI, acloser, err := lcli.GetFullNodeAPI(cctx) if err != nil { return err } defer acloser() ctx := lcli.ReqContext(cctx) if maddr.Empty() { minerAPI, closer, err := lcli.GetStorageMinerAPI(cctx) if err != nil { return err } defer closer() maddr, err = minerAPI.ActorAddress(ctx) if err != nil { return err } } mi, err := nodeAPI.StateMinerInfo(ctx, maddr, types.EmptyTSK) if err != nil { return err } tw := tablewriter.New( tablewriter.Col("name"), tablewriter.Col("ID"), tablewriter.Col("key"), tablewriter.Col("balance"), ) printKey := func(name string, a address.Address) { b, err := nodeAPI.WalletBalance(ctx, a) if err != nil { fmt.Printf("%s\t%s: error getting balance: %s\n", name, a, err) return } k, err := nodeAPI.StateAccountKey(ctx, a, types.EmptyTSK) if err != nil { fmt.Printf("%s\t%s: error getting account key: %s\n", name, a, err) return } kstr := k.String() if !cctx.Bool("verbose") { kstr = kstr[:9] + "..." } bstr := types.FIL(b).String() switch { case b.LessThan(types.FromFil(10)): bstr = color.RedString(bstr) case b.LessThan(types.FromFil(50)): bstr = color.YellowString(bstr) default: bstr = color.GreenString(bstr) } tw.Write(map[string]interface{}{ "name": name, "ID": a, "key": kstr, "balance": bstr, }) } printKey("owner", mi.Owner) printKey("worker", mi.Worker) for i, ca := range mi.ControlAddresses { printKey(fmt.Sprintf("control-%d", i), ca) } return tw.Flush(os.Stdout) }, } var actorControlSet = &cli.Command{ Name: "set", Usage: "Set control address(-es)", ArgsUsage: "[...address]", Flags: []cli.Flag{ &cli.StringFlag{ Name: "actor", Usage: "specify the address of miner actor", }, &cli.BoolFlag{ Name: "really-do-it", Usage: "Actually send transaction performing the action", Value: false, }, }, Action: func(cctx *cli.Context) error { if !cctx.Bool("really-do-it") { fmt.Println("Pass --really-do-it to actually execute this action") return nil } var maddr address.Address if act := cctx.String("actor"); act != "" { var err error maddr, err = address.NewFromString(act) if err != nil { return fmt.Errorf("parsing address %s: %w", act, err) } } nodeAPI, acloser, err := lcli.GetFullNodeAPI(cctx) if err != nil { return err } defer acloser() ctx := lcli.ReqContext(cctx) if maddr.Empty() { minerAPI, closer, err := lcli.GetStorageMinerAPI(cctx) if err != nil { return err } defer closer() maddr, err = minerAPI.ActorAddress(ctx) if err != nil { return err } } mi, err := nodeAPI.StateMinerInfo(ctx, maddr, types.EmptyTSK) if err != nil { return err } del := map[address.Address]struct{}{} existing := map[address.Address]struct{}{} for _, controlAddress := range mi.ControlAddresses { ka, err := nodeAPI.StateAccountKey(ctx, controlAddress, types.EmptyTSK) if err != nil { return err } del[ka] = struct{}{} existing[ka] = struct{}{} } var toSet []address.Address for i, as := range cctx.Args().Slice() { a, err := address.NewFromString(as) if err != nil { return xerrors.Errorf("parsing address %d: %w", i, err) } ka, err := nodeAPI.StateAccountKey(ctx, a, types.EmptyTSK) if err != nil { return err } // make sure the address exists on chain _, err = nodeAPI.StateLookupID(ctx, ka, types.EmptyTSK) if err != nil { return xerrors.Errorf("looking up %s: %w", ka, err) } delete(del, ka) toSet = append(toSet, ka) } for a := range del { fmt.Println("Remove", a) } for _, a := range toSet { if _, exists := existing[a]; !exists { fmt.Println("Add", a) } } cwp := &miner2.ChangeWorkerAddressParams{ NewWorker: mi.Worker, NewControlAddrs: toSet, } sp, err := actors.SerializeParams(cwp) if err != nil { return xerrors.Errorf("serializing params: %w", err) } smsg, err := nodeAPI.MpoolPushMessage(ctx, &types.Message{ From: mi.Owner, To: maddr, Method: miner.Methods.ChangeWorkerAddress, Value: big.Zero(), Params: sp, }, nil) if err != nil { return xerrors.Errorf("mpool push: %w", err) } fmt.Println("Message CID:", smsg.Cid()) return nil }, } var actorProposeChangeWorker = &cli.Command{ Name: "propose-change-worker", Usage: "Propose a worker address change", ArgsUsage: "[address]", Flags: []cli.Flag{ &cli.StringFlag{ Name: "actor", Usage: "specify the address of miner actor", }, &cli.BoolFlag{ Name: "really-do-it", Usage: "Actually send transaction performing the action", Value: false, }, }, Action: func(cctx *cli.Context) error { if !cctx.Args().Present() { return fmt.Errorf("must pass address of new worker address") } if !cctx.Bool("really-do-it") { fmt.Fprintln(cctx.App.Writer, "Pass --really-do-it to actually execute this action") return nil } var maddr address.Address if act := cctx.String("actor"); act != "" { var err error maddr, err = address.NewFromString(act) if err != nil { return fmt.Errorf("parsing address %s: %w", act, err) } } nodeAPI, acloser, err := lcli.GetFullNodeAPI(cctx) if err != nil { return err } defer acloser() ctx := lcli.ReqContext(cctx) na, err := address.NewFromString(cctx.Args().First()) if err != nil { return err } newAddr, err := nodeAPI.StateLookupID(ctx, na, types.EmptyTSK) if err != nil { return err } if maddr.Empty() { minerAPI, closer, err := lcli.GetStorageMinerAPI(cctx) if err != nil { return err } defer closer() maddr, err = minerAPI.ActorAddress(ctx) if err != nil { return err } } mi, err := nodeAPI.StateMinerInfo(ctx, maddr, types.EmptyTSK) if err != nil { return err } if mi.NewWorker.Empty() { if mi.Worker == newAddr { return fmt.Errorf("worker address already set to %s", na) } } else { if mi.NewWorker == newAddr { return fmt.Errorf("change to worker address %s already pending", na) } } cwp := &miner2.ChangeWorkerAddressParams{ NewWorker: newAddr, NewControlAddrs: mi.ControlAddresses, } sp, err := actors.SerializeParams(cwp) if err != nil { return xerrors.Errorf("serializing params: %w", err) } smsg, err := nodeAPI.MpoolPushMessage(ctx, &types.Message{ From: mi.Owner, To: maddr, Method: miner.Methods.ChangeWorkerAddress, Value: big.Zero(), Params: sp, }, nil) if err != nil { return xerrors.Errorf("mpool push: %w", err) } fmt.Fprintln(cctx.App.Writer, "Propose Message CID:", smsg.Cid()) // wait for it to get mined into a block wait, err := nodeAPI.StateWaitMsg(ctx, smsg.Cid(), build.MessageConfidence) if err != nil { return err } // check it executed successfully if wait.Receipt.ExitCode != 0 { fmt.Fprintln(cctx.App.Writer, "Propose worker change failed!") return err } mi, err = nodeAPI.StateMinerInfo(ctx, maddr, wait.TipSet) if err != nil { return err } if mi.NewWorker != newAddr { return fmt.Errorf("Proposed worker address change not reflected on chain: expected '%s', found '%s'", na, mi.NewWorker) } fmt.Fprintf(cctx.App.Writer, "Worker key change to %s successfully proposed.\n", na) fmt.Fprintf(cctx.App.Writer, "Call 'confirm-change-worker' at or after height %d to complete.\n", mi.WorkerChangeEpoch) return nil }, } var actorConfirmChangeWorker = &cli.Command{ Name: "confirm-change-worker", Usage: "Confirm a worker address change", ArgsUsage: "[address]", Flags: []cli.Flag{ &cli.StringFlag{ Name: "actor", Usage: "specify the address of miner actor", }, &cli.BoolFlag{ Name: "really-do-it", Usage: "Actually send transaction performing the action", Value: false, }, }, Action: func(cctx *cli.Context) error { if !cctx.Args().Present() { return fmt.Errorf("must pass address of new worker address") } if !cctx.Bool("really-do-it") { fmt.Fprintln(cctx.App.Writer, "Pass --really-do-it to actually execute this action") return nil } var maddr address.Address if act := cctx.String("actor"); act != "" { var err error maddr, err = address.NewFromString(act) if err != nil { return fmt.Errorf("parsing address %s: %w", act, err) } } nodeAPI, acloser, err := lcli.GetFullNodeAPI(cctx) if err != nil { return err } defer acloser() ctx := lcli.ReqContext(cctx) na, err := address.NewFromString(cctx.Args().First()) if err != nil { return err } newAddr, err := nodeAPI.StateLookupID(ctx, na, types.EmptyTSK) if err != nil { return err } if maddr.Empty() { minerAPI, closer, err := lcli.GetStorageMinerAPI(cctx) if err != nil { return err } defer closer() maddr, err = minerAPI.ActorAddress(ctx) if err != nil { return err } } mi, err := nodeAPI.StateMinerInfo(ctx, maddr, types.EmptyTSK) if err != nil { return err } if mi.NewWorker.Empty() { return xerrors.Errorf("no worker key change proposed") } else if mi.NewWorker != newAddr { return xerrors.Errorf("worker key %s does not match current worker key proposal %s", newAddr, mi.NewWorker) } if head, err := nodeAPI.ChainHead(ctx); err != nil { return xerrors.Errorf("failed to get the chain head: %w", err) } else if head.Height() < mi.WorkerChangeEpoch { return xerrors.Errorf("worker key change cannot be confirmed until %d, current height is %d", mi.WorkerChangeEpoch, head.Height()) } smsg, err := nodeAPI.MpoolPushMessage(ctx, &types.Message{ From: mi.Owner, To: maddr, Method: miner.Methods.ConfirmUpdateWorkerKey, Value: big.Zero(), }, nil) if err != nil { return xerrors.Errorf("mpool push: %w", err) } fmt.Fprintln(cctx.App.Writer, "Confirm Message CID:", smsg.Cid()) // wait for it to get mined into a block wait, err := nodeAPI.StateWaitMsg(ctx, smsg.Cid(), build.MessageConfidence) if err != nil { return err } // check it executed successfully if wait.Receipt.ExitCode != 0 { fmt.Fprintln(cctx.App.Writer, "Worker change failed!") return err } mi, err = nodeAPI.StateMinerInfo(ctx, maddr, wait.TipSet) if err != nil { return err } if mi.Worker != newAddr { return fmt.Errorf("Confirmed worker address change not reflected on chain: expected '%s', found '%s'", newAddr, mi.Worker) } return nil }, }