gossmap: add callback for gossipd to see dying messages.
What changed, and why it matters
This commit adds a new internal callback mechanism so the gossipd component can be notified when the network graph sees a 'dying channel' message. It is a code cleanup/refactoring change: previously gossipd had to scan the entire gossip store at startup to find these messages, now it can receive them through a callback. There is no indication this fixes a security bug or introduces a vulnerability.
No security action required. Treat as normal code maintenance. If deploying, verify tests pass and that future consumers of the callback validate the scid/blockheight values they receive.
Security signals we found
No security-relevant keywords in commit title or message
No input validation changes beyond existing fromwire decoding
No privilege, authentication, or cryptographic changes
New callback is optional and currently unused by gossipd (NULL passed)
Change is architectural/refactoring in nature
Evidence from the diff
The change extends the gossmap API with an optional dying-channel callback passed through gossmap_load_ and map_catchup. When a WIRE_GOSSIP_STORE_CHAN_DYING record is encountered, report_dying_cb decodes it and invokes the callback. The current gossipd consumer passes NULL for the callback, so the new path is unused in production. Existing call sites and tests are updated for the new signatures. No bounds-checking, memory-safety, or cryptographic logic changes are present.
Changed components
common/gossmap.ccommon/gossmap.hgossipd/gossmap_manage.ccommon/test/run-gossmap_canned.ccommon/test/run-gossmap_local.cconnectd/test/run-crc32_of_update.cgossipd/test/run-next_block_range.cInspect captured patch +98 / −15
diff --git a/common/gossmap.c b/common/gossmap.c
index d0bb948f..45ea204b 100644
--- a/common/gossmap.c
+++ b/common/gossmap.c
@@ -641,8 +641,47 @@ static void node_announcement(struct gossmap *map, u64 nann_off)
n->nann_off = nann_off;
}
+static bool report_dying_cb(struct gossmap *map,
+ u64 dying_off,
+ u16 msglen,
+ void (*dyingcb)(struct short_channel_id scid,
+ u32 blockheight,
+ u64 offset,
+ void *cb_arg),
+ void *cb_arg)
+{
+ struct short_channel_id scid;
+ u32 blockheight;
+ u8 *msg;
+
+ msg = tal_arr(NULL, u8, msglen);
+ map_copy(map, dying_off, msg, msglen);
+
+ if (!fromwire_gossip_store_chan_dying(msg, &scid, &blockheight)) {
+ map->logcb(map->cbarg,
+ LOG_BROKEN,
+ "Invalid chan_dying message @%"PRIu64
+ "/%"PRIu64": %s",
+ dying_off, map->map_size, msglen,
+ tal_hex(tmpctx, msg));
+ tal_free(msg);
+ return false;
+ }
+ tal_free(msg);
+
+ dyingcb(scid, blockheight, dying_off, cb_arg);
+ return true;
+}
+
/* Mutual recursion */
-static bool map_catchup(struct gossmap *map, bool must_be_clean, bool *changed);
+static bool map_catchup(struct gossmap *map,
+ void (*dyingcb)(struct short_channel_id scid,
+ u32 blockheight,
+ u64 offset,
+ void *cb_arg),
+ void *cb_arg,
+ bool must_be_clean,
+ bool *changed);
static void init_map_structs(struct gossmap *map)
{
@@ -724,7 +763,7 @@ static bool reopen_store(struct gossmap *map, u64 ended_off)
map->generation++;
/* Now do reload. */
- map_catchup(map, false, &changed);
+ map_catchup(map, NULL, NULL, false, &changed);
return changed;
}
@@ -744,7 +783,14 @@ static bool csum_matches(const struct gossmap *map,
}
/* Returns false only if must_be_clean is true. */
-static bool map_catchup(struct gossmap *map, bool must_be_clean, bool *changed)
+static bool map_catchup(struct gossmap *map,
+ void (*dyingcb)(struct short_channel_id scid,
+ u32 blockheight,
+ u64 offset,
+ void *cb_arg),
+ void *cb_arg,
+ bool must_be_clean,
+ bool *changed)
{
size_t reclen, num_bad_cupdates = 0;
@@ -830,6 +876,8 @@ static bool map_catchup(struct gossmap *map, bool must_be_clean, bool *changed)
/* We absorbed this in add_channel; ignore */
continue;
} else if (type == WIRE_GOSSIP_STORE_CHAN_DYING) {
+ if (dyingcb && !report_dying_cb(map, off, msglen, dyingcb, cb_arg))
+ return false;
/* We don't really care until it's deleted */
continue;
} else if (type == WIRE_GOSSIP_STORE_UUID) {
@@ -856,7 +904,13 @@ static bool map_catchup(struct gossmap *map, bool must_be_clean, bool *changed)
return true;
}
-static bool load_gossip_store(struct gossmap *map, bool must_be_clean)
+static bool load_gossip_store(struct gossmap *map,
+ void (*dyingcb)(struct short_channel_id scid,
+ u32 blockheight,
+ u64 offset,
+ void *cb_arg),
+ void *cb_arg,
+ bool must_be_clean)
{
bool updated;
@@ -880,7 +934,7 @@ static bool load_gossip_store(struct gossmap *map, bool must_be_clean)
init_map_structs(map);
map->map_end = 1;
- return map_catchup(map, must_be_clean, &updated);
+ return map_catchup(map, dyingcb, cb_arg, must_be_clean, &updated);
}
static void destroy_map(struct gossmap *map)
@@ -1292,7 +1346,7 @@ bool gossmap_refresh(struct gossmap *map)
}
}
- map_catchup(map, false, &changed);
+ map_catchup(map, NULL, NULL, false, &changed);
return changed;
}
@@ -1319,7 +1373,11 @@ struct gossmap *gossmap_load_(const tal_t *ctx,
enum log_level level,
const char *fmt,
...),
- void *cbarg)
+ void (*dyingcb)(struct short_channel_id scid,
+ u32 blockheight,
+ u64 offset,
+ void *cb_arg),
+ void *cb_arg)
{
map = tal(ctx, struct gossmap);
map->generation = 0;
@@ -1331,15 +1389,15 @@ struct gossmap *gossmap_load_(const tal_t *ctx,
map->logcb = logcb;
else
map->logcb = log_stderr;
- map->cbarg = cbarg;
+ map->cbarg = cb_arg;
tal_add_destructor(map, destroy_map);
- if (!load_gossip_store(map, expected_len != 0))
+ if (!load_gossip_store(map, dyingcb, cb_arg, expected_len != 0))
return tal_free(map);
if (expected_len != 0
&& (map->map_size != map->map_end
|| map->map_size != expected_len)) {
- logcb(cbarg, LOG_BROKEN,
+ logcb(cb_arg, LOG_BROKEN,
"gossip_store only processed %"PRIu64
" bytes of %"PRIu64" (expected %"PRIu64")",
map->map_end, map->map_size, expected_len);
diff --git a/common/gossmap.h b/common/gossmap.h
index 1b60a1fd..0c1a0425 100644
--- a/common/gossmap.h
+++ b/common/gossmap.h
@@ -46,17 +46,21 @@ struct gossmap_chan {
enum log_level, \
const char *fmt, \
...), \
- (cbarg))
+ NULL, (cbarg))
/* If we're the author of the gossmap, it should have no redundant records, corruption, etc.
* So this fails if that's not the case. */
-#define gossmap_load_initial(ctx, filename, expected_len, logcb, cbarg) \
+#define gossmap_load_initial(ctx, filename, expected_len, logcb, dyingcb, cb_arg) \
gossmap_load_((ctx), (filename), (expected_len), \
- typesafe_cb_postargs(void, void *, (logcb), (cbarg), \
+ typesafe_cb_postargs(void, void *, (logcb), (cb_arg), \
enum log_level, \
const char *fmt, \
...), \
- (cbarg))
+ typesafe_cb_preargs(void, void *, (dyingcb), (cb_arg), \
+ struct short_channel_id, \
+ u32, \
+ u64), \
+ (cb_arg))
struct gossmap *gossmap_load_(const tal_t *ctx,
const char *filename,
@@ -65,6 +69,10 @@ struct gossmap *gossmap_load_(const tal_t *ctx,
enum log_level level,
const char *fmt,
...),
+ void (*dyingcb)(struct short_channel_id scid,
+ u32 blockheight,
+ u64 offset,
+ void *cb_arg),
void *cb_arg);
/* Disable mmap. Noop if already disabled. */
@@ -305,4 +313,5 @@ u64 gossmap_lengths(const struct gossmap *map, u64 *total);
/* Debugging: connectd wants to enumerate fds */
int gossmap_fd(const struct gossmap *map);
+
#endif /* LIGHTNING_COMMON_GOSSMAP_H */
diff --git a/common/test/run-gossmap_canned.c b/common/test/run-gossmap_canned.c
index fd5af5e0..8ae7210d 100644
--- a/common/test/run-gossmap_canned.c
+++ b/common/test/run-gossmap_canned.c
@@ -19,6 +19,9 @@ bigsize_t fromwire_bigsize(const u8 **cursor UNNEEDED, size_t *max UNNEEDED)
bool fromwire_channel_id(const u8 **cursor UNNEEDED, size_t *max UNNEEDED,
struct channel_id *channel_id UNNEEDED)
{ fprintf(stderr, "fromwire_channel_id called!\n"); abort(); }
+/* Generated stub for fromwire_gossip_store_chan_dying */
+bool fromwire_gossip_store_chan_dying(const void *p UNNEEDED, struct short_channel_id *scid UNNEEDED, u32 *blockheight UNNEEDED)
+{ fprintf(stderr, "fromwire_gossip_store_chan_dying called!\n"); abort(); }
/* Generated stub for sciddir_or_pubkey_from_node_id */
bool sciddir_or_pubkey_from_node_id(struct sciddir_or_pubkey *sciddpk UNNEEDED,
const struct node_id *node_id UNNEEDED)
diff --git a/common/test/run-gossmap_local.c b/common/test/run-gossmap_local.c
index b425710a..0575bcae 100644
--- a/common/test/run-gossmap_local.c
+++ b/common/test/run-gossmap_local.c
@@ -19,6 +19,9 @@ bigsize_t fromwire_bigsize(const u8 **cursor UNNEEDED, size_t *max UNNEEDED)
bool fromwire_channel_id(const u8 **cursor UNNEEDED, size_t *max UNNEEDED,
struct channel_id *channel_id UNNEEDED)
{ fprintf(stderr, "fromwire_channel_id called!\n"); abort(); }
+/* Generated stub for fromwire_gossip_store_chan_dying */
+bool fromwire_gossip_store_chan_dying(const void *p UNNEEDED, struct short_channel_id *scid UNNEEDED, u32 *blockheight UNNEEDED)
+{ fprintf(stderr, "fromwire_gossip_store_chan_dying called!\n"); abort(); }
/* Generated stub for sciddir_or_pubkey_from_node_id */
bool sciddir_or_pubkey_from_node_id(struct sciddir_or_pubkey *sciddpk UNNEEDED,
const struct node_id *node_id UNNEEDED)
diff --git a/connectd/test/run-crc32_of_update.c b/connectd/test/run-crc32_of_update.c
index 85521395..62c3c5fe 100644
--- a/connectd/test/run-crc32_of_update.c
+++ b/connectd/test/run-crc32_of_update.c
@@ -10,12 +10,16 @@ int unused_main(int argc, char *argv[]);
#include <common/memleak.h>
#include <common/onionreply.h>
#include <common/setup.h>
+#include <common/gossip_store_wiregen.h>
#include <stdio.h>
/* AUTOGENERATED MOCKS START */
/* Generated stub for fromwire_connectd_dev_set_max_scids_encode_size */
bool fromwire_connectd_dev_set_max_scids_encode_size(const void *p UNNEEDED, u32 *max UNNEEDED)
{ fprintf(stderr, "fromwire_connectd_dev_set_max_scids_encode_size called!\n"); abort(); }
+/* Generated stub for fromwire_gossip_store_chan_dying */
+bool fromwire_gossip_store_chan_dying(const void *p UNNEEDED, struct short_channel_id *scid UNNEEDED, u32 *blockheight UNNEEDED)
+{ fprintf(stderr, "fromwire_gossip_store_chan_dying called!\n"); abort(); }
/* Generated stub for get_gossmap */
struct gossmap *get_gossmap(struct daemon *daemon UNNEEDED)
{ fprintf(stderr, "get_gossmap called!\n"); abort(); }
diff --git a/gossipd/gossmap_manage.c b/gossipd/gossmap_manage.c
index 4ae675d8..cc4c6264 100644
--- a/gossipd/gossmap_manage.c
+++ b/gossipd/gossmap_manage.c
@@ -464,7 +464,9 @@ static bool setup_gossmap(struct gossmap_manage *gm,
/* This actually loads it into memory, with strict checks. */
gm->raw_gossmap = gossmap_load_initial(gm, GOSSIP_STORE_FILENAME,
expected_len,
- gossmap_logcb, daemon);
+ gossmap_logcb,
+ NULL,
+ daemon);
if (!gm->raw_gossmap) {
gm->gs = tal_free(gm->gs);
return false;
diff --git a/gossipd/test/run-next_block_range.c b/gossipd/test/run-next_block_range.c
index 60b9530a..f4dae400 100644
--- a/gossipd/test/run-next_block_range.c
+++ b/gossipd/test/run-next_block_range.c
@@ -9,6 +9,7 @@
#include <common/sciddir_or_pubkey.h>
#include <common/setup.h>
#include <common/wireaddr.h>
+#include <common/gossip_store_wiregen.h>
#include <stdio.h>
/* AUTOGENERATED MOCKS START */
@@ -19,6 +20,9 @@ struct peer *find_peer(struct daemon *daemon UNNEEDED, const struct node_id *id
struct peer *first_random_peer(struct daemon *daemon UNNEEDED,
struct peer_node_id_map_iter *it UNNEEDED)
{ fprintf(stderr, "first_random_peer called!\n"); abort(); }
+/* Generated stub for fromwire_gossip_store_chan_dying */
+bool fromwire_gossip_store_chan_dying(const void *p UNNEEDED, struct short_channel_id *scid UNNEEDED, u32 *blockheight UNNEEDED)
+{ fprintf(stderr, "fromwire_gossip_store_chan_dying called!\n"); abort(); }
/* Generated stub for gossmap_manage_get_gossmap */
struct gossmap *gossmap_manage_get_gossmap(struct gossmap_manage *gm UNNEEDED)
{ fprintf(stderr, "gossmap_manage_get_gossmap called!\n"); abort(); }
Why this scored 18/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.