Skip to content

Commit 80fe31a

Browse files
authored
feat: expire peerstore data (#3019)
Expires peer store multiaddrs after an hour and removes peer store peers after six hours if their addresses have not been updated. Fixes #3017
1 parent 6074de6 commit 80fe31a

File tree

12 files changed

+464
-188
lines changed

12 files changed

+464
-188
lines changed

packages/peer-store/src/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export const MAX_ADDRESS_AGE = 3_600_000
2+
export const MAX_PEER_AGE = 21_600_000

packages/peer-store/src/index.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,31 @@ export interface AddressFilter {
2727
}
2828

2929
export interface PersistentPeerStoreInit {
30+
/**
31+
* Used to remove multiaddrs of peers before storing them. The default is to
32+
* store all addresses
33+
*/
3034
addressFilter?: AddressFilter
35+
36+
/**
37+
* The multiaddrs for a given peer will expire after this number of ms after
38+
* which they must be re-fetched using the peer routing.
39+
*
40+
* Defaults to one hour.
41+
*
42+
* @default 3_600_000
43+
*/
44+
maxAddressAge?: number
45+
46+
/**
47+
* Any peer without multiaddrs that has not been updated after this number of
48+
* ms will be evicted from the peer store.
49+
*
50+
* Defaults to six hours.
51+
*
52+
* @default 21_600_000
53+
*/
54+
maxPeerAge?: number
3155
}
3256

3357
/**

packages/peer-store/src/pb/peer.proto

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ message Peer {
1818

1919
// Any tags the peer has
2020
map<string, Tag> tags = 7;
21+
22+
// timestamp in ms of when this peer was last updated
23+
optional uint64 updated = 8 [jstype = JS_NUMBER];
2124
}
2225

2326
// Address represents a single multiaddr
@@ -26,6 +29,9 @@ message Address {
2629

2730
// Flag to indicate if the address comes from a certified source
2831
optional bool isCertified = 2;
32+
33+
// timestamp in ms of when this address was observed
34+
optional uint64 observed = 3 [jstype = JS_NUMBER];
2935
}
3036

3137
message Tag {

packages/peer-store/src/pb/peer.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export interface Peer {
1515
peerRecordEnvelope?: Uint8Array
1616
metadata: Map<string, Uint8Array>
1717
tags: Map<string, Tag>
18+
updated?: number
1819
}
1920

2021
export namespace Peer {
@@ -208,6 +209,11 @@ export namespace Peer {
208209
}
209210
}
210211

212+
if (obj.updated != null) {
213+
w.uint32(64)
214+
w.uint64Number(obj.updated)
215+
}
216+
211217
if (opts.lengthDelimited !== false) {
212218
w.ldelim()
213219
}
@@ -273,6 +279,10 @@ export namespace Peer {
273279
obj.tags.set(entry.key, entry.value)
274280
break
275281
}
282+
case 8: {
283+
obj.updated = reader.uint64Number()
284+
break
285+
}
276286
default: {
277287
reader.skipType(tag & 7)
278288
break
@@ -299,6 +309,7 @@ export namespace Peer {
299309
export interface Address {
300310
multiaddr: Uint8Array
301311
isCertified?: boolean
312+
observed?: number
302313
}
303314

304315
export namespace Address {
@@ -321,6 +332,11 @@ export namespace Address {
321332
w.bool(obj.isCertified)
322333
}
323334

335+
if (obj.observed != null) {
336+
w.uint32(24)
337+
w.uint64Number(obj.observed)
338+
}
339+
324340
if (opts.lengthDelimited !== false) {
325341
w.ldelim()
326342
}
@@ -343,6 +359,10 @@ export namespace Address {
343359
obj.isCertified = reader.bool()
344360
break
345361
}
362+
case 3: {
363+
obj.observed = reader.uint64Number()
364+
break
365+
}
346366
default: {
347367
reader.skipType(tag & 7)
348368
break

packages/peer-store/src/store.ts

Lines changed: 95 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import { InvalidParametersError } from '@libp2p/interface'
1+
import { NotFoundError } from '@libp2p/interface'
22
import { peerIdFromCID } from '@libp2p/peer-id'
33
import mortice, { type Mortice } from 'mortice'
44
import { base32 } from 'multiformats/bases/base32'
55
import { CID } from 'multiformats/cid'
6-
import { equals as uint8ArrayEquals } from 'uint8arrays/equals'
6+
import { MAX_ADDRESS_AGE, MAX_PEER_AGE } from './constants.js'
77
import { Peer as PeerPB } from './pb/peer.js'
8-
import { bytesToPeer } from './utils/bytes-to-peer.js'
8+
import { bytesToPeer, pbToPeer } from './utils/bytes-to-peer.js'
9+
import { peerEquals } from './utils/peer-equals.js'
910
import { NAMESPACE_COMMON, peerIdToDatastoreKey } from './utils/peer-id-to-datastore-key.js'
1011
import { toPeerPB } from './utils/to-peer-pb.js'
1112
import type { AddressFilter, PersistentPeerStoreComponents, PersistentPeerStoreInit } from './index.js'
@@ -19,27 +20,33 @@ export interface PeerUpdate extends PeerUpdateExternal {
1920
updated: boolean
2021
}
2122

22-
function decodePeer (key: Key, value: Uint8Array): Peer {
23+
export interface ExistingPeer {
24+
peerPB: PeerPB
25+
peer: Peer
26+
}
27+
28+
function keyToPeerId (key: Key): PeerId {
2329
// /peers/${peer-id-as-libp2p-key-cid-string-in-base-32}
2430
const base32Str = key.toString().split('/')[2]
2531
const buf = CID.parse(base32Str, base32)
26-
const peerId = peerIdFromCID(buf)
2732

28-
return bytesToPeer(peerId, value)
33+
return peerIdFromCID(buf)
2934
}
3035

31-
function mapQuery (query: PeerQuery): Query {
32-
if (query == null) {
33-
return {}
34-
}
36+
function decodePeer (key: Key, value: Uint8Array, maxAddressAge: number): Peer {
37+
const peerId = keyToPeerId(key)
38+
39+
return bytesToPeer(peerId, value, maxAddressAge)
40+
}
3541

42+
function mapQuery (query: PeerQuery, maxAddressAge: number): Query {
3643
return {
3744
prefix: NAMESPACE_COMMON,
3845
filters: (query.filters ?? []).map(fn => ({ key, value }) => {
39-
return fn(decodePeer(key, value))
46+
return fn(decodePeer(key, value, maxAddressAge))
4047
}),
4148
orders: (query.orders ?? []).map(fn => (a, b) => {
42-
return fn(decodePeer(a.key, a.value), decodePeer(b.key, b.value))
49+
return fn(decodePeer(a.key, a.value, maxAddressAge), decodePeer(b.key, b.value, maxAddressAge))
4350
})
4451
}
4552
}
@@ -50,6 +57,8 @@ export class PersistentStore {
5057
public readonly lock: Mortice
5158
private readonly addressFilter?: AddressFilter
5259
private readonly log: Logger
60+
private readonly maxAddressAge: number
61+
private readonly maxPeerAge: number
5362

5463
constructor (components: PersistentPeerStoreComponents, init: PersistentPeerStoreInit = {}) {
5564
this.log = components.logger.forComponent('libp2p:peer-store')
@@ -60,115 +69,146 @@ export class PersistentStore {
6069
name: 'peer-store',
6170
singleProcess: true
6271
})
72+
this.maxAddressAge = init.maxAddressAge ?? MAX_ADDRESS_AGE
73+
this.maxPeerAge = init.maxPeerAge ?? MAX_PEER_AGE
6374
}
6475

6576
async has (peerId: PeerId): Promise<boolean> {
66-
return this.datastore.has(peerIdToDatastoreKey(peerId))
77+
try {
78+
await this.load(peerId)
79+
80+
return true
81+
} catch (err: any) {
82+
if (err.name !== 'NotFoundError') {
83+
throw err
84+
}
85+
}
86+
87+
return false
6788
}
6889

6990
async delete (peerId: PeerId): Promise<void> {
7091
if (this.peerId.equals(peerId)) {
71-
throw new InvalidParametersError('Cannot delete self peer')
92+
return
7293
}
7394

7495
await this.datastore.delete(peerIdToDatastoreKey(peerId))
7596
}
7697

7798
async load (peerId: PeerId): Promise<Peer> {
78-
const buf = await this.datastore.get(peerIdToDatastoreKey(peerId))
99+
const key = peerIdToDatastoreKey(peerId)
100+
const buf = await this.datastore.get(key)
101+
const peer = PeerPB.decode(buf)
102+
103+
if (this.#peerIsExpired(peer)) {
104+
await this.datastore.delete(key)
105+
throw new NotFoundError()
106+
}
79107

80-
return bytesToPeer(peerId, buf)
108+
return pbToPeer(peerId, peer, this.maxAddressAge)
81109
}
82110

83111
async save (peerId: PeerId, data: PeerData): Promise<PeerUpdate> {
84-
const {
85-
existingBuf,
86-
existingPeer
87-
} = await this.#findExistingPeer(peerId)
112+
const existingPeer = await this.#findExistingPeer(peerId)
88113

89114
const peerPb: PeerPB = await toPeerPB(peerId, data, 'patch', {
90115
addressFilter: this.addressFilter
91116
})
92117

93-
return this.#saveIfDifferent(peerId, peerPb, existingBuf, existingPeer)
118+
return this.#saveIfDifferent(peerId, peerPb, existingPeer)
94119
}
95120

96121
async patch (peerId: PeerId, data: Partial<PeerData>): Promise<PeerUpdate> {
97-
const {
98-
existingBuf,
99-
existingPeer
100-
} = await this.#findExistingPeer(peerId)
122+
const existingPeer = await this.#findExistingPeer(peerId)
101123

102124
const peerPb: PeerPB = await toPeerPB(peerId, data, 'patch', {
103125
addressFilter: this.addressFilter,
104126
existingPeer
105127
})
106128

107-
return this.#saveIfDifferent(peerId, peerPb, existingBuf, existingPeer)
129+
return this.#saveIfDifferent(peerId, peerPb, existingPeer)
108130
}
109131

110132
async merge (peerId: PeerId, data: PeerData): Promise<PeerUpdate> {
111-
const {
112-
existingBuf,
113-
existingPeer
114-
} = await this.#findExistingPeer(peerId)
133+
const existingPeer = await this.#findExistingPeer(peerId)
115134

116135
const peerPb: PeerPB = await toPeerPB(peerId, data, 'merge', {
117136
addressFilter: this.addressFilter,
118137
existingPeer
119138
})
120139

121-
return this.#saveIfDifferent(peerId, peerPb, existingBuf, existingPeer)
140+
return this.#saveIfDifferent(peerId, peerPb, existingPeer)
122141
}
123142

124143
async * all (query?: PeerQuery): AsyncGenerator<Peer, void, unknown> {
125-
for await (const { key, value } of this.datastore.query(mapQuery(query ?? {}))) {
126-
const peer = decodePeer(key, value)
144+
for await (const { key, value } of this.datastore.query(mapQuery(query ?? {}, this.maxAddressAge))) {
145+
const peerId = keyToPeerId(key)
127146

128-
if (peer.id.equals(this.peerId)) {
129-
// Skip self peer if present
147+
// skip self peer if present
148+
if (peerId.equals(this.peerId)) {
130149
continue
131150
}
132151

133-
yield peer
152+
const peer = PeerPB.decode(value)
153+
154+
// remove expired peer
155+
if (this.#peerIsExpired(peer)) {
156+
await this.datastore.delete(key)
157+
continue
158+
}
159+
160+
yield pbToPeer(peerId, peer, this.maxAddressAge)
134161
}
135162
}
136163

137-
async #findExistingPeer (peerId: PeerId): Promise<{ existingBuf?: Uint8Array, existingPeer?: Peer }> {
164+
async #findExistingPeer (peerId: PeerId): Promise<ExistingPeer | undefined> {
138165
try {
139-
const existingBuf = await this.datastore.get(peerIdToDatastoreKey(peerId))
140-
const existingPeer = bytesToPeer(peerId, existingBuf)
166+
const key = peerIdToDatastoreKey(peerId)
167+
const buf = await this.datastore.get(key)
168+
const peerPB = PeerPB.decode(buf)
169+
170+
// remove expired peer
171+
if (this.#peerIsExpired(peerPB)) {
172+
await this.datastore.delete(key)
173+
throw new NotFoundError()
174+
}
141175

142176
return {
143-
existingBuf,
144-
existingPeer
177+
peerPB,
178+
peer: bytesToPeer(peerId, buf, this.maxAddressAge)
145179
}
146180
} catch (err: any) {
147181
if (err.name !== 'NotFoundError') {
148182
this.log.error('invalid peer data found in peer store - %e', err)
149183
}
150184
}
151-
152-
return {}
153185
}
154186

155-
async #saveIfDifferent (peerId: PeerId, peer: PeerPB, existingBuf?: Uint8Array, existingPeer?: Peer): Promise<PeerUpdate> {
187+
async #saveIfDifferent (peerId: PeerId, peer: PeerPB, existingPeer?: ExistingPeer): Promise<PeerUpdate> {
188+
// record last update
189+
peer.updated = Date.now()
156190
const buf = PeerPB.encode(peer)
157191

158-
if (existingBuf != null && uint8ArrayEquals(buf, existingBuf)) {
159-
return {
160-
peer: bytesToPeer(peerId, buf),
161-
previous: existingPeer,
162-
updated: false
163-
}
164-
}
165-
166192
await this.datastore.put(peerIdToDatastoreKey(peerId), buf)
167193

168194
return {
169-
peer: bytesToPeer(peerId, buf),
170-
previous: existingPeer,
171-
updated: true
195+
peer: bytesToPeer(peerId, buf, this.maxAddressAge),
196+
previous: existingPeer?.peer,
197+
updated: existingPeer == null || !peerEquals(peer, existingPeer.peerPB)
198+
}
199+
}
200+
201+
#peerIsExpired (peer: PeerPB): boolean {
202+
if (peer.updated == null) {
203+
return true
172204
}
205+
206+
const expired = peer.updated < (Date.now() - this.maxPeerAge)
207+
const minAddressObserved = Date.now() - this.maxAddressAge
208+
const addrs = peer.addresses.filter(addr => {
209+
return addr.observed != null && addr.observed > minAddressObserved
210+
})
211+
212+
return expired && addrs.length === 0
173213
}
174214
}

0 commit comments

Comments
 (0)