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