-
Notifications
You must be signed in to change notification settings - Fork 91
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
Conversation
68073fa
to
dcd8f50
Compare
client/core/trade.go
Outdated
@@ -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), |
There was a problem hiding this comment.
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?
server/market/orderrouter.go
Outdated
return nil | ||
} | ||
lastKnownFeeRate := r.feeSource.LastFeeRate(fundingAsset.ID) | ||
feeMinimum := uint64(math.Round(float64(lastKnownFeeRate) * 0.9)) |
There was a problem hiding this comment.
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?
When you rebase, you are welcome to squash too. A couple conflict notes: context was removed from data api constructor (was unused), and |
// copy creates a copy. Note that the OrderID is not a deep copy. | ||
func (o *Order) copy() *Order { | ||
return &(*o) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, yeah. Nice.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
crap
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
client/orderbook/orderbook.go
Outdated
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() |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
Line 373 in 9bf1a3e
Qty: float64(o.Quantity) / conversionFactor, |
But I do think I messed something up here. Lemme get into it.
There was a problem hiding this comment.
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
?
ob.ordersMtx.Lock() | ||
order, ok := ob.orders[oid] | ||
ob.ordersMtx.Unlock() |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
server/market/bookrouter.go
Outdated
func (r *BookRouter) LastFeeRate(assetID uint32) uint64 { | ||
return atomic.LoadUint64(r.feeRateCache[assetID]) | ||
} |
There was a problem hiding this comment.
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 Market
s, 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.
567a16b
to
b01f2ca
Compare
server/swap/swap.go
Outdated
match.FeeRateBase = matchSet.FeeRateBase | ||
match.FeeRateQuote = matchSet.FeeRateQuote |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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.
There was a problem hiding this 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.
Add the most recent fee rate estimate to 1) The
orderbook
response, 2) theepoch_report
notification, and 3) a customfee_rate
endpoint. There are two major reason for this.Order estimates should also be more accurate now.