diff --git a/channeld/channeld.c b/channeld/channeld.c index 70f4e970086d..feba13169f9e 100644 --- a/channeld/channeld.c +++ b/channeld/channeld.c @@ -2300,6 +2300,8 @@ static void peer_in(struct peer *peer, const u8 *msg) case WIRE_WARNING: case WIRE_ERROR: case WIRE_ONION_MESSAGE: + case WIRE_PEER_STORAGE: + case WIRE_YOUR_PEER_STORAGE: abort(); } diff --git a/common/features.c b/common/features.c index 203c0d2f39b8..a86f6f30d02a 100644 --- a/common/features.c +++ b/common/features.c @@ -136,6 +136,12 @@ static const struct feature_style feature_styles[] = { [NODE_ANNOUNCE_FEATURE] = FEATURE_REPRESENT, [BOLT11_FEATURE] = FEATURE_DONT_REPRESENT, [CHANNEL_FEATURE] = FEATURE_DONT_REPRESENT } }, + { OPT_WANT_PEER_BACKUP_STORAGE, + .copy_style = { [INIT_FEATURE] = FEATURE_REPRESENT, + [NODE_ANNOUNCE_FEATURE] = FEATURE_REPRESENT } }, + { OPT_PROVIDE_PEER_BACKUP_STORAGE, + .copy_style = { [INIT_FEATURE] = FEATURE_REPRESENT, + [NODE_ANNOUNCE_FEATURE] = FEATURE_REPRESENT } }, }; struct dependency { @@ -456,8 +462,8 @@ const char *feature_name(const tal_t *ctx, size_t f) "option_quiesce", /* https://github.com/lightning/bolts/pull/869 */ NULL, "option_onion_messages", /* https://github.com/lightning/bolts/pull/759 */ - "option_want_peer_backup", /* 40/41 */ /* https://github.com/lightning/bolts/pull/881 */ - "option_provide_peer_backup", /* https://github.com/lightning/bolts/pull/881 */ + "option_want_peer_backup_storage", /* 40/41 */ /* https://github.com/lightning/bolts/pull/881/files */ + "option_provide_peer_backup_storage", /* https://github.com/lightning/bolts/pull/881/files */ "option_channel_type", "option_scid_alias", /* https://github.com/lightning/bolts/pull/910 */ "option_payment_metadata", diff --git a/common/features.h b/common/features.h index a862bafe1c53..d9ec5744ed72 100644 --- a/common/features.h +++ b/common/features.h @@ -15,7 +15,6 @@ enum feature_place { BOLT12_INVOICE_FEATURE, }; #define NUM_FEATURE_PLACE (BOLT12_INVOICE_FEATURE+1) - extern const char *feature_place_names[NUM_FEATURE_PLACE]; /* The complete set of features for all contexts */ @@ -165,4 +164,12 @@ struct feature_set *feature_set_dup(const tal_t *ctx, #define OPT_SHUTDOWN_WRONG_FUNDING 104 +/* BOLT-peer-storage #9: + * + * | 40/41 | `want_peer_backup_storage` | Want to use other nodes to store encrypted backup data | IN ... + * | 42/43 | `provide_peer_backup_storage` | Can store other nodes' encrypted backup data | IN ... + */ +#define OPT_WANT_PEER_BACKUP_STORAGE 40 +#define OPT_PROVIDE_PEER_BACKUP_STORAGE 42 + #endif /* LIGHTNING_COMMON_FEATURES_H */ diff --git a/connectd/gossip_rcvd_filter.c b/connectd/gossip_rcvd_filter.c index b3a73ee24fba..740b52957ffe 100644 --- a/connectd/gossip_rcvd_filter.c +++ b/connectd/gossip_rcvd_filter.c @@ -87,6 +87,8 @@ static bool is_msg_gossip_broadcast(const u8 *cursor) case WIRE_TX_INIT_RBF: case WIRE_TX_ACK_RBF: case WIRE_TX_ABORT: + case WIRE_PEER_STORAGE: + case WIRE_YOUR_PEER_STORAGE: case WIRE_OPEN_CHANNEL2: case WIRE_ACCEPT_CHANNEL2: #if EXPERIMENTAL_FEATURES diff --git a/connectd/gossip_store.c b/connectd/gossip_store.c index 9101812c1736..0ebb4f37772a 100644 --- a/connectd/gossip_store.c +++ b/connectd/gossip_store.c @@ -92,6 +92,8 @@ static bool public_msg_type(enum peer_wire type) case WIRE_REPLY_CHANNEL_RANGE: case WIRE_GOSSIP_TIMESTAMP_FILTER: case WIRE_ONION_MESSAGE: + case WIRE_PEER_STORAGE: + case WIRE_YOUR_PEER_STORAGE: #if EXPERIMENTAL_FEATURES case WIRE_STFU: #endif diff --git a/connectd/multiplex.c b/connectd/multiplex.c index 5623a4c6b992..72db922e9931 100644 --- a/connectd/multiplex.c +++ b/connectd/multiplex.c @@ -385,6 +385,8 @@ static bool is_urgent(enum peer_wire type) case WIRE_REPLY_CHANNEL_RANGE: case WIRE_GOSSIP_TIMESTAMP_FILTER: case WIRE_ONION_MESSAGE: + case WIRE_PEER_STORAGE: + case WIRE_YOUR_PEER_STORAGE: #if EXPERIMENTAL_FEATURES case WIRE_STFU: #endif @@ -721,7 +723,7 @@ static bool handle_custommsg(struct daemon *daemon, const u8 *msg) { enum peer_wire type = fromwire_peektype(msg); - if (type % 2 == 1 && !peer_wire_is_defined(type)) { + if (type % 2 == 1 && !peer_wire_is_internal(type)) { /* The message is not part of the messages we know how to * handle. Assuming this is a custommsg, we just forward it to the * master. */ diff --git a/doc/lightning-listconfigs.7.md b/doc/lightning-listconfigs.7.md index 3232d88a0fb9..fe99657469a3 100644 --- a/doc/lightning-listconfigs.7.md +++ b/doc/lightning-listconfigs.7.md @@ -62,6 +62,7 @@ On success, an object is returned, containing: - **experimental-offers** (boolean, optional): `experimental-offers` field from config or cmdline, or default - **experimental-shutdown-wrong-funding** (boolean, optional): `experimental-shutdown-wrong-funding` field from config or cmdline, or default - **experimental-websocket-port** (u16, optional): `experimental-websocket-port` field from config or cmdline, or default +- **experimental-peer-storage** (boolean, optional): `experimental-peer-storage` field from config or cmdline, or default *(added v23.02)* - **database-upgrade** (boolean, optional): `database-upgrade` field from config or cmdline - **rgb** (hex, optional): `rgb` field from config or cmdline, or default (always 6 characters) - **alias** (string, optional): `alias` field from config or cmdline, or default @@ -223,4 +224,4 @@ RESOURCES Main web site: -[comment]: # ( SHA256STAMP:581225b26efd84bfa99dc98e7a91e6fae11ef0b11939031d3da07f751f6d8f87) +[comment]: # ( SHA256STAMP:1088401b9aeae1e079dab550d3b035ef82195f0466ad471bc7373386182f37dc) diff --git a/doc/lightningd-config.5.md b/doc/lightningd-config.5.md index f5037669ef2a..0621ccb72e91 100644 --- a/doc/lightningd-config.5.md +++ b/doc/lightningd-config.5.md @@ -714,6 +714,12 @@ connections on that port, on any IPv4 and IPv6 addresses you listen to ([bolt][bolt] #891). The normal protocol is expected to be sent over WebSocket binary frames once the connection is upgraded. +* **experimental-peer-storage** + + Specifying this option means we will store up to 64k of encrypted +data for our peers, and give them our (encrypted!) backup data to +store as well, based on a protocol similar to [bolt][bolt] #881. + BUGS ---- diff --git a/doc/schemas/listconfigs.schema.json b/doc/schemas/listconfigs.schema.json index 8149e40c1627..b2ebdd003c68 100644 --- a/doc/schemas/listconfigs.schema.json +++ b/doc/schemas/listconfigs.schema.json @@ -137,6 +137,11 @@ "type": "u16", "description": "`experimental-websocket-port` field from config or cmdline, or default" }, + "experimental-peer-storage": { + "type": "boolean", + "added": "v23.02", + "description": "`experimental-peer-storage` field from config or cmdline, or default" + }, "database-upgrade": { "type": "boolean", "description": "`database-upgrade` field from config or cmdline" diff --git a/gossipd/gossipd.c b/gossipd/gossipd.c index 9d34f91c57db..ab995192190d 100644 --- a/gossipd/gossipd.c +++ b/gossipd/gossipd.c @@ -569,6 +569,8 @@ static void handle_recv_gossip(struct daemon *daemon, const u8 *outermsg) case WIRE_OPEN_CHANNEL2: case WIRE_ACCEPT_CHANNEL2: case WIRE_ONION_MESSAGE: + case WIRE_PEER_STORAGE: + case WIRE_YOUR_PEER_STORAGE: #if EXPERIMENTAL_FEATURES case WIRE_STFU: #endif diff --git a/lightningd/connect_control.c b/lightningd/connect_control.c index ca50c7c42d32..93c617a0cc6b 100644 --- a/lightningd/connect_control.c +++ b/lightningd/connect_control.c @@ -752,7 +752,9 @@ static struct command_result *json_sendcustommsg(struct command *cmd, return command_param_failed(); type = fromwire_peektype(msg); - if (peer_wire_is_defined(type)) { + + /* Allow peer_storage and your_peer_storage msgtypes */ + if (peer_wire_is_internal(type)) { return command_fail( cmd, JSONRPC2_INVALID_REQUEST, "Cannot send messages of type %d (%s). It is not possible " @@ -779,11 +781,11 @@ static struct command_result *json_sendcustommsg(struct command *cmd, type_to_string(cmd, struct node_id, dest)); } - if (peer->connected != PEER_CONNECTED) + /* We allow messages from plugins responding to peer_connected hook, + * so can be PEER_CONNECTING. */ + if (peer->connected == PEER_DISCONNECTED) return command_fail(cmd, JSONRPC2_INVALID_REQUEST, - "Peer is %s", - peer->connected == PEER_DISCONNECTED - ? "not connected" : "still connecting"); + "Peer is not connected"); subd_send_msg(cmd->ld->connectd, take(towire_connectd_custommsg_out(cmd, dest, msg))); diff --git a/lightningd/options.c b/lightningd/options.c index 380de41464bd..475c0d387cc5 100644 --- a/lightningd/options.c +++ b/lightningd/options.c @@ -1078,6 +1078,15 @@ static char *opt_set_shutdown_wrong_funding(struct lightningd *ld) return NULL; } +static char *opt_set_peer_storage(struct lightningd *ld) +{ + feature_set_or(ld->our_features, + take(feature_set_for_feature(NULL, OPT_PROVIDE_PEER_BACKUP_STORAGE))); + feature_set_or(ld->our_features, + take(feature_set_for_feature(NULL, OPT_WANT_PEER_BACKUP_STORAGE))); + return NULL; +} + static char *opt_set_offers(struct lightningd *ld) { ld->config.exp_offers = true; @@ -1156,6 +1165,9 @@ static void register_opts(struct lightningd *ld) opt_register_early_noarg("--experimental-shutdown-wrong-funding", opt_set_shutdown_wrong_funding, ld, "EXPERIMENTAL: allow shutdown with alternate txids"); + opt_register_early_noarg("--experimental-peer-storage", + opt_set_peer_storage, ld, + "EXPERIMENTAL: enable peer backup storage and restore"); opt_register_early_arg("--announce-addr-dns", opt_set_bool_arg, opt_show_bool, &ld->announce_dns, @@ -1641,6 +1653,11 @@ static void add_config(struct lightningd *ld, feature_offered(ld->our_features ->bits[INIT_FEATURE], OPT_SHUTDOWN_WRONG_FUNDING)); + } else if (opt->cb == (void *)opt_set_peer_storage) { + json_add_bool(response, name0, + feature_offered(ld->our_features + ->bits[INIT_FEATURE], + OPT_PROVIDE_PEER_BACKUP_STORAGE)); } else if (opt->cb == (void *)plugin_opt_flag_set) { /* Noop, they will get added below along with the * OPT_HASARG options. */ diff --git a/lightningd/peer_control.c b/lightningd/peer_control.c index 6a9d19369111..dd137efe6192 100644 --- a/lightningd/peer_control.c +++ b/lightningd/peer_control.c @@ -1071,7 +1071,8 @@ struct peer_connected_hook_payload { struct wireaddr_internal addr; struct wireaddr *remote_addr; bool incoming; - struct peer *peer; + /* We don't keep a pointer to peer: it might be freed! */ + struct node_id peer_id; u8 *error; }; @@ -1079,9 +1080,8 @@ static void peer_connected_serialize(struct peer_connected_hook_payload *payload, struct json_stream *stream, struct plugin *plugin) { - const struct peer *p = payload->peer; json_object_start(stream, "peer"); - json_add_node_id(stream, "id", &p->id); + json_add_node_id(stream, "id", &payload->peer_id); json_add_string(stream, "direction", payload->incoming ? "in" : "out"); json_add_string( stream, "addr", @@ -1090,7 +1090,10 @@ peer_connected_serialize(struct peer_connected_hook_payload *payload, json_add_string( stream, "remote_addr", type_to_string(stream, struct wireaddr, payload->remote_addr)); - json_add_hex_talarr(stream, "features", p->their_features); + /* Since this is start of hook, peer is always in table! */ + json_add_hex_talarr(stream, "features", + peer_by_id(payload->ld, &payload->peer_id) + ->their_features); json_object_end(stream); /* .peer */ } @@ -1186,7 +1189,7 @@ static void peer_connected_hook_final(struct peer_connected_hook_payload *payloa struct lightningd *ld = payload->ld; struct channel *channel; struct wireaddr_internal addr = payload->addr; - struct peer *peer = payload->peer; + struct peer *peer; u8 *error; /* Whatever happens, we free payload (it's currently a child @@ -1194,9 +1197,16 @@ static void peer_connected_hook_final(struct peer_connected_hook_payload *payloa * subd). */ tal_steal(tmpctx, payload); + /* Peer might have gone away while we were waiting for plugin! */ + peer = peer_by_id(ld, &payload->peer_id); + if (!peer) + return; + /* If we disconnected in the meantime, forget about it. - * (disconnect will have failed any connect commands). */ - if (peer->connected == PEER_DISCONNECTED) + * (disconnect will have failed any connect commands). + * And if it has reconnected, and we're the second time the + * hook has been called, it'll be PEER_CONNECTED. */ + if (peer->connected != PEER_CONNECTING) return; /* Check for specific errors of a hook */ @@ -1425,9 +1435,7 @@ void peer_connected(struct lightningd *ld, const u8 *msg) tal_free(peer->remote_addr); peer->remote_addr = NULL; peer_update_features(peer, their_features); - - tal_steal(peer, hook_payload); - hook_payload->peer = peer; + hook_payload->peer_id = id; /* If there's a connect command, use its id as basis for hook id */ cmd_id = connect_any_cmd_id(tmpctx, ld, peer); diff --git a/openingd/dualopend.c b/openingd/dualopend.c index 4b2956f97508..e8f68ce1ed49 100644 --- a/openingd/dualopend.c +++ b/openingd/dualopend.c @@ -1443,6 +1443,8 @@ static u8 *opening_negotiate_msg(const tal_t *ctx, struct state *state) case WIRE_WARNING: case WIRE_PING: case WIRE_PONG: + case WIRE_PEER_STORAGE: + case WIRE_YOUR_PEER_STORAGE: #if EXPERIMENTAL_FEATURES case WIRE_STFU: #endif @@ -1817,6 +1819,8 @@ static bool run_tx_interactive(struct state *state, case WIRE_REPLY_SHORT_CHANNEL_IDS_END: case WIRE_PING: case WIRE_PONG: + case WIRE_PEER_STORAGE: + case WIRE_YOUR_PEER_STORAGE: #if EXPERIMENTAL_FEATURES case WIRE_STFU: #endif @@ -4166,6 +4170,8 @@ static u8 *handle_peer_in(struct state *state) case WIRE_WARNING: case WIRE_PING: case WIRE_PONG: + case WIRE_PEER_STORAGE: + case WIRE_YOUR_PEER_STORAGE: #if EXPERIMENTAL_FEATURES case WIRE_STFU: #endif diff --git a/plugins/chanbackup.c b/plugins/chanbackup.c index 606a96ab7cd0..bf906b7edc76 100644 --- a/plugins/chanbackup.c +++ b/plugins/chanbackup.c @@ -5,12 +5,15 @@ #include #include #include +#include #include #include +#include #include #include #include #include +#include #include #include #include @@ -20,16 +23,19 @@ #define HEADER_LEN crypto_secretstream_xchacha20poly1305_HEADERBYTES #define ABYTES crypto_secretstream_xchacha20poly1305_ABYTES +#define FILENAME "emergency.recover" + /* VERSION is the current version of the data encrypted in the file */ #define VERSION ((u64)1) /* Global secret object to keep the derived encryption key for the SCB */ static struct secret secret; +static bool peer_backup; /* Helper to fetch out SCB from the RPC call */ static bool json_to_scb_chan(const char *buffer, - const jsmntok_t *tok, - struct scb_chan ***channels) + const jsmntok_t *tok, + struct scb_chan ***channels) { size_t i; const jsmntok_t *t; @@ -69,10 +75,10 @@ static void write_scb(struct plugin *p, scb_chan_arr)); u8 *encrypted_scb = tal_arr(tmpctx, - u8, - tal_bytelen(decrypted_scb) + - ABYTES + - HEADER_LEN); + u8, + tal_bytelen(decrypted_scb) + + ABYTES + + HEADER_LEN); crypto_secretstream_xchacha20poly1305_state crypto_state; @@ -85,20 +91,20 @@ static void write_scb(struct plugin *p, } if (crypto_secretstream_xchacha20poly1305_push(&crypto_state, - encrypted_scb + - HEADER_LEN, - NULL, decrypted_scb, - tal_bytelen(decrypted_scb), - /* Additional data and tag */ - NULL, 0, 0)) { + encrypted_scb + + HEADER_LEN, + NULL, decrypted_scb, + tal_bytelen(decrypted_scb), + /* Additional data and tag */ + NULL, 0, 0)) { plugin_err(p, "Can't encrypt the data!"); return; } if (!write_all(fd, encrypted_scb, tal_bytelen(encrypted_scb))) { - unlink_noerr("scb.tmp"); - plugin_err(p, "Writing encrypted SCB: %s", - strerror(errno)); + unlink_noerr("scb.tmp"); + plugin_err(p, "Writing encrypted SCB: %s", + strerror(errno)); } } @@ -110,7 +116,7 @@ static void maybe_create_new_scb(struct plugin *p, /* Note that this is opened for write-only, even though the permissions * are set to read-only. That's perfectly valid! */ - int fd = open("emergency.recover", O_CREAT|O_EXCL|O_WRONLY, 0400); + int fd = open(FILENAME, O_CREAT|O_EXCL|O_WRONLY, 0400); if (fd < 0) { /* Don't do anything if the file already exists. */ if (errno == EEXIST) @@ -119,7 +125,7 @@ static void maybe_create_new_scb(struct plugin *p, } /* Comes here only if the file haven't existed before */ - unlink_noerr("emergency.recover"); + unlink_noerr(FILENAME); /* This couldn't give EEXIST because we call unlink_noerr("scb.tmp") * in INIT */ @@ -160,60 +166,54 @@ static void maybe_create_new_scb(struct plugin *p, close(fd); /* This will update the scb file */ - rename("scb.tmp", "emergency.recover"); + rename("scb.tmp", FILENAME); } +static u8 *get_file_data(const tal_t *ctx, struct plugin *p) +{ + u8 *scb = grab_file(ctx, FILENAME); + if (!scb) { + plugin_err(p, "Cannot read emergency.recover: %s", strerror(errno)); + } else { + /* grab_file adds nul term */ + tal_resize(&scb, tal_bytelen(scb) - 1); + } + return scb; +} /* Returns decrypted SCB in form of a u8 array */ static u8 *decrypt_scb(struct plugin *p) { - struct stat st; - int fd = open("emergency.recover", O_RDONLY); - - if (stat("emergency.recover", &st) != 0) - plugin_err(p, "SCB file is corrupted!: %s", - strerror(errno)); - - u8 final[st.st_size]; - - if (!read_all(fd, &final, st.st_size)) { - plugin_log(p, LOG_DBG, "SCB file is corrupted!: %s", - strerror(errno)); - return NULL; - } + u8 *filedata = get_file_data(tmpctx, p); crypto_secretstream_xchacha20poly1305_state crypto_state; - if (st.st_size < ABYTES + - HEADER_LEN) + if (tal_bytelen(filedata) < ABYTES + + HEADER_LEN) plugin_err(p, "SCB file is corrupted!"); - u8 *ans = tal_arr(tmpctx, u8, st.st_size - - ABYTES - - HEADER_LEN); + u8 *decrypt_scb = tal_arr(tmpctx, u8, tal_bytelen(filedata) - + ABYTES - + HEADER_LEN); /* The header part */ if (crypto_secretstream_xchacha20poly1305_init_pull(&crypto_state, - final, + filedata, (&secret)->data) != 0) { plugin_err(p, "SCB file is corrupted!"); } - if (crypto_secretstream_xchacha20poly1305_pull(&crypto_state, ans, + if (crypto_secretstream_xchacha20poly1305_pull(&crypto_state, decrypt_scb, NULL, 0, - final + + filedata + HEADER_LEN, - st.st_size - + tal_bytelen(filedata)- HEADER_LEN, NULL, 0) != 0) { plugin_err(p, "SCB file is corrupted!"); } - - if (close(fd) != 0) - plugin_err(p, "Closing: %s", strerror(errno)); - - return ans; + return decrypt_scb; } static struct command_result *after_recover_rpc(struct command *cmd, @@ -236,8 +236,8 @@ static struct command_result *after_recover_rpc(struct command *cmd, /* Recovers the channels by making RPC to `recoverchannel` */ static struct command_result *json_emergencyrecover(struct command *cmd, - const char *buf, - const jsmntok_t *params) + const char *buf, + const jsmntok_t *params) { struct out_req *req; u64 version; @@ -259,12 +259,12 @@ static struct command_result *json_emergencyrecover(struct command *cmd, if (version != VERSION) { plugin_err(cmd->plugin, - "Incompatible version, Contact the admin!"); + "Incompatible SCB file version on disk, contact the admin!"); } req = jsonrpc_request_start(cmd->plugin, cmd, "recoverchannel", - after_recover_rpc, - &forward_error, NULL); + after_recover_rpc, + &forward_error, NULL); json_array_start(req->js, "scb"); for (size_t i=0; iplugin, LOG_DBG, "Sent their peer storage!"); + return command_hook_success(cmd); +} + +static struct command_result +*peer_after_send_their_peer_strg_err(struct command *cmd, + const char *buf, + const jsmntok_t *params, + void *cb_arg UNUSED) +{ + plugin_log(cmd->plugin, LOG_DBG, "Unable to send Peer storage!"); + return command_hook_success(cmd); +} + +static struct command_result *peer_after_listdatastore(struct command *cmd, + const u8 *hexdata, + struct node_id *nodeid) +{ + if (tal_bytelen(hexdata) == 0) + return command_hook_success(cmd); + struct out_req *req; + + u8 *payload = towire_your_peer_storage(cmd, hexdata); + + plugin_log(cmd->plugin, LOG_DBG, + "sending their backup from our datastore"); + + req = jsonrpc_request_start(cmd->plugin, + cmd, + "sendcustommsg", + peer_after_send_their_peer_strg, + peer_after_send_their_peer_strg_err, + NULL); + + json_add_node_id(req->js, "node_id", nodeid); + json_add_hex(req->js, "msg", payload, + tal_bytelen(payload)); + + return send_outreq(cmd->plugin, req); +} + +static struct command_result *peer_after_send_scb(struct command *cmd, + const char *buf, + const jsmntok_t *params, + struct node_id *nodeid) +{ + plugin_log(cmd->plugin, LOG_DBG, "Peer storage sent!"); + + return jsonrpc_get_datastore_binary(cmd->plugin, + cmd, + tal_fmt(cmd, + "chanbackup/peers/%s", + type_to_string(tmpctx, + struct node_id, + nodeid)), + peer_after_listdatastore, + nodeid); +} + +static struct command_result *peer_after_send_scb_failed(struct command *cmd, + const char *buf, + const jsmntok_t *params, + struct node_id *nodeid) +{ + plugin_log(cmd->plugin, LOG_DBG, "Peer storage send failed %.*s!", + json_tok_full_len(params), json_tok_full(buf, params)); + return command_hook_success(cmd); +} + +struct info { + size_t idx; +}; + +static struct command_result *after_send_scb_single(struct command *cmd, + const char *buf, + const jsmntok_t *params, + struct info *info) +{ + plugin_log(cmd->plugin, LOG_INFORM, "Peer storage sent!"); + if (--info->idx != 0) + return command_still_pending(cmd); + + return notification_handled(cmd); +} + +static struct command_result *after_send_scb_single_fail(struct command *cmd, + const char *buf, + const jsmntok_t *params, + struct info *info) +{ + plugin_log(cmd->plugin, LOG_DBG, "Peer storage send failed!"); + if (--info->idx != 0) + return command_still_pending(cmd); + + return notification_handled(cmd); +} + +static struct command_result *after_listpeers(struct command *cmd, + const char *buf, + const jsmntok_t *params, + void *cb_arg UNUSED) +{ + const jsmntok_t *peers, *peer; + struct out_req *req; + size_t i; + struct info *info = tal(cmd, struct info); + bool is_connected; + u8 *serialise_scb; + + serialise_scb = towire_peer_storage(cmd, + get_file_data(tmpctx, cmd->plugin)); + + peers = json_get_member(buf, params, "peers"); + + info->idx = 0; + json_for_each_arr(i, peer, peers) { + json_to_bool(buf, json_get_member(buf, peer, "connected"), + &is_connected); + + if (is_connected) { + const jsmntok_t *nodeid; + struct node_id node_id; + + nodeid = json_get_member(buf, peer, "id"); + json_to_node_id(buf, nodeid, &node_id); + + req = jsonrpc_request_start(cmd->plugin, + cmd, + "sendcustommsg", + after_send_scb_single, + after_send_scb_single_fail, + info); + + json_add_node_id(req->js, "node_id", &node_id); + json_add_hex(req->js, "msg", serialise_scb, + tal_bytelen(serialise_scb)); + info->idx++; + send_outreq(cmd->plugin, req); + } + } + + if (info->idx == 0) + return notification_handled(cmd); + return command_still_pending(cmd); } static struct command_result *after_staticbackup(struct command *cmd, @@ -324,11 +477,20 @@ static struct command_result *after_staticbackup(struct command *cmd, { struct scb_chan **scb_chan; const jsmntok_t *scbs = json_get_member(buf, params, "scb"); + struct out_req *req; json_to_scb_chan(buf, scbs, &scb_chan); plugin_log(cmd->plugin, LOG_INFORM, "Updating the SCB"); update_scb(cmd->plugin, scb_chan); - return notification_handled(cmd); + struct info *info = tal(cmd, struct info); + info->idx = 0; + req = jsonrpc_request_start(cmd->plugin, + cmd, + "listpeers", + after_listpeers, + &forward_error, + info); + return send_outreq(cmd->plugin, req); } static struct command_result *json_state_changed(struct command *cmd, @@ -338,16 +500,11 @@ static struct command_result *json_state_changed(struct command *cmd, const jsmntok_t *notiftok = json_get_member(buf, params, "channel_state_changed"), - *statetok = json_get_member(buf, notiftok, "new_state"); + *statetok = json_get_member(buf, notiftok, "new_state"); - /* FIXME: I wanted to update the file on CHANNELD_AWAITING_LOCKIN, - * But I don't get update for it, maybe because there is - * no previous_state, also apparently `channel_opened` gets published - * when *peer* funded a channel with us? - * So, is their no way to get a notif on CHANNELD_AWAITING_LOCKIN? */ if (json_tok_streq(buf, statetok, "CLOSED") || - json_tok_streq(buf, statetok, "CHANNELD_NORMAL")) { - + json_tok_streq(buf, statetok, "CHANNELD_AWAITING_LOCKIN") || + json_tok_streq(buf, statetok, "DUALOPENED_AWAITING_LOCKIN")) { struct out_req *req; req = jsonrpc_request_start(cmd->plugin, cmd, @@ -362,6 +519,223 @@ static struct command_result *json_state_changed(struct command *cmd, return notification_handled(cmd); } + +/* We use the hook here, since we want to send data to peer before any + * reconnect messages (which might make it hang up!) */ +static struct command_result *peer_connected(struct command *cmd, + const char *buf, + const jsmntok_t *params) +{ + struct node_id *node_id; + struct out_req *req; + u8 *serialise_scb; + const char *err; + + if (!peer_backup) + return command_hook_success(cmd); + + serialise_scb = towire_peer_storage(cmd, + get_file_data(tmpctx, cmd->plugin)); + node_id = tal(cmd, struct node_id); + err = json_scan(cmd, buf, params, + "{peer:{id:%}}", + JSON_SCAN(json_to_node_id, node_id)); + if (err) { + plugin_err(cmd->plugin, + "peer_connected hook did not scan %s: %.*s", + err, json_tok_full_len(params), + json_tok_full(buf, params)); + } + + req = jsonrpc_request_start(cmd->plugin, + cmd, + "sendcustommsg", + peer_after_send_scb, + peer_after_send_scb_failed, + node_id); + + json_add_node_id(req->js, "node_id", node_id); + json_add_hex(req->js, "msg", serialise_scb, + tal_bytelen(serialise_scb)); + + return send_outreq(cmd->plugin, req); +} + +static struct command_result *failed_peer_restore(struct command *cmd, + struct node_id *node_id, + char *reason) +{ + plugin_log(cmd->plugin, LOG_DBG, "PeerStorageFailed!: %s: %s", + type_to_string(tmpctx, struct node_id, node_id), + reason); + return command_hook_success(cmd); +} + +static struct command_result *datastore_success(struct command *cmd, + const char *buf, + const jsmntok_t *result, + char *what) +{ + plugin_log(cmd->plugin, LOG_DBG, "datastore succeeded for %s", what); + return command_hook_success(cmd); +} + +static struct command_result *datastore_failed(struct command *cmd, + const char *buf, + const jsmntok_t *result, + char *what) +{ + plugin_log(cmd->plugin, LOG_DBG, "datastore failed for %s: %.*s", + what, json_tok_full_len(result), json_tok_full(buf, result)); + return command_hook_success(cmd); +} + +static struct command_result *handle_your_peer_storage(struct command *cmd, + const char *buf, + const jsmntok_t *params) +{ + struct node_id node_id; + u8 *payload, *payload_deserialise; + const char *err; + + if (!peer_backup) + return command_hook_success(cmd); + + err = json_scan(cmd, buf, params, + "{payload:%,peer_id:%}", + JSON_SCAN_TAL(cmd, json_tok_bin_from_hex, &payload), + JSON_SCAN(json_to_node_id, &node_id)); + if (err) { + plugin_err(cmd->plugin, + "`your_peer_storage` response did not scan %s: %.*s", + err, json_tok_full_len(params), + json_tok_full(buf, params)); + } + + if (fromwire_peer_storage(cmd, payload, &payload_deserialise)) { + return jsonrpc_set_datastore_binary(cmd->plugin, + cmd, + tal_fmt(cmd, + "chanbackup/peers/%s", + type_to_string(tmpctx, + struct node_id, + &node_id)), + payload_deserialise, + "create-or-replace", + datastore_success, + datastore_failed, + "Saving chanbackup/peers/"); + } else if (fromwire_your_peer_storage(cmd, payload, &payload_deserialise)) { + plugin_log(cmd->plugin, LOG_DBG, + "Received peer_storage from peer."); + + crypto_secretstream_xchacha20poly1305_state crypto_state; + + if (tal_bytelen(payload_deserialise) < ABYTES + + HEADER_LEN) + return failed_peer_restore(cmd, &node_id, + "Too short!"); + + u8 *decoded_bkp = tal_arr(tmpctx, u8, + tal_bytelen(payload_deserialise) - + ABYTES - + HEADER_LEN); + + /* The header part */ + if (crypto_secretstream_xchacha20poly1305_init_pull(&crypto_state, + payload_deserialise, + (&secret)->data) != 0) + return failed_peer_restore(cmd, &node_id, + "Peer altered our data"); + + if (crypto_secretstream_xchacha20poly1305_pull(&crypto_state, + decoded_bkp, + NULL, 0, + payload_deserialise + + HEADER_LEN, + tal_bytelen(payload_deserialise) - + HEADER_LEN, + NULL, 0) != 0) + return failed_peer_restore(cmd, &node_id, + "Peer altered our data"); + + + return jsonrpc_set_datastore_binary(cmd->plugin, + cmd, + "chanbackup/latestscb", + decoded_bkp, + "create-or-replace", + datastore_success, + datastore_failed, + "Saving latestscb"); + } else { + plugin_log(cmd->plugin, LOG_DBG, + "Peer sent bad custom message for chanbackup!"); + return command_hook_success(cmd); + } +} + +static struct command_result *after_latestscb(struct command *cmd, + const u8 *res, + void *cb_arg UNUSED) +{ + u64 version; + u32 timestamp; + struct scb_chan **scb; + struct json_stream *response; + struct out_req *req; + + if (tal_bytelen(res) == 0) { + response = jsonrpc_stream_success(cmd); + + json_add_string(response, "result", + "No backup received from peers"); + return command_finished(cmd, response); + } + + if (!fromwire_static_chan_backup(cmd, + res, + &version, + ×tamp, + &scb)) { + plugin_err(cmd->plugin, "Corrupted SCB on disk!"); + } + + if (version != VERSION) { + plugin_err(cmd->plugin, + "Incompatible version, Contact the admin!"); + } + + req = jsonrpc_request_start(cmd->plugin, cmd, "recoverchannel", + after_recover_rpc, + &forward_error, NULL); + + json_array_start(req->js, "scb"); + for (size_t i=0; ijs, NULL, scb_hex, tal_bytelen(scb_hex)); + } + json_array_end(req->js); + + return send_outreq(cmd->plugin, req); + +} + +static struct command_result *json_restorefrompeer(struct command *cmd, + const char *buf, + const jsmntok_t *params) +{ + if (!param(cmd, buf, params, NULL)) + return command_param_failed(); + + return jsonrpc_get_datastore_binary(cmd->plugin, + cmd, + "chanbackup/latestscb", + after_latestscb, + NULL); +} + static const char *init(struct plugin *p, const char *buf UNUSED, const jsmntok_t *config UNUSED) @@ -369,6 +743,14 @@ static const char *init(struct plugin *p, struct scb_chan **scb_chan; const char *info = "scb secret"; u8 *info_hex = tal_dup_arr(tmpctx, u8, (u8*)info, strlen(info), 0); + u8 *features; + + /* Figure out if they specified --experimental-peer-storage */ + rpc_scan(p, "getinfo", + take(json_out_obj(NULL, NULL, NULL)), + "{our_features:{init:%}}", + JSON_SCAN_TAL(tmpctx, json_tok_bin_from_hex, &features)); + peer_backup = feature_offered(features, OPT_WANT_PEER_BACKUP_STORAGE); rpc_scan(p, "staticbackup", take(json_out_obj(NULL, NULL, NULL)), @@ -398,21 +780,43 @@ static const struct plugin_notification notifs[] = { } }; -static const struct plugin_command commands[] = { { +static const struct plugin_hook hooks[] = { + { + "custommsg", + handle_your_peer_storage, + }, + { + "peer_connected", + peer_connected, + }, +}; + +static const struct plugin_command commands[] = { + { "emergencyrecover", "recovery", "Populates the DB with stub channels", "returns stub channel-id's on completion", json_emergencyrecover, - } + }, + { + "restorefrompeer", + "recovery", + "Checks if i have got a backup from a peer, and if so, will stub " + "those channels in the database and if is successful, will return " + "list of channels that have been successfully stubbed", + "return channel-id's on completion", + json_restorefrompeer, + }, }; int main(int argc, char *argv[]) { - setup_locale(); - plugin_main(argv, init, PLUGIN_RESTARTABLE, true, NULL, + setup_locale(); + + plugin_main(argv, init, PLUGIN_STATIC, true, NULL, commands, ARRAY_SIZE(commands), - notifs, ARRAY_SIZE(notifs), NULL, 0, + notifs, ARRAY_SIZE(notifs), hooks, ARRAY_SIZE(hooks), NULL, 0, /* Notification topics we publish */ NULL); } diff --git a/tests/test_misc.py b/tests/test_misc.py index 10532241a4aa..d144c66a89aa 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -2505,6 +2505,48 @@ def test_emergencyrecover(node_factory, bitcoind): assert l2.rpc.listfunds()["channels"][0]["state"] == "ONCHAIN" +@unittest.skipIf(os.getenv('TEST_DB_PROVIDER', 'sqlite3') != 'sqlite3', "deletes database, which is assumed sqlite3") +def test_restorefrompeer(node_factory, bitcoind): + """ + Test restorefrompeer + """ + l1, l2 = node_factory.get_nodes(2, [{'allow_broken_log': True, + 'experimental-peer-storage': None, + 'may_reconnect': True}, + {'experimental-peer-storage': None, + 'may_reconnect': True}]) + + l1.rpc.connect(l2.info['id'], 'localhost', l2.port) + + c12, _ = l1.fundchannel(l2, 10**5) + assert l1.daemon.is_in_log('Peer storage sent!') + assert l2.daemon.is_in_log('Peer storage sent!') + + l1.stop() + os.unlink(os.path.join(l1.daemon.lightning_dir, TEST_NETWORK, "lightningd.sqlite3")) + + l1.start() + assert l1.daemon.is_in_log('Server started with public key') + + l1.rpc.connect(l2.info['id'], 'localhost', l2.port) + l1.daemon.wait_for_log('peer_in WIRE_YOUR_PEER_STORAGE') + + assert l1.rpc.restorefrompeer()['stubs'][0] == _['channel_id'] + + l1.daemon.wait_for_log('peer_out WIRE_ERROR') + l2.daemon.wait_for_log('State changed from CHANNELD_NORMAL to AWAITING_UNILATERAL') + + bitcoind.generate_block(5, wait_for_mempool=1) + sync_blockheight(bitcoind, [l1, l2]) + + l1.daemon.wait_for_log(r'All outputs resolved.*') + wait_for(lambda: l1.rpc.listfunds()["channels"][0]["state"] == "ONCHAIN") + + # Check if funds are recovered. + assert l1.rpc.listfunds()["channels"][0]["state"] == "ONCHAIN" + assert l2.rpc.listfunds()["channels"][0]["state"] == "ONCHAIN" + + def test_commitfee_option(node_factory): """Sanity check for the --commit-fee startup option.""" l1, l2 = node_factory.get_nodes(2, opts=[{"commit-fee": "200"}, {}]) diff --git a/wire/extracted_peer_07_peer_storage.patch b/wire/extracted_peer_07_peer_storage.patch new file mode 100644 index 000000000000..71afebd85cd0 --- /dev/null +++ b/wire/extracted_peer_07_peer_storage.patch @@ -0,0 +1,15 @@ +--- wire/peer_wire.csv 2022-07-18 13:49:29.079542016 +0530 ++++ - 2022-07-18 13:58:17.706696582 +0530 +@@ -249,6 +249,12 @@ + msgdata,channel_reestablish,next_revocation_number,u64, + msgdata,channel_reestablish,your_last_per_commitment_secret,byte,32 + msgdata,channel_reestablish,my_current_per_commitment_point,point, ++msgtype,peer_storage,7 ++msgdata,peer_storage,len,u16, ++msgdata,peer_storage,blob,byte,len ++msgtype,your_peer_storage,9 ++msgdata,your_peer_storage,len,u16, ++msgdata,your_peer_storage,blob,byte,len + msgtype,announcement_signatures,259 + msgdata,announcement_signatures,channel_id,channel_id, + msgdata,announcement_signatures,short_channel_id,short_channel_id, diff --git a/wire/extracted_peer_exp_upgradable.patch b/wire/extracted_peer_exp_upgradable.patch index e5818692ffe4..c168586abeca 100644 --- a/wire/extracted_peer_exp_upgradable.patch +++ b/wire/extracted_peer_exp_upgradable.patch @@ -13,6 +13,6 @@ +tlvdata,channel_reestablish_tlvs,current_channel_type,type,byte,... +tlvtype,channel_reestablish_tlvs,upgradable_channel_type,7 +tlvdata,channel_reestablish_tlvs,upgradable_channel_type,type,byte,... - msgtype,announcement_signatures,259 - msgdata,announcement_signatures,channel_id,channel_id, - msgdata,announcement_signatures,short_channel_id,short_channel_id, + msgtype,peer_storage,7 + msgdata,peer_storage,len,u16, + msgdata,peer_storage,blob,byte,len diff --git a/wire/peer_wire.c b/wire/peer_wire.c index 29e67f386db3..1d3772c8fd65 100644 --- a/wire/peer_wire.c +++ b/wire/peer_wire.c @@ -45,6 +45,8 @@ static bool unknown_type(enum peer_wire t) case WIRE_TX_INIT_RBF: case WIRE_TX_ACK_RBF: case WIRE_TX_ABORT: + case WIRE_PEER_STORAGE: + case WIRE_YOUR_PEER_STORAGE: case WIRE_OPEN_CHANNEL2: case WIRE_ACCEPT_CHANNEL2: #if EXPERIMENTAL_FEATURES @@ -101,6 +103,8 @@ bool is_msg_for_gossipd(const u8 *cursor) case WIRE_OPEN_CHANNEL2: case WIRE_ACCEPT_CHANNEL2: case WIRE_ONION_MESSAGE: + case WIRE_PEER_STORAGE: + case WIRE_YOUR_PEER_STORAGE: #if EXPERIMENTAL_FEATURES case WIRE_STFU: #endif @@ -116,6 +120,20 @@ bool is_unknown_msg_discardable(const u8 *cursor) return unknown_type(t) && (t & 1); } +/* Returns true if the message type should be handled by CLN's core */ +bool peer_wire_is_internal(enum peer_wire type) +{ + /* Unknown messages are not handled by CLN */ + if (!peer_wire_is_defined(type)) + return false; + + /* handled by pluigns */ + if (type == WIRE_PEER_STORAGE || type == WIRE_YOUR_PEER_STORAGE) + return false; + + return true; +} + /* Extract channel_id from various packets, return true if possible. */ bool extract_channel_id(const u8 *in_pkt, struct channel_id *channel_id) { @@ -140,6 +158,8 @@ bool extract_channel_id(const u8 *in_pkt, struct channel_id *channel_id) case WIRE_REPLY_CHANNEL_RANGE: case WIRE_GOSSIP_TIMESTAMP_FILTER: case WIRE_ONION_MESSAGE: + case WIRE_PEER_STORAGE: + case WIRE_YOUR_PEER_STORAGE: return false; /* Special cases: */ diff --git a/wire/peer_wire.csv b/wire/peer_wire.csv index 398a7b4352bd..f76088cfb74a 100644 --- a/wire/peer_wire.csv +++ b/wire/peer_wire.csv @@ -262,6 +262,12 @@ msgdata,channel_reestablish,next_commitment_number,u64, msgdata,channel_reestablish,next_revocation_number,u64, msgdata,channel_reestablish,your_last_per_commitment_secret,byte,32 msgdata,channel_reestablish,my_current_per_commitment_point,point, +msgtype,peer_storage,7 +msgdata,peer_storage,len,u16, +msgdata,peer_storage,blob,byte,len +msgtype,your_peer_storage,9 +msgdata,your_peer_storage,len,u16, +msgdata,your_peer_storage,blob,byte,len msgtype,announcement_signatures,259 msgdata,announcement_signatures,channel_id,channel_id, msgdata,announcement_signatures,short_channel_id,short_channel_id, diff --git a/wire/peer_wire.h b/wire/peer_wire.h index 12c951b8ff3e..f92a9bce968e 100644 --- a/wire/peer_wire.h +++ b/wire/peer_wire.h @@ -23,7 +23,8 @@ bool is_unknown_msg_discardable(const u8 *cursor); /* Return true if it's a message for gossipd. */ bool is_msg_for_gossipd(const u8 *cursor); - +/* Returns true if the message type should be treated as a custommsg */ +bool peer_wire_is_internal(enum peer_wire type); /* Extract channel_id from various packets, return true if possible. */ bool extract_channel_id(const u8 *in_pkt, struct channel_id *channel_id);