add execution components

This commit is contained in:
Callum Waters
2022-11-15 10:20:58 +01:00
parent a0c68b4941
commit f7184192a0
5 changed files with 243 additions and 183 deletions

View File

@@ -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
}

View File

@@ -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,
},

View File

@@ -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)

View File

@@ -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{

View File

@@ -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
}