chanbackup: Store the latest recvd peer storage only
What changed, and why it matters
This change fixes a bug in Core Lightning's channel backup plugin. Previously, when recovering a backup from a peer, the node would save whatever backup the peer sent, even if it was older than the backup already stored. Now it checks a timestamp inside the backup and only keeps the newest one. Using an old backup during recovery could, in edge cases, lead to stale or incorrect channel state being restored.
Treat as a low-to-moderate security fix. Users relying on experimental peer storage should upgrade to ensure recovery uses the most recent backup. No immediate emergency response is indicated, but the fix should be included in the next release.
Security signals we found
Prevents storage of stale/outdated peer channel backups
Adds timestamp comparison before overwriting critical recovery data
Protects against potential downgrade/rollback of SCB state during peer storage recovery
Changelog labels the fix under Protocol behavior
Evidence from the diff
The patch modifies plugins/chanbackup.c so that handle_your_peer_storage() no longer blindly overwrites the ‘latestscb’ datastore entry with every received peer backup. A new store_latest_scb() helper parses the incoming static channel backup (SCB), extracts its timestamp, and compares it against cb->latest_timestamp. If the received backup is older, it is ignored; otherwise it is stored and latest_timestamp is updated. On plugin init, the existing latestscb datastore entry is read and its timestamp is cached into cb->latest_timestamp to handle upgrades from prior versions. The change is defensive and prevents rollback to older SCB versions.
Changed components
plugins/chanbackup.cpeer storage recovery pathstatic channel backup (SCB) datastoreInspect captured patch +53 / −11
diff --git a/plugins/chanbackup.c b/plugins/chanbackup.c
index 00be2888..35e288af 100644
--- a/plugins/chanbackup.c
+++ b/plugins/chanbackup.c
@@ -67,6 +67,9 @@ struct chanbackup {
/* Global secret object to keep the derived encryption key for the SCB */
struct secret secret;
+ /* We store the timestamp of the latest peer storage that we have. */
+ u32 latest_timestamp;
+
/* Cache of backups for each peer we know about */
struct backup_map *backups;
@@ -819,6 +822,41 @@ 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,
+ struct chanbackup *cb,
+ const u8 *received_scb)
+{
+ size_t recvd_scb_len = tal_bytelen(received_scb);
+ u64 version;
+ struct modern_scb_chan **scb_tlvs;
+ u32 timestamp_new;
+ bool is_converted;
+
+ if (!read_static_chan_backup(cmd, received_scb, &version, ×tamp_new, &scb_tlvs, &is_converted)) {
+ plugin_log(cmd->plugin, LOG_BROKEN, "Ignoring invalid peer storage: %s",
+ tal_hex(tmpctx, received_scb));
+ return command_hook_success(cmd);
+ }
+
+ if (timestamp_new < cb->latest_timestamp) {
+ plugin_log(cmd->plugin, LOG_DBG, "Ignoring old Peer Storage");
+ return command_hook_success(cmd);
+ }
+
+ cb->latest_timestamp = timestamp_new;
+
+ return jsonrpc_set_datastore_binary(cmd,
+ mkdatastorekey(tmpctx, "chanbackup", "latestscb"),
+ received_scb, recvd_scb_len,
+ "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)
@@ -826,7 +864,7 @@ static struct command_result *handle_your_peer_storage(struct command *cmd,
struct node_id node_id;
u8 *payload, *payload_deserialise;
const char *err;
- const struct chanbackup *cb = chanbackup(cmd->plugin);
+ struct chanbackup *cb = chanbackup(cmd->plugin);
err = json_scan(cmd, buf, params,
"{payload:%,peer_id:%}",
@@ -906,16 +944,7 @@ static struct command_result *handle_your_peer_storage(struct command *cmd,
NULL, 0) != 0)
return failed_peer_restore(cmd, &node_id,
"Peer altered our data");
-
-
- return jsonrpc_set_datastore_binary(cmd,
- mkdatastorekey(tmpctx, "chanbackup", "latestscb"),
- decoded_bkp,
- tal_bytelen(decoded_bkp),
- "create-or-replace",
- datastore_success,
- datastore_failed,
- "Saving latestscb");
+ return store_latest_scb(cmd, cb, decoded_bkp);
} else {
/* Any other message we ignore */
return command_hook_success(cmd);
@@ -1092,6 +1121,7 @@ static const char *init(struct command *init_cmd,
const char *info = "scb secret";
u8 *info_hex = tal_dup_arr(tmpctx, u8, (u8*)info, strlen(info), 0);
u8 *features;
+ u8 *latestscb;
/* Figure out if they specified --experimental-peer-storage */
rpc_scan(init_cmd, "getinfo",
@@ -1115,6 +1145,18 @@ static const char *init(struct command *init_cmd,
tal_bytelen(info_hex)))),
"{secret:%}", JSON_SCAN(json_to_secret, &cb->secret));
+ /* Caching timestamp of latestscb that we have stored. This
+ * would be useful for upgrading `chanbackup/latestscb`. */
+ cb->latest_timestamp = 0;
+ if (rpc_scan_datastore_hex(tmpctx, init_cmd,
+ mkdatastorekey(tmpctx, "chanbackup", "latestscb"),
+ JSON_SCAN_TAL(tmpctx, json_tok_bin_from_hex, &latestscb)) == NULL) {
+ u64 version;
+ struct modern_scb_chan **scb_tlvs;
+ bool is_converted;
+ read_static_chan_backup(init_cmd, latestscb, &version, &cb->latest_timestamp, &scb_tlvs, &is_converted);
+ }
+
setup_backup_map(init_cmd, cb);
plugin_set_data(init_cmd->plugin, cb);
plugin_log(init_cmd->plugin, LOG_DBG, "Chanbackup Initialised!");
Why this scored 61/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.