#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 #include #include #include 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 { 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 extract_q; /* Actual leaf nodes. */ std::vector 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 create( S::Bus& bus , Boss::Mod::Rpc& rpc , Boss::Mod::Waiter& waiter , Ln::NodeId const& self_id ) { return std::shared_ptr( new Run(bus, rpc, waiter, self_id) ); } Ev::Io run() { auto self = shared_from_this(); return self->core_run().then([self]() { return Ev::lift(); }); } private: Ev::Io 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 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 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 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 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 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 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( 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(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([this](Msg::Init const& init) { rpc = &init.rpc; self_id = init.self_id; return Ev::lift(); }); bus.subscribe([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(); })); }); } }}