mirror of
https://github.com/ElementsProject/lightning.git
synced 2025-03-09 15:52:50 +01:00
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:
parent
d61dec8925
commit
9bcabbc912
3 changed files with 55 additions and 127 deletions
|
@ -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".
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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):
|
||||||
|
|
Loading…
Add table
Reference in a new issue