diff --git a/contrib/msggen/msggen/schema.json b/contrib/msggen/msggen/schema.json index 1ffc7fafbf92..27c7ff0279cd 100644 --- a/contrib/msggen/msggen/schema.json +++ b/contrib/msggen/msggen/schema.json @@ -13860,7 +13860,9 @@ }, "response": { "required": [ - "filedata" + "filedata", + "can_create_penalty", + "backed_up_channel_ids" ], "additionalProperties": false, "properties": { @@ -13869,6 +13871,21 @@ "description": [ "The raw, hex-encoded, emergency.recover file" ] + }, + "can_create_penalty": { + "type": "boolean", + "description": [ + "If false, you are using legacy file version, which can not create penalty transactions, kindly delete emergency.recover and restart the node!" + ] + }, + "backed_up_channel_ids": { + "type": "array", + "items": { + "type": "hex", + "description": [ + "Channel IDs of channels backed up inside emergency.recover" + ] + } } } }, diff --git a/doc/schemas/getemergencyrecoverdata.json b/doc/schemas/getemergencyrecoverdata.json index d9b48e679906..d00b46384b60 100644 --- a/doc/schemas/getemergencyrecoverdata.json +++ b/doc/schemas/getemergencyrecoverdata.json @@ -13,7 +13,9 @@ }, "response": { "required": [ - "filedata" + "filedata", + "can_create_penalty", + "backed_up_channel_ids" ], "additionalProperties": false, "properties": { @@ -22,6 +24,21 @@ "description": [ "The raw, hex-encoded, emergency.recover file" ] + }, + "can_create_penalty": { + "type": "boolean", + "description": [ + "If false, you are using legacy file version, which can not create penalty transactions, kindly delete emergency.recover and restart the node!" + ] + }, + "backed_up_channel_ids": { + "type": "array", + "items": { + "type": "hex", + "description": [ + "Channel IDs of channels backed up inside emergency.recover" + ] + } } } }, diff --git a/plugins/chanbackup.c b/plugins/chanbackup.c index 1fb660a3fb88..0d9ab53ca825 100644 --- a/plugins/chanbackup.c +++ b/plugins/chanbackup.c @@ -323,6 +323,35 @@ static struct modern_scb_chan *convert_from_legacy(const tal_t *ctx, struct lega return modern_scb_tlv; } +/* Reads WIRE_STATIC_CHAN_BACKUP and converts from legacy_scb_chan to modern_scb_chan, if required. */ +static void read_static_chan_backup(struct command *cmd, const u8 *blob, u64 *version, u32 *timestamp, struct modern_scb_chan ***scb_tlvs, bool *is_converted) { + bool is_tlvs = false; + struct legacy_scb_chan **scb; + + if(!fromwire_static_chan_backup(cmd, + blob, + version, + timestamp, + &scb)) { + is_tlvs = true; + if (!fromwire_static_chan_backup_with_tlvs(cmd, + blob, + version, + timestamp, + scb_tlvs)) { + plugin_err(cmd->plugin, "Corrupted SCB!"); + } + } + *is_converted = !is_tlvs; + if (!is_tlvs) { + *scb_tlvs = tal_count(scb) ? tal_arr(cmd, struct modern_scb_chan *, tal_count(scb)): NULL; + for (size_t i=0; i < tal_count(scb); i++){ + (*scb_tlvs)[i] = convert_from_legacy(cmd, scb[i]); + } + } + return; +} + /* Recovers the channels by making RPC to `recoverchannel` */ static struct command_result *json_emergencyrecover(struct command *cmd, const char *buf, @@ -331,28 +360,14 @@ static struct command_result *json_emergencyrecover(struct command *cmd, struct out_req *req; u64 version; u32 timestamp; - struct legacy_scb_chan **scb; + bool is_converted; struct modern_scb_chan **scb_tlvs; if (!param(cmd, buf, params, NULL)) return command_param_failed(); u8 *res = decrypt_scb(cmd->plugin); - bool is_tlvs = false; - if (!fromwire_static_chan_backup(cmd, - res, - &version, - ×tamp, - &scb)) { - if(!fromwire_static_chan_backup_with_tlvs(cmd, - res, - &version, - ×tamp, - &scb_tlvs)) { - plugin_err(cmd->plugin, "Corrupted SCB!"); - } - is_tlvs = true; - } + read_static_chan_backup(cmd, res, &version, ×tamp, &scb_tlvs, &is_converted); if ((version & 0x5555555555555555ULL) != (VERSION & 0x5555555555555555ULL)) { plugin_err(cmd->plugin, @@ -363,26 +378,18 @@ static struct command_result *json_emergencyrecover(struct command *cmd, after_recover_rpc, forward_error, NULL); - json_array_start(req->js, "scb"); - if (is_tlvs) { - for (size_t i=0; ijs, NULL, scb_hex); - } - } else { + if (is_converted) { plugin_notify_message(cmd, LOG_DBG, "Processing legacy emergency.recover file format. " - "Please migrate to the latest file format for improved " - "compatibility and fund recovery."); - - for (size_t i=0; ijs, NULL, scb_hex); - } + "Please migrate to the latest file format for improved " + "compatibility and fund recovery."); } + json_array_start(req->js, "scb"); + for (size_t i=0; ijs, NULL, scb_hex); + } json_array_end(req->js); return send_outreq(req); @@ -807,6 +814,48 @@ static struct command_result *datastore_failed(struct command *cmd, return command_hook_success(cmd); } +/* Compares the data between the stored scb and latest recvd scb, stores the most + * recent one only. */ +static struct command_result *store_latest_scb(struct command *cmd, + const u8 *latest_stored_scb, + u8 *received_scb) +{ + size_t stored_scb_len = tal_bytelen(latest_stored_scb); + size_t recvd_scb_len = tal_bytelen(received_scb); + const u8 *recvd_scb_const = received_scb; + + if (stored_scb_len == 0) + goto store_data; + + // We only need the timestamps, so instead of fully deserializing with `fromwire_static_chan_backup`, + // we manually parse just enough fields to reach the timestamp in each SCB blob. + assert(fromwire_u16(&latest_stored_scb, &stored_scb_len) == WIRE_STATIC_CHAN_BACKUP); + assert(fromwire_u16(&recvd_scb_const, &recvd_scb_len) == WIRE_STATIC_CHAN_BACKUP); + + // Fetching version from the SCB. + fromwire_u64(&latest_stored_scb, &stored_scb_len); + fromwire_u64(&recvd_scb_const, &recvd_scb_len); + + u32 timestamp = fromwire_u32(&latest_stored_scb, &stored_scb_len); + u32 timestamp_new = fromwire_u32(&recvd_scb_const, &recvd_scb_len); + + if (timestamp_new > timestamp) + goto store_data; + + plugin_log(cmd->plugin, LOG_DBG, "Ignoring old Peer Storage"); + return command_hook_success(cmd); + +store_data: + return jsonrpc_set_datastore_binary(cmd, + "chanbackup/latestscb", + received_scb, + "create-or-replace", + datastore_success, + datastore_failed, + "Saving latestscb"); + +} + static struct command_result *handle_your_peer_storage(struct command *cmd, const char *buf, const jsmntok_t *params) @@ -895,14 +944,10 @@ static struct command_result *handle_your_peer_storage(struct command *cmd, return failed_peer_restore(cmd, &node_id, "Peer altered our data"); - - return jsonrpc_set_datastore_binary(cmd, - "chanbackup/latestscb", - decoded_bkp, - "create-or-replace", - datastore_success, - datastore_failed, - "Saving latestscb"); + return jsonrpc_get_datastore_binary(cmd, + "chanbackup/latestscb", + store_latest_scb, + decoded_bkp); } else { /* Any other message we ignore */ return command_hook_success(cmd); @@ -915,8 +960,8 @@ static struct command_result *after_latestscb(struct command *cmd, { u64 version; u32 timestamp; + bool is_converted; struct modern_scb_chan **scb_tlvs; - struct legacy_scb_chan **scb; struct json_stream *response; struct out_req *req; @@ -928,21 +973,7 @@ static struct command_result *after_latestscb(struct command *cmd, return command_finished(cmd, response); } - bool is_tlvs = false; - if (!fromwire_static_chan_backup(cmd, - res, - &version, - ×tamp, - &scb)) { - if(!fromwire_static_chan_backup_with_tlvs(cmd, - res, - &version, - ×tamp, - &scb_tlvs)) { - plugin_err(cmd->plugin, "Corrupted SCB!"); - } - is_tlvs = true; - } + read_static_chan_backup(cmd, res, &version, ×tamp, &scb_tlvs, &is_converted); if ((version & 0x5555555555555555ULL) != (VERSION & 0x5555555555555555ULL)) { plugin_err(cmd->plugin, @@ -954,28 +985,11 @@ static struct command_result *after_latestscb(struct command *cmd, &forward_error, NULL); json_array_start(req->js, "scb"); - if (is_tlvs) { - for (size_t i=0; ijs, NULL, scb_hex); - } - } else { - for (size_t i=0; iid = scb[i]->id; - tmp_scb_tlv->addr = scb[i]->addr; - tmp_scb_tlv->cid = scb[i]->cid; - tmp_scb_tlv->funding = scb[i]->funding; - tmp_scb_tlv->funding_sats = scb[i]->funding_sats; - tmp_scb_tlv->type = scb[i]->type; - tmp_scb_tlv->tlvs = tlv_scb_tlvs_new(cmd); - towire_modern_scb_chan(&scb_hex, tmp_scb_tlv); - json_add_hex_talarr(req->js, NULL, scb_hex); - } + for (size_t i=0; ijs, NULL, scb_hex); } - json_array_end(req->js); return send_outreq(req); @@ -1009,7 +1023,21 @@ static struct command_result *json_getemergencyrecoverdata(struct command *cmd, response = jsonrpc_stream_success(cmd); json_add_hex_talarr(response, "filedata", filedata); - + // Add details about the SCB. + const u8 *decrypted_filedata = decrypt_scb(cmd->plugin); + u64 version; + u32 timestamp; + struct modern_scb_chan **scb_tlvs; + bool is_converted; + read_static_chan_backup(cmd, decrypted_filedata, &version, ×tamp, &scb_tlvs, &is_converted); + + // If false, update the emergency.recover file immediately! + json_add_bool(response, "can_create_penalty", !is_converted); + json_array_start(response, "backed_up_channel_ids"); + for (int i = 0; i < tal_count(scb_tlvs); i++) { + json_add_channel_id(response, NULL, &scb_tlvs[i]->cid); + } + json_array_end(response); return command_finished(cmd, response); } diff --git a/tests/test_misc.py b/tests/test_misc.py index c0f551ceabb8..d89d35c98c55 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -2941,8 +2941,10 @@ def test_getemergencyrecoverdata(node_factory): Test getemergencyrecoverdata """ l1 = node_factory.get_node() - filedata = l1.rpc.getemergencyrecoverdata()['filedata'] - + rpc = l1.rpc.getemergencyrecoverdata() + filedata = rpc['filedata'] + assert rpc['can_create_penalty'] == True + assert len(rpc['backed_up_channel_ids']) == 0 with open(os.path.join(l1.daemon.lightning_dir, TEST_NETWORK, "emergency.recover"), "rb") as f: lines = f.read().hex() assert lines == filedata