From e201dbb111d0c67427926ac6d46ea2dd5d556ac7 Mon Sep 17 00:00:00 2001 From: Aditya Sharma Date: Tue, 22 Jul 2025 14:20:45 +0530 Subject: [PATCH 1/2] chanbackup: Store the latest recvd peer storage only Node should not store SCB that is older than what we already have. Key Changes: - store_latest_scb: Compares the data between the stored scb and latest recvd scb. Compares the data between the stored SCB and the latest received SCB. Since we only need the timestamps, there is no need to deserialize the entire blob. --- plugins/chanbackup.c | 54 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 46 insertions(+), 8 deletions(-) diff --git a/plugins/chanbackup.c b/plugins/chanbackup.c index 1fb660a3fb88..7358f7fcf780 100644 --- a/plugins/chanbackup.c +++ b/plugins/chanbackup.c @@ -807,6 +807,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 +937,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); From 854529dce9f52b1f907d8a3b8462eda96a3252c9 Mon Sep 17 00:00:00 2001 From: Aditya Sharma Date: Tue, 22 Jul 2025 14:34:30 +0530 Subject: [PATCH 2/2] chanbackup: make getemergencyrecoverdata rpc more verbose Adding more information to geremergencyrecoverdata, to let users know if they are using legacy file format and the list of all the backed up channel ids. Key Changes: - Added: 1. can_create_penalty: To let user know if they need to update the file. 2. backed_up_channel_ids: List of all the backed up channels --- contrib/msggen/msggen/schema.json | 19 +++- doc/schemas/getemergencyrecoverdata.json | 19 +++- plugins/chanbackup.c | 132 +++++++++++------------ tests/test_misc.py | 6 +- 4 files changed, 101 insertions(+), 75 deletions(-) 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 7358f7fcf780..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); @@ -953,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; @@ -966,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, @@ -992,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); @@ -1047,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