gossipd: write uuid record on startup.
What changed, and why it matters
This commit adds a new unique identifier (UUID) record to the gossip store file when the lightning node starts up or compacts its gossip database. The UUID is random and is intended to help identify different versions of the gossip store. The change is internal bookkeeping and does not fix or introduce any known security issue.
No security action required. This is a normal feature/infrastructure commit.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch introduces WIRE_GOSSIP_STORE_UUID, a 32-byte random record written at the start of the gossip store. It updates the gossip store compaction logic to generate a UUID for old store versions and to preserve existing UUIDs. The gossmap parser is updated to ignore UUID records. Test expectations for store byte sizes are adjusted accordingly. There is no evidence of a vulnerability being fixed or introduced.
Changed components
gossipd/gossip_store.ccommon/gossmap.ctests/test_gossip.pyInspect captured patch +43 / −7
diff --git a/common/gossmap.c b/common/gossmap.c
index 09d9272c..69a3dbef 100644
--- a/common/gossmap.c
+++ b/common/gossmap.c
@@ -832,6 +832,9 @@ static bool map_catchup(struct gossmap *map, bool must_be_clean, bool *changed)
} else if (type == WIRE_GOSSIP_STORE_CHAN_DYING) {
/* We don't really care until it's deleted */
continue;
+ } else if (type == WIRE_GOSSIP_STORE_UUID) {
+ /* We handled this reopen, otherwise we don't care. */
+ continue;
} else {
map->logcb(map->cbarg, LOG_BROKEN,
"Unknown record %u@%u (size %zu) in gossmap: ignoring",
diff --git a/gossipd/gossip_store.c b/gossipd/gossip_store.c
index 32c2ae7a..91c6c28f 100644
--- a/gossipd/gossip_store.c
+++ b/gossipd/gossip_store.c
@@ -179,6 +179,18 @@ static bool upgrade_field(u8 oldversion,
return true;
}
+static u8 *new_uuid_record(const tal_t *ctx, int fd, u64 *off)
+{
+ u8 *uuid = tal_arr(ctx, u8, 32);
+
+ for (size_t i = 0; i < tal_bytelen(uuid); i++)
+ uuid[i] = pseudorand(256);
+ append_msg(fd, towire_gossip_store_uuid(tmpctx, uuid), 0, off, NULL);
+ /* append_msg does not change file offset, so do that now. */
+ lseek(fd, 0, SEEK_END);
+ return uuid;
+}
+
/* Read gossip store entries, copy non-deleted ones. Check basic
* validity, but this code is written as simply and robustly as
* possible!
@@ -198,6 +210,7 @@ static int gossip_store_compact(struct daemon *daemon,
struct stat st;
struct timemono start = time_mono();
const char *bad;
+ u8 *uuid = NULL;
*populated = false;
old_len = 1;
@@ -241,6 +254,10 @@ static int gossip_store_compact(struct daemon *daemon,
cur_off = old_len = sizeof(oldversion);
+ /* Make up uuid for old version */
+ if (oldversion < 16)
+ uuid = new_uuid_record(tmpctx, new_fd, total_len);
+
/* Read everything, write non-deleted ones to new_fd. If something goes wrong,
* we end up with truncated store. */
while (read_all(old_fd, &hdr, sizeof(hdr))) {
@@ -327,6 +344,13 @@ static int gossip_store_compact(struct daemon *daemon,
case WIRE_NODE_ANNOUNCEMENT:
nannounces++;
break;
+ case WIRE_GOSSIP_STORE_UUID:
+ uuid = tal_arr(tmpctx, u8, 32);
+ if (!fromwire_gossip_store_uuid(msg, uuid)) {
+ bad = "Corrupt uuid";
+ goto badmsg;
+ }
+ break;
}
if (!write_all(new_fd, &hdr, sizeof(hdr))
@@ -348,6 +372,16 @@ static int gossip_store_compact(struct daemon *daemon,
}
rename_new:
+ /* If we didn't copy a uuid, do so now. */
+ if (!uuid) {
+ if (lseek(new_fd, 0, SEEK_END) != 1) {
+ bad = tal_fmt(tmpctx,
+ "missing uuid in version %u", oldversion);
+ goto badmsg;
+ }
+ uuid = new_uuid_record(tmpctx, new_fd, total_len);
+ }
+
if (rename(GOSSIP_STORE_TEMP_FILENAME, GOSSIP_STORE_FILENAME) != 0) {
status_failed(STATUS_FAIL_INTERNAL_ERROR,
"gossip_store_compact: rename failed: %s",
@@ -357,7 +391,6 @@ rename_new:
/* Create end marker now new file exists. */
if (old_fd != -1) {
/* FIXME: real uuid! */
- u8 uuid[32] = {0};
append_msg(old_fd, towire_gossip_store_ended(tmpctx, *total_len, uuid),
0, &old_len, NULL);
close(old_fd);
diff --git a/tests/test_gossip.py b/tests/test_gossip.py
index b29f3b97..e6a2bb07 100644
--- a/tests/test_gossip.py
+++ b/tests/test_gossip.py
@@ -1207,7 +1207,7 @@ def test_gossip_store_load(node_factory):
l1.start()
# May preceed the Started msg waited for in 'start'.
- wait_for(lambda: l1.daemon.is_in_log('Read 1/1/1/0 cannounce/cupdate/nannounce/delete from store in 832 bytes, now 778 bytes'))
+ wait_for(lambda: l1.daemon.is_in_log('Read 1/1/1/0 cannounce/cupdate/nannounce/delete from store in 832 bytes, now 824 bytes'))
assert not l1.daemon.is_in_log('gossip_store.*truncating')
@@ -1275,13 +1275,13 @@ def test_gossip_store_load_announce_before_update(node_factory):
l1.start()
# May preceed the Started msg waited for in 'start'.
- wait_for(lambda: l1.daemon.is_in_log('Read 1/1/1/1 cannounce/cupdate/nannounce/delete from store in 950 bytes, now 778 bytes'))
+ wait_for(lambda: l1.daemon.is_in_log('Read 1/1/1/1 cannounce/cupdate/nannounce/delete from store in 982 bytes, now 824 bytes'))
assert not l1.daemon.is_in_log('gossip_store.*truncating')
def test_gossip_store_load_amount_truncated(node_factory):
"""Make sure we can read canned gossip store with truncated amount"""
- l1 = node_factory.get_node(start=False, broken_log=r'gossip_store only processed 1 bytes of 445 \(expected 445\)|Moving to gossip_store.corrupt|plugin-cln-renepay:.*unable to fetch channel capacity')
+ l1 = node_factory.get_node(start=False, broken_log=r'gossip_store only processed 47 bytes of 491 \(expected 491\)|Moving to gossip_store.corrupt|plugin-cln-renepay:.*unable to fetch channel capacity')
with open(os.path.join(l1.daemon.lightning_dir, TEST_NETWORK, 'gossip_store'), 'wb') as f:
f.write(bytearray.fromhex("0c" # GOSSIP_STORE_VERSION
"000001b0" # len
@@ -1292,9 +1292,9 @@ def test_gossip_store_load_amount_truncated(node_factory):
l1.start()
# May preceed the Started msg waited for in 'start'.
- wait_for(lambda: l1.daemon.is_in_log(r'\*\*BROKEN\*\* gossipd: gossip_store only processed 1 bytes of 445 \(expected 445\)'))
+ wait_for(lambda: l1.daemon.is_in_log(r'\*\*BROKEN\*\* gossipd: gossip_store only processed 47 bytes of 491 \(expected 491\)'))
wait_for(lambda: l1.daemon.is_in_log(r'\*\*BROKEN\*\* gossipd: gossip_store: Moving to gossip_store.corrupt'))
- wait_for(lambda: l1.daemon.is_in_log(r'gossip_store: Read 0/0/0/0 cannounce/cupdate/nannounce/delete from store in 1 bytes, now 1 bytes \(populated=false\)'))
+ wait_for(lambda: l1.daemon.is_in_log(r'gossip_store: Read 0/0/0/0 cannounce/cupdate/nannounce/delete from store in 1 bytes, now 47 bytes \(populated=false\)'))
assert os.path.exists(os.path.join(l1.daemon.lightning_dir, TEST_NETWORK, 'gossip_store.corrupt'))
@@ -1658,7 +1658,7 @@ def test_gossip_store_load_no_channel_update(node_factory):
l1.start()
# May preceed the Started msg waited for in 'start'.
- wait_for(lambda: l1.daemon.is_in_log('Read 1/0/1/0 cannounce/cupdate/nannounce/delete from store in 650 bytes, now 628 bytes'))
+ wait_for(lambda: l1.daemon.is_in_log('Read 1/0/1/0 cannounce/cupdate/nannounce/delete from store in 682 bytes, now 674 bytes'))
assert not os.path.exists(os.path.join(l1.daemon.lightning_dir, TEST_NETWORK, 'gossip_store.corrupt'))
Why this scored 12/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.