mirror of
https://github.com/aljazceru/lightning.git
synced 2025-12-19 15:14:23 +01:00
fetchinvoice: implement timeout.
Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
Reference in New Issue
Block a user