//------------------------------------------------------------------------------ /* Copyright (c) 2011-2013, OpenCoin, Inc. */ //============================================================================== SETUP_LOG (LedgerEntrySet) // #define META_DEBUG // VFALCO TODO Replace this macro with a documented language constant // NOTE Is this part of the protocol? // #define DIR_NODE_MAX 32 void LedgerEntrySet::init (Ledger::ref ledger, uint256 const& transactionID, uint32 ledgerID, TransactionEngineParams params) { mEntries.clear (); mLedger = ledger; mSet.init (transactionID, ledgerID); mParams = params; mSeq = 0; } void LedgerEntrySet::clear () { mEntries.clear (); mSet.clear (); } LedgerEntrySet LedgerEntrySet::duplicate () const { return LedgerEntrySet (mLedger, mEntries, mSet, mSeq + 1); } void LedgerEntrySet::setTo (const LedgerEntrySet& e) { mLedger = e.mLedger; mEntries = e.mEntries; mSet = e.mSet; mParams = e.mParams; mSeq = e.mSeq; } void LedgerEntrySet::swapWith (LedgerEntrySet& e) { std::swap (mLedger, e.mLedger); mEntries.swap (e.mEntries); mSet.swap (e.mSet); std::swap (mParams, e.mParams); std::swap (mSeq, e.mSeq); } // Find an entry in the set. If it has the wrong sequence number, copy it and update the sequence number. // This is basically: copy-on-read. SLE::pointer LedgerEntrySet::getEntry (uint256 const& index, LedgerEntryAction& action) { std::map::iterator it = mEntries.find (index); if (it == mEntries.end ()) { action = taaNONE; return SLE::pointer (); } if (it->second.mSeq != mSeq) { assert (it->second.mSeq < mSeq); it->second.mEntry = boost::make_shared (*it->second.mEntry); it->second.mSeq = mSeq; } action = it->second.mAction; return it->second.mEntry; } SLE::pointer LedgerEntrySet::entryCreate (LedgerEntryType letType, uint256 const& index) { assert (index.isNonZero ()); SLE::pointer sleNew = boost::make_shared (letType, index); entryCreate (sleNew); return sleNew; } SLE::pointer LedgerEntrySet::entryCache (LedgerEntryType letType, uint256 const& index) { assert (mLedger); SLE::pointer sleEntry; if (index.isNonZero ()) { LedgerEntryAction action; sleEntry = getEntry (index, action); if (!sleEntry) { assert (action != taaDELETE); sleEntry = mImmutable ? mLedger->getSLEi (index) : mLedger->getSLE (index); if (sleEntry) entryCache (sleEntry); } else if (action == taaDELETE) sleEntry.reset (); } return sleEntry; } LedgerEntryAction LedgerEntrySet::hasEntry (uint256 const& index) const { std::map::const_iterator it = mEntries.find (index); if (it == mEntries.end ()) return taaNONE; return it->second.mAction; } void LedgerEntrySet::entryCache (SLE::ref sle) { assert (mLedger); assert (sle->isMutable () || mImmutable); // Don't put an immutable SLE in a mutable LES std::map::iterator it = mEntries.find (sle->getIndex ()); if (it == mEntries.end ()) { mEntries.insert (std::make_pair (sle->getIndex (), LedgerEntrySetEntry (sle, taaCACHED, mSeq))); return; } switch (it->second.mAction) { case taaCACHED: assert (sle == it->second.mEntry); it->second.mSeq = mSeq; it->second.mEntry = sle; return; default: throw std::runtime_error ("Cache after modify/delete/create"); } } void LedgerEntrySet::entryCreate (SLE::ref sle) { assert (mLedger && !mImmutable); assert (sle->isMutable ()); std::map::iterator it = mEntries.find (sle->getIndex ()); if (it == mEntries.end ()) { mEntries.insert (std::make_pair (sle->getIndex (), LedgerEntrySetEntry (sle, taaCREATE, mSeq))); return; } switch (it->second.mAction) { case taaDELETE: WriteLog (lsDEBUG, LedgerEntrySet) << "Create after Delete = Modify"; it->second.mEntry = sle; it->second.mAction = taaMODIFY; it->second.mSeq = mSeq; break; case taaMODIFY: throw std::runtime_error ("Create after modify"); case taaCREATE: throw std::runtime_error ("Create after create"); // This could be made to work case taaCACHED: throw std::runtime_error ("Create after cache"); default: throw std::runtime_error ("Unknown taa"); } assert (it->second.mSeq == mSeq); } void LedgerEntrySet::entryModify (SLE::ref sle) { assert (sle->isMutable () && !mImmutable); assert (mLedger); std::map::iterator it = mEntries.find (sle->getIndex ()); if (it == mEntries.end ()) { mEntries.insert (std::make_pair (sle->getIndex (), LedgerEntrySetEntry (sle, taaMODIFY, mSeq))); return; } assert (it->second.mSeq == mSeq); assert (it->second.mEntry == sle); switch (it->second.mAction) { case taaCACHED: it->second.mAction = taaMODIFY; fallthru (); case taaCREATE: case taaMODIFY: it->second.mSeq = mSeq; it->second.mEntry = sle; break; case taaDELETE: throw std::runtime_error ("Modify after delete"); default: throw std::runtime_error ("Unknown taa"); } } void LedgerEntrySet::entryDelete (SLE::ref sle) { assert (sle->isMutable () && !mImmutable); assert (mLedger); std::map::iterator it = mEntries.find (sle->getIndex ()); if (it == mEntries.end ()) { assert (false); // deleting an entry not cached? mEntries.insert (std::make_pair (sle->getIndex (), LedgerEntrySetEntry (sle, taaDELETE, mSeq))); return; } assert (it->second.mSeq == mSeq); assert (it->second.mEntry == sle); switch (it->second.mAction) { case taaCACHED: case taaMODIFY: it->second.mSeq = mSeq; it->second.mEntry = sle; it->second.mAction = taaDELETE; break; case taaCREATE: mEntries.erase (it); break; case taaDELETE: break; default: throw std::runtime_error ("Unknown taa"); } } bool LedgerEntrySet::hasChanges () { typedef std::map::value_type u256_LES_pair; BOOST_FOREACH (u256_LES_pair & it, mEntries) if (it.second.mAction != taaCACHED) return true; return false; } bool LedgerEntrySet::intersect (const LedgerEntrySet& lesLeft, const LedgerEntrySet& lesRight) { return true; // XXX Needs implementation } Json::Value LedgerEntrySet::getJson (int) const { Json::Value ret (Json::objectValue); Json::Value nodes (Json::arrayValue); for (std::map::const_iterator it = mEntries.begin (), end = mEntries.end (); it != end; ++it) { Json::Value entry (Json::objectValue); entry["node"] = it->first.GetHex (); switch (it->second.mEntry->getType ()) { case ltINVALID: entry["type"] = "invalid"; break; case ltACCOUNT_ROOT: entry["type"] = "acccount_root"; break; case ltDIR_NODE: entry["type"] = "dir_node"; break; case ltGENERATOR_MAP: entry["type"] = "generator_map"; break; case ltRIPPLE_STATE: entry["type"] = "ripple_state"; break; case ltNICKNAME: entry["type"] = "nickname"; break; case ltOFFER: entry["type"] = "offer"; break; default: assert (false); } switch (it->second.mAction) { case taaCACHED: entry["action"] = "cache"; break; case taaMODIFY: entry["action"] = "modify"; break; case taaDELETE: entry["action"] = "delete"; break; case taaCREATE: entry["action"] = "create"; break; default: assert (false); } nodes.append (entry); } ret["nodes" ] = nodes; ret["metaData"] = mSet.getJson (0); return ret; } SLE::pointer LedgerEntrySet::getForMod (uint256 const& node, Ledger::ref ledger, boost::unordered_map& newMods) { std::map::iterator it = mEntries.find (node); if (it != mEntries.end ()) { if (it->second.mAction == taaDELETE) { WriteLog (lsFATAL, LedgerEntrySet) << "Trying to thread to deleted node"; return SLE::pointer (); } if (it->second.mAction == taaCACHED) it->second.mAction = taaMODIFY; if (it->second.mSeq != mSeq) { it->second.mEntry = boost::make_shared (*it->second.mEntry); it->second.mSeq = mSeq; } return it->second.mEntry; } boost::unordered_map::iterator me = newMods.find (node); if (me != newMods.end ()) { assert (me->second); return me->second; } SLE::pointer ret = ledger->getSLE (node); if (ret) newMods.insert (std::make_pair (node, ret)); return ret; } bool LedgerEntrySet::threadTx (const RippleAddress& threadTo, Ledger::ref ledger, boost::unordered_map& newMods) { #ifdef META_DEBUG WriteLog (lsTRACE, LedgerEntrySet) << "Thread to " << threadTo.getAccountID (); #endif SLE::pointer sle = getForMod (Ledger::getAccountRootIndex (threadTo.getAccountID ()), ledger, newMods); if (!sle) { WriteLog (lsFATAL, LedgerEntrySet) << "Threading to non-existent account: " << threadTo.humanAccountID (); assert (false); return false; } return threadTx (sle, ledger, newMods); } bool LedgerEntrySet::threadTx (SLE::ref threadTo, Ledger::ref ledger, boost::unordered_map& newMods) { // node = the node that was modified/deleted/created // threadTo = the node that needs to know uint256 prevTxID; uint32 prevLgrID; if (!threadTo->thread (mSet.getTxID (), mSet.getLgrSeq (), prevTxID, prevLgrID)) return false; if (prevTxID.isZero () || TransactionMetaSet::thread (mSet.getAffectedNode (threadTo, sfModifiedNode), prevTxID, prevLgrID)) return true; assert (false); return false; } bool LedgerEntrySet::threadOwners (SLE::ref node, Ledger::ref ledger, boost::unordered_map& newMods) { // thread new or modified node to owner or owners if (node->hasOneOwner ()) // thread to owner's account { #ifdef META_DEBUG WriteLog (lsTRACE, LedgerEntrySet) << "Thread to single owner"; #endif return threadTx (node->getOwner (), ledger, newMods); } else if (node->hasTwoOwners ()) // thread to owner's accounts { #ifdef META_DEBUG WriteLog (lsTRACE, LedgerEntrySet) << "Thread to two owners"; #endif return threadTx (node->getFirstOwner (), ledger, newMods) && threadTx (node->getSecondOwner (), ledger, newMods); } else return false; } void LedgerEntrySet::calcRawMeta (Serializer& s, TER result, uint32 index) { // calculate the raw meta data and return it. This must be called before the set is committed // Entries modified only as a result of building the transaction metadata boost::unordered_map newMod; typedef std::map::value_type u256_LES_pair; BOOST_FOREACH (u256_LES_pair & it, mEntries) { SField::ptr type = &sfGeneric; switch (it.second.mAction) { case taaMODIFY: #ifdef META_DEBUG WriteLog (lsTRACE, LedgerEntrySet) << "Modified Node " << it.first; #endif type = &sfModifiedNode; break; case taaDELETE: #ifdef META_DEBUG WriteLog (lsTRACE, LedgerEntrySet) << "Deleted Node " << it.first; #endif type = &sfDeletedNode; break; case taaCREATE: #ifdef META_DEBUG WriteLog (lsTRACE, LedgerEntrySet) << "Created Node " << it.first; #endif type = &sfCreatedNode; break; default: // ignore these break; } if (type == &sfGeneric) continue; SLE::pointer origNode = mLedger->getSLEi (it.first); SLE::pointer curNode = it.second.mEntry; if ((type == &sfModifiedNode) && (*curNode == *origNode)) continue; uint16 nodeType = curNode ? curNode->getFieldU16 (sfLedgerEntryType) : origNode->getFieldU16 (sfLedgerEntryType); mSet.setAffectedNode (it.first, *type, nodeType); if (type == &sfDeletedNode) { assert (origNode && curNode); threadOwners (origNode, mLedger, newMod); // thread transaction to owners STObject prevs (sfPreviousFields); BOOST_FOREACH (const SerializedType & obj, *origNode) { // go through the original node for modified fields saved on modification if (obj.getFName ().shouldMeta (SField::sMD_ChangeOrig) && !curNode->hasMatchingEntry (obj)) prevs.addObject (obj); } if (!prevs.empty ()) mSet.getAffectedNode (it.first).addObject (prevs); STObject finals (sfFinalFields); BOOST_FOREACH (const SerializedType & obj, *curNode) { // go through the final node for final fields if (obj.getFName ().shouldMeta (SField::sMD_Always | SField::sMD_DeleteFinal)) finals.addObject (obj); } if (!finals.empty ()) mSet.getAffectedNode (it.first).addObject (finals); } else if (type == &sfModifiedNode) { assert (curNode && origNode); if (curNode->isThreadedType ()) // thread transaction to node it modified threadTx (curNode, mLedger, newMod); STObject prevs (sfPreviousFields); BOOST_FOREACH (const SerializedType & obj, *origNode) { // search the original node for values saved on modify if (obj.getFName ().shouldMeta (SField::sMD_ChangeOrig) && !curNode->hasMatchingEntry (obj)) prevs.addObject (obj); } if (!prevs.empty ()) mSet.getAffectedNode (it.first).addObject (prevs); STObject finals (sfFinalFields); BOOST_FOREACH (const SerializedType & obj, *curNode) { // search the final node for values saved always if (obj.getFName ().shouldMeta (SField::sMD_Always | SField::sMD_ChangeNew)) finals.addObject (obj); } if (!finals.empty ()) mSet.getAffectedNode (it.first).addObject (finals); } else if (type == &sfCreatedNode) // if created, thread to owner(s) { assert (curNode && !origNode); threadOwners (curNode, mLedger, newMod); if (curNode->isThreadedType ()) // always thread to self threadTx (curNode, mLedger, newMod); STObject news (sfNewFields); BOOST_FOREACH (const SerializedType & obj, *curNode) { // save non-default values if (!obj.isDefault () && obj.getFName ().shouldMeta (SField::sMD_Create | SField::sMD_Always)) news.addObject (obj); } if (!news.empty ()) mSet.getAffectedNode (it.first).addObject (news); } else assert (false); } // add any new modified nodes to the modification set typedef std::map::value_type u256_sle_pair; BOOST_FOREACH (u256_sle_pair & it, newMod) entryModify (it.second); mSet.addRaw (s, result, index); WriteLog (lsTRACE, LedgerEntrySet) << "Metadata:" << mSet.getJson (0); } TER LedgerEntrySet::dirCount (uint256 const& uRootIndex, uint32& uCount) { uint64 uNodeDir = 0; uCount = 0; do { SLE::pointer sleNode = entryCache (ltDIR_NODE, Ledger::getDirNodeIndex (uRootIndex, uNodeDir)); if (sleNode) { uCount += sleNode->getFieldV256 (sfIndexes).peekValue ().size (); uNodeDir = sleNode->getFieldU64 (sfIndexNext); // Get next node. } else if (uNodeDir) { WriteLog (lsWARNING, LedgerEntrySet) << "dirCount: no such node"; assert (false); return tefBAD_LEDGER; } } while (uNodeDir); return tesSUCCESS; } // <-- uNodeDir: For deletion, present to make dirDelete efficient. // --> uRootIndex: The index of the base of the directory. Nodes are based off of this. // --> uLedgerIndex: Value to add to directory. // Only append. This allow for things that watch append only structure to just monitor from the last node on ward. // Within a node with no deletions order of elements is sequential. Otherwise, order of elements is random. TER LedgerEntrySet::dirAdd ( uint64& uNodeDir, uint256 const& uRootIndex, uint256 const& uLedgerIndex, FUNCTION_TYPE fDescriber) { WriteLog (lsTRACE, LedgerEntrySet) << boost::str (boost::format ("dirAdd: uRootIndex=%s uLedgerIndex=%s") % uRootIndex.ToString () % uLedgerIndex.ToString ()); SLE::pointer sleNode; STVector256 svIndexes; SLE::pointer sleRoot = entryCache (ltDIR_NODE, uRootIndex); if (!sleRoot) { // No root, make it. sleRoot = entryCreate (ltDIR_NODE, uRootIndex); sleRoot->setFieldH256 (sfRootIndex, uRootIndex); fDescriber (sleRoot); sleNode = sleRoot; uNodeDir = 0; } else { uNodeDir = sleRoot->getFieldU64 (sfIndexPrevious); // Get index to last directory node. if (uNodeDir) { // Try adding to last node. sleNode = entryCache (ltDIR_NODE, Ledger::getDirNodeIndex (uRootIndex, uNodeDir)); assert (sleNode); } else { // Try adding to root. Didn't have a previous set to the last node. sleNode = sleRoot; } svIndexes = sleNode->getFieldV256 (sfIndexes); if (DIR_NODE_MAX != svIndexes.peekValue ().size ()) { // Add to current node. entryModify (sleNode); } // Add to new node. else if (!++uNodeDir) { return tecDIR_FULL; } else { // Have old last point to new node sleNode->setFieldU64 (sfIndexNext, uNodeDir); entryModify (sleNode); // Have root point to new node. sleRoot->setFieldU64 (sfIndexPrevious, uNodeDir); entryModify (sleRoot); // Create the new node. sleNode = entryCreate (ltDIR_NODE, Ledger::getDirNodeIndex (uRootIndex, uNodeDir)); sleNode->setFieldH256 (sfRootIndex, uRootIndex); if (uNodeDir != 1) sleNode->setFieldU64 (sfIndexPrevious, uNodeDir - 1); fDescriber (sleNode); svIndexes = STVector256 (); } } svIndexes.peekValue ().push_back (uLedgerIndex); // Append entry. sleNode->setFieldV256 (sfIndexes, svIndexes); // Save entry. WriteLog (lsTRACE, LedgerEntrySet) << "dirAdd: creating: root: " << uRootIndex.ToString (); WriteLog (lsTRACE, LedgerEntrySet) << "dirAdd: appending: Entry: " << uLedgerIndex.ToString (); WriteLog (lsTRACE, LedgerEntrySet) << "dirAdd: appending: Node: " << strHex (uNodeDir); // WriteLog (lsINFO, LedgerEntrySet) << "dirAdd: appending: PREV: " << svIndexes.peekValue()[0].ToString(); return tesSUCCESS; } // Ledger must be in a state for this to work. TER LedgerEntrySet::dirDelete ( const bool bKeepRoot, // --> True, if we never completely clean up, after we overflow the root node. const uint64& uNodeDir, // --> Node containing entry. uint256 const& uRootIndex, // --> The index of the base of the directory. Nodes are based off of this. uint256 const& uLedgerIndex, // --> Value to remove from directory. const bool bStable, // --> True, not to change relative order of entries. const bool bSoft) // --> True, uNodeDir is not hard and fast (pass uNodeDir=0). { uint64 uNodeCur = uNodeDir; SLE::pointer sleNode = entryCache (ltDIR_NODE, uNodeCur ? Ledger::getDirNodeIndex (uRootIndex, uNodeCur) : uRootIndex); if (!sleNode) { WriteLog (lsWARNING, LedgerEntrySet) << boost::str (boost::format ("dirDelete: no such node: uRootIndex=%s uNodeDir=%s uLedgerIndex=%s") % uRootIndex.ToString () % strHex (uNodeDir) % uLedgerIndex.ToString ()); if (!bSoft) { assert (false); return tefBAD_LEDGER; } else if (uNodeDir < 20) { // Go the extra mile. Even if node doesn't exist, try the next node. return dirDelete (bKeepRoot, uNodeDir + 1, uRootIndex, uLedgerIndex, bStable, true); } else { return tefBAD_LEDGER; } } STVector256 svIndexes = sleNode->getFieldV256 (sfIndexes); std::vector& vuiIndexes = svIndexes.peekValue (); std::vector::iterator it; it = std::find (vuiIndexes.begin (), vuiIndexes.end (), uLedgerIndex); if (vuiIndexes.end () == it) { if (!bSoft) { assert (false); WriteLog (lsWARNING, LedgerEntrySet) << "dirDelete: no such entry"; return tefBAD_LEDGER; } else if (uNodeDir < 20) { // Go the extra mile. Even if entry not in node, try the next node. return dirDelete (bKeepRoot, uNodeDir + 1, uRootIndex, uLedgerIndex, bStable, true); } else { return tefBAD_LEDGER; } } // Remove the element. if (vuiIndexes.size () > 1) { if (bStable) { vuiIndexes.erase (it); } else { *it = vuiIndexes[vuiIndexes.size () - 1]; vuiIndexes.resize (vuiIndexes.size () - 1); } } else { vuiIndexes.clear (); } sleNode->setFieldV256 (sfIndexes, svIndexes); entryModify (sleNode); if (vuiIndexes.empty ()) { // May be able to delete nodes. uint64 uNodePrevious = sleNode->getFieldU64 (sfIndexPrevious); uint64 uNodeNext = sleNode->getFieldU64 (sfIndexNext); if (!uNodeCur) { // Just emptied root node. if (!uNodePrevious) { // Never overflowed the root node. Delete it. entryDelete (sleNode); } // Root overflowed. else if (bKeepRoot) { // If root overflowed and not allowed to delete overflowed root node. nothing (); } else if (uNodePrevious != uNodeNext) { // Have more than 2 nodes. Can't delete root node. nothing (); } else { // Have only a root node and a last node. SLE::pointer sleLast = entryCache (ltDIR_NODE, Ledger::getDirNodeIndex (uRootIndex, uNodeNext)); assert (sleLast); if (sleLast->getFieldV256 (sfIndexes).peekValue ().empty ()) { // Both nodes are empty. entryDelete (sleNode); // Delete root. entryDelete (sleLast); // Delete last. } else { // Have an entry, can't delete root node. nothing (); } } } // Just emptied a non-root node. else if (uNodeNext) { // Not root and not last node. Can delete node. SLE::pointer slePrevious = entryCache (ltDIR_NODE, uNodePrevious ? Ledger::getDirNodeIndex (uRootIndex, uNodePrevious) : uRootIndex); assert (slePrevious); SLE::pointer sleNext = entryCache (ltDIR_NODE, uNodeNext ? Ledger::getDirNodeIndex (uRootIndex, uNodeNext) : uRootIndex); assert (slePrevious); assert (sleNext); if (!slePrevious) { WriteLog (lsWARNING, LedgerEntrySet) << "dirDelete: previous node is missing"; return tefBAD_LEDGER; } if (!sleNext) { WriteLog (lsWARNING, LedgerEntrySet) << "dirDelete: next node is missing"; return tefBAD_LEDGER; } // Fix previous to point to its new next. slePrevious->setFieldU64 (sfIndexNext, uNodeNext); entryModify (slePrevious); // Fix next to point to its new previous. sleNext->setFieldU64 (sfIndexPrevious, uNodePrevious); entryModify (sleNext); entryDelete(sleNode); } // Last node. else if (bKeepRoot || uNodePrevious) { // Not allowed to delete last node as root was overflowed. // Or, have pervious entries preventing complete delete. nothing (); } else { // Last and only node besides the root. SLE::pointer sleRoot = entryCache (ltDIR_NODE, uRootIndex); assert (sleRoot); if (sleRoot->getFieldV256 (sfIndexes).peekValue ().empty ()) { // Both nodes are empty. entryDelete (sleRoot); // Delete root. entryDelete (sleNode); // Delete last. } else { // Root has an entry, can't delete. nothing (); } } } return tesSUCCESS; } // Return the first entry and advance uDirEntry. // <-- true, if had a next entry. bool LedgerEntrySet::dirFirst ( uint256 const& uRootIndex, // --> Root of directory. SLE::pointer& sleNode, // <-- current node unsigned int& uDirEntry, // <-- next entry uint256& uEntryIndex) // <-- The entry, if available. Otherwise, zero. { sleNode = entryCache (ltDIR_NODE, uRootIndex); uDirEntry = 0; assert (sleNode); // Never probe for directories. return LedgerEntrySet::dirNext (uRootIndex, sleNode, uDirEntry, uEntryIndex); } // Return the current entry and advance uDirEntry. // <-- true, if had a next entry. bool LedgerEntrySet::dirNext ( uint256 const& uRootIndex, // --> Root of directory SLE::pointer& sleNode, // <-> current node unsigned int& uDirEntry, // <-> next entry uint256& uEntryIndex) // <-- The entry, if available. Otherwise, zero. { STVector256 svIndexes = sleNode->getFieldV256 (sfIndexes); std::vector& vuiIndexes = svIndexes.peekValue (); assert (uDirEntry <= vuiIndexes.size ()); if (uDirEntry >= vuiIndexes.size ()) { uint64 uNodeNext = sleNode->getFieldU64 (sfIndexNext); if (!uNodeNext) { uEntryIndex.zero (); return false; } else { SLE::pointer sleNext = entryCache (ltDIR_NODE, Ledger::getDirNodeIndex (uRootIndex, uNodeNext)); uDirEntry = 0; if (!sleNext) { // This should never happen WriteLog (lsFATAL, LedgerEntrySet) << "Corrupt directory: index:" << uRootIndex << " next:" << uNodeNext; return false; } sleNode = sleNext; return dirNext (uRootIndex, sleNode, uDirEntry, uEntryIndex); } } uEntryIndex = vuiIndexes[uDirEntry++]; WriteLog (lsTRACE, LedgerEntrySet) << boost::str (boost::format ("dirNext: uDirEntry=%d uEntryIndex=%s") % uDirEntry % uEntryIndex); return true; } uint256 LedgerEntrySet::getNextLedgerIndex (uint256 const& uHash) { // find next node in ledger that isn't deleted by LES uint256 ledgerNext = uHash; std::map::const_iterator it; do { ledgerNext = mLedger->getNextLedgerIndex (ledgerNext); it = mEntries.find (ledgerNext); } while ((it != mEntries.end ()) && (it->second.mAction == taaDELETE)); // find next node in LES that isn't deleted for (it = mEntries.upper_bound (uHash); it != mEntries.end (); ++it) { // node found in LES, node found in ledger, return earliest if (it->second.mAction != taaDELETE) return (ledgerNext.isNonZero () && (ledgerNext < it->first)) ? ledgerNext : it->first; } // nothing next in LES, return next ledger node return ledgerNext; } uint256 LedgerEntrySet::getNextLedgerIndex (uint256 const& uHash, uint256 const& uEnd) { uint256 next = getNextLedgerIndex (uHash); if (next > uEnd) return uint256 (); return next; } // If there is a count, adjust the owner count by iAmount. Otherwise, compute the owner count and store it. void LedgerEntrySet::ownerCountAdjust (const uint160& uOwnerID, int iAmount, SLE::ref sleAccountRoot) { SLE::pointer sleHold = sleAccountRoot ? SLE::pointer () : entryCache (ltACCOUNT_ROOT, Ledger::getAccountRootIndex (uOwnerID)); SLE::ref sleRoot = sleAccountRoot ? sleAccountRoot : sleHold; const uint32 uOwnerCount = sleRoot->getFieldU32 (sfOwnerCount); const uint32 uNew = iAmount + int (uOwnerCount) > 0 ? uOwnerCount + iAmount : 0; if (uOwnerCount != uNew) { sleRoot->setFieldU32 (sfOwnerCount, uOwnerCount + iAmount); entryModify (sleRoot); } } TER LedgerEntrySet::offerDelete (SLE::ref sleOffer, uint256 const& uOfferIndex, const uint160& uOwnerID) { bool bOwnerNode = sleOffer->isFieldPresent (sfOwnerNode); // Detect legacy dirs. uint64 uOwnerNode = sleOffer->getFieldU64 (sfOwnerNode); TER terResult = dirDelete (false, uOwnerNode, Ledger::getOwnerDirIndex (uOwnerID), uOfferIndex, false, !bOwnerNode); if (tesSUCCESS == terResult) { ownerCountAdjust (uOwnerID, -1); uint256 uDirectory = sleOffer->getFieldH256 (sfBookDirectory); uint64 uBookNode = sleOffer->getFieldU64 (sfBookNode); // Offer delete is always hard. Always have hints. terResult = dirDelete (false, uBookNode, uDirectory, uOfferIndex, true, true); } entryDelete (sleOffer); return terResult; } TER LedgerEntrySet::offerDelete (uint256 const& uOfferIndex) { SLE::pointer sleOffer = entryCache (ltOFFER, uOfferIndex); if (!sleOffer) return tesSUCCESS; const uint160 uOwnerID = sleOffer->getFieldAccount160 (sfAccount); return offerDelete (sleOffer, uOfferIndex, uOwnerID); } // Returns amount owed by uToAccountID to uFromAccountID. // <-- $owed/uCurrencyID/uToAccountID: positive: uFromAccountID holds IOUs., negative: uFromAccountID owes IOUs. STAmount LedgerEntrySet::rippleOwed (const uint160& uToAccountID, const uint160& uFromAccountID, const uint160& uCurrencyID) { STAmount saBalance; SLE::pointer sleRippleState = entryCache (ltRIPPLE_STATE, Ledger::getRippleStateIndex (uToAccountID, uFromAccountID, uCurrencyID)); if (sleRippleState) { saBalance = sleRippleState->getFieldAmount (sfBalance); if (uToAccountID < uFromAccountID) saBalance.negate (); saBalance.setIssuer (uToAccountID); } else { saBalance.zero (uCurrencyID, uToAccountID); WriteLog (lsDEBUG, LedgerEntrySet) << "rippleOwed: No credit line between " << RippleAddress::createHumanAccountID (uFromAccountID) << " and " << RippleAddress::createHumanAccountID (uToAccountID) << " for " << STAmount::createHumanCurrency (uCurrencyID) << "." ; #if 0 // We could cut off coming here if we test for no line sooner. assert (false); #endif } return saBalance; } // Maximum amount of IOUs uToAccountID will hold from uFromAccountID. // <-- $amount/uCurrencyID/uToAccountID. STAmount LedgerEntrySet::rippleLimit (const uint160& uToAccountID, const uint160& uFromAccountID, const uint160& uCurrencyID) { STAmount saLimit; SLE::pointer sleRippleState = entryCache (ltRIPPLE_STATE, Ledger::getRippleStateIndex (uToAccountID, uFromAccountID, uCurrencyID)); if (sleRippleState) { saLimit = sleRippleState->getFieldAmount (uToAccountID < uFromAccountID ? sfLowLimit : sfHighLimit); saLimit.setIssuer (uToAccountID); } else { saLimit.zero (uCurrencyID, uToAccountID); #if 0 // We could cut off coming here if we test for no line sooner. assert (false); #endif } return saLimit; } uint32 LedgerEntrySet::rippleTransferRate (const uint160& uIssuerID) { SLE::pointer sleAccount = entryCache (ltACCOUNT_ROOT, Ledger::getAccountRootIndex (uIssuerID)); uint32 uQuality = sleAccount && sleAccount->isFieldPresent (sfTransferRate) ? sleAccount->getFieldU32 (sfTransferRate) : QUALITY_ONE; WriteLog (lsDEBUG, LedgerEntrySet) << boost::str (boost::format ("rippleTransferRate: uIssuerID=%s account_exists=%d transfer_rate=%f") % RippleAddress::createHumanAccountID (uIssuerID) % !!sleAccount % (uQuality / 1000000000.0)); return uQuality; } uint32 LedgerEntrySet::rippleTransferRate (const uint160& uSenderID, const uint160& uReceiverID, const uint160& uIssuerID) { return uSenderID == uIssuerID || uReceiverID == uIssuerID ? QUALITY_ONE : rippleTransferRate (uIssuerID); } // XXX Might not need this, might store in nodes on calc reverse. uint32 LedgerEntrySet::rippleQualityIn (const uint160& uToAccountID, const uint160& uFromAccountID, const uint160& uCurrencyID, SField::ref sfLow, SField::ref sfHigh) { uint32 uQuality = QUALITY_ONE; SLE::pointer sleRippleState; if (uToAccountID == uFromAccountID) { nothing (); } else { sleRippleState = entryCache (ltRIPPLE_STATE, Ledger::getRippleStateIndex (uToAccountID, uFromAccountID, uCurrencyID)); if (sleRippleState) { SField::ref sfField = uToAccountID < uFromAccountID ? sfLow : sfHigh; uQuality = sleRippleState->isFieldPresent (sfField) ? sleRippleState->getFieldU32 (sfField) : QUALITY_ONE; if (!uQuality) uQuality = 1; // Avoid divide by zero. } else { // XXX Ideally, catch no before this. So we can assert to be stricter. uQuality = QUALITY_ONE; } } WriteLog (lsTRACE, LedgerEntrySet) << boost::str (boost::format ("rippleQuality: %s uToAccountID=%s uFromAccountID=%s uCurrencyID=%s bLine=%d uQuality=%f") % (sfLow == sfLowQualityIn ? "in" : "out") % RippleAddress::createHumanAccountID (uToAccountID) % RippleAddress::createHumanAccountID (uFromAccountID) % STAmount::createHumanCurrency (uCurrencyID) % !!sleRippleState % (uQuality / 1000000000.0)); // assert(uToAccountID == uFromAccountID || !!sleRippleState); return uQuality; } // Return how much of uIssuerID's uCurrencyID IOUs that uAccountID holds. May be negative. // <-- IOU's uAccountID has of uIssuerID. STAmount LedgerEntrySet::rippleHolds (const uint160& uAccountID, const uint160& uCurrencyID, const uint160& uIssuerID) { STAmount saBalance; SLE::pointer sleRippleState = entryCache (ltRIPPLE_STATE, Ledger::getRippleStateIndex (uAccountID, uIssuerID, uCurrencyID)); if (!sleRippleState) { saBalance.zero (uCurrencyID, uIssuerID); } else if (uAccountID > uIssuerID) { saBalance = sleRippleState->getFieldAmount (sfBalance); saBalance.negate (); // Put balance in uAccountID terms. saBalance.setIssuer (uIssuerID); } else { saBalance = sleRippleState->getFieldAmount (sfBalance); saBalance.setIssuer (uIssuerID); } return saBalance; } // Returns the amount an account can spend without going into debt. // // <-- saAmount: amount of uCurrencyID held by uAccountID. May be negative. STAmount LedgerEntrySet::accountHolds (const uint160& uAccountID, const uint160& uCurrencyID, const uint160& uIssuerID) { STAmount saAmount; if (!uCurrencyID) { SLE::pointer sleAccount = entryCache (ltACCOUNT_ROOT, Ledger::getAccountRootIndex (uAccountID)); uint64 uReserve = mLedger->getReserve (sleAccount->getFieldU32 (sfOwnerCount)); STAmount saBalance = sleAccount->getFieldAmount (sfBalance); if (saBalance < uReserve) { saAmount.zero (); } else { saAmount = saBalance - uReserve; } WriteLog (lsTRACE, LedgerEntrySet) << boost::str (boost::format ("accountHolds: uAccountID=%s saAmount=%s saBalance=%s uReserve=%d") % RippleAddress::createHumanAccountID (uAccountID) % saAmount.getFullText () % saBalance.getFullText () % uReserve); } else { saAmount = rippleHolds (uAccountID, uCurrencyID, uIssuerID); WriteLog (lsTRACE, LedgerEntrySet) << boost::str (boost::format ("accountHolds: uAccountID=%s saAmount=%s") % RippleAddress::createHumanAccountID (uAccountID) % saAmount.getFullText ()); } return saAmount; } // Returns the funds available for uAccountID for a currency/issuer. // Use when you need a default for rippling uAccountID's currency. // XXX Should take into account quality? // --> saDefault/currency/issuer // <-- saFunds: Funds available. May be negative. // If the issuer is the same as uAccountID, funds are unlimited, use result is saDefault. STAmount LedgerEntrySet::accountFunds (const uint160& uAccountID, const STAmount& saDefault) { STAmount saFunds; if (!saDefault.isNative () && saDefault.getIssuer () == uAccountID) { saFunds = saDefault; WriteLog (lsTRACE, LedgerEntrySet) << boost::str (boost::format ("accountFunds: uAccountID=%s saDefault=%s SELF-FUNDED") % RippleAddress::createHumanAccountID (uAccountID) % saDefault.getFullText ()); } else { saFunds = accountHolds (uAccountID, saDefault.getCurrency (), saDefault.getIssuer ()); WriteLog (lsTRACE, LedgerEntrySet) << boost::str (boost::format ("accountFunds: uAccountID=%s saDefault=%s saFunds=%s") % RippleAddress::createHumanAccountID (uAccountID) % saDefault.getFullText () % saFunds.getFullText ()); } return saFunds; } // Calculate transit fee. STAmount LedgerEntrySet::rippleTransferFee (const uint160& uSenderID, const uint160& uReceiverID, const uint160& uIssuerID, const STAmount& saAmount) { if (uSenderID != uIssuerID && uReceiverID != uIssuerID) { uint32 uTransitRate = rippleTransferRate (uIssuerID); if (QUALITY_ONE != uTransitRate) { STAmount saTransitRate (CURRENCY_ONE, ACCOUNT_ONE, static_cast (uTransitRate), -9); STAmount saTransferTotal = STAmount::multiply (saAmount, saTransitRate, saAmount.getCurrency (), saAmount.getIssuer ()); STAmount saTransferFee = saTransferTotal - saAmount; WriteLog (lsDEBUG, LedgerEntrySet) << boost::str (boost::format ("rippleTransferFee: saTransferFee=%s") % saTransferFee.getFullText ()); return saTransferFee; } } return STAmount (saAmount.getCurrency (), saAmount.getIssuer ()); } TER LedgerEntrySet::trustCreate ( const bool bSrcHigh, const uint160& uSrcAccountID, const uint160& uDstAccountID, uint256 const& uIndex, // --> ripple state entry SLE::ref sleAccount, // --> the account being set. const bool bAuth, // --> authorize account. const STAmount& saBalance, // --> balance of account being set. Issuer should be ACCOUNT_ONE const STAmount& saLimit, // --> limit for account being set. Issuer should be the account being set. const uint32 uQualityIn, const uint32 uQualityOut) { const uint160& uLowAccountID = !bSrcHigh ? uSrcAccountID : uDstAccountID; const uint160& uHighAccountID = bSrcHigh ? uSrcAccountID : uDstAccountID; SLE::pointer sleRippleState = entryCreate (ltRIPPLE_STATE, uIndex); uint64 uLowNode; uint64 uHighNode; TER terResult = dirAdd ( uLowNode, Ledger::getOwnerDirIndex (uLowAccountID), sleRippleState->getIndex (), BIND_TYPE (&Ledger::ownerDirDescriber, P_1, uLowAccountID)); if (tesSUCCESS == terResult) { terResult = dirAdd ( uHighNode, Ledger::getOwnerDirIndex (uHighAccountID), sleRippleState->getIndex (), BIND_TYPE (&Ledger::ownerDirDescriber, P_1, uHighAccountID)); } if (tesSUCCESS == terResult) { const bool bSetDst = saLimit.getIssuer () == uDstAccountID; const bool bSetHigh = bSrcHigh ^ bSetDst; sleRippleState->setFieldU64 (sfLowNode, uLowNode); // Remember deletion hints. sleRippleState->setFieldU64 (sfHighNode, uHighNode); sleRippleState->setFieldAmount (!bSetHigh ? sfLowLimit : sfHighLimit, saLimit); sleRippleState->setFieldAmount ( bSetHigh ? sfLowLimit : sfHighLimit, STAmount (saBalance.getCurrency (), bSetDst ? uSrcAccountID : uDstAccountID)); if (uQualityIn) sleRippleState->setFieldU32 (!bSetHigh ? sfLowQualityIn : sfHighQualityIn, uQualityIn); if (uQualityOut) sleRippleState->setFieldU32 (!bSetHigh ? sfLowQualityOut : sfHighQualityOut, uQualityOut); uint32 uFlags = !bSetHigh ? lsfLowReserve : lsfHighReserve; if (bAuth) { uFlags |= (!bSetHigh ? lsfLowAuth : lsfHighAuth); } sleRippleState->setFieldU32 (sfFlags, uFlags); ownerCountAdjust (!bSetDst ? uSrcAccountID : uDstAccountID, 1, sleAccount); sleRippleState->setFieldAmount (sfBalance, bSetHigh ? -saBalance : saBalance); // ONLY: Create ripple balance. } return terResult; } TER LedgerEntrySet::trustDelete (SLE::ref sleRippleState, const uint160& uLowAccountID, const uint160& uHighAccountID) { bool bLowNode = sleRippleState->isFieldPresent (sfLowNode); // Detect legacy dirs. bool bHighNode = sleRippleState->isFieldPresent (sfHighNode); uint64 uLowNode = sleRippleState->getFieldU64 (sfLowNode); uint64 uHighNode = sleRippleState->getFieldU64 (sfHighNode); TER terResult; WriteLog (lsTRACE, LedgerEntrySet) << "trustDelete: Deleting ripple line: low"; terResult = dirDelete (false, uLowNode, Ledger::getOwnerDirIndex (uLowAccountID), sleRippleState->getIndex (), false, !bLowNode); if (tesSUCCESS == terResult) { WriteLog (lsTRACE, LedgerEntrySet) << "trustDelete: Deleting ripple line: high"; terResult = dirDelete (false, uHighNode, Ledger::getOwnerDirIndex (uHighAccountID), sleRippleState->getIndex (), false, !bHighNode); } WriteLog (lsTRACE, LedgerEntrySet) << "trustDelete: Deleting ripple line: state"; entryDelete (sleRippleState); return terResult; } // Direct send w/o fees: // - Redeeming IOUs and/or sending sender's own IOUs. // - Create trust line of needed. // --> bCheckIssuer : normally require issuer to be involved. TER LedgerEntrySet::rippleCredit (const uint160& uSenderID, const uint160& uReceiverID, const STAmount& saAmount, bool bCheckIssuer) { uint160 uIssuerID = saAmount.getIssuer (); uint160 uCurrencyID = saAmount.getCurrency (); // Make sure issuer is involved. assert (!bCheckIssuer || uSenderID == uIssuerID || uReceiverID == uIssuerID); // Disallow sending to self. assert (uSenderID != uReceiverID); bool bSenderHigh = uSenderID > uReceiverID; uint256 uIndex = Ledger::getRippleStateIndex (uSenderID, uReceiverID, saAmount.getCurrency ()); SLE::pointer sleRippleState = entryCache (ltRIPPLE_STATE, uIndex); TER terResult; assert (!!uSenderID && uSenderID != ACCOUNT_ONE); assert (!!uReceiverID && uReceiverID != ACCOUNT_ONE); if (!sleRippleState) { STAmount saReceiverLimit = STAmount (uCurrencyID, uReceiverID); STAmount saBalance = saAmount; saBalance.setIssuer (ACCOUNT_ONE); WriteLog (lsDEBUG, LedgerEntrySet) << boost::str (boost::format ("rippleCredit: create line: %s --> %s : %s") % RippleAddress::createHumanAccountID (uSenderID) % RippleAddress::createHumanAccountID (uReceiverID) % saAmount.getFullText ()); terResult = trustCreate ( bSenderHigh, uSenderID, uReceiverID, uIndex, entryCache (ltACCOUNT_ROOT, Ledger::getAccountRootIndex (uReceiverID)), false, saBalance, saReceiverLimit); } else { STAmount saBalance = sleRippleState->getFieldAmount (sfBalance); if (bSenderHigh) saBalance.negate (); // Put balance in sender terms. STAmount saBefore = saBalance; saBalance -= saAmount; WriteLog (lsDEBUG, LedgerEntrySet) << boost::str (boost::format ("rippleCredit: %s --> %s : before=%s amount=%s after=%s") % RippleAddress::createHumanAccountID (uSenderID) % RippleAddress::createHumanAccountID (uReceiverID) % saBefore.getFullText () % saAmount.getFullText () % saBalance.getFullText ()); bool bDelete = false; uint32 uFlags; // YYY Could skip this if rippling in reverse. if (saBefore.isPositive () // Sender balance was positive. && !saBalance.isPositive () // Sender is zero or negative. && isSetBit ((uFlags = sleRippleState->getFieldU32 (sfFlags)), !bSenderHigh ? lsfLowReserve : lsfHighReserve) // Sender reserve is set. && !sleRippleState->getFieldAmount (!bSenderHigh ? sfLowLimit : sfHighLimit) // Sender trust limit is 0. && !sleRippleState->getFieldU32 (!bSenderHigh ? sfLowQualityIn : sfHighQualityIn) // Sender quality in is 0. && !sleRippleState->getFieldU32 (!bSenderHigh ? sfLowQualityOut : sfHighQualityOut)) // Sender quality out is 0. { // Clear the reserve of the sender, possibly delete the line! SLE::pointer sleSender = entryCache (ltACCOUNT_ROOT, Ledger::getAccountRootIndex (uSenderID)); ownerCountAdjust (uSenderID, -1, sleSender); sleRippleState->setFieldU32 (sfFlags, uFlags & (!bSenderHigh ? ~lsfLowReserve : ~lsfHighReserve)); // Clear reserve flag. bDelete = !saBalance // Balance is zero. && !isSetBit (uFlags, bSenderHigh ? lsfLowReserve : lsfHighReserve); // Receiver reserve is clear. } if (bSenderHigh) saBalance.negate (); // Want to reflect balance to zero even if we are deleting line. sleRippleState->setFieldAmount (sfBalance, saBalance); // ONLY: Adjust ripple balance. if (bDelete) { terResult = trustDelete (sleRippleState, bSenderHigh ? uReceiverID : uSenderID, !bSenderHigh ? uReceiverID : uSenderID); } else { entryModify (sleRippleState); terResult = tesSUCCESS; } } return terResult; } // Send regardless of limits. // --> saAmount: Amount/currency/issuer to deliver to reciever. // <-- saActual: Amount actually cost. Sender pay's fees. TER LedgerEntrySet::rippleSend (const uint160& uSenderID, const uint160& uReceiverID, const STAmount& saAmount, STAmount& saActual) { const uint160 uIssuerID = saAmount.getIssuer (); TER terResult; assert (!!uSenderID && !!uReceiverID); assert (uSenderID != uReceiverID); if (uSenderID == uIssuerID || uReceiverID == uIssuerID || uIssuerID == ACCOUNT_ONE) { // Direct send: redeeming IOUs and/or sending own IOUs. terResult = rippleCredit (uSenderID, uReceiverID, saAmount, false); saActual = saAmount; terResult = tesSUCCESS; } else { // Sending 3rd party IOUs: transit. STAmount saTransitFee = rippleTransferFee (uSenderID, uReceiverID, uIssuerID, saAmount); saActual = !saTransitFee ? saAmount : saAmount + saTransitFee; saActual.setIssuer (uIssuerID); // XXX Make sure this done in + above. WriteLog (lsDEBUG, LedgerEntrySet) << boost::str (boost::format ("rippleSend> %s -- > %s : deliver=%s fee=%s cost=%s") % RippleAddress::createHumanAccountID (uSenderID) % RippleAddress::createHumanAccountID (uReceiverID) % saAmount.getFullText () % saTransitFee.getFullText () % saActual.getFullText ()); terResult = rippleCredit (uIssuerID, uReceiverID, saAmount); if (tesSUCCESS == terResult) terResult = rippleCredit (uSenderID, uIssuerID, saActual); } return terResult; } TER LedgerEntrySet::accountSend (const uint160& uSenderID, const uint160& uReceiverID, const STAmount& saAmount) { TER terResult = tesSUCCESS; assert (!saAmount.isNegative ()); if (!saAmount || (uSenderID == uReceiverID)) { nothing (); } else if (saAmount.isNative ()) { // XRP send which does not check reserve and can do pure adjustment. SLE::pointer sleSender = !!uSenderID ? entryCache (ltACCOUNT_ROOT, Ledger::getAccountRootIndex (uSenderID)) : SLE::pointer (); SLE::pointer sleReceiver = !!uReceiverID ? entryCache (ltACCOUNT_ROOT, Ledger::getAccountRootIndex (uReceiverID)) : SLE::pointer (); WriteLog (lsDEBUG, LedgerEntrySet) << boost::str (boost::format ("accountSend> %s (%s) -> %s (%s) : %s") % RippleAddress::createHumanAccountID (uSenderID) % (sleSender ? (sleSender->getFieldAmount (sfBalance)).getFullText () : "-") % RippleAddress::createHumanAccountID (uReceiverID) % (sleReceiver ? (sleReceiver->getFieldAmount (sfBalance)).getFullText () : "-") % saAmount.getFullText ()); if (sleSender) { if (sleSender->getFieldAmount (sfBalance) < saAmount) { terResult = isSetBit (mParams, tapOPEN_LEDGER) ? telFAILED_PROCESSING : tecFAILED_PROCESSING; } else { sleSender->setFieldAmount (sfBalance, sleSender->getFieldAmount (sfBalance) - saAmount); // Decrement XRP balance. entryModify (sleSender); } } if (tesSUCCESS == terResult && sleReceiver) { sleReceiver->setFieldAmount (sfBalance, sleReceiver->getFieldAmount (sfBalance) + saAmount); // Increment XRP balance. entryModify (sleReceiver); } WriteLog (lsDEBUG, LedgerEntrySet) << boost::str (boost::format ("accountSend< %s (%s) -> %s (%s) : %s") % RippleAddress::createHumanAccountID (uSenderID) % (sleSender ? (sleSender->getFieldAmount (sfBalance)).getFullText () : "-") % RippleAddress::createHumanAccountID (uReceiverID) % (sleReceiver ? (sleReceiver->getFieldAmount (sfBalance)).getFullText () : "-") % saAmount.getFullText ()); } else { STAmount saActual; WriteLog (lsDEBUG, LedgerEntrySet) << boost::str (boost::format ("accountSend: %s -> %s : %s") % RippleAddress::createHumanAccountID (uSenderID) % RippleAddress::createHumanAccountID (uReceiverID) % saAmount.getFullText ()); terResult = rippleSend (uSenderID, uReceiverID, saAmount, saActual); } return terResult; } // vim:ts=4