Use payment flow code for offer crossing (RIPD-1094):

Replace Taker.cpp with calls to the payment flow() code.

This change required a number of tweaks in the payment flow code.
These tweaks are conditionalized on whether or not offer crossing
is taking place.  The flag is explicitly passed as a parameter to
the flow code.

For testing, a class was added that identifies differences in the
contents of two PaymentSandboxes.  That code may be reusable in
the future.

None of the Taker offer crossing code is removed.  Both versions
of the code are co-resident to support an amendment cut-over.

The code that identifies differences between Taker and Flow offer
crossing is enabled by a feature.  That makes it easy to enable
or disable difference logging by changing the config file.  This
approach models what was done with the payment flow code.  The
differencing code should never be enabled on a production server.

Extensive offer crossing unit tests are added to examine and
verify the behavior of corner cases.  The tests are currently
configured to run against both Taker and Flow offer crossing.
This gives us confidence that most cases run identically and
some of the (few) differences in behavior are documented.
This commit is contained in:
Scott Schurr
2016-03-15 17:17:09 -07:00
parent 2cd55ebf98
commit 369909df84
50 changed files with 5367 additions and 711 deletions

View File

@@ -2067,6 +2067,8 @@
</ClInclude>
<ClInclude Include="..\..\src\ripple\ledger\CachedView.h">
</ClInclude>
<ClInclude Include="..\..\src\ripple\ledger\CashDiff.h">
</ClInclude>
<ClInclude Include="..\..\src\ripple\ledger\detail\ApplyStateTable.h">
</ClInclude>
<ClInclude Include="..\..\src\ripple\ledger\detail\ApplyViewBase.h">
@@ -2103,6 +2105,10 @@
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="..\..\src\ripple\ledger\impl\CashDiff.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="..\..\src\ripple\ledger\impl\Directory.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
@@ -4701,6 +4707,10 @@
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="..\..\src\test\ledger\CashDiff_test.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="..\..\src\test\ledger\Directory_test.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>

View File

@@ -2682,6 +2682,9 @@
<ClInclude Include="..\..\src\ripple\ledger\CachedView.h">
<Filter>ripple\ledger</Filter>
</ClInclude>
<ClInclude Include="..\..\src\ripple\ledger\CashDiff.h">
<Filter>ripple\ledger</Filter>
</ClInclude>
<ClInclude Include="..\..\src\ripple\ledger\detail\ApplyStateTable.h">
<Filter>ripple\ledger\detail</Filter>
</ClInclude>
@@ -2718,6 +2721,9 @@
<ClCompile Include="..\..\src\ripple\ledger\impl\CachedView.cpp">
<Filter>ripple\ledger\impl</Filter>
</ClCompile>
<ClCompile Include="..\..\src\ripple\ledger\impl\CashDiff.cpp">
<Filter>ripple\ledger\impl</Filter>
</ClCompile>
<ClCompile Include="..\..\src\ripple\ledger\impl\Directory.cpp">
<Filter>ripple\ledger\impl</Filter>
</ClCompile>
@@ -5451,6 +5457,9 @@
<ClCompile Include="..\..\src\test\ledger\BookDirs_test.cpp">
<Filter>test\ledger</Filter>
</ClCompile>
<ClCompile Include="..\..\src\test\ledger\CashDiff_test.cpp">
<Filter>test\ledger</Filter>
</ClCompile>
<ClCompile Include="..\..\src\test\ledger\Directory_test.cpp">
<Filter>test\ledger</Filter>
</ClCompile>

View File

@@ -53,7 +53,8 @@ supportedAmendments ()
{ "07D43DCE529B15A10827E5E04943B496762F9A88E3268269D69C44BE49E21104 Escrow" },
{ "86E83A7D2ECE3AD5FA87AB2195AE015C950469ABF0B72EAACED318F74886AE90 CryptoConditionsSuite" },
{ "42EEA5E28A97824821D4EF97081FE36A54E9593C6E4F20CBAE098C69D2E072DC fix1373" },
{ "DC9CA96AEA1DCF83E527D1AFC916EFAF5D27388ECA4060A88817C1238CAEE0BF EnforceInvariants" }
{ "DC9CA96AEA1DCF83E527D1AFC916EFAF5D27388ECA4060A88817C1238CAEE0BF EnforceInvariants" },
{ "3012E8230864E95A58C60FD61430D7E1B4D3353195F2981DC12B0C7C0950FFAC FlowCross" }
};
}

View File

@@ -63,6 +63,7 @@ flow (
bool defaultPaths,
bool partialPayment,
bool ownerPaysTransferFee,
bool offerCrossing,
boost::optional<Quality> const& limitQuality,
boost::optional<STAmount> const& sendMax,
beast::Journal j,
@@ -84,8 +85,8 @@ flow (
// convert the paths to a collection of strands. Each strand is the collection
// of account->account steps and book steps that may be used in this payment.
auto sr = toStrands (sb, src, dst, dstIssue, sendMaxIssue, paths,
defaultPaths, ownerPaysTransferFee, j);
auto sr = toStrands (sb, src, dst, dstIssue, limitQuality, sendMaxIssue,
paths, defaultPaths, ownerPaysTransferFee, offerCrossing, j);
if (sr.first != tesSUCCESS)
{
@@ -123,7 +124,7 @@ flow (
{
return finishFlow (sb, srcIssue, dstIssue,
flow<XRPAmount, XRPAmount> (
sb, strands, asDeliver.xrp, defaultPaths, partialPayment,
sb, strands, asDeliver.xrp, partialPayment, offerCrossing,
limitQuality, sendMax, j, flowDebugInfo));
}
@@ -131,7 +132,7 @@ flow (
{
return finishFlow (sb, srcIssue, dstIssue,
flow<XRPAmount, IOUAmount> (
sb, strands, asDeliver.iou, defaultPaths, partialPayment,
sb, strands, asDeliver.iou, partialPayment, offerCrossing,
limitQuality, sendMax, j, flowDebugInfo));
}
@@ -139,14 +140,14 @@ flow (
{
return finishFlow (sb, srcIssue, dstIssue,
flow<IOUAmount, XRPAmount> (
sb, strands, asDeliver.xrp, defaultPaths, partialPayment,
sb, strands, asDeliver.xrp, partialPayment, offerCrossing,
limitQuality, sendMax, j, flowDebugInfo));
}
assert (!srcIsXRP && !dstIsXRP);
return finishFlow (sb, srcIssue, dstIssue,
flow<IOUAmount, IOUAmount> (
sb, strands, asDeliver.iou, defaultPaths, partialPayment,
sb, strands, asDeliver.iou, partialPayment, offerCrossing,
limitQuality, sendMax, j, flowDebugInfo));
}

View File

@@ -44,9 +44,12 @@ struct FlowDebugInfo;
@param defaultPaths Include defaultPaths in the path set
@param partialPayment If the payment cannot deliver the entire
requested amount, deliver as much as possible, given the constraints
@param ownerPaysTransferFee If true then owner, not sender, pays fee
@param offerCrossing If true then flow is executing offer crossing, not payments
@param limitQuality Do not use liquidity below this quality threshold
@param sendMax Do not spend more than this amount
@param logs Logs to write journal messages to
@param j Journal to write journal messages to
@param flowDebugInfo If non-null a pointer to FlowDebugInfo for debugging
@return Actual amount in and out, and the result code
*/
path::RippleCalc::Output
@@ -58,6 +61,7 @@ flow (PaymentSandbox& view,
bool defaultPaths,
bool partialPayment,
bool ownerPaysTransferFee,
bool offerCrossing,
boost::optional<Quality> const& limitQuality,
boost::optional<STAmount> const& sendMax,
beast::Journal j,

View File

@@ -142,7 +142,7 @@ RippleCalc::Output RippleCalc::rippleCalculate (
auto const timeIt = flowV2FlowDebugInfo.timeBlock ("main");
flowV2Out = flow (flowV2SB, saDstAmountReq, uSrcAccountID,
uDstAccountID, spsPaths, defaultPaths, partialPayment,
ownerPaysTransferFee, limitQuality, sendMax, j,
ownerPaysTransferFee, /* offerCrossing */ false, limitQuality, sendMax, j,
compareFlowV1V2 ? &flowV2FlowDebugInfo : nullptr);
}
catch (std::exception& e)

View File

@@ -39,21 +39,15 @@
namespace ripple {
template<class TIn, class TOut>
class TOffer;
template<class TIn, class TOut>
struct TAmounts;
template<class TIn, class TOut>
class BookStep : public StepImp<TIn, TOut, BookStep<TIn, TOut>>
template<class TIn, class TOut, class TDerived>
class BookStep : public StepImp<TIn, TOut, BookStep<TIn, TOut, TDerived>>
{
private:
protected:
static constexpr uint32_t maxOffersToConsume_ = 2000;
Book book_;
AccountID strandSrc_;
AccountID strandDst_;
// Charge transfer fees whan the prev step redeems
// Charge transfer fees when the prev step redeems
Step const* const prevStep_ = nullptr;
bool const ownerPaysTransferFee_;
beast::Journal j_;
@@ -72,19 +66,15 @@ private:
boost::optional<Cache> cache_;
public:
BookStep (Issue const& in,
Issue const& out,
AccountID const& strandSrc,
AccountID const& strandDst,
Step const* prevStep,
bool ownerPaysTransferFee,
beast::Journal j)
BookStep (StrandContext const& ctx,
Issue const& in,
Issue const& out)
: book_ (in, out)
, strandSrc_ (strandSrc)
, strandDst_ (strandDst)
, prevStep_ (prevStep)
, ownerPaysTransferFee_ (ownerPaysTransferFee)
, j_ (j)
, strandSrc_ (ctx.strandSrc)
, strandDst_ (ctx.strandDst)
, prevStep_ (ctx.prevStep)
, ownerPaysTransferFee_ (ctx.ownerPaysTransferFee)
, j_ (ctx.j)
{
}
@@ -121,6 +111,9 @@ public:
return book_;
}
boost::optional<Quality>
qualityUpperBound(ReadView const& v, bool& redeems) const override;
std::pair<TIn, TOut>
revImp (
PaymentSandbox& sb,
@@ -144,6 +137,19 @@ public:
// Check for errors frozen constraints.
TER check(StrandContext const& ctx) const;
protected:
std::string logStringImpl (char const* name) const
{
std::ostringstream ostr;
ostr <<
name << ": " <<
"\ninIss: " << book_.in.account <<
"\noutIss: " << book_.out.account <<
"\ninCur: " << book_.in.currency <<
"\noutCur: " << book_.out.currency;
return ostr.str ();
}
private:
friend bool operator==(BookStep const& lhs, BookStep const& rhs)
{
@@ -157,34 +163,267 @@ private:
bool equal (Step const& rhs) const override;
// Iterate through the offers at the best quality in a book.
// Unfunded offers and bad offers are skipped (and returned).
// callback is called with the offer SLE, taker pays, taker gets.
// If callback returns false, don't process any more offers.
// Return the unfunded and bad offers and the number of offers consumed.
template <class Callback>
std::pair<boost::container::flat_set<uint256>, std::uint32_t>
forEachOffer (
PaymentSandbox& sb,
ApplyView& afView,
bool prevStepRedeems,
Callback& callback) const;
void consumeOffer (PaymentSandbox& sb,
TOffer<TIn, TOut>& offer,
TAmounts<TIn, TOut> const& ofrAmt,
TAmounts<TIn, TOut> const& stepAmt,
TOut const& ownerGives) const;
};
//------------------------------------------------------------------------------
// Flow is used in two different circumstances for transferring funds:
// o Payments, and
// o Offer crossing.
// The rules for handling funds in these two cases are almost, but not
// quite, the same.
// Payment BookStep template class (not offer crossing).
template<class TIn, class TOut>
class BookPaymentStep
: public BookStep<TIn, TOut, BookPaymentStep<TIn, TOut>>
{
public:
using BookStep<TIn, TOut, BookPaymentStep<TIn, TOut>>::BookStep;
using BookStep<TIn, TOut, BookPaymentStep<TIn, TOut>>::qualityUpperBound;
// Never limit self cross quality on a payment.
bool limitSelfCrossQuality (AccountID const&, AccountID const&,
TOffer<TIn, TOut> const& offer, boost::optional<Quality>&,
FlowOfferStream<TIn, TOut>&, bool) const
{
return false;
}
// A payment can look at offers of any quality
bool checkQualityThreshold(TOffer<TIn, TOut> const& offer) const
{
return true;
}
// For a payment ofrInRate is always the same as trIn.
std::uint32_t getOfrInRate (
Step const*, TOffer<TIn, TOut> const&, std::uint32_t trIn) const
{
return trIn;
}
// For a payment ofrOutRate is always the same as trOut.
std::uint32_t getOfrOutRate (Step const*, TOffer<TIn, TOut> const&,
AccountID const&, std::uint32_t trOut) const
{
return trOut;
}
Quality
qualityUpperBound(ReadView const& v,
Quality const& ofrQ,
bool prevStepRedeems) const
{
// Charge the offer owner, not the sender
// Charge a fee even if the owner is the same as the issuer
// (the old code does not charge a fee)
// Calculate amount that goes to the taker and the amount charged the
// offer owner
auto rate = [&](AccountID const& id) {
if (isXRP(id) || id == this->strandDst_)
return parityRate;
return transferRate(v, id);
};
auto const trIn =
prevStepRedeems ? rate(this->book_.in.account) : parityRate;
// Always charge the transfer fee, even if the owner is the issuer
auto const trOut =
this->ownerPaysTransferFee_
? rate(this->book_.out.account)
: parityRate;
Quality const q1{getRate(STAmount(trOut.value), STAmount(trIn.value))};
return composed_quality(q1, ofrQ);
}
std::string logString () const override
{
std::ostringstream ostr;
ostr <<
"BookStep" <<
"\ninIss: " << book_.in.account <<
"\noutIss: " << book_.out.account <<
"\ninCur: " << book_.in.currency <<
"\noutCur: " << book_.out.currency;
return ostr.str ();
return this->logStringImpl ("BookPaymentStep");
}
};
template <class TIn, class TOut>
bool BookStep<TIn, TOut>::equal (Step const& rhs) const
// Offer crossing BookStep template class (not a payment).
template<class TIn, class TOut>
class BookOfferCrossingStep
: public BookStep<TIn, TOut, BookOfferCrossingStep<TIn, TOut>>
{
if (auto bs = dynamic_cast<BookStep<TIn, TOut> const*>(&rhs))
using BookStep<TIn, TOut, BookOfferCrossingStep<TIn, TOut>>::qualityUpperBound;
private:
// Helper function that throws if the optional passed to the constructor
// is none.
static Quality getQuality (boost::optional<Quality> const& limitQuality)
{
// It's really a programming error if the quality is missing.
assert (limitQuality);
if (!limitQuality)
Throw<FlowException> (tefINTERNAL, "Offer requires quality.");
return *limitQuality;
}
public:
BookOfferCrossingStep (
StrandContext const& ctx, Issue const& in, Issue const& out)
: BookStep<TIn, TOut, BookOfferCrossingStep<TIn, TOut>> (ctx, in, out)
, defaultPath_ (ctx.isDefaultPath)
, qualityThreshold_ (getQuality (ctx.limitQuality))
{
}
bool limitSelfCrossQuality (AccountID const& strandSrc,
AccountID const& strandDst, TOffer<TIn, TOut> const& offer,
boost::optional<Quality>& ofrQ, FlowOfferStream<TIn, TOut>& offers,
bool const offerAttempted) const
{
// This method supports some correct but slightly surprising
// behavior in offer crossing. The scenario:
//
// o alice has already created one or more offers.
// o alice creates another offer that can be directly crossed (not
// autobridged) by one or more of her previously created offer(s).
//
// What does the offer crossing do?
//
// o The offer crossing could go ahead and cross the offers leaving
// either one reduced offer (partial crossing) or zero offers
// (exact crossing) in the ledger. We don't do this. And, really,
// the offer creator probably didn't want us to.
//
// o We could skip over the self offer in the book and only cross
// offers that are not our own. This would make a lot of sense,
// but we don't do it. Part of the rationale is that we can only
// operate on the tip of the order book. We can't leave an offer
// behind -- it would sit on the tip and block access to other
// offers.
//
// o We could delete the self-crossable offer(s) off the tip of the
// book and continue with offer crossing. That's what we do.
//
// To support this scenario offer crossing has a special rule. If:
// a. We're offer crossing using default path (no autobridging), and
// b. The offer's quality is at least as good as our quality, and
// c. We're about to cross one of our own offers, then
// d. Delete the old offer from the ledger.
if (defaultPath_ && offer.quality() >= qualityThreshold_ &&
strandSrc == offer.owner() && strandDst == offer.owner())
{
// Remove this offer even if no crossing occurs.
offers.permRmOffer (offer.key());
// If no offers have been attempted yet then it's okay to move to
// a different quality.
if (!offerAttempted)
ofrQ = boost::none;
// Return true so the current offer will be deleted.
return true;
}
return false;
}
// Offer crossing can prune the offers it needs to look at with a
// quality threshold.
bool checkQualityThreshold(TOffer<TIn, TOut> const& offer) const
{
return !defaultPath_ || offer.quality() >= qualityThreshold_;
}
// For offer crossing don't pay the transfer fee if alice is paying alice.
// A regular (non-offer-crossing) payment does not apply this rule.
std::uint32_t getOfrInRate (Step const* prevStep,
TOffer<TIn, TOut> const& offer, std::uint32_t trIn) const
{
auto const srcAcct = prevStep ?
prevStep->directStepSrcAcct() :
boost::none;
return // If offer crossing
srcAcct && // && prevStep is DirectI
offer.owner() == *srcAcct // && src is offer owner
? QUALITY_ONE : trIn; // then rate = QUALITY_ONE
}
// See comment on getOfrInRate().
std::uint32_t getOfrOutRate (
Step const* prevStep, TOffer<TIn, TOut> const& offer,
AccountID const& strandDst, std::uint32_t trOut) const
{
return // If offer crossing
prevStep && prevStep->bookStepBook() && // && prevStep is BookStep
offer.owner() == strandDst // && dest is offer owner
? QUALITY_ONE : trOut; // then rate = QUALITY_ONE
}
Quality
qualityUpperBound(ReadView const& v,
Quality const& ofrQ,
bool prevStepRedeems) const
{
// Offer x-ing does not charge a transfer fee when the offer's owner
// is the same as the strand dst. It is important that `qualityUpperBound`
// is an upper bound on the quality (it is used to ignore strands
// whose quality cannot meet a minimum threshold). When calculating
// quality assume no fee is charged, or the estimate will no longer
// be an upper bound.
return ofrQ;
}
std::string logString () const override
{
return this->logStringImpl ("BookOfferCrossingStep");
}
private:
bool const defaultPath_;
Quality const qualityThreshold_;
};
//------------------------------------------------------------------------------
template <class TIn, class TOut, class TDerived>
bool BookStep<TIn, TOut, TDerived>::equal (Step const& rhs) const
{
if (auto bs = dynamic_cast<BookStep<TIn, TOut, TDerived> const*>(&rhs))
return book_ == bs->book_;
return false;
}
template <class TIn, class TOut, class TDerived>
boost::optional<Quality>
BookStep<TIn, TOut, TDerived>::qualityUpperBound(
ReadView const& v, bool& redeems) const
{
auto const prevStepRedeems = redeems;
redeems = this->redeems(v, true);
// This can be simplified (and sped up) if directories are never empty.
Sandbox sb(&v, tapNONE);
BookTip bt(sb, book_);
if (!bt.step(j_))
return boost::none;
return static_cast<TDerived const*>(this)->qualityUpperBound(
v, bt.quality(), prevStepRedeems);
}
// Adjust the offer amount and step amount subject to the given input limit
template <class TIn, class TOut>
@@ -231,67 +470,76 @@ void limitStepOut (Quality const& ofrQ,
}
}
/* Iterate through the offers at the best quality in a book.
Unfunded offers and bad offers are skipped (and returned).
TakerGets/Taker pays reflects funding.
callback is called with the offer SLE, taker pays, taker gets.
If callback returns false, don't process any more offers.
Return the unfunded and bad offers and the number of offers consumed.
*/
template <class TAmtIn, class TAmtOut, class Callback>
static
template <class TIn, class TOut, class TDerived>
template <class Callback>
std::pair<boost::container::flat_set<uint256>, std::uint32_t>
forEachOffer (
BookStep<TIn, TOut, TDerived>::forEachOffer (
PaymentSandbox& sb,
ApplyView& afView,
Book const& book,
AccountID const& src,
AccountID const& dst,
bool prevStepRedeems,
bool ownerPaysTransferFee,
Callback& callback,
std::uint32_t limit,
beast::Journal j)
Callback& callback) const
{
// Charge the offer owner, not the sender
// Charge a fee even if the owner is the same as the issuer
// (the old code does not charge a fee)
// Calculate amount that goes to the taker and the amount charged the offer owner
auto rate = [&](AccountID const& id)->std::uint32_t
auto rate = [this, &sb](AccountID const& id)->std::uint32_t
{
if (isXRP (id) || id == dst)
if (isXRP (id) || id == this->strandDst_)
return QUALITY_ONE;
return transferRate (sb, id).value;
};
std::uint32_t const trIn = prevStepRedeems
? rate (book.in.account)
? rate (book_.in.account)
: QUALITY_ONE;
// Always charge the transfer fee, even if the owner is the issuer
std::uint32_t const trOut = ownerPaysTransferFee
? rate (book.out.account)
std::uint32_t const trOut = ownerPaysTransferFee_
? rate (book_.out.account)
: QUALITY_ONE;
typename FlowOfferStream<TAmtIn, TAmtOut>::StepCounter counter (limit, j);
FlowOfferStream<TAmtIn, TAmtOut> offers (
sb, afView, book, sb.parentCloseTime (), counter, j);
typename FlowOfferStream<TIn, TOut>::StepCounter
counter (maxOffersToConsume_, j_);
FlowOfferStream<TIn, TOut> offers (
sb, afView, book_, sb.parentCloseTime (), counter, j_);
bool offerAttempted = false;
boost::optional<Quality> ofrQ;
while (offers.step ())
{
auto& offer = offers.tip ();
// Note that offer.quality() returns a (non-optional) Quality. So
// ofrQ is always safe to use below this point in the loop.
if (!ofrQ)
ofrQ = offer.quality ();
else if (*ofrQ != offer.quality ())
break;
if (static_cast<TDerived const*>(this)->limitSelfCrossQuality (
strandSrc_, strandDst_, offer, ofrQ, offers, offerAttempted))
continue;
if (! static_cast<TDerived const*>(this)->checkQualityThreshold(offer))
break;
auto const ofrInRate =
static_cast<TDerived const*>(this)->getOfrInRate (
prevStep_, offer, trIn);
auto const ofrOutRate =
static_cast<TDerived const*>(this)->getOfrOutRate (
prevStep_, offer, strandDst_, trOut);
auto ofrAmt = offer.amount ();
auto stpAmt = make_Amounts (
mulRatio (ofrAmt.in, trIn, QUALITY_ONE, /*roundUp*/ true),
mulRatio (ofrAmt.in, ofrInRate, QUALITY_ONE, /*roundUp*/ true),
ofrAmt.out);
// owner pays the transfer fee
// owner pays the transfer fee.
auto ownerGives =
mulRatio (ofrAmt.out, trOut, QUALITY_ONE, /*roundUp*/ false);
mulRatio (ofrAmt.out, ofrOutRate, QUALITY_ONE, /*roundUp*/ false);
auto const funds =
(offer.owner () == offer.issueOut ().account)
@@ -303,21 +551,23 @@ forEachOffer (
// We already know offer.owner()!=offer.issueOut().account
ownerGives = funds;
stpAmt.out = mulRatio (
ownerGives, QUALITY_ONE, trOut, /*roundUp*/ false);
ownerGives, QUALITY_ONE, ofrOutRate, /*roundUp*/ false);
ofrAmt = ofrQ->ceil_out (ofrAmt, stpAmt.out);
stpAmt.in = mulRatio (
ofrAmt.in, trIn, QUALITY_ONE, /*roundUp*/ true);
ofrAmt.in, ofrInRate, QUALITY_ONE, /*roundUp*/ true);
}
if (!callback (offer, ofrAmt, stpAmt, ownerGives, trIn, trOut))
offerAttempted = true;
if (!callback (
offer, ofrAmt, stpAmt, ownerGives, ofrInRate, ofrOutRate))
break;
}
return {offers.permToRemove (), counter.count()};
}
template <class TIn, class TOut>
void BookStep<TIn, TOut>::consumeOffer (
template <class TIn, class TOut, class TDerived>
void BookStep<TIn, TOut, TDerived>::consumeOffer (
PaymentSandbox& sb,
TOffer<TIn, TOut>& offer,
TAmounts<TIn, TOut> const& ofrAmt,
@@ -355,9 +605,9 @@ auto sum (TCollection const& col)
return std::accumulate (col.begin () + 1, col.end (), *col.begin ());
};
template<class TIn, class TOut>
template<class TIn, class TOut, class TDerived>
std::pair<TIn, TOut>
BookStep<TIn, TOut>::revImp (
BookStep<TIn, TOut, TDerived>::revImp (
PaymentSandbox& sb,
ApplyView& afView,
boost::container::flat_set<uint256>& ofrsToRm,
@@ -427,9 +677,7 @@ BookStep<TIn, TOut>::revImp (
{
auto const prevStepRedeems = prevStep_ && prevStep_->redeems (sb, false);
auto const r = forEachOffer<TIn, TOut> (sb, afView, book_, strandSrc_,
strandDst_, prevStepRedeems, ownerPaysTransferFee_, eachOffer,
maxOffersToConsume_, j_);
auto const r = forEachOffer (sb, afView, prevStepRedeems, eachOffer);
boost::container::flat_set<uint256> toRm = std::move(std::get<0>(r));
std::uint32_t const offersConsumed = std::get<1>(r);
ofrsToRm.insert (boost::container::ordered_unique_range_t{},
@@ -466,9 +714,9 @@ BookStep<TIn, TOut>::revImp (
return {result.in, result.out};
}
template<class TIn, class TOut>
template<class TIn, class TOut, class TDerived>
std::pair<TIn, TOut>
BookStep<TIn, TOut>::fwdImp (
BookStep<TIn, TOut, TDerived>::fwdImp (
PaymentSandbox& sb,
ApplyView& afView,
boost::container::flat_set<uint256>& ofrsToRm,
@@ -583,9 +831,7 @@ BookStep<TIn, TOut>::fwdImp (
{
auto const prevStepRedeems = prevStep_ && prevStep_->redeems (sb, true);
auto const r = forEachOffer<TIn, TOut> (sb, afView, book_, strandSrc_,
strandDst_, prevStepRedeems, ownerPaysTransferFee_, eachOffer,
maxOffersToConsume_, j_);
auto const r = forEachOffer (sb, afView, prevStepRedeems, eachOffer);
boost::container::flat_set<uint256> toRm = std::move(std::get<0>(r));
std::uint32_t const offersConsumed = std::get<1>(r);
ofrsToRm.insert (boost::container::ordered_unique_range_t{},
@@ -622,9 +868,9 @@ BookStep<TIn, TOut>::fwdImp (
return {result.in, result.out};
}
template<class TIn, class TOut>
template<class TIn, class TOut, class TDerived>
std::pair<bool, EitherAmount>
BookStep<TIn, TOut>::validFwd (
BookStep<TIn, TOut, TDerived>::validFwd (
PaymentSandbox& sb,
ApplyView& afView,
EitherAmount const& in)
@@ -661,9 +907,9 @@ BookStep<TIn, TOut>::validFwd (
return {true, EitherAmount (cache_->out)};
}
template<class TIn, class TOut>
template<class TIn, class TOut, class TDerived>
TER
BookStep<TIn, TOut>::check(StrandContext const& ctx) const
BookStep<TIn, TOut, TDerived>::check(StrandContext const& ctx) const
{
if (book_.in == book_.out)
{
@@ -721,11 +967,11 @@ namespace test
{
// Needed for testing
template <class TIn, class TOut>
template <class TIn, class TOut, class TDerived>
static
bool equalHelper (Step const& step, ripple::Book const& book)
{
if (auto bs = dynamic_cast<BookStep<TIn, TOut> const*> (&step))
if (auto bs = dynamic_cast<BookStep<TIn, TOut, TDerived> const*> (&step))
return book == bs->book ();
return false;
}
@@ -735,13 +981,17 @@ bool bookStepEqual (Step const& step, ripple::Book const& book)
bool const inXRP = isXRP (book.in.currency);
bool const outXRP = isXRP (book.out.currency);
if (inXRP && outXRP)
return equalHelper<XRPAmount, XRPAmount> (step, book);
return equalHelper<XRPAmount, XRPAmount,
BookPaymentStep<XRPAmount, XRPAmount>> (step, book);
if (inXRP && !outXRP)
return equalHelper<XRPAmount, IOUAmount> (step, book);
return equalHelper<XRPAmount, IOUAmount,
BookPaymentStep<XRPAmount, IOUAmount>> (step, book);
if (!inXRP && outXRP)
return equalHelper<IOUAmount, XRPAmount> (step, book);
return equalHelper<IOUAmount, XRPAmount,
BookPaymentStep<IOUAmount, XRPAmount>> (step, book);
if (!inXRP && !outXRP)
return equalHelper<IOUAmount, IOUAmount> (step, book);
return equalHelper<IOUAmount, IOUAmount,
BookPaymentStep<IOUAmount, IOUAmount>> (step, book);
return false;
}
}
@@ -756,14 +1006,26 @@ make_BookStepHelper (
Issue const& in,
Issue const& out)
{
auto r = std::make_unique<BookStep<TIn, TOut>> (
in, out, ctx.strandSrc, ctx.strandDst, ctx.prevStep,
ctx.ownerPaysTransferFee, ctx.j);
auto ter = r->check (ctx);
TER ter = tefINTERNAL;
std::unique_ptr<Step> r;
if (ctx.offerCrossing)
{
auto offerCrossingStep =
std::make_unique<BookOfferCrossingStep<TIn, TOut>> (ctx, in, out);
ter = offerCrossingStep->check (ctx);
r = std::move (offerCrossingStep);
}
else // payment
{
auto paymentStep =
std::make_unique<BookPaymentStep<TIn, TOut>> (ctx, in, out);
ter = paymentStep->check (ctx);
r = std::move (paymentStep);
}
if (ter != tesSUCCESS)
return {ter, nullptr};
return {tesSUCCESS, std::move (r)};
return {tesSUCCESS, std::move(r)};
}
std::pair<TER, std::unique_ptr<Step>>
@@ -780,8 +1042,7 @@ make_BookStepIX (
StrandContext const& ctx,
Issue const& in)
{
Issue out;
return make_BookStepHelper<IOUAmount, XRPAmount> (ctx, in, out);
return make_BookStepHelper<IOUAmount, XRPAmount> (ctx, in, xrpIssue());
}
std::pair<TER, std::unique_ptr<Step>>
@@ -789,8 +1050,7 @@ make_BookStepXI (
StrandContext const& ctx,
Issue const& out)
{
Issue in;
return make_BookStepHelper<XRPAmount, IOUAmount> (ctx, in, out);
return make_BookStepHelper<XRPAmount, IOUAmount> (ctx, xrpIssue(), out);
}
} // ripple

View File

@@ -33,17 +33,17 @@
namespace ripple {
class DirectStepI : public StepImp<IOUAmount, IOUAmount, DirectStepI>
template <class TDerived>
class DirectStepI : public StepImp<IOUAmount, IOUAmount, DirectStepI<TDerived>>
{
private:
protected:
AccountID src_;
AccountID dst_;
Currency currency_;
bool isLast_ = false;
// Charge transfer fees when the prev step redeems
Step const* const prevStep_ = nullptr;
bool const isLast_;
beast::Journal j_;
struct Cache
@@ -67,27 +67,33 @@ class DirectStepI : public StepImp<IOUAmount, IOUAmount, DirectStepI>
boost::optional<Cache> cache_;
// Compute the maximum value that can flow from src->dst at
// the best available quality.
// return: first element is max amount that can flow,
// second is true if dst holds an iou from src.
std::pair<IOUAmount, bool>
maxPaymentFlow (
ReadView const& sb) const;
// Returns srcQOut, dstQIn
std::pair <std::uint32_t, std::uint32_t>
qualities (
PaymentSandbox& sb,
ReadView const& sb,
bool srcRedeems,
bool fwd) const;
public:
public:
DirectStepI (
StrandContext const& ctx,
AccountID const& src,
AccountID const& dst,
Currency const& c,
Step const* prevStep,
bool isLast,
beast::Journal j)
:src_(src)
Currency const& c)
: src_(src)
, dst_(dst)
, currency_ (c)
, isLast_ (isLast)
, prevStep_ (prevStep)
, j_ (j)
, prevStep_ (ctx.prevStep)
, isLast_ (ctx.isLast)
, j_ (ctx.j)
{}
AccountID const& src () const
@@ -136,6 +142,9 @@ class DirectStepI : public StepImp<IOUAmount, IOUAmount, DirectStepI>
std::uint32_t
lineQualityIn (ReadView const& v) const override;
boost::optional<Quality>
qualityUpperBound(ReadView const& v, bool& redeems) const override;
std::pair<IOUAmount, IOUAmount>
revImp (
PaymentSandbox& sb,
@@ -178,7 +187,18 @@ class DirectStepI : public StepImp<IOUAmount, IOUAmount, DirectStepI>
return ! (lhs == rhs);
}
private:
protected:
std::string logStringImpl (char const* name) const
{
std::ostringstream ostr;
ostr <<
name << ": " <<
"\nSrc: " << src_ <<
"\nDst: " << dst_;
return ostr.str ();
}
private:
bool equal (Step const& rhs) const override
{
if (auto ds = dynamic_cast<DirectStepI const*> (&rhs))
@@ -187,62 +207,288 @@ class DirectStepI : public StepImp<IOUAmount, IOUAmount, DirectStepI>
}
return false;
}
};
//------------------------------------------------------------------------------
// Flow is used in two different circumstances for transferring funds:
// o Payments, and
// o Offer crossing.
// The rules for handling funds in these two cases are almost, but not
// quite, the same.
// Payment DirectStep class (not offer crossing).
class DirectIPaymentStep : public DirectStepI<DirectIPaymentStep>
{
public:
using DirectStepI<DirectIPaymentStep>::DirectStepI;
using DirectStepI<DirectIPaymentStep>::check;
bool verifyPrevStepRedeems (bool) const
{
// A payment doesn't care whether or not prevStepRedeems.
return true;
}
bool verifyDstQualityIn (std::uint32_t dstQIn) const
{
// Payments have no particular expectations for what dstQIn will be.
return true;
}
std::uint32_t
quality (ReadView const& sb,
// set true for quality in, false for quality out
bool qin) const;
// Compute the maximum value that can flow from src->dst at
// the best available quality.
// return: first element is max amount that can flow,
// second is true if dst holds an iou from src.
std::pair<IOUAmount, bool>
maxFlow (ReadView const& sb, IOUAmount const& desired) const;
// Verify the consistency of the step. These checks are specific to
// payments and assume that general checks were already performed.
TER
check (StrandContext const& ctx,
std::shared_ptr<const SLE> const& sleSrc) const;
std::string logString () const override
{
std::ostringstream ostr;
ostr <<
"DirectStepI: " <<
"\nSrc: " << src_ <<
"\nDst: " << dst_;
return ostr.str ();
return logStringImpl ("DirectIPaymentStep");
}
};
// Compute the maximum value that can flow from src->dst at
// the best available quality.
// return: first element is max amount that can flow,
// second is if src redeems to dst
static
// Offer crossing DirectStep class (not a payment).
class DirectIOfferCrossingStep : public DirectStepI<DirectIOfferCrossingStep>
{
public:
using DirectStepI<DirectIOfferCrossingStep>::DirectStepI;
using DirectStepI<DirectIOfferCrossingStep>::check;
bool verifyPrevStepRedeems (bool prevStepRedeems) const
{
// During offer crossing we rely on the fact that prevStepRedeems
// will *always* be false. That's because:
// o If there's a prevStep_, it will always be a BookStep.
// o BookStep::redeems() aways returns false when offer crossing.
// An assert based on this return value will tell us if that
// behavior changes.
return !prevStepRedeems;
}
bool verifyDstQualityIn (std::uint32_t dstQIn) const
{
// Due to a couple of factors dstQIn is always QUALITY_ONE for
// offer crossing. If that changes we need to know.
return dstQIn == QUALITY_ONE;
}
std::uint32_t
quality (ReadView const& sb,
// set true for quality in, false for quality out
bool qin) const;
// Compute the maximum value that can flow from src->dst at
// the best available quality.
// return: first element is max amount that can flow,
// second is true if dst holds an iou from src.
std::pair<IOUAmount, bool>
maxFlow (ReadView const& sb, IOUAmount const& desired) const;
// Verify the consistency of the step. These checks are specific to
// offer crossing and assume that general checks were already performed.
TER
check (StrandContext const& ctx,
std::shared_ptr<const SLE> const& sleSrc) const;
std::string logString () const override
{
return logStringImpl ("DirectIOfferCrossingStep");
}
};
//------------------------------------------------------------------------------
std::uint32_t
DirectIPaymentStep::quality (ReadView const& sb,
// set true for quality in, false for quality out
bool qin) const
{
if (src_ == dst_)
return QUALITY_ONE;
auto const sle = sb.read (keylet::line (dst_, src_, currency_));
if (!sle)
return QUALITY_ONE;
auto const& field = [this, qin]() -> SF_U32 const&
{
if (qin)
{
// compute dst quality in
if (this->dst_ < this->src_)
return sfLowQualityIn;
else
return sfHighQualityIn;
}
else
{
// compute src quality out
if (this->src_ < this->dst_)
return sfLowQualityOut;
else
return sfHighQualityOut;
}
}();
if (! sle->isFieldPresent (field))
return QUALITY_ONE;
auto const q = (*sle)[field];
if (!q)
return QUALITY_ONE;
return q;
}
std::uint32_t
DirectIOfferCrossingStep::quality (ReadView const&,
// set true for quality in, false for quality out
bool) const
{
// If offer crossing then ignore trust line Quality fields. This
// preserves a long-standing tradition.
return QUALITY_ONE;
}
std::pair<IOUAmount, bool>
maxFlow (
PaymentSandbox const& sb,
AccountID const& src,
AccountID const& dst,
Currency const& cur)
DirectIPaymentStep::maxFlow (ReadView const& sb, IOUAmount const&) const
{
return maxPaymentFlow (sb);
}
std::pair<IOUAmount, bool>
DirectIOfferCrossingStep::maxFlow (
ReadView const& sb, IOUAmount const& desired) const
{
// When isLast and offer crossing then ignore trust line limits. Offer
// crossing has the ability to exceed the limit set by a trust line.
// We presume that if someone is creating an offer then they intend to
// fill as much of that offer as possible, even if the offer exceeds
// the limit that a trust line sets.
//
// A note on using "out" as the desired parameter for maxFlow. In some
// circumstances during payments we end up needing a value larger than
// "out" for "maxSrcToDst". But as of now (June 2016) that never happens
// during offer crossing. That's because, due to a couple of factors,
// "dstQIn" is always QUALITY_ONE for offer crossing.
if (isLast_)
return {desired, false};
return maxPaymentFlow (sb);
}
TER
DirectIPaymentStep::check (
StrandContext const& ctx, std::shared_ptr<const SLE> const& sleSrc) const
{
// Since this is a payment a trust line must be present. Perform all
// trust line related checks.
{
auto const sleLine = ctx.view.read (keylet::line (src_, dst_, currency_));
if (!sleLine)
{
JLOG (j_.trace()) << "DirectStepI: No credit line. " << *this;
return terNO_LINE;
}
auto const authField = (src_ > dst_) ? lsfHighAuth : lsfLowAuth;
if (((*sleSrc)[sfFlags] & lsfRequireAuth) &&
!((*sleLine)[sfFlags] & authField) &&
(*sleLine)[sfBalance] == zero)
{
JLOG (j_.warn())
<< "DirectStepI: can't receive IOUs from issuer without auth."
<< " src: " << src_;
return terNO_AUTH;
}
if (ctx.prevStep &&
fix1449(ctx.view.info().parentCloseTime))
{
if (ctx.prevStep->bookStepBook())
{
auto const noRippleSrcToDst =
((*sleLine)[sfFlags] &
((src_ > dst_) ? lsfHighNoRipple : lsfLowNoRipple));
if (noRippleSrcToDst)
return terNO_RIPPLE;
}
}
}
{
auto const owed = creditBalance (ctx.view, dst_, src_, currency_);
if (owed <= zero)
{
auto const limit = creditLimit (ctx.view, dst_, src_, currency_);
if (-owed >= limit)
{
JLOG (j_.debug())
<< "DirectStepI: dry: owed: " << owed << " limit: " << limit;
return tecPATH_DRY;
}
}
}
return tesSUCCESS;
}
TER
DirectIOfferCrossingStep::check (
StrandContext const&, std::shared_ptr<const SLE> const&) const
{
// The standard checks are all we can do because any remaining checks
// require the existence of a trust line. Offer crossing does not
// require a pre-existing trust line.
return tesSUCCESS;
}
//------------------------------------------------------------------------------
template <class TDerived>
std::pair<IOUAmount, bool>
DirectStepI<TDerived>::maxPaymentFlow (ReadView const& sb) const
{
auto const srcOwed = toAmount<IOUAmount> (
accountHolds (sb, src, cur, dst, fhIGNORE_FREEZE, beast::Journal{}));
accountHolds (sb, src_, currency_, dst_, fhIGNORE_FREEZE, j_));
if (srcOwed.signum () > 0)
return {srcOwed, true};
// srcOwed is negative or zero
return {creditLimit2 (sb, dst, src, cur) + srcOwed, false};
return {creditLimit2 (sb, dst_, src_, currency_) + srcOwed, false};
}
template <class TDerived>
bool
DirectStepI::redeems (ReadView const& sb, bool fwd) const
DirectStepI<TDerived>::redeems (ReadView const& sb, bool fwd) const
{
if (!fwd)
{
auto const srcOwed = accountHolds (
sb, src_, currency_, dst_, fhIGNORE_FREEZE, beast::Journal{});
return srcOwed.signum () > 0;
}
else
{
if (!cache_)
{
assert (0);
return false;
}
if (fwd && cache_)
return cache_->srcRedeems;
}
auto const srcOwed = accountHolds (
sb, src_, currency_, dst_, fhIGNORE_FREEZE, j_);
return srcOwed.signum () > 0;
}
template <class TDerived>
std::pair<IOUAmount, IOUAmount>
DirectStepI::revImp (
DirectStepI<TDerived>::revImp (
PaymentSandbox& sb,
ApplyView& /*afView*/,
boost::container::flat_set<uint256>& /*ofrsToRm*/,
@@ -252,11 +498,13 @@ DirectStepI::revImp (
bool srcRedeems;
IOUAmount maxSrcToDst;
std::tie (maxSrcToDst, srcRedeems) =
maxFlow (sb, src_, dst_, currency_);
static_cast<TDerived const*>(this)->maxFlow (sb, out);
std::uint32_t srcQOut, dstQIn;
std::tie (srcQOut, dstQIn) = qualities (sb, srcRedeems, false);
assert (static_cast<TDerived const*>(this)->verifyDstQualityIn (dstQIn));
Issue const srcToDstIss (currency_, srcRedeems ? dst_ : src_);
@@ -284,7 +532,6 @@ DirectStepI::revImp (
if (srcToDst <= maxSrcToDst)
{
IOUAmount const in = mulRatio (
srcToDst, srcQOut, QUALITY_ONE, /*roundUp*/ true);
cache_.emplace (in, srcToDst, out, srcRedeems);
@@ -322,8 +569,9 @@ DirectStepI::revImp (
// pass. But sometimes rounding differences cause the forward pass to
// deliver more liquidity. Use the cached values from the reverse pass
// to prevent this.
template <class TDerived>
void
DirectStepI::setCacheLimiting (
DirectStepI<TDerived>::setCacheLimiting (
IOUAmount const& fwdIn,
IOUAmount const& fwdSrcToDst,
IOUAmount const& fwdOut,
@@ -362,8 +610,9 @@ DirectStepI::setCacheLimiting (
cache_->srcRedeems = srcRedeems;
};
template <class TDerived>
std::pair<IOUAmount, IOUAmount>
DirectStepI::fwdImp (
DirectStepI<TDerived>::fwdImp (
PaymentSandbox& sb,
ApplyView& /*afView*/,
boost::container::flat_set<uint256>& /*ofrsToRm*/,
@@ -374,7 +623,7 @@ DirectStepI::fwdImp (
bool srcRedeems;
IOUAmount maxSrcToDst;
std::tie (maxSrcToDst, srcRedeems) =
maxFlow (sb, src_, dst_, currency_);
static_cast<TDerived const*>(this)->maxFlow (sb, cache_->srcToDst);
std::uint32_t srcQOut, dstQIn;
std::tie (srcQOut, dstQIn) = qualities (sb, srcRedeems, true);
@@ -439,8 +688,9 @@ DirectStepI::fwdImp (
return {cache_->in, cache_->out};
}
template <class TDerived>
std::pair<bool, EitherAmount>
DirectStepI::validFwd (
DirectStepI<TDerived>::validFwd (
PaymentSandbox& sb,
ApplyView& afView,
EitherAmount const& in)
@@ -459,7 +709,7 @@ DirectStepI::validFwd (
bool srcRedeems;
IOUAmount maxSrcToDst;
std::tie (maxSrcToDst, srcRedeems) =
maxFlow (sb, src_, dst_, currency_);
static_cast<TDerived const*>(this)->maxFlow (sb, cache_->srcToDst);
try
{
@@ -495,58 +745,11 @@ DirectStepI::validFwd (
return {true, EitherAmount (cache_->out)};
}
static
std::uint32_t
quality (
ReadView const& sb,
AccountID const& src,
AccountID const& dst,
Currency const& currency,
// set true for dst quality in, false for src quality out
bool qin)
{
if (src == dst)
return QUALITY_ONE;
auto const sle = sb.read (
keylet::line (dst, src, currency));
if (!sle)
return QUALITY_ONE;
auto const& field = [&]() -> SF_U32 const&
{
if (qin)
{
// compute dst quality in
if (dst < src)
return sfLowQualityIn;
else
return sfHighQualityIn;
}
else
{
// compute src quality out
if (src < dst)
return sfLowQualityOut;
else
return sfHighQualityOut;
}
}();
if (! sle->isFieldPresent (field))
return QUALITY_ONE;
auto const q = (*sle)[field];
if (!q)
return QUALITY_ONE;
return q;
}
// Returns srcQOut, dstQIn
template <class TDerived>
std::pair<std::uint32_t, std::uint32_t>
DirectStepI::qualities (
PaymentSandbox& sb,
DirectStepI<TDerived>::qualities (
ReadView const& sb,
bool srcRedeems,
bool fwd) const
{
@@ -556,7 +759,9 @@ DirectStepI::qualities (
return {QUALITY_ONE, QUALITY_ONE};
auto const prevStepQIn = prevStep_->lineQualityIn (sb);
auto srcQOut = quality (sb, src_, dst_, currency_, false);
auto srcQOut = static_cast<TDerived const*>(this)->quality (
sb, /* src quality out */ false);
if (prevStepQIn > srcQOut)
srcQOut = prevStepQIn;
return {srcQOut, QUALITY_ONE};
@@ -565,24 +770,50 @@ DirectStepI::qualities (
{
// Charge a transfer rate when issuing and previous step redeems
auto const prevStepRedeems = prevStep_ && prevStep_->redeems (sb, fwd);
assert (static_cast<TDerived const*>(this)->verifyPrevStepRedeems (
prevStepRedeems));
std::uint32_t const srcQOut =
prevStepRedeems ? transferRate (sb, src_).value : QUALITY_ONE;
auto dstQIn = quality (sb, src_, dst_, currency_, true);
auto dstQIn = static_cast<TDerived const*>(this)->quality (
sb, /* dst quality in */ true);
if (isLast_ && dstQIn > QUALITY_ONE)
dstQIn = QUALITY_ONE;
return {srcQOut, dstQIn};
}
}
template <class TDerived>
std::uint32_t
DirectStepI::lineQualityIn (ReadView const& v) const
DirectStepI<TDerived>::lineQualityIn (ReadView const& v) const
{
// dst quality in
return quality (v, src_, dst_, currency_, true);
return static_cast<TDerived const*>(this)->quality (
v, /* dst quality in */ true);
}
TER DirectStepI::check (StrandContext const& ctx) const
template <class TDerived>
boost::optional<Quality>
DirectStepI<TDerived>::qualityUpperBound(ReadView const& v, bool& redeems) const
{
auto const prevRedeems = redeems;
redeems = this->redeems(v, true);
std::uint32_t const srcQOut =
(prevRedeems && !redeems) ? transferRate(v, src_).value : QUALITY_ONE;
auto dstQIn = static_cast<TDerived const*>(this)->quality (
v, /* dst quality in */ true);
if (isLast_ && dstQIn > QUALITY_ONE)
dstQIn = QUALITY_ONE;
Issue const iss{currency_, src_};
return Quality(getRate(STAmount(iss, srcQOut), STAmount(iss, dstQIn)));
}
template <class TDerived>
TER DirectStepI<TDerived>::check (StrandContext const& ctx) const
{
// The following checks apply for both payments and offer crossing.
if (!src_ || !dst_)
{
JLOG (j_.debug()) << "DirectStepI: specified bad account.";
@@ -595,69 +826,35 @@ TER DirectStepI::check (StrandContext const& ctx) const
return temBAD_PATH;
}
auto const sleSrc = ctx.view.read (keylet::account (src_));
if (!sleSrc)
{
auto sleSrc = ctx.view.read (keylet::account (src_));
if (!sleSrc)
JLOG (j_.warn())
<< "DirectStepI: can't receive IOUs from non-existent issuer: "
<< src_;
return terNO_ACCOUNT;
}
// pure issue/redeem can't be frozen
if (!(ctx.isLast && ctx.isFirst))
{
auto const ter = checkFreeze(ctx.view, src_, dst_, currency_);
if (ter != tesSUCCESS)
return ter;
}
// If previous step was a direct step then we need to check
// no ripple flags.
if (ctx.prevStep)
{
if (auto prevSrc = ctx.prevStep->directStepSrcAcct())
{
JLOG (j_.warn())
<< "DirectStepI: can't receive IOUs from non-existent issuer: "
<< src_;
return terNO_ACCOUNT;
}
auto const sleLine = ctx.view.read (keylet::line (src_, dst_, currency_));
if (!sleLine)
{
JLOG (j_.trace()) << "DirectStepI: No credit line. " << *this;
return terNO_LINE;
}
auto const authField = (src_ > dst_) ? lsfHighAuth : lsfLowAuth;
if (((*sleSrc)[sfFlags] & lsfRequireAuth) &&
!((*sleLine)[sfFlags] & authField) &&
(*sleLine)[sfBalance] == zero)
{
JLOG (j_.warn())
<< "DirectStepI: can't receive IOUs from issuer without auth."
<< " src: " << src_;
return terNO_AUTH;
}
// pure issue/redeem can't be frozen
if (!(ctx.isLast && ctx.isFirst))
{
auto const ter = checkFreeze(ctx.view, src_, dst_, currency_);
auto const ter = checkNoRipple(
ctx.view, *prevSrc, src_, dst_, currency_, j_);
if (ter != tesSUCCESS)
return ter;
}
if (ctx.prevStep)
{
if (auto prevSrc = ctx.prevStep->directStepSrcAcct())
{
auto const ter = checkNoRipple(
ctx.view, *prevSrc, src_, dst_, currency_, j_);
if (ter != tesSUCCESS)
return ter;
}
if (fix1449(ctx.view.info().parentCloseTime))
{
if (ctx.prevStep->bookStepBook())
{
auto const noRippleSrcToDst =
((*sleLine)[sfFlags] &
((src_ > dst_) ? lsfHighNoRipple : lsfLowNoRipple));
if (noRippleSrcToDst)
return terNO_RIPPLE;
}
}
}
}
{
Issue const srcIssue{currency_, src_};
Issue const dstIssue{currency_, dst_};
@@ -688,21 +885,7 @@ TER DirectStepI::check (StrandContext const& ctx) const
}
}
{
auto const owed = creditBalance (ctx.view, dst_, src_, currency_);
if (owed <= zero)
{
auto const limit = creditLimit (ctx.view, dst_, src_, currency_);
if (-owed >= limit)
{
JLOG (j_.debug())
<< "DirectStepI: dry: owed: " << owed << " limit: " << limit;
return tecPATH_DRY;
}
}
}
return tesSUCCESS;
return static_cast<TDerived const*>(this)->check (ctx, sleSrc);
}
//------------------------------------------------------------------------------
@@ -715,7 +898,8 @@ bool directStepEqual (Step const& step,
AccountID const& dst,
Currency const& currency)
{
if (auto ds = dynamic_cast<DirectStepI const*> (&step))
if (auto ds =
dynamic_cast<DirectStepI<DirectIPaymentStep> const*> (&step))
{
return ds->src () == src && ds->dst () == dst &&
ds->currency () == currency;
@@ -733,12 +917,25 @@ make_DirectStepI (
AccountID const& dst,
Currency const& c)
{
// Only charge a transfer fee if the previous step redeems
auto r = std::make_unique<DirectStepI> (
src, dst, c, ctx.prevStep, ctx.isLast, ctx.j);
auto ter = r->check (ctx);
TER ter = tefINTERNAL;
std::unique_ptr<Step> r;
if (ctx.offerCrossing)
{
auto offerCrossingStep =
std::make_unique<DirectIOfferCrossingStep> (ctx, src, dst, c);
ter = offerCrossingStep->check (ctx);
r = std::move (offerCrossingStep);
}
else // payment
{
auto paymentStep =
std::make_unique<DirectIPaymentStep> (ctx, src, dst, c);
ter = paymentStep->check (ctx);
r = std::move (paymentStep);
}
if (ter != tesSUCCESS)
return {ter, nullptr};
return {tesSUCCESS, std::move (r)};
}

View File

@@ -141,9 +141,11 @@ toStrandV1 (
AccountID const& src,
AccountID const& dst,
Issue const& deliver,
boost::optional<Quality> const& limitQuality,
boost::optional<Issue> const& sendMaxIssue,
STPath const& path,
bool ownerPaysTransferFee,
bool offerCrossing,
beast::Journal j)
{
if (isXRP (src))
@@ -204,9 +206,13 @@ toStrandV1 (
pes.push_back (&*sendMaxPE);
for (auto& i : path)
pes.push_back (&i);
auto const lastCurrency =
(*boost::find_if (boost::adaptors::reverse (pes), hasCurrency))->getCurrency ();
if (lastCurrency != deliver.currency)
// Note that for offer crossing (only) we do use an offer book even if
// all that is changing is the Issue.account.
STPathElement const* const lastCurrency =
*boost::find_if (boost::adaptors::reverse (pes), hasCurrency);
if ((lastCurrency->getCurrency() != deliver.currency) ||
(offerCrossing && lastCurrency->getIssuerID() != deliver.account))
{
deliverOfferNode.emplace (boost::none, deliver.currency, deliver.account);
pes.push_back (&*deliverOfferNode);
@@ -222,6 +228,7 @@ toStrandV1 (
auto const strandSrc = firstNode.getAccountID ();
auto const strandDst = lastNode.getAccountID ();
bool const isDefaultPath = path.empty();
Strand result;
result.reserve (2 * pes.size ());
@@ -239,8 +246,9 @@ toStrandV1 (
seenBookOuts.reserve (pes.size());
auto ctx = [&](bool isLast = false)
{
return StrandContext{view, result, strandSrc, strandDst, isLast,
ownerPaysTransferFee, seenDirectIssues, seenBookOuts, j};
return StrandContext{view, result, strandSrc, strandDst, deliver,
limitQuality, isLast, ownerPaysTransferFee, offerCrossing,
isDefaultPath, seenDirectIssues, seenBookOuts, j};
};
for (int i = 0; i < pes.size () - 1; ++i)
@@ -381,9 +389,11 @@ toStrandV2 (
AccountID const& src,
AccountID const& dst,
Issue const& deliver,
boost::optional<Quality> const& limitQuality,
boost::optional<Issue> const& sendMaxIssue,
STPath const& path,
bool ownerPaysTransferFee,
bool offerCrossing,
beast::Journal j)
{
if (isXRP(src) || isXRP(dst) ||
@@ -447,12 +457,20 @@ toStrandV2 (
for (auto const& i : path)
normPath.push_back(i);
auto const lastCurrency =
(*boost::find_if(boost::adaptors::reverse(normPath), hasCurrency))
.getCurrency();
if (lastCurrency != deliver.currency)
normPath.emplace_back(
boost::none, deliver.currency, deliver.account);
{
// Note that for offer crossing (only) we do use an offer book
// even if all that is changing is the Issue.account.
STPathElement const& lastCurrency =
*boost::find_if (boost::adaptors::reverse (normPath),
hasCurrency);
if ((lastCurrency.getCurrency() != deliver.currency) ||
(offerCrossing &&
lastCurrency.getIssuerID() != deliver.account))
{
normPath.emplace_back(
boost::none, deliver.currency, deliver.account);
}
}
if (!((normPath.back().isAccount() &&
normPath.back().getAccountID() == deliver.account) ||
@@ -470,6 +488,7 @@ toStrandV2 (
auto const strandSrc = normPath.front().getAccountID ();
auto const strandDst = normPath.back().getAccountID ();
bool const isDefaultPath = path.empty();
Strand result;
result.reserve (2 * normPath.size ());
@@ -487,8 +506,9 @@ toStrandV2 (
seenBookOuts.reserve (normPath.size());
auto ctx = [&](bool isLast = false)
{
return StrandContext{view, result, strandSrc, strandDst, isLast,
ownerPaysTransferFee, seenDirectIssues, seenBookOuts, j};
return StrandContext{view, result, strandSrc, strandDst, deliver,
limitQuality, isLast, ownerPaysTransferFee, offerCrossing,
isDefaultPath, seenDirectIssues, seenBookOuts, j};
};
for (std::size_t i = 0; i < normPath.size () - 1; ++i)
@@ -663,17 +683,19 @@ toStrand (
AccountID const& src,
AccountID const& dst,
Issue const& deliver,
boost::optional<Quality> const& limitQuality,
boost::optional<Issue> const& sendMaxIssue,
STPath const& path,
bool ownerPaysTransferFee,
bool offerCrossing,
beast::Journal j)
{
if (view.rules().enabled(fix1373))
return toStrandV2(
view, src, dst, deliver, sendMaxIssue, path, ownerPaysTransferFee, j);
return toStrandV2(view, src, dst, deliver, limitQuality,
sendMaxIssue, path, ownerPaysTransferFee, offerCrossing, j);
else
return toStrandV1(
view, src, dst, deliver, sendMaxIssue, path, ownerPaysTransferFee, j);
return toStrandV1(view, src, dst, deliver, limitQuality,
sendMaxIssue, path, ownerPaysTransferFee, offerCrossing, j);
}
std::pair<TER, std::vector<Strand>>
@@ -682,10 +704,12 @@ toStrands (
AccountID const& src,
AccountID const& dst,
Issue const& deliver,
boost::optional<Quality> const& limitQuality,
boost::optional<Issue> const& sendMax,
STPathSet const& paths,
bool addDefaultPath,
bool ownerPaysTransferFee,
bool offerCrossing,
beast::Journal j)
{
std::vector<Strand> result;
@@ -701,8 +725,8 @@ toStrands (
if (addDefaultPath)
{
auto sp = toStrand (
view, src, dst, deliver, sendMax, STPath (), ownerPaysTransferFee, j);
auto sp = toStrand (view, src, dst, deliver, limitQuality,
sendMax, STPath(), ownerPaysTransferFee, offerCrossing, j);
auto const ter = sp.first;
auto& strand = sp.second;
@@ -733,8 +757,8 @@ toStrands (
TER lastFailTer = tesSUCCESS;
for (auto const& p : paths)
{
auto sp = toStrand (
view, src, dst, deliver, sendMax, p, ownerPaysTransferFee, j);
auto sp = toStrand (view, src, dst, deliver,
limitQuality, sendMax, p, ownerPaysTransferFee, offerCrossing, j);
auto ter = sp.first;
auto& strand = sp.second;
@@ -768,19 +792,27 @@ StrandContext::StrandContext (
std::vector<std::unique_ptr<Step>> const& strand_,
// A strand may not include an inner node that
// replicates the source or destination.
AccountID strandSrc_,
AccountID strandDst_,
AccountID const& strandSrc_,
AccountID const& strandDst_,
Issue const& strandDeliver_,
boost::optional<Quality> const& limitQuality_,
bool isLast_,
bool ownerPaysTransferFee_,
bool offerCrossing_,
bool isDefaultPath_,
std::array<boost::container::flat_set<Issue>, 2>& seenDirectIssues_,
boost::container::flat_set<Issue>& seenBookOuts_,
beast::Journal j_)
: view (view_)
, strandSrc (strandSrc_)
, strandDst (strandDst_)
, strandDeliver (strandDeliver_)
, limitQuality (limitQuality_)
, isFirst (strand_.empty ())
, isLast (isLast_)
, ownerPaysTransferFee (ownerPaysTransferFee_)
, offerCrossing (offerCrossing_)
, isDefaultPath (isDefaultPath_)
, strandSize (strand_.size ())
, prevStep (!strand_.empty () ? strand_.back ().get ()
: nullptr)

View File

@@ -34,7 +34,7 @@ class PaymentSandbox;
class ReadView;
class ApplyView;
/*
/**
A step in a payment path
There are five concrete step classes:
@@ -63,19 +63,20 @@ class ApplyView;
class Step
{
public:
/** Step destructor. */
virtual ~Step () = default;
/**
Find the amount we need to put into the step to get the requested out
subject to liquidity limits
@param sb view with the strands state of balances and offers
@param sb view with the strand's state of balances and offers
@param afView view the the state of balances before the strand runs
this determines if an offer becomes unfunded or is found unfunded
@param ofrsToRm offers found unfunded or in an error state are added to this collection
@param out requested step output
@return actual step input and output
*/
*/
virtual
std::pair<EitherAmount, EitherAmount>
rev (
@@ -88,7 +89,7 @@ public:
Find the amount we get out of the step given the input
subject to liquidity limits
@param sb view with the strands state of balances and offers
@param sb view with the strand's state of balances and offers
@param afView view the the state of balances before the strand runs
this determines if an offer becomes unfunded or is found unfunded
@param ofrsToRm offers found unfunded or in an error state are added to this collection
@@ -98,15 +99,23 @@ public:
virtual
std::pair<EitherAmount, EitherAmount>
fwd (
PaymentSandbox&,
PaymentSandbox& sb,
ApplyView& afView,
boost::container::flat_set<uint256>& ofrsToRm,
EitherAmount const& in) = 0;
/**
Amount of currency computed coming into the Step the last time the
step ran in reverse.
*/
virtual
boost::optional<EitherAmount>
cachedIn () const = 0;
/**
Amount of currency computed coming out of the Step the last time the
step ran in reverse.
*/
virtual
boost::optional<EitherAmount>
cachedOut () const = 0;
@@ -134,6 +143,9 @@ public:
otherwise return false.
If this step is a BookStep, return false if the owner pays the transfer fee,
otherwise return true.
@param sb view with the strand's state of balances and offers
@param fwd false -> called from rev(); true -> called from fwd().
*/
virtual bool
redeems (ReadView const& sb, bool fwd) const
@@ -141,7 +153,8 @@ public:
return false;
}
/** If this step is a DirectStepI, return the quality in of the dst account.
/**
If this step is a DirectStepI, return the quality in of the dst account.
*/
virtual std::uint32_t
lineQualityIn (ReadView const&) const
@@ -149,6 +162,19 @@ public:
return QUALITY_ONE;
}
/**
Find an upper bound of quality for the step
@param v view to query the ledger state from
@param redeems in/out param. Set to true if the previous step redeems.
Will be set to true if this step redeems; Will be set to false if this
step does not redeem.
@return The upper bound of quality for the step, or boost::none if the
step is dry.
*/
virtual boost::optional<Quality>
qualityUpperBound(ReadView const& v, bool& redeems) const = 0;
/**
If this step is a BookStep, return the book.
*/
@@ -165,12 +191,18 @@ public:
bool
dry (EitherAmount const& out) const = 0;
/**
Return true if Out of lhs == Out of rhs.
*/
virtual
bool
equalOut (
EitherAmount const& lhs,
EitherAmount const& rhs) const = 0;
/**
Return true if In of lhs == In of rhs.
*/
virtual bool equalIn (
EitherAmount const& lhs,
EitherAmount const& rhs) const = 0;
@@ -182,7 +214,7 @@ public:
@param afView view the the state of balances before the strand runs
this determines if an offer becomes unfunded or is found unfunded
@param in requested step input
@return first element is true is step is valid, second element is out amount
@return first element is true if step is valid, second element is out amount
*/
virtual
std::pair<bool, EitherAmount>
@@ -191,16 +223,29 @@ public:
ApplyView& afView,
EitherAmount const& in) = 0;
/** Return true if lhs == rhs.
@param lhs Step to compare.
@param rhs Step to compare.
@return true if lhs == rhs.
*/
friend bool operator==(Step const& lhs, Step const& rhs)
{
return lhs.equal (rhs);
}
/** Return true if lhs != rhs.
@param lhs Step to compare.
@param rhs Step to compare.
@return true if lhs != rhs.
*/
friend bool operator!=(Step const& lhs, Step const& rhs)
{
return ! (lhs == rhs);
}
/** Streaming operator for a Step. */
friend
std::ostream&
operator << (
@@ -210,6 +255,7 @@ public:
stream << step.logString ();
return stream;
}
private:
virtual
std::string
@@ -218,8 +264,11 @@ private:
virtual bool equal (Step const& rhs) const = 0;
};
/// @cond INTERNAL
using Strand = std::vector<std::unique_ptr<Step>>;
/// @endcond
/// @cond INTERNAL
inline
bool operator==(Strand const& lhs, Strand const& rhs)
{
@@ -230,6 +279,7 @@ bool operator==(Strand const& lhs, Strand const& rhs)
return false;
return true;
}
/// @endcond
/*
Normalize a path by inserting implied accounts and offers
@@ -251,20 +301,26 @@ normalizePath(AccountID const& src,
boost::optional<Issue> const& sendMaxIssue,
STPath const& path);
/*
Create a strand for the specified path
/**
Create a Strand for the specified path
@param sb view for trust lines, balances, and attributes like auth and freeze
@param src Account that is sending assets
@param dst Account that is receiving assets
@param deliver Asset the dst account will receive
(if issuer of deliver == dst, then accept any issuer)
@param sendMax Optional asset to send.
(if issuer of deliver == dst, then accept any issuer)
@param limitQuality Offer crossing BookSteps use this value in an
optimization. If, during direct offer crossing, the
quality of the tip of the book drops below this value,
then evaluating the strand can stop.
@param sendMaxIssue Optional asset to send.
@param path Liquidity sources to use for this strand of the payment. The path
contains an ordered collection of the offer books to use and
accounts to ripple through.
@param l logs to write journal messages to
@return error code and collection of strands
@param ownerPaysTransferFee false -> charge sender; true -> charge offer owner
@param offerCrossing false -> payment; true -> offer crossing
@param j Journal for logging messages
@return Error code and constructed Strand
*/
std::pair<TER, Strand>
toStrand (
@@ -272,26 +328,34 @@ toStrand (
AccountID const& src,
AccountID const& dst,
Issue const& deliver,
boost::optional<Quality> const& limitQuality,
boost::optional<Issue> const& sendMaxIssue,
STPath const& path,
bool ownerPaysTransferFee,
bool offerCrossing,
beast::Journal j);
/**
Create a strand for each specified path (including the default path, if
specified)
Create a Strand for each specified path (including the default path, if
indicated)
@param sb view for trust lines, balances, and attributes like auth and freeze
@param sb View for trust lines, balances, and attributes like auth and freeze
@param src Account that is sending assets
@param dst Account that is receiving assets
@param deliver Asset the dst account will receive
(if issuer of deliver == dst, then accept any issuer)
@param limitQuality Offer crossing BookSteps use this value in an
optimization. If, during direct offer crossing, the
quality of the tip of the book drops below this value,
then evaluating the strand can stop.
@param sendMax Optional asset to send.
@param paths Paths to use to fullfill the payment. Each path in the pathset
contains an ordered collection of the offer books to use and
accounts to ripple through.
@param addDefaultPath Determines if the default path should be considered
@param l logs to write journal messages to
@param addDefaultPath Determines if the default path should be included
@param ownerPaysTransferFee false -> charge sender; true -> charge offer owner
@param offerCrossing false -> payment; true -> offer crossing
@param j Journal for logging messages
@return error code and collection of strands
*/
std::pair<TER, std::vector<Strand>>
@@ -299,12 +363,15 @@ toStrands (ReadView const& sb,
AccountID const& src,
AccountID const& dst,
Issue const& deliver,
boost::optional<Quality> const& limitQuality,
boost::optional<Issue> const& sendMax,
STPathSet const& paths,
bool addDefaultPath,
bool ownerPaysTransferFee,
bool offerCrossing,
beast::Journal j);
/// @cond INTERNAL
template <class TIn, class TOut, class TDerived>
struct StepImp : public Step
{
@@ -352,7 +419,9 @@ struct StepImp : public Step
return get<TIn> (lhs) == get<TIn> (rhs);
}
};
/// @endcond
/// @cond INTERNAL
// Thrown when unexpected errors occur
class FlowException : public std::runtime_error
{
@@ -373,47 +442,66 @@ public:
{
}
};
/// @endcond
/// @cond INTERNAL
// Check equal with tolerance
bool checkNear (IOUAmount const& expected, IOUAmount const& actual);
bool checkNear (XRPAmount const& expected, XRPAmount const& actual);
/// @endcond
/**
Context needed for error checking
Context needed to build Strand Steps and for error checking
*/
struct StrandContext
{
ReadView const& view;
AccountID const strandSrc;
AccountID const strandDst;
bool const isFirst;
bool const isLast = false;
bool ownerPaysTransferFee;
size_t const strandSize;
// The previous step in the strand. Needed to check the no ripple constraint
ReadView const& view; ///< Current ReadView
AccountID const strandSrc; ///< Strand source account
AccountID const strandDst; ///< Strand destination account
Issue const strandDeliver; ///< Issue strand delivers
boost::optional<Quality> const limitQuality; ///< Worst accepted quality
bool const isFirst; ///< true if Step is first in Strand
bool const isLast = false; ///< true if Step is last in Strand
bool const ownerPaysTransferFee; ///< true if owner, not sender, pays fee
bool const offerCrossing; ///< true if offer crossing, not payment
bool const isDefaultPath; ///< true if Strand is default path
size_t const strandSize; ///< Length of Strand
/** The previous step in the strand. Needed to check the no ripple
constraint
*/
Step const* const prevStep = nullptr;
// A strand may not include the same account node more than once
// in the same currency. In a direct step, an account will show up
// at most twice: once as a src and once as a dst (hence the two element array).
// The strandSrc and strandDst will only show up once each.
/** A strand may not include the same account node more than once
in the same currency. In a direct step, an account will show up
at most twice: once as a src and once as a dst (hence the two element array).
The strandSrc and strandDst will only show up once each.
*/
std::array<boost::container::flat_set<Issue>, 2>& seenDirectIssues;
// A strand may not include an offer that output the same issue more than once
/** A strand may not include an offer that output the same issue more
than once
*/
boost::container::flat_set<Issue>& seenBookOuts;
beast::Journal j;
beast::Journal j; ///< Journal for logging
/** StrandContext constructor. */
StrandContext (ReadView const& view_,
std::vector<std::unique_ptr<Step>> const& strand_,
// A strand may not include an inner node that
// replicates the source or destination.
AccountID strandSrc_,
AccountID strandDst_,
AccountID const& strandSrc_,
AccountID const& strandDst_,
Issue const& strandDeliver_,
boost::optional<Quality> const& limitQuality_,
bool isLast_,
bool ownerPaysTransferFee_,
std::array<boost::container::flat_set<Issue>, 2>& seenDirectIssues_,
boost::container::flat_set<Issue>& seenBookOuts_,
beast::Journal j);
bool offerCrossing_,
bool isDefaultPath_,
std::array<boost::container::flat_set<Issue>, 2>&
seenDirectIssues_, ///< For detecting currency loops
boost::container::flat_set<Issue>& seenBookOuts_, ///< For detecting book loops
beast::Journal j_); ///< Journal for logging
};
/// @cond INTERNAL
namespace test {
// Needed for testing
bool directStepEqual (Step const& step,
@@ -457,6 +545,7 @@ make_XRPEndpointStep (
template<class InAmt, class OutAmt>
bool
isDirectXrpToXrp(Strand const& strand);
/// @endcond
} // ripple

View File

@@ -39,15 +39,17 @@
namespace ripple {
/** Result of flow() execution of a single Strand. */
template<class TInAmt, class TOutAmt>
struct StrandResult
{
TER ter = temUNKNOWN;
TInAmt in = beast::zero;
TOutAmt out = beast::zero;
boost::optional<PaymentSandbox> sandbox;
boost::container::flat_set<uint256> ofrsToRm; // offers to remove
TER ter = temUNKNOWN; ///< Result code
TInAmt in = beast::zero; ///< Currency amount in
TOutAmt out = beast::zero; ///< Currency amount out
boost::optional<PaymentSandbox> sandbox; ///< Resulting Sandbox state
boost::container::flat_set<uint256> ofrsToRm; ///< Offers to remove
/** Strand result constructor */
StrandResult () = default;
StrandResult (TInAmt const& in_,
@@ -68,7 +70,7 @@ struct StrandResult
}
};
/*
/**
Request `out` amount from a strand
@param baseView Trust lines and balances
@@ -231,6 +233,7 @@ flow (
}
}
/// @cond INTERNAL
template<class TInAmt, class TOutAmt>
struct FlowResult
{
@@ -271,7 +274,9 @@ struct FlowResult
{
}
};
/// @endcond
/// @cond INTERNAL
/* Track the non-dry strands
flow will search the non-dry strands (stored in `cur_`) for the best
@@ -337,8 +342,26 @@ public:
return cur_.size ();
}
};
/// @endcond
/*
/// @cond INTERNAL
boost::optional<Quality>
qualityUpperBound(ReadView const& v, Strand const& strand)
{
Quality q{STAmount::uRateOne};
bool redeems = false;
for(auto const& step : strand)
{
if (auto const stepQ = step->qualityUpperBound(v, redeems))
q = composed_quality(q, *stepQ);
else
return boost::none;
}
return q;
};
/// @endcond
/**
Request `out` amount from a collection of strands
Attempt to fullfill the payment by using liquidity from the strands in order
@@ -348,8 +371,12 @@ public:
@param strands Each strand contains the steps of accounts to ripple through
and offer books to use
@param outReq Amount of output requested from the strand
@param flowParams Constraints and options on the payment
@param logs Logs to write journal messages to
@param partialPayment If true allow less than the full payment
@param offerCrossing If true offer crossing, not handling a standard payment
@param limitQuality If present, the minimum quality for any strand taken
@param sendMaxST If present, the maximum STAmount to send
@param j Journal to write journal messages to
@param flowDebugInfo If pointer is non-null, write flow debug info here
@return Actual amount in and out from the strands, errors, and payment sandbox
*/
template <class TInAmt, class TOutAmt>
@@ -357,15 +384,13 @@ FlowResult<TInAmt, TOutAmt>
flow (PaymentSandbox const& baseView,
std::vector<Strand> const& strands,
TOutAmt const& outReq,
bool defaultPaths,
bool partialPayment,
bool offerCrossing,
boost::optional<Quality> const& limitQuality,
boost::optional<STAmount> const& sendMaxST,
beast::Journal j,
path::detail::FlowDebugInfo* flowDebugInfo=nullptr)
{
using Result = FlowResult<TInAmt, TOutAmt>;
// Used to track the strand that offers the best quality (output/input ratio)
struct BestStrand
{
@@ -392,16 +417,19 @@ flow (PaymentSandbox const& baseView,
std::size_t const maxTries = 1000;
std::size_t curTry = 0;
auto const sendMax = [&sendMaxST]()->boost::optional<TInAmt>
{
if (sendMaxST && *sendMaxST >= beast::zero)
{
return toAmount<TInAmt> (*sendMaxST);
}
return boost::none;
}();
// There is a bug in gcc that incorrectly warns about using uninitialized
// values if `remainingIn` is initialized through a copy constructor. We can
// get similar warnings for `sendMax` if it is initialized in the most
// natural way. Using `make_optional`, allows us to work around this bug.
TInAmt const sendMaxInit =
sendMaxST ? toAmount<TInAmt>(*sendMaxST) : TInAmt{beast::zero};
boost::optional<TInAmt> const sendMax =
boost::make_optional(sendMaxST && sendMaxInit >= beast::zero, sendMaxInit);
boost::optional<TInAmt> remainingIn =
boost::make_optional(!!sendMax, sendMaxInit);
// boost::optional<TInAmt> remainingIn{sendMax};
TOutAmt remainingOut (outReq);
boost::optional<TInAmt> remainingIn (sendMax);
PaymentSandbox sb (&baseView);
@@ -444,6 +472,12 @@ flow (PaymentSandbox const& baseView,
if (flowDebugInfo) flowDebugInfo->newLiquidityPass();
for (auto strand : activeStrands)
{
if (offerCrossing && limitQuality)
{
auto const strandQ = qualityUpperBound(sb, *strand);
if (!strandQ || *strandQ < *limitQuality)
continue;
}
auto f = flow<TInAmt, TOutAmt> (
sb, *strand, remainingIn, remainingOut, j);
@@ -543,15 +577,29 @@ flow (PaymentSandbox const& baseView,
}
if (!partialPayment)
{
return {tecPATH_PARTIAL, actualIn, actualOut, std::move(ofrsToRmOnFail)};
// If we're offerCrossing a !partialPayment, then we're
// handling tfFillOrKill. That case is handled below; not here.
if (!offerCrossing)
return {tecPATH_PARTIAL,
actualIn, actualOut, std::move(ofrsToRmOnFail)};
}
else if (actualOut == beast::zero)
{
return {tecPATH_DRY, std::move(ofrsToRmOnFail)};
}
}
if (offerCrossing && !partialPayment)
{
// If we're offer crossing and partialPayment is *not* true, then
// we're handling a FillOrKill offer. In this case remainingIn must
// be zero (all funds must be consumed) or else we kill the offer.
assert (remainingIn);
if (remainingIn && *remainingIn != zero)
return {tecPATH_PARTIAL,
actualIn, actualOut, std::move(ofrsToRmOnFail)};
}
return Result (actualIn, actualOut, std::move (sb), std::move(ofrsToRmOnFail));
return {actualIn, actualOut, std::move (sb), std::move(ofrsToRmOnFail)};
}
} // ripple

View File

@@ -35,11 +35,13 @@
namespace ripple {
class XRPEndpointStep : public StepImp<XRPAmount, XRPAmount, XRPEndpointStep>
template <class TDerived>
class XRPEndpointStep : public StepImp<
XRPAmount, XRPAmount, XRPEndpointStep<TDerived>>
{
private:
private:
AccountID acc_;
bool isLast_;
bool const isLast_;
beast::Journal j_;
// Since this step will always be an endpoint in a strand
@@ -54,14 +56,14 @@ class XRPEndpointStep : public StepImp<XRPAmount, XRPAmount, XRPEndpointStep>
return boost::none;
return EitherAmount (*cache_);
}
public:
public:
XRPEndpointStep (
AccountID const& acc,
bool isLast,
beast::Journal j)
:acc_(acc)
, isLast_(isLast)
, j_ (j) {}
StrandContext const& ctx,
AccountID const& acc)
: acc_(acc)
, isLast_(ctx.isLast)
, j_ (ctx.j) {}
AccountID const& acc () const
{
@@ -88,6 +90,9 @@ class XRPEndpointStep : public StepImp<XRPAmount, XRPAmount, XRPEndpointStep>
return cached ();
}
boost::optional<Quality>
qualityUpperBound(ReadView const& v, bool& redeems) const override;
std::pair<XRPAmount, XRPAmount>
revImp (
PaymentSandbox& sb,
@@ -111,10 +116,31 @@ class XRPEndpointStep : public StepImp<XRPAmount, XRPAmount, XRPEndpointStep>
// Check for errors and violations of frozen constraints.
TER check (StrandContext const& ctx) const;
private:
friend bool operator==(XRPEndpointStep const& lhs, XRPEndpointStep const& rhs);
protected:
XRPAmount
xrpLiquidImpl (ReadView& sb, std::int32_t reserveReduction) const
{
return ripple::xrpLiquid (sb, acc_, reserveReduction, j_);
}
friend bool operator!=(XRPEndpointStep const& lhs, XRPEndpointStep const& rhs)
std::string logStringImpl (char const* name) const
{
std::ostringstream ostr;
ostr <<
name << ": " <<
"\nAcc: " << acc_;
return ostr.str ();
}
private:
template <class P>
friend bool operator==(
XRPEndpointStep<P> const& lhs,
XRPEndpointStep<P> const& rhs);
friend bool operator!=(
XRPEndpointStep const& lhs,
XRPEndpointStep const& rhs)
{
return ! (lhs == rhs);
}
@@ -127,39 +153,108 @@ private:
}
return false;
}
};
//------------------------------------------------------------------------------
// Flow is used in two different circumstances for transferring funds:
// o Payments, and
// o Offer crossing.
// The rules for handling funds in these two cases are almost, but not
// quite, the same.
// Payment XRPEndpointStep class (not offer crossing).
class XRPEndpointPaymentStep : public XRPEndpointStep<XRPEndpointPaymentStep>
{
public:
using XRPEndpointStep<XRPEndpointPaymentStep>::XRPEndpointStep;
XRPAmount
xrpLiquid (ReadView& sb) const
{
return xrpLiquidImpl (sb, 0);;
}
std::string logString () const override
{
std::ostringstream ostr;
ostr <<
"XRPEndpointStep: " <<
"\nAcc: " << acc_;
return ostr.str ();
return logStringImpl ("XRPEndpointPaymentStep");
}
};
inline bool operator==(XRPEndpointStep const& lhs, XRPEndpointStep const& rhs)
// Offer crossing XRPEndpointStep class (not a payment).
class XRPEndpointOfferCrossingStep :
public XRPEndpointStep<XRPEndpointOfferCrossingStep>
{
private:
// For historical reasons, offer crossing is allowed to dig further
// into the XRP reserve than an ordinary payment. (I believe it's
// because the trust line was created after the XRP was removed.)
// Return how much the reserve should be reduced.
//
// Note that reduced reserve only happens if the trust line does not
// currently exist.
static std::int32_t computeReserveReduction (
StrandContext const& ctx, AccountID const& acc)
{
if (ctx.isFirst &&
!ctx.view.read (keylet::line (acc, ctx.strandDeliver)))
return -1;
return 0;
}
public:
XRPEndpointOfferCrossingStep (
StrandContext const& ctx, AccountID const& acc)
: XRPEndpointStep<XRPEndpointOfferCrossingStep> (ctx, acc)
, reserveReduction_ (computeReserveReduction (ctx, acc))
{
}
XRPAmount
xrpLiquid (ReadView& sb) const
{
return xrpLiquidImpl (sb, reserveReduction_);
}
std::string logString () const override
{
return logStringImpl ("XRPEndpointOfferCrossingStep");
}
private:
std::int32_t const reserveReduction_;
};
//------------------------------------------------------------------------------
template <class TDerived>
inline bool operator==(XRPEndpointStep<TDerived> const& lhs,
XRPEndpointStep<TDerived> const& rhs)
{
return lhs.acc_ == rhs.acc_ && lhs.isLast_ == rhs.isLast_;
}
static
XRPAmount
xrpLiquid (ReadView& sb, AccountID const& src)
template <class TDerived>
boost::optional<Quality>
XRPEndpointStep<TDerived>::qualityUpperBound(
ReadView const& v, bool& redeems) const
{
return accountHolds(
sb, src, xrpCurrency(), xrpAccount(), fhIGNORE_FREEZE, {}).xrp();
redeems = this->redeems(v, true);
return Quality{STAmount::uRateOne};
}
template <class TDerived>
std::pair<XRPAmount, XRPAmount>
XRPEndpointStep::revImp (
XRPEndpointStep<TDerived>::revImp (
PaymentSandbox& sb,
ApplyView& afView,
boost::container::flat_set<uint256>& ofrsToRm,
XRPAmount const& out)
{
auto const balance = xrpLiquid (sb, acc_);
auto const balance = static_cast<TDerived const*>(this)->xrpLiquid (sb);
auto const result = isLast_ ? out : std::min (balance, out);
auto& sender = isLast_ ? xrpAccount() : acc_;
@@ -172,15 +267,17 @@ XRPEndpointStep::revImp (
return {result, result};
}
template <class TDerived>
std::pair<XRPAmount, XRPAmount>
XRPEndpointStep::fwdImp (
XRPEndpointStep<TDerived>::fwdImp (
PaymentSandbox& sb,
ApplyView& afView,
boost::container::flat_set<uint256>& ofrsToRm,
XRPAmount const& in)
{
assert (cache_);
auto const balance = xrpLiquid (sb, acc_);
auto const balance = static_cast<TDerived const*>(this)->xrpLiquid (sb);
auto const result = isLast_ ? in : std::min (balance, in);
auto& sender = isLast_ ? xrpAccount() : acc_;
@@ -193,8 +290,9 @@ XRPEndpointStep::fwdImp (
return {result, result};
}
template <class TDerived>
std::pair<bool, EitherAmount>
XRPEndpointStep::validFwd (
XRPEndpointStep<TDerived>::validFwd (
PaymentSandbox& sb,
ApplyView& afView,
EitherAmount const& in)
@@ -208,7 +306,7 @@ XRPEndpointStep::validFwd (
assert (in.native);
auto const& xrpIn = in.xrp;
auto const balance = xrpLiquid (sb, acc_);
auto const balance = static_cast<TDerived const*>(this)->xrpLiquid (sb);
if (!isLast_ && balance < xrpIn)
{
@@ -227,8 +325,9 @@ XRPEndpointStep::validFwd (
return {true, in};
}
template <class TDerived>
TER
XRPEndpointStep::check (StrandContext const& ctx) const
XRPEndpointStep<TDerived>::check (StrandContext const& ctx) const
{
if (!acc_)
{
@@ -239,7 +338,7 @@ XRPEndpointStep::check (StrandContext const& ctx) const
auto sleAcc = ctx.view.read (keylet::account (acc_));
if (!sleAcc)
{
JLOG (j_.warn()) << "XRPEndpointStep: can't send or receive XRPs from "
JLOG (j_.warn()) << "XRPEndpointStep: can't send or receive XRP from "
"non-existent account: "
<< acc_;
return terNO_ACCOUNT;
@@ -266,7 +365,8 @@ namespace test
// Needed for testing
bool xrpEndpointStepEqual (Step const& step, AccountID const& acc)
{
if (auto xs = dynamic_cast<XRPEndpointStep const*> (&step))
if (auto xs =
dynamic_cast<XRPEndpointStep<XRPEndpointPaymentStep> const*> (&step))
{
return xs->acc () == acc;
}
@@ -281,10 +381,25 @@ make_XRPEndpointStep (
StrandContext const& ctx,
AccountID const& acc)
{
auto r = std::make_unique<XRPEndpointStep> (acc, ctx.isLast, ctx.j);
auto ter = r->check (ctx);
TER ter = tefINTERNAL;
std::unique_ptr<Step> r;
if (ctx.offerCrossing)
{
auto offerCrossingStep =
std::make_unique<XRPEndpointOfferCrossingStep> (ctx, acc);
ter = offerCrossingStep->check (ctx);
r = std::move (offerCrossingStep);
}
else // payment
{
auto paymentStep =
std::make_unique<XRPEndpointPaymentStep> (ctx, acc);
ter = paymentStep->check (ctx);
r = std::move (paymentStep);
}
if (ter != tesSUCCESS)
return {ter, nullptr};
return {tesSUCCESS, std::move (r)};
}

View File

@@ -19,18 +19,14 @@
#include <BeastConfig.h>
#include <ripple/app/tx/impl/CreateOffer.h>
#include <ripple/app/ledger/Ledger.h>
#include <ripple/app/ledger/OrderBookDB.h>
#include <ripple/basics/contract.h>
#include <ripple/app/paths/Flow.h>
#include <ripple/ledger/CashDiff.h>
#include <ripple/ledger/PaymentSandbox.h>
#include <ripple/protocol/Feature.h>
#include <ripple/protocol/st.h>
#include <ripple/protocol/Quality.h>
#include <ripple/basics/Log.h>
#include <ripple/json/to_string.h>
#include <ripple/ledger/Sandbox.h>
#include <ripple/beast/utility/Journal.h>
#include <ripple/beast/utility/WrappedSink.h>
#include <memory>
#include <stdexcept>
namespace ripple {
@@ -331,11 +327,11 @@ CreateOffer::bridged_cross (
ApplyView& view_cancel,
NetClock::time_point const when)
{
auto const& taker_amount = taker.original_offer ();
auto const& takerAmount = taker.original_offer ();
assert (!isXRP (taker_amount.in) && !isXRP (taker_amount.out));
assert (!isXRP (takerAmount.in) && !isXRP (takerAmount.out));
if (isXRP (taker_amount.in) || isXRP (taker_amount.out))
if (isXRP (takerAmount.in) || isXRP (takerAmount.out))
Throw<std::logic_error> ("Bridging with XRP and an endpoint.");
OfferStream offers_direct (view, view_cancel,
@@ -517,6 +513,7 @@ CreateOffer::direct_cross (
stream << " offer: " << offer;
stream << " in: " << offer.amount ().in;
stream << " out: " << offer.amount ().out;
stream << "quality: " << offer.quality();
stream << " owner: " << offer.owner ();
stream << " funds: " << accountFunds(view,
offer.owner (), offer.amount ().out, fhIGNORE_FREEZE,
@@ -589,16 +586,16 @@ CreateOffer::step_account (OfferStream& stream, Taker const& taker)
// already on the books. Return the status and the amount of
// the offer to left unfilled.
std::pair<TER, Amounts>
CreateOffer::cross (
ApplyView& view,
ApplyView& cancel_view,
Amounts const& taker_amount)
CreateOffer::takerCross (
PaymentSandbox& psb,
PaymentSandbox& psbCancel,
Amounts const& takerAmount)
{
NetClock::time_point const when{ctx_.view().parentCloseTime()};
beast::WrappedSink takerSink (j_, "Taker ");
Taker taker (cross_type_, view, account_, taker_amount,
Taker taker (cross_type_, psb, account_, takerAmount,
ctx_.tx.getFlags(), beast::Journal (takerSink));
// If the taker is unfunded before we begin crossing
@@ -612,15 +609,15 @@ CreateOffer::cross (
{
JLOG (j_.debug()) <<
"Not crossing: taker is unfunded.";
return { tecUNFUNDED_OFFER, taker_amount };
return { tecUNFUNDED_OFFER, takerAmount };
}
try
{
if (cross_type_ == CrossType::IouToIou)
return bridged_cross (taker, view, cancel_view, when);
return bridged_cross (taker, psb, psbCancel, when);
return direct_cross (taker, view, cancel_view, when);
return direct_cross (taker, psb, psbCancel, when);
}
catch (std::exception const& e)
{
@@ -630,6 +627,392 @@ CreateOffer::cross (
}
}
std::pair<TER, Amounts>
CreateOffer::flowCross (
PaymentSandbox& psb,
PaymentSandbox& psbCancel,
Amounts const& takerAmount)
{
try
{
// If the taker is unfunded before we begin crossing there's nothing
// to do - just return an error.
//
// We check this in preclaim, but when selling XRP charged fees can
// cause a user's available balance to go to 0 (by causing it to dip
// below the reserve) so we check this case again.
STAmount const inStartBalance = accountFunds (
psb, account_, takerAmount.in, fhZERO_IF_FROZEN, j_);
if (inStartBalance <= zero)
{
// The account balance can't cover even part of the offer.
JLOG (j_.debug()) <<
"Not crossing: taker is unfunded.";
return { tecUNFUNDED_OFFER, takerAmount };
}
// If the gateway has a transfer rate, accommodate that. The
// gateway takes its cut without any special consent from the
// offer taker. Set sendMax to allow for the gateway's cut.
Rate gatewayXferRate {QUALITY_ONE};
STAmount sendMax = takerAmount.in;
if (! sendMax.native() && (account_ != sendMax.getIssuer()))
{
gatewayXferRate = transferRate (psb, sendMax.getIssuer());
if (gatewayXferRate.value != QUALITY_ONE)
{
sendMax = multiplyRound (takerAmount.in,
gatewayXferRate, takerAmount.in.issue(), true);
}
}
// Payment flow code compares quality after the transfer rate is
// included. Since transfer rate is incorporated compute threshold.
Quality threshold { takerAmount.out, sendMax };
// If we're creating a passive offer adjust the threshold so we only
// cross offers that have a better quality than this one.
std::uint32_t const txFlags = ctx_.tx.getFlags();
if (txFlags & tfPassive)
++threshold;
// Don't send more than our balance.
if (sendMax > inStartBalance)
sendMax = inStartBalance;
// Always invoke flow() with the default path. However if neither
// of the takerAmount currencies are XRP then we cross through an
// additional path with XRP as the intermediate between two books.
// This second path we have to build ourselves.
STPathSet paths;
if (!takerAmount.in.native() & !takerAmount.out.native())
{
STPath path;
path.emplace_back (boost::none, xrpCurrency(), boost::none);
paths.emplace_back (std::move(path));
}
// Special handling for the tfSell flag.
STAmount deliver = takerAmount.out;
if (txFlags & tfSell)
{
// We are selling, so we will accept *more* than the offer
// specified. Since we don't know how much they might offer,
// we allow delivery of the largest possible amount.
if (deliver.native())
deliver = STAmount { STAmount::cMaxNative };
else
// We can't use the maximum possible currency here because
// there might be a gateway transfer rate to account for.
// Since the transfer rate cannot exceed 200%, we use 1/2
// maxValue for our limit.
deliver = STAmount { takerAmount.out.issue(),
STAmount::cMaxValue / 2, STAmount::cMaxOffset };
}
// Call the payment engine's flow() to do the actual work.
auto const result = flow (psb, deliver, account_, account_,
paths,
true, // default path
! (txFlags & tfFillOrKill), // partial payment
true, // owner pays transfer fee
true, // offer crossing
threshold,
sendMax, j_);
// If stale offers were found remove them.
for (auto const& toRemove : result.removableOffers)
{
if (auto otr = psb.peek (keylet::offer (toRemove)))
offerDelete (psb, otr, j_);
if (auto otr = psbCancel.peek (keylet::offer (toRemove)))
offerDelete (psbCancel, otr, j_);
}
// Determine the size of the final offer after crossing.
auto afterCross = takerAmount; // If !tesSUCCESS offer unchanged
if (isTesSuccess (result.result()))
{
STAmount const takerInBalance = accountFunds (
psb, account_, takerAmount.in, fhZERO_IF_FROZEN, j_);
if (takerInBalance <= zero)
{
// If offer crossing exhausted the account's funds don't
// create the offer.
afterCross.in.clear();
afterCross.out.clear();
}
else
{
STAmount const rate {
Quality{takerAmount.out, takerAmount.in}.rate() };
if (txFlags & tfSell)
{
// If selling then scale the new out amount based on how
// much we sold during crossing. This preserves the offer
// Quality,
// Reduce the offer that is placed by the crossed amount.
// Note that we must ignore the portion of the
// actualAmountIn that may have been consumed by a
// gateway's transfer rate.
STAmount nonGatewayAmountIn = result.actualAmountIn;
if (gatewayXferRate.value != QUALITY_ONE)
nonGatewayAmountIn = divideRound (result.actualAmountIn,
gatewayXferRate, takerAmount.in.issue(), true);
afterCross.in -= nonGatewayAmountIn;
// It's possible that the divRound will cause our subtract
// to go slightly negative. So limit afterCross.in to zero.
if (afterCross.in < zero)
// We should verify that the difference *is* small, but
// what is a good threshold to check?
afterCross.in.clear();
afterCross.out = divRound (afterCross.in,
rate, takerAmount.out.issue(), true);
}
else
{
// If not selling, we scale the input based on the
// remaining output. This too preserves the offer
// Quality.
afterCross.out -= result.actualAmountOut;
assert (afterCross.out >= zero);
if (afterCross.out < zero)
afterCross.out.clear();
afterCross.in = mulRound (afterCross.out,
rate, takerAmount.in.issue(), true);
}
}
}
// Return how much of the offer is left.
return { tesSUCCESS, afterCross };
}
catch (std::exception const& e)
{
JLOG (j_.error()) <<
"Exception during offer crossing: " << e.what ();
}
return { tecINTERNAL, takerAmount };
}
enum class SBoxCmp
{
same,
dustDiff,
offerDelDiff,
diff
};
static std::string to_string (SBoxCmp c)
{
switch (c)
{
case SBoxCmp::same:
return "same";
case SBoxCmp::dustDiff:
return "dust diffs";
case SBoxCmp::offerDelDiff:
return "offer del diffs";
case SBoxCmp::diff:
return "different";
}
return {};
}
static SBoxCmp compareSandboxes (char const* name, ApplyContext const& ctx,
PaymentSandbox const& psbTaker, PaymentSandbox const& psbFlow,
beast::Journal j)
{
SBoxCmp c = SBoxCmp::same;
CashDiff diff = cashFlowDiff (
CashFilter::treatZeroOfferAsDeletion, psbTaker,
CashFilter::none, psbFlow);
if (diff.hasDiff())
{
using namespace beast::severities;
c = SBoxCmp::dustDiff;
Severity s = kInfo;
std::string diffDesc = ", but only dust.";
diff.rmDust();
if (diff.hasDiff())
{
// From here on we want to note the transaction ID of differences.
std::stringstream txIdSs;
txIdSs << ". tx: " << ctx.tx.getTransactionID();
auto txID = txIdSs.str();
// Sometimes one version deletes offers that the other doesn't
// delete. That's okay, but keep track of it.
c = SBoxCmp::offerDelDiff;
s = kWarning;
int sides = diff.rmLhsDeletedOffers() ? 1 : 0;
sides |= diff.rmRhsDeletedOffers() ? 2 : 0;
if (!diff.hasDiff())
{
char const* t = "";
switch (sides)
{
case 1: t = "; Taker deleted more offers"; break;
case 2: t = "; Flow deleted more offers"; break;
case 3: t = "; Taker and Flow deleted different offers"; break;
default: break;
}
diffDesc = std::string(t) + txID;
}
else
{
// A difference without a broad classification...
c = SBoxCmp::diff;
std::stringstream ss;
ss << "; common entries: " << diff.commonCount()
<< "; Taker unique: " << diff.lhsOnlyCount()
<< "; Flow unique: " << diff.rhsOnlyCount() << txID;
diffDesc = ss.str();
}
}
j.stream (s) << "FlowCross: " << name << " different" << diffDesc;
}
return c;
}
std::pair<TER, Amounts>
CreateOffer::cross (
PaymentSandbox& psb,
PaymentSandbox& psbCancel,
Amounts const& takerAmount)
{
// There are features for Flow offer crossing and for comparing results
// between Taker and Flow offer crossing. Turn those into bools.
bool const useFlowCross { psb.rules().enabled (featureFlowCross) };
bool const doCompare { psb.rules().enabled (featureCompareTakerFlowCross) };
PaymentSandbox psbTaker { &psb };
PaymentSandbox psbCancelTaker { &psbCancel };
auto const takerR = (!useFlowCross || doCompare)
? takerCross (psbTaker, psbCancelTaker, takerAmount)
: std::make_pair (tecINTERNAL, takerAmount);
PaymentSandbox psbFlow { &psb };
PaymentSandbox psbCancelFlow { &psbCancel };
auto const flowR = (useFlowCross || doCompare)
? flowCross (psbFlow, psbCancelFlow, takerAmount)
: std::make_pair (tecINTERNAL, takerAmount);
if (doCompare)
{
SBoxCmp c = SBoxCmp::same;
if (takerR.first != flowR.first)
{
c = SBoxCmp::diff;
j_.warn() << "FlowCross: Offer cross tec codes different. tx: "
<< ctx_.tx.getTransactionID();
}
else if ((takerR.second.in == zero && flowR.second.in == zero) ||
(takerR.second.out == zero && flowR.second.out == zero))
{
c = compareSandboxes ("Both Taker and Flow fully crossed",
ctx_, psbTaker, psbFlow, j_);
}
else if (takerR.second.in == zero && takerR.second.out == zero)
{
char const * crossType = "Taker fully crossed, Flow partially crossed";
if (flowR.second.in == takerAmount.in &&
flowR.second.out == takerAmount.out)
crossType = "Taker fully crossed, Flow not crossed";
c = compareSandboxes (crossType, ctx_, psbTaker, psbFlow, j_);
}
else if (flowR.second.in == zero && flowR.second.out == zero)
{
char const * crossType =
"Taker partially crossed, Flow fully crossed";
if (takerR.second.in == takerAmount.in &&
takerR.second.out == takerAmount.out)
crossType = "Taker not crossed, Flow fully crossed";
c = compareSandboxes (crossType, ctx_, psbTaker, psbFlow, j_);
}
else if (ctx_.tx.getFlags() & tfFillOrKill)
{
c = compareSandboxes (
"FillOrKill offer", ctx_, psbCancelTaker, psbCancelFlow, j_);
}
else if (takerR.second.in == takerAmount.in &&
flowR.second.in == takerAmount.in &&
takerR.second.out == takerAmount.out &&
flowR.second.out == takerAmount.out)
{
char const * crossType = "Neither Taker nor Flow crossed";
c = compareSandboxes (crossType, ctx_, psbTaker, psbFlow, j_);
}
else if (takerR.second.in == takerAmount.in &&
takerR.second.out == takerAmount.out)
{
char const * crossType = "Taker not crossed, Flow partially crossed";
c = compareSandboxes (crossType, ctx_, psbTaker, psbFlow, j_);
}
else if (flowR.second.in == takerAmount.in &&
flowR.second.out == takerAmount.out)
{
char const * crossType = "Taker partially crossed, Flow not crossed";
c = compareSandboxes (crossType, ctx_, psbTaker, psbFlow, j_);
}
else
{
c = compareSandboxes (
"Partial cross offer", ctx_, psbTaker, psbFlow, j_);
// If we've gotten this far then the returned amounts matter.
if (c <= SBoxCmp::dustDiff && takerR.second != flowR.second)
{
c = SBoxCmp::dustDiff;
using namespace beast::severities;
Severity s = kInfo;
std::string onlyDust = ", but only dust.";
if (! diffIsDust (takerR.second.in, flowR.second.in) ||
(! diffIsDust (takerR.second.out, flowR.second.out)))
{
char const* outSame = "";
if (takerR.second.out == flowR.second.out)
outSame = " but outs same";
c = SBoxCmp::diff;
s = kWarning;
std::stringstream ss;
ss << outSame
<< ". Taker in: " << takerR.second.in.getText()
<< "; Taker out: " << takerR.second.out.getText()
<< "; Flow in: " << flowR.second.in.getText()
<< "; Flow out: " << flowR.second.out.getText()
<< ". tx: " << ctx_.tx.getTransactionID();
onlyDust = ss.str();
}
j_.stream (s) << "FlowCross: Partial cross amounts different"
<< onlyDust;
}
}
j_.error() << "FlowCross cmp result: " << to_string (c);
}
// Return one result or the other based on amendment.
if (useFlowCross)
{
psbFlow.apply (psb);
psbCancelFlow.apply (psbCancel);
return flowR;
}
psbTaker.apply (psb);
psbCancelTaker.apply (psbCancel);
return takerR;
}
std::string
CreateOffer::format_amount (STAmount const& amount)
{
@@ -656,7 +1039,7 @@ CreateOffer::preCompute()
}
std::pair<TER, bool>
CreateOffer::applyGuts (ApplyView& view, ApplyView& view_cancel)
CreateOffer::applyGuts (PaymentSandbox& psb, PaymentSandbox& psbCancel)
{
std::uint32_t const uTxFlags = ctx_.tx.getFlags ();
@@ -673,9 +1056,6 @@ CreateOffer::applyGuts (ApplyView& view, ApplyView& view_cancel)
// FIXME understand why we use SequenceNext instead of current transaction
// sequence to determine the transaction. Why is the offer sequence
// number insufficient?
auto const sleCreator = view.peek (keylet::account(account_));
auto const uSequence = ctx_.tx.getSequence ();
// This is the original rate of the offer, and is the rate at which
@@ -690,7 +1070,7 @@ CreateOffer::applyGuts (ApplyView& view, ApplyView& view_cancel)
// Process a cancellation request that's passed along with an offer.
if (cancelSequence)
{
auto const sleCancel = view.peek(
auto const sleCancel = psb.peek(
keylet::offer(account_, *cancelSequence));
// It's not an error to not find the offer to cancel: it might have
@@ -699,7 +1079,7 @@ CreateOffer::applyGuts (ApplyView& view, ApplyView& view_cancel)
if (sleCancel)
{
JLOG(j_.debug()) << "Create cancels order " << *cancelSequence;
result = offerDelete (view, sleCancel, viewJ);
result = offerDelete (psb, sleCancel, viewJ);
}
}
@@ -731,7 +1111,7 @@ CreateOffer::applyGuts (ApplyView& view, ApplyView& view_cancel)
if (!isXRP (uPaysIssuerID))
{
auto const sle =
view.read(keylet::account(uPaysIssuerID));
psb.read(keylet::account(uPaysIssuerID));
if (sle && sle->isFieldPresent (sfTickSize))
uTickSize = std::min (uTickSize,
(*sle)[sfTickSize]);
@@ -739,7 +1119,7 @@ CreateOffer::applyGuts (ApplyView& view, ApplyView& view_cancel)
if (!isXRP (uGetsIssuerID))
{
auto const sle =
view.read(keylet::account(uGetsIssuerID));
psb.read(keylet::account(uGetsIssuerID));
if (sle && sle->isFieldPresent (sfTickSize))
uTickSize = std::min (uTickSize,
(*sle)[sfTickSize]);
@@ -776,7 +1156,7 @@ CreateOffer::applyGuts (ApplyView& view, ApplyView& view_cancel)
}
// We reverse pays and gets because during crossing we are taking.
Amounts const taker_amount (saTakerGets, saTakerPays);
Amounts const takerAmount (saTakerGets, saTakerPays);
// The amount of the offer that is unfilled after crossing has been
// performed. It may be equal to the original amount (didn't cross),
@@ -784,19 +1164,20 @@ CreateOffer::applyGuts (ApplyView& view, ApplyView& view_cancel)
Amounts place_offer;
JLOG(j_.debug()) << "Attempting cross: " <<
to_string (taker_amount.in.issue ()) << " -> " <<
to_string (taker_amount.out.issue ());
to_string (takerAmount.in.issue ()) << " -> " <<
to_string (takerAmount.out.issue ());
if (auto stream = j_.trace())
{
stream << " mode: " <<
(bPassive ? "passive " : "") <<
(bSell ? "sell" : "buy");
stream <<" in: " << format_amount (taker_amount.in);
stream << " out: " << format_amount (taker_amount.out);
stream <<" in: " << format_amount (takerAmount.in);
stream << " out: " << format_amount (takerAmount.out);
}
std::tie(result, place_offer) = cross (view, view_cancel, taker_amount);
std::tie(result, place_offer) = cross (psb, psbCancel, takerAmount);
// We expect the implementation of cross to succeed
// or give a tec.
assert(result == tesSUCCESS || isTecClaim(result));
@@ -820,7 +1201,7 @@ CreateOffer::applyGuts (ApplyView& view, ApplyView& view_cancel)
assert (saTakerGets.issue () == place_offer.in.issue ());
assert (saTakerPays.issue () == place_offer.out.issue ());
if (taker_amount != place_offer)
if (takerAmount != place_offer)
crossed = true;
// The offer that we need to place after offer crossing should
@@ -877,6 +1258,7 @@ CreateOffer::applyGuts (ApplyView& view, ApplyView& view_cancel)
return { tesSUCCESS, true };
}
auto const sleCreator = psb.peek (keylet::account(account_));
{
XRPAmount reserve = ctx_.view().fees().accountReserve(
sleCreator->getFieldU32 (sfOwnerCount) + 1);
@@ -905,14 +1287,14 @@ CreateOffer::applyGuts (ApplyView& view, ApplyView& view_cancel)
std::uint64_t uOwnerNode;
// Add offer to owner's directory.
std::tie(result, std::ignore) = dirAdd(view, uOwnerNode,
std::tie(result, std::ignore) = dirAdd(psb, uOwnerNode,
keylet::ownerDir (account_), offer_index,
describeOwnerDir (account_), viewJ);
if (result == tesSUCCESS)
{
// Update owner count.
adjustOwnerCount(view, sleCreator, 1, viewJ);
adjustOwnerCount(psb, sleCreator, 1, viewJ);
JLOG (j_.trace()) <<
"adding to book: " << to_string (saTakerPays.issue ()) <<
@@ -926,7 +1308,7 @@ CreateOffer::applyGuts (ApplyView& view, ApplyView& view_cancel)
// before any crossing occured.
auto dir = keylet::quality (keylet::book (book), uRate);
std::tie(result, isNewBook) = dirAdd (view, uBookNode,
std::tie(result, isNewBook) = dirAdd (psb, uBookNode,
dir, offer_index, [&](SLE::ref sle)
{
sle->setFieldH160 (sfTakerPaysCurrency,
@@ -956,7 +1338,7 @@ CreateOffer::applyGuts (ApplyView& view, ApplyView& view_cancel)
sleOffer->setFlag (lsfPassive);
if (bSell)
sleOffer->setFlag (lsfSell);
view.insert(sleOffer);
psb.insert(sleOffer);
if (isNewBook)
ctx_.app.getOrderBookDB().addOrderBook(book);
@@ -977,18 +1359,18 @@ CreateOffer::doApply()
{
// This is the ledger view that we work against. Transactions are applied
// as we go on processing transactions.
Sandbox view (&ctx_.view());
PaymentSandbox psb (&ctx_.view());
// This is a ledger with just the fees paid and any unfunded or expired
// offers we encounter removed. It's used when handling Fill-or-Kill offers,
// if the order isn't going to be placed, to avoid wasting the work we did.
Sandbox viewCancel (&ctx_.view());
PaymentSandbox psbCancel (&ctx_.view());
auto const result = applyGuts(view, viewCancel);
auto const result = applyGuts(psb, psbCancel);
if (result.second)
view.apply(ctx_.rawView());
psb.apply(ctx_.rawView());
else
viewCancel.apply(ctx_.rawView());
psbCancel.apply(ctx_.rawView());
return result.first;
}

View File

@@ -20,55 +20,55 @@
#ifndef RIPPLE_TX_CREATEOFFER_H_INCLUDED
#define RIPPLE_TX_CREATEOFFER_H_INCLUDED
#include <ripple/app/ledger/Ledger.h>
#include <ripple/app/tx/impl/OfferStream.h>
#include <ripple/app/tx/impl/Taker.h>
#include <ripple/app/tx/impl/Transactor.h>
#include <ripple/basics/chrono.h>
#include <ripple/basics/Log.h>
#include <ripple/json/to_string.h>
#include <ripple/protocol/Quality.h>
#include <ripple/beast/utility/Journal.h>
#include <ripple/beast/utility/WrappedSink.h>
#include <memory>
#include <stdexcept>
#include <utility>
namespace ripple {
class PaymentSandbox;
/** Transactor specialized for creating offers in the ledger. */
class CreateOffer
: public Transactor
{
public:
/** Construct a Transactor subclass that creates an offer in the ledger. */
CreateOffer (ApplyContext& ctx)
: Transactor(ctx)
, stepCounter_ (1000, j_)
{
}
/** Override default behavior provided by Transactor base class. */
static
XRPAmount
calculateMaxSpend(STTx const& tx);
/** Enforce constraints beyond those of the Transactor base class. */
static
TER
preflight (PreflightContext const& ctx);
/** Enforce constraints beyond those of the Transactor base class. */
static
TER
preclaim(PreclaimContext const& ctx);
/** Gather information beyond what the Transactor base class gathers. */
void
preCompute() override;
std::pair<TER, bool>
applyGuts (ApplyView& view, ApplyView& view_cancel);
/** Precondition: fee collection is likely. Attempt to create the offer. */
TER
doApply() override;
private:
/** Determine if we are authorized to hold the asset we want to get */
std::pair<TER, bool>
applyGuts (PaymentSandbox& view, PaymentSandbox& view_cancel);
// Determine if we are authorized to hold the asset we want to get.
static
TER
checkAcceptAsset(ReadView const& view,
@@ -115,10 +115,27 @@ private:
//
// Charges fees on top to taker.
std::pair<TER, Amounts>
takerCross (
PaymentSandbox& psb,
PaymentSandbox& psbCancel,
Amounts const& takerAmount);
// Use the payment flow code to perform offer crossing.
std::pair<TER, Amounts>
flowCross (
PaymentSandbox& psb,
PaymentSandbox& psbCancel,
Amounts const& takerAmount);
// Temporary
// This is a central location that invokes both versions of cross
// so the results can be compared. Eventually this layer will be
// removed once flowCross is determined to be stable.
std::pair<TER, Amounts>
cross (
ApplyView& view,
ApplyView& cancel_view,
Amounts const& taker_amount);
PaymentSandbox& psb,
PaymentSandbox& psbCancel,
Amounts const& takerAmount);
static
std::string

View File

@@ -123,6 +123,11 @@ public:
return to_string (m_entry->key());
}
uint256 key () const
{
return m_entry->key();
}
Issue issueIn () const;
Issue issueOut () const;
};

View File

@@ -153,7 +153,7 @@ TOfferStreamBase<TIn, TOut>::step ()
{
JLOG(j_.trace()) <<
"Removing expired offer " << entry->key();
permRmOffer (entry);
permRmOffer (entry->key());
continue;
}
@@ -166,7 +166,7 @@ TOfferStreamBase<TIn, TOut>::step ()
{
JLOG(j_.warn()) <<
"Removing bad offer " << entry->key();
permRmOffer (entry);
permRmOffer (entry->key());
offer_ = TOffer<TIn, TOut>{};
continue;
}
@@ -187,7 +187,7 @@ TOfferStreamBase<TIn, TOut>::step ()
if (original_funds == *ownerFunds_)
{
permRmOffer (entry);
permRmOffer (entry->key());
JLOG(j_.trace()) <<
"Removing unfunded offer " << entry->key();
}
@@ -207,16 +207,16 @@ TOfferStreamBase<TIn, TOut>::step ()
}
void
OfferStream::permRmOffer (std::shared_ptr<SLE> const& sle)
OfferStream::permRmOffer (uint256 const& offerIndex)
{
offerDelete (cancelView_,
cancelView_.peek(keylet::offer(sle->key())), j_);
cancelView_.peek(keylet::offer(offerIndex)), j_);
}
template<class TIn, class TOut>
void FlowOfferStream<TIn, TOut>::permRmOffer (std::shared_ptr<SLE> const& sle)
void FlowOfferStream<TIn, TOut>::permRmOffer (uint256 const& offerIndex)
{
permToRemove_.insert (sle->key());
permToRemove_.insert (offerIndex);
}
template class FlowOfferStream<STAmount, STAmount>;

View File

@@ -84,7 +84,7 @@ protected:
virtual
void
permRmOffer (std::shared_ptr<SLE> const& sle) = 0;
permRmOffer (uint256 const& offerIndex) = 0;
public:
TOfferStreamBase (ApplyView& view, ApplyView& cancelView,
@@ -140,7 +140,7 @@ class OfferStream : public TOfferStreamBase<STAmount, STAmount>
{
protected:
void
permRmOffer (std::shared_ptr<SLE> const& sle) override;
permRmOffer (uint256 const& offerIndex) override;
public:
using TOfferStreamBase<STAmount, STAmount>::TOfferStreamBase;
};
@@ -167,13 +167,17 @@ class FlowOfferStream : public TOfferStreamBase<TIn, TOut>
{
private:
boost::container::flat_set<uint256> permToRemove_;
protected:
void
permRmOffer (std::shared_ptr<SLE> const& sle) override;
public:
using TOfferStreamBase<TIn, TOut>::TOfferStreamBase;
// The following interface allows offer crossing to permanently
// remove self crossed offers. The motivation is somewhat
// unintuitive. See the discussion in the comments for
// BookOfferCrossingStep::limitSelfCrossQuality().
void
permRmOffer (uint256 const& offerIndex) override;
boost::container::flat_set<uint256> const& permToRemove () const
{
return permToRemove_;

View File

@@ -0,0 +1,131 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2016 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#ifndef RIPPLE_LEDGER_CASHDIFF_H_INCLUDED
#define RIPPLE_LEDGER_CASHDIFF_H_INCLUDED
#include <ripple/protocol/STAmount.h>
#include <memory> // std::unique_ptr
namespace ripple {
class ReadView;
namespace detail {
class ApplyStateTable;
}
// Used by CashDiff to specify filters applied while processing differences.
// Entries are bit flags that can be ANDed and ORed.
enum class CashFilter : std::uint8_t
{
none = 0x0,
treatZeroOfferAsDeletion = 0x1
};
inline CashFilter operator| (CashFilter lhs, CashFilter rhs)
{
using ul_t = std::underlying_type<CashFilter>::type;
return static_cast<CashFilter>(
static_cast<ul_t>(lhs) | static_cast<ul_t>(rhs));
}
inline CashFilter operator& (CashFilter lhs, CashFilter rhs)
{
using ul_t = std::underlying_type<CashFilter>::type;
return static_cast<CashFilter>(
static_cast<ul_t>(lhs) & static_cast<ul_t>(rhs));
}
//------------------------------------------------------------------------------
// A class to identify differences between two ApplyStateTable instances
// for debugging.
class CashDiff
{
public:
CashDiff() = delete;
CashDiff (CashDiff const&) = delete;
CashDiff (CashDiff&& other);
CashDiff& operator= (CashDiff const&) = delete;
~CashDiff();
CashDiff (ReadView const& view,
CashFilter lhsFilter, detail::ApplyStateTable const& lhs,
CashFilter rhsFilter, detail::ApplyStateTable const& rhs);
// Returns the number of cases where lhs and rhs had the same entries
// (but not necessarily the same amounts)
std::size_t commonCount () const;
// Returns the number of entries that were present in rhs but not in lhs.
std::size_t rhsOnlyCount () const;
// Returns the number of entries that were present in lhs but not in rhs.
std::size_t lhsOnlyCount () const;
// Returns true is there are any differences to report.
bool hasDiff() const;
// Remove dust-sized differences. Returns true is dust was removed.
bool rmDust();
// Remove offer deletion differences from a given side. Returns true
// if any deleted offers were removed from the differences.
bool rmLhsDeletedOffers();
bool rmRhsDeletedOffers();
struct OfferAmounts
{
static std::size_t constexpr count_ = 2;
static std::size_t constexpr count() { return count_; }
STAmount amounts[count_];
STAmount const& takerPays() const { return amounts[0]; }
STAmount const& takerGets() const { return amounts[1]; }
STAmount const& operator[] (std::size_t i) const
{
assert (i < count());
return amounts[i];
}
friend bool operator< (OfferAmounts const& lhs, OfferAmounts const& rhs)
{
if (lhs[0] < rhs[0])
return true;
if (lhs[0] > rhs[0])
return false;
return lhs[1] < rhs[1];
}
};
private:
class Impl;
std::unique_ptr<Impl> impl_;
};
// Return true if the difference between two STAmounts is "small".
//
// If v1 and v2 have different issues, then their difference is never dust.
// If v1 < v2, smallness is computed as v1 / (v2 - v1).
// The e10 argument says at least how big that ratio must be. Default is 10^6.
// If both v1 and v2 are XRP, any difference of 2 or smaller is considered dust.
bool diffIsDust (STAmount const& v1, STAmount const& v2, std::uint8_t e10 = 6);
} // ripple
#endif

View File

@@ -74,6 +74,16 @@ accountFunds (ReadView const& view, AccountID const& id,
STAmount const& saDefault, FreezeHandling freezeHandling,
beast::Journal j);
// Return the account's liquid (not reserved) XRP. Generally prefer
// calling accountHolds() over this interface. However this interface
// allows the caller to temporarily adjust the owner count should that be
// necessary.
//
// @param ownerCountAdj positive to add to count, negative to reduce count.
XRPAmount
xrpLiquid (ReadView const& view, AccountID const& id,
std::int32_t ownerCountAdj, beast::Journal j);
/** Iterate all items in an account's owner directory. */
void
forEachItem (ReadView const& view, AccountID const& id,
@@ -186,7 +196,7 @@ bool areCompatible (uint256 const& validHash, LedgerIndex validIndex,
void
adjustOwnerCount (ApplyView& view,
std::shared_ptr<SLE> const& sle,
int amount, beast::Journal j);
std::int32_t amount, beast::Journal j);
// Return the first entry and advance uDirEntry.
// <-- true, if had a next entry.

View File

@@ -88,7 +88,7 @@ public:
Keylet const& k);
std::size_t
size ();
size () const;
void
visit (ReadView const& base,

View File

@@ -21,6 +21,7 @@
#define RIPPLE_LEDGER_APPLYVIEWBASE_H_INCLUDED
#include <ripple/ledger/ApplyView.h>
#include <ripple/ledger/CashDiff.h>
#include <ripple/ledger/OpenView.h>
#include <ripple/ledger/ReadView.h>
#include <ripple/ledger/detail/ApplyStateTable.h>
@@ -127,6 +128,11 @@ public:
rawDestroyXRP (
XRPAmount const& feeDrops) override;
friend
CashDiff cashFlowDiff (
CashFilter lhsFilter, ApplyViewBase const& lhs,
CashFilter rhsFilter, ApplyViewBase const& rhs);
protected:
ApplyFlags flags_;
ReadView const* base_;

View File

@@ -19,7 +19,6 @@
#include <BeastConfig.h>
#include <ripple/ledger/detail/ApplyStateTable.h>
#include <ripple/basics/contract.h>
#include <ripple/basics/Log.h>
#include <ripple/json/to_string.h>
#include <ripple/protocol/st.h>
@@ -54,7 +53,7 @@ ApplyStateTable::apply (RawView& to) const
}
std::size_t
ApplyStateTable::size ()
ApplyStateTable::size () const
{
std::size_t ret = 0;
for (auto& item : items_)

View File

@@ -20,6 +20,7 @@
#include <BeastConfig.h>
#include <ripple/ledger/detail/ApplyViewBase.h>
#include <ripple/basics/contract.h>
#include <ripple/ledger/CashDiff.h>
namespace ripple {
namespace detail {
@@ -191,5 +192,16 @@ ApplyViewBase::rawDestroyXRP(
items_.destroyXRP(fee);
}
//---
CashDiff cashFlowDiff (
CashFilter lhsFilter, ApplyViewBase const& lhs,
CashFilter rhsFilter, ApplyViewBase const& rhs)
{
assert (lhs.base_ == rhs.base_);
return CashDiff {
*lhs.base_, lhsFilter, lhs.items_, rhsFilter, rhs.items_ };
}
} // detail
} // ripple

View File

@@ -0,0 +1,733 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2016 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <BeastConfig.h>
#include <ripple/ledger/CashDiff.h>
#include <ripple/ledger/detail/ApplyStateTable.h>
#include <ripple/protocol/st.h>
#include <boost/container/static_vector.hpp>
#include <cassert>
#include <cstdlib> // std::abs()
namespace ripple {
namespace detail {
// Data structure that summarize cash changes in a single ApplyStateTable.
struct CashSummary
{
// Sorted vectors. All of the vectors fill in for std::maps.
std::vector<std::pair<
AccountID, XRPAmount>> xrpChanges;
std::vector<std::pair<
std::tuple<AccountID, AccountID, Currency>, STAmount>> trustChanges;
std::vector<std::pair<
std::tuple<AccountID, AccountID, Currency>, bool>> trustDeletions;
std::vector<std::pair<
std::tuple<AccountID, std::uint32_t>,
CashDiff::OfferAmounts>> offerChanges;
// Note that the OfferAmounts hold the amount *prior* to deletion.
std::vector<std::pair<
std::tuple<AccountID, std::uint32_t>,
CashDiff::OfferAmounts>> offerDeletions;
bool hasDiff () const
{
return !xrpChanges.empty()
|| !trustChanges.empty()
|| !trustDeletions.empty()
|| !offerChanges.empty()
|| !offerDeletions.empty();
}
void reserve (size_t newCap)
{
xrpChanges.reserve (newCap);
trustChanges.reserve (newCap);
trustDeletions.reserve (newCap);
offerChanges.reserve (newCap);
offerDeletions.reserve (newCap);
}
void shrink_to_fit()
{
xrpChanges.shrink_to_fit();
trustChanges.shrink_to_fit();
trustDeletions.shrink_to_fit();
offerChanges.shrink_to_fit();
offerDeletions.shrink_to_fit();
}
void sort()
{
std::sort (xrpChanges.begin(), xrpChanges.end());
std::sort (trustChanges.begin(), trustChanges.end());
std::sort (trustDeletions.begin(), trustDeletions.end());
std::sort (offerChanges.begin(), offerChanges.end());
std::sort (offerDeletions.begin(), offerDeletions.end());
}
};
// treatZeroOfferAsDeletion()
//
// Older payment code might set an Offer's TakerPays and TakerGets to
// zero and let the offer be cleaned up later. A more recent version
// may be more proactive about removing offers. We attempt to paper
// over that difference here.
//
// Two conditions are checked:
//
// o A modified Offer with both TakerPays and TakerGets set to zero is
// added to offerDeletions (not offerChanges).
//
// o Any deleted offer that was zero before deletion is ignored. It will
// have been treated as deleted when the offer was first set to zero.
//
// The returned bool indicates whether the passed in data was handled.
// This allows the caller to avoid further handling.
static bool treatZeroOfferAsDeletion (CashSummary& result, bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after)
{
if (!before)
return false;
auto const& prev = *before;
if (isDelete)
{
if (prev.getType() == ltOFFER &&
prev[sfTakerPays] == zero && prev[sfTakerGets] == zero)
{
// Offer was previously treated as deleted when it was zeroed.
return true;
}
}
else
{
// modify
if (!after)
return false;
auto const& cur = *after;
if (cur.getType() == ltOFFER &&
cur[sfTakerPays] == zero && cur[sfTakerGets] == zero)
{
// Either ignore or treat as delete.
auto const oldTakerPays = prev[sfTakerPays];
auto const oldTakerGets = prev[sfTakerGets];
if (oldTakerPays != zero && oldTakerGets != zero)
{
result.offerDeletions.push_back (
std::make_pair (
std::make_tuple (prev[sfAccount], prev[sfSequence]),
CashDiff::OfferAmounts {{oldTakerPays, oldTakerGets}}));
return true;
}
}
}
return false;
}
static bool getBasicCashFlow (CashSummary& result, bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after)
{
if (isDelete)
{
if (!before)
return false;
auto const& prev = *before;
switch(prev.getType())
{
case ltACCOUNT_ROOT:
result.xrpChanges.push_back (
std::make_pair (prev[sfAccount], XRPAmount {0}));
return true;
case ltRIPPLE_STATE:
result.trustDeletions.push_back(
std::make_pair (
std::make_tuple (
prev[sfLowLimit].getIssuer(),
prev[sfHighLimit].getIssuer(),
prev[sfBalance].getCurrency()), false));
return true;
case ltOFFER:
result.offerDeletions.push_back (
std::make_pair (
std::make_tuple (prev[sfAccount], prev[sfSequence]),
CashDiff::OfferAmounts {{
prev[sfTakerPays],
prev[sfTakerGets]}}));
return true;
default:
return false;
}
}
else
{
// insert or modify
if (!after)
{
assert (after);
return false;
}
auto const& cur = *after;
switch(cur.getType())
{
case ltACCOUNT_ROOT:
{
auto const curXrp = cur[sfBalance].xrp();
if (!before || (*before)[sfBalance].xrp() != curXrp)
result.xrpChanges.push_back (
std::make_pair (cur[sfAccount], curXrp));
return true;
}
case ltRIPPLE_STATE:
{
auto const curBalance = cur[sfBalance];
if (!before || (*before)[sfBalance] != curBalance)
result.trustChanges.push_back (
std::make_pair (
std::make_tuple (
cur[sfLowLimit].getIssuer(),
cur[sfHighLimit].getIssuer(),
curBalance.getCurrency()),
curBalance));
return true;
}
case ltOFFER:
{
auto const curTakerPays = cur[sfTakerPays];
auto const curTakerGets = cur[sfTakerGets];
if (!before || (*before)[sfTakerGets] != curTakerGets ||
(*before)[sfTakerPays] != curTakerPays)
{
result.offerChanges.push_back (
std::make_pair (
std::make_tuple (cur[sfAccount], cur[sfSequence]),
CashDiff::OfferAmounts {{curTakerPays, curTakerGets}}));
}
return true;
}
default:
break;
}
}
return false;
}
// Extract the final cash state from an ApplyStateTable.
static CashSummary
getCashFlow (ReadView const& view, CashFilter f, ApplyStateTable const& table)
{
CashSummary result;
result.reserve (table.size());
// Make a container of filters based on the passed in filter flags.
using FuncType = decltype (&getBasicCashFlow);
boost::container::static_vector<FuncType, 2> filters;
if ((f & CashFilter::treatZeroOfferAsDeletion) != CashFilter::none)
filters.push_back (treatZeroOfferAsDeletion);
filters.push_back (&getBasicCashFlow);
auto each = [&result, &filters](uint256 const& key, bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) {
std::find_if (filters.begin(), filters.end(),
[&result, isDelete, &before, &after] (FuncType func) {
return func (result, isDelete, before, after);
});
};
table.visit (view, each);
result.sort();
result.shrink_to_fit();
return result;
}
} // detail
//------------------------------------------------------------------------------
// Holds all of the CashDiff-related data.
class CashDiff::Impl
{
private:
// Note differences in destroyed XRP between two ApplyStateTables.
struct DropsGone
{
XRPAmount lhs;
XRPAmount rhs;
};
ReadView const& view_;
std::size_t commonKeys_ = 0; // Number of keys common to both rhs and lhs.
std::size_t lhsKeys_ = 0; // Number of keys in lhs but not rhs.
std::size_t rhsKeys_ = 0; // Number of keys in rhs but not lhs.
boost::optional<DropsGone> dropsGone_;
detail::CashSummary lhsDiffs_;
detail::CashSummary rhsDiffs_;
public:
// Constructor.
Impl (ReadView const& view,
CashFilter lhsFilter, detail::ApplyStateTable const& lhs,
CashFilter rhsFilter, detail::ApplyStateTable const& rhs)
: view_ (view)
{
findDiffs (lhsFilter, lhs, rhsFilter, rhs);
}
std::size_t commonCount() const
{
return commonKeys_;
}
std::size_t lhsOnlyCount() const
{
return lhsKeys_;
}
std::size_t rhsOnlyCount () const
{
return rhsKeys_;
}
bool hasDiff () const
{
return dropsGone_ != boost::none
|| lhsDiffs_.hasDiff()
|| rhsDiffs_.hasDiff();
}
// Filter out differences that are small enough to be in the floating
// point noise.
bool rmDust ();
// Remove offer deletion differences from a given side
bool rmLhsDeletedOffers();
bool rmRhsDeletedOffers();
private:
void findDiffs (
CashFilter lhsFilter, detail::ApplyStateTable const& lhs,
CashFilter rhsFilter, detail::ApplyStateTable const& rhs);
};
// Template function to count difference types in individual CashDiff vectors.
// Assumes those vectors are sorted.
//
// Returned array:
// [0] count of keys present in both vectors.
// [1] count of keys present in lhs only.
// [2] count of keys present in rhs only.
template <typename T, typename U>
static std::array<std::size_t, 3> countKeys (
std::vector<std::pair<T, U>> const& lhs,
std::vector<std::pair<T, U>> const& rhs)
{
std::array<std::size_t, 3> ret {}; // Zero initialize;
auto lhsItr = lhs.cbegin();
auto rhsItr = rhs.cbegin();
while (lhsItr != lhs.cend() || rhsItr != rhs.cend())
{
if (lhsItr == lhs.cend())
{
// rhs has an entry that is not in lhs.
ret[2] += 1;
++rhsItr;
}
else if (rhsItr == rhs.cend())
{
// lhs has an entry that is not in rhs.
ret[1] += 1;
++lhsItr;
}
else if (lhsItr->first < rhsItr->first)
{
// This key is only in lhs.
ret[1] += 1;
++lhsItr;
}
else if (rhsItr->first < lhsItr->first)
{
// This key is only in rhs.
ret[2] += 1;
++rhsItr;
}
else
{
// The equivalent key is present in both vectors.
ret[0] += 1;
++lhsItr;
++rhsItr;
}
}
return ret;
}
// Given two CashSummary instances, count the keys. Assumes both
// CashSummaries have sorted entries.
//
// Returned array:
// [0] count of keys present in both vectors.
// [1] count of keys present in lhs only.
// [2] count of keys present in rhs only.
static std::array<std::size_t, 3>
countKeys (detail::CashSummary const& lhs, detail::CashSummary const& rhs)
{
std::array<std::size_t, 3> ret {}; // Zero initialize;
// Lambda to add in new results.
auto addIn = [&ret] (std::array<std::size_t, 3> const& a)
{
std::transform (a.cbegin(), a.cend(),
ret.cbegin(), ret.begin(), std::plus<std::size_t>());
};
addIn (countKeys(lhs.xrpChanges, rhs.xrpChanges));
addIn (countKeys(lhs.trustChanges, rhs.trustChanges));
addIn (countKeys(lhs.trustDeletions, rhs.trustDeletions));
addIn (countKeys(lhs.offerChanges, rhs.offerChanges));
addIn (countKeys(lhs.offerDeletions, rhs.offerDeletions));
return ret;
}
// Function that compares two CashDiff::OfferAmounts and returns true if
// the difference is dust-sized.
static bool diffIsDust (
CashDiff::OfferAmounts const& lhs, CashDiff::OfferAmounts const& rhs)
{
for (auto i = 0; i < lhs.count(); ++i)
{
if (!diffIsDust (lhs[i], rhs[i]))
return false;
}
return true;
}
// Template function to remove dust from individual CashDiff vectors.
template <typename T, typename U, typename L>
static bool
rmVecDust (
std::vector<std::pair<T, U>>& lhs,
std::vector<std::pair<T, U>>& rhs,
L&& justDust)
{
static_assert (
std::is_same<bool,
decltype (justDust (lhs[0].second, rhs[0].second))>::value,
"Invalid lambda passed to rmVecDust");
bool dustWasRemoved = false;
auto lhsItr = lhs.begin();
while (lhsItr != lhs.end())
{
using value_t = std::pair<T, U>;
auto const found = std::equal_range (rhs.begin(), rhs.end(), *lhsItr,
[] (value_t const& a, value_t const& b)
{
return a.first < b.first;
});
if (found.first != found.second)
{
// The same entry changed for both lhs and rhs. Check whether
// the differences are small enough to be removed.
if (justDust (lhsItr->second, found.first->second))
{
dustWasRemoved = true;
rhs.erase (found.first);
// Dodge an invalid iterator by using erase's return value.
lhsItr = lhs.erase (lhsItr);
continue;
}
}
++lhsItr;
}
return dustWasRemoved;
}
bool CashDiff::Impl::rmDust ()
{
bool removedDust = false;
// Four of the containers can have small (floating point style)
// amount differences: xrpChanges, trustChanges, offerChanges, and
// offerDeletions. Rifle through those containers and remove any
// entries that are _almost_ the same between lhs and rhs.
// xrpChanges. We call a difference of 2 drops or less dust.
removedDust |= rmVecDust (lhsDiffs_.xrpChanges, rhsDiffs_.xrpChanges,
[](XRPAmount const& lhs, XRPAmount const& rhs)
{
return diffIsDust (lhs, rhs);
});
// trustChanges.
removedDust |= rmVecDust (lhsDiffs_.trustChanges, rhsDiffs_.trustChanges,
[](STAmount const& lhs, STAmount const& rhs)
{
return diffIsDust (lhs, rhs);
});
// offerChanges.
removedDust |= rmVecDust (lhsDiffs_.offerChanges, rhsDiffs_.offerChanges,
[](CashDiff::OfferAmounts const& lhs, CashDiff::OfferAmounts const& rhs)
{
return diffIsDust (lhs, rhs);
});
// offerDeletions.
removedDust |= rmVecDust (
lhsDiffs_.offerDeletions, rhsDiffs_.offerDeletions,
[](CashDiff::OfferAmounts const& lhs, CashDiff::OfferAmounts const& rhs)
{
return diffIsDust (lhs, rhs);
});
return removedDust;
}
bool CashDiff::Impl::rmLhsDeletedOffers()
{
bool const ret = !lhsDiffs_.offerDeletions.empty();
if (ret)
lhsDiffs_.offerDeletions.clear();
return ret;
}
bool CashDiff::Impl::rmRhsDeletedOffers()
{
bool const ret = !rhsDiffs_.offerDeletions.empty();
if (ret)
rhsDiffs_.offerDeletions.clear();
return ret;
}
// Deposits differences between two sorted vectors into a destination.
template <typename T>
static void setDiff (
std::vector<T> const& a, std::vector<T> const& b, std::vector<T>& dest)
{
dest.clear();
std::set_difference (a.cbegin(), a.cend(),
b.cbegin(), b.cend(), std::inserter (dest, dest.end()));
}
void CashDiff::Impl::findDiffs (
CashFilter lhsFilter, detail::ApplyStateTable const& lhs,
CashFilter rhsFilter, detail::ApplyStateTable const& rhs)
{
// If dropsDestroyed_ is different, note that.
if (lhs.dropsDestroyed() != rhs.dropsDestroyed())
{
dropsGone_ = DropsGone{lhs.dropsDestroyed(), rhs.dropsDestroyed()};
}
// Extract cash flow changes from the state tables
auto lhsDiffs = getCashFlow (view_, lhsFilter, lhs);
auto rhsDiffs = getCashFlow (view_, rhsFilter, rhs);
// Get statistics on keys.
auto const counts = countKeys (lhsDiffs, rhsDiffs);
commonKeys_ = counts[0];
lhsKeys_ = counts[1];
rhsKeys_ = counts[2];
// Save only the differences between the results.
// xrpChanges:
setDiff (lhsDiffs.xrpChanges, rhsDiffs.xrpChanges, lhsDiffs_.xrpChanges);
setDiff (rhsDiffs.xrpChanges, lhsDiffs.xrpChanges, rhsDiffs_.xrpChanges);
// trustChanges:
setDiff (lhsDiffs.trustChanges, rhsDiffs.trustChanges, lhsDiffs_.trustChanges);
setDiff (rhsDiffs.trustChanges, lhsDiffs.trustChanges, rhsDiffs_.trustChanges);
// trustDeletions:
setDiff (lhsDiffs.trustDeletions, rhsDiffs.trustDeletions, lhsDiffs_.trustDeletions);
setDiff (rhsDiffs.trustDeletions, lhsDiffs.trustDeletions, rhsDiffs_.trustDeletions);
// offerChanges:
setDiff (lhsDiffs.offerChanges, rhsDiffs.offerChanges, lhsDiffs_.offerChanges);
setDiff (rhsDiffs.offerChanges, lhsDiffs.offerChanges, rhsDiffs_.offerChanges);
// offerDeletions:
setDiff (lhsDiffs.offerDeletions, rhsDiffs.offerDeletions, lhsDiffs_.offerDeletions);
setDiff (rhsDiffs.offerDeletions, lhsDiffs.offerDeletions, rhsDiffs_.offerDeletions);
}
//------------------------------------------------------------------------------
// Locates differences between two ApplyStateTables.
CashDiff::CashDiff (CashDiff&& other)
: impl_ (std::move (other.impl_))
{
}
CashDiff::~CashDiff()
{
}
CashDiff::CashDiff (ReadView const& view,
CashFilter lhsFilter, detail::ApplyStateTable const& lhs,
CashFilter rhsFilter, detail::ApplyStateTable const& rhs)
: impl_ (new Impl (view, lhsFilter, lhs, rhsFilter, rhs))
{
}
std::size_t CashDiff::commonCount () const
{
return impl_->commonCount();
}
std::size_t CashDiff::rhsOnlyCount () const
{
return impl_->rhsOnlyCount();
}
std::size_t CashDiff::lhsOnlyCount () const
{
return impl_->lhsOnlyCount();
}
bool CashDiff::hasDiff() const
{
return impl_->hasDiff();
}
bool CashDiff::rmDust()
{
return impl_->rmDust();
}
bool CashDiff::rmLhsDeletedOffers()
{
return impl_->rmLhsDeletedOffers();
}
bool CashDiff::rmRhsDeletedOffers()
{
return impl_->rmRhsDeletedOffers();
}
//------------------------------------------------------------------------------
// Function that compares two STAmounts and returns true if the difference
// is dust-sized.
bool diffIsDust (STAmount const& v1, STAmount const& v2, std::uint8_t e10)
{
// If one value is positive and the other negative then there's something
// odd afoot.
if (v1 != zero && v2 != zero && (v1.negative() != v2.negative()))
return false;
// v1 and v2 must be the same Issue for their difference to make sense.
if (v1.native() != v2.native())
return false;
if (!v1.native() && (v1.issue() != v2.issue()))
return false;
// If v1 == v2 then the dust is vanishingly small.
if (v1 == v2)
return true;
STAmount const& small = v1 < v2 ? v1 : v2;
STAmount const& large = v1 < v2 ? v2 : v1;
// Handling XRP is different from IOU.
if (v1.native())
{
std::uint64_t const s = small.mantissa();
std::uint64_t const l = large.mantissa();
// Always allow a couple of drops of noise.
if (l - s <= 2)
return true;
static_assert (sizeof (1ULL) == sizeof (std::uint64_t), "");
std::uint64_t ratio = s / (l - s);
static constexpr std::uint64_t e10Lookup[]
{
1ULL,
10ULL,
100ULL,
1'000ULL,
10'000ULL,
100'000ULL,
1'000'000ULL,
10'000'000ULL,
100'000'000ULL,
1'000'000'000ULL,
10'000'000'000ULL,
100'000'000'000ULL,
1'000'000'000'000ULL,
10'000'000'000'000ULL,
100'000'000'000'000ULL,
1'000'000'000'000'000ULL,
10'000'000'000'000'000ULL,
100'000'000'000'000'000ULL,
1'000'000'000'000'000'000ULL,
10'000'000'000'000'000'000ULL,
};
static std::size_t constexpr maxIndex =
sizeof (e10Lookup) / sizeof e10Lookup[0];
// Make sure the table is big enough.
static_assert (
std::numeric_limits<std::uint64_t>::max() /
e10Lookup[maxIndex - 1] < 10, "Table too small");
if (e10 >= maxIndex)
return false;
return ratio >= e10Lookup[e10];
}
// Non-native. Note that even though large and small may not be equal,
// their difference may be zero. One way that can happen is if two
// values are different, but their difference results in an STAmount
// with an exponent less than -96.
STAmount const diff = large - small;
if (diff == zero)
return true;
STAmount const ratio = divide (small, diff, v1.issue());
STAmount const one (v1.issue(), 1);
int const ratioExp = ratio.exponent() - one.exponent();
return ratioExp >= e10;
};
} // ripple

View File

@@ -324,8 +324,7 @@ PaymentSandbox::balanceChanges (ReadView const& view) const
break;
}
}
if (!before)
else if (!before)
{
// insert
auto const at = after->getType ();

View File

@@ -176,59 +176,7 @@ accountHolds (ReadView const& view,
STAmount amount;
if (isXRP(currency))
{
// XRP: return balance minus reserve
if (fix1141 (view.info ().parentCloseTime))
{
auto const sle = view.read(
keylet::account(account));
auto const ownerCount =
view.ownerCountHook (account, sle->getFieldU32 (sfOwnerCount));
auto const reserve =
view.fees().accountReserve(ownerCount);
auto const fullBalance =
sle->getFieldAmount(sfBalance);
auto const balance = view.balanceHook(
account, issuer, fullBalance).xrp();
if (balance < reserve)
amount.clear ();
else
amount = balance - reserve;
JLOG (j.trace()) << "accountHolds:" <<
" account=" << to_string (account) <<
" amount=" << amount.getFullText () <<
" fullBalance=" << to_string (fullBalance.xrp()) <<
" balance=" << to_string (balance) <<
" reserve=" << to_string (reserve);
return amount;
}
else
{
// pre-switchover
// XRP: return balance minus reserve
auto const sle = view.read(
keylet::account(account));
auto const reserve =
view.fees().accountReserve(
sle->getFieldU32(sfOwnerCount));
auto const balance =
sle->getFieldAmount(sfBalance).xrp ();
if (balance < reserve)
amount.clear ();
else
amount = balance - reserve;
JLOG (j.trace()) << "accountHolds:" <<
" account=" << to_string (account) <<
" amount=" << amount.getFullText () <<
" balance=" << to_string (balance) <<
" reserve=" << to_string (reserve);
return view.balanceHook(
account, issuer, amount);
}
return {xrpLiquid (view, account, 0, j)};
}
// IOU: Return balance on trust line modulo freeze
@@ -290,6 +238,115 @@ accountFunds (ReadView const& view, AccountID const& id,
return saFunds;
}
// Prevent ownerCount from wrapping under error conditions.
//
// adjustment allows the ownerCount to be adjusted up or down in multiple steps.
// If id != boost.none, then do error reporting.
//
// Returns adjusted owner count.
static
std::uint32_t
confineOwnerCount (std::uint32_t current, std::int32_t adjustment,
boost::optional<AccountID> const& id = boost::none,
beast::Journal j = beast::Journal{})
{
std::uint32_t adjusted {current + adjustment};
if (adjustment > 0)
{
// Overflow is well defined on unsigned
if (adjusted < current)
{
if (id)
{
JLOG (j.fatal()) <<
"Account " << *id <<
" owner count exceeds max!";
}
adjusted = std::numeric_limits<std::uint32_t>::max ();
}
}
else
{
// Underflow is well defined on unsigned
if (adjusted > current)
{
if (id)
{
JLOG (j.fatal()) <<
"Account " << *id <<
" owner count set below 0!";
}
adjusted = 0;
assert(!id);
}
}
return adjusted;
}
XRPAmount
xrpLiquid (ReadView const& view, AccountID const& id,
std::int32_t ownerCountAdj, beast::Journal j)
{
auto const sle = view.read(keylet::account(id));
if (sle == nullptr)
return zero;
// Return balance minus reserve
if (fix1141 (view.info ().parentCloseTime))
{
std::uint32_t const ownerCount = confineOwnerCount (
view.ownerCountHook (id, sle->getFieldU32 (sfOwnerCount)),
ownerCountAdj);
auto const reserve =
view.fees().accountReserve(ownerCount);
auto const fullBalance =
sle->getFieldAmount(sfBalance);
auto const balance = view.balanceHook(id, xrpAccount(), fullBalance);
STAmount amount = balance - reserve;
if (balance < reserve)
amount.clear ();
JLOG (j.trace()) << "accountHolds:" <<
" account=" << to_string (id) <<
" amount=" << amount.getFullText() <<
" fullBalance=" << fullBalance.getFullText() <<
" balance=" << balance.getFullText() <<
" reserve=" << to_string (reserve) <<
" ownerCount=" << to_string (ownerCount) <<
" ownerCountAdj=" << to_string (ownerCountAdj);
return amount.xrp();
}
else
{
// pre-switchover
// XRP: return balance minus reserve
std::uint32_t const ownerCount =
confineOwnerCount (sle->getFieldU32 (sfOwnerCount), ownerCountAdj);
auto const reserve =
view.fees().accountReserve(sle->getFieldU32(sfOwnerCount));
auto const balance = sle->getFieldAmount(sfBalance);
STAmount amount = balance - reserve;
if (balance < reserve)
amount.clear ();
JLOG (j.trace()) << "accountHolds:" <<
" account=" << to_string (id) <<
" amount=" << amount.getFullText() <<
" balance=" << balance.getFullText() <<
" reserve=" << to_string (reserve) <<
" ownerCount=" << to_string (ownerCount) <<
" ownerCountAdj=" << to_string (ownerCountAdj);
return view.balanceHook(id, xrpAccount(), amount).xrp();
}
}
void
forEachItem (ReadView const& view, AccountID const& id,
std::function<void(std::shared_ptr<SLE const> const&)> f)
@@ -678,37 +735,12 @@ hashOfSeq (ReadView const& ledger, LedgerIndex seq,
void
adjustOwnerCount (ApplyView& view,
std::shared_ptr<SLE> const& sle,
int amount, beast::Journal j)
std::int32_t amount, beast::Journal j)
{
assert(amount != 0);
auto const current =
sle->getFieldU32 (sfOwnerCount);
auto adjusted = current + amount;
std::uint32_t const current {sle->getFieldU32 (sfOwnerCount)};
AccountID const id = (*sle)[sfAccount];
if (amount > 0)
{
// Overflow is well defined on unsigned
if (adjusted < current)
{
JLOG (j.fatal()) <<
"Account " << id <<
" owner count exceeds max!";
adjusted =
std::numeric_limits<std::uint32_t>::max ();
}
}
else
{
// Underflow is well defined on unsigned
if (adjusted > current)
{
JLOG (j.fatal()) <<
"Account " << id <<
" owner count set below 0!";
adjusted = 0;
assert(false);
}
}
std::uint32_t const adjusted = confineOwnerCount (current, amount, id, j);
view.adjustOwnerCountHook (id, current, adjusted);
sle->setFieldU32 (sfOwnerCount, adjusted);
view.update(sle);
@@ -1257,7 +1289,7 @@ offerDelete (ApplyView& view,
// Direct send w/o fees:
// - Redeeming IOUs and/or sending sender's own IOUs.
// - Create trust line of needed.
// - Create trust line if needed.
// --> bCheckIssuer : normally require issuer to be involved.
TER
rippleCredit (ApplyView& view,

View File

@@ -43,6 +43,8 @@ extern uint256 const featureCompareFlowV1V2;
extern uint256 const featureSHAMapV2;
extern uint256 const featurePayChan;
extern uint256 const featureFlow;
extern uint256 const featureCompareTakerFlowCross;
extern uint256 const featureFlowCross;
extern uint256 const featureCryptoConditions;
extern uint256 const featureTickSize;
extern uint256 const fix1368;

View File

@@ -241,6 +241,20 @@ public:
return lhs.m_value < rhs.m_value;
}
friend
bool
operator<= (Quality const& lhs, Quality const& rhs) noexcept
{
return !(lhs > rhs);
}
friend
bool
operator>= (Quality const& lhs, Quality const& rhs) noexcept
{
return !(lhs < rhs);
}
friend
bool
operator== (Quality const& lhs, Quality const& rhs) noexcept

View File

@@ -54,6 +54,8 @@ uint256 const featureCompareFlowV1V2 = feature("CompareFlowV1V2");
uint256 const featureSHAMapV2 = feature("SHAMapV2");
uint256 const featurePayChan = feature("PayChan");
uint256 const featureFlow = feature("Flow");
uint256 const featureCompareTakerFlowCross = feature("CompareTakerFlowCross");
uint256 const featureFlowCross = feature("FlowCross");
uint256 const featureCryptoConditions = feature("CryptoConditions");
uint256 const featureTickSize = feature("TickSize");
uint256 const fix1368 = feature("fix1368");

View File

@@ -25,6 +25,7 @@
#include <ripple/ledger/impl/BookDirs.cpp>
#include <ripple/ledger/impl/CachedSLEs.cpp>
#include <ripple/ledger/impl/CachedView.cpp>
#include <ripple/ledger/impl/CashDiff.cpp>
#include <ripple/ledger/impl/Directory.cpp>
#include <ripple/ledger/impl/OpenView.cpp>
#include <ripple/ledger/impl/PaymentSandbox.cpp>

View File

@@ -42,7 +42,7 @@ private:
}
public:
void
testStepLimit(std::initializer_list<uint256> fs)
{
@@ -76,7 +76,7 @@ public:
balance("bob", USD(0)), owners("bob", 1),
balance("dan", USD(1)), owners("dan", 2)));
}
void
testCrossingLimit(std::initializer_list<uint256> fs)
{
@@ -160,6 +160,7 @@ public:
testAll({});
testAll({featureFlow});
testAll({featureFlow, fix1373});
testAll({featureFlow, fix1373, featureFlowCross});
}
};

View File

@@ -114,6 +114,8 @@ public:
test_convert_all_of_an_asset({});
test_convert_all_of_an_asset({featureFlow});
test_convert_all_of_an_asset({featureFlow, fix1373});
test_convert_all_of_an_asset(
{featureFlow, fix1373, featureFlowCross});
}
};

View File

@@ -147,6 +147,7 @@ public:
testXRPDiscrepancy ({});
testXRPDiscrepancy ({featureFlow});
testXRPDiscrepancy ({featureFlow, fix1373});
testXRPDiscrepancy ({featureFlow, fix1373, featureFlowCross});
}
};

View File

@@ -467,8 +467,8 @@ struct Flow_test : public beast::unit_test::suite
paths.push_back (p2);
}
return flow (sb, deliver, alice, carol, paths, false, false, true,
boost::none, smax, flowJournal);
return flow (sb, deliver, alice, carol, paths, false, false,
true, false, boost::none, smax, flowJournal);
}();
BEAST_EXPECT(flowResult.removableOffers.size () == 1);
@@ -1199,6 +1199,44 @@ struct Flow_test : public beast::unit_test::suite
env.close();
}
void
testSelfPayLowQualityOffer (std::initializer_list<uint256> fs)
{
// The new payment code used to assert if an offer was made for more
// XRP than the offering account held. This unit test reproduces
// that failing case.
testcase ("Self crossing low quality offer");
using namespace jtx;
Env env(*this, features (fs));
auto const ann = Account("ann");
auto const gw = Account("gateway");
auto const CTB = gw["CTB"];
auto const fee = env.current ()->fees ().base;
env.fund (reserve(env, 2) + drops (9999640) + (fee), ann);
env.fund (reserve(env, 2) + (fee*4), gw);
env.close();
env (rate(gw, 1.002));
env (trust(ann, CTB(10)));
env.close();
env (pay(gw, ann, CTB(2.856)));
env.close();
env (offer(ann, drops(365611702030), CTB(5.713)));
env.close();
// This payment caused the assert.
env (pay(ann, ann, CTB(0.687)),
sendmax (drops(20000000000)), txflags (tfPartialPayment));
}
void run() override
{
testLimitQuality();
@@ -1225,10 +1263,12 @@ struct Flow_test : public beast::unit_test::suite
testUnfundedOffer(true, {fs...});
testUnfundedOffer(false, {fs...});
testReexecuteDirectStep({fix1368, fs...});
testSelfPayLowQualityOffer({fs...});
};
testWithFeats();
testWithFeats(featureFlow);
testWithFeats(featureFlow, fix1373);
testWithFeats(featureFlow, fix1373, featureFlowCross);
}
};

View File

@@ -533,6 +533,7 @@ public:
testAll({});
testAll({featureFlow});
testAll({featureFlow, fix1373});
testAll({featureFlow, fix1373, featureFlowCross});
}
};

File diff suppressed because it is too large Load Diff

View File

@@ -638,14 +638,14 @@ struct PayStrand_test : public beast::unit_test::suite
// Test every combination of element type pairs on a path
void
testAllPairs()
testAllPairs(std::initializer_list<uint256> fs)
{
testcase("All pairs");
using namespace jtx;
using RippleCalc = ::ripple::path::RippleCalc;
ExistingElementPool eep;
Env env(*this, features(fix1373));
Env env(*this, features(fs));
auto const closeTime = fix1298Time() +
100 * env.closed()->info().closeTimeResolution;
@@ -892,9 +892,11 @@ struct PayStrand_test : public beast::unit_test::suite
alice,
bob,
deliver,
boost::none,
sendMaxIssue,
path,
true,
false,
env.app().logs().journal("Flow"));
BEAST_EXPECT(r.first == expTer);
if (sizeof...(expSteps))
@@ -917,9 +919,11 @@ struct PayStrand_test : public beast::unit_test::suite
alice,
alice,
/*deliver*/ xrpIssue(),
/*limitQuality*/ boost::none,
/*sendMaxIssue*/ EUR.issue(),
path,
true,
false,
env.app().logs().journal("Flow"));
BEAST_EXPECT(r.first == tesSUCCESS);
}
@@ -930,9 +934,11 @@ struct PayStrand_test : public beast::unit_test::suite
alice,
alice,
/*deliver*/ xrpIssue(),
/*limitQuality*/ boost::none,
/*sendMaxIssue*/ xrpIssue(),
path,
true,
false,
env.app().logs().journal("Flow"));
BEAST_EXPECT(r.first == tesSUCCESS);
}
@@ -1043,9 +1049,11 @@ struct PayStrand_test : public beast::unit_test::suite
alice,
xrpAccount(),
XRP,
boost::none,
USD.issue(),
STPath(),
true,
false,
flowJournal);
BEAST_EXPECT(r.first == temBAD_PATH);
}
@@ -1057,8 +1065,10 @@ struct PayStrand_test : public beast::unit_test::suite
alice,
XRP,
boost::none,
boost::none,
STPath(),
true,
false,
flowJournal);
BEAST_EXPECT(r.first == temBAD_PATH);
}
@@ -1070,8 +1080,10 @@ struct PayStrand_test : public beast::unit_test::suite
bob,
USD,
boost::none,
boost::none,
STPath(),
true,
false,
flowJournal);
BEAST_EXPECT(r.first == terNO_ACCOUNT);
}
@@ -1204,8 +1216,10 @@ struct PayStrand_test : public beast::unit_test::suite
gw,
USD,
boost::none,
boost::none,
STPath(),
true,
false,
env.app().logs().journal("Flow"));
BEAST_EXPECT(r.first == tesSUCCESS);
BEAST_EXPECT(equal(r.second, D{alice, gw, usdC}));
@@ -1242,9 +1256,11 @@ struct PayStrand_test : public beast::unit_test::suite
alice,
bob,
XRP,
boost::none,
USD.issue(),
path,
false,
false,
env.app().logs().journal("Flow"));
BEAST_EXPECT(r.first == tesSUCCESS);
BEAST_EXPECT(equal(r.second, D{alice, gw, usdC}, B{USD.issue(), xrpIssue()}, XRPS{bob}));
@@ -1402,14 +1418,18 @@ struct PayStrand_test : public beast::unit_test::suite
void
run() override
{
testAllPairs();
testAllPairs({featureFlow, fix1373});
testAllPairs({featureFlow, fix1373, featureFlowCross});
testToStrand({featureFlow});
testToStrand({featureFlow, fix1373});
testToStrand({featureFlow, fix1373, featureFlowCross});
testRIPD1373({});
testRIPD1373({featureFlow, fix1373});
testRIPD1373({featureFlow, fix1373, featureFlowCross});
testLoop({});
testLoop({featureFlow});
testLoop({featureFlow, fix1373});
testLoop({featureFlow, fix1373, featureFlowCross});
}
};

View File

@@ -78,6 +78,7 @@ struct SetAuth_test : public beast::unit_test::suite
testAuth({});
testAuth({featureFlow});
testAuth({featureFlow, fix1373});
testAuth({featureFlow, fix1373, featureFlowCross});
}
};

View File

@@ -509,6 +509,7 @@ public:
testWithFeatures({});
testWithFeatures({featureFlow});
testWithFeatures({featureFlow, fix1373});
testWithFeatures({featureFlow, fix1373, featureFlowCross});
}
};

View File

@@ -83,6 +83,14 @@ features (std::initializer_list<uint256> keys)
return keys;
}
/** Activate features in the Env ctor */
inline
auto
features (std::vector<uint256> keys)
{
return keys;
}
//------------------------------------------------------------------------------
/** A transaction testing environment. */
@@ -143,6 +151,14 @@ private:
app().config().features.insert(key);
}
void
construct_arg (
std::vector<uint256> const& list)
{
for(auto const& key : list)
app().config().features.insert(key);
}
public:
Env() = delete;
Env (Env const&) = delete;

View File

@@ -27,16 +27,28 @@ namespace jtx {
Json::Value
offer (Account const& account,
STAmount const& in, STAmount const& out)
STAmount const& in, STAmount const& out, std::uint32_t flags)
{
Json::Value jv;
jv[jss::Account] = account.human();
jv[jss::TakerPays] = in.getJson(0);
jv[jss::TakerGets] = out.getJson(0);
if (flags)
jv[jss::Flags] = flags;
jv[jss::TransactionType] = "OfferCreate";
return jv;
}
Json::Value
offer_cancel (Account const& account, std::uint32_t offerSeq)
{
Json::Value jv;
jv[jss::Account] = account.human();
jv[jss::OfferSequence] = offerSeq;
jv[jss::TransactionType] = "OfferCancel";
return jv;
}
} // jtx
} // test
} // ripple

View File

@@ -31,7 +31,11 @@ namespace jtx {
/** Create an offer. */
Json::Value
offer (Account const& account,
STAmount const& in, STAmount const& out);
STAmount const& in, STAmount const& out, std::uint32_t flags = 0);
/** Cancel an offer. */
Json::Value
offer_cancel (Account const& account, std::uint32_t offerSeq);
} // jtx
} // test

View File

@@ -42,7 +42,7 @@ public:
operator()(Env&, JTx& jtx) const;
};
/** Sets the QualityIn on a trust JTx as a percentage. */
/** Sets the QualityIn on a trust JTx. */
class qualityInPercent
{
private:

View File

@@ -96,6 +96,7 @@ struct BookDirs_test : public beast::unit_test::suite
{
test_bookdir({});
test_bookdir({featureFlow, fix1373});
test_bookdir({featureFlow, fix1373, featureFlowCross});
}
};

View File

@@ -0,0 +1,104 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2016 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <BeastConfig.h>
#include <ripple/ledger/CashDiff.h>
#include <ripple/protocol/STAmount.h>
#include <ripple/beast/unit_test.h>
namespace ripple {
namespace test {
class CashDiff_test : public beast::unit_test::suite
{
// Exercise diffIsDust (STAmount, STAmount)
void
testDust ()
{
testcase ("diffIsDust (STAmount, STAmount)");
Issue const usd (Currency (0x5553440000000000), AccountID (0x4985601));
Issue const usf (Currency (0x5553460000000000), AccountID (0x4985601));
// Positive and negative are never dust.
expect (!diffIsDust (STAmount{usd, 1}, STAmount{usd, -1}));
// Different issues are never dust.
expect (!diffIsDust (STAmount{usd, 1}, STAmount{usf, 1}));
// Native and non-native are never dust.
expect (!diffIsDust (STAmount{usd, 1}, STAmount{1}));
// Equal values are always dust.
expect (diffIsDust (STAmount{0}, STAmount{0}));
{
// Test IOU.
std::uint64_t oldProbe = 0;
std::uint64_t newProbe = 10;
std::uint8_t e10 = 1;
do
{
STAmount large (usd, newProbe + 1);
STAmount small (usd, newProbe);
expect (diffIsDust (large, small, e10));
expect (diffIsDust (large, small, e10+1) == (e10 > 13));
oldProbe = newProbe;
newProbe = oldProbe * 10;
e10 += 1;
} while (newProbe > oldProbe);
}
{
// Test XRP.
// A delta of 2 or less is always dust.
expect (diffIsDust (STAmount{2}, STAmount{0}));
std::uint64_t oldProbe = 0;
std::uint64_t newProbe = 10;
std::uint8_t e10 = 0;
do
{
// Differences of 2 of fewer drops are always treated as dust,
// so use a delta of 3.
STAmount large (newProbe + 3);
STAmount small (newProbe);
expect (diffIsDust (large, small, e10));
expect (diffIsDust (large, small, e10+1) == (e10 >= 20));
oldProbe = newProbe;
newProbe = oldProbe * 10;
e10 += 1;
} while (newProbe > oldProbe);
}
}
public:
void run ()
{
testDust();
}
};
BEAST_DEFINE_TESTSUITE (CashDiff, ledger, ripple);
} // test
} // ripple

View File

@@ -331,7 +331,40 @@ class PaymentSandbox_test : public beast::unit_test::suite
accountSend (sb, alice, xrpAccount (), XRP(100), dj);
BEAST_EXPECT(accountFundsXRP (sb, alice) == beast::zero);
}
}
void testBalanceHook(std::initializer_list<uint256> fs)
{
// Make sure the Issue::Account returned by PAymentSandbox::balanceHook
// is correct.
testcase ("balanceHook");
using namespace jtx;
Env env (*this, features(fs));
Account const gw ("gw");
auto const USD = gw["USD"];
Account const alice ("alice");
auto const closeTime = fix1274Time () +
100 * env.closed ()->info ().closeTimeResolution;
env.close (closeTime);
ApplyViewImpl av (&*env.current (), tapNONE);
PaymentSandbox sb (&av);
// The currency we pass for the last argument mimics the currency that
// is typically passed to creditHook, since it comes from a trust line.
Issue tlIssue = noIssue();
tlIssue.currency = USD.issue().currency;
sb.creditHook (gw.id(), alice.id(), {USD, 400}, {tlIssue, 600});
sb.creditHook (gw.id(), alice.id(), {USD, 100}, {tlIssue, 600});
// Expect that the STAmount issuer returned by balanceHook() is correct.
STAmount const balance =
sb.balanceHook (gw.id(), alice.id(), {USD, 600});
BEAST_EXPECT (balance.getIssuer() == USD.issue().account);
}
public:
@@ -342,9 +375,11 @@ public:
testSubtractCredits(fs);
testTinyBalance(fs);
testReserve(fs);
testBalanceHook(fs);
};
testAll({});
testAll({featureFlow, fix1373});
testAll({featureFlow, fix1373, featureFlowCross});
}
};

View File

@@ -280,6 +280,20 @@ public:
BEAST_EXPECT(q12 < q13);
BEAST_EXPECT(q31 < q21);
BEAST_EXPECT(q21 < q11);
BEAST_EXPECT(q11 >= q11);
BEAST_EXPECT(q12 >= q11);
BEAST_EXPECT(q13 >= q12);
BEAST_EXPECT(q21 >= q31);
BEAST_EXPECT(q11 >= q21);
BEAST_EXPECT(q12 > q11);
BEAST_EXPECT(q13 > q12);
BEAST_EXPECT(q21 > q31);
BEAST_EXPECT(q11 > q21);
BEAST_EXPECT(q11 <= q11);
BEAST_EXPECT(q11 <= q12);
BEAST_EXPECT(q12 <= q13);
BEAST_EXPECT(q31 <= q21);
BEAST_EXPECT(q21 <= q11);
BEAST_EXPECT(q31 != q21);
}

View File

@@ -155,6 +155,7 @@ public:
{
testGWB({});
testGWB({featureFlow, fix1373});
testGWB({featureFlow, fix1373, featureFlowCross});
}
};

View File

@@ -220,6 +220,7 @@ public:
withFeatsTests({});
withFeatsTests({featureFlow});
withFeatsTests({featureFlow, fix1373});
withFeatsTests({featureFlow, fix1373, featureFlowCross});
}
};

View File

@@ -19,6 +19,7 @@
//==============================================================================
#include <test/ledger/BookDirs_test.cpp>
#include <test/ledger/CashDiff_test.cpp>
#include <test/ledger/Directory_test.cpp>
#include <test/ledger/Invariants_test.cpp>
#include <test/ledger/PaymentSandbox_test.cpp>