Skip to content

Commit

Permalink
add account-based asset funding validation
Browse files Browse the repository at this point in the history
Adds funding validation for account-based assets i.e. eth.

dex: Add RedeemSize field to Asset.

msgjson: Add RedeemSig field to Trade. RedeemSig should be
populated when the redemption is to an account-based asset.
This PR also indirectly proposes a protocol for specifying
the funding account. The account address should be utf-8 encoded
and passed as the coin ID of the only coin, along with the pubkey
and signature. The data signed will be the serialized msgjson.Limit or
msgjson.Market, with server stamp set to 0. Same signature input
for the RedeemSig.

dex/order: The Trade type has methods for accessing the account address.

server/asset: AccountBalancer gets a ValidateSignature method.

general scheme: Orders and matches involving account-based assets are
indexed by Swapper and OrderPQ in the same way they are indexed for
users (account.AccountID). When an order comes into the order router,
stats for existing orders, including outstanding redemptions, is
fetched from all markets and the swapper. These stats are used with
the new order info to validate the users balance. The user must have
sufficient balance to cover all outstanding orders and redemptions.

server/book: All OrderPQ are given an AccountTracker set up for
any account-based assets, or none. The AccountTracker is essentially
the same as the userOrders index, but indexed by account address
instead. This is important because more than one user could be
using the same account.

AccountTracker has methods for iterating booked orders, exposed via
Book and Market methods used by Market as part of the MarketTunnel
interface method, PendingAccount, described below.

server/swap: Swapper gets an account index to mirror its account index.
The new AccountStats method returns information about in-process matches
that aren't yet swapped/redeemed for a particular asset.

server/market: OrderRouter is refactored for improved re-use between
handleLimit and handleMarket. For account based assets, OrderRouter
checks the MarketTunnels and MatchNegotiator for outstanding
order and match info, and then queries the backend (via DEXBalancer)
to see if the account has sufficient balance.

NewMarket handles balance checks on startup. This is accomplished
by additional tracking through the orders loop, and then a balance
check for account-based assets immediately before adding to the
order book. utxo-based asset handling is unchanged except that the
lot size compliance check it moved earlier in the loop.
  • Loading branch information
buck54321 committed Nov 23, 2021
1 parent 05b8d97 commit eae5215
Show file tree
Hide file tree
Showing 25 changed files with 1,972 additions and 488 deletions.
4 changes: 2 additions & 2 deletions client/asset/btc/btc_test.go
Expand Up @@ -2928,12 +2928,12 @@ func testTryRedemptionRequests(t *testing.T, segwit bool, walletType string) {

node.truncateChains()
wallet.findRedemptionQueue = make(map[outPoint]*findRedemptionReq)
node.blockchainMtx.RLock()
node.blockchainMtx.Lock()
node.getBestBlockHashErr = nil
if tt.forcedErr {
node.getBestBlockHashErr = tErr
}
node.blockchainMtx.RUnlock()
node.blockchainMtx.Unlock()
addBlocks(tt.numBlocks)
var startBlock *chainhash.Hash
if tt.startBlockHeight >= 0 {
Expand Down
3 changes: 2 additions & 1 deletion dex/asset.go
Expand Up @@ -118,7 +118,8 @@ type Asset struct {
Version uint32 `json:"version"`
MaxFeeRate uint64 `json:"maxFeeRate"`
SwapSize uint64 `json:"swapSize"`
SwapSizeBase uint64 `json:"swapSizeBase"`
SwapSizeBase uint64 `json:"swapSizeBase"` // = SwapSize for account-based assets
RedeemSize uint64 `json:"redeemSize,omitempty"` // Account-based assets only
SwapConf uint32 `json:"swapConf"`
UnitInfo UnitInfo `json:"unitInfo"`
}
Expand Down
1 change: 1 addition & 0 deletions dex/bip-id.go
Expand Up @@ -479,6 +479,7 @@ var bipIDs = map[uint32]string{
890: "xsel",
900: "lmo",
916: "meta",
966: "matic",
970: "twins",
996: "okp",
997: "sum",
Expand Down
4 changes: 4 additions & 0 deletions dex/calc/fees.go
Expand Up @@ -14,12 +14,16 @@ import (
// number of lots in the order. For the quote asset, maxSwaps is not swapVal /
// lotSize, so it must be a separate parameter. The chained swap txns will be
// the standard size as they will spend a previous swap's change output.
// For account-based assets, inputsSize will be zero, and nfo.SwapSize =
// nfo.SwapSizeBase.
func RequiredOrderFunds(swapVal, inputsSize, maxSwaps uint64, nfo *dex.Asset) uint64 {
return RequiredOrderFundsAlt(swapVal, inputsSize, maxSwaps, nfo.SwapSizeBase, nfo.SwapSize, nfo.MaxFeeRate)
}

// RequiredOrderFundsAlt is the same as RequiredOrderFunds, but built-in type
// parameters.
// For account-based assets, inputsSize will be zero, and swapSize =
// swapSizeBase.
func RequiredOrderFundsAlt(swapVal, inputsSize, maxSwaps, swapSizeBase, swapSize, feeRate uint64) uint64 {
baseBytes := maxSwaps * swapSize
// SwapSize already includes one input, replace the size of the first swap
Expand Down
21 changes: 15 additions & 6 deletions dex/msgjson/types.go
Expand Up @@ -666,8 +666,6 @@ func (p *Prefix) Stamp(t uint64) {
p.ServerTime = t
}

// TODO: Update prefix serialization with commitment.

// Serialize serializes the Prefix data.
func (p *Prefix) Serialize() []byte {
// serialization: account ID (32) + base asset (4) + quote asset (4) +
Expand All @@ -679,16 +677,20 @@ func (p *Prefix) Serialize() []byte {
b = append(b, uint32Bytes(p.Quote)...)
b = append(b, p.OrderType)
b = append(b, uint64Bytes(p.ClientTime)...)
// Note: ServerTime is zero for the client's signature message, but non-zero
// for the server's. This is in contrast to an order.Order which cannot
// even be serialized without the server's timestamp.
b = append(b, uint64Bytes(p.ServerTime)...)
return append(b, p.Commit...)
}

// Trade is common to Limit and Market Payloads.
type Trade struct {
Side uint8 `json:"side"`
Quantity uint64 `json:"ordersize"`
Coins []*Coin `json:"coins"`
Address string `json:"address"`
Side uint8 `json:"side"`
Quantity uint64 `json:"ordersize"`
Coins []*Coin `json:"coins"`
Address string `json:"address"`
RedeemSig *RedeemSig `json:"redeemsig,omitempty"` // account-based assets only. not serialized.
}

// Serialize serializes the Trade data.
Expand Down Expand Up @@ -753,6 +755,13 @@ func (c *CancelOrder) Serialize() []byte {
return append(c.Prefix.Serialize(), c.TargetID...)
}

// RedeemSig is a signature proving ownership of the redeeming address. This is
// only necessary as part of a Trade if the asset received is account-based.
type RedeemSig struct {
PubKey dex.Bytes `json:"pubkey"`
Sig dex.Bytes `json:"sig"`
}

// OrderResult is returned from the order-placing routes.
type OrderResult struct {
Sig Bytes `json:"sig"`
Expand Down
36 changes: 36 additions & 0 deletions dex/order/order.go
Expand Up @@ -473,6 +473,42 @@ func (t *Trade) SwapAddress() string {
return t.Address
}

// FromAccount is the account that the order originates from. Only useful for
// account-based assets. Use of this method assumes that account coin has
// already been added.
func (t *Trade) FromAccount() string {
if len(t.Coins) == 0 {
return "no coins?"
}
return string(t.Coins[0])
}

// ToAccount is the account that the order pays to. Only useful for
// account-based assets.
func (t *Trade) ToAccount() string {
return t.Address
}

// BaseAccount is the account address associated with the base asset for the
// order. Only useful for account-based assets. Use of this method assumes that
// account coin has already been added (when sell = true).
func (t *Trade) BaseAccount() string {
if t.Sell {
return t.FromAccount()
}
return t.ToAccount()
}

// QuoteAccount is the account address associated with the quote asset for the
// order. Only useful for account-based assets. Use of this method assumes that
// account coin has already been added (when sell = false).
func (t *Trade) QuoteAccount() string {
if t.Sell {
return t.ToAccount()
}
return t.FromAccount()
}

// serializeSize returns the length of the serialized Trade.
func (t *Trade) serializeSize() int {
// Compute the size of the serialized Coin IDs.
Expand Down
3 changes: 3 additions & 0 deletions server/asset/common.go
Expand Up @@ -92,6 +92,9 @@ type OutputTracker interface {
type AccountBalancer interface {
// AccountBalance retrieves the current account balance.
AccountBalance(addr string) (uint64, error)
// ValidateSignature checks that the pubkey is correct for the address and
// that the signature shows ownership of the associated private key.
ValidateSignature(addr string, pubkey, msg, sig []byte) error
}

// Coin represents a transaction input or output.
Expand Down
2 changes: 1 addition & 1 deletion server/asset/eth/eth.go
Expand Up @@ -239,7 +239,7 @@ func (eth *Backend) InitTxSize() uint32 {
// need to be per transaction. Setting this to zero produces the expected
// result in fee calculations.
func (eth *Backend) InitTxSizeBase() uint32 {
return 0
return InitGas
}

// FeeRate returns the current optimal fee rate in gwei / gas.
Expand Down
3 changes: 2 additions & 1 deletion server/asset/eth/eth_test.go
Expand Up @@ -456,8 +456,9 @@ func TestRequiredOrderFunds(t *testing.T) {
SwapSize: initSize,
MaxFeeRate: feeRate,
}

// Second argument called inputsSize same as another initSize.
got := calc.RequiredOrderFunds(swapVal, initSize, numSwaps, nfo)
got := calc.RequiredOrderFunds(swapVal, 0, numSwaps, nfo)
if got != want {
t.Fatalf("want %v got %v for fees", want, got)
}
Expand Down
124 changes: 124 additions & 0 deletions server/book/accounts.go
@@ -0,0 +1,124 @@
// This code is available on the terms of the project LICENSE.md file,
// also available online at https://blueoakcouncil.org/license/1.0.0.

package book

import (
"decred.org/dcrdex/dex/order"
)

// AccountTracking is a bitfield representing the assets which need account
// tracking.
type AccountTracking uint8

const (
// AccountTrackingBase should be included if the base asset is
// account-based.
AccountTrackingBase AccountTracking = 1 << iota
// AccountTrackingQuote should be included if the quote asset is
// account-based.
AccountTrackingQuote
)

func (a AccountTracking) base() bool {
return (a & AccountTrackingBase) > 0
}

func (a AccountTracking) quote() bool {
return (a & AccountTrackingQuote) > 0
}

// accountTracker tracks orders for account-based assets. Account tracker only
// tracks assets that are account-based, as specified in the constructor.
// If neither base or quote is account-based, then all of accountTracker's
// methods do nothing, so there's no harm in using a newAccountTracker(0) rather
// than checking whether assets are actually account-based everywhere.
// The accountTracker is not thread-safe. In use, synchronization is provided by
// the *OrderPQ's mutex.
type accountTracker struct {
tracking AccountTracking
base, quote map[string]map[order.OrderID]*order.LimitOrder
}

func newAccountTracker(tracking AccountTracking) *accountTracker {
// nilness is used to signal that an asset is not account-based and does
// not need tracking.
var base, quote map[string]map[order.OrderID]*order.LimitOrder
if tracking.base() {
base = make(map[string]map[order.OrderID]*order.LimitOrder)
}
if tracking.quote() {
quote = make(map[string]map[order.OrderID]*order.LimitOrder)
}
return &accountTracker{
tracking: tracking,
base: base,
quote: quote,
}
}

// add an order to tracking.
func (a *accountTracker) add(lo *order.LimitOrder) {
if a.base != nil {
addAccountOrder(lo.BaseAccount(), a.base, lo)
}
if a.quote != nil {
addAccountOrder(lo.QuoteAccount(), a.quote, lo)
}
}

// remove an order from tracking.
func (a *accountTracker) remove(lo *order.LimitOrder) {
if a.base != nil {
removeAccountOrder(lo.BaseAccount(), a.base, lo.ID())
}
if a.quote != nil {
removeAccountOrder(lo.QuoteAccount(), a.quote, lo.ID())
}
}

// addAccountOrder adds the order to the account address -> orders map, creating
// an entry if necessary.
func addAccountOrder(addr string, acctOrds map[string]map[order.OrderID]*order.LimitOrder, lo *order.LimitOrder) {
ords, found := acctOrds[addr]
if !found {
ords = make(map[order.OrderID]*order.LimitOrder)
acctOrds[addr] = ords
}
ords[lo.ID()] = lo
}

// removeAccountOrder removes the order from the account address -> orders map,
// deleting the map if empty.
func removeAccountOrder(addr string, acctOrds map[string]map[order.OrderID]*order.LimitOrder, oid order.OrderID) {
ords, found := acctOrds[addr]
if !found {
return
}
delete(ords, oid)
if len(ords) == 0 {
delete(acctOrds, addr)
}
}

// iterateBaseAccount calls the provided function for every tracked order with a
// base asset corresponding to the specified account address.
func (a *accountTracker) iterateBaseAccount(acctAddr string, f func(*order.LimitOrder)) {
if a.base == nil {
return
}
for _, lo := range a.base[acctAddr] {
f(lo)
}
}

// iterateQuoteAccount calls the provided function for every tracked order with
// a quote asset corresponding to the specified account address.
func (a *accountTracker) iterateQuoteAccount(acctAddr string, f func(*order.LimitOrder)) {
if a.quote == nil {
return
}
for _, lo := range a.quote[acctAddr] {
f(lo)
}
}

0 comments on commit eae5215

Please sign in to comment.