diff --git a/Boss/Mod/ChannelCreator/Manager.cpp b/Boss/Mod/ChannelCreator/Manager.cpp index 015a7ac..665e6c3 100644 --- a/Boss/Mod/ChannelCreator/Manager.cpp +++ b/Boss/Mod/ChannelCreator/Manager.cpp @@ -22,6 +22,7 @@ #include"Util/make_unique.hpp" #include #include +#include #include namespace { @@ -161,6 +162,19 @@ Manager::on_request_channel_creation(Ln::Amount amt) { * nodes with similar locations. */ return reprioritize(std::move(proposals)); + }).then([this](std::vector> proposals) { + /* Finally, partition by earnings track record, so + * proven earners are funded first and known + * underperformers only when nothing else can absorb + * the funds. This runs after the rearranger and + * reprioritizer on purpose: those two only perturb + * the order, and must not promote a candidate across + * a track-record tier boundary. The Planner consumes + * proposals in order until funds run out, so placing + * a tier last implements "only if there are no + * others" without an outright veto. + */ + return prioritize_by_track_record(std::move(proposals)); }).then([ num_chans , amt , dowser_func @@ -307,5 +321,103 @@ Manager::reprioritize(std::vector> proposals_v return Ev::lift(std::move(*proposals)); }); } +Ev::Io>> +Manager::prioritize_by_track_record(std::vector> proposals_v) { + typedef std::vector> Proposals; + + if (proposals_v.empty()) + return Ev::lift(std::move(proposals_v)); + + auto nodes = std::vector(); + for (auto const& p : proposals_v) + nodes.push_back(p.first); + auto proposals = std::make_shared(std::move(proposals_v)); + + return track_record.execute(Msg::RequestPeerTrackRecord{ + nullptr, std::move(nodes) + }).then([ this + , proposals + ](Msg::ResponsePeerTrackRecord resp) { + auto keepers = Proposals(); + auto no_records = Proposals(); + auto underperformers = Proposals(); + + /* Per-tier report text; nodes within a tier keep their + * relative order from the earlier stages. */ + auto keepers_s = std::string(); + auto no_records_s = std::string(); + auto underperformers_s = std::string(); + auto append = []( std::string& s + , std::string const& entry + ) { + if (!s.empty()) + s += ", "; + s += entry; + }; + + for (auto const& p : *proposals) { + auto rec = Msg::TrackRecord{ + Msg::TrackRecordVerdict::NoRecord, + 0.0, 0.0, 0 + }; + auto it = resp.records.find(p.first); + if (it != resp.records.end()) + rec = it->second; + + auto os = std::ostringstream(); + os << p.first; + if (rec.verdict != Msg::TrackRecordVerdict::NoRecord) + os << "(" << std::showpos + << (long long) std::llround(rec.tral_bps) + << std::noshowpos << "bps/" + << (long long) std::llround(rec.op_days) + << "d)" + ; + + switch (rec.verdict) { + case Msg::TrackRecordVerdict::Keeper: + keepers.push_back(p); + append(keepers_s, os.str()); + break; + case Msg::TrackRecordVerdict::NoRecord: + no_records.push_back(p); + append(no_records_s, os.str()); + break; + case Msg::TrackRecordVerdict::Underperformer: + underperformers.push_back(p); + append(underperformers_s, os.str()); + break; + } + } + + auto report = std::string(); + if (!keepers_s.empty()) + report += "keepers: " + keepers_s + "; "; + if (!no_records_s.empty()) + report += "no record: " + no_records_s + "; "; + if (!underperformers_s.empty()) + report += "underperformers: " + + underperformers_s + "; " + ; + /* Trim the trailing "; ". */ + report.erase(report.size() - 2); + + *proposals = std::move(keepers); + proposals->insert( proposals->end() + , no_records.begin(), no_records.end() + ); + proposals->insert( proposals->end() + , underperformers.begin() + , underperformers.end() + ); + + return Boss::log( bus, Info + , "ChannelCreator: Track records: %s" + , report.c_str() + ).then([proposals]() { + return Ev::lift(std::move(*proposals)); + }); + }); +} }}} diff --git a/Boss/Mod/ChannelCreator/Manager.hpp b/Boss/Mod/ChannelCreator/Manager.hpp index a443867..bf51bc0 100644 --- a/Boss/Mod/ChannelCreator/Manager.hpp +++ b/Boss/Mod/ChannelCreator/Manager.hpp @@ -4,7 +4,9 @@ #include"Boss/Mod/ChannelCreator/Reprioritizer.hpp" #include"Boss/ModG/ReqResp.hpp" #include"Boss/Msg/RequestDowser.hpp" +#include"Boss/Msg/RequestPeerTrackRecord.hpp" #include"Boss/Msg/ResponseDowser.hpp" +#include"Boss/Msg/ResponsePeerTrackRecord.hpp" #include"Ln/NodeId.hpp" #include #include @@ -40,6 +42,9 @@ private: Ln::NodeId self; ModG::ReqResp dowser; + ModG::ReqResp< Msg::RequestPeerTrackRecord + , Msg::ResponsePeerTrackRecord + > track_record; std::unique_ptr reprioritizer; @@ -56,6 +61,9 @@ private: /* Perform reprioritization and log it. */ Ev::Io>> reprioritize(std::vector>); + /* Partition proposals by earnings track record and log it. */ + Ev::Io>> + prioritize_by_track_record(std::vector>); public: Manager() =delete; @@ -72,6 +80,7 @@ public: , carpenter(carpenter_) , self() , dowser(bus) + , track_record(bus) { start(); } diff --git a/contrib/clboss-forwarding-stats b/contrib/clboss-forwarding-stats index 3446154..9c69f19 100755 --- a/contrib/clboss-forwarding-stats +++ b/contrib/clboss-forwarding-stats @@ -2,7 +2,7 @@ # This script produces a summary of channel forwarding stats # -# - Displays PeerID, SCID, and Alias for each channel +# - Displays SCID and Alias (or the full PeerID with --ids) for each channel # - Uses `clboss-recent-earnings` to limit the history considered # # The channels at the top of the list are good, the ones at the bottom are bad. @@ -222,6 +222,11 @@ def main(): "--sort", choices=["fral", "tral"], default="fral", help="Column to sort by (default: fral).", ) + parser.add_argument( + "--ids", action="store_true", + help="Show full peer node ids instead of aliases " + "(paste-ready for commands that take a nodeid).", + ) args = parser.parse_args() @@ -389,7 +394,10 @@ def main(): tralstr = f"{tral:.3f}" if tral is not None else "--" fwded = peer.get("in_forwarded", 0) + peer.get("out_forwarded", 0) netearnings = peer_net_earnings(peer) - alias = pad_string(peer["alias"], max_alias_length) + if args.ids: + alias = peer_id + else: + alias = pad_string(peer["alias"], max_alias_length) opener = "L" if channels[short_channel_id]["opener"] == "local" else "R" agestr = str(int(peer_age_days(peer))) opdaysstr = f"{op_days:.1f}" @@ -414,9 +422,10 @@ def main(): ) # Print the table without grid + id_col = "PeerID" if args.ids else "Alias" table_str = tabulate( table_data, - headers=["Alias", "SCID", "O", "curr_to_us", "avg_to_us", "Age", "OpDays", "Forwarded", "NetEarn", "FRAL**", "TRAL*"], + headers=[id_col, "SCID", "O", "curr_to_us", "avg_to_us", "Age", "OpDays", "Forwarded", "NetEarn", "FRAL**", "TRAL*"], tablefmt="plain", stralign="left", numalign="right",