diff --git a/modules/coin/handler.go b/modules/coin/handler.go index 0e1740f1c1..41eda35900 100644 --- a/modules/coin/handler.go +++ b/modules/coin/handler.go @@ -29,12 +29,18 @@ func (_ Handler) Name() string { // CheckTx checks if there is enough money in the account func (h Handler) CheckTx(ctx basecoin.Context, store types.KVStore, tx basecoin.Tx) (res basecoin.Result, err error) { - _, err = checkTx(ctx, tx) + send, err := checkTx(ctx, tx) if err != nil { return res, err } // now make sure there is money + for _, in := range send.Inputs { + _, err = h.CheckCoins(store, in.Address, in.Coins, in.Sequence) + if err != nil { + return res, err + } + } // otherwise, we are good return res, nil @@ -42,12 +48,29 @@ func (h Handler) CheckTx(ctx basecoin.Context, store types.KVStore, tx basecoin. // DeliverTx moves the money func (h Handler) DeliverTx(ctx basecoin.Context, store types.KVStore, tx basecoin.Tx) (res basecoin.Result, err error) { - _, err = checkTx(ctx, tx) + send, err := checkTx(ctx, tx) if err != nil { return res, err } - // now move the money + // deduct from all input accounts + for _, in := range send.Inputs { + _, err = h.ChangeCoins(store, in.Address, in.Coins.Negative(), in.Sequence) + if err != nil { + return res, err + } + } + + // add to all output accounts + for _, out := range send.Outputs { + // note: sequence number is ignored when adding coins, only checked for subtracting + _, err = h.ChangeCoins(store, out.Address, out.Coins, 0) + if err != nil { + return res, err + } + } + + // a-ok! return basecoin.Result{}, nil } diff --git a/modules/coin/handler_test.go b/modules/coin/handler_test.go index 52560f128b..a2804f2591 100644 --- a/modules/coin/handler_test.go +++ b/modules/coin/handler_test.go @@ -4,72 +4,159 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/tendermint/basecoin" "github.com/tendermint/basecoin/stack" "github.com/tendermint/basecoin/types" ) -func TestHandlerPermissions(t *testing.T) { +// this makes sure that txs are rejected with invalid data or permissions +func TestHandlerValidation(t *testing.T) { assert := assert.New(t) - // TODO: need to update this when we actually have token store - h := NewHandler() // these are all valid, except for minusCoins addr1 := basecoin.Actor{App: "coin", Address: []byte{1, 2}} addr2 := basecoin.Actor{App: "role", Address: []byte{7, 8}} someCoins := types.Coins{{"atom", 123}} + doubleCoins := types.Coins{{"atom", 246}} minusCoins := types.Coins{{"eth", -34}} cases := []struct { valid bool - tx SendTx + tx basecoin.Tx perms []basecoin.Actor }{ // auth works with different apps {true, - SendTx{ - Inputs: []TxInput{NewTxInput(addr1, someCoins, 2)}, - Outputs: []TxOutput{NewTxOutput(addr2, someCoins)}}, + NewSendTx( + []TxInput{NewTxInput(addr1, someCoins, 2)}, + []TxOutput{NewTxOutput(addr2, someCoins)}), []basecoin.Actor{addr1}}, {true, - SendTx{ - Inputs: []TxInput{NewTxInput(addr2, someCoins, 2)}, - Outputs: []TxOutput{NewTxOutput(addr1, someCoins)}}, + NewSendTx( + []TxInput{NewTxInput(addr2, someCoins, 2)}, + []TxOutput{NewTxOutput(addr1, someCoins)}), + []basecoin.Actor{addr1, addr2}}, + // check multi-input with both sigs + {true, + NewSendTx( + []TxInput{NewTxInput(addr1, someCoins, 2), NewTxInput(addr2, someCoins, 3)}, + []TxOutput{NewTxOutput(addr1, doubleCoins)}), []basecoin.Actor{addr1, addr2}}, // wrong permissions fail {false, - SendTx{ - Inputs: []TxInput{NewTxInput(addr1, someCoins, 2)}, - Outputs: []TxOutput{NewTxOutput(addr2, someCoins)}}, + NewSendTx( + []TxInput{NewTxInput(addr1, someCoins, 2)}, + []TxOutput{NewTxOutput(addr2, someCoins)}), []basecoin.Actor{}}, {false, - SendTx{ - Inputs: []TxInput{NewTxInput(addr1, someCoins, 2)}, - Outputs: []TxOutput{NewTxOutput(addr2, someCoins)}}, + NewSendTx( + []TxInput{NewTxInput(addr1, someCoins, 2)}, + []TxOutput{NewTxOutput(addr2, someCoins)}), []basecoin.Actor{addr2}}, + {false, + NewSendTx( + []TxInput{NewTxInput(addr1, someCoins, 2), NewTxInput(addr2, someCoins, 3)}, + []TxOutput{NewTxOutput(addr1, doubleCoins)}), + []basecoin.Actor{addr1}}, // invalid input fails {false, - SendTx{ - Inputs: []TxInput{NewTxInput(addr1, minusCoins, 2)}, - Outputs: []TxOutput{NewTxOutput(addr2, minusCoins)}}, + NewSendTx( + []TxInput{NewTxInput(addr1, minusCoins, 2)}, + []TxOutput{NewTxOutput(addr2, minusCoins)}), []basecoin.Actor{addr2}}, } for i, tc := range cases { ctx := stack.MockContext().WithPermissions(tc.perms...) - _, err := h.CheckTx(ctx, nil, tc.tx.Wrap()) + _, err := checkTx(ctx, tc.tx) if tc.valid { assert.Nil(err, "%d: %+v", i, err) } else { assert.NotNil(err, "%d", i) } + } +} - _, err = h.DeliverTx(ctx, nil, tc.tx.Wrap()) - if tc.valid { +func TestDeliverTx(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + // some sample settings + addr1 := basecoin.Actor{App: "coin", Address: []byte{1, 2}} + addr2 := basecoin.Actor{App: "role", Address: []byte{7, 8}} + addr3 := basecoin.Actor{App: "coin", Address: []byte{6, 5, 4, 3}} + + someCoins := types.Coins{{"atom", 123}} + moreCoins := types.Coins{{"atom", 6487}} + diffCoins := moreCoins.Minus(someCoins) + otherCoins := types.Coins{{"eth", 11}} + mixedCoins := someCoins.Plus(otherCoins) + + type money struct { + addr basecoin.Actor + coins types.Coins + } + + cases := []struct { + init []money + tx basecoin.Tx + perms []basecoin.Actor + final []money // nil for error + }{ + { + []money{{addr1, moreCoins}}, + NewSendTx( + []TxInput{NewTxInput(addr1, someCoins, 1)}, + []TxOutput{NewTxOutput(addr2, someCoins)}), + []basecoin.Actor{addr1}, + []money{{addr1, diffCoins}, {addr2, someCoins}}, + }, + // simple multi-sig 2 accounts to 1 + { + []money{{addr1, mixedCoins}, {addr2, moreCoins}}, + NewSendTx( + []TxInput{NewTxInput(addr1, otherCoins, 1), NewTxInput(addr2, someCoins, 1)}, + []TxOutput{NewTxOutput(addr3, mixedCoins)}), + []basecoin.Actor{addr1, addr2}, + []money{{addr1, someCoins}, {addr2, diffCoins}, {addr3, mixedCoins}}, + }, + // multi-sig with one account sending many times + { + []money{{addr1, moreCoins.Plus(otherCoins)}}, + NewSendTx( + []TxInput{NewTxInput(addr1, otherCoins, 1), NewTxInput(addr1, someCoins, 2)}, + []TxOutput{NewTxOutput(addr2, mixedCoins)}), + []basecoin.Actor{addr1}, + []money{{addr1, diffCoins}, {addr2, mixedCoins}}, + }, + } + + h := NewHandler() + for i, tc := range cases { + // setup the cases.... + store := types.NewMemKVStore() + for _, m := range tc.init { + acct := Account{Coins: m.coins} + err := storeAccount(store, h.makeKey(m.addr), acct) + require.Nil(err, "%d: %+v", i, err) + } + + ctx := stack.MockContext().WithPermissions(tc.perms...) + _, err := h.DeliverTx(ctx, store, tc.tx) + if len(tc.final) > 0 { // valid assert.Nil(err, "%d: %+v", i, err) + // make sure the final balances are correct + for _, f := range tc.final { + acct, err := loadAccount(store, h.makeKey(f.addr)) + assert.Nil(err, "%d: %+v", i, err) + assert.Equal(f.coins, acct.Coins) + } } else { assert.NotNil(err, "%d", i) + // TODO: make sure balances unchanged! } } + } diff --git a/modules/coin/store.go b/modules/coin/store.go index bede0c009c..15a280131d 100644 --- a/modules/coin/store.go +++ b/modules/coin/store.go @@ -45,18 +45,24 @@ func (a Accountant) ChangeCoins(store types.KVStore, addr basecoin.Actor, coins // it doesn't save anything, that is up to you to decide (Check/Change Coins) func (a Accountant) updateCoins(store types.KVStore, addr basecoin.Actor, coins types.Coins, seq int) (acct Account, err error) { acct, err = loadAccount(store, a.makeKey(addr)) + // we can increase an empty account... + if IsNoAccountErr(err) && coins.IsPositive() { + err = nil + } if err != nil { return acct, err } - // check sequence - if seq != acct.Sequence+1 { - return acct, ErrInvalidSequence() + // check sequence if we are deducting... ugh, need a cleaner replay protection + if !coins.IsPositive() { + if seq != acct.Sequence+1 { + return acct, ErrInvalidSequence() + } + acct.Sequence += 1 } - acct.Sequence += 1 // check amount - final := acct.Coins.Minus(coins) + final := acct.Coins.Plus(coins) if !final.IsNonnegative() { return acct, ErrInsufficientFunds() }