clboss/Boss/Mod/ChannelFinderByDistance.cpp

424 lines
9.5 KiB
C++

#include"Boss/Mod/ChannelFinderByDistance.hpp"
#include"Boss/Mod/Rpc.hpp"
#include"Boss/Mod/Waiter.hpp"
#include"Boss/Msg/Init.hpp"
#include"Boss/Msg/PreinvestigateChannelCandidates.hpp"
#include"Boss/Msg/SolicitChannelCandidates.hpp"
#include"Boss/concurrent.hpp"
#include"Boss/log.hpp"
#include"Boss/random_engine.hpp"
#include"Ev/Io.hpp"
#include"Ev/now.hpp"
#include"Ev/yield.hpp"
#include"Graph/Dijkstra.hpp"
#include"Jsmn/Object.hpp"
#include"Json/Out.hpp"
#include"Ln/Amount.hpp"
#include"S/Bus.hpp"
#include"Stats/ReservoirSampler.hpp"
#include"Util/stringify.hpp"
#include<algorithm>
#include<math.h>
#include<memory>
#include<queue>
namespace {
/* Amount to simulate passing through channels to
* measure their cost.
*/
auto const reference_amount = Ln::Amount::sat(10000);
/* Conversion rate of delay to millisatoshi.
*/
auto const msat_per_block = double(1.0);
/* Maximum fee per hop.
*/
auto const max_fee = Ln::Amount::sat(50); /* 0.5% of reference_amount */
/* Maximum number to give to preinvestigation. */
auto const max_preinvestigate = std::size_t(40);
/* Maximum number to tell preinvestigation to pass. */
auto const max_candidates = std::size_t(3);
/* At initial startup, we can have channels, but all of them
* are inactive because not connected yet.
* If we have any channels and all are inactive, defer by
* these number of seconds.
*/
auto const initial_startup_delay = double(30.0);
}
namespace Boss { namespace Mod {
class ChannelFinderByDistance::Run : public std::enable_shared_from_this<Run> {
private:
S::Bus& bus;
Boss::Mod::Rpc& rpc;
Boss::Mod::Waiter& waiter;
Ln::NodeId self_id;
typedef
Graph::Dijkstra< Ln::NodeId
, Ln::Amount
> Dijkstra;
typedef Dijkstra::TreeNode TreeNode;
typedef Dijkstra::Result DijkstraResult;
Dijkstra djk;
/* Resulting navigation guide. */
DijkstraResult nav;
/* Progress reporting. */
double prev_time;
std::size_t progress_count;
/* Used to extact leaf nodes in the Dijkstra run. */
std::queue<TreeNode const*> extract_q;
/* Actual leaf nodes. */
std::vector<TreeNode const*> leaves;
Run( S::Bus& bus_
, Boss::Mod::Rpc& rpc_
, Boss::Mod::Waiter& waiter_
, Ln::NodeId const& self_id_
) : bus(bus_)
, rpc(rpc_)
, waiter(waiter_)
, self_id(self_id_)
, djk(self_id_, Ln::Amount::sat(0))
{ }
public:
Run() =delete;
Run(Run&&) =delete;
Run(Run const&) =delete;
static
std::shared_ptr<Run>
create( S::Bus& bus
, Boss::Mod::Rpc& rpc
, Boss::Mod::Waiter& waiter
, Ln::NodeId const& self_id
) {
return std::shared_ptr<Run>(
new Run(bus, rpc, waiter, self_id)
);
}
Ev::Io<void> run() {
auto self = shared_from_this();
return self->core_run().then([self]() {
return Ev::lift();
});
}
private:
Ev::Io<void> core_run() {
return Ev::lift().then([this]() {
/* Check first if local channels exist and are
* inactive.
*/
auto parms = Json::Out()
.start_object()
.field("source", std::string(self_id))
.end_object()
;
return rpc.command("listchannels", std::move(parms)
);
}).then([this](Jsmn::Object res) {
auto empty = true;
auto any_active = false;
try {
auto cs = res["channels"];
empty = cs.size() == 0;
for (auto c : cs) {
if (c["active"]) {
any_active = true;
break;
}
}
} catch (Jsmn::TypeError const&) {
return Boss::log( bus, Error
, "ChannelFinderByDistance: "
"Starting listchannels "
"gave unexpected result: "
"%s"
, Util::stringify(res)
.c_str()
);
}
if (empty)
return Boss::log( bus, Info
, "ChannelFinderByDistance: "
"We are not on network."
);
auto act = Ev::lift();
if (!any_active) {
act += Boss::log( bus, Info
, "ChannelFinderByDistance: "
"Local channels not yet "
"active, will wait %.0f "
"seconds."
, initial_startup_delay
);
act += waiter.wait(initial_startup_delay);
}
act += start_processing();
return act;
});
}
Ev::Io<void> start_processing() {
return Ev::lift().then([this]() {
prev_time = Ev::now();
progress_count = 0;
return Boss::log( bus, Debug
, "ChannelFinderByDistance: "
"Starting Dijkstra."
)
+ loop()
+ find_leaves()
+ analyze()
;
});
}
Ev::Io<void> loop() {
auto act = Ev::yield();
if (Ev::now() - prev_time >= 5.0) {
prev_time = Ev::now();
act += Boss::log( bus, Info
, "ChannelFinderByDistance: "
"Dijkstra progress: "
"%zu nodes scanned."
, progress_count
);
}
return std::move(act).then([this]() {
auto n = djk.current();
if (!n)
return Boss::log( bus, Debug
, "ChannelFinderByDistance: "
"Dijkstra complete."
);
return step(*n);
});
}
Ev::Io<void> step(Ln::NodeId const& n) {
++progress_count;
auto parms = Json::Out()
.start_object()
.field("source", std::string(n))
.end_object()
;
return rpc.command("listchannels", std::move(parms)
).then([this](Jsmn::Object res) {
try {
auto cs = res["channels"];
for (auto c : cs) {
if (!c["active"])
continue;
/* Neighbor. */
auto n = Ln::NodeId(std::string(
c["destination"]
));
/* (b)ase and (p)roportional fees. */
auto b = Ln::Amount::msat(double(
c["base_fee_millisatoshi"]
));
auto p = double(
c["fee_per_millionth"]
);
/* (d)elay in blocks. */
auto d = double(
c["delay"]
);
auto cost = b
+ ( reference_amount
* ( p
/ 1000000.0
))
+ Ln::Amount::msat(
msat_per_block * d
)
;
if (cost > max_fee)
continue;
djk.neighbor(n, cost);
}
} catch (Jsmn::TypeError const&) {
return Boss::log( bus, Error
, "ChannelFinderByDistance: "
"Unexpected listchannels "
"result: %s"
, Util::stringify(res)
.c_str()
);
}
djk.end_neighbors();
return loop();
});
}
Ev::Io<void> find_leaves() {
return Ev::lift().then([this]() {
prev_time = Ev::now();
progress_count = 0;
nav = std::move(djk).finalize();
auto n = nav[self_id].get();
extract_q.push(n);
return find_leaves_loop();
});
}
Ev::Io<void> find_leaves_loop() {
auto act = Ev::yield();
if (Ev::now() - prev_time >= 5.0) {
prev_time = Ev::now();
act += Boss::log( bus, Info
, "ChannelFinderByDistance: "
"Leaf progress: %zu nodes scanned."
, progress_count
);
}
return std::move(act).then([this]() {
if (extract_q.empty())
return Boss::log( bus, Debug
, "ChannelFinderByDistance: "
"Found %zu leaves."
, leaves.size()
);
auto n = extract_q.front();
extract_q.pop();
++progress_count;
/* Is it a leaf? */
if (n->children.empty())
leaves.push_back(n);
else
/* Push the children. */
for (auto c : n->children)
extract_q.push(c);
return find_leaves_loop();
});
}
Ev::Io<void> analyze() {
struct Entry {
Ln::NodeId proposal;
Ln::NodeId patron;
Ln::Amount cost;
};
return Ev::lift().then([this]() {
/* Use reservoir sampling, with the cost to
* reach the node as the weight, to select
* some number of leaves.
*/
auto rsv = Stats::ReservoirSampler<Entry>(
max_preinvestigate
);
for (auto n : leaves) {
/* Ignore self and direct channels of self. */
if (!n->parent)
continue;
if (*n->parent->data.first == self_id)
continue;
auto cost = n->data.second;
auto e = Entry{ *n->data.first
, *n->parent->data.first
, cost
};
rsv.add( std::move(e)
, cost.to_btc()
, Boss::random_engine
);
}
auto entries = std::move(rsv).finalize();
/* Sort by cost from highest to lowest. */
std::sort( entries.begin(), entries.end()
, [](Entry const& a, Entry const& b) {
return b.cost < a.cost;
});
/* Build a report. */
auto os = std::ostringstream();
auto first = true;
auto count = std::size_t(0);
for (auto const& e : entries) {
if (first) {
os << "Preinvestigating: ";
first = false;
} else
os << ", ";
if (count >= 8) {
os << " ... "
<< entries.size() - count
<< " more"
;
break;
}
++count;
os << e.proposal << "("
<< e.cost
<< ")"
;
}
if (first)
os << "No candidate leaves.";
auto act = Boss::log( bus, Info
, "ChannelFinderByDistance: "
"%s"
, os.str().c_str()
);
/* Create the message. */
auto cands = std::vector<Msg::ProposeChannelCandidates
>(entries.size());
std::transform( entries.begin(), entries.end()
, cands.begin()
, [](Entry const& e) {
return Msg::ProposeChannelCandidates{
std::move(e.proposal),
std::move(e.patron)
};
});
auto msg = Msg::PreinvestigateChannelCandidates{
std::move(cands), max_candidates
};
act += bus.raise(msg);
return act;
});
}
};
void ChannelFinderByDistance::start() {
bus.subscribe<Msg::Init
>([this](Msg::Init const& init) {
rpc = &init.rpc;
self_id = init.self_id;
return Ev::lift();
});
bus.subscribe<Msg::SolicitChannelCandidates
>([this](Msg::SolicitChannelCandidates const& _) {
if (!rpc)
return Ev::lift();
if (running)
return Ev::lift();
running = true;
auto run = Run::create(bus, *rpc, waiter, self_id);
return Boss::concurrent(run->run().then([this]() {
running = false;
return Ev::lift();
}));
});
}
}}