-
Notifications
You must be signed in to change notification settings - Fork 1.2k
/
runner.go
395 lines (339 loc) · 12.9 KB
/
runner.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
package conformance
import (
"bytes"
"compress/gzip"
"context"
"encoding/base64"
"fmt"
"io/ioutil"
"math"
"os"
"os/exec"
"strconv"
"github.com/fatih/color"
"github.com/hashicorp/go-multierror"
"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"
blocks "github.com/ipfs/go-libipfs/blocks"
"github.com/ipfs/go-merkledag"
"github.com/ipld/go-car"
"github.com/filecoin-project/go-state-types/abi"
"github.com/filecoin-project/go-state-types/exitcode"
"github.com/filecoin-project/go-state-types/network"
"github.com/filecoin-project/test-vectors/schema"
"github.com/filecoin-project/lotus/blockstore"
"github.com/filecoin-project/lotus/chain/consensus/filcns"
"github.com/filecoin-project/lotus/chain/types"
"github.com/filecoin-project/lotus/chain/vm"
)
// 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)
}
var TipsetVectorOpts struct {
// PipelineBaseFee pipelines the basefee in multi-tipset vectors from one
// tipset to another. Basefees in the vector are ignored, except for that of
// the first tipset. UNUSED.
PipelineBaseFee bool
// OnTipsetApplied contains callback functions called after a tipset has been
// applied.
OnTipsetApplied []func(bs blockstore.Blockstore, params *ExecuteTipsetParams, res *ExecuteTipsetResult)
}
type GasPricingRestoreFn func()
// adjustGasPricing adjusts the global gas price mapping to make sure that the
// gas pricelist for vector's network version is used at the vector's epoch.
// Because it manipulates a global, it returns a function that reverts the
// change. The caller MUST invoke this function or the test vector runner will
// become invalid.
func adjustGasPricing(vectorEpoch abi.ChainEpoch, vectorNv network.Version) GasPricingRestoreFn {
// Stash the current pricing mapping.
// Ok to take a reference instead of a copy, because we override the map
// with a new one below.
var old = vm.Prices
// Resolve the epoch at which the vector network version kicks in.
var epoch abi.ChainEpoch = math.MaxInt64
if vectorNv == network.Version0 {
// genesis is not an upgrade.
epoch = 0
} else {
for _, u := range filcns.DefaultUpgradeSchedule() {
if u.Network == vectorNv {
epoch = u.Height
break
}
}
}
if epoch == math.MaxInt64 {
panic(fmt.Sprintf("could not resolve network version %d to height", vectorNv))
}
// Find the right pricelist for this network version.
pricelist := vm.PricelistByEpoch(epoch)
// Override the pricing mapping by setting the relevant pricelist for the
// network version at the epoch where the vector runs.
vm.Prices = map[abi.ChainEpoch]vm.Pricelist{
vectorEpoch: pricelist,
}
// Return a function to restore the original mapping.
return func() {
vm.Prices = old
}
}
// ExecuteMessageVector executes a message-class test vector.
func ExecuteMessageVector(r Reporter, vector *schema.TestVector, variant *schema.Variant) (diffs []string, err error) {
var (
ctx = context.Background()
baseEpoch = abi.ChainEpoch(variant.Epoch)
nv = network.Version(variant.NetworkVersion)
root = vector.Pre.StateTree.RootCID
)
// Load the CAR into a new temporary Blockstore.
bs, err := LoadBlockstore(vector.CAR)
if err != nil {
r.Fatalf("failed to load the vector CAR: %w", err)
}
// Create a new Driver.
driver := NewDriver(ctx, vector.Selector, DriverOpts{DisableVMFlush: true})
// Monkey patch the gas pricing.
revertFn := adjustGasPricing(baseEpoch, nv)
defer revertFn()
// Apply every message.
for i, m := range vector.ApplyMessages {
msg, err := types.DecodeMessage(m.Bytes)
if err != nil {
r.Fatalf("failed to deserialize message: %s", err)
}
// add the epoch offset if one is set.
if m.EpochOffset != nil {
baseEpoch += abi.ChainEpoch(*m.EpochOffset)
}
// Execute the message.
var ret *vm.ApplyRet
ret, root, err = driver.ExecuteMessage(bs, ExecuteMessageParams{
Preroot: root,
Epoch: baseEpoch,
Message: msg,
BaseFee: BaseFeeOrDefault(vector.Pre.BaseFee),
CircSupply: CircSupplyOrDefault(vector.Pre.CircSupply),
Rand: NewReplayingRand(r, vector.Randomness),
NetworkVersion: nv,
})
if err != nil {
r.Fatalf("fatal failure when executing message: %s", err)
}
// Assert that the receipt matches what the test vector expects.
AssertMsgResult(r, 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 {
ierr := fmt.Errorf("wrong post root cid; expected %v, but got %v", expected, actual)
r.Errorf(ierr.Error())
err = multierror.Append(err, ierr)
diffs = dumpThreeWayStateDiff(r, vector, bs, root)
}
return diffs, err
}
// ExecuteTipsetVector executes a tipset-class test vector.
func ExecuteTipsetVector(r Reporter, vector *schema.TestVector, variant *schema.Variant) (diffs []string, err error) {
var (
ctx = context.Background()
baseEpoch = abi.ChainEpoch(variant.Epoch)
root = vector.Pre.StateTree.RootCID
tmpds = ds.NewMapDatastore()
)
// Load the vector CAR into a new temporary Blockstore.
bs, err := LoadBlockstore(vector.CAR)
if err != nil {
r.Fatalf("failed to load the vector CAR: %w", err)
return nil, err
}
// Create a new Driver.
driver := NewDriver(ctx, vector.Selector, DriverOpts{})
// Apply every tipset.
var receiptsIdx int
var prevEpoch = baseEpoch
for i, ts := range vector.ApplyTipsets {
ts := ts // capture
execEpoch := baseEpoch + abi.ChainEpoch(ts.EpochOffset)
params := ExecuteTipsetParams{
Preroot: root,
ParentEpoch: prevEpoch,
Tipset: &ts,
ExecEpoch: execEpoch,
Rand: NewReplayingRand(r, vector.Randomness),
}
ret, err := driver.ExecuteTipset(bs, tmpds, params)
if err != nil {
r.Fatalf("failed to apply tipset %d: %s", i, err)
return nil, err
}
// invoke callbacks.
for _, cb := range TipsetVectorOpts.OnTipsetApplied {
cb(bs, ¶ms, ret)
}
for j, v := range ret.AppliedResults {
AssertMsgResult(r, 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 {
ierr := fmt.Errorf("post receipts root doesn't match; expected: %s, was: %s", expected, actual)
r.Errorf(ierr.Error())
err = multierror.Append(err, ierr)
}
prevEpoch = execEpoch
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 {
ierr := fmt.Errorf("wrong post root cid; expected %v, but got %v", expected, actual)
r.Errorf(ierr.Error())
err = multierror.Append(err, ierr)
diffs = dumpThreeWayStateDiff(r, vector, bs, root)
}
return diffs, err
}
// 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(r Reporter, expected *schema.Receipt, actual *vm.ApplyRet, label string) {
r.Helper()
applyret := actual
if expected, actual := exitcode.ExitCode(expected.ExitCode), actual.ExitCode; expected != actual {
r.Errorf("exit code of msg %s did not match; expected: %s, got: %s", label, expected, actual)
r.Errorf("\t\\==> actor error: %s", applyret.ActorErr)
}
if expected, actual := expected.GasUsed, actual.GasUsed; expected != actual {
r.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) {
r.Errorf("return value of msg %s did not match; expected: %s, got: %s", label, base64.StdEncoding.EncodeToString(expected), base64.StdEncoding.EncodeToString(actual))
}
}
func dumpThreeWayStateDiff(r Reporter, vector *schema.TestVector, bs blockstore.Blockstore, actual cid.Cid) []string {
// check if statediff exists; if not, skip.
if err := exec.Command("statediff", "--help").Run(); err != nil {
r.Log("could not dump 3-way state tree diff upon test failure: statediff command not found")
r.Log("install statediff with:")
r.Log("$ git clone https://github.com/filecoin-project/statediff.git")
r.Log("$ cd statediff")
r.Log("$ go generate ./...")
r.Log("$ go install ./cmd/statediff")
return nil
}
tmpCar, err := writeStateToTempCAR(bs,
vector.Pre.StateTree.RootCID,
vector.Post.StateTree.RootCID,
actual,
)
if err != nil {
r.Fatalf("failed to write temporary state CAR: %s", err)
return nil
}
defer os.RemoveAll(tmpCar) //nolint:errcheck
color.NoColor = false // enable colouring.
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]")
)
diff := func(left, right cid.Cid) string {
cmd := exec.Command("statediff", "car", "--file", tmpCar, left.String(), right.String())
b, err := cmd.CombinedOutput()
if err != nil {
r.Fatalf("statediff failed: %s", err)
}
return string(b)
}
bold := color.New(color.Bold).SprintfFunc()
r.Log(bold("-----BEGIN STATEDIFF-----"))
// run state diffs.
r.Log(bold("=== dumping 3-way diffs between %s, %s, %s ===", a, b, c))
r.Log(bold("--- %s left: %s; right: %s ---", d1, a, b))
diffA := diff(vector.Post.StateTree.RootCID, actual)
r.Log(bold("----------BEGIN STATEDIFF A----------"))
r.Log(diffA)
r.Log(bold("----------END STATEDIFF A----------"))
r.Log(bold("--- %s left: %s; right: %s ---", d2, c, b))
diffB := diff(vector.Pre.StateTree.RootCID, actual)
r.Log(bold("----------BEGIN STATEDIFF B----------"))
r.Log(diffB)
r.Log(bold("----------END STATEDIFF B----------"))
r.Log(bold("--- %s left: %s; right: %s ---", d3, c, a))
diffC := diff(vector.Pre.StateTree.RootCID, vector.Post.StateTree.RootCID)
r.Log(bold("----------BEGIN STATEDIFF C----------"))
r.Log(diffC)
r.Log(bold("----------END STATEDIFF C----------"))
r.Log(bold("-----END STATEDIFF-----"))
return []string{diffA, diffB, diffC}
}
// 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(bs blockstore.Blockstore, roots ...cid.Cid) (string, error) {
tmp, err := ioutil.TempFile("", "lotus-tests-*.car")
if err != nil {
return "", fmt.Errorf("failed to create temp file to dump CAR for diffing: %w", err)
}
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
}
// ignore things we don't have, the state tree is incomplete.
if has, err := bs.Has(context.TODO(), link.Cid); err != nil {
return nil, err
} else if has {
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 {
return "", fmt.Errorf("failed to dump CAR for diffing: %w", err)
}
_ = tmp.Close()
return tmp.Name(), nil
}
func LoadBlockstore(vectorCAR schema.Base64EncodedBytes) (blockstore.Blockstore, error) {
bs := blockstore.Blockstore(blockstore.NewMemory())
// Read the base64-encoded CAR from the vector, and inflate the gzip.
buf := bytes.NewReader(vectorCAR)
r, err := gzip.NewReader(buf)
if err != nil {
return nil, fmt.Errorf("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(context.TODO(), bs, r)
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
}