586 lines
15 KiB
Go
586 lines
15 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/docker/go-units"
|
|
"github.com/ipfs/go-cid"
|
|
"github.com/mitchellh/go-homedir"
|
|
"github.com/urfave/cli/v2"
|
|
"golang.org/x/xerrors"
|
|
|
|
"github.com/filecoin-project/go-address"
|
|
"github.com/filecoin-project/go-bitfield"
|
|
"github.com/filecoin-project/go-state-types/abi"
|
|
"github.com/filecoin-project/go-state-types/big"
|
|
"github.com/filecoin-project/go-state-types/builtin"
|
|
miner8 "github.com/filecoin-project/go-state-types/builtin/v8/miner"
|
|
"github.com/filecoin-project/go-state-types/crypto"
|
|
power7 "github.com/filecoin-project/specs-actors/v7/actors/builtin/power"
|
|
"github.com/filecoin-project/specs-actors/v7/actors/runtime/proof"
|
|
|
|
"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/actors/builtin/power"
|
|
"github.com/filecoin-project/lotus/chain/types"
|
|
lcli "github.com/filecoin-project/lotus/cli"
|
|
)
|
|
|
|
var minerCmd = &cli.Command{
|
|
Name: "miner",
|
|
Usage: "miner-related utilities",
|
|
Subcommands: []*cli.Command{
|
|
minerUnpackInfoCmd,
|
|
minerCreateCmd,
|
|
minerFaultsCmd,
|
|
sendInvalidWindowPoStCmd,
|
|
generateAndSendConsensusFaultCmd,
|
|
},
|
|
}
|
|
|
|
var minerFaultsCmd = &cli.Command{
|
|
Name: "faults",
|
|
Usage: "Display a list of faulty sectors for a SP",
|
|
ArgsUsage: "[minerAddress]",
|
|
Flags: []cli.Flag{
|
|
&cli.Uint64Flag{
|
|
Name: "expiring-in",
|
|
Usage: "only list sectors that are expiring in the next <n> epochs",
|
|
Value: 0,
|
|
},
|
|
},
|
|
Action: func(cctx *cli.Context) error {
|
|
if !cctx.Args().Present() {
|
|
return fmt.Errorf("must pass miner address")
|
|
}
|
|
|
|
api, closer, err := lcli.GetFullNodeAPI(cctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer closer()
|
|
|
|
ctx := lcli.ReqContext(cctx)
|
|
|
|
m, err := address.NewFromString(cctx.Args().First())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
faultBf, err := api.StateMinerFaults(ctx, m, types.EmptyTSK)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
faults, err := faultBf.All(abi.MaxSectorNumber)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(faults) == 0 {
|
|
fmt.Println("no faults")
|
|
return nil
|
|
}
|
|
|
|
expEpoch := abi.ChainEpoch(cctx.Uint64("expiring-in"))
|
|
|
|
if expEpoch == 0 {
|
|
fmt.Print("faulty sectors: ")
|
|
for _, v := range faults {
|
|
fmt.Printf("%d ", v)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
h, err := api.ChainHead(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Printf("faulty sectors expiring in the next %d epochs: ", expEpoch)
|
|
for _, v := range faults {
|
|
ss, err := api.StateSectorExpiration(ctx, m, abi.SectorNumber(v), types.EmptyTSK)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if ss.Early < h.Height()+expEpoch {
|
|
fmt.Printf("%d ", v)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
}
|
|
|
|
var minerCreateCmd = &cli.Command{
|
|
Name: "create",
|
|
Usage: "sends a create miner message",
|
|
ArgsUsage: "[sender] [owner] [worker] [sector size]",
|
|
Action: func(cctx *cli.Context) error {
|
|
wapi, closer, err := lcli.GetFullNodeAPI(cctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer closer()
|
|
ctx := lcli.ReqContext(cctx)
|
|
|
|
if cctx.NArg() != 4 {
|
|
return lcli.IncorrectNumArgs(cctx)
|
|
}
|
|
|
|
sender, err := address.NewFromString(cctx.Args().First())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
owner, err := address.NewFromString(cctx.Args().Get(1))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
worker, err := address.NewFromString(cctx.Args().Get(2))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ssize, err := units.RAMInBytes(cctx.Args().Get(3))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse sector size: %w", err)
|
|
}
|
|
|
|
// make sure the sender account exists on chain
|
|
_, err = wapi.StateLookupID(ctx, owner, types.EmptyTSK)
|
|
if err != nil {
|
|
return xerrors.Errorf("sender must exist on chain: %w", err)
|
|
}
|
|
|
|
// make sure the worker account exists on chain
|
|
_, err = wapi.StateLookupID(ctx, worker, types.EmptyTSK)
|
|
if err != nil {
|
|
signed, err := wapi.MpoolPushMessage(ctx, &types.Message{
|
|
From: sender,
|
|
To: worker,
|
|
Value: types.NewInt(0),
|
|
}, nil)
|
|
if err != nil {
|
|
return xerrors.Errorf("push worker init: %w", err)
|
|
}
|
|
|
|
log.Infof("Initializing worker account %s, message: %s", worker, signed.Cid())
|
|
log.Infof("Waiting for confirmation")
|
|
|
|
mw, err := wapi.StateWaitMsg(ctx, signed.Cid(), build.MessageConfidence)
|
|
if err != nil {
|
|
return xerrors.Errorf("waiting for worker init: %w", err)
|
|
}
|
|
|
|
if mw.Receipt.ExitCode != 0 {
|
|
return xerrors.Errorf("initializing worker account failed: exit code %d", mw.Receipt.ExitCode)
|
|
}
|
|
}
|
|
|
|
// make sure the owner account exists on chain
|
|
_, err = wapi.StateLookupID(ctx, owner, types.EmptyTSK)
|
|
if err != nil {
|
|
signed, err := wapi.MpoolPushMessage(ctx, &types.Message{
|
|
From: sender,
|
|
To: owner,
|
|
Value: types.NewInt(0),
|
|
}, nil)
|
|
if err != nil {
|
|
return xerrors.Errorf("push owner init: %w", err)
|
|
}
|
|
|
|
log.Infof("Initializing owner account %s, message: %s", worker, signed.Cid())
|
|
log.Infof("Wating for confirmation")
|
|
|
|
mw, err := wapi.StateWaitMsg(ctx, signed.Cid(), build.MessageConfidence)
|
|
if err != nil {
|
|
return xerrors.Errorf("waiting for owner init: %w", err)
|
|
}
|
|
|
|
if mw.Receipt.ExitCode != 0 {
|
|
return xerrors.Errorf("initializing owner account failed: exit code %d", mw.Receipt.ExitCode)
|
|
}
|
|
}
|
|
|
|
// Note: the correct thing to do would be to call SealProofTypeFromSectorSize if actors version is v3 or later, but this still works
|
|
spt, err := miner.WindowPoStProofTypeFromSectorSize(abi.SectorSize(ssize))
|
|
if err != nil {
|
|
return xerrors.Errorf("getting post proof type: %w", err)
|
|
}
|
|
|
|
params, err := actors.SerializeParams(&power7.CreateMinerParams{
|
|
Owner: owner,
|
|
Worker: worker,
|
|
WindowPoStProofType: spt,
|
|
})
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
createStorageMinerMsg := &types.Message{
|
|
To: power.Address,
|
|
From: sender,
|
|
Value: big.Zero(),
|
|
|
|
Method: power.Methods.CreateMiner,
|
|
Params: params,
|
|
}
|
|
|
|
signed, err := wapi.MpoolPushMessage(ctx, createStorageMinerMsg, nil)
|
|
if err != nil {
|
|
return xerrors.Errorf("pushing createMiner message: %w", err)
|
|
}
|
|
|
|
log.Infof("Pushed CreateMiner message: %s", signed.Cid())
|
|
log.Infof("Waiting for confirmation")
|
|
|
|
mw, err := wapi.StateWaitMsg(ctx, signed.Cid(), build.MessageConfidence)
|
|
if err != nil {
|
|
return xerrors.Errorf("waiting for createMiner message: %w", err)
|
|
}
|
|
|
|
if mw.Receipt.ExitCode != 0 {
|
|
return xerrors.Errorf("create miner failed: exit code %d", mw.Receipt.ExitCode)
|
|
}
|
|
|
|
var retval power7.CreateMinerReturn
|
|
if err := retval.UnmarshalCBOR(bytes.NewReader(mw.Receipt.Return)); err != nil {
|
|
return err
|
|
}
|
|
|
|
log.Infof("New miners address is: %s (%s)", retval.IDAddress, retval.RobustAddress)
|
|
|
|
return nil
|
|
},
|
|
}
|
|
|
|
var minerUnpackInfoCmd = &cli.Command{
|
|
Name: "unpack-info",
|
|
Usage: "unpack miner info all dump",
|
|
ArgsUsage: "[allinfo.txt] [dir]",
|
|
Action: func(cctx *cli.Context) error {
|
|
if cctx.NArg() != 2 {
|
|
return lcli.IncorrectNumArgs(cctx)
|
|
}
|
|
|
|
src, err := homedir.Expand(cctx.Args().Get(0))
|
|
if err != nil {
|
|
return xerrors.Errorf("expand src: %w", err)
|
|
}
|
|
|
|
f, err := os.Open(src)
|
|
if err != nil {
|
|
return xerrors.Errorf("open file: %w", err)
|
|
}
|
|
defer f.Close() // nolint
|
|
|
|
dest, err := homedir.Expand(cctx.Args().Get(1))
|
|
if err != nil {
|
|
return xerrors.Errorf("expand dest: %w", err)
|
|
}
|
|
|
|
var outf *os.File
|
|
|
|
r := bufio.NewReader(f)
|
|
for {
|
|
l, _, err := r.ReadLine()
|
|
if err == io.EOF {
|
|
if outf != nil {
|
|
return outf.Close()
|
|
}
|
|
}
|
|
if err != nil {
|
|
return xerrors.Errorf("read line: %w", err)
|
|
}
|
|
sl := string(l)
|
|
|
|
if strings.HasPrefix(sl, "#") {
|
|
if strings.Contains(sl, "..") {
|
|
return xerrors.Errorf("bad name %s", sl)
|
|
}
|
|
|
|
if strings.HasPrefix(sl, "#: ") {
|
|
if outf != nil {
|
|
if err := outf.Close(); err != nil {
|
|
return xerrors.Errorf("close out file: %w", err)
|
|
}
|
|
}
|
|
p := filepath.Join(dest, sl[len("#: "):])
|
|
if err := os.MkdirAll(filepath.Dir(p), 0775); err != nil {
|
|
return xerrors.Errorf("mkdir: %w", err)
|
|
}
|
|
outf, err = os.Create(p)
|
|
if err != nil {
|
|
return xerrors.Errorf("create out file: %w", err)
|
|
}
|
|
continue
|
|
}
|
|
|
|
if strings.HasPrefix(sl, "##: ") {
|
|
if outf != nil {
|
|
if err := outf.Close(); err != nil {
|
|
return xerrors.Errorf("close out file: %w", err)
|
|
}
|
|
}
|
|
p := filepath.Join(dest, "Per Sector Infos", sl[len("##: "):])
|
|
if err := os.MkdirAll(filepath.Dir(p), 0775); err != nil {
|
|
return xerrors.Errorf("mkdir: %w", err)
|
|
}
|
|
outf, err = os.Create(p)
|
|
if err != nil {
|
|
return xerrors.Errorf("create out file: %w", err)
|
|
}
|
|
continue
|
|
}
|
|
}
|
|
|
|
if outf != nil {
|
|
if _, err := outf.Write(l); err != nil {
|
|
return xerrors.Errorf("write line: %w", err)
|
|
}
|
|
if _, err := outf.Write([]byte("\n")); err != nil {
|
|
return xerrors.Errorf("write line end: %w", err)
|
|
}
|
|
}
|
|
}
|
|
},
|
|
}
|
|
|
|
var sendInvalidWindowPoStCmd = &cli.Command{
|
|
Name: "send-invalid-windowed-post",
|
|
Usage: "Sends an invalid windowed post for a specific deadline",
|
|
Description: `Note: This is meant for testing purposes and should NOT be used on mainnet or you will be slashed`,
|
|
Flags: []cli.Flag{
|
|
&cli.BoolFlag{
|
|
Name: "really-do-it",
|
|
Usage: "Actually send transaction performing the action",
|
|
Value: false,
|
|
},
|
|
&cli.Int64SliceFlag{
|
|
Name: "partitions",
|
|
Usage: "list of partitions to submit invalid post for",
|
|
Required: true,
|
|
},
|
|
&cli.StringFlag{
|
|
Name: "actor",
|
|
Usage: "Specify the address of the miner to run this command",
|
|
},
|
|
},
|
|
Action: func(cctx *cli.Context) error {
|
|
if !cctx.Bool("really-do-it") {
|
|
return xerrors.Errorf("Pass --really-do-it to actually execute this action")
|
|
}
|
|
|
|
api, acloser, err := lcli.GetFullNodeAPI(cctx)
|
|
if err != nil {
|
|
return xerrors.Errorf("getting api: %w", err)
|
|
}
|
|
defer acloser()
|
|
|
|
ctx := lcli.ReqContext(cctx)
|
|
|
|
maddr, err := address.NewFromString(cctx.String("actor"))
|
|
if err != nil {
|
|
return xerrors.Errorf("getting actor address: %w", err)
|
|
}
|
|
|
|
minfo, err := api.StateMinerInfo(ctx, maddr, types.EmptyTSK)
|
|
if err != nil {
|
|
return xerrors.Errorf("getting mienr info: %w", err)
|
|
}
|
|
|
|
deadline, err := api.StateMinerProvingDeadline(ctx, maddr, types.EmptyTSK)
|
|
if err != nil {
|
|
return xerrors.Errorf("getting deadline: %w", err)
|
|
}
|
|
|
|
partitionIndices := cctx.Int64Slice("partitions")
|
|
if len(partitionIndices) <= 0 {
|
|
return fmt.Errorf("must include at least one partition to compact")
|
|
}
|
|
|
|
chainHead, err := api.ChainHead(ctx)
|
|
if err != nil {
|
|
return xerrors.Errorf("getting chain head: %w", err)
|
|
}
|
|
|
|
checkRand, err := api.StateGetRandomnessFromTickets(ctx, crypto.DomainSeparationTag_PoStChainCommit, deadline.Challenge, nil, chainHead.Key())
|
|
if err != nil {
|
|
return xerrors.Errorf("getting randomness: %w", err)
|
|
}
|
|
|
|
proofSize, err := minfo.WindowPoStProofType.ProofSize()
|
|
if err != nil {
|
|
return xerrors.Errorf("getting proof size: %w", err)
|
|
}
|
|
|
|
var partitions []miner8.PoStPartition
|
|
|
|
emptyProof := []proof.PoStProof{{
|
|
PoStProof: minfo.WindowPoStProofType,
|
|
ProofBytes: make([]byte, proofSize)}}
|
|
|
|
for _, partition := range partitionIndices {
|
|
newPartition := miner8.PoStPartition{
|
|
Index: uint64(partition),
|
|
Skipped: bitfield.New(),
|
|
}
|
|
partitions = append(partitions, newPartition)
|
|
}
|
|
|
|
params := miner8.SubmitWindowedPoStParams{
|
|
Deadline: deadline.Index,
|
|
Partitions: partitions,
|
|
Proofs: emptyProof,
|
|
ChainCommitEpoch: deadline.Challenge,
|
|
ChainCommitRand: checkRand,
|
|
}
|
|
|
|
sp, err := actors.SerializeParams(¶ms)
|
|
if err != nil {
|
|
return xerrors.Errorf("serializing params: %w", err)
|
|
}
|
|
|
|
fmt.Printf("submitting bad PoST for %d paritions\n", len(partitionIndices))
|
|
smsg, err := api.MpoolPushMessage(ctx, &types.Message{
|
|
From: minfo.Worker,
|
|
To: maddr,
|
|
Method: builtin.MethodsMiner.SubmitWindowedPoSt,
|
|
Value: big.Zero(),
|
|
Params: sp,
|
|
}, nil)
|
|
if err != nil {
|
|
return xerrors.Errorf("mpool push: %w", err)
|
|
}
|
|
|
|
fmt.Printf("Invalid PoST in message %s\n", smsg.Cid())
|
|
|
|
wait, err := api.StateWaitMsg(ctx, smsg.Cid(), 0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// check it executed successfully
|
|
if wait.Receipt.ExitCode.IsError() {
|
|
fmt.Println(cctx.App.Writer, "Invalid PoST message failed!")
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
},
|
|
}
|
|
|
|
var generateAndSendConsensusFaultCmd = &cli.Command{
|
|
Name: "generate-and-send-consensus-fault",
|
|
Usage: "Provided a block CID mined by the miner, will create another block at the same height, and send both block headers to generate a consensus fault.",
|
|
Description: `Note: This is meant for testing purposes and should NOT be used on mainnet or you will be slashed`,
|
|
ArgsUsage: "blockCID",
|
|
Action: func(cctx *cli.Context) error {
|
|
if cctx.NArg() != 1 {
|
|
return lcli.IncorrectNumArgs(cctx)
|
|
}
|
|
|
|
blockCid, err := cid.Parse(cctx.Args().First())
|
|
if err != nil {
|
|
return xerrors.Errorf("getting first arg: %w", err)
|
|
}
|
|
|
|
api, acloser, err := lcli.GetFullNodeAPI(cctx)
|
|
if err != nil {
|
|
return xerrors.Errorf("getting chain head: %w", err)
|
|
}
|
|
defer acloser()
|
|
|
|
ctx := lcli.ReqContext(cctx)
|
|
|
|
blockHeader, err := api.ChainGetBlock(ctx, blockCid)
|
|
if err != nil {
|
|
return xerrors.Errorf("getting block header: %w", err)
|
|
}
|
|
|
|
maddr := blockHeader.Miner
|
|
|
|
minfo, err := api.StateMinerInfo(ctx, maddr, types.EmptyTSK)
|
|
if err != nil {
|
|
return xerrors.Errorf("getting miner info: %w", err)
|
|
}
|
|
|
|
// We are changing one field in the block header, then resigning the new block.
|
|
// This gives two different blocks signed by the same miner at the same height which will result in a consensus fault.
|
|
blockHeaderCopy := *blockHeader
|
|
blockHeaderCopy.ForkSignaling = blockHeader.ForkSignaling + 1
|
|
|
|
signingBytes, err := blockHeaderCopy.SigningBytes()
|
|
if err != nil {
|
|
return xerrors.Errorf("getting bytes to sign second block: %w", err)
|
|
}
|
|
|
|
sig, err := api.WalletSign(ctx, minfo.Worker, signingBytes)
|
|
if err != nil {
|
|
return xerrors.Errorf("signing second block: %w", err)
|
|
}
|
|
blockHeaderCopy.BlockSig = sig
|
|
|
|
buf1 := new(bytes.Buffer)
|
|
err = blockHeader.MarshalCBOR(buf1)
|
|
if err != nil {
|
|
return xerrors.Errorf("marshalling block header 1: %w", err)
|
|
}
|
|
buf2 := new(bytes.Buffer)
|
|
err = blockHeaderCopy.MarshalCBOR(buf2)
|
|
if err != nil {
|
|
return xerrors.Errorf("marshalling block header 2: %w", err)
|
|
}
|
|
|
|
params := miner8.ReportConsensusFaultParams{
|
|
BlockHeader1: buf1.Bytes(),
|
|
BlockHeader2: buf2.Bytes(),
|
|
}
|
|
|
|
sp, err := actors.SerializeParams(¶ms)
|
|
if err != nil {
|
|
return xerrors.Errorf("serializing params: %w", err)
|
|
}
|
|
|
|
smsg, err := api.MpoolPushMessage(ctx, &types.Message{
|
|
From: minfo.Worker,
|
|
To: maddr,
|
|
Method: builtin.MethodsMiner.ReportConsensusFault,
|
|
Value: big.Zero(),
|
|
Params: sp,
|
|
}, nil)
|
|
if err != nil {
|
|
return xerrors.Errorf("mpool push: %w", err)
|
|
}
|
|
|
|
fmt.Printf("Consensus fault reported in message %s\n", smsg.Cid())
|
|
|
|
wait, err := api.StateWaitMsg(ctx, smsg.Cid(), 0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// check it executed successfully
|
|
if wait.Receipt.ExitCode.IsError() {
|
|
fmt.Println(cctx.App.Writer, "Report consensus fault failed!")
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
},
|
|
}
|