Merge branch 'master' of github.com:jedmccaleb/NewCoin

This commit is contained in:
JoelKatz
2012-12-11 15:48:17 -08:00
5 changed files with 297 additions and 72 deletions

View File

@@ -75,9 +75,13 @@ PathOption::PathOption(PathOption::pointer other)
}
#endif
static bool bQualityCmp(std::pair<uint32, unsigned int> a, std::pair<uint32, unsigned int> b)
{
return a.first < b.first;
}
// Return true, if path is a default path with an element.
// A path is a default path if it is implied via src, dst, send, and sendmax.
// XXX Could be determined via STAmount
bool Pathfinder::bDefaultPath(const STPath& spPath)
{
if (2 == spPath.mPath.size()) {
@@ -87,6 +91,15 @@ bool Pathfinder::bDefaultPath(const STPath& spPath)
return true;
}
if (!mPsDefault)
{
// No default path.
// There might not be a direct credit line or there may be no implied nodes
// in send and sendmax.
return false; // Didn't generate a default path. So can't match.
}
PathState::pointer pspCurrent = boost::make_shared<PathState>(mDstAmount, mSrcAmount, mLedger);
if (pspCurrent)
@@ -98,6 +111,8 @@ bool Pathfinder::bDefaultPath(const STPath& spPath)
bDefault = pspCurrent->vpnNodes == mPsDefault->vpnNodes;
cLog(lsDEBUG) << "findPaths: expanded path: " << pspCurrent->getJson();
// Path is a default (implied). Don't need to add it to return set.
cLog(lsDEBUG) << "findPaths: default path: indirect: " << spPath.getJson(0);
@@ -119,28 +134,38 @@ Pathfinder::Pathfinder(const RippleAddress& uSrcAccountID, const RippleAddress&
// Construct the default path for later comparison.
mPsDefault = boost::make_shared<PathState>(mDstAmount, mSrcAmount, mLedger);
PathState::pointer psDefault = boost::make_shared<PathState>(mDstAmount, mSrcAmount, mLedger);
if (mPsDefault)
if (psDefault)
{
LedgerEntrySet lesActive(mLedger);
mPsDefault->setExpanded(lesActive, STPath(), mDstAccountID, mSrcAccountID);
psDefault->setExpanded(lesActive, STPath(), mDstAccountID, mSrcAccountID);
if (tesSUCCESS == psDefault->terStatus)
{
cLog(lsDEBUG) << "Pathfinder: reference path: " << psDefault->getJson();
mPsDefault = psDefault;
}
else
{
cLog(lsDEBUG) << "Pathfinder: reference path: NONE: " << transToken(psDefault->terStatus);
}
}
}
// If possible, returns a single path.
// --> maxSearchSteps: unused XXX
// --> maxPay: unused XXX
// --> iMaxSteps: Maximum nodes in paths to return.
// --> iMaxPaths: Maximum number of paths to return.
// <-- retPathSet: founds paths not including default paths.
// Returns true if found paths.
//
// When generating a path set blindly, don't allow the empty path, it is implied by default.
// When generating a path set for estimates, allow an empty path instead of no paths to indicate a path exists. The caller will
// need to strip the empty path when submitting the transaction.
bool Pathfinder::findPaths(int maxSearchSteps, int maxPay, STPathSet& retPathSet)
bool Pathfinder::findPaths(const unsigned int iMaxSteps, const unsigned int iMaxPaths, STPathSet& spsDst)
{
bool bFound = false;
bool bFound = false; // True, iff found a path.
cLog(lsDEBUG) << boost::str(boost::format("findPaths> mSrcAccountID=%s mDstAccountID=%s mDstAmount=%s mSrcCurrencyID=%s mSrcIssuerID=%s")
% RippleAddress::createHumanAccountID(mSrcAccountID)
@@ -150,80 +175,87 @@ bool Pathfinder::findPaths(int maxSearchSteps, int maxPay, STPathSet& retPathSet
% RippleAddress::createHumanAccountID(mSrcIssuerID)
);
if (!mPsDefault)
if (mLedger)
{
cLog(lsDEBUG) << boost::str(boost::format("findPaths: failed to generate default path."));
std::vector<STPath> vspResults;
std::queue<STPath> qspExplore;
return false;
}
else if (mLedger)
{
std::queue<STPath> pqueue;
STPathElement ele(mSrcAccountID,
mSrcCurrencyID,
uint160()); // XXX Might add source issuer.
STPathElement speEnd(mSrcAccountID,
mSrcCurrencyID,
uint160()); // XXX Might add source issuer.
STPath path;
path.addElement(ele); // Add the source.
path.addElement(speEnd); // Add the source.
pqueue.push(path);
qspExplore.push(path);
while (pqueue.size()) {
STPath path = pqueue.front();
while (qspExplore.size()) {
STPath spPath = qspExplore.front();
pqueue.pop(); // Pop the first path from the queue.
qspExplore.pop(); // Pop the first path from the queue.
ele = path.mPath.back(); // Get the last node from the path.
speEnd = spPath.mPath.back(); // Get the last node from the path.
// Done, if dest wants XRP and last element produces XRP.
if (!ele.mCurrencyID // Tail output is XRP.
if (!speEnd.mCurrencyID // Tail output is XRP.
&& !mDstAmount.getCurrency()) { // Which is dst currency.
// Remove implied first.
path.mPath.erase(path.mPath.begin());
spPath.mPath.erase(spPath.mPath.begin());
if (path.size())
if (spPath.size())
{
// There is an actual path element.
retPathSet.addPath(path); // Return the path.
vspResults.push_back(spPath); // Potential result.
cLog(lsDEBUG) << "findPaths: adding: " << path.getJson(0);
cLog(lsDEBUG) << "findPaths: adding: " << spPath.getJson(0);
}
else
{
cLog(lsDEBUG) << "findPaths: empty path: XRP->XRP";
}
return true;
continue;
}
// Done, if dest wants non-XRP and last element is dest.
// YYY Allows going through self. Is this wanted?
if (ele.mAccountID == mDstAccountID // Tail is destination account.
&& ele.mCurrencyID == mDstAmount.getCurrency()) { // With correct output currency.
if (speEnd.mAccountID == mDstAccountID // Tail is destination account.
&& speEnd.mCurrencyID == mDstAmount.getCurrency()) { // With correct output currency.
// Found a path to the destination.
if (bDefaultPath(path)) {
cLog(lsDEBUG) << "findPaths: default path: dropping: " << path.getJson(0);
return true;
if (bDefaultPath(spPath)) {
cLog(lsDEBUG) << "findPaths: default path: dropping: " << spPath.getJson(0);
bFound = true;
}
else
{
// Remove implied first and last nodes.
spPath.mPath.erase(spPath.mPath.begin());
spPath.mPath.erase(spPath.mPath.begin() + spPath.mPath.size()-1);
vspResults.push_back(spPath); // Potential result.
cLog(lsDEBUG) << "findPaths: adding: " << spPath.getJson(0);
}
// Remove implied first and last nodes.
path.mPath.erase(path.mPath.begin());
path.mPath.erase(path.mPath.begin() + path.mPath.size()-1);
// Return the path.
retPathSet.addPath(path);
cLog(lsDEBUG) << "findPaths: adding: " << path.getJson(0);
return true;
continue;
}
bool bContinued = false;
if (!ele.mCurrencyID) {
if (spPath.mPath.size() == iMaxSteps)
{
// Path is at maximum size. Don't want to add more.
cLog(lsDEBUG)
<< boost::str(boost::format("findPaths: path would exceed max steps: dropping"));
}
else if (!speEnd.mCurrencyID)
{
// Last element is for XRP continue with qualifying books.
BOOST_FOREACH(OrderBook::pointer book, mOrderBook.getXRPInBooks())
{
@@ -231,7 +263,7 @@ bool Pathfinder::findPaths(int maxSearchSteps, int maxPay, STPathSet& retPathSet
//if (!path.hasSeen(line->getAccountIDPeer().getAccountID()))
{
STPath new_path(path);
STPath new_path(spPath);
STPathElement new_ele(uint160(), book->getCurrencyOut(), book->getIssuerOut());
new_path.mPath.push_back(new_ele);
@@ -243,7 +275,7 @@ bool Pathfinder::findPaths(int maxSearchSteps, int maxPay, STPathSet& retPathSet
% STAmount::createHumanCurrency(new_path.mCurrencyID)
% RippleAddress::createHumanAccountID(new_path.mCurrentAccount));
pqueue.push(new_path);
qspExplore.push(new_path);
bContinued = true;
}
@@ -251,33 +283,34 @@ bool Pathfinder::findPaths(int maxSearchSteps, int maxPay, STPathSet& retPathSet
tLog(!bContinued, lsDEBUG)
<< boost::str(boost::format("findPaths: XRP input - dead end"));
} else {
}
else
{
// Last element is for non-XRP continue by adding ripple lines and order books.
// Create new paths for each outbound account not already in the path.
AccountItems rippleLines(ele.mAccountID, mLedger, AccountItem::pointer(new RippleState()));
AccountItems rippleLines(speEnd.mAccountID, mLedger, AccountItem::pointer(new RippleState()));
BOOST_FOREACH(AccountItem::pointer item, rippleLines.getItems())
{
RippleState* line=(RippleState*)item.get();
if (!path.hasSeen(line->getAccountIDPeer().getAccountID()))
if (!spPath.hasSeen(line->getAccountIDPeer().getAccountID()))
{
STPath new_path(path);
STPath new_path(spPath);
STPathElement new_ele(line->getAccountIDPeer().getAccountID(),
ele.mCurrencyID,
speEnd.mCurrencyID,
uint160());
cLog(lsDEBUG) <<
boost::str(boost::format("findPaths: %s/%s --> %s/%s")
% RippleAddress::createHumanAccountID(ele.mAccountID)
% STAmount::createHumanCurrency(ele.mCurrencyID)
% RippleAddress::createHumanAccountID(speEnd.mAccountID)
% STAmount::createHumanCurrency(speEnd.mCurrencyID)
% RippleAddress::createHumanAccountID(line->getAccountIDPeer().getAccountID())
% STAmount::createHumanCurrency(ele.mCurrencyID));
% STAmount::createHumanCurrency(speEnd.mCurrencyID));
new_path.mPath.push_back(new_ele);
pqueue.push(new_path);
qspExplore.push(new_path);
bContinued = true;
}
@@ -285,27 +318,27 @@ bool Pathfinder::findPaths(int maxSearchSteps, int maxPay, STPathSet& retPathSet
{
cLog(lsDEBUG) <<
boost::str(boost::format("findPaths: SEEN: %s/%s --> %s/%s")
% RippleAddress::createHumanAccountID(ele.mAccountID)
% STAmount::createHumanCurrency(ele.mCurrencyID)
% RippleAddress::createHumanAccountID(speEnd.mAccountID)
% STAmount::createHumanCurrency(speEnd.mCurrencyID)
% RippleAddress::createHumanAccountID(line->getAccountIDPeer().getAccountID())
% STAmount::createHumanCurrency(ele.mCurrencyID));
% STAmount::createHumanCurrency(speEnd.mCurrencyID));
}
}
// Every book that wants the source currency.
std::vector<OrderBook::pointer> books;
mOrderBook.getBooks(path.mCurrentAccount, path.mCurrencyID, books);
mOrderBook.getBooks(spPath.mCurrentAccount, spPath.mCurrencyID, books);
BOOST_FOREACH(OrderBook::pointer book,books)
{
STPath new_path(path);
STPath new_path(spPath);
STPathElement new_ele(uint160(), book->getCurrencyOut(), book->getIssuerOut());
cLog(lsDEBUG) <<
boost::str(boost::format("findPaths: %s/%s :: %s/%s")
% STAmount::createHumanCurrency(ele.mCurrencyID)
% RippleAddress::createHumanAccountID(ele.mAccountID)
% STAmount::createHumanCurrency(speEnd.mCurrencyID)
% RippleAddress::createHumanAccountID(speEnd.mAccountID)
% STAmount::createHumanCurrency(book->getCurrencyOut())
% RippleAddress::createHumanAccountID(book->getIssuerOut()));
@@ -313,14 +346,42 @@ bool Pathfinder::findPaths(int maxSearchSteps, int maxPay, STPathSet& retPathSet
new_path.mCurrentAccount=book->getIssuerOut();
new_path.mCurrencyID=book->getCurrencyOut();
pqueue.push(new_path);
qspExplore.push(new_path);
bContinued = true;
}
tLog(!bContinued, lsDEBUG)
<< boost::str(boost::format("findPaths: non-XRP input - dead end"));
}
}
unsigned int iLimit = std::min(iMaxPaths, (unsigned int) vspResults.size());
if (iLimit)
{
std::vector< std::pair<uint32, unsigned int> > vMap;
// Build map of quality to entry.
for (int i = vspResults.size(); i--;)
{
uint32 uQuality = 1;
if (uQuality)
vMap.push_back(std::make_pair(uQuality, i));
}
tLog(!bContinued, lsDEBUG)
<< boost::str(boost::format("findPaths: non-XRP input - dead end"));
std::sort(vMap.begin(), vMap.end(), bQualityCmp);
// Output best quality entries.
for (int i = 0; i != iLimit; ++i)
{
spsDst.addPath(vspResults[vMap[i].second]);
}
bFound = true;
cLog(lsWARNING) << boost::str(boost::format("findPaths: RESULTS: %s") % spsDst.getJson(0));
}
}
else

View File

@@ -58,7 +58,7 @@ class Pathfinder
public:
Pathfinder(const RippleAddress& srcAccountID, const RippleAddress& dstAccountID, const uint160& srcCurrencyID, const uint160& srcIssuerID, const STAmount& dstAmount);
bool findPaths(int maxSearchSteps, int maxPay, STPathSet& retPathSet);
bool findPaths(const unsigned int iMaxSteps, const unsigned int iMaxPaths, STPathSet& spsDst);
bool bDefaultPath(const STPath& spPath);
};

View File

@@ -765,7 +765,7 @@ Json::Value RPCHandler::doRipplePathFind(Json::Value jvRequest)
STPathSet spsComputed;
Pathfinder pf(raSrc, raDst, uSrcCurrencyID, uSrcIssuerID, saDstAmount);
if (!pf.findPaths(5, 1, spsComputed))
if (!pf.findPaths(5, 3, spsComputed))
{
cLog(lsDEBUG) << "ripple_path_find: No paths found.";
}
@@ -958,7 +958,7 @@ Json::Value RPCHandler::doSubmit(Json::Value jvRequest)
Pathfinder pf(raSrcAddressID, dstAccountID, saSendMax.getCurrency(), saSendMax.getIssuer(), saSend);
if (!pf.findPaths(5, 1, spsPaths))
if (!pf.findPaths(5, 3, spsPaths))
{
cLog(lsDEBUG) << "payment: build_path: No paths found.";

View File

@@ -62,6 +62,7 @@ bool PathState::lessPriority(PathState& lhs, PathState& rhs)
// - 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-XRP.
TER PathState::pushImply(
const uint160& uAccountID, // --> Delivering to this account.
const uint160& uCurrencyID, // --> Delivering this currency.
@@ -186,6 +187,8 @@ TER PathState::pushNode(
else
{
// Add required intermediate nodes to deliver to current account.
cLog(lsDEBUG) << "pushNode: imply for account.";
terResult = pushImply(
pnCur.uAccountID, // Current account.
pnCur.uCurrencyID, // Wanted currency.
@@ -262,6 +265,7 @@ TER PathState::pushNode(
else if (!!pnPrv.uAccountID)
{
// Previous is an account.
cLog(lsDEBUG) << "pushNode: imply for offer.";
// Insert intermediary issuer account if needed.
terResult = pushImply(
@@ -364,7 +368,10 @@ cLog(lsDEBUG) << boost::str(boost::format("PathState: sender implied: account=%s
BOOST_FOREACH(const STPathElement& speElement, spSourcePath)
{
if (tesSUCCESS == terStatus)
{
cLog(lsDEBUG) << boost::str(boost::format("PathState: element in path:"));
terStatus = pushNode(speElement.getNodeType(), speElement.getAccountID(), speElement.getCurrency(), speElement.getIssuerID());
}
}
const PaymentNode& pnPrv = vpnNodes.back();

View File

@@ -13,9 +13,161 @@ require("../src/js/remote.js").config = require("./config.js");
buster.testRunner.timeout = 5000;
buster.testCase("Path finding", {
// 'setUp' : testutils.build_setup({ verbose: true, no_server: true }),
// 'setUp' : testutils.build_setup({ verbose: true }),
'setUp' : testutils.build_setup(),
'tearDown' : testutils.build_teardown(),
"no direct path, no intermediary -> no alternatives" :
function (done) {
var self = this;
async.waterfall([
function (callback) {
self.what = "Create accounts.";
testutils.create_accounts(self.remote, "root", "10000", ["alice", "bob"], callback);
},
function (callback) {
self.what = "Find path from alice to bob";
self.remote.request_ripple_path_find("alice", "bob", "5/USD/bob",
[ { 'currency' : "USD" } ])
.on('success', function (m) {
// console.log("proposed: %s", JSON.stringify(m));
callback(m.alternatives.length);
})
.request();
},
], function (error) {
buster.refute(error, self.what);
done();
});
},
"direct path, no intermediary" :
function (done) {
var self = this;
async.waterfall([
function (callback) {
self.what = "Create accounts.";
testutils.create_accounts(self.remote, "root", "10000", ["alice", "bob"], callback);
},
function (callback) {
self.what = "Set credit limits.";
testutils.credit_limits(self.remote,
{
"bob" : "700/USD/alice",
},
callback);
},
// function (callback) {
// self.what = "Display ledger";
//
// self.remote.request_ledger('current', true)
// .on('success', function (m) {
// console.log("Ledger: %s", JSON.stringify(m, undefined, 2));
//
// callback();
// })
// .request();
// },
// function (callback) {
// self.what = "Display available lines from alice";
//
// self.remote.request_account_lines("alice", undefined, 'CURRENT')
// .on('success', function (m) {
// console.log("LINES: %s", JSON.stringify(m, undefined, 2));
//
// callback();
// })
// .request();
// },
function (callback) {
self.what = "Find path from alice to bob";
self.remote.request_ripple_path_find("alice", "bob", "5/USD/bob",
[ { 'currency' : "USD" } ])
.on('success', function (m) {
// console.log("proposed: %s", JSON.stringify(m));
// 1 alternative.
buster.assert.equals(1, m.alternatives.length)
// Path is empty.
buster.assert.equals(0, m.alternatives[0].paths_canonical.length)
callback();
})
.request();
},
], function (error) {
buster.refute(error, self.what);
done();
});
},
"payment auto path find (using build_path)" :
function (done) {
var self = this;
async.waterfall([
function (callback) {
self.what = "Create accounts.";
testutils.create_accounts(self.remote, "root", "10000", ["alice", "bob", "mtgox"], callback);
},
function (callback) {
self.what = "Set credit limits.";
testutils.credit_limits(self.remote,
{
"alice" : "600/USD/mtgox",
"bob" : "700/USD/mtgox",
},
callback);
},
function (callback) {
self.what = "Distribute funds.";
testutils.payments(self.remote,
{
"mtgox" : [ "70/USD/alice" ],
},
callback);
},
function (callback) {
self.what = "Payment with auto path";
self.remote.transaction()
.payment('alice', 'bob', "24/USD/bob")
.build_path(true)
.once('proposed', function (m) {
// console.log("proposed: %s", JSON.stringify(m));
callback(m.result !== 'tesSUCCESS');
})
.submit();
},
function (callback) {
self.what = "Verify balances.";
testutils.verify_balances(self.remote,
{
"alice" : "46/USD/mtgox",
"mtgox" : [ "-46/USD/alice", "-24/USD/bob" ],
"bob" : "24/USD/mtgox",
},
callback);
},
], function (error) {
buster.refute(error, self.what);
done();
});
},
"path find" :
function (done) {
var self = this;
@@ -51,7 +203,12 @@ buster.testCase("Path finding", {
self.remote.request_ripple_path_find("alice", "bob", "5/USD/mtgox",
[ { 'currency' : "USD" } ])
.on('success', function (m) {
console.log("proposed: m", JSON.stringify(m));
// console.log("proposed: %s", JSON.stringify(m));
// 1 alternative.
buster.assert.equals(1, m.alternatives.length)
// Path is empty.
buster.assert.equals(0, m.alternatives[0].paths_canonical.length)
callback();
})