diff --git a/cmd/tvx/exec.go b/cmd/tvx/exec.go index 89ad23913..f1ef91315 100644 --- a/cmd/tvx/exec.go +++ b/cmd/tvx/exec.go @@ -16,7 +16,8 @@ import ( ) var execFlags struct { - file string + file string + fallbackBlockstore bool } var execCmd = &cli.Command{ @@ -30,10 +31,23 @@ var execCmd = &cli.Command{ TakesFile: true, Destination: &execFlags.file, }, + &cli.BoolFlag{ + Name: "fallback-blockstore", + Usage: "sets the full node API as a fallback blockstore; use this if you're transplanting vectors and get block not found errors", + Destination: &execFlags.fallbackBlockstore, + }, }, } -func runExecLotus(_ *cli.Context) error { +func runExecLotus(c *cli.Context) error { + if execFlags.fallbackBlockstore { + if err := initialize(c); err != nil { + return fmt.Errorf("fallback blockstore was enabled, but could not resolve lotus API endpoint: %w", err) + } + defer destroy(c) //nolint:errcheck + conformance.FallbackBlockstoreGetter = FullAPI + } + if file := execFlags.file; file != "" { // we have a single test vector supplied as a file. file, err := os.Open(file) diff --git a/cmd/tvx/extract.go b/cmd/tvx/extract.go index 894fa0fbc..61808882c 100644 --- a/cmd/tvx/extract.go +++ b/cmd/tvx/extract.go @@ -1,30 +1,8 @@ package main import ( - "bytes" - "compress/gzip" - "context" - "encoding/json" "fmt" - "io" - "log" - "os" - "path/filepath" - "github.com/fatih/color" - "github.com/filecoin-project/go-address" - - "github.com/filecoin-project/lotus/api" - "github.com/filecoin-project/lotus/chain/actors/builtin" - init_ "github.com/filecoin-project/lotus/chain/actors/builtin/init" - "github.com/filecoin-project/lotus/chain/actors/builtin/reward" - "github.com/filecoin-project/lotus/chain/types" - "github.com/filecoin-project/lotus/chain/vm" - "github.com/filecoin-project/lotus/conformance" - - "github.com/filecoin-project/test-vectors/schema" - - "github.com/ipfs/go-cid" "github.com/urfave/cli/v2" ) @@ -38,6 +16,7 @@ type extractOpts struct { block string class string cid string + tsk string file string retain string precursor string @@ -56,7 +35,7 @@ var extractCmd = &cli.Command{ &repoFlag, &cli.StringFlag{ Name: "class", - Usage: "class of vector to extract; other required flags depend on the; values: 'message'", + Usage: "class of vector to extract; values: 'message', 'tipset'", Value: "message", Destination: &extractFlags.class, }, @@ -79,9 +58,13 @@ var extractCmd = &cli.Command{ &cli.StringFlag{ Name: "cid", Usage: "message CID to generate test vector from", - Required: true, Destination: &extractFlags.cid, }, + &cli.StringFlag{ + Name: "tsk", + Usage: "tipset key to extract into a vector", + Destination: &extractFlags.tsk, + }, &cli.StringFlag{ Name: "out", Aliases: []string{"o"}, @@ -114,413 +97,12 @@ var extractCmd = &cli.Command{ } func runExtract(_ *cli.Context) error { - return doExtract(extractFlags) -} - -func doExtract(opts extractOpts) error { - ctx := context.Background() - - mcid, err := cid.Decode(opts.cid) - if err != nil { - return err - } - - msg, execTs, incTs, err := resolveFromChain(ctx, FullAPI, mcid, opts.block) - if err != nil { - return fmt.Errorf("failed to resolve message and tipsets from chain: %w", err) - } - - // get the circulating supply before the message was executed. - circSupplyDetail, err := FullAPI.StateVMCirculatingSupplyInternal(ctx, incTs.Key()) - if err != nil { - return fmt.Errorf("failed while fetching circulating supply: %w", err) - } - - circSupply := circSupplyDetail.FilCirculating - - log.Printf("message was executed in tipset: %s", execTs.Key()) - log.Printf("message was included in tipset: %s", incTs.Key()) - log.Printf("circulating supply at inclusion tipset: %d", circSupply) - log.Printf("finding precursor messages using mode: %s", opts.precursor) - - // Fetch messages in canonical order from inclusion tipset. - msgs, err := FullAPI.ChainGetParentMessages(ctx, execTs.Blocks()[0].Cid()) - if err != nil { - return fmt.Errorf("failed to fetch messages in canonical order from inclusion tipset: %w", err) - } - - related, found, err := findMsgAndPrecursors(opts.precursor, mcid, msg.From, msgs) - if err != nil { - return fmt.Errorf("failed while finding message and precursors: %w", err) - } - - if !found { - return fmt.Errorf("message not found; precursors found: %d", len(related)) - } - - var ( - precursors = related[:len(related)-1] - precursorsCids []cid.Cid - ) - - for _, p := range precursors { - precursorsCids = append(precursorsCids, p.Cid()) - } - - log.Println(color.GreenString("found message; precursors (count: %d): %v", len(precursors), precursorsCids)) - - var ( - // create a read-through store that uses ChainGetObject to fetch unknown CIDs. - pst = NewProxyingStores(ctx, FullAPI) - g = NewSurgeon(ctx, FullAPI, pst) - ) - - driver := conformance.NewDriver(ctx, schema.Selector{}, conformance.DriverOpts{ - DisableVMFlush: true, - }) - - // this is the root of the state tree we start with. - root := incTs.ParentState() - log.Printf("base state tree root CID: %s", root) - - basefee := incTs.Blocks()[0].ParentBaseFee - log.Printf("basefee: %s", basefee) - - // on top of that state tree, we apply all precursors. - log.Printf("number of precursors to apply: %d", len(precursors)) - for i, m := range precursors { - log.Printf("applying precursor %d, cid: %s", i, m.Cid()) - _, root, err = driver.ExecuteMessage(pst.Blockstore, conformance.ExecuteMessageParams{ - Preroot: root, - Epoch: execTs.Height(), - Message: m, - CircSupply: circSupplyDetail.FilCirculating, - BaseFee: basefee, - // recorded randomness will be discarded. - Rand: conformance.NewRecordingRand(new(conformance.LogReporter), FullAPI), - }) - if err != nil { - return fmt.Errorf("failed to execute precursor message: %w", err) - } - } - - var ( - preroot cid.Cid - postroot cid.Cid - applyret *vm.ApplyRet - carWriter func(w io.Writer) error - retention = opts.retain - - // recordingRand will record randomness so we can embed it in the test vector. - recordingRand = conformance.NewRecordingRand(new(conformance.LogReporter), FullAPI) - ) - - log.Printf("using state retention strategy: %s", retention) - switch retention { - case "accessed-cids": - tbs, ok := pst.Blockstore.(TracingBlockstore) - if !ok { - return fmt.Errorf("requested 'accessed-cids' state retention, but no tracing blockstore was present") - } - - tbs.StartTracing() - - preroot = root - applyret, postroot, err = driver.ExecuteMessage(pst.Blockstore, conformance.ExecuteMessageParams{ - Preroot: preroot, - Epoch: execTs.Height(), - Message: msg, - CircSupply: circSupplyDetail.FilCirculating, - BaseFee: basefee, - Rand: recordingRand, - }) - if err != nil { - return fmt.Errorf("failed to execute message: %w", err) - } - accessed := tbs.FinishTracing() - carWriter = func(w io.Writer) error { - return g.WriteCARIncluding(w, accessed, preroot, postroot) - } - - case "accessed-actors": - log.Printf("calculating accessed actors") - // get actors accessed by message. - retain, err := g.GetAccessedActors(ctx, FullAPI, mcid) - if err != nil { - return fmt.Errorf("failed to calculate accessed actors: %w", err) - } - // also append the reward actor and the burnt funds actor. - retain = append(retain, reward.Address, builtin.BurntFundsActorAddr, init_.Address) - log.Printf("calculated accessed actors: %v", retain) - - // get the masked state tree from the root, - preroot, err = g.GetMaskedStateTree(root, retain) - if err != nil { - return err - } - applyret, postroot, err = driver.ExecuteMessage(pst.Blockstore, conformance.ExecuteMessageParams{ - Preroot: preroot, - Epoch: execTs.Height(), - Message: msg, - CircSupply: circSupplyDetail.FilCirculating, - BaseFee: basefee, - Rand: recordingRand, - }) - if err != nil { - return fmt.Errorf("failed to execute message: %w", err) - } - carWriter = func(w io.Writer) error { - return g.WriteCAR(w, preroot, postroot) - } - + switch extractFlags.class { + case "message": + return doExtractMessage(extractFlags) + case "tipset": + return doExtractTipset(extractFlags) default: - return fmt.Errorf("unknown state retention option: %s", retention) + return fmt.Errorf("unsupported vector class") } - - log.Printf("message applied; preroot: %s, postroot: %s", preroot, postroot) - log.Println("performing sanity check on receipt") - - // TODO sometimes this returns a nil receipt and no error ¯\_(ツ)_/¯ - // ex: https://filfox.info/en/message/bafy2bzacebpxw3yiaxzy2bako62akig46x3imji7fewszen6fryiz6nymu2b2 - // This code is lenient and skips receipt comparison in case of a nil receipt. - rec, err := FullAPI.StateGetReceipt(ctx, mcid, execTs.Key()) - if err != nil { - return fmt.Errorf("failed to find receipt on chain: %w", err) - } - log.Printf("found receipt: %+v", rec) - - // generate the schema receipt; if we got - var receipt *schema.Receipt - if rec != nil { - receipt = &schema.Receipt{ - ExitCode: int64(rec.ExitCode), - ReturnValue: rec.Return, - GasUsed: rec.GasUsed, - } - - reporter := new(conformance.LogReporter) - conformance.AssertMsgResult(reporter, receipt, applyret, "as locally executed") - if reporter.Failed() { - if opts.ignoreSanityChecks { - 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")) - } - - } else { - receipt = &schema.Receipt{ - ExitCode: int64(applyret.ExitCode), - ReturnValue: applyret.Return, - GasUsed: applyret.GasUsed, - } - log.Println(color.YellowString("skipping receipts comparison; we got back a nil receipt from lotus")) - } - - log.Println("generating vector") - msgBytes, err := msg.Serialize() - if err != nil { - return err - } - - var ( - out = new(bytes.Buffer) - gw = gzip.NewWriter(out) - ) - if err := carWriter(gw); 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 { - return err - } - - ntwkName, err := FullAPI.StateNetworkName(ctx) - if err != nil { - return err - } - - nv, err := FullAPI.StateNetworkVersion(ctx, execTs.Key()) - if err != nil { - return err - } - - codename := GetProtocolCodename(execTs.Height()) - - // Write out the test vector. - vector := schema.TestVector{ - Class: schema.ClassMessage, - Meta: &schema.Metadata{ - ID: opts.id, - // TODO need to replace schema.GenerationData with a more flexible - // data structure that makes no assumption about the traceability - // data that's being recorded; a flexible map[string]string - // would do. - Gen: []schema.GenerationData{ - {Source: fmt.Sprintf("network:%s", ntwkName)}, - {Source: fmt.Sprintf("message:%s", msg.Cid().String())}, - {Source: fmt.Sprintf("inclusion_tipset:%s", incTs.Key().String())}, - {Source: fmt.Sprintf("execution_tipset:%s", execTs.Key().String())}, - {Source: "github.com/filecoin-project/lotus", Version: version.String()}}, - }, - Selector: schema.Selector{ - schema.SelectorMinProtocolVersion: codename, - }, - Randomness: recordingRand.Recorded(), - CAR: out.Bytes(), - Pre: &schema.Preconditions{ - Variants: []schema.Variant{ - {ID: codename, Epoch: int64(execTs.Height()), NetworkVersion: uint(nv)}, - }, - CircSupply: circSupply.Int, - BaseFee: basefee.Int, - StateTree: &schema.StateTree{ - RootCID: preroot, - }, - }, - ApplyMessages: []schema.Message{{Bytes: msgBytes}}, - Post: &schema.Postconditions{ - StateTree: &schema.StateTree{ - RootCID: postroot, - }, - Receipts: []*schema.Receipt{ - { - ExitCode: int64(applyret.ExitCode), - ReturnValue: applyret.Return, - GasUsed: applyret.GasUsed, - }, - }, - }, - } - - return writeVector(vector, opts.file) -} - -func writeVector(vector schema.TestVector, file string) (err error) { - output := io.WriteCloser(os.Stdout) - if file := file; file != "" { - dir := filepath.Dir(file) - if err := os.MkdirAll(dir, 0755); err != nil { - return fmt.Errorf("unable to create directory %s: %w", dir, err) - } - output, err = os.Create(file) - if err != nil { - return err - } - defer output.Close() //nolint:errcheck - defer log.Printf("wrote test vector to file: %s", file) - } - - enc := json.NewEncoder(output) - enc.SetIndent("", " ") - return enc.Encode(&vector) -} - -// resolveFromChain queries the chain for the provided message, using the block CID to -// speed up the query, if provided -func resolveFromChain(ctx context.Context, api api.FullNode, mcid cid.Cid, block string) (msg *types.Message, execTs *types.TipSet, incTs *types.TipSet, err error) { - // Extract the full message. - msg, err = api.ChainGetMessage(ctx, mcid) - if err != nil { - return nil, nil, nil, err - } - - log.Printf("found message with CID %s: %+v", mcid, msg) - - if block == "" { - log.Printf("locating message in blockchain") - - // Locate the message. - msgInfo, err := api.StateSearchMsg(ctx, mcid) - if err != nil { - return nil, nil, nil, fmt.Errorf("failed to locate message: %w", err) - } - - log.Printf("located message at tipset %s (height: %d) with exit code: %s", msgInfo.TipSet, msgInfo.Height, msgInfo.Receipt.ExitCode) - - execTs, incTs, err = fetchThisAndPrevTipset(ctx, api, msgInfo.TipSet) - return msg, execTs, incTs, err - } - - bcid, err := cid.Decode(block) - if err != nil { - return nil, nil, nil, err - } - - log.Printf("message inclusion block CID was provided; scanning around it: %s", bcid) - - blk, err := api.ChainGetBlock(ctx, bcid) - if err != nil { - return nil, nil, nil, fmt.Errorf("failed to get block: %w", err) - } - - // types.EmptyTSK hints to use the HEAD. - execTs, err = api.ChainGetTipSetByHeight(ctx, blk.Height+1, types.EmptyTSK) - if err != nil { - return nil, nil, nil, fmt.Errorf("failed to get message execution tipset: %w", err) - } - - // walk back from the execTs instead of HEAD, to save time. - incTs, err = api.ChainGetTipSetByHeight(ctx, blk.Height, execTs.Key()) - if err != nil { - return nil, nil, nil, fmt.Errorf("failed to get message inclusion tipset: %w", err) - } - - return msg, execTs, incTs, nil -} - -// fetchThisAndPrevTipset returns the full tipset identified by the key, as well -// as the previous tipset. In the context of vector generation, the target -// tipset is the one where a message was executed, and the previous tipset is -// the one where the message was included. -func fetchThisAndPrevTipset(ctx context.Context, api api.FullNode, target types.TipSetKey) (targetTs *types.TipSet, prevTs *types.TipSet, err error) { - // get the tipset on which this message was "executed" on. - // https://github.com/filecoin-project/lotus/issues/2847 - targetTs, err = api.ChainGetTipSet(ctx, target) - if err != nil { - return nil, nil, err - } - // get the previous tipset, on which this message was mined, - // i.e. included on-chain. - prevTs, err = api.ChainGetTipSet(ctx, targetTs.Parents()) - if err != nil { - return nil, nil, err - } - return targetTs, prevTs, nil -} - -// findMsgAndPrecursors ranges through the canonical messages slice, locating -// the target message and returning precursors in accordance to the supplied -// mode. -func findMsgAndPrecursors(mode string, msgCid cid.Cid, sender address.Address, msgs []api.Message) (related []*types.Message, found bool, err error) { - // Range through canonicalised messages, selecting only the precursors based - // on selection mode. - for _, other := range msgs { - switch { - case mode == PrecursorSelectAll: - fallthrough - case mode == PrecursorSelectSender && other.Message.From == sender: - related = append(related, other.Message) - } - - // this message is the target; we're done. - if other.Cid == msgCid { - return related, true, nil - } - } - - // this could happen because a block contained related messages, but not - // the target (that is, messages with a lower nonce, but ultimately not the - // target). - return related, false, nil } diff --git a/cmd/tvx/extract_many.go b/cmd/tvx/extract_many.go index 048271456..081678a17 100644 --- a/cmd/tvx/extract_many.go +++ b/cmd/tvx/extract_many.go @@ -189,7 +189,7 @@ func runExtractMany(c *cli.Context) error { precursor: PrecursorSelectSender, } - if err := doExtract(opts); err != nil { + if err := doExtractMessage(opts); err != nil { log.Println(color.RedString("failed to extract vector for message %s: %s; queuing for 'all' precursor selection", mcid, err)) retry = append(retry, opts) continue @@ -206,7 +206,7 @@ func runExtractMany(c *cli.Context) error { log.Printf("retrying %s: %s", r.cid, r.id) r.precursor = PrecursorSelectAll - if err := doExtract(r); err != nil { + if err := doExtractMessage(r); err != nil { merr = multierror.Append(merr, fmt.Errorf("failed to extract vector for message %s: %w", r.cid, err)) continue } diff --git a/cmd/tvx/extract_message.go b/cmd/tvx/extract_message.go new file mode 100644 index 000000000..286910c38 --- /dev/null +++ b/cmd/tvx/extract_message.go @@ -0,0 +1,440 @@ +package main + +import ( + "bytes" + "compress/gzip" + "context" + "encoding/json" + "fmt" + "io" + "log" + "os" + "path/filepath" + + "github.com/fatih/color" + "github.com/filecoin-project/go-address" + + "github.com/filecoin-project/lotus/api" + "github.com/filecoin-project/lotus/chain/actors/builtin" + init_ "github.com/filecoin-project/lotus/chain/actors/builtin/init" + "github.com/filecoin-project/lotus/chain/actors/builtin/reward" + "github.com/filecoin-project/lotus/chain/types" + "github.com/filecoin-project/lotus/chain/vm" + "github.com/filecoin-project/lotus/conformance" + + "github.com/filecoin-project/test-vectors/schema" + + "github.com/ipfs/go-cid" +) + +func doExtractMessage(opts extractOpts) error { + ctx := context.Background() + + if opts.cid == "" { + return fmt.Errorf("missing message CID") + } + + mcid, err := cid.Decode(opts.cid) + if err != nil { + return err + } + + msg, execTs, incTs, err := resolveFromChain(ctx, FullAPI, mcid, opts.block) + if err != nil { + return fmt.Errorf("failed to resolve message and tipsets from chain: %w", err) + } + + // get the circulating supply before the message was executed. + circSupplyDetail, err := FullAPI.StateVMCirculatingSupplyInternal(ctx, incTs.Key()) + if err != nil { + return fmt.Errorf("failed while fetching circulating supply: %w", err) + } + + circSupply := circSupplyDetail.FilCirculating + + log.Printf("message was executed in tipset: %s", execTs.Key()) + log.Printf("message was included in tipset: %s", incTs.Key()) + log.Printf("circulating supply at inclusion tipset: %d", circSupply) + log.Printf("finding precursor messages using mode: %s", opts.precursor) + + // Fetch messages in canonical order from inclusion tipset. + msgs, err := FullAPI.ChainGetParentMessages(ctx, execTs.Blocks()[0].Cid()) + if err != nil { + return fmt.Errorf("failed to fetch messages in canonical order from inclusion tipset: %w", err) + } + + related, found, err := findMsgAndPrecursors(opts.precursor, mcid, msg.From, msgs) + if err != nil { + return fmt.Errorf("failed while finding message and precursors: %w", err) + } + + if !found { + return fmt.Errorf("message not found; precursors found: %d", len(related)) + } + + var ( + precursors = related[:len(related)-1] + precursorsCids []cid.Cid + ) + + for _, p := range precursors { + precursorsCids = append(precursorsCids, p.Cid()) + } + + log.Println(color.GreenString("found message; precursors (count: %d): %v", len(precursors), precursorsCids)) + + var ( + // create a read-through store that uses ChainGetObject to fetch unknown CIDs. + pst = NewProxyingStores(ctx, FullAPI) + g = NewSurgeon(ctx, FullAPI, pst) + ) + + driver := conformance.NewDriver(ctx, schema.Selector{}, conformance.DriverOpts{ + DisableVMFlush: true, + }) + + // this is the root of the state tree we start with. + root := incTs.ParentState() + log.Printf("base state tree root CID: %s", root) + + basefee := incTs.Blocks()[0].ParentBaseFee + log.Printf("basefee: %s", basefee) + + // on top of that state tree, we apply all precursors. + log.Printf("number of precursors to apply: %d", len(precursors)) + for i, m := range precursors { + log.Printf("applying precursor %d, cid: %s", i, m.Cid()) + _, root, err = driver.ExecuteMessage(pst.Blockstore, conformance.ExecuteMessageParams{ + Preroot: root, + Epoch: execTs.Height(), + Message: m, + CircSupply: circSupplyDetail.FilCirculating, + BaseFee: basefee, + // recorded randomness will be discarded. + Rand: conformance.NewRecordingRand(new(conformance.LogReporter), FullAPI), + }) + if err != nil { + return fmt.Errorf("failed to execute precursor message: %w", err) + } + } + + var ( + preroot cid.Cid + postroot cid.Cid + applyret *vm.ApplyRet + carWriter func(w io.Writer) error + retention = opts.retain + + // recordingRand will record randomness so we can embed it in the test vector. + recordingRand = conformance.NewRecordingRand(new(conformance.LogReporter), FullAPI) + ) + + log.Printf("using state retention strategy: %s", retention) + switch retention { + case "accessed-cids": + tbs, ok := pst.Blockstore.(TracingBlockstore) + if !ok { + return fmt.Errorf("requested 'accessed-cids' state retention, but no tracing blockstore was present") + } + + tbs.StartTracing() + + preroot = root + applyret, postroot, err = driver.ExecuteMessage(pst.Blockstore, conformance.ExecuteMessageParams{ + Preroot: preroot, + Epoch: execTs.Height(), + Message: msg, + CircSupply: circSupplyDetail.FilCirculating, + BaseFee: basefee, + Rand: recordingRand, + }) + if err != nil { + return fmt.Errorf("failed to execute message: %w", err) + } + accessed := tbs.FinishTracing() + carWriter = func(w io.Writer) error { + return g.WriteCARIncluding(w, accessed, preroot, postroot) + } + + case "accessed-actors": + log.Printf("calculating accessed actors") + // get actors accessed by message. + retain, err := g.GetAccessedActors(ctx, FullAPI, mcid) + if err != nil { + return fmt.Errorf("failed to calculate accessed actors: %w", err) + } + // also append the reward actor and the burnt funds actor. + retain = append(retain, reward.Address, builtin.BurntFundsActorAddr, init_.Address) + log.Printf("calculated accessed actors: %v", retain) + + // get the masked state tree from the root, + preroot, err = g.GetMaskedStateTree(root, retain) + if err != nil { + return err + } + applyret, postroot, err = driver.ExecuteMessage(pst.Blockstore, conformance.ExecuteMessageParams{ + Preroot: preroot, + Epoch: execTs.Height(), + Message: msg, + CircSupply: circSupplyDetail.FilCirculating, + BaseFee: basefee, + Rand: recordingRand, + }) + if err != nil { + return fmt.Errorf("failed to execute message: %w", err) + } + carWriter = func(w io.Writer) error { + return g.WriteCAR(w, preroot, postroot) + } + + default: + return fmt.Errorf("unknown state retention option: %s", retention) + } + + log.Printf("message applied; preroot: %s, postroot: %s", preroot, postroot) + log.Println("performing sanity check on receipt") + + // TODO sometimes this returns a nil receipt and no error ¯\_(ツ)_/¯ + // ex: https://filfox.info/en/message/bafy2bzacebpxw3yiaxzy2bako62akig46x3imji7fewszen6fryiz6nymu2b2 + // This code is lenient and skips receipt comparison in case of a nil receipt. + rec, err := FullAPI.StateGetReceipt(ctx, mcid, execTs.Key()) + if err != nil { + return fmt.Errorf("failed to find receipt on chain: %w", err) + } + log.Printf("found receipt: %+v", rec) + + // generate the schema receipt; if we got + var receipt *schema.Receipt + if rec != nil { + receipt = &schema.Receipt{ + ExitCode: int64(rec.ExitCode), + ReturnValue: rec.Return, + GasUsed: rec.GasUsed, + } + + reporter := new(conformance.LogReporter) + conformance.AssertMsgResult(reporter, receipt, applyret, "as locally executed") + if reporter.Failed() { + if opts.ignoreSanityChecks { + 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")) + } + + } else { + receipt = &schema.Receipt{ + ExitCode: int64(applyret.ExitCode), + ReturnValue: applyret.Return, + GasUsed: applyret.GasUsed, + } + log.Println(color.YellowString("skipping receipts comparison; we got back a nil receipt from lotus")) + } + + log.Println("generating vector") + msgBytes, err := msg.Serialize() + if err != nil { + return err + } + + var ( + out = new(bytes.Buffer) + gw = gzip.NewWriter(out) + ) + if err := carWriter(gw); 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 { + return err + } + + ntwkName, err := FullAPI.StateNetworkName(ctx) + if err != nil { + return err + } + + nv, err := FullAPI.StateNetworkVersion(ctx, execTs.Key()) + if err != nil { + return err + } + + codename := GetProtocolCodename(execTs.Height()) + + // Write out the test vector. + vector := schema.TestVector{ + Class: schema.ClassMessage, + Meta: &schema.Metadata{ + ID: opts.id, + // TODO need to replace schema.GenerationData with a more flexible + // data structure that makes no assumption about the traceability + // data that's being recorded; a flexible map[string]string + // would do. + Gen: []schema.GenerationData{ + {Source: fmt.Sprintf("network:%s", ntwkName)}, + {Source: fmt.Sprintf("message:%s", msg.Cid().String())}, + {Source: fmt.Sprintf("inclusion_tipset:%s", incTs.Key().String())}, + {Source: fmt.Sprintf("execution_tipset:%s", execTs.Key().String())}, + {Source: "github.com/filecoin-project/lotus", Version: version.String()}}, + }, + Selector: schema.Selector{ + schema.SelectorMinProtocolVersion: codename, + }, + Randomness: recordingRand.Recorded(), + CAR: out.Bytes(), + Pre: &schema.Preconditions{ + Variants: []schema.Variant{ + {ID: codename, Epoch: int64(execTs.Height()), NetworkVersion: uint(nv)}, + }, + CircSupply: circSupply.Int, + BaseFee: basefee.Int, + StateTree: &schema.StateTree{ + RootCID: preroot, + }, + }, + ApplyMessages: []schema.Message{{Bytes: msgBytes}}, + Post: &schema.Postconditions{ + StateTree: &schema.StateTree{ + RootCID: postroot, + }, + Receipts: []*schema.Receipt{ + { + ExitCode: int64(applyret.ExitCode), + ReturnValue: applyret.Return, + GasUsed: applyret.GasUsed, + }, + }, + }, + } + + return writeVector(vector, opts.file) +} + +func writeVector(vector schema.TestVector, file string) (err error) { + output := io.WriteCloser(os.Stdout) + if file := file; file != "" { + dir := filepath.Dir(file) + if err := os.MkdirAll(dir, 0755); err != nil { + return fmt.Errorf("unable to create directory %s: %w", dir, err) + } + output, err = os.Create(file) + if err != nil { + return err + } + defer output.Close() //nolint:errcheck + defer log.Printf("wrote test vector to file: %s", file) + } + + enc := json.NewEncoder(output) + enc.SetIndent("", " ") + return enc.Encode(&vector) +} + +// resolveFromChain queries the chain for the provided message, using the block CID to +// speed up the query, if provided +func resolveFromChain(ctx context.Context, api api.FullNode, mcid cid.Cid, block string) (msg *types.Message, execTs *types.TipSet, incTs *types.TipSet, err error) { + // Extract the full message. + msg, err = api.ChainGetMessage(ctx, mcid) + if err != nil { + return nil, nil, nil, err + } + + log.Printf("found message with CID %s: %+v", mcid, msg) + + if block == "" { + log.Printf("locating message in blockchain") + + // Locate the message. + msgInfo, err := api.StateSearchMsg(ctx, mcid) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to locate message: %w", err) + } + + log.Printf("located message at tipset %s (height: %d) with exit code: %s", msgInfo.TipSet, msgInfo.Height, msgInfo.Receipt.ExitCode) + + execTs, incTs, err = fetchThisAndPrevTipset(ctx, api, msgInfo.TipSet) + return msg, execTs, incTs, err + } + + bcid, err := cid.Decode(block) + if err != nil { + return nil, nil, nil, err + } + + log.Printf("message inclusion block CID was provided; scanning around it: %s", bcid) + + blk, err := api.ChainGetBlock(ctx, bcid) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to get block: %w", err) + } + + // types.EmptyTSK hints to use the HEAD. + execTs, err = api.ChainGetTipSetByHeight(ctx, blk.Height+1, types.EmptyTSK) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to get message execution tipset: %w", err) + } + + // walk back from the execTs instead of HEAD, to save time. + incTs, err = api.ChainGetTipSetByHeight(ctx, blk.Height, execTs.Key()) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to get message inclusion tipset: %w", err) + } + + return msg, execTs, incTs, nil +} + +// fetchThisAndPrevTipset returns the full tipset identified by the key, as well +// as the previous tipset. In the context of vector generation, the target +// tipset is the one where a message was executed, and the previous tipset is +// the one where the message was included. +func fetchThisAndPrevTipset(ctx context.Context, api api.FullNode, target types.TipSetKey) (targetTs *types.TipSet, prevTs *types.TipSet, err error) { + // get the tipset on which this message was "executed" on. + // https://github.com/filecoin-project/lotus/issues/2847 + targetTs, err = api.ChainGetTipSet(ctx, target) + if err != nil { + return nil, nil, err + } + // get the previous tipset, on which this message was mined, + // i.e. included on-chain. + prevTs, err = api.ChainGetTipSet(ctx, targetTs.Parents()) + if err != nil { + return nil, nil, err + } + return targetTs, prevTs, nil +} + +// findMsgAndPrecursors ranges through the canonical messages slice, locating +// the target message and returning precursors in accordance to the supplied +// mode. +func findMsgAndPrecursors(mode string, msgCid cid.Cid, sender address.Address, msgs []api.Message) (related []*types.Message, found bool, err error) { + // Range through canonicalised messages, selecting only the precursors based + // on selection mode. + for _, other := range msgs { + switch { + case mode == PrecursorSelectAll: + fallthrough + case mode == PrecursorSelectSender && other.Message.From == sender: + related = append(related, other.Message) + } + + // this message is the target; we're done. + if other.Cid == msgCid { + return related, true, nil + } + } + + // this could happen because a block contained related messages, but not + // the target (that is, messages with a lower nonce, but ultimately not the + // target). + return related, false, nil +} diff --git a/cmd/tvx/extract_tipset.go b/cmd/tvx/extract_tipset.go new file mode 100644 index 000000000..20c35f8ac --- /dev/null +++ b/cmd/tvx/extract_tipset.go @@ -0,0 +1,186 @@ +package main + +import ( + "bytes" + "compress/gzip" + "context" + "fmt" + "log" + + "github.com/filecoin-project/test-vectors/schema" + "github.com/ipfs/go-cid" + + lcli "github.com/filecoin-project/lotus/cli" + "github.com/filecoin-project/lotus/conformance" +) + +func doExtractTipset(opts extractOpts) error { + ctx := context.Background() + + if opts.tsk == "" { + return fmt.Errorf("tipset key cannot be empty") + } + + if opts.retain != "accessed-cids" { + return fmt.Errorf("tipset extraction only supports 'accessed-cids' state retention") + } + + ts, err := lcli.ParseTipSetRef(ctx, FullAPI, opts.tsk) + if err != nil { + return fmt.Errorf("failed to fetch tipset: %w", err) + } + + log.Printf("tipset block count: %d", len(ts.Blocks())) + + var blocks []schema.Block + for _, b := range ts.Blocks() { + msgs, err := FullAPI.ChainGetBlockMessages(ctx, b.Cid()) + if err != nil { + return fmt.Errorf("failed to get block messages (cid: %s): %w", b.Cid(), err) + } + + log.Printf("block %s has %d messages", b.Cid(), len(msgs.Cids)) + + packed := make([]schema.Base64EncodedBytes, 0, len(msgs.Cids)) + for _, m := range msgs.BlsMessages { + b, err := m.Serialize() + if err != nil { + return fmt.Errorf("failed to serialize message: %w", err) + } + packed = append(packed, b) + } + for _, m := range msgs.SecpkMessages { + b, err := m.Message.Serialize() + if err != nil { + return fmt.Errorf("failed to serialize message: %w", err) + } + packed = append(packed, b) + } + blocks = append(blocks, schema.Block{ + MinerAddr: b.Miner, + WinCount: b.ElectionProof.WinCount, + Messages: packed, + }) + } + + var ( + // create a read-through store that uses ChainGetObject to fetch unknown CIDs. + pst = NewProxyingStores(ctx, FullAPI) + g = NewSurgeon(ctx, FullAPI, pst) + ) + + driver := conformance.NewDriver(ctx, schema.Selector{}, conformance.DriverOpts{ + DisableVMFlush: true, + }) + + // this is the root of the state tree we start with. + root := ts.ParentState() + log.Printf("base state tree root CID: %s", root) + + basefee := ts.Blocks()[0].ParentBaseFee + log.Printf("basefee: %s", basefee) + + tipset := schema.Tipset{ + BaseFee: *basefee.Int, + Blocks: blocks, + } + + // recordingRand will record randomness so we can embed it in the test vector. + recordingRand := conformance.NewRecordingRand(new(conformance.LogReporter), FullAPI) + + log.Printf("using state retention strategy: %s", extractFlags.retain) + + tbs, ok := pst.Blockstore.(TracingBlockstore) + if !ok { + return fmt.Errorf("requested 'accessed-cids' state retention, but no tracing blockstore was present") + } + + tbs.StartTracing() + + params := conformance.ExecuteTipsetParams{ + Preroot: ts.ParentState(), + ParentEpoch: ts.Height() - 1, + Tipset: &tipset, + ExecEpoch: ts.Height(), + Rand: recordingRand, + } + result, err := driver.ExecuteTipset(pst.Blockstore, pst.Datastore, params) + if err != nil { + return fmt.Errorf("failed to execute tipset: %w", err) + } + + accessed := tbs.FinishTracing() + + // write a CAR with the accessed state into a buffer. + var ( + out = new(bytes.Buffer) + gw = gzip.NewWriter(out) + ) + if err := g.WriteCARIncluding(gw, accessed, ts.ParentState(), result.PostStateRoot); err != nil { + return err + } + if err = gw.Flush(); err != nil { + return err + } + if err = gw.Close(); err != nil { + return err + } + + codename := GetProtocolCodename(ts.Height()) + nv, err := FullAPI.StateNetworkVersion(ctx, ts.Key()) + if err != nil { + return err + } + + version, err := FullAPI.Version(ctx) + if err != nil { + return err + } + + ntwkName, err := FullAPI.StateNetworkName(ctx) + if err != nil { + return err + } + + vector := schema.TestVector{ + Class: schema.ClassTipset, + Meta: &schema.Metadata{ + ID: opts.id, + Gen: []schema.GenerationData{ + {Source: fmt.Sprintf("network:%s", ntwkName)}, + {Source: fmt.Sprintf("tipset:%s", ts.Key())}, + {Source: "github.com/filecoin-project/lotus", Version: version.String()}}, + }, + Selector: schema.Selector{ + schema.SelectorMinProtocolVersion: codename, + }, + Randomness: recordingRand.Recorded(), + CAR: out.Bytes(), + Pre: &schema.Preconditions{ + Variants: []schema.Variant{ + {ID: codename, Epoch: int64(ts.Height()), NetworkVersion: uint(nv)}, + }, + BaseFee: basefee.Int, + StateTree: &schema.StateTree{ + RootCID: ts.ParentState(), + }, + }, + ApplyTipsets: []schema.Tipset{tipset}, + Post: &schema.Postconditions{ + StateTree: &schema.StateTree{ + RootCID: result.PostStateRoot, + }, + ReceiptsRoots: []cid.Cid{result.ReceiptsRoot}, + }, + } + + for _, res := range result.AppliedResults { + vector.Post.Receipts = append(vector.Post.Receipts, &schema.Receipt{ + ExitCode: int64(res.ExitCode), + ReturnValue: res.Return, + GasUsed: res.GasUsed, + }) + } + + return writeVector(vector, opts.file) +} diff --git a/cmd/tvx/main.go b/cmd/tvx/main.go index 8de851ed5..8dae39958 100644 --- a/cmd/tvx/main.go +++ b/cmd/tvx/main.go @@ -102,7 +102,7 @@ func initialize(c *cli.Context) error { // Make the API client. var err error if FullAPI, Closer, err = lcli.GetFullNodeAPI(c); err != nil { - err = fmt.Errorf("failed to locate Lotus node; ") + err = fmt.Errorf("failed to locate Lotus node; err: %w", err) } return err } diff --git a/conformance/driver.go b/conformance/driver.go index 833d50d7b..1d5450622 100644 --- a/conformance/driver.go +++ b/conformance/driver.go @@ -73,24 +73,43 @@ type ExecuteTipsetResult struct { AppliedResults []*vm.ApplyRet } +type ExecuteTipsetParams struct { + Preroot cid.Cid + // ParentEpoch is the last epoch in which an actual tipset was processed. This + // is used by Lotus for null block counting and cron firing. + ParentEpoch abi.ChainEpoch + Tipset *schema.Tipset + ExecEpoch abi.ChainEpoch + // Rand is an optional vm.Rand implementation to use. If nil, the driver + // will use a vm.Rand that returns a fixed value for all calls. + Rand vm.Rand + // BaseFee if not nil or zero, will override the basefee of the tipset. + BaseFee abi.TokenAmount +} + // ExecuteTipset executes the supplied tipset on top of the state represented // by the preroot CID. // -// parentEpoch is the last epoch in which an actual tipset was processed. This -// is used by Lotus for null block counting and cron firing. -// // This method returns the the receipts root, the poststate root, and the VM // message results. The latter _include_ implicit messages, such as cron ticks // and reward withdrawal per miner. -func (d *Driver) ExecuteTipset(bs blockstore.Blockstore, ds ds.Batching, preroot cid.Cid, parentEpoch abi.ChainEpoch, tipset *schema.Tipset, execEpoch abi.ChainEpoch) (*ExecuteTipsetResult, error) { +func (d *Driver) ExecuteTipset(bs blockstore.Blockstore, ds ds.Batching, params ExecuteTipsetParams) (*ExecuteTipsetResult, error) { var ( + tipset = params.Tipset syscalls = vm.Syscalls(ffiwrapper.ProofVerifier) - vmRand = NewFixedRand() cs = store.NewChainStore(bs, bs, ds, syscalls, nil) sm = stmgr.NewStateManager(cs) ) + if params.Rand == nil { + params.Rand = NewFixedRand() + } + + if params.BaseFee.NilOrZero() { + params.BaseFee = abi.NewTokenAmount(tipset.BaseFee.Int64()) + } + defer cs.Close() //nolint:errcheck blocks := make([]store.BlockMessages, 0, len(tipset.Blocks)) @@ -122,15 +141,23 @@ func (d *Driver) ExecuteTipset(bs blockstore.Blockstore, ds ds.Batching, preroot var ( messages []*types.Message results []*vm.ApplyRet - - basefee = abi.NewTokenAmount(tipset.BaseFee.Int64()) ) - postcid, receiptsroot, err := sm.ApplyBlocks(context.Background(), parentEpoch, preroot, blocks, execEpoch, vmRand, func(_ cid.Cid, msg *types.Message, ret *vm.ApplyRet) error { + recordOutputs := func(_ cid.Cid, msg *types.Message, ret *vm.ApplyRet) error { messages = append(messages, msg) results = append(results, ret) return nil - }, basefee, nil) + } + postcid, receiptsroot, err := sm.ApplyBlocks(context.Background(), + params.ParentEpoch, + params.Preroot, + blocks, + params.ExecEpoch, + params.Rand, + recordOutputs, + params.BaseFee, + nil, + ) if err != nil { return nil, err diff --git a/conformance/runner.go b/conformance/runner.go index 6f9d73305..6eff25274 100644 --- a/conformance/runner.go +++ b/conformance/runner.go @@ -14,6 +14,7 @@ import ( "github.com/fatih/color" "github.com/filecoin-project/go-state-types/abi" "github.com/filecoin-project/go-state-types/exitcode" + blocks "github.com/ipfs/go-block-format" "github.com/ipfs/go-blockservice" "github.com/ipfs/go-cid" ds "github.com/ipfs/go-datastore" @@ -29,6 +30,14 @@ import ( "github.com/filecoin-project/lotus/lib/blockstore" ) +// FallbackBlockstoreGetter is a fallback blockstore to use for resolving CIDs +// unknown to the test vector. This is rarely used, usually only needed +// when transplanting vectors across versions. This is an interface tighter +// than ChainModuleAPI. It can be backed by a FullAPI client. +var FallbackBlockstoreGetter interface { + ChainReadObj(context.Context, cid.Cid) ([]byte, error) +} + // ExecuteMessageVector executes a message-class test vector. func ExecuteMessageVector(r Reporter, vector *schema.TestVector, variant *schema.Variant) { var ( @@ -38,7 +47,7 @@ func ExecuteMessageVector(r Reporter, vector *schema.TestVector, variant *schema ) // Load the CAR into a new temporary Blockstore. - bs, err := LoadVectorCAR(vector.CAR) + bs, err := LoadBlockstore(vector.CAR) if err != nil { r.Fatalf("failed to load the vector CAR: %w", err) } @@ -95,7 +104,7 @@ func ExecuteTipsetVector(r Reporter, vector *schema.TestVector, variant *schema. ) // Load the vector CAR into a new temporary Blockstore. - bs, err := LoadVectorCAR(vector.CAR) + bs, err := LoadBlockstore(vector.CAR) if err != nil { r.Fatalf("failed to load the vector CAR: %w", err) } @@ -109,9 +118,15 @@ func ExecuteTipsetVector(r Reporter, vector *schema.TestVector, variant *schema. for i, ts := range vector.ApplyTipsets { ts := ts // capture execEpoch := baseEpoch + abi.ChainEpoch(ts.EpochOffset) - ret, err := driver.ExecuteTipset(bs, tmpds, root, prevEpoch, &ts, execEpoch) + ret, err := driver.ExecuteTipset(bs, tmpds, ExecuteTipsetParams{ + Preroot: root, + ParentEpoch: prevEpoch, + Tipset: &ts, + ExecEpoch: execEpoch, + Rand: NewReplayingRand(r, vector.Randomness), + }) if err != nil { - r.Fatalf("failed to apply tipset %d message: %s", i, err) + r.Fatalf("failed to apply tipset %d: %s", i, err) } for j, v := range ret.AppliedResults { @@ -248,8 +263,8 @@ func writeStateToTempCAR(bs blockstore.Blockstore, roots ...cid.Cid) (string, er return tmp.Name(), nil } -func LoadVectorCAR(vectorCAR schema.Base64EncodedBytes) (blockstore.Blockstore, error) { - bs := blockstore.NewTemporary() +func LoadBlockstore(vectorCAR schema.Base64EncodedBytes) (blockstore.Blockstore, error) { + bs := blockstore.Blockstore(blockstore.NewTemporary()) // Read the base64-encoded CAR from the vector, and inflate the gzip. buf := bytes.NewReader(vectorCAR) @@ -264,5 +279,18 @@ func LoadVectorCAR(vectorCAR schema.Base64EncodedBytes) (blockstore.Blockstore, if err != nil { return nil, fmt.Errorf("failed to load state tree car from test vector: %s", err) } + + if FallbackBlockstoreGetter != nil { + fbs := &blockstore.FallbackStore{Blockstore: bs} + fbs.SetFallback(func(ctx context.Context, c cid.Cid) (blocks.Block, error) { + b, err := FallbackBlockstoreGetter.ChainReadObj(ctx, c) + if err != nil { + return nil, err + } + return blocks.NewBlockWithCid(b, c) + }) + bs = fbs + } + return bs, nil }