From 59abc37258d886653fa9a261cc6b15c41122a7b0 Mon Sep 17 00:00:00 2001 From: ZmnSCPxj Date: Tue, 23 Jan 2018 01:02:10 +0000 Subject: [PATCH] invoices: Add expiration timer system. Fixes: #502 Changes behavior of waitinvoice API!! --- lightningd/invoice.c | 6 +-- wallet/invoices.c | 103 +++++++++++++++++++++++++++++++++++++++++-- wallet/invoices.h | 1 - wallet/test/Makefile | 1 + wallet/wallet.h | 5 ++- 5 files changed, 108 insertions(+), 8 deletions(-) diff --git a/lightningd/invoice.c b/lightningd/invoice.c index 0419b7659..28f131aa8 100644 --- a/lightningd/invoice.c +++ b/lightningd/invoice.c @@ -23,7 +23,7 @@ static const char *invoice_status_str(const struct invoice *inv) { if (inv->state == PAID) return "paid"; - if (time_now().ts.tv_sec > inv->expiry_time) + if (inv->state == EXPIRED) return "expired"; return "unpaid"; } @@ -396,7 +396,7 @@ static void json_waitinvoice(struct command *cmd, if (!i) { command_fail(cmd, "Label not found"); return; - } else if (i->state == PAID) { + } else if (i->state == PAID || i->state == EXPIRED) { tell_waiter(cmd, i); return; } else { @@ -410,7 +410,7 @@ static void json_waitinvoice(struct command *cmd, static const struct json_command waitinvoice_command = { "waitinvoice", json_waitinvoice, - "Wait for an incoming payment matching the invoice with {label}" + "Wait for an incoming payment matching the invoice with {label}, or if the invoice expires" }; AUTODATA(json_command, &waitinvoice_command); diff --git a/wallet/invoices.c b/wallet/invoices.c index a0162b2b7..b237c6de5 100644 --- a/wallet/invoices.c +++ b/wallet/invoices.c @@ -11,6 +11,8 @@ #include #include #include +#include +#include #include struct invoice_waiter { @@ -69,6 +71,8 @@ static bool wallet_stmt2invoice(sqlite3_stmt *stmt, struct invoice *inv) } list_head_init(&inv->waitone_waiters); + inv->expiration_timer = NULL; + return true; } @@ -89,13 +93,96 @@ struct invoices *invoices_new(const tal_t *ctx, return invs; } +static void install_expiration_timer(struct invoices *invoices, + struct invoice *i); +static void trigger_expiration(struct invoice *i) +{ + struct invoices *invoices = i->owner; + u64 now = time_now().ts.tv_sec; + sqlite3_stmt *stmt; + struct invoice_waiter *w; + + assert(i->state == UNPAID); + + /* Timer already triggered, destroy the timer object. */ + i->expiration_timer = tal_free(i->expiration_timer); + + /* There may be discrepancies between time_mono + * (used by the timer system) and time_now (used + * by the expiry time measurements). So check that + * time_now is reached. */ + if (i->expiry_time <= now) { + const tal_t *tmpctx = tal_tmpctx(i); + + /* Update in-memory and db. */ + i->state = EXPIRED; + stmt = db_prepare(invoices->db, + "UPDATE invoices" + " SET state = ?" + " WHERE id = ?;"); + sqlite3_bind_int(stmt, 1, EXPIRED); + sqlite3_bind_int64(stmt, 2, i->id); + db_exec_prepared(invoices->db, stmt); + /* Wake up all waiters. */ + while ((w = list_pop(&i->waitone_waiters, + struct invoice_waiter, + list)) != NULL) { + tal_steal(tmpctx, w); + trigger_invoice_waiter(w, i); + } + + tal_free(tmpctx); + } else + install_expiration_timer(invoices, i); + +} +static void install_expiration_timer(struct invoices *invoices, + struct invoice *i) +{ + struct timerel rel; + struct timeabs expiry; + struct timeabs now = time_now(); + + assert(i->state == UNPAID); + assert(!i->expiration_timer); + + memset(&expiry, 0, sizeof(expiry)); + expiry.ts.tv_sec = i->expiry_time; + + /* now > expiry */ + if (time_after(now, expiry)) + expiry = now; + + /* rel = expiry - now */ + rel = time_between(expiry, now); + + /* The oneshot is parented on the invoice. Thus if + * the invoice is deleted, the oneshot is destroyed + * also and this removes the timer. */ + i->expiration_timer = new_reltimer(invoices->timers, + i, + rel, + &trigger_expiration, i); +} bool invoices_load(struct invoices *invoices) { int count = 0; + u64 now = time_now().ts.tv_sec; struct invoice *i; sqlite3_stmt *stmt; + /* Update expirations. */ + stmt = db_prepare(invoices->db, + "UPDATE invoices" + " SET state = ?" + " WHERE state = ?" + " AND expiry_time <= ?;"); + sqlite3_bind_int(stmt, 1, EXPIRED); + sqlite3_bind_int(stmt, 2, UNPAID); + sqlite3_bind_int64(stmt, 3, now); + db_exec_prepared(invoices->db, stmt); + /* Load invoices from db. */ stmt = db_query(__func__, invoices->db, "SELECT id, state, payment_key, payment_hash" @@ -109,17 +196,21 @@ bool invoices_load(struct invoices *invoices) while (sqlite3_step(stmt) == SQLITE_ROW) { i = tal(invoices, struct invoice); + i->owner = invoices; if (!wallet_stmt2invoice(stmt, i)) { log_broken(invoices->log, "Error deserializing invoice"); sqlite3_finalize(stmt); return false; } + if (i->state == UNPAID) + install_expiration_timer(invoices, i); list_add_tail(&invoices->invlist, &i->list); count++; } log_debug(invoices->log, "Loaded %d invoices from DB", count); sqlite3_finalize(stmt); + return true; } @@ -133,6 +224,7 @@ const struct invoice *invoices_create(struct invoices *invoices, struct preimage r; struct sha256 rhash; u64 expiry_time; + u64 now = time_now().ts.tv_sec; if (invoices_find_by_label(invoices, label)) { if (taken(msatoshi)) @@ -143,7 +235,7 @@ const struct invoice *invoices_create(struct invoices *invoices, } /* Compute expiration. */ - expiry_time = time_now().ts.tv_sec + expiry; + expiry_time = now + expiry; /* Generate random secret preimage and hash. */ randombytes_buf(r.r, sizeof(r.r)); sha256(&rhash, r.r, sizeof(r.r)); @@ -177,6 +269,7 @@ const struct invoice *invoices_create(struct invoices *invoices, /* Create and load in-memory structure. */ invoice = tal(invoices, struct invoice); + invoice->owner = invoices; invoice->id = sqlite3_last_insert_rowid(invoices->db->sql); invoice->state = UNPAID; @@ -186,10 +279,14 @@ const struct invoice *invoices_create(struct invoices *invoices, memcpy(&invoice->rhash, &rhash, sizeof(invoice->rhash)); invoice->expiry_time = expiry_time; list_head_init(&invoice->waitone_waiters); + invoice->expiration_timer = NULL; /* Add to invoices object. */ list_add_tail(&invoices->invlist, &invoice->list); + /* Install expiration trigger. */ + install_expiration_timer(invoices, invoice); + return invoice; } @@ -314,6 +411,7 @@ void invoices_resolve(struct invoices *invoices, invoice->pay_index = pay_index; invoice->msatoshi_received = msatoshi_received; invoice->paid_timestamp = paid_timestamp; + invoice->expiration_timer = tal_free(invoice->expiration_timer); /* Tell all the waitany waiters about the new paid invoice. */ while ((w = list_pop(&invoices->waitany_waiters, @@ -408,8 +506,7 @@ void invoices_waitone(const tal_t *ctx, void *cbarg) { struct invoice *invoice = (struct invoice*) cinvoice; - /* FIXME: Handle expired state. */ - if (invoice->state == PAID) { + if (invoice->state == PAID || invoice->state == EXPIRED) { cb(invoice, cbarg); return; } diff --git a/wallet/invoices.h b/wallet/invoices.h index 57f84818b..b9ca50e8d 100644 --- a/wallet/invoices.h +++ b/wallet/invoices.h @@ -152,7 +152,6 @@ void invoices_waitany(const tal_t *ctx, * invoice. * @cbarg - the callback data. * - * FIXME: actually trigger on expired invoices. */ void invoices_waitone(const tal_t *ctx, struct invoices *invoices, diff --git a/wallet/test/Makefile b/wallet/test/Makefile index baed27147..8154b4775 100644 --- a/wallet/test/Makefile +++ b/wallet/test/Makefile @@ -7,6 +7,7 @@ WALLET_TEST_COMMON_OBJS := \ common/type_to_string.o \ common/memleak.o \ common/pseudorand.o \ + common/timeout.o \ common/utils.o \ common/wireaddr.o \ wallet/invoices.o \ diff --git a/wallet/wallet.h b/wallet/wallet.h index 809a0cdaf..01ee62895 100644 --- a/wallet/wallet.h +++ b/wallet/wallet.h @@ -16,6 +16,7 @@ struct invoices; struct lightningd; +struct oneshot; struct pubkey; struct timers; @@ -362,6 +363,7 @@ bool wallet_htlcs_reconnect(struct wallet *wallet, enum invoice_status { UNPAID, PAID, + EXPIRED, }; struct invoice { @@ -375,6 +377,8 @@ struct invoice { struct list_head waitone_waiters; /* Any expiration timer in effect */ struct oneshot *expiration_timer; + /* The owning invoices object. */ + struct invoices *owner; /* Publicly-useable fields. */ enum invoice_status state; @@ -527,7 +531,6 @@ void wallet_invoice_waitany(const tal_t *ctx, * invoice. * @cbarg - the callback data. * - * FIXME: actually trigger on expired invoices. */ void wallet_invoice_waitone(const tal_t *ctx, struct wallet *wallet,