Merge pull request #5194 from filecoin-project/raulk/tvx-extract-tipset
tvx command to extract tipset vectors.
This commit is contained in:
commit
0d72d742b4
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
440
cmd/tvx/extract_message.go
Normal 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
186
cmd/tvx/extract_tipset.go
Normal 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)
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user