Skip to content

Commit aa5ba42

Browse files
authored
fix(floodsub): use monotonic sequence numbers (#3672)
* fix(floodsub): use monotonic sequence numbers Seed the sequence number from the wall clock in nanoseconds and increment it, as the pubsub spec requires, instead of picking random values. * test(floodsub): compare bigint sequence numbers directly
1 parent 9f08ba1 commit aa5ba42

6 files changed

Lines changed: 30 additions & 22 deletions

File tree

‎packages/floodsub/src/floodsub.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { protocol, StrictNoSign, TopicValidatorResult, StrictSign } from './inde
1010
import { RPC } from './message/rpc.ts'
1111
import { PeerStreams } from './peer-streams.ts'
1212
import { signMessage, verifySignature } from './sign.ts'
13-
import { toMessage, noSignMsgId, msgId, toRpcMessage, randomSeqno } from './utils.ts'
13+
import { toMessage, noSignMsgId, msgId, toRpcMessage, nextSeqno } from './utils.ts'
1414
import type { RPCDecodeLimits } from './decodeRpc.ts'
1515
import type { FloodSubComponents, FloodSubEvents, FloodSubInit, FloodSub as FloodSubInterface, Message, PublishResult, SubscriptionChangeData, TopicValidatorFn } from './index.ts'
1616
import type { Logger, Connection, PeerId, Stream } from '@libp2p/interface'
@@ -601,7 +601,7 @@ export class FloodSub extends TypedEventEmitter<FloodSubEvents> implements Flood
601601
from: this.components.peerId,
602602
topic,
603603
data: data ?? new Uint8Array(0),
604-
sequenceNumber: randomSeqno()
604+
sequenceNumber: nextSeqno()
605605
}
606606

607607
this.log('publish topic: %s from: %p data: %m', topic, message.from, message.data)

‎packages/floodsub/src/utils.ts‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,15 @@ import type { PubSubRPCMessage } from './floodsub.ts'
99
import type { Message } from './index.ts'
1010
import type { PublicKey } from '@libp2p/interface'
1111

12+
// monotonic sequence number seeded from the wall clock in nanoseconds, the
13+
// pubsub spec requires a linearly increasing 64-bit big-endian uint
14+
let seqnoCounter = BigInt(Date.now()) * 1_000_000n
15+
1216
/**
13-
* Generate a random sequence number
17+
* Generate the next sequence number
1418
*/
15-
export function randomSeqno (): bigint {
16-
return BigInt(`0x${uint8ArrayToString(crypto.getRandomValues(new Uint8Array(8)), 'base16')}`)
19+
export function nextSeqno (): bigint {
20+
return ++seqnoCounter
1721
}
1822

1923
/**

‎packages/floodsub/test/forgery.spec.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { floodsub } from '../src/index.ts'
1111
import { RPC } from '../src/message/rpc.ts'
1212
import { PeerStreams } from '../src/peer-streams.ts'
1313
import { SignPrefix } from '../src/sign.ts'
14-
import { randomSeqno, toRpcMessage } from '../src/utils.ts'
14+
import { nextSeqno, toRpcMessage } from '../src/utils.ts'
1515
import type { PubSubRPCMessage } from '../src/floodsub.ts'
1616
import type { FloodSub } from '../src/index.ts'
1717
import type { PeerId, PrivateKey, PublicKey } from '@libp2p/interface'
@@ -27,7 +27,7 @@ async function wireMessage (from: PeerId, signer: PrivateKey, key: PublicKey): P
2727
type: 'signed',
2828
from,
2929
data,
30-
sequenceNumber: randomSeqno(),
30+
sequenceNumber: nextSeqno(),
3131
topic,
3232
signature: new Uint8Array(0),
3333
key

‎packages/floodsub/test/message.spec.ts‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
1010
import { floodsub } from '../src/index.ts'
1111
import { RPC } from '../src/message/rpc.ts'
1212
import { SignPrefix } from '../src/sign.ts'
13-
import { randomSeqno, toMessage, toRpcMessage } from '../src/utils.ts'
13+
import { nextSeqno, toMessage, toRpcMessage } from '../src/utils.ts'
1414
import type { FloodSub, SignedMessage } from '../src/index.ts'
1515
import type { PeerId } from '@libp2p/interface'
1616
import type { Registrar } from '@libp2p/interface-internal'
@@ -43,7 +43,7 @@ describe('pubsub base messages', () => {
4343
from: peerId,
4444
data: uint8ArrayFromString('hello'),
4545
topic: 'test-topic',
46-
sequenceNumber: randomSeqno()
46+
sequenceNumber: nextSeqno()
4747
}
4848

4949
// @ts-expect-error private method
@@ -58,7 +58,7 @@ describe('pubsub base messages', () => {
5858
from: peerId,
5959
data: uint8ArrayFromString('hello'),
6060
topic: 'test-topic',
61-
sequenceNumber: randomSeqno()
61+
sequenceNumber: nextSeqno()
6262
}
6363

6464
sinon.stub(pubsub, 'globalSignaturePolicy').value('StrictSign')
@@ -92,7 +92,7 @@ describe('pubsub base messages', () => {
9292
from: peerId,
9393
data: uint8ArrayFromString('hello'),
9494
topic: 'test-topic',
95-
sequenceNumber: randomSeqno()
95+
sequenceNumber: nextSeqno()
9696
}
9797

9898
sinon.stub(pubsub, 'globalSignaturePolicy').value('StrictNoSign')
@@ -127,7 +127,7 @@ describe('pubsub base messages', () => {
127127
from: victim,
128128
data: uint8ArrayFromString('hello'),
129129
topic: 'test-topic',
130-
sequenceNumber: randomSeqno()
130+
sequenceNumber: nextSeqno()
131131
}
132132
const bytes = uint8ArrayConcat([SignPrefix, RPC.Message.encode(toRpcMessage(forged)).subarray()])
133133
forged.signature = await attackerKey.sign(bytes)

‎packages/floodsub/test/sign.spec.ts‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
SignPrefix,
1010
verifySignature
1111
} from '../src/sign.ts'
12-
import { randomSeqno, toRpcMessage } from '../src/utils.ts'
12+
import { nextSeqno, toRpcMessage } from '../src/utils.ts'
1313
import type { PubSubRPCMessage } from '../src/floodsub.ts'
1414
import type { Message, SignedMessage } from '../src/index.ts'
1515
import type { PeerId, PrivateKey } from '@libp2p/interface'
@@ -33,7 +33,7 @@ describe('message signing', () => {
3333
type: 'signed',
3434
from: peerId,
3535
data: uint8ArrayFromString('hello'),
36-
sequenceNumber: randomSeqno(),
36+
sequenceNumber: nextSeqno(),
3737
topic: 'test-topic'
3838
}
3939

@@ -65,7 +65,7 @@ describe('message signing', () => {
6565
type: 'signed',
6666
from: secPeerId,
6767
data: uint8ArrayFromString('hello'),
68-
sequenceNumber: randomSeqno(),
68+
sequenceNumber: nextSeqno(),
6969
topic: 'test-topic'
7070
}
7171

@@ -93,7 +93,7 @@ describe('message signing', () => {
9393
type: 'signed',
9494
from: peerId,
9595
data: uint8ArrayFromString('hello'),
96-
sequenceNumber: randomSeqno(),
96+
sequenceNumber: nextSeqno(),
9797
topic: 'test-topic'
9898
}
9999

@@ -127,7 +127,7 @@ describe('author key binding', () => {
127127
type: 'signed',
128128
from: victim,
129129
data,
130-
sequenceNumber: randomSeqno(),
130+
sequenceNumber: nextSeqno(),
131131
topic
132132
}
133133
const bytes = uint8ArrayConcat([SignPrefix, encodeMessage(toRpcMessage(message)).subarray()])
@@ -163,7 +163,7 @@ describe('author key binding', () => {
163163
})
164164

165165
it('accepts a message whose key derives to the author', async () => {
166-
const signed = await signMessage(rsaKey, { from: author, topic, data, sequenceNumber: randomSeqno() }, encodeMessage)
166+
const signed = await signMessage(rsaKey, { from: author, topic, data, sequenceNumber: nextSeqno() }, encodeMessage)
167167
// signMessage rebuilds `from` from the private key, so it carries the key;
168168
// use the wire form so the derive-to-author check is exercised
169169
signed.from = author

‎packages/floodsub/test/utils.spec.ts‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,17 @@ import type { PubSubRPCMessage } from '../src/floodsub.ts'
66
import type { Message } from '../src/index.ts'
77

88
describe('utils', () => {
9-
it('randomSeqno', () => {
10-
const first = utils.randomSeqno()
11-
const second = utils.randomSeqno()
9+
it('nextSeqno', () => {
10+
const first = utils.nextSeqno()
11+
const second = utils.nextSeqno()
1212

1313
expect(first).to.be.a('BigInt')
1414
expect(second).to.be.a('BigInt')
15-
expect(first).to.not.equal(second)
15+
expect(second > first).to.be.true()
16+
})
17+
18+
it('encodes sequence numbers as 8 bytes', () => {
19+
expect(utils.bigIntToBytes(utils.nextSeqno())).to.have.lengthOf(8)
1620
})
1721

1822
it('msgId should not generate same ID for two different Uint8Arrays', async () => {

0 commit comments

Comments
 (0)