Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

multi: fee rate estimate feed #1000

Merged
merged 1 commit into from
Mar 31, 2021
Merged

multi: fee rate estimate feed #1000

merged 1 commit into from
Mar 31, 2021

Conversation

buck54321
Copy link
Member

@buck54321 buck54321 commented Mar 4, 2021

Add the most recent fee rate estimate to 1) The orderbook response, 2) the epoch_report notification, and 3) a custom fee_rate endpoint. There are two major reason for this.

  1. SPV clients will have no way to estimate fees. They can use the suggested fee rate with some basic sanity checks.
  2. This finally closes a small vulnerability regarding fee rates on zero-conf funding coins. Previously, we handled this only client side by ensuring a reasonably high fee rate. But since the server's latest fee rate is available at the client now, we can more justifiably reject low-fee zero-conf coin-funded orders. Right now, the filter is set to >= 90% of the most recently procured rate estimate.

Order estimates should also be more accurate now.

server/market/market.go Outdated Show resolved Hide resolved
@chappjc chappjc added this to the 0.2 milestone Mar 6, 2021
@buck54321 buck54321 force-pushed the rate-feed branch 2 times, most recently from 68073fa to dcd8f50 Compare March 10, 2021 22:00
@@ -1599,7 +1599,8 @@ func (c *Core) redeemMatchGroup(t *trackedTrade, matches []*matchTracker, errs *
// Send the transaction.
redeemWallet, redeemAsset := t.wallets.toWallet, t.wallets.toAsset // this is our redeem
coinIDs, outCoin, fees, err := redeemWallet.Redeem(&asset.RedeemForm{
Redemptions: redemptions,
Redemptions: redemptions,
FeeSuggestion: c.feeSuggestion(t.dc, t.dc.bookie(t.mktID), redeemWallet.AssetID, !t.Trade().Sell),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The feeSuggestion call can make a request to the server. That's a problem in redeemMatchGroup. We split out the actual init and redeem requests to make them asynchronous (sendInitAsync and sendRedeemAsync) because of the trouble this can cause, but ideally we'd have no requests at all for this. All of the automated (tick or block based) actions need to be snappy and not have the potential to block. Is there any way to avoid this here, either changing feeSuggestion or ensuring there is already a value ready somewhere for this?

return nil
}
lastKnownFeeRate := r.feeSource.LastFeeRate(fundingAsset.ID)
feeMinimum := uint64(math.Round(float64(lastKnownFeeRate) * 0.9))
Copy link
Member

@chappjc chappjc Mar 15, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0.9 seems pretty reasonable. Let's make this a named constant and describe.

Also what do you think about a minimum difference allowed? e.g. say the last was 15, then feeMinimum is 14 (round(15*0.9 = 13.5)), and it makes me wonder if that's too tight tolerance toward the smaller end.
Obviously we don't want things slowed down, but blocking incoming orders would suck.

Or if all client eventually update would there theoretically be no difference if they all use the fee suggestion from one of the various sources?

client/asset/btc/btc.go Outdated Show resolved Hide resolved
client/asset/dcr/dcr.go Outdated Show resolved Hide resolved
client/orderbook/orderbook.go Outdated Show resolved Hide resolved
server/market/bookrouter.go Outdated Show resolved Hide resolved
client/core/trade.go Outdated Show resolved Hide resolved
@chappjc
Copy link
Member

chappjc commented Mar 30, 2021

When you rebase, you are welcome to squash too. A couple conflict notes: context was removed from data api constructor (was unused), and handleFeeRate can switch to using msg.Unmarshal as per the changes in #1014. The one conflict with the stopBook closure can go back to one line too if you like.

client/asset/interface.go Show resolved Hide resolved
Comment on lines +28 to +30
// copy creates a copy. Note that the OrderID is not a deep copy.
func (o *Order) copy() *Order {
return &(*o)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since order.OrderID is an array, it would be a deep copy too.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, yeah. Nice.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment still says not a deep copy

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

crap

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's alright. I realized this wasn't making a copy of anything actually. Go was simplifying it to just returning the same pointer: a71b9d6
Working on some tweaks for this.


// Clear all orders, if any.
ob.orders = make(map[order.OrderID]*Order)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Funny, we must have been (unintentionally?) avoiding a race with a higher level synchronization mechanism in the caller.

Comment on lines 367 to 375
ob.ordersMtx.Lock()
ord, found := ob.orders[oid]
if found {
newOrder := ord.copy()
newOrder.Quantity = note.Remaining
ob.orders[oid] = newOrder
}

ord = ob.buys.UpdateRemaining(oid, note.Remaining)
if ord != nil {
return nil
ob.ordersMtx.Unlock()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a little unsure about the need to replace the order in the map with a fresh Order instance, but also the need to do anything with the orders map at all since the two booksides refer to the map entries (pointers).
Did you observe a bug or did this just seem like a needed change?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was an attempt at fixing a race condition. We can't update the field, since a caller might be reading, e.g.

Qty: float64(o.Quantity) / conversionFactor,

But I do think I messed something up here. Lemme get into it.

Copy link
Member

@chappjc chappjc Mar 30, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious that the race detector hasn't hit it before. Maybe because when that code is called in bookie.go we've always been locking booky.mtx like in (*dexConnection).syncBook?

Comment on lines +424 to +427
ob.ordersMtx.Lock()
order, ok := ob.orders[oid]
ob.ordersMtx.Unlock()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, another surprising one. OrderBook use patterns must have been preventing concurrent access to the map.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do believe that's the case, but not behavior I wanted to rely upon.

Comment on lines 317 to 319
func (r *BookRouter) LastFeeRate(assetID uint32) uint64 {
return atomic.LoadUint64(r.feeRateCache[assetID])
}
Copy link
Member

@chappjc chappjc Mar 30, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had some thoughts about the roles of BookRouter, Market, and OrderRouter with respect to the fee rate fetching, caching, and retrieval that I shared in matrix. The following is my high level understanding of how the pieces currently fit and what is responsible for what:

The assets implement market.FeeFetcher, which is a FeeRate() (uint64, error) method. These are provided to Markets, who use the fetchers at match time to get fee rates per asset for all the match sets.
These base/quote fee rates make their way to the outgoing BookRouter via an epoch_report signal. The BookRouter stores these rates when it gets them.
BookRouter also implements LastFeeRate(assetID uint32) uint64 (this commented line), and OrderRouter uses BookRouter as a FeeSource interface for this method. The OrderRouter uses this interface to ensure zero conf funded orders have high enough fee rate on funding txn.
So BookRouter with it's fee rate cache is the fee source for both itself and OrderRouter, while Market uses the assets directly for fee rates.

One question I had was what fee rates are known/cached before the first epoch_report? Also, how can we populate the book router's fee rate cache earlier, ideally construction time?

A general suggestion I had was to reduce and clarify the roles of these three server/market subsystems with respect to fee rate fetching/caching. Presently the flow of fee rates is from RPC fetch in Market on epoch close -> fee rate cache in BookRouter via epoch_report -> retrieve from cache in OrderRouter via BookRouter's LastFeeRate method. This design makes BookRouter implement the cache, while Market actually does retrieval. So I'm contemplating if a FeeSource can be some other type implemented outside of the market package, like something in server/dex that can do the RPC fetch and cache management without troubling the server/market types. Also, server/dex.DEX has the backends so it can hit FeeRate() to prepopulate the fee rate caches before even firing up the market subsystems.

Part of the appeal of a type that handles fee fetching and caching is consolidating these responsibilities that are now shared between the three market subsystems, and a fetch can automatically update the cache directly. Both OrderRouter and BookRouter can get the same FeeSource, and calls from Market to this type's FetchFeeRate(assetID uint32) method (indirectly via wrapper for the relevant assetID) could update the cache.

Comment on lines 2027 to 2028
match.FeeRateBase = matchSet.FeeRateBase
match.FeeRateQuote = matchSet.FeeRateQuote
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think matchSet.Matches() -> dex/order.newMatch has this job now, so redundant I believe.

func (f *feeFetcher) FeeRate() uint64 {
r, err := f.Backend.FeeRate()
if err != nil {
log.Errorf("Error retrieving fee rate for %s: %v", f.Symbol, err)
Copy link
Member

@chappjc chappjc Mar 31, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this return 0 instead of going on to atomic.StoreUint64?

Copy link
Member Author

@buck54321 buck54321 Mar 31, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was a little chat in matrix about this. In short, no. If the backend returns an error, we would want to pass that on to the client as a zero, indicating no estimate is available. Returning any other value would require the server to reason about either the validity of outdated estimates or the meaning of particular errors, which is undesireable, imo.

client/core/trade.go Outdated Show resolved Hide resolved
Copy link
Member

@chappjc chappjc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good and tests well. I had a handful of other questions and suggestions, but I'm happy to merge this regardless.

Getting client to hit the fee_rate route was a bit tricky too, needing to use dexcctl trade with the browser closed and unsubscribed from the book. Does that seem right?

I'm also still a bit confused by the OrderBook stuff and I'm wondering if this is gonna resolve #835

Add the most recent fee rate estimate to 1) The orderbook response,
2) the epoch_report notification, and 3) a custom fee_rate endpoint.
There are two major reason for this.

1. SPV clients will have no way to estimate fees. They can use the
   suggested fee rate with some basic sanity checks.
2. This finally closes a small vulnerability regarding fee rates on
   zero-conf funding coins. Previously, we handled this only client
   side by ensuring a reasonably high fee rate. But since the server's
   latest fee rate is available at the client now, we can more justifiably
   reject low-fee zero-conf coin-funded orders. Right now, the filter
   is set to >= 90% of the most recently procured rate estimate.

Order estimates should also be more accurate now.
@chappjc chappjc merged commit 79a1cb0 into decred:master Mar 31, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants