From 38897de6d79bb0f0032c6c84a62093584d891488 Mon Sep 17 00:00:00 2001 From: Sam Ricotta Date: Mon, 14 Nov 2022 14:54:52 +0100 Subject: [PATCH] orderbook example --- abci/example/orderbook/app.go | 149 +++++++++++++++++------------ abci/example/orderbook/app_test.go | 88 ++++++++++++----- abci/example/orderbook/store.go | 11 +-- abci/example/orderbook/types.go | 7 ++ 4 files changed, 165 insertions(+), 90 deletions(-) diff --git a/abci/example/orderbook/app.go b/abci/example/orderbook/app.go index b47508250..8edd9e644 100644 --- a/abci/example/orderbook/app.go +++ b/abci/example/orderbook/app.go @@ -1,6 +1,7 @@ package orderbook import ( + "crypto/ed25519" "fmt" "github.com/cosmos/gogoproto/proto" @@ -13,17 +14,23 @@ var _ types.Application = (*StateMachine)(nil) const Version = 1 -//TO DO: Error codes +const ( + // In tendermint a zero code is okay and all non zero codes are errors + StatusOK = iota + ErrDecoding + ErrUnknownMessage + ErrValidateBasic + ErrNoAccount + ErrNoPair +) type StateMachine struct { // persisted state db dbm.DB - // in-memory state - accounts map[uint64]*Account - // ephemeral state (not used for the app hash) but for // convienience + accounts map[uint64]*Account pairs map[string]struct{} // lookup pairs commodities map[string]struct{} // lookup commodities publicKeys map[string]struct{} // lookup existence of an account @@ -34,12 +41,13 @@ type StateMachine struct { func New(db dbm.DB) *StateMachine { // execute a database call that fetches the data for accounts - StateMachine := StateMachine { - accounts: map[uint64]*Account{}, - pairs: map[string]struct{}, - commodities: map[string]struct{}, - publicKeys: map[string]struct{}, - markets: map[string]*Market{}, + StateMachine := StateMachine{ + accounts: make(map[uint64]*Account), + pairs: make(map[string]struct{}), + commodities: make(map[string]struct{}), + publicKeys: make(map[string]struct{}), + markets: make(map[string]*Market), + db: db, } return &StateMachine } @@ -54,79 +62,59 @@ func (sm *StateMachine) CheckTx(req types.RequestCheckTx) types.ResponseCheckTx err := proto.Unmarshal(req.Tx, msg) if err != nil { - return types.ResponseCheckTx{Code: 1} // decoding error + return types.ResponseCheckTx{Code: ErrDecoding, Log: err.Error()} // decoding error } // validations for each msg below switch m := msg.Sum.(type) { case *Msg_MsgRegisterPair: if err := m.MsgRegisterPair.ValidateBasic(); err != nil { - return types.ResponseCheckTx{Code: 3} + return types.ResponseCheckTx{Code: ErrValidateBasic, Log: err.Error()} } case *Msg_MsgCreateAccount: if err := m.MsgCreateAccount.ValidateBasic(); err != nil { - return types.ResponseCheckTx{Code: 3} + return types.ResponseCheckTx{Code: ErrValidateBasic, Log: err.Error()} } case *Msg_MsgBid: if err := m.MsgBid.ValidateBasic(); err != nil { - return types.ResponseCheckTx{Code: 3, Log: err.Error()} + return types.ResponseCheckTx{Code: ErrValidateBasic, Log: err.Error()} } // check if account exists if _, ok := sm.accounts[m.MsgBid.BidOrder.OwnerId]; !ok { - return types.ResponseCheckTx{Code: 4} + return types.ResponseCheckTx{Code: ErrNoAccount} } // check the pair exists if _, ok := sm.pairs[m.MsgBid.Pair.String()]; !ok { - return types.ResponseCheckTx{Code: 4} + return types.ResponseCheckTx{Code: ErrNoPair} } case *Msg_MsgAsk: if err := m.MsgAsk.ValidateBasic(); err != nil { - return types.ResponseCheckTx{Code: 3, Log: err.Error()} + return types.ResponseCheckTx{Code: ErrValidateBasic, Log: err.Error()} } // check if account exists - account, ok := sm.accounts[m.MsgAsk.AskOrder.OwnerId] + _, ok := sm.accounts[m.MsgAsk.AskOrder.OwnerId] if !ok { - return types.ResponseCheckTx{Code: 4} + return types.ResponseCheckTx{Code: ErrNoAccount} } // check the pair exists if _, ok := sm.pairs[m.MsgAsk.Pair.String()]; !ok { - return types.ResponseCheckTx{Code: 4} - } - - // check if pair is registered - if _, ok := sm.markets[m.MsgAsk.Pair.String()]; !ok { - return types.ResponseCheckTx{Code: 4} - - } - - // check the account has a enough quantity - found := false - for _, commodity := range account.Commodities { - if commodity.Denom == m.MsgAsk.Pair.SellersDenomination { - if m.MsgAsk.AskOrder.Quantity > commodity.Quantity { - return types.ResponseCheckTx{Code: 4} - } - found = true - } - } - if !found { - return types.ResponseCheckTx{Code: 4} + return types.ResponseCheckTx{Code: ErrNoPair} } default: - return types.ResponseCheckTx{Code: 2} // unknown message type + return types.ResponseCheckTx{Code: ErrUnknownMessage} // unknown message type } - return types.ResponseCheckTx{Code: 0} + return types.ResponseCheckTx{Code: StatusOK} } func (sm *StateMachine) Commit() types.ResponseCommit { @@ -280,8 +268,11 @@ func (sm *StateMachine) ProcessProposal(req types.RequestProcessProposal) types. return rejectProposal() } - case *Msg_MsgAsk, *Msg_MsgBid: + // TODO: verify the signature + + case *Msg_MsgAsk, *Msg_MsgBid: // MsgAsk and MsgBid are not allowed individually - they need to be matched as a TradeSet return rejectProposal() + case *Msg_MsgCreateAccount: if err := m.MsgCreateAccount.ValidateBasic(); err != nil { return rejectProposal() @@ -292,6 +283,8 @@ func (sm *StateMachine) ProcessProposal(req types.RequestProcessProposal) types. return rejectProposal() } + // TODO: verify the signature + 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 @@ -299,6 +292,24 @@ func (sm *StateMachine) ProcessProposal(req types.RequestProcessProposal) types. 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 { + if !sm.isMatchedOrderValid(order, m.MsgTradeSet.TradeSet.Pair) { + return rejectProposal() + } + + // bidOwner := sm.accounts[order.OrderBid.OwnerId] + // askOwner := sm.accounts[order.OrderAsk.OwnerId] + + // ed25519.Verify(bidOwner.PublicKey, ) + } + + + default: return rejectProposal() @@ -312,25 +323,7 @@ func (sm *StateMachine) validateTradeSetAgainstState(tradeSet *TradeSet) *TradeS output := &TradeSet{Pair: tradeSet.Pair} for _, matchedOrder := range tradeSet.MatchedOrders { - bidOwner := sm.accounts[matchedOrder.OrderBid.OwnerId] - askOwner := sm.accounts[matchedOrder.OrderAsk.OwnerId] - - askCommodities := askOwner.FindCommidity(tradeSet.Pair.SellersDenomination) - if askCommodities == nil { - continue - } - buyCommodities := bidOwner.FindCommidity(tradeSet.Pair.BuyersDenomination) - if buyCommodities == nil { - continue - } - - // Seller has enough of the commodity - if askCommodities.Quantity-matchedOrder.OrderAsk.Quantity < 0 { - continue - } - - // Buyer has enough of the buying commodity - if buyCommodities.Quantity-(matchedOrder.OrderAsk.AskPrice*matchedOrder.OrderAsk.Quantity) < 0 { + if !sm.isMatchedOrderValid(matchedOrder, tradeSet.Pair) { continue } @@ -341,6 +334,38 @@ func (sm *StateMachine) validateTradeSetAgainstState(tradeSet *TradeSet) *TradeS return output } +func (sm *StateMachine) isMatchedOrderValid(order *MatchedOrder, pair *Pair) bool { + bidOwner, exists := sm.accounts[order.OrderBid.OwnerId] + if !exists { + return false + } + askOwner, exists := sm.accounts[order.OrderAsk.OwnerId] + if !exists { + return false + } + + askCommodities := askOwner.FindCommidity(pair.SellersDenomination) + if askCommodities == nil { + return false + } + buyCommodities := bidOwner.FindCommidity(pair.BuyersDenomination) + if buyCommodities == nil { + return false + } + + // Seller has enough of the commodity + if askCommodities.Quantity-order.OrderAsk.Quantity < 0 { + return false + } + + // Buyer has enough of the buying commodity + if buyCommodities.Quantity-(order.OrderAsk.AskPrice*order.OrderAsk.Quantity) < 0 { + return false + } + + return true +} + func rejectProposal() types.ResponseProcessProposal { return types.ResponseProcessProposal{Status: types.ResponseProcessProposal_REJECT} } diff --git a/abci/example/orderbook/app_test.go b/abci/example/orderbook/app_test.go index 191567aef..f0a49719c 100644 --- a/abci/example/orderbook/app_test.go +++ b/abci/example/orderbook/app_test.go @@ -3,35 +3,79 @@ package orderbook_test import ( "testing" + "github.com/cosmos/gogoproto/proto" + "github.com/stretchr/testify/require" + dbm "github.com/tendermint/tm-db" + "github.com/tendermint/tendermint/abci/example/orderbook" "github.com/tendermint/tendermint/abci/types" ) -func TestNew(t *testing.T) { - StateMachine := New() +func TestCheckTx(t *testing.T) { + app := orderbook.New(dbm.NewMemDB()) - - // initialise market - market := orderbook.NewMarket(testPair) - // create transaction - response := S.PrepareProposal(types.RequestPrepareProposal{}) + testCases := []struct { + name string + msg *orderbook.Msg + responseCode int + }{ + { + name: "test empty tx", + msg: &orderbook.Msg{}, + responseCode: orderbook.ErrUnknownMessage, + }, + { + name: "test msg ask", + msg: &orderbook.Msg{Sum: &orderbook.Msg_MsgAsk{MsgAsk: &orderbook.MsgAsk{ + Pair: testPair, + AskOrder: &orderbook.OrderAsk{ + Quantity: 10, + AskPrice: 1, + OwnerId: 1, + Signature: []byte("signature"), + }, + }}}, + responseCode: orderbook.StatusOK, + }, + { + name: "test msg bid", + msg: &orderbook.Msg{Sum: &orderbook.Msg_MsgBid{MsgBid: &orderbook.MsgBid{ + Pair: testPair, + OrderBid: &orderbook.OrderBid{ + MaxQuantity: 15, + MaxPrice: 5, + OwnerId: 1, + Signature: []byte("signature"), + }, + }}, + responseCode: orderbook.StatusOK, + }, + { + name: "test msg register pair", + msg: &orderbook.Msg{Sum: &orderbook.Msg_MsgRegisterPair{MsgRegisterPair: &orderbook.MsgRegisterPair{ + Pair: testPair, + }}, + responseCode: orderbook.StatusOK, + } - // check to see if the market collected all of the pairs - // test to see if the tradeset is valid - // require.EqualValues(t, ) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + bz, err := proto.Marshal(tc.msg) + require.NoError(t, err) + resp := app.CheckTx(types.RequestCheckTx{Tx: bz}) + require.Equal(t, tc.responseCode, resp.Code) + }) + } } -func TestNew(t *testing.T) { - var b map[string]string - var a StateMachine +// func TestPrepareProposal(t *testing.T) { +// app := orderbook.New(dbm.NewMemDB()) +// } +// func TestProcessProposal(t *testing.T) { +// app := orderbook.New(dbm.NewMemDB()) +// } - -} - - -type B struct { - Field string - anotherField []int - anotherField2 [10]int -} \ No newline at end of file +// func TestFinalizeBlock(t *testing.T) { +// app := orderbook.New(dbm.NewMemDB()) +// } diff --git a/abci/example/orderbook/store.go b/abci/example/orderbook/store.go index 8c21fc16c..8f2da4f8e 100644 --- a/abci/example/orderbook/store.go +++ b/abci/example/orderbook/store.go @@ -1,12 +1,11 @@ package orderbook +import dbm "github.com/tendermint/tm-db" - type AccountStore struct { - db dbm.DB - } - +type AccountStore struct { + db dbm.DB +} // iterate over the account database -// Add to the account database +// Add to the account database // find an account - diff --git a/abci/example/orderbook/types.go b/abci/example/orderbook/types.go index 7b168b115..c13ce1a74 100644 --- a/abci/example/orderbook/types.go +++ b/abci/example/orderbook/types.go @@ -100,6 +100,7 @@ func (o *OrderBid) ValidateBasic() error { return nil } + // check signatures are valid func (m *MatchedOrder) ValidateBasic() error { @@ -139,7 +140,11 @@ func (o *OrderAsk) ValidateBasic() error { if o.AskPrice <= 0 { return errors.New("min price must be greater than 0") } + return nil +} +func (o *OrderAsk) DeterministicSignatureBytes(pair *Pair) []byte { + // buf := bytes.NewBuffer() return nil } @@ -149,5 +154,7 @@ func (a *Account) FindCommidity(denom string) *Commodity { return c } } + return nil + }