Skip to content

Commit 52ad6cd

Browse files
Branimir Rakiccursoragent
authored andcommitted
fix(network): reject empty publisherIdBytes in dkgGossipMsgIdRaw (Codex PR #501 round 6)
The libp2p adapter (`dkgGossipMsgId`) rejects unsigned messages because they have no publisher identity, but the raw primitive (`dkgGossipMsgIdRaw`) used to silently accept the equivalent `publisherIdBytes.length === 0` case — reopening the exact false-dedup hazard the adapter was built to close. Any future backend or consumer that forgot to plumb publisher-identity bytes through the raw primitive would have shipped a quiet correctness bug at the cross-backend boundary the primitive was meant to lock down. Add `DkgGossipMissingPublisherError` (exported from network/index + core/index) and throw it from `dkgGossipMsgIdRaw` on empty input. Updated the "returns 32-byte SHA256" test to use a non-empty publisher (the empty-publisher case is now an error, not a vector). Added a regression test pinning the throw. 17/17 gossip-msg-id tests pass. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 038844a commit 52ad6cd

4 files changed

Lines changed: 51 additions & 2 deletions

File tree

packages/core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export {
2525
dkgGossipMsgIdRaw,
2626
type DkgGossipMsgIdInput,
2727
DkgGossipUnsignedMessageError,
28+
DkgGossipMissingPublisherError,
2829
} from './network/index.js';
2930
export {
3031
ProtocolRouter,

packages/core/src/network/gossip-msg-id.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,8 @@ export class DkgGossipUnsignedMessageError extends Error {
116116
* - `data` — raw payload bytes.
117117
* - `publisherIdBytes` — canonical bytes identifying the publisher.
118118
* For libp2p, this is `peerId.toMultihash().bytes`. For other
119-
* backends, the equivalent canonical identity bytes.
119+
* backends, the equivalent canonical identity bytes. MUST be
120+
* non-empty — see `DkgGossipMissingPublisherError` below.
120121
* - `sequenceNumber` — per-publisher monotonic sequence (gossipsub
121122
* seqno, iroh sequence, etc.).
122123
*
@@ -129,11 +130,40 @@ export interface DkgGossipMsgIdInput {
129130
sequenceNumber: bigint;
130131
}
131132

133+
/**
134+
* Thrown when `publisherIdBytes` is empty. The DKG msgId scheme is
135+
* built on the invariant that two payload-identical publishes from
136+
* different publishers must hash to different ids; an empty publisher
137+
* identity collapses that distinction.
138+
*
139+
* @experimental
140+
*/
141+
export class DkgGossipMissingPublisherError extends Error {
142+
constructor() {
143+
super(
144+
'dkgGossipMsgIdRaw: publisherIdBytes must be non-empty. The DKG ' +
145+
'msgId scheme requires publisher identity to avoid false dedup of ' +
146+
'payload-identical publishes from different publishers. Future ' +
147+
'backends MUST plumb their canonical publisher-identity bytes ' +
148+
'into this primitive.',
149+
);
150+
this.name = 'DkgGossipMissingPublisherError';
151+
}
152+
}
153+
132154
/**
133155
* Backend-agnostic msgId primitive. Every gossip backend adapter
134156
* normalises into `DkgGossipMsgIdInput` and the framing + hash
135157
* lives here once.
136158
*
159+
* Throws `DkgGossipMissingPublisherError` if `publisherIdBytes` is
160+
* empty. Codex review feedback PR #501 round 6: the libp2p adapter
161+
* already rejects unsigned messages (where publisher identity is
162+
* absent), but the raw primitive used to accept the equivalent
163+
* `publisherIdBytes.length === 0` case, reopening the false-dedup
164+
* hazard at the cross-backend boundary the primitive was meant to
165+
* lock down.
166+
*
137167
* @experimental
138168
*/
139169
export function dkgGossipMsgIdRaw(input: DkgGossipMsgIdInput): Uint8Array {
@@ -142,6 +172,10 @@ export function dkgGossipMsgIdRaw(input: DkgGossipMsgIdInput): Uint8Array {
142172
const fromBytes = input.publisherIdBytes;
143173
const seqno = input.sequenceNumber;
144174

175+
if (fromBytes.length === 0) {
176+
throw new DkgGossipMissingPublisherError();
177+
}
178+
145179
const total =
146180
4 + topicBytes.length +
147181
4 + data.length +

packages/core/src/network/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,5 @@ export {
2424
dkgGossipMsgId,
2525
dkgGossipMsgIdRaw,
2626
DkgGossipUnsignedMessageError,
27+
DkgGossipMissingPublisherError,
2728
} from './gossip-msg-id.js';

packages/core/test/gossip-msg-id.test.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,8 +283,21 @@ describe('dkgGossipMsgIdRaw (RFC 07 §5.4 — backend-agnostic primitive)', () =
283283
it('returns a 32-byte SHA256 digest', () => {
284284
const id = dkgGossipMsgIdRaw({
285285
topic: 't', data: new Uint8Array(),
286-
publisherIdBytes: new Uint8Array(), sequenceNumber: 0n,
286+
publisherIdBytes: new Uint8Array([1]), sequenceNumber: 0n,
287287
});
288288
expect(id.length).toBe(32);
289289
});
290+
291+
// Codex review feedback PR #501 round 6 (branarakic, gossip-msg-id.ts:142):
292+
// the libp2p adapter rejects unsigned messages because they have no
293+
// publisher identity, but the raw primitive used to silently accept
294+
// an empty publisherIdBytes — reopening the false-dedup hazard the
295+
// adapter fixed. Reject empty publishers at the raw API boundary too.
296+
it('Codex PR #501 round 6: throws when publisherIdBytes is empty', () => {
297+
expect(() => dkgGossipMsgIdRaw({
298+
topic: 't', data: new Uint8Array([1, 2, 3]),
299+
publisherIdBytes: new Uint8Array(),
300+
sequenceNumber: 0n,
301+
})).toThrow(/publisherIdBytes must be non-empty/);
302+
});
290303
});

0 commit comments

Comments
 (0)