mirror of
https://github.com/ZmnSCPxj/clboss.git
synced 2026-08-14 12:43:19 +02:00
220 lines
5.6 KiB
C++
220 lines
5.6 KiB
C++
#include"Boss/Mod/PeerJudge/Algo.hpp"
|
|
#include"Json/Out.hpp"
|
|
#include"Stats/WeightedMedian.hpp"
|
|
#include"Util/format.hpp"
|
|
#include"Util/make_unique.hpp"
|
|
#include<algorithm>
|
|
#include<assert.h>
|
|
|
|
namespace Boss { namespace Mod { namespace PeerJudge {
|
|
|
|
class Algo::Impl {
|
|
private:
|
|
std::vector<Info> infos;
|
|
double feerate;
|
|
std::uint64_t reopen_weight;
|
|
|
|
Ln::Amount reopen_cost;
|
|
|
|
struct ScoreData {
|
|
Ln::NodeId node;
|
|
Ln::Amount total;
|
|
Ln::Amount earned;
|
|
|
|
/* Score data. */
|
|
double earned_per_size;
|
|
|
|
/* Judgment data. */
|
|
Ln::Amount expected_earnings_if_reopened;
|
|
Ln::Amount expected_improvement_after_reopen_cost;
|
|
};
|
|
std::vector<ScoreData> scores;
|
|
std::vector<ScoreData>::iterator median;
|
|
struct ScoreDataItComp {
|
|
bool operator()( std::vector<ScoreData>::iterator a
|
|
, std::vector<ScoreData>::iterator b
|
|
) const {
|
|
return a->earned_per_size < b->earned_per_size;
|
|
}
|
|
};
|
|
std::map<Ln::NodeId, std::string> to_close;
|
|
|
|
Json::Out get_inputs_status() const {
|
|
auto rv = Json::Out();
|
|
auto arr = rv.start_array();
|
|
for (auto const& i : scores) {
|
|
arr.start_object()
|
|
.field("node", std::string(i.node))
|
|
.field("earned", std::string(i.earned))
|
|
.field("total", std::string(i.total))
|
|
.field("earned_over_total", i.earned_per_size)
|
|
.field( "earned_over_total_ppm"
|
|
, i.earned_per_size * 1000000.0
|
|
)
|
|
.end_object()
|
|
;
|
|
}
|
|
arr.end_array();
|
|
return rv;
|
|
}
|
|
Json::Out get_candidates_status() const {
|
|
auto rv = Json::Out();
|
|
auto arr = rv.start_array();
|
|
for (auto it = median + 1; it != scores.end(); ++it) {
|
|
arr.start_object()
|
|
.field("node", std::string(it->node))
|
|
.field( "expected_earnings_if_reopened"
|
|
, std::string(it->expected_earnings_if_reopened)
|
|
)
|
|
.field( "actual_earned"
|
|
, std::string(it->earned)
|
|
)
|
|
.field( "expected_improvement_after_reopen_cost"
|
|
, std::string(it->expected_improvement_after_reopen_cost)
|
|
)
|
|
.field( "close"
|
|
, bool(to_close.find(it->node) != to_close.end())
|
|
)
|
|
.end_object()
|
|
;
|
|
}
|
|
arr.end_array();
|
|
return rv;
|
|
}
|
|
|
|
public:
|
|
Impl( std::vector<Info> infos_
|
|
, double feerate_
|
|
, std::uint64_t reopen_weight_
|
|
) : infos(std::move(infos_))
|
|
, feerate(feerate_)
|
|
, reopen_weight(reopen_weight_)
|
|
{ }
|
|
|
|
Json::Out get_status() const {
|
|
return Json::Out()
|
|
.start_object()
|
|
.field("feerate_perkw", feerate)
|
|
.field("reopen_weight", reopen_weight)
|
|
.field("channels", get_inputs_status())
|
|
.field("reopen_cost", std::string(reopen_cost))
|
|
.field("weighted_median", median->earned_per_size)
|
|
.field( "weighted_median_ppm"
|
|
, median->earned_per_size * 1000000.0
|
|
)
|
|
.field("to_close", get_candidates_status())
|
|
.end_object()
|
|
;
|
|
}
|
|
std::map<Ln::NodeId, std::string> get_close() const {
|
|
return to_close;
|
|
}
|
|
void judge_peers() {
|
|
assert(infos.size() != 0);
|
|
|
|
/* Reset data. */
|
|
to_close.clear();
|
|
scores.clear();
|
|
|
|
/* Compute cost to reopen. */
|
|
reopen_cost = Ln::Amount::sat((feerate * reopen_weight) / 1000.0);
|
|
|
|
/* Create the scores and sort by score from
|
|
* highest to lowest. */
|
|
for (auto const& i : infos) {
|
|
scores.emplace_back(ScoreData{
|
|
i.id,
|
|
i.total_normal,
|
|
i.earned,
|
|
double(i.earned.to_msat()) / double(i.total_normal.to_msat())
|
|
});
|
|
}
|
|
std::sort( scores.begin(), scores.end()
|
|
, [](ScoreData const& a, ScoreData const& b) {
|
|
return a.earned_per_size > b.earned_per_size;
|
|
});
|
|
|
|
assert(scores.size() == infos.size());
|
|
|
|
/* Now fill in WeightedMedian. */
|
|
auto wm = Stats::WeightedMedian< std::vector<ScoreData>::iterator
|
|
, Ln::Amount
|
|
, ScoreDataItComp
|
|
>();
|
|
for (auto it = scores.begin(); it != scores.end(); ++it) {
|
|
wm.add(it, it->total);
|
|
}
|
|
median = std::move(wm).finalize();
|
|
|
|
auto median_per_total = median->earned_per_size;
|
|
|
|
/* Now actually judge. */
|
|
for (auto it = median + 1; it != scores.end(); ++it) {
|
|
/* We expect the earnings to on average be the
|
|
* the median earnings-per-channel-size,
|
|
* times the size of this particular channel.
|
|
*/
|
|
it->expected_earnings_if_reopened = median_per_total * it->total;
|
|
/* Take advantage of the fact that Ln::Amount saturates
|
|
* to 0 on subtraction. */
|
|
it->expected_improvement_after_reopen_cost =
|
|
( it->expected_earnings_if_reopened
|
|
- it->earned
|
|
- reopen_cost
|
|
);
|
|
if ( it->expected_improvement_after_reopen_cost
|
|
!= Ln::Amount::sat(0)
|
|
) {
|
|
auto reason = Util::format( "Expected (median) "
|
|
"earning rate: %f (%f ppm); "
|
|
"expected earnings given "
|
|
"size: %s; "
|
|
"actual earnings: %s; "
|
|
"reopen cost: %s"
|
|
, median_per_total
|
|
, median_per_total * 1000000.0
|
|
, std::string(
|
|
it->expected_earnings_if_reopened
|
|
).c_str()
|
|
, std::string(
|
|
it->earned
|
|
).c_str()
|
|
, std::string(
|
|
reopen_cost
|
|
).c_str()
|
|
);
|
|
to_close[it->node] = reason;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
Algo::Algo() : pimpl() { }
|
|
Algo::~Algo() =default;
|
|
|
|
Json::Out Algo::get_status() const {
|
|
if (!pimpl)
|
|
return Json::Out::direct("no data");
|
|
return pimpl->get_status();
|
|
}
|
|
std::map<Ln::NodeId, std::string> Algo::get_close() const {
|
|
if (!pimpl)
|
|
return std::map<Ln::NodeId, std::string>();
|
|
return pimpl->get_close();
|
|
}
|
|
void Algo::judge_peers( std::vector<Info> infos
|
|
, double feerate
|
|
, std::uint64_t reopen_weight
|
|
) {
|
|
if (infos.size() == 0) {
|
|
pimpl = nullptr;
|
|
return;
|
|
}
|
|
pimpl = Util::make_unique<Impl>( std::move(infos)
|
|
, feerate
|
|
, reopen_weight
|
|
);
|
|
pimpl->judge_peers();
|
|
}
|
|
|
|
}}}
|