#include"Boss/Mod/SwapManager.hpp" #include"Boss/Msg/AcceptSwapQuotation.hpp" #include"Boss/Msg/Block.hpp" #include"Boss/Msg/DbResource.hpp" #include"Boss/Msg/ListfundsResult.hpp" #include"Boss/Msg/PayInvoice.hpp" #include"Boss/Msg/ProvideStatus.hpp" #include"Boss/Msg/ProvideSwapQuotation.hpp" #include"Boss/Msg/RequestListpays.hpp" #include"Boss/Msg/RequestNewaddr.hpp" #include"Boss/Msg/ResponseListpays.hpp" #include"Boss/Msg/ResponseNewaddr.hpp" #include"Boss/Msg/SolicitStatus.hpp" #include"Boss/Msg/SolicitSwapQuotation.hpp" #include"Boss/Msg/SwapCreation.hpp" #include"Boss/Msg/SwapRequest.hpp" #include"Boss/Msg/SwapResponse.hpp" #include"Boss/Msg/Timer10Minutes.hpp" #include"Boss/concurrent.hpp" #include"Boss/log.hpp" #include"Boss/random_engine.hpp" #include"Ev/Io.hpp" #include"Ev/foreach.hpp" #include"Ev/yield.hpp" #include"S/Bus.hpp" #include"Sqlite3.hpp" #include"Util/make_unique.hpp" #include"Uuid.hpp" #include #include #include #include #include namespace Boss { namespace Mod { class SwapManager::Impl { private: S::Bus& bus; Sqlite3::Db db; bool getting_address; bool getting_invoice; public: Impl() =delete; Impl(Impl&&) =delete; Impl(Impl const&) =delete; explicit Impl( S::Bus& bus_ ) : bus(bus_) , getting_address(false) , getting_invoice(false) { start(); } private: /* /!\ db column 'state', do not change numbers! */ enum State { NeedsOnchainAddress = 0 , NeedsInvoice = 1 , AwaitingResult = 2 }; void start() { bus.subscribe([this](Msg::DbResource const& r) { db = r.db; return on_init(); }); bus.subscribe([this](Msg::Timer10Minutes const& _) { return Boss::concurrent(on_periodic()); }); bus.subscribe([this](Msg::ResponseNewaddr const& r) { if (r.requester != this) return Ev::lift(); return Boss::concurrent( on_response_newaddr(r.address) ); }); bus.subscribe([this](Msg::ProvideSwapQuotation const& q) { if (q.solicitor != this) return Ev::lift(); /* Not concurrent since the processing expects * synchronous response. */ return on_provide_swap_quotation(q.fee, q.provider); }); bus.subscribe([this](Msg::SwapCreation const& s) { if (s.solicitor != this) return Ev::lift(); return Boss::concurrent( on_swap_creation(s) ); }); bus.subscribe([this](Msg::ResponseListpays const& r) { return Boss::concurrent( on_response_listpays(r) ); }); bus.subscribe([this](Msg::ListfundsResult const& r) { return Boss::concurrent( on_listfunds_result(r) ); }); bus.subscribe([this](Msg::SwapRequest const& r) { /* Not concurrent since we need the db transaction. */ return on_swap_request(r); }); bus.subscribe([this](Msg::Block const& b) { return Boss::concurrent( on_block(b.height) ); }); bus.subscribe([this](Msg::SolicitStatus const&) { /* Not concurrent, this has to reply * synchronously. */ return on_solicit_status(); }); } /* On initialize. */ Ev::Io on_init() { return db.transact().then([](Sqlite3::Tx tx) { tx.query_execute(R"QRY( CREATE TABLE IF NOT EXISTS "SwapManager" ( uuid TEXT UNIQUE -- in millisatoshis , amount INTEGER NOT NULL , min_amount INTEGER NOT NULL -- current state. , state INTEGER NOT NULL -- onchain address. , address TEXT -- current being tried , payment_hash TEXT , timeout INTEGER ); CREATE INDEX IF NOT EXISTS "SwapManager_hash_index" ON "SwapManager"(payment_hash) ; CREATE INDEX IF NOT EXISTS "SwapManager_address_index" ON "SwapManager"(address) ; -- This is just a list of addresses whose swap has -- failed. -- When a swap is failed, we delete the swap and -- put its address here, and future swaps can -- use these instead of generating new addresses. CREATE TABLE IF NOT EXISTS "SwapManager_addrcache" ( id INTEGER PRIMARY KEY -- ROWID , address TEXT NOT NULL ); )QRY"); tx.commit(); return Ev::lift(); }).then([this]() { return Boss::concurrent(on_periodic()); }); } /* Queue of swaps that need a new address. */ std::queue needs_address; /* Queue of swaps that need an invoice from a swap provider. */ std::queue needs_invoice; Ev::Io on_periodic() { return Ev::lift().then([this]() { return load_queue( needs_address , NeedsOnchainAddress ).then([this]() { return Boss::concurrent( process_needs_address() ); }); }).then([this]() { return load_queue( needs_invoice , NeedsInvoice ).then([this]() { return Boss::concurrent( process_needs_invoice() ); }); }).then([this]() { return initiate_check_payments(); }); } Ev::Io load_queue(std::queue& q, State s) { return db.transact().then([ &q , s ](Sqlite3::Tx tx) { if (!q.empty()) { /* Abort after all. */ tx.rollback(); return Ev::lift(); } auto res = tx.query(R"QRY( SELECT uuid FROM "SwapManager" WHERE state = :state ; )QRY") .bind(":state", int(s)) .execute(); for (auto& r : res) { q.push(Uuid( r.get(0) )); } tx.commit(); return Ev::lift(); }); } /* Processing of items in needs-address. */ Ev::Io process_needs_address() { if (getting_address) return Ev::lift(); getting_address = true; return loop_needs_address(); } Ev::Io loop_needs_address() { if (needs_address.empty()) { getting_address = false; return Boss::log( bus, Debug , "SwapManager: " "no more swaps need addresses" ); } auto uuid = needs_address.front(); return Boss::log( bus, Debug , "SwapManager: Swap %s getting address." , std::string(uuid).c_str() ).then([this]() { return db.transact(); }).then([this](Sqlite3::Tx tx) { auto uuid = needs_address.front(); /* First, try to get an address from the addrcache. */ auto check = tx.query(R"QRY( SELECT id, address FROM "SwapManager_addrcache" ORDER BY id LIMIT 1 ; )QRY").execute(); for (auto& r : check) { /* Got one! Move from address cache to * actual swap. */ auto id = r.get(0); auto address = r.get(1); tx.query(R"QRY( DELETE FROM SwapManager_addrcache WHERE id = :id ; )QRY") .bind(":id", id) .execute(); return load_address( std::move(tx) , std::move(uuid) , std::move(address) ); } tx.commit(); return bus.raise(Msg::RequestNewaddr{this}); }); } Ev::Io on_response_newaddr(std::string n_address) { assert(getting_address); auto address = std::make_shared( std::move(n_address) ); return db.transact().then([this, address](Sqlite3::Tx tx) { auto uuid = needs_address.front(); return load_address( std::move(tx) , std::move(uuid) , std::move(*address) ); }); } Ev::Io load_address( Sqlite3::Tx tx , Uuid uuid , std::string address ) { tx.query(R"QRY( UPDATE SwapManager SET address = :address , state = :newstate WHERE uuid = :uuid AND state = :oldstate ; )QRY") .bind(":address", address) .bind(":oldstate" , (int) NeedsOnchainAddress ) .bind(":newstate", (int) NeedsInvoice) .bind(":uuid", std::string(uuid)) .execute(); tx.commit(); /* Pop it off. */ needs_address.pop(); auto act = Boss::log( bus, Debug , "SwapManager: Swap %s got address %s" , std::string(uuid).c_str() , std::string(address).c_str() ); /* Do we need to start up the invoice-getting loop as well? */ if (needs_invoice.empty()) { act = std::move(act).then([this]() { return Boss::concurrent( process_needs_invoice() ); }); } /* Push it in the needs-invoice loop. */ needs_invoice.push(std::move(uuid)); return std::move(act).then([]() { return Ev::yield(); }).then([this]() { return loop_needs_address(); }); } /* Processing of items in needs-invoice. */ Ev::Io process_needs_invoice() { if (getting_invoice) return Ev::lift(); getting_invoice = true; return loop_needs_invoice(); } std::vector> quotations; Ln::Amount amount; std::string address; Ev::Io loop_needs_invoice() { if (needs_invoice.empty()) { getting_invoice = false; return Ev::lift(); } quotations.clear(); return db.transact().then([this](Sqlite3::Tx tx) { /* Get the amount of the swap. */ auto uuid = needs_invoice.front(); auto fetch = tx.query(R"QRY( SELECT amount, address FROM "SwapManager" WHERE uuid = :uuid ; )QRY") .bind(":uuid", std::string(uuid)) .execute() ; for (auto& r : fetch) { amount = Ln::Amount::msat( r.get(0) ); address = r.get(1); } tx.commit(); return bus.raise(Msg::SolicitSwapQuotation{ amount, this }); }).then([this]() { auto uuid = needs_invoice.front(); return Boss::log( bus, Debug , "SwapManager: Swap %s got %zu " "quotes for amount %s." , std::string(uuid).c_str() , quotations.size() , std::string(amount).c_str() ); }).then([this]() { /* Randomize quotations. */ for (auto& q : quotations) { auto dist = std::uniform_int_distribution( 0, q.first ); q.first = dist(Boss::random_engine); } /* Sort quotations, from highest-fee to lowest-fee. */ std::sort( quotations.begin(), quotations.end() , []( std::pair const& a , std::pair const& b ) { return a.first > b.first; }); /* Enter the quotations loop. */ return loop_quotations(); }); } Ev::Io on_provide_swap_quotation( Ln::Amount fee , void* provider ) { quotations.push_back(std::make_pair(fee.to_msat(), provider)); return Ev::lift(); } /* Process the quotations. */ Ev::Io loop_quotations() { if (quotations.size() == 0) { auto uuid = std::move(needs_invoice.front()); /* Fail the current swap. */ needs_invoice.pop(); return Ev::yield().then([this, uuid]() { return swap_reduce_or_fail(uuid); }).then([this]() { /* Go to next swap. */ return loop_needs_invoice(); }); } auto const& quotation = quotations[quotations.size() - 1]; return bus.raise(Msg::AcceptSwapQuotation{ amount, address, this, quotation.second }); } /* On swap creation. */ Ev::Io on_swap_creation(Msg::SwapCreation const& s) { if (!s.success) { /* Remove a quotation and try again. */ quotations.pop_back(); return Ev::yield().then([this]() { return loop_quotations(); }); } auto swap = std::make_shared(s); /* Otherwise, set up the swap. */ return db.transact().then([this, swap](Sqlite3::Tx tx) { auto uuid = needs_invoice.front(); tx.query(R"QRY( UPDATE "SwapManager" SET state = :state , payment_hash = :payment_hash , timeout = :timeout WHERE uuid = :uuid ; )QRY") .bind(":state", (int)AwaitingResult) .bind(":payment_hash" , std::string(swap->hash) ) .bind(":timeout" , swap->timeout_blockheight ) .bind(":uuid", std::string(uuid)) .execute(); tx.commit(); /* Now send the PayInvoice message. */ return bus.raise(Msg::PayInvoice{swap->invoice}); }).then([this, swap]() { auto uuid = needs_invoice.front(); return Boss::log( bus, Debug , "SwapManager: Swap %s got " "invoice %s hash %s " "timeout %u" , std::string(uuid).c_str() , swap->invoice.c_str() , std::string(swap->hash).c_str() , (unsigned int) swap->timeout_blockheight ); }).then([]() { return Ev::yield(); }).then([this]() { needs_invoice.pop(); return loop_needs_invoice(); }); } /* Try to reduce the specified swap, and if we reduced, * put it back into the needs-invoice loop. */ Ev::Io swap_reduce_or_fail(Uuid uuid) { return db.transact().then([this, uuid](Sqlite3::Tx tx) { auto fetch = tx.query(R"QRY( SELECT amount, min_amount FROM "SwapManager" WHERE uuid = :uuid ; )QRY") .bind(":uuid", std::string(uuid)) .execute(); auto amount = std::uint64_t(); auto min_amount = std::uint64_t(); for (auto& r : fetch) { amount = r.get(0); min_amount = r.get(1); } /* cannot go lower! */ if (amount == min_amount) return fail_swap(std::move(tx), uuid); /* Reduce. */ auto dist = std::uniform_int_distribution( amount / 4, amount * 3 / 4 ); amount = dist(Boss::random_engine); if (amount < min_amount) amount = min_amount; tx.query(R"QRY( UPDATE "SwapManager" SET state = :state , amount = :amount , payment_hash = NULL , timeout = NULL WHERE uuid = :uuid ; )QRY") .bind(":state", (int)NeedsInvoice) .bind(":amount", amount) .bind(":uuid", std::string(uuid)) .execute(); tx.commit(); /* Push it back to the needs-invoice queue. */ needs_invoice.push(uuid); /* Log, then restart needs-invoice if needed. */ return Boss::log( bus, Debug , "SwapManager: Swap %s reduced to " "%zu msat." , std::string(uuid).c_str() , std::size_t(amount) ).then([this]() { return Boss::concurrent( process_needs_invoice() ); }); }); } /* Called when we know that the swap will definitely fail * as we cannot go lower. */ Ev::Io fail_swap(Sqlite3::Tx tx, Uuid uuid) { /* Move the address to the addrcache. */ auto fetch = tx.query(R"QRY( SELECT address FROM "SwapManager" WHERE uuid = :uuid ; )QRY") .bind(":uuid", std::string(uuid)) .execute(); auto address = std::string(); for (auto& r : fetch) address = r.get(0); tx.query(R"QRY( INSERT INTO "SwapManager_addrcache" VALUES(NULL, :address); )QRY") .bind(":address", address) .execute(); /* Delete the swap itself. */ tx.query(R"QRY( DELETE FROM "SwapManager" WHERE uuid = :uuid ; )QRY") .bind(":uuid", std::string(uuid)) .execute(); /* Move the db tx to a shared pointer. */ auto sh_tx = std::make_shared(std::move(tx)); /* Inform the failure. */ return Boss::log( bus, Info , "SwapManager: Swap %s failed." , std::string(uuid).c_str() ).then([this, sh_tx, uuid]() { return bus.raise(Msg::SwapResponse{ sh_tx, uuid, false, Ln::Amount() }); }).then([sh_tx]() { if (*sh_tx) sh_tx->commit(); return Ev::lift(); }); } /* Raise RequestListpays on all AwaitingResult * swaps. */ Ev::Io initiate_check_payments() { return db.transact().then([this](Sqlite3::Tx tx) { auto fetch = tx.query(R"QRY( SELECT payment_hash FROM "SwapManager" WHERE state = :state ; )QRY") .bind(":state", (int)AwaitingResult) .execute(); auto hashes = std::vector(); for (auto& r : fetch) hashes.push_back(Sha256::Hash( r.get(0) )); tx.commit(); auto f = [this](Sha256::Hash h) { return bus.raise(Msg::RequestListpays{h}); }; return Ev::foreach(f, std::move(hashes)); }); } /* Check response to RequestListpays. */ Ev::Io on_response_listpays(Msg::ResponseListpays const& r) { auto status = r.status; /* We only actually care if it failed. * For success, we only consider it if the money appears * onchain. */ if (status != Msg::StatusListpays_failed) return Ev::lift(); auto hash = r.payment_hash; return db.transact().then([this, hash](Sqlite3::Tx tx) { /* Check if it is in our table. */ auto check = tx.query(R"QRY( SELECT uuid FROM "SwapManager" WHERE payment_hash = :hash ; )QRY") .bind(":hash", std::string(hash)) .execute(); auto found = false; auto uuid = Uuid(); for (auto& r : check) { found = true; uuid = Uuid(r.get(0)); } tx.commit(); /* Not in our table. */ if (!found) return Ev::lift(); /* Lower it or fail! */ return swap_reduce_or_fail(uuid); }); } /* Check for funds appearing onchain when the swap completes. */ std::queue> onchain_funds; Ev::Io on_listfunds_result(Msg::ListfundsResult const& r) { auto outputs = r.outputs; return Ev::lift().then([this, outputs]() { auto was_empty = onchain_funds.empty(); /* Iterate over the outputs array. */ for (auto out : outputs) onchain_funds.push(std::make_pair( std::string(out["address"]), Ln::Amount(std::string( out["amount_msat"] )) )); if (was_empty) return Boss::concurrent(loop_onchain_funds()); return Ev::lift(); }); } Ev::Io loop_onchain_funds() { return Ev::yield().then([this]() { return db.transact(); }).then([this](Sqlite3::Tx tx) { if (onchain_funds.empty()) return Ev::lift(); auto fund = std::move(onchain_funds.front()); onchain_funds.pop(); auto check = tx.query(R"QRY( SELECT uuid FROM "SwapManager" WHERE address = :address AND state = :state )QRY") .bind(":address", fund.first) .bind(":state", (int)AwaitingResult) .execute(); auto found = false; auto uuid = Uuid(); for (auto& r : check) { found = true; uuid = Uuid(r.get(0)); } auto act = Ev::lift(); if (found) { /* Remove it. */ tx.query(R"QRY( DELETE FROM "SwapManager" WHERE uuid = :uuid ; )QRY") .bind(":uuid", std::string(uuid)) .execute(); /* Construct action to log it * and broadcast. */ auto sh_tx = std::make_shared( std::move(tx) ); auto amount = fund.second; act += Boss::log( bus, Info , "SwapManager: " "Swap %s completed " "with %s onchain." , std::string(uuid) .c_str() , std::string(amount) .c_str() ); act += bus.raise(Msg::SwapResponse{ sh_tx, uuid, true, amount }).then([sh_tx]() { if (*sh_tx) sh_tx->commit(); return Ev::lift(); }); act = Boss::concurrent(std::move(act)); } else tx.commit(); return std::move(act).then([this]() { return loop_onchain_funds(); }); }); } Ev::Io on_swap_request(Msg::SwapRequest const& r) { return Ev::lift().then([this, r]() { auto tx = std::move(*r.dbtx); auto uuid = r.id; /* Check the uuid is not exist yet. */ auto check = tx.query(R"QRY( SELECT uuid FROM "SwapManager" WHERE uuid = :uuid ; )QRY") .bind(":uuid", std::string(uuid)) .execute(); for (auto& r : check) { (void) r; tx.rollback(); return Boss::log( bus, Error , "SwapManager: Swap %s " "duplicated." ); } auto amount = r.max_offchain_amount; auto min_amount = r.min_offchain_amount; /* Make new entry. */ tx.query(R"QRY( INSERT INTO "SwapManager" ( uuid , amount , min_amount , state ) VALUES( :uuid , :amount , :min_amount , :state ); )QRY") .bind(":uuid", std::string(uuid)) .bind(":amount" , amount.to_msat() ) .bind(":min_amount" , min_amount.to_msat() ) .bind(":state", (int)NeedsOnchainAddress) .execute(); tx.commit(); needs_address.push(uuid); return Boss::log( bus, Info , "SwapManager: Swap %s started " "for %s." , std::string(uuid).c_str() , std::string(amount).c_str() ).then([this]() { return Boss::concurrent( process_needs_address() ); }); }); } Ev::Io on_block(std::uint32_t height) { return db.transact().then([this, height](Sqlite3::Tx tx) { /* Gather items to remove. */ auto remove = std::vector(); auto fetch = tx.query(R"QRY( SELECT uuid FROM "SwapManager" WHERE state = :state AND timeout <= :height ; )QRY") .bind(":height", height) .bind(":state", (int)AwaitingResult) .execute(); for (auto& r : fetch) remove.push_back(Uuid( r.get(0) )); tx.commit(); /* Process items to remove. */ auto f = [this](Uuid uuid) { return Boss::log( bus, Warn , "SwapManager: Swap %s " "timed out." , std::string(uuid).c_str() ).then([this, uuid]() { return swap_reduce_or_fail(uuid); }); }; return Ev::foreach(f, std::move(remove)); }); } Ev::Io on_solicit_status() { return db.transact().then([this](Sqlite3::Tx tx) { auto out = Json::Out(); auto arr = out.start_array(); auto fetch = tx.query(R"QRY( SELECT uuid -- 0 , amount -- 1 , min_amount -- 2 , state -- 3 , address -- 4 , payment_hash -- 5 , timeout -- 6 FROM "SwapManager"; )QRY").execute(); for (auto& r : fetch) { auto obj = arr.start_object(); obj.field("uuid", r.get(0)); obj.field("amount", r.get(1)); obj.field("min_amount" , r.get(2) ); obj.field("state", r.get(3)); auto state = State(r.get(3)); if (state != NeedsOnchainAddress) obj.field("address" , r.get(4) ); if (state == AwaitingResult) { obj.field("hash" , r.get(5) ); obj.field("timeout" , r.get(6) ); } obj.end_object(); } arr.end_array(); return bus.raise(Msg::ProvideStatus{ "swap_manager", std::move(out) }); }); } }; SwapManager::SwapManager(SwapManager&&) =default; SwapManager& SwapManager::operator=(SwapManager&&) =default; SwapManager::~SwapManager() =default; SwapManager::SwapManager(S::Bus& bus ) : pimpl(Util::make_unique(bus)) { } }}