#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 #include #include #include 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 { 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 fee_budget; std::shared_ptr 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 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 fee_budget_ , std::shared_ptr 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 run() { auto self = shared_from_this(); return self->core_run().then([self]() { return Ev::lift(self->ok); }); } private: Ev::Io 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 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 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 compute_source_amount() { struct Fail { }; auto hop1_amount = std::make_shared(); auto hop1_delay = std::make_shared(); 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 const&) { return Ev::lift(false); }).catching([](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 sendpay() { auto payment_hash = std::make_shared(); 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([ 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( 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 delpay( std::shared_ptr 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 const& _) { return Ev::lift(); }); } Ev::Io 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 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 fee_budget , std::shared_ptr 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( 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(); } }}}