Merge pull request #5194 from filecoin-project/raulk/tvx-extract-tipset

tvx command to extract tipset vectors.
This commit is contained in:
Łukasz Magiera 2020-12-15 19:53:10 +01:00 committed by GitHub
commit 0d72d742b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 728 additions and 451 deletions

View File

@ -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)

View File

@ -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
}

View File

@ -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
}

440
cmd/tvx/extract_message.go Normal file
View File

@ -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
}

186
cmd/tvx/extract_tipset.go Normal file
View File

@ -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)
}

View File

@ -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
}

View File

@ -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

View File

@ -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
}