Util::Str::group_digits -- readable msat/sat amounts in the xrebalance logs
Some checks are pending
Code Base Sanity Check / tests (push) Waiting to run
Code Base Sanity Check / coverage (push) Waiting to run
Code Base Sanity Check / build-clang (push) Waiting to run

Large amounts in the XRebalancer / XMoveFunds log lines were
unbroken digit runs (delivered 18851040 msat); grep-heavy log
sessions kept miscounting them.  Add Util::Str::group_digits,
which renders an integer with an underscore every three digit
places (18_851_040), matching the digit grouping
clboss-xrebalance-view and the other contrib tools already
print.

Applied to the amounts that get large:
- XRebalancer cycle line (derived N, request)
- XRebalancer transfer-done line (delivered, fee)
- XMoveFunds planning line (amount, maxfee)
- XMoveFunds 204 summary and fee-picture diagnostics
  (alloc_fee, required_out)
- XMoveFunds per-part budget refusals (would deliver)
- format_route_fees per-hop table (in/out/fee)

Counts, ppm values, and cltv deltas stay plain.  Signed
overload handles the int64 call sites (INT64_MIN-safe).

New tests/util/test_str covers both overloads and the
boundary cases.
This commit is contained in:
Ken Sedgwick 2026-06-11 14:46:43 -07:00
parent cd63a195fe
commit 9b155d1982
No known key found for this signature in database
GPG key ID: DBD2AF0849D711A9
6 changed files with 95 additions and 19 deletions

View file

@ -285,8 +285,10 @@ format_route_fees(Jsmn::Object const& path) {
auto cout = hop.has("cltv_out")
? std::uint32_t(double(hop["cltv_out"])) : 0u;
os << " [" << i << "] " << scidd
<< " in=" << ain << " out=" << aout
<< " fee=" << fee << "msat(" << ppm << "ppm)"
<< " in=" << Util::Str::group_digits(ain)
<< " out=" << Util::Str::group_digits(aout)
<< " fee=" << Util::Str::group_digits(fee)
<< "msat(" << ppm << "ppm)"
<< " cltv " << cin << "->" << cout
<< " d=" << (cin >= cout ? cin - cout : 0u) << "\n";
}
@ -1215,7 +1217,9 @@ private:
<< std::dec << " erring=" << echan_str << "/" << edir
<< " node=" << std::string(enode)
<< " from_target=" << from_target
<< " alloc_fee=" << alloc << "msat";
<< " alloc_fee="
<< Util::Str::group_digits(alloc)
<< "msat";
ChanUpdate scu;
if (data.has("raw_message")
&& eidx < askrene_path.size()
@ -1227,7 +1231,9 @@ private:
auto req_out = std::uint64_t(scu.fee_base_msat)
+ std::uint64_t(scu.fee_proportional_millionths)
* out / 1000000;
sum << " required_out=" << req_out << "msat";
sum << " required_out="
<< Util::Str::group_digits(req_out)
<< "msat";
if (scu.has_inbound_fee)
sum << " inbound_ppm="
<< scu.inbound_fee_proportional_millionths;
@ -1265,7 +1271,8 @@ private:
auto alloc = (in >= out) ? in - out
: std::uint64_t(0);
os << " erring hop[" << eidx
<< "] allocated fee=" << alloc
<< "] allocated fee="
<< Util::Str::group_digits(alloc)
<< "msat\n";
if (data.has("raw_message")) {
ChanUpdate dcu;
@ -1285,7 +1292,10 @@ private:
<< " out_ppm="
<< dcu.fee_proportional_millionths
<< " -> required_out="
<< req_out << "msat";
<< Util::Str::
group_digits(
req_out)
<< "msat";
if (dcu.has_inbound_fee)
os << "; inbound_base="
<< dcu.inbound_fee_base_msat
@ -1784,10 +1794,12 @@ private:
"refused: %.0f ppm "
"exceeds budget %.0f "
"ppm (would deliver "
"%" PRIu64 " msat)"
"%s msat)"
, i, part_ppm
, budget_ppm
, deliv_msat );
, Util::Str::group_digits(
deliv_msat)
.c_str() );
continue;
}
}
@ -2265,13 +2277,15 @@ private:
return Boss::log( bus, Info
, "XMoveFunds: planning %s -> %s, "
"amount=%" PRIu64 " msat, "
"maxfee=%" PRIu64 " msat, "
"amount=%s msat, "
"maxfee=%s msat, "
"maxparts=%" PRIu32 ", execute=%s"
, join_scids(p->source_scids).c_str()
, join_scids(p->dest_scids).c_str()
, std::uint64_t(p->amount.to_msat())
, std::uint64_t(p->maxfee.to_msat())
, Util::Str::group_digits(std::uint64_t(
p->amount.to_msat())).c_str()
, Util::Str::group_digits(std::uint64_t(
p->maxfee.to_msat())).c_str()
, p->maxparts
, p->execute ? "true" : "false"
)

View file

@ -20,6 +20,7 @@
#include"Ln/NodeId.hpp"
#include"S/Bus.hpp"
#include"Sqlite3.hpp"
#include"Util/Str.hpp"
#include"Util/make_unique.hpp"
#include<algorithm>
#include<ctime>
@ -700,15 +701,18 @@ private:
return Boss::log( bus, Info, "%s", levels_str.c_str() )
+ Boss::log( bus, Info
, "XRebalancer: cycle [xrebalance] floor=%.1f%s window=%.0fd "
"-> derived N=%lld sat, joint=%.1f ppm "
"-> derived N=%s sat, joint=%.1f ppm "
"(fill>=%.1f + drain>=%.1f); size_factor=%.3g%s "
"-> request=%lld sat (maxfee %u ppm); "
"-> request=%s sat (maxfee %u ppm); "
"sources=%zu dests=%zu; executing."
, effective_floor, picked_note.c_str(), window_days
, (long long)best_n, best_joint
, Util::Str::group_digits(
std::int64_t(best_n)).c_str(), best_joint
, best_fill_ppm, best_drain_ppm
, effective_size_factor, sf_note.c_str()
, (long long)requested, (unsigned)maxfee
, Util::Str::group_digits(
std::int64_t(requested)).c_str()
, (unsigned)maxfee
, source_scids.size(), dest_scids.size()
).then([this, source_scids, dest_scids]() {
return Boss::log( bus, Debug
@ -860,9 +864,15 @@ private:
* economics; reason is present only on a partial. */
return Boss::log( bus, Info
, "XRebalancer: transfer done: %.0f/%.0f parts, "
"delivered %.0f msat, fee %.0f msat%s%s."
"delivered %s msat, fee %s msat%s%s."
, num("parts_complete"), num("parts")
, delivered, fee, ppm.c_str(), reason.c_str() );
, Util::Str::group_digits(
std::int64_t(std::llround(
delivered))).c_str()
, Util::Str::group_digits(
std::int64_t(std::llround(
fee))).c_str()
, ppm.c_str(), reason.c_str() );
/* Nothing delivered: a clean failure -- show the part count
* attempted and the chokepoint, not three zeros. */
return Boss::log( bus, Info

View file

@ -699,7 +699,8 @@ TESTS = \
tests/util/test_date \
tests/util/test_duration \
tests/util/test_either \
tests/util/test_format
tests/util/test_format \
tests/util/test_str
check_PROGRAMS = $(TESTS)
if USE_VALGRIND

View file

@ -92,6 +92,25 @@ std::string trim(std::string const& s) {
return std::string(start, end);
}
std::string group_digits(std::uint64_t v) {
auto s = std::to_string(v);
auto out = std::string();
auto n = s.size();
for (auto i = std::size_t(0); i < n; ++i) {
if (i != 0 && (n - i) % 3 == 0)
out += '_';
out += s[i];
}
return out;
}
std::string group_digits(std::int64_t v) {
if (v < 0)
/* -(v + 1) is representable even for INT64_MIN. */
return "-" + group_digits(std::uint64_t(-(v + 1)) + 1);
return group_digits(std::uint64_t(v));
}
std::string fmt(char const *tpl, ...) {
va_list ap;

View file

@ -34,6 +34,13 @@ bool ishex(std::string const&);
std::string trim(std::string const& s);
/* Renders an integer with '_' between every three digit places
* (18851040 -> "18_851_040"), for log lines carrying large msat/sat
* amounts. Matches the digit grouping clboss-xrebalance-view and
* the other contrib tools print. */
std::string group_digits(std::uint64_t);
std::string group_digits(std::int64_t);
/* Like `sprintf`. */
std::string fmt(char const *tpl, ...)
#if HAVE_ATTRIBUTE_FORMAT

25
tests/util/test_str.cpp Normal file
View file

@ -0,0 +1,25 @@
#undef NDEBUG
#include"Util/Str.hpp"
#include<assert.h>
#include<cstdint>
int main() {
using Util::Str::group_digits;
/* Unsigned. */
assert(group_digits(std::uint64_t(0)) == "0");
assert(group_digits(std::uint64_t(999)) == "999");
assert(group_digits(std::uint64_t(1000)) == "1_000");
assert(group_digits(std::uint64_t(18851040)) == "18_851_040");
assert(group_digits(std::uint64_t(220708066)) == "220_708_066");
assert( group_digits(std::uint64_t(18446744073709551615ull))
== "18_446_744_073_709_551_615");
/* Signed. */
assert(group_digits(std::int64_t(-1)) == "-1");
assert(group_digits(std::int64_t(-18851040)) == "-18_851_040");
assert( group_digits(std::int64_t(INT64_MIN))
== "-9_223_372_036_854_775_808");
return 0;
}