From 6164d16f19daeafa8ed7218e084336a8e93c55da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Kripalani?= Date: Sun, 16 Aug 2020 19:27:53 +0100 Subject: [PATCH] migrate multisig suite. (#241) --- tvx/_suite_messages_multisig.go | 362 -------------------------------- tvx/builders/actors.go | 47 +++-- tvx/builders/messages.go | 8 +- tvx/builders/messages_sugar.go | 10 +- tvx/scripts/multisig/main.go | 51 +++++ tvx/scripts/multisig/ok.go | 305 +++++++++++++++++++++++++++ tvx/scripts/nested/nested.go | 6 +- 7 files changed, 403 insertions(+), 386 deletions(-) delete mode 100644 tvx/_suite_messages_multisig.go create mode 100644 tvx/scripts/multisig/main.go create mode 100644 tvx/scripts/multisig/ok.go diff --git a/tvx/_suite_messages_multisig.go b/tvx/_suite_messages_multisig.go deleted file mode 100644 index 03b41f9f5..000000000 --- a/tvx/_suite_messages_multisig.go +++ /dev/null @@ -1,362 +0,0 @@ -package main - -import ( - "fmt" - "os" - - address "github.com/filecoin-project/go-address" - "github.com/filecoin-project/oni/tvx/chain" - "github.com/filecoin-project/oni/tvx/drivers" - 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" - multisig_spec "github.com/filecoin-project/specs-actors/actors/builtin/multisig" - exitcode_spec "github.com/filecoin-project/specs-actors/actors/runtime/exitcode" - "github.com/minio/blake2b-simd" -) - -func MessageTest_MultiSigActor() error { - err := func(testname string) error { - const numApprovals = 1 - const unlockDuration = 10 - var valueSend = abi_spec.NewTokenAmount(10) - var initialBal = abi_spec.NewTokenAmount(200000000000) - - td := drivers.NewTestDriver() - td.Vector.Meta.Desc = testname - - // creator of the multisig actor - alice, aliceID := td.NewAccountActor(drivers.SECP, initialBal) - - // expected address of the actor - multisigAddr := chain.MustNewIDAddr(1 + chain.MustIDFromAddress(aliceID)) - - preroot := td.GetStateRoot() - - createRet := td.ComputeInitActorExecReturn(alice, 0, 0, multisigAddr) - td.MustCreateAndVerifyMultisigActor(0, valueSend, multisigAddr, alice, - &multisig_spec.ConstructorParams{ - Signers: []address.Address{aliceID}, - NumApprovalsThreshold: numApprovals, - UnlockDuration: unlockDuration, - }, - exitcode_spec.Ok, chain.MustSerialize(&createRet)) - - postroot := td.GetStateRoot() - - td.Vector.CAR = td.MustMarshalGzippedCAR(preroot, postroot) - td.Vector.Pre.StateTree.RootCID = preroot - td.Vector.Post.StateTree.RootCID = postroot - - // encode and output - fmt.Fprintln(os.Stdout, string(td.Vector.MustMarshalJSON())) - - return nil - }("constructor test") - if err != nil { - return err - } - - err = func(testname string) error { - const numApprovals = 2 - const unlockDuration = 10 - var valueSend = abi_spec.NewTokenAmount(10) - var initialBal = abi_spec.NewTokenAmount(200000000000) - - td := drivers.NewTestDriver() - td.Vector.Meta.Desc = testname - - alice, aliceID := td.NewAccountActor(drivers.SECP, initialBal) - - bob, bobID := td.NewAccountActor(drivers.SECP, initialBal) - outsider, outsiderID := td.NewAccountActor(drivers.SECP, initialBal) - - multisigAddr := chain.MustNewIDAddr(1 + chain.MustIDFromAddress(outsiderID)) - - preroot := td.GetStateRoot() - - createRet := td.ComputeInitActorExecReturn(alice, 0, 0, multisigAddr) - // create the multisig actor - td.MustCreateAndVerifyMultisigActor(0, valueSend, multisigAddr, alice, - &multisig_spec.ConstructorParams{ - Signers: []address.Address{aliceID, bobID}, - NumApprovalsThreshold: numApprovals, - UnlockDuration: unlockDuration, - }, - exitcode_spec.Ok, chain.MustSerialize(&createRet)) - td.AssertBalance(multisigAddr, valueSend) - - // alice proposes that outsider should receive 'valueSend' FIL. - pparams := multisig_spec.ProposeParams{ - To: outsider, - Value: valueSend, - Method: builtin_spec.MethodSend, - Params: nil, - } - - // propose the transaction and assert it exists in the actor state - txID0 := multisig_spec.TxnID(0) - expected := multisig_spec.ProposeReturn{ - TxnID: 0, - Applied: false, - Code: 0, - Ret: nil, - } - td.ApplyExpect( - td.MessageProducer.MultisigPropose(alice, multisigAddr, &pparams, chain.Nonce(1)), - chain.MustSerialize(&expected)) - - txn0 := multisig_spec.Transaction{ - To: pparams.To, - Value: pparams.Value, - Method: pparams.Method, - Params: pparams.Params, - Approved: []address.Address{aliceID}, - } - ph := mustMakeProposalHash(&txn0) - td.AssertMultisigTransaction(multisigAddr, txID0, txn0) - - // bob cancels alice's transaction. This fails as bob did not create alice's transaction. - td.ApplyFailure( - td.MessageProducer.MultisigCancel(bob, multisigAddr, &multisig_spec.TxnIDParams{ID: txID0, ProposalHash: ph}, chain.Nonce(0)), - exitcode_spec.ErrForbidden) - - // alice cancels their transaction. The outsider doesn't receive any FIL, the multisig actor's balance is empty, and the - // transaction is canceled. - td.ApplyOk( - td.MessageProducer.MultisigCancel(alice, multisigAddr, &multisig_spec.TxnIDParams{ID: txID0, ProposalHash: ph}, chain.Nonce(2)), - ) - td.AssertMultisigState(multisigAddr, multisig_spec.State{ - Signers: []address.Address{aliceID, bobID}, - NumApprovalsThreshold: numApprovals, - NextTxnID: 1, - InitialBalance: valueSend, - StartEpoch: 1, - UnlockDuration: unlockDuration, - }) - td.AssertBalance(multisigAddr, valueSend) - - postroot := td.GetStateRoot() - - td.Vector.CAR = td.MustMarshalGzippedCAR(preroot, postroot) - td.Vector.Pre.StateTree.RootCID = preroot - td.Vector.Post.StateTree.RootCID = postroot - - // encode and output - fmt.Fprintln(os.Stdout, string(td.Vector.MustMarshalJSON())) - - return nil - }("propose and cancel") - if err != nil { - return err - } - - err = func(testname string) error { - td := drivers.NewTestDriver() - td.Vector.Meta.Desc = testname - - var initialBal = abi_spec.NewTokenAmount(200000000000) - const numApprovals = 2 - const unlockDuration = 1 - var valueSend = abi_spec.NewTokenAmount(10) - - // Signers - alice, aliceID := td.NewAccountActor(drivers.SECP, initialBal) - bob, bobID := td.NewAccountActor(drivers.SECP, initialBal) - - // Not Signer - outsider, outsiderID := td.NewAccountActor(drivers.SECP, initialBal) - - preroot := td.GetStateRoot() - - // Multisig actor address - multisigAddr := chain.MustNewIDAddr(1 + chain.MustIDFromAddress(outsiderID)) - createRet := td.ComputeInitActorExecReturn(alice, 0, 0, multisigAddr) - - // create the multisig actor - td.MustCreateAndVerifyMultisigActor(0, valueSend, multisigAddr, alice, - &multisig_spec.ConstructorParams{ - Signers: []address.Address{aliceID, bobID}, - NumApprovalsThreshold: numApprovals, - UnlockDuration: unlockDuration, - }, - exitcode_spec.Ok, chain.MustSerialize(&createRet)) - - // setup propose expected values and params - pparams := multisig_spec.ProposeParams{ - To: outsider, - Value: valueSend, - Method: builtin_spec.MethodSend, - Params: nil, - } - - // propose the transaction and assert it exists in the actor state - txID0 := multisig_spec.TxnID(0) - expectedPropose := multisig_spec.ProposeReturn{ - TxnID: 0, - Applied: false, - Code: 0, - Ret: nil, - } - td.ApplyExpect( - td.MessageProducer.MultisigPropose(alice, multisigAddr, &pparams, chain.Nonce(1)), - chain.MustSerialize(&expectedPropose)) - - txn0 := multisig_spec.Transaction{ - To: pparams.To, - Value: pparams.Value, - Method: pparams.Method, - Params: pparams.Params, - Approved: []address.Address{aliceID}, - } - ph := mustMakeProposalHash(&txn0) - td.AssertMultisigTransaction(multisigAddr, txID0, txn0) - - // outsider proposes themselves to receive 'valueSend' FIL. This fails as they are not a signer. - td.ApplyFailure( - td.MessageProducer.MultisigPropose(outsider, multisigAddr, &pparams, chain.Nonce(0)), - exitcode_spec.ErrForbidden) - - // outsider approves the value transfer alice sent. This fails as they are not a signer. - td.ApplyFailure( - td.MessageProducer.MultisigApprove(outsider, multisigAddr, &multisig_spec.TxnIDParams{ID: txID0, ProposalHash: ph}, chain.Nonce(1)), - exitcode_spec.ErrForbidden) - - // increment the epoch to unlock the funds - td.ExeCtx.Epoch += unlockDuration - balanceBefore := td.GetBalance(outsider) - - // bob approves transfer of 'valueSend' FIL to outsider. - expectedApprove := multisig_spec.ApproveReturn{ - Applied: true, - Code: 0, - Ret: nil, - } - td.ApplyExpect( - td.MessageProducer.MultisigApprove(bob, multisigAddr, &multisig_spec.TxnIDParams{ID: txID0, ProposalHash: ph}, chain.Nonce(0)), - chain.MustSerialize(&expectedApprove)) - - txID1 := multisig_spec.TxnID(1) - td.AssertMultisigState(multisigAddr, multisig_spec.State{ - Signers: []address.Address{aliceID, bobID}, - NumApprovalsThreshold: numApprovals, - NextTxnID: txID1, - InitialBalance: valueSend, - StartEpoch: 1, - UnlockDuration: unlockDuration, - }) - td.AssertMultisigContainsTransaction(multisigAddr, txID0, false) - // Multisig balance has been transferred to outsider. - td.AssertBalance(multisigAddr, big_spec.Zero()) - td.AssertBalance(outsider, big_spec.Add(balanceBefore, valueSend)) - - postroot := td.GetStateRoot() - - td.Vector.CAR = td.MustMarshalGzippedCAR(preroot, postroot) - td.Vector.Pre.StateTree.RootCID = preroot - td.Vector.Post.StateTree.RootCID = postroot - - // encode and output - fmt.Fprintln(os.Stdout, string(td.Vector.MustMarshalJSON())) - - return nil - }("propose and approve") - if err != nil { - return err - } - - err = func(testname string) error { - const initialNumApprovals = 1 - var msValue = abi_spec.NewTokenAmount(100000000000) - var initialBal = abi_spec.NewTokenAmount(200000000000) - - td := drivers.NewTestDriver() - td.Vector.Meta.Desc = testname - - alice, aliceID := td.NewAccountActor(drivers.SECP, initialBal) // 101 - _, bobID := td.NewAccountActor(drivers.SECP, initialBal) // 102 - var initialSigners = []address.Address{aliceID} - - multisigAddr := chain.MustNewIDAddr(1 + chain.MustIDFromAddress(bobID)) - - preroot := td.GetStateRoot() - - createRet := td.ComputeInitActorExecReturn(alice, 0, 0, multisigAddr) - - td.MustCreateAndVerifyMultisigActor(0, msValue, multisigAddr, alice, - &multisig_spec.ConstructorParams{ - Signers: initialSigners, - NumApprovalsThreshold: initialNumApprovals, - UnlockDuration: 0, - }, - exitcode_spec.Ok, - chain.MustSerialize(&createRet), - ) - - addSignerParams := multisig_spec.AddSignerParams{ - Signer: bobID, - Increase: false, - } - - // alice fails to call directly since AddSigner - td.ApplyFailure( - td.MessageProducer.MultisigAddSigner(alice, multisigAddr, &addSignerParams, chain.Nonce(1)), - exitcode_spec.SysErrForbidden, - ) - - // AddSigner must be staged through the multisig itself - // Alice proposes the AddSigner. - // Since approvals = 1 this auto-approves the transaction. - expected := multisig_spec.ProposeReturn{ - TxnID: 0, - Applied: true, - Code: 0, - Ret: nil, - } - td.ApplyExpect( - td.MessageProducer.MultisigPropose(alice, multisigAddr, &multisig_spec.ProposeParams{ - To: multisigAddr, - Value: big_spec.Zero(), - Method: builtin_spec.MethodsMultisig.AddSigner, - Params: chain.MustSerialize(&addSignerParams), - }, chain.Nonce(2)), - chain.MustSerialize(&expected), - ) - - // TODO also exercise the approvals = 2 case with explicit approval. - - // Check that bob is now a signer - td.AssertMultisigState(multisigAddr, multisig_spec.State{ - Signers: append(initialSigners, bobID), - NumApprovalsThreshold: initialNumApprovals, - NextTxnID: multisig_spec.TxnID(1), - InitialBalance: big_spec.Zero(), - StartEpoch: 0, - UnlockDuration: 0, - }) - - postroot := td.GetStateRoot() - - td.Vector.CAR = td.MustMarshalGzippedCAR(preroot, postroot) - td.Vector.Pre.StateTree.RootCID = preroot - td.Vector.Post.StateTree.RootCID = postroot - - // encode and output - fmt.Fprintln(os.Stdout, string(td.Vector.MustMarshalJSON())) - - return nil - }("add signer") - if err != nil { - return err - } - - return nil -} - -func mustMakeProposalHash(txn *multisig_spec.Transaction) []byte { - txnHash, err := multisig_spec.ComputeProposalHash(txn, blake2b.Sum256) - if err != nil { - panic(err) - } - return txnHash -} diff --git a/tvx/builders/actors.go b/tvx/builders/actors.go index bf72c39c5..ba5ab7bdc 100644 --- a/tvx/builders/actors.go +++ b/tvx/builders/actors.go @@ -18,30 +18,37 @@ import ( cbg "github.com/whyrusleeping/cbor-gen" ) +type registeredActor struct { + handle AddressHandle + initial abi.TokenAmount +} + // Actors is an object that manages actors in the test vector. type Actors struct { // registered stores registered actors and their initial balances. - registered map[AddressHandle]abi.TokenAmount + registered []registeredActor b *Builder } func newActors(b *Builder) *Actors { - return &Actors{ - registered: make(map[AddressHandle]abi.TokenAmount), - b: b, - } + return &Actors{b: b} +} + +// Count returns the number of actors registered during preconditions. +func (a *Actors) Count() int { + return len(a.registered) } // HandleFor gets the canonical handle for a registered address, which can // appear at either ID or Robust position. func (a *Actors) HandleFor(addr address.Address) AddressHandle { - for h := range a.registered { - if h.ID == addr || h.Robust == addr { - return h + for _, r := range a.registered { + if r.handle.ID == addr || r.handle.Robust == addr { + return r.handle } } - a.b.Assert.FailNowf("asked for initial balance of unknown actor", "actor: %s", addr) + a.b.Assert.FailNowf("asked for handle of unknown actor", "actor: %s", addr) return AddressHandle{} // will never reach here. } @@ -49,8 +56,22 @@ func (a *Actors) HandleFor(addr address.Address) AddressHandle { // during preconditions. It matches against both the ID and Robust // addresses. It records an assertion failure if the actor is unknown. func (a *Actors) InitialBalance(addr address.Address) abi.TokenAmount { - handle := a.HandleFor(addr) - return a.registered[handle] + for _, r := range a.registered { + if r.handle.ID == addr || r.handle.Robust == addr { + return r.initial + } + } + a.b.Assert.FailNowf("asked for initial balance of unknown actor", "actor: %s", addr) + return big.Zero() // will never reach here. +} + +// Handles returns the AddressHandles for all registered actors. +func (a *Actors) Handles() []AddressHandle { + ret := make([]AddressHandle, 0, len(a.registered)) + for _, r := range a.registered { + ret = append(ret, r.handle) + } + return ret } // AccountN creates many account actors of the specified kind, with the @@ -78,7 +99,7 @@ func (a *Actors) Account(typ address.Protocol, balance abi.TokenAmount) AddressH actorState := &account.State{Address: addr} handle := a.CreateActor(builtin.AccountActorCodeID, addr, balance, actorState) - a.registered[handle] = balance + a.registered = append(a.registered, registeredActor{handle, balance}) return handle } @@ -169,7 +190,7 @@ func (a *Actors) Miner(cfg MinerActorCfg) (minerActor, owner, worker AddressHand panic(err) } - a.registered[handle] = big.Zero() + a.registered = append(a.registered, registeredActor{handle, big.Zero()}) return handle, owner, worker } diff --git a/tvx/builders/messages.go b/tvx/builders/messages.go index 5cebe5dfd..58b91b4cf 100644 --- a/tvx/builders/messages.go +++ b/tvx/builders/messages.go @@ -85,7 +85,11 @@ func (m *Messages) Raw(from, to address.Address, method abi.MethodNum, params [] // - 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 { + for i, other := range m.messages { + if other.Result != nil { + // message has been applied, continue. + continue + } if am == other { // we have scanned all preceding messages, and verified they had been applied. // we are ready to perform the application. @@ -94,7 +98,7 @@ func (m *Messages) ApplyOne(am *ApplicableMessage) { } // 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.Nil(other.Result, "preceding messages must have been applied when calling Apply*; index of first unapplied: %d", i) } m.b.Assert.True(found, "ApplicableMessage not found") m.b.applyMessage(am) diff --git a/tvx/builders/messages_sugar.go b/tvx/builders/messages_sugar.go index 59d9121d5..bf6982db6 100644 --- a/tvx/builders/messages_sugar.go +++ b/tvx/builders/messages_sugar.go @@ -31,16 +31,10 @@ func (s *sugarMsg) CreatePaychActor(from, to address.Address, opts ...MsgOpt) *A }), 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, - } - +func (s *sugarMsg) CreateMultisigActor(from address.Address, params *multisig.ConstructorParams, opts ...MsgOpt) *ApplicableMessage { return s.m.Typed(from, builtin.InitActorAddr, InitExec(&init_.ExecParams{ CodeCID: builtin.MultisigActorCodeID, - ConstructorParams: MustSerialize(ctorparams), + ConstructorParams: MustSerialize(params), }), opts...) } diff --git a/tvx/scripts/multisig/main.go b/tvx/scripts/multisig/main.go new file mode 100644 index 000000000..f8885c832 --- /dev/null +++ b/tvx/scripts/multisig/main.go @@ -0,0 +1,51 @@ +package main + +import ( + . "github.com/filecoin-project/oni/tvx/builders" + "github.com/filecoin-project/oni/tvx/schema" +) + +const ( + gasLimit = 1_000_000_000 + gasFeeCap = 200 +) + +func main() { + g := NewGenerator() + defer g.Wait() + + g.MessageVectorGroup("basic", + &MessageVectorGenItem{ + Metadata: &schema.Metadata{ + ID: "ok-create", + Version: "v1", + Desc: "multisig actor constructor ok", + }, + Func: constructor, + }, + &MessageVectorGenItem{ + Metadata: &schema.Metadata{ + ID: "ok-propose-and-cancel", + Version: "v1", + Desc: "multisig actor propose and cancel ok", + }, + Func: proposeAndCancelOk, + }, + &MessageVectorGenItem{ + Metadata: &schema.Metadata{ + ID: "ok-propose-and-approve", + Version: "v1", + Desc: "multisig actor propose, unauthorized proposals+approval, and approval ok", + }, + Func: proposeAndApprove, + }, + &MessageVectorGenItem{ + Metadata: &schema.Metadata{ + ID: "ok-add-signer", + Version: "v1", + Desc: "multisig actor accepts only AddSigner messages that go through a reflexive flow", + }, + Func: addSigner, + }, + ) +} diff --git a/tvx/scripts/multisig/ok.go b/tvx/scripts/multisig/ok.go new file mode 100644 index 000000000..d76bce8eb --- /dev/null +++ b/tvx/scripts/multisig/ok.go @@ -0,0 +1,305 @@ +package main + +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/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/runtime/exitcode" + "github.com/filecoin-project/specs-actors/actors/util/adt" + + "github.com/filecoin-project/go-address" + "github.com/minio/blake2b-simd" + + . "github.com/filecoin-project/oni/tvx/builders" +) + +func constructor(v *Builder) { + var balance = abi.NewTokenAmount(1_000_000_000_000) + var amount = abi.NewTokenAmount(10) + + v.Messages.SetDefaults(GasLimit(gasLimit), GasPremium(1), GasFeeCap(gasFeeCap)) + + // Set up one account. + alice := v.Actors.Account(address.SECP256K1, balance) + v.CommitPreconditions() + + createMultisig(v, alice, []address.Address{alice.ID}, 1, Value(amount), Nonce(0)) + v.CommitApplies() +} + +func proposeAndCancelOk(v *Builder) { + var ( + initial = abi.NewTokenAmount(1_000_000_000_000) + amount = abi.NewTokenAmount(10) + unlockDuration = abi.ChainEpoch(10) + ) + + v.Messages.SetDefaults(Value(big.Zero()), Epoch(1), GasLimit(gasLimit), GasPremium(1), GasFeeCap(gasFeeCap)) + + // Set up three accounts: alice and bob (signers), and charlie (outsider). + var alice, bob, charlie AddressHandle + v.Actors.AccountN(address.SECP256K1, initial, &alice, &bob, &charlie) + v.CommitPreconditions() + + // create the multisig actor; created by alice. + multisigAddr := createMultisig(v, alice, []address.Address{alice.ID, bob.ID}, 2, Value(amount), Nonce(0)) + + // alice proposes that charlie should receive 'amount' FIL. + hash := proposeOk(v, proposeOpts{ + multisigAddr: multisigAddr, + sender: alice.ID, + recipient: charlie.ID, + amount: amount, + }, Nonce(1)) + + // bob cancels alice's transaction. This fails as bob did not create alice's transaction. + bobCancelMsg := v.Messages.Typed(bob.ID, multisigAddr, MultisigCancel(&multisig.TxnIDParams{ + ID: multisig.TxnID(0), + ProposalHash: hash, + }), Nonce(0)) + v.Messages.ApplyOne(bobCancelMsg) + v.Assert.Equal(bobCancelMsg.Result.ExitCode, exitcode.ErrForbidden) + + // alice cancels their transaction; charlie doesn't receive any FIL, + // the multisig actor's balance is empty, and the transaction is canceled. + aliceCancelMsg := v.Messages.Typed(alice.ID, multisigAddr, MultisigCancel(&multisig.TxnIDParams{ + ID: multisig.TxnID(0), + ProposalHash: hash, + }), Nonce(2)) + v.Messages.ApplyOne(aliceCancelMsg) + v.Assert.Equal(exitcode.Ok, aliceCancelMsg.Result.ExitCode) + + v.CommitApplies() + + // verify balance is untouched. + v.Assert.BalanceEq(multisigAddr, amount) + + // reload the multisig state and verify + var multisigState multisig.State + v.Actors.ActorState(multisigAddr, &multisigState) + v.Assert.Equal(&multisig.State{ + Signers: []address.Address{alice.ID, bob.ID}, + NumApprovalsThreshold: 2, + NextTxnID: 1, + InitialBalance: amount, + StartEpoch: 1, + UnlockDuration: unlockDuration, + PendingTxns: EmptyMapCid, + }, &multisigState) +} + +func proposeAndApprove(v *Builder) { + var ( + initial = abi.NewTokenAmount(1_000_000_000_000) + amount = abi.NewTokenAmount(10) + unlockDuration = abi.ChainEpoch(10) + ) + + v.Messages.SetDefaults(Value(big.Zero()), Epoch(1), GasLimit(gasLimit), GasPremium(1), GasFeeCap(gasFeeCap)) + + // Set up three accounts: alice and bob (signers), and charlie (outsider). + var alice, bob, charlie AddressHandle + v.Actors.AccountN(address.SECP256K1, initial, &alice, &bob, &charlie) + v.CommitPreconditions() + + // create the multisig actor; created by alice. + multisigAddr := createMultisig(v, alice, []address.Address{alice.ID, bob.ID}, 2, Value(amount), Nonce(0)) + + // alice proposes that charlie should receive 'amount' FIL. + hash := proposeOk(v, proposeOpts{ + multisigAddr: multisigAddr, + sender: alice.ID, + recipient: charlie.ID, + amount: amount, + }, Nonce(1)) + + // charlie proposes himself -> fails. + charliePropose := v.Messages.Typed(charlie.ID, multisigAddr, + MultisigPropose(&multisig.ProposeParams{ + To: charlie.ID, + Value: amount, + Method: builtin.MethodSend, + Params: nil, + }), Nonce(0)) + v.Messages.ApplyOne(charliePropose) + v.Assert.Equal(exitcode.ErrForbidden, charliePropose.Result.ExitCode) + + // charlie attempts to accept the pending transaction -> fails. + charlieApprove := v.Messages.Typed(charlie.ID, multisigAddr, + MultisigApprove(&multisig.TxnIDParams{ + ID: multisig.TxnID(0), + ProposalHash: hash, + }), Nonce(1)) + v.Messages.ApplyOne(charlieApprove) + v.Assert.Equal(exitcode.ErrForbidden, charlieApprove.Result.ExitCode) + + // bob approves transfer of 'amount' FIL to charlie. + // epoch is unlockDuration + 1 + bobApprove := v.Messages.Typed(bob.ID, multisigAddr, + MultisigApprove(&multisig.TxnIDParams{ + ID: multisig.TxnID(0), + ProposalHash: hash, + }), Nonce(0), Epoch(unlockDuration+1)) + v.Messages.ApplyOne(bobApprove) + v.Assert.Equal(exitcode.Ok, bobApprove.Result.ExitCode) + + v.CommitApplies() + + var approveRet multisig.ApproveReturn + MustDeserialize(bobApprove.Result.Return, &approveRet) + v.Assert.Equal(multisig.ApproveReturn{ + Applied: true, + Code: 0, + Ret: nil, + }, approveRet) + + // assert that the multisig balance has been drained, and charlie's incremented. + v.Assert.BalanceEq(multisigAddr, big.Zero()) + v.Assert.MessageSendersSatisfy(BalanceUpdated(amount), charliePropose, charlieApprove) + + // reload the multisig state and verify + var multisigState multisig.State + v.Actors.ActorState(multisigAddr, &multisigState) + v.Assert.Equal(&multisig.State{ + Signers: []address.Address{alice.ID, bob.ID}, + NumApprovalsThreshold: 2, + NextTxnID: 1, + InitialBalance: amount, + StartEpoch: 1, + UnlockDuration: unlockDuration, + PendingTxns: EmptyMapCid, + }, &multisigState) +} + +func addSigner(v *Builder) { + var ( + initial = abi.NewTokenAmount(1_000_000_000_000) + amount = abi.NewTokenAmount(10) + ) + + v.Messages.SetDefaults(Value(big.Zero()), Epoch(1), GasLimit(gasLimit), GasPremium(1), GasFeeCap(gasFeeCap)) + + // Set up three accounts: alice and bob (signers), and charlie (outsider). + var alice, bob, charlie AddressHandle + v.Actors.AccountN(address.SECP256K1, initial, &alice, &bob, &charlie) + v.CommitPreconditions() + + // create the multisig actor; created by alice. + multisigAddr := createMultisig(v, alice, []address.Address{alice.ID}, 1, Value(amount), Nonce(0)) + + addParams := &multisig.AddSignerParams{ + Signer: bob.ID, + Increase: false, + } + + // attempt to add bob as a signer; this fails because the addition needs to go through + // the multisig flow, as it is subject to the same approval policy. + v.Messages.Typed(alice.ID, multisigAddr, MultisigAddSigner(addParams), Nonce(1)) + + // go through the multisig wallet. + // since approvals = 1, this auto-approves the transaction. + v.Messages.Typed(alice.ID, multisigAddr, MultisigPropose(&multisig.ProposeParams{ + To: multisigAddr, + Value: big.Zero(), + Method: builtin.MethodsMultisig.AddSigner, + Params: MustSerialize(addParams), + }), Nonce(2)) + + // TODO also exercise the approvals = 2 case with explicit approval. + + v.CommitApplies() + + // reload the multisig state and verify that bob is now a signer. + var multisigState multisig.State + v.Actors.ActorState(multisigAddr, &multisigState) + v.Assert.Equal(&multisig.State{ + Signers: []address.Address{alice.ID, bob.ID}, + NumApprovalsThreshold: 1, + NextTxnID: 1, + InitialBalance: amount, + StartEpoch: 1, + UnlockDuration: 10, + PendingTxns: EmptyMapCid, + }, &multisigState) +} + +type proposeOpts struct { + multisigAddr address.Address + sender address.Address + recipient address.Address + amount abi.TokenAmount +} + +func proposeOk(v *Builder, proposeOpts proposeOpts, opts ...MsgOpt) []byte { + propose := &multisig.ProposeParams{ + To: proposeOpts.recipient, + Value: proposeOpts.amount, + Method: builtin.MethodSend, + Params: nil, + } + proposeMsg := v.Messages.Typed(proposeOpts.sender, proposeOpts.multisigAddr, MultisigPropose(propose), opts...) + + v.Messages.ApplyOne(proposeMsg) + + // verify that the multisig state contains the outstanding TX. + var multisigState multisig.State + v.Actors.ActorState(proposeOpts.multisigAddr, &multisigState) + + id := multisig.TxnID(0) + actualTxn := loadMultisigTxn(v, multisigState, id) + v.Assert.Equal(&multisig.Transaction{ + To: propose.To, + Value: propose.Value, + Method: propose.Method, + Params: propose.Params, + Approved: []address.Address{proposeOpts.sender}, + }, actualTxn) + + return makeProposalHash(v, actualTxn) +} + +func createMultisig(v *Builder, creator AddressHandle, approvers []address.Address, threshold uint64, opts ...MsgOpt) address.Address { + const unlockDuration = abi.ChainEpoch(10) + // create the multisig actor. + params := &multisig.ConstructorParams{ + Signers: approvers, + NumApprovalsThreshold: threshold, + UnlockDuration: unlockDuration, + } + msg := v.Messages.Sugar().CreateMultisigActor(creator.ID, params, opts...) + v.Messages.ApplyOne(msg) + + // verify ok + v.Assert.EveryMessageResultSatisfies(ExitCode(exitcode.Ok)) + + // verify the assigned addess is as expected. + var ret init_.ExecReturn + MustDeserialize(msg.Result.Return, &ret) + v.Assert.Equal(creator.NextActorAddress(msg.Message.Nonce, 0), ret.RobustAddress) + handles := v.Actors.Handles() + v.Assert.Equal(MustNewIDAddr(MustIDFromAddress(handles[len(handles)-1].ID)+1), ret.IDAddress) + + // the multisig address's balance is incremented by the value sent to it. + v.Assert.BalanceEq(ret.IDAddress, msg.Message.Value) + + return ret.IDAddress +} + +func loadMultisigTxn(v *Builder, state multisig.State, id multisig.TxnID) *multisig.Transaction { + pending, err := adt.AsMap(v.Stores.ADTStore, state.PendingTxns) + v.Assert.NoError(err) + + var actualTxn multisig.Transaction + found, err := pending.Get(id, &actualTxn) + v.Assert.True(found) + v.Assert.NoError(err) + return &actualTxn +} + +func makeProposalHash(v *Builder, txn *multisig.Transaction) []byte { + ret, err := multisig.ComputeProposalHash(txn, blake2b.Sum256) + v.Assert.NoError(err) + return ret +} diff --git a/tvx/scripts/nested/nested.go b/tvx/scripts/nested/nested.go index c5f4f4be2..a516031a0 100644 --- a/tvx/scripts/nested/nested.go +++ b/tvx/scripts/nested/nested.go @@ -308,7 +308,11 @@ func prepareStage(v *Builder, creatorBalance, msBalance abi.TokenAmount) *msStag 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)) + msg := v.Messages.Sugar().CreateMultisigActor(creator.ID, &multisig.ConstructorParams{ + Signers: []address.Address{creator.ID}, + NumApprovalsThreshold: 1, + UnlockDuration: 0, + }, Value(msBalance), Nonce(0)) v.Messages.ApplyOne(msg) v.Assert.Equal(msg.Result.ExitCode, exitcode.Ok)