package main import ( "bufio" "fmt" "io" "os" "strconv" "strings" "time" "github.com/filecoin-project/go-state-types/abi" "github.com/filecoin-project/lotus/api" "github.com/filecoin-project/lotus/api/client" "github.com/filecoin-project/lotus/build" "github.com/filecoin-project/lotus/chain/types" lcli "github.com/filecoin-project/lotus/cli" cliutil "github.com/filecoin-project/lotus/cli/util" "github.com/libp2p/go-libp2p-core/peer" "github.com/multiformats/go-multiaddr" "github.com/urfave/cli/v2" ) var consensusCmd = &cli.Command{ Name: "consensus", Usage: "tools for gathering information about consensus between nodes", Flags: []cli.Flag{}, Subcommands: []*cli.Command{ consensusCheckCmd, }, } type consensusItem struct { multiaddr multiaddr.Multiaddr genesisTipset *types.TipSet targetTipset *types.TipSet headTipset *types.TipSet peerID peer.ID version api.APIVersion api api.FullNode } var consensusCheckCmd = &cli.Command{ Name: "check", Usage: "verify if all nodes agree upon a common tipset for a given tipset height", Description: `Consensus check verifies that all nodes share a common tipset for a given height. The height flag specifies a chain height to start a comparison from. There are two special arguments for this flag. All other expected values should be chain tipset heights. @common - Use the maximum common chain height between all nodes @expected - Use the current time and the genesis timestamp to determine a height Examples Find the highest common tipset and look back 10 tipsets lotus-shed consensus check --height @common --lookback 10 Calculate the expected tipset height and look back 10 tipsets lotus-shed consensus check --height @expected --lookback 10 Check if nodes all share a common genesis lotus-shed consensus check --height 0 Check that all nodes agree upon the tipset for 1day post genesis lotus-shed consensus check --height 2880 --lookback 0 `, Flags: []cli.Flag{ &cli.StringFlag{ Name: "height", Value: "@common", Usage: "height of tipset to start check from", }, &cli.IntFlag{ Name: "lookback", Value: int(build.MessageConfidence * 2), Usage: "number of tipsets behind to look back when comparing nodes", }, }, Action: func(cctx *cli.Context) error { filePath := cctx.Args().First() var input *bufio.Reader if cctx.Args().Len() == 0 { input = bufio.NewReader(os.Stdin) } else { var err error inputFile, err := os.Open(filePath) if err != nil { return err } defer inputFile.Close() //nolint:errcheck input = bufio.NewReader(inputFile) } var nodes []*consensusItem ctx := lcli.ReqContext(cctx) for { strma, errR := input.ReadString('\n') strma = strings.TrimSpace(strma) if len(strma) == 0 { if errR == io.EOF { break } continue } apima, err := multiaddr.NewMultiaddr(strma) if err != nil { return err } ainfo := cliutil.APIInfo{Addr: apima.String()} addr, err := ainfo.DialArgs("v1") if err != nil { return err } api, closer, err := client.NewFullNodeRPC(cctx.Context, addr, nil) if err != nil { return err } defer closer() peerID, err := api.ID(ctx) if err != nil { return err } version, err := api.Version(ctx) if err != nil { return err } genesisTipset, err := api.ChainGetGenesis(ctx) if err != nil { return err } headTipset, err := api.ChainHead(ctx) if err != nil { return err } nodes = append(nodes, &consensusItem{ genesisTipset: genesisTipset, headTipset: headTipset, multiaddr: apima, api: api, peerID: peerID, version: version, }) if errR != nil && errR != io.EOF { return err } if errR == io.EOF { break } } if len(nodes) == 0 { return fmt.Errorf("no nodes") } genesisBuckets := make(map[types.TipSetKey][]*consensusItem) for _, node := range nodes { genesisBuckets[node.genesisTipset.Key()] = append(genesisBuckets[node.genesisTipset.Key()], node) } if len(genesisBuckets) != 1 { for _, nodes := range genesisBuckets { for _, node := range nodes { log.Errorw( "genesis do not match", "genesis_tipset", node.genesisTipset.Key(), "peer_id", node.peerID, "version", node.version, ) } } return fmt.Errorf("genesis does not match between all nodes") } target := abi.ChainEpoch(0) switch cctx.String("height") { case "@common": minTipset := nodes[0].headTipset for _, node := range nodes { if node.headTipset.Height() < minTipset.Height() { minTipset = node.headTipset } } target = minTipset.Height() case "@expected": tnow := uint64(time.Now().Unix()) tgen := nodes[0].genesisTipset.MinTimestamp() target = abi.ChainEpoch((tnow - tgen) / build.BlockDelaySecs) default: h, err := strconv.Atoi(strings.TrimSpace(cctx.String("height"))) if err != nil { return fmt.Errorf("failed to parse string: %s", cctx.String("height")) } target = abi.ChainEpoch(h) } lookback := abi.ChainEpoch(cctx.Int("lookback")) if lookback > target { target = abi.ChainEpoch(0) } else { target = target - lookback } for _, node := range nodes { targetTipset, err := node.api.ChainGetTipSetByHeight(ctx, target, types.EmptyTSK) if err != nil { log.Errorw("error checking target", "err", err) node.targetTipset = nil } else { node.targetTipset = targetTipset } } for _, node := range nodes { log.Debugw( "node info", "peer_id", node.peerID, "version", node.version, "genesis_tipset", node.genesisTipset.Key(), "head_tipset", node.headTipset.Key(), "target_tipset", node.targetTipset.Key(), ) } targetBuckets := make(map[types.TipSetKey][]*consensusItem) for _, node := range nodes { if node.targetTipset == nil { targetBuckets[types.EmptyTSK] = append(targetBuckets[types.EmptyTSK], node) continue } targetBuckets[node.targetTipset.Key()] = append(targetBuckets[node.targetTipset.Key()], node) } if nodes, ok := targetBuckets[types.EmptyTSK]; ok { for _, node := range nodes { log.Errorw( "targeted tipset not found", "peer_id", node.peerID, "version", node.version, "genesis_tipset", node.genesisTipset.Key(), "head_tipset", node.headTipset.Key(), "target_tipset", node.targetTipset.Key(), ) } return fmt.Errorf("targeted tipset not found") } if len(targetBuckets) != 1 { for _, nodes := range targetBuckets { for _, node := range nodes { log.Errorw( "targeted tipset not found", "peer_id", node.peerID, "version", node.version, "genesis_tipset", node.genesisTipset.Key(), "head_tipset", node.headTipset.Key(), "target_tipset", node.targetTipset.Key(), ) } } return fmt.Errorf("nodes not in consensus at tipset height %d", target) } return nil }, }