From 419cb60b1be16c87906fd8ea1235851f9aaf7027 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Sat, 16 Jul 2022 22:48:27 +0930 Subject: [PATCH] commando: add commando-rune command. Can both mint new runes, and add one or more restrictions to existing ones. Signed-off-by: Rusty Russell --- plugins/commando.c | 149 ++++++++++++++++++++++++++++++++++++++++++- tests/test_plugin.py | 35 +++++++++- 2 files changed, 182 insertions(+), 2 deletions(-) diff --git a/plugins/commando.c b/plugins/commando.c index 1521b3baf..c3e3aca2b 100644 --- a/plugins/commando.c +++ b/plugins/commando.c @@ -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[]) diff --git a/tests/test_plugin.py b/tests/test_plugin.py index f0e656e04..b706baaaf 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -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'