clboss/Boss/Mod/XMoveFunds/Claimer.cpp
Ken Sedgwick bf68431cb2
XMoveFunds: implement sendpay execution path
Flips clboss-xmovefunds from plan-only to actually sending the
returned route via sendpay when execute=true.  All the masking
and getroutes work from the previous commit is unchanged; this
commit adds the post-getroutes sendpay/waitsendpay machinery
plus the supporting infrastructure for self-payment HTLC
resolution.

Mechanism

Once getroutes returns one or more routes (potentially multi-
part), do_execute:

  1. Generates a fresh preimage and payment_secret via the new
     XMoveFunds::Claimer (mirroring FundsMover::Claimer's
     pattern but maintained independently so the two
     subsystems coexist without sharing the entry table).  The
     preimage is registered in the claim table so that when
     the resulting HTLCs arrive at us we auto-resolve them.
  2. Constructs a sendpay-format route per part by copying the
     askrene path[] hops verbatim (mapping
     short_channel_id_dir, amount_out_msat, cltv_out, and
     node_id_out into the sendpay hop shape) and appending a
     closing hop (fill_peer -> self_id via the dest_scid that
     matches the route's last node_id_out).
  3. Issues sendpay for each part with shared payment_hash,
     payment_secret, label, and groupid.  Single-part
     payments use partid=0 (non-MPP).  Multi-part payments use
     partid 1..N and pass amount_msat as the total across all
     parts.
  4. Waits for every part via waitsendpay.  Sendpay or
     waitsendpay errors per part are captured into an errors
     array so the overall reply can still summarise what
     happened to each part instead of bailing on the first
     failure.

Label format is "clboss-xrebalance-<unix-ts>" so the eventual
EarningsTracker integration can disambiguate xrebalance-family
payments from FundsMover ones (see R4 in the plan).

execute default flipped to true

Per the plan's "Manual command actually executes (signet)"
decision, the default is now execute=true.  Caller explicitly
passes execute=false to get the plan-only response (the
existing "predict-and-compare" mode), which still returns the
askrene plan with no sendpay.

API surface

The reply gains:

  status            "executed" when sendpay+waitsendpay ran,
                    "execute_skipped" when execute=true was
                    requested but getroutes errored, or
                    "planned" when execute=false.
  execution         object, only present when status =
                    "executed":
    payment_hash    hex
    preimage        hex
    label           "clboss-xrebalance-<ts>"
    groupid         u64
    parts           number of MPP parts
    results         array of per-part waitsendpay responses
    errors          array of per-part error strings
                    (only present if any failed)
2026-05-31 20:46:58 -07:00

104 lines
2.9 KiB
C++

#include"Boss/Mod/XMoveFunds/Claimer.hpp"
#include"Boss/Msg/ProvideHtlcAcceptedDeferrer.hpp"
#include"Boss/Msg/ReleaseHtlcAccepted.hpp"
#include"Boss/Msg/SolicitHtlcAcceptedDeferrer.hpp"
#include"Boss/Msg/TimerRandomHourly.hpp"
#include"Boss/concurrent.hpp"
#include"Ev/Io.hpp"
#include"Ev/now.hpp"
#include"Ln/HtlcAccepted.hpp"
#include"S/Bus.hpp"
namespace {
/* Self-payment claim entries are kept for 24 hours. Anything
* older than that almost certainly belongs to a stuck or
* abandoned request and is safe to drop -- the in-flight HTLCs
* will time out via the on-chain mechanism. */
auto const timeout = double(3600 * 24);
}
namespace Boss { namespace Mod { namespace XMoveFunds {
void Claimer::start() {
bus.subscribe<Msg::SolicitHtlcAcceptedDeferrer
>([this](Msg::SolicitHtlcAcceptedDeferrer const&) {
auto f = [this](Ln::HtlcAccepted::Request const& r) {
auto const& h = r.payment_hash;
auto it = entries.find(h);
if (it == entries.end())
return Ev::lift(false);
auto const& payment_secret =
it->second.payment_secret;
if (r.payment_secret != payment_secret)
return Ev::lift(false);
/* Extract data. Note: we COPY the preimage and
* do NOT erase the entry, unlike FundsMover's
* Claimer. Each xrebalance self-payment can
* involve multiple MPP parts arriving at us
* with the same payment_hash + payment_secret;
* we want every part to settle, not just the
* first. For self-rebalancing, partial
* delivery is still a win -- every msat that
* settles is real rebalance value. Stale
* entries are pruned by the periodic timer
* below on the 24h cadence; in practice every
* MPP arrives well inside that window. */
auto id = r.id;
auto preimage = it->second.preimage;
/* Prepare background action. */
auto act = bus.raise(Msg::ReleaseHtlcAccepted{
Ln::HtlcAccepted::Response::resolve(
id, std::move(preimage)
)
});
/* Launch background action and return. */
return Boss::concurrent(act).then([]() {
return Ev::lift(true);
});
};
return bus.raise(Msg::ProvideHtlcAcceptedDeferrer{
std::move(f)
});
});
bus.subscribe<Msg::TimerRandomHourly
>([this](Msg::TimerRandomHourly const&) {
auto now = Ev::now();
/* Scan all entries and erase those that have gone
* past timeout. */
for ( auto it = entries.begin(), next = entries.begin()
; it != entries.end()
; it = next
) {
/* Save next entry. */
next = it;
++next;
if (it->second.timeout < now)
entries.erase(it);
}
return Ev::lift();
});
}
std::pair<Ln::Preimage, Ln::Preimage> Claimer::generate() {
auto pre = Ln::Preimage(rand);
auto sec = Ln::Preimage(rand);
auto h = pre.sha256();
auto& entry = entries[h];
entry.timeout = Ev::now() + timeout;
entry.preimage = pre;
entry.payment_secret = sec;
return std::make_pair(std::move(pre), std::move(sec));
}
}}}