2020-08-15 22:57:27 +00:00
package conformance
import (
"bytes"
"compress/gzip"
"context"
2020-08-27 15:42:01 +00:00
"encoding/base64"
2020-08-15 22:57:27 +00:00
"encoding/json"
2020-09-01 14:35:19 +00:00
"fmt"
2020-08-15 22:57:27 +00:00
"io/ioutil"
"os"
2020-09-09 12:03:43 +00:00
"os/exec"
2020-08-15 22:57:27 +00:00
"path/filepath"
2020-09-01 14:35:19 +00:00
"strconv"
2020-08-15 22:57:27 +00:00
"strings"
"testing"
2020-09-08 20:50:25 +00:00
"github.com/filecoin-project/go-state-types/abi"
"github.com/filecoin-project/go-state-types/exitcode"
2020-09-09 12:03:43 +00:00
"github.com/ipfs/go-blockservice"
2020-09-01 14:35:19 +00:00
"github.com/ipfs/go-cid"
ds "github.com/ipfs/go-datastore"
2020-09-09 13:28:56 +00:00
offline "github.com/ipfs/go-ipfs-exchange-offline"
format "github.com/ipfs/go-ipld-format"
2020-09-09 12:03:43 +00:00
"github.com/ipfs/go-merkledag"
2020-09-01 14:35:19 +00:00
2020-08-15 22:57:27 +00:00
"github.com/filecoin-project/lotus/chain/types"
"github.com/filecoin-project/lotus/chain/vm"
"github.com/filecoin-project/lotus/lib/blockstore"
2020-08-26 14:07:51 +00:00
2020-08-19 16:33:30 +00:00
"github.com/filecoin-project/test-vectors/schema"
2020-08-15 22:57:27 +00:00
2020-08-26 17:35:41 +00:00
"github.com/fatih/color"
2020-08-15 22:57:27 +00:00
"github.com/ipld/go-car"
)
const (
2020-08-26 14:07:51 +00:00
// 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"
2020-08-15 22:57:27 +00:00
// defaultCorpusRoot is the directory where the test vector corpus is hosted.
// It is mounted on the Lotus repo as a git submodule.
2020-08-15 22:57:27 +00:00
//
2020-08-15 22:57:27 +00:00
// When running this test, the corpus root can be overridden through the
2020-08-16 21:22:12 +00:00
// -conformance.corpus CLI flag to run an alternate corpus.
2020-08-16 21:15:12 +00:00
defaultCorpusRoot = "../extern/test-vectors/corpus"
2020-08-15 22:57:27 +00:00
)
2020-08-26 14:07:51 +00:00
// ignore is a set of paths relative to root to skip.
var ignore = map [ string ] struct { } {
".git" : { } ,
"schema.json" : { } ,
2020-08-15 22:57:27 +00:00
}
// TestConformance is the entrypoint test that runs all test vectors found
2020-08-15 22:57:27 +00:00
// in the corpus root directory.
2020-08-15 22:57:27 +00:00
//
// 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 ) {
2020-08-26 14:07:51 +00:00
if skip := strings . TrimSpace ( os . Getenv ( EnvSkipConformance ) ) ; skip == "1" {
2020-08-16 21:22:12 +00:00
t . SkipNow ( )
}
2020-08-26 14:07:51 +00:00
// 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
}
2020-08-15 22:57:27 +00:00
var vectors [ ] string
2020-08-15 22:57:27 +00:00
err := filepath . Walk ( corpusRoot + "/" , func ( path string , info os . FileInfo , err error ) error {
2020-08-15 22:57:27 +00:00
if err != nil {
t . Fatal ( err )
}
filename := filepath . Base ( path )
2020-08-15 22:57:27 +00:00
rel , err := filepath . Rel ( corpusRoot , path )
2020-08-15 22:57:27 +00:00
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 {
2020-08-16 21:15:12 +00:00
path := filepath . Join ( corpusRoot , v )
raw , err := ioutil . ReadFile ( path )
if err != nil {
t . Fatalf ( "failed to read test raw file: %s" , path )
}
2020-08-15 22:57:27 +00:00
2020-08-19 16:33:30 +00:00
var vector schema . TestVector
2020-08-16 21:15:12 +00:00
err = json . Unmarshal ( raw , & vector )
if err != nil {
t . Errorf ( "failed to parse test vector %s: %s; skipping" , path , err )
continue
}
2020-08-15 22:57:27 +00:00
2020-08-16 21:15:12 +00:00
t . Run ( v , func ( t * testing . T ) {
2020-08-26 10:45:34 +00:00
for _ , h := range vector . Hints {
if h == schema . HintIncorrect {
t . Logf ( "skipping vector marked as incorrect: %s" , vector . Meta . ID )
t . SkipNow ( )
}
}
2020-08-15 22:57:27 +00:00
// dispatch the execution depending on the vector class.
switch vector . Class {
case "message" :
executeMessageVector ( t , & vector )
2020-09-01 14:35:19 +00:00
case "tipset" :
executeTipsetVector ( t , & vector )
2020-08-15 22:57:27 +00:00
default :
t . Fatalf ( "test vector class not supported: %s" , vector . Class )
}
} )
}
}
// executeMessageVector executes a message-class test vector.
2020-08-19 16:33:30 +00:00
func executeMessageVector ( t * testing . T , vector * schema . TestVector ) {
2020-08-15 22:57:27 +00:00
var (
ctx = context . Background ( )
epoch = vector . Pre . Epoch
root = vector . Pre . StateTree . RootCID
)
2020-09-01 14:35:19 +00:00
// Load the CAR into a new temporary Blockstore.
bs := loadCAR ( t , vector . CAR )
2020-08-15 22:57:27 +00:00
// Create a new Driver.
2020-09-01 14:35:19 +00:00
driver := NewDriver ( ctx , vector . Selector )
2020-08-15 22:57:27 +00:00
// Apply every message.
2020-08-26 15:48:47 +00:00
for i , m := range vector . ApplyMessages {
2020-08-15 22:57:27 +00:00
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
}
2020-08-15 22:57:27 +00:00
// Execute the message.
2020-08-15 22:57:27 +00:00
var ret * vm . ApplyRet
2020-09-08 20:50:25 +00:00
ret , root , err = driver . ExecuteMessage ( bs , root , abi . ChainEpoch ( epoch ) , msg )
2020-08-15 22:57:27 +00:00
if err != nil {
t . Fatalf ( "fatal failure when executing message: %s" , err )
}
2020-08-15 22:57:27 +00:00
// Assert that the receipt matches what the test vector expects.
2020-09-01 14:35:19 +00:00
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.
2020-09-09 13:08:54 +00:00
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 )
2020-09-01 14:35:19 +00:00
dumpThreeWayStateDiff ( t , vector , bs , root )
2020-09-09 13:08:54 +00:00
t . FailNow ( )
2020-09-01 14:35:19 +00:00
}
}
// 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
2020-09-08 20:50:25 +00:00
ret , err := driver . ExecuteTipset ( bs , tmpds , root , abi . ChainEpoch ( prevEpoch ) , & ts )
2020-09-01 14:35:19 +00:00
if err != nil {
t . Fatalf ( "failed to apply tipset %d message: %s" , i , err )
2020-08-15 22:57:27 +00:00
}
2020-09-01 14:35:19 +00:00
for j , v := range ret . AppliedResults {
assertMsgResult ( t , vector . Post . Receipts [ receiptsIdx ] , v , fmt . Sprintf ( "%d of tipset %d" , j , i ) )
receiptsIdx ++
2020-08-15 22:57:27 +00:00
}
2020-09-01 14:35:19 +00:00
// 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 )
2020-08-27 15:42:01 +00:00
}
2020-09-01 14:35:19 +00:00
prevEpoch = ts . Epoch
root = ret . PostStateRoot
2020-08-15 22:57:27 +00:00
}
2020-08-15 22:57:27 +00:00
// Once all messages are applied, assert that the final state root matches
// the expected postcondition root.
2020-09-09 13:08:54 +00:00
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 )
2020-09-01 14:35:19 +00:00
dumpThreeWayStateDiff ( t , vector , bs , root )
2020-09-09 13:08:54 +00:00
t . FailNow ( )
2020-09-01 14:35:19 +00:00
}
}
2020-08-26 17:35:41 +00:00
2020-09-01 14:35:19 +00:00
// 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 ( )
2020-08-26 14:07:51 +00:00
2020-09-08 20:50:25 +00:00
if expected , actual := exitcode . ExitCode ( expected . ExitCode ) , actual . ExitCode ; expected != actual {
2020-09-01 14:35:19 +00:00
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 ) {
2020-09-09 12:03:43 +00:00
// 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 ,
)
2020-09-01 14:35:19 +00:00
color . NoColor = false // enable colouring.
2020-08-26 17:35:41 +00:00
2020-09-01 14:35:19 +00:00
t . Errorf ( "wrong post root cid; expected %v, but got %v" , vector . Post . StateTree . RootCID , actual )
2020-08-26 14:07:51 +00:00
2020-09-01 14:35:19 +00:00
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]" )
)
2020-08-26 14:07:51 +00:00
2020-09-09 13:08:54 +00:00
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 ) )
}
2020-09-01 14:35:19 +00:00
bold := color . New ( color . Bold ) . SprintfFunc ( )
2020-08-26 14:07:51 +00:00
2020-09-01 14:35:19 +00:00
// run state diffs.
t . Log ( bold ( "=== dumping 3-way diffs between %s, %s, %s ===" , a , b , c ) )
2020-08-26 14:07:51 +00:00
2020-09-01 14:35:19 +00:00
t . Log ( bold ( "--- %s left: %s; right: %s ---" , d1 , a , b ) )
2020-09-09 13:08:54 +00:00
printDiff ( vector . Post . StateTree . RootCID , actual )
2020-09-01 14:35:19 +00:00
t . Log ( bold ( "--- %s left: %s; right: %s ---" , d2 , c , b ) )
2020-09-09 13:08:54 +00:00
printDiff ( vector . Pre . StateTree . RootCID , actual )
2020-09-01 14:35:19 +00:00
t . Log ( bold ( "--- %s left: %s; right: %s ---" , d3 , c , a ) )
2020-09-09 13:08:54 +00:00
printDiff ( vector . Pre . StateTree . RootCID , vector . Post . StateTree . RootCID )
2020-09-09 12:03:43 +00:00
}
// 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 ( )
2020-09-01 14:35:19 +00:00
}
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 )
2020-08-15 22:57:27 +00:00
}
2020-09-01 14:35:19 +00:00
return bs
2020-08-15 22:57:27 +00:00
}