lotus/cli/chain.go
Jakub Sztandera ae366972a9
Chain is love
Signed-off-by: Jakub Sztandera <kubuxu@protocol.ai>
2020-10-12 05:11:04 +02:00

1237 lines
27 KiB
Go

package cli
import (
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"path"
"sort"
"strconv"
"strings"
"time"
"github.com/filecoin-project/go-address"
cborutil "github.com/filecoin-project/go-cbor-util"
"github.com/filecoin-project/go-state-types/abi"
"github.com/filecoin-project/go-state-types/big"
"github.com/filecoin-project/specs-actors/actors/builtin"
"github.com/filecoin-project/specs-actors/actors/builtin/account"
"github.com/filecoin-project/specs-actors/actors/builtin/market"
"github.com/filecoin-project/specs-actors/actors/builtin/miner"
"github.com/filecoin-project/specs-actors/actors/builtin/power"
"github.com/filecoin-project/specs-actors/actors/util/adt"
cid "github.com/ipfs/go-cid"
"github.com/urfave/cli/v2"
cbg "github.com/whyrusleeping/cbor-gen"
"golang.org/x/xerrors"
lapi "github.com/filecoin-project/lotus/api"
"github.com/filecoin-project/lotus/build"
"github.com/filecoin-project/lotus/chain/actors"
"github.com/filecoin-project/lotus/chain/stmgr"
types "github.com/filecoin-project/lotus/chain/types"
)
var chainCmd = &cli.Command{
Name: "chain",
Usage: "Interact with filecoin blockchain",
Subcommands: []*cli.Command{
chainHeadCmd,
chainGetBlock,
chainReadObjCmd,
chainDeleteObjCmd,
chainStatObjCmd,
chainGetMsgCmd,
chainSetHeadCmd,
chainListCmd,
chainGetCmd,
chainBisectCmd,
chainExportCmd,
slashConsensusFault,
chainGasPriceCmd,
chainInspectUsage,
},
}
var chainHeadCmd = &cli.Command{
Name: "head",
Usage: "Print chain head",
Action: func(cctx *cli.Context) error {
api, closer, err := GetFullNodeAPI(cctx)
if err != nil {
return err
}
defer closer()
ctx := ReqContext(cctx)
head, err := api.ChainHead(ctx)
if err != nil {
return err
}
for _, c := range head.Cids() {
fmt.Println(c)
}
return nil
},
}
var chainGetBlock = &cli.Command{
Name: "getblock",
Usage: "Get a block and print its details",
ArgsUsage: "[blockCid]",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "raw",
Usage: "print just the raw block header",
},
},
Action: func(cctx *cli.Context) error {
api, closer, err := GetFullNodeAPI(cctx)
if err != nil {
return err
}
defer closer()
ctx := ReqContext(cctx)
if !cctx.Args().Present() {
return fmt.Errorf("must pass cid of block to print")
}
bcid, err := cid.Decode(cctx.Args().First())
if err != nil {
return err
}
blk, err := api.ChainGetBlock(ctx, bcid)
if err != nil {
return xerrors.Errorf("get block failed: %w", err)
}
if cctx.Bool("raw") {
out, err := json.MarshalIndent(blk, "", " ")
if err != nil {
return err
}
fmt.Println(string(out))
return nil
}
msgs, err := api.ChainGetBlockMessages(ctx, bcid)
if err != nil {
return xerrors.Errorf("failed to get messages: %w", err)
}
pmsgs, err := api.ChainGetParentMessages(ctx, bcid)
if err != nil {
return xerrors.Errorf("failed to get parent messages: %w", err)
}
recpts, err := api.ChainGetParentReceipts(ctx, bcid)
if err != nil {
log.Warn(err)
//return xerrors.Errorf("failed to get receipts: %w", err)
}
cblock := struct {
types.BlockHeader
BlsMessages []*types.Message
SecpkMessages []*types.SignedMessage
ParentReceipts []*types.MessageReceipt
ParentMessages []cid.Cid
}{}
cblock.BlockHeader = *blk
cblock.BlsMessages = msgs.BlsMessages
cblock.SecpkMessages = msgs.SecpkMessages
cblock.ParentReceipts = recpts
cblock.ParentMessages = apiMsgCids(pmsgs)
out, err := json.MarshalIndent(cblock, "", " ")
if err != nil {
return err
}
fmt.Println(string(out))
return nil
},
}
func apiMsgCids(in []lapi.Message) []cid.Cid {
out := make([]cid.Cid, len(in))
for k, v := range in {
out[k] = v.Cid
}
return out
}
var chainReadObjCmd = &cli.Command{
Name: "read-obj",
Usage: "Read the raw bytes of an object",
ArgsUsage: "[objectCid]",
Action: func(cctx *cli.Context) error {
api, closer, err := GetFullNodeAPI(cctx)
if err != nil {
return err
}
defer closer()
ctx := ReqContext(cctx)
c, err := cid.Decode(cctx.Args().First())
if err != nil {
return fmt.Errorf("failed to parse cid input: %s", err)
}
obj, err := api.ChainReadObj(ctx, c)
if err != nil {
return err
}
fmt.Printf("%x\n", obj)
return nil
},
}
var chainDeleteObjCmd = &cli.Command{
Name: "delete-obj",
Usage: "Delete an object from the chain blockstore",
Description: "WARNING: Removing wrong objects from the chain blockstore may lead to sync issues",
ArgsUsage: "[objectCid]",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "really-do-it",
},
},
Action: func(cctx *cli.Context) error {
api, closer, err := GetFullNodeAPI(cctx)
if err != nil {
return err
}
defer closer()
ctx := ReqContext(cctx)
c, err := cid.Decode(cctx.Args().First())
if err != nil {
return fmt.Errorf("failed to parse cid input: %s", err)
}
if !cctx.Bool("really-do-it") {
return xerrors.Errorf("pass the --really-do-it flag to proceed")
}
err = api.ChainDeleteObj(ctx, c)
if err != nil {
return err
}
fmt.Printf("Obj %s deleted\n", c.String())
return nil
},
}
var chainStatObjCmd = &cli.Command{
Name: "stat-obj",
Usage: "Collect size and ipld link counts for objs",
ArgsUsage: "[cid]",
Description: `Collect object size and ipld link count for an object.
When a base is provided it will be walked first, and all links visisted
will be ignored when the passed in object is walked.
`,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "base",
Usage: "ignore links found in this obj",
},
},
Action: func(cctx *cli.Context) error {
api, closer, err := GetFullNodeAPI(cctx)
if err != nil {
return err
}
defer closer()
ctx := ReqContext(cctx)
obj, err := cid.Decode(cctx.Args().First())
if err != nil {
return fmt.Errorf("failed to parse cid input: %s", err)
}
base := cid.Undef
if cctx.IsSet("base") {
base, err = cid.Decode(cctx.String("base"))
if err != nil {
return err
}
}
stats, err := api.ChainStatObj(ctx, obj, base)
if err != nil {
return err
}
fmt.Printf("Links: %d\n", stats.Links)
fmt.Printf("Size: %s (%d)\n", types.SizeStr(types.NewInt(stats.Size)), stats.Size)
return nil
},
}
var chainGetMsgCmd = &cli.Command{
Name: "getmessage",
Usage: "Get and print a message by its cid",
ArgsUsage: "[messageCid]",
Action: func(cctx *cli.Context) error {
if !cctx.Args().Present() {
return fmt.Errorf("must pass a cid of a message to get")
}
api, closer, err := GetFullNodeAPI(cctx)
if err != nil {
return err
}
defer closer()
ctx := ReqContext(cctx)
c, err := cid.Decode(cctx.Args().First())
if err != nil {
return xerrors.Errorf("failed to parse cid input: %w", err)
}
mb, err := api.ChainReadObj(ctx, c)
if err != nil {
return xerrors.Errorf("failed to read object: %w", err)
}
var i interface{}
m, err := types.DecodeMessage(mb)
if err != nil {
sm, err := types.DecodeSignedMessage(mb)
if err != nil {
return xerrors.Errorf("failed to decode object as a message: %w", err)
}
i = sm
} else {
i = m
}
enc, err := json.MarshalIndent(i, "", " ")
if err != nil {
return err
}
fmt.Println(string(enc))
return nil
},
}
var chainSetHeadCmd = &cli.Command{
Name: "sethead",
Usage: "manually set the local nodes head tipset (Caution: normally only used for recovery)",
ArgsUsage: "[tipsetkey]",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "genesis",
Usage: "reset head to genesis",
},
&cli.Uint64Flag{
Name: "epoch",
Usage: "reset head to given epoch",
},
},
Action: func(cctx *cli.Context) error {
api, closer, err := GetFullNodeAPI(cctx)
if err != nil {
return err
}
defer closer()
ctx := ReqContext(cctx)
var ts *types.TipSet
if cctx.Bool("genesis") {
ts, err = api.ChainGetGenesis(ctx)
}
if ts == nil && cctx.IsSet("epoch") {
ts, err = api.ChainGetTipSetByHeight(ctx, abi.ChainEpoch(cctx.Uint64("epoch")), types.EmptyTSK)
}
if ts == nil {
ts, err = parseTipSet(ctx, api, cctx.Args().Slice())
}
if err != nil {
return err
}
if ts == nil {
return fmt.Errorf("must pass cids for tipset to set as head")
}
if err := api.ChainSetHead(ctx, ts.Key()); err != nil {
return err
}
return nil
},
}
var chainInspectUsage = &cli.Command{
Name: "inspect-usage",
Usage: "Inspect block space usage of a given tipset",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "tipset",
Usage: "specify tipset to view block space usage of",
Value: "@head",
},
&cli.IntFlag{
Name: "length",
Usage: "length of chain to inspect block space usage for",
Value: 1,
},
&cli.IntFlag{
Name: "num-results",
Usage: "number of results to print per category",
Value: 10,
},
},
Action: func(cctx *cli.Context) error {
api, closer, err := GetFullNodeAPI(cctx)
if err != nil {
return err
}
defer closer()
ctx := ReqContext(cctx)
ts, err := LoadTipSet(ctx, cctx, api)
if err != nil {
return err
}
cur := ts
var msgs []lapi.Message
for i := 0; i < cctx.Int("length"); i++ {
pmsgs, err := api.ChainGetParentMessages(ctx, cur.Blocks()[0].Cid())
if err != nil {
return err
}
msgs = append(msgs, pmsgs...)
next, err := api.ChainGetTipSet(ctx, cur.Parents())
if err != nil {
return err
}
cur = next
}
codeCache := make(map[address.Address]cid.Cid)
lookupActorCode := func(a address.Address) (cid.Cid, error) {
c, ok := codeCache[a]
if ok {
return c, nil
}
act, err := api.StateGetActor(ctx, a, ts.Key())
if err != nil {
return cid.Undef, err
}
codeCache[a] = act.Code
return act.Code, nil
}
bySender := make(map[string]int64)
byDest := make(map[string]int64)
byMethod := make(map[string]int64)
bySenderC := make(map[string]int64)
byDestC := make(map[string]int64)
byMethodC := make(map[string]int64)
var sum int64
for _, m := range msgs {
bySender[m.Message.From.String()] += m.Message.GasLimit
bySenderC[m.Message.From.String()]++
byDest[m.Message.To.String()] += m.Message.GasLimit
byDestC[m.Message.To.String()]++
sum += m.Message.GasLimit
code, err := lookupActorCode(m.Message.To)
if err != nil {
return err
}
mm := stmgr.MethodsMap[code][m.Message.Method]
byMethod[mm.Name] += m.Message.GasLimit
byMethodC[mm.Name]++
}
type keyGasPair struct {
Key string
Gas int64
}
mapToSortedKvs := func(m map[string]int64) []keyGasPair {
var vals []keyGasPair
for k, v := range m {
vals = append(vals, keyGasPair{
Key: k,
Gas: v,
})
}
sort.Slice(vals, func(i, j int) bool {
return vals[i].Gas > vals[j].Gas
})
return vals
}
senderVals := mapToSortedKvs(bySender)
destVals := mapToSortedKvs(byDest)
methodVals := mapToSortedKvs(byMethod)
numRes := cctx.Int("num-results")
fmt.Printf("Total Gas Limit: %d\n", sum)
fmt.Printf("By Sender:\n")
for i := 0; i < numRes && i < len(senderVals); i++ {
sv := senderVals[i]
fmt.Printf("%s\t%0.2f%%\t(total: %d, count: %d)\n", sv.Key, (100*float64(sv.Gas))/float64(sum), sv.Gas, bySenderC[sv.Key])
}
fmt.Println()
fmt.Printf("By Receiver:\n")
for i := 0; i < numRes && i < len(destVals); i++ {
sv := destVals[i]
fmt.Printf("%s\t%0.2f%%\t(total: %d, count: %d)\n", sv.Key, (100*float64(sv.Gas))/float64(sum), sv.Gas, byDestC[sv.Key])
}
fmt.Println()
fmt.Printf("By Method:\n")
for i := 0; i < numRes && i < len(methodVals); i++ {
sv := methodVals[i]
fmt.Printf("%s\t%0.2f%%\t(total: %d, count: %d)\n", sv.Key, (100*float64(sv.Gas))/float64(sum), sv.Gas, byMethodC[sv.Key])
}
return nil
},
}
var chainListCmd = &cli.Command{
Name: "list",
Aliases: []string{"love"},
Usage: "View a segment of the chain",
Flags: []cli.Flag{
&cli.Uint64Flag{Name: "height"},
&cli.IntFlag{Name: "count", Value: 30},
&cli.StringFlag{
Name: "format",
Usage: "specify the format to print out tipsets",
Value: "<height>: (<time>) <blocks>",
},
&cli.BoolFlag{
Name: "gas-stats",
Usage: "view gas statistics for the chain",
},
},
Action: func(cctx *cli.Context) error {
api, closer, err := GetFullNodeAPI(cctx)
if err != nil {
return err
}
defer closer()
ctx := ReqContext(cctx)
var head *types.TipSet
if cctx.IsSet("height") {
head, err = api.ChainGetTipSetByHeight(ctx, abi.ChainEpoch(cctx.Uint64("height")), types.EmptyTSK)
} else {
head, err = api.ChainHead(ctx)
}
if err != nil {
return err
}
count := cctx.Int("count")
if count < 1 {
return nil
}
tss := make([]*types.TipSet, 0, count)
tss = append(tss, head)
for i := 1; i < count; i++ {
if head.Height() == 0 {
break
}
head, err = api.ChainGetTipSet(ctx, head.Parents())
if err != nil {
return err
}
tss = append(tss, head)
}
if cctx.Bool("gas-stats") {
otss := make([]*types.TipSet, 0, len(tss))
for i := len(tss) - 1; i >= 0; i-- {
otss = append(otss, tss[i])
}
tss = otss
for i, ts := range tss {
pbf := ts.Blocks()[0].ParentBaseFee
fmt.Printf("%d: %d blocks (baseFee: %s -> maxFee: %s)\n", ts.Height(), len(ts.Blocks()), ts.Blocks()[0].ParentBaseFee, types.FIL(types.BigMul(pbf, types.NewInt(uint64(build.BlockGasLimit)))))
for _, b := range ts.Blocks() {
msgs, err := api.ChainGetBlockMessages(ctx, b.Cid())
if err != nil {
return err
}
var limitSum int64
psum := big.NewInt(0)
for _, m := range msgs.BlsMessages {
limitSum += m.GasLimit
psum = big.Add(psum, m.GasPremium)
}
for _, m := range msgs.SecpkMessages {
limitSum += m.Message.GasLimit
psum = big.Add(psum, m.Message.GasPremium)
}
lenmsgs := len(msgs.BlsMessages) + len(msgs.SecpkMessages)
avgpremium := big.Zero()
if lenmsgs > 0 {
avgpremium = big.Div(psum, big.NewInt(int64(lenmsgs)))
}
fmt.Printf("\t%s: \t%d msgs, gasLimit: %d / %d (%0.2f%%), avgPremium: %s\n", b.Miner, len(msgs.BlsMessages)+len(msgs.SecpkMessages), limitSum, build.BlockGasLimit, 100*float64(limitSum)/float64(build.BlockGasLimit), avgpremium)
}
if i < len(tss)-1 {
msgs, err := api.ChainGetParentMessages(ctx, tss[i+1].Blocks()[0].Cid())
if err != nil {
return err
}
var limitSum int64
for _, m := range msgs {
limitSum += m.Message.GasLimit
}
recpts, err := api.ChainGetParentReceipts(ctx, tss[i+1].Blocks()[0].Cid())
if err != nil {
return err
}
var gasUsed int64
for _, r := range recpts {
gasUsed += r.GasUsed
}
fmt.Printf("\ttipset: \t%d msgs, %d / %d (%0.2f%%)\n", len(msgs), gasUsed, limitSum, 100*float64(gasUsed)/float64(limitSum))
}
fmt.Println()
}
} else {
for i := len(tss) - 1; i >= 0; i-- {
printTipSet(cctx.String("format"), tss[i])
}
}
return nil
},
}
var chainGetCmd = &cli.Command{
Name: "get",
Usage: "Get chain DAG node by path",
ArgsUsage: "[path]",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "as-type",
Usage: "specify type to interpret output as",
},
&cli.BoolFlag{
Name: "verbose",
Value: false,
},
&cli.StringFlag{
Name: "tipset",
Usage: "specify tipset for /pstate (pass comma separated array of cids)",
},
},
Description: `Get ipld node under a specified path:
lotus chain get /ipfs/[cid]/some/path
Path prefixes:
- /ipfs/[cid], /ipld/[cid] - traverse IPLD path
- /pstate - traverse from head.ParentStateRoot
Note:
You can use special path elements to traverse through some data structures:
- /ipfs/[cid]/@H:elem - get 'elem' from hamt
- /ipfs/[cid]/@Hi:123 - get varint elem 123 from hamt
- /ipfs/[cid]/@Hu:123 - get uvarint elem 123 from hamt
- /ipfs/[cid]/@Ha:t01 - get element under Addr(t01).Bytes
- /ipfs/[cid]/@A:10 - get 10th amt element
- .../@Ha:t01/@state - get pretty map-based actor state
List of --as-type types:
- raw
- block
- message
- smessage, signedmessage
- actor
- amt
- hamt-epoch
- hamt-address
- cronevent
- account-state
`,
Action: func(cctx *cli.Context) error {
api, closer, err := GetFullNodeAPI(cctx)
if err != nil {
return err
}
defer closer()
ctx := ReqContext(cctx)
p := path.Clean(cctx.Args().First())
if strings.HasPrefix(p, "/pstate") {
p = p[len("/pstate"):]
ts, err := LoadTipSet(ctx, cctx, api)
if err != nil {
return err
}
if ts == nil {
ts, err = api.ChainHead(ctx)
if err != nil {
return err
}
}
p = "/ipfs/" + ts.ParentState().String() + p
if cctx.Bool("verbose") {
fmt.Println(p)
}
}
obj, err := api.ChainGetNode(ctx, p)
if err != nil {
return err
}
t := strings.ToLower(cctx.String("as-type"))
if t == "" {
b, err := json.MarshalIndent(obj.Obj, "", "\t")
if err != nil {
return err
}
fmt.Println(string(b))
return nil
}
var cbu cbg.CBORUnmarshaler
switch t {
case "raw":
cbu = nil
case "block":
cbu = new(types.BlockHeader)
case "message":
cbu = new(types.Message)
case "smessage", "signedmessage":
cbu = new(types.SignedMessage)
case "actor":
cbu = new(types.Actor)
case "amt":
return handleAmt(ctx, api, obj.Cid)
case "hamt-epoch":
return handleHamtEpoch(ctx, api, obj.Cid)
case "hamt-address":
return handleHamtAddress(ctx, api, obj.Cid)
case "cronevent":
cbu = new(power.CronEvent)
case "account-state":
cbu = new(account.State)
case "miner-state":
cbu = new(miner.State)
case "power-state":
cbu = new(power.State)
case "market-state":
cbu = new(market.State)
default:
return fmt.Errorf("unknown type: %q", t)
}
raw, err := api.ChainReadObj(ctx, obj.Cid)
if err != nil {
return err
}
if cbu == nil {
fmt.Printf("%x", raw)
return nil
}
if err := cbu.UnmarshalCBOR(bytes.NewReader(raw)); err != nil {
return fmt.Errorf("failed to unmarshal as %q", t)
}
b, err := json.MarshalIndent(cbu, "", "\t")
if err != nil {
return err
}
fmt.Println(string(b))
return nil
},
}
type apiIpldStore struct {
ctx context.Context
api lapi.FullNode
}
func (ht *apiIpldStore) Context() context.Context {
return ht.ctx
}
func (ht *apiIpldStore) Get(ctx context.Context, c cid.Cid, out interface{}) error {
raw, err := ht.api.ChainReadObj(ctx, c)
if err != nil {
return err
}
cu, ok := out.(cbg.CBORUnmarshaler)
if ok {
if err := cu.UnmarshalCBOR(bytes.NewReader(raw)); err != nil {
return err
}
return nil
}
return fmt.Errorf("Object does not implement CBORUnmarshaler")
}
func (ht *apiIpldStore) Put(ctx context.Context, v interface{}) (cid.Cid, error) {
panic("No mutations allowed")
}
func handleAmt(ctx context.Context, api lapi.FullNode, r cid.Cid) error {
s := &apiIpldStore{ctx, api}
mp, err := adt.AsArray(s, r)
if err != nil {
return err
}
return mp.ForEach(nil, func(key int64) error {
fmt.Printf("%d\n", key)
return nil
})
}
func handleHamtEpoch(ctx context.Context, api lapi.FullNode, r cid.Cid) error {
s := &apiIpldStore{ctx, api}
mp, err := adt.AsMap(s, r)
if err != nil {
return err
}
return mp.ForEach(nil, func(key string) error {
ik, err := abi.ParseIntKey(key)
if err != nil {
return err
}
fmt.Printf("%d\n", ik)
return nil
})
}
func handleHamtAddress(ctx context.Context, api lapi.FullNode, r cid.Cid) error {
s := &apiIpldStore{ctx, api}
mp, err := adt.AsMap(s, r)
if err != nil {
return err
}
return mp.ForEach(nil, func(key string) error {
addr, err := address.NewFromBytes([]byte(key))
if err != nil {
return err
}
fmt.Printf("%s\n", addr)
return nil
})
}
func printTipSet(format string, ts *types.TipSet) {
format = strings.ReplaceAll(format, "<height>", fmt.Sprint(ts.Height()))
format = strings.ReplaceAll(format, "<time>", time.Unix(int64(ts.MinTimestamp()), 0).Format(time.Stamp))
blks := "[ "
for _, b := range ts.Blocks() {
blks += fmt.Sprintf("%s: %s,", b.Cid(), b.Miner)
}
blks += " ]"
sCids := make([]string, 0, len(blks))
for _, c := range ts.Cids() {
sCids = append(sCids, c.String())
}
format = strings.ReplaceAll(format, "<tipset>", strings.Join(sCids, ","))
format = strings.ReplaceAll(format, "<blocks>", blks)
format = strings.ReplaceAll(format, "<weight>", fmt.Sprint(ts.Blocks()[0].ParentWeight))
fmt.Println(format)
}
var chainBisectCmd = &cli.Command{
Name: "bisect",
Usage: "bisect chain for an event",
ArgsUsage: "[minHeight maxHeight path shellCommand <shellCommandArgs (if any)>]",
Description: `Bisect the chain state tree:
lotus chain bisect [min height] [max height] '1/2/3/state/path' 'shell command' 'args'
Returns the first tipset in which condition is true
v
[start] FFFFFFFTTT [end]
Example: find height at which deal ID 100 000 appeared
- lotus chain bisect 1 32000 '@Ha:t03/1' jq -e '.[2] > 100000'
For special path elements see 'chain get' help
`,
Action: func(cctx *cli.Context) error {
api, closer, err := GetFullNodeAPI(cctx)
if err != nil {
return err
}
defer closer()
ctx := ReqContext(cctx)
if cctx.Args().Len() < 4 {
return xerrors.New("need at least 4 args")
}
start, err := strconv.ParseUint(cctx.Args().Get(0), 10, 64)
if err != nil {
return err
}
end, err := strconv.ParseUint(cctx.Args().Get(1), 10, 64)
if err != nil {
return err
}
subPath := cctx.Args().Get(2)
highest, err := api.ChainGetTipSetByHeight(ctx, abi.ChainEpoch(end), types.EmptyTSK)
if err != nil {
return xerrors.Errorf("getting end tipset: %w", err)
}
prev := highest.Height()
for {
mid := (start + end) / 2
if end-start == 1 {
mid = end
start = end
}
midTs, err := api.ChainGetTipSetByHeight(ctx, abi.ChainEpoch(mid), highest.Key())
if err != nil {
return err
}
path := "/ipld/" + midTs.ParentState().String() + "/" + subPath
fmt.Printf("* Testing %d (%d - %d) (%s): ", mid, start, end, path)
nd, err := api.ChainGetNode(ctx, path)
if err != nil {
return err
}
b, err := json.MarshalIndent(nd.Obj, "", "\t")
if err != nil {
return err
}
cmd := exec.CommandContext(ctx, cctx.Args().Get(3), cctx.Args().Slice()[4:]...)
cmd.Stdin = bytes.NewReader(b)
var out bytes.Buffer
var serr bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = &serr
switch cmd.Run().(type) {
case nil:
// it's lower
if strings.TrimSpace(out.String()) != "false" {
end = mid
highest = midTs
fmt.Println("true")
} else {
start = mid
fmt.Printf("false (cli)\n")
}
case *exec.ExitError:
if len(serr.String()) > 0 {
fmt.Println("error")
fmt.Printf("> Command: %s\n---->\n", strings.Join(cctx.Args().Slice()[3:], " "))
fmt.Println(string(b))
fmt.Println("<----")
return xerrors.Errorf("error running bisect check: %s", serr.String())
}
start = mid
fmt.Println("false")
default:
return err
}
if start == end {
if strings.TrimSpace(out.String()) == "true" {
fmt.Println(midTs.Height())
} else {
fmt.Println(prev)
}
return nil
}
prev = abi.ChainEpoch(mid)
}
},
}
var chainExportCmd = &cli.Command{
Name: "export",
Usage: "export chain to a car file",
ArgsUsage: "[outputPath]",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "tipset",
},
&cli.Int64Flag{
Name: "recent-stateroots",
Usage: "specify the number of recent state roots to include in the export",
},
&cli.BoolFlag{
Name: "skip-old-msgs",
},
},
Action: func(cctx *cli.Context) error {
api, closer, err := GetFullNodeAPI(cctx)
if err != nil {
return err
}
defer closer()
ctx := ReqContext(cctx)
if !cctx.Args().Present() {
return fmt.Errorf("must specify filename to export chain to")
}
rsrs := abi.ChainEpoch(cctx.Int64("recent-stateroots"))
if cctx.IsSet("recent-stateroots") && rsrs < build.Finality {
return fmt.Errorf("\"recent-stateroots\" has to be greater than %d", build.Finality)
}
fi, err := os.Create(cctx.Args().First())
if err != nil {
return err
}
defer func() {
err := fi.Close()
if err != nil {
fmt.Printf("error closing output file: %+v", err)
}
}()
ts, err := LoadTipSet(ctx, cctx, api)
if err != nil {
return err
}
skipold := cctx.Bool("skip-old-msgs")
if rsrs == 0 && skipold {
return fmt.Errorf("must pass recent stateroots along with skip-old-msgs")
}
stream, err := api.ChainExport(ctx, rsrs, skipold, ts.Key())
if err != nil {
return err
}
var last bool
for b := range stream {
last = len(b) == 0
_, err := fi.Write(b)
if err != nil {
return err
}
}
if !last {
return xerrors.Errorf("incomplete export (remote connection lost?)")
}
return nil
},
}
var slashConsensusFault = &cli.Command{
Name: "slash-consensus",
Usage: "Report consensus fault",
ArgsUsage: "[blockCid1 blockCid2]",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "miner",
Usage: "Miner address",
},
&cli.StringFlag{
Name: "extra",
Usage: "Extra block cid",
},
},
Action: func(cctx *cli.Context) error {
api, closer, err := GetFullNodeAPI(cctx)
if err != nil {
return err
}
defer closer()
ctx := ReqContext(cctx)
c1, err := cid.Parse(cctx.Args().Get(0))
if err != nil {
return xerrors.Errorf("parsing cid 1: %w", err)
}
b1, err := api.ChainGetBlock(ctx, c1)
if err != nil {
return xerrors.Errorf("getting block 1: %w", err)
}
c2, err := cid.Parse(cctx.Args().Get(1))
if err != nil {
return xerrors.Errorf("parsing cid 2: %w", err)
}
b2, err := api.ChainGetBlock(ctx, c2)
if err != nil {
return xerrors.Errorf("getting block 2: %w", err)
}
def, err := api.WalletDefaultAddress(ctx)
if err != nil {
return err
}
bh1, err := cborutil.Dump(b1)
if err != nil {
return err
}
bh2, err := cborutil.Dump(b2)
if err != nil {
return err
}
params := miner.ReportConsensusFaultParams{
BlockHeader1: bh1,
BlockHeader2: bh2,
}
if cctx.String("extra") != "" {
cExtra, err := cid.Parse(cctx.String("extra"))
if err != nil {
return xerrors.Errorf("parsing cid extra: %w", err)
}
bExtra, err := api.ChainGetBlock(ctx, cExtra)
if err != nil {
return xerrors.Errorf("getting block extra: %w", err)
}
be, err := cborutil.Dump(bExtra)
if err != nil {
return err
}
params.BlockHeaderExtra = be
}
enc, err := actors.SerializeParams(&params)
if err != nil {
return err
}
if cctx.String("miner") == "" {
return xerrors.Errorf("--miner flag is required")
}
maddr, err := address.NewFromString(cctx.String("miner"))
if err != nil {
return err
}
msg := &types.Message{
To: maddr,
From: def,
Value: types.NewInt(0),
Method: builtin.MethodsMiner.ReportConsensusFault,
Params: enc,
}
smsg, err := api.MpoolPushMessage(ctx, msg, nil)
if err != nil {
return err
}
fmt.Println(smsg.Cid())
return nil
},
}
var chainGasPriceCmd = &cli.Command{
Name: "gas-price",
Usage: "Estimate gas prices",
Action: func(cctx *cli.Context) error {
api, closer, err := GetFullNodeAPI(cctx)
if err != nil {
return err
}
defer closer()
ctx := ReqContext(cctx)
nb := []int{1, 2, 3, 5, 10, 20, 50, 100, 300}
for _, nblocks := range nb {
addr := builtin.SystemActorAddr // TODO: make real when used in GasEstimateGasPremium
est, err := api.GasEstimateGasPremium(ctx, uint64(nblocks), addr, 10000, types.EmptyTSK)
if err != nil {
return err
}
fmt.Printf("%d blocks: %s (%s)\n", nblocks, est, types.FIL(est))
}
return nil
},
}