fetchinvoice: implement timeout.

Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
This commit is contained in:
Rusty Russell 2021-01-08 05:10:47 +10:30
parent 075c25fc08
commit dae477175c
2 changed files with 61 additions and 53 deletions

View File

@ -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)

View File

@ -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')