Example #1
0
TER PathCursor::liquidity () const
{
    TER resultCode = tecPATH_DRY;
    PathCursor pc = *this;

    pathState_.resetView (rippleCalc_.view);

    for (pc.nodeIndex_ = pc.nodeSize(); pc.nodeIndex_--; )
    {
        JLOG (j_.trace())
            << "reverseLiquidity>"
            << " nodeIndex=" << pc.nodeIndex_
            << ".issue_.account=" << to_string (pc.node().issue_.account);

        resultCode = pc.reverseLiquidity();

        if (!pc.node().transferRate_)
            return tefINTERNAL;

        JLOG (j_.trace())
            << "reverseLiquidity< "
            << "nodeIndex=" << pc.nodeIndex_
            << " resultCode=" << transToken (resultCode)
            << " transferRate_=" << *pc.node().transferRate_
            << ": " << resultCode;

        if (resultCode != tesSUCCESS)
            break;
    }

    // VFALCO-FIXME this generates errors
    // JLOG (j_.trace())
    //     << "nextIncrement: Path after reverse: " << pathState_.getJson ();

    if (resultCode != tesSUCCESS)
        return resultCode;

    pathState_.resetView (rippleCalc_.view);

    for (pc.nodeIndex_ = 0; pc.nodeIndex_ < pc.nodeSize(); ++pc.nodeIndex_)
    {
        JLOG (j_.trace())
            << "forwardLiquidity> nodeIndex=" << nodeIndex_;

        resultCode = pc.forwardLiquidity();
        if (resultCode != tesSUCCESS)
            return resultCode;

        JLOG (j_.trace())
            << "forwardLiquidity<"
            << " nodeIndex:" << pc.nodeIndex_
            << " resultCode:" << resultCode;

        if (pathState_.isDry())
            resultCode = tecPATH_DRY;
    }
    return resultCode;
}
Example #2
0
// - A ripple nodes output issuer must be the node's account or the next node's
//   account.
// - Offers can only go directly to another offer if the currency and issuer are
//   an exact match.
// - Real issuers must be specified for non-XRP.
TER PathState::pushImpliedNodes (
    AccountID const& account,    // --> Delivering to this account.
    Currency const& currency,  // --> Delivering this currency.
    AccountID const& issuer)    // --> Delivering this issuer.
{
    TER resultCode = tesSUCCESS;

     JLOG (j_.trace) << "pushImpliedNodes>" <<
         " " << account <<
         " " << currency <<
         " " << issuer;

    if (nodes_.back ().issue_.currency != currency)
    {
        // Currency is different, need to convert via an offer from an order
        // book.  xrpAccount() does double duty as signaling "this is an order
        // book".

        // Corresponds to "Implies an offer directory" in the diagram, currently
        // at http://goo.gl/Uj3HAB.

        auto type = isXRP(currency) ? STPathElement::typeCurrency
            : STPathElement::typeCurrency | STPathElement::typeIssuer;

        // The offer's output is what is now wanted.
        // xrpAccount() is a placeholder for offers.
        resultCode = pushNode (type, xrpAccount(), currency, issuer);
    }


    // For ripple, non-XRP, ensure the issuer is on at least one side of the
    // transaction.
    if (resultCode == tesSUCCESS
        && !isXRP(currency)
        && nodes_.back ().account_ != issuer
        // Previous is not issuing own IOUs.
        && account != issuer)
        // Current is not receiving own IOUs.
    {
        // Need to ripple through issuer's account.
        // Case "Implies an another node: (pushImpliedNodes)" in the document.
        // Intermediate account is the needed issuer.
        resultCode = pushNode (
            STPathElement::typeAll, issuer, currency, issuer);
    }

    JLOG (j_.trace)
        << "pushImpliedNodes< : " << transToken (resultCode);

    return resultCode;
}
Example #3
0
bool RippleCalc::addPathState(STPath const& path, TER& resultCode)
{
    auto pathState = std::make_shared<PathState> (
        saDstAmountReq_, saMaxAmountReq_);

    if (!pathState)
    {
        resultCode = temUNKNOWN;
        return false;
    }

    pathState->expandPath (
        mActiveLedger,
        path,
        uDstAccountID_,
        uSrcAccountID_);

    if (pathState->status() == tesSUCCESS)
        pathState->checkNoRipple (uDstAccountID_, uSrcAccountID_);

    if (pathState->status() == tesSUCCESS)
        pathState->checkFreeze ();

    pathState->setIndex (pathStateList_.size ());

    WriteLog (lsDEBUG, RippleCalc)
        << "rippleCalc: Build direct:"
        << " status: " << transToken (pathState->status());

    // Return if malformed.
    if (isTemMalformed (pathState->status()))
    {
        resultCode = pathState->status();
        return false;
    }

    if (pathState->status () == tesSUCCESS)
    {
        resultCode = pathState->status();
        pathStateList_.push_back (pathState);
    }
    else if (pathState->status () != terNO_LINE)
    {
        resultCode = pathState->status();
    }

    return true;
}
Example #4
0
// Make sure last path node delivers to uAccountID: uCurrencyID from uIssuerID.
//
// If the unadded next node as specified by arguments would not work as is, then add the necessary nodes so it would work.
//
// Rules:
// - Currencies must be converted via an offer.
// - A node names it's output.
// - A ripple nodes output issuer must be the node's account or the next node's account.
// - Offers can only go directly to another offer if the currency and issuer are an exact match.
// - Real issuers must be specified for non-STR.
TER PathState::pushImply (
    const uint160& uAccountID,  // --> Delivering to this account.
    const uint160& uCurrencyID, // --> Delivering this currency.
    const uint160& uIssuerID)   // --> Delivering this issuer.
{
    const Node&  pnPrv       = vpnNodes.back ();
    TER          terResult   = tesSUCCESS;

    WriteLog (lsTRACE, RippleCalc) << "pushImply>" <<
        " " << RippleAddress::createHumanAccountID (uAccountID) <<
        " " << STAmount::createHumanCurrency (uCurrencyID) <<
        " " << RippleAddress::createHumanAccountID (uIssuerID);

    if (pnPrv.uCurrencyID != uCurrencyID)
    {
        // Currency is different, need to convert via an offer.

        terResult   = pushNode ( // Offer.
                          !!uCurrencyID
                          ? STPathElement::typeCurrency | STPathElement::typeIssuer
                          : STPathElement::typeCurrency,
                          ACCOUNT_STR,                    // Placeholder for offers.
                          uCurrencyID,                    // The offer's output is what is now wanted.
                          uIssuerID);
    }

    const Node&  pnBck       = vpnNodes.back ();

    // For ripple, non-STR, ensure the issuer is on at least one side of the transaction.
    if (tesSUCCESS == terResult
            && !!uCurrencyID                                // Not STR.
            && (pnBck.uAccountID != uIssuerID               // Previous is not issuing own IOUs.
                && uAccountID != uIssuerID))                // Current is not receiving own IOUs.
    {
        // Need to ripple through uIssuerID's account.

        terResult   = pushNode (
                          STPathElement::typeAccount | STPathElement::typeCurrency | STPathElement::typeIssuer,
                          uIssuerID,                      // Intermediate account is the needed issuer.
                          uCurrencyID,
                          uIssuerID);
    }

    WriteLog (lsTRACE, RippleCalc) << "pushImply< : " << transToken (terResult);

    return terResult;
}
Example #5
0
TER computeReverseLiqudity (
    RippleCalc& rippleCalc,
    const unsigned int nodeIndex, PathState& pathState,
    const bool bMultiQuality)
{
    auto& node = pathState.nodes()[nodeIndex];

    // Every account has a transfer rate for its issuances.

    // TOMOVE: The account charges
    // a fee when third parties transfer that account's own issuances.

    // node.transferRate_ caches the output transfer rate for this node.
    node.transferRate_ = STAmount::saFromRate (
        rippleCalc.mActiveLedger.rippleTransferRate (node.issuer_));

    WriteLog (lsTRACE, RippleCalc)
        << "computeReverseLiqudity>"
        << " nodeIndex=" << nodeIndex
        << " issuer_=" << RippleAddress::createHumanAccountID (node.issuer_)
        << " transferRate_=" << node.transferRate_;

    auto resultCode = node.isAccount()
        ? computeReverseLiquidityForAccount (
            rippleCalc, nodeIndex, pathState, bMultiQuality)
        : computeReverseLiquidityForOffer (
            rippleCalc, nodeIndex, pathState, bMultiQuality);

    // Do previous.
    if (resultCode == tesSUCCESS && nodeIndex)
    {
        // Continue in reverse.  TODO(tom): remove unnecessary recursion.
        resultCode = computeReverseLiqudity (
            rippleCalc, nodeIndex - 1, pathState, bMultiQuality);
    }

    WriteLog (lsTRACE, RippleCalc)
        << "computeReverseLiqudity< "
        << "nodeIndex=" << nodeIndex
        << " resultCode=%s" << transToken (resultCode)
        << "/" << resultCode;

    return resultCode;
}
Example #6
0
// Append a node and insert before it any implied nodes.
// Offers may go back to back.
// <-- terResult: tesSUCCESS, temBAD_PATH, terNO_ACCOUNT, terNO_AUTH, terNO_LINE, tecPATH_DRY
TER PathState::pushNode (
    const int iType,
    const uint160& uAccountID,
    const uint160& uCurrencyID,
    const uint160& uIssuerID)
{
    Node                pnCur;
    const bool          bFirst      = vpnNodes.empty ();
    const Node&         pnPrv       = bFirst ? Node () : vpnNodes.back ();
    // true, iff node is a ripple account. false, iff node is an offer node.
    const bool          bAccount    = is_bit_set (iType, STPathElement::typeAccount);
    // true, iff currency supplied.
    // Currency is specified for the output of the current node.
    const bool          bCurrency   = is_bit_set (iType, STPathElement::typeCurrency);
    // Issuer is specified for the output of the current node.
    const bool          bIssuer     = is_bit_set (iType, STPathElement::typeIssuer);
    TER                 terResult   = tesSUCCESS;

    WriteLog (lsTRACE, RippleCalc) << "pushNode> " <<
       iType <<
       ": " << (bAccount ? RippleAddress::createHumanAccountID (uAccountID) : "-") <<
       " " << (bCurrency ? STAmount::createHumanCurrency (uCurrencyID) : "-") <<
       "/" << (bIssuer ? RippleAddress::createHumanAccountID (uIssuerID) : "-");

    pnCur.uFlags        = iType;
    pnCur.uCurrencyID   = bCurrency ? uCurrencyID : pnPrv.uCurrencyID;

    if (iType & ~STPathElement::typeValidBits)
    {
        WriteLog (lsDEBUG, RippleCalc) << "pushNode: bad bits.";

        terResult   = temBAD_PATH;
    }
    else if (bIssuer && !pnCur.uCurrencyID)
    {
        WriteLog (lsDEBUG, RippleCalc) << "pushNode: issuer specified for STR.";

        terResult   = temBAD_PATH;
    }
    else if (bIssuer && !uIssuerID)
    {
        WriteLog (lsDEBUG, RippleCalc) << "pushNode: specified bad issuer.";

        terResult   = temBAD_PATH;
    }
    else if (!bAccount && !bCurrency && !bIssuer)
    {
        WriteLog (lsDEBUG, RippleCalc) << "pushNode: offer must specify at least currency or issuer.";

        terResult   = temBAD_PATH;
    }
    else if (bAccount)
    {
        // Account link

        pnCur.uAccountID    = uAccountID;
        pnCur.uIssuerID     = bIssuer
                              ? uIssuerID
                              : !!pnCur.uCurrencyID
                              ? uAccountID
                              : ACCOUNT_STR;
        pnCur.saRevRedeem   = STAmount (pnCur.uCurrencyID, uAccountID);
        pnCur.saRevIssue    = STAmount (pnCur.uCurrencyID, uAccountID);
        pnCur.saRevDeliver  = STAmount (pnCur.uCurrencyID, pnCur.uIssuerID);
        pnCur.saFwdDeliver  = pnCur.saRevDeliver;

        if (bFirst)
        {
            // The first node is always correct as is.

            nothing ();
        }
        else if (!uAccountID)
        {
            WriteLog (lsDEBUG, RippleCalc) << "pushNode: specified bad account.";

            terResult   = temBAD_PATH;
        }
        else
        {
            // Add required intermediate nodes to deliver to current account.
            WriteLog (lsTRACE, RippleCalc) << "pushNode: imply for account.";

            terResult   = pushImply (
                              pnCur.uAccountID,                                   // Current account.
                              pnCur.uCurrencyID,                                  // Wanted currency.
                              !!pnCur.uCurrencyID ? uAccountID : ACCOUNT_STR);    // Account as wanted issuer.

            // Note: pnPrv may no longer be the immediately previous node.
        }

        if (tesSUCCESS == terResult && !vpnNodes.empty ())
        {
            const Node&     pnBck       = vpnNodes.back ();
            bool            bBckAccount = is_bit_set (pnBck.uFlags, STPathElement::typeAccount);

            if (bBckAccount)
            {
                SLE::pointer    sleRippleState  = lesEntries.entryCache (ltRIPPLE_STATE, Ledger::getRippleStateIndex (pnBck.uAccountID, pnCur.uAccountID, pnPrv.uCurrencyID));

                if (!sleRippleState)
                {
                    WriteLog (lsTRACE, RippleCalc) << "pushNode: No credit line between "
                                                   << RippleAddress::createHumanAccountID (pnBck.uAccountID)
                                                   << " and "
                                                   << RippleAddress::createHumanAccountID (pnCur.uAccountID)
                                                   << " for "
                                                   << STAmount::createHumanCurrency (pnCur.uCurrencyID)
                                                   << "." ;

                    WriteLog (lsTRACE, RippleCalc) << getJson ();

                    terResult   = terNO_LINE;
                }
                else
                {
                    WriteLog (lsTRACE, RippleCalc) << "pushNode: Credit line found between "
                                                   << RippleAddress::createHumanAccountID (pnBck.uAccountID)
                                                   << " and "
                                                   << RippleAddress::createHumanAccountID (pnCur.uAccountID)
                                                   << " for "
                                                   << STAmount::createHumanCurrency (pnCur.uCurrencyID)
                                                   << "." ;

                    SLE::pointer        sleBck  = lesEntries.entryCache (ltACCOUNT_ROOT, Ledger::getAccountRootIndex (pnBck.uAccountID));
                    bool                bHigh   = pnBck.uAccountID > pnCur.uAccountID;

                    if (!sleBck)
                    {
                        WriteLog (lsWARNING, RippleCalc) << "pushNode: delay: can't receive IOUs from non-existent issuer: " << RippleAddress::createHumanAccountID (pnBck.uAccountID);

                        terResult   = terNO_ACCOUNT;
                    }
                    else if ((is_bit_set (sleBck->getFieldU32 (sfFlags), lsfRequireAuth)
                             && !is_bit_set (sleRippleState->getFieldU32 (sfFlags), (bHigh ? lsfHighAuth : lsfLowAuth)))) 
                    {
                        WriteLog (lsWARNING, RippleCalc) << "pushNode: delay: can't receive IOUs from issuer without auth.";

                        terResult   = terNO_AUTH;
                    }

                    if (tesSUCCESS == terResult)
                    {
                        STAmount    saOwed  = lesEntries.rippleOwed (pnCur.uAccountID, pnBck.uAccountID, pnCur.uCurrencyID);
                        STAmount    saLimit;

                        if (saOwed <= zero
                                && -saOwed >= (saLimit = lesEntries.rippleLimit (pnCur.uAccountID, pnBck.uAccountID, pnCur.uCurrencyID)))
                        {
                            WriteLog (lsWARNING, RippleCalc) <<
                                "pushNode: dry:" <<
                                " saOwed=" << saOwed <<
                                " saLimit=" << saLimit;

                            terResult   = tecPATH_DRY;
                        }
                    }
                }
            }
        }

        if (tesSUCCESS == terResult)
        {
            vpnNodes.push_back (pnCur);
        }
    }
    else
    {
        // Offer link
        // Offers bridge a change in currency & issuer or just a change in issuer.
        pnCur.uIssuerID     = bIssuer
                              ? uIssuerID
                              : !!pnCur.uCurrencyID
                              ? !!pnPrv.uIssuerID
                              ? pnPrv.uIssuerID   // Default to previous issuer
                              : pnPrv.uAccountID  // Or previous account if no previous issuer.
                      : ACCOUNT_STR;
        pnCur.saRateMax     = saZero;
        pnCur.saRevDeliver  = STAmount (pnCur.uCurrencyID, pnCur.uIssuerID);
        pnCur.saFwdDeliver  = pnCur.saRevDeliver;

        if (!!pnCur.uCurrencyID != !!pnCur.uIssuerID)
        {
            WriteLog (lsDEBUG, RippleCalc) << "pushNode: currency is inconsistent with issuer.";

            terResult   = temBAD_PATH;
        }
        else if(!LedgerDump::enactHistoricalQuirk (QuirkSameCurrencyOffer) &&
                pnPrv.uCurrencyID == pnCur.uCurrencyID &&
                pnPrv.uIssuerID == pnCur.uIssuerID)
        {
            WriteLog(lsDEBUG, RippleCalc) <<
                "pushNode: bad path: offer to same currency and issuer";
            terResult = temBAD_PATH;
        }
        else
        {
            // Previous is an account.
            WriteLog (lsTRACE, RippleCalc) << "pushNode: imply for offer.";

            // Insert intermediary issuer account if needed.
            terResult   = pushImply (
                ACCOUNT_STR, // Rippling, but offers don't have an account.
                pnPrv.uCurrencyID,
                pnPrv.uIssuerID);
        }

        if (tesSUCCESS == terResult)
        {
            vpnNodes.push_back (pnCur);
        }
    }

    WriteLog (lsTRACE, RippleCalc) << "pushNode< : " << transToken (terResult);

    return terResult;
}
Example #7
0
// OPTIMIZE: When calculating path increment, note if increment consumes all
// liquidity. No need to revisit path in the future if all liquidity is used.
//
TER PathCursor::advanceNode (bool const bReverse) const
{
    TER resultCode = tesSUCCESS;

    // Taker is the active party against an offer in the ledger - the entity
    // that is taking advantage of an offer in the order book.
    WriteLog (lsTRACE, RippleCalc)
            << "advanceNode: TakerPays:"
            << node().saTakerPays << " TakerGets:" << node().saTakerGets;

    int loopCount = 0;

    do
    {
        // VFALCO NOTE Why not use a for() loop?
        // VFALCO TODO The limit on loop iterations puts an
        //             upper limit on the number of different quality
        // levels (ratio of pay:get) that will be considered for one path.
        // Changing this value has repercusssions on validation and consensus.
        //
        if (++loopCount > NODE_ADVANCE_MAX_LOOPS)
        {
            WriteLog (lsWARNING, RippleCalc) << "Loop count exceeded";
            return tefEXCEPTION;
        }

        bool bDirectDirDirty = node().directory.initialize (
            { previousNode().issue_, node().issue_},
            view());

        if (auto advance = node().directory.advance (view()))
        {
            bDirectDirDirty = true;
            if (advance == NodeDirectory::NEW_QUALITY)
            {
                // We didn't run off the end of this order book and found
                // another quality directory.
                WriteLog (lsTRACE, RippleCalc)
                    << "advanceNode: Quality advance: node.directory.current="
                    << node().directory.current;
            }
            else if (bReverse)
            {
                WriteLog (lsTRACE, RippleCalc)
                    << "advanceNode: No more offers.";

                node().offerIndex_ = 0;
                break;
            }
            else
            {
                // No more offers. Should be done rather than fall off end of
                // book.
                WriteLog (lsWARNING, RippleCalc)
                    << "advanceNode: Unreachable: "
                    << "Fell off end of order book.";
                // FIXME: why?
                return telFAILED_PROCESSING;
            }
        }

        if (bDirectDirDirty)
        {
            // Our quality changed since last iteration.
            // Use the rate from the directory.
            node().saOfrRate = amountFromQuality (
                getQuality (node().directory.current));
            // For correct ratio
            node().uEntry = 0;
            node().bEntryAdvance   = true;

            WriteLog (lsTRACE, RippleCalc)
                << "advanceNode: directory dirty: node.saOfrRate="
                << node().saOfrRate;
        }

        if (!node().bEntryAdvance)
        {
            if (node().bFundsDirty)
            {
                // We were called again probably merely to update structure
                // variables.
                node().saTakerPays
                        = node().sleOffer->getFieldAmount (sfTakerPays);
                node().saTakerGets
                        = node().sleOffer->getFieldAmount (sfTakerGets);

                // Funds left.
                node().saOfferFunds = accountFunds(view(),
                    node().offerOwnerAccount_,
                    node().saTakerGets,
                    fhZERO_IF_FROZEN,
                    getConfig());
                node().bFundsDirty = false;

                WriteLog (lsTRACE, RippleCalc)
                    << "advanceNode: funds dirty: node().saOfrRate="
                    << node().saOfrRate;
            }
            else
            {
                WriteLog (lsTRACE, RippleCalc) << "advanceNode: as is";
            }
        }
        else if (!dirNext (view(),
            node().directory.current,
            node().directory.ledgerEntry,
            node().uEntry,
            node().offerIndex_))
            // This is the only place that offerIndex_ changes.
        {
            // Failed to find an entry in directory.
            // Do another cur directory iff multiQuality_
            if (multiQuality_)
            {
                // We are allowed to process multiple qualities if this is the
                // only path.
                WriteLog (lsTRACE, RippleCalc)
                    << "advanceNode: next quality";
                node().directory.advanceNeeded  = true;  // Process next quality.
            }
            else if (!bReverse)
            {
                // We didn't run dry going backwards - why are we running dry
                // going forwards - this should be impossible!
                // TODO(tom): these warnings occur in production!  They
                // shouldn't.
                WriteLog (lsWARNING, RippleCalc)
                    << "advanceNode: unreachable: ran out of offers";
                return telFAILED_PROCESSING;
            }
            else
            {
                // Ran off end of offers.
                node().bEntryAdvance   = false;        // Done.
                node().offerIndex_ = 0;            // Report no more entries.
            }
        }
        else
        {
            // Got a new offer.
            node().sleOffer = view().peek (keylet::offer(node().offerIndex_));

            if (!node().sleOffer)
            {
                // Corrupt directory that points to an entry that doesn't exist.
                // This has happened in production.
                WriteLog (lsWARNING, RippleCalc) <<
                    "Missing offer in directory";
                node().bEntryAdvance = true;
            }
            else
            {
                node().offerOwnerAccount_
                        = node().sleOffer->getAccountID (sfAccount);
                node().saTakerPays
                        = node().sleOffer->getFieldAmount (sfTakerPays);
                node().saTakerGets
                        = node().sleOffer->getFieldAmount (sfTakerGets);

                AccountIssue const accountIssue (
                    node().offerOwnerAccount_, node().issue_);

                WriteLog (lsTRACE, RippleCalc)
                    << "advanceNode: offerOwnerAccount_="
                    << to_string (node().offerOwnerAccount_)
                    << " node.saTakerPays=" << node().saTakerPays
                    << " node.saTakerGets=" << node().saTakerGets
                    << " node.offerIndex_=" << node().offerIndex_;

                if (node().sleOffer->isFieldPresent (sfExpiration) &&
                        (node().sleOffer->getFieldU32 (sfExpiration) <=
                            view().parentCloseTime()))
                {
                    // Offer is expired.
                    WriteLog (lsTRACE, RippleCalc)
                        << "advanceNode: expired offer";
                    rippleCalc_.permanentlyUnfundedOffers_.insert(
                        node().offerIndex_);
                    continue;
                }

                if (node().saTakerPays <= zero || node().saTakerGets <= zero)
                {
                    // Offer has bad amounts. Offers should never have a bad
                    // amounts.
                    auto const index = node().offerIndex_;
                    if (bReverse)
                    {
                        // Past internal error, offer had bad amounts.
                        // This has occurred in production.
                        WriteLog (lsWARNING, RippleCalc)
                            << "advanceNode: PAST INTERNAL ERROR"
                            << " REVERSE: OFFER NON-POSITIVE:"
                            << " node.saTakerPays=" << node().saTakerPays
                            << " node.saTakerGets=" << node().saTakerGets;

                        // Mark offer for always deletion.
                        rippleCalc_.permanentlyUnfundedOffers_.insert (
                            node().offerIndex_);
                    }
                    else if (rippleCalc_.permanentlyUnfundedOffers_.find (index)
                             != rippleCalc_.permanentlyUnfundedOffers_.end ())
                    {
                        // Past internal error, offer was found failed to place
                        // this in permanentlyUnfundedOffers_.
                        // Just skip it. It will be deleted.
                        WriteLog (lsDEBUG, RippleCalc)
                            << "advanceNode: PAST INTERNAL ERROR "
                            << " FORWARD CONFIRM: OFFER NON-POSITIVE:"
                            << " node.saTakerPays=" << node().saTakerPays
                            << " node.saTakerGets=" << node().saTakerGets;

                    }
                    else
                    {
                        // Reverse should have previously put bad offer in list.
                        // An internal error previously left a bad offer.
                        WriteLog (lsWARNING, RippleCalc)
                            << "advanceNode: INTERNAL ERROR"

                            <<" FORWARD NEWLY FOUND: OFFER NON-POSITIVE:"
                            << " node.saTakerPays=" << node().saTakerPays
                            << " node.saTakerGets=" << node().saTakerGets;

                        // Don't process at all, things are in an unexpected
                        // state for this transactions.
                        resultCode = tefEXCEPTION;
                    }

                    continue;
                }

                // Allowed to access source from this node?
                //
                // XXX This can get called multiple times for same source in a
                // row, caching result would be nice.
                //
                // XXX Going forward could we fund something with a worse
                // quality which was previously skipped? Might need to check
                // quality.
                auto itForward = pathState_.forward().find (accountIssue);
                const bool bFoundForward =
                        itForward != pathState_.forward().end ();

                // Only allow a source to be used once, in the first node
                // encountered from initial path scan.  This prevents
                // conflicting uses of the same balance when going reverse vs
                // forward.
                if (bFoundForward &&
                    itForward->second != nodeIndex_ &&
                    node().offerOwnerAccount_ != node().issue_.account)
                {
                    // Temporarily unfunded. Another node uses this source,
                    // ignore in this offer.
                    WriteLog (lsTRACE, RippleCalc)
                        << "advanceNode: temporarily unfunded offer"
                        << " (forward)";
                    continue;
                }

                // This is overly strict. For contributions to past. We should
                // only count source if actually used.
                auto itReverse = pathState_.reverse().find (accountIssue);
                bool bFoundReverse = itReverse != pathState_.reverse().end ();

                // For this quality increment, only allow a source to be used
                // from a single node, in the first node encountered from
                // applying offers in reverse.
                if (bFoundReverse &&
                    itReverse->second != nodeIndex_ &&
                    node().offerOwnerAccount_ != node().issue_.account)
                {
                    // Temporarily unfunded. Another node uses this source,
                    // ignore in this offer.
                    WriteLog (lsTRACE, RippleCalc)
                        << "advanceNode: temporarily unfunded offer"
                        <<" (reverse)";
                    continue;
                }

                // Determine if used in past.
                // We only need to know if it might need to be marked unfunded.
                auto itPast = rippleCalc_.mumSource_.find (accountIssue);
                bool bFoundPast = (itPast != rippleCalc_.mumSource_.end ());

                // Only the current node is allowed to use the source.

                node().saOfferFunds = accountFunds(view(),
                    node().offerOwnerAccount_,
                    node().saTakerGets,
                    fhZERO_IF_FROZEN,
                    getConfig());
                // Funds held.

                if (node().saOfferFunds <= zero)
                {
                    // Offer is unfunded.
                    WriteLog (lsTRACE, RippleCalc)
                        << "advanceNode: unfunded offer";

                    if (bReverse && !bFoundReverse && !bFoundPast)
                    {
                        // Never mentioned before, clearly just: found unfunded.
                        // That is, even if this offer fails due to fill or kill
                        // still do deletions.
                        // Mark offer for always deletion.
                        rippleCalc_.permanentlyUnfundedOffers_.insert (node().offerIndex_);
                    }
                    else
                    {
                        // Moving forward, don't need to insert again
                        // Or, already found it.
                    }

                    // YYY Could verify offer is correct place for unfundeds.
                    continue;
                }

                if (bReverse            // Need to remember reverse mention.
                    && !bFoundPast      // Not mentioned in previous passes.
                    && !bFoundReverse)  // New to pass.
                {
                    // Consider source mentioned by current path state.
                    WriteLog (lsTRACE, RippleCalc)
                        << "advanceNode: remember="
                        <<  node().offerOwnerAccount_
                        << "/"
                        << node().issue_;

                    pathState_.insertReverse (accountIssue, nodeIndex_);
                }

                node().bFundsDirty = false;
                node().bEntryAdvance = false;
            }
        }
    }
    while (resultCode == tesSUCCESS &&
           (node().bEntryAdvance || node().directory.advanceNeeded));

    if (resultCode == tesSUCCESS)
    {
        WriteLog (lsTRACE, RippleCalc)
            << "advanceNode: node.offerIndex_=" << node().offerIndex_;
    }
    else
    {
        WriteLog (lsDEBUG, RippleCalc)
            << "advanceNode: resultCode=" << transToken (resultCode);
    }

    return resultCode;
}
Example #8
0
TER
CreateOffer::doApply ()
{
    if (m_journal.debug) m_journal.debug <<
        "OfferCreate> " << mTxn.getJson (0);

    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 (!saTakerPays.isLegalNet () || !saTakerGets.isLegalNet ())
        return temBAD_AMOUNT;

    auto const& uPaysIssuerID = saTakerPays.getIssuer ();
    auto const& uPaysCurrency = saTakerPays.getCurrency ();

    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 seuqnce
    //       number insufficient?

    std::uint32_t const uAccountSequenceNext = mTxnAccount->getFieldU32 (sfSequence);
    std::uint32_t const uSequence = mTxn.getSequence ();

    const uint256 uLedgerIndex = Ledger::getOfferIndex (mTxnAccountID, uSequence);

    if (m_journal.debug)
    {
        m_journal.debug <<
            "Creating offer node: " << to_string (uLedgerIndex) <<
            " uSequence=" << uSequence;

        if (bImmediateOrCancel)
            m_journal.debug << "Transaction: IoC set.";

        if (bFillOrKill)
            m_journal.debug << "Transaction: FoK set.";
    }

    // 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 = STAmount::getRate (saTakerGets, saTakerPays);

    TER terResult (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, Ledger::getAccountRootIndex (mTxnAccountID));

    if (uTxFlags & tfOfferCreateMask)
    {
        if (m_journal.debug) m_journal.debug <<
            "Malformed transaction: Invalid flags set.";

        terResult = temINVALID_FLAG;
    }
    else if (bImmediateOrCancel && bFillOrKill)
    {
        if (m_journal.debug) m_journal.debug <<
            "Malformed transaction: both IoC and FoK set.";

        terResult = temINVALID_FLAG;
    }
    else if (bHaveExpiration && !uExpiration)
    {
        m_journal.warning <<
            "Malformed offer: bad expiration";

        terResult = temBAD_EXPIRATION;
    }
    else if (saTakerPays.isNative () && saTakerGets.isNative ())
    {
        m_journal.warning <<
            "Malformed offer: XRP for XRP";

        terResult = temBAD_OFFER;
    }
    else if (saTakerPays <= zero || saTakerGets <= zero)
    {
        m_journal.warning <<
            "Malformed offer: bad amount";

        terResult = temBAD_OFFER;
    }
    else if (uPaysCurrency == uGetsCurrency && uPaysIssuerID == uGetsIssuerID)
    {
        m_journal.warning <<
            "Malformed offer: redundant offer";

        terResult = 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.";

        terResult = temBAD_CURRENCY;
    }
    else if (saTakerPays.isNative () != !uPaysIssuerID ||
             saTakerGets.isNative () != !uGetsIssuerID)
    {
        m_journal.warning <<
            "Malformed offer: bad issuer";

        terResult = temBAD_ISSUER;
    }
    else if (view.isGlobalFrozen (uPaysIssuerID) || view.isGlobalFrozen (uGetsIssuerID))
    {
        m_journal.warning <<
            "Offer involves frozen asset";

        terResult = tecFROZEN;
    }
    else if (view.accountFunds (
        mTxnAccountID, saTakerGets, fhZERO_IF_FROZEN) <= zero)
    {
        m_journal.warning <<
            "delay: Offers must be at least partially funded.";

        terResult = 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;

        terResult = temBAD_SEQUENCE;
    }

    if (terResult != tesSUCCESS)
    {
        if (m_journal.debug) m_journal.debug <<
            "final terResult=" << transToken (terResult);

        return terResult;
    }

    // Process a cancellation request that's passed along with an offer.
    if ((terResult == tesSUCCESS) && bHaveCancel)
    {
        uint256 const uCancelIndex (
            Ledger::getOfferIndex (mTxnAccountID, uCancelSequence));
        SLE::pointer sleCancel = mEngine->entryCache (ltOFFER, uCancelIndex);

        // 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)
        {
            m_journal.warning <<
                "Cancelling order with sequence " << uCancelSequence;

            terResult = view.offerDelete (sleCancel);
        }
    }

    // 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;
    }

    // Make sure that we are authorized to hold what the taker will pay us.
    if (terResult == tesSUCCESS && !saTakerPays.isNative ())
        terResult = checkAcceptAsset (Issue (uPaysCurrency, uPaysIssuerID));

    bool crossed = false;
    bool const bOpenLedger (mParams & tapOPEN_LEDGER);

    if (terResult == tesSUCCESS)
    {
        // We reverse gets and pays because during offer crossing we are taking.
        core::Amounts const taker_amount (saTakerGets, saTakerPays);

        // 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;

        std::tie(terResult, place_offer) = crossOffers (view, taker_amount);

        if (terResult == tecFAILED_PROCESSING && bOpenLedger)
            terResult = telFAILED_PROCESSING;

        if (terResult == 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 ());

            if (taker_amount != place_offer)
                crossed = true;

            if (m_journal.debug)
            {
                m_journal.debug << "Offer Crossing: " << transToken (terResult);

                if (terResult == tesSUCCESS)
                {
                    m_journal.debug <<
                        "    takerPays: " << saTakerPays.getFullText () <<
                        " -> " << place_offer.out.getFullText ();
                    m_journal.debug <<
                        "    takerGets: " << saTakerGets.getFullText () <<
                        " -> " << place_offer.in.getFullText ();
                }
            }

            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=" <<saTakerPays.getFullText ();
        m_journal.debug <<
            "takeOffers: saTakerGets=" << saTakerGets.getFullText ();
        m_journal.debug <<
            "takeOffers: mTxnAccountID=" <<
            to_string (mTxnAccountID);
        m_journal.debug <<
            "takeOffers:         FUNDS=" << view.accountFunds (
            mTxnAccountID, saTakerGets, fhZERO_IF_FROZEN).getFullText ();
    }

    if (saTakerPays < zero || saTakerGets < zero)
    {
        // Earlier, we verified that the amounts, as specified in the offer,
        // were not negative. That they are now suggests that something went
        // very wrong with offer crossing.
        m_journal.fatal << (crossed ? "Partially consumed" : "Full") <<
            " offer has negative component:" <<
            " pays=" << saTakerPays.getFullText () <<
            " gets=" << saTakerGets.getFullText ();

        assert (saTakerPays >= zero);
        assert (saTakerGets >= zero);
        return tefINTERNAL;
    }

    if (bFillOrKill && (saTakerPays != zero || saTakerGets != zero))
    {
        // Fill or kill and have leftovers.
        view.swapWith (view_checkpoint); // Restore with just fees paid.
        return tesSUCCESS;
    }

    // What the reserve would be if this offer was placed.
    auto const accountReserve (mEngine->getLedger ()->getReserve (
        sleCreator->getFieldU32 (sfOwnerCount) + 1));

    if (saTakerPays == zero ||                // Wants nothing more.
        saTakerGets == zero ||                // Offering nothing more.
        bImmediateOrCancel)                   // Do not persist.
    {
        // Complete as is.
    }
    else if (mPriorBalance.getNValue () < accountReserve)
    {
        // If we are here, the signing account had an insufficient reserve
        // *prior* to our processing. We use the prior balance to simplify
        // client writing and make the user experience better.

        if (bOpenLedger) // Ledger is not final, can vote no.
        {
            // Hope for more reserve to come in or more offers to consume. If we
            // specified a local error this transaction will not be retried, so
            // specify a tec to distribute the transaction and allow it to be
            // retried. In particular, it may have been successful to a
            // degree (partially filled) and if it hasn't, it might succeed.
            terResult = tecINSUF_RESERVE_OFFER;
        }
        else if (!crossed)
        {
            // Ledger is final, insufficent reserve to create offer, processed
            // nothing.
            terResult = tecINSUF_RESERVE_OFFER;
        }
        else
        {
            // Ledger is final, insufficent reserve to create offer, processed
            // something.

            // Consider the offer unfunded. Treat as tesSUCCESS.
        }
    }
    else
    {
        assert (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 ();

        std::uint64_t uOwnerNode;
        std::uint64_t uBookNode;
        uint256 uDirectory;

        // Add offer to owner's directory.
        terResult = view.dirAdd (uOwnerNode,
            Ledger::getOwnerDirIndex (mTxnAccountID), uLedgerIndex,
            std::bind (
                &Ledger::ownerDirDescriber, std::placeholders::_1,
                std::placeholders::_2, mTxnAccountID));

        if (tesSUCCESS == terResult)
        {
            // Update owner count.
            view.ownerCountAdjust (mTxnAccountID, 1, sleCreator);

            uint256 const uBookBase (Ledger::getBookBase (
                {{uPaysCurrency, uPaysIssuerID},
                    {uGetsCurrency, uGetsIssuerID}}));

            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 ());

            // We use the original rate to place the offer.
            uDirectory = Ledger::getQualityIndex (uBookBase, uRate);

            // Add offer to order book.
            terResult = view.dirAdd (uBookNode, uDirectory, uLedgerIndex,
                std::bind (
                    &Ledger::qualityDirDescriber, std::placeholders::_1,
                    std::placeholders::_2, saTakerPays.getCurrency (),
                    uPaysIssuerID, saTakerGets.getCurrency (),
                    uGetsIssuerID, uRate));
        }

        if (tesSUCCESS == terResult)
        {
            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));

            sleOffer->setFieldAccount (sfAccount, mTxnAccountID);
            sleOffer->setFieldU32 (sfSequence, uSequence);
            sleOffer->setFieldH256 (sfBookDirectory, uDirectory);
            sleOffer->setFieldAmount (sfTakerPays, saTakerPays);
            sleOffer->setFieldAmount (sfTakerGets, saTakerGets);
            sleOffer->setFieldU64 (sfOwnerNode, uOwnerNode);
            sleOffer->setFieldU64 (sfBookNode, uBookNode);

            if (uExpiration)
                sleOffer->setFieldU32 (sfExpiration, uExpiration);

            if (bPassive)
                sleOffer->setFlag (lsfPassive);

            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;
}
Example #9
0
// Append a node, then create and insert before it any implied nodes.  Order
// book nodes may go back to back.
//
// For each non-matching pair of IssuedCurrency, there's an order book.
//
// <-- resultCode: tesSUCCESS, temBAD_PATH, terNO_ACCOUNT, terNO_AUTH,
//                 terNO_LINE, tecPATH_DRY
TER PathState::pushNode (
    const int iType,
    AccountID const& account,    // If not specified, means an order book.
    Currency const& currency,  // If not specified, default to previous.
    AccountID const& issuer)     // If not specified, default to previous.
{
    path::Node node;
    const bool pathIsEmpty = nodes_.empty ();

    // TODO(tom): if pathIsEmpty, we probably don't need to do ANYTHING below.
    // Indeed, we might just not even call pushNode in the first place!

    auto const& backNode = pathIsEmpty ? path::Node () : nodes_.back ();

    // true, iff node is a ripple account. false, iff node is an offer node.
    const bool hasAccount = (iType & STPathElement::typeAccount);

    // Is currency specified for the output of the current node?
    const bool hasCurrency = (iType & STPathElement::typeCurrency);

    // Issuer is specified for the output of the current node.
    const bool hasIssuer = (iType & STPathElement::typeIssuer);

    TER resultCode = tesSUCCESS;

    JLOG (j_.trace)
         << "pushNode> " << iType << ": "
         << (hasAccount ? to_string(account) : std::string("-")) << " "
         << (hasCurrency ? to_string(currency) : std::string("-")) << "/"
         << (hasIssuer ? to_string(issuer) : std::string("-")) << "/";

    node.uFlags = iType;
    node.issue_.currency = hasCurrency ?
            currency : backNode.issue_.currency;

    // TODO(tom): we can probably just return immediately whenever we hit an
    // error in these next pages.

    if (iType & ~STPathElement::typeAll)
    {
        // Of course, this could never happen.
        JLOG (j_.debug) << "pushNode: bad bits.";
        resultCode = temBAD_PATH;
    }
    else if (hasIssuer && isXRP (node.issue_))
    {
        JLOG (j_.debug) << "pushNode: issuer specified for XRP.";

        resultCode = temBAD_PATH;
    }
    else if (hasIssuer && !issuer)
    {
        JLOG (j_.debug) << "pushNode: specified bad issuer.";

        resultCode = temBAD_PATH;
    }
    else if (!hasAccount && !hasCurrency && !hasIssuer)
    {
        // You can't default everything to the previous node as you would make
        // no progress.
        JLOG (j_.debug)
            << "pushNode: offer must specify at least currency or issuer.";
        resultCode = temBAD_PATH;
    }
    else if (hasAccount)
    {
        // Account link
        node.account_ = account;
        node.issue_.account = hasIssuer ? issuer :
                (isXRP (node.issue_) ? xrpAccount() : account);
        // Zero value - for accounts.
        node.saRevRedeem = STAmount ({node.issue_.currency, account});
        node.saRevIssue = node.saRevRedeem;

        // For order books only - zero currency with the issuer ID.
        node.saRevDeliver = STAmount (node.issue_);
        node.saFwdDeliver = node.saRevDeliver;

        if (pathIsEmpty)
        {
            // The first node is always correct as is.
        }
        else if (!account)
        {
            JLOG (j_.debug)
                << "pushNode: specified bad account.";
            resultCode = temBAD_PATH;
        }
        else
        {
            // Add required intermediate nodes to deliver to current account.
            JLOG (j_.trace)
                << "pushNode: imply for account.";

            resultCode = pushImpliedNodes (
                node.account_,
                node.issue_.currency,
                isXRP(node.issue_.currency) ? xrpAccount() : account);

            // Note: backNode may no longer be the immediately previous node.
        }

        if (resultCode == tesSUCCESS && !nodes_.empty ())
        {
            auto const& backNode = nodes_.back ();
            if (backNode.isAccount())
            {
                auto sleRippleState = view().peek(
                    keylet::line(backNode.account_, node.account_, backNode.issue_.currency));

                // A "RippleState" means a balance betweeen two accounts for a
                // specific currency.
                if (!sleRippleState)
                {
                    JLOG (j_.trace)
                            << "pushNode: No credit line between "
                            << backNode.account_ << " and " << node.account_
                            << " for " << node.issue_.currency << "." ;

                    JLOG (j_.trace) << getJson ();

                    resultCode   = terNO_LINE;
                }
                else
                {
                    JLOG (j_.trace)
                            << "pushNode: Credit line found between "
                            << backNode.account_ << " and " << node.account_
                            << " for " << node.issue_.currency << "." ;

                    auto sleBck  = view().peek (
                        keylet::account(backNode.account_));
                    // Is the source account the highest numbered account ID?
                    bool bHigh = backNode.account_ > node.account_;

                    if (!sleBck)
                    {
                        JLOG (j_.warning)
                            << "pushNode: delay: can't receive IOUs from "
                            << "non-existent issuer: " << backNode.account_;

                        resultCode   = terNO_ACCOUNT;
                    }
                    else if ((sleBck->getFieldU32 (sfFlags) & lsfRequireAuth) &&
                             !(sleRippleState->getFieldU32 (sfFlags) &
                                  (bHigh ? lsfHighAuth : lsfLowAuth)) &&
                             sleRippleState->getFieldAmount(sfBalance) == zero)
                    {
                        JLOG (j_.warning)
                                << "pushNode: delay: can't receive IOUs from "
                                << "issuer without auth.";

                        resultCode   = terNO_AUTH;
                    }

                    if (resultCode == tesSUCCESS)
                    {
                        STAmount saOwed = creditBalance (view(),
                            node.account_, backNode.account_,
                            node.issue_.currency);
                        STAmount saLimit;

                        if (saOwed <= zero)
                        {
                            saLimit = creditLimit (view(),
                                node.account_,
                                backNode.account_,
                                node.issue_.currency);
                            if (-saOwed >= saLimit)
                            {
                                JLOG (j_.debug) <<
                                    "pushNode: dry:" <<
                                    " saOwed=" << saOwed <<
                                    " saLimit=" << saLimit;

                                resultCode   = tecPATH_DRY;
                            }
                        }
                    }
                }
            }
        }

        if (resultCode == tesSUCCESS)
            nodes_.push_back (node);
    }
    else
    {
        // Offer link.
        //
        // Offers bridge a change in currency and issuer, or just a change in
        // issuer.
        if (hasIssuer)
            node.issue_.account = issuer;
        else if (isXRP (node.issue_.currency))
            node.issue_.account = xrpAccount();
        else if (isXRP (backNode.issue_.account))
            node.issue_.account = backNode.account_;
        else
            node.issue_.account = backNode.issue_.account;

        node.saRateMax = STAmount::saZero;
        node.saRevDeliver = STAmount (node.issue_);
        node.saFwdDeliver = node.saRevDeliver;

        if (!isConsistent (node.issue_))
        {
            JLOG (j_.debug)
                << "pushNode: currency is inconsistent with issuer.";

            resultCode = temBAD_PATH;
        }
        else if (backNode.issue_ == node.issue_)
        {
            JLOG (j_.debug) <<
                "pushNode: bad path: offer to same currency and issuer";
            resultCode = temBAD_PATH;
        }
        else {
            JLOG (j_.trace) << "pushNode: imply for offer.";

            // Insert intermediary issuer account if needed.
            resultCode   = pushImpliedNodes (
                xrpAccount(), // Rippling, but offers don't have an account.
                backNode.issue_.currency,
                backNode.issue_.account);
        }

        if (resultCode == tesSUCCESS)
            nodes_.push_back (node);
    }

    JLOG (j_.trace) << "pushNode< : " << transToken (resultCode);
    return resultCode;
}
// Take as much as possible. Adjusts account balances. Charges fees on top to taker.
// -->    uBookBase: The order book to take against.
// -->  saTakerPays: What the taker offers (w/ issuer)
// -->  saTakerGets: What the taker wanted (w/ issuer)
// <--  saTakerPaid: What taker could have paid including saved not including fees. To reduce an offer.
// <--   saTakerGot: What taker got not including fees. To reduce an offer.
// <--    terResult: tesSUCCESS, terNO_ACCOUNT, telFAILED_PROCESSING, or tecFAILED_PROCESSING
// <--    bUnfunded: if tesSUCCESS, consider offer unfunded after taking.
TER OfferCreateTransactor::takeOffers (
    const bool          bOpenLedger,
    const bool          bPassive,
    const bool          bSell,
    uint256 const&      uBookBase,
    const uint160&      uTakerAccountID,
    SLE::ref            sleTakerAccount,
    const STAmount&     saTakerPays,
    const STAmount&     saTakerGets,
    STAmount&           saTakerPaid,
    STAmount&           saTakerGot,
    bool&               bUnfunded)
{
    // The book has the most elements. Take the perspective of the book.
    // Book is ordered for taker: taker pays / taker gets (smaller is better)

    // The order is for the other books currencys for get and pays are opposites.
    // We want the same ratio for the respective currencies.
    // So we swap paid and gets for determing take quality.

    assert (saTakerPays && saTakerGets);

    WriteLog (lsDEBUG, OfferCreateTransactor) << "takeOffers: bSell: " << bSell << ": against book: " << uBookBase.ToString ();

    LedgerEntrySet&         lesActive           = mEngine->getNodes ();
    uint256                 uTipIndex           = uBookBase;
    const uint256           uBookEnd            = Ledger::getQualityNext (uBookBase);
    const uint64            uTakeQuality        = STAmount::getRate (saTakerGets, saTakerPays);
    STAmount                saTakerRate         = STAmount::setRate (uTakeQuality);
    const uint160           uTakerPaysAccountID = saTakerPays.getIssuer ();
    const uint160           uTakerGetsAccountID = saTakerGets.getIssuer ();
    TER                     terResult           = temUNCERTAIN;

    boost::unordered_set<uint256>   usOfferUnfundedBecame;  // Offers that became unfunded.
    boost::unordered_set<uint160>   usAccountTouched;       // Accounts touched.

    saTakerPaid     = STAmount (saTakerPays.getCurrency (), saTakerPays.getIssuer ());
    saTakerGot      = STAmount (saTakerGets.getCurrency (), saTakerGets.getIssuer ());
    bUnfunded       = false;

    while (temUNCERTAIN == terResult)
    {
        SLE::pointer    sleOfferDir;
        uint64          uTipQuality     = 0;
        STAmount        saTakerFunds    = lesActive.accountFunds (uTakerAccountID, saTakerPays);
        STAmount        saSubTakerPays  = saTakerPays - saTakerPaid; // How much more to spend.
        STAmount        saSubTakerGets  = saTakerGets - saTakerGot; // How much more is wanted.

        // Figure out next offer to take, if needed.
        if (saTakerFunds.isPositive ()          // Taker has funds available.
                && saSubTakerPays.isPositive ()
                && saSubTakerGets.isPositive ())
        {
            sleOfferDir     = mEngine->entryCache (ltDIR_NODE, lesActive.getNextLedgerIndex (uTipIndex, uBookEnd));

            if (sleOfferDir)
            {
                uTipIndex       = sleOfferDir->getIndex ();
                uTipQuality     = Ledger::getQuality (uTipIndex);

                WriteLog (lsDEBUG, OfferCreateTransactor) << boost::str (boost::format ("takeOffers: possible counter offer found: uTipQuality=%d uTipIndex=%s")
                        % uTipQuality
                        % uTipIndex.ToString ());

            }
            else
            {
                WriteLog (lsTRACE, OfferCreateTransactor) << "takeOffers: counter offer book is empty: "
                        << uTipIndex.ToString ()
                        << " ... "
                        << uBookEnd.ToString ();
            }
        }

        if (!saTakerFunds.isPositive ())                    // Taker has no funds.
        {
            // Done. Ran out of funds on previous round. As fees aren't calculated directly in this routine, funds are checked here.
            WriteLog (lsDEBUG, OfferCreateTransactor) << "takeOffers: done: taker unfunded.";

            bUnfunded   = true;                             // Don't create an order.
            terResult   = tesSUCCESS;
        }
        else if (!sleOfferDir                               // No offer directory to take.
                 || uTakeQuality < uTipQuality                   // No offers of sufficient quality available.
                 || (bPassive && uTakeQuality == uTipQuality))
        {
            // Done.
            STAmount    saTipRate           = sleOfferDir ? STAmount::setRate (uTipQuality) : saTakerRate;

            WriteLog (lsDEBUG, OfferCreateTransactor) << boost::str (boost::format ("takeOffers: done: dir=%d uTakeQuality=%d %c uTipQuality=%d saTakerRate=%s %c saTipRate=%s bPassive=%d")
                    % !!sleOfferDir
                    % uTakeQuality
                    % (uTakeQuality == uTipQuality
                       ? '='
                       : uTakeQuality < uTipQuality
                       ? '<'
                       : '>')
                    % uTipQuality
                    % saTakerRate
                    % (saTakerRate == saTipRate
                       ? '='
                       : saTakerRate < saTipRate
                       ? '<'
                       : '>')
                    % saTipRate
                    % bPassive);

            terResult   = tesSUCCESS;
        }
        else
        {
            // Have an offer directory to consider.
            WriteLog (lsTRACE, OfferCreateTransactor) << "takeOffers: considering dir: " << sleOfferDir->getJson (0);

            SLE::pointer    sleBookNode;
            unsigned int    uBookEntry;
            uint256         uOfferIndex;

            lesActive.dirFirst (uTipIndex, sleBookNode, uBookEntry, uOfferIndex);

            SLE::pointer    sleOffer        = mEngine->entryCache (ltOFFER, uOfferIndex);

            WriteLog (lsDEBUG, OfferCreateTransactor) << "takeOffers: considering offer : " << sleOffer->getJson (0);

            const uint160   uOfferOwnerID   = sleOffer->getFieldAccount160 (sfAccount);
            STAmount        saOfferPays     = sleOffer->getFieldAmount (sfTakerGets);
            STAmount        saOfferGets     = sleOffer->getFieldAmount (sfTakerPays);

            STAmount        saOfferFunds;   // Funds of offer owner to payout.
            bool            bValid;

            bValid  =  bValidOffer (
                           sleOfferDir, uOfferIndex, uOfferOwnerID, saOfferPays, saOfferGets,
                           uTakerAccountID,
                           usOfferUnfundedFound, usOfferUnfundedBecame, usAccountTouched,
                           saOfferFunds);

            if (bValid)
            {
                STAmount    saSubTakerPaid;
                STAmount    saSubTakerGot;
                STAmount    saTakerIssuerFee;
                STAmount    saOfferIssuerFee;
                STAmount    saOfferRate = STAmount::setRate (uTipQuality);

                WriteLog (lsDEBUG, OfferCreateTransactor) << "takeOffers: applyOffer:    saTakerPays: " << saTakerPays.getFullText ();
                WriteLog (lsDEBUG, OfferCreateTransactor) << "takeOffers: applyOffer:    saTakerPaid: " << saTakerPaid.getFullText ();
                WriteLog (lsDEBUG, OfferCreateTransactor) << "takeOffers: applyOffer:   saTakerFunds: " << saTakerFunds.getFullText ();
                WriteLog (lsDEBUG, OfferCreateTransactor) << "takeOffers: applyOffer:   saOfferFunds: " << saOfferFunds.getFullText ();
                WriteLog (lsDEBUG, OfferCreateTransactor) << "takeOffers: applyOffer:    saOfferPays: " << saOfferPays.getFullText ();
                WriteLog (lsDEBUG, OfferCreateTransactor) << "takeOffers: applyOffer:    saOfferGets: " << saOfferGets.getFullText ();
                WriteLog (lsDEBUG, OfferCreateTransactor) << "takeOffers: applyOffer:    saOfferRate: " << saOfferRate.getFullText ();
                WriteLog (lsDEBUG, OfferCreateTransactor) << "takeOffers: applyOffer: saSubTakerPays: " << saSubTakerPays.getFullText ();
                WriteLog (lsDEBUG, OfferCreateTransactor) << "takeOffers: applyOffer: saSubTakerGets: " << saSubTakerGets.getFullText ();
                WriteLog (lsDEBUG, OfferCreateTransactor) << "takeOffers: applyOffer:    saTakerPays: " << saTakerPays.getFullText ();
                WriteLog (lsDEBUG, OfferCreateTransactor) << "takeOffers: applyOffer:    saTakerGets: " << saTakerGets.getFullText ();

                bool    bOfferDelete    = STAmount::applyOffer (
                                              bSell,
                                              lesActive.rippleTransferRate (uTakerAccountID, uOfferOwnerID, uTakerPaysAccountID),
                                              lesActive.rippleTransferRate (uOfferOwnerID, uTakerAccountID, uTakerGetsAccountID),
                                              saOfferRate,
                                              saOfferFunds,
                                              saTakerFunds,
                                              saOfferPays,
                                              saOfferGets,
                                              saSubTakerPays,
                                              saSubTakerGets,
                                              saSubTakerPaid,
                                              saSubTakerGot,
                                              saTakerIssuerFee,
                                              saOfferIssuerFee);

                WriteLog (lsDEBUG, OfferCreateTransactor) << "takeOffers: applyOffer: saSubTakerPaid: " << saSubTakerPaid.getFullText ();
                WriteLog (lsDEBUG, OfferCreateTransactor) << "takeOffers: applyOffer:  saSubTakerGot: " << saSubTakerGot.getFullText ();

                // Adjust offer

                // Offer owner will pay less.  Subtract what taker just got.
                sleOffer->setFieldAmount (sfTakerGets, saOfferPays -= saSubTakerGot);

                // Offer owner will get less.  Subtract what owner just paid.
                sleOffer->setFieldAmount (sfTakerPays, saOfferGets -= saSubTakerPaid);

                mEngine->entryModify (sleOffer);

                if (bOfferDelete)
                {
                    // Offer now fully claimed or now unfunded.
                    WriteLog (lsDEBUG, OfferCreateTransactor) << "takeOffers: Offer claimed: Delete.";

                    usOfferUnfundedBecame.insert (uOfferIndex); // Delete unfunded offer on success.

                    // Offer owner's account is no longer pristine.
                    usAccountTouched.insert (uOfferOwnerID);
                }
                else if (saSubTakerGot)
                {
                    WriteLog (lsDEBUG, OfferCreateTransactor) << "takeOffers: Offer partial claim.";

                    if (!saOfferPays.isPositive () || !saOfferGets.isPositive ())
                    {
                        WriteLog (lsWARNING, OfferCreateTransactor) << "takeOffers: ILLEGAL OFFER RESULT.";
                        bUnfunded   = true;
                        terResult   = bOpenLedger ? telFAILED_PROCESSING : tecFAILED_PROCESSING;
                    }
                }
                else
                {
                    // Taker got nothing, probably due to rounding. Consider taker unfunded.
                    WriteLog (lsDEBUG, OfferCreateTransactor) << "takeOffers: No claim.";

                    bUnfunded   = true;
                    terResult   = tesSUCCESS;                   // Done.
                }

                assert (uTakerGetsAccountID == saSubTakerGot.getIssuer ());
                assert (uTakerPaysAccountID == saSubTakerPaid.getIssuer ());

                if (!bUnfunded)
                {
                    // Distribute funds. The sends charge appropriate fees which are implied by offer.

                    terResult   = lesActive.accountSend (uOfferOwnerID, uTakerAccountID, saSubTakerGot);            // Offer owner pays taker.

                    if (tesSUCCESS == terResult)
                        terResult   = lesActive.accountSend (uTakerAccountID, uOfferOwnerID, saSubTakerPaid);           // Taker pays offer owner.

                    if (!bSell)
                    {
                        // Buy semantics: Reduce amount considered paid by taker's rate. Not by actual cost which is lower.
                        // That is, take less as to just satify our buy requirement.
                        STAmount    saTakerCould    = saTakerPays - saTakerPaid;    // Taker could pay.

                        if (saTakerFunds < saTakerCould)
                            saTakerCould    = saTakerFunds;

                        STAmount    saTakerUsed = STAmount::multiply (saSubTakerGot, saTakerRate, saTakerPays);

                        WriteLog (lsDEBUG, OfferCreateTransactor) << "takeOffers: applyOffer:   saTakerCould: " << saTakerCould.getFullText ();
                        WriteLog (lsDEBUG, OfferCreateTransactor) << "takeOffers: applyOffer:  saSubTakerGot: " << saSubTakerGot.getFullText ();
                        WriteLog (lsDEBUG, OfferCreateTransactor) << "takeOffers: applyOffer:    saTakerRate: " << saTakerRate.getFullText ();
                        WriteLog (lsDEBUG, OfferCreateTransactor) << "takeOffers: applyOffer:    saTakerUsed: " << saTakerUsed.getFullText ();

                        saSubTakerPaid  = std::min (saTakerCould, saTakerUsed);
                    }

                    saTakerPaid     += saSubTakerPaid;
                    saTakerGot      += saSubTakerGot;

                    if (tesSUCCESS == terResult)
                        terResult   = temUNCERTAIN;
                }
            }
        }
    }

    WriteLog (lsDEBUG, OfferCreateTransactor) << "takeOffers: " << transToken (terResult);

    if (tesSUCCESS == terResult)
    {
        // On success, delete offers that became unfunded.
        BOOST_FOREACH (uint256 const & uOfferIndex, usOfferUnfundedBecame)
        {
            WriteLog (lsDEBUG, OfferCreateTransactor) << "takeOffers: became unfunded: " << uOfferIndex.ToString ();

            terResult   = lesActive.offerDelete (uOfferIndex);

            if (tesSUCCESS != terResult)
                break;
        }
Example #11
0
// Take as much as possible. Adjusts account balances. Charges fees on top to taker.
// -->    uBookBase: The order book to take against.
// -->  saTakerPays: What the taker offers (w/ issuer)
// -->  saTakerGets: What the taker wanted (w/ issuer)
// <--  saTakerPaid: What taker could have paid including saved not including fees. To reduce an offer.
// <--   saTakerGot: What taker got not including fees. To reduce an offer.
// <--    terResult: tesSUCCESS, terNO_ACCOUNT, telFAILED_PROCESSING, or tecFAILED_PROCESSING
// <--    bUnfunded: if tesSUCCESS, consider offer unfunded after taking.
TER OfferCreateTransactor::takeOffers (
    const bool          bOpenLedger,
    const bool          bPassive,
    const bool          bSell,
    uint256 const&      uBookBase,
    const uint160&      uTakerAccountID,
    SLE::ref            sleTakerAccount,
    const STAmount&     saTakerPays,
    const STAmount&     saTakerGets,
    STAmount&           saTakerPaid,
    STAmount&           saTakerGot,
    bool&               bUnfunded)
{
    // The book has the most elements. Take the perspective of the book.
    // Book is ordered for taker: taker pays / taker gets (smaller is better)

    // The order is for the other books currencys for get and pays are opposites.
    // We want the same ratio for the respective currencies.
    // So we swap paid and gets for determing take quality.

    assert (saTakerPays && saTakerGets);

    WriteLog (lsDEBUG, OfferCreateTransactor) << "takeOffers: bSell: " << bSell << ": against book: " << uBookBase.ToString ();

    LedgerEntrySet&         lesActive           = mEngine->getNodes ();
    const uint64            uTakeQuality        = STAmount::getRate (saTakerGets, saTakerPays);
    STAmount                saTakerRate         = STAmount::setRate (uTakeQuality);
    const uint160           uTakerPaysAccountID = saTakerPays.getIssuer ();
    const uint160           uTakerGetsAccountID = saTakerGets.getIssuer ();
    TER                     terResult           = temUNCERTAIN;

    boost::unordered_set<uint256>   usOfferUnfundedBecame;  // Offers that became unfunded.
    boost::unordered_set<uint160>   usAccountTouched;       // Accounts touched.

    saTakerPaid     = STAmount (saTakerPays.getCurrency (), saTakerPays.getIssuer ());
    saTakerGot      = STAmount (saTakerGets.getCurrency (), saTakerGets.getIssuer ());
    bUnfunded       = false;

    OrderBookIterator bookIterator (lesActive,
        saTakerPays.getCurrency(), saTakerPays.getIssuer(),
        saTakerGets.getCurrency(), saTakerGets.getIssuer());

    while ((temUNCERTAIN == terResult) && bookIterator.nextOffer())
    {
        STAmount        saTakerFunds    = lesActive.accountFunds (uTakerAccountID, saTakerPays);
        STAmount        saSubTakerPays  = saTakerPays - saTakerPaid; // How much more to spend.
        STAmount        saSubTakerGets  = saTakerGets - saTakerGot; // How much more is wanted.
        uint64          uTipQuality     = bookIterator.getCurrentQuality();

        if (!saTakerFunds.isPositive ())
        {
            // Taker is out of funds. Don't create the offer.
            bUnfunded = true;
            terResult = tesSUCCESS;
        }
        else if (!saSubTakerPays.isPositive() || !saSubTakerGets.isPositive())
        {
            // Offer is completely consumed
            terResult = tesSUCCESS;
        }
        else if ((uTakeQuality < uTipQuality)
                 || (bPassive && uTakeQuality == uTipQuality))
        {
            // Offer does not cross this offer
            STAmount    saTipRate           = STAmount::setRate (uTipQuality);

            WriteLog (lsDEBUG, OfferCreateTransactor) << boost::str (boost::format ("takeOffers: done: uTakeQuality=%d %c uTipQuality=%d saTakerRate=%s %c saTipRate=%s bPassive=%d")
                    % uTakeQuality
                    % (uTakeQuality == uTipQuality
                       ? '='
                       : uTakeQuality < uTipQuality
                       ? '<'
                       : '>')
                    % uTipQuality
                    % saTakerRate
                    % (saTakerRate == saTipRate
                       ? '='
                       : saTakerRate < saTipRate
                       ? '<'
                       : '>')
                    % saTipRate
                    % bPassive);

            terResult   = tesSUCCESS;
        }
        else
        {
            // We have a crossing offer to consider.

            SLE::pointer    sleOffer        = bookIterator.getCurrentOffer ();

            if (!sleOffer)
            { // offer is in directory but not in ledger
                uint256 offerIndex = bookIterator.getCurrentIndex ();
                WriteLog (lsWARNING, OfferCreateTransactor) << "takeOffers: offer not found : " << offerIndex;
                usMissingOffers.insert (missingOffer_t (
                    bookIterator.getCurrentIndex (), bookIterator.getCurrentDirectory ()));
            }
            else
            {
                WriteLog (lsDEBUG, OfferCreateTransactor) << "takeOffers: considering offer : " << sleOffer->getJson (0);

                const uint160&  uOfferOwnerID   = sleOffer->getFieldAccount160 (sfAccount);
                STAmount        saOfferPays     = sleOffer->getFieldAmount (sfTakerGets);
                STAmount        saOfferGets     = sleOffer->getFieldAmount (sfTakerPays);

                STAmount        saOfferFunds;   // Funds of offer owner to payout.
                bool            bValid;

                bValid  =  bValidOffer (
                               sleOffer, uOfferOwnerID, saOfferPays, saOfferGets,
                               uTakerAccountID,
                               usOfferUnfundedFound, usOfferUnfundedBecame, usAccountTouched,
                               saOfferFunds);

                if (bValid)
                {
                    STAmount    saSubTakerPaid;
                    STAmount    saSubTakerGot;
                    STAmount    saTakerIssuerFee;
                    STAmount    saOfferIssuerFee;
                    STAmount    saOfferRate = STAmount::setRate (uTipQuality);

                    WriteLog (lsTRACE, OfferCreateTransactor) << "takeOffers: applyOffer:    saTakerPays: " << saTakerPays.getFullText ();
                    WriteLog (lsTRACE, OfferCreateTransactor) << "takeOffers: applyOffer:    saTakerPaid: " << saTakerPaid.getFullText ();
                    WriteLog (lsTRACE, OfferCreateTransactor) << "takeOffers: applyOffer:   saTakerFunds: " << saTakerFunds.getFullText ();
                    WriteLog (lsTRACE, OfferCreateTransactor) << "takeOffers: applyOffer:   saOfferFunds: " << saOfferFunds.getFullText ();
                    WriteLog (lsTRACE, OfferCreateTransactor) << "takeOffers: applyOffer:    saOfferPays: " << saOfferPays.getFullText ();
                    WriteLog (lsTRACE, OfferCreateTransactor) << "takeOffers: applyOffer:    saOfferGets: " << saOfferGets.getFullText ();
                    WriteLog (lsTRACE, OfferCreateTransactor) << "takeOffers: applyOffer:    saOfferRate: " << saOfferRate.getFullText ();
                    WriteLog (lsTRACE, OfferCreateTransactor) << "takeOffers: applyOffer: saSubTakerPays: " << saSubTakerPays.getFullText ();
                    WriteLog (lsTRACE, OfferCreateTransactor) << "takeOffers: applyOffer: saSubTakerGets: " << saSubTakerGets.getFullText ();
                    WriteLog (lsTRACE, OfferCreateTransactor) << "takeOffers: applyOffer:    saTakerPays: " << saTakerPays.getFullText ();
                    WriteLog (lsTRACE, OfferCreateTransactor) << "takeOffers: applyOffer:    saTakerGets: " << saTakerGets.getFullText ();

                    bool    bOfferDelete    = STAmount::applyOffer (
                                                  bSell,
                                                  lesActive.rippleTransferRate (uTakerAccountID, uOfferOwnerID, uTakerPaysAccountID),
                                                  lesActive.rippleTransferRate (uOfferOwnerID, uTakerAccountID, uTakerGetsAccountID),
                                                  saOfferRate,
                                                  saOfferFunds,
                                                  saTakerFunds,
                                                  saOfferPays,
                                                  saOfferGets,
                                                  saSubTakerPays,
                                                  saSubTakerGets,
                                                  saSubTakerPaid,
                                                  saSubTakerGot,
                                                  saTakerIssuerFee,
                                                  saOfferIssuerFee);

                    WriteLog (lsDEBUG, OfferCreateTransactor) << "takeOffers: applyOffer: saSubTakerPaid: " << saSubTakerPaid.getFullText ();
                    WriteLog (lsDEBUG, OfferCreateTransactor) << "takeOffers: applyOffer:  saSubTakerGot: " << saSubTakerGot.getFullText ();

                    // Adjust offer

                    // Offer owner will pay less.  Subtract what taker just got.
                    sleOffer->setFieldAmount (sfTakerGets, saOfferPays -= saSubTakerGot);

                    // Offer owner will get less.  Subtract what owner just paid.
                    sleOffer->setFieldAmount (sfTakerPays, saOfferGets -= saSubTakerPaid);

                    mEngine->entryModify (sleOffer);

                    if (bOfferDelete)
                    {
                        // Offer now fully claimed or now unfunded.
                        WriteLog (lsDEBUG, OfferCreateTransactor) << "takeOffers: Offer claimed: Delete.";

                        usOfferUnfundedBecame.insert (sleOffer->getIndex()); // Delete unfunded offer on success.

                        // Offer owner's account is no longer pristine.
                        usAccountTouched.insert (uOfferOwnerID);
                    }
                    else if (saSubTakerGot)
                    {
                        WriteLog (lsDEBUG, OfferCreateTransactor) << "takeOffers: Offer partial claim.";

                        if (!saOfferPays.isPositive () || !saOfferGets.isPositive ())
                        {
                            WriteLog (lsWARNING, OfferCreateTransactor) << "takeOffers: ILLEGAL OFFER RESULT.";
                            bUnfunded   = true;
                            terResult   = bOpenLedger ? telFAILED_PROCESSING : tecFAILED_PROCESSING;
                        }
                    }
                    else
                    {
                        // Taker got nothing, probably due to rounding. Consider taker unfunded.
                        WriteLog (lsDEBUG, OfferCreateTransactor) << "takeOffers: No claim.";

                        bUnfunded   = true;
                        terResult   = tesSUCCESS;                   // Done.
                    }

                    assert (uTakerGetsAccountID == saSubTakerGot.getIssuer ());
                    assert (uTakerPaysAccountID == saSubTakerPaid.getIssuer ());

                    if (!bUnfunded)
                    {
                        // Distribute funds. The sends charge appropriate fees which are implied by offer.

                        terResult   = lesActive.accountSend (uOfferOwnerID, uTakerAccountID, saSubTakerGot);            // Offer owner pays taker.

                        if (tesSUCCESS == terResult)
                            terResult   = lesActive.accountSend (uTakerAccountID, uOfferOwnerID, saSubTakerPaid);           // Taker pays offer owner.

                        if (bSell)
                        {
                            // Sell semantics:
                            // Reduce amount considered received to original offer's rate.
                            // Not by crossing rate, which is higher.
                            STAmount saEffectiveGot = STAmount::divide(saSubTakerPaid, saTakerRate, saTakerGets);
                            saSubTakerGot = std::min(saEffectiveGot, saSubTakerGot);
                        }
                        else
                        {
                            // Buy semantics: Reduce amount considered paid by taker's rate. Not by actual cost which is lower.
                            // That is, take less as to just satify our buy requirement.
                            STAmount    saTakerCould    = saTakerPays - saTakerPaid;    // Taker could pay.

                            if (saTakerFunds < saTakerCould)
                                saTakerCould    = saTakerFunds;

                            STAmount    saTakerUsed = STAmount::multiply (saSubTakerGot, saTakerRate, saTakerPays);

                            WriteLog (lsDEBUG, OfferCreateTransactor) << "takeOffers: applyOffer:   saTakerCould: " << saTakerCould.getFullText ();
                            WriteLog (lsDEBUG, OfferCreateTransactor) << "takeOffers: applyOffer:  saSubTakerGot: " << saSubTakerGot.getFullText ();
                            WriteLog (lsDEBUG, OfferCreateTransactor) << "takeOffers: applyOffer:    saTakerRate: " << saTakerRate.getFullText ();
                            WriteLog (lsDEBUG, OfferCreateTransactor) << "takeOffers: applyOffer:    saTakerUsed: " << saTakerUsed.getFullText ();

                            saSubTakerPaid  = std::min (saTakerCould, saTakerUsed);
                        }

                        saTakerPaid     += saSubTakerPaid;
                        saTakerGot      += saSubTakerGot;

                        if (tesSUCCESS == terResult)
                            terResult   = temUNCERTAIN;
                    }
                }
            }
        }
    }

    if (temUNCERTAIN == terResult)
        terResult = tesSUCCESS;

    WriteLog (lsDEBUG, OfferCreateTransactor) << "takeOffers: " << transToken (terResult);

    if (tesSUCCESS == terResult)
    {
        // On success, delete offers that became unfunded.
        BOOST_FOREACH (uint256 const & uOfferIndex, usOfferUnfundedBecame)
        {
            WriteLog (lsDEBUG, OfferCreateTransactor) << "takeOffers: became unfunded: " << uOfferIndex.ToString ();

            terResult   = lesActive.offerDelete (uOfferIndex);

            if (tesSUCCESS != terResult)
                break;
        }
Example #12
0
TER TransactionEngine::applyTransaction (
    STTx const& txn,
    TransactionEngineParams params,
    bool& didApply)
{
    WriteLog (lsTRACE, TransactionEngine) << "applyTransaction>";
    didApply = false;
    assert (mLedger);
    uint256 const& txID = txn.getTransactionID ();
    mNodes.init (mLedger, txID, mLedger->getLedgerSeq (), params);

#ifdef BEAST_DEBUG
    if (1)
    {
        Serializer ser;
        txn.add (ser);
        SerializerIterator sit (ser);
        STTx s2 (sit);

        if (!s2.isEquivalent (txn))
        {
            WriteLog (lsFATAL, TransactionEngine) <<
                "Transaction serdes mismatch";
            WriteLog (lsINFO, TransactionEngine) << txn.getJson (0);
            WriteLog (lsFATAL, TransactionEngine) << s2.getJson (0);
            assert (false);
        }
    }
#endif

    if (!txID)
    {
        WriteLog (lsWARNING, TransactionEngine) <<
            "applyTransaction: invalid transaction id";
        return temINVALID;
    }

    TER terResult = Transactor::transact (txn, params, this);

    if (terResult == temUNKNOWN)
    {
        WriteLog (lsWARNING, TransactionEngine) <<
            "applyTransaction: Invalid transaction: unknown transaction type";
        return temUNKNOWN;
    }

    if (ShouldLog (lsDEBUG, TransactionEngine))
    {
        std::string strToken;
        std::string strHuman;

        transResultInfo (terResult, strToken, strHuman);

        WriteLog (lsDEBUG, TransactionEngine) <<
            "applyTransaction: terResult=" << strToken <<
            " : " << terResult <<
            " : " << strHuman;
    }

    if (isTesSuccess (terResult))
        didApply = true;
    else if (isTecClaim (terResult) && !(params & tapRETRY))
    {
        // only claim the transaction fee
        WriteLog (lsDEBUG, TransactionEngine) << "Reprocessing to only claim fee";
        mNodes.clear ();

        SLE::pointer txnAcct = entryCache (ltACCOUNT_ROOT,
            getAccountRootIndex (txn.getSourceAccount ()));

        if (!txnAcct)
            terResult = terNO_ACCOUNT;
        else
        {
            std::uint32_t t_seq = txn.getSequence ();
            std::uint32_t a_seq = txnAcct->getFieldU32 (sfSequence);

            if (a_seq < t_seq)
                terResult = terPRE_SEQ;
            else if (a_seq > t_seq)
                terResult = tefPAST_SEQ;
            else
            {
                STAmount fee        = txn.getTransactionFee ();
                STAmount balance    = txnAcct->getFieldAmount (sfBalance);
                STAmount balanceVBC = txnAcct->getFieldAmount(sfBalanceVBC);

                // We retry/reject the transaction if the account
                // balance is zero or we're applying against an open
                // ledger and the balance is less than the fee
                if ((balance == zero) || (balanceVBC.getNValue() == 0) ||
                    ((params & tapOPEN_LEDGER) && (balance < fee)))
                {
                    // Account has no funds or ledger is open
                    terResult = terINSUF_FEE_B;
                }
                else
                {
                    if (fee > balance)
                        fee = balance;
                    txnAcct->setFieldAmount (sfBalance, balance - fee);
                    txnAcct->setFieldAmount(sfBalanceVBC, balanceVBC);
                    txnAcct->setFieldU32 (sfSequence, t_seq + 1);
                    entryModify (txnAcct);
                    didApply = true;
                }
            }
        }
    }
    else
        WriteLog (lsDEBUG, TransactionEngine) << "Not applying transaction " << txID;

    if (didApply)
    {
        if (!checkInvariants (terResult, txn, params))
        {
            WriteLog (lsFATAL, TransactionEngine) <<
                "Transaction violates invariants";
            WriteLog (lsFATAL, TransactionEngine) <<
                txn.getJson (0);
            WriteLog (lsFATAL, TransactionEngine) <<
                transToken (terResult) << ": " << transHuman (terResult);
            WriteLog (lsFATAL, TransactionEngine) <<
                mNodes.getJson (0);
            didApply = false;
            terResult = tefINTERNAL;
        }
        else
        {
            // Transaction succeeded fully or (retries are not allowed and the
            // transaction could claim a fee)
            Serializer m;
            mNodes.calcRawMeta (m, terResult, mTxnSeq++);

            txnWrite ();

            Serializer s;
            txn.add (s);

            if (params & tapOPEN_LEDGER)
            {
                if (!mLedger->addTransaction (txID, s))
                {
                    WriteLog (lsFATAL, TransactionEngine) <<
                        "Tried to add transaction to open ledger that already had it";
                    assert (false);
                    throw std::runtime_error ("Duplicate transaction applied");
                }
            }
            else
            {
                if (!mLedger->addTransaction (txID, s, m))
                {
                    WriteLog (lsFATAL, TransactionEngine) <<
                        "Tried to add transaction to ledger that already had it";
                    assert (false);
                    throw std::runtime_error ("Duplicate transaction applied to closed ledger");
                }

                // Charge whatever fee they specified.
                STAmount saPaid = txn.getTransactionFee ();
                mLedger->destroyCoins (saPaid.getNValue ());
            }
        }
    }

    mTxnAccount.reset ();
    mNodes.clear ();

    if (!(params & tapOPEN_LEDGER) && isTemMalformed (terResult))
    {
        // XXX Malformed or failed transaction in closed ledger must bow out.
    }

    return terResult;
}
Example #13
0
//------------------------------------------------------------------------------
std::pair<TER, bool>
Transactor::operator()()
{
    JLOG(j_.trace()) << "apply: " << ctx_.tx.getTransactionID ();

#ifdef BEAST_DEBUG
    {
        Serializer ser;
        ctx_.tx.add (ser);
        SerialIter sit(ser.slice());
        STTx s2 (sit);

        if (! s2.isEquivalent(ctx_.tx))
        {
            JLOG(j_.fatal()) <<
                "Transaction serdes mismatch";
            JLOG(j_.info()) << to_string(ctx_.tx.getJson (0));
            JLOG(j_.fatal()) << s2.getJson (0);
            assert (false);
        }
    }
#endif

    auto result = ctx_.preclaimResult;
    if (result == tesSUCCESS)
        result = apply();

    // No transaction can return temUNKNOWN from apply,
    // and it can't be passed in from a preclaim.
    assert(result != temUNKNOWN);

    if (auto stream = j_.trace())
        stream << "preclaim result: " << transToken(result);

    bool applied = isTesSuccess (result);
    auto fee = ctx_.tx.getFieldAmount(sfFee).xrp ();

    if (ctx_.size() > oversizeMetaDataCap)
        result = tecOVERSIZE;

    if ((result == tecOVERSIZE) ||
        (isTecClaimHardFail (result, view().flags())))
    {
        JLOG(j_.trace()) << "reapplying because of " << transToken(result);

        std::vector<uint256> removedOffers;

        if (result == tecOVERSIZE)
        {
            ctx_.visit (
                [&removedOffers](
                    uint256 const& index,
                    bool isDelete,
                    std::shared_ptr <SLE const> const& before,
                    std::shared_ptr <SLE const> const& after)
                {
                    if (isDelete)
                    {
                        assert (before && after);
                        if (before && after &&
                            (before->getType() == ltOFFER) &&
                            (before->getFieldAmount(sfTakerPays) == after->getFieldAmount(sfTakerPays)))
                        {
                            // Removal of offer found or made unfunded
                            removedOffers.push_back (index);
                        }
                    }
                });
        }

        // Reset the context, potentially adjusting the fee
        fee = reset(fee);

        // If necessary, remove any offers found unfunded during processing
        if (result == tecOVERSIZE)
            removeUnfundedOffers (view(), removedOffers, ctx_.app.journal ("View"));

        applied = true;
    }

    if (applied)
    {
        // Check invariants: if `tecINVARIANT_FAILED` is not returned, we can
        // proceed to apply the tx
        result = ctx_.checkInvariants(result, fee);

        if (result == tecINVARIANT_FAILED)
        {
            // if invariants checking failed again, reset the context and
            // attempt to only claim a fee.
            fee = reset(fee);

            // Check invariants again to ensure the fee claiming doesn't
            // violate invariants.
            result = ctx_.checkInvariants(result, fee);
        }

        // We ran through the invariant checker, which can, in some cases,
        // return a tef error code. Don't apply the transaction in that case.
        if (!isTecClaim(result) && !isTesSuccess(result))
            applied = false;
    }

    if (applied)
    {
        // Transaction succeeded fully or (retries are not allowed and the
        // transaction could claim a fee)

        // The transactor and invariant checkers guarantee that this will
        // *never* trigger but if it, somehow, happens, don't allow a tx
        // that charges a negative fee.
        if (fee < beast::zero)
            Throw<std::logic_error> ("fee charged is negative!");

        // Charge whatever fee they specified. The fee has already been
        // deducted from the balance of the account that issued the
        // transaction. We just need to account for it in the ledger
        // header.
        if (!view().open() && fee != beast::zero)
            ctx_.destroyXRP (fee);

        // Once we call apply, we will no longer be able to look at view()
        ctx_.apply(result);
    }

    JLOG(j_.trace()) << (applied ? "applied" : "not applied") << transToken(result);

    return { result, applied };
}
Example #14
0
// OPTIMIZE: When calculating path increment, note if increment consumes all
// liquidity. No need to revisit path in the future if all liquidity is used.
//
// nodeAdvance advances through offers in an order book.
// If needed, advance to next funded offer.
// - Automatically advances to first offer.
// --> bEntryAdvance: true, to advance to next entry. false, recalculate.
// <-- uOfferIndex : 0=end of list.
TER nodeAdvance (
    RippleCalc& rippleCalc,
    const unsigned int          nodeIndex,
    PathState&                  pathState,
    const bool                  bMultiQuality,
    const bool                  bReverse)
{
    auto& previousNode = pathState.nodes()[nodeIndex - 1];
    auto& node = pathState.nodes()[nodeIndex];
    TER             resultCode       = tesSUCCESS;

    // Taker is the active party against an offer in the ledger - the entity
    // that is taking advantage of an offer in the order book.
    WriteLog (lsTRACE, RippleCalc)
            << "nodeAdvance: TakerPays:"
            << node.saTakerPays << " TakerGets:" << node.saTakerGets;

    int loopCount = 0;

    do
    {
        // VFALCO NOTE Why not use a for() loop?
        // VFALCO TODO The limit on loop iterations puts an
        //             upper limit on the number of different quality
        // levels (ratio of pay:get) that will be considered for one path.
        // Changing this value has repercusssions on validation and consensus.
        //
        if (++loopCount > NODE_ADVANCE_MAX_LOOPS)
        {
            WriteLog (lsWARNING, RippleCalc) << "Loop count exceeded";
            return tefEXCEPTION;
        }

        bool bDirectDirDirty = false;

        if (!node.currentDirectory_)
        {
            // Need to initialize current node.

            node.currentDirectory_.copyFrom(Ledger::getBookBase ({
                {previousNode.currency_, previousNode.issuer_},
                {node.currency_, node.issuer_}
            }));
            node.nextDirectory_.copyFrom(
                Ledger::getQualityNext (node.currentDirectory_));

            // TODO(tom): it seems impossible that any actual offers with
            // quality == 0 could occur - we should disallow them, and clear
            // sleDirectDir without the database call in the next line.
            node.sleDirectDir = rippleCalc.mActiveLedger.entryCache (
                                    ltDIR_NODE, node.currentDirectory_);

            // Associated vars are dirty, if found it.
            bDirectDirDirty = !!node.sleDirectDir;

            // Advance, if didn't find it. Normal not to be unable to lookup
            // firstdirectory. Maybe even skip this lookup.
            node.bDirectAdvance  = !node.sleDirectDir;
            node.bDirectRestart  = false;

            WriteLog (lsTRACE, RippleCalc)
                    << "nodeAdvance: Initialize node:"
                    << " node.currentDirectory_=" << node.currentDirectory_
                    <<" node.nextDirectory_=" << node.nextDirectory_
                    << " node.bDirectAdvance=" << node.bDirectAdvance;
        }

        if (node.bDirectAdvance || node.bDirectRestart)
        {
            // Get next quality.
            if (node.bDirectAdvance)
            {
                // This works because the Merkel radix tree is ordered by key so
                // we can go to the next one in O(1).
                node.currentDirectory_  = rippleCalc.mActiveLedger.getNextLedgerIndex (
                                              node.currentDirectory_, node.nextDirectory_);
            }

            bDirectDirDirty = true;
            node.bDirectAdvance  = false;
            node.bDirectRestart  = false;

            if (node.currentDirectory_ != zero)
            {
                // We didn't run off the end of this order book and found
                // another quality directory.
                WriteLog (lsTRACE, RippleCalc)
                        << "nodeAdvance: Quality advance: node.currentDirectory_="
                        << node.currentDirectory_;

                node.sleDirectDir = rippleCalc.mActiveLedger.entryCache (ltDIR_NODE, node.currentDirectory_);
            }
            else if (bReverse)
            {
                WriteLog (lsTRACE, RippleCalc)
                        << "nodeAdvance: No more offers.";

                node.offerIndex_ = 0;
                break;
            }
            else
            {
                // No more offers. Should be done rather than fall off end of
                // book.
                WriteLog (lsWARNING, RippleCalc)
                        << "nodeAdvance: Unreachable: "
                        << "Fell off end of order book.";
                // FIXME: why?
                return rippleCalc.mOpenLedger ? telFAILED_PROCESSING :
                       tecFAILED_PROCESSING;
            }
        }

        if (bDirectDirDirty)
        {
            // Our quality changed since last iteration.
            // Use the rate from the directory.
            node.saOfrRate = STAmount::setRate (Ledger::getQuality (node.currentDirectory_));
            // For correct ratio
            node.uEntry          = 0;
            node.bEntryAdvance   = true;

            WriteLog (lsTRACE, RippleCalc)
                    << "nodeAdvance: directory dirty: node.saOfrRate="
                    << node.saOfrRate;
        }

        if (!node.bEntryAdvance)
        {
            if (node.bFundsDirty)
            {
                // We were called again probably merely to update structure
                // variables.
                node.saTakerPays = node.sleOffer->getFieldAmount (sfTakerPays);
                node.saTakerGets = node.sleOffer->getFieldAmount (sfTakerGets);

                // Funds left.
                node.saOfferFunds = rippleCalc.mActiveLedger.accountFunds (
                                        node.offerOwnerAccount_, node.saTakerGets);
                node.bFundsDirty     = false;

                WriteLog (lsTRACE, RippleCalc)
                        << "nodeAdvance: funds dirty: node.saOfrRate="
                        << node.saOfrRate;
            }
            else
            {
                WriteLog (lsTRACE, RippleCalc) << "nodeAdvance: as is";
            }
        }
        else if (!rippleCalc.mActiveLedger.dirNext (
                     node.currentDirectory_, node.sleDirectDir, node.uEntry, node.offerIndex_))
            // This is the only place that offerIndex_ changes.
        {
            // Failed to find an entry in directory.
            // Do another cur directory iff bMultiQuality
            if (bMultiQuality)
            {
                // We are allowed to process multiple qualities if this is the
                // only path.
                WriteLog (lsTRACE, RippleCalc)
                        << "nodeAdvance: next quality";
                node.bDirectAdvance  = true;  // Process next quality.
            }
            else if (!bReverse)
            {
                // We didn't run dry going backwards - why are we running dry
                // going forwards - this should be impossible!
                // TODO(tom): these warnings occur in production!  They
                // shouldn't.
                WriteLog (lsWARNING, RippleCalc)
                        << "nodeAdvance: unreachable: ran out of offers";
                return rippleCalc.mOpenLedger ? telFAILED_PROCESSING :
                       tecFAILED_PROCESSING;
            }
            else
            {
                // Ran off end of offers.
                node.bEntryAdvance   = false;        // Done.
                node.offerIndex_ = 0;            // Report no more entries.
            }
        }
        else
        {
            // Got a new offer.
            node.sleOffer = rippleCalc.mActiveLedger.entryCache (
                                ltOFFER, node.offerIndex_);

            if (!node.sleOffer)
            {
                // Corrupt directory that points to an entry that doesn't exist.
                // This has happened in production.
                WriteLog (lsWARNING, RippleCalc) <<
                                                 "Missing offer in directory";
                node.bEntryAdvance = true;
            }
            else
            {
                node.offerOwnerAccount_
                    = node.sleOffer->getFieldAccount160 (sfAccount);
                node.saTakerPays = node.sleOffer->getFieldAmount (sfTakerPays);
                node.saTakerGets = node.sleOffer->getFieldAmount (sfTakerGets);

                const AccountCurrencyIssuer asLine (
                    node.offerOwnerAccount_, node.currency_, node.issuer_);

                WriteLog (lsTRACE, RippleCalc)
                        << "nodeAdvance: offerOwnerAccount_="
                        << to_string (node.offerOwnerAccount_)
                        << " node.saTakerPays=" << node.saTakerPays
                        << " node.saTakerGets=" << node.saTakerGets
                        << " node.offerIndex_=" << node.offerIndex_;

                if (node.sleOffer->isFieldPresent (sfExpiration) &&
                        (node.sleOffer->getFieldU32 (sfExpiration) <=
                         rippleCalc.mActiveLedger.getLedger ()->getParentCloseTimeNC ()))
                {
                    // Offer is expired.
                    WriteLog (lsTRACE, RippleCalc)
                            << "nodeAdvance: expired offer";
                    rippleCalc.mUnfundedOffers.insert(node.offerIndex_);
                    continue;
                }

                if (node.saTakerPays <= zero || node.saTakerGets <= zero)
                {
                    // Offer has bad amounts. Offers should never have a bad
                    // amounts.

                    if (bReverse)
                    {
                        // Past internal error, offer had bad amounts.
                        // This has occurred in production.
                        WriteLog (lsWARNING, RippleCalc)
                                << "nodeAdvance: PAST INTERNAL ERROR"
                                << " REVERSE: OFFER NON-POSITIVE:"
                                << " node.saTakerPays=" << node.saTakerPays
                                << " node.saTakerGets=%s" << node.saTakerGets;

                        // Mark offer for always deletion.
                        rippleCalc.mUnfundedOffers.insert (node.offerIndex_);
                    }
                    else if (rippleCalc.mUnfundedOffers.find (node.offerIndex_)
                             != rippleCalc.mUnfundedOffers.end ())
                    {
                        // Past internal error, offer was found failed to place
                        // this in mUnfundedOffers.
                        // Just skip it. It will be deleted.
                        WriteLog (lsDEBUG, RippleCalc)
                                << "nodeAdvance: PAST INTERNAL ERROR "
                                << " FORWARD CONFIRM: OFFER NON-POSITIVE:"
                                << " node.saTakerPays=" << node.saTakerPays
                                << " node.saTakerGets=" << node.saTakerGets;

                    }
                    else
                    {
                        // Reverse should have previously put bad offer in list.
                        // An internal error previously left a bad offer.
                        WriteLog (lsWARNING, RippleCalc)
                                << "nodeAdvance: INTERNAL ERROR"

                                <<" FORWARD NEWLY FOUND: OFFER NON-POSITIVE:"
                                << " node.saTakerPays=" << node.saTakerPays
                                << " node.saTakerGets=" << node.saTakerGets;

                        // Don't process at all, things are in an unexpected
                        // state for this transactions.
                        resultCode = tefEXCEPTION;
                    }

                    continue;
                }

                // Allowed to access source from this node?
                //
                // XXX This can get called multiple times for same source in a
                // row, caching result would be nice.
                //
                // XXX Going forward could we fund something with a worse
                // quality which was previously skipped? Might need to check
                // quality.
                auto itForward = pathState.forward().find (asLine);
                const bool bFoundForward = itForward != pathState.forward().end ();

                // Only allow a source to be used once, in the first node
                // encountered from initial path scan.  This prevents
                // conflicting uses of the same balance when going reverse vs
                // forward.
                if (bFoundForward &&
                        itForward->second != nodeIndex &&
                        node.offerOwnerAccount_ != node.issuer_)
                {
                    // Temporarily unfunded. Another node uses this source,
                    // ignore in this offer.
                    WriteLog (lsTRACE, RippleCalc)
                            << "nodeAdvance: temporarily unfunded offer"
                            << " (forward)";
                    continue;
                }

                // This is overly strict. For contributions to past. We should
                // only count source if actually used.
                auto itReverse = pathState.reverse().find (asLine);
                bool bFoundReverse = itReverse != pathState.reverse().end ();

                // For this quality increment, only allow a source to be used
                // from a single node, in the first node encountered from
                // applying offers in reverse.
                if (bFoundReverse &&
                        itReverse->second != nodeIndex &&
                        node.offerOwnerAccount_ != node.issuer_)
                {
                    // Temporarily unfunded. Another node uses this source,
                    // ignore in this offer.
                    WriteLog (lsTRACE, RippleCalc)
                            << "nodeAdvance: temporarily unfunded offer"
                            <<" (reverse)";
                    continue;
                }

                // Determine if used in past.
                // We only need to know if it might need to be marked unfunded.
                auto itPast = rippleCalc.mumSource.find (asLine);
                bool bFoundPast = (itPast != rippleCalc.mumSource.end ());

                // Only the current node is allowed to use the source.

                node.saOfferFunds = rippleCalc.mActiveLedger.accountFunds
                                    (node.offerOwnerAccount_, node.saTakerGets); // Funds held.

                if (node.saOfferFunds <= zero)
                {
                    // Offer is unfunded.
                    WriteLog (lsTRACE, RippleCalc)
                            << "nodeAdvance: unfunded offer";

                    if (bReverse && !bFoundReverse && !bFoundPast)
                    {
                        // Never mentioned before, clearly just: found unfunded.
                        // That is, even if this offer fails due to fill or kill
                        // still do deletions.
                        // Mark offer for always deletion.
                        rippleCalc.mUnfundedOffers.insert (node.offerIndex_);
                    }
                    else
                    {
                        // Moving forward, don't need to insert again
                        // Or, already found it.
                    }

                    // YYY Could verify offer is correct place for unfundeds.
                    continue;
                }

                if (bReverse            // Need to remember reverse mention.
                        && !bFoundPast      // Not mentioned in previous passes.
                        && !bFoundReverse)  // New to pass.
                {
                    // Consider source mentioned by current path state.
                    WriteLog (lsTRACE, RippleCalc)
                            << "nodeAdvance: remember="
                            <<  to_string (node.offerOwnerAccount_)
                            << "/"
                            << to_string (node.currency_)
                            << "/"
                            << to_string (node.issuer_);

                    pathState.reverse().insert (std::make_pair (asLine, nodeIndex));
                }

                node.bFundsDirty     = false;
                node.bEntryAdvance   = false;
            }
        }
    }
    while (resultCode == tesSUCCESS && (node.bEntryAdvance || node.bDirectAdvance));

    if (resultCode == tesSUCCESS)
    {
        WriteLog (lsTRACE, RippleCalc)
                << "nodeAdvance: node.offerIndex_=" << node.offerIndex_;
    }
    else
    {
        WriteLog (lsDEBUG, RippleCalc)
                << "nodeAdvance: resultCode=" << transToken (resultCode);
    }

    return resultCode;
}