diff --git a/api/v0api/full.go b/api/v0api/full.go index 076c37013..f646aa9fd 100644 --- a/api/v0api/full.go +++ b/api/v0api/full.go @@ -629,7 +629,7 @@ type FullNode interface { // proposal. This method of approval can be used to ensure you only approve // exactly the transaction you think you are. // It takes the following params: , , , , , - // , , + // , , MsigApproveTxnHash(context.Context, address.Address, uint64, address.Address, address.Address, types.BigInt, address.Address, uint64, []byte) (cid.Cid, error) //perm:sign // MsigCancel cancels a previously-proposed multisig message diff --git a/cmd/lotus-shed/main.go b/cmd/lotus-shed/main.go index 7c4391f18..e06b63080 100644 --- a/cmd/lotus-shed/main.go +++ b/cmd/lotus-shed/main.go @@ -59,6 +59,7 @@ func main() { signaturesCmd, actorCmd, minerTypesCmd, + minerMultisigsCmd, } app := &cli.App{ diff --git a/cmd/lotus-shed/miner-multisig.go b/cmd/lotus-shed/miner-multisig.go new file mode 100644 index 000000000..d9f158090 --- /dev/null +++ b/cmd/lotus-shed/miner-multisig.go @@ -0,0 +1,388 @@ +package main + +import ( + "bytes" + "fmt" + "strconv" + + "github.com/filecoin-project/go-state-types/abi" + miner5 "github.com/filecoin-project/specs-actors/v5/actors/builtin/miner" + + msig5 "github.com/filecoin-project/specs-actors/v5/actors/builtin/multisig" + + "github.com/filecoin-project/go-address" + "github.com/filecoin-project/go-state-types/big" + "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" + "github.com/urfave/cli/v2" + "golang.org/x/xerrors" +) + +var minerMultisigsCmd = &cli.Command{ + Name: "miner-multisig", + Description: "a collection of utilities for using multisigs as owner addresses of miners", + Subcommands: []*cli.Command{ + mmProposeWithdrawBalance, + mmApproveWithdrawBalance, + mmProposeChangeOwner, + mmApproveChangeOwner, + }, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "from", + Usage: "specify address to send message from", + Required: true, + }, + &cli.StringFlag{ + Name: "multisig", + Usage: "specify multisig that will receive the message", + Required: true, + }, + &cli.StringFlag{ + Name: "miner", + Usage: "specify miner being acted upon", + Required: true, + }, + }, +} + +var mmProposeWithdrawBalance = &cli.Command{ + Name: "propose-withdraw", + Usage: "Propose to withdraw FIL from the miner", + ArgsUsage: "[amount]", + Action: func(cctx *cli.Context) error { + if !cctx.Args().Present() { + return fmt.Errorf("must pass amount to withdraw") + } + + api, closer, err := lcli.GetFullNodeAPI(cctx) + if err != nil { + return err + } + defer closer() + + ctx := lcli.ReqContext(cctx) + + multisigAddr, sender, minerAddr, err := getInputs(cctx) + if err != nil { + return err + } + + val, err := types.ParseFIL(cctx.Args().First()) + if err != nil { + return err + } + + sp, err := actors.SerializeParams(&miner5.WithdrawBalanceParams{ + AmountRequested: abi.TokenAmount(val), + }) + if err != nil { + return err + } + + pcid, err := api.MsigPropose(ctx, multisigAddr, minerAddr, big.Zero(), sender, uint64(miner.Methods.WithdrawBalance), sp) + if err != nil { + return xerrors.Errorf("proposing message: %w", err) + } + + fmt.Fprintln(cctx.App.Writer, "Propose Message CID:", pcid) + + // wait for it to get mined into a block + wait, err := api.StateWaitMsg(ctx, pcid, build.MessageConfidence) + if err != nil { + return err + } + + // check it executed successfully + if wait.Receipt.ExitCode != 0 { + fmt.Fprintln(cctx.App.Writer, "Propose owner change tx failed!") + return err + } + + var retval msig5.ProposeReturn + if err := retval.UnmarshalCBOR(bytes.NewReader(wait.Receipt.Return)); err != nil { + return fmt.Errorf("failed to unmarshal propose return value: %w", err) + } + + fmt.Printf("Transaction ID: %d\n", retval.TxnID) + if retval.Applied { + fmt.Printf("Transaction was executed during propose\n") + fmt.Printf("Exit Code: %d\n", retval.Code) + fmt.Printf("Return Value: %x\n", retval.Ret) + } + + return nil + }, +} + +var mmApproveWithdrawBalance = &cli.Command{ + Name: "approve-withdraw", + Usage: "Approve to withdraw FIL from the miner", + ArgsUsage: "[amount txnId proposer]", + Action: func(cctx *cli.Context) error { + if cctx.NArg() != 3 { + return fmt.Errorf("must pass amount, txn Id, and proposer address") + } + + api, closer, err := lcli.GetFullNodeAPI(cctx) + if err != nil { + return err + } + defer closer() + + ctx := lcli.ReqContext(cctx) + + multisigAddr, sender, minerAddr, err := getInputs(cctx) + if err != nil { + return err + } + + val, err := types.ParseFIL(cctx.Args().First()) + if err != nil { + return err + } + + sp, err := actors.SerializeParams(&miner5.WithdrawBalanceParams{ + AmountRequested: abi.TokenAmount(val), + }) + if err != nil { + return err + } + + txid, err := strconv.ParseUint(cctx.Args().Get(1), 10, 64) + if err != nil { + return err + } + + proposer, err := address.NewFromString(cctx.Args().Get(2)) + if err != nil { + return err + } + + acid, err := api.MsigApproveTxnHash(ctx, multisigAddr, txid, proposer, minerAddr, big.Zero(), sender, uint64(miner.Methods.WithdrawBalance), sp) + if err != nil { + return xerrors.Errorf("approving message: %w", err) + } + + fmt.Fprintln(cctx.App.Writer, "Approve Message CID:", acid) + + // wait for it to get mined into a block + wait, err := api.StateWaitMsg(ctx, acid, build.MessageConfidence) + if err != nil { + return err + } + + // check it executed successfully + if wait.Receipt.ExitCode != 0 { + fmt.Fprintln(cctx.App.Writer, "Approve owner change tx failed!") + return err + } + + var retval msig5.ApproveReturn + if err := retval.UnmarshalCBOR(bytes.NewReader(wait.Receipt.Return)); err != nil { + return fmt.Errorf("failed to unmarshal approve return value: %w", err) + } + + if retval.Applied { + fmt.Printf("Transaction was executed with the approve\n") + fmt.Printf("Exit Code: %d\n", retval.Code) + fmt.Printf("Return Value: %x\n", retval.Ret) + } else { + fmt.Println("Transaction was approved, but not executed") + } + return nil + }, +} + +var mmProposeChangeOwner = &cli.Command{ + Name: "propose-change-owner", + Usage: "Propose an owner address change", + ArgsUsage: "[newOwner]", + Action: func(cctx *cli.Context) error { + if !cctx.Args().Present() { + return fmt.Errorf("must pass new owner address") + } + + api, closer, err := lcli.GetFullNodeAPI(cctx) + if err != nil { + return err + } + defer closer() + + ctx := lcli.ReqContext(cctx) + + multisigAddr, sender, minerAddr, err := getInputs(cctx) + if err != nil { + return err + } + + na, err := address.NewFromString(cctx.Args().First()) + if err != nil { + return err + } + + newAddr, err := api.StateLookupID(ctx, na, types.EmptyTSK) + if err != nil { + return err + } + + mi, err := api.StateMinerInfo(ctx, minerAddr, types.EmptyTSK) + if err != nil { + return err + } + + if mi.Owner == newAddr { + return fmt.Errorf("owner address already set to %s", na) + } + + sp, err := actors.SerializeParams(&newAddr) + if err != nil { + return xerrors.Errorf("serializing params: %w", err) + } + + pcid, err := api.MsigPropose(ctx, multisigAddr, minerAddr, big.Zero(), sender, uint64(miner.Methods.ChangeOwnerAddress), sp) + if err != nil { + return xerrors.Errorf("proposing message: %w", err) + } + + fmt.Fprintln(cctx.App.Writer, "Propose Message CID:", pcid) + + // wait for it to get mined into a block + wait, err := api.StateWaitMsg(ctx, pcid, build.MessageConfidence) + if err != nil { + return err + } + + // check it executed successfully + if wait.Receipt.ExitCode != 0 { + fmt.Fprintln(cctx.App.Writer, "Propose owner change tx failed!") + return err + } + + var retval msig5.ProposeReturn + if err := retval.UnmarshalCBOR(bytes.NewReader(wait.Receipt.Return)); err != nil { + return fmt.Errorf("failed to unmarshal propose return value: %w", err) + } + + fmt.Printf("Transaction ID: %d\n", retval.TxnID) + if retval.Applied { + fmt.Printf("Transaction was executed during propose\n") + fmt.Printf("Exit Code: %d\n", retval.Code) + fmt.Printf("Return Value: %x\n", retval.Ret) + } + return nil + }, +} + +var mmApproveChangeOwner = &cli.Command{ + Name: "approve-change-owner", + Usage: "Approve an owner address change", + ArgsUsage: "[newOwner txnId proposer]", + Action: func(cctx *cli.Context) error { + if cctx.NArg() != 3 { + return fmt.Errorf("must pass new owner address, txn Id, and proposer address") + } + + api, closer, err := lcli.GetFullNodeAPI(cctx) + if err != nil { + return err + } + defer closer() + + ctx := lcli.ReqContext(cctx) + + multisigAddr, sender, minerAddr, err := getInputs(cctx) + if err != nil { + return err + } + + na, err := address.NewFromString(cctx.Args().First()) + if err != nil { + return err + } + + newAddr, err := api.StateLookupID(ctx, na, types.EmptyTSK) + if err != nil { + return err + } + + txid, err := strconv.ParseUint(cctx.Args().Get(1), 10, 64) + if err != nil { + return err + } + + proposer, err := address.NewFromString(cctx.Args().Get(2)) + if err != nil { + return err + } + + mi, err := api.StateMinerInfo(ctx, minerAddr, types.EmptyTSK) + if err != nil { + return err + } + + if mi.Owner == newAddr { + return fmt.Errorf("owner address already set to %s", na) + } + + sp, err := actors.SerializeParams(&newAddr) + if err != nil { + return xerrors.Errorf("serializing params: %w", err) + } + + acid, err := api.MsigApproveTxnHash(ctx, multisigAddr, txid, proposer, minerAddr, big.Zero(), sender, uint64(miner.Methods.ChangeOwnerAddress), sp) + if err != nil { + return xerrors.Errorf("approving message: %w", err) + } + + fmt.Fprintln(cctx.App.Writer, "Approve Message CID:", acid) + + // wait for it to get mined into a block + wait, err := api.StateWaitMsg(ctx, acid, build.MessageConfidence) + if err != nil { + return err + } + + // check it executed successfully + if wait.Receipt.ExitCode != 0 { + fmt.Fprintln(cctx.App.Writer, "Approve owner change tx failed!") + return err + } + + var retval msig5.ApproveReturn + if err := retval.UnmarshalCBOR(bytes.NewReader(wait.Receipt.Return)); err != nil { + return fmt.Errorf("failed to unmarshal approve return value: %w", err) + } + + if retval.Applied { + fmt.Printf("Transaction was executed with the approve\n") + fmt.Printf("Exit Code: %d\n", retval.Code) + fmt.Printf("Return Value: %x\n", retval.Ret) + } else { + fmt.Println("Transaction was approved, but not executed") + } + return nil + }, +} + +func getInputs(cctx *cli.Context) (address.Address, address.Address, address.Address, error) { + multisigAddr, err := address.NewFromString(cctx.String("multisig")) + if err != nil { + return address.Undef, address.Undef, address.Undef, err + } + + sender, err := address.NewFromString(cctx.String("from")) + if err != nil { + return address.Undef, address.Undef, address.Undef, err + } + + minerAddr, err := address.NewFromString(cctx.String("miner")) + if err != nil { + return address.Undef, address.Undef, address.Undef, err + } + + return multisigAddr, sender, minerAddr, nil +} diff --git a/documentation/en/api-v0-methods.md b/documentation/en/api-v0-methods.md index f6da2244c..7e7216f16 100644 --- a/documentation/en/api-v0-methods.md +++ b/documentation/en/api-v0-methods.md @@ -2505,7 +2505,7 @@ using both transaction ID and a hash of the parameters used in the proposal. This method of approval can be used to ensure you only approve exactly the transaction you think you are. It takes the following params: , , , , , -, , +, , Perms: sign