diff --git a/modules/coin/errors.go b/modules/coin/errors.go index 430b59bffe..39c334e8b2 100644 --- a/modules/coin/errors.go +++ b/modules/coin/errors.go @@ -10,12 +10,13 @@ import ( ) var ( - errNoAccount = fmt.Errorf("No such account") - errInsufficientFunds = fmt.Errorf("Insufficient funds") - errNoInputs = fmt.Errorf("No input coins") - errNoOutputs = fmt.Errorf("No output coins") - errInvalidAddress = fmt.Errorf("Invalid address") - errInvalidCoins = fmt.Errorf("Invalid coins") + errNoAccount = fmt.Errorf("No such account") + errInsufficientFunds = fmt.Errorf("Insufficient funds") + errInsufficientCredit = fmt.Errorf("Insufficient credit") + errNoInputs = fmt.Errorf("No input coins") + errNoOutputs = fmt.Errorf("No output coins") + errInvalidAddress = fmt.Errorf("Invalid address") + errInvalidCoins = fmt.Errorf("Invalid coins") invalidInput = abci.CodeType_BaseInvalidInput invalidOutput = abci.CodeType_BaseInvalidOutput @@ -66,6 +67,13 @@ func IsInsufficientFundsErr(err error) bool { return errors.IsSameError(errInsufficientFunds, err) } +func ErrInsufficientCredit() errors.TMError { + return errors.WithCode(errInsufficientCredit, invalidInput) +} +func IsInsufficientCreditErr(err error) bool { + return errors.IsSameError(errInsufficientCredit, err) +} + func ErrNoInputs() errors.TMError { return errors.WithCode(errNoInputs, invalidInput) } diff --git a/modules/coin/handler.go b/modules/coin/handler.go index 823bfb78aa..a38c7b6cf3 100644 --- a/modules/coin/handler.go +++ b/modules/coin/handler.go @@ -37,28 +37,57 @@ func (Handler) AssertDispatcher() {} func (h Handler) CheckTx(ctx basecoin.Context, store state.SimpleDB, tx basecoin.Tx, _ basecoin.Checker) (res basecoin.Result, err error) { - send, err := checkTx(ctx, tx) + err = tx.ValidateBasic() if err != nil { return res, err } - // now make sure there is money - for _, in := range send.Inputs { - _, err = CheckCoins(store, in.Address, in.Coins.Negative()) - if err != nil { - return res, err - } + switch t := tx.Unwrap().(type) { + case SendTx: + return res, h.checkSendTx(ctx, store, t) + case CreditTx: + return h.creditTx(ctx, store, t) } - - // otherwise, we are good - return res, nil + return res, errors.ErrUnknownTxType(tx.Unwrap()) } // DeliverTx moves the money func (h Handler) DeliverTx(ctx basecoin.Context, store state.SimpleDB, tx basecoin.Tx, cb basecoin.Deliver) (res basecoin.Result, err error) { - send, err := checkTx(ctx, tx) + err = tx.ValidateBasic() + if err != nil { + return res, err + } + + switch t := tx.Unwrap().(type) { + case SendTx: + return h.sendTx(ctx, store, t, cb) + case CreditTx: + return h.creditTx(ctx, store, t) + } + return res, errors.ErrUnknownTxType(tx.Unwrap()) +} + +// SetOption - sets the genesis account balance +func (h Handler) SetOption(l log.Logger, store state.SimpleDB, + module, key, value string, cb basecoin.SetOptioner) (log string, err error) { + if module != NameCoin { + return "", errors.ErrUnknownModule(module) + } + switch key { + case "account": + return setAccount(store, value) + case "issuer": + return setIssuer(store, value) + } + return "", errors.ErrUnknownKey(key) +} + +func (h Handler) sendTx(ctx basecoin.Context, store state.SimpleDB, + send SendTx, cb basecoin.Deliver) (res basecoin.Result, err error) { + + err = checkTx(ctx, send) if err != nil { return res, err } @@ -107,43 +136,65 @@ func (h Handler) DeliverTx(ctx basecoin.Context, store state.SimpleDB, return res, nil } -// SetOption - sets the genesis account balance -func (h Handler) SetOption(l log.Logger, store state.SimpleDB, - module, key, value string, _ basecoin.SetOptioner) (log string, err error) { +func (h Handler) creditTx(ctx basecoin.Context, store state.SimpleDB, + credit CreditTx) (res basecoin.Result, err error) { - if module != NameCoin { - return "", errors.ErrUnknownModule(module) + // first check permissions!! + info, err := loadHandlerInfo(store) + if err != nil { + return res, err } - switch key { - case "account": - return setAccount(store, value) - case "issuer": - return setIssuer(store, value) + if info.Issuer.Empty() || !ctx.HasPermission(info.Issuer) { + return res, errors.ErrUnauthorized() } - return "", errors.ErrUnknownKey(key) + + // load up the account + addr := ChainAddr(credit.Debitor) + acct, err := GetAccount(store, addr) + if err != nil { + return res, err + } + + // make and check changes + acct.Coins = acct.Coins.Plus(credit.Credit) + if !acct.Coins.IsNonnegative() { + return res, ErrInsufficientFunds() + } + acct.Credit = acct.Credit.Plus(credit.Credit) + if !acct.Credit.IsNonnegative() { + return res, ErrInsufficientCredit() + } + + err = storeAccount(store, addr.Bytes(), acct) + return res, err } -func checkTx(ctx basecoin.Context, tx basecoin.Tx) (send SendTx, err error) { - // check if the tx is proper type and valid - send, ok := tx.Unwrap().(SendTx) - if !ok { - return send, errors.ErrInvalidFormat(TypeSend, tx) - } - err = send.ValidateBasic() - if err != nil { - return send, err - } - +func checkTx(ctx basecoin.Context, send SendTx) error { // check if all inputs have permission for _, in := range send.Inputs { if !ctx.HasPermission(in.Address) { - return send, errors.ErrUnauthorized() + return errors.ErrUnauthorized() } } - return send, nil + return nil } -func setAccount(store state.KVStore, value string) (log string, err error) { +func (Handler) checkSendTx(ctx basecoin.Context, store state.SimpleDB, send SendTx) error { + err := checkTx(ctx, send) + if err != nil { + return err + } + // now make sure there is money + for _, in := range send.Inputs { + _, err := CheckCoins(store, in.Address, in.Coins.Negative()) + if err != nil { + return err + } + } + return nil +} + +func setAccount(store state.SimpleDB, value string) (log string, err error) { var acc GenesisAccount err = data.FromJSON([]byte(value), &acc) if err != nil { @@ -165,7 +216,7 @@ func setAccount(store state.KVStore, value string) (log string, err error) { // setIssuer sets a permission for some super-powerful account to // mint money -func setIssuer(store state.KVStore, value string) (log string, err error) { +func setIssuer(store state.SimpleDB, value string) (log string, err error) { var issuer basecoin.Actor err = data.FromJSON([]byte(value), &issuer) if err != nil { diff --git a/modules/coin/handler_test.go b/modules/coin/handler_test.go index dc0086fd3a..b0f20c42fd 100644 --- a/modules/coin/handler_test.go +++ b/modules/coin/handler_test.go @@ -11,6 +11,7 @@ import ( "github.com/tendermint/tmlibs/log" "github.com/tendermint/basecoin" + "github.com/tendermint/basecoin/errors" "github.com/tendermint/basecoin/modules/auth" "github.com/tendermint/basecoin/stack" "github.com/tendermint/basecoin/state" @@ -75,7 +76,7 @@ func TestHandlerValidation(t *testing.T) { for i, tc := range cases { ctx := stack.MockContext("base-chain", 100).WithPermissions(tc.perms...) - _, err := checkTx(ctx, tc.tx) + err := checkTx(ctx, tc.tx.Unwrap().(SendTx)) if tc.valid { assert.Nil(err, "%d: %+v", i, err) } else { @@ -84,7 +85,7 @@ func TestHandlerValidation(t *testing.T) { } } -func TestDeliverTx(t *testing.T) { +func TestDeliverSendTx(t *testing.T) { assert := assert.New(t) require := require.New(t) @@ -247,3 +248,109 @@ func TestSetIssuer(t *testing.T) { assert.Equal(tc.issuer, info.Issuer) } } + +func TestDeliverCreditTx(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + // sample coins + someCoins := Coins{{"atom", 6570}} + minusCoins := Coins{{"atom", -1234}} + lessCoins := someCoins.Plus(minusCoins) + otherCoins := Coins{{"eth", 11}} + mixedCoins := someCoins.Plus(otherCoins) + + // some sample addresses + owner := basecoin.Actor{App: "foo", Address: []byte("rocks")} + addr1 := basecoin.Actor{App: "coin", Address: []byte{1, 2}} + key := NewAccountWithKey(someCoins) + addr2 := key.Actor() + addr3 := basecoin.Actor{ChainID: "other", App: "sigs", Address: []byte{3, 9}} + + h := NewHandler() + store := state.NewMemKVStore() + ctx := stack.MockContext("secret", 77) + + // set the owner who can issue credit + js, err := json.Marshal(owner) + require.Nil(err, "%+v", err) + _, err = h.SetOption(log.NewNopLogger(), store, "coin", "issuer", string(js), nil) + require.Nil(err, "%+v", err) + + // give addr2 some coins to start + _, err = h.SetOption(log.NewNopLogger(), store, "coin", "account", key.MakeOption(), nil) + require.Nil(err, "%+v", err) + + cases := []struct { + tx basecoin.Tx + perm basecoin.Actor + check errors.CheckErr + addr basecoin.Actor + expected Account + }{ + // require permission + { + tx: NewCreditTx(addr1, someCoins), + check: errors.IsUnauthorizedErr, + }, + // add credit + { + tx: NewCreditTx(addr1, someCoins), + perm: owner, + check: errors.NoErr, + addr: addr1, + expected: Account{Coins: someCoins, Credit: someCoins}, + }, + // remove some + { + tx: NewCreditTx(addr1, minusCoins), + perm: owner, + check: errors.NoErr, + addr: addr1, + expected: Account{Coins: lessCoins, Credit: lessCoins}, + }, + // can't remove more cash than there is + { + tx: NewCreditTx(addr1, otherCoins.Negative()), + perm: owner, + check: IsInsufficientFundsErr, + }, + // cumulative with initial state + { + tx: NewCreditTx(addr2, otherCoins), + perm: owner, + check: errors.NoErr, + addr: addr2, + expected: Account{Coins: mixedCoins, Credit: otherCoins}, + }, + // Even if there is cash, credit can't go negative + { + tx: NewCreditTx(addr2, minusCoins), + perm: owner, + check: IsInsufficientCreditErr, + }, + // make sure it works for other chains + { + tx: NewCreditTx(addr3, mixedCoins), + perm: owner, + check: errors.NoErr, + addr: ChainAddr(addr3), + expected: Account{Coins: mixedCoins, Credit: mixedCoins}, + }, + } + + for i, tc := range cases { + myStore := store.Checkpoint() + + myCtx := ctx.WithPermissions(tc.perm) + _, err = h.DeliverTx(myCtx, myStore, tc.tx, nil) + assert.True(tc.check(err), "%d: %+v", i, err) + + if err == nil { + store.Commit(myStore) + acct, err := GetAccount(store, tc.addr) + require.Nil(err, "%+v", err) + assert.Equal(tc.expected, acct, "%d", i) + } + } +} diff --git a/modules/coin/ibc_test.go b/modules/coin/ibc_test.go index 29c5462e55..7b4014a324 100644 --- a/modules/coin/ibc_test.go +++ b/modules/coin/ibc_test.go @@ -117,7 +117,7 @@ func TestIBCPostPacket(t *testing.T) { } -func assertPacket(t *testing.T, istore state.KVStore, destID string, amount Coins) { +func assertPacket(t *testing.T, istore state.SimpleDB, destID string, amount Coins) { assert := assert.New(t) require := require.New(t) diff --git a/modules/ibc/handler.go b/modules/ibc/handler.go index cba5579704..4665f5f328 100644 --- a/modules/ibc/handler.go +++ b/modules/ibc/handler.go @@ -78,12 +78,6 @@ func (h Handler) CheckTx(ctx basecoin.Context, store state.SimpleDB, tx basecoin switch t := tx.Unwrap().(type) { case RegisterChainTx: - // check permission to attach, do it here, so no permission check - // by SetOption - info := LoadInfo(store) - if !info.Registrar.Empty() && !ctx.HasPermission(info.Registrar) { - return res, errors.ErrUnauthorized() - } return h.initSeed(ctx, store, t) case UpdateChainTx: return h.updateSeed(ctx, store, t) @@ -103,12 +97,6 @@ func (h Handler) DeliverTx(ctx basecoin.Context, store state.SimpleDB, tx baseco switch t := tx.Unwrap().(type) { case RegisterChainTx: - // check permission to attach, do it here, so no permission check - // by SetOption - info := LoadInfo(store) - if !info.Registrar.Empty() && !ctx.HasPermission(info.Registrar) { - return res, errors.ErrUnauthorized() - } return h.initSeed(ctx, store, t) case UpdateChainTx: return h.updateSeed(ctx, store, t) @@ -125,6 +113,11 @@ func (h Handler) DeliverTx(ctx basecoin.Context, store state.SimpleDB, tx baseco func (h Handler) initSeed(ctx basecoin.Context, store state.SimpleDB, t RegisterChainTx) (res basecoin.Result, err error) { + info := LoadInfo(store) + if !info.Registrar.Empty() && !ctx.HasPermission(info.Registrar) { + return res, errors.ErrUnauthorized() + } + // verify that the header looks reasonable chainID := t.ChainID() s := NewChainSet(store)