Merge pull request #4554 from filecoin-project/tvx-simulate-2

tvx simulate command; tvx extract --ignore-sanity-checks
This commit is contained in:
Łukasz Magiera 2020-10-23 15:58:22 +02:00 committed by GitHub
commit 91fae36b86
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 338 additions and 70 deletions

View File

@ -19,7 +19,6 @@ import (
"github.com/filecoin-project/lotus/chain/actors/builtin/reward" "github.com/filecoin-project/lotus/chain/actors/builtin/reward"
"github.com/filecoin-project/lotus/chain/types" "github.com/filecoin-project/lotus/chain/types"
"github.com/filecoin-project/lotus/chain/vm" "github.com/filecoin-project/lotus/chain/vm"
lcli "github.com/filecoin-project/lotus/cli"
"github.com/filecoin-project/lotus/conformance" "github.com/filecoin-project/lotus/conformance"
"github.com/filecoin-project/test-vectors/schema" "github.com/filecoin-project/test-vectors/schema"
@ -34,13 +33,14 @@ const (
) )
type extractOpts struct { type extractOpts struct {
id string id string
block string block string
class string class string
cid string cid string
file string file string
retain string retain string
precursor string precursor string
ignoreSanityChecks bool
} }
var extractFlags extractOpts var extractFlags extractOpts
@ -49,6 +49,8 @@ var extractCmd = &cli.Command{
Name: "extract", Name: "extract",
Description: "generate a test vector by extracting it from a live chain", Description: "generate a test vector by extracting it from a live chain",
Action: runExtract, Action: runExtract,
Before: initialize,
After: destroy,
Flags: []cli.Flag{ Flags: []cli.Flag{
&repoFlag, &repoFlag,
&cli.StringFlag{ &cli.StringFlag{
@ -88,53 +90,42 @@ var extractCmd = &cli.Command{
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: "precursor-select", Name: "precursor-select",
Usage: "precursors to apply; values: 'all', 'sender'; 'all' selects all preceding" + Usage: "precursors to apply; values: 'all', 'sender'; 'all' selects all preceding " +
"messages in the canonicalised tipset, 'sender' selects only preceding messages from the same" + "messages in the canonicalised tipset, 'sender' selects only preceding messages from the same " +
"sender. Usually, 'sender' is a good tradeoff and gives you sufficient accuracy. If the receipt sanity" + "sender. Usually, 'sender' is a good tradeoff and gives you sufficient accuracy. If the receipt sanity " +
"check fails due to gas reasons, switch to 'all', as previous messages in the tipset may have" + "check fails due to gas reasons, switch to 'all', as previous messages in the tipset may have " +
"affected state in a disruptive way", "affected state in a disruptive way",
Value: "sender", Value: "sender",
Destination: &extractFlags.precursor, Destination: &extractFlags.precursor,
}, },
&cli.BoolFlag{
Name: "ignore-sanity-checks",
Usage: "generate vector even if sanity checks fail",
Value: false,
Destination: &extractFlags.ignoreSanityChecks,
},
}, },
} }
func runExtract(c *cli.Context) error { func runExtract(_ *cli.Context) error {
// LOTUS_DISABLE_VM_BUF disables what's called "VM state tree buffering", return doExtract(extractFlags)
// which stashes write operations in a BufferedBlockstore
// (https://github.com/filecoin-project/lotus/blob/b7a4dbb07fd8332b4492313a617e3458f8003b2a/lib/bufbstore/buf_bstore.go#L21)
// such that they're not written until the VM is actually flushed.
//
// For some reason, the standard behaviour was not working for me (raulk),
// and disabling it (such that the state transformations are written immediately
// to the blockstore) worked.
_ = os.Setenv("LOTUS_DISABLE_VM_BUF", "iknowitsabadidea")
ctx := context.Background()
// Make the API client.
fapi, closer, err := lcli.GetFullNodeAPI(c)
if err != nil {
return err
}
defer closer()
return doExtract(ctx, fapi, extractFlags)
} }
func doExtract(ctx context.Context, fapi api.FullNode, opts extractOpts) error { func doExtract(opts extractOpts) error {
ctx := context.Background()
mcid, err := cid.Decode(opts.cid) mcid, err := cid.Decode(opts.cid)
if err != nil { if err != nil {
return err return err
} }
msg, execTs, incTs, err := resolveFromChain(ctx, fapi, mcid, opts.block) msg, execTs, incTs, err := resolveFromChain(ctx, FullAPI, mcid, opts.block)
if err != nil { if err != nil {
return fmt.Errorf("failed to resolve message and tipsets from chain: %w", err) return fmt.Errorf("failed to resolve message and tipsets from chain: %w", err)
} }
// get the circulating supply before the message was executed. // get the circulating supply before the message was executed.
circSupplyDetail, err := fapi.StateVMCirculatingSupplyInternal(ctx, incTs.Key()) circSupplyDetail, err := FullAPI.StateVMCirculatingSupplyInternal(ctx, incTs.Key())
if err != nil { if err != nil {
return fmt.Errorf("failed while fetching circulating supply: %w", err) return fmt.Errorf("failed while fetching circulating supply: %w", err)
} }
@ -147,7 +138,7 @@ func doExtract(ctx context.Context, fapi api.FullNode, opts extractOpts) error {
log.Printf("finding precursor messages using mode: %s", opts.precursor) log.Printf("finding precursor messages using mode: %s", opts.precursor)
// Fetch messages in canonical order from inclusion tipset. // Fetch messages in canonical order from inclusion tipset.
msgs, err := fapi.ChainGetParentMessages(ctx, execTs.Blocks()[0].Cid()) msgs, err := FullAPI.ChainGetParentMessages(ctx, execTs.Blocks()[0].Cid())
if err != nil { if err != nil {
return fmt.Errorf("failed to fetch messages in canonical order from inclusion tipset: %w", err) return fmt.Errorf("failed to fetch messages in canonical order from inclusion tipset: %w", err)
} }
@ -174,8 +165,8 @@ func doExtract(ctx context.Context, fapi api.FullNode, opts extractOpts) error {
var ( var (
// create a read-through store that uses ChainGetObject to fetch unknown CIDs. // create a read-through store that uses ChainGetObject to fetch unknown CIDs.
pst = NewProxyingStores(ctx, fapi) pst = NewProxyingStores(ctx, FullAPI)
g = NewSurgeon(ctx, fapi, pst) g = NewSurgeon(ctx, FullAPI, pst)
) )
driver := conformance.NewDriver(ctx, schema.Selector{}, conformance.DriverOpts{ driver := conformance.NewDriver(ctx, schema.Selector{}, conformance.DriverOpts{
@ -200,7 +191,7 @@ func doExtract(ctx context.Context, fapi api.FullNode, opts extractOpts) error {
CircSupply: circSupplyDetail.FilCirculating, CircSupply: circSupplyDetail.FilCirculating,
BaseFee: basefee, BaseFee: basefee,
// recorded randomness will be discarded. // recorded randomness will be discarded.
Rand: conformance.NewRecordingRand(new(conformance.LogReporter), fapi), Rand: conformance.NewRecordingRand(new(conformance.LogReporter), FullAPI),
}) })
if err != nil { if err != nil {
return fmt.Errorf("failed to execute precursor message: %w", err) return fmt.Errorf("failed to execute precursor message: %w", err)
@ -215,7 +206,7 @@ func doExtract(ctx context.Context, fapi api.FullNode, opts extractOpts) error {
retention = opts.retain retention = opts.retain
// recordingRand will record randomness so we can embed it in the test vector. // recordingRand will record randomness so we can embed it in the test vector.
recordingRand = conformance.NewRecordingRand(new(conformance.LogReporter), fapi) recordingRand = conformance.NewRecordingRand(new(conformance.LogReporter), FullAPI)
) )
log.Printf("using state retention strategy: %s", retention) log.Printf("using state retention strategy: %s", retention)
@ -248,7 +239,7 @@ func doExtract(ctx context.Context, fapi api.FullNode, opts extractOpts) error {
case "accessed-actors": case "accessed-actors":
log.Printf("calculating accessed actors") log.Printf("calculating accessed actors")
// get actors accessed by message. // get actors accessed by message.
retain, err := g.GetAccessedActors(ctx, fapi, mcid) retain, err := g.GetAccessedActors(ctx, FullAPI, mcid)
if err != nil { if err != nil {
return fmt.Errorf("failed to calculate accessed actors: %w", err) return fmt.Errorf("failed to calculate accessed actors: %w", err)
} }
@ -286,7 +277,7 @@ func doExtract(ctx context.Context, fapi api.FullNode, opts extractOpts) error {
// TODO sometimes this returns a nil receipt and no error ¯\_(ツ)_/¯ // TODO sometimes this returns a nil receipt and no error ¯\_(ツ)_/¯
// ex: https://filfox.info/en/message/bafy2bzacebpxw3yiaxzy2bako62akig46x3imji7fewszen6fryiz6nymu2b2 // ex: https://filfox.info/en/message/bafy2bzacebpxw3yiaxzy2bako62akig46x3imji7fewszen6fryiz6nymu2b2
// This code is lenient and skips receipt comparison in case of a nil receipt. // This code is lenient and skips receipt comparison in case of a nil receipt.
rec, err := fapi.StateGetReceipt(ctx, mcid, execTs.Key()) rec, err := FullAPI.StateGetReceipt(ctx, mcid, execTs.Key())
if err != nil { if err != nil {
return fmt.Errorf("failed to find receipt on chain: %w", err) return fmt.Errorf("failed to find receipt on chain: %w", err)
} }
@ -300,13 +291,20 @@ func doExtract(ctx context.Context, fapi api.FullNode, opts extractOpts) error {
ReturnValue: rec.Return, ReturnValue: rec.Return,
GasUsed: rec.GasUsed, GasUsed: rec.GasUsed,
} }
reporter := new(conformance.LogReporter) reporter := new(conformance.LogReporter)
conformance.AssertMsgResult(reporter, receipt, applyret, "as locally executed") conformance.AssertMsgResult(reporter, receipt, applyret, "as locally executed")
if reporter.Failed() { if reporter.Failed() {
log.Println(color.RedString("receipt sanity check failed; aborting")) if opts.ignoreSanityChecks {
return fmt.Errorf("vector generation aborted") log.Println(color.YellowString("receipt sanity check failed; proceeding anyway"))
} else {
log.Println(color.RedString("receipt sanity check failed; aborting"))
return fmt.Errorf("vector generation aborted")
}
} else {
log.Println(color.GreenString("receipt sanity check succeeded"))
} }
log.Println(color.GreenString("receipt sanity check succeeded"))
} else { } else {
receipt = &schema.Receipt{ receipt = &schema.Receipt{
ExitCode: int64(applyret.ExitCode), ExitCode: int64(applyret.ExitCode),
@ -336,17 +334,17 @@ func doExtract(ctx context.Context, fapi api.FullNode, opts extractOpts) error {
return err return err
} }
version, err := fapi.Version(ctx) version, err := FullAPI.Version(ctx)
if err != nil { if err != nil {
return err return err
} }
ntwkName, err := fapi.StateNetworkName(ctx) ntwkName, err := FullAPI.StateNetworkName(ctx)
if err != nil { if err != nil {
return err return err
} }
nv, err := fapi.StateNetworkVersion(ctx, execTs.Key()) nv, err := FullAPI.StateNetworkVersion(ctx, execTs.Key())
if err != nil { if err != nil {
return err return err
} }
@ -399,8 +397,12 @@ func doExtract(ctx context.Context, fapi api.FullNode, opts extractOpts) error {
}, },
} }
return writeVector(vector, opts.file)
}
func writeVector(vector schema.TestVector, file string) (err error) {
output := io.WriteCloser(os.Stdout) output := io.WriteCloser(os.Stdout)
if file := opts.file; file != "" { if file := file; file != "" {
dir := filepath.Dir(file) dir := filepath.Dir(file)
if err := os.MkdirAll(dir, 0755); err != nil { if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("unable to create directory %s: %w", dir, err) return fmt.Errorf("unable to create directory %s: %w", dir, err)
@ -415,11 +417,7 @@ func doExtract(ctx context.Context, fapi api.FullNode, opts extractOpts) error {
enc := json.NewEncoder(output) enc := json.NewEncoder(output)
enc.SetIndent("", " ") enc.SetIndent("", " ")
if err := enc.Encode(&vector); err != nil { return enc.Encode(&vector)
return err
}
return nil
} }
// resolveFromChain queries the chain for the provided message, using the block CID to // resolveFromChain queries the chain for the provided message, using the block CID to

View File

@ -1,7 +1,6 @@
package main package main
import ( import (
"context"
"encoding/csv" "encoding/csv"
"fmt" "fmt"
"io" "io"
@ -20,7 +19,6 @@ import (
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"github.com/filecoin-project/lotus/chain/stmgr" "github.com/filecoin-project/lotus/chain/stmgr"
lcli "github.com/filecoin-project/lotus/cli"
) )
var extractManyFlags struct { var extractManyFlags struct {
@ -45,6 +43,8 @@ var extractManyCmd = &cli.Command{
after these compulsory seven. after these compulsory seven.
`, `,
Action: runExtractMany, Action: runExtractMany,
Before: initialize,
After: destroy,
Flags: []cli.Flag{ Flags: []cli.Flag{
&repoFlag, &repoFlag,
&cli.StringFlag{ &cli.StringFlag{
@ -77,15 +77,6 @@ func runExtractMany(c *cli.Context) error {
// to the blockstore) worked. // to the blockstore) worked.
_ = os.Setenv("LOTUS_DISABLE_VM_BUF", "iknowitsabadidea") _ = os.Setenv("LOTUS_DISABLE_VM_BUF", "iknowitsabadidea")
ctx := context.Background()
// Make the API client.
fapi, closer, err := lcli.GetFullNodeAPI(c)
if err != nil {
return err
}
defer closer()
var ( var (
in = extractManyFlags.in in = extractManyFlags.in
outdir = extractManyFlags.outdir outdir = extractManyFlags.outdir
@ -198,8 +189,8 @@ func runExtractMany(c *cli.Context) error {
precursor: PrecursorSelectSender, precursor: PrecursorSelectSender,
} }
if err := doExtract(ctx, fapi, opts); err != nil { if err := doExtract(opts); err != nil {
log.Println(color.RedString("failed to extract vector for message %s: %s; queuing for 'canonical' precursor selection", mcid, err)) log.Println(color.RedString("failed to extract vector for message %s: %s; queuing for 'all' precursor selection", mcid, err))
retry = append(retry, opts) retry = append(retry, opts)
continue continue
} }
@ -215,7 +206,7 @@ func runExtractMany(c *cli.Context) error {
log.Printf("retrying %s: %s", r.cid, r.id) log.Printf("retrying %s: %s", r.cid, r.id)
r.precursor = PrecursorSelectAll r.precursor = PrecursorSelectAll
if err := doExtract(ctx, fapi, r); err != nil { if err := doExtract(r); err != nil {
merr = multierror.Append(merr, fmt.Errorf("failed to extract vector for message %s: %w", r.cid, err)) merr = multierror.Append(merr, fmt.Errorf("failed to extract vector for message %s: %w", r.cid, err))
continue continue
} }

View File

@ -1,13 +1,26 @@
package main package main
import ( import (
"fmt"
"log" "log"
"os" "os"
"sort" "sort"
"github.com/filecoin-project/go-jsonrpc"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"github.com/filecoin-project/lotus/api"
lcli "github.com/filecoin-project/lotus/cli"
) )
// FullAPI is a JSON-RPC client targeting a full node. It's initialized in a
// cli.BeforeFunc.
var FullAPI api.FullNode
// Closer is the closer for the JSON-RPC client, which must be called on
// cli.AfterFunc.
var Closer jsonrpc.ClientCloser
// DefaultLotusRepoPath is where the fallback path where to look for a Lotus // DefaultLotusRepoPath is where the fallback path where to look for a Lotus
// client repo. It is expanded with mitchellh/go-homedir, so it'll work with all // client repo. It is expanded with mitchellh/go-homedir, so it'll work with all
// OSes despite the Unix twiddle notation. // OSes despite the Unix twiddle notation.
@ -23,7 +36,7 @@ var repoFlag = cli.StringFlag{
func main() { func main() {
app := &cli.App{ app := &cli.App{
Name: "tvx", Name: "tvx",
Description: `tvx is a tool for extracting and executing test vectors. It has three subcommands. Description: `tvx is a tool for extracting and executing test vectors. It has four subcommands.
tvx extract extracts a test vector from a live network. It requires access to tvx extract extracts a test vector from a live network. It requires access to
a Filecoin client that exposes the standard JSON-RPC API endpoint. Only a Filecoin client that exposes the standard JSON-RPC API endpoint. Only
@ -35,6 +48,10 @@ func main() {
tvx extract-many performs a batch extraction of many messages, supplied in a tvx extract-many performs a batch extraction of many messages, supplied in a
CSV file. Refer to the help of that subcommand for more info. CSV file. Refer to the help of that subcommand for more info.
tvx simulate takes a raw message and simulates it on top of the supplied
epoch, reporting the result on stderr and writing a test vector on stdout
or into the specified file.
SETTING THE JSON-RPC API ENDPOINT SETTING THE JSON-RPC API ENDPOINT
You can set the JSON-RPC API endpoint through one of the following methods. You can set the JSON-RPC API endpoint through one of the following methods.
@ -57,6 +74,7 @@ func main() {
extractCmd, extractCmd,
execCmd, execCmd,
extractManyCmd, extractManyCmd,
simulateCmd,
}, },
} }
@ -69,3 +87,29 @@ func main() {
log.Fatal(err) log.Fatal(err)
} }
} }
func initialize(c *cli.Context) error {
// LOTUS_DISABLE_VM_BUF disables what's called "VM state tree buffering",
// which stashes write operations in a BufferedBlockstore
// (https://github.com/filecoin-project/lotus/blob/b7a4dbb07fd8332b4492313a617e3458f8003b2a/lib/bufbstore/buf_bstore.go#L21)
// such that they're not written until the VM is actually flushed.
//
// For some reason, the standard behaviour was not working for me (raulk),
// and disabling it (such that the state transformations are written immediately
// to the blockstore) worked.
_ = os.Setenv("LOTUS_DISABLE_VM_BUF", "iknowitsabadidea")
// Make the API client.
var err error
if FullAPI, Closer, err = lcli.GetFullNodeAPI(c); err != nil {
err = fmt.Errorf("failed to locate Lotus node; ")
}
return err
}
func destroy(_ *cli.Context) error {
if Closer != nil {
Closer()
}
return nil
}

235
cmd/tvx/simulate.go Normal file
View File

@ -0,0 +1,235 @@
package main
import (
"bytes"
"compress/gzip"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"log"
"os/exec"
"github.com/fatih/color"
"github.com/filecoin-project/go-state-types/abi"
"github.com/filecoin-project/test-vectors/schema"
"github.com/urfave/cli/v2"
"github.com/filecoin-project/lotus/api"
"github.com/filecoin-project/lotus/chain/types"
"github.com/filecoin-project/lotus/conformance"
)
var simulateFlags struct {
msg string
epoch int64
out string
statediff bool
}
var simulateCmd = &cli.Command{
Name: "simulate",
Description: "simulate a raw message on top of the supplied epoch (or HEAD), " +
"reporting the result on stderr and writing a test vector on stdout " +
"or into the specified file",
Action: runSimulateCmd,
Before: initialize,
After: destroy,
Flags: []cli.Flag{
&repoFlag,
&cli.StringFlag{
Name: "msg",
Usage: "base64 cbor-encoded message",
Destination: &simulateFlags.msg,
Required: true,
},
&cli.Int64Flag{
Name: "at-epoch",
Usage: "epoch at which to run this message (or HEAD if not provided)",
Destination: &simulateFlags.epoch,
},
&cli.StringFlag{
Name: "out",
Usage: "file to write the test vector to; if nil, the vector will be written to stdout",
TakesFile: true,
Destination: &simulateFlags.out,
},
&cli.BoolFlag{
Name: "statediff",
Usage: "display a statediff of the precondition and postcondition states",
Destination: &simulateFlags.statediff,
},
},
}
func runSimulateCmd(_ *cli.Context) error {
ctx := context.Background()
r := new(conformance.LogReporter)
msgb, err := base64.StdEncoding.DecodeString(simulateFlags.msg)
if err != nil {
return fmt.Errorf("failed to base64-decode message: %w", err)
}
msg, err := types.DecodeMessage(msgb)
if err != nil {
return fmt.Errorf("failed to deserialize message: %w", err)
}
log.Printf("message to simulate has CID: %s", msg.Cid())
msgjson, err := json.Marshal(msg)
if err != nil {
return fmt.Errorf("failed to serialize message to json for printing: %w", err)
}
log.Printf("message to simulate: %s", string(msgjson))
// Resolve the tipset, root, epoch.
var ts *types.TipSet
if epochIn := simulateFlags.epoch; epochIn == 0 {
ts, err = FullAPI.ChainHead(ctx)
} else {
ts, err = FullAPI.ChainGetTipSetByHeight(ctx, abi.ChainEpoch(epochIn), types.EmptyTSK)
}
if err != nil {
return fmt.Errorf("failed to get tipset: %w", err)
}
var (
preroot = ts.ParentState()
epoch = ts.Height()
baseFee = ts.Blocks()[0].ParentBaseFee
circSupply api.CirculatingSupply
)
// Get circulating supply.
circSupply, err = FullAPI.StateVMCirculatingSupplyInternal(ctx, ts.Key())
if err != nil {
return fmt.Errorf("failed to get circulating supply for tipset %s: %w", ts.Key(), err)
}
// Create the driver.
stores := NewProxyingStores(ctx, FullAPI)
driver := conformance.NewDriver(ctx, schema.Selector{}, conformance.DriverOpts{
DisableVMFlush: true,
})
rand := conformance.NewRecordingRand(r, FullAPI)
tbs, ok := stores.Blockstore.(TracingBlockstore)
if !ok {
return fmt.Errorf("no tracing blockstore available")
}
tbs.StartTracing()
applyret, postroot, err := driver.ExecuteMessage(stores.Blockstore, conformance.ExecuteMessageParams{
Preroot: preroot,
Epoch: epoch,
Message: msg,
CircSupply: circSupply.FilCirculating,
BaseFee: baseFee,
Rand: rand,
})
if err != nil {
return fmt.Errorf("failed to apply message: %w", err)
}
accessed := tbs.FinishTracing()
var (
out = new(bytes.Buffer)
gw = gzip.NewWriter(out)
g = NewSurgeon(ctx, FullAPI, stores)
)
if err := g.WriteCARIncluding(gw, accessed, preroot, postroot); err != nil {
return err
}
if err = gw.Flush(); err != nil {
return err
}
if err = gw.Close(); err != nil {
return err
}
version, err := FullAPI.Version(ctx)
if err != nil {
log.Printf("failed to get node version: %s; falling back to unknown", err)
version = api.Version{}
}
nv, err := FullAPI.StateNetworkVersion(ctx, ts.Key())
if err != nil {
return err
}
codename := GetProtocolCodename(epoch)
// Write out the test vector.
vector := schema.TestVector{
Class: schema.ClassMessage,
Meta: &schema.Metadata{
ID: fmt.Sprintf("simulated-%s", msg.Cid()),
Gen: []schema.GenerationData{
{Source: "github.com/filecoin-project/lotus", Version: version.String()}},
},
Selector: schema.Selector{
schema.SelectorMinProtocolVersion: codename,
},
Randomness: rand.Recorded(),
CAR: out.Bytes(),
Pre: &schema.Preconditions{
Variants: []schema.Variant{
{ID: codename, Epoch: int64(epoch), NetworkVersion: uint(nv)},
},
CircSupply: circSupply.FilCirculating.Int,
BaseFee: baseFee.Int,
StateTree: &schema.StateTree{
RootCID: preroot,
},
},
ApplyMessages: []schema.Message{{Bytes: msgb}},
Post: &schema.Postconditions{
StateTree: &schema.StateTree{
RootCID: postroot,
},
Receipts: []*schema.Receipt{
{
ExitCode: int64(applyret.ExitCode),
ReturnValue: applyret.Return,
GasUsed: applyret.GasUsed,
},
},
},
}
if err := writeVector(vector, simulateFlags.out); err != nil {
return fmt.Errorf("failed to write vector: %w", err)
}
log.Printf(color.GreenString("wrote vector at: %s"), simulateFlags.out)
if !simulateFlags.statediff {
return nil
}
if simulateFlags.out == "" {
log.Print("omitting statediff in non-file mode")
return nil
}
// check if statediff is installed; if not, skip.
if err := exec.Command("statediff", "--help").Run(); err != nil {
log.Printf("could not perform statediff on generated vector; command not found (%s)", err)
log.Printf("install statediff with:")
log.Printf("$ GOMODULE111=off go get github.com/filecoin-project/statediff/cmd/statediff")
return err
}
stdiff, err := exec.Command("statediff", "vector", "--file", simulateFlags.out).CombinedOutput()
if err != nil {
return fmt.Errorf("failed to statediff: %w", err)
}
log.Print(string(stdiff))
return nil
}