commando: add commando-rune command.

Can both mint new runes, and add one or more restrictions to existing ones.

Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
This commit is contained in:
Rusty Russell 2022-07-16 22:48:27 +09:30
parent cf4374c4ed
commit 419cb60b1b
2 changed files with 182 additions and 2 deletions

View file

@ -520,11 +520,152 @@ static struct command_result *json_commando(struct command *cmd,
return send_more_cmd(cmd, NULL, NULL, outgoing);
}
static struct command_result *param_rune(struct command *cmd, const char *name,
const char * buffer, const jsmntok_t *tok,
struct rune **rune)
{
*rune = rune_from_base64n(cmd, buffer + tok->start, tok->end - tok->start);
if (!*rune)
return command_fail_badparam(cmd, name, buffer, tok,
"should be base64 string");
return NULL;
}
static struct rune_restr **readonly_restrictions(const tal_t *ctx)
{
struct rune_restr **restrs = tal_arr(ctx, struct rune_restr *, 2);
/* Any list*, get*, or summary:
* method^list|method^get|method=summary
*/
restrs[0] = rune_restr_new(restrs);
rune_restr_add_altern(restrs[0],
take(rune_altern_new(NULL,
"method",
RUNE_COND_BEGINS,
"list")));
rune_restr_add_altern(restrs[0],
take(rune_altern_new(NULL,
"method",
RUNE_COND_BEGINS,
"get")));
rune_restr_add_altern(restrs[0],
take(rune_altern_new(NULL,
"method",
RUNE_COND_EQUAL,
"summary")));
/* But not listdatastore!
* method/listdatastore
*/
restrs[1] = rune_restr_new(restrs);
rune_restr_add_altern(restrs[1],
take(rune_altern_new(NULL,
"method",
RUNE_COND_NOT_EQUAL,
"listdatastore")));
return restrs;
}
static struct command_result *param_restrictions(struct command *cmd,
const char *name,
const char *buffer,
const jsmntok_t *tok,
struct rune_restr ***restrs)
{
if (json_tok_streq(buffer, tok, "readonly"))
*restrs = readonly_restrictions(cmd);
else if (tok->type == JSMN_ARRAY) {
size_t i;
const jsmntok_t *t;
*restrs = tal_arr(cmd, struct rune_restr *, tok->size);
json_for_each_arr(i, t, tok) {
(*restrs)[i] = rune_restr_from_string(*restrs,
buffer + t->start,
t->end - t->start);
if (!(*restrs)[i])
return command_fail_badparam(cmd, name, buffer, t,
"not a valid restriction");
}
} else {
*restrs = tal_arr(cmd, struct rune_restr *, 1);
(*restrs)[0] = rune_restr_from_string(*restrs,
buffer + tok->start,
tok->end - tok->start);
if (!(*restrs)[0])
return command_fail_badparam(cmd, name, buffer, tok,
"not a valid restriction");
}
return NULL;
}
static struct command_result *reply_with_rune(struct command *cmd,
const char *buf UNUSED,
const jsmntok_t *result UNUSED,
struct rune *rune)
{
struct json_stream *js = jsonrpc_stream_success(cmd);
json_add_string(js, "rune", rune_to_base64(tmpctx, rune));
json_add_string(js, "unique_id", rune->unique_id);
return command_finished(cmd, js);
}
static struct command_result *json_commando_rune(struct command *cmd,
const char *buffer,
const jsmntok_t *params)
{
struct rune *rune;
struct rune_restr **restrs;
struct out_req *req;
if (!param(cmd, buffer, params,
p_opt("rune", param_rune, &rune),
p_opt("restrictions", param_restrictions, &restrs),
NULL))
return command_param_failed();
if (rune) {
for (size_t i = 0; i < tal_count(restrs); i++)
rune_add_restr(rune, restrs[i]);
return reply_with_rune(cmd, NULL, NULL, rune);
}
rune = rune_derive_start(cmd, master_rune,
tal_fmt(tmpctx, "%"PRIu64,
rune_counter ? *rune_counter : 0));
for (size_t i = 0; i < tal_count(restrs); i++)
rune_add_restr(rune, restrs[i]);
/* Now update datastore, before returning rune */
req = jsonrpc_request_start(plugin, cmd, "datastore",
reply_with_rune, forward_error, rune);
json_array_start(req->js, "key");
json_add_string(req->js, NULL, "commando");
json_add_string(req->js, NULL, "rune_counter");
json_array_end(req->js);
if (rune_counter) {
(*rune_counter)++;
json_add_string(req->js, "mode", "must-replace");
} else {
/* This used to say "🌩🤯🧨🔫!" but our log filters are too strict :( */
plugin_log(plugin, LOG_INFORM, "Commando powers enabled: BOOM!");
rune_counter = tal(plugin, u64);
*rune_counter = 1;
json_add_string(req->js, "mode", "must-create");
}
json_add_u64(req->js, "string", *rune_counter);
return send_outreq(plugin, req);
}
#if DEVELOPER
static void memleak_mark_globals(struct plugin *p, struct htable *memtable)
{
memleak_remove_region(memtable, outgoing_commands, tal_bytelen(outgoing_commands));
memleak_remove_region(memtable, incoming_commands, tal_bytelen(incoming_commands));
memleak_remove_region(memtable, master_rune, sizeof(*master_rune));
if (rune_counter)
memleak_remove_region(memtable, rune_counter, sizeof(*rune_counter));
}
@ -578,7 +719,13 @@ static const struct plugin_command commands[] = { {
"Send a commando message to a direct peer, wait for response",
"Sends {peer_id} {method} with optional {params} and {rune}",
json_commando,
}
}, {
"commando-rune",
"utility",
"Create or restrict a rune",
"Takes an optional {rune} with optional {restrictions} and returns {rune}",
json_commando_rune,
},
};
int main(int argc, char *argv[])

View file

@ -2546,7 +2546,7 @@ def test_plugin_shutdown(node_factory):
'test_libplugin: failed to self-terminate in time, killing.'])
def test_commando(node_factory):
def test_commando(node_factory, executor):
l1, l2 = node_factory.line_graph(2, fundchannel=False)
# This works
@ -2610,3 +2610,36 @@ def test_commando(node_factory):
'channel': '1x2x3'}],
'payment_hash': '00' * 32}})
assert exc_info.value.error['data']['erring_index'] == 0
def test_commando_rune(node_factory):
l1, l2 = node_factory.line_graph(2, fundchannel=False)
# l1's commando secret is 1241faef85297127c2ac9bde95421b2c51e5218498ae4901dc670c974af4284b.
# I put that into a test node's commando.py to generate these runes (modified readonly to match ours):
# $ l1-cli commando-rune
# "rune": "zKc2W88jopslgUBl0UE77aEe5PNCLn5WwqSusU_Ov3A9MA=="
# $ l1-cli commando-rune restrictions=readonly
# "rune": "1PJnoR9a7u4Bhglj2s7rVOWqRQnswIwUoZrDVMKcLTY9MSZtZXRob2RebGlzdHxtZXRob2ReZ2V0fG1ldGhvZD1zdW1tYXJ5Jm1ldGhvZC9saXN0ZGF0YXN0b3Jl"
# $ l1-cli commando-rune restrictions='time>1656675211'
# "rune": "RnlWC4lwBULFaObo6ZP8jfqYRyTbfWPqcMT3qW-Wmso9MiZ0aW1lPjE2NTY2NzUyMTE="
# $ l1-cli commando-rune restrictions='["id^022d223620a359a47ff7","method=listpeers"]'
# "rune": "lXFWzb51HjWxKV5TmfdiBgd74w0moeyChj3zbLoxmws9MyZpZF4wMjJkMjIzNjIwYTM1OWE0N2ZmNyZtZXRob2Q9bGlzdHBlZXJz"
# $ l1-cli commando-rune lXFWzb51HjWxKV5TmfdiBgd74w0moeyChj3zbLoxmws9MyZpZF4wMjJkMjIzNjIwYTM1OWE0N2ZmNyZtZXRob2Q9bGlzdHBlZXJz 'pnamelevel!|pnamelevel/io'
# "rune": "Dw2tzGCoUojAyT0JUw7fkYJYqExpEpaDRNTkyvWKoJY9MyZpZF4wMjJkMjIzNjIwYTM1OWE0N2ZmNyZtZXRob2Q9bGlzdHBlZXJzJnBuYW1lbGV2ZWwhfHBuYW1lbGV2ZWwvaW8="
rune1 = l1.rpc.commando_rune()
assert rune1['rune'] == 'zKc2W88jopslgUBl0UE77aEe5PNCLn5WwqSusU_Ov3A9MA=='
assert rune1['unique_id'] == '0'
rune2 = l1.rpc.commando_rune(restrictions="readonly")
assert rune2['rune'] == '1PJnoR9a7u4Bhglj2s7rVOWqRQnswIwUoZrDVMKcLTY9MSZtZXRob2RebGlzdHxtZXRob2ReZ2V0fG1ldGhvZD1zdW1tYXJ5Jm1ldGhvZC9saXN0ZGF0YXN0b3Jl'
assert rune2['unique_id'] == '1'
rune3 = l1.rpc.commando_rune(restrictions="time>1656675211")
assert rune3['rune'] == 'RnlWC4lwBULFaObo6ZP8jfqYRyTbfWPqcMT3qW-Wmso9MiZ0aW1lPjE2NTY2NzUyMTE='
assert rune3['unique_id'] == '2'
rune4 = l1.rpc.commando_rune(restrictions=["id^022d223620a359a47ff7", "method=listpeers"])
assert rune4['rune'] == 'lXFWzb51HjWxKV5TmfdiBgd74w0moeyChj3zbLoxmws9MyZpZF4wMjJkMjIzNjIwYTM1OWE0N2ZmNyZtZXRob2Q9bGlzdHBlZXJz'
assert rune4['unique_id'] == '3'
rune5 = l1.rpc.commando_rune(rune4['rune'], "pnamelevel!|pnamelevel/io")
assert rune5['rune'] == 'Dw2tzGCoUojAyT0JUw7fkYJYqExpEpaDRNTkyvWKoJY9MyZpZF4wMjJkMjIzNjIwYTM1OWE0N2ZmNyZtZXRob2Q9bGlzdHBlZXJzJnBuYW1lbGV2ZWwhfHBuYW1lbGV2ZWwvaW8='
assert rune5['unique_id'] == '3'