runes: Reimplement rate in terms of per

Changelog-Changed: JSON-RPC: `checkrune` `rate` restriction is slightly stricter (exact division of time like `per`)
This commit is contained in:
ShahanaFarooqui 2023-09-21 19:05:51 -07:00 committed by Rusty Russell
parent d61dec8925
commit 9bcabbc912
3 changed files with 55 additions and 127 deletions

View file

@ -35,8 +35,8 @@ being run:
* time: the current UNIX time, e.g. "time<1656759180". * time: the current UNIX time, e.g. "time<1656759180".
* id: the node\_id of the peer, e.g. "id=024b9a1fa8e006f1e3937f65f66c408e6da8e1ca728ea43222a7381df1cc449605". * id: the node\_id of the peer, e.g. "id=024b9a1fa8e006f1e3937f65f66c408e6da8e1ca728ea43222a7381df1cc449605".
* method: the command being run, e.g. "method=withdraw". * 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". * per: how often the rune can be used, with suffix "sec" (default), "min", "hour", "day" or "msec", "usec" or "nsec". e.g. "per=5sec".
* rate: the rate limit, per minute, e.g. "rate=60" is equivalent to "per=1sec".
* pnum: the number of parameters. e.g. "pnum<2". * pnum: the number of parameters. e.g. "pnum<2".
* pnameX: the parameter named X (with any punctuation like `_` removed). e.g. "pnamedestination=1RustyRX2oai4EYYDpQGWvEL62BBGqN9T". * pnameX: the parameter named X (with any punctuation like `_` removed). e.g. "pnamedestination=1RustyRX2oai4EYYDpQGWvEL62BBGqN9T".
* parrN: the N'th parameter. e.g. "parr0=1RustyRX2oai4EYYDpQGWvEL62BBGqN9T". * parrN: the N'th parameter. e.g. "parr0=1RustyRX2oai4EYYDpQGWvEL62BBGqN9T".

View file

@ -19,27 +19,7 @@
#include <lightningd/runes.h> #include <lightningd/runes.h>
#include <wallet/wallet.h> #include <wallet/wallet.h>
struct usage { static const u64 sec_per_nsec = 1000000000;
/* If you really issue more than 2^32 runes, they'll share ratelimit buckets */
u32 id;
u32 counter;
};
static u64 usage_id(const struct usage *u)
{
return u->id;
}
static size_t id_hash(u64 id)
{
return siphash24(siphash_seed(), &id, sizeof(id));
}
static bool usage_eq_id(const struct usage *u, u64 id)
{
return u->id == id;
}
HTABLE_DEFINE_TYPE(struct usage, usage_id, id_hash, usage_eq_id, usage_table);
struct cond_info { struct cond_info {
const struct runes *runes; const struct runes *runes;
@ -49,7 +29,6 @@ struct cond_info {
struct timeabs now; struct timeabs now;
const jsmntok_t *params; const jsmntok_t *params;
STRMAP(const jsmntok_t *) cached_params; STRMAP(const jsmntok_t *) cached_params;
struct usage *usage;
}; };
/* This is lightningd->runes */ /* This is lightningd->runes */
@ -58,7 +37,6 @@ struct runes {
struct rune *master; struct rune *master;
u64 next_unique_id; u64 next_unique_id;
struct rune_blacklist *blacklist; struct rune_blacklist *blacklist;
struct usage_table *usage_table;
}; };
const char *rune_is_ours(struct lightningd *ld, const struct rune *rune) const char *rune_is_ours(struct lightningd *ld, const struct rune *rune)
@ -66,23 +44,6 @@ const char *rune_is_ours(struct lightningd *ld, const struct rune *rune)
return rune_is_derived(ld->runes->master, rune); return rune_is_derived(ld->runes->master, rune);
} }
static void memleak_help_usage_table(struct htable *memtable,
struct usage_table *usage_table)
{
memleak_scan_htable(memtable, &usage_table->raw);
}
/* Every minute we forget entries. */
static void flush_usage_table(struct runes *runes)
{
tal_free(runes->usage_table);
runes->usage_table = tal(runes, struct usage_table);
usage_table_init(runes->usage_table);
memleak_add_helper(runes->usage_table, memleak_help_usage_table);
notleak(new_reltimer(runes->ld->timers, runes, time_from_sec(60), flush_usage_table, runes));
}
/* Convert unique_id string to u64. We only expect this to fail when we're /* Convert unique_id string to u64. We only expect this to fail when we're
* dealing with runes from elsewhere (i.e. param_rune) */ * dealing with runes from elsewhere (i.e. param_rune) */
static bool unique_id_num(const struct rune *rune, u64 *num) static bool unique_id_num(const struct rune *rune, u64 *num)
@ -114,16 +75,36 @@ u64 rune_unique_id(const struct rune *rune)
return num; return num;
} }
static const char *last_time_check(const tal_t *ctx, static const char *last_time_check(const struct rune *rune,
struct cond_info *cinfo,
u64 n_sec)
{
u64 diff;
struct timeabs last_used;
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 < n_sec) {
return "too soon";
}
return NULL;
}
static const char *per_time_check(const tal_t *ctx,
const struct runes *runes, const struct runes *runes,
const struct rune *rune, const struct rune *rune,
const struct rune_altern *alt, const struct rune_altern *alt,
struct cond_info *cinfo) struct cond_info *cinfo)
{ {
u64 r, multiplier, diff; u64 r, multiplier;
struct timeabs last_used;
char *endp; char *endp;
const u64 sec_per_nsec = 1000000000;
if (alt->condition != '=') if (alt->condition != '=')
return "per operator must be ="; return "per operator must be =";
@ -151,19 +132,7 @@ static const char *last_time_check(const tal_t *ctx,
if (mul_overflows_u64(r, multiplier)) { if (mul_overflows_u64(r, multiplier)) {
return "per overflow"; return "per overflow";
} }
if (!wallet_get_rune(tmpctx, cinfo->runes->ld->wallet, atol(rune->unique_id), &last_used)) { return last_time_check(rune, cinfo, r * multiplier);
/* 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, static const char *rate_limit_check(const tal_t *ctx,
@ -174,8 +143,6 @@ static const char *rate_limit_check(const tal_t *ctx,
{ {
unsigned long r; unsigned long r;
char *endp; char *endp;
u64 unique_id = rune_unique_id(rune);
if (alt->condition != '=') if (alt->condition != '=')
return "rate operator must be ="; return "rate operator must be =";
@ -183,22 +150,7 @@ static const char *rate_limit_check(const tal_t *ctx,
if (endp == alt->value || *endp || r == 0 || r >= UINT32_MAX) if (endp == alt->value || *endp || r == 0 || r >= UINT32_MAX)
return "malformed rate"; return "malformed rate";
/* We cache this: we only add usage counter if whole rune succeeds! */ return last_time_check(rune, cinfo, 60 * sec_per_nsec / r);
if (!cinfo->usage) {
cinfo->usage = usage_table_get(runes->usage_table, unique_id);
if (!cinfo->usage) {
cinfo->usage = tal(runes->usage_table, struct usage);
cinfo->usage->id = unique_id;
cinfo->usage->counter = 0;
usage_table_add(runes->usage_table, cinfo->usage);
}
}
/* >= becuase if we allow this, counter will increment */
if (cinfo->usage->counter >= r)
return tal_fmt(ctx, "Rate of %lu per minute exceeded", r);
return NULL;
} }
/* We need to initialize master runes secret early, so db can use rune_is_ours */ /* We need to initialize master runes secret early, so db can use rune_is_ours */
@ -227,10 +179,6 @@ void runes_finish_init(struct runes *runes)
runes->next_unique_id = db_get_intvar(ld->wallet->db, "runes_uniqueid", 0); runes->next_unique_id = db_get_intvar(ld->wallet->db, "runes_uniqueid", 0);
runes->blacklist = wallet_get_runes_blacklist(runes, ld->wallet); runes->blacklist = wallet_get_runes_blacklist(runes, ld->wallet);
/* Initialize usage table and start flush timer. */
runes->usage_table = NULL;
flush_usage_table(runes);
} }
struct rune_and_string { struct rune_and_string {
@ -795,7 +743,7 @@ static const char *check_condition(const tal_t *ctx,
} else if (streq(alt->fieldname, "rate")) { } else if (streq(alt->fieldname, "rate")) {
return rate_limit_check(ctx, cinfo->runes, rune, alt, cinfo); return rate_limit_check(ctx, cinfo->runes, rune, alt, cinfo);
} else if (streq(alt->fieldname, "per")) { } else if (streq(alt->fieldname, "per")) {
return last_time_check(ctx, cinfo->runes, rune, alt, cinfo); return per_time_check(ctx, cinfo->runes, rune, alt, cinfo);
} }
/* Rest are params looksup: generate this once! */ /* Rest are params looksup: generate this once! */
@ -850,6 +798,7 @@ static const char *check_condition(const tal_t *ctx,
static void update_rune_usage_time(struct runes *runes, static void update_rune_usage_time(struct runes *runes,
struct rune *rune, struct timeabs now) struct rune *rune, struct timeabs now)
{ {
/* FIXME: we could batch DB access if this is too slow */
wallet_rune_update_last_used(runes->ld->wallet, rune, now); wallet_rune_update_last_used(runes->ld->wallet, rune, now);
} }
@ -882,8 +831,6 @@ static struct command_result *json_checkrune(struct command *cmd,
cinfo.method = method; cinfo.method = method;
cinfo.params = methodparams; cinfo.params = methodparams;
cinfo.now = time_now(); cinfo.now = time_now();
/* We will populate it in rate_limit_check if required. */
cinfo.usage = NULL;
strmap_init(&cinfo.cached_params); strmap_init(&cinfo.cached_params);
err = rune_is_ours(cmd->ld, ras->rune); err = rune_is_ours(cmd->ld, ras->rune);
@ -900,9 +847,6 @@ static struct command_result *json_checkrune(struct command *cmd,
return command_fail(cmd, RUNE_NOT_PERMITTED, "Not permitted: %s", err); return command_fail(cmd, RUNE_NOT_PERMITTED, "Not permitted: %s", err);
} }
/* 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); update_rune_usage_time(cmd->ld->runes, ras->rune, cinfo.now);
js = json_stream_success(cmd); js = json_stream_success(cmd);

View file

@ -124,10 +124,7 @@ def test_createrune(node_factory):
(rune2, "getinfo", {}), (rune2, "getinfo", {}),
(rune3, "getinfo", {}), (rune3, "getinfo", {}),
(rune7, "listpeers", []), (rune7, "listpeers", []),
(rune7, "getinfo", {}), (rune7, "getinfo", {}))
(rune9, "getinfo", {}),
(rune8, "getinfo", {}),
(rune8, "getinfo", {}))
failures = ((rune2, "withdraw", {}), failures = ((rune2, "withdraw", {}),
(rune2, "plugin", {'subcommand': 'list'}), (rune2, "plugin", {'subcommand': 'list'}),
@ -136,7 +133,10 @@ def test_createrune(node_factory):
(rune5, "listpeers", {'id': l1.info['id'], 'level': 'io'}), (rune5, "listpeers", {'id': l1.info['id'], 'level': 'io'}),
(rune6, "listpeers", [l1.info['id'], 'io']), (rune6, "listpeers", [l1.info['id'], 'io']),
(rune7, "listpeers", [l1.info['id']]), (rune7, "listpeers", [l1.info['id']]),
(rune7, "listpeers", {'id': l1.info['id']})) (rune7, "listpeers", {'id': l1.info['id']}),
# These are derived from rune7, so they have been recently used
(rune8, "getinfo", {}),
(rune9, "getinfo", {}))
for rune, cmd, params in successes: for rune, cmd, params in successes:
l1.rpc.checkrune(nodeid=l1.info['id'], l1.rpc.checkrune(nodeid=l1.info['id'],
@ -156,17 +156,11 @@ def test_createrune(node_factory):
params=params) params=params)
assert exc_info.value.error['code'] == 0x5de assert exc_info.value.error['code'] == 0x5de
# Now, this can flake if we cross a minute boundary! So wait until # Rune 7 succeeded, so we need to wait for 20 seconds
# It succeeds again. time.sleep(21)
while True: l1.rpc.checkrune(nodeid=l1.info['id'],
try: rune=rune8['rune'],
l1.rpc.checkrune(nodeid=l1.info['id'], method='getinfo')['valid'] is True
rune=rune8['rune'],
method='getinfo')
break
except RpcError as e:
assert e.error['code'] == 0x5de
time.sleep(1)
# This fails immediately, since we've done one. # This fails immediately, since we've done one.
with pytest.raises(RpcError, match='Not permitted:') as exc_info: with pytest.raises(RpcError, match='Not permitted:') as exc_info:
@ -176,22 +170,6 @@ def test_createrune(node_factory):
params={}) params={})
assert exc_info.value.error['code'] == 0x5de assert exc_info.value.error['code'] == 0x5de
# Two more succeed for rune8.
for _ in range(2):
l1.rpc.checkrune(nodeid=l1.info['id'],
rune=rune8['rune'],
method='getinfo',
params={})
assert exc_info.value.error['code'] == 0x5de
# Now we've had 3 in one minute, this will fail.
with pytest.raises(RpcError, match='Not permitted:') as exc_info:
l1.rpc.checkrune(nodeid=l1.info['id'],
rune=rune8['rune'],
method='getinfo',
params={})
assert exc_info.value.error['code'] == 0x5de
# rune5 can only be used by l2: # rune5 can only be used by l2:
with pytest.raises(RpcError, match='Not permitted:') as exc_info: with pytest.raises(RpcError, match='Not permitted:') as exc_info:
l1.rpc.checkrune(nodeid=l1.info['id'], l1.rpc.checkrune(nodeid=l1.info['id'],
@ -200,16 +178,22 @@ def test_createrune(node_factory):
params={}) params={})
assert exc_info.value.error['code'] == 0x5de assert exc_info.value.error['code'] == 0x5de
# Now wait for ratelimit expiry, ratelimits should reset. # Rune8 has rate 3 per minute (20 seconds) and rune9 has rate 1 per minute (60 seconds)
time.sleep(61) time.sleep(21)
with pytest.raises(RpcError, match='Not permitted: too soon'):
l1.rpc.checkrune(nodeid=l1.info['id'],
rune=rune9['rune'],
method="getinfo")
for rune, cmd, params in ((rune9, "getinfo", {}), assert l1.rpc.checkrune(nodeid=l1.info['id'],
(rune8, "getinfo", {}), rune=rune8['rune'],
(rune8, "getinfo", {})): method="getinfo")['valid'] is True
assert l1.rpc.checkrune(nodeid=l1.info['id'],
rune=rune['rune'], # Rune8 uses the same unique_id as rune9, so we need to wait for full 1 minute
method=cmd, time.sleep(61)
params=params)['valid'] is True assert l1.rpc.checkrune(nodeid=l1.info['id'],
rune=rune9['rune'],
method="getinfo")['valid'] is True
def do_test_rune_per_restriction(l1, rune_to_test, per_sec): def do_test_rune_per_restriction(l1, rune_to_test, per_sec):