mirror of
https://github.com/ZmnSCPxj/clboss.git
synced 2026-08-14 12:43:19 +02:00
551 lines
15 KiB
C++
551 lines
15 KiB
C++
#include"Boss/Mod/SendpayResultMonitor.hpp"
|
|
#include"Boss/Msg/DbResource.hpp"
|
|
#include"Boss/Msg/ManifestNotification.hpp"
|
|
#include"Boss/Msg/Manifestation.hpp"
|
|
#include"Boss/Msg/Notification.hpp"
|
|
#include"Boss/Msg/RpcCommandHook.hpp"
|
|
#include"Boss/Msg/SendpayResult.hpp"
|
|
#include"Boss/Msg/Timer10Minutes.hpp"
|
|
#include"Boss/concurrent.hpp"
|
|
#include"Boss/log.hpp"
|
|
#include"Ev/Io.hpp"
|
|
#include"Ev/now.hpp"
|
|
#include"Jsmn/Object.hpp"
|
|
#include"Json/Out.hpp"
|
|
#include"Ln/NodeId.hpp"
|
|
#include"S/Bus.hpp"
|
|
#include"Sha256/Hash.hpp"
|
|
#include"Sqlite3.hpp"
|
|
#include"Util/make_unique.hpp"
|
|
#include"Util/stringify.hpp"
|
|
#include<inttypes.h>
|
|
|
|
/*
|
|
The primary purpose for this module is to track the result of
|
|
payment attempts.
|
|
|
|
If a payment attempt fails, we blame the direct peer through
|
|
which the payment is done.
|
|
|
|
Now it might seem unfair, since the reason for failing might not
|
|
be because of the peer itself.
|
|
|
|
But we lay the blame on the peer, because the peer "should" know
|
|
that it is connected to some bad peers itself, and "should"
|
|
prefer to close the channels to those.
|
|
*/
|
|
|
|
namespace Boss { namespace Mod {
|
|
|
|
class SendpayResultMonitor::Impl {
|
|
private:
|
|
S::Bus& bus;
|
|
Sqlite3::Db db;
|
|
|
|
public:
|
|
Impl() =delete;
|
|
Impl(Impl&&) =delete;
|
|
|
|
Impl(S::Bus& bus_) : bus(bus_) { start(); }
|
|
|
|
private:
|
|
void start() {
|
|
bus.subscribe<Msg::Manifestation
|
|
>([this](Msg::Manifestation const&_) {
|
|
return bus.raise(Msg::ManifestNotification{
|
|
"sendpay_success"
|
|
})
|
|
+ bus.raise(Msg::ManifestNotification{
|
|
"sendpay_failure"
|
|
})
|
|
;
|
|
});
|
|
bus.subscribe<Msg::DbResource
|
|
>([this](Msg::DbResource const& m) {
|
|
db = m.db;
|
|
return db.transact().then([](Sqlite3::Tx tx) {
|
|
tx.query_execute(R"QRY(
|
|
CREATE TABLE
|
|
IF NOT EXISTS "SendpayResultMonitor"
|
|
( payment_hash TEXT NOT NULL
|
|
, partid INTEGER NOT NULL
|
|
, first_hop TEXT NOT NULL
|
|
, creation REAL NOT NULL
|
|
);
|
|
CREATE UNIQUE INDEX
|
|
IF NOT EXISTS "SendpayResultMonitor_idx"
|
|
ON "SendpayResultMonitor"
|
|
(payment_hash, partid);
|
|
|
|
CREATE TABLE IF NOT EXISTS
|
|
"SendpayResultMonitor_rl"
|
|
( payment_hash TEXT NOT NULL
|
|
, partid INTEGER NOT NULL
|
|
, route_len INTEGER NOT NULL
|
|
, FOREIGN KEY(payment_hash, partid)
|
|
REFERENCES "SendpayResultMonitor"
|
|
(payment_hash, partid)
|
|
ON DELETE CASCADE
|
|
);
|
|
CREATE UNIQUE INDEX IF NOT EXISTS
|
|
"SendpayResultMonitor_rlidx"
|
|
ON "SendpayResultMonitor_rl"
|
|
(payment_hash, partid);
|
|
)QRY");
|
|
tx.commit();
|
|
return Ev::lift();
|
|
});
|
|
});
|
|
/* Extract the first hop for the payment, from the
|
|
* RPC command used.
|
|
*/
|
|
bus.subscribe<Msg::RpcCommandHook
|
|
>([this](Msg::RpcCommandHook const& r) {
|
|
return Boss::concurrent(process_rpc_command(r))
|
|
;
|
|
});
|
|
/* Check for `sendpay_failure` or `sendpay_success`.
|
|
*/
|
|
bus.subscribe<Msg::Notification
|
|
>([this](Msg::Notification const& n) {
|
|
if (n.notification == "sendpay_failure")
|
|
return Boss::concurrent(
|
|
on_sendpay_failure(n.params)
|
|
);
|
|
else if (n.notification == "sendpay_success")
|
|
return Boss::concurrent(
|
|
on_sendpay_success(n.params)
|
|
);
|
|
return Ev::lift();
|
|
});
|
|
/* If a recorded sendpay is not seen by us after 30 days,
|
|
* delete it and ignore.
|
|
* Our assumption is that it was resolved, but we were
|
|
* not able to see it for various reason, such as the
|
|
* node operator neglecting to start us up.
|
|
* Our assumption as well is that node operator is not
|
|
* going to increase `max-locktime-blocks`, default 2
|
|
* weeks, to past 30 days.
|
|
*/
|
|
bus.subscribe<Msg::Timer10Minutes
|
|
>([this](Msg::Timer10Minutes const& _) {
|
|
return Boss::concurrent(on_periodic());
|
|
});
|
|
}
|
|
|
|
/* Record the arguments of `sendonion` and `sendpay`
|
|
* commands.
|
|
*/
|
|
Ev::Io<void>
|
|
process_rpc_command(Msg::RpcCommandHook const& r) {
|
|
auto params = std::make_shared<Jsmn::Object>(r.params);
|
|
return Ev::lift().then([this, params]() {
|
|
try {
|
|
auto payload = (*params)["rpc_command"];
|
|
auto m = std::string(payload["method"]);
|
|
auto p = payload["params"];
|
|
if (m == "sendonion")
|
|
return process_sendonion(p);
|
|
else if (m == "sendpay")
|
|
return process_sendpay(p);
|
|
} catch (Jsmn::TypeError const& _) {
|
|
return Boss::log( bus, Error
|
|
, "SendpayResultMonitor: "
|
|
"Unexpected rpc_command "
|
|
"payload: %s"
|
|
, Util::stringify(*params)
|
|
.c_str()
|
|
);
|
|
}
|
|
return Ev::lift();
|
|
});
|
|
}
|
|
Ev::Io<void>
|
|
process_sendonion(Jsmn::Object const& params) {
|
|
auto first_hop_j = Jsmn::Object();
|
|
auto payment_hash_j = Jsmn::Object();
|
|
auto partid_j = Jsmn::Object();
|
|
auto shared_secrets_j = Jsmn::Object();
|
|
if (params.is_array() && params.size() >= 3) {
|
|
first_hop_j = params[1];
|
|
payment_hash_j = params[2];
|
|
if (params.size() >= 6)
|
|
partid_j = params[5];
|
|
if (params.size() >= 5)
|
|
shared_secrets_j = params[4];
|
|
} else if (params.is_object()) {
|
|
if (params.has("first_hop"))
|
|
first_hop_j = params["first_hop"];
|
|
if (params.has("payment_hash"))
|
|
payment_hash_j = params["payment_hash"];
|
|
if (params.has("partid"))
|
|
partid_j = params["partid"];
|
|
if (params.has("shared_secrets"))
|
|
shared_secrets_j = params["shared_secrets"];
|
|
}
|
|
auto len = std::unique_ptr<std::size_t>();
|
|
if (shared_secrets_j.is_array())
|
|
len = Util::make_unique<std::size_t>(
|
|
shared_secrets_j.size()
|
|
);
|
|
return record_pending_sendpay_j( payment_hash_j
|
|
, partid_j
|
|
, first_hop_j
|
|
, std::move(len)
|
|
);
|
|
}
|
|
Ev::Io<void>
|
|
process_sendpay(Jsmn::Object const& params) {
|
|
auto route_j = Jsmn::Object();
|
|
auto payment_hash_j = Jsmn::Object();
|
|
auto partid_j = Jsmn::Object();
|
|
if (params.is_array() && params.size() >= 2) {
|
|
route_j = params[0];
|
|
payment_hash_j = params[1];
|
|
if (params.size() >= 7)
|
|
partid_j = params[6];
|
|
} else if (params.is_object()) {
|
|
if (params.has("route"))
|
|
route_j = params["route"];
|
|
if (params.has("payment_hash"))
|
|
payment_hash_j = params["payment_hash"];
|
|
if (params.has("partid"))
|
|
partid_j = params["partid"];
|
|
}
|
|
if ( !route_j.is_array()
|
|
|| route_j.size() < 1
|
|
)
|
|
return Ev::lift();
|
|
auto first_hop_j = route_j[0];
|
|
auto len = Util::make_unique<std::size_t>(route_j.size());
|
|
|
|
return record_pending_sendpay_j( payment_hash_j
|
|
, partid_j
|
|
, first_hop_j
|
|
, std::move(len)
|
|
);
|
|
}
|
|
Ev::Io<void>
|
|
record_pending_sendpay_j( Jsmn::Object const& payment_hash_j
|
|
, Jsmn::Object const& partid_j
|
|
, Jsmn::Object const& first_hop_j
|
|
, std::unique_ptr<std::size_t> route_len
|
|
) {
|
|
/* Params come from external, so do not get fooled. */
|
|
if ( first_hop_j.is_object()
|
|
&& first_hop_j.has("id")
|
|
&& first_hop_j["id"].is_string()
|
|
&& Ln::NodeId::valid_string(std::string(first_hop_j["id"]))
|
|
&& payment_hash_j.is_string()
|
|
&& Sha256::Hash::valid_string(std::string(payment_hash_j))
|
|
&& (partid_j.is_null() || partid_j.is_number())
|
|
) {
|
|
auto first_hop = Ln::NodeId(std::string(
|
|
first_hop_j["id"]
|
|
));
|
|
auto payment_hash = Sha256::Hash(std::string(
|
|
payment_hash_j
|
|
));
|
|
auto partid = std::uint64_t();
|
|
if (partid_j.is_null())
|
|
partid = 0;
|
|
else
|
|
partid = std::uint64_t(double(partid_j));
|
|
return record_pending_sendpay( std::move(payment_hash)
|
|
, partid
|
|
, std::move(first_hop)
|
|
, std::move(route_len)
|
|
);
|
|
}
|
|
return Ev::lift();
|
|
}
|
|
Ev::Io<void>
|
|
record_pending_sendpay( Sha256::Hash payment_hash
|
|
, std::uint64_t partid
|
|
, Ln::NodeId first_hop
|
|
, std::unique_ptr<std::size_t> n_route_len
|
|
) {
|
|
/* Promote unique pointer to shared for easier copying. */
|
|
auto route_len = std::shared_ptr<std::size_t>(
|
|
std::move(n_route_len)
|
|
);
|
|
return db.transact().then([ this
|
|
, payment_hash
|
|
, partid
|
|
, first_hop
|
|
, route_len
|
|
](Sqlite3::Tx tx) {
|
|
tx.query(R"QRY(
|
|
INSERT OR REPLACE
|
|
INTO "SendpayResultMonitor"
|
|
VALUES( :payment_hash
|
|
, :partid
|
|
, :first_hop
|
|
, :creation
|
|
)
|
|
)QRY")
|
|
.bind( ":payment_hash"
|
|
, std::string(payment_hash)
|
|
)
|
|
.bind(":partid" , partid)
|
|
.bind( ":first_hop"
|
|
, std::string(first_hop)
|
|
)
|
|
.bind(":creation", Ev::now())
|
|
.execute()
|
|
;
|
|
if (route_len) {
|
|
tx.query(R"QRY(
|
|
INSERT OR REPLACE
|
|
INTO "SendpayResultMonitor_rl"
|
|
VALUES( :payment_hash
|
|
, :partid
|
|
, :route_len
|
|
);
|
|
)QRY")
|
|
.bind( ":payment_hash"
|
|
, std::string(payment_hash)
|
|
)
|
|
.bind(":partid" , partid)
|
|
.bind(":route_len", *route_len)
|
|
.execute()
|
|
;
|
|
}
|
|
tx.commit();
|
|
return Boss::log( bus, Debug
|
|
, "SendpayResultMonitor: "
|
|
"Monitoring %s part %" PRIu64
|
|
" peer %s route_len %s."
|
|
, std::string(payment_hash).c_str()
|
|
, partid
|
|
, std::string(first_hop).c_str()
|
|
, route_len ?
|
|
Util::stringify(*route_len)
|
|
.c_str() :
|
|
"unknown"
|
|
);
|
|
});
|
|
}
|
|
|
|
Ev::Io<void> on_sendpay_failure(Jsmn::Object const& params) {
|
|
if (!params.is_object() || !params.has("sendpay_failure"))
|
|
return Ev::lift();
|
|
auto payload = params["sendpay_failure"];
|
|
/* For `sendpay_failure` the data we need is wrapped in
|
|
* a `data` object.
|
|
* We also have to extract the `code` integer.
|
|
*/
|
|
if (!payload.is_object() || !payload.has("data"))
|
|
return Ev::lift();
|
|
auto data = payload["data"];
|
|
if (!data.is_object())
|
|
return Ev::lift();
|
|
|
|
if (!payload.has("code"))
|
|
return Ev::lift();
|
|
auto code = payload["code"];
|
|
if (!code.is_number())
|
|
return Ev::lift();
|
|
|
|
/* We determine if we reached the destination
|
|
* by checking the erring_index and seeing
|
|
* if it is the correct path length.
|
|
* Some errors at the destination, such as
|
|
* `mpp_timeout`, are not permanent, and would
|
|
* not trigger `PAY_DESTINATION_PERM_FAIL`.
|
|
*/
|
|
if (!data.has("erring_index"))
|
|
return Ev::lift();
|
|
auto erring_index_j = data["erring_index"];
|
|
if (!erring_index_j.is_number())
|
|
return Ev::lift();
|
|
auto erring_index = std::size_t(double(erring_index_j));
|
|
return on_sendpay_resolve( data
|
|
, false
|
|
, erring_index
|
|
);
|
|
}
|
|
Ev::Io<void> on_sendpay_success(Jsmn::Object const& params) {
|
|
if (!params.is_object() || !params.has("sendpay_success"))
|
|
return Ev::lift();
|
|
auto payload = params["sendpay_success"];
|
|
return on_sendpay_resolve(payload, true, 0);
|
|
}
|
|
Ev::Io<void> on_sendpay_resolve( Jsmn::Object const& data
|
|
, bool success
|
|
, std::size_t erring_index
|
|
) {
|
|
if (!data.has("payment_hash"))
|
|
return Ev::lift();
|
|
auto payment_hash_s = std::string(data["payment_hash"]);
|
|
if (!Sha256::Hash::valid_string(payment_hash_s))
|
|
return Ev::lift();
|
|
auto payment_hash = Sha256::Hash(payment_hash_s);
|
|
|
|
auto partid = std::uint64_t(0);
|
|
if (data.has("partid") && data["partid"].is_number())
|
|
partid = std::uint64_t(double(data["partid"]));
|
|
|
|
return db.transact().then([ this
|
|
, payment_hash
|
|
, partid
|
|
, success
|
|
, erring_index
|
|
](Sqlite3::Tx tx) {
|
|
auto first_hop = Ln::NodeId();
|
|
auto creation = double();
|
|
auto route_len = std::unique_ptr<std::size_t>();
|
|
auto found = get_sendpay_data( tx
|
|
, payment_hash
|
|
, partid
|
|
|
|
, first_hop
|
|
, creation
|
|
, route_len
|
|
);
|
|
|
|
if (!found)
|
|
/* Do nothing. */
|
|
return Ev::lift();
|
|
|
|
auto reached_destination = success;
|
|
if (!success && route_len)
|
|
/* If the erring index is at the destination,
|
|
* we actually did in fact reach the
|
|
* destination.
|
|
* Use <= in case in the future rendezvous
|
|
* routing somehow gets implemented.
|
|
* That would mean we would not know the
|
|
* shared secrets within the postfix, and
|
|
* as far as we know the route length is
|
|
* only up to the rendezvous point.
|
|
*/
|
|
if (*route_len <= erring_index)
|
|
reached_destination = true;
|
|
|
|
/* Remove it. */
|
|
tx.query(R"QRY(
|
|
DELETE FROM "SendpayResultMonitor"
|
|
WHERE payment_hash = :payment_hash
|
|
AND partid = :partid
|
|
;
|
|
)QRY")
|
|
.bind( ":payment_hash"
|
|
, std::string(payment_hash)
|
|
)
|
|
.bind(":partid", partid)
|
|
.execute();
|
|
tx.commit();
|
|
/* Report and broadcast. */
|
|
auto act = Ev::lift();
|
|
act += Boss::log( bus, Debug
|
|
, "SendpayResultMonitor: "
|
|
"Resolved %s part %" PRIu64
|
|
" peer %s: %s, %s%s"
|
|
, std::string(payment_hash).c_str()
|
|
, partid
|
|
, std::string(first_hop).c_str()
|
|
, success ? "success" : "failure"
|
|
, reached_destination ?
|
|
"reached destination" :
|
|
"destination not reached"
|
|
, success ? "" :
|
|
( std::string(", ")
|
|
+ "erring_index: "
|
|
+ Util::stringify(erring_index)
|
|
).c_str()
|
|
);
|
|
act += bus.raise(Msg::SendpayResult{
|
|
creation,
|
|
Ev::now(),
|
|
std::move(first_hop),
|
|
std::move(payment_hash),
|
|
partid,
|
|
success,
|
|
reached_destination
|
|
});
|
|
return act;
|
|
});
|
|
}
|
|
|
|
bool get_sendpay_data( Sqlite3::Tx& tx
|
|
, Sha256::Hash const& payment_hash
|
|
, std::uint64_t partid
|
|
/* Data to extract. */
|
|
, Ln::NodeId& first_hop
|
|
, double& creation
|
|
, std::unique_ptr<std::size_t>& route_len
|
|
) {
|
|
auto fetch = tx.query(R"QRY(
|
|
SELECT first_hop, creation
|
|
FROM "SendpayResultMonitor"
|
|
WHERE payment_hash = :payment_hash
|
|
AND partid = :partid
|
|
;
|
|
)QRY")
|
|
.bind( ":payment_hash"
|
|
, std::string(payment_hash)
|
|
)
|
|
.bind(":partid", partid)
|
|
.execute();
|
|
|
|
auto found = false;
|
|
for (auto& r : fetch) {
|
|
first_hop = Ln::NodeId(
|
|
r.get<std::string>(0)
|
|
);
|
|
creation = r.get<double>(1);
|
|
found = true;
|
|
}
|
|
|
|
if (!found)
|
|
return false;
|
|
|
|
auto fetch2 = tx.query(R"QRY(
|
|
SELECT route_len
|
|
FROM "SendpayResultMonitor_rl"
|
|
WHERE payment_hash = :payment_hash
|
|
AND partid = :partid
|
|
;
|
|
)QRY")
|
|
.bind( ":payment_hash"
|
|
, std::string(payment_hash)
|
|
)
|
|
.bind(":partid", partid)
|
|
.execute();
|
|
/* This could return empty, i.e. from old database. */
|
|
for (auto& r : fetch2)
|
|
route_len = Util::make_unique<std::size_t>(
|
|
r.get<std::size_t>(0)
|
|
);
|
|
|
|
return found;
|
|
}
|
|
|
|
Ev::Io<void> on_periodic() {
|
|
return db.transact().then([](Sqlite3::Tx tx) {
|
|
/* Just drop rows whose creation is more
|
|
* than a month ago.
|
|
* TODO: maybe a better solution would be to
|
|
* get `delay` from first hop and add some margin.
|
|
*/
|
|
tx.query(R"QRY(
|
|
DELETE FROM "SendpayResultMonitor"
|
|
WHERE creation + (30 * 24 * 3600) < :current_time
|
|
;
|
|
)QRY")
|
|
.bind(":current_time", Ev::now())
|
|
.execute();
|
|
tx.commit();
|
|
return Ev::lift();
|
|
});
|
|
}
|
|
};
|
|
|
|
SendpayResultMonitor::SendpayResultMonitor(SendpayResultMonitor&&) =default;
|
|
SendpayResultMonitor::~SendpayResultMonitor() =default;
|
|
|
|
SendpayResultMonitor::SendpayResultMonitor
|
|
(S::Bus& bus) : pimpl(Util::make_unique<Impl>(bus)) { }
|
|
|
|
}}
|