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

Rm some offers where the quality is reduced #3827

Closed
wants to merge 5 commits into from

Conversation

seelabs
Copy link
Collaborator

@seelabs seelabs commented Apr 20, 2021

High Level Overview of Change

Substantial reductions in an offer's effective quality from its initial quality may clog offer books. This patch removes such offers, much like unfunded offers are removed.

Type of Change

  • [ x] Bug fix (non-breaking change which fixes an issue)

return offer_.amount();
}();

XRPAmount const thresholdToRm{2};
Copy link
Contributor

Choose a reason for hiding this comment

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

A comment to explain why this is 2 rather than 1 would be helpful in understanding the issue.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@donovanhide I changed it back to 1. Anything with values larger than 1 should give a quality that's reasonably in line with the initial quality. The reason I made it larger is I was thinking "removing some more tiny offers is a good thing", but they should be removed with regular operation. There's no need for the larger threshold.

Fixed in the patch 7474063b72 [fold] Change the threshold and minor refactor

@seelabs
Copy link
Collaborator Author

seelabs commented Apr 21, 2021

I made some changes to support IOU/IOU offers and squashed (squashed because the implementation was different enough that it wasn't useful to look at the older patch).

Substantial reductions in an offer's effective quality from its
initial quality may clog offer books.
@donovanhide
Copy link
Contributor

I've just encountered this blocking offer, as seen through

rippled book_offers "XRP" "CNY/rKiCet8SdvWxPXnAgYarFUXMh1zCPz432Y"

   "result" : {
      "ledger_current_index" : 63075722,
      "offers" : [
         {
            "Account" : "rBf5SF3p3UNssTwVQKn7egKREG1xRB7XRP",
            "BookDirectory" : "49789A0B460DC77A2CED9349C432AEA97352345BA3C7313A5A03E53E344E70C2",
            "BookNode" : "0",
            "Flags" : 0,
            "LedgerEntryType" : "Offer",
            "OwnerNode" : "0",
            "PreviousTxnID" : "5BC8338002DC3F89873529470FE85C283D39091013E0EE65AF4A861343E4278E",
            "PreviousTxnLgrSeq" : 63075059,
            "Sequence" : 1247166,
            "TakerGets" : {
               "currency" : "CNY",
               "issuer" : "rKiCet8SdvWxPXnAgYarFUXMh1zCPz432Y",
               "value" : "0.001632496332"
            },
            "TakerPays" : "179",
            "index" : "58887D81BD62D50C1B57B06F32ADFCAAC514BC159F7CDE7F984731D98E533B8F",
            "owner_funds" : "0.000003594192",
            "quality" : "109648.025842093",
            "taker_gets_funded" : {
               "currency" : "CNY",
               "issuer" : "rKiCet8SdvWxPXnAgYarFUXMh1zCPz432Y",
               "value" : "0.000003594192"
            },
            "taker_pays_funded" : "0"
         },

Is the definite proof that this offer is a blocker the taker_pays_funded value being 0 or the tiny taker_gets_funded amount?

@scottschurr
Copy link
Collaborator

I haven't finished my review yet, but the results are very promising. I just finished replaying the transaction that started this hunt. With the new code in place both Taker and FlowCross have the same results for that transaction. I think that's great news!

@seelabs
Copy link
Collaborator Author

seelabs commented Apr 23, 2021

@donovanhide I don't think we can look at the offer alone and find definite proof that it's a blocker. We need to look at the original offer quality and see how owner funds effect it. So I could concoct an offer with tiny owner funds and a reported taker_pays_funded of zero that may still not change the quality to block in practice.

Having said that, a reported XRP amount of taker_pays_funded of zero is a very strong indication that the offer would block. It means the taker_pays can't get lower and the quality is going to become lower than the initial quality.

As an aside, that taker_pays_funded looks like a bug to me. It should actually be 1, not 0. It looks like an edge case that isn't handed correctly in the reporting code.

@donovanhide
Copy link
Contributor

donovanhide commented Apr 23, 2021

@seelabs So the correct way to calculate the "blockerness" is to do TakerPays/TakerGets compared with taker_pays_funded/taker_gets_funded. If different, compare to the next offer's quality in the directory of offers for that orderbook and side combination? Obviously this doesn't work if taker_pays_funded gives the wrong value, but I can calculate XRP balances and reserve requirements using my own code.

@seelabs
Copy link
Collaborator Author

seelabs commented Apr 23, 2021

@donovanhide A decent check would be: 1) only look at offers with Taker Pays of XRP. 2) Calculate taker_pays_funded. If this is over a drop, it's not blocking. 3) If this is under a drop, round up to a drop (you could just look at the reported taker_pays_funded and check if it's zero. If so, assume it should have been one). 4) Calculate the "effective quality" of taker_gets_funded/taker_pays_funded. If the "effective quality" is smaller than the quality it was initially placed at, it may be blocking. The larger the quality difference, the more blocking it is.

Having said all that, a good "quick and dirty check" is to look for funded offers with a reported taker_pays_funded of zero drops. It's not foolproof, but it should work well in practice.

One more quick note: A small payment that sends XRP and delivers CNY should clear that blocking offer. I'll also be AFK most of the day today, but will respond to further messages later tonight to tomorrow.

@donovanhide
Copy link
Contributor

@seelabs That's great, thanks for the details!

@yxxyun
Copy link

yxxyun commented Apr 24, 2021

Screenshot_20210424-100805_Chrome
XRP/XLM.Ripplefox is blocked by dust offer, path payment seems can't remove it?

@scottschurr
Copy link
Collaborator

I'm still working on the code review, but I'm almost done. Nice work!

I noted the concern that we're not removing underfunded offers when the payment/offer crossing fails. So I decided to see how important that concern might be. I kept a record of Offer Crossing and Payments for an hour to see the ratio of successes to tec failures. A failure with something other than tesSUCCESS or tec* won't have any effect on the ledger.

               Count  Percent
Offer Create:  44541   100.0%
  Success:     44272    99.4%
  Claim:         269     0.6%

Payment:        6208   100.0%
  Success:      5563    89.6%
  Claim:         645    10.4%

Total:         50749   100.0%
  Success:     49835    98.2%
  Claim:         914     1.8%

I was surprised that the percentage of failing payments was that high. But even at that, if we look a the sum of offer crossing and payments success vs tec the failure rate is under 2%, at least for that one hour sampling window. I take that as a suggestion that we don't need to worry too much about removing very small offers on transactions that fail with a tec.

Copy link
Collaborator

@scottschurr scottschurr left a comment

Choose a reason for hiding this comment

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

Nice changes and great unit tests. I left a few thoughts for you to consider, but none of them are show stoppers for me.

env.close();
env.trust(USD(1000), alice, bob, carol);
// underfund carol's offer
auto initialCarolUSD = USD(0.1);
Copy link
Collaborator

Choose a reason for hiding this comment

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

FWIW, this value can be raised to USD(0.499) and still have the test behave as expected. At USD(0.5) the case that enables all amendments other than rmSmallIncreasedQOffers fails (which is what we'd hope for).

My inclination would be to use USD(0.499) for the test. It's nice to set the test conditions near an inflection point so we are more sensitive to conditions changing in the future. But that's just a suggestion.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

fixed in d77ad06020 [fold] Change test params so they're closer to behavior change thresholds

env.close();
env.trust(USD(1000), alice, bob, carol);
env.close();
auto const initialCarolUSD = USD(0.1);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Oh! Interesting! Here we can run this value all the way up to USD(0.999) and have the test pass as expected under the various enabled amendments. If I take the value to USD(1.0) then the test fails the case that enables all amendments other than rmSmallIncreasedQOffers.

So my inclination would be to use USD(0.999) here, since it's close to the cusp where the test would start to fail. That way if conditions change in the future we're more likely to see it. That's just a suggestion, however.

It's interesting that the critical point is different between offer crossing and payments. I suspect it's a difference in rounding behaviors, but I didn't chase it down. I think the difference is interesting, but not critical.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

fixed in d77ad06020 [fold] Change test params so they're closer to behavior change thresholds

static_assert(
std::is_same_v<TTakerPays, IOUAmount> ||
std::is_same_v<TTakerPays, XRPAmount>,
"");
Copy link
Collaborator

Choose a reason for hiding this comment

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

Rather than leave the quotes empty, I might be inclined to say "STAmount is not supported", since that's the most likely alternative. But your call.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

fixed in 9b8c050cb3 [fold] Minor cleanups

static_assert(
std::is_same_v<TTakerGets, IOUAmount> ||
std::is_same_v<TTakerGets, XRPAmount>,
"");
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same comment as above.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

fixed in 9b8c050cb3 [fold] Minor cleanups

return false;

// Consider removing the offer if `TakerPays` is XRP or `TakerPays` and
// `TakerGets` are both IOU and `TakerPays` < `TakerGets`
Copy link
Collaborator

Choose a reason for hiding this comment

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

It took quite a while for me to make sense out the comment regarding IOUs. Everything here is correct, but the consequences did not jump out at me. Consider rewriting the comment something like this:

    // Consider removing the offer...
    //  o If `TakerPays` is XRP (because of XRP drops granularity) or
    //  o If...
    //     - `TakerPays` and `TakerGets` are both IOU and
    //     - `TakerPays` < `TakerGets` (numerator < denominator)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

fixed in 9b8c050cb3 [fold] Minor cleanups

@@ -129,6 +129,9 @@ class IOUAmount : private boost::totally_ordered<IOUAmount>,
{
return mantissa_;
}

static IOUAmount
minPositiveAmount();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Long term, this could be made constexpr. But that will require making IOUAmount::normalize() constexpr, which is too big a change to make right now.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

If it were easy I'd do it, but this is going to be a rarely called function; making this constexpr isn't going to to make much difference one way or the other.

}
}
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'll mention that there's a test case in this file (around line 5700) that displays "false assert". Taken out of context that name is a little scary. I chased it briefly until I figured out where it was coming from. As a drive-by change consider changing

testcase("false assert");

to

testcase("incorrect assert fixed");

Just a thought.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

fixed in 9b8c050cb3 [fold] Minor cleanups

@@ -222,6 +291,66 @@ TOfferStreamBase<TIn, TOut>::step()
<< "Removing became unfunded offer " << entry->key();
}
offer_ = TOffer<TIn, TOut>{};
// See comment at top of loop for why the offer is removed
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think the comments in the block describe why we're removing the offer, what's not quite clear at this point is how. The comment at the top of the loop makes that clear. Consider changing this comment to "... for how the ...".

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

fixed in 9b8c050cb3 [fold] Minor cleanups

// Without the `if constexpr`, the
// `shouldRmSmallIncreasedQOffer` template will be instantiated
// even if it is never used. This can cause compiler errors in
// some cases, hense the `if constexpr` guard.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Great comment! Thanks! BTW, perhaps change "hense" to "hence'?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

fixed in 9b8c050cb3 [fold] Minor cleanups

<< entry->key();
}
offer_ = TOffer<TIn, TOut>{};
// See comment at top of loop for why the offer is removed
Copy link
Collaborator

Choose a reason for hiding this comment

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

Similar to earlier comment. Consider "... for how the ...".

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

fixed in 9b8c050cb3 [fold] Minor cleanups

@seelabs
Copy link
Collaborator Author

seelabs commented Apr 27, 2021

Thanks for the great view @scottschurr! I think I addressed all the comments.

Copy link
Collaborator

@scottschurr scottschurr left a comment

Choose a reason for hiding this comment

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

👍

Copy link
Contributor

@nbougalis nbougalis left a comment

Choose a reason for hiding this comment

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

Looks reasonable. Left some comments that I'd like to see answered/addressed before signing off.

Comment on lines 105 to 111
template <class T>
T
toAmount(IOUAmount const& amt)
{
static_assert(sizeof(T) == -1, "Must use specialized function");
return T(0);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not just = delete this instead of using the curious sizeof(T) == -1 construct?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I never think of using delete for anything but constructors and assignment operators. But you're right, this is a perfect use for them. Thanks!

Fixed in a06ca4c951 Misc cleanups (Nik's review)


// Carol doesn't quite have enough funds for this offer
// The amount left after this offer is taken will cause
// STAmount to incorrectly round to zero when the next offer
// (at a good quality) is considered. (when the
// stAmountCalcSwitchover2 patch is inactive)
env(offer(carol, drops(1), USD(1)));
env(offer(carol, drops(1), USD(0.99999)));
Copy link
Contributor

Choose a reason for hiding this comment

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

I think stAmountCalcSwitchover2 was removed, so while here we should update the comment.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I didn't want to lose the reference to stAmountCalcSwitchover2. I modified the comment to say that is "now removed". Keeping that reference is a reminder of the motivation for this test case.

Fixed in a06ca4c951 Misc cleanups (Nik's review)


TTakerPays const thresh = TTakerPays::minPositiveAmount();

if (effectiveAmounts.in > thresh)
Copy link
Contributor

Choose a reason for hiding this comment

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

At your option, micronit: thresh is only ever used here and could just be inlined: if (effectiveAmounts.in > TTakerPays::minPositiveAmount())

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in a06ca4c951 Misc cleanups (Nik's review)

Comment on lines +308 to +310
if constexpr (!(std::is_same_v<TIn, IOUAmount> ||
std::is_same_v<TOut, XRPAmount>))
return shouldRmSmallIncreasedQOffer<XRPAmount, IOUAmount>();
Copy link
Contributor

Choose a reason for hiding this comment

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

The logic here just confuses me. First, is there a reason we don't use (!std::is_same_v<TIn, IOUAmount> && !std::is_same_v<TOut, XRPAmount>)? For that matter, why not just say what we, apparently, mean, which I think is: (std::is_same_v<TIn, XRPAmount> && std::is_same_v<TOut, IOUAmount>)?

Second, and maybe it's the fact that it's almost midnight, but I'm confused why the conditional is even necessary? Under what circumstances do we enter this codepath where inIsXRP == true but std::is_same_v<TIn, XRPAmount> == false?

Copy link
Collaborator Author

@seelabs seelabs May 6, 2021

Choose a reason for hiding this comment

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

There are three questions:

  1. Why not (!std::is_same_v<TIn, IOUAmount> && !std::is_same_v<TOut, XRPAmount>)?
    Sometimes I write !(error_condition1 || error_condition2) instead of (!error_condition1 && !error_conditions2). Think of the huge savings by saving one fewer !. More seriously, I don't have a strong preference; I find both about equally readable.

  2. Why not (std::is_same_v<TIn, XRPAmount> && std::is_same_v<TOut, IOUAmount>)?
    Because TIn can be XRPAmount or STAmount, and TOut can be IOUAmount or STAmount.

  3. Why is the conditional even needed? Under what circumstances do we enter this codepath...?
    While the codepath would never be executed, the template would still be instantiated, and we'd get compiler errors. In particular, we'd end up calling things like: toAmount<IOUAmount>(xrpAmount); Even though we know this will never be executed, the compiler will still try to compile that and we'd get a compiler error.The if constexpr prevents the template from being instantiated at all.

I added some minor clarifying comments, but kept the code essentially the same. I can revisit if you still think this is confusing.

}
if (!inIsXRP && outIsXRP)
{
// See comment above for `if constexpr` rational
Copy link
Contributor

Choose a reason for hiding this comment

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

Typo: rational should be rationale.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in a06ca4c951 Misc cleanups (Nik's review)

}
else
{
JLOG(j_.trace()) << "Removing became tiny offer due to "
Copy link
Contributor

Choose a reason for hiding this comment

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

"Removing became" is gramatically incorrect.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It mirrors the "became unfunded" terminology we use for offers. Having said that, I don't mind rewording it. Changed to "removing tiny offer that became tiny due to..."

Fixed in a06ca4c951 Misc cleanups (Nik's review)

* Inline minPositiveAmount
* Add some clarifying comments
* Fix mispelling
* Use deleted function instead of static_assert
Copy link
Collaborator

@scottschurr scottschurr 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 to me!

@manojsdoshi manojsdoshi mentioned this pull request May 7, 2021
This was referenced Jun 2, 2021
@donovanhide
Copy link
Contributor

donovanhide commented Dec 7, 2021

Unfortunately, looks like this is still happening:

rippled book_offers XRP CNY/rKiCet8SdvWxPXnAgYarFUXMh1zCPz432Y | more
Loading: "/etc/opt/ripple/rippled.cfg"
2021-Dec-07 12:28:03.605525281 UTC HTTPClient:NFO Connecting to 127.0.0.1:5005

{
   "result" : {
      "ledger_current_index" : 68183403,
      "offers" : [
         {
            "Account" : "rKcLoZAqpYTgiu47kWnV1XxzRNeXKkTcau",
            "BookDirectory" : "49789A0B460DC77A2CED9349C432AEA97352345BA3C7313A5A06C7AD4A172715",
            "BookNode" : "0",
            "Flags" : 0,
            "LedgerEntryType" : "Offer",
            "OwnerNode" : "0",
            "PreviousTxnID" : "A6CFD591B2FBD41CC19E8F693F4C45212DFBB18AB5FF3E9CFD0279F617826527",
            "PreviousTxnLgrSeq" : 68183064,
            "Sequence" : 333,
            "TakerGets" : {
               "currency" : "CNY",
               "issuer" : "rKiCet8SdvWxPXnAgYarFUXMh1zCPz432Y",
               "value" : "1.2000061817857"
            },
            "TakerPays" : "229008",
            "index" : "BEC40A8F5FD0A87F86EAA91496613EE41B6C9C8358222E8F2C31703D25FD4AE6",
            "owner_funds" : "0.0000061817857",
            "quality" : "190839.6946564885",
            "taker_gets_funded" : {
               "currency" : "CNY",
               "issuer" : "rKiCet8SdvWxPXnAgYarFUXMh1zCPz432Y",
               "value" : "0.0000061817857"
            },
            "taker_pays_funded" : "1"
         },

@donovanhide
Copy link
Contributor

donovanhide commented Dec 19, 2021

Might not be related, but this offer is uncrossable.

$ rippled book_offers ADA/rnNDjH8YTJLVQMccqLebzC77KLkWxrdEZi XRP
Loading: "/etc/opt/ripple/rippled.cfg"
2021-Dec-19 08:03:07.132081913 UTC HTTPClient:NFO Connecting to 127.0.0.1:5005

{
   "result" : {
      "ledger_current_index" : 68434259,
      "offers" : [
         {
            "Account" : "rLZLMZ922JCRHHMvzAtttw9gCdFfnTyZ34",
            "BookDirectory" : "2818605EC090C5150A69A8B4D3632B4271AC8257A6991685510AA87BEE538000",
            "BookNode" : "0",
            "Flags" : 131072,
            "LedgerEntryType" : "Offer",
            "OwnerNode" : "1",
            "PreviousTxnID" : "BCF09BA40BE31A8BDC2AC6C7651A96140E864A3CF83FDEB49170BD0149535E80",
            "PreviousTxnLgrSeq" : 68375269,
            "Sequence" : 66481802,
            "TakerGets" : "10000",
            "TakerPays" : {
               "currency" : "ADA",
               "issuer" : "rnNDjH8YTJLVQMccqLebzC77KLkWxrdEZi",
               "value" : "3"
            },
            "index" : "7CF342B72DC283536B4242BEB76C63C44574C552DED857C439881B792FE2C4FC",
            "owner_funds" : "17688155210",
            "quality" : "0.0003"
         }
      ],
      "status" : "success",
      "validated" : false
   }
}

@intelliot
Copy link
Collaborator

If relevant, please feel free to copy the above examples to #4081 (new issue where this problem is being worked on)

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.

6 participants