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