From 3ccbd7c9b2e44775b5534f38e55bc52d356e06f6 Mon Sep 17 00:00:00 2001 From: Nik Bougalis Date: Mon, 27 Oct 2014 11:23:17 -0800 Subject: [PATCH] Finalize autobridging implementation (RIPD-179): Autobridging uses XRP as a natural bridge currency to allow IOU-to-IOU orders to be satisfied not only from the direct IOU-to-IOU books but also over the combined IOU-to-XRP and XRP-to-IOU books. This commit addresses the following issues: * RIPD-486: Refactoring the taker into a unit-testable architecture * RIPD-659: Asset-aware offer crossing * RIPD-491: Unit tests for IOU to XRP, XRP to IOU and IOU to IOU * RIPD-441: Handle case when autobridging over same owner offers * RIPD-665: Handle case when autobridging over own offers * RIPD-273: Groom order books while crossing --- Builds/VisualStudio2013/RippleD.vcxproj | 11 +- .../VisualStudio2013/RippleD.vcxproj.filters | 12 +- src/BeastConfig.h | 2 +- src/ripple/app/book/Offer.h | 58 +- src/ripple/app/book/OfferStream.h | 11 - src/ripple/app/book/Taker.h | 266 ++++- src/ripple/app/book/Types.h | 8 + src/ripple/app/book/impl/OfferStream.cpp | 20 +- src/ripple/app/book/impl/Taker.cpp | 695 ++++++++--- src/ripple/app/book/tests/Taker.test.cpp | 373 ++++++ src/ripple/app/ledger/LedgerEntrySet.cpp | 2 +- src/ripple/app/transactors/CancelTicket.h | 28 - src/ripple/app/transactors/CreateOffer.cpp | 1028 +++++++++++------ src/ripple/app/transactors/CreateOffer.h | 83 -- .../app/transactors/CreateOfferBridged.cpp | 197 ---- .../app/transactors/CreateOfferDirect.cpp | 102 -- src/ripple/app/transactors/CreateTicket.h | 27 - src/ripple/app/transactors/Transactor.cpp | 57 +- src/ripple/protocol/Issue.h | 3 + src/ripple/protocol/STAmount.h | 1 + src/ripple/protocol/TER.h | 1 + src/ripple/protocol/impl/TER.cpp | 1 + src/ripple/unity/app4.cpp | 1 + src/ripple/unity/app9.cpp | 2 - 24 files changed, 1867 insertions(+), 1122 deletions(-) create mode 100644 src/ripple/app/book/tests/Taker.test.cpp delete mode 100644 src/ripple/app/transactors/CancelTicket.h delete mode 100644 src/ripple/app/transactors/CreateOffer.h delete mode 100644 src/ripple/app/transactors/CreateOfferBridged.cpp delete mode 100644 src/ripple/app/transactors/CreateOfferDirect.cpp delete mode 100644 src/ripple/app/transactors/CreateTicket.h diff --git a/Builds/VisualStudio2013/RippleD.vcxproj b/Builds/VisualStudio2013/RippleD.vcxproj index 2ff2aa02b37..252d9984721 100644 --- a/Builds/VisualStudio2013/RippleD.vcxproj +++ b/Builds/VisualStudio2013/RippleD.vcxproj @@ -1779,6 +1779,9 @@ True + + True + @@ -2119,14 +2122,6 @@ True - - - - True - - - True - True diff --git a/Builds/VisualStudio2013/RippleD.vcxproj.filters b/Builds/VisualStudio2013/RippleD.vcxproj.filters index 7650cebafa6..86aca62833b 100644 --- a/Builds/VisualStudio2013/RippleD.vcxproj.filters +++ b/Builds/VisualStudio2013/RippleD.vcxproj.filters @@ -2709,6 +2709,9 @@ ripple\app\book\tests + + ripple\app\book\tests + ripple\app\book @@ -3114,15 +3117,6 @@ ripple\app\transactors - - ripple\app\transactors - - - ripple\app\transactors - - - ripple\app\transactors - ripple\app\transactors diff --git a/src/BeastConfig.h b/src/BeastConfig.h index b0457379a43..b83ba814e95 100644 --- a/src/BeastConfig.h +++ b/src/BeastConfig.h @@ -186,7 +186,7 @@ This determines whether ripple implements offer autobridging via XRP. */ #ifndef RIPPLE_ENABLE_AUTOBRIDGING -#define RIPPLE_ENABLE_AUTOBRIDGING 0 +#define RIPPLE_ENABLE_AUTOBRIDGING 1 #endif /** Config: RIPPLE_SINGLE_IO_SERVICE_THREAD diff --git a/src/ripple/app/book/Offer.h b/src/ripple/app/book/Offer.h index f5aca5bc953..99e29dab085 100644 --- a/src/ripple/app/book/Offer.h +++ b/src/ripple/app/book/Offer.h @@ -30,6 +30,7 @@ #include #include +#include namespace ripple { namespace core { @@ -42,6 +43,9 @@ class Offer private: SLE::pointer m_entry; Quality m_quality; + Account m_account; + + mutable Amounts m_amounts; public: Offer() = default; @@ -49,6 +53,10 @@ class Offer Offer (SLE::pointer const& entry, Quality quality) : m_entry (entry) , m_quality (quality) + , m_account (m_entry->getFieldAccount160 (sfAccount)) + , m_amounts ( + m_entry->getFieldAmount (sfTakerPays), + m_entry->getFieldAmount (sfTakerGets)) { } @@ -62,46 +70,60 @@ class Offer original quality. */ Quality const - quality() const noexcept + quality () const noexcept { return m_quality; } /** Returns the account id of the offer's owner. */ - Account const - account() const + Account const& + owner () const { - return m_entry->getFieldAccount160 (sfAccount); + return m_account; } /** Returns the in and out amounts. Some or all of the out amount may be unfunded. */ - Amounts const - amount() const + Amounts const& + amount () const { - return Amounts ( - m_entry->getFieldAmount (sfTakerPays), - m_entry->getFieldAmount (sfTakerGets)); + return m_amounts; } /** Returns `true` if no more funds can flow through this offer. */ bool - fully_consumed() const + fully_consumed () const { - if (m_entry->getFieldAmount (sfTakerPays) <= zero) + if (m_amounts.in <= zero) return true; - if (m_entry->getFieldAmount (sfTakerGets) <= zero) + if (m_amounts.out <= zero) return true; return false; } - /** Returns the ledger entry underlying the offer. */ - // AVOID USING THIS - SLE::pointer - entry() const noexcept + /** Adjusts the offer to indicate that we consumed some (or all) of it. */ + void + consume (LedgerView& view, Amounts const& consumed) const + { + if (consumed.in > m_amounts.in) + throw std::logic_error ("can't consume more than is available."); + + if (consumed.out > m_amounts.out) + throw std::logic_error ("can't produce more than is available."); + + m_amounts.in -= consumed.in; + m_amounts.out -= consumed.out; + + m_entry->setFieldAmount (sfTakerPays, m_amounts.in); + m_entry->setFieldAmount (sfTakerGets, m_amounts.out); + + view.entryModify (m_entry); + } + + std::string id () const { - return m_entry; + return to_string (m_entry->getIndex()); } }; @@ -109,7 +131,7 @@ inline std::ostream& operator<< (std::ostream& os, Offer const& offer) { - return os << offer.entry()->getIndex(); + return os << offer.id (); } } diff --git a/src/ripple/app/book/OfferStream.h b/src/ripple/app/book/OfferStream.h index 025b6cfcb40..76de3e2db6e 100644 --- a/src/ripple/app/book/OfferStream.h +++ b/src/ripple/app/book/OfferStream.h @@ -104,17 +104,6 @@ class OfferStream */ bool step (); - - /** Advance to the next valid offer that is not from the specified account. - This automatically removes: - - Offers with missing ledger entries - - Offers found unfunded - - Offers from the same account - - Expired offers - @return `true` if there is a valid offer. - */ - bool - step_account (Account const& account); }; } diff --git a/src/ripple/app/book/Taker.h b/src/ripple/app/book/Taker.h index f7e15c6b700..4044f245b85 100644 --- a/src/ripple/app/book/Taker.h +++ b/src/ripple/app/book/Taker.h @@ -25,6 +25,7 @@ #include #include #include + #include #include @@ -32,85 +33,167 @@ namespace ripple { namespace core { /** State for the active party during order book or payment operations. */ -class Taker +class BasicTaker { -public: - struct Options +private: + class Rate { - Options() = delete; - - explicit - Options (std::uint32_t tx_flags) - : sell (tx_flags & tfSell) - , passive (tx_flags & tfPassive) - , fill_or_kill (tx_flags & tfFillOrKill) - , immediate_or_cancel (tx_flags & tfImmediateOrCancel) + private: + std::uint32_t quality_; + Amount rate_; + + public: + Rate (std::uint32_t quality) + : quality_ (quality) { + assert (quality_ != 0); + rate_ = amountFromRate (quality_); } - bool const sell; - bool const passive; - bool const fill_or_kill; - bool const immediate_or_cancel; + Amount + divide (Amount const& amount) const; + + Amount + multiply (Amount const& amount) const; }; private: - std::reference_wrapper m_view; - Account m_account; - Options m_options; - Quality m_quality; - Quality m_threshold; + Account account_; + Quality quality_; + Quality threshold_; + + bool sell_; // The original in and out quantities. - Amounts const m_amount; + Amounts const original_; // The amounts still left over for us to try and take. - Amounts m_remain; + Amounts remaining_; - Amounts - flow (Amounts amount, Offer const& offer, Account const& taker); + // The issuers for the input and output + Issue const& issue_in_; + Issue const& issue_out_; - TER - fill (Offer const& offer, Amounts const& amount); + // The rates that will be paid when the input and output currencies are + // transfer when the currency issuer isn't involved: + std::uint32_t const m_rate_in; + std::uint32_t const m_rate_out; - TER - fill (Offer const& leg1, Amounts const& amount1, - Offer const& leg2, Amounts const& amount2); + // The type of crossing that we are performing + CrossType cross_type_; - void - consume (Offer const& offer, Amounts const& consumed) const; +protected: + struct Flow + { + Amounts order; + Amounts issuers; -public: - Taker (LedgerView& view, Account const& account, - Amounts const& amount, Options const& options); + bool sanity_check () const + { + if (isXRP (order.in) && isXRP (order.out)) + return false; + + return order.in >= zero && + order.out >= zero && + issuers.in >= zero && + issuers.out >= zero; + } + }; + +private: + Flow + flow_xrp_to_iou (Amounts const& offer, Quality quality, + Amount const& owner_funds, Amount const& taker_funds, + Rate const& rate_out); + + Flow + flow_iou_to_xrp (Amounts const& offer, Quality quality, + Amount const& owner_funds, Amount const& taker_funds, + Rate const& rate_in); + + Flow + flow_iou_to_iou (Amounts const& offer, Quality quality, + Amount const& owner_funds, Amount const& taker_funds, + Rate const& rate_in, Rate const& rate_out); - LedgerView& - view () const noexcept + // Calculates the transfer rate that we should use when calculating + // flows for a particular issue between two accounts. + static + Rate + effective_rate (std::uint32_t rate, Issue const &issue, + Account const& from, Account const& to); + + // The transfer rate for the input currency between the given accounts + Rate + in_rate (Account const& from, Account const& to) const + { + return effective_rate (m_rate_in, original_.in.issue (), from, to); + } + + // The transfer rate for the output currency between the given accounts + Rate + out_rate (Account const& from, Account const& to) const { - return m_view; + return effective_rate (m_rate_out, original_.out.issue (), from, to); } +public: + BasicTaker () = delete; + BasicTaker (BasicTaker const&) = delete; + + BasicTaker ( + CrossType cross_type, Account const& account, Amounts const& amount, + Quality const& quality, std::uint32_t flags, std::uint32_t rate_in, + std::uint32_t rate_out); + + virtual ~BasicTaker () = default; + /** Returns the amount remaining on the offer. This is the amount at which the offer should be placed. It may either be for the full amount when there were no crossing offers, or for zero when the offer fully crossed, or any amount in between. - It is always at the original offer quality (m_quality) + It is always at the original offer quality (quality_) */ Amounts remaining_offer () const; + /** Returns the amount that the offer was originally placed at. */ + Amounts const& + original_offer () const; + /** Returns the account identifier of the taker. */ Account const& account () const noexcept { - return m_account; + return account_; } /** Returns `true` if the quality does not meet the taker's requirements. */ bool reject (Quality const& quality) const noexcept { - return quality < m_threshold; + return quality < threshold_; + } + + /** Returns the type of crossing that is being performed */ + CrossType + cross_type () const + { + return cross_type_; + } + + /** Returns the Issue associated with the input of the offer */ + Issue const& + issue_in () const + { + return issue_in_; + } + + /** Returns the Issue associated with the output of the offer */ + Issue const& + issue_out () const + { + return issue_out_; } /** Returns `true` if order crossing should not continue. @@ -121,24 +204,105 @@ class Taker done () const; /** Perform direct crossing through given offer. - @return tesSUCCESS on success, error code otherwise. + @return an `Amounts` describing the flow achieved during cross */ - TER - cross (Offer const& offer); + BasicTaker::Flow + do_cross (Amounts offer, Quality quality, Account const& owner); /** Perform bridged crossing through given offers. - @return tesSUCCESS on success, error code otherwise. + @return a pair of `Amounts` describing the flow achieved during cross */ - TER - cross (Offer const& leg1, Offer const& leg2); + std::pair + do_cross ( + Amounts offer1, Quality quality1, Account const& owner1, + Amounts offer2, Quality quality2, Account const& owner2); + + virtual + Amount + get_funds (Account const& account, Amount const& funds) const = 0; }; -inline -std::ostream& -operator<< (std::ostream& os, Taker const& taker) +class Taker + : public BasicTaker { - return os << taker.account(); -} +private: + static + std::uint32_t + calculateRate (LedgerView& view, Account const& issuer, Account const& account); + + // The underlying ledger entry we are dealing with + LedgerView& m_view; + + // The amount of XRP that flowed if we were autobridging + STAmount xrp_flow_; + + // The number direct crossings that we performed + std::uint32_t direct_crossings_; + + // The number autobridged crossings that we performed + std::uint32_t bridge_crossings_; + + TER + fill (BasicTaker::Flow const& flow, Offer const& offer); + + TER + fill ( + BasicTaker::Flow const& flow1, Offer const& leg1, + BasicTaker::Flow const& flow2, Offer const& leg2); + + TER + transfer_xrp (Account const& from, Account const& to, Amount const& amount); + + TER + redeem_iou (Account const& account, Amount const& amount, Issue const& issue); + + TER + issue_iou (Account const& account, Amount const& amount, Issue const& issue); + +public: + Taker () = delete; + Taker (Taker const&) = delete; + + Taker (CrossType cross_type, LedgerView& view, Account const& account, + Amounts const& offer, std::uint32_t flags); + ~Taker () = default; + + void + consume_offer (Offer const& offer, Amounts const& order); + + Amount + get_funds (Account const& account, Amount const& funds) const; + + STAmount const& + get_xrp_flow () const + { + return xrp_flow_; + } + + std::uint32_t + get_direct_crossings () const + { + return direct_crossings_; + } + + std::uint32_t + get_bridge_crossings () const + { + return bridge_crossings_; + } + + /** Perform a direct or bridged offer crossing as appropriate. + Funds will be transferred accordingly, and offers will be adjusted. + @return tesSUCCESS if successful, or an error code otherwise. + */ + /** @{ */ + TER + cross (Offer const& offer); + + TER + cross (Offer const& leg1, Offer const& leg2); + /** @} */ +}; } } diff --git a/src/ripple/app/book/Types.h b/src/ripple/app/book/Types.h index 79a035dd1d0..66945ac0adf 100644 --- a/src/ripple/app/book/Types.h +++ b/src/ripple/app/book/Types.h @@ -29,6 +29,14 @@ namespace ripple { namespace core { +/** The flavor of an offer crossing */ +enum class CrossType +{ + XrpToIou, + IouToXrp, + IouToIou +}; + /** A mutable view that overlays an immutable ledger to track changes. */ typedef LedgerEntrySet LedgerView; diff --git a/src/ripple/app/book/impl/OfferStream.cpp b/src/ripple/app/book/impl/OfferStream.cpp index a905ae1cd26..9af92f7c0ad 100644 --- a/src/ripple/app/book/impl/OfferStream.cpp +++ b/src/ripple/app/book/impl/OfferStream.cpp @@ -125,7 +125,7 @@ OfferStream::step () // NIKB NOTE The calling code also checks the funds, how expensive is // looking up the funds twice? Amount const owner_funds (view().accountFunds ( - m_offer.account(), m_offer.amount().out, fhZERO_IF_FROZEN)); + m_offer.owner(), amount.out, fhZERO_IF_FROZEN)); // Check for unfunded offer if (owner_funds <= zero) @@ -133,8 +133,10 @@ OfferStream::step () // If the owner's balance in the pristine view is the same, // we haven't modified the balance and therefore the // offer is "found unfunded" versus "became unfunded" - if (view_cancel().accountFunds (m_offer.account(), - m_offer.amount().out, fhZERO_IF_FROZEN) == owner_funds) + auto const original_funds = view_cancel().accountFunds ( + m_offer.owner(), amount.out, fhZERO_IF_FROZEN); + + if (original_funds == owner_funds) { view_cancel().offerDelete (entry->getIndex()); if (m_journal.trace) m_journal.trace << @@ -155,17 +157,5 @@ OfferStream::step () return true; } -bool -OfferStream::step_account (Account const& account) -{ - while (step ()) - { - if (tip ().account () != account) - return true; - } - - return false; -} - } } diff --git a/src/ripple/app/book/impl/Taker.cpp b/src/ripple/app/book/impl/Taker.cpp index c1abc1bc5ec..ab98ed6c915 100644 --- a/src/ripple/app/book/impl/Taker.cpp +++ b/src/ripple/app/book/impl/Taker.cpp @@ -23,261 +23,626 @@ namespace ripple { namespace core { -Taker::Taker (LedgerView& view, Account const& account, - Amounts const& amount, Options const& options) - : m_view (view) - , m_account (account) - , m_options (options) - , m_quality (amount) - , m_threshold (m_quality) - , m_amount (amount) - , m_remain (amount) +Amount +BasicTaker::Rate::divide (Amount const& amount) const { - assert (m_remain.in > zero); - assert (m_remain.out > zero); + if (quality_ == QUALITY_ONE) + return amount; - // If this is a passive order (tfPassive), this prevents - // offers at the same quality level from being consumed. - if (m_options.passive) - ++m_threshold; + return ripple::divide (amount, rate_, amount.issue ()); +} + +Amount +BasicTaker::Rate::multiply (Amount const& amount) const +{ + if (quality_ == QUALITY_ONE) + return amount; + + return ripple::multiply (amount, rate_, amount.issue ()); +} + +BasicTaker::BasicTaker ( + CrossType cross_type, Account const& account, Amounts const& amount, + Quality const& quality, std::uint32_t flags, std::uint32_t rate_in, + std::uint32_t rate_out) + : account_ (account) + , quality_ (quality) + , threshold_ (quality_) + , sell_ (flags & tfSell) + , original_ (amount) + , remaining_ (amount) + , issue_in_ (remaining_.in.issue ()) + , issue_out_ (remaining_.out.issue ()) + , m_rate_in (rate_in) + , m_rate_out (rate_out) + , cross_type_ (cross_type) +{ + assert (remaining_.in > zero); + assert (remaining_.out > zero); + + assert (m_rate_in != 0); + assert (m_rate_out != 0); + + // If we are dealing with a particular flavor, make sure that it's the + // flavor we expect: + assert (cross_type != CrossType::XrpToIou || + (isXRP (issue_in ()) && !isXRP (issue_out ()))); + + assert (cross_type != CrossType::IouToXrp || + (!isXRP (issue_in ()) && isXRP (issue_out ()))); + + // And make sure we're not crossing XRP for XRP + assert (!isXRP (issue_in ()) || !isXRP (issue_out ())); + + // If this is a passive order, we adjust the quality so as to prevent offers + // at the same quality level from being consumed. + if (flags & tfPassive) + ++threshold_; +} + +BasicTaker::Rate +BasicTaker::effective_rate ( + std::uint32_t rate, Issue const &issue, + Account const& from, Account const& to) +{ + assert (rate != 0); + + if (rate != QUALITY_ONE) + { + // We ignore the transfer if the sender is also the recipient since no + // actual transfer takes place in that case. We also ignore if either + // the sender or the receiver is the issuer. + + if (from != to && from != issue.account && to != issue.account) + return Rate (rate); + } + + return Rate (QUALITY_ONE); +} + +bool +BasicTaker::done () const +{ + // We are done if we have consumed all the input currency + if (remaining_.in <= zero) + return true; + + // We are done if using buy semantics and we received the + // desired amount of output currency + if (!sell_ && (remaining_.out <= zero)) + return true; + + // We are done if the taker is out of funds + if (get_funds (account(), remaining_.in) <= zero) + return true; + + return false; } Amounts -Taker::remaining_offer () const +BasicTaker::remaining_offer () const { // If the taker is done, then there's no offer to place. if (done ()) - return Amounts (m_amount.in.zeroed(), m_amount.out.zeroed()); + return Amounts (remaining_.in.zeroed(), remaining_.out.zeroed()); // Avoid math altogether if we didn't cross. - if (m_amount == m_remain) - return m_amount; + if (original_ == remaining_) + return original_; - if (m_options.sell) + if (sell_) { - assert (m_remain.in > zero); + assert (remaining_.in > zero); // We scale the output based on the remaining input: - return Amounts (m_remain.in, divRound ( - m_remain.in, m_quality.rate (), m_remain.out, true)); + return Amounts (remaining_.in, divRound ( + remaining_.in, quality_.rate (), remaining_.out, true)); } - assert (m_remain.out > zero); + assert (remaining_.out > zero); // We scale the input based on the remaining output: return Amounts (mulRound ( - m_remain.out, m_quality.rate (), m_remain.in, true), m_remain.out); + remaining_.out, quality_.rate (), remaining_.in, true), remaining_.out); } -/** Calculate the amount particular user could get through an offer. - @param amount the maximum flow that is available to the taker. - @param offer the offer to flow through. - @param taker the person taking the offer. - @return the maximum amount that can flow through this offer. -*/ -Amounts -Taker::flow (Amounts amount, Offer const& offer, Account const& taker) +Amounts const& +BasicTaker::original_offer () const +{ + return original_; +} + +// TODO: the presence of 'output' is an artifact caused by the fact that +// Amounts carry issue information which should be decoupled. +static +Amount +qual_div (Amount const& amount, Quality const& quality, Amount const& output) +{ + auto result = divide (amount, quality.rate (), output.issue ()); + return std::min (result, output); +} + +static +Amount +qual_mul (Amount const& amount, Quality const& quality, Amount const& output) { - // Limit taker's input by available funds less fees - Amount const taker_funds (view ().accountFunds ( - taker, amount.in, fhZERO_IF_FROZEN)); + auto result = multiply (amount, quality.rate (), output.issue ()); + return std::min (result, output); +} - // Get fee rate paid by taker - std::uint32_t const taker_charge_rate (rippleTransferRate (view (), - taker, offer.account (), amount.in.getIssuer())); +BasicTaker::Flow +BasicTaker::flow_xrp_to_iou ( + Amounts const& order, Quality quality, + Amount const& owner_funds, Amount const& taker_funds, + Rate const& rate_out) +{ + Flow f; + f.order = order; + f.issuers.out = rate_out.multiply (f.order.out); - // Skip some math when there's no fee - if (taker_charge_rate == QUALITY_ONE) + // Clamp on owner balance + if (owner_funds < f.issuers.out) { - amount = offer.quality ().ceil_in (amount, taker_funds); + f.issuers.out = owner_funds; + f.order.out = rate_out.divide (f.issuers.out); + f.order.in = qual_mul (f.order.out, quality, f.order.in); } - else + + // Clamp if taker wants to limit the output + if (!sell_ && remaining_.out < f.order.out) { - Amount const taker_charge (amountFromRate (taker_charge_rate)); - amount = offer.quality ().ceil_in (amount, - divide (taker_funds, taker_charge)); + f.order.out = remaining_.out; + f.order.in = qual_mul (f.order.out, quality, f.order.in); + f.issuers.out = rate_out.multiply (f.order.out); } - // Best flow the owner can get. - // Start out assuming entire offer will flow. - Amounts owner_amount (amount); + // Clamp on the taker's funds + if (taker_funds < f.order.in) + { + f.order.in = taker_funds; + f.order.out = qual_div (f.order.in, quality, f.order.out); + f.issuers.out = rate_out.multiply (f.order.out); + } - // Limit owner's output by available funds less fees - Amount const owner_funds (view ().accountFunds ( - offer.account (), owner_amount.out, fhZERO_IF_FROZEN)); + // Clamp on remaining offer if we are not handling the second leg + // of an autobridge. + if (cross_type_ == CrossType::XrpToIou && (remaining_.in < f.order.in)) + { + f.order.in = remaining_.in; + f.order.out = qual_div (f.order.in, quality, f.order.out); + f.issuers.out = rate_out.multiply (f.order.out); + } + + return f; +} - // Get fee rate paid by owner - std::uint32_t const owner_charge_rate (rippleTransferRate (view (), - offer.account (), taker, amount.out.getIssuer())); +BasicTaker::Flow +BasicTaker::flow_iou_to_xrp ( + Amounts const& order, Quality quality, + Amount const& owner_funds, Amount const& taker_funds, + Rate const& rate_in) +{ + Flow f; + f.order = order; + f.issuers.in = rate_in.multiply (f.order.in); - if (owner_charge_rate == QUALITY_ONE) + // Clamp on owner's funds + if (owner_funds < f.order.out) { - // Skip some math when there's no fee - owner_amount = offer.quality ().ceil_out (owner_amount, owner_funds); + f.order.out = owner_funds; + f.order.in = qual_mul (f.order.out, quality, f.order.in); + f.issuers.in = rate_in.multiply (f.order.in); } - else + + // Clamp if taker wants to limit the output and we are not the + // first leg of an autobridge. + if (!sell_ && cross_type_ == CrossType::IouToXrp) + { + if (remaining_.out < f.order.out) + { + f.order.out = remaining_.out; + f.order.in = qual_mul (f.order.out, quality, f.order.in); + f.issuers.in = rate_in.multiply (f.order.in); + } + } + + // Clamp on the taker's input offer + if (remaining_.in < f.order.in) + { + f.order.in = remaining_.in; + f.issuers.in = rate_in.multiply (f.order.in); + f.order.out = qual_div (f.order.in, quality, f.order.out); + } + + // Clamp on the taker's input balance + if (taker_funds < f.issuers.in) { - Amount const owner_charge (amountFromRate (owner_charge_rate)); - owner_amount = offer.quality ().ceil_out (owner_amount, - divide (owner_funds, owner_charge)); + f.issuers.in = taker_funds; + f.order.in = rate_in.divide (f.issuers.in); + f.order.out = qual_div (f.order.in, quality, f.order.out); } - // Calculate the amount that will flow through the offer - // This does not include the fees. - return (owner_amount.in < amount.in) - ? owner_amount - : amount; + return f; } -// Adjust an offer to indicate that we are consuming some (or all) of it. -void -Taker::consume (Offer const& offer, Amounts const& consumed) const +BasicTaker::Flow +BasicTaker::flow_iou_to_iou ( + Amounts const& order, Quality quality, + Amount const& owner_funds, Amount const& taker_funds, + Rate const& rate_in, Rate const& rate_out) { - Amounts const& remaining (offer.amount ()); + Flow f; + f.order = order; + f.issuers.in = rate_in.multiply (f.order.in); + f.issuers.out = rate_out.multiply (f.order.out); - assert (remaining.in > zero && remaining.out > zero); - assert (remaining.in >= consumed.in && remaining.out >= consumed.out); + // Clamp on owner balance + if (owner_funds < f.issuers.out) + { + f.issuers.out = owner_funds; + f.order.out = rate_out.divide (f.issuers.out); + f.order.in = qual_mul (f.order.out, quality, f.order.in); + f.issuers.in = rate_in.multiply (f.order.in); + } - offer.entry ()->setFieldAmount (sfTakerPays, remaining.in - consumed.in); - offer.entry ()->setFieldAmount (sfTakerGets, remaining.out - consumed.out); + // Clamp on taker's offer + if (!sell_ && remaining_.out < f.order.out) + { + f.order.out = remaining_.out; + f.order.in = qual_mul (f.order.out, quality, f.order.in); + f.issuers.out = rate_out.multiply (f.order.out); + f.issuers.in = rate_in.multiply (f.order.in); + } - view ().entryModify (offer.entry()); + // Clamp on the taker's input balance and input offer + if (remaining_.in < f.order.in) + { + f.order.in = remaining_.in; + f.issuers.in = rate_in.multiply (f.order.in); + f.order.out = qual_div (f.order.in, quality, f.order.out); + f.issuers.out = rate_out.multiply (f.order.out); + } + + if (taker_funds < f.order.in) + { + f.issuers.in = taker_funds; + f.order.in = rate_in.divide (f.issuers.in); + f.order.out = qual_div (f.order.in, quality, f.order.out); + f.issuers.out = rate_out.multiply (f.order.out); + } - assert (offer.entry ()->getFieldAmount (sfTakerPays) >= zero); - assert (offer.entry ()->getFieldAmount (sfTakerGets) >= zero); + return f; } -// Fill a direct offer. -// @param offer the offer we are going to use. -// @param amount the amount to flow through the offer. -// @returns: tesSUCCESS if successful, or an error code otherwise. -TER -Taker::fill (Offer const& offer, Amounts const& amount) +// Calculates the direct flow through the specified offer +BasicTaker::Flow +BasicTaker::do_cross (Amounts offer, Quality quality, Account const& owner) { - consume (offer, amount); + assert (!done ()); - // Pay the taker, then the owner - TER result = view ().accountSend (offer.account(), account(), amount.out); + auto const owner_funds = get_funds (owner, offer.out); + auto const taker_funds = get_funds (account (), offer.in); - if (result == tesSUCCESS) - result = view ().accountSend (account(), offer.account(), amount.in); + Flow result; + + if (cross_type_ == CrossType::XrpToIou) + { + result = flow_xrp_to_iou (offer, quality, owner_funds, taker_funds, + out_rate (owner, account ())); + } + else if (cross_type_ == CrossType::IouToXrp) + { + result = flow_iou_to_xrp (offer, quality, owner_funds, taker_funds, + in_rate (owner, account ())); + } + else + { + result = flow_iou_to_iou (offer, quality, owner_funds, taker_funds, + in_rate (owner, account ()), out_rate (owner, account ())); + } + + if (!result.sanity_check ()) + throw std::logic_error ("Computed flow fails sanity check."); + + remaining_.out -= result.order.out; + remaining_.in -= result.order.in; + + assert (remaining_.in >= zero); return result; } -// Fill a bridged offer. -// @param leg1 the first leg we are going to use. -// @param amount1 the amount to flow through the first leg of the offer. -// @param leg2 the second leg we are going to use. -// @param amount2 the amount to flow through the second leg of the offer. -// @return tesSUCCESS if successful, or an error code otherwise. -TER -Taker::fill ( - Offer const& leg1, Amounts const& amount1, - Offer const& leg2, Amounts const& amount2) +// Calculates the bridged flow through the specified offers +std::pair +BasicTaker::do_cross ( + Amounts offer1, Quality quality1, Account const& owner1, + Amounts offer2, Quality quality2, Account const& owner2) { - assert (amount1.out == amount2.in); + assert (!done ()); - consume (leg1, amount1); - consume (leg2, amount2); + assert (!offer1.in.isNative ()); + assert (offer1.out.isNative ()); + assert (offer2.in.isNative ()); + assert (!offer2.out.isNative ()); - /* It is possible that m_account is the same as leg1.account, leg2.account - * or both. This could happen when bridging over one's own offer. In that - * case, accountSend won't actually do a send, which is what we want. - */ - TER result = view ().accountSend (m_account, leg1.account (), amount1.in); + // If the taker owns the first leg of the offer, then the taker's available + // funds aren't the limiting factor for the input - the offer itself is. + auto leg1_in_funds = get_funds (account (), offer1.in); - if (result == tesSUCCESS) - result = view ().accountSend (leg1.account (), leg2.account (), amount1.out); + if (account () == owner1) + leg1_in_funds = std::max (leg1_in_funds, offer1.in); - if (result == tesSUCCESS) - result = view ().accountSend (leg2.account (), m_account, amount2.out); + // If the taker owns the second leg of the offer, then the taker's available + // funds are not the limiting factor for the output - the offer itself is. + auto leg2_out_funds = get_funds (owner2, offer2.out); - return result; -} + if (account () == owner2) + leg2_out_funds = std::max (leg2_out_funds, offer2.out); -bool -Taker::done () const -{ - if (m_options.sell && (m_remain.in <= zero)) + // The amount available to flow via XRP is the amount that the owner of the + // first leg of the bridge has, up to the first leg's output. + // + // But, when both legs of a bridge are owned by the same person, the amount + // of XRP that can flow between the two legs is, essentially, infinite + // since all the owner is doing is taking out XRP of his left pocket + // and putting it in his right pocket. In that case, we set the available + // XRP to the largest of the two offers. + auto xrp_funds = get_funds (owner1, offer1.out); + + if (owner1 == owner2) + xrp_funds = std::max (offer1.out, offer2.in); + + auto const leg1_rate = in_rate (owner1, account ()); + auto const leg2_rate = out_rate (owner2, account ()); + + // Attempt to determine the maximal flow that can be achieved across each + // leg independent of the other. + auto flow1 = flow_iou_to_xrp (offer1, quality1, xrp_funds, leg1_in_funds, leg1_rate); + + if (!flow1.sanity_check ()) + throw std::logic_error ("Computed flow1 fails sanity check."); + + auto flow2 = flow_xrp_to_iou (offer2, quality2, leg2_out_funds, xrp_funds, leg2_rate); + + if (!flow2.sanity_check ()) + throw std::logic_error ("Computed flow2 fails sanity check."); + + // We now have the maximal flows across each leg individually. We need to + // equalize them, so that the amount of XRP that flows out of the first leg + // is the same as the amount of XRP that flows into the second leg. We take + // the side which is the limiting factor (if any) and adjust the other. + if (flow1.order.out < flow2.order.in) { - // Sell semantics: we consumed all the input currency - return true; + // Adjust the second leg of the offer down: + flow2.order.in = flow1.order.out; + flow2.order.out = qual_div (flow2.order.in, quality2, flow2.order.out); + flow2.issuers.out = leg2_rate.multiply (flow2.order.out); } - - if (!m_options.sell && (m_remain.out <= zero)) + else if (flow1.order.out > flow2.order.in) { - // Buy semantics: we received the desired amount of output currency - return true; + // Adjust the first leg of the offer down: + flow1.order.out = flow2.order.in; + flow1.order.in = qual_mul (flow1.order.out, quality1, flow1.order.in); + flow1.issuers.in = leg1_rate.multiply (flow1.order.in); } - // We are finished if the taker is out of funds - return view().accountFunds ( - account(), m_remain.in, fhZERO_IF_FROZEN) <= zero; + if (flow1.order.out != flow2.order.in) + throw std::logic_error ("Bridged flow is out of balance."); + + remaining_.out -= flow2.order.out; + remaining_.in -= flow1.order.in; + + return std::make_pair (flow1, flow2); } -TER -Taker::cross (Offer const& offer) +//============================================================================== + +std::uint32_t +Taker::calculateRate ( + LedgerView& view, Account const& issuer, Account const& account) { - assert (!done ()); + return isXRP (issuer) || (account == issuer) + ? QUALITY_ONE + : rippleTransferRate (view, issuer); +} + +Taker::Taker (CrossType cross_type, LedgerView& view, Account const& account, + Amounts const& offer, std::uint32_t flags) + : BasicTaker (cross_type, account, offer, Quality(offer), flags, + calculateRate(view, offer.in.getIssuer(), account), + calculateRate(view, offer.out.getIssuer(), account)) + , m_view (view) + , xrp_flow_ (0) + , direct_crossings_ (0) + , bridge_crossings_ (0) +{ + assert (issue_in () == offer.in.issue ()); + assert (issue_out () == offer.out.issue ()); +} - /* Before we call flow we must set the limit right; for buy semantics we - need to clamp the output. And we always want to clamp the input. - */ - Amounts limit (offer.amount()); +void +Taker::consume_offer (Offer const& offer, Amounts const& order) +{ + if (order.in < zero) + throw std::logic_error ("flow with negative input."); + + if (order.out < zero) + throw std::logic_error ("flow with negative output."); + + return offer.consume (m_view, order); +} + +Amount +Taker::get_funds (Account const& account, Amount const& funds) const +{ + return m_view.accountFunds (account, funds, fhZERO_IF_FROZEN); +} + +TER Taker::transfer_xrp ( + Account const& from, + Account const& to, + Amount const& amount) +{ + if (!isXRP (amount)) + throw std::logic_error ("Using transfer_xrp with IOU"); + + if (from == to) + return tesSUCCESS; - if (! m_options.sell) - limit = offer.quality ().ceil_out (limit, m_remain.out); - limit = offer.quality().ceil_in (limit, m_remain.in); + return m_view.transfer_xrp (from, to, amount); +} + +TER Taker::redeem_iou ( + Account const& account, + Amount const& amount, + Issue const& issue) +{ + if (isXRP (amount)) + throw std::logic_error ("Using redeem_iou with XRP"); - assert (limit.in <= offer.amount().in); - assert (limit.out <= offer.amount().out); - assert (limit.in <= m_remain.in); + if (account == issue.account) + return tesSUCCESS; - Amounts const amount (flow (limit, offer, account ())); + return m_view.redeem_iou (account, amount, issue); +} + +TER Taker::issue_iou ( + Account const& account, + Amount const& amount, + Issue const& issue) +{ + if (isXRP (amount)) + throw std::logic_error ("Using issue_iou with XRP"); - m_remain.out -= amount.out; - m_remain.in -= amount.in; + if (account == issue.account) + return tesSUCCESS; - assert (m_remain.in >= zero); - return fill (offer, amount); + return m_view.issue_iou (account, amount, issue); } +// Performs funds transfers to fill the given offer and adjusts offer. TER -Taker::cross (Offer const& leg1, Offer const& leg2) +Taker::fill (BasicTaker::Flow const& flow, Offer const& offer) { - assert (!done ()); + // adjust offer + consume_offer (offer, flow.order); - assert (leg1.amount ().out.isNative ()); - assert (leg2.amount ().in.isNative ()); + TER result = tesSUCCESS; - Amounts amount1 (leg1.amount()); - Amounts amount2 (leg2.amount()); + if (cross_type () != CrossType::XrpToIou) + { + assert (!isXRP (flow.order.in)); - if (m_options.sell) - amount1 = leg1.quality().ceil_in (amount1, m_remain.in); + if(result == tesSUCCESS) + result = redeem_iou (account (), flow.issuers.in, flow.issuers.in.issue ()); + + if (result == tesSUCCESS) + result = issue_iou (offer.owner (), flow.order.in, flow.order.in.issue ()); + } else - amount2 = leg2.quality().ceil_out (amount2, m_remain.out); + { + assert (isXRP (flow.order.in)); + + if (result == tesSUCCESS) + result = transfer_xrp (account (), offer.owner (), flow.order.in); + } + + // Now send funds from the account whose offer we're taking + if (cross_type () != CrossType::IouToXrp) + { + assert (!isXRP (flow.order.out)); + + if(result == tesSUCCESS) + result = redeem_iou (offer.owner (), flow.issuers.out, flow.issuers.out.issue ()); - if (amount1.out <= amount2.in) - amount2 = leg2.quality().ceil_in (amount2, amount1.out); + if (result == tesSUCCESS) + result = issue_iou (account (), flow.order.out, flow.order.out.issue ()); + } else - amount1 = leg1.quality().ceil_out (amount1, amount2.in); + { + assert (isXRP (flow.order.out)); + + if (result == tesSUCCESS) + result = transfer_xrp (offer.owner (), account (), flow.order.out); + } + + if (result == tesSUCCESS) + direct_crossings_++; + + return result; +} + +// Performs bridged funds transfers to fill the given offers and adjusts offers. +TER +Taker::fill ( + BasicTaker::Flow const& flow1, Offer const& leg1, + BasicTaker::Flow const& flow2, Offer const& leg2) +{ + // Adjust offers accordingly + consume_offer (leg1, flow1.order); + consume_offer (leg2, flow2.order); + + TER result = tesSUCCESS; + + // Taker to leg1: IOU + if (leg1.owner () != account ()) + { + if (result == tesSUCCESS) + result = redeem_iou (account (), flow1.issuers.in, flow1.issuers.in.issue ()); - assert (amount1.out == amount2.in); + if (result == tesSUCCESS) + result = issue_iou (leg1.owner (), flow1.order.in, flow1.order.in.issue ()); + } + + // leg1 to leg2: bridging over XRP + if (result == tesSUCCESS) + result = transfer_xrp (leg1.owner (), leg2.owner (), flow1.order.out); + + // leg2 to Taker: IOU + if (leg2.owner () != account ()) + { + if (result == tesSUCCESS) + result = redeem_iou (leg2.owner (), flow2.issuers.out, flow2.issuers.out.issue ()); + + if (result == tesSUCCESS) + result = issue_iou (account (), flow2.order.out, flow2.order.out.issue ()); + } + + if (result == tesSUCCESS) + { + bridge_crossings_++; + xrp_flow_ += flow1.order.out; + } - // As written, flow can't handle a 3-party transfer, but this works for - // us because the output of leg1 and the input leg2 are XRP. - Amounts flow1 (flow (amount1, leg1, m_account)); + return result; +} + +TER +Taker::cross (Offer const& offer) +{ + // In direct crossings, at least one leg must not be XRP. + if (isXRP (offer.amount ().in) && isXRP (offer.amount ().out)) + return tefINTERNAL; - amount2 = leg2.quality().ceil_in (amount2, flow1.out); + auto const amount = do_cross ( + offer.amount (), offer.quality (), offer.owner ()); - Amounts flow2 (flow (amount2, leg2, m_account)); + return fill (amount, offer); +} + +TER +Taker::cross (Offer const& leg1, Offer const& leg2) +{ + // In bridged crossings, XRP must can't be the input to the first leg + // or the output of the second leg. + if (isXRP (leg1.amount ().in) || isXRP (leg2.amount ().out)) + return tefINTERNAL; - m_remain.out -= amount2.out; - m_remain.in -= amount1.in; + auto ret = do_cross ( + leg1.amount (), leg1.quality (), leg1.owner (), + leg2.amount (), leg2.quality (), leg2.owner ()); - return fill (leg1, flow1, leg2, flow2); + return fill (ret.first, leg1, ret.second, leg2); } } diff --git a/src/ripple/app/book/tests/Taker.test.cpp b/src/ripple/app/book/tests/Taker.test.cpp new file mode 100644 index 00000000000..126ff5c3dff --- /dev/null +++ b/src/ripple/app/book/tests/Taker.test.cpp @@ -0,0 +1,373 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012, 2013 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 +#include + +#include +#include + +#include + + +namespace ripple { +namespace core { + +class Taker_test : public beast::unit_test::suite +{ + static bool const Buy = false; + static bool const Sell = true; + + class TestTaker + : public BasicTaker + { + Amount funds_; + Amount cross_funds; + + public: + TestTaker ( + CrossType cross_type, Amounts const& amount, Quality const& quality, + Amount const& funds, std::uint32_t flags, std::uint32_t rate_in, + std::uint32_t rate_out) + : BasicTaker (cross_type, Account(0x4701), amount, quality, flags, rate_in, rate_out) + , funds_ (funds) + { + } + + void + set_funds (Amount const& funds) + { + cross_funds = funds; + } + + Amount + get_funds (Account const& owner, Amount const& funds) const + { + if (owner == account ()) + return funds_; + + return cross_funds; + } + + Amounts + cross (Amounts offer, Quality quality) + { + if (reject (quality)) + return Amounts (offer.in.zeroed (), offer.out.zeroed ()); + + // we need to emulate "unfunded offers" behavior + if (get_funds (Account (0x4702), offer.out) == zero) + return Amounts (offer.in.zeroed (), offer.out.zeroed ()); + + if (done ()) + return Amounts (offer.in.zeroed (), offer.out.zeroed ()); + + auto result = do_cross (offer, quality, Account (0x4702)); + + funds_ -= result.order.in; + + return result.order; + } + + std::pair + cross (Amounts offer1, Quality quality1, Amounts offer2, Quality quality2) + { + /* check if composed quality should be rejected */ + Quality const quality (core::composed_quality (quality1, quality2)); + + if (reject (quality)) + return std::make_pair( + Amounts { offer1.in.zeroed (), offer1.out.zeroed () }, + Amounts { offer2.in.zeroed (), offer2.out.zeroed () }); + + if (done ()) + return std::make_pair( + Amounts { offer1.in.zeroed (), offer1.out.zeroed () }, + Amounts { offer2.in.zeroed (), offer2.out.zeroed () }); + + auto result = do_cross ( + offer1, quality1, Account (0x4703), + offer2, quality2, Account (0x4704)); + + return std::make_pair (result.first.order, result.second.order); + } + }; + +private: + Issue const& usd () const + { + static Issue const issue ( + Currency (0x5553440000000000), Account (0x4985601)); + return issue; + } + + Issue const& eur () const + { + static Issue const issue ( + Currency (0x4555520000000000), Account (0x4985602)); + return issue; + } + + Issue const& xrp () const + { + static Issue const issue ( + xrpCurrency (), xrpAccount ()); + return issue; + } + + Amount parse_amount (std::string const& amount, Issue const& issue) + { + Amount result (issue); + expect (result.setValue (amount), amount); + return result; + } + + Amounts parse_amounts ( + std::string const& amount_in, Issue const& issue_in, + std::string const& amount_out, Issue const& issue_out) + { + Amount const in (parse_amount (amount_in, issue_in)); + Amount const out (parse_amount (amount_out, issue_out)); + + return { in, out }; + } + + struct cross_attempt_offer + { + cross_attempt_offer (std::string const& in_, + std::string const &out_) + : in (in_) + , out (out_) + { + } + + std::string in; + std::string out; + }; + +private: + std::string + format_amount (STAmount const& amount) + { + std::string txt = amount.getText (); + txt += "/"; + txt += amount.getHumanCurrency (); + return txt; + } + + void + attempt ( + bool sell, + std::string name, + Quality taker_quality, + cross_attempt_offer const offer, + std::string const funds, + Quality cross_quality, + cross_attempt_offer const cross, + std::string const cross_funds, + cross_attempt_offer const flow, + Issue const& issue_in, + Issue const& issue_out, + std::uint32_t rate_in = QUALITY_ONE, + std::uint32_t rate_out = QUALITY_ONE) + { + Amounts taker_offer (parse_amounts ( + offer.in, issue_in, + offer.out, issue_out)); + + Amounts cross_offer (parse_amounts ( + cross.in, issue_in, + cross.out, issue_out)); + + CrossType cross_type; + + if (isXRP (issue_out)) + cross_type = core::CrossType::IouToXrp; + else if (isXRP (issue_in)) + cross_type = core::CrossType::XrpToIou; + else + cross_type = CrossType::IouToIou; + + // FIXME: We are always invoking the IOU-to-IOU taker. We should select + // the correct type dynamically. + TestTaker taker (cross_type, taker_offer, taker_quality, + parse_amount (funds, issue_in), sell ? tfSell : 0, + rate_in, rate_out); + + taker.set_funds (parse_amount (cross_funds, issue_out)); + + auto result = taker.cross (cross_offer, cross_quality); + + Amounts const expected (parse_amounts ( + flow.in, issue_in, + flow.out, issue_out)); + + expect (expected == result, name + (sell ? " (s)" : " (b)")); + + if (expected != result) + { + log << + "Expected: " << format_amount (expected.in) << + " : " << format_amount (expected.out); + log << + " Actual: " << format_amount (result.in) << + " : " << format_amount (result.out); + } + + assert (expected == result); + } + + Quality get_quality(std::string in, std::string out) + { + return Quality (parse_amounts (in, xrp(), out, xrp ())); + } + +public: + Taker_test () + { + } + + // Notation for clamp scenario descriptions: + // + // IN:OUT (with the last in the list being limiting factor) + // N = Nothing + // T = Taker Offer Balance + // A = Taker Account Balance + // B = Owner Account Balance + // + // (s) = sell semantics: taker wants unlimited output + // (b) = buy semantics: taker wants a limited amount out + + // NIKB TODO: Augment TestTaker so currencies and rates can be specified + // once without need for repetition. + void + test_xrp_to_iou () + { + testcase ("XRP Quantization: input"); + + Quality q1 = get_quality ("1", "1"); + + // TAKER OWNER + // QUAL OFFER FUNDS QUAL OFFER FUNDS EXPECTED + // XRP USD + attempt (Sell, "N:N", q1, { "2", "2" }, "2", q1, { "2", "2" }, "2", { "2", "2" }, xrp(), usd()); + attempt (Sell, "N:B", q1, { "2", "2" }, "2", q1, { "2", "2" }, "1.8", { "1", "1.8" }, xrp(), usd()); + attempt (Buy, "N:T", q1, { "1", "1" }, "2", q1, { "2", "2" }, "2", { "1", "1" }, xrp(), usd()); + attempt (Buy, "N:BT", q1, { "1", "1" }, "2", q1, { "2", "2" }, "1.8", { "1", "1" }, xrp(), usd()); + attempt (Buy, "N:TB", q1, { "1", "1" }, "2", q1, { "2", "2" }, "0.8", { "0", "0.8" }, xrp(), usd()); + + attempt (Sell, "T:N", q1, { "1", "1" }, "2", q1, { "2", "2" }, "2", { "1", "1" }, xrp(), usd()); + attempt (Sell, "T:B", q1, { "1", "1" }, "2", q1, { "2", "2" }, "1.8", { "1", "1.8" }, xrp(), usd()); + attempt (Buy, "T:T", q1, { "1", "1" }, "2", q1, { "2", "2" }, "2", { "1", "1" }, xrp(), usd()); + attempt (Buy, "T:BT", q1, { "1", "1" }, "2", q1, { "2", "2" }, "1.8", { "1", "1" }, xrp(), usd()); + attempt (Buy, "T:TB", q1, { "1", "1" }, "2", q1, { "2", "2" }, "0.8", { "0", "0.8" }, xrp(), usd()); + + attempt (Sell, "A:N", q1, { "2", "2" }, "1", q1, { "2", "2" }, "2", { "1", "1" }, xrp(), usd()); + attempt (Sell, "A:B", q1, { "2", "2" }, "1", q1, { "2", "2" }, "1.8", { "1", "1.8" }, xrp(), usd()); + attempt (Buy, "A:T", q1, { "2", "2" }, "1", q1, { "3", "3" }, "3", { "1", "1" }, xrp(), usd()); + attempt (Buy, "A:BT", q1, { "2", "2" }, "1", q1, { "3", "3" }, "2.4", { "1", "1" }, xrp(), usd()); + attempt (Buy, "A:TB", q1, { "2", "2" }, "1", q1, { "3", "3" }, "0.8", { "0", "0.8" }, xrp(), usd()); + + attempt (Sell, "TA:N", q1, { "2", "2" }, "1", q1, { "2", "2" }, "2", { "1", "1" }, xrp(), usd()); + attempt (Sell, "TA:B", q1, { "2", "2" }, "1", q1, { "3", "3" }, "1.8", { "1", "1.8" }, xrp(), usd()); + attempt (Buy, "TA:T", q1, { "2", "2" }, "1", q1, { "3", "3" }, "3", { "1", "1" }, xrp(), usd()); + attempt (Buy, "TA:BT", q1, { "2", "2" }, "1", q1, { "3", "3" }, "1.8", { "1", "1.8" }, xrp(), usd()); + attempt (Buy, "TA:TB", q1, { "2", "2" }, "1", q1, { "3", "3" }, "1.8", { "1", "1.8" }, xrp(), usd()); + + attempt (Sell, "AT:N", q1, { "2", "2" }, "1", q1, { "3", "3" }, "3", { "1", "1" }, xrp(), usd()); + attempt (Sell, "AT:B", q1, { "2", "2" }, "1", q1, { "3", "3" }, "1.8", { "1", "1.8" }, xrp(), usd()); + attempt (Buy, "AT:T", q1, { "2", "2" }, "1", q1, { "3", "3" }, "3", { "1", "1" }, xrp(), usd()); + attempt (Buy, "AT:BT", q1, { "2", "2" }, "1", q1, { "3", "3" }, "1.8", { "1", "1.8" }, xrp(), usd()); + attempt (Buy, "AT:TB", q1, { "2", "2" }, "1", q1, { "3", "3" }, "0.8", { "0", "0.8" }, xrp(), usd()); + } + + void + test_iou_to_xrp () + { + testcase ("XRP Quantization: output"); + + Quality q1 = get_quality ("1", "1"); + + // TAKER OWNER + // QUAL OFFER FUNDS QUAL OFFER FUNDS EXPECTED + // USD XRP + attempt (Sell, "N:N", q1, { "3", "3" }, "3", q1, { "3", "3" }, "3", { "3", "3" }, usd(), xrp()); + attempt (Sell, "N:B", q1, { "3", "3" }, "3", q1, { "3", "3" }, "2", { "2", "2" }, usd(), xrp()); + attempt (Buy, "N:T", q1, { "3", "3" }, "2.5", q1, { "5", "5" }, "5", { "2.5", "2" }, usd(), xrp()); + attempt (Buy, "N:BT", q1, { "3", "3" }, "1.5", q1, { "5", "5" }, "4", { "1.5", "1" }, usd(), xrp()); + attempt (Buy, "N:TB", q1, { "3", "3" }, "2.2", q1, { "5", "5" }, "1", { "1", "1" }, usd(), xrp()); + + attempt (Sell, "T:N", q1, { "1", "1" }, "2", q1, { "2", "2" }, "2", { "1", "1" }, usd(), xrp()); + attempt (Sell, "T:B", q1, { "2", "2" }, "2", q1, { "3", "3" }, "1", { "1", "1" }, usd(), xrp()); + attempt (Buy, "T:T", q1, { "1", "1" }, "2", q1, { "2", "2" }, "2", { "1", "1" }, usd(), xrp()); + attempt (Buy, "T:BT", q1, { "1", "1" }, "2", q1, { "3", "3" }, "2", { "1", "1" }, usd(), xrp()); + attempt (Buy, "T:TB", q1, { "2", "2" }, "2", q1, { "3", "3" }, "1", { "1", "1" }, usd(), xrp()); + + attempt (Sell, "A:N", q1, { "2", "2" }, "1.5", q1, { "2", "2" }, "2", { "1.5", "1" }, usd(), xrp()); + attempt (Sell, "A:B", q1, { "2", "2" }, "1.8", q1, { "3", "3" }, "2", { "1.8", "1" }, usd(), xrp()); + attempt (Buy, "A:T", q1, { "2", "2" }, "1.2", q1, { "3", "3" }, "3", { "1.2", "1" }, usd(), xrp()); + attempt (Buy, "A:BT", q1, { "2", "2" }, "1.5", q1, { "4", "4" }, "3", { "1.5", "1" }, usd(), xrp()); + attempt (Buy, "A:TB", q1, { "2", "2" }, "1.5", q1, { "4", "4" }, "1", { "1", "1" }, usd(), xrp()); + + attempt (Sell, "TA:N", q1, { "2", "2" }, "1.5", q1, { "2", "2" }, "2", { "1.5", "1" }, usd(), xrp()); + attempt (Sell, "TA:B", q1, { "2", "2" }, "1.5", q1, { "3", "3" }, "1", { "1", "1" }, usd(), xrp()); + attempt (Buy, "TA:T", q1, { "2", "2" }, "1.5", q1, { "3", "3" }, "3", { "1.5", "1" }, usd(), xrp()); + attempt (Buy, "TA:BT", q1, { "2", "2" }, "1.8", q1, { "4", "4" }, "3", { "1.8", "1" }, usd(), xrp()); + attempt (Buy, "TA:TB", q1, { "2", "2" }, "1.2", q1, { "3", "3" }, "1", { "1", "1" }, usd(), xrp()); + + attempt (Sell, "AT:N", q1, { "2", "2" }, "2.5", q1, { "4", "4" }, "4", { "2", "2" }, usd(), xrp()); + attempt (Sell, "AT:B", q1, { "2", "2" }, "2.5", q1, { "3", "3" }, "1", { "1", "1" }, usd(), xrp()); + attempt (Buy, "AT:T", q1, { "2", "2" }, "2.5", q1, { "3", "3" }, "3", { "2", "2" }, usd(), xrp()); + attempt (Buy, "AT:BT", q1, { "2", "2" }, "2.5", q1, { "4", "4" }, "3", { "2", "2" }, usd(), xrp()); + attempt (Buy, "AT:TB", q1, { "2", "2" }, "2.5", q1, { "3", "3" }, "1", { "1", "1" }, usd(), xrp()); + } + + void + test_iou_to_iou () + { + testcase ("IOU to IOU"); + + Quality q1 = get_quality ("1", "1"); + + // Highly exaggerated 50% transfer rate for the input and output: + std::uint32_t rate = QUALITY_ONE + (QUALITY_ONE / 2); + + // TAKER OWNER + // QUAL OFFER FUNDS QUAL OFFER FUNDS EXPECTED + // EUR USD + attempt (Sell, "N:N", q1, { "2", "2" }, "10", q1, { "2", "2" }, "10", { "2", "2" }, eur(), usd(), rate, rate); + attempt (Sell, "N:B", q1, { "4", "4" }, "10", q1, { "4", "4" }, "4", { "2.666666666666666", "2.666666666666666" }, eur(), usd(), rate, rate); + attempt (Buy, "N:T", q1, { "1", "1" }, "10", q1, { "2", "2" }, "10", { "1", "1" }, eur(), usd(), rate, rate); + attempt (Buy, "N:BT", q1, { "2", "2" }, "10", q1, { "6", "6" }, "5", { "2", "2" }, eur(), usd(), rate, rate); + attempt (Buy, "N:TB", q1, { "2", "2" }, "2", q1, { "6", "6" }, "1", { "0.6666666666666667", "0.6666666666666667" }, eur(), usd(), rate, rate); + } + + void + run() + { + test_xrp_to_iou (); + test_iou_to_xrp (); + test_iou_to_iou (); + } +}; + +BEAST_DEFINE_TESTSUITE(Taker,core,ripple); + +} +} diff --git a/src/ripple/app/ledger/LedgerEntrySet.cpp b/src/ripple/app/ledger/LedgerEntrySet.cpp index 61f20ff3653..f69655e2062 100644 --- a/src/ripple/app/ledger/LedgerEntrySet.cpp +++ b/src/ripple/app/ledger/LedgerEntrySet.cpp @@ -1587,7 +1587,7 @@ TER LedgerEntrySet::rippleSend ( if (QUALITY_ONE != transit_rate) { - saActual = multiply (saActual, STAmount::saFromRate (transit_rate), + saActual = multiply (saActual, amountFromRate (transit_rate), saActual.issue ()); } diff --git a/src/ripple/app/transactors/CancelTicket.h b/src/ripple/app/transactors/CancelTicket.h deleted file mode 100644 index 971ad128aa8..00000000000 --- a/src/ripple/app/transactors/CancelTicket.h +++ /dev/null @@ -1,28 +0,0 @@ -//------------------------------------------------------------------------------ -/* - This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2014 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_TX_CANCELTICKET_H_INCLUDED -#define RIPPLE_TX_CANCELTICKET_H_INCLUDED - -namespace ripple { - - -} - -#endif diff --git a/src/ripple/app/transactors/CreateOffer.cpp b/src/ripple/app/transactors/CreateOffer.cpp index bea3c5aa93d..d9e75e2b631 100644 --- a/src/ripple/app/transactors/CreateOffer.cpp +++ b/src/ripple/app/transactors/CreateOffer.cpp @@ -22,459 +22,768 @@ #include #include #include -#include +#include #include #include + #include +#include namespace ripple { -TER -CreateOffer::checkAcceptAsset(IssueRef issue) const +class CreateOffer + : public Transactor { - /* Only valid for custom currencies */ - assert (!isXRP (issue.currency)); +private: + // What kind of offer we are placing + core::CrossType cross_type_; - SLE::pointer const issuerAccount = mEngine->entryCache ( - ltACCOUNT_ROOT, getAccountRootIndex (issue.account)); - - if (!issuerAccount) + /** Determine if we are authorized to hold the asset we want to get */ + TER + checkAcceptAsset(IssueRef issue) const { - if (m_journal.warning) m_journal.warning << - "delay: can't receive IOUs from non-existent issuer: " << - to_string (issue.account); - - return (mParams & tapRETRY) - ? terNO_ACCOUNT - : tecNO_ISSUER; - } + // Only valid for custom currencies + assert (!isXRP (issue.currency)); - if (issuerAccount->getFieldU32 (sfFlags) & lsfRequireAuth) - { - SLE::pointer const trustLine (mEngine->entryCache ( - ltRIPPLE_STATE, getRippleStateIndex ( - mTxnAccountID, issue.account, issue.currency))); + SLE::pointer const issuerAccount = mEngine->entryCache ( + ltACCOUNT_ROOT, getAccountRootIndex (issue.account)); - if (!trustLine) + if (!issuerAccount) { + if (m_journal.warning) m_journal.warning << + "delay: can't receive IOUs from non-existent issuer: " << + to_string (issue.account); + return (mParams & tapRETRY) - ? terNO_LINE - : tecNO_LINE; + ? terNO_ACCOUNT + : tecNO_ISSUER; } - // Entries have a canonical representation, determined by a - // lexicographical "greater than" comparison employing strict weak - // ordering. Determine which entry we need to access. - bool const canonical_gt (mTxnAccountID > issue.account); + if (issuerAccount->getFieldU32 (sfFlags) & lsfRequireAuth) + { + SLE::pointer const trustLine (mEngine->entryCache ( + ltRIPPLE_STATE, getRippleStateIndex ( + mTxnAccountID, issue.account, issue.currency))); + + if (!trustLine) + { + return (mParams & tapRETRY) + ? terNO_LINE + : tecNO_LINE; + } - bool const is_authorized (trustLine->getFieldU32 (sfFlags) & - (canonical_gt ? lsfLowAuth : lsfHighAuth)); + // Entries have a canonical representation, determined by a + // lexicographical "greater than" comparison employing strict weak + // ordering. Determine which entry we need to access. + bool const canonical_gt (mTxnAccountID > issue.account); - if (!is_authorized) - { - if (m_journal.debug) m_journal.debug << - "delay: can't receive IOUs from issuer without auth."; + bool const is_authorized (trustLine->getFieldU32 (sfFlags) & + (canonical_gt ? lsfLowAuth : lsfHighAuth)); - return (mParams & tapRETRY) - ? terNO_AUTH - : tecNO_AUTH; + if (!is_authorized) + { + if (m_journal.debug) m_journal.debug << + "delay: can't receive IOUs from issuer without auth."; + + return (mParams & tapRETRY) + ? terNO_AUTH + : tecNO_AUTH; + } } + + return tesSUCCESS; } - return tesSUCCESS; -} + static + bool + dry_offer (core::LedgerView& view, core::Offer const& offer) + { + if (offer.fully_consumed ()) + return true; -std::pair -CreateOffer::crossOffers (core::LedgerView& view, - core::Amounts const& taker_amount) -{ -#if RIPPLE_ENABLE_AUTOBRIDGING - if (autobridging_) - return crossOffersBridged (view, taker_amount); -#endif + auto const funds (view.accountFunds (offer.owner(), + offer.amount().out, fhZERO_IF_FROZEN)); - return crossOffersDirect (view, taker_amount); -} + return (funds <= zero); + } -CreateOffer::CreateOffer (bool autobridging, STTx const& txn, - TransactionEngineParams params, TransactionEngine* engine) - : Transactor (txn, params, engine, deprecatedLogs().journal("CreateOffer")) -#if RIPPLE_ENABLE_AUTOBRIDGING - , autobridging_ (autobridging) -#endif -{ -} + static + std::pair + select_path ( + bool have_direct, core::OfferStream const& direct, + bool have_bridge, core::OfferStream const& leg1, core::OfferStream const& leg2) + { + // If we don't have any viable path, why are we here?! + assert (have_direct || have_bridge); -TER -CreateOffer::doApply() -{ - if (m_journal.debug) m_journal.debug << - "OfferCreate> " << mTxn.getJson (0); + // If there's no bridged path, the direct is the best by default. + if (!have_bridge) + return std::make_pair (true, direct.tip ().quality ()); - std::uint32_t const uTxFlags = mTxn.getFlags (); + core::Quality const bridged_quality (core::composed_quality ( + leg1.tip ().quality (), leg2.tip ().quality ())); - bool const bPassive (uTxFlags & tfPassive); - bool const bImmediateOrCancel (uTxFlags & tfImmediateOrCancel); - bool const bFillOrKill (uTxFlags & tfFillOrKill); - bool const bSell (uTxFlags & tfSell); + if (have_direct) + { + // We compare the quality of the composed quality of the bridged + // offers and compare it against the direct offer to pick the best. + core::Quality const direct_quality (direct.tip ().quality ()); - STAmount saTakerPays = mTxn.getFieldAmount (sfTakerPays); - STAmount saTakerGets = mTxn.getFieldAmount (sfTakerGets); + if (bridged_quality < direct_quality) + return std::make_pair (true, direct_quality); + } - if (!isLegalNet (saTakerPays) || !isLegalNet (saTakerGets)) - return temBAD_AMOUNT; + // Either there was no direct offer, or it didn't have a better quality + // than the bridge. + return std::make_pair (false, bridged_quality); + } - auto const& uPaysIssuerID = saTakerPays.getIssuer (); - auto const& uPaysCurrency = saTakerPays.getCurrency (); + std::pair + bridged_cross ( + core::Taker& taker, + core::LedgerView& view, + core::LedgerView& view_cancel, + core::Clock::time_point const when) + { + auto const& taker_amount = taker.original_offer (); - auto const& uGetsIssuerID = saTakerGets.getIssuer (); - auto const& uGetsCurrency = saTakerGets.getCurrency (); + assert (!isXRP (taker_amount.in)&& !isXRP (taker_amount.in)); - bool const bHaveExpiration (mTxn.isFieldPresent (sfExpiration)); - bool const bHaveCancel (mTxn.isFieldPresent (sfOfferSequence)); + if (isXRP (taker_amount.in) || isXRP (taker_amount.out)) + throw std::logic_error ("Bridging with XRP and an endpoint."); - std::uint32_t const uExpiration = mTxn.getFieldU32 (sfExpiration); - std::uint32_t const uCancelSequence = mTxn.getFieldU32 (sfOfferSequence); + core::OfferStream offers_direct (view, view_cancel, + Book (taker.issue_in (), taker.issue_out ()), when, m_journal); - // FIXME understand why we use SequenceNext instead of current transaction - // sequence to determine the transaction. Why is the offer seuqnce - // number insufficient? + core::OfferStream offers_leg1 (view, view_cancel, + Book (taker.issue_in (), xrpIssue ()), when, m_journal); - std::uint32_t const uAccountSequenceNext = mTxnAccount->getFieldU32 (sfSequence); - std::uint32_t const uSequence = mTxn.getSequence (); + core::OfferStream offers_leg2 (view, view_cancel, + Book (xrpIssue (), taker.issue_out ()), when, m_journal); - const uint256 uLedgerIndex = getOfferIndex (mTxnAccountID, uSequence); + TER cross_result = tesSUCCESS; - if (m_journal.debug) - { - m_journal.debug << - "Creating offer node: " << to_string (uLedgerIndex) << - " uSequence=" << uSequence; + // Note the subtle distinction here: self-offers encountered in the + // bridge are taken, but self-offers encountered in the direct book + // are not. + bool have_bridge = offers_leg1.step () && offers_leg2.step (); + bool have_direct = step_account (offers_direct, taker); + int count = 0; - if (bImmediateOrCancel) - m_journal.debug << "Transaction: IoC set."; + // Modifying the order or logic of the operations in the loop will cause + // a protocol breaking change. + while (have_direct || have_bridge) + { + bool leg1_consumed = false; + bool leg2_consumed = false; + bool direct_consumed = false; - if (bFillOrKill) - m_journal.debug << "Transaction: FoK set."; - } + core::Quality quality; + bool use_direct; - // This is the original rate of this offer, and is the rate at which it will - // be placed, even if crossing offers change the amounts. - std::uint64_t const uRate = getRate (saTakerGets, saTakerPays); + std::tie (use_direct, quality) = select_path ( + have_direct, offers_direct, + have_bridge, offers_leg1, offers_leg2); - TER terResult (tesSUCCESS); + // We are always looking at the best quality; we are done with + // crossing as soon as we cross the quality boundary. + if (taker.reject(quality)) + break; - // This is the ledger view that we work against. Transactions are applied - // as we go on processing transactions. - core::LedgerView& view (mEngine->view ()); + count++; - // This is a checkpoint with just the fees paid. If something goes wrong - // with this transaction, we roll back to this ledger. - core::LedgerView view_checkpoint (view); + if (use_direct) + { + if (m_journal.debug) + { + m_journal.debug << count << " Direct:"; + m_journal.debug << " offer: " << offers_direct.tip (); + m_journal.debug << " in: " << offers_direct.tip ().amount().in; + m_journal.debug << " out: " << offers_direct.tip ().amount ().out; + m_journal.debug << " owner: " << offers_direct.tip ().owner (); + m_journal.debug << " funds: " << view.accountFunds ( + offers_direct.tip ().owner (), + offers_direct.tip ().amount ().out, + fhIGNORE_FREEZE); + } - view.bumpSeq (); // Begin ledger variance. + cross_result = taker.cross(offers_direct.tip ()); - SLE::pointer sleCreator = mEngine->entryCache ( - ltACCOUNT_ROOT, getAccountRootIndex (mTxnAccountID)); + m_journal.debug << "Direct Result: " << transToken (cross_result); - if (uTxFlags & tfOfferCreateMask) - { - if (m_journal.debug) m_journal.debug << - "Malformed transaction: Invalid flags set."; + if (dry_offer (view, offers_direct.tip ())) + { + direct_consumed = true; + have_direct = step_account (offers_direct, taker); + } + } + else + { + if (m_journal.debug) + { + auto const owner1_funds_before = view.accountFunds ( + offers_leg1.tip ().owner (), + offers_leg1.tip ().amount ().out, + fhIGNORE_FREEZE); + + auto const owner2_funds_before = view.accountFunds ( + offers_leg2.tip ().owner (), + offers_leg2.tip ().amount ().out, + fhIGNORE_FREEZE); + + m_journal.debug << count << " Bridge:"; + m_journal.debug << " offer1: " << offers_leg1.tip (); + m_journal.debug << " in: " << offers_leg1.tip ().amount().in; + m_journal.debug << " out: " << offers_leg1.tip ().amount ().out; + m_journal.debug << " owner: " << offers_leg1.tip ().owner (); + m_journal.debug << " funds: " << owner1_funds_before; + m_journal.debug << " offer2: " << offers_leg2.tip (); + m_journal.debug << " in: " << offers_leg2.tip ().amount ().in; + m_journal.debug << " out: " << offers_leg2.tip ().amount ().out; + m_journal.debug << " owner: " << offers_leg2.tip ().owner (); + m_journal.debug << " funds: " << owner2_funds_before; + } - terResult = temINVALID_FLAG; - } - else if (bImmediateOrCancel && bFillOrKill) - { - if (m_journal.debug) m_journal.debug << - "Malformed transaction: both IoC and FoK set."; + cross_result = taker.cross(offers_leg1.tip (), offers_leg2.tip ()); - terResult = temINVALID_FLAG; - } - else if (bHaveExpiration && !uExpiration) - { - m_journal.warning << - "Malformed offer: bad expiration"; + m_journal.debug << "Bridge Result: " << transToken (cross_result); - terResult = temBAD_EXPIRATION; - } - else if (saTakerPays.isNative () && saTakerGets.isNative ()) - { - m_journal.warning << - "Malformed offer: XRP for XRP"; + if (dry_offer (view, offers_leg1.tip ())) + { + leg1_consumed = true; + have_bridge = (have_bridge && offers_leg1.step ()); + } + if (dry_offer (view, offers_leg2.tip ())) + { + leg2_consumed = true; + have_bridge = (have_bridge && offers_leg2.step ()); + } + } - terResult = temBAD_OFFER; - } - else if (saTakerPays <= zero || saTakerGets <= zero) - { - m_journal.warning << - "Malformed offer: bad amount"; + if (cross_result != tesSUCCESS) + { + cross_result = tecFAILED_PROCESSING; + break; + } - terResult = temBAD_OFFER; - } - else if (uPaysCurrency == uGetsCurrency && uPaysIssuerID == uGetsIssuerID) - { - m_journal.warning << - "Malformed offer: redundant offer"; + if (taker.done()) + { + m_journal.debug << "The taker reports he's done during crossing!"; + break; + } + + // Postcondition: If we aren't done, then we *must* have consumed at + // least one offer fully. + assert (direct_consumed || leg1_consumed || leg2_consumed); - terResult = temREDUNDANT; + if (!direct_consumed && !leg1_consumed && !leg2_consumed) + throw std::logic_error ("bridged crossing: nothing was fully consumed."); + } + + return std::make_pair(cross_result, taker.remaining_offer ()); } - // We don't allow a non-native currency to use the currency code XRP. - else if (badCurrency() == uPaysCurrency || badCurrency() == uGetsCurrency) + + std::pair + direct_cross ( + core::Taker& taker, + core::LedgerView& view, + core::LedgerView& view_cancel, + core::Clock::time_point const when) { - m_journal.warning << - "Malformed offer: Bad currency."; + core::OfferStream offers ( + view, view_cancel, + Book (taker.issue_in (), taker.issue_out ()), + when, m_journal); + + TER cross_result (tesSUCCESS); + int count = 0; + + bool have_offer = step_account (offers, taker); + + // Modifying the order or logic of the operations in the loop will cause + // a protocol breaking change. + while (have_offer) + { + bool direct_consumed = false; + auto const& offer (offers.tip()); + + // We are done with crossing as soon as we cross the quality boundary + if (taker.reject (offer.quality())) + break; + + count++; + + if (m_journal.debug) + { + m_journal.debug << count << " Direct:"; + m_journal.debug << " offer: " << offer; + m_journal.debug << " in: " << offer.amount ().in; + m_journal.debug << " out: " << offer.amount ().out; + m_journal.debug << " owner: " << offer.owner (); + m_journal.debug << " funds: " << view.accountFunds ( + offer.owner (), offer.amount ().out, fhIGNORE_FREEZE); + } + + cross_result = taker.cross (offer); + + m_journal.debug << "Direct Result: " << transToken (cross_result); + + if (dry_offer (view, offer)) + { + direct_consumed = true; + have_offer = step_account (offers, taker); + } + + if (cross_result != tesSUCCESS) + { + cross_result = tecFAILED_PROCESSING; + break; + } + + if (taker.done()) + { + m_journal.debug << "The taker reports he's done during crossing!"; + break; + } - terResult = temBAD_CURRENCY; + // Postcondition: If we aren't done, then we *must* have consumed the + // offer on the books fully! + assert (direct_consumed); + + if (!direct_consumed) + throw std::logic_error ("direct crossing: nothing was fully consumed."); + } + + return std::make_pair(cross_result, taker.remaining_offer ()); } - else if (saTakerPays.isNative () != !uPaysIssuerID || - saTakerGets.isNative () != !uGetsIssuerID) + + // Step through the stream for as long as possible, skipping any offers + // that are from the taker or which cross the taker's threshold. + // Return false if the is no offer in the book, true otherwise. + static + bool + step_account (core::OfferStream& stream, core::Taker const& taker) { - m_journal.warning << - "Malformed offer: bad issuer"; + while (stream.step ()) + { + auto const& offer = stream.tip (); + + // This offer at the tip crosses the taker's threshold. We're done. + if (taker.reject (offer.quality ())) + return true; + + // This offer at the tip is not from the taker. We're done. + if (offer.owner () != taker.account ()) + return true; + } - terResult = temBAD_ISSUER; + // We ran out of offers. Can't advance. + return false; } - else if (view.isGlobalFrozen (uPaysIssuerID) || view.isGlobalFrozen (uGetsIssuerID)) + + // Fill offer as much as possible by consuming offers already on the books, + // and adjusting account balances accordingly. + // + // Charges fees on top to taker. + std::pair + cross ( + core::LedgerView& view, + core::LedgerView& cancel_view, + core::Amounts const& taker_amount) { - m_journal.warning << - "Offer involves frozen asset"; + core::Clock::time_point const when ( + mEngine->getLedger ()->getParentCloseTimeNC ()); + + core::Taker taker (cross_type_, view, mTxnAccountID, taker_amount, mTxn.getFlags()); + + try + { + if (m_journal.debug) + { + auto const funds = view.accountFunds ( + taker.account(), taker_amount.in, fhIGNORE_FREEZE); + + m_journal.debug << "Crossing:"; + m_journal.debug << " Taker: " << to_string (mTxnAccountID); + m_journal.debug << " Balance: " << format_amount (funds); + } - terResult = tecFROZEN; +#if RIPPLE_ENABLE_AUTOBRIDGING + if (cross_type_ == core::CrossType::IouToIou) + return bridged_cross (taker, view, cancel_view, when); +#endif + + return direct_cross (taker, view, cancel_view, when); + } + catch (std::exception const& e) + { + m_journal.error << "Exception during offer crossing: " << e.what (); + return std::make_pair (tecINTERNAL, taker.remaining_offer ()); + } + catch (...) + { + m_journal.error << "Exception during offer crossing."; + return std::make_pair (tecINTERNAL, taker.remaining_offer ()); + } } - else if (view.accountFunds ( - mTxnAccountID, saTakerGets, fhZERO_IF_FROZEN) <= zero) - { - m_journal.warning << - "delay: Offers must be at least partially funded."; - terResult = tecUNFUNDED_OFFER; + static + std::string + format_amount (STAmount const& amount) + { + std::string txt = amount.getText (); + txt += "/"; + txt += amount.getHumanCurrency (); + return txt; } - // This can probably be simplified to make sure that you cancel sequences - // before the transaction sequence number. - else if (bHaveCancel && (!uCancelSequence || uAccountSequenceNext - 1 <= uCancelSequence)) + +public: + CreateOffer ( + core::CrossType cross_type, + STTx const& txn, + TransactionEngineParams params, + TransactionEngine* engine) + : Transactor ( + txn, + params, + engine, + deprecatedLogs().journal("CreateOffer")) + , cross_type_ (cross_type) { - if (m_journal.debug) m_journal.debug << - "uAccountSequenceNext=" << uAccountSequenceNext << - " uOfferSequence=" << uCancelSequence; - terResult = temBAD_SEQUENCE; } - if (terResult != tesSUCCESS) + /** Returns the reserve the account would have if an offer was added. */ + std::uint32_t + getAccountReserve (SLE::pointer account) { - if (m_journal.debug) m_journal.debug << - "final terResult=" << transToken (terResult); - - return terResult; + return mEngine->getLedger ()->getReserve ( + account->getFieldU32 (sfOwnerCount) + 1); } - // Process a cancellation request that's passed along with an offer. - if ((terResult == tesSUCCESS) && bHaveCancel) + TER + doApply () override { - uint256 const uCancelIndex ( - getOfferIndex (mTxnAccountID, uCancelSequence)); - SLE::pointer sleCancel = mEngine->entryCache (ltOFFER, uCancelIndex); + std::uint32_t const uTxFlags = mTxn.getFlags (); + + bool const bPassive (uTxFlags & tfPassive); + bool const bImmediateOrCancel (uTxFlags & tfImmediateOrCancel); + bool const bFillOrKill (uTxFlags & tfFillOrKill); + bool const bSell (uTxFlags & tfSell); + + STAmount saTakerPays = mTxn.getFieldAmount (sfTakerPays); + STAmount saTakerGets = mTxn.getFieldAmount (sfTakerGets); + + if (!isLegalNet (saTakerPays) || !isLegalNet (saTakerGets)) + return temBAD_AMOUNT; + + auto const& uPaysIssuerID = saTakerPays.getIssuer (); + auto const& uPaysCurrency = saTakerPays.getCurrency (); - // It's not an error to not find the offer to cancel: it might have - // been consumed or removed as we are processing. - if (sleCancel) + auto const& uGetsIssuerID = saTakerGets.getIssuer (); + auto const& uGetsCurrency = saTakerGets.getCurrency (); + + bool const bHaveExpiration (mTxn.isFieldPresent (sfExpiration)); + bool const bHaveCancel (mTxn.isFieldPresent (sfOfferSequence)); + + std::uint32_t const uExpiration = mTxn.getFieldU32 (sfExpiration); + std::uint32_t const uCancelSequence = mTxn.getFieldU32 (sfOfferSequence); + + // FIXME understand why we use SequenceNext instead of current transaction + // sequence to determine the transaction. Why is the offer sequence + // number insufficient? + + std::uint32_t const uAccountSequenceNext = mTxnAccount->getFieldU32 (sfSequence); + std::uint32_t const uSequence = mTxn.getSequence (); + + // This is the original rate of the offer, and is the rate at which + // it will be placed, even if crossing offers change the amounts that + // end up on the books. + std::uint64_t const uRate = getRate (saTakerGets, saTakerPays); + + TER result = tesSUCCESS; + + // This is the ledger view that we work against. Transactions are applied + // as we go on processing transactions. + core::LedgerView& view (mEngine->view ()); + + // This is a checkpoint with just the fees paid. If something goes wrong + // with this transaction, we roll back to this ledger. + core::LedgerView view_checkpoint (view); + + view.bumpSeq (); // Begin ledger variance. + + SLE::pointer sleCreator = mEngine->entryCache ( + ltACCOUNT_ROOT, getAccountRootIndex (mTxnAccountID)); + + if (uTxFlags & tfOfferCreateMask) + { + if (m_journal.debug) m_journal.debug << + "Malformed transaction: Invalid flags set."; + + result = temINVALID_FLAG; + } + else if (bImmediateOrCancel && bFillOrKill) + { + if (m_journal.debug) m_journal.debug << + "Malformed transaction: both IoC and FoK set."; + + result = temINVALID_FLAG; + } + else if (bHaveExpiration && !uExpiration) { m_journal.warning << - "Cancelling order with sequence " << uCancelSequence; + "Malformed offer: bad expiration"; - terResult = view.offerDelete (sleCancel); + result = temBAD_EXPIRATION; } - } + else if (saTakerPays.isNative () && saTakerGets.isNative ()) + { + m_journal.warning << + "Malformed offer: XRP for XRP"; - // Expiration is defined in terms of the close time of the parent ledger, - // because we definitively know the time that it closed but we do not - // know the closing time of the ledger that is under construction. - if (bHaveExpiration && - (mEngine->getLedger ()->getParentCloseTimeNC () >= uExpiration)) - { - return tesSUCCESS; - } + result = temBAD_OFFER; + } + else if (saTakerPays <= zero || saTakerGets <= zero) + { + m_journal.warning << + "Malformed offer: bad amount"; + + result = temBAD_OFFER; + } + else if (uPaysCurrency == uGetsCurrency && uPaysIssuerID == uGetsIssuerID) + { + m_journal.warning << + "Malformed offer: redundant offer"; - // Make sure that we are authorized to hold what the taker will pay us. - if (terResult == tesSUCCESS && !saTakerPays.isNative ()) - terResult = checkAcceptAsset (Issue (uPaysCurrency, uPaysIssuerID)); + result = temREDUNDANT; + } + // We don't allow a non-native currency to use the currency code XRP. + else if (badCurrency() == uPaysCurrency || badCurrency() == uGetsCurrency) + { + m_journal.warning << + "Malformed offer: Bad currency."; - bool crossed = false; - bool const bOpenLedger (mParams & tapOPEN_LEDGER); + result = temBAD_CURRENCY; + } + else if (saTakerPays.isNative () != !uPaysIssuerID || + saTakerGets.isNative () != !uGetsIssuerID) + { + m_journal.warning << + "Malformed offer: bad issuer"; - if (terResult == tesSUCCESS) - { - // We reverse gets and pays because during offer crossing we are taking. - core::Amounts const taker_amount (saTakerGets, saTakerPays); + result = temBAD_ISSUER; + } + else if (view.isGlobalFrozen (uPaysIssuerID) || view.isGlobalFrozen (uGetsIssuerID)) + { + m_journal.warning << + "Offer involves frozen asset"; + + result = tecFROZEN; + } + else if (view.accountFunds ( + mTxnAccountID, saTakerGets, fhZERO_IF_FROZEN) <= zero) + { + m_journal.warning << + "delay: Offers must be at least partially funded."; + + result = tecUNFUNDED_OFFER; + } + // This can probably be simplified to make sure that you cancel sequences + // before the transaction sequence number. + else if (bHaveCancel && (!uCancelSequence || uAccountSequenceNext - 1 <= uCancelSequence)) + { + if (m_journal.debug) m_journal.debug << + "uAccountSequenceNext=" << uAccountSequenceNext << + " uOfferSequence=" << uCancelSequence; + + result = temBAD_SEQUENCE; + } + + if (result != tesSUCCESS) + { + m_journal.debug << "final result: " << transToken (result); + return result; + } + + // Process a cancellation request that's passed along with an offer. + if ((result == tesSUCCESS) && bHaveCancel) + { + SLE::pointer sleCancel = mEngine->entryCache (ltOFFER, + getOfferIndex (mTxnAccountID, uCancelSequence)); + + // It's not an error to not find the offer to cancel: it might have + // been consumed or removed. If it is found, however, it's an error + // to fail to delete it. + if (sleCancel) + { + m_journal.debug << "Create cancels order " << uCancelSequence; + result = view.offerDelete (sleCancel); + } + } - // The amount of the offer that we will need to place, after we finish - // offer crossing processing. It may be equal to the original amount, - // empty (fully crossed), or something in-between. - core::Amounts place_offer; + // Expiration is defined in terms of the close time of the parent ledger, + // because we definitively know the time that it closed but we do not + // know the closing time of the ledger that is under construction. + if (bHaveExpiration && + (mEngine->getLedger ()->getParentCloseTimeNC () >= uExpiration)) + { + return tesSUCCESS; + } - std::tie(terResult, place_offer) = crossOffers (view, taker_amount); + // Make sure that we are authorized to hold what the taker will pay us. + if (result == tesSUCCESS && !saTakerPays.isNative ()) + result = checkAcceptAsset (Issue (uPaysCurrency, uPaysIssuerID)); - if (terResult == tecFAILED_PROCESSING && bOpenLedger) - terResult = telFAILED_PROCESSING; + bool const bOpenLedger (mParams & tapOPEN_LEDGER); + bool crossed = false; - if (terResult == tesSUCCESS) + if (result == tesSUCCESS) { - // We now need to reduce the offer by the cross flow. We reverse - // in and out here, since during crossing we were takers. - assert (saTakerPays.getCurrency () == place_offer.out.getCurrency ()); - assert (saTakerPays.getIssuer () == place_offer.out.getIssuer ()); - assert (saTakerGets.getCurrency () == place_offer.in.getCurrency ()); - assert (saTakerGets.getIssuer () == place_offer.in.getIssuer ()); + // We reverse pays and gets because during crossing we are taking. + core::Amounts const taker_amount (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), + // empty (fully crossed), or something in-between. + core::Amounts place_offer; + + m_journal.debug << "Attempting cross: " << + to_string (taker_amount.in.issue ()) << " -> " << + to_string (taker_amount.out.issue ()); + + if (m_journal.trace) + { + m_journal.debug << " mode: " << + (bPassive ? "passive " : "") << + (bSell ? "sell" : "buy"); + m_journal.trace <<" in: " << format_amount (taker_amount.in); + m_journal.trace << " out: " << format_amount (taker_amount.out); + } + + std::tie(result, place_offer) = cross (view, view_checkpoint, taker_amount); + assert (result != tefINTERNAL); + + if (m_journal.trace) + { + m_journal.trace << "Cross result: " << transToken (result); + m_journal.trace << " in: " << format_amount (place_offer.in); + m_journal.trace << " out: " << format_amount (place_offer.out); + } + + if (result == tecFAILED_PROCESSING && bOpenLedger) + result = telFAILED_PROCESSING; + + if (result != tesSUCCESS) + { + m_journal.debug << "final result: " << transToken (result); + return result; + } + + assert (saTakerGets.issue () == place_offer.in.issue ()); + assert (saTakerPays.issue () == place_offer.out.issue ()); if (taker_amount != place_offer) crossed = true; - if (m_journal.debug) + // The offer that we need to place after offer crossing should + // never be negative. If it is, something went very very wrong. + if (place_offer.in < zero || place_offer.out < zero) { - m_journal.debug << "Offer Crossing: " << transToken (terResult); + m_journal.fatal << "Cross left offer negative!"; + return tefINTERNAL; + } - if (terResult == tesSUCCESS) - { - m_journal.debug << - " takerPays: " << saTakerPays.getFullText () << - " -> " << place_offer.out.getFullText (); - m_journal.debug << - " takerGets: " << saTakerGets.getFullText () << - " -> " << place_offer.in.getFullText (); - } + if (place_offer.in == zero || place_offer.out == zero) + { + m_journal.debug << "Offer fully crossed!"; + return result; } + // We now need to adjust the offer to reflect the amount left after + // crossing. We reverse in and out here, since during crossing we + // were the taker. saTakerPays = place_offer.out; saTakerGets = place_offer.in; } - } - - if (terResult != tesSUCCESS) - { - m_journal.debug << - "final terResult=" << transToken (terResult); - - return terResult; - } - - if (m_journal.debug) - { - m_journal.debug << - "takeOffers: saTakerPays=" < zero); - assert (saTakerGets > zero); // We need to place the remainder of the offer into its order book. - if (m_journal.debug) m_journal.debug << - "offer not fully consumed:" << - " saTakerPays=" << saTakerPays.getFullText () << - " saTakerGets=" << saTakerGets.getFullText (); + auto const offer_index = getOfferIndex (mTxnAccountID, uSequence); std::uint64_t uOwnerNode; std::uint64_t uBookNode; uint256 uDirectory; // Add offer to owner's directory. - terResult = view.dirAdd (uOwnerNode, - getOwnerDirIndex (mTxnAccountID), uLedgerIndex, + result = view.dirAdd (uOwnerNode, + getOwnerDirIndex (mTxnAccountID), offer_index, std::bind ( &Ledger::ownerDirDescriber, std::placeholders::_1, std::placeholders::_2, mTxnAccountID)); - if (tesSUCCESS == terResult) + if (result == tesSUCCESS) { // Update owner count. view.incrementOwnerCount (sleCreator); - uint256 const uBookBase (getBookBase ( - {{uPaysCurrency, uPaysIssuerID}, - {uGetsCurrency, uGetsIssuerID}})); + if (m_journal.trace) m_journal.trace << + "adding to book: " << to_string (saTakerPays.issue ()) << + " : " << to_string (saTakerGets.issue ()); - if (m_journal.debug) m_journal.debug << - "adding to book: " << to_string (uBookBase) << - " : " << saTakerPays.getHumanCurrency () << - "/" << to_string (saTakerPays.getIssuer ()) << - " -> " << saTakerGets.getHumanCurrency () << - "/" << to_string (saTakerGets.getIssuer ()); + uint256 const book_base (getBookBase ( + { saTakerPays.issue (), saTakerGets.issue () })); // We use the original rate to place the offer. - uDirectory = getQualityIndex (uBookBase, uRate); + uDirectory = getQualityIndex (book_base, uRate); // Add offer to order book. - terResult = view.dirAdd (uBookNode, uDirectory, uLedgerIndex, + result = view.dirAdd (uBookNode, uDirectory, offer_index, std::bind ( &Ledger::qualityDirDescriber, std::placeholders::_1, std::placeholders::_2, saTakerPays.getCurrency (), @@ -482,34 +791,9 @@ CreateOffer::doApply() uGetsIssuerID, uRate)); } - if (tesSUCCESS == terResult) + if (result == tesSUCCESS) { - if (m_journal.debug) - { - m_journal.debug << - "sfAccount=" << - to_string (mTxnAccountID); - m_journal.debug << - "uPaysIssuerID=" << - to_string (uPaysIssuerID); - m_journal.debug << - "uGetsIssuerID=" << - to_string (uGetsIssuerID); - m_journal.debug << - "saTakerPays.isNative()=" << - saTakerPays.isNative (); - m_journal.debug << - "saTakerGets.isNative()=" << - saTakerGets.isNative (); - m_journal.debug << - "uPaysCurrency=" << - saTakerPays.getHumanCurrency (); - m_journal.debug << - "uGetsCurrency=" << - saTakerGets.getHumanCurrency (); - } - - SLE::pointer sleOffer (mEngine->entryCreate (ltOFFER, uLedgerIndex)); + SLE::pointer sleOffer (mEngine->entryCreate (ltOFFER, offer_index)); sleOffer->setFieldAccount (sfAccount, mTxnAccountID); sleOffer->setFieldU32 (sfSequence, uSequence); @@ -527,34 +811,32 @@ CreateOffer::doApply() if (bSell) sleOffer->setFlag (lsfSell); - - if (m_journal.debug) m_journal.debug << - "final terResult=" << transToken (terResult) << - " sleOffer=" << sleOffer->getJson (0); } - } - if (terResult != tesSUCCESS) - { - m_journal.debug << - "final terResult=" << transToken (terResult); - } - - return terResult; -} + if (result != tesSUCCESS) + m_journal.debug << "final result: " << transToken (result); -//------------------------------------------------------------------------------ + return result; + } +}; TER -transact_CreateOffer (STTx const& txn, - TransactionEngineParams params, TransactionEngine* engine) +transact_CreateOffer ( + STTx const& txn, + TransactionEngineParams params, + TransactionEngine* engine) { - // Autobridging is performed only when the offer does not involve XRP - bool autobridging = - ! txn.getFieldAmount (sfTakerPays).isNative() && - ! txn.getFieldAmount (sfTakerGets).isNative (); + core::CrossType cross_type = core::CrossType::IouToIou; + + bool const pays_xrp = txn.getFieldAmount (sfTakerPays).isNative (); + bool const gets_xrp = txn.getFieldAmount (sfTakerGets).isNative (); + + if (pays_xrp && !gets_xrp) + cross_type = core::CrossType::IouToXrp; + else if (gets_xrp && !pays_xrp) + cross_type = core::CrossType::XrpToIou; - return CreateOffer (autobridging, txn, params, engine).apply (); + return CreateOffer (cross_type, txn, params, engine).apply (); } } diff --git a/src/ripple/app/transactors/CreateOffer.h b/src/ripple/app/transactors/CreateOffer.h deleted file mode 100644 index e51aaf7a144..00000000000 --- a/src/ripple/app/transactors/CreateOffer.h +++ /dev/null @@ -1,83 +0,0 @@ -//------------------------------------------------------------------------------ -/* - This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2012, 2013 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_APP_CREATEOFFER_H_INCLUDED -#define RIPPLE_APP_CREATEOFFER_H_INCLUDED - -#include -#include -#include -#include -#include -#include -#include -#include - -namespace ripple { - -class CreateOffer - : public Transactor -{ -private: - // What kind of offer we are placing -#if RIPPLE_ENABLE_AUTOBRIDGING - bool autobridging_; -#endif - - // Determine if we are authorized to hold the asset we want to get - TER - checkAcceptAsset(IssueRef issue) const; - - /* Fill offer as much as possible by consuming offers already on the books. - We adjusts account balances and charges fees on top to taker. - - @param taker_amount.in How much the taker offers - @param taker_amount.out How much the taker wants - - @return result.first crossing operation success/failure indicator. - result.second amount of offer left unfilled - only meaningful - if result.first is tesSUCCESS. - */ - std::pair - crossOffersBridged (core::LedgerView& view, - core::Amounts const& taker_amount); - - std::pair - crossOffersDirect (core::LedgerView& view, - core::Amounts const& taker_amount); - - std::pair - crossOffers (core::LedgerView& view, - core::Amounts const& taker_amount); - -public: - CreateOffer (bool autobridging, STTx const& txn, - TransactionEngineParams params, TransactionEngine* engine); - - TER - doApply() override; -}; - -TER -transact_CreateOffer (STTx const& txn, - TransactionEngineParams params, TransactionEngine* engine); - -} - -#endif diff --git a/src/ripple/app/transactors/CreateOfferBridged.cpp b/src/ripple/app/transactors/CreateOfferBridged.cpp deleted file mode 100644 index 06cc24bd06a..00000000000 --- a/src/ripple/app/transactors/CreateOfferBridged.cpp +++ /dev/null @@ -1,197 +0,0 @@ -//------------------------------------------------------------------------------ -/* - This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2012, 2013 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 -#include -#include -#include -#include -#include - -namespace ripple { - -std::pair -CreateOffer::crossOffersBridged ( - core::LedgerView& view, - core::Amounts const& taker_amount) -{ - assert (!taker_amount.in.isNative () && !taker_amount.out.isNative ()); - - if (taker_amount.in.isNative () || taker_amount.out.isNative ()) - return std::make_pair (tefINTERNAL, core::Amounts ()); - - core::Clock::time_point const when ( - mEngine->getLedger ()->getParentCloseTimeNC ()); - - core::Taker::Options const options (mTxn.getFlags()); - - core::LedgerView view_cancel (view.duplicate()); - - auto& asset_in = taker_amount.in.issue(); - auto& asset_out = taker_amount.out.issue(); - - core::OfferStream offers_direct (view, view_cancel, - Book (asset_in, asset_out), when, m_journal); - - core::OfferStream offers_leg1 (view, view_cancel, - Book (asset_in, xrpIssue ()), when, m_journal); - - core::OfferStream offers_leg2 (view, view_cancel, - Book (xrpIssue (), asset_out), when, m_journal); - - core::Taker taker (view, mTxnAccountID, taker_amount, options); - - if (m_journal.debug) m_journal.debug << - "process_order: " << - (options.sell? "sell" : "buy") << " " << - (options.passive? "passive" : "") << std::endl << - " taker: " << taker.account() << std::endl << - " balances: " << - view.accountFunds (taker.account(), taker_amount.in, fhIGNORE_FREEZE) - << ", " << - view.accountFunds (taker.account(), taker_amount.out, fhIGNORE_FREEZE); - - TER cross_result (tesSUCCESS); - - /* Note the subtle distinction here: self-offers encountered in the bridge - * are taken, but self-offers encountered in the direct book are not. - */ - bool have_bridged (offers_leg1.step () && offers_leg2.step ()); - bool have_direct (offers_direct.step_account (taker.account ())); - bool place_order (true); - - while (have_direct || have_bridged) - { - core::Quality quality; - bool use_direct; - bool leg1_consumed(false); - bool leg2_consumed(false); - bool direct_consumed(false); - - // Logic: - // We calculate the qualities of any direct and bridged offers at the - // tip of the order book, and choose the best one of the two. - - if (have_direct) - { - core::Quality const direct_quality (offers_direct.tip ().quality ()); - - if (have_bridged) - { - core::Quality const bridged_quality (core::composed_quality ( - offers_leg1.tip ().quality (), - offers_leg2.tip ().quality ())); - - if (bridged_quality < direct_quality) - { - use_direct = true; - quality = direct_quality; - } - else - { - use_direct = false; - quality = bridged_quality; - } - } - else - { - use_direct = true; - quality = offers_direct.tip ().quality (); - } - } - else - { - use_direct = false; - quality = core::composed_quality ( - offers_leg1.tip ().quality (), - offers_leg2.tip ().quality ()); - } - - // We are always looking at the best quality available, so if we reject - // that, we know that we are done. - if (taker.reject(quality)) - break; - - if (use_direct) - { - if (m_journal.debug) m_journal.debug << "Direct:" << std::endl << - " Offer: " << offers_direct.tip () << std::endl << - " " << offers_direct.tip ().amount().in << - " : " << offers_direct.tip ().amount ().out; - - cross_result = taker.cross(offers_direct.tip ()); - - if (offers_direct.tip ().fully_consumed ()) - { - direct_consumed = true; - have_direct = offers_direct.step_account (taker.account()); - } - } - else - { - if (m_journal.debug) m_journal.debug << "Bridge:" << std::endl << - " Offer1: " << offers_leg1.tip () << std::endl << - " " << offers_leg1.tip ().amount().in << - " : " << offers_leg1.tip ().amount ().out << std::endl << - " Offer2: " << offers_leg2.tip () << std::endl << - " " << offers_leg2.tip ().amount ().in << - " : " << offers_leg2.tip ().amount ().out; - - cross_result = taker.cross(offers_leg1.tip (), offers_leg2.tip ()); - - if (offers_leg1.tip ().fully_consumed ()) - { - leg1_consumed = true; - have_bridged = offers_leg1.step (); - } - if (have_bridged && offers_leg2.tip ().fully_consumed ()) - { - leg2_consumed = true; - have_bridged = offers_leg2.step (); - } - } - - if (cross_result != tesSUCCESS) - { - cross_result = tecFAILED_PROCESSING; - break; - } - - if (taker.done()) - { - m_journal.debug << "The taker reports he's done during crossing!"; - place_order = false; - break; - } - - // Postcondition: If we aren't done, then we *must* have consumed at - // least one offer fully. - assert (direct_consumed || leg1_consumed || leg2_consumed); - - if (!direct_consumed && !leg1_consumed && !leg2_consumed) - { - cross_result = tefINTERNAL; - break; - } - } - - return std::make_pair(cross_result, taker.remaining_offer ()); -} - -} diff --git a/src/ripple/app/transactors/CreateOfferDirect.cpp b/src/ripple/app/transactors/CreateOfferDirect.cpp deleted file mode 100644 index 3b44dd9a276..00000000000 --- a/src/ripple/app/transactors/CreateOfferDirect.cpp +++ /dev/null @@ -1,102 +0,0 @@ -//------------------------------------------------------------------------------ -/* - This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2012, 2013 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 -#include -#include -#include -#include - -namespace ripple { - -std::pair -CreateOffer::crossOffersDirect ( - core::LedgerView& view, - core::Amounts const& taker_amount) -{ - core::Taker::Options const options (mTxn.getFlags()); - - core::Clock::time_point const when ( - mEngine->getLedger ()->getParentCloseTimeNC ()); - - core::LedgerView view_cancel (view.duplicate()); - core::OfferStream offers ( - view, view_cancel, - Book (taker_amount.in.issue(), taker_amount.out.issue()), - when, m_journal); - core::Taker taker (offers.view(), mTxnAccountID, taker_amount, options); - - TER cross_result (tesSUCCESS); - - while (true) - { - // Modifying the order or logic of these - // operations causes a protocol breaking change. - - // Checks which remove offers are performed early so we - // can reduce the size of the order book as much as possible - // before terminating the loop. - - if (taker.done()) - { - m_journal.debug << "The taker reports he's done during crossing!"; - break; - } - - if (! offers.step ()) - { - // Place the order since there are no - // more offers and the order has a balance. - m_journal.debug << "No more offers to consider during crossing!"; - break; - } - - auto const& offer (offers.tip()); - - if (taker.reject (offer.quality())) - { - // Place the order since there are no more offers - // at the desired quality, and the order has a balance. - break; - } - - if (offer.account() == taker.account()) - { - // Skip offer from self. The offer will be considered expired and - // will get deleted. - continue; - } - - if (m_journal.debug) m_journal.debug << - " Offer: " << offer.entry()->getIndex() << std::endl << - " " << offer.amount().in << " : " << offer.amount().out; - - cross_result = taker.cross (offer); - - if (cross_result != tesSUCCESS) - { - cross_result = tecFAILED_PROCESSING; - break; - } - } - - return std::make_pair(cross_result, taker.remaining_offer ()); -} - -} diff --git a/src/ripple/app/transactors/CreateTicket.h b/src/ripple/app/transactors/CreateTicket.h deleted file mode 100644 index 5cadeb98937..00000000000 --- a/src/ripple/app/transactors/CreateTicket.h +++ /dev/null @@ -1,27 +0,0 @@ -//------------------------------------------------------------------------------ -/* - This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2014 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_TX_CREATETICKET_H_INCLUDED -#define RIPPLE_TX_CREATETICKET_H_INCLUDED - -namespace ripple { - -} - -#endif diff --git a/src/ripple/app/transactors/Transactor.cpp b/src/ripple/app/transactors/Transactor.cpp index e7cf9d210ca..17cb2557e45 100644 --- a/src/ripple/app/transactors/Transactor.cpp +++ b/src/ripple/app/transactors/Transactor.cpp @@ -155,57 +155,52 @@ TER Transactor::payFee () TER Transactor::checkSig () { - // Consistency: Check signature - // Verify the transaction's signing public key is the key authorized for signing. - if (mSigningPubKey.getAccountID () == mTxnAccountID) + // Consistency: Check signature and verify the transaction's signing public + // key is the key authorized for signing. + + auto const signing_account = mSigningPubKey.getAccountID (); + + if (signing_account == mTxnAccountID) { - // Authorized to continue. - mSigMaster = true; if (mTxnAccount->isFlag(lsfDisableMaster)) - return tefMASTER_DISABLED; - } - else if (mHasAuthKey && mSigningPubKey.getAccountID () == mTxnAccount->getFieldAccount160 (sfRegularKey)) - { - // Authorized to continue. - } - else if (mHasAuthKey) - { - m_journal.trace << "applyTransaction: Delay: Not authorized to use account."; - return tefBAD_AUTH; + return tefMASTER_DISABLED; + + mSigMaster = true; + return tesSUCCESS; } - else - { - m_journal.trace << "applyTransaction: Invalid: Not authorized to use account."; + if (!mHasAuthKey) + { + m_journal.trace << "Invalid: Not authorized to use account."; return temBAD_AUTH_MASTER; } - return tesSUCCESS; + if (signing_account == mTxnAccount->getFieldAccount160 (sfRegularKey)) + return tesSUCCESS; + + m_journal.trace << "Delay: Not authorized to use account."; + return tefBAD_AUTH; } TER Transactor::checkSeq () { - std::uint32_t t_seq = mTxn.getSequence (); - std::uint32_t a_seq = mTxnAccount->getFieldU32 (sfSequence); - - m_journal.trace << "Aseq=" << a_seq << ", Tseq=" << t_seq; + std::uint32_t const t_seq = mTxn.getSequence (); + std::uint32_t const a_seq = mTxnAccount->getFieldU32 (sfSequence); if (t_seq != a_seq) { if (a_seq < t_seq) { - m_journal.trace << "apply: transaction has future sequence number"; - + m_journal.trace << "Transaction has future sequence number " << + "a_seq=" << a_seq << " t_seq=" << t_seq; return terPRE_SEQ; } - else - { - if (mEngine->getLedger ()->hasTransaction (mTxn.getTransactionID ())) - return tefALREADY; - } - m_journal.warning << "apply: transaction has past sequence number"; + if (mEngine->getLedger ()->hasTransaction (mTxn.getTransactionID ())) + return tefALREADY; + m_journal.trace << "Transaction has past sequence number " << + "a_seq=" << a_seq << " t_seq=" << t_seq; return tefPAST_SEQ; } diff --git a/src/ripple/protocol/Issue.h b/src/ripple/protocol/Issue.h index b7631daf928..7d107920cfc 100644 --- a/src/ripple/protocol/Issue.h +++ b/src/ripple/protocol/Issue.h @@ -86,6 +86,9 @@ bool isConsistent(IssueType const& ac) template std::string to_string (IssueType const& ac) { + if (isXRP (ac.account)) + return to_string (ac.currency); + return to_string(ac.account) + "/" + to_string(ac.currency); } diff --git a/src/ripple/protocol/STAmount.h b/src/ripple/protocol/STAmount.h index 3fc73aebf25..66597b04f5f 100644 --- a/src/ripple/protocol/STAmount.h +++ b/src/ripple/protocol/STAmount.h @@ -56,6 +56,7 @@ class STAmount : public STBase static const int cMinOffset = -96; static const int cMaxOffset = 80; + // Maximum native value supported by the code static const std::uint64_t cMinValue = 1000000000000000ull; static const std::uint64_t cMaxValue = 9999999999999999ull; static const std::uint64_t cMaxNative = 9000000000000000000ull; diff --git a/src/ripple/protocol/TER.h b/src/ripple/protocol/TER.h index b0ad1f46810..1407cfa1ac7 100644 --- a/src/ripple/protocol/TER.h +++ b/src/ripple/protocol/TER.h @@ -193,6 +193,7 @@ enum TER // aka TransactionEngineResult tecINSUFFICIENT_RESERVE = 141, tecNEED_MASTER_KEY = 142, tecDST_TAG_NEEDED = 143, + tecINTERNAL = 144, }; inline bool isTelLocal(TER x) diff --git a/src/ripple/protocol/impl/TER.cpp b/src/ripple/protocol/impl/TER.cpp index a89e50b7140..792b2a91c3f 100644 --- a/src/ripple/protocol/impl/TER.cpp +++ b/src/ripple/protocol/impl/TER.cpp @@ -67,6 +67,7 @@ bool transResultInfo (TER code, std::string& token, std::string& text) { tecINSUFFICIENT_RESERVE, "tecINSUFFICIENT_RESERVE", "Insufficient reserve to complete requested operation." }, { tecNEED_MASTER_KEY, "tecNEED_MASTER_KEY", "The operation requires the use of the Master Key." }, { tecDST_TAG_NEEDED, "tecDST_TAG_NEEDED", "A destination tag is required." }, + { tecINTERNAL, "tecINTERNAL", "An internal error has occurred during processing." }, { tefALREADY, "tefALREADY", "The exact transaction was already in this ledger." }, { tefBAD_ADD_AUTH, "tefBAD_ADD_AUTH", "Not authorized to add account." }, diff --git a/src/ripple/unity/app4.cpp b/src/ripple/unity/app4.cpp index 6355f71634a..e8b8e255046 100644 --- a/src/ripple/unity/app4.cpp +++ b/src/ripple/unity/app4.cpp @@ -21,6 +21,7 @@ #include #include +#include #include #include #include diff --git a/src/ripple/unity/app9.cpp b/src/ripple/unity/app9.cpp index 5e688b25dee..73800ebf2b6 100644 --- a/src/ripple/unity/app9.cpp +++ b/src/ripple/unity/app9.cpp @@ -32,7 +32,5 @@ #include #include #include -#include -#include #include #include