decred.org/dcrdex@v1.0.5/client/orderbook/orderbook.go (about)

     1  // This code is available on the terms of the project LICENSE.md file,
     2  // also available online at https://blueoakcouncil.org/license/1.0.0.
     3  
     4  package orderbook
     5  
     6  import (
     7  	"bytes"
     8  	"fmt"
     9  	"sync"
    10  	"sync/atomic"
    11  
    12  	"decred.org/dcrdex/dex"
    13  	"decred.org/dcrdex/dex/msgjson"
    14  	"decred.org/dcrdex/dex/order"
    15  	"decred.org/dcrdex/dex/utils"
    16  )
    17  
    18  // ErrEmptyOrderbook is returned from MidGap when the order book is empty.
    19  const ErrEmptyOrderbook = dex.ErrorKind("cannot calculate mid-gap from empty order book")
    20  
    21  // Order represents an ask or bid.
    22  type Order struct {
    23  	OrderID  order.OrderID
    24  	Side     uint8
    25  	Quantity uint64
    26  	Rate     uint64
    27  	Time     uint64
    28  	// Epoch is only used in the epoch queue, otherwise it is ignored.
    29  	Epoch uint64
    30  }
    31  
    32  func (o *Order) sell() bool {
    33  	return o.Side == msgjson.SellOrderNum
    34  }
    35  
    36  // RemoteOrderBook defines the functions a client tracked order book
    37  // must implement.
    38  type RemoteOrderBook interface {
    39  	// Sync instantiates a client tracked order book with the
    40  	// current order book snapshot.
    41  	Sync(*msgjson.OrderBook)
    42  	// Book adds a new order to the order book.
    43  	Book(*msgjson.BookOrderNote)
    44  	// Unbook removes an order from the order book.
    45  	Unbook(*msgjson.UnbookOrderNote) error
    46  }
    47  
    48  // CachedOrderNote represents a cached order not entry.
    49  type cachedOrderNote struct {
    50  	Route     string
    51  	OrderNote any
    52  }
    53  
    54  // rateSell provides the rate and book side information about an order that is
    55  // required for efficiently referencing it in a bookSide.
    56  type rateSell struct {
    57  	rate uint64
    58  	sell bool
    59  }
    60  
    61  // MatchSummary summarizes one or more consecutive matches at a given rate and
    62  // buy/sell direction. Consecutive matches of the same rate and direction are
    63  // binned by the server.
    64  type MatchSummary struct {
    65  	Rate  uint64 `json:"rate"`
    66  	Qty   uint64 `json:"qty"`
    67  	Stamp uint64 `json:"stamp"`
    68  	Sell  bool   `json:"sell"`
    69  }
    70  
    71  // OrderBook represents a client tracked order book.
    72  type OrderBook struct {
    73  	// feeRates is at the top to account for atomic field alignment in
    74  	// 32-bit systems. See also https://golang.org/pkg/sync/atomic/#pkg-note-BUG
    75  	feeRates struct {
    76  		base  uint64
    77  		quote uint64
    78  	}
    79  
    80  	log      dex.Logger
    81  	seqMtx   sync.Mutex
    82  	seq      uint64
    83  	marketID string
    84  
    85  	noteQueueMtx sync.Mutex
    86  	noteQueue    []*cachedOrderNote
    87  
    88  	// Track the orders stored in each bookSide.
    89  	ordersMtx sync.Mutex
    90  	orders    map[order.OrderID]rateSell
    91  
    92  	buys  *bookSide
    93  	sells *bookSide
    94  
    95  	syncedMtx sync.Mutex
    96  	synced    bool
    97  
    98  	epochMtx     sync.Mutex
    99  	currentEpoch uint64
   100  	proofedEpoch uint64
   101  	epochQueues  map[uint64]*EpochQueue
   102  
   103  	matchSummaryMtx sync.Mutex
   104  	matchesSummary  []*MatchSummary
   105  }
   106  
   107  // NewOrderBook creates a new order book.
   108  func NewOrderBook(logger dex.Logger) *OrderBook {
   109  	ob := &OrderBook{
   110  		log:         logger,
   111  		noteQueue:   make([]*cachedOrderNote, 0, 16),
   112  		orders:      make(map[order.OrderID]rateSell),
   113  		buys:        newBookSide(descending),
   114  		sells:       newBookSide(ascending),
   115  		epochQueues: make(map[uint64]*EpochQueue),
   116  	}
   117  	return ob
   118  }
   119  
   120  // BaseFeeRate is the last reported base asset fee rate.
   121  func (ob *OrderBook) BaseFeeRate() uint64 {
   122  	return atomic.LoadUint64(&ob.feeRates.base)
   123  }
   124  
   125  // QuoteFeeRate is the last reported quote asset fee rate.
   126  func (ob *OrderBook) QuoteFeeRate() uint64 {
   127  	return atomic.LoadUint64(&ob.feeRates.quote)
   128  }
   129  
   130  // setSynced sets the synced state of the order book.
   131  func (ob *OrderBook) setSynced(value bool) {
   132  	ob.syncedMtx.Lock()
   133  	ob.synced = value
   134  	ob.syncedMtx.Unlock()
   135  }
   136  
   137  // isSynced returns the synced state of the order book.
   138  func (ob *OrderBook) isSynced() bool {
   139  	ob.syncedMtx.Lock()
   140  	defer ob.syncedMtx.Unlock()
   141  	return ob.synced
   142  }
   143  
   144  // setSeq should be called whenever a sequenced message is received. If seq is
   145  // out of sequence, an error is logged.
   146  func (ob *OrderBook) setSeq(seq uint64) {
   147  	ob.seqMtx.Lock()
   148  	defer ob.seqMtx.Unlock()
   149  	if seq != ob.seq+1 {
   150  		ob.log.Errorf("notification received out of sync. %d != %d - 1", ob.seq, seq)
   151  	}
   152  	if seq > ob.seq {
   153  		ob.seq = seq
   154  	}
   155  }
   156  
   157  // cacheOrderNote caches an order note.
   158  func (ob *OrderBook) cacheOrderNote(route string, entry any) error {
   159  	note := new(cachedOrderNote)
   160  
   161  	switch route {
   162  	case msgjson.BookOrderRoute, msgjson.UnbookOrderRoute, msgjson.UpdateRemainingRoute:
   163  		note.Route = route
   164  		note.OrderNote = entry
   165  
   166  		ob.noteQueueMtx.Lock()
   167  		ob.noteQueue = append(ob.noteQueue, note)
   168  		ob.noteQueueMtx.Unlock()
   169  
   170  		return nil
   171  
   172  	default:
   173  		return fmt.Errorf("unknown route provided %s", route)
   174  	}
   175  }
   176  
   177  // processCachedNotes processes all cached notes, each processed note is
   178  // removed from the cache.
   179  func (ob *OrderBook) processCachedNotes() error {
   180  	ob.noteQueueMtx.Lock()
   181  	defer ob.noteQueueMtx.Unlock()
   182  
   183  	ob.log.Debugf("Processing %d cached order notes", len(ob.noteQueue))
   184  	for len(ob.noteQueue) > 0 {
   185  		var entry *cachedOrderNote
   186  		entry, ob.noteQueue = ob.noteQueue[0], ob.noteQueue[1:] // so much for preallocating
   187  
   188  		switch entry.Route {
   189  		case msgjson.BookOrderRoute:
   190  			note, ok := entry.OrderNote.(*msgjson.BookOrderNote)
   191  			if !ok {
   192  				panic("failed to cast cached book order note as a BookOrderNote")
   193  			}
   194  			err := ob.book(note, true)
   195  			if err != nil {
   196  				return err
   197  			}
   198  
   199  		case msgjson.UnbookOrderRoute:
   200  			note, ok := entry.OrderNote.(*msgjson.UnbookOrderNote)
   201  			if !ok {
   202  				panic("failed to cast cached unbook order note as an UnbookOrderNote")
   203  			}
   204  			err := ob.unbook(note, true)
   205  			if err != nil {
   206  				return err
   207  			}
   208  
   209  		case msgjson.UpdateRemainingRoute:
   210  			note, ok := entry.OrderNote.(*msgjson.UpdateRemainingNote)
   211  			if !ok {
   212  				panic("failed to cast cached update_remaining note as an UnbookOrderNote")
   213  			}
   214  			err := ob.updateRemaining(note, true)
   215  			if err != nil {
   216  				return err
   217  			}
   218  
   219  		default:
   220  			return fmt.Errorf("unknown cached note route provided: %s", entry.Route)
   221  		}
   222  	}
   223  
   224  	return nil
   225  }
   226  
   227  // Sync updates a client tracked order book with an order book snapshot. It is
   228  // an error if the the OrderBook is already synced.
   229  func (ob *OrderBook) Sync(snapshot *msgjson.OrderBook) error {
   230  	if ob.isSynced() {
   231  		return fmt.Errorf("order book is already synced")
   232  	}
   233  	return ob.Reset(snapshot)
   234  }
   235  
   236  // Reset forcibly updates a client tracked order book with an order book
   237  // snapshot. This resets the sequence.
   238  // TODO: eliminate this and half of the mutexes!
   239  func (ob *OrderBook) Reset(snapshot *msgjson.OrderBook) error {
   240  	// Don't use setSeq here, since this message is the seed and is not expected
   241  	// to be 1 more than the current seq value.
   242  	ob.seqMtx.Lock()
   243  	ob.seq = snapshot.Seq
   244  	ob.seqMtx.Unlock()
   245  
   246  	atomic.StoreUint64(&ob.feeRates.base, snapshot.BaseFeeRate)
   247  	atomic.StoreUint64(&ob.feeRates.quote, snapshot.QuoteFeeRate)
   248  
   249  	ob.marketID = snapshot.MarketID
   250  
   251  	func() { // Using a function for mutex management with defer.
   252  		ob.matchSummaryMtx.Lock()
   253  		defer ob.matchSummaryMtx.Unlock()
   254  
   255  		ob.matchesSummary = make([]*MatchSummary, len(snapshot.RecentMatches))
   256  		for i, match := range snapshot.RecentMatches {
   257  			rate, qty, ts := match[0], match[1], match[2]
   258  			sell := true
   259  			if match[1] < 0 {
   260  				qty *= -1
   261  				sell = false
   262  			}
   263  
   264  			ob.matchesSummary[i] = &MatchSummary{
   265  				Rate:  uint64(rate),
   266  				Qty:   uint64(qty),
   267  				Sell:  sell,
   268  				Stamp: uint64(ts),
   269  			}
   270  		}
   271  	}()
   272  
   273  	err := func() error { // Using a function for mutex management with defer.
   274  		ob.ordersMtx.Lock()
   275  		defer ob.ordersMtx.Unlock()
   276  
   277  		ob.orders = make(map[order.OrderID]rateSell, len(snapshot.Orders))
   278  		ob.buys.reset()
   279  		ob.sells.reset()
   280  		for _, o := range snapshot.Orders {
   281  			if len(o.OrderID) != order.OrderIDSize {
   282  				return fmt.Errorf("expected order id length of %d, got %d", order.OrderIDSize, len(o.OrderID))
   283  			}
   284  
   285  			var oid order.OrderID
   286  			copy(oid[:], o.OrderID)
   287  			order := &Order{
   288  				OrderID:  oid,
   289  				Side:     o.Side,
   290  				Quantity: o.Quantity,
   291  				Rate:     o.Rate,
   292  				Time:     o.Time,
   293  			}
   294  
   295  			ob.orders[oid] = rateSell{order.Rate, order.sell()}
   296  
   297  			// Append the order to the order book.
   298  			switch o.Side {
   299  			case msgjson.BuyOrderNum:
   300  				ob.buys.Add(order)
   301  
   302  			case msgjson.SellOrderNum:
   303  				ob.sells.Add(order)
   304  
   305  			default:
   306  				ob.log.Errorf("unknown order side provided: %d", o.Side)
   307  			}
   308  		}
   309  		return nil
   310  	}()
   311  	if err != nil {
   312  		return err
   313  	}
   314  
   315  	// Process cached order notes.
   316  	err = ob.processCachedNotes()
   317  	if err != nil {
   318  		return err
   319  	}
   320  
   321  	ob.setSynced(true)
   322  
   323  	return nil
   324  }
   325  
   326  // book is the workhorse of the exported Book function. It allows booking
   327  // cached and uncached order notes.
   328  func (ob *OrderBook) book(note *msgjson.BookOrderNote, cached bool) error {
   329  	if ob.marketID != note.MarketID {
   330  		return fmt.Errorf("invalid note market id %s", note.MarketID)
   331  	}
   332  
   333  	if !cached {
   334  		// Cache the note if the order book is not synced.
   335  		if !ob.isSynced() {
   336  			return ob.cacheOrderNote(msgjson.BookOrderRoute, note)
   337  		}
   338  	}
   339  
   340  	ob.setSeq(note.Seq)
   341  
   342  	if len(note.OrderID) != order.OrderIDSize {
   343  		return fmt.Errorf("expected order id length of %d, got %d",
   344  			order.OrderIDSize, len(note.OrderID))
   345  	}
   346  
   347  	var oid order.OrderID
   348  	copy(oid[:], note.OrderID)
   349  
   350  	order := &Order{
   351  		OrderID:  oid,
   352  		Side:     note.Side,
   353  		Quantity: note.Quantity,
   354  		Rate:     note.Rate,
   355  		Time:     note.Time,
   356  	}
   357  
   358  	ob.ordersMtx.Lock()
   359  	ob.orders[order.OrderID] = rateSell{order.Rate, order.sell()}
   360  	ob.ordersMtx.Unlock()
   361  
   362  	// Add the order to its associated books side.
   363  	switch order.Side {
   364  	case msgjson.BuyOrderNum:
   365  		ob.buys.Add(order)
   366  
   367  	case msgjson.SellOrderNum:
   368  		ob.sells.Add(order)
   369  
   370  	default:
   371  		return fmt.Errorf("unknown order side provided: %d", order.Side)
   372  	}
   373  
   374  	return nil
   375  }
   376  
   377  // Book adds a new order to the order book.
   378  func (ob *OrderBook) Book(note *msgjson.BookOrderNote) error {
   379  	return ob.book(note, false)
   380  }
   381  
   382  // updateRemaining is the workhorse of the exported UpdateRemaining function. It
   383  // allows updating cached and uncached orders.
   384  func (ob *OrderBook) updateRemaining(note *msgjson.UpdateRemainingNote, cached bool) error {
   385  	if ob.marketID != note.MarketID {
   386  		return fmt.Errorf("invalid update_remaining note market id %s", note.MarketID)
   387  	}
   388  
   389  	if !cached {
   390  		// Cache the note if the order book is not synced.
   391  		if !ob.isSynced() {
   392  			return ob.cacheOrderNote(msgjson.UpdateRemainingRoute, note)
   393  		}
   394  	}
   395  
   396  	ob.setSeq(note.Seq)
   397  
   398  	if len(note.OrderID) != order.OrderIDSize {
   399  		return fmt.Errorf("expected order id length of %d, got %d",
   400  			order.OrderIDSize, len(note.OrderID))
   401  	}
   402  
   403  	var oid order.OrderID
   404  	copy(oid[:], note.OrderID)
   405  
   406  	ob.ordersMtx.Lock()
   407  	ordInfo, found := ob.orders[oid]
   408  	ob.ordersMtx.Unlock()
   409  	if !found {
   410  		return fmt.Errorf("update_remaining order %s not found", oid)
   411  	}
   412  
   413  	if ordInfo.sell {
   414  		ob.sells.UpdateRemaining(oid, ordInfo.rate, note.Remaining)
   415  	} else {
   416  		ob.buys.UpdateRemaining(oid, ordInfo.rate, note.Remaining)
   417  	}
   418  	return nil
   419  }
   420  
   421  // UpdateRemaining updates the remaining quantity of a booked order.
   422  func (ob *OrderBook) UpdateRemaining(note *msgjson.UpdateRemainingNote) error {
   423  	return ob.updateRemaining(note, false)
   424  }
   425  
   426  // LogEpochReport is currently a no-op, and will update market history charts in
   427  // the future.
   428  func (ob *OrderBook) LogEpochReport(note *msgjson.EpochReportNote) error {
   429  	// TODO: update future candlestick charts.
   430  	atomic.StoreUint64(&ob.feeRates.base, note.BaseFeeRate)
   431  	atomic.StoreUint64(&ob.feeRates.quote, note.QuoteFeeRate)
   432  	return nil
   433  }
   434  
   435  // unbook is the workhorse of the exported Unbook function. It allows unbooking
   436  // cached and uncached order notes.
   437  func (ob *OrderBook) unbook(note *msgjson.UnbookOrderNote, cached bool) error {
   438  	if ob.marketID != note.MarketID {
   439  		return fmt.Errorf("invalid note market id %s", note.MarketID)
   440  	}
   441  
   442  	if !cached {
   443  		// Cache the note if the order book is not synced.
   444  		if !ob.isSynced() {
   445  			return ob.cacheOrderNote(msgjson.UnbookOrderRoute, note)
   446  		}
   447  	}
   448  
   449  	ob.setSeq(note.Seq)
   450  
   451  	if len(note.OrderID) != order.OrderIDSize {
   452  		return fmt.Errorf("expected order id length of %d, got %d",
   453  			order.OrderIDSize, len(note.OrderID))
   454  	}
   455  
   456  	var oid order.OrderID
   457  	copy(oid[:], note.OrderID)
   458  
   459  	ob.ordersMtx.Lock()
   460  	defer ob.ordersMtx.Unlock() // slightly longer than necessary
   461  	ordInfo, ok := ob.orders[oid]
   462  	if !ok {
   463  		return fmt.Errorf("no order found with id %v", oid)
   464  	}
   465  	delete(ob.orders, oid)
   466  
   467  	// Remove the order from its associated book side and rate bin.
   468  	if ordInfo.sell {
   469  		return ob.sells.Remove(oid, ordInfo.rate)
   470  	}
   471  	return ob.buys.Remove(oid, ordInfo.rate)
   472  }
   473  
   474  // Unbook removes an order from the order book.
   475  func (ob *OrderBook) Unbook(note *msgjson.UnbookOrderNote) error {
   476  	return ob.unbook(note, false)
   477  }
   478  
   479  // BestNOrders returns the best n orders from the provided side.
   480  func (ob *OrderBook) BestNOrders(n int, sell bool) ([]*Order, bool, error) {
   481  	if !ob.isSynced() {
   482  		return nil, false, fmt.Errorf("order book is unsynced")
   483  	}
   484  
   485  	var orders []*Order
   486  	var filled bool
   487  	if sell {
   488  		orders, filled = ob.sells.BestNOrders(n)
   489  	} else {
   490  		orders, filled = ob.buys.BestNOrders(n)
   491  	}
   492  
   493  	return orders, filled, nil
   494  }
   495  
   496  // OrderIsBooked checks if an order is booked or in the epoch queue.
   497  func (ob *OrderBook) OrderIsBooked(oid order.OrderID, sell bool) bool {
   498  	findOrder := func(orders []*Order) bool {
   499  		for _, order := range orders {
   500  			if order.OrderID == oid {
   501  				return true
   502  			}
   503  		}
   504  
   505  		return false
   506  	}
   507  
   508  	var orders []*Order
   509  	if sell {
   510  		orders = ob.sells.Orders()
   511  	} else {
   512  		orders = ob.buys.Orders()
   513  	}
   514  
   515  	if findOrder(orders) {
   516  		return true
   517  	}
   518  
   519  	ob.epochMtx.Lock()
   520  	eq := ob.epochQueues[ob.currentEpoch]
   521  	ob.epochMtx.Unlock()
   522  	var epochOrders []*Order
   523  	if eq != nil {
   524  		epochOrders = eq.Orders()
   525  	}
   526  
   527  	return findOrder(epochOrders)
   528  }
   529  
   530  // VWAP calculates the volume weighted average price for the specified number
   531  // of lots.
   532  func (ob *OrderBook) VWAP(lots, lotSize uint64, sell bool) (avg, extrema uint64, filled bool, err error) {
   533  	orders, _, err := ob.BestNOrders(int(lots), sell)
   534  	if err != nil {
   535  		return 0, 0, false, err
   536  	}
   537  
   538  	remainingLots := lots
   539  	var weightedSum uint64
   540  	for _, order := range orders {
   541  		extrema = order.Rate
   542  		lotsInOrder := order.Quantity / lotSize
   543  		if lotsInOrder >= remainingLots {
   544  			weightedSum += remainingLots * extrema
   545  			filled = true
   546  			break
   547  		}
   548  		remainingLots -= lotsInOrder
   549  		weightedSum += lotsInOrder * extrema
   550  	}
   551  
   552  	if !filled {
   553  		return 0, 0, false, nil
   554  	}
   555  
   556  	return weightedSum / lots, extrema, true, nil
   557  }
   558  
   559  // Orders is the full order book, as slices of sorted buys and sells, and
   560  // unsorted epoch orders in the current epoch.
   561  func (ob *OrderBook) Orders() ([]*Order, []*Order, []*Order) {
   562  	ob.epochMtx.Lock()
   563  	eq := ob.epochQueues[ob.currentEpoch]
   564  	ob.epochMtx.Unlock()
   565  	var epochOrders []*Order
   566  	if eq != nil {
   567  		// NOTE: This epoch is either (1) open or (2) closed but awaiting a
   568  		// match_proof and with no orders for a subsequent epoch yet.
   569  		epochOrders = eq.Orders()
   570  	}
   571  	return ob.buys.Orders(), ob.sells.Orders(), epochOrders
   572  }
   573  
   574  // Enqueue appends the provided order note to the corresponding epoch's queue.
   575  func (ob *OrderBook) Enqueue(note *msgjson.EpochOrderNote) error {
   576  	ob.setSeq(note.Seq)
   577  	idx := note.Epoch
   578  	ob.epochMtx.Lock()
   579  	defer ob.epochMtx.Unlock()
   580  	eq, have := ob.epochQueues[idx]
   581  	if !have {
   582  		eq = NewEpochQueue()
   583  		ob.epochQueues[idx] = eq // NOTE: trusting server here a bit not to flood us with fake epochs
   584  		if idx > ob.currentEpoch {
   585  			ob.currentEpoch = idx
   586  		} else {
   587  			ob.log.Errorf("epoch order note received for epoch %d but current epoch is %d", idx, ob.currentEpoch)
   588  		}
   589  	}
   590  
   591  	return eq.Enqueue(note)
   592  }
   593  
   594  // CurrentEpoch returns the current epoch.
   595  func (ob *OrderBook) CurrentEpoch() uint64 {
   596  	ob.epochMtx.Lock()
   597  	defer ob.epochMtx.Unlock()
   598  	return ob.currentEpoch
   599  }
   600  
   601  // ValidateMatchProof ensures the match proof data provided is correct by
   602  // comparing it to a locally generated proof from the same epoch queue.
   603  func (ob *OrderBook) ValidateMatchProof(note msgjson.MatchProofNote) error {
   604  	idx := note.Epoch
   605  	noteSize := len(note.Preimages) + len(note.Misses)
   606  
   607  	// Extract the EpochQueue in a closure for clean epochMtx handling.
   608  	var firstProof bool
   609  	extractEpochQueue := func() (*EpochQueue, error) {
   610  		ob.epochMtx.Lock()
   611  		defer ob.epochMtx.Unlock()
   612  		firstProof = ob.proofedEpoch == 0
   613  		ob.proofedEpoch = idx
   614  		if eq := ob.epochQueues[idx]; eq != nil {
   615  			delete(ob.epochQueues, idx) // there will be no more additions to this epoch
   616  			return eq, nil
   617  		}
   618  		// This is expected for an empty match proof or if we started mid-epoch.
   619  		if noteSize == 0 || firstProof {
   620  			return nil, nil
   621  		}
   622  		return nil, fmt.Errorf("epoch %d match proof note references %d orders, but local epoch queue is empty",
   623  			idx, noteSize)
   624  	}
   625  	eq, err := extractEpochQueue()
   626  	if eq == nil /* includes err != nil */ {
   627  		return err
   628  	}
   629  
   630  	if noteSize > 0 {
   631  		ob.log.Tracef("Validating match proof note for epoch %d (%s) with %d preimages and %d misses.",
   632  			idx, note.MarketID, len(note.Preimages), len(note.Misses))
   633  	}
   634  	if localSize := eq.Size(); noteSize != localSize {
   635  		if firstProof && localSize < noteSize {
   636  			return nil // we only saw part of the epoch
   637  		}
   638  		// Since match_proof lags epoch close by up to preimage request timeout,
   639  		// this can still happen for multiple proofs after (re)connect.
   640  		return fmt.Errorf("epoch %d match proof note references %d orders, but local epoch queue has %d",
   641  			idx, noteSize, localSize)
   642  	}
   643  	if len(note.Preimages) == 0 {
   644  		return nil
   645  	}
   646  
   647  	pimgs := make([]order.Preimage, len(note.Preimages))
   648  	for i, entry := range note.Preimages {
   649  		copy(pimgs[i][:], entry)
   650  	}
   651  
   652  	misses := make([]order.OrderID, len(note.Misses))
   653  	for i, entry := range note.Misses {
   654  		copy(misses[i][:], entry)
   655  	}
   656  
   657  	seed, csum, err := eq.GenerateMatchProof(pimgs, misses)
   658  	if err != nil {
   659  		return fmt.Errorf("unable to generate match proof for epoch %d: %w",
   660  			idx, err)
   661  	}
   662  
   663  	if !bytes.Equal(seed, note.Seed) {
   664  		return fmt.Errorf("match proof seed mismatch for epoch %d: "+
   665  			"expected %s, got %s", idx, note.Seed, seed)
   666  	}
   667  
   668  	if !bytes.Equal(csum, note.CSum) {
   669  		return fmt.Errorf("match proof csum mismatch for epoch %d: "+
   670  			"expected %s, got %s", idx, note.CSum, csum)
   671  	}
   672  
   673  	return nil
   674  }
   675  
   676  // MidGap returns the mid-gap price for the market. If one market side is empty
   677  // the bets rate from the other side will be used. If both sides are empty, an
   678  // error will be returned.
   679  func (ob *OrderBook) MidGap() (uint64, error) {
   680  	s, senough := ob.sells.BestNOrders(1)
   681  	b, benough := ob.buys.BestNOrders(1)
   682  	if !senough {
   683  		if !benough {
   684  			return 0, ErrEmptyOrderbook
   685  		}
   686  		return b[0].Rate, nil
   687  	}
   688  	if !benough {
   689  		return s[0].Rate, nil
   690  	}
   691  	return (s[0].Rate + b[0].Rate) / 2, nil
   692  }
   693  
   694  // BestFill is the best (rate, quantity) fill for an order of the type and
   695  // quantity specified. BestFill should be used when the exact quantity of base asset
   696  // is known, i.e. limit orders and market sell orders. For market buy orders,
   697  // use BestFillMarketBuy.
   698  func (ob *OrderBook) BestFill(sell bool, qty uint64) ([]*Fill, bool) {
   699  	if sell {
   700  		return ob.buys.BestFill(qty)
   701  	}
   702  	return ob.sells.BestFill(qty)
   703  }
   704  
   705  // BestFillMarketBuy is the best (rate, quantity) fill for a market buy order.
   706  // The qty given will be in units of quote asset.
   707  func (ob *OrderBook) BestFillMarketBuy(qty, lotSize uint64) ([]*Fill, bool) {
   708  	return ob.sells.bestFill(qty, true, lotSize)
   709  }
   710  
   711  // AddRecentMatches adds the recent matches. If the recent matches cache length
   712  // grows bigger than 100, it will slice out the ones first added.
   713  func (ob *OrderBook) AddRecentMatches(matches [][2]int64, ts uint64) []*MatchSummary {
   714  	if matches == nil {
   715  		return nil
   716  	}
   717  	newMatches := make([]*MatchSummary, len(matches))
   718  	for i, m := range matches {
   719  		rate, qty := m[0], m[1]
   720  		// negative qty means maker is a sell
   721  		sell := true
   722  		if qty < 0 {
   723  			qty *= -1
   724  			sell = false
   725  		}
   726  		newMatches[i] = &MatchSummary{
   727  			Rate:  uint64(rate),
   728  			Qty:   uint64(qty),
   729  			Stamp: ts,
   730  			Sell:  sell,
   731  		}
   732  	}
   733  
   734  	// Put the newest first.
   735  	utils.ReverseSlice(newMatches)
   736  
   737  	ob.matchSummaryMtx.Lock()
   738  	defer ob.matchSummaryMtx.Unlock()
   739  	ob.matchesSummary = append(newMatches, ob.matchesSummary...) // nolint:makezero
   740  	const maxLength = 100
   741  	// if ob.matchesSummary length is greater than max length, we slice the array
   742  	// to maxLength, removing values first added.
   743  	if len(ob.matchesSummary) > maxLength {
   744  		ob.matchesSummary = ob.matchesSummary[:maxLength]
   745  	}
   746  	return newMatches
   747  }
   748  
   749  // RecentMatches returns up to 100 recent matches, newest first.
   750  func (ob *OrderBook) RecentMatches() []*MatchSummary {
   751  	ob.matchSummaryMtx.Lock()
   752  	defer ob.matchSummaryMtx.Unlock()
   753  	return ob.matchesSummary
   754  }