#include"Boss/Mod/InitialRebalancer.hpp" #include"Boss/ModG/ReqResp.hpp" #include"Boss/Msg/ListpeersResult.hpp" #include"Boss/Msg/RequestMoveFunds.hpp" #include"Boss/Msg/ResponseMoveFunds.hpp" #include"Boss/concurrent.hpp" #include"Boss/log.hpp" #include"Boss/random_engine.hpp" #include"Ev/Io.hpp" #include"Ev/yield.hpp" #include"Jsmn/Object.hpp" #include"Json/Out.hpp" #include"Ln/Amount.hpp" #include"Ln/NodeId.hpp" #include"S/Bus.hpp" #include"Stats/ReservoirSampler.hpp" #include"Util/make_unique.hpp" #include"Util/stringify.hpp" #include #include #include namespace { /* If the spendable amount exceeds this percent of the channel total, * this code triggers. */ auto constexpr spendable_percent = double(80.0); /* Limit on rebalance fee. */ auto const min_rebalance_fee = Ln::Amount::sat(5); auto constexpr rebalance_fee_percent = double(0.5); } namespace Boss { namespace Mod { class InitialRebalancer::Impl { private: S::Bus& bus; /* Interface to funds mover. */ typedef ModG::ReqResp< Msg::RequestMoveFunds , Msg::ResponseMoveFunds > MoveRR; MoveRR move_rr; void start() { bus.subscribe([this](Msg::ListpeersResult const& m) { /* If this is the initial startup, then we might not * be connected to the peers involved yet, so better * to wait and let it "simmer" a bit. */ if (m.initial) return Ev::lift(); return run(m.peers); }); } class Run { private: class Impl; std::shared_ptr pimpl; public: Run() =delete; Run(Run&&) =default; ~Run() =default; explicit Run(S::Bus& bus, Jsmn::Object const& peers, MoveRR& move_rr); Ev::Io run(); }; Ev::Io run(Jsmn::Object const& peers) { return Boss::concurrent(Run(bus, peers, move_rr).run()); } public: Impl() =delete; Impl(Impl&&) =delete; Impl(Impl const&) =delete; explicit Impl( S::Bus& bus_ ) : bus(bus_) , move_rr( bus_ , [](Msg::RequestMoveFunds& msg, void* p) { msg.requester = p; } , [](Msg::ResponseMoveFunds& msg) { return msg.requester; } ) { start(); } }; class InitialRebalancer::Impl::Run::Impl { private: S::Bus& bus; Jsmn::Object peers; /* Data about a peer. */ struct Info { Ln::Amount spendable; Ln::Amount receivable; Ln::Amount total; }; std::map info; /* Plan to move. */ std::vector> plan; /* Interface to funds mover. */ ModG::ReqResp< Msg::RequestMoveFunds , Msg::ResponseMoveFunds >& move_rr; Ev::Io core_run() { return Ev::lift().then([this]() { try { for (auto p : peers) { auto spendable = Ln::Amount::sat(0); auto receivable = Ln::Amount::sat(0); auto total = Ln::Amount::sat(0); auto id = Ln::NodeId(std::string( p["id"] )); auto cs = p["channels"]; for (auto c : cs) { auto priv = bool( c["private"] ); if (priv) continue; auto state = std::string( c["state"] ); if (state != "CHANNELD_NORMAL") continue; compute_spendable( spendable , receivable , total , c ); } if (total == Ln::Amount::sat(0)) continue; info[id].spendable = spendable; info[id].receivable = receivable; info[id].total = total; } } catch (std::exception const& _) { return Boss::log( bus, Error , "InitialRebalancer: " "Unexpected result from " "listpeers: %s" , Util::stringify(peers) .c_str() ); } return plan_move(); }); } void compute_spendable( Ln::Amount& a_spendable , Ln::Amount& a_receivable , Ln::Amount& a_total , Jsmn::Object const& c ) { if ( !c.has("to_us_msat") || !c.has("total_msat") || !c.has("htlcs") ) return; /* FIXME: Handle reserves. */ auto to_us = Ln::Amount(std::string(c["to_us_msat"])); auto total = Ln::Amount(std::string(c["total_msat"])); auto to_them = total - to_us; for (auto h : c["htlcs"]) to_them -= Ln::Amount(std::string( h["amount_msat"] )); a_spendable += to_us; a_receivable += to_them; a_total += total; } Ev::Io plan_move() { auto msg = std::ostringstream(); auto first = true; /* Gather sources and destinations. */ auto sources = std::vector(); auto destinations = std::map(); for ( auto it = info.begin(), next = info.begin() ; it != info.end() ; it = next ) { next = it; ++next; if (first) first = false; else msg << ", "; auto& info = it->second; auto peer_spendable_percent = ( info.spendable / info.total ) * 100.0 ; msg << it->first << ": " << peer_spendable_percent << "% " ; if (peer_spendable_percent >= spendable_percent) { sources.push_back(it->first); msg << "(source)"; } else { destinations.insert(*it); msg << "(destination)"; } } auto act = Ev::lift(); if (!first) act += Boss::log( bus, Debug , "InitialRebalancer: %s" , msg.str().c_str() ); /* Nothing to do. */ if (sources.empty()) return act; for (auto& s : sources) { if (destinations.empty()) break; auto sampler = Stats::ReservoirSampler(1); for (auto& d : destinations) { auto& info = d.second; sampler.add( d.first , info.receivable / info.total , Boss::random_engine ); } auto dest = std::move(sampler).finalize()[0]; plan.push_back(std::make_pair(s, dest)); } return std::move(act) + execute_plan(); } Ev::Io execute_plan() { auto act = Ev::lift(); for (auto& p : plan) { auto source = p.first; auto destination = p.second; auto s_info = info[source]; auto d_info = info[destination]; auto max_send = s_info.spendable / 2.0; auto max_dest = d_info.total * ( spendable_percent / 100.0 ); auto max_receive = max_dest - d_info.spendable; auto amount = max_send; if (amount > max_receive) amount = max_receive; act += Boss::log( bus, Debug , "InitialRebalancer: %s --> %s --> %s" , std::string(source).c_str() , std::string(amount).c_str() , std::string(destination).c_str() ); act += Boss::concurrent(move_funds( source , destination , amount )); } return act; } Ev::Io move_funds( Ln::NodeId const& source , Ln::NodeId const& destination , Ln::Amount amount ) { auto this_rebalance_fee = amount * (rebalance_fee_percent / 100.0) ; if (this_rebalance_fee < min_rebalance_fee) this_rebalance_fee = min_rebalance_fee; return move_rr.execute(Msg::RequestMoveFunds{ nullptr, source, destination, amount, this_rebalance_fee }).then([](Msg::ResponseMoveFunds _) { return Ev::lift(); }); } public: Impl( S::Bus& bus_ , Jsmn::Object const& peers_ , MoveRR& move_rr_ ) : bus(bus_), peers(peers_) , move_rr(move_rr_) { } static Ev::Io run(std::shared_ptr self) { return self->core_run().then([self]() { return Ev::lift(); }); } }; InitialRebalancer::Impl::Run::Run( S::Bus& bus , Jsmn::Object const& peers , MoveRR& move_rr ) : pimpl(std::make_shared(bus, peers, move_rr)) { } Ev::Io InitialRebalancer::Impl::Run::run() { return Impl::run(pimpl); } InitialRebalancer::InitialRebalancer(InitialRebalancer&&) =default; InitialRebalancer::~InitialRebalancer() =default; InitialRebalancer::InitialRebalancer(S::Bus& bus) : pimpl(Util::make_unique(bus)) { } }}