mirror of
https://github.com/ZmnSCPxj/clboss.git
synced 2026-08-15 12:50:42 +02:00
588 lines
15 KiB
C++
588 lines
15 KiB
C++
#include"Boss/Mod/FundsMover/Attempter.hpp"
|
|
#include"Boss/Mod/FundsMover/create_label.hpp"
|
|
#include"Boss/Mod/Rpc.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"Ln/Preimage.hpp"
|
|
#include"Ln/Scid.hpp"
|
|
#include"Sha256/Hash.hpp"
|
|
#include"Util/stringify.hpp"
|
|
#include<assert.h>
|
|
#include<random>
|
|
#include<string>
|
|
#include<vector>
|
|
|
|
namespace {
|
|
|
|
auto initial_fuzzpercent = double(11.0);
|
|
auto step_fuzzpercent = double(11.0);
|
|
|
|
}
|
|
|
|
namespace Boss { namespace Mod { namespace FundsMover {
|
|
|
|
class Attempter::Impl : public std::enable_shared_from_this<Impl> {
|
|
private:
|
|
S::Bus& bus;
|
|
|
|
Boss::Mod::Rpc& rpc;
|
|
Ln::NodeId self_id;
|
|
Ln::Preimage preimage;
|
|
Ln::Preimage payment_secret;
|
|
Ln::NodeId source;
|
|
Ln::NodeId destination;
|
|
Ln::Amount amount;
|
|
std::shared_ptr<Ln::Amount> fee_budget;
|
|
std::shared_ptr<Ln::Amount> remaining_amount;
|
|
/* Details of the last channel from destination to us. */
|
|
Ln::Scid last_scid;
|
|
Ln::Amount base_fee;
|
|
std::uint32_t proportional_fee;
|
|
std::uint32_t cltv_delta;
|
|
/* Details of the first channel from us to source. */
|
|
Ln::Scid first_scid;
|
|
|
|
bool ok;
|
|
|
|
double fuzzpercent;
|
|
std::vector<std::string> excludes;
|
|
Ln::Amount dest_amount;
|
|
Ln::Amount source_amount;
|
|
std::uint32_t source_delay;
|
|
|
|
/* Route from source to destination, not including the
|
|
* us->source and destination->us hops.
|
|
*/
|
|
Jsmn::Object route;
|
|
|
|
/* The fee we currently have. */
|
|
Ln::Amount our_fee;
|
|
|
|
public:
|
|
Impl( S::Bus& bus_
|
|
, Boss::Mod::Rpc& rpc_
|
|
, Ln::NodeId self_id_
|
|
, Ln::Preimage preimage_
|
|
, Ln::Preimage payment_secret_
|
|
, Ln::NodeId source_
|
|
, Ln::NodeId destination_
|
|
, Ln::Amount amount_
|
|
, std::shared_ptr<Ln::Amount> fee_budget_
|
|
, std::shared_ptr<Ln::Amount> remaining_amount_
|
|
, Ln::Scid last_scid_
|
|
, Ln::Amount base_fee_
|
|
, std::uint32_t proportional_fee_
|
|
, std::uint32_t cltv_delta_
|
|
, Ln::Scid first_scid_
|
|
) : bus(bus_)
|
|
, rpc(rpc_)
|
|
, self_id(std::move(self_id_))
|
|
, preimage(std::move(preimage_))
|
|
, payment_secret(std::move(payment_secret_))
|
|
, source(std::move(source_))
|
|
, destination(std::move(destination_))
|
|
, amount(amount_)
|
|
, fee_budget(std::move(fee_budget_))
|
|
, remaining_amount(std::move(remaining_amount_))
|
|
, last_scid(last_scid_)
|
|
, base_fee(base_fee_)
|
|
, proportional_fee(proportional_fee_)
|
|
, cltv_delta(cltv_delta_)
|
|
, first_scid(first_scid_)
|
|
, ok(false)
|
|
{ }
|
|
Ev::Io<bool> run() {
|
|
auto self = shared_from_this();
|
|
return self->core_run().then([self]() {
|
|
return Ev::lift(self->ok);
|
|
});
|
|
}
|
|
|
|
private:
|
|
Ev::Io<void> core_run() {
|
|
return Ev::lift().then([this]() {
|
|
/* Initialize. */
|
|
excludes.push_back(std::string(self_id));
|
|
dest_amount = amount + base_fee
|
|
+ (amount * ( double(proportional_fee)
|
|
/ 1000000
|
|
))
|
|
+ Ln::Amount::msat(1)
|
|
;
|
|
fuzzpercent = initial_fuzzpercent;
|
|
|
|
return getroute();
|
|
});
|
|
}
|
|
Ev::Io<void> getroute() {
|
|
return Ev::yield().then([this]() {
|
|
auto parms = Json::Out()
|
|
.start_object()
|
|
.field("id", std::string(destination))
|
|
.field("amount_msat", dest_amount.to_msat())
|
|
.field("riskfactor", 10)
|
|
.field("cltv", cltv_delta + 14)
|
|
.field("fromid", std::string(source))
|
|
.field("fuzzpercent", fuzzpercent)
|
|
.field("exclude", make_excludes())
|
|
.end_object()
|
|
;
|
|
return rpc.command("getroute", std::move(parms));
|
|
}).then([this](Jsmn::Object res) {
|
|
if (!res.is_object() || !res.has("route"))
|
|
return Ev::lift(false);
|
|
route = res["route"];
|
|
return Ev::lift(true);
|
|
}).catching<RpcError>([](RpcError const&) {
|
|
return Ev::lift(false);
|
|
}).then([this](bool ok) {
|
|
if (!ok)
|
|
/* Maybe lowering the fuzzpercent will work
|
|
* this time? */
|
|
return fee_failed();
|
|
return compute_source_amount();
|
|
});
|
|
}
|
|
Json::Out make_excludes() {
|
|
auto rv = Json::Out();
|
|
auto arr = rv.start_array();
|
|
for (auto const& s : excludes)
|
|
arr.entry(s);
|
|
arr.end_array();
|
|
return rv;
|
|
}
|
|
Ev::Io<void> compute_source_amount() {
|
|
struct Fail { };
|
|
auto hop1_amount = std::make_shared<Ln::Amount>();
|
|
auto hop1_delay = std::make_shared<std::uint32_t>();
|
|
|
|
return Ev::lift().then([this, hop1_amount, hop1_delay]() {
|
|
auto hop1 = Ln::Scid();
|
|
try {
|
|
auto hop1_data = route[0];
|
|
hop1 = Ln::Scid(std::string(
|
|
hop1_data["channel"]
|
|
));
|
|
*hop1_amount = Ln::Amount::object(
|
|
hop1_data["amount_msat"]
|
|
);
|
|
*hop1_delay = std::uint32_t(double(
|
|
hop1_data["delay"]
|
|
));
|
|
} catch (Jsmn::TypeError const& ) {
|
|
return Boss::log( bus, Error
|
|
, "FundsMover: Unexpected "
|
|
"route from getroute: %s"
|
|
, Util::stringify(route)
|
|
.c_str()
|
|
).then([]() {
|
|
throw Fail();
|
|
return Ev::lift(Jsmn::Object());
|
|
});
|
|
}
|
|
auto parms = Json::Out()
|
|
.start_object()
|
|
.field( "short_channel_id"
|
|
, std::string(hop1)
|
|
)
|
|
.end_object()
|
|
;
|
|
return rpc.command("listchannels", std::move(parms));
|
|
}).then([this, hop1_amount, hop1_delay](Jsmn::Object res) {
|
|
auto base_fee = Ln::Amount();
|
|
auto prop_fee = std::uint32_t();
|
|
auto cltv_delta = std::uint32_t();
|
|
try {
|
|
auto found = false;
|
|
auto cs = res["channels"];
|
|
for (auto c : cs) {
|
|
auto csrc = Ln::NodeId(std::string(
|
|
c["source"]
|
|
));
|
|
if (csrc != source)
|
|
continue;
|
|
found = true;
|
|
base_fee = Ln::Amount::msat(double(
|
|
c["base_fee_millisatoshi"]
|
|
));
|
|
prop_fee = std::uint32_t(double(
|
|
c["fee_per_millionth"]
|
|
));
|
|
cltv_delta = std::uint32_t(double(
|
|
c["delay"]
|
|
));
|
|
}
|
|
if (!found)
|
|
throw Jsmn::TypeError();
|
|
} catch (Jsmn::TypeError const&) {
|
|
return Boss::log( bus, Error
|
|
, "FundsMover: Unexpected "
|
|
"result from listchannels: "
|
|
"%s"
|
|
, Util::stringify(res)
|
|
.c_str()
|
|
).then([]() {
|
|
return Ev::lift(false);
|
|
});
|
|
}
|
|
source_amount = *hop1_amount + base_fee
|
|
+ (*hop1_amount * ( double(prop_fee)
|
|
/ 1000000
|
|
))
|
|
+ Ln::Amount::msat(1)
|
|
;
|
|
source_delay = *hop1_delay + cltv_delta;
|
|
our_fee = source_amount - amount;
|
|
|
|
assert(amount <= *remaining_amount);
|
|
|
|
/* Make our fee budget proportional to how large we are. */
|
|
auto prorata = amount / *remaining_amount;
|
|
auto prorated_fee_budget = *fee_budget * prorata;
|
|
|
|
if (our_fee > prorated_fee_budget)
|
|
return Ev::lift(false);
|
|
*fee_budget -= our_fee;
|
|
*remaining_amount -= amount;
|
|
return Ev::lift(true);
|
|
}).catching<RpcError>([](RpcError const&) {
|
|
return Ev::lift(false);
|
|
}).catching<Fail>([](Fail const&) {
|
|
return Ev::lift(false);
|
|
}).then([this](bool success) {
|
|
if (!success)
|
|
return fee_failed();
|
|
/* At this point we have deducted our fee from the
|
|
* fee budget.
|
|
*/
|
|
return sendpay();
|
|
});
|
|
}
|
|
|
|
Ev::Io<void> sendpay() {
|
|
auto payment_hash = std::make_shared<Sha256::Hash>();
|
|
return Ev::lift().then([this, payment_hash]() {
|
|
*payment_hash = preimage.sha256();
|
|
auto label = create_label(*payment_hash);
|
|
auto parms = Json::Out()
|
|
.start_object()
|
|
.field("route", make_route())
|
|
.field( "payment_hash"
|
|
, std::string(*payment_hash)
|
|
)
|
|
.field("label" , label)
|
|
.field( "payment_secret"
|
|
, std::string(payment_secret)
|
|
)
|
|
.end_object()
|
|
;
|
|
return rpc.command("sendpay", std::move(parms));
|
|
}).then([this, payment_hash](Jsmn::Object _) {
|
|
auto parms = Json::Out()
|
|
.start_object()
|
|
.field( "payment_hash"
|
|
, std::string(*payment_hash)
|
|
)
|
|
.end_object()
|
|
;
|
|
return rpc.command("waitsendpay", std::move(parms));
|
|
}).then([this, payment_hash](Jsmn::Object _) {
|
|
/* Success? Now we can stop. */
|
|
ok = true;
|
|
return delpay(payment_hash, true)
|
|
+ Boss::log( bus, Info
|
|
, "FundsMover: Moved %s from %s, "
|
|
"getting %s to %s, costing us "
|
|
"%s."
|
|
, std::string(source_amount).c_str()
|
|
, std::string(source).c_str()
|
|
, std::string(amount).c_str()
|
|
, std::string(destination).c_str()
|
|
, std::string(our_fee).c_str()
|
|
)
|
|
;
|
|
}).catching<RpcError>([ this
|
|
, payment_hash
|
|
](RpcError const& e) {
|
|
/* Starting action: delete failing moves. */
|
|
auto act = delpay(payment_hash, false);
|
|
|
|
/* Return our fee to the budget. */
|
|
*fee_budget += our_fee;
|
|
*remaining_amount += amount;
|
|
|
|
/* Figure out the error. */
|
|
auto code = int();
|
|
auto eidx = std::size_t();
|
|
auto echan = Ln::Scid();
|
|
auto edir = int();
|
|
auto enode = Ln::NodeId();
|
|
auto fail = std::uint16_t();
|
|
try {
|
|
auto& error = e.error;
|
|
code = int(double(
|
|
error["code"]
|
|
));
|
|
/* Failure along route. */
|
|
if (code == 204) {
|
|
auto data = error["data"];
|
|
eidx = std::size_t(double(
|
|
data["erring_index"]
|
|
));
|
|
echan = Ln::Scid(std::string(
|
|
data["erring_channel"]
|
|
));
|
|
edir = int(double(
|
|
data["erring_direction"]
|
|
));
|
|
enode = Ln::NodeId(std::string(
|
|
data["erring_node"]
|
|
));
|
|
fail = std::uint16_t(double(
|
|
data["failcode"]
|
|
));
|
|
}
|
|
} catch (std::exception const& ex) {
|
|
return std::move(act)
|
|
+ Boss::log( bus, Error
|
|
, "FundsMover: Attempt: "
|
|
"Unexpected error from "
|
|
"%s: %s: %s"
|
|
, e.command.c_str()
|
|
, Util::stringify(e.error).c_str()
|
|
, ex.what()
|
|
);
|
|
}
|
|
|
|
if (code != 202 && code != 204)
|
|
return std::move(act)
|
|
+ Boss::log( bus, Info
|
|
, "FundsMover: Attempt: "
|
|
"Unexpected error code "
|
|
"%d from %s, error: %s"
|
|
, code
|
|
, e.command.c_str()
|
|
, Util::stringify(e.error)
|
|
.c_str()
|
|
);
|
|
/* Unparsable onion with a 1-hop route means the
|
|
* source or destination node has massive issues,
|
|
* so cannot advance. */
|
|
if (code == 202 && route.size() <= 1)
|
|
return std::move(act)
|
|
+ Boss::log( bus, Info
|
|
, "FundsMover: Attempt: "
|
|
"Unparsable onion, cannot "
|
|
"advance further."
|
|
);
|
|
|
|
if (code == 204) {
|
|
act += Boss::log( bus, Debug
|
|
, "FundsMover: code 204, "
|
|
"erring_index: %zu, "
|
|
"erring_channel: %s/%d, "
|
|
"erring_node: %s, "
|
|
"failcode: 0x%04x"
|
|
, eidx
|
|
, std::string(echan).c_str()
|
|
, edir
|
|
, std::string(enode).c_str()
|
|
, int(fail)
|
|
);
|
|
|
|
if ( eidx == 0
|
|
|| (eidx == 1 && (fail & 0x2000))
|
|
)
|
|
return std::move(act)
|
|
+ Boss::log( bus, Info
|
|
, "FundsMover: "
|
|
"Failed at source, "
|
|
"cannot advance "
|
|
"further."
|
|
)
|
|
;
|
|
if (eidx == route.size() + 1)
|
|
return std::move(act)
|
|
+ Boss::log( bus, Info
|
|
, "FundsMover: "
|
|
"Failed at "
|
|
"destination, "
|
|
"cannot advance "
|
|
"further."
|
|
)
|
|
;
|
|
/* 0x2000 == NODE level error. */
|
|
if ((fail & 0x2000))
|
|
excludes.push_back(std::string(
|
|
enode
|
|
));
|
|
else
|
|
excludes.push_back(
|
|
std::string(echan) + "/" +
|
|
Util::stringify(edir)
|
|
);
|
|
} else {
|
|
/* Unparsable onion, exclude any node. */
|
|
auto dist = std::uniform_int_distribution<std::size_t>(
|
|
0, route.size() - 2
|
|
);
|
|
auto hop = route[dist(Boss::random_engine)];
|
|
excludes.push_back(std::string(
|
|
hop["id"]
|
|
));
|
|
}
|
|
|
|
return std::move(act) + getroute();
|
|
});
|
|
}
|
|
/* Splice the us->source and destination->us hops. */
|
|
Json::Out make_route() {
|
|
auto ret = Json::Out();
|
|
auto arr = ret.start_array();
|
|
arr.entry(make_hop0());
|
|
for (auto step : route)
|
|
arr.entry(step);
|
|
arr.entry(make_hoplast());
|
|
arr.end_array();
|
|
return ret;
|
|
}
|
|
Json::Out make_hop0() {
|
|
/* Us->source hop. */
|
|
return Json::Out()
|
|
.start_object()
|
|
.field("id", std::string(source))
|
|
.field("channel", std::string(first_scid))
|
|
.field( "direction"
|
|
, self_id > source ? 1 : 0
|
|
)
|
|
.field("amount_msat", source_amount.to_msat())
|
|
.field("delay", source_delay)
|
|
/* This used to be an explicit "style": "legacy",
|
|
* since we did not want to have to parse listnodes
|
|
* just to find out if the first peer supports
|
|
* "tlv".
|
|
* However, recent C-Lightning versions have
|
|
* dropped support for legacy completely, so at
|
|
* this point it is now safer to switch explicitly
|
|
* to "style": "tlv".
|
|
* 0.11.x C-Lightning has it optional and requires
|
|
* it to be "tlv" if specified, but some older
|
|
* versions have it optional and default to
|
|
* "legacy".
|
|
* Unfortunately, some new node software releases
|
|
* *do not support* "legacy" *at all*, so if our
|
|
* first peer runs such no-legacy-nope-nada
|
|
* software, dropping "style" may lead to our
|
|
* own node using "legacy"-by-default, which
|
|
* would cause funds movement to fail.
|
|
* So now we explicitly say "tlv" here.
|
|
*/
|
|
.field("style", "tlv")
|
|
.end_object()
|
|
;
|
|
}
|
|
Json::Out make_hoplast() {
|
|
/* Destination->us hop. */
|
|
return Json::Out()
|
|
.start_object()
|
|
.field("id", std::string(self_id))
|
|
.field("channel", std::string(last_scid))
|
|
.field( "direction"
|
|
, destination > self_id ? 1 : 0
|
|
)
|
|
.field("amount_msat", amount.to_msat())
|
|
.field("delay", 14)
|
|
/* We always support "tlv", at least for now... */
|
|
.field("style", "tlv")
|
|
.end_object()
|
|
;
|
|
}
|
|
|
|
Ev::Io<void> delpay( std::shared_ptr<Sha256::Hash> payment_hash
|
|
, bool success
|
|
) {
|
|
return Ev::lift().then([this, payment_hash, success]() {
|
|
auto status = std::string(
|
|
success ? "complete" : "failed"
|
|
);
|
|
auto parms = Json::Out()
|
|
.start_object()
|
|
.field( "payment_hash"
|
|
, std::string(*payment_hash)
|
|
)
|
|
.field("status", status)
|
|
.end_object()
|
|
;
|
|
return rpc.command("delpay", std::move(parms));
|
|
/* Do not care if it succeeds or fails or what
|
|
* it returns. */
|
|
}).then([](Jsmn::Object _) {
|
|
return Ev::lift();
|
|
}).catching<RpcError>([](RpcError const& _) {
|
|
return Ev::lift();
|
|
});
|
|
}
|
|
|
|
Ev::Io<void> fee_failed() {
|
|
if (fuzzpercent > 0) {
|
|
fuzzpercent -= step_fuzzpercent;
|
|
if (fuzzpercent < 0)
|
|
fuzzpercent = 0;
|
|
return getroute();
|
|
}
|
|
return Boss::log( bus, Debug
|
|
, "FundsMover: Giving up attempt to move "
|
|
"%s from %s to %s."
|
|
, std::string(amount).c_str()
|
|
, std::string(source).c_str()
|
|
, std::string(destination).c_str()
|
|
);
|
|
}
|
|
};
|
|
|
|
Ev::Io<bool>
|
|
Attempter::run( S::Bus& bus
|
|
, Boss::Mod::Rpc& rpc
|
|
, Ln::NodeId self
|
|
/* The preimage should have been pre-arranged to be claimed. */
|
|
, Ln::Preimage preimage
|
|
, Ln::Preimage payment_secret
|
|
, Ln::NodeId source
|
|
, Ln::NodeId destination
|
|
, Ln::Amount amount
|
|
, std::shared_ptr<Ln::Amount> fee_budget
|
|
, std::shared_ptr<Ln::Amount> remaining_amount
|
|
/* Details of the channel from destination to us. */
|
|
, Ln::Scid last_scid
|
|
, Ln::Amount base_fee
|
|
, std::uint32_t proportional_fee
|
|
, std::uint32_t cltv_delta
|
|
/* The channel from us to source. */
|
|
, Ln::Scid first_scid
|
|
) {
|
|
auto impl = std::make_shared<Impl>( bus
|
|
, rpc
|
|
, std::move(self)
|
|
, std::move(preimage)
|
|
, std::move(payment_secret)
|
|
, std::move(source)
|
|
, std::move(destination)
|
|
, amount
|
|
, std::move(fee_budget)
|
|
, std::move(remaining_amount)
|
|
, last_scid
|
|
, base_fee
|
|
, proportional_fee
|
|
, cltv_delta
|
|
, first_scid
|
|
);
|
|
return impl->run();
|
|
}
|
|
|
|
}}}
|