Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

lotus-shed: add consensus check command #3933

Merged
merged 1 commit into from
Sep 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
286 changes: 286 additions & 0 deletions cmd/lotus-shed/consensus.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
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"
"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.Version
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 := lcli.APIInfo{Addr: apima}
addr, err := ainfo.DialArgs()
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
},
}
8 changes: 8 additions & 0 deletions cmd/lotus-shed/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ func main() {
mathCmd,
mpoolStatsCmd,
exportChainCmd,
consensusCmd,
}

app := &cli.App{
Expand All @@ -49,6 +50,13 @@ func main() {
Hidden: true,
Value: "~/.lotus", // TODO: Consider XDG_DATA_HOME
},
&cli.StringFlag{
Name: "log-level",
Value: "info",
},
},
Before: func(cctx *cli.Context) error {
return logging.SetLogLevel("lotus-shed", cctx.String("log-level"))
},
}

Expand Down