Browse Source

fetchinvoice: implement timeout.

Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
ppa
Rusty Russell 4 years ago
parent
commit
dae477175c
  1. 68
      plugins/fetchinvoice.c
  2. 36
      tests/test_pay.py

68
plugins/fetchinvoice.c

@ -45,7 +45,7 @@ struct sent {
struct preimage inv_preimage; struct preimage inv_preimage;
struct json_escape *inv_label; struct json_escape *inv_label;
/* How long to wait for response before giving up. */ /* 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) static struct sent *find_sent(const struct pubkey *blinding)
@ -415,12 +415,22 @@ static void destroy_sent(struct sent *sent)
list_del(&sent->list); 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, static struct command_result *sendonionmsg_done(struct command *cmd,
const char *buf UNUSED, const char *buf UNUSED,
const jsmntok_t *result UNUSED, const jsmntok_t *result UNUSED,
struct sent *sent) 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; sent->cmd = cmd;
list_add_tail(&sent_list, &sent->list); list_add_tail(&sent_list, &sent->list);
tal_add_destructor(sent, destroy_sent); tal_add_destructor(sent, destroy_sent);
@ -631,7 +641,7 @@ static struct command_result *prepare_inv_timeout(struct command *cmd,
struct sent *sent) struct sent *sent)
{ {
tal_steal(cmd, plugin_timer(cmd->plugin, tal_steal(cmd, plugin_timer(cmd->plugin,
time_from_sec(sent->inv_wait_timeout), time_from_sec(sent->wait_timeout),
timeout_sent_inv, sent)); timeout_sent_inv, sent));
return sendonionmsg_done(cmd, buf, result, 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, static struct command_result *invreq_done(struct command *cmd,
const char *buf, const char *buf,
const jsmntok_t *result, const jsmntok_t *result,
struct tlv_offer *offer) struct sent *sent)
{ {
const jsmntok_t *t; const jsmntok_t *t;
struct sent *sent;
char *fail; char *fail;
u8 *rawinvreq; 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 */ /* Get invoice request */
t = json_get_member(buf, result, "bolt12"); t = json_get_member(buf, result, "bolt12");
if (!t) if (!t)
@ -688,16 +693,16 @@ static struct command_result *json_fetchinvoice(struct command *cmd,
const char *buffer, const char *buffer,
const jsmntok_t *params) const jsmntok_t *params)
{ {
struct tlv_offer *offer;
struct amount_msat *msat; struct amount_msat *msat;
const char *rec_label; const char *rec_label;
struct out_req *req; struct out_req *req;
struct tlv_invoice_request *invreq; 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, 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("msatoshi", param_msat, &msat),
p_opt("quantity", param_u64, &invreq->quantity), p_opt("quantity", param_u64, &invreq->quantity),
p_opt("recurrence_counter", param_number, 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, p_opt("recurrence_start", param_number,
&invreq->recurrence_start), &invreq->recurrence_start),
p_opt("recurrence_label", param_string, &rec_label), p_opt("recurrence_label", param_string, &rec_label),
p_opt_def("timeout", param_number, &timeout, 60),
NULL)) NULL))
return command_param_failed(); return command_param_failed();
sent->wait_timeout = *timeout;
/* BOLT-offers #12: /* BOLT-offers #12:
* - MUST set `offer_id` to the merkle root of the offer as described * - MUST set `offer_id` to the merkle root of the offer as described
* in [Signature Calculation](#signature-calculation). * in [Signature Calculation](#signature-calculation).
*/ */
invreq->offer_id = tal(invreq, struct sha256); 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. */ /* 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, return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"Offer wants an invoice, not invoice_request"); "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 * - SHOULD not respond to an offer if the current time is after
* `absolute_expiry`. * `absolute_expiry`.
*/ */
if (offer->absolute_expiry if (sent->offer->absolute_expiry
&& time_now().ts.tv_sec > *offer->absolute_expiry) && time_now().ts.tv_sec > *sent->offer->absolute_expiry)
return command_fail(cmd, OFFER_EXPIRED, "Offer expired"); return command_fail(cmd, OFFER_EXPIRED, "Offer expired");
/* BOLT-offers #12: /* BOLT-offers #12:
@ -736,7 +744,7 @@ static struct command_result *json_fetchinvoice(struct command *cmd,
* - otherwise: * - otherwise:
* - MUST NOT set `amount` * - MUST NOT set `amount`
*/ */
if (offer->amount) { if (sent->offer->amount) {
if (msat) if (msat)
return command_fail(cmd, JSONRPC2_INVALID_PARAMS, return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"msatoshi parameter unnecessary"); "msatoshi parameter unnecessary");
@ -755,20 +763,20 @@ static struct command_result *json_fetchinvoice(struct command *cmd,
* - otherwise: * - otherwise:
* - MUST NOT set `quantity` * - MUST NOT set `quantity`
*/ */
if (offer->quantity_min || offer->quantity_max) { if (sent->offer->quantity_min || sent->offer->quantity_max) {
if (!invreq->quantity) if (!invreq->quantity)
return command_fail(cmd, JSONRPC2_INVALID_PARAMS, return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"quantity parameter required"); "quantity parameter required");
if (offer->quantity_min if (sent->offer->quantity_min
&& *invreq->quantity < *offer->quantity_min) && *invreq->quantity < *sent->offer->quantity_min)
return command_fail(cmd, JSONRPC2_INVALID_PARAMS, return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"quantity must be >= %"PRIu64, "quantity must be >= %"PRIu64,
*offer->quantity_min); *sent->offer->quantity_min);
if (offer->quantity_max if (sent->offer->quantity_max
&& *invreq->quantity > *offer->quantity_max) && *invreq->quantity > *sent->offer->quantity_max)
return command_fail(cmd, JSONRPC2_INVALID_PARAMS, return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"quantity must be <= %"PRIu64, "quantity must be <= %"PRIu64,
*offer->quantity_max); *sent->offer->quantity_max);
} else { } else {
if (invreq->quantity) if (invreq->quantity)
return command_fail(cmd, JSONRPC2_INVALID_PARAMS, return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
@ -778,7 +786,7 @@ static struct command_result *json_fetchinvoice(struct command *cmd,
/* BOLT-offers #12: /* BOLT-offers #12:
* - if the offer contained `recurrence`: * - if the offer contained `recurrence`:
*/ */
if (offer->recurrence) { if (sent->offer->recurrence) {
/* BOLT-offers #12: /* BOLT-offers #12:
* - for the initial request: * - for the initial request:
*... *...
@ -802,8 +810,8 @@ static struct command_result *json_fetchinvoice(struct command *cmd,
* - otherwise: * - otherwise:
* - MUST NOT include `recurrence_start` * - MUST NOT include `recurrence_start`
*/ */
if (offer->recurrence_base if (sent->offer->recurrence_base
&& offer->recurrence_base->start_any_period) { && sent->offer->recurrence_base->start_any_period) {
if (!invreq->recurrence_start) if (!invreq->recurrence_start)
return command_fail(cmd, JSONRPC2_INVALID_PARAMS, return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"needs recurrence_start"); "needs recurrence_start");
@ -860,7 +868,7 @@ static struct command_result *json_fetchinvoice(struct command *cmd,
req = jsonrpc_request_start(cmd->plugin, cmd, "createinvoicerequest", req = jsonrpc_request_start(cmd->plugin, cmd, "createinvoicerequest",
&invreq_done, &invreq_done,
&forward_error, &forward_error,
offer); sent);
json_add_string(req->js, "bolt12", invrequest_encode(tmpctx, invreq)); json_add_string(req->js, "bolt12", invrequest_encode(tmpctx, invreq));
if (rec_label) if (rec_label)
json_add_string(req->js, "recurrence_label", 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(); return command_param_failed();
/* This is how long we'll wait for a reply for. */ /* 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. */ /* Check they are really trying to send us money. */
if (!sent->offer->send_invoice) if (!sent->offer->send_invoice)

36
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) l1, l2, l3 = node_factory.line_graph(3, wait_for_announce=True)
# Simple offer first. # Simple offer first.
offer = l3.rpc.call('offer', {'amount': '1msat', offer1 = l3.rpc.call('offer', {'amount': '1msat',
'description': 'simple test'})['bolt12'] 'description': 'simple test'})['bolt12']
print(offer)
inv1 = l1.rpc.call('fetchinvoice', {'offer': offer}) inv1 = l1.rpc.call('fetchinvoice', {'offer': offer1})
inv2 = l1.rpc.call('fetchinvoice', {'offer': offer}) inv2 = l1.rpc.call('fetchinvoice', {'offer': offer1})
assert inv1 != inv2 assert inv1 != inv2
assert 'next_period' not in inv1 assert 'next_period' not in inv1
assert 'next_period' not in inv2 assert 'next_period' not in inv2
@ -3863,13 +3862,12 @@ def test_fetchinvoice(node_factory, bitcoind):
l1.rpc.pay(inv2['invoice']) l1.rpc.pay(inv2['invoice'])
# Single-use invoice can be fetched multiple times, only paid once. # Single-use invoice can be fetched multiple times, only paid once.
offer = l3.rpc.call('offer', {'amount': '1msat', offer2 = l3.rpc.call('offer', {'amount': '1msat',
'description': 'single-use test', 'description': 'single-use test',
'single_use': True})['bolt12'] 'single_use': True})['bolt12']
print(offer)
inv1 = l1.rpc.call('fetchinvoice', {'offer': offer}) inv1 = l1.rpc.call('fetchinvoice', {'offer': offer2})
inv2 = l1.rpc.call('fetchinvoice', {'offer': offer}) inv2 = l1.rpc.call('fetchinvoice', {'offer': offer2})
assert inv1 != inv2 assert inv1 != inv2
assert 'next_period' not in inv1 assert 'next_period' not in inv1
assert 'next_period' not in inv2 assert 'next_period' not in inv2
@ -3882,18 +3880,16 @@ def test_fetchinvoice(node_factory, bitcoind):
# We can't reuse the offer, either. # We can't reuse the offer, either.
with pytest.raises(RpcError, match='Offer no longer available'): with pytest.raises(RpcError, match='Offer no longer available'):
l1.rpc.call('fetchinvoice', {'offer': offer}) l1.rpc.call('fetchinvoice', {'offer': offer2})
# Recurring offer. # Recurring offer.
offer = l2.rpc.call('offer', {'amount': '1msat', offer3 = l2.rpc.call('offer', {'amount': '1msat',
'description': 'recurring test', 'description': 'recurring test',
'recurrence': '1minutes'})['bolt12'] 'recurrence': '1minutes'})['bolt12']
print(offer)
ret = l1.rpc.call('fetchinvoice', {'offer': offer, ret = l1.rpc.call('fetchinvoice', {'offer': offer3,
'recurrence_counter': 0, 'recurrence_counter': 0,
'recurrence_label': 'test recurrence'}) 'recurrence_label': 'test recurrence'})
print(ret)
period1 = ret['next_period'] period1 = ret['next_period']
assert period1['counter'] == 1 assert period1['counter'] == 1
assert period1['endtime'] == period1['starttime'] + 59 assert period1['endtime'] == period1['starttime'] + 59
@ -3902,10 +3898,9 @@ def test_fetchinvoice(node_factory, bitcoind):
l1.rpc.pay(ret['invoice'], label='test recurrence') 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_counter': 1,
'recurrence_label': 'test recurrence'}) 'recurrence_label': 'test recurrence'})
print(ret)
period2 = ret['next_period'] period2 = ret['next_period']
assert period2['counter'] == 2 assert period2['counter'] == 2
assert period2['starttime'] == period1['endtime'] + 1 assert period2['starttime'] == period1['endtime'] + 1
@ -3915,7 +3910,7 @@ def test_fetchinvoice(node_factory, bitcoind):
# Can't request 2 before paying 1. # Can't request 2 before paying 1.
with pytest.raises(RpcError, match='previous invoice has not been paid'): 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_counter': 2,
'recurrence_label': 'test recurrence'}) 'recurrence_label': 'test recurrence'})
@ -3923,7 +3918,7 @@ def test_fetchinvoice(node_factory, bitcoind):
# Now we can, but it's too early: # Now we can, but it's too early:
with pytest.raises(RpcError, match='Remote node sent failure message.*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_counter': 2,
'recurrence_label': 'test recurrence'}) 'recurrence_label': 'test recurrence'})
@ -3931,10 +3926,15 @@ def test_fetchinvoice(node_factory, bitcoind):
while time.time() < period1['starttime']: while time.time() < period1['starttime']:
time.sleep(1) time.sleep(1)
l1.rpc.call('fetchinvoice', {'offer': offer, l1.rpc.call('fetchinvoice', {'offer': offer3,
'recurrence_counter': 2, 'recurrence_counter': 2,
'recurrence_label': 'test recurrence'}) '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): def test_pay_waitblockheight_timeout(node_factory, bitcoind):
plugin = os.path.join(os.path.dirname(__file__), 'plugins', 'endlesswaitblockheight.py') plugin = os.path.join(os.path.dirname(__file__), 'plugins', 'endlesswaitblockheight.py')

Loading…
Cancel
Save