diff --git a/tvx/builders/asserter.go b/tvx/builders/asserter.go index 4829dec73..8e026f5f0 100644 --- a/tvx/builders/asserter.go +++ b/tvx/builders/asserter.go @@ -6,6 +6,7 @@ import ( "github.com/filecoin-project/go-address" "github.com/filecoin-project/specs-actors/actors/abi" + "github.com/ipfs/go-cid" "github.com/stretchr/testify/require" ) @@ -46,6 +47,13 @@ func (a *Asserter) NonceEq(addr address.Address, expected uint64) { a.Equal(expected, actor.Nonce, "expected actor %s nonce: %d, got: %d", addr, expected, actor.Nonce) } +// HeadEq verifies that the head of the actor equals the expected one. +func (a *Asserter) HeadEq(addr address.Address, expected cid.Cid) { + actor, err := a.b.StateTree.GetActor(addr) + a.NoError(err, "failed to fetch actor %s from state", addr) + a.Equal(expected, actor.Head, "expected actor %s head: %v, got: %v", addr, expected, actor.Head) +} + // ActorExists verifies that the actor exists in the state tree. func (a *Asserter) ActorExists(addr address.Address) { _, err := a.b.StateTree.GetActor(addr) diff --git a/tvx/builders/messages_typed.go b/tvx/builders/messages_typed.go index c4d9ea332..c357ce9df 100644 --- a/tvx/builders/messages_typed.go +++ b/tvx/builders/messages_typed.go @@ -5,6 +5,7 @@ import ( "github.com/filecoin-project/specs-actors/actors/abi" "github.com/filecoin-project/specs-actors/actors/abi/big" + "github.com/filecoin-project/specs-actors/actors/puppet" "github.com/filecoin-project/specs-actors/actors/util/adt" "github.com/filecoin-project/specs-actors/actors/builtin" @@ -360,3 +361,19 @@ func InitExec(params *init_.ExecParams) TypedCall { return builtin.MethodsInit.Exec, MustSerialize(params) } } + +// ---------------------------------------------------------------------------- +// | PUPPET +// ---------------------------------------------------------------------------- + +func PuppetConstructor(params *adt.EmptyValue) TypedCall { + return func() (abi.MethodNum, []byte) { + return puppet.MethodsPuppet.Constructor, MustSerialize(params) + } +} + +func PuppetSend(params *puppet.SendParams) TypedCall { + return func() (abi.MethodNum, []byte) { + return puppet.MethodsPuppet.Send, MustSerialize(params) + } +} diff --git a/tvx/scripts/nested.go b/tvx/scripts/nested.go index 05eda4ebf..41621917e 100644 --- a/tvx/scripts/nested.go +++ b/tvx/scripts/nested.go @@ -1,35 +1,101 @@ package main import ( - "os" "bytes" + "os" + //"fmt" "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" + 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/runtime" + typegen "github.com/whyrusleeping/cbor-gen" //"github.com/filecoin-project/specs-actors/actors/crypto" + "github.com/filecoin-project/specs-actors/actors/builtin/reward" + "github.com/filecoin-project/specs-actors/actors/puppet" "github.com/filecoin-project/specs-actors/actors/runtime/exitcode" + "github.com/filecoin-project/specs-actors/actors/util/adt" . "github.com/filecoin-project/oni/tvx/builders" "github.com/filecoin-project/oni/tvx/schema" //"github.com/davecgh/go-spew/spew" ) +var ( + acctDefaultBalance = abi.NewTokenAmount(1_000_000_000_000) + multisigBalance = abi.NewTokenAmount(1_000_000_000) + nonce = uint64(1) + PuppetAddress address.Address +) + +func init() { + var err error + // the address before the burnt funds address + PuppetAddress, err = address.NewIDAddress(builtin.FirstNonSingletonActorId - 2) + if err != nil { + panic(err) + } +} + func main() { nestedSends_OkBasic() + nestedSends_OkToNewActor() + nestedSends_OkToNewActorWithInvoke() + nestedSends_OkRecursive() + nestedSends_OKNonCBORParamsWithTransfer() + + // TODO: Tests to exercise invalid "syntax" of the inner message. + // These would fail message syntax validation if the message were top-level. + // + // Some of these require handcrafting the proposal params serialization. + // - malformed address: zero-length, one-length, too-short pubkeys, invalid UVarints, ... + // - negative method num + // + // Unfortunately the multisig actor can't be used to trigger a negative-value internal transfer because + // it checks just before sending. + // We need a custom actor for staging whackier messages. + + // + // The following tests exercise invalid semantics of the inner message + // + + nestedSends_FailNonexistentIDAddress() + nestedSends_FailNonexistentActorAddress() + nestedSends_FailInvalidMethodNumNewActor() + nestedSends_FailInvalidMethodNumForActor() + + // The multisig actor checks before attempting to transfer more than its balance, so we can't exercise that + // the VM also checks this. Need a custome actor to exercise this. + //t.Run("fail insufficient funds", func(t *testing.T) { + // td := builder.Build(t) + // defer td.Complete() + // + // stage := prepareStage(td, acctDefaultBalance, multisigBalance) + // balanceBefore := td.GetBalance(stage.creator) + // + // // Attempt to transfer from the multisig more than the balance it has. + // // The proposal to do should succeed, but the inner message fail. + // amtSent := big.Add(multisigBalance, abi.NewTokenAmount(1)) + // result := stage.send(stage.creator, amtSent, builtin.MethodSend, nil, nonce) + // assert.Equal(t, exitcode_spec.Ok, result.Receipt.ExitCode) + // + // td.AssertBalance(stage.msAddr, multisigBalance) // No change. + // td.AssertBalance(stage.creator, big.Sub(balanceBefore, result.Receipt.GasUsed.Big())) // Pay gas, don't receive funds. + //}) + + nestedSends_FailMissingParams() + nestedSends_FailMismatchParams() + nestedSends_FailInnerAbort() + nestedSends_FailAbortedExec() + nestedSends_FailInsufficientFundsForTransferInInnerSend() } 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) @@ -49,9 +115,318 @@ func nestedSends_OkBasic() { v.Finish(os.Stdout) } +func nestedSends_OkToNewActor() { + metadata := &schema.Metadata{ID: "nested-sends-ok-to-new-actor", 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 to new address. + newAddr := v.Wallet.NewSECP256k1Account() + amtSent := abi.NewTokenAmount(1) + result := stage.sendOk(newAddr, amtSent, builtin.MethodSend, nil, nonce) + + v.Assert.BalanceEq(stage.msAddr, big.Sub(multisigBalance, amtSent)) + v.Assert.BalanceEq(stage.creator, big.Sub(balanceBefore, big.NewInt(result.MessageReceipt.GasUsed))) + v.Assert.BalanceEq(newAddr, amtSent) + + v.Finish(os.Stdout) +} + +func nestedSends_OkToNewActorWithInvoke() { + metadata := &schema.Metadata{ID: "nested-sends-ok-to-new-actor-with-invoke", 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 to new address and invokes pubkey method at the same time. + newAddr := v.Wallet.NewSECP256k1Account() + amtSent := abi.NewTokenAmount(1) + result := stage.sendOk(newAddr, amtSent, builtin.MethodsAccount.PubkeyAddress, nil, nonce) + // TODO: use an explicit Approve() and check the return value is the correct pubkey address + // when the multisig Approve() method plumbs through the inner exit code and value. + // https://github.com/filecoin-project/specs-actors/issues/113 + //expected := bytes.Buffer{} + //require.NoError(t, newAddr.MarshalCBOR(&expected)) + //assert.Equal(t, expected.Bytes(), result.Receipt.ReturnValue) + + v.Assert.BalanceEq(stage.msAddr, big.Sub(multisigBalance, amtSent)) + v.Assert.BalanceEq(stage.creator, big.Sub(balanceBefore, big.NewInt(result.MessageReceipt.GasUsed))) + v.Assert.BalanceEq(newAddr, amtSent) + + v.Finish(os.Stdout) +} + +func nestedSends_OkRecursive() { + metadata := &schema.Metadata{ID: "nested-sends-ok-recursive", Version: "v1", Desc: ""} + + v := MessageVector(metadata) + v.Messages.SetDefaults(GasLimit(1_000_000_000), GasPrice(1)) + + another := v.Actors.Account(address.SECP256K1, big.Zero()) + stage := prepareStage(v, acctDefaultBalance, multisigBalance) + balanceBefore := v.Actors.Balance(stage.creator) + + // Multisig sends to itself. + params := multisig.AddSignerParams{ + Signer: another.ID, + Increase: false, + } + result := stage.sendOk(stage.msAddr, big.Zero(), builtin.MethodsMultisig.AddSigner, ¶ms, nonce) + + v.Assert.BalanceEq(stage.msAddr, multisigBalance) + v.Assert.Equal(big.Sub(balanceBefore, big.NewInt(result.MessageReceipt.GasUsed)), v.Actors.Balance(stage.creator)) + + var st multisig.State + v.Actors.ActorState(stage.msAddr, &st) + v.Assert.Equal([]address.Address{stage.creator, another.ID}, st.Signers) + + v.Finish(os.Stdout) +} + +func nestedSends_OKNonCBORParamsWithTransfer() { + metadata := &schema.Metadata{ID: "nested-sends-ok-non-cbor-params-with-transfer", Version: "v1", Desc: ""} + + v := MessageVector(metadata) + v.Messages.SetDefaults(GasLimit(1_000_000_000), GasPrice(1)) + + stage := prepareStage(v, acctDefaultBalance, multisigBalance) + + newAddr := v.Wallet.NewSECP256k1Account() + amtSent := abi.NewTokenAmount(1) + // So long as the parameters are not actually used by the method, a message can carry arbitrary bytes. + params := typegen.Deferred{Raw: []byte{1, 2, 3, 4}} + stage.sendOk(newAddr, amtSent, builtin.MethodSend, ¶ms, nonce) + + v.Assert.BalanceEq(stage.msAddr, big.Sub(multisigBalance, amtSent)) + v.Assert.BalanceEq(newAddr, amtSent) + + v.Finish(os.Stdout) +} + +func nestedSends_FailNonexistentIDAddress() { + metadata := &schema.Metadata{ID: "nested-sends-fail-nonexistent-id-address", Version: "v1", Desc: ""} + + v := MessageVector(metadata) + v.Messages.SetDefaults(GasLimit(1_000_000_000), GasPrice(1)) + + stage := prepareStage(v, acctDefaultBalance, multisigBalance) + + newAddr := MustNewIDAddr(1234) + amtSent := abi.NewTokenAmount(1) + stage.sendOk(newAddr, amtSent, builtin.MethodSend, nil, nonce) + + v.Assert.BalanceEq(stage.msAddr, multisigBalance) // No change. + v.Assert.ActorMissing(newAddr) + + v.Finish(os.Stdout) +} + +func nestedSends_FailNonexistentActorAddress() { + metadata := &schema.Metadata{ID: "nested-sends-fail-nonexistent-actor-address", Version: "v1", Desc: ""} + + v := MessageVector(metadata) + v.Messages.SetDefaults(GasLimit(1_000_000_000), GasPrice(1)) + + stage := prepareStage(v, acctDefaultBalance, multisigBalance) + + newAddr := MustNewActorAddr("1234") + amtSent := abi.NewTokenAmount(1) + stage.sendOk(newAddr, amtSent, builtin.MethodSend, nil, nonce) + + v.Assert.BalanceEq(stage.msAddr, multisigBalance) // No change. + v.Assert.ActorMissing(newAddr) + + v.Finish(os.Stdout) +} + +func nestedSends_FailInvalidMethodNumNewActor() { + metadata := &schema.Metadata{ID: "nested-sends-fail-invalid-methodnum-new-actor", Version: "v1", Desc: ""} + + v := MessageVector(metadata) + v.Messages.SetDefaults(GasLimit(1_000_000_000), GasPrice(1)) + + stage := prepareStage(v, acctDefaultBalance, multisigBalance) + + newAddr := v.Wallet.NewSECP256k1Account() + amtSent := abi.NewTokenAmount(1) + stage.sendOk(newAddr, amtSent, abi.MethodNum(99), nil, nonce) + + v.Assert.BalanceEq(stage.msAddr, multisigBalance) // No change. + v.Assert.ActorMissing(newAddr) + + v.Finish(os.Stdout) +} + +func nestedSends_FailInvalidMethodNumForActor() { + metadata := &schema.Metadata{ID: "nested-sends-fail-invalid-methodnum-for-actor", 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) + + amtSent := abi.NewTokenAmount(1) + result := stage.sendOk(stage.creator, amtSent, abi.MethodNum(99), nil, nonce) + + v.Assert.BalanceEq(stage.msAddr, multisigBalance) // No change. + v.Assert.BalanceEq(stage.creator, big.Sub(balanceBefore, big.NewInt(result.MessageReceipt.GasUsed))) // Pay gas, don't receive funds. + + v.Finish(os.Stdout) +} + +func nestedSends_FailMissingParams() { + metadata := &schema.Metadata{ID: "nested-sends-fail-missing-params", 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) + + params := adt.Empty // Missing params required by AddSigner + amtSent := abi.NewTokenAmount(1) + result := stage.sendOk(stage.msAddr, amtSent, builtin.MethodsMultisig.AddSigner, params, nonce) + + v.Assert.BalanceEq(stage.creator, big.Sub(balanceBefore, big.NewInt(result.MessageReceipt.GasUsed))) + v.Assert.BalanceEq(stage.msAddr, multisigBalance) // No change. + v.Assert.Equal(1, len(stage.state().Signers)) // No new signers + + v.Finish(os.Stdout) +} + +func nestedSends_FailMismatchParams() { + metadata := &schema.Metadata{ID: "nested-sends-fail-mismatched-params", 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) + + // Wrong params for AddSigner + params := multisig.ProposeParams{ + To: stage.creator, + Value: big.Zero(), + Method: builtin.MethodSend, + Params: nil, + } + amtSent := abi.NewTokenAmount(1) + result := stage.sendOk(stage.msAddr, amtSent, builtin.MethodsMultisig.AddSigner, ¶ms, nonce) + + v.Assert.BalanceEq(stage.creator, big.Sub(balanceBefore, big.NewInt(result.MessageReceipt.GasUsed))) + v.Assert.BalanceEq(stage.msAddr, multisigBalance) // No change. + v.Assert.Equal(1, len(stage.state().Signers)) // No new signers + + v.Finish(os.Stdout) +} + +func nestedSends_FailInnerAbort() { + metadata := &schema.Metadata{ID: "nested-sends-fail-inner-abort", Version: "v1", Desc: ""} + + v := MessageVector(metadata) + v.Messages.SetDefaults(GasLimit(1_000_000_000), GasPrice(1)) + + stage := prepareStage(v, acctDefaultBalance, multisigBalance) + prevHead := v.Actors.Head(builtin.RewardActorAddr) + + // AwardBlockReward will abort unless invoked by the system actor + params := reward.AwardBlockRewardParams{ + Miner: stage.creator, + Penalty: big.Zero(), + GasReward: big.Zero(), + } + amtSent := abi.NewTokenAmount(1) + stage.sendOk(builtin.RewardActorAddr, amtSent, builtin.MethodsReward.AwardBlockReward, ¶ms, nonce) + + v.Assert.BalanceEq(stage.msAddr, multisigBalance) // No change. + v.Assert.HeadEq(builtin.RewardActorAddr, prevHead) + + v.Finish(os.Stdout) +} + +func nestedSends_FailAbortedExec() { + metadata := &schema.Metadata{ID: "nested-sends-fail-aborted-exec", Version: "v1", Desc: ""} + + v := MessageVector(metadata) + v.Messages.SetDefaults(GasLimit(1_000_000_000), GasPrice(1)) + + stage := prepareStage(v, acctDefaultBalance, multisigBalance) + prevHead := v.Actors.Head(builtin.InitActorAddr) + + // Illegal paych constructor params (addresses are not accounts) + ctorParams := paych.ConstructorParams{ + From: builtin.SystemActorAddr, + To: builtin.SystemActorAddr, + } + execParams := init_.ExecParams{ + CodeCID: builtin.PaymentChannelActorCodeID, + ConstructorParams: MustSerialize(&ctorParams), + } + + amtSent := abi.NewTokenAmount(1) + stage.sendOk(builtin.InitActorAddr, amtSent, builtin.MethodsInit.Exec, &execParams, nonce) + + v.Assert.BalanceEq(stage.msAddr, multisigBalance) // No change. + v.Assert.HeadEq(builtin.InitActorAddr, prevHead) // Init state unchanged. + + v.Finish(os.Stdout) +} + +func nestedSends_FailInsufficientFundsForTransferInInnerSend() { + metadata := &schema.Metadata{ID: "nested-sends-fail-insufficient-funds-for-transfer-in-inner-send", Version: "v1", Desc: ""} + + v := MessageVector(metadata) + v.Messages.SetDefaults(GasLimit(1_000_000_000), GasPrice(1)) + + // puppet actor has zero funds + puppetBalance := big.Zero() + _ = v.Actors.CreateActor(puppet.PuppetActorCodeID, PuppetAddress, puppetBalance, &puppet.State{}) + + alice := v.Actors.Account(address.SECP256K1, acctDefaultBalance) + bob := v.Actors.Account(address.SECP256K1, big.Zero()) + + v.CommitPreconditions() + + // alice tells the puppet actor to send funds to bob, the puppet actor has 0 balance so the inner send will fail, + // and alice will pay the gas cost. + amtSent := abi.NewTokenAmount(1) + msg := v.Messages.Typed(alice.ID, PuppetAddress, PuppetSend(&puppet.SendParams{ + To: bob.ID, + Value: amtSent, + Method: builtin.MethodSend, + Params: nil, + }), Nonce(0), Value(big.Zero())) + + v.Messages.ApplyOne(msg) + + v.CommitApplies() + + // the outer message should be applied successfully + v.Assert.Equal(exitcode.Ok, msg.Result.ExitCode) + + var puppetRet puppet.SendReturn + MustDeserialize(msg.Result.MessageReceipt.Return, &puppetRet) + + // the inner message should fail + v.Assert.Equal(exitcode.SysErrInsufficientFunds, puppetRet.Code) + + // alice should be charged for the gas cost and bob should have not received any funds. + v.Assert.BalanceEq(alice.ID, big.Sub(acctDefaultBalance, big.NewInt(msg.Result.GasUsed))) + v.Assert.BalanceEq(bob.ID, big.Zero()) + + v.Finish(os.Stdout) +} type msStage struct { - v *Builder + 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. } @@ -71,15 +446,13 @@ func prepareStage(v *Builder, creatorBalance, msBalance abi.TokenAmount) *msStag var ret init_.ExecReturn MustDeserialize(msg.Result.Return, &ret) - return &msStage{ - v: v, + 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 { @@ -87,7 +460,6 @@ func (s *msStage) sendOk(to address.Address, value abi.TokenAmount, method abi.M if err != nil { panic(err) } - //require.NoError(drivers.T, err) } pparams := multisig.ProposeParams{ To: to, @@ -96,9 +468,7 @@ func (s *msStage) sendOk(to address.Address, value abi.TokenAmount, method abi.M 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)) @@ -106,8 +476,8 @@ func (s *msStage) sendOk(to address.Address, value abi.TokenAmount, method abi.M return msg.Result } -//func (s *msStage) state() *multisig.State { - //var msState multisig.State - //s.driver.GetActorState(s.msAddr, &msState) - //return &msState -//} +func (s *msStage) state() *multisig.State { + var msState multisig.State + s.v.Actors.ActorState(s.msAddr, &msState) + return &msState +}