diff --git a/common/json_stream.c b/common/json_stream.c index 7ef3750ddf9b..1abfaed289cc 100644 --- a/common/json_stream.c +++ b/common/json_stream.c @@ -370,11 +370,11 @@ void json_add_timeabs(struct json_stream *result, const char *fieldname, struct timeabs t) { json_add_primitive_fmt(result, fieldname, - "%" PRIu64 ".%03" PRIu64, - (u64)t.ts.tv_sec, (u64)t.ts.tv_nsec / 1000000); + "%" PRIu64 ".%09" PRIu64, + (u64)t.ts.tv_sec, (u64)t.ts.tv_nsec); } -void json_add_time(struct json_stream *result, const char *fieldname, +void json_add_timestr(struct json_stream *result, const char *fieldname, struct timespec ts) { char timebuf[100]; diff --git a/common/json_stream.h b/common/json_stream.h index bc68569caa08..4b37aee83ab5 100644 --- a/common/json_stream.h +++ b/common/json_stream.h @@ -255,7 +255,7 @@ void json_add_timeabs(struct json_stream *result, const char *fieldname, struct timeabs t); /* used in log.c and notification.c*/ -void json_add_time(struct json_stream *result, const char *fieldname, +void json_add_timestr(struct json_stream *result, const char *fieldname, struct timespec ts); /* Add ISO_8601 timestamp string, i.e. "2019-09-07T15:50+01:00" */ diff --git a/doc/lightning-commando-listrunes.7.md b/doc/lightning-commando-listrunes.7.md index 5e626a0b6c3f..8f9c1fdbabb8 100644 --- a/doc/lightning-commando-listrunes.7.md +++ b/doc/lightning-commando-listrunes.7.md @@ -31,6 +31,7 @@ On success, an object containing **runes** is returned. It is an array of objec - **restrictions\_as\_english** (string): English readable description of the restrictions array above - **stored** (boolean, optional): This is false if the rune does not appear in our datastore (only possible when `rune` is specified) (always *false*) - **blacklisted** (boolean, optional): The rune has been blacklisted; see commando-blacklist(7) (always *true*) +- **last\_used** (number, optional): The last time this rune was successfully used *(added 23.11)* - **our\_rune** (boolean, optional): This is not a rune for this node (only possible when `rune` is specified) (always *false*) [comment]: # (GENERATE-FROM-SCHEMA-END) @@ -50,4 +51,4 @@ RESOURCES Main web site: -[comment]: # ( SHA256STAMP:cd0e75bbeef3d5824448f67485de4679b0c163e97f405673b2ba9495f970d498) +[comment]: # ( SHA256STAMP:900e91777cd1e181c87a78913ab6f914585fcd99cd0dba16da19a81159f98aea) diff --git a/doc/lightning-createrune.7.md b/doc/lightning-createrune.7.md index 35d34176c074..00e711f5ccb5 100644 --- a/doc/lightning-createrune.7.md +++ b/doc/lightning-createrune.7.md @@ -36,6 +36,7 @@ being run: * id: the node\_id of the peer, e.g. "id=024b9a1fa8e006f1e3937f65f66c408e6da8e1ca728ea43222a7381df1cc449605". * method: the command being run, e.g. "method=withdraw". * rate: the rate limit, per minute, e.g. "rate=60". +* per: how often the rune can be used, with suffix "sec" (default), "min", "hour", "day" or "msec", "usec" or "nsec". e.g. "per=5sec". * pnum: the number of parameters. e.g. "pnum<2". * pnameX: the parameter named X (with any punctuation like `_` removed). e.g. "pnamedestination=1RustyRX2oai4EYYDpQGWvEL62BBGqN9T". * parrN: the N'th parameter. e.g. "parr0=1RustyRX2oai4EYYDpQGWvEL62BBGqN9T". diff --git a/doc/lightning-showrunes.7.md b/doc/lightning-showrunes.7.md index fb7ce42b8128..8128e0a84b70 100644 --- a/doc/lightning-showrunes.7.md +++ b/doc/lightning-showrunes.7.md @@ -29,6 +29,7 @@ On success, an object containing **runes** is returned. It is an array of objec - **restrictions\_as\_english** (string): English readable description of the restrictions array above - **stored** (boolean, optional): This is false if the rune does not appear in our datastore (only possible when `rune` is specified) (always *false*) - **blacklisted** (boolean, optional): The rune has been blacklisted; see commando-blacklist(7) (always *true*) +- **last\_used** (number, optional): The last time this rune was successfully used *(added 23.11)* - **our\_rune** (boolean, optional): This is not a rune for this node (only possible when `rune` is specified) (always *false*) [comment]: # (GENERATE-FROM-SCHEMA-END) @@ -48,4 +49,4 @@ RESOURCES Main web site: -[comment]: # ( SHA256STAMP:cd0e75bbeef3d5824448f67485de4679b0c163e97f405673b2ba9495f970d498) +[comment]: # ( SHA256STAMP:900e91777cd1e181c87a78913ab6f914585fcd99cd0dba16da19a81159f98aea) diff --git a/doc/schemas/commando-listrunes.schema.json b/doc/schemas/commando-listrunes.schema.json index c485d65b5d73..0fa759100f1f 100644 --- a/doc/schemas/commando-listrunes.schema.json +++ b/doc/schemas/commando-listrunes.schema.json @@ -93,6 +93,11 @@ ], "description": "The rune has been blacklisted; see commando-blacklist(7)" }, + "last_used": { + "type": "number", + "description": "The last time this rune was successfully used", + "added": "23.11" + }, "our_rune": { "type": "boolean", "enum": [ diff --git a/doc/schemas/showrunes.schema.json b/doc/schemas/showrunes.schema.json index c485d65b5d73..0fa759100f1f 100644 --- a/doc/schemas/showrunes.schema.json +++ b/doc/schemas/showrunes.schema.json @@ -93,6 +93,11 @@ ], "description": "The rune has been blacklisted; see commando-blacklist(7)" }, + "last_used": { + "type": "number", + "description": "The last time this rune was successfully used", + "added": "23.11" + }, "our_rune": { "type": "boolean", "enum": [ diff --git a/lightningd/log.c b/lightningd/log.c index 4c9b6b9bec02..2afd0a373b95 100644 --- a/lightningd/log.c +++ b/lightningd/log.c @@ -1087,7 +1087,7 @@ static void log_to_json(unsigned int skipped, : level == LOG_IO_IN ? "IO_IN" : level == LOG_IO_OUT ? "IO_OUT" : "UNKNOWN"); - json_add_time(info->response, "time", diff.ts); + json_add_timestr(info->response, "time", diff.ts); if (node_id) json_add_node_id(info->response, "node_id", node_id); json_add_string(info->response, "source", prefix); @@ -1148,7 +1148,7 @@ static struct command_result *json_getlog(struct command *cmd, response = json_stream_success(cmd); /* Suppress logging for this stream, to not bloat io logs */ json_stream_log_suppress_for_cmd(response, cmd); - json_add_time(response, "created_at", log_book->init_time.ts); + json_add_timestr(response, "created_at", log_book->init_time.ts); json_add_num(response, "bytes_used", (unsigned int)log_book->mem_used); json_add_num(response, "bytes_max", (unsigned int)log_book->max_mem); json_add_log(response, log_book, NULL, *minlevel); diff --git a/lightningd/notification.c b/lightningd/notification.c index edb00ed874ec..8012477d164e 100644 --- a/lightningd/notification.c +++ b/lightningd/notification.c @@ -136,7 +136,7 @@ static void warning_notification_serialize(struct json_stream *stream, : "warn"); /* unsuaul/broken event is rare, plugin pay more attentions on * the absolute time, like when channels failed. */ - json_add_time(stream, "time", l->time.ts); + json_add_timestr(stream, "time", l->time.ts); json_add_timeiso(stream, "timestamp", &l->time); json_add_string(stream, "source", l->prefix->prefix); json_add_string(stream, "log", l->log); diff --git a/lightningd/runes.c b/lightningd/runes.c index 40b467e7912f..01d2e218b636 100644 --- a/lightningd/runes.c +++ b/lightningd/runes.c @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -45,6 +46,7 @@ struct cond_info { const struct node_id *peer; const char *buf; const char *method; + struct timeabs now; const jsmntok_t *params; STRMAP(const jsmntok_t *) cached_params; struct usage *usage; @@ -114,6 +116,58 @@ u64 rune_unique_id(const struct rune *rune) return num; } +static const char *last_time_check(const tal_t *ctx, + const struct runes *runes, + const struct rune *rune, + const struct rune_altern *alt, + struct cond_info *cinfo) +{ + u64 r, multiplier, diff; + struct timeabs last_used; + char *endp; + const u64 sec_per_nsec = 1000000000; + + if (alt->condition != '=') + return "per operator must be ="; + + r = strtoul(alt->value, &endp, 10); + if (endp == alt->value || r == 0 || r >= UINT32_MAX) + return "malformed per"; + if (streq(endp, "") || streq(endp, "sec")) { + multiplier = sec_per_nsec; + } else if (streq(endp, "nsec")) { + multiplier = 1; + } else if (streq(endp, "usec")) { + multiplier = 1000; + } else if (streq(endp, "msec")) { + multiplier = 1000000; + } else if (streq(endp, "min")) { + multiplier = 60 * sec_per_nsec; + } else if (streq(endp, "hour")) { + multiplier = 60 * 60 * sec_per_nsec; + } else if (streq(endp, "day")) { + multiplier = 24 * 60 * 60 * sec_per_nsec; + } else { + return "malformed suffix"; + } + if (mul_overflows_u64(r, multiplier)) { + return "per overflow"; + } + if (!wallet_get_rune(tmpctx, cinfo->runes->ld->wallet, atol(rune->unique_id), &last_used)) { + /* FIXME: If we do not know the rune, per does not work */ + return NULL; + } + if (time_before(cinfo->now, last_used)) { + last_used = cinfo->now; + } + diff = time_to_nsec(time_between(cinfo->now, last_used)); + + if (diff < (r * multiplier)) { + return "too soon"; + } + return NULL; +} + static const char *rate_limit_check(const tal_t *ctx, const struct runes *runes, const struct rune *rune, @@ -317,7 +371,8 @@ static struct command_result *json_add_rune(struct lightningd *ld, const char *fieldname, const char *runestr, const struct rune *rune, - bool stored) + bool stored, + struct timeabs last_used) { char *rune_english; rune_english = ""; @@ -332,6 +387,9 @@ static struct command_result *json_add_rune(struct lightningd *ld, if (rune_is_ours(ld, rune) != NULL) { json_add_bool(js, "our_rune", false); } + if (last_used.ts.tv_sec != 0) { + json_add_timeabs(js, "last_used", last_used); + } json_add_string(js, "unique_id", rune->unique_id); json_array_start(js, "restrictions"); for (size_t i = 0; i < tal_count(rune->restrs); i++) { @@ -374,16 +432,21 @@ static struct command_result *json_showrunes(struct command *cmd, json_array_start(response, "runes"); if (ras) { u64 uid = rune_unique_id(ras->rune); - const char *from_db = wallet_get_rune(tmpctx, cmd->ld->wallet, uid); + struct timeabs last_used; + const char *from_db = wallet_get_rune(tmpctx, cmd->ld->wallet, uid, &last_used); + /* This is how we indicate no timestamp: */ + if (!from_db) + last_used.ts.tv_sec = 0; /* We consider it stored iff this is exactly stored */ json_add_rune(cmd->ld, response, NULL, ras->runestr, ras->rune, - from_db && streq(from_db, ras->runestr)); + from_db && streq(from_db, ras->runestr), last_used); } else { - const char **strs = wallet_get_runes(cmd, cmd->ld->wallet); + struct timeabs *last_used; + const char **strs = wallet_get_runes(cmd, cmd->ld->wallet, &last_used); for (size_t i = 0; i < tal_count(strs); i++) { const struct rune *r = rune_from_base64(cmd, strs[i]); - json_add_rune(cmd->ld, response, NULL, strs[i], r, true); + json_add_rune(cmd->ld, response, NULL, strs[i], r, true, last_used[i]); } } json_array_end(response); @@ -716,7 +779,7 @@ static const char *check_condition(const tal_t *ctx, const jsmntok_t *ptok; if (streq(alt->fieldname, "time")) { - return rune_alt_single_int(ctx, alt, time_now().ts.tv_sec); + return rune_alt_single_int(ctx, alt, cinfo->now.ts.tv_sec); } else if (streq(alt->fieldname, "id")) { if (cinfo->peer) { const char *id = node_id_to_hexstr(tmpctx, cinfo->peer); @@ -733,6 +796,8 @@ static const char *check_condition(const tal_t *ctx, return rune_alt_single_int(ctx, alt, (cinfo && cinfo->params) ? cinfo->params->size : 0); } else if (streq(alt->fieldname, "rate")) { return rate_limit_check(ctx, cinfo->runes, rune, alt, cinfo); + } else if (streq(alt->fieldname, "per")) { + return last_time_check(ctx, cinfo->runes, rune, alt, cinfo); } /* Rest are params looksup: generate this once! */ @@ -784,6 +849,12 @@ static const char *check_condition(const tal_t *ctx, ptok->end - ptok->start); } +static void update_rune_usage_time(struct runes *runes, + struct rune *rune, struct timeabs now) +{ + wallet_rune_update_last_used(runes->ld->wallet, rune, now); +} + static struct command_result *json_checkrune(struct command *cmd, const char *buffer, const jsmntok_t *obj UNNEEDED, @@ -812,6 +883,7 @@ static struct command_result *json_checkrune(struct command *cmd, cinfo.buf = buffer; cinfo.method = method; cinfo.params = methodparams; + cinfo.now = time_now(); /* We will populate it in rate_limit_check if required. */ cinfo.usage = NULL; strmap_init(&cinfo.cached_params); @@ -833,6 +905,7 @@ static struct command_result *json_checkrune(struct command *cmd, /* If it succeeded, *now* we increment any associated usage counter. */ if (cinfo.usage) cinfo.usage->counter++; + update_rune_usage_time(cmd->ld->runes, ras->rune, cinfo.now); js = json_stream_success(cmd); json_add_bool(js, "valid", true); diff --git a/lightningd/test/run-log-pruning.c b/lightningd/test/run-log-pruning.c index 2e03ed27d3b5..d3a6fe6ecf16 100644 --- a/lightningd/test/run-log-pruning.c +++ b/lightningd/test/run-log-pruning.c @@ -51,10 +51,10 @@ void json_add_string(struct json_stream *js UNNEEDED, const char *fieldname UNNEEDED, const char *str TAKES UNNEEDED) { fprintf(stderr, "json_add_string called!\n"); abort(); } -/* Generated stub for json_add_time */ -void json_add_time(struct json_stream *result UNNEEDED, const char *fieldname UNNEEDED, +/* Generated stub for json_add_timestr */ +void json_add_timestr(struct json_stream *result UNNEEDED, const char *fieldname UNNEEDED, struct timespec ts UNNEEDED) -{ fprintf(stderr, "json_add_time called!\n"); abort(); } +{ fprintf(stderr, "json_add_timestr called!\n"); abort(); } /* Generated stub for json_array_end */ void json_array_end(struct json_stream *js UNNEEDED) { fprintf(stderr, "json_array_end called!\n"); abort(); } diff --git a/lightningd/test/run-log_filter.c b/lightningd/test/run-log_filter.c index be53d73a0fce..acc101685f65 100644 --- a/lightningd/test/run-log_filter.c +++ b/lightningd/test/run-log_filter.c @@ -55,10 +55,10 @@ void json_add_string(struct json_stream *js UNNEEDED, const char *fieldname UNNEEDED, const char *str TAKES UNNEEDED) { fprintf(stderr, "json_add_string called!\n"); abort(); } -/* Generated stub for json_add_time */ -void json_add_time(struct json_stream *result UNNEEDED, const char *fieldname UNNEEDED, +/* Generated stub for json_add_timestr */ +void json_add_timestr(struct json_stream *result UNNEEDED, const char *fieldname UNNEEDED, struct timespec ts UNNEEDED) -{ fprintf(stderr, "json_add_time called!\n"); abort(); } +{ fprintf(stderr, "json_add_timestr called!\n"); abort(); } /* Generated stub for json_array_end */ void json_array_end(struct json_stream *js UNNEEDED) { fprintf(stderr, "json_array_end called!\n"); abort(); } diff --git a/tests/test_runes.py b/tests/test_runes.py index fa18b6346aa9..a769ad9b6e53 100644 --- a/tests/test_runes.py +++ b/tests/test_runes.py @@ -212,6 +212,82 @@ def test_createrune(node_factory): params=params)['valid'] is True +def do_test_rune_per_restriction(l1, rune_to_test, per_sec): + assert 'last_used' not in l1.rpc.showrunes(rune=rune_to_test)['runes'][0] + + before = time.time() + checkrune_result_1 = l1.rpc.checkrune(nodeid=l1.info['id'], + rune=rune_to_test, + method='getinfo', + params={}) + + show_rune = l1.rpc.showrunes(rune=rune_to_test)['runes'][0] + after = time.time() + + assert checkrune_result_1['valid'] is True + assert before < show_rune['last_used'] < after + + # cannot use same rune till 'per_sec' seconds + with pytest.raises(RpcError, match='Not permitted:') as exc_info: + l1.rpc.checkrune(nodeid=l1.info['id'], + rune=rune_to_test, + method='listpeers', + params={}) + + assert exc_info.value.error['code'] == 0x5de + assert exc_info.value.error['message'] == 'Not permitted: too soon' + assert l1.rpc.showrunes(rune=rune_to_test)['runes'][0]['last_used'] == show_rune['last_used'] + + print(f'PER SEC VALUE: {per_sec}') + print(show_rune['last_used']) + print(time.time()) + time.sleep(per_sec + 1) + print(time.time()) + + # rune should again be valid after 'per_sec' seconds + checkrune_result_3 = l1.rpc.checkrune(nodeid=l1.info['id'], + rune=rune_to_test, + method='listinvoices', + params={}) + + assert checkrune_result_3['valid'] is True + assert show_rune['last_used'] <= l1.rpc.showrunes(rune=rune_to_test)['runes'][0]['last_used'] <= time.time() + + +def test_createrune_per_restriction(node_factory): + l1 = node_factory.get_node() + + # 1 sec = 1,000,000,000 nanoseconds (nsec) + rune_per_nano_sec = l1.rpc.createrune(restrictions=[["per=1000000000nsec"]])['rune'] + assert rune_per_nano_sec == 'Bl0V_vkVkGr4h356JbCMCcoDyyKE8djkoQ2156iPB509MCZwZXI9MTAwMDAwMDAwMG5zZWM=' + do_test_rune_per_restriction(l1, rune_per_nano_sec, 1) + + # 1 sec = 1,000,000 microseconds (usec) + rune_per_micro_sec = l1.rpc.createrune(restrictions=[["per=2000000usec"]])['rune'] + assert rune_per_micro_sec == 'i8H9Rk5iDvXdiNgRUbeWqKUdMH2x0h58-1LqE1jthio9MSZwZXI9MjAwMDAwMHVzZWM=' + do_test_rune_per_restriction(l1, rune_per_micro_sec, 2) + + # 1 sec = 1,000 milliseconds (msec) + rune_per_milli_sec = l1.rpc.createrune(restrictions=[["per=1000msec"]])['rune'] + assert rune_per_milli_sec == 'EzVpQwjYe2aoNQiRa4_s7FJtomD3kWzx7lusMpzA59w9MiZwZXI9MTAwMG1zZWM=' + do_test_rune_per_restriction(l1, rune_per_milli_sec, 1) + + # 1 sec + rune_per_sec = l1.rpc.createrune(restrictions=[["per=2sec"]])['rune'] + assert rune_per_sec == 'dBbGI4T85cF4eSHvuQF_kW8bXgSDJY8Wr9cTsPGRCqg9MyZwZXI9MnNlYw==' + do_test_rune_per_restriction(l1, rune_per_sec, 2) + + # default (sec) + rune_per_default = l1.rpc.createrune(restrictions=[["per=1"]])['rune'] + assert rune_per_default == 'NrM7go6C4qzfRQDkUSv1DtRroJWSKqdjIOuvGS4TLFE9NCZwZXI9MQ==' + do_test_rune_per_restriction(l1, rune_per_default, 1) + + # 1 minute + rune_per_min = l1.rpc.createrune(restrictions=[["per=1min"]])['rune'] + assert rune_per_min == 'ZfWDjFa7wTiadUWOjwpztSClfiubwVusxxUEtoLtCBk9NSZwZXI9MW1pbg==' + do_test_rune_per_restriction(l1, rune_per_min, 60) + + def test_showrunes(node_factory): l1 = node_factory.get_node() rune1 = l1.rpc.createrune() @@ -250,6 +326,26 @@ def test_showrunes(node_factory): assert not_our_rune['stored'] is False assert not_our_rune['our_rune'] is False + # test that we don't set timestamp if rune fails + new_rune = l1.rpc.createrune(restrictions=[["method=getinfo"]])['rune'] + assert "last_used" not in l1.rpc.showrunes(rune=new_rune)['runes'][0] + + with pytest.raises(RpcError, match='Not permitted:'): + l1.rpc.checkrune(nodeid=l1.info['id'], + rune=new_rune, + method='listchannels', + params={}) + assert "last_used" not in l1.rpc.showrunes(rune=new_rune)['runes'][0] + + before = time.time() + l1.rpc.checkrune(nodeid=l1.info['id'], + rune=new_rune, + method='getinfo', + params={}) + after = time.time() + + assert before <= l1.rpc.showrunes(rune=new_rune)['runes'][0]['last_used'] <= after + def test_blacklistrune(node_factory): l1 = node_factory.get_node() diff --git a/wallet/db.c b/wallet/db.c index e7f49cb3d104..5aaad02d4941 100644 --- a/wallet/db.c +++ b/wallet/db.c @@ -975,6 +975,7 @@ static struct migration dbmigrations[] = { {SQL("ALTER TABLE htlc_sigs ADD inflight_tx_outnum INTEGER"), NULL}, {SQL("ALTER TABLE channel_funding_inflights ADD splice_amnt BIGINT DEFAULT 0"), NULL}, {SQL("ALTER TABLE channel_funding_inflights ADD i_am_initiator INTEGER DEFAULT 0"), NULL}, + {SQL("ALTER TABLE runes ADD last_used_nsec BIGINT DEFAULT NULL"), NULL}, {NULL, migrate_runes_idfix}, }; diff --git a/wallet/test/run-wallet.c b/wallet/test/run-wallet.c index 4bb4bcbdb070..bb518cbf57cc 100644 --- a/wallet/test/run-wallet.c +++ b/wallet/test/run-wallet.c @@ -797,7 +797,7 @@ u8 *towire_gossipd_discovered_ip(const tal_t *ctx UNNEEDED, const struct wireadd u8 *towire_hsmd_check_pubkey(const tal_t *ctx UNNEEDED, u32 index UNNEEDED, const struct pubkey *pubkey UNNEEDED) { fprintf(stderr, "towire_hsmd_check_pubkey called!\n"); abort(); } /* Generated stub for towire_hsmd_client_hsmfd */ -u8 *towire_hsmd_client_hsmfd(const tal_t *ctx UNNEEDED, const struct node_id *id UNNEEDED, u64 dbid UNNEEDED, u64 capabilities UNNEEDED) +u8 *towire_hsmd_client_hsmfd(const tal_t *ctx UNNEEDED, const struct node_id *id UNNEEDED, u64 dbid UNNEEDED, u64 permissions UNNEEDED) { fprintf(stderr, "towire_hsmd_client_hsmfd called!\n"); abort(); } /* Generated stub for towire_hsmd_derive_secret */ u8 *towire_hsmd_derive_secret(const tal_t *ctx UNNEEDED, const u8 *info UNNEEDED) diff --git a/wallet/wallet.c b/wallet/wallet.c index 3bcc63f2b6bc..15f4c2dcd00e 100644 --- a/wallet/wallet.c +++ b/wallet/wallet.c @@ -5679,44 +5679,60 @@ struct rune_blacklist *wallet_get_runes_blacklist(const tal_t *ctx, struct walle return blist; } -const char *wallet_get_rune(const tal_t *ctx, struct wallet *wallet, u64 unique_id) +static struct timeabs db_col_time_from_nsec(struct db_stmt *stmt, const char *colname) +{ + struct timerel t; + struct timeabs tabs; + + if (db_col_is_null(stmt, colname)) + t = time_from_nsec(0); + else + t = time_from_nsec(db_col_u64(stmt, colname)); + tabs.ts = t.ts; + return tabs; +} + +const char *wallet_get_rune(const tal_t *ctx, struct wallet *wallet, u64 unique_id, struct timeabs *last_used) { struct db_stmt *stmt; const char *runestr; - stmt = db_prepare_v2(wallet->db, SQL("SELECT rune FROM runes WHERE id = ?")); + stmt = db_prepare_v2(wallet->db, SQL("SELECT rune, last_used_nsec FROM runes WHERE id = ?")); db_bind_u64(stmt, unique_id); db_query_prepared(stmt); - if (db_step(stmt)) + if (db_step(stmt)) { runestr = db_col_strdup(ctx, stmt, "rune"); - else + *last_used = db_col_time_from_nsec(stmt, "last_used_nsec"); + } else { runestr = NULL; + } tal_free(stmt); return runestr; } /* Migration code needs db, not wallet access */ -static const char **db_get_runes(const tal_t *ctx, struct db *db) +static const char **db_get_runes(const tal_t *ctx, struct db *db, struct timeabs **last_used) { struct db_stmt *stmt; const char **strs = tal_arr(ctx, const char *, 0); - stmt = db_prepare_v2(db, SQL("SELECT rune FROM runes")); + *last_used = tal_arr(ctx, struct timeabs, 0); + stmt = db_prepare_v2(db, SQL("SELECT rune, last_used_nsec FROM runes")); db_query_prepared(stmt); while (db_step(stmt)) { const char *str = db_col_strdup(strs, stmt, "rune"); tal_arr_expand(&strs, str); + tal_arr_expand(last_used, db_col_time_from_nsec(stmt, "last_used_nsec")); } tal_free(stmt); return strs; } -const char **wallet_get_runes(const tal_t *ctx, struct wallet *wallet) +const char **wallet_get_runes(const tal_t *ctx, struct wallet *wallet, struct timeabs **last_used) { - return db_get_runes(ctx, wallet->db); - + return db_get_runes(ctx, wallet->db, last_used); } static void db_rune_insert(struct db *db, @@ -5737,6 +5753,20 @@ void wallet_rune_insert(struct wallet *wallet, const struct rune *rune) db_rune_insert(wallet->db, rune); } +void wallet_rune_update_last_used(struct wallet *wallet, const struct rune *rune, struct timeabs last_used) +{ + struct db_stmt *stmt; + struct timerel t; + + t.ts = last_used.ts; + stmt = db_prepare_v2(wallet->db, + SQL("UPDATE runes SET last_used_nsec = ? WHERE id = ?;")); + db_bind_u64(stmt, time_to_nsec(t)); + db_bind_u64(stmt, rune_unique_id(rune)); + db_exec_prepared_v2(stmt); + tal_free(stmt); +} + static void db_insert_blacklist(struct db *db, const struct rune_blacklist *entry) { @@ -5836,7 +5866,8 @@ void migrate_datastore_commando_runes(struct lightningd *ld, struct db *db) void migrate_runes_idfix(struct lightningd *ld, struct db *db) { /* ID fields were wrong. Pull them all out and put them back */ - const char **runes = db_get_runes(tmpctx, db); + struct timeabs *last_used; + const char **runes = db_get_runes(tmpctx, db, &last_used); struct db_stmt *stmt; stmt = db_prepare_v2(db, SQL("DELETE FROM runes;")); diff --git a/wallet/wallet.h b/wallet/wallet.h index 9c05a6762fc1..b0f2c1a9a3c8 100644 --- a/wallet/wallet.h +++ b/wallet/wallet.h @@ -1585,17 +1585,19 @@ struct wally_psbt *psbt_using_utxos(const tal_t *ctx, * @ctx: tal ctx for return to be tallocated from * @wallet: the wallet * @unique_id: the id of the rune. + * @last_used: absolute time rune was last used * * Returns NULL if it's not found. */ -const char *wallet_get_rune(const tal_t *ctx, struct wallet *wallet, u64 unique_id); +const char *wallet_get_rune(const tal_t *ctx, struct wallet *wallet, u64 unique_id, struct timeabs *last_used); /** * Get every runestring from the db * @ctx: tal ctx for return to be tallocated from * @wallet: the wallet + * @last_used: absolute time rune was last used */ -const char **wallet_get_runes(const tal_t *ctx, struct wallet *wallet); +const char **wallet_get_runes(const tal_t *ctx, struct wallet *wallet, struct timeabs **last_used); /** * wallet_rune_insert -- Insert the newly created rune into the database @@ -1605,6 +1607,15 @@ const char **wallet_get_runes(const tal_t *ctx, struct wallet *wallet); */ void wallet_rune_insert(struct wallet *wallet, const struct rune *rune); +/** + * wallet_rune_update_last_used -- Update the timestamp on an existing rune + * + * @wallet: the wallet to save into + * @rune: the instance to store + * @last_used: now + */ +void wallet_rune_update_last_used(struct wallet *wallet, const struct rune *rune, struct timeabs last_used); + /* Load the runes blacklist */ struct rune_blacklist { u64 start, end;