diff --git a/client/lcd/lcd_test.go b/client/lcd/lcd_test.go index c29eb64a7bd9..d9e99c0e59cd 100644 --- a/client/lcd/lcd_test.go +++ b/client/lcd/lcd_test.go @@ -259,7 +259,7 @@ func TestCoinSendAccAuto(t *testing.T) { require.Equal(t, expectedBalance.Amount.SubRaw(1), coins[0].Amount) } -func TestCoinSendGenerateOnly(t *testing.T) { +func TestCoinMultiSendGenerateOnly(t *testing.T) { addr, seed := CreateAddr(t, name1, pw, GetKeyBase(t)) cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{addr}) defer cleanup() @@ -277,7 +277,7 @@ func TestCoinSendGenerateOnly(t *testing.T) { require.Equal(t, memo, stdTx.Memo) require.NotZero(t, stdTx.Fee.Gas) require.IsType(t, stdTx.GetMsgs()[0], bank.MsgSend{}) - require.Equal(t, addr, stdTx.GetMsgs()[0].(bank.MsgSend).Inputs[0].Address) + require.Equal(t, addr, stdTx.GetMsgs()[0].(bank.MsgSend).FromAddress) } func TestCoinSendGenerateSignAndBroadcast(t *testing.T) { diff --git a/cmd/gaia/app/benchmarks/txsize_test.go b/cmd/gaia/app/benchmarks/txsize_test.go index 2726dcd1bbe7..af381a3fd6ef 100644 --- a/cmd/gaia/app/benchmarks/txsize_test.go +++ b/cmd/gaia/app/benchmarks/txsize_test.go @@ -23,7 +23,7 @@ func ExampleTxSendSize() { priv2 := secp256k1.GenPrivKeySecp256k1([]byte{1}) addr2 := sdk.AccAddress(priv2.PubKey().Address()) coins := sdk.Coins{sdk.NewCoin("denom", sdk.NewInt(10))} - msg1 := bank.MsgSend{ + msg1 := bank.MsgMultiSend{ Inputs: []bank.Input{bank.NewInput(addr1, coins)}, Outputs: []bank.Output{bank.NewOutput(addr2, coins)}, } diff --git a/cmd/gaia/app/sim_test.go b/cmd/gaia/app/sim_test.go index f7d645a61bf7..860404a56355 100644 --- a/cmd/gaia/app/sim_test.go +++ b/cmd/gaia/app/sim_test.go @@ -266,7 +266,8 @@ func randIntBetween(r *rand.Rand, min, max int) int { func testAndRunTxs(app *GaiaApp) []simulation.WeightedOperation { return []simulation.WeightedOperation{ {5, authsim.SimulateDeductFee(app.accountKeeper, app.feeCollectionKeeper)}, - {100, banksim.SingleInputSendMsg(app.accountKeeper, app.bankKeeper)}, + {100, banksim.SendMsg(app.accountKeeper, app.bankKeeper)}, + {10, banksim.SingleInputMsgMultiSend(app.accountKeeper, app.bankKeeper)}, {50, distrsim.SimulateMsgSetWithdrawAddress(app.accountKeeper, app.distrKeeper)}, {50, distrsim.SimulateMsgWithdrawDelegatorReward(app.accountKeeper, app.distrKeeper)}, {50, distrsim.SimulateMsgWithdrawValidatorCommission(app.accountKeeper, app.distrKeeper)}, diff --git a/x/bank/app_test.go b/x/bank/app_test.go index f32d037810e5..824f48670b6b 100644 --- a/x/bank/app_test.go +++ b/x/bank/app_test.go @@ -45,18 +45,20 @@ var ( manyCoins = sdk.Coins{sdk.NewInt64Coin("foocoin", 1), sdk.NewInt64Coin("barcoin", 1)} freeFee = auth.NewStdFee(100000, sdk.Coins{sdk.NewInt64Coin("foocoin", 0)}) - sendMsg1 = MsgSend{ + sendMsg1 = NewMsgSend(addr1, addr2, coins) + + multiSendMsg1 = MsgMultiSend{ Inputs: []Input{NewInput(addr1, coins)}, Outputs: []Output{NewOutput(addr2, coins)}, } - sendMsg2 = MsgSend{ + multiSendMsg2 = MsgMultiSend{ Inputs: []Input{NewInput(addr1, coins)}, Outputs: []Output{ NewOutput(addr2, halfCoins), NewOutput(addr3, halfCoins), }, } - sendMsg3 = MsgSend{ + multiSendMsg3 = MsgMultiSend{ Inputs: []Input{ NewInput(addr1, coins), NewInput(addr4, coins), @@ -66,7 +68,7 @@ var ( NewOutput(addr3, coins), }, } - sendMsg4 = MsgSend{ + multiSendMsg4 = MsgMultiSend{ Inputs: []Input{ NewInput(addr2, coins), }, @@ -74,7 +76,7 @@ var ( NewOutput(addr1, coins), }, } - sendMsg5 = MsgSend{ + multiSendMsg5 = MsgMultiSend{ Inputs: []Input{ NewInput(addr1, manyCoins), }, @@ -102,7 +104,7 @@ func getInitChainer(mapp *mock.App, keeper BaseKeeper) sdk.InitChainer { } } -func TestMsgSendWithAccounts(t *testing.T) { +func TestMsgMultiSendWithAccounts(t *testing.T) { mapp := getMockApp(t) acc := &auth.BaseAccount{ Address: addr1, @@ -119,7 +121,7 @@ func TestMsgSendWithAccounts(t *testing.T) { testCases := []appTestCase{ { - msgs: []sdk.Msg{sendMsg1}, + msgs: []sdk.Msg{multiSendMsg1}, accNums: []uint64{0}, accSeqs: []uint64{0}, expSimPass: true, @@ -131,7 +133,7 @@ func TestMsgSendWithAccounts(t *testing.T) { }, }, { - msgs: []sdk.Msg{sendMsg1, sendMsg2}, + msgs: []sdk.Msg{multiSendMsg1, multiSendMsg2}, accNums: []uint64{0}, accSeqs: []uint64{0}, expSimPass: true, // doesn't check signature @@ -149,7 +151,7 @@ func TestMsgSendWithAccounts(t *testing.T) { } } -func TestMsgSendMultipleOut(t *testing.T) { +func TestMsgMultiSendMultipleOut(t *testing.T) { mapp := getMockApp(t) acc1 := &auth.BaseAccount{ @@ -165,7 +167,7 @@ func TestMsgSendMultipleOut(t *testing.T) { testCases := []appTestCase{ { - msgs: []sdk.Msg{sendMsg2}, + msgs: []sdk.Msg{multiSendMsg2}, accNums: []uint64{0}, accSeqs: []uint64{0}, expSimPass: true, @@ -188,7 +190,7 @@ func TestMsgSendMultipleOut(t *testing.T) { } } -func TestSengMsgMultipleInOut(t *testing.T) { +func TestMsgMultiSendMultipleInOut(t *testing.T) { mapp := getMockApp(t) acc1 := &auth.BaseAccount{ @@ -208,7 +210,7 @@ func TestSengMsgMultipleInOut(t *testing.T) { testCases := []appTestCase{ { - msgs: []sdk.Msg{sendMsg3}, + msgs: []sdk.Msg{multiSendMsg3}, accNums: []uint64{0, 0}, accSeqs: []uint64{0, 0}, expSimPass: true, @@ -232,7 +234,7 @@ func TestSengMsgMultipleInOut(t *testing.T) { } } -func TestMsgSendDependent(t *testing.T) { +func TestMsgMultiSendDependent(t *testing.T) { mapp := getMockApp(t) acc1 := &auth.BaseAccount{ @@ -244,7 +246,7 @@ func TestMsgSendDependent(t *testing.T) { testCases := []appTestCase{ { - msgs: []sdk.Msg{sendMsg1}, + msgs: []sdk.Msg{multiSendMsg1}, accNums: []uint64{0}, accSeqs: []uint64{0}, expSimPass: true, @@ -256,7 +258,7 @@ func TestMsgSendDependent(t *testing.T) { }, }, { - msgs: []sdk.Msg{sendMsg4}, + msgs: []sdk.Msg{multiSendMsg4}, accNums: []uint64{0}, accSeqs: []uint64{0}, expSimPass: true, diff --git a/x/bank/bench_test.go b/x/bank/bench_test.go index 5eb70271315e..95918110c4de 100644 --- a/x/bank/bench_test.go +++ b/x/bank/bench_test.go @@ -57,3 +57,33 @@ func BenchmarkOneBankSendTxPerBlock(b *testing.B) { benchmarkApp.Commit() } } + +func BenchmarkOneBankMultiSendTxPerBlock(b *testing.B) { + benchmarkApp, _ := getBenchmarkMockApp() + + // Add an account at genesis + acc := &auth.BaseAccount{ + Address: addr1, + // Some value conceivably higher than the benchmarks would ever go + Coins: sdk.Coins{sdk.NewInt64Coin("foocoin", 100000000000)}, + } + accs := []auth.Account{acc} + + // Construct genesis state + mock.SetGenesis(benchmarkApp, accs) + // Precompute all txs + txs := mock.GenSequenceOfTxs([]sdk.Msg{multiSendMsg1}, []uint64{0}, []uint64{uint64(0)}, b.N, priv1) + b.ResetTimer() + // Run this with a profiler, so its easy to distinguish what time comes from + // Committing, and what time comes from Check/Deliver Tx. + for i := 0; i < b.N; i++ { + benchmarkApp.BeginBlock(abci.RequestBeginBlock{}) + x := benchmarkApp.Check(txs[i]) + if !x.IsOK() { + panic("something is broken in checking transaction") + } + benchmarkApp.Deliver(txs[i]) + benchmarkApp.EndBlock(abci.RequestEndBlock{}) + benchmarkApp.Commit() + } +} diff --git a/x/bank/client/cli/sendtx.go b/x/bank/client/cli/sendtx.go index 9da232789a50..4f805bf427ff 100644 --- a/x/bank/client/cli/sendtx.go +++ b/x/bank/client/cli/sendtx.go @@ -9,7 +9,7 @@ import ( "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" authtxb "github.com/cosmos/cosmos-sdk/x/auth/client/txbuilder" - bankClient "github.com/cosmos/cosmos-sdk/x/bank/client" + "github.com/cosmos/cosmos-sdk/x/bank" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -62,7 +62,7 @@ func SendTxCmd(cdc *codec.Codec) *cobra.Command { } // build and sign the transaction, then broadcast to Tendermint - msg := bankClient.CreateMsg(from, to, coins) + msg := bank.NewMsgSend(from, to, coins) if cliCtx.GenerateOnly { return utils.PrintUnsignedStdTx(os.Stdout, txBldr, cliCtx, []sdk.Msg{msg}, false) } diff --git a/x/bank/client/rest/sendtx.go b/x/bank/client/rest/sendtx.go index 70aae05fccc0..c53e1431a970 100644 --- a/x/bank/client/rest/sendtx.go +++ b/x/bank/client/rest/sendtx.go @@ -10,7 +10,6 @@ import ( "github.com/cosmos/cosmos-sdk/crypto/keys" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/bank" - bankclient "github.com/cosmos/cosmos-sdk/x/bank/client" "github.com/gorilla/mux" ) @@ -64,7 +63,7 @@ func SendRequestHandlerFn(cdc *codec.Codec, kb keys.Keybase, cliCtx context.CLIC return } - msg := bankclient.CreateMsg(fromAddr, toAddr, req.Amount) + msg := bank.NewMsgSend(fromAddr, toAddr, req.Amount) rest.WriteGenerateStdTxResponse(w, cdc, cliCtx, req.BaseReq, []sdk.Msg{msg}) return } @@ -77,7 +76,7 @@ func SendRequestHandlerFn(cdc *codec.Codec, kb keys.Keybase, cliCtx context.CLIC } cliCtx = cliCtx.WithFromName(fromName).WithFromAddress(fromAddress) - msg := bankclient.CreateMsg(cliCtx.GetFromAddress(), toAddr, req.Amount) + msg := bank.NewMsgSend(cliCtx.GetFromAddress(), toAddr, req.Amount) rest.CompleteAndBroadcastTxREST(w, r, cliCtx, req.BaseReq, []sdk.Msg{msg}, cdc) } diff --git a/x/bank/client/util.go b/x/bank/client/util.go deleted file mode 100644 index b647f00d3f90..000000000000 --- a/x/bank/client/util.go +++ /dev/null @@ -1,14 +0,0 @@ -package client - -import ( - sdk "github.com/cosmos/cosmos-sdk/types" - bank "github.com/cosmos/cosmos-sdk/x/bank" -) - -// create the sendTx msg -func CreateMsg(from sdk.AccAddress, to sdk.AccAddress, coins sdk.Coins) sdk.Msg { - input := bank.NewInput(from, coins) - output := bank.NewOutput(to, coins) - msg := bank.NewMsgSend([]bank.Input{input}, []bank.Output{output}) - return msg -} diff --git a/x/bank/codec.go b/x/bank/codec.go index 2195e4853a0e..505ffd5b87ef 100644 --- a/x/bank/codec.go +++ b/x/bank/codec.go @@ -7,6 +7,7 @@ import ( // Register concrete types on codec codec func RegisterCodec(cdc *codec.Codec) { cdc.RegisterConcrete(MsgSend{}, "cosmos-sdk/Send", nil) + cdc.RegisterConcrete(MsgMultiSend{}, "cosmos-sdk/MultiSend", nil) } var msgCdc = codec.New() diff --git a/x/bank/handler.go b/x/bank/handler.go index 2d1abf822697..e58738135158 100644 --- a/x/bank/handler.go +++ b/x/bank/handler.go @@ -10,6 +10,8 @@ func NewHandler(k Keeper) sdk.Handler { switch msg := msg.(type) { case MsgSend: return handleMsgSend(ctx, k, msg) + case MsgMultiSend: + return handleMsgMultiSend(ctx, k, msg) default: errMsg := "Unrecognized bank Msg type: %s" + msg.Type() return sdk.ErrUnknownRequest(errMsg).Result() @@ -19,6 +21,21 @@ func NewHandler(k Keeper) sdk.Handler { // Handle MsgSend. func handleMsgSend(ctx sdk.Context, k Keeper, msg MsgSend) sdk.Result { + if !k.GetSendEnabled(ctx) { + return ErrSendDisabled(k.Codespace()).Result() + } + tags, err := k.SendCoins(ctx, msg.FromAddress, msg.ToAddress, msg.Amount) + if err != nil { + return err.Result() + } + + return sdk.Result{ + Tags: tags, + } +} + +// Handle MsgMultiSend. +func handleMsgMultiSend(ctx sdk.Context, k Keeper, msg MsgMultiSend) sdk.Result { // NOTE: totalIn == totalOut should already have been checked if !k.GetSendEnabled(ctx) { return ErrSendDisabled(k.Codespace()).Result() diff --git a/x/bank/msgs.go b/x/bank/msgs.go index 059b49525073..83bdc6633eea 100644 --- a/x/bank/msgs.go +++ b/x/bank/msgs.go @@ -9,15 +9,16 @@ const RouterKey = "bank" // MsgSend - high level transaction of the coin module type MsgSend struct { - Inputs []Input `json:"inputs"` - Outputs []Output `json:"outputs"` + FromAddress sdk.AccAddress `json:"from_address"` + ToAddress sdk.AccAddress `json:"to_address"` + Amount sdk.Coins `json:"amount"` } var _ sdk.Msg = MsgSend{} // NewMsgSend - construct arbitrary multi-in, multi-out send msg. -func NewMsgSend(in []Input, out []Output) MsgSend { - return MsgSend{Inputs: in, Outputs: out} +func NewMsgSend(fromAddr, toAddr sdk.AccAddress, amount sdk.Coins) MsgSend { + return MsgSend{FromAddress: fromAddr, ToAddress: toAddr, Amount: amount} } // Implements Msg. @@ -27,6 +28,48 @@ func (msg MsgSend) Type() string { return "send" } // Implements Msg. func (msg MsgSend) ValidateBasic() sdk.Error { + if msg.FromAddress.Empty() { + return sdk.ErrInvalidAddress("missing sender address") + } + if msg.ToAddress.Empty() { + return sdk.ErrInvalidAddress("missing recipient address") + } + if !msg.Amount.IsPositive() { + return sdk.ErrInsufficientCoins("send amount must be positive") + } + return nil +} + +// Implements Msg. +func (msg MsgSend) GetSignBytes() []byte { + return sdk.MustSortJSON(msgCdc.MustMarshalJSON(msg)) +} + +// Implements Msg. +func (msg MsgSend) GetSigners() []sdk.AccAddress { + return []sdk.AccAddress{msg.FromAddress} +} + +// MsgMultiSend - high level transaction of the coin module +type MsgMultiSend struct { + Inputs []Input `json:"inputs"` + Outputs []Output `json:"outputs"` +} + +var _ sdk.Msg = MsgMultiSend{} + +// NewMsgMultiSend - construct arbitrary multi-in, multi-out send msg. +func NewMsgMultiSend(in []Input, out []Output) MsgMultiSend { + return MsgMultiSend{Inputs: in, Outputs: out} +} + +// Implements Msg. +// nolint +func (msg MsgMultiSend) Route() string { return RouterKey } +func (msg MsgMultiSend) Type() string { return "multisend" } + +// Implements Msg. +func (msg MsgMultiSend) ValidateBasic() sdk.Error { // this just makes sure all the inputs and outputs are properly formatted, // not that they actually have the money inside if len(msg.Inputs) == 0 { @@ -40,13 +83,12 @@ func (msg MsgSend) ValidateBasic() sdk.Error { } // Implements Msg. -func (msg MsgSend) GetSignBytes() []byte { - bz := msgCdc.MustMarshalJSON(msg) - return sdk.MustSortJSON(bz) +func (msg MsgMultiSend) GetSignBytes() []byte { + return sdk.MustSortJSON(msgCdc.MustMarshalJSON(msg)) } // Implements Msg. -func (msg MsgSend) GetSigners() []sdk.AccAddress { +func (msg MsgMultiSend) GetSigners() []sdk.AccAddress { addrs := make([]sdk.AccAddress, len(msg.Inputs)) for i, in := range msg.Inputs { addrs[i] = in.Address @@ -77,7 +119,7 @@ func (in Input) ValidateBasic() sdk.Error { return nil } -// NewInput - create a transaction input, used with MsgSend +// NewInput - create a transaction input, used with MsgMultiSend func NewInput(addr sdk.AccAddress, coins sdk.Coins) Input { return Input{ Address: addr, @@ -108,7 +150,7 @@ func (out Output) ValidateBasic() sdk.Error { return nil } -// NewOutput - create a transaction output, used with MsgSend +// NewOutput - create a transaction output, used with MsgMultiSend func NewOutput(addr sdk.AccAddress, coins sdk.Coins) Output { return Output{ Address: addr, diff --git a/x/bank/msgs_test.go b/x/bank/msgs_test.go index 3f8b68fd454e..fbc42ddc2a76 100644 --- a/x/bank/msgs_test.go +++ b/x/bank/msgs_test.go @@ -9,20 +9,79 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) -func TestNewMsgSend(t *testing.T) {} - func TestMsgSendRoute(t *testing.T) { + addr1 := sdk.AccAddress([]byte("from")) + addr2 := sdk.AccAddress([]byte("to")) + coins := sdk.Coins{sdk.NewInt64Coin("atom", 10)} + var msg = NewMsgSend(addr1, addr2, coins) + + require.Equal(t, msg.Route(), "bank") + require.Equal(t, msg.Type(), "send") +} + +func TestMsgSendValidation(t *testing.T) { + addr1 := sdk.AccAddress([]byte("from")) + addr2 := sdk.AccAddress([]byte("to")) + atom123 := sdk.Coins{sdk.NewInt64Coin("atom", 123)} + atom0 := sdk.Coins{sdk.NewInt64Coin("atom", 0)} + atom123eth123 := sdk.Coins{sdk.NewInt64Coin("atom", 123), sdk.NewInt64Coin("eth", 123)} + atom123eth0 := sdk.Coins{sdk.NewInt64Coin("atom", 123), sdk.NewInt64Coin("eth", 0)} + + var emptyAddr sdk.AccAddress + + cases := []struct { + valid bool + tx MsgSend + }{ + {true, NewMsgSend(addr1, addr2, atom123)}, // valid send + {true, NewMsgSend(addr1, addr2, atom123eth123)}, // valid send with multiple coins + {false, NewMsgSend(addr1, addr2, atom0)}, // non positive coin + {false, NewMsgSend(addr1, addr2, atom123eth0)}, // non positive coin in multicoins + {false, NewMsgSend(emptyAddr, addr2, atom123)}, // empty from addr + {false, NewMsgSend(addr1, emptyAddr, atom123)}, // empty to addr + } + + for i, tc := range cases { + err := tc.tx.ValidateBasic() + if tc.valid { + require.Nil(t, err, "%d: %+v", i, err) + } else { + require.NotNil(t, err, "%d", i) + } + } +} + +func TestMsgSendGetSignBytes(t *testing.T) { + addr1 := sdk.AccAddress([]byte("input")) + addr2 := sdk.AccAddress([]byte("output")) + coins := sdk.Coins{sdk.NewInt64Coin("atom", 10)} + var msg = NewMsgSend(addr1, addr2, coins) + res := msg.GetSignBytes() + + expected := `{"type":"cosmos-sdk/Send","value":{"amount":[{"amount":"10","denom":"atom"}],"from_address":"cosmos1d9h8qat57ljhcm","to_address":"cosmos1da6hgur4wsmpnjyg"}}` + require.Equal(t, expected, string(res)) +} + +func TestMsgSendGetSigners(t *testing.T) { + var msg = NewMsgSend(sdk.AccAddress([]byte("input1")), sdk.AccAddress{}, sdk.Coins{}) + res := msg.GetSigners() + // TODO: fix this ! + require.Equal(t, fmt.Sprintf("%v", res), "[696E70757431]") +} + +func TestMsgMultiSendRoute(t *testing.T) { // Construct a MsgSend addr1 := sdk.AccAddress([]byte("input")) addr2 := sdk.AccAddress([]byte("output")) coins := sdk.Coins{sdk.NewInt64Coin("atom", 10)} - var msg = MsgSend{ + var msg = MsgMultiSend{ Inputs: []Input{NewInput(addr1, coins)}, Outputs: []Output{NewOutput(addr2, coins)}, } // TODO some failures for bad result require.Equal(t, msg.Route(), "bank") + require.Equal(t, msg.Type(), "multisend") } func TestInputValidation(t *testing.T) { @@ -101,7 +160,7 @@ func TestOutputValidation(t *testing.T) { } } -func TestMsgSendValidation(t *testing.T) { +func TestMsgMultiSendValidation(t *testing.T) { addr1 := sdk.AccAddress([]byte{1, 2}) addr2 := sdk.AccAddress([]byte{7, 8}) atom123 := sdk.Coins{sdk.NewInt64Coin("atom", 123)} @@ -120,40 +179,40 @@ func TestMsgSendValidation(t *testing.T) { cases := []struct { valid bool - tx MsgSend + tx MsgMultiSend }{ - {false, MsgSend{}}, // no input or output - {false, MsgSend{Inputs: []Input{input1}}}, // just input - {false, MsgSend{Outputs: []Output{output1}}}, // just output - {false, MsgSend{ + {false, MsgMultiSend{}}, // no input or output + {false, MsgMultiSend{Inputs: []Input{input1}}}, // just input + {false, MsgMultiSend{Outputs: []Output{output1}}}, // just output + {false, MsgMultiSend{ Inputs: []Input{NewInput(emptyAddr, atom123)}, // invalid input Outputs: []Output{output1}}}, - {false, MsgSend{ + {false, MsgMultiSend{ Inputs: []Input{input1}, Outputs: []Output{{emptyAddr, atom123}}}, // invalid output }, - {false, MsgSend{ + {false, MsgMultiSend{ Inputs: []Input{input1}, Outputs: []Output{output2}}, // amounts dont match }, - {false, MsgSend{ + {false, MsgMultiSend{ Inputs: []Input{input1}, Outputs: []Output{output3}}, // amounts dont match }, - {false, MsgSend{ + {false, MsgMultiSend{ Inputs: []Input{input1}, Outputs: []Output{outputMulti}}, // amounts dont match }, - {false, MsgSend{ + {false, MsgMultiSend{ Inputs: []Input{input2}, Outputs: []Output{output1}}, // amounts dont match }, - {true, MsgSend{ + {true, MsgMultiSend{ Inputs: []Input{input1}, Outputs: []Output{output1}}, }, - {true, MsgSend{ + {true, MsgMultiSend{ Inputs: []Input{input1, input2}, Outputs: []Output{outputMulti}}, }, @@ -169,22 +228,22 @@ func TestMsgSendValidation(t *testing.T) { } } -func TestMsgSendGetSignBytes(t *testing.T) { +func TestMsgMultiSendGetSignBytes(t *testing.T) { addr1 := sdk.AccAddress([]byte("input")) addr2 := sdk.AccAddress([]byte("output")) coins := sdk.Coins{sdk.NewInt64Coin("atom", 10)} - var msg = MsgSend{ + var msg = MsgMultiSend{ Inputs: []Input{NewInput(addr1, coins)}, Outputs: []Output{NewOutput(addr2, coins)}, } res := msg.GetSignBytes() - expected := `{"type":"cosmos-sdk/Send","value":{"inputs":[{"address":"cosmos1d9h8qat57ljhcm","coins":[{"amount":"10","denom":"atom"}]}],"outputs":[{"address":"cosmos1da6hgur4wsmpnjyg","coins":[{"amount":"10","denom":"atom"}]}]}}` + expected := `{"type":"cosmos-sdk/MultiSend","value":{"inputs":[{"address":"cosmos1d9h8qat57ljhcm","coins":[{"amount":"10","denom":"atom"}]}],"outputs":[{"address":"cosmos1da6hgur4wsmpnjyg","coins":[{"amount":"10","denom":"atom"}]}]}}` require.Equal(t, expected, string(res)) } -func TestMsgSendGetSigners(t *testing.T) { - var msg = MsgSend{ +func TestMsgMultiSendGetSigners(t *testing.T) { + var msg = MsgMultiSend{ Inputs: []Input{ NewInput(sdk.AccAddress([]byte("input1")), nil), NewInput(sdk.AccAddress([]byte("input2")), nil), diff --git a/x/bank/simulation/msgs.go b/x/bank/simulation/msgs.go index c4d4919eb270..a3b47823d744 100644 --- a/x/bank/simulation/msgs.go +++ b/x/bank/simulation/msgs.go @@ -16,11 +16,30 @@ import ( "github.com/cosmos/cosmos-sdk/x/mock/simulation" ) -// SingleInputSendTx tests and runs a single msg send w/ auth, with one input and one output, where both +// SendTx tests and runs a single msg send where both // accounts already exist. -func SingleInputSendTx(mapper auth.AccountKeeper) simulation.Operation { +func SendMsg(mapper auth.AccountKeeper, bk bank.Keeper) simulation.Operation { + handler := bank.NewHandler(bk) + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simulation.Account, event func(string)) (action string, fOps []simulation.FutureOperation, err error) { + fromAcc, action, msg, abort := createSendMsg(r, ctx, accs, mapper) + if abort { + return action, nil, nil + } + err = sendAndVerifyMsgSend(app, mapper, msg, ctx, []crypto.PrivKey{fromAcc.PrivKey}, handler) + if err != nil { + return "", nil, err + } + event("bank/sendAndVerifyTxSend/ok") + + return action, nil, nil + } +} + +// SendTx tests and runs a single tx send, with auth where both +// accounts already exist. +func SendTx(mapper auth.AccountKeeper) simulation.Operation { return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simulation.Account, event func(string)) (action string, fOps []simulation.FutureOperation, err error) { - fromAcc, action, msg, abort := createSingleInputSendMsg(r, ctx, accs, mapper) + fromAcc, action, msg, abort := createSendMsg(r, ctx, accs, mapper) if abort { return action, nil, nil } @@ -34,26 +53,124 @@ func SingleInputSendTx(mapper auth.AccountKeeper) simulation.Operation { } } -// SingleInputSendMsg tests and runs a single msg send, with one input and one output, where both +func createSendMsg(r *rand.Rand, ctx sdk.Context, accs []simulation.Account, mapper auth.AccountKeeper) (fromAcc simulation.Account, action string, msg bank.MsgSend, abort bool) { + fromAcc = simulation.RandomAcc(r, accs) + toAcc := simulation.RandomAcc(r, accs) + // Disallow sending money to yourself + for { + if !fromAcc.PubKey.Equals(toAcc.PubKey) { + break + } + toAcc = simulation.RandomAcc(r, accs) + } + toAddr := toAcc.Address + initFromCoins := mapper.GetAccount(ctx, fromAcc.Address).SpendableCoins(ctx.BlockHeader().Time) + + if len(initFromCoins) == 0 { + return fromAcc, "skipping, no coins at all", msg, true + } + + denomIndex := r.Intn(len(initFromCoins)) + amt, goErr := randPositiveInt(r, initFromCoins[denomIndex].Amount) + if goErr != nil { + return fromAcc, "skipping bank send due to account having no coins of denomination " + initFromCoins[denomIndex].Denom, msg, true + } + + action = fmt.Sprintf("%s is sending %s %s to %s", + fromAcc.Address.String(), + amt.String(), + initFromCoins[denomIndex].Denom, + toAddr.String(), + ) + + coins := sdk.Coins{sdk.NewCoin(initFromCoins[denomIndex].Denom, amt)} + msg = bank.NewMsgSend(fromAcc.Address, toAcc.Address, coins) + return +} + +// Sends and verifies the transition of a msg send. +func sendAndVerifyMsgSend(app *baseapp.BaseApp, mapper auth.AccountKeeper, msg bank.MsgSend, ctx sdk.Context, privkeys []crypto.PrivKey, handler sdk.Handler) error { + fromAcc := mapper.GetAccount(ctx, msg.FromAddress) + AccountNumbers := []uint64{fromAcc.GetAccountNumber()} + SequenceNumbers := []uint64{fromAcc.GetSequence()} + initialFromAddrCoins := fromAcc.GetCoins() + + toAcc := mapper.GetAccount(ctx, msg.ToAddress) + initialToAddrCoins := toAcc.GetCoins() + + if handler != nil { + res := handler(ctx, msg) + if !res.IsOK() { + if res.Code == bank.CodeSendDisabled { + return nil + } + // TODO: Do this in a more 'canonical' way + return fmt.Errorf("handling msg failed %v", res) + } + } else { + tx := mock.GenTx([]sdk.Msg{msg}, + AccountNumbers, + SequenceNumbers, + privkeys...) + res := app.Deliver(tx) + if !res.IsOK() { + // TODO: Do this in a more 'canonical' way + return fmt.Errorf("Deliver failed %v", res) + } + } + + fromAcc = mapper.GetAccount(ctx, msg.FromAddress) + toAcc = mapper.GetAccount(ctx, msg.ToAddress) + + if !initialFromAddrCoins.Minus(msg.Amount).IsEqual(fromAcc.GetCoins()) { + return fmt.Errorf("fromAddress %s had an incorrect amount of coins", fromAcc.GetAddress()) + } + + if !initialToAddrCoins.Plus(msg.Amount).IsEqual(toAcc.GetCoins()) { + return fmt.Errorf("toAddress %s had an incorrect amount of coins", toAcc.GetAddress()) + } + + return nil +} + +// SingleInputSendTx tests and runs a single msg multisend w/ auth, with one input and one output, where both +// accounts already exist. +func SingleInputMultiSendTx(mapper auth.AccountKeeper) simulation.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simulation.Account, event func(string)) (action string, fOps []simulation.FutureOperation, err error) { + fromAcc, action, msg, abort := createSingleInputMsgMultiSend(r, ctx, accs, mapper) + if abort { + return action, nil, nil + } + err = sendAndVerifyMsgMultiSend(app, mapper, msg, ctx, []crypto.PrivKey{fromAcc.PrivKey}, nil) + if err != nil { + return "", nil, err + } + event("bank/sendAndVerifyTxMultiSend/ok") + + return action, nil, nil + } +} + +// SingleInputSendMsg tests and runs a single msg multisend, with one input and one output, where both // accounts already exist. -func SingleInputSendMsg(mapper auth.AccountKeeper, bk bank.Keeper) simulation.Operation { +func SingleInputMsgMultiSend(mapper auth.AccountKeeper, bk bank.Keeper) simulation.Operation { handler := bank.NewHandler(bk) return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simulation.Account, event func(string)) (action string, fOps []simulation.FutureOperation, err error) { - fromAcc, action, msg, abort := createSingleInputSendMsg(r, ctx, accs, mapper) + fromAcc, action, msg, abort := createSingleInputMsgMultiSend(r, ctx, accs, mapper) if abort { return action, nil, nil } - err = sendAndVerifyMsgSend(app, mapper, msg, ctx, []crypto.PrivKey{fromAcc.PrivKey}, handler) + err = sendAndVerifyMsgMultiSend(app, mapper, msg, ctx, []crypto.PrivKey{fromAcc.PrivKey}, handler) if err != nil { return "", nil, err } - event("bank/sendAndVerifyMsgSend/ok") + event("bank/sendAndVerifyMsgMultiSend/ok") return action, nil, nil } } -func createSingleInputSendMsg(r *rand.Rand, ctx sdk.Context, accs []simulation.Account, mapper auth.AccountKeeper) (fromAcc simulation.Account, action string, msg bank.MsgSend, abort bool) { +func createSingleInputMsgMultiSend(r *rand.Rand, ctx sdk.Context, accs []simulation.Account, mapper auth.AccountKeeper) (fromAcc simulation.Account, action string, msg bank.MsgMultiSend, abort bool) { fromAcc = simulation.RandomAcc(r, accs) toAcc := simulation.RandomAcc(r, accs) // Disallow sending money to yourself @@ -84,16 +201,16 @@ func createSingleInputSendMsg(r *rand.Rand, ctx sdk.Context, accs []simulation.A ) coins := sdk.Coins{sdk.NewCoin(initFromCoins[denomIndex].Denom, amt)} - msg = bank.MsgSend{ + msg = bank.MsgMultiSend{ Inputs: []bank.Input{bank.NewInput(fromAcc.Address, coins)}, Outputs: []bank.Output{bank.NewOutput(toAddr, coins)}, } return } -// Sends and verifies the transition of a msg send. This fails if there are repeated inputs or outputs +// Sends and verifies the transition of a msg multisend. This fails if there are repeated inputs or outputs // pass in handler as nil to handle txs, otherwise handle msgs -func sendAndVerifyMsgSend(app *baseapp.BaseApp, mapper auth.AccountKeeper, msg bank.MsgSend, ctx sdk.Context, privkeys []crypto.PrivKey, handler sdk.Handler) error { +func sendAndVerifyMsgMultiSend(app *baseapp.BaseApp, mapper auth.AccountKeeper, msg bank.MsgMultiSend, ctx sdk.Context, privkeys []crypto.PrivKey, handler sdk.Handler) error { initialInputAddrCoins := make([]sdk.Coins, len(msg.Inputs)) initialOutputAddrCoins := make([]sdk.Coins, len(msg.Outputs)) AccountNumbers := make([]uint64, len(msg.Inputs))