From dae477175ce89850903b334b23425ff33c45b3a3 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Fri, 8 Jan 2021 05:10:47 +1030 Subject: [PATCH] fetchinvoice: implement timeout. Signed-off-by: Rusty Russell --- plugins/fetchinvoice.c | 68 +++++++++++++++++++++++------------------- tests/test_pay.py | 46 ++++++++++++++-------------- 2 files changed, 61 insertions(+), 53 deletions(-) diff --git a/plugins/fetchinvoice.c b/plugins/fetchinvoice.c index 078c5e0bf..4d6581fbd 100644 --- a/plugins/fetchinvoice.c +++ b/plugins/fetchinvoice.c @@ -45,7 +45,7 @@ struct sent { struct preimage inv_preimage; struct json_escape *inv_label; /* How long to wait for response before giving up. */ - u32 inv_wait_timeout; + u32 wait_timeout; }; static struct sent *find_sent(const struct pubkey *blinding) @@ -415,12 +415,22 @@ static void destroy_sent(struct sent *sent) list_del(&sent->list); } +/* We've received neither a reply nor a payment; return failure. */ +static void timeout_sent_invreq(struct sent *sent) +{ + /* This will free sent! */ + discard_result(command_fail(sent->cmd, OFFER_TIMEOUT, + "Timeout waiting for response")); +} + static struct command_result *sendonionmsg_done(struct command *cmd, const char *buf UNUSED, const jsmntok_t *result UNUSED, struct sent *sent) { - /* FIXME: timeout! */ + tal_steal(cmd, plugin_timer(cmd->plugin, + time_from_sec(sent->wait_timeout), + timeout_sent_invreq, sent)); sent->cmd = cmd; list_add_tail(&sent_list, &sent->list); tal_add_destructor(sent, destroy_sent); @@ -631,7 +641,7 @@ static struct command_result *prepare_inv_timeout(struct command *cmd, struct sent *sent) { tal_steal(cmd, plugin_timer(cmd->plugin, - time_from_sec(sent->inv_wait_timeout), + time_from_sec(sent->wait_timeout), timeout_sent_inv, sent)); return sendonionmsg_done(cmd, buf, result, sent); } @@ -639,17 +649,12 @@ static struct command_result *prepare_inv_timeout(struct command *cmd, static struct command_result *invreq_done(struct command *cmd, const char *buf, const jsmntok_t *result, - struct tlv_offer *offer) + struct sent *sent) { const jsmntok_t *t; - struct sent *sent; char *fail; u8 *rawinvreq; - /* We need to remember both offer and invreq to check reply. */ - sent = tal(cmd, struct sent); - sent->offer = tal_steal(sent, offer); - /* Get invoice request */ t = json_get_member(buf, result, "bolt12"); if (!t) @@ -688,16 +693,16 @@ static struct command_result *json_fetchinvoice(struct command *cmd, const char *buffer, const jsmntok_t *params) { - struct tlv_offer *offer; struct amount_msat *msat; const char *rec_label; struct out_req *req; struct tlv_invoice_request *invreq; + struct sent *sent = tal(cmd, struct sent); + u32 *timeout; - invreq = tlv_invoice_request_new(cmd); - + invreq = tlv_invoice_request_new(sent); if (!param(cmd, buffer, params, - p_req("offer", param_offer, &offer), + p_req("offer", param_offer, &sent->offer), p_opt("msatoshi", param_msat, &msat), p_opt("quantity", param_u64, &invreq->quantity), p_opt("recurrence_counter", param_number, @@ -705,18 +710,21 @@ static struct command_result *json_fetchinvoice(struct command *cmd, p_opt("recurrence_start", param_number, &invreq->recurrence_start), p_opt("recurrence_label", param_string, &rec_label), + p_opt_def("timeout", param_number, &timeout, 60), NULL)) return command_param_failed(); + sent->wait_timeout = *timeout; + /* BOLT-offers #12: * - MUST set `offer_id` to the merkle root of the offer as described * in [Signature Calculation](#signature-calculation). */ invreq->offer_id = tal(invreq, struct sha256); - merkle_tlv(offer->fields, invreq->offer_id); + merkle_tlv(sent->offer->fields, invreq->offer_id); /* Check if they are trying to send us money. */ - if (offer->send_invoice) + if (sent->offer->send_invoice) return command_fail(cmd, JSONRPC2_INVALID_PARAMS, "Offer wants an invoice, not invoice_request"); @@ -724,8 +732,8 @@ static struct command_result *json_fetchinvoice(struct command *cmd, * - SHOULD not respond to an offer if the current time is after * `absolute_expiry`. */ - if (offer->absolute_expiry - && time_now().ts.tv_sec > *offer->absolute_expiry) + if (sent->offer->absolute_expiry + && time_now().ts.tv_sec > *sent->offer->absolute_expiry) return command_fail(cmd, OFFER_EXPIRED, "Offer expired"); /* BOLT-offers #12: @@ -736,7 +744,7 @@ static struct command_result *json_fetchinvoice(struct command *cmd, * - otherwise: * - MUST NOT set `amount` */ - if (offer->amount) { + if (sent->offer->amount) { if (msat) return command_fail(cmd, JSONRPC2_INVALID_PARAMS, "msatoshi parameter unnecessary"); @@ -755,20 +763,20 @@ static struct command_result *json_fetchinvoice(struct command *cmd, * - otherwise: * - MUST NOT set `quantity` */ - if (offer->quantity_min || offer->quantity_max) { + if (sent->offer->quantity_min || sent->offer->quantity_max) { if (!invreq->quantity) return command_fail(cmd, JSONRPC2_INVALID_PARAMS, "quantity parameter required"); - if (offer->quantity_min - && *invreq->quantity < *offer->quantity_min) + if (sent->offer->quantity_min + && *invreq->quantity < *sent->offer->quantity_min) return command_fail(cmd, JSONRPC2_INVALID_PARAMS, "quantity must be >= %"PRIu64, - *offer->quantity_min); - if (offer->quantity_max - && *invreq->quantity > *offer->quantity_max) + *sent->offer->quantity_min); + if (sent->offer->quantity_max + && *invreq->quantity > *sent->offer->quantity_max) return command_fail(cmd, JSONRPC2_INVALID_PARAMS, "quantity must be <= %"PRIu64, - *offer->quantity_max); + *sent->offer->quantity_max); } else { if (invreq->quantity) return command_fail(cmd, JSONRPC2_INVALID_PARAMS, @@ -778,7 +786,7 @@ static struct command_result *json_fetchinvoice(struct command *cmd, /* BOLT-offers #12: * - if the offer contained `recurrence`: */ - if (offer->recurrence) { + if (sent->offer->recurrence) { /* BOLT-offers #12: * - for the initial request: *... @@ -802,8 +810,8 @@ static struct command_result *json_fetchinvoice(struct command *cmd, * - otherwise: * - MUST NOT include `recurrence_start` */ - if (offer->recurrence_base - && offer->recurrence_base->start_any_period) { + if (sent->offer->recurrence_base + && sent->offer->recurrence_base->start_any_period) { if (!invreq->recurrence_start) return command_fail(cmd, JSONRPC2_INVALID_PARAMS, "needs recurrence_start"); @@ -860,7 +868,7 @@ static struct command_result *json_fetchinvoice(struct command *cmd, req = jsonrpc_request_start(cmd->plugin, cmd, "createinvoicerequest", &invreq_done, &forward_error, - offer); + sent); json_add_string(req->js, "bolt12", invrequest_encode(tmpctx, invreq)); if (rec_label) json_add_string(req->js, "recurrence_label", rec_label); @@ -1090,7 +1098,7 @@ static struct command_result *json_sendinvoice(struct command *cmd, return command_param_failed(); /* This is how long we'll wait for a reply for. */ - sent->inv_wait_timeout = *timeout; + sent->wait_timeout = *timeout; /* Check they are really trying to send us money. */ if (!sent->offer->send_invoice) diff --git a/tests/test_pay.py b/tests/test_pay.py index 7109094dc..48d31ecfb 100644 --- a/tests/test_pay.py +++ b/tests/test_pay.py @@ -3850,12 +3850,11 @@ def test_fetchinvoice(node_factory, bitcoind): l1, l2, l3 = node_factory.line_graph(3, wait_for_announce=True) # Simple offer first. - offer = l3.rpc.call('offer', {'amount': '1msat', - 'description': 'simple test'})['bolt12'] - print(offer) + offer1 = l3.rpc.call('offer', {'amount': '1msat', + 'description': 'simple test'})['bolt12'] - inv1 = l1.rpc.call('fetchinvoice', {'offer': offer}) - inv2 = l1.rpc.call('fetchinvoice', {'offer': offer}) + inv1 = l1.rpc.call('fetchinvoice', {'offer': offer1}) + inv2 = l1.rpc.call('fetchinvoice', {'offer': offer1}) assert inv1 != inv2 assert 'next_period' not in inv1 assert 'next_period' not in inv2 @@ -3863,13 +3862,12 @@ def test_fetchinvoice(node_factory, bitcoind): l1.rpc.pay(inv2['invoice']) # Single-use invoice can be fetched multiple times, only paid once. - offer = l3.rpc.call('offer', {'amount': '1msat', - 'description': 'single-use test', - 'single_use': True})['bolt12'] - print(offer) + offer2 = l3.rpc.call('offer', {'amount': '1msat', + 'description': 'single-use test', + 'single_use': True})['bolt12'] - inv1 = l1.rpc.call('fetchinvoice', {'offer': offer}) - inv2 = l1.rpc.call('fetchinvoice', {'offer': offer}) + inv1 = l1.rpc.call('fetchinvoice', {'offer': offer2}) + inv2 = l1.rpc.call('fetchinvoice', {'offer': offer2}) assert inv1 != inv2 assert 'next_period' not in inv1 assert 'next_period' not in inv2 @@ -3882,18 +3880,16 @@ def test_fetchinvoice(node_factory, bitcoind): # We can't reuse the offer, either. with pytest.raises(RpcError, match='Offer no longer available'): - l1.rpc.call('fetchinvoice', {'offer': offer}) + l1.rpc.call('fetchinvoice', {'offer': offer2}) # Recurring offer. - offer = l2.rpc.call('offer', {'amount': '1msat', - 'description': 'recurring test', - 'recurrence': '1minutes'})['bolt12'] - print(offer) + offer3 = l2.rpc.call('offer', {'amount': '1msat', + 'description': 'recurring test', + 'recurrence': '1minutes'})['bolt12'] - ret = l1.rpc.call('fetchinvoice', {'offer': offer, + ret = l1.rpc.call('fetchinvoice', {'offer': offer3, 'recurrence_counter': 0, 'recurrence_label': 'test recurrence'}) - print(ret) period1 = ret['next_period'] assert period1['counter'] == 1 assert period1['endtime'] == period1['starttime'] + 59 @@ -3902,10 +3898,9 @@ def test_fetchinvoice(node_factory, bitcoind): l1.rpc.pay(ret['invoice'], label='test recurrence') - ret = l1.rpc.call('fetchinvoice', {'offer': offer, + ret = l1.rpc.call('fetchinvoice', {'offer': offer3, 'recurrence_counter': 1, 'recurrence_label': 'test recurrence'}) - print(ret) period2 = ret['next_period'] assert period2['counter'] == 2 assert period2['starttime'] == period1['endtime'] + 1 @@ -3915,7 +3910,7 @@ def test_fetchinvoice(node_factory, bitcoind): # Can't request 2 before paying 1. with pytest.raises(RpcError, match='previous invoice has not been paid'): - l1.rpc.call('fetchinvoice', {'offer': offer, + l1.rpc.call('fetchinvoice', {'offer': offer3, 'recurrence_counter': 2, 'recurrence_label': 'test recurrence'}) @@ -3923,7 +3918,7 @@ def test_fetchinvoice(node_factory, bitcoind): # Now we can, but it's too early: with pytest.raises(RpcError, match='Remote node sent failure message.*too early'): - l1.rpc.call('fetchinvoice', {'offer': offer, + l1.rpc.call('fetchinvoice', {'offer': offer3, 'recurrence_counter': 2, 'recurrence_label': 'test recurrence'}) @@ -3931,10 +3926,15 @@ def test_fetchinvoice(node_factory, bitcoind): while time.time() < period1['starttime']: time.sleep(1) - l1.rpc.call('fetchinvoice', {'offer': offer, + l1.rpc.call('fetchinvoice', {'offer': offer3, 'recurrence_counter': 2, 'recurrence_label': 'test recurrence'}) + # Test timeout. + l3.stop() + with pytest.raises(RpcError, match='Timeout waiting for response'): + l1.rpc.call('fetchinvoice', {'offer': offer1, 'timeout': 10}) + def test_pay_waitblockheight_timeout(node_factory, bitcoind): plugin = os.path.join(os.path.dirname(__file__), 'plugins', 'endlesswaitblockheight.py')