package conformance import ( "bytes" "compress/gzip" "context" "encoding/base64" "encoding/json" "fmt" "io/ioutil" "os" "os/exec" "path/filepath" "strconv" "strings" "testing" "github.com/filecoin-project/go-state-types/abi" "github.com/filecoin-project/go-state-types/exitcode" "github.com/ipfs/go-blockservice" "github.com/ipfs/go-cid" ds "github.com/ipfs/go-datastore" offline "github.com/ipfs/go-ipfs-exchange-offline" format "github.com/ipfs/go-ipld-format" "github.com/ipfs/go-merkledag" "github.com/filecoin-project/lotus/chain/types" "github.com/filecoin-project/lotus/chain/vm" "github.com/filecoin-project/lotus/lib/blockstore" "github.com/filecoin-project/test-vectors/schema" "github.com/fatih/color" "github.com/ipld/go-car" ) const ( // EnvSkipConformance, if 1, skips the conformance test suite. EnvSkipConformance = "SKIP_CONFORMANCE" // EnvCorpusRootDir is the name of the environment variable where the path // to an alternative corpus location can be provided. // // The default is defaultCorpusRoot. EnvCorpusRootDir = "CORPUS_DIR" // defaultCorpusRoot is the directory where the test vector corpus is hosted. // It is mounted on the Lotus repo as a git submodule. // // When running this test, the corpus root can be overridden through the // -conformance.corpus CLI flag to run an alternate corpus. defaultCorpusRoot = "../extern/test-vectors/corpus" ) // ignore is a set of paths relative to root to skip. var ignore = map[string]struct{}{ ".git": {}, "schema.json": {}, } // TestConformance is the entrypoint test that runs all test vectors found // in the corpus root directory. // // It locates all json files via a recursive walk, skipping over the ignore set, // as well as files beginning with _. It parses each file as a test vector, and // runs it via the Driver. func TestConformance(t *testing.T) { if skip := strings.TrimSpace(os.Getenv(EnvSkipConformance)); skip == "1" { t.SkipNow() } // corpusRoot is the effective corpus root path, taken from the `-conformance.corpus` CLI flag, // falling back to defaultCorpusRoot if not provided. corpusRoot := defaultCorpusRoot if dir := strings.TrimSpace(os.Getenv(EnvCorpusRootDir)); dir != "" { corpusRoot = dir } var vectors []string err := filepath.Walk(corpusRoot+"/", func(path string, info os.FileInfo, err error) error { if err != nil { t.Fatal(err) } filename := filepath.Base(path) rel, err := filepath.Rel(corpusRoot, path) if err != nil { t.Fatal(err) } if _, ok := ignore[rel]; ok { // skip over using the right error. if info.IsDir() { return filepath.SkipDir } return nil } if info.IsDir() { // dive into directories. return nil } if filepath.Ext(path) != ".json" { // skip if not .json. return nil } if ignored := strings.HasPrefix(filename, "_"); ignored { // ignore files starting with _. t.Logf("ignoring: %s", rel) return nil } vectors = append(vectors, rel) return nil }) if err != nil { t.Fatal(err) } if len(vectors) == 0 { t.Fatalf("no test vectors found") } // Run a test for each vector. for _, v := range vectors { path := filepath.Join(corpusRoot, v) raw, err := ioutil.ReadFile(path) if err != nil { t.Fatalf("failed to read test raw file: %s", path) } var vector schema.TestVector err = json.Unmarshal(raw, &vector) if err != nil { t.Errorf("failed to parse test vector %s: %s; skipping", path, err) continue } t.Run(v, func(t *testing.T) { for _, h := range vector.Hints { if h == schema.HintIncorrect { t.Logf("skipping vector marked as incorrect: %s", vector.Meta.ID) t.SkipNow() } } // dispatch the execution depending on the vector class. switch vector.Class { case "message": executeMessageVector(t, &vector) case "tipset": executeTipsetVector(t, &vector) default: t.Fatalf("test vector class not supported: %s", vector.Class) } }) } } // executeMessageVector executes a message-class test vector. func executeMessageVector(t *testing.T, vector *schema.TestVector) { var ( ctx = context.Background() epoch = vector.Pre.Epoch root = vector.Pre.StateTree.RootCID ) // Load the CAR into a new temporary Blockstore. bs := loadCAR(t, vector.CAR) // Create a new Driver. driver := NewDriver(ctx, vector.Selector) // Apply every message. for i, m := range vector.ApplyMessages { msg, err := types.DecodeMessage(m.Bytes) if err != nil { t.Fatalf("failed to deserialize message: %s", err) } // add an epoch if one's set. if m.Epoch != nil { epoch = *m.Epoch } // Execute the message. var ret *vm.ApplyRet ret, root, err = driver.ExecuteMessage(bs, root, abi.ChainEpoch(epoch), msg) if err != nil { t.Fatalf("fatal failure when executing message: %s", err) } // Assert that the receipt matches what the test vector expects. assertMsgResult(t, vector.Post.Receipts[i], ret, strconv.Itoa(i)) } // Once all messages are applied, assert that the final state root matches // the expected postcondition root. if expected, actual := vector.Post.StateTree.RootCID, root; expected != actual { t.Logf("actual state root CID doesn't match expected one; expected: %s, actual: %s", expected, actual) dumpThreeWayStateDiff(t, vector, bs, root) t.FailNow() } } // executeTipsetVector executes a tipset-class test vector. func executeTipsetVector(t *testing.T, vector *schema.TestVector) { var ( ctx = context.Background() prevEpoch = vector.Pre.Epoch root = vector.Pre.StateTree.RootCID tmpds = ds.NewMapDatastore() ) // Load the CAR into a new temporary Blockstore. bs := loadCAR(t, vector.CAR) // Create a new Driver. driver := NewDriver(ctx, vector.Selector) // Apply every tipset. var receiptsIdx int for i, ts := range vector.ApplyTipsets { ts := ts // capture ret, err := driver.ExecuteTipset(bs, tmpds, root, abi.ChainEpoch(prevEpoch), &ts) if err != nil { t.Fatalf("failed to apply tipset %d message: %s", i, err) } for j, v := range ret.AppliedResults { assertMsgResult(t, vector.Post.Receipts[receiptsIdx], v, fmt.Sprintf("%d of tipset %d", j, i)) receiptsIdx++ } // Compare the receipts root. if expected, actual := vector.Post.ReceiptsRoots[i], ret.ReceiptsRoot; expected != actual { t.Errorf("post receipts root doesn't match; expected: %s, was: %s", expected, actual) } prevEpoch = ts.Epoch root = ret.PostStateRoot } // Once all messages are applied, assert that the final state root matches // the expected postcondition root. if expected, actual := vector.Post.StateTree.RootCID, root; expected != actual { t.Logf("actual state root CID doesn't match expected one; expected: %s, actual: %s", expected, actual) dumpThreeWayStateDiff(t, vector, bs, root) t.FailNow() } } // assertMsgResult compares a message result. It takes the expected receipt // encoded in the vector, the actual receipt returned by Lotus, and a message // label to log in the assertion failure message to facilitate debugging. func assertMsgResult(t *testing.T, expected *schema.Receipt, actual *vm.ApplyRet, label string) { t.Helper() if expected, actual := exitcode.ExitCode(expected.ExitCode), actual.ExitCode; expected != actual { t.Errorf("exit code of msg %s did not match; expected: %s, got: %s", label, expected, actual) } if expected, actual := expected.GasUsed, actual.GasUsed; expected != actual { t.Errorf("gas used of msg %s did not match; expected: %d, got: %d", label, expected, actual) } if expected, actual := []byte(expected.ReturnValue), actual.Return; !bytes.Equal(expected, actual) { t.Errorf("return value of msg %s did not match; expected: %s, got: %s", label, base64.StdEncoding.EncodeToString(expected), base64.StdEncoding.EncodeToString(actual)) } } func dumpThreeWayStateDiff(t *testing.T, vector *schema.TestVector, bs blockstore.Blockstore, actual cid.Cid) { // check if statediff exists; if not, skip. if err := exec.Command("statediff", "--help").Run(); err != nil { t.Log("could not dump 3-way state tree diff upon test failure: statediff command not found") t.Log("install statediff with:") t.Log("$ git clone https://github.com/filecoin-project/statediff.git") t.Log("$ cd statediff") t.Log("$ go generate ./...") t.Log("$ go install ./cmd/statediff") return } tmpCar := writeStateToTempCAR(t, bs, vector.Pre.StateTree.RootCID, vector.Post.StateTree.RootCID, actual, ) color.NoColor = false // enable colouring. t.Errorf("wrong post root cid; expected %v, but got %v", vector.Post.StateTree.RootCID, actual) var ( a = color.New(color.FgMagenta, color.Bold).Sprint("(A) expected final state") b = color.New(color.FgYellow, color.Bold).Sprint("(B) actual final state") c = color.New(color.FgCyan, color.Bold).Sprint("(C) initial state") d1 = color.New(color.FgGreen, color.Bold).Sprint("[Δ1]") d2 = color.New(color.FgGreen, color.Bold).Sprint("[Δ2]") d3 = color.New(color.FgGreen, color.Bold).Sprint("[Δ3]") ) printDiff := func(left, right cid.Cid) { cmd := exec.Command("statediff", "car", "--file", tmpCar, left.String(), right.String()) b, err := cmd.CombinedOutput() if err != nil { t.Fatalf("statediff failed: %s", err) } t.Log(string(b)) } bold := color.New(color.Bold).SprintfFunc() // run state diffs. t.Log(bold("=== dumping 3-way diffs between %s, %s, %s ===", a, b, c)) t.Log(bold("--- %s left: %s; right: %s ---", d1, a, b)) printDiff(vector.Post.StateTree.RootCID, actual) t.Log(bold("--- %s left: %s; right: %s ---", d2, c, b)) printDiff(vector.Pre.StateTree.RootCID, actual) t.Log(bold("--- %s left: %s; right: %s ---", d3, c, a)) printDiff(vector.Pre.StateTree.RootCID, vector.Post.StateTree.RootCID) } // writeStateToTempCAR writes the provided roots to a temporary CAR that'll be // cleaned up via t.Cleanup(). It returns the full path of the temp file. func writeStateToTempCAR(t *testing.T, bs blockstore.Blockstore, roots ...cid.Cid) string { tmp, err := ioutil.TempFile("", "lotus-tests-*.car") if err != nil { t.Fatalf("failed to create temp file to dump CAR for diffing: %s", err) } // register a cleanup function to delete the CAR. t.Cleanup(func() { _ = os.Remove(tmp.Name()) }) carWalkFn := func(nd format.Node) (out []*format.Link, err error) { for _, link := range nd.Links() { if link.Cid.Prefix().Codec == cid.FilCommitmentSealed || link.Cid.Prefix().Codec == cid.FilCommitmentUnsealed { continue } out = append(out, link) } return out, nil } var ( offl = offline.Exchange(bs) blkserv = blockservice.New(bs, offl) dserv = merkledag.NewDAGService(blkserv) ) err = car.WriteCarWithWalker(context.Background(), dserv, roots, tmp, carWalkFn) if err != nil { t.Fatalf("failed to dump CAR for diffing: %s", err) } _ = tmp.Close() return tmp.Name() } func loadCAR(t *testing.T, vectorCAR schema.Base64EncodedBytes) blockstore.Blockstore { bs := blockstore.NewTemporary() // Read the base64-encoded CAR from the vector, and inflate the gzip. buf := bytes.NewReader(vectorCAR) r, err := gzip.NewReader(buf) if err != nil { t.Fatalf("failed to inflate gzipped CAR: %s", err) } defer r.Close() // nolint // Load the CAR embedded in the test vector into the Blockstore. _, err = car.LoadCar(bs, r) if err != nil { t.Fatalf("failed to load state tree car from test vector: %s", err) } return bs }