diff --git a/abci/example/orderbook/app.go b/abci/example/orderbook/app.go index 9b22929cf..db2194683 100644 --- a/abci/example/orderbook/app.go +++ b/abci/example/orderbook/app.go @@ -1,12 +1,13 @@ package orderbook import ( - "fmt" + "encoding/binary" "github.com/cosmos/gogoproto/proto" dbm "github.com/tendermint/tm-db" "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/crypto/tmhash" "github.com/tendermint/tendermint/crypto/ed25519" ) @@ -17,12 +18,15 @@ const Version = 1 const ( // In tendermint a zero code is okay and all non zero codes are errors StatusOK = iota - ErrDecoding - ErrUnknownMessage - ErrValidateBasic - ErrNoAccount - ErrNoPair - ErrInvalidSignature + StatusErrDecoding + StatusErrUnknownMessage + StatusErrValidateBasic + StatusErrNoAccount + StatusErrAccountExists + StatusErrNoPair + StatusErrPairExists + StatusErrInvalidOrder + StatusErrUnacceptableMessage ) type StateMachine struct { @@ -35,6 +39,9 @@ type StateMachine struct { pairs map[string]struct{} // lookup pairs commodities map[string]struct{} // lookup commodities publicKeys map[string]struct{} // lookup existence of an account + // a list of transactions that have been modified by the most recent block + // and will need to result in an update to the db + touchedAccounts map[uint64]struct{} // app-side mempool (also emphemeral) markets map[string]*Market // i.e. ATOM/USDC @@ -57,80 +64,102 @@ func (sm *StateMachine) Info(req types.RequestInfo) types.ResponseInfo { return types.ResponseInfo{} } -// CheckTx to be stateless func (sm *StateMachine) CheckTx(req types.RequestCheckTx) types.ResponseCheckTx { var msg = new(Msg) err := proto.Unmarshal(req.Tx, msg) if err != nil { - return types.ResponseCheckTx{Code: ErrDecoding, Log: err.Error()} // decoding error + return types.ResponseCheckTx{Code: StatusErrDecoding, Log: err.Error()} // decoding error } - // validations for each msg below + if err := msg.ValidateBasic(); err != nil { + return types.ResponseCheckTx{Code: StatusErrValidateBasic, Log: err.Error()} + } + + // add either bids or asks to the market which will match them in PrepareProposal switch m := msg.Sum.(type) { - case *Msg_MsgRegisterPair: - if err := m.MsgRegisterPair.ValidateBasic(); err != nil { - return types.ResponseCheckTx{Code: ErrValidateBasic, Log: err.Error()} - } - - case *Msg_MsgCreateAccount: - if err := m.MsgCreateAccount.ValidateBasic(); err != nil { - return types.ResponseCheckTx{Code: ErrValidateBasic, Log: err.Error()} - } - - case *Msg_MsgBid: - - if err := m.MsgBid.ValidateBasic(); err != nil { - return types.ResponseCheckTx{Code: ErrValidateBasic, Log: err.Error()} - } - - // check if account exists - account, ok := sm.accounts[m.MsgBid.BidOrder.OwnerId] - if !ok { - return types.ResponseCheckTx{Code: ErrNoAccount} - } - - // check the pair exists - if _, ok := sm.pairs[m.MsgBid.Pair.String()]; !ok { - return types.ResponseCheckTx{Code: ErrNoPair} - } - - // verify signature - if !m.MsgBid.BidOrder.ValidateSignature(ed25519.PubKey(account.PublicKey), m.MsgBid.Pair) { - return types.ResponseCheckTx{Code: ErrInvalidSignature} - } - case *Msg_MsgAsk: - - if err := m.MsgAsk.ValidateBasic(); err != nil { - return types.ResponseCheckTx{Code: ErrValidateBasic, Log: err.Error()} - } - - // check if account exists - account, ok := sm.accounts[m.MsgAsk.AskOrder.OwnerId] + market, ok := sm.markets[m.MsgAsk.Pair.String()] if !ok { - return types.ResponseCheckTx{Code: ErrNoAccount} + return types.ResponseCheckTx{Code: StatusErrNoPair} } - - // check the pair exists - if _, ok := sm.pairs[m.MsgAsk.Pair.String()]; !ok { - return types.ResponseCheckTx{Code: ErrNoPair} + market.AddAsk(m.MsgAsk.AskOrder) + case *Msg_MsgBid: + market, ok := sm.markets[m.MsgBid.Pair.String()] + if !ok { + return types.ResponseCheckTx{Code: StatusErrNoPair} } - - // verify signature - if !m.MsgAsk.AskOrder.ValidateSignature(ed25519.PubKey(account.PublicKey), m.MsgAsk.Pair) { - return types.ResponseCheckTx{Code: ErrInvalidSignature} - } - - default: - return types.ResponseCheckTx{Code: ErrUnknownMessage} // unknown message type + market.AddBid(m.MsgBid.BidOrder) } return types.ResponseCheckTx{Code: StatusOK} } +func (sm *StateMachine) ValidateTx(msg *Msg) uint32 { + if err := msg.ValidateBasic(); err != nil { + return StatusErrValidateBasic + } + + switch m := msg.Sum.(type) { + case *Msg_MsgRegisterPair: + pair := m.MsgRegisterPair.Pair + if _, ok := sm.pairs[pair.String()]; ok { + return StatusErrPairExists + } + + reversePair := &Pair{BuyersDenomination: pair.SellersDenomination, SellersDenomination: pair.BuyersDenomination} + if _, ok := sm.pairs[reversePair.String()]; ok { + return StatusErrPairExists + } + + case *Msg_MsgAsk, *Msg_MsgBid: // MsgAsk and MsgBid are not allowed individually - they need to be matched as a TradeSet + return StatusErrUnacceptableMessage + + case *Msg_MsgCreateAccount: + // check for duplicate accounts in state machine + if _, ok := sm.publicKeys[string(m.MsgCreateAccount.PublicKey)]; ok { + return StatusErrAccountExists + } + + case *Msg_MsgTradeSet: + // check the pair exists + if _, ok := sm.pairs[m.MsgTradeSet.TradeSet.Pair.String()]; !ok { + return StatusErrNoPair + } + + for _, order := range m.MsgTradeSet.TradeSet.MatchedOrders { + // validate matched order i.e. users have funds and signatures are valid + if !sm.isMatchedOrderValid(order, m.MsgTradeSet.TradeSet.Pair) { + return StatusErrInvalidOrder + } + } + + default: + return StatusErrUnknownMessage + } + + return StatusOK +} + func (sm *StateMachine) Commit() types.ResponseCommit { - return types.ResponseCommit{} + batch := sm.db.NewBatch() + for accountID := range sm.touchedAccounts { + value, err := proto.Marshal(sm.accounts[accountID]) + if err != nil { + panic(err) + } + var key []byte + binary.BigEndian.PutUint64(key, accountID) + + batch.Set(key, value) + } + batch.WriteSync() + + return types.ResponseCommit{Data: sm.hash()} +} + +func (sm *StateMachine) hash() []byte { + return tmhash.Sum([]byte("hash")) } func (sm *StateMachine) Query(req types.RequestQuery) types.ResponseQuery { @@ -146,9 +175,54 @@ func (sm *StateMachine) BeginBlock(req types.RequestBeginBlock) types.ResponseBe } func (sm *StateMachine) DeliverTx(req types.RequestDeliverTx) types.ResponseDeliverTx { - tradeSet := new(TradeSet) - if err := proto.Unmarshal(req.Tx, tradeSet); err != nil { - panic(fmt.Sprintf("unmarshalling tx: %v", err)) + var msg = new(Msg) + + err := proto.Unmarshal(req.Tx, msg) + if err != nil { + return types.ResponseDeliverTx{Code: StatusErrDecoding, Log: err.Error()} // decoding error + } + + if status := sm.ValidateTx(msg); status != StatusOK { + return types.ResponseDeliverTx{Code: status} + } + + switch m := msg.Sum.(type) { + case *Msg_MsgRegisterPair: + sm.markets[m.MsgRegisterPair.Pair.String()] = NewMarket(m.MsgRegisterPair.Pair) + sm.pairs[m.MsgRegisterPair.Pair.String()] = struct{}{} + + case *Msg_MsgCreateAccount: + nextAccountID := uint64(len(sm.accounts)) + sm.accounts[nextAccountID] = &Account{ + Index: nextAccountID, + PublicKey: m.MsgCreateAccount.PublicKey, + Commodities: m.MsgCreateAccount.Commodities, + } + sm.touchedAccounts[nextAccountID] = struct{}{} + + case *Msg_MsgTradeSet: + pair := m.MsgTradeSet.TradeSet.Pair + for _, order := range m.MsgTradeSet.TradeSet.MatchedOrders { + buyer := sm.accounts[order.OrderBid.OwnerId] + seller := sm.accounts[order.OrderAsk.OwnerId] + + // the buyer gets quantity of the asset that the seller was selling + buyer.AddCommodity(NewCommodity(pair.SellersDenomination, order.OrderAsk.Quantity)) + // the buyer gives up quantity * ask price of the buyers denomination + buyer.SubtractCommodity(NewCommodity(pair.BuyersDenomination, order.OrderAsk.Quantity * order.OrderAsk.AskPrice)) + + // the seller gets quantity * ask price of the asset that the buyer was paying with + seller.AddCommodity(NewCommodity(pair.BuyersDenomination, order.OrderAsk.Quantity * order.OrderAsk.AskPrice)) + // the seller gives up quantity of the commodity they were selling + seller.SubtractCommodity(NewCommodity(pair.SellersDenomination, order.OrderAsk.Quantity)) + + // mark that these account have been touched + sm.touchedAccounts[order.OrderBid.OwnerId] = struct{}{} + sm.touchedAccounts[order.OrderAsk.OwnerId] = struct{}{} + } + + default: + return types.ResponseDeliverTx{Code: StatusErrUnknownMessage} } return types.ResponseDeliverTx{Code: 0} @@ -178,7 +252,34 @@ func (sm *StateMachine) PrepareProposal(req types.RequestPrepareProposal) types. // declare transaction with the size of 0 txs := make([][]byte, 0) - // fetch and match all the bids and asks for each market + // go through the transactions passed up via Tendermint first + for _, tx := range req.Txs { + var msg = new(Msg) + err := proto.Unmarshal(tx, msg) + if err != nil { + panic(err) + } + + // skip over the bids and asks that are proposed. We already have them + if _, ok := msg.Sum.(*Msg_MsgBid); ok { + continue + } + if _, ok := msg.Sum.(*Msg_MsgAsk); ok { + continue + } + + // make sure we're proposing valid transactions + if status := sm.ValidateTx(msg); status != StatusOK { + continue + } + + if len(txs)+len(tx) > int(req.MaxTxBytes) { + return types.ResponsePrepareProposal{Txs: txs} + } + txs = append(txs, tx) + } + + // fetch and match all the bids and asks for each market and add these for _, market := range sm.markets { tradeSet := market.Match() // tradesets into bytes and bytes into a transaction @@ -205,53 +306,6 @@ func (sm *StateMachine) PrepareProposal(req types.RequestPrepareProposal) types. txs = append(txs, bz) } - for _, tx := range req.Txs { - var msg = new(Msg) - err := proto.Unmarshal(tx, msg) - if err != nil { - panic(err) - } - - switch m := msg.Sum.(type) { - case *Msg_MsgRegisterPair: - // run the validation checks to see if duplicates within the pairs - pair := m.MsgRegisterPair.Pair - if _, ok := sm.pairs[pair.String()]; ok { - // this pair already exists so we skip over the message - // garbage collection should pick it up - continue - } - - reversePair := &Pair{BuyersDenomination: pair.SellersDenomination, SellersDenomination: pair.BuyersDenomination} - if _, ok := sm.pairs[reversePair.String()]; ok { - // the reverse pair already exists so we skip over it - continue - } - - // check to see that we don't over populate the block - if len(txs)+len(tx) > int(req.MaxTxBytes) { - return types.ResponsePrepareProposal{Txs: txs} - } - txs = append(txs, tx) - - case *Msg_MsgCreateAccount: - // check for duplicate accounts in sm - if _, ok := sm.publicKeys[string(m.MsgCreateAccount.PublicKey)]; ok { - continue - } - - // check to see that we don't over populate the block - if len(txs)+len(tx) > int(req.MaxTxBytes) { - return types.ResponsePrepareProposal{Txs: txs} - } - txs = append(txs, tx) - case *Msg_MsgAsk, *Msg_MsgBid: - // Already have these in the market and are paring together so not necessary to include here - default: - panic(fmt.Sprintf("unknown msg type in prepare proposal %T", m)) - } - } - return types.ResponsePrepareProposal{Txs: req.Txs} } @@ -261,70 +315,11 @@ func (sm *StateMachine) ProcessProposal(req types.RequestProcessProposal) types. var msg = new(Msg) err := proto.Unmarshal(tx, msg) if err != nil { - panic(err) + return rejectProposal() } - switch m := msg.Sum.(type) { - case *Msg_MsgRegisterPair: - if err := m.MsgRegisterPair.ValidateBasic(); err != nil { - return rejectProposal() - } - - pair := m.MsgRegisterPair.Pair - if _, ok := sm.pairs[pair.String()]; ok { - return rejectProposal() - } - - reversePair := &Pair{BuyersDenomination: pair.SellersDenomination, SellersDenomination: pair.BuyersDenomination} - if _, ok := sm.pairs[reversePair.String()]; ok { - return rejectProposal() - } - - case *Msg_MsgAsk, *Msg_MsgBid: // MsgAsk and MsgBid are not allowed individually - they need to be matched as a TradeSet + if status := sm.ValidateTx(msg); status != StatusOK { return rejectProposal() - - case *Msg_MsgCreateAccount: - if err := m.MsgCreateAccount.ValidateBasic(); err != nil { - return rejectProposal() - } - - // check for duplicate accounts in sm - if _, ok := sm.publicKeys[string(m.MsgCreateAccount.PublicKey)]; ok { - return rejectProposal() - } - - case *Msg_MsgTradeSet: - // for each matched order - // check the accounts exist, that the signatures are valid and that they have the available funds to make the swap - if err := m.MsgTradeSet.TradeSet.ValidateBasic(); err != nil { - return rejectProposal() - } - - // check the pair exists - if _, ok := sm.pairs[m.MsgTradeSet.TradeSet.Pair.String()]; !ok { - return rejectProposal() - } - - for _, order := range m.MsgTradeSet.TradeSet.MatchedOrders { - // validate matched order i.e. users have funds - if !sm.isMatchedOrderValid(order, m.MsgTradeSet.TradeSet.Pair) { - return rejectProposal() - } - - // verify signatures - bidOwner := sm.accounts[order.OrderBid.OwnerId] - askOwner := sm.accounts[order.OrderAsk.OwnerId] - if !order.OrderAsk.ValidateSignature(ed25519.PubKey(askOwner.PublicKey), m.MsgTradeSet.TradeSet.Pair) { - return rejectProposal() - } - if !order.OrderBid.ValidateSignature(ed25519.PubKey(bidOwner.PublicKey), m.MsgTradeSet.TradeSet.Pair) { - return rejectProposal() - } - } - - default: - return rejectProposal() - } } @@ -375,6 +370,13 @@ func (sm *StateMachine) isMatchedOrderValid(order *MatchedOrder, pair *Pair) boo return false } + if !order.OrderAsk.ValidateSignature(ed25519.PubKey(askOwner.PublicKey), pair) { + return false + } + if !order.OrderBid.ValidateSignature(ed25519.PubKey(bidOwner.PublicKey), pair) { + return false + } + return true } diff --git a/abci/example/orderbook/app_test.go b/abci/example/orderbook/app_test.go index 83017dd68..e813cde5f 100644 --- a/abci/example/orderbook/app_test.go +++ b/abci/example/orderbook/app_test.go @@ -22,12 +22,12 @@ func TestCheckTx(t *testing.T) { { name: "test empty tx", msg: &orderbook.Msg{}, - responseCode: orderbook.ErrUnknownMessage, + responseCode: orderbook.StatusErrUnknownMessage, }, { name: "test msg ask", msg: &orderbook.Msg{Sum: &orderbook.Msg_MsgAsk{MsgAsk: &orderbook.MsgAsk{ - Pair: &testPair, + Pair: testPair, AskOrder: &orderbook.OrderAsk{ Quantity: 10, AskPrice: 1, @@ -40,7 +40,7 @@ func TestCheckTx(t *testing.T) { { name: "test msg bid", msg: &orderbook.Msg{Sum: &orderbook.Msg_MsgBid{MsgBid: &orderbook.MsgBid{ - Pair: &testPair, + Pair: testPair, BidOrder: &orderbook.OrderBid{ MaxQuantity: 15, MaxPrice: 5, @@ -53,7 +53,7 @@ func TestCheckTx(t *testing.T) { { name: "test msg register pair", msg: &orderbook.Msg{Sum: &orderbook.Msg_MsgRegisterPair{MsgRegisterPair: &orderbook.MsgRegisterPair{ - Pair: &testPair, + Pair: testPair, }}}, responseCode: orderbook.StatusOK, }, diff --git a/abci/example/orderbook/market.go b/abci/example/orderbook/market.go index 317372825..348be0ec4 100644 --- a/abci/example/orderbook/market.go +++ b/abci/example/orderbook/market.go @@ -5,14 +5,14 @@ import ( ) type Market struct { - pair Pair // i.e. EUR/USD (a market is bidirectional) + pair *Pair // i.e. EUR/USD (a market is bidirectional) askOrders *AskOrders // i.e. buying EUR for USD lowestAsk float64 bidOrders *BidOrders // i.e. selling EUR for USD or buying USD for EUR highestBid float64 } -func NewMarket(p Pair) *Market { +func NewMarket(p *Pair) *Market { askOrders := make(AskOrders, 0) bidOrders := make(BidOrders, 0) return &Market{pair: p, askOrders: &askOrders, bidOrders: &bidOrders} @@ -47,7 +47,7 @@ func (m *Market) Match() *TradeSet { return nil } - t := &TradeSet{Pair: &m.pair} + t := &TradeSet{Pair: m.pair} bids := make([]*OrderBid, 0) asks := make([]*OrderAsk, 0) diff --git a/abci/example/orderbook/market_test.go b/abci/example/orderbook/market_test.go index f3e2f228b..9f5399dd9 100644 --- a/abci/example/orderbook/market_test.go +++ b/abci/example/orderbook/market_test.go @@ -7,7 +7,7 @@ import ( "github.com/tendermint/tendermint/abci/example/orderbook" ) -var testPair = orderbook.Pair{BuyersDenomination: "ATOM", SellersDenomination: "USD"} +var testPair = &orderbook.Pair{BuyersDenomination: "ATOM", SellersDenomination: "USD"} func testBid(price, quantity float64) *orderbook.OrderBid { return &orderbook.OrderBid{ diff --git a/abci/example/orderbook/types.go b/abci/example/orderbook/types.go index 3e5350f8b..d9cb65462 100644 --- a/abci/example/orderbook/types.go +++ b/abci/example/orderbook/types.go @@ -115,6 +115,13 @@ func (msg *MsgRegisterPair) ValidateBasic() error { return msg.Pair.ValidateBasic() } +func NewCommodity(denom string, quantity float64) *Commodity { + return &Commodity{ + Denom: denom, + Quantity: quantity, + } +} + func (c *Commodity) ValidateBasic() error { if c.Quantity <= 0 { return errors.New("quantity must be greater than zero") @@ -235,3 +242,54 @@ func (a *Account) FindCommidity(denom string) *Commodity { return nil } + +func (a *Account) AddCommodity(c *Commodity) { + curr := a.FindCommidity(c.Denom) + if curr == nil { + a.Commodities = append(a.Commodities, c) + } else { + curr.Quantity += c.Quantity + } +} + +func (a *Account) SubtractCommodity(c *Commodity) { + curr := a.FindCommidity(c.Denom) + if curr == nil { + panic("trying to remove a commodity the account does not have") + } + curr.Quantity -= c.Quantity +} + +func (msg *Msg) ValidateBasic() error { + switch m := msg.Sum.(type) { + case *Msg_MsgRegisterPair: + if err := m.MsgRegisterPair.ValidateBasic(); err != nil { + return err + } + + case *Msg_MsgCreateAccount: + if err := m.MsgCreateAccount.ValidateBasic(); err != nil { + return err + } + + case *Msg_MsgBid: + if err := m.MsgBid.ValidateBasic(); err != nil { + return err + } + + case *Msg_MsgAsk: + if err := m.MsgAsk.ValidateBasic(); err != nil { + return err + } + + case *Msg_MsgTradeSet: + if err := m.MsgTradeSet.TradeSet.ValidateBasic(); err != nil { + return err + } + + default: + return errors.New("unknown tx") + } + + return nil +} \ No newline at end of file