decred.org/dcrdex@v1.0.5/client/orderbook/epochqueue.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  	"sort"
    10  	"sync"
    11  
    12  	"decred.org/dcrdex/dex/msgjson"
    13  	"decred.org/dcrdex/dex/order"
    14  	"github.com/decred/dcrd/crypto/blake256"
    15  )
    16  
    17  // epochOrder represents a compact EpochOrderNote.
    18  type epochOrder struct {
    19  	Side       uint8
    20  	Quantity   uint64
    21  	Rate       uint64
    22  	Commitment order.Commitment
    23  	epoch      uint64
    24  }
    25  
    26  // EpochQueue represents a client epoch queue.
    27  type EpochQueue struct {
    28  	mtx    sync.RWMutex
    29  	orders map[order.OrderID]*epochOrder
    30  }
    31  
    32  // NewEpochQueue creates a client epoch queue.
    33  func NewEpochQueue() *EpochQueue {
    34  	return &EpochQueue{
    35  		orders: make(map[order.OrderID]*epochOrder),
    36  	}
    37  }
    38  
    39  // Enqueue appends the provided order note to the epoch queue.
    40  func (eq *EpochQueue) Enqueue(note *msgjson.EpochOrderNote) error {
    41  	eq.mtx.Lock()
    42  	defer eq.mtx.Unlock()
    43  
    44  	if len(note.OrderID) != order.OrderIDSize {
    45  		return fmt.Errorf("expected order id length of %d, got %d",
    46  			order.OrderIDSize, len(note.OrderID))
    47  	}
    48  
    49  	var oid order.OrderID
    50  	copy(oid[:], note.OrderID)
    51  
    52  	// Ensure the provided epoch order is not already queued.
    53  	_, ok := eq.orders[oid]
    54  	if ok {
    55  		return fmt.Errorf("%s is already queued", oid)
    56  	}
    57  
    58  	var commitment order.Commitment
    59  	copy(commitment[:], note.Commit)
    60  
    61  	order := &epochOrder{
    62  		Commitment: commitment,
    63  		Quantity:   note.Quantity,
    64  		Rate:       note.Rate,
    65  		Side:       note.Side,
    66  		epoch:      note.Epoch,
    67  	}
    68  
    69  	eq.orders[oid] = order
    70  
    71  	return nil
    72  }
    73  
    74  // Size returns the number of entries in the epoch queue.
    75  func (eq *EpochQueue) Size() int {
    76  	eq.mtx.RLock()
    77  	size := len(eq.orders)
    78  	eq.mtx.RUnlock()
    79  	return size
    80  }
    81  
    82  // Exists checks if the provided order id is in the queue.
    83  func (eq *EpochQueue) Exists(oid order.OrderID) bool {
    84  	eq.mtx.RLock()
    85  	_, ok := eq.orders[oid]
    86  	eq.mtx.RUnlock()
    87  	return ok
    88  }
    89  
    90  // pimgMatch pairs matched preimage and epochOrder while generating the match
    91  // proof.
    92  type pimgMatch struct {
    93  	id   order.OrderID
    94  	ord  *epochOrder
    95  	pimg order.Preimage
    96  }
    97  
    98  // GenerateMatchProof calculates the sorting seed used in order matching
    99  // as well as the commitment checksum from the provided epoch queue
   100  // preimages and misses.
   101  //
   102  // The epoch queue needs to be reset if there are preimage mismatches or
   103  // non-existent orders for preimage errors.
   104  func (eq *EpochQueue) GenerateMatchProof(preimages []order.Preimage, misses []order.OrderID) (msgjson.Bytes, msgjson.Bytes, error) {
   105  	eq.mtx.Lock()
   106  	defer eq.mtx.Unlock()
   107  
   108  	if len(eq.orders) == 0 {
   109  		return nil, nil, fmt.Errorf("cannot generate match proof with an empty epoch queue")
   110  	}
   111  
   112  	// Get the commitments for all orders in the epoch queue.
   113  	commits := make([]*order.Commitment, 0, len(eq.orders))
   114  	for _, ord := range eq.orders {
   115  		commits = append(commits, &ord.Commitment)
   116  	}
   117  
   118  	// Sort the commitments slice.
   119  	sort.Slice(commits, func(i, j int) bool {
   120  		return bytes.Compare(commits[i][:], commits[j][:]) < 0
   121  	})
   122  
   123  	// Generate the commitment checksum.
   124  	comH := blake256.New()
   125  	for _, commit := range commits {
   126  		comH.Write(commit[:])
   127  	}
   128  	csum := comH.Sum(nil)
   129  
   130  	// Remove all misses.
   131  	for _, oid := range misses {
   132  		delete(eq.orders, oid)
   133  	}
   134  
   135  	// Map the preimages received with their associated epoch order ids.
   136  	matches := make([]*pimgMatch, 0, len(preimages))
   137  outer:
   138  	for _, pimg := range preimages {
   139  		commitment := blake256.Sum256(pimg[:])
   140  		for oid, ord := range eq.orders {
   141  			if ord.Commitment == commitment {
   142  				matches = append(matches, &pimgMatch{
   143  					id:   oid,
   144  					ord:  ord,
   145  					pimg: pimg,
   146  				})
   147  				delete(eq.orders, oid)
   148  				continue outer
   149  			}
   150  		}
   151  		return nil, nil, fmt.Errorf("no order match found for preimage %x", pimg)
   152  	}
   153  
   154  	sort.Slice(matches, func(i, j int) bool {
   155  		return bytes.Compare(matches[i].id[:], matches[j].id[:]) < 0
   156  	})
   157  
   158  	for oid := range eq.orders {
   159  		fmt.Printf("WARNING! Order not accounted for in match proof: %v\n", oid)
   160  		// csum will not match, so just note what was omitted for debugging.
   161  	}
   162  
   163  	// Compute the hash of the concatenated preimages, sorted by order ID.
   164  	piH := blake256.New()
   165  	for _, match := range matches {
   166  		piH.Write(match.pimg[:])
   167  	}
   168  	seed := piH.Sum(nil)
   169  
   170  	return seed, csum, nil
   171  }
   172  
   173  // Orders returns the epoch queue as a []*Order.
   174  func (eq *EpochQueue) Orders() (orders []*Order) {
   175  	eq.mtx.Lock()
   176  	defer eq.mtx.Unlock()
   177  	for oid, ord := range eq.orders {
   178  		orders = append(orders, &Order{
   179  			OrderID:  oid,
   180  			Side:     ord.Side,
   181  			Quantity: ord.Quantity,
   182  			Rate:     ord.Rate,
   183  			Epoch:    ord.epoch,
   184  		})
   185  	}
   186  	return
   187  }