From 99f466e35e20dd83ad69f2fa410d2b67cb2af41f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Kripalani?= Date: Fri, 14 Aug 2020 13:51:43 +0100 Subject: [PATCH] test vector builder API + partial migration of suites (#229) Co-authored-by: Anton Evangelatov --- tvx/builders/actors.go | 213 +++++++++++++ tvx/builders/address.go | 89 ++++++ tvx/builders/asserter.go | 82 +++++ tvx/builders/builder.go | 194 ++++++++++++ tvx/builders/messages.go | 153 +++++++++ tvx/builders/messages_sugar.go | 56 ++++ tvx/builders/messages_typed.go | 362 ++++++++++++++++++++++ tvx/builders/predicates.go | 19 ++ tvx/builders/serialization.go | 38 +++ tvx/builders/state_zero.go | 172 ++++++++++ tvx/builders/wallet.go | 105 +++++++ tvx/lotus/driver.go | 16 +- tvx/scripts/msg_application.go | 247 +++++++++++++++ tvx/scripts/nested.go | 113 +++++++ tvx/scripts/paych.go | 185 +++++++++++ tvx/state/store.go | 57 ++-- tvx/state/surgeon.go | 4 +- tvx/suite_messages_create_actor.go | 2 - tvx/suite_messages_message_application.go | 267 ---------------- tvx/suite_messages_paych.go | 185 ----------- 20 files changed, 2072 insertions(+), 487 deletions(-) create mode 100644 tvx/builders/actors.go create mode 100644 tvx/builders/address.go create mode 100644 tvx/builders/asserter.go create mode 100644 tvx/builders/builder.go create mode 100644 tvx/builders/messages.go create mode 100644 tvx/builders/messages_sugar.go create mode 100644 tvx/builders/messages_typed.go create mode 100644 tvx/builders/predicates.go create mode 100644 tvx/builders/serialization.go create mode 100644 tvx/builders/state_zero.go create mode 100644 tvx/builders/wallet.go create mode 100644 tvx/scripts/msg_application.go create mode 100644 tvx/scripts/nested.go create mode 100644 tvx/scripts/paych.go delete mode 100644 tvx/suite_messages_message_application.go delete mode 100644 tvx/suite_messages_paych.go diff --git a/tvx/builders/actors.go b/tvx/builders/actors.go new file mode 100644 index 0000000000..c27a9c0860 --- /dev/null +++ b/tvx/builders/actors.go @@ -0,0 +1,213 @@ +package builders + +import ( + "context" + "log" + + "github.com/filecoin-project/go-address" + "github.com/filecoin-project/lotus/chain/types" + "github.com/filecoin-project/specs-actors/actors/abi" + abi_spec "github.com/filecoin-project/specs-actors/actors/abi" + "github.com/filecoin-project/specs-actors/actors/abi/big" + "github.com/filecoin-project/specs-actors/actors/builtin" + "github.com/filecoin-project/specs-actors/actors/builtin/account" + "github.com/filecoin-project/specs-actors/actors/builtin/miner" + "github.com/filecoin-project/specs-actors/actors/builtin/power" + "github.com/filecoin-project/specs-actors/actors/runtime" + "github.com/filecoin-project/specs-actors/actors/util/adt" + "github.com/ipfs/go-cid" + cbg "github.com/whyrusleeping/cbor-gen" +) + +// Actors is an object that manages actors in the test vector. +type Actors struct { + accounts []AddressHandle + miners []AddressHandle + + b *Builder +} + +// AccountN creates many account actors of the specified kind, with the +// specified balance, and places their addresses in the supplied AddressHandles. +func (a *Actors) AccountN(typ address.Protocol, balance abi.TokenAmount, handles ...*AddressHandle) { + for _, handle := range handles { + h := a.Account(typ, balance) + *handle = h + } +} + +// Account creates a single account actor of the specified kind, with the +// specified balance, and returns its AddressHandle. +func (a *Actors) Account(typ address.Protocol, balance abi.TokenAmount) AddressHandle { + a.b.Assert.In(typ, address.SECP256K1, address.BLS) + + var addr address.Address + switch typ { + case address.SECP256K1: + addr = a.b.Wallet.NewSECP256k1Account() + case address.BLS: + addr = a.b.Wallet.NewBLSAccount() + } + + actorState := &account.State{Address: addr} + handle := a.CreateActor(builtin.AccountActorCodeID, addr, balance, actorState) + + a.accounts = append(a.accounts, handle) + return handle +} + +type MinerActorCfg struct { + SealProofType abi.RegisteredSealProof + PeriodBoundary abi.ChainEpoch + OwnerBalance abi.TokenAmount +} + +// Miner creates an owner account, a worker account, and a miner actor managed +// by those accounts. +func (a *Actors) Miner(cfg MinerActorCfg) (minerActor, owner, worker AddressHandle) { + owner = a.Account(address.SECP256K1, cfg.OwnerBalance) + worker = a.Account(address.BLS, big.Zero()) + // expectedMinerActorIDAddress := chain.MustNewIDAddr(chain.MustIDFromAddress(minerWorkerID) + 1) + // minerActorAddrs := computeInitActorExecReturn(minerWorkerPk, 0, 1, expectedMinerActorIDAddress) + + ss, err := cfg.SealProofType.SectorSize() + a.b.Assert.NoError(err, "seal proof sector size") + + ps, err := cfg.SealProofType.WindowPoStPartitionSectors() + a.b.Assert.NoError(err, "seal proof window PoSt partition sectors") + + mi := &miner.MinerInfo{ + Owner: owner.ID, + Worker: worker.ID, + PendingWorkerKey: nil, + PeerId: abi.PeerID("chain-validation"), + Multiaddrs: nil, + SealProofType: cfg.SealProofType, + SectorSize: ss, + WindowPoStPartitionSectors: ps, + } + infoCid, err := a.b.Stores.CBORStore.Put(context.Background(), mi) + if err != nil { + panic(err) + } + + // create the miner actor s.t. it exists in the init actors map + minerState, err := miner.ConstructState(infoCid, + cfg.PeriodBoundary, + EmptyBitfieldCid, + EmptyArrayCid, + EmptyMapCid, + EmptyDeadlinesCid, + ) + if err != nil { + panic(err) + } + + minerActorAddr := worker.NextActorAddress(0, 0) + handle := a.CreateActor(builtin.StorageMinerActorCodeID, minerActorAddr, big.Zero(), minerState) + + // assert miner actor has been created, exists in the state tree, and has an entry in the init actor. + // next update the storage power actor to track the miner + + var spa power.State + a.ActorState(builtin.StoragePowerActorAddr, &spa) + + // set the miners claim + hm, err := adt.AsMap(adt.WrapStore(context.Background(), a.b.Stores.CBORStore), spa.Claims) + if err != nil { + panic(err) + } + + // add claim for the miner + err = hm.Put(adt.AddrKey(handle.ID), &power.Claim{ + RawBytePower: abi.NewStoragePower(0), + QualityAdjPower: abi.NewTokenAmount(0), + }) + if err != nil { + panic(err) + } + + // save the claim + spa.Claims, err = hm.Root() + if err != nil { + panic(err) + } + + // update miner count + spa.MinerCount += 1 + + // update storage power actor's state in the tree + _, err = a.b.Stores.CBORStore.Put(context.Background(), &spa) + if err != nil { + panic(err) + } + + a.miners = append(a.miners, handle) + return handle, owner, worker +} + +// CreateActor creates an actor in the state tree, of the specified kind, with +// the specified address and balance, and sets its state to the supplied state. +func (a *Actors) CreateActor(code cid.Cid, addr address.Address, balance abi.TokenAmount, state runtime.CBORMarshaler) AddressHandle { + var id address.Address + if addr.Protocol() != address.ID { + var err error + id, err = a.b.StateTree.RegisterNewAddress(addr) + if err != nil { + log.Panicf("register new address for actor: %v", err) + } + } + + // Store the new state. + head, err := a.b.StateTree.Store.Put(context.Background(), state) + if err != nil { + panic(err) + } + + // Set the actor's head to point to that state. + actr := &types.Actor{ + Code: code, + Head: head, + Balance: balance, + } + if err := a.b.StateTree.SetActor(addr, actr); err != nil { + log.Panicf("setting new actor for actor: %v", err) + } + return AddressHandle{id, addr} +} + +// ActorState retrieves the state of the supplied actor, and sets it in the +// provided object. It also returns the actor's header from the state tree. +func (a *Actors) ActorState(addr address.Address, out cbg.CBORUnmarshaler) *types.Actor { + actor := a.Header(addr) + err := a.b.StateTree.Store.Get(context.Background(), actor.Head, out) + a.b.Assert.NoError(err, "failed to load state for actorr %s; head=%s", addr, actor.Head) + return actor +} + +// Header returns the actor's header from the state tree. +func (a *Actors) Header(addr address.Address) *types.Actor { + actor, err := a.b.StateTree.GetActor(addr) + a.b.Assert.NoError(err, "failed to fetch actor %s from state", addr) + return actor +} + +// Balance is a shortcut for Header(addr).Balance. +func (a *Actors) Balance(addr address.Address) abi_spec.TokenAmount { + return a.Header(addr).Balance +} + +// Head is a shortcut for Header(addr).Head. +func (a *Actors) Head(addr address.Address) cid.Cid { + return a.Header(addr).Head +} + +// Nonce is a shortcut for Header(addr).Nonce. +func (a *Actors) Nonce(addr address.Address) uint64 { + return a.Header(addr).Nonce +} + +// Code is a shortcut for Header(addr).Code. +func (a *Actors) Code(addr address.Address) cid.Cid { + return a.Header(addr).Code +} diff --git a/tvx/builders/address.go b/tvx/builders/address.go new file mode 100644 index 0000000000..4518fac7b0 --- /dev/null +++ b/tvx/builders/address.go @@ -0,0 +1,89 @@ +package builders + +import ( + "bytes" + "encoding/binary" + + "github.com/filecoin-project/go-address" + "github.com/filecoin-project/lotus/chain/actors/aerrors" + "github.com/multiformats/go-varint" +) + +// AddressHandle encapsulates both the ID and Robust addresses of an actor. +type AddressHandle struct { + ID, Robust address.Address +} + +// NextActorAddress predicts the address of the next actor created by this address. +// +// Code is adapted from vm.Runtime#NewActorAddress() +func (ah *AddressHandle) NextActorAddress(nonce, numActorsCreated uint64) address.Address { + var b bytes.Buffer + if err := ah.Robust.MarshalCBOR(&b); err != nil { + panic(aerrors.Fatalf("writing caller address into assert buffer: %v", err)) + } + + if err := binary.Write(&b, binary.BigEndian, nonce); err != nil { + panic(aerrors.Fatalf("writing nonce address into assert buffer: %v", err)) + } + if err := binary.Write(&b, binary.BigEndian, numActorsCreated); err != nil { + panic(aerrors.Fatalf("writing callSeqNum address into assert buffer: %v", err)) + } + addr, err := address.NewActorAddress(b.Bytes()) + if err != nil { + panic(aerrors.Fatalf("create actor address: %v", err)) + } + return addr +} + +// MustNewIDAddr returns an address.Address of kind ID. +func MustNewIDAddr(id uint64) address.Address { + addr, err := address.NewIDAddress(id) + if err != nil { + panic(err) + } + return addr +} + +// MustNewSECP256K1Addr returns an address.Address of kind secp256k1. +func MustNewSECP256K1Addr(pubkey string) address.Address { + // the pubkey of assert secp256k1 address is hashed for consistent length. + addr, err := address.NewSecp256k1Address([]byte(pubkey)) + if err != nil { + panic(err) + } + return addr +} + +// MustNewBLSAddr returns an address.Address of kind bls. +func MustNewBLSAddr(seed int64) address.Address { + buf := make([]byte, address.BlsPublicKeyBytes) + binary.PutVarint(buf, seed) + + addr, err := address.NewBLSAddress(buf) + if err != nil { + panic(err) + } + return addr +} + +// MustNewActorAddr returns an address.Address of kind actor. +func MustNewActorAddr(data string) address.Address { + addr, err := address.NewActorAddress([]byte(data)) + if err != nil { + panic(err) + } + return addr +} + +// MustIDFromAddress returns the integer ID from an ID address. +func MustIDFromAddress(a address.Address) uint64 { + if a.Protocol() != address.ID { + panic("must be ID protocol address") + } + id, _, err := varint.FromUvarint(a.Payload()) + if err != nil { + panic(err) + } + return id +} diff --git a/tvx/builders/asserter.go b/tvx/builders/asserter.go new file mode 100644 index 0000000000..4829dec738 --- /dev/null +++ b/tvx/builders/asserter.go @@ -0,0 +1,82 @@ +package builders + +import ( + "fmt" + "os" + + "github.com/filecoin-project/go-address" + "github.com/filecoin-project/specs-actors/actors/abi" + "github.com/stretchr/testify/require" +) + +// Asserter offers useful assertions to verify outcomes at various stages of +// the test vector creation. +type Asserter struct { + *require.Assertions + + b *Builder + stage Stage +} + +var _ require.TestingT = &Asserter{} + +func newAsserter(b *Builder, stage Stage) *Asserter { + a := &Asserter{stage: stage, b: b} + a.Assertions = require.New(a) + return a +} + +// In is assert fluid version of require.Contains. It inverts the argument order, +// such that the admissible set can be supplied through assert variadic argument. +func (a *Asserter) In(v interface{}, set ...interface{}) { + a.Contains(set, v, "set %v does not contain element %v", set, v) +} + +// BalanceEq verifies that the balance of the address equals the expected one. +func (a *Asserter) BalanceEq(addr address.Address, expected abi.TokenAmount) { + actor, err := a.b.StateTree.GetActor(addr) + a.NoError(err, "failed to fetch actor %s from state", addr) + a.Equal(expected, actor.Balance, "balances mismatch for address %s", addr) +} + +// NonceEq verifies that the nonce of the actor equals the expected one. +func (a *Asserter) NonceEq(addr address.Address, expected uint64) { + actor, err := a.b.StateTree.GetActor(addr) + a.NoError(err, "failed to fetch actor %s from state", addr) + a.Equal(expected, actor.Nonce, "expected actor %s nonce: %d, got: %d", addr, expected, actor.Nonce) +} + +// ActorExists verifies that the actor exists in the state tree. +func (a *Asserter) ActorExists(addr address.Address) { + _, err := a.b.StateTree.GetActor(addr) + a.NoError(err, "expected no error while looking up actor %s", addr) +} + +// ActorExists verifies that the actor is absent from the state tree. +func (a *Asserter) ActorMissing(addr address.Address) { + _, err := a.b.StateTree.GetActor(addr) + a.Error(err, "expected error while looking up actor %s", addr) +} + +// EveryMessageResultSatisfies verifies that every message result satisfies the +// provided predicate. +func (a *Asserter) EveryMessageResultSatisfies(predicate ApplyRetPredicate, except ...*ApplicableMessage) { + exceptm := make(map[*ApplicableMessage]struct{}, len(except)) + for _, am := range except { + exceptm[am] = struct{}{} + } + for i, m := range a.b.Messages.messages { + if _, ok := exceptm[m]; ok { + continue + } + a.NoError(predicate(m.Result), "message result predicate failed on message %d", i) + } +} + +func (a *Asserter) FailNow() { + os.Exit(1) +} + +func (a *Asserter) Errorf(format string, args ...interface{}) { + fmt.Printf("%s: "+format, append([]interface{}{a.stage}, args...)) +} diff --git a/tvx/builders/builder.go b/tvx/builders/builder.go new file mode 100644 index 0000000000..0991de5970 --- /dev/null +++ b/tvx/builders/builder.go @@ -0,0 +1,194 @@ +package builders + +import ( + "bytes" + "compress/gzip" + "context" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "log" + + "github.com/filecoin-project/lotus/chain/state" + "github.com/ipfs/go-cid" + format "github.com/ipfs/go-ipld-format" + "github.com/ipld/go-car" + + "github.com/filecoin-project/oni/tvx/lotus" + "github.com/filecoin-project/oni/tvx/schema" + ostate "github.com/filecoin-project/oni/tvx/state" +) + +type Stage string + +const ( + StagePreconditions = Stage("preconditions") + StageApplies = Stage("applies") + StageChecks = Stage("checks") + StageFinished = Stage("finished") +) + +func init() { + // disable logs, as we need a clean stdout output. + log.SetOutput(ioutil.Discard) + log.SetFlags(0) +} + +// TODO use stage.Surgeon with non-proxying blockstore. +type Builder struct { + Actors *Actors + Assert *Asserter + Messages *Messages + Driver *lotus.Driver + Root cid.Cid + Wallet *Wallet + StateTree *state.StateTree + Stores *ostate.Stores + + vector schema.TestVector + stage Stage +} + +// MessageVector creates a builder for a message-class vector. +func MessageVector(metadata *schema.Metadata) *Builder { + stores := ostate.NewLocalStores(context.Background()) + + // Create a brand new state tree. + st, err := state.NewStateTree(stores.CBORStore) + if err != nil { + panic(err) + } + + b := &Builder{ + stage: StagePreconditions, + Stores: stores, + StateTree: st, + Driver: lotus.NewDriver(context.Background()), + } + + b.Wallet = newWallet() + b.Assert = newAsserter(b, StagePreconditions) + b.Actors = &Actors{b: b} + b.Messages = &Messages{b: b} + + b.vector.Class = schema.ClassMessage + b.vector.Meta = metadata + b.vector.Pre = &schema.Preconditions{} + b.vector.Post = &schema.Postconditions{} + + b.initializeZeroState() + + return b +} + +func (b *Builder) CommitPreconditions() { + if b.stage != StagePreconditions { + panic("called CommitPreconditions at the wrong time") + } + + // capture the preroot after applying all preconditions. + preroot := b.FlushState() + + b.vector.Pre.Epoch = 0 + b.vector.Pre.StateTree = &schema.StateTree{RootCID: preroot} + + b.Root = preroot + b.stage = StageApplies + b.Assert = newAsserter(b, StageApplies) +} + +func (b *Builder) CommitApplies() { + if b.stage != StageApplies { + panic("called CommitApplies at the wrong time") + } + + for _, am := range b.Messages.All() { + // apply all messages that are pending application. + if am.Result == nil { + b.applyMessage(am) + } + } + + b.vector.Post.StateTree = &schema.StateTree{RootCID: b.Root} + b.stage = StageChecks + b.Assert = newAsserter(b, StageChecks) +} + +// applyMessage executes the provided message via the driver, records the new +// root, refreshes the state tree, and updates the underlying vector with the +// message and its receipt. +func (b *Builder) applyMessage(am *ApplicableMessage) { + var err error + am.Result, b.Root, err = b.Driver.ExecuteMessage(am.Message, b.Root, b.Stores.Blockstore, am.Epoch) + b.Assert.NoError(err) + + // replace the state tree. + b.StateTree, err = state.LoadStateTree(b.Stores.CBORStore, b.Root) + b.Assert.NoError(err) + + b.vector.ApplyMessages = append(b.vector.ApplyMessages, schema.Message{ + Bytes: MustSerialize(am.Message), + Epoch: &am.Epoch, + }) + fmt.Println(am.Result.ExitCode) + b.vector.Post.Receipts = append(b.vector.Post.Receipts, &schema.Receipt{ + ExitCode: am.Result.ExitCode, + ReturnValue: am.Result.Return, + GasUsed: am.Result.GasUsed, + }) +} + +func (b *Builder) Finish(w io.Writer) { + if b.stage != StageChecks { + panic("called Finish at the wrong time") + } + + out := new(bytes.Buffer) + gw := gzip.NewWriter(out) + if err := b.WriteCAR(gw, b.vector.Pre.StateTree.RootCID, b.vector.Post.StateTree.RootCID); err != nil { + panic(err) + } + if err := gw.Flush(); err != nil { + panic(err) + } + if err := gw.Close(); err != nil { + panic(err) + } + + b.vector.CAR = out.Bytes() + + b.stage = StageFinished + b.Assert = nil + + encoder := json.NewEncoder(w) + if err := encoder.Encode(b.vector); err != nil { + panic(err) + } +} + +// WriteCAR recursively writes the tree referenced by the root as assert CAR into the +// supplied io.Writer. +// +// TODO use state.Surgeon instead. (This is assert copy of Surgeon#WriteCAR). +func (b *Builder) WriteCAR(w io.Writer, roots ...cid.Cid) error { + 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 + } + + return car.WriteCarWithWalker(context.Background(), b.Stores.DAGService, roots, w, carWalkFn) +} + +func (b *Builder) FlushState() cid.Cid { + preroot, err := b.StateTree.Flush(context.Background()) + if err != nil { + panic(err) + } + return preroot +} diff --git a/tvx/builders/messages.go b/tvx/builders/messages.go new file mode 100644 index 0000000000..6b53183eb4 --- /dev/null +++ b/tvx/builders/messages.go @@ -0,0 +1,153 @@ +package builders + +import ( + "github.com/filecoin-project/go-address" + "github.com/filecoin-project/lotus/chain/types" + "github.com/filecoin-project/lotus/chain/vm" + "github.com/filecoin-project/specs-actors/actors/abi" + "github.com/filecoin-project/specs-actors/actors/abi/big" +) + +// TypedCall represents a call to a known built-in actor kind. +type TypedCall func() (method abi.MethodNum, params []byte) + +// Messages accumulates the messages to be executed within the test vector. +type Messages struct { + b *Builder + defaults msgOpts + + messages []*ApplicableMessage +} + +// SetDefaults sets default options for all messages. +func (m *Messages) SetDefaults(opts ...MsgOpt) *Messages { + for _, opt := range opts { + opt(&m.defaults) + } + return m +} + +// ApplicableMessage represents a message to be applied on the test vector. +type ApplicableMessage struct { + Epoch abi.ChainEpoch + Message *types.Message + Result *vm.ApplyRet +} + +func (m *Messages) Sugar() *sugarMsg { + return &sugarMsg{m} +} + +// All returns all ApplicableMessages that have been accumulated, in the same +// order they were added. +func (m *Messages) All() []*ApplicableMessage { + return m.messages +} + +// Typed adds a typed call to this message accumulator. +func (m *Messages) Typed(from, to address.Address, typedm TypedCall, opts ...MsgOpt) *ApplicableMessage { + method, params := typedm() + return m.Raw(from, to, method, params, opts...) +} + +// Raw adds a raw message to this message accumulator. +func (m *Messages) Raw(from, to address.Address, method abi.MethodNum, params []byte, opts ...MsgOpt) *ApplicableMessage { + options := m.defaults + for _, opt := range opts { + opt(&options) + } + + msg := &types.Message{ + To: to, + From: from, + Nonce: options.nonce, + Value: options.value, + Method: method, + Params: params, + GasPrice: options.gasPrice, + GasLimit: options.gasLimit, + } + + am := &ApplicableMessage{ + Epoch: options.epoch, + Message: msg, + } + + m.messages = append(m.messages, am) + return am +} + +// ApplyOne applies the provided message. The following constraints are checked: +// - all previous messages have been applied. +// - we know about this message (i.e. it has been added through Typed, Raw or Sugar). +func (m *Messages) ApplyOne(am *ApplicableMessage) { + var found bool + for _, other := range m.messages { + if am == other { + // we have scanned all preceding messages, and verified they had been applied. + // we are ready to perform the application. + found = true + break + } + // verify that preceding messages have been applied. + // this will abort if unsatisfied. + m.b.Assert.Nil(other.Result, "preceding messages must have been applied when calling Apply*; first unapplied: %v", other) + } + m.b.Assert.True(found, "ApplicableMessage not found") + m.b.applyMessage(am) +} + +// ApplyN calls ApplyOne for the supplied messages, in the order they are passed. +// The constraints described in ApplyOne apply. +func (m *Messages) ApplyN(ams ...*ApplicableMessage) { + for _, am := range ams { + m.ApplyOne(am) + } +} + +type msgOpts struct { + nonce uint64 + value big.Int + gasPrice big.Int + gasLimit int64 + epoch abi.ChainEpoch +} + +// MsgOpt is an option configuring message value, gas parameters, execution +// epoch, and other elements. +type MsgOpt func(*msgOpts) + +// Value sets a value on a message. +func Value(value big.Int) MsgOpt { + return func(opts *msgOpts) { + opts.value = value + } +} + +// Nonce sets the nonce of a message. +func Nonce(n uint64) MsgOpt { + return func(opts *msgOpts) { + opts.nonce = n + } +} + +// GasLimit sets the gas limit of a message. +func GasLimit(limit int64) MsgOpt { + return func(opts *msgOpts) { + opts.gasLimit = limit + } +} + +// GasPrice sets the gas price of a message. +func GasPrice(price int64) MsgOpt { + return func(opts *msgOpts) { + opts.gasPrice = big.NewInt(price) + } +} + +// Epoch sets the epoch in which a message is to be executed. +func Epoch(epoch abi.ChainEpoch) MsgOpt { + return func(opts *msgOpts) { + opts.epoch = epoch + } +} diff --git a/tvx/builders/messages_sugar.go b/tvx/builders/messages_sugar.go new file mode 100644 index 0000000000..59d9121d5e --- /dev/null +++ b/tvx/builders/messages_sugar.go @@ -0,0 +1,56 @@ +package builders + +import ( + "github.com/filecoin-project/go-address" + + "github.com/filecoin-project/specs-actors/actors/abi" + "github.com/filecoin-project/specs-actors/actors/builtin" + init_ "github.com/filecoin-project/specs-actors/actors/builtin/init" + "github.com/filecoin-project/specs-actors/actors/builtin/multisig" + "github.com/filecoin-project/specs-actors/actors/builtin/paych" + "github.com/filecoin-project/specs-actors/actors/builtin/power" + + "github.com/libp2p/go-libp2p-core/peer" +) + +type sugarMsg struct{ m *Messages } + +// Transfer enlists a value transfer message. +func (s *sugarMsg) Transfer(from, to address.Address, opts ...MsgOpt) *ApplicableMessage { + return s.m.Typed(from, to, Transfer(), opts...) +} + +func (s *sugarMsg) CreatePaychActor(from, to address.Address, opts ...MsgOpt) *ApplicableMessage { + ctorparams := &paych.ConstructorParams{ + From: from, + To: to, + } + return s.m.Typed(from, builtin.InitActorAddr, InitExec(&init_.ExecParams{ + CodeCID: builtin.PaymentChannelActorCodeID, + ConstructorParams: MustSerialize(ctorparams), + }), opts...) +} + +func (s *sugarMsg) CreateMultisigActor(from address.Address, signers []address.Address, unlockDuration abi.ChainEpoch, numApprovals uint64, opts ...MsgOpt) *ApplicableMessage { + ctorparams := &multisig.ConstructorParams{ + Signers: signers, + NumApprovalsThreshold: numApprovals, + UnlockDuration: unlockDuration, + } + + return s.m.Typed(from, builtin.InitActorAddr, InitExec(&init_.ExecParams{ + CodeCID: builtin.MultisigActorCodeID, + ConstructorParams: MustSerialize(ctorparams), + }), opts...) +} + +func (s *sugarMsg) CreateMinerActor(owner, worker address.Address, sealProofType abi.RegisteredSealProof, pid peer.ID, maddrs []abi.Multiaddrs, opts ...MsgOpt) *ApplicableMessage { + params := &power.CreateMinerParams{ + Worker: worker, + Owner: owner, + SealProofType: sealProofType, + Peer: abi.PeerID(pid), + Multiaddrs: maddrs, + } + return s.m.Typed(owner, builtin.StoragePowerActorAddr, PowerCreateMiner(params), opts...) +} diff --git a/tvx/builders/messages_typed.go b/tvx/builders/messages_typed.go new file mode 100644 index 0000000000..c4d9ea332e --- /dev/null +++ b/tvx/builders/messages_typed.go @@ -0,0 +1,362 @@ +package builders + +import ( + "github.com/filecoin-project/go-address" + + "github.com/filecoin-project/specs-actors/actors/abi" + "github.com/filecoin-project/specs-actors/actors/abi/big" + "github.com/filecoin-project/specs-actors/actors/util/adt" + + "github.com/filecoin-project/specs-actors/actors/builtin" + "github.com/filecoin-project/specs-actors/actors/builtin/cron" + init_ "github.com/filecoin-project/specs-actors/actors/builtin/init" + "github.com/filecoin-project/specs-actors/actors/builtin/market" + "github.com/filecoin-project/specs-actors/actors/builtin/miner" + "github.com/filecoin-project/specs-actors/actors/builtin/multisig" + "github.com/filecoin-project/specs-actors/actors/builtin/paych" + "github.com/filecoin-project/specs-actors/actors/builtin/power" + "github.com/filecoin-project/specs-actors/actors/builtin/reward" +) + +func Transfer() TypedCall { + return func() (method abi.MethodNum, params []byte) { + return builtin.MethodSend, []byte{} + } +} + +// ---------------------------------------------------------------------------- +// | ACCOUNT +// ---------------------------------------------------------------------------- + +func AccountConstructor(params *address.Address) TypedCall { + return func() (abi.MethodNum, []byte) { + return builtin.MethodsAccount.Constructor, MustSerialize(params) + } +} + +func AccountPubkeyAddress(params *adt.EmptyValue) TypedCall { + return func() (abi.MethodNum, []byte) { + return builtin.MethodsAccount.PubkeyAddress, MustSerialize(params) + } +} + +// ---------------------------------------------------------------------------- +// | MARKET +// ---------------------------------------------------------------------------- + +func MarketConstructor(params *adt.EmptyValue) TypedCall { + return func() (abi.MethodNum, []byte) { + return builtin.MethodsMarket.Constructor, MustSerialize(params) + } +} + +func MarketAddBalance(params *address.Address) TypedCall { + return func() (abi.MethodNum, []byte) { + return builtin.MethodsMarket.AddBalance, MustSerialize(params) + } +} +func MarketWithdrawBalance(params *market.WithdrawBalanceParams) TypedCall { + return func() (abi.MethodNum, []byte) { + return builtin.MethodsMarket.WithdrawBalance, MustSerialize(params) + } +} +func MarketPublishStorageDeals(params *market.PublishStorageDealsParams) TypedCall { + return func() (abi.MethodNum, []byte) { + return builtin.MethodsMarket.PublishStorageDeals, MustSerialize(params) + } +} +func MarketVerifyDealsForActivation(params *market.VerifyDealsForActivationParams) TypedCall { + return func() (abi.MethodNum, []byte) { + return builtin.MethodsMarket.VerifyDealsForActivation, MustSerialize(params) + } +} +func MarketActivateDeals(params *market.ActivateDealsParams) TypedCall { + return func() (abi.MethodNum, []byte) { + return builtin.MethodsMarket.ActivateDeals, MustSerialize(params) + } +} +func MarketOnMinerSectorsTerminate(params *market.OnMinerSectorsTerminateParams) TypedCall { + return func() (abi.MethodNum, []byte) { + return builtin.MethodsMarket.OnMinerSectorsTerminate, MustSerialize(params) + } +} +func MarketComputeDataCommitment(params *market.ComputeDataCommitmentParams) TypedCall { + return func() (abi.MethodNum, []byte) { + return builtin.MethodsMarket.ComputeDataCommitment, MustSerialize(params) + } +} +func MarketCronTick(params *adt.EmptyValue) TypedCall { + return func() (abi.MethodNum, []byte) { + return builtin.MethodsMarket.CronTick, MustSerialize(params) + } +} + +// ---------------------------------------------------------------------------- +// | MINER +// ---------------------------------------------------------------------------- + +func MinerConstructor(params *power.MinerConstructorParams) TypedCall { + return func() (abi.MethodNum, []byte) { + return builtin.MethodsMiner.Constructor, MustSerialize(params) + } +} +func MinerControlAddresses(params *adt.EmptyValue) TypedCall { + return func() (abi.MethodNum, []byte) { + return builtin.MethodsMiner.ControlAddresses, MustSerialize(params) + } +} +func MinerChangeWorkerAddress(params *miner.ChangeWorkerAddressParams) TypedCall { + return func() (abi.MethodNum, []byte) { + return builtin.MethodsMiner.ChangeWorkerAddress, MustSerialize(params) + } +} +func MinerChangePeerID(params *miner.ChangePeerIDParams) TypedCall { + return func() (abi.MethodNum, []byte) { + return builtin.MethodsMiner.ChangePeerID, MustSerialize(params) + } +} +func MinerSubmitWindowedPoSt(params *miner.SubmitWindowedPoStParams) TypedCall { + return func() (abi.MethodNum, []byte) { + return builtin.MethodsMiner.SubmitWindowedPoSt, MustSerialize(params) + } +} +func MinerPreCommitSector(params *miner.SectorPreCommitInfo) TypedCall { + return func() (abi.MethodNum, []byte) { + return builtin.MethodsMiner.PreCommitSector, MustSerialize(params) + } +} +func MinerProveCommitSector(params *miner.ProveCommitSectorParams) TypedCall { + return func() (abi.MethodNum, []byte) { + return builtin.MethodsMiner.ProveCommitSector, MustSerialize(params) + } +} +func MinerExtendSectorExpiration(params *miner.ExtendSectorExpirationParams) TypedCall { + return func() (abi.MethodNum, []byte) { + return builtin.MethodsMiner.ExtendSectorExpiration, MustSerialize(params) + } +} +func MinerTerminateSectors(params *miner.TerminateSectorsParams) TypedCall { + return func() (abi.MethodNum, []byte) { + return builtin.MethodsMiner.TerminateSectors, MustSerialize(params) + } +} +func MinerDeclareFaults(params *miner.DeclareFaultsParams) TypedCall { + return func() (abi.MethodNum, []byte) { + return builtin.MethodsMiner.DeclareFaults, MustSerialize(params) + } +} +func MinerDeclareFaultsRecovered(params *miner.DeclareFaultsRecoveredParams) TypedCall { + return func() (abi.MethodNum, []byte) { + return builtin.MethodsMiner.DeclareFaultsRecovered, MustSerialize(params) + } +} +func MinerOnDeferredCronEvent(params *miner.CronEventPayload) TypedCall { + return func() (abi.MethodNum, []byte) { + return builtin.MethodsMiner.OnDeferredCronEvent, MustSerialize(params) + } +} +func MinerCheckSectorProven(params *miner.CheckSectorProvenParams) TypedCall { + return func() (abi.MethodNum, []byte) { + return builtin.MethodsMiner.CheckSectorProven, MustSerialize(params) + } +} +func MinerAddLockedFund(params *big.Int) TypedCall { + return func() (abi.MethodNum, []byte) { + return builtin.MethodsMiner.AddLockedFund, MustSerialize(params) + } +} +func MinerReportConsensusFault(params *miner.ReportConsensusFaultParams) TypedCall { + return func() (abi.MethodNum, []byte) { + return builtin.MethodsMiner.ReportConsensusFault, MustSerialize(params) + } +} +func MinerWithdrawBalance(params *miner.WithdrawBalanceParams) TypedCall { + return func() (abi.MethodNum, []byte) { + return builtin.MethodsMiner.WithdrawBalance, MustSerialize(params) + } +} +func MinerConfirmSectorProofsValid(params *builtin.ConfirmSectorProofsParams) TypedCall { + return func() (abi.MethodNum, []byte) { + return builtin.MethodsMiner.ConfirmSectorProofsValid, MustSerialize(params) + } +} +func MinerChangeMultiaddrs(params *miner.ChangeMultiaddrsParams) TypedCall { + return func() (abi.MethodNum, []byte) { + return builtin.MethodsMiner.ChangeMultiaddrs, MustSerialize(params) + } +} + +// ---------------------------------------------------------------------------- +// | MULTISIG +// ---------------------------------------------------------------------------- + +func MultisigConstructor(params *multisig.ConstructorParams) TypedCall { + return func() (abi.MethodNum, []byte) { + return builtin.MethodsMultisig.Constructor, MustSerialize(params) + } +} +func MultisigPropose(params *multisig.ProposeParams) TypedCall { + return func() (abi.MethodNum, []byte) { + return builtin.MethodsMultisig.Propose, MustSerialize(params) + } +} +func MultisigApprove(params *multisig.TxnIDParams) TypedCall { + return func() (abi.MethodNum, []byte) { + return builtin.MethodsMultisig.Approve, MustSerialize(params) + } +} +func MultisigCancel(params *multisig.TxnIDParams) TypedCall { + return func() (abi.MethodNum, []byte) { + return builtin.MethodsMultisig.Cancel, MustSerialize(params) + } +} +func MultisigAddSigner(params *multisig.AddSignerParams) TypedCall { + return func() (abi.MethodNum, []byte) { + return builtin.MethodsMultisig.AddSigner, MustSerialize(params) + } +} +func MultisigRemoveSigner(params *multisig.RemoveSignerParams) TypedCall { + return func() (abi.MethodNum, []byte) { + return builtin.MethodsMultisig.RemoveSigner, MustSerialize(params) + } +} +func MultisigSwapSigner(params *multisig.SwapSignerParams) TypedCall { + return func() (abi.MethodNum, []byte) { + return builtin.MethodsMultisig.SwapSigner, MustSerialize(params) + } +} +func MultisigChangeNumApprovalsThreshold(params *multisig.ChangeNumApprovalsThresholdParams) TypedCall { + return func() (abi.MethodNum, []byte) { + return builtin.MethodsMultisig.ChangeNumApprovalsThreshold, MustSerialize(params) + } +} + +// ---------------------------------------------------------------------------- +// | POWER +// ---------------------------------------------------------------------------- + +func PowerConstructor(params *adt.EmptyValue) TypedCall { + return func() (abi.MethodNum, []byte) { + return builtin.MethodsPower.Constructor, MustSerialize(params) + } +} +func PowerCreateMiner(params *power.CreateMinerParams) TypedCall { + return func() (abi.MethodNum, []byte) { + return builtin.MethodsPower.CreateMiner, MustSerialize(params) + } +} +func PowerUpdateClaimedPower(params *power.UpdateClaimedPowerParams) TypedCall { + return func() (abi.MethodNum, []byte) { + return builtin.MethodsPower.UpdateClaimedPower, MustSerialize(params) + } +} +func PowerEnrollCronEvent(params *power.EnrollCronEventParams) TypedCall { + return func() (abi.MethodNum, []byte) { + return builtin.MethodsPower.EnrollCronEvent, MustSerialize(params) + } +} +func PowerOnEpochTickEnd(params *adt.EmptyValue) TypedCall { + return func() (abi.MethodNum, []byte) { + return builtin.MethodsPower.OnEpochTickEnd, MustSerialize(params) + } +} +func PowerUpdatePledgeTotal(params *big.Int) TypedCall { + return func() (abi.MethodNum, []byte) { + return builtin.MethodsPower.UpdatePledgeTotal, MustSerialize(params) + } +} +func PowerOnConsensusFault(params *big.Int) TypedCall { + return func() (abi.MethodNum, []byte) { + return builtin.MethodsPower.OnConsensusFault, MustSerialize(params) + } +} +func PowerSubmitPoRepForBulkVerify(params *abi.SealVerifyInfo) TypedCall { + return func() (abi.MethodNum, []byte) { + return builtin.MethodsPower.SubmitPoRepForBulkVerify, MustSerialize(params) + } +} +func PowerCurrentTotalPower(params *adt.EmptyValue) TypedCall { + return func() (abi.MethodNum, []byte) { + return builtin.MethodsPower.CurrentTotalPower, MustSerialize(params) + } +} + +// ---------------------------------------------------------------------------- +// | REWARD +// ---------------------------------------------------------------------------- + +func RewardConstructor(params *adt.EmptyValue) TypedCall { + return func() (abi.MethodNum, []byte) { + return builtin.MethodsReward.Constructor, MustSerialize(params) + } +} +func RewardAwardBlockReward(params *reward.AwardBlockRewardParams) TypedCall { + return func() (abi.MethodNum, []byte) { + return builtin.MethodsReward.AwardBlockReward, MustSerialize(params) + } +} +func RewardThisEpochReward(params *adt.EmptyValue) TypedCall { + return func() (abi.MethodNum, []byte) { + return builtin.MethodsReward.ThisEpochReward, MustSerialize(params) + } +} +func RewardUpdateNetworkKPI(params *big.Int) TypedCall { + return func() (abi.MethodNum, []byte) { + return builtin.MethodsReward.UpdateNetworkKPI, MustSerialize(params) + } +} + +// ---------------------------------------------------------------------------- +// | PAYCH +// ---------------------------------------------------------------------------- + +func PaychConstructor(params *paych.ConstructorParams) TypedCall { + return func() (abi.MethodNum, []byte) { + return builtin.MethodsPaych.Constructor, MustSerialize(params) + } +} +func PaychUpdateChannelState(params *paych.UpdateChannelStateParams) TypedCall { + return func() (abi.MethodNum, []byte) { + return builtin.MethodsPaych.UpdateChannelState, MustSerialize(params) + } +} +func PaychSettle(params *adt.EmptyValue) TypedCall { + return func() (abi.MethodNum, []byte) { + return builtin.MethodsPaych.Settle, MustSerialize(params) + } +} +func PaychCollect(params *adt.EmptyValue) TypedCall { + return func() (abi.MethodNum, []byte) { + return builtin.MethodsPaych.Collect, MustSerialize(params) + } +} + +// ---------------------------------------------------------------------------- +// | CRON +// ---------------------------------------------------------------------------- + +func CronConstructor(params *cron.ConstructorParams) TypedCall { + return func() (abi.MethodNum, []byte) { + return builtin.MethodsCron.Constructor, MustSerialize(params) + } +} +func CronEpochTick(params *adt.EmptyValue) TypedCall { + return func() (abi.MethodNum, []byte) { + return builtin.MethodsCron.EpochTick, MustSerialize(params) + } +} + +// ---------------------------------------------------------------------------- +// | INIT +// ---------------------------------------------------------------------------- + +func InitConstructor(params *init_.ConstructorParams) TypedCall { + return func() (abi.MethodNum, []byte) { + return builtin.MethodsInit.Constructor, MustSerialize(params) + } +} +func InitExec(params *init_.ExecParams) TypedCall { + return func() (abi.MethodNum, []byte) { + return builtin.MethodsInit.Exec, MustSerialize(params) + } +} diff --git a/tvx/builders/predicates.go b/tvx/builders/predicates.go new file mode 100644 index 0000000000..0a42ed1818 --- /dev/null +++ b/tvx/builders/predicates.go @@ -0,0 +1,19 @@ +package builders + +import ( + "fmt" + + "github.com/filecoin-project/lotus/chain/vm" + "github.com/filecoin-project/specs-actors/actors/runtime/exitcode" +) + +type ApplyRetPredicate func(ret *vm.ApplyRet) error + +func ExitCode(expect exitcode.ExitCode) ApplyRetPredicate { + return func(ret *vm.ApplyRet) error { + if ret.ExitCode == expect { + return nil + } + return fmt.Errorf("message exit code was %d; expected %d", ret.ExitCode, expect) + } +} diff --git a/tvx/builders/serialization.go b/tvx/builders/serialization.go new file mode 100644 index 0000000000..e520f880db --- /dev/null +++ b/tvx/builders/serialization.go @@ -0,0 +1,38 @@ +package builders + +import ( + "bytes" + "fmt" + + cbg "github.com/whyrusleeping/cbor-gen" +) + +func MustSerialize(i cbg.CBORMarshaler) []byte { + out, err := Serialize(i) + if err != nil { + panic(err) + } + return out +} + +func Serialize(i cbg.CBORMarshaler) ([]byte, error) { + buf := new(bytes.Buffer) + if err := i.MarshalCBOR(buf); err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +func MustDeserialize(b []byte, out interface{}) { + if err := Deserialize(b, out); err != nil { + panic(err) + } +} + +func Deserialize(b []byte, out interface{}) error { + um, ok := out.(cbg.CBORUnmarshaler) + if !ok { + return fmt.Errorf("type %T does not implement UnmarshalCBOR", out) + } + return um.UnmarshalCBOR(bytes.NewReader(b)) +} diff --git a/tvx/builders/state_zero.go b/tvx/builders/state_zero.go new file mode 100644 index 0000000000..ad55e81d76 --- /dev/null +++ b/tvx/builders/state_zero.go @@ -0,0 +1,172 @@ +package builders + +import ( + "context" + + "github.com/filecoin-project/go-address" + "github.com/filecoin-project/go-bitfield" + abi_spec "github.com/filecoin-project/specs-actors/actors/abi" + big_spec "github.com/filecoin-project/specs-actors/actors/abi/big" + builtin_spec "github.com/filecoin-project/specs-actors/actors/builtin" + account_spec "github.com/filecoin-project/specs-actors/actors/builtin/account" + cron_spec "github.com/filecoin-project/specs-actors/actors/builtin/cron" + init_spec "github.com/filecoin-project/specs-actors/actors/builtin/init" + market_spec "github.com/filecoin-project/specs-actors/actors/builtin/market" + "github.com/filecoin-project/specs-actors/actors/builtin/miner" + power_spec "github.com/filecoin-project/specs-actors/actors/builtin/power" + reward_spec "github.com/filecoin-project/specs-actors/actors/builtin/reward" + "github.com/filecoin-project/specs-actors/actors/builtin/system" + runtime_spec "github.com/filecoin-project/specs-actors/actors/runtime" + adt_spec "github.com/filecoin-project/specs-actors/actors/util/adt" + "github.com/ipfs/go-cid" +) + +const ( + totalFilecoin = 2_000_000_000 + filecoinPrecision = 1_000_000_000_000_000_000 +) + +var ( + TotalNetworkBalance = big_spec.Mul(big_spec.NewInt(totalFilecoin), big_spec.NewInt(filecoinPrecision)) + EmptyReturnValue = []byte{} +) + +var ( + // initialized by calling initializeStoreWithAdtRoots + EmptyArrayCid cid.Cid + EmptyDeadlinesCid cid.Cid + EmptyMapCid cid.Cid + EmptyMultiMapCid cid.Cid + EmptyBitfieldCid cid.Cid +) + +const ( + TestSealProofType = abi_spec.RegisteredSealProof_StackedDrg2KiBV1 +) + +func (b *Builder) initializeZeroState() { + if err := insertEmptyStructures(b.Stores.ADTStore); err != nil { + panic(err) + } + + type ActorState struct { + Addr address.Address + Balance abi_spec.TokenAmount + Code cid.Cid + State runtime_spec.CBORMarshaler + } + + var actors []ActorState + + actors = append(actors, ActorState{ + Addr: builtin_spec.InitActorAddr, + Balance: big_spec.Zero(), + Code: builtin_spec.InitActorCodeID, + State: init_spec.ConstructState(EmptyMapCid, "chain-validation"), + }) + + zeroRewardState := reward_spec.ConstructState(big_spec.Zero()) + zeroRewardState.ThisEpochReward = big_spec.NewInt(1e17) + + actors = append(actors, ActorState{ + Addr: builtin_spec.RewardActorAddr, + Balance: TotalNetworkBalance, + Code: builtin_spec.RewardActorCodeID, + State: zeroRewardState, + }) + + actors = append(actors, ActorState{ + Addr: builtin_spec.BurntFundsActorAddr, + Balance: big_spec.Zero(), + Code: builtin_spec.AccountActorCodeID, + State: &account_spec.State{Address: builtin_spec.BurntFundsActorAddr}, + }) + + actors = append(actors, ActorState{ + Addr: builtin_spec.StoragePowerActorAddr, + Balance: big_spec.Zero(), + Code: builtin_spec.StoragePowerActorCodeID, + State: power_spec.ConstructState(EmptyMapCid, EmptyMultiMapCid), + }) + + actors = append(actors, ActorState{ + Addr: builtin_spec.StorageMarketActorAddr, + Balance: big_spec.Zero(), + Code: builtin_spec.StorageMarketActorCodeID, + State: &market_spec.State{ + Proposals: EmptyArrayCid, + States: EmptyArrayCid, + PendingProposals: EmptyMapCid, + EscrowTable: EmptyMapCid, + LockedTable: EmptyMapCid, + NextID: abi_spec.DealID(0), + DealOpsByEpoch: EmptyMultiMapCid, + LastCron: 0, + }, + }) + + actors = append(actors, ActorState{ + Addr: builtin_spec.SystemActorAddr, + Balance: big_spec.Zero(), + Code: builtin_spec.SystemActorCodeID, + State: &system.State{}, + }) + + actors = append(actors, ActorState{ + Addr: builtin_spec.CronActorAddr, + Balance: big_spec.Zero(), + Code: builtin_spec.CronActorCodeID, + State: &cron_spec.State{Entries: []cron_spec.Entry{ + { + Receiver: builtin_spec.StoragePowerActorAddr, + MethodNum: builtin_spec.MethodsPower.OnEpochTickEnd, + }, + }}, + }) + + for _, act := range actors { + _ = b.Actors.CreateActor(act.Code, act.Addr, act.Balance, act.State) + } +} + +func insertEmptyStructures(store adt_spec.Store) error { + var err error + _, err = store.Put(context.TODO(), []struct{}{}) + if err != nil { + return err + } + + EmptyArrayCid, err = adt_spec.MakeEmptyArray(store).Root() + if err != nil { + return err + } + + EmptyMapCid, err = adt_spec.MakeEmptyMap(store).Root() + if err != nil { + return err + } + + EmptyMultiMapCid, err = adt_spec.MakeEmptyMultimap(store).Root() + if err != nil { + return err + } + + EmptyDeadlinesCid, err = store.Put(context.TODO(), &miner.Deadline{ + Partitions: EmptyArrayCid, + ExpirationsEpochs: EmptyArrayCid, + PostSubmissions: abi_spec.NewBitField(), + EarlyTerminations: abi_spec.NewBitField(), + LiveSectors: 0, + }) + if err != nil { + return err + } + + emptyBitfield := bitfield.NewFromSet(nil) + EmptyBitfieldCid, err = store.Put(context.TODO(), emptyBitfield) + if err != nil { + return err + } + + return nil +} diff --git a/tvx/builders/wallet.go b/tvx/builders/wallet.go new file mode 100644 index 0000000000..30fbd99076 --- /dev/null +++ b/tvx/builders/wallet.go @@ -0,0 +1,105 @@ +package builders + +import ( + "fmt" + "math/rand" + + "github.com/minio/blake2b-simd" + + "github.com/filecoin-project/go-address" + "github.com/filecoin-project/go-crypto" + acrypto "github.com/filecoin-project/specs-actors/actors/crypto" + + "github.com/filecoin-project/lotus/chain/wallet" + + "github.com/filecoin-project/lotus/chain/types" +) + +type Wallet struct { + // Private keys by address + keys map[address.Address]*wallet.Key + + // Seed for deterministic secp key generation. + secpSeed int64 + // Seed for deterministic bls key generation. + blsSeed int64 // nolint: structcheck +} + +func newWallet() *Wallet { + return &Wallet{ + keys: make(map[address.Address]*wallet.Key), + secpSeed: 0, + } +} + +func (w *Wallet) NewSECP256k1Account() address.Address { + secpKey := w.newSecp256k1Key() + w.keys[secpKey.Address] = secpKey + return secpKey.Address +} + +func (w *Wallet) NewBLSAccount() address.Address { + blsKey := w.newBLSKey() + w.keys[blsKey.Address] = blsKey + return blsKey.Address +} + +func (w *Wallet) Sign(addr address.Address, data []byte) (acrypto.Signature, error) { + ki, ok := w.keys[addr] + if !ok { + return acrypto.Signature{}, fmt.Errorf("unknown address %v", addr) + } + var sigType acrypto.SigType + if ki.Type == wallet.KTSecp256k1 { + sigType = acrypto.SigTypeBLS + hashed := blake2b.Sum256(data) + sig, err := crypto.Sign(ki.PrivateKey, hashed[:]) + if err != nil { + return acrypto.Signature{}, err + } + + return acrypto.Signature{ + Type: sigType, + Data: sig, + }, nil + } else if ki.Type == wallet.KTBLS { + panic("lotus validator cannot sign BLS messages") + } else { + panic("unknown signature type") + } + +} + +func (w *Wallet) newSecp256k1Key() *wallet.Key { + randSrc := rand.New(rand.NewSource(w.secpSeed)) + prv, err := crypto.GenerateKeyFromSeed(randSrc) + if err != nil { + panic(err) + } + w.secpSeed++ + key, err := wallet.NewKey(types.KeyInfo{ + Type: wallet.KTSecp256k1, + PrivateKey: prv, + }) + if err != nil { + panic(err) + } + return key +} + +func (w *Wallet) newBLSKey() *wallet.Key { + // FIXME: bls needs deterministic key generation + //sk := ffi.PrivateKeyGenerate(s.blsSeed) + // s.blsSeed++ + sk := [32]byte{} + sk[0] = uint8(w.blsSeed) // hack to keep gas values determinist + w.blsSeed++ + key, err := wallet.NewKey(types.KeyInfo{ + Type: wallet.KTBLS, + PrivateKey: sk[:], + }) + if err != nil { + panic(err) + } + return key +} diff --git a/tvx/lotus/driver.go b/tvx/lotus/driver.go index 518376bbb0..6c56b81b56 100644 --- a/tvx/lotus/driver.go +++ b/tvx/lotus/driver.go @@ -2,7 +2,7 @@ package lotus import ( "context" - "fmt" + "log" "github.com/filecoin-project/lotus/chain/state" "github.com/filecoin-project/lotus/chain/types" @@ -24,7 +24,7 @@ func NewDriver(ctx context.Context) *Driver { } func (d *Driver) ExecuteMessage(msg *types.Message, preroot cid.Cid, bs blockstore.Blockstore, epoch abi.ChainEpoch) (*vm.ApplyRet, cid.Cid, error) { - fmt.Println("execution sanity check") + log.Println("execution sanity check") cst := cbor.NewCborStore(bs) st, err := state.LoadStateTree(cst, preroot) if err != nil { @@ -33,12 +33,12 @@ func (d *Driver) ExecuteMessage(msg *types.Message, preroot cid.Cid, bs blocksto actor, err := st.GetActor(msg.From) if err != nil { - fmt.Println("from actor not found: ", msg.From) + log.Println("from actor not found: ", msg.From) } else { - fmt.Println("from actor found: ", actor) + log.Println("from actor found: ", actor) } - fmt.Println("creating vm") + log.Println("creating vm") lvm, err := vm.NewVM(preroot, epoch, &vmRand{}, bs, mkFakedSigSyscalls(vm.Syscalls(ffiwrapper.ProofVerifier)), nil) if err != nil { return nil, cid.Undef, err @@ -51,15 +51,15 @@ func (d *Driver) ExecuteMessage(msg *types.Message, preroot cid.Cid, bs blocksto return nil, cid.Undef, err } - fmt.Println("applying message") + log.Println("applying message") ret, err := lvm.ApplyMessage(d.ctx, msg) if err != nil { return nil, cid.Undef, err } - fmt.Printf("applied message: %+v\n", ret) + log.Printf("applied message: %+v\n", ret) - fmt.Println("flushing") + log.Println("flushing") root, err := lvm.Flush(d.ctx) return ret, root, err } diff --git a/tvx/scripts/msg_application.go b/tvx/scripts/msg_application.go new file mode 100644 index 0000000000..b1f05aa290 --- /dev/null +++ b/tvx/scripts/msg_application.go @@ -0,0 +1,247 @@ +package main + +import ( + "os" + + "github.com/filecoin-project/go-address" + "github.com/filecoin-project/specs-actors/actors/abi" + "github.com/filecoin-project/specs-actors/actors/abi/big" + "github.com/filecoin-project/specs-actors/actors/builtin/paych" + "github.com/filecoin-project/specs-actors/actors/crypto" + "github.com/filecoin-project/specs-actors/actors/runtime/exitcode" + + . "github.com/filecoin-project/oni/tvx/builders" + "github.com/filecoin-project/oni/tvx/schema" +) + +var ( + unknown = MustNewIDAddr(10000000) + balance1T = abi.NewTokenAmount(1_000_000_000_000) + transferAmnt = abi.NewTokenAmount(10) +) + +func main() { + failCoverReceiptGasCost() + failCoverOnChainSizeGasCost() + failUnknownSender() + failInvalidActorNonce() + failInvalidReceiverMethod() + failInexistentReceiver() + failCoverTransferAccountCreationGasStepwise() + failActorExecutionAborted() +} + +func failCoverReceiptGasCost() { + metadata := &schema.Metadata{ + ID: "msg-apply-fail-receipt-gas", + Version: "v1", + Desc: "fail to cover gas cost for message receipt on chain", + } + + v := MessageVector(metadata) + v.Messages.SetDefaults(GasLimit(1_000_000_000), GasPrice(1)) + + alice := v.Actors.Account(address.SECP256K1, balance1T) + v.CommitPreconditions() + + v.Messages.Sugar().Transfer(alice.ID, alice.ID, Value(transferAmnt), Nonce(0), GasLimit(8)) + v.CommitApplies() + + v.Assert.EveryMessageResultSatisfies(ExitCode(exitcode.SysErrOutOfGas)) + v.Finish(os.Stdout) +} + +func failCoverOnChainSizeGasCost() { + metadata := &schema.Metadata{ + ID: "msg-apply-fail-onchainsize-gas", + Version: "v1", + Desc: "not enough gas to pay message on-chain-size cost", + } + + v := MessageVector(metadata) + v.Messages.SetDefaults(GasLimit(1_000_000_000), GasPrice(10)) + + alice := v.Actors.Account(address.SECP256K1, balance1T) + v.CommitPreconditions() + + v.Messages.Sugar().Transfer(alice.ID, alice.ID, Value(transferAmnt), Nonce(0), GasLimit(1)) + v.CommitApplies() + + v.Assert.EveryMessageResultSatisfies(ExitCode(exitcode.SysErrOutOfGas)) + v.Finish(os.Stdout) +} + +func failUnknownSender() { + metadata := &schema.Metadata{ + ID: "msg-apply-fail-unknown-sender", + Version: "v1", + Desc: "fail due to lack of gas when sender is unknown", + } + + v := MessageVector(metadata) + v.Messages.SetDefaults(GasLimit(1_000_000_000), GasPrice(1)) + + alice := v.Actors.Account(address.SECP256K1, balance1T) + v.CommitPreconditions() + + v.Messages.Sugar().Transfer(unknown, alice.ID, Value(transferAmnt), Nonce(0)) + v.CommitApplies() + + v.Assert.EveryMessageResultSatisfies(ExitCode(exitcode.SysErrSenderInvalid)) + v.Finish(os.Stdout) +} + +func failInvalidActorNonce() { + metadata := &schema.Metadata{ + ID: "msg-apply-fail-invalid-nonce", + Version: "v1", + Desc: "invalid actor nonce", + } + + v := MessageVector(metadata) + v.Messages.SetDefaults(GasLimit(1_000_000_000), GasPrice(1)) + + alice := v.Actors.Account(address.SECP256K1, balance1T) + v.CommitPreconditions() + + // invalid nonce from known account. + msg1 := v.Messages.Sugar().Transfer(alice.ID, alice.ID, Value(transferAmnt), Nonce(1)) + + // invalid nonce from an unknown account. + msg2 := v.Messages.Sugar().Transfer(unknown, alice.ID, Value(transferAmnt), Nonce(1)) + v.CommitApplies() + + v.Assert.Equal(msg1.Result.ExitCode, exitcode.SysErrSenderStateInvalid) + v.Assert.Equal(msg2.Result.ExitCode, exitcode.SysErrSenderInvalid) + + v.Finish(os.Stdout) +} + +func failInvalidReceiverMethod() { + metadata := &schema.Metadata{ + ID: "msg-apply-fail-invalid-receiver-method", + Version: "v1", + Desc: "invalid receiver method", + } + + v := MessageVector(metadata) + v.Messages.SetDefaults(GasLimit(1_000_000_000), GasPrice(1)) + + alice := v.Actors.Account(address.SECP256K1, balance1T) + v.CommitPreconditions() + + v.Messages.Typed(alice.ID, alice.ID, MarketComputeDataCommitment(nil), Nonce(0), Value(big.Zero())) + v.CommitApplies() + + v.Assert.EveryMessageResultSatisfies(ExitCode(exitcode.SysErrInvalidMethod)) + + v.Finish(os.Stdout) +} + +func failInexistentReceiver() { + metadata := &schema.Metadata{ + ID: "msg-apply-fail-inexistent-receiver", + Version: "v1", + Desc: "inexistent receiver", + Comment: `Note that this test is not a valid message, since it is using +an unknown actor. However in the event that an invalid message isn't filtered by +block validation we need to ensure behaviour is consistent across VM implementations.`, + } + + v := MessageVector(metadata) + v.Messages.SetDefaults(GasLimit(1_000_000_000), GasPrice(1)) + + alice := v.Actors.Account(address.SECP256K1, balance1T) + v.CommitPreconditions() + + // Sending a message to non-existent ID address must produce an error. + unknownID := MustNewIDAddr(10000000) + v.Messages.Sugar().Transfer(alice.ID, unknownID, Value(transferAmnt), Nonce(0)) + + unknownActor := MustNewActorAddr("1234") + v.Messages.Sugar().Transfer(alice.ID, unknownActor, Value(transferAmnt), Nonce(1)) + v.CommitApplies() + + v.Assert.EveryMessageResultSatisfies(ExitCode(exitcode.SysErrInvalidReceiver)) + v.Finish(os.Stdout) +} + +func failCoverTransferAccountCreationGasStepwise() { + metadata := &schema.Metadata{ + ID: "msg-apply-fail-transfer-accountcreation-gas", + Version: "v1", + Desc: "fail not enough gas to cover account actor creation on transfer", + } + + v := MessageVector(metadata) + v.Messages.SetDefaults(GasLimit(1_000_000_000), GasPrice(1)) + + var alice, bob, charlie AddressHandle + alice = v.Actors.Account(address.SECP256K1, balance1T) + bob.Robust, charlie.Robust = MustNewSECP256K1Addr("1"), MustNewSECP256K1Addr("2") + v.CommitPreconditions() + + var nonce uint64 + ref := v.Messages.Sugar().Transfer(alice.Robust, bob.Robust, Value(transferAmnt), Nonce(nonce)) + nonce++ + v.Messages.ApplyOne(ref) + v.Assert.EveryMessageResultSatisfies(ExitCode(exitcode.Ok)) + + // decrease the gas cost by `gasStep` for each apply and ensure `SysErrOutOfGas` is always returned. + trueGas := ref.Result.GasUsed + gasStep := trueGas / 100 + for tryGas := trueGas - gasStep; tryGas > 0; tryGas -= gasStep { + v.Messages.Sugar().Transfer(alice.Robust, charlie.Robust, Value(transferAmnt), Nonce(nonce), GasPrice(1), GasLimit(tryGas)) + nonce++ + } + v.CommitApplies() + + v.Assert.EveryMessageResultSatisfies(ExitCode(exitcode.SysErrOutOfGas), ref) + v.Finish(os.Stdout) +} + +func failActorExecutionAborted() { + metadata := &schema.Metadata{ + ID: "msg-apply-fail-actor-execution-illegal-arg", + Version: "v1", + Desc: "abort during actor execution due to illegal argument", + } + + // Set up sender and receiver accounts. + var sender, receiver AddressHandle + var paychAddr AddressHandle + + v := MessageVector(metadata) + v.Messages.SetDefaults(GasLimit(1_000_000_000), GasPrice(1)) + + v.Actors.AccountN(address.SECP256K1, balance1T, &sender, &receiver) + paychAddr = AddressHandle{ + ID: MustNewIDAddr(MustIDFromAddress(receiver.ID) + 1), + Robust: sender.NextActorAddress(0, 0), + } + v.CommitPreconditions() + + // Construct the payment channel. + createMsg := v.Messages.Sugar().CreatePaychActor(sender.Robust, receiver.Robust, Value(abi.NewTokenAmount(10_000))) + + // Update the payment channel. + updateMsg := v.Messages.Typed(sender.Robust, paychAddr.Robust, PaychUpdateChannelState(&paych.UpdateChannelStateParams{ + Sv: paych.SignedVoucher{ + ChannelAddr: paychAddr.Robust, + TimeLockMin: abi.ChainEpoch(10), + Lane: 123, + Nonce: 1, + Amount: big.NewInt(10), + Signature: &crypto.Signature{ + Type: crypto.SigTypeBLS, + Data: []byte("Grrr im an invalid signature, I cause panics in the payment channel actor"), + }, + }}), Nonce(1), Value(big.Zero())) + + v.CommitApplies() + + v.Assert.Equal(exitcode.Ok, createMsg.Result.ExitCode) + v.Assert.Equal(exitcode.ErrIllegalArgument, updateMsg.Result.ExitCode) + + v.Finish(os.Stdout) +} diff --git a/tvx/scripts/nested.go b/tvx/scripts/nested.go new file mode 100644 index 0000000000..05eda4ebfc --- /dev/null +++ b/tvx/scripts/nested.go @@ -0,0 +1,113 @@ +package main + +import ( + "os" + "bytes" + + "github.com/filecoin-project/go-address" + "github.com/filecoin-project/lotus/chain/vm" + "github.com/filecoin-project/specs-actors/actors/abi" + "github.com/filecoin-project/specs-actors/actors/abi/big" + init_ "github.com/filecoin-project/specs-actors/actors/builtin/init" + builtin "github.com/filecoin-project/specs-actors/actors/builtin" + "github.com/filecoin-project/specs-actors/actors/runtime" + //"github.com/filecoin-project/specs-actors/actors/builtin/paych" + "github.com/filecoin-project/specs-actors/actors/builtin/multisig" + //"github.com/filecoin-project/specs-actors/actors/crypto" + "github.com/filecoin-project/specs-actors/actors/runtime/exitcode" + + . "github.com/filecoin-project/oni/tvx/builders" + "github.com/filecoin-project/oni/tvx/schema" + //"github.com/davecgh/go-spew/spew" +) + +func main() { + nestedSends_OkBasic() +} + +func nestedSends_OkBasic() { + var acctDefaultBalance = abi.NewTokenAmount(1_000_000_000_000) + var multisigBalance = abi.NewTokenAmount(1_000_000_000) + nonce := uint64(1) + + metadata := &schema.Metadata{ID: "nested-sends-ok-basic", Version: "v1", Desc: ""} + + v := MessageVector(metadata) + v.Messages.SetDefaults(GasLimit(1_000_000_000), GasPrice(1)) + + stage := prepareStage(v, acctDefaultBalance, multisigBalance) + balanceBefore := v.Actors.Balance(stage.creator) + + // Multisig sends back to the creator. + amtSent := abi.NewTokenAmount(1) + result := stage.sendOk(stage.creator, amtSent, builtin.MethodSend, nil, nonce) + + //td.AssertActor(stage.creator, big.Sub(big.Add(balanceBefore, amtSent), result.Receipt.GasUsed.Big()), nonce+1) + v.Assert.NonceEq(stage.creator, nonce+1) + v.Assert.BalanceEq(stage.creator, big.Sub(big.Add(balanceBefore, amtSent), big.NewInt(result.MessageReceipt.GasUsed))) + + v.Finish(os.Stdout) +} + + +type msStage struct { + v *Builder + creator address.Address // Address of the creator and sole signer of the multisig. + msAddr address.Address // Address of the multisig actor from which nested messages are sent. +} + +// Creates a multisig actor with its creator as sole approver. +func prepareStage(v *Builder, creatorBalance, msBalance abi.TokenAmount) *msStage { + // Set up sender and receiver accounts. + creator := v.Actors.Account(address.SECP256K1, creatorBalance) + v.CommitPreconditions() + + msg := v.Messages.Sugar().CreateMultisigActor(creator.ID, []address.Address{creator.ID}, 0, 1, Value(msBalance), Nonce(0)) + v.Messages.ApplyOne(msg) + + v.Assert.Equal(msg.Result.ExitCode, exitcode.Ok) + + // Verify init actor return. + var ret init_.ExecReturn + MustDeserialize(msg.Result.Return, &ret) + + + return &msStage{ + v: v, + creator: creator.ID, + msAddr: ret.IDAddress, + } +} + +//func (s *msStage) sendOk(to address.Address, value abi.TokenAmount, method abi.MethodNum, params runtime.CBORMarshaler, approverNonce uint64) vtypes.ApplyMessageResult { +func (s *msStage) sendOk(to address.Address, value abi.TokenAmount, method abi.MethodNum, params runtime.CBORMarshaler, approverNonce uint64) *vm.ApplyRet { + buf := bytes.Buffer{} + if params != nil { + err := params.MarshalCBOR(&buf) + if err != nil { + panic(err) + } + //require.NoError(drivers.T, err) + } + pparams := multisig.ProposeParams{ + To: to, + Value: value, + Method: method, + Params: buf.Bytes(), + } + msg := s.v.Messages.Typed(s.creator, s.msAddr, MultisigPropose(&pparams), Nonce(approverNonce), Value(big.NewInt(0))) + //result := s.driver.ApplyMessage(msg) + s.v.CommitApplies() + //s.v.Assert.Equal(exitcode_spec.Ok, result.Receipt.ExitCode) + + // all messages succeeded. + s.v.Assert.EveryMessageResultSatisfies(ExitCode(exitcode.Ok)) + + return msg.Result +} + +//func (s *msStage) state() *multisig.State { + //var msState multisig.State + //s.driver.GetActorState(s.msAddr, &msState) + //return &msState +//} diff --git a/tvx/scripts/paych.go b/tvx/scripts/paych.go new file mode 100644 index 0000000000..3d809c33cf --- /dev/null +++ b/tvx/scripts/paych.go @@ -0,0 +1,185 @@ +package main + +import ( + "os" + + "github.com/filecoin-project/go-address" + "github.com/filecoin-project/specs-actors/actors/abi" + "github.com/filecoin-project/specs-actors/actors/abi/big" + init_ "github.com/filecoin-project/specs-actors/actors/builtin/init" + "github.com/filecoin-project/specs-actors/actors/builtin/paych" + "github.com/filecoin-project/specs-actors/actors/crypto" + "github.com/filecoin-project/specs-actors/actors/runtime/exitcode" + + . "github.com/filecoin-project/oni/tvx/builders" + "github.com/filecoin-project/oni/tvx/schema" +) + +var ( + balance200B = abi.NewTokenAmount(200_000_000_000) + toSend = abi.NewTokenAmount(10_000) +) + +func main() { + happyPathCreate() + happyPathUpdate() + happyPathCollect() +} + +func happyPathCreate() { + metadata := &schema.Metadata{ID: "paych-create-ok", Version: "v1", Desc: "payment channel create"} + + v := MessageVector(metadata) + v.Messages.SetDefaults(GasLimit(1_000_000_000), GasPrice(1)) + + // Set up sender and receiver accounts. + var sender, receiver AddressHandle + v.Actors.AccountN(address.SECP256K1, balance200B, &sender, &receiver) + v.CommitPreconditions() + + // Add the constructor message. + createMsg := v.Messages.Sugar().CreatePaychActor(sender.Robust, receiver.Robust, Value(toSend)) + v.CommitApplies() + + expectedActorAddr := AddressHandle{ + ID: MustNewIDAddr(MustIDFromAddress(receiver.ID) + 1), + Robust: sender.NextActorAddress(0, 0), + } + + // Verify init actor return. + var ret init_.ExecReturn + MustDeserialize(createMsg.Result.Return, &ret) + v.Assert.Equal(expectedActorAddr.Robust, ret.RobustAddress) + v.Assert.Equal(expectedActorAddr.ID, ret.IDAddress) + + // Verify the paych state. + var state paych.State + actor := v.Actors.ActorState(ret.IDAddress, &state) + v.Assert.Equal(sender.ID, state.From) + v.Assert.Equal(receiver.ID, state.To) + v.Assert.Equal(toSend, actor.Balance) + + v.Finish(os.Stdout) +} + +func happyPathUpdate() { + metadata := &schema.Metadata{ID: "paych-update-ok", Version: "v1", Desc: "payment channel update"} + + var ( + timelock = abi.ChainEpoch(0) + lane = uint64(123) + nonce = uint64(1) + amount = big.NewInt(10) + ) + + // Set up sender and receiver accounts. + var sender, receiver AddressHandle + var paychAddr AddressHandle + + v := MessageVector(metadata) + v.Messages.SetDefaults(GasLimit(1_000_000_000), GasPrice(1)) + + v.Actors.AccountN(address.SECP256K1, balance200B, &sender, &receiver) + paychAddr = AddressHandle{ + ID: MustNewIDAddr(MustIDFromAddress(receiver.ID) + 1), + Robust: sender.NextActorAddress(0, 0), + } + v.CommitPreconditions() + + // Construct the payment channel. + createMsg := v.Messages.Sugar().CreatePaychActor(sender.Robust, receiver.Robust, Value(toSend)) + + // Update the payment channel. + v.Messages.Typed(sender.Robust, paychAddr.Robust, PaychUpdateChannelState(&paych.UpdateChannelStateParams{ + Sv: paych.SignedVoucher{ + ChannelAddr: paychAddr.Robust, + TimeLockMin: timelock, + TimeLockMax: 0, // TimeLockMax set to 0 means no timeout + Lane: lane, + Nonce: nonce, + Amount: amount, + MinSettleHeight: 0, + Signature: &crypto.Signature{ + Type: crypto.SigTypeBLS, + Data: []byte("signature goes here"), // TODO may need to generate an actual signature + }, + }}), Nonce(1), Value(big.Zero())) + + v.CommitApplies() + + // all messages succeeded. + v.Assert.EveryMessageResultSatisfies(ExitCode(exitcode.Ok)) + + // Verify init actor return. + var ret init_.ExecReturn + MustDeserialize(createMsg.Result.Return, &ret) + + // Verify the paych state. + var state paych.State + v.Actors.ActorState(ret.RobustAddress, &state) + v.Assert.Len(state.LaneStates, 1) + + ls := state.LaneStates[0] + v.Assert.Equal(amount, ls.Redeemed) + v.Assert.Equal(nonce, ls.Nonce) + v.Assert.Equal(lane, ls.ID) + + v.Finish(os.Stdout) +} + +func happyPathCollect() { + metadata := &schema.Metadata{ID: "paych-collect-ok", Version: "v1", Desc: "payment channel collect"} + + v := MessageVector(metadata) + v.Messages.SetDefaults(GasLimit(1_000_000_000), GasPrice(1)) + + // Set up sender and receiver accounts. + var sender, receiver AddressHandle + var paychAddr AddressHandle + v.Actors.AccountN(address.SECP256K1, balance200B, &sender, &receiver) + paychAddr = AddressHandle{ + ID: MustNewIDAddr(MustIDFromAddress(receiver.ID) + 1), + Robust: sender.NextActorAddress(0, 0), + } + + v.CommitPreconditions() + + // Construct the payment channel. + v.Messages.Sugar().CreatePaychActor(sender.Robust, receiver.Robust, Value(toSend)) + + // Update the payment channel. + v.Messages.Typed(sender.Robust, paychAddr.Robust, PaychUpdateChannelState(&paych.UpdateChannelStateParams{ + Sv: paych.SignedVoucher{ + ChannelAddr: paychAddr.Robust, + TimeLockMin: 0, + TimeLockMax: 0, // TimeLockMax set to 0 means no timeout + Lane: 1, + Nonce: 1, + Amount: toSend, + MinSettleHeight: 0, + Signature: &crypto.Signature{ + Type: crypto.SigTypeBLS, + Data: []byte("signature goes here"), // TODO may need to generate an actual signature + }, + }}), Nonce(1), Value(big.Zero())) + + settleMsg := v.Messages.Typed(receiver.Robust, paychAddr.Robust, PaychSettle(nil), Value(big.Zero()), Nonce(0)) + + // advance the epoch so the funds may be redeemed. + collectMsg := v.Messages.Typed(receiver.Robust, paychAddr.Robust, PaychCollect(nil), Value(big.Zero()), Nonce(1), Epoch(paych.SettleDelay)) + + v.CommitApplies() + + // all messages succeeded. + v.Assert.EveryMessageResultSatisfies(ExitCode(exitcode.Ok)) + + // receiver_balance = initial_balance + paych_send - settle_paych_msg_gas - collect_paych_msg_gas + gasUsed := big.Add(big.NewInt(settleMsg.Result.MessageReceipt.GasUsed), big.NewInt(collectMsg.Result.MessageReceipt.GasUsed)) + v.Assert.BalanceEq(receiver.Robust, big.Sub(big.Add(toSend, balance200B), gasUsed)) + + // the paych actor should have been deleted after the collect + v.Assert.ActorMissing(paychAddr.Robust) + v.Assert.ActorMissing(paychAddr.ID) + + v.Finish(os.Stdout) +} diff --git a/tvx/state/store.go b/tvx/state/store.go index e6c59e041e..6e6a82696a 100644 --- a/tvx/state/store.go +++ b/tvx/state/store.go @@ -19,8 +19,10 @@ import ( "github.com/ipfs/go-merkledag" ) -// ProxyingStores implements the ipld store where unknown items are fetched over the node API. -type ProxyingStores struct { +// Stores is a collection of the different stores and services that are needed +// to deal with the data layer of Filecoin, conveniently interlinked with one +// another. +type Stores struct { CBORStore cbor.IpldStore ADTStore adt.Store Datastore ds.Batching @@ -30,6 +32,33 @@ type ProxyingStores struct { DAGService format.DAGService } +func newStores(ctx context.Context, ds ds.Batching, bs blockstore.Blockstore) *Stores { + var ( + cborstore = cbor.NewCborStore(bs) + offl = offline.Exchange(bs) + blkserv = blockservice.New(bs, offl) + dserv = merkledag.NewDAGService(blkserv) + ) + + return &Stores{ + CBORStore: cborstore, + ADTStore: adt.WrapStore(ctx, cborstore), + Datastore: ds, + Blockstore: bs, + Exchange: offl, + BlockService: blkserv, + DAGService: dserv, + } +} + +// NewLocalStores creates a Stores object that operates entirely in-memory with +// no read-through remote fetch fallback. +func NewLocalStores(ctx context.Context) *Stores { + ds := ds.NewMapDatastore() + bs := blockstore.NewBlockstore(ds) + return newStores(ctx, ds, bs) +} + type proxyingBlockstore struct { ctx context.Context api api.FullNode @@ -60,12 +89,9 @@ func (pb *proxyingBlockstore) Get(cid cid.Cid) (blocks.Block, error) { return block, nil } -// NewProxyingStore is a blockstore that proxies get requests for unknown CIDs +// NewProxyingStore is a Stores that proxies get requests for unknown CIDs // to a Filecoin node, via the ChainReadObj RPC. -// -// It also contains all possible stores, services and gadget that IPLD -// requires (quite a handful). -func NewProxyingStore(ctx context.Context, api api.FullNode) *ProxyingStores { +func NewProxyingStore(ctx context.Context, api api.FullNode) *Stores { ds := ds.NewMapDatastore() bs := &proxyingBlockstore{ @@ -74,20 +100,5 @@ func NewProxyingStore(ctx context.Context, api api.FullNode) *ProxyingStores { Blockstore: blockstore.NewBlockstore(ds), } - var ( - cborstore = cbor.NewCborStore(bs) - offl = offline.Exchange(bs) - blkserv = blockservice.New(bs, offl) - dserv = merkledag.NewDAGService(blkserv) - ) - - return &ProxyingStores{ - CBORStore: cborstore, - ADTStore: adt.WrapStore(ctx, cborstore), - Datastore: ds, - Blockstore: bs, - Exchange: offl, - BlockService: blkserv, - DAGService: dserv, - } + return newStores(ctx, ds, bs) } diff --git a/tvx/state/surgeon.go b/tvx/state/surgeon.go index 24ced62a02..389548c0cf 100644 --- a/tvx/state/surgeon.go +++ b/tvx/state/surgeon.go @@ -26,12 +26,12 @@ import ( type Surgeon struct { ctx context.Context api api.FullNode - stores *ProxyingStores + stores *Stores } // NewSurgeon returns a state surgeon, an object used to fetch and manipulate // state. -func NewSurgeon(ctx context.Context, api api.FullNode, stores *ProxyingStores) *Surgeon { +func NewSurgeon(ctx context.Context, api api.FullNode, stores *Stores) *Surgeon { return &Surgeon{ ctx: ctx, api: api, diff --git a/tvx/suite_messages_create_actor.go b/tvx/suite_messages_create_actor.go index 34ac6be440..245c5bca9f 100644 --- a/tvx/suite_messages_create_actor.go +++ b/tvx/suite_messages_create_actor.go @@ -26,9 +26,7 @@ func suiteMessages(c *cli.Context) error { var err *multierror.Error err = multierror.Append(MessageTest_AccountActorCreation()) err = multierror.Append(MessageTest_InitActorSequentialIDAddressCreate()) - err = multierror.Append(MessageTest_MessageApplicationEdgecases()) err = multierror.Append(MessageTest_MultiSigActor()) - err = multierror.Append(MessageTest_Paych()) err = multierror.Append(MessageTest_ValueTransferSimple()) err = multierror.Append(MessageTest_ValueTransferAdvance()) err = multierror.Append(MessageTest_NestedSends()) diff --git a/tvx/suite_messages_message_application.go b/tvx/suite_messages_message_application.go deleted file mode 100644 index 9a4fbffdf5..0000000000 --- a/tvx/suite_messages_message_application.go +++ /dev/null @@ -1,267 +0,0 @@ -package main - -import ( - "os" - - abi_spec "github.com/filecoin-project/specs-actors/actors/abi" - big_spec "github.com/filecoin-project/specs-actors/actors/abi/big" - paych_spec "github.com/filecoin-project/specs-actors/actors/builtin/paych" - crypto_spec "github.com/filecoin-project/specs-actors/actors/crypto" - exitcode_spec "github.com/filecoin-project/specs-actors/actors/runtime/exitcode" - - "github.com/filecoin-project/oni/tvx/chain" - "github.com/filecoin-project/oni/tvx/drivers" -) - -func MessageTest_MessageApplicationEdgecases() error { - var aliceBal = abi_spec.NewTokenAmount(1_000_000_000_000) - var transferAmnt = abi_spec.NewTokenAmount(10) - - err := func(testname string) error { - td := drivers.NewTestDriver() - td.Vector.Meta.Desc = testname - - alice, _ := td.NewAccountActor(drivers.SECP, aliceBal) - - td.UpdatePreStateRoot() - - msg := td.MessageProducer.Transfer(alice, alice, chain.Value(transferAmnt), chain.Nonce(0), chain.GasPrice(1), chain.GasLimit(8)) - - td.ApplyFailure( - msg, - exitcode_spec.SysErrOutOfGas) - - td.MustSerialize(os.Stdout) - - return nil - }("fail to cover gas cost for message receipt on chain") - if err != nil { - return err - } - - err = func(testname string) error { - td := drivers.NewTestDriver() - td.Vector.Meta.Desc = testname - - alice, _ := td.NewAccountActor(drivers.SECP, aliceBal) - - td.UpdatePreStateRoot() - - msg := td.MessageProducer.Transfer(alice, alice, chain.Value(transferAmnt), chain.Nonce(0), chain.GasPrice(10), chain.GasLimit(1)) - - // Expect Message application to fail due to lack of gas - td.ApplyFailure( - msg, - exitcode_spec.SysErrOutOfGas) - - unknown := chain.MustNewIDAddr(10000000) - msg = td.MessageProducer.Transfer(unknown, alice, chain.Value(transferAmnt), chain.Nonce(0), chain.GasPrice(10), chain.GasLimit(1)) - - // Expect Message application to fail due to lack of gas when sender is unknown - td.ApplyFailure( - msg, - exitcode_spec.SysErrOutOfGas) - - td.MustSerialize(os.Stdout) - - return nil - }("not enough gas to pay message on-chain-size cost") - if err != nil { - return err - } - - err = func(testname string) error { - td := drivers.NewTestDriver() - td.Vector.Meta.Desc = testname - - alice, _ := td.NewAccountActor(drivers.SECP, aliceBal) - td.UpdatePreStateRoot() - - aliceNonce := uint64(0) - aliceNonceF := func() uint64 { - defer func() { aliceNonce++ }() - return aliceNonce - } - newAccountA := chain.MustNewSECP256K1Addr("1") - - msg := td.MessageProducer.Transfer(alice, newAccountA, chain.Value(transferAmnt), chain.Nonce(aliceNonceF())) - - // get the "true" gas cost of applying the message - result := td.ApplyOk(msg) - - // decrease the gas cost by `gasStep` for each apply and ensure `SysErrOutOfGas` is always returned. - trueGas := int64(result.GasUsed()) - gasStep := int64(trueGas / 100) - newAccountB := chain.MustNewSECP256K1Addr("2") - for tryGas := trueGas - gasStep; tryGas > 0; tryGas -= gasStep { - msg := td.MessageProducer.Transfer(alice, newAccountB, chain.Value(transferAmnt), chain.Nonce(aliceNonceF()), chain.GasPrice(1), chain.GasLimit(tryGas)) - - td.ApplyFailure( - msg, - exitcode_spec.SysErrOutOfGas, - ) - } - - td.MustSerialize(os.Stdout) - - return nil - }("fail not enough gas to cover account actor creation") - if err != nil { - return err - } - - err = func(testname string) error { - td := drivers.NewTestDriver() - td.Vector.Meta.Desc = testname - - alice, _ := td.NewAccountActor(drivers.SECP, aliceBal) - - td.UpdatePreStateRoot() - - msg := td.MessageProducer.Transfer(alice, alice, chain.Value(transferAmnt), chain.Nonce(1)) - - // Expect Message application to fail due to callseqnum being invalid: 1 instead of 0 - td.ApplyFailure( - msg, - exitcode_spec.SysErrSenderStateInvalid) - - unknown := chain.MustNewIDAddr(10000000) - msg = td.MessageProducer.Transfer(unknown, alice, chain.Value(transferAmnt), chain.Nonce(1)) - - // Expect message application to fail due to unknow actor when call seq num is also incorrect - td.ApplyFailure( - msg, - exitcode_spec.SysErrSenderInvalid) - - td.MustSerialize(os.Stdout) - - return nil - }("invalid actor nonce") - if err != nil { - return err - } - - err = func(testname string) error { - td := drivers.NewTestDriver() - td.Vector.Meta.Desc = testname - - const pcTimeLock = abi_spec.ChainEpoch(10) - const pcLane = uint64(123) - const pcNonce = uint64(1) - var pcAmount = big_spec.NewInt(10) - var initialBal = abi_spec.NewTokenAmount(200_000_000_000) - var toSend = abi_spec.NewTokenAmount(10_000) - var pcSig = &crypto_spec.Signature{ - Type: crypto_spec.SigTypeBLS, - Data: []byte("Grrr im an invalid signature, I cause panics in the payment channel actor"), - } - - // will create and send on payment channel - sender, _ := td.NewAccountActor(drivers.SECP, initialBal) - // will be receiver on paych - receiver, receiverID := td.NewAccountActor(drivers.SECP, initialBal) - - // the _expected_ address of the payment channel - paychAddr := chain.MustNewIDAddr(chain.MustIDFromAddress(receiverID) + 1) - createRet := td.ComputeInitActorExecReturn(sender, 0, 0, paychAddr) - - td.UpdatePreStateRoot() - - msg := td.MessageProducer.CreatePaymentChannelActor(sender, receiver, chain.Value(toSend), chain.Nonce(0)) - - td.ApplyExpect( - msg, - chain.MustSerialize(&createRet)) - - msg = td.MessageProducer.PaychUpdateChannelState(sender, paychAddr, &paych_spec.UpdateChannelStateParams{ - Sv: paych_spec.SignedVoucher{ - ChannelAddr: paychAddr, - TimeLockMin: pcTimeLock, - TimeLockMax: pcTimeLock, - SecretPreimage: nil, - Extra: nil, - Lane: pcLane, - Nonce: pcNonce, - Amount: pcAmount, - MinSettleHeight: 0, - Merges: nil, - Signature: pcSig, // construct with invalid signature - }, - }, chain.Nonce(1), chain.Value(big_spec.Zero())) - - // message application fails due to invalid argument (signature). - td.ApplyFailure( - msg, - exitcode_spec.ErrIllegalArgument) - - td.MustSerialize(os.Stdout) - - return nil - }("abort during actor execution") - if err != nil { - return err - } - - err = func(testname string) error { - td := drivers.NewTestDriver() - td.Vector.Meta.Desc = testname - - alice, _ := td.NewAccountActor(drivers.SECP, aliceBal) - - td.UpdatePreStateRoot() - - msg := td.MessageProducer.MarketComputeDataCommitment(alice, alice, nil, chain.Nonce(0)) - - // message application fails because ComputeDataCommitment isn't defined - // on the recipient actor - td.ApplyFailure( - msg, - exitcode_spec.SysErrInvalidMethod) - - td.MustSerialize(os.Stdout) - - return nil - }("invalid method for receiver") - if err != nil { - return err - } - - err = func(testname string) error { - td := drivers.NewTestDriver() - td.Vector.Meta.Desc = testname - td.Vector.Meta.Comment = "Note that this test is not a valid message, since it is using an unknown actor. However in the event that an invalid message isn't filtered by block validation we need to ensure behaviour is consistent across VM implementations." - - alice, _ := td.NewAccountActor(drivers.SECP, aliceBal) - - td.UpdatePreStateRoot() - - // Sending a message to non-existent ID address must produce an error. - unknownA := chain.MustNewIDAddr(10000000) - msg := td.MessageProducer.Transfer(alice, unknownA, chain.Value(transferAmnt), chain.Nonce(0)) - - td.ApplyFailure( - msg, - exitcode_spec.SysErrInvalidReceiver) - - // Sending a message to non-existing actor address must produce an error. - unknownB := chain.MustNewActorAddr("1234") - msg = td.MessageProducer.Transfer(alice, unknownB, chain.Value(transferAmnt), chain.Nonce(1)) - - td.ApplyFailure( - msg, - exitcode_spec.SysErrInvalidReceiver) - - td.MustSerialize(os.Stdout) - - return nil - }("receiver ID/Actor address does not exist") - if err != nil { - return err - } - - return nil - - // TODO more tests: - // - missing/mismatched params for receiver - // - various out-of-gas cases -} diff --git a/tvx/suite_messages_paych.go b/tvx/suite_messages_paych.go deleted file mode 100644 index a3c11c85e9..0000000000 --- a/tvx/suite_messages_paych.go +++ /dev/null @@ -1,185 +0,0 @@ -package main - -import ( - "os" - - abi_spec "github.com/filecoin-project/specs-actors/actors/abi" - big_spec "github.com/filecoin-project/specs-actors/actors/abi/big" - paych_spec "github.com/filecoin-project/specs-actors/actors/builtin/paych" - crypto_spec "github.com/filecoin-project/specs-actors/actors/crypto" - "github.com/stretchr/testify/assert" - - "github.com/filecoin-project/oni/tvx/chain" - "github.com/filecoin-project/oni/tvx/drivers" -) - -func MessageTest_Paych() error { - var initialBal = abi_spec.NewTokenAmount(200_000_000_000) - var toSend = abi_spec.NewTokenAmount(10_000) - - err := func(testname string) error { - td := drivers.NewTestDriver() - td.Vector.Meta.Desc = testname - - // will create and send on payment channel - sender, senderID := td.NewAccountActor(drivers.SECP, initialBal) - - // will be receiver on paych - receiver, receiverID := td.NewAccountActor(drivers.SECP, initialBal) - - td.UpdatePreStateRoot() - - // the _expected_ address of the payment channel - paychAddr := chain.MustNewIDAddr(chain.MustIDFromAddress(receiverID) + 1) - createRet := td.ComputeInitActorExecReturn(sender, 0, 0, paychAddr) - - msg := td.MessageProducer.CreatePaymentChannelActor(sender, receiver, chain.Value(toSend), chain.Nonce(0)) - - // init actor creates the payment channel - td.ApplyExpect( - msg, - chain.MustSerialize(&createRet)) - - var pcState paych_spec.State - td.GetActorState(paychAddr, &pcState) - assert.Equal(drivers.T, senderID, pcState.From) - assert.Equal(drivers.T, receiverID, pcState.To) - td.AssertBalance(paychAddr, toSend) - - td.MustSerialize(os.Stdout) - - return nil - }("happy path constructor") - if err != nil { - return err - } - - err = func(testname string) error { - td := drivers.NewTestDriver() - td.Vector.Meta.Desc = testname - - //const pcTimeLock = abi_spec.ChainEpoch(1) - const pcTimeLock = abi_spec.ChainEpoch(0) - const pcLane = uint64(123) - const pcNonce = uint64(1) - var pcAmount = big_spec.NewInt(10) - var pcSig = &crypto_spec.Signature{ - Type: crypto_spec.SigTypeBLS, - Data: []byte("signature goes here"), // TODO may need to generate an actual signature - } - - // will create and send on payment channel - sender, _ := td.NewAccountActor(drivers.SECP, initialBal) - - // will be receiver on paych - receiver, receiverID := td.NewAccountActor(drivers.SECP, initialBal) - - td.UpdatePreStateRoot() - - // the _expected_ address of the payment channel - paychAddr := chain.MustNewIDAddr(chain.MustIDFromAddress(receiverID) + 1) - createRet := td.ComputeInitActorExecReturn(sender, 0, 0, paychAddr) - - msg := td.MessageProducer.CreatePaymentChannelActor(sender, receiver, chain.Value(toSend), chain.Nonce(0)) - td.ApplyExpect( - msg, - chain.MustSerialize(&createRet)) - - msg = td.MessageProducer.PaychUpdateChannelState(sender, paychAddr, &paych_spec.UpdateChannelStateParams{ - Sv: paych_spec.SignedVoucher{ - ChannelAddr: paychAddr, - TimeLockMin: pcTimeLock, - TimeLockMax: 0, // TimeLockMax set to 0 means no timeout - SecretPreimage: nil, - Extra: nil, - Lane: pcLane, - Nonce: pcNonce, - Amount: pcAmount, - MinSettleHeight: 0, - Merges: nil, - Signature: pcSig, - }, - }, chain.Nonce(1), chain.Value(big_spec.Zero())) - td.ApplyOk(msg) - - var pcState paych_spec.State - td.GetActorState(paychAddr, &pcState) - assert.Equal(drivers.T, 1, len(pcState.LaneStates)) - ls := pcState.LaneStates[0] - assert.Equal(drivers.T, pcAmount, ls.Redeemed) - assert.Equal(drivers.T, pcNonce, ls.Nonce) - assert.Equal(drivers.T, pcLane, ls.ID) - - td.MustSerialize(os.Stdout) - - return nil - }("happy path update") - if err != nil { - return err - } - - err = func(testname string) error { - td := drivers.NewTestDriver() - td.Vector.Meta.Desc = testname - - // create the payment channel - sender, _ := td.NewAccountActor(drivers.SECP, initialBal) - receiver, receiverID := td.NewAccountActor(drivers.SECP, initialBal) - paychAddr := chain.MustNewIDAddr(chain.MustIDFromAddress(receiverID) + 1) - initRet := td.ComputeInitActorExecReturn(sender, 0, 0, paychAddr) - - td.UpdatePreStateRoot() - - msg := td.MessageProducer.CreatePaymentChannelActor(sender, receiver, chain.Value(toSend), chain.Nonce(0)) - td.ApplyExpect( - msg, - chain.MustSerialize(&initRet)) - td.AssertBalance(paychAddr, toSend) - - msg = td.MessageProducer.PaychUpdateChannelState(sender, paychAddr, &paych_spec.UpdateChannelStateParams{ - Sv: paych_spec.SignedVoucher{ - ChannelAddr: paychAddr, - TimeLockMin: abi_spec.ChainEpoch(0), - TimeLockMax: 0, // TimeLockMax set to 0 means no timeout - SecretPreimage: nil, - Extra: nil, - Lane: 1, - Nonce: 1, - Amount: toSend, // the amount that can be redeemed by receiver, - MinSettleHeight: 0, - Merges: nil, - Signature: &crypto_spec.Signature{ - Type: crypto_spec.SigTypeBLS, - Data: []byte("signature goes here"), - }, - }, - }, chain.Nonce(1), chain.Value(big_spec.Zero())) - - td.ApplyOk(msg) - - // settle the payment channel so it may be collected - - msg = td.MessageProducer.PaychSettle(receiver, paychAddr, nil, chain.Value(big_spec.Zero()), chain.Nonce(0)) - settleResult := td.ApplyOk(msg) - - // advance the epoch so the funds may be redeemed. - td.ExeCtx.Epoch += paych_spec.SettleDelay - - msg = td.MessageProducer.PaychCollect(receiver, paychAddr, nil, chain.Nonce(1), chain.Value(big_spec.Zero())) - collectResult := td.ApplyOk(msg) - - // receiver_balance = initial_balance + paych_send - settle_paych_msg_gas - collect_paych_msg_gas - td.AssertBalance(receiver, big_spec.Sub(big_spec.Sub(big_spec.Add(toSend, initialBal), settleResult.Receipt.GasUsed.Big()), collectResult.Receipt.GasUsed.Big())) - // the paych actor should have been deleted after the collect - td.AssertNoActor(paychAddr) - - td.MustSerialize(os.Stdout) - - return nil - }("happy path collect") - if err != nil { - return err - } - - return nil -}