Skip to content

Commit b56d918

Browse files
authored
fix!: accept Uint8Arrays as keys (#2909)
To allow use cases like fetching IPNS records in a way compatible with go-libp2p we need to send binary as fetch identifiers. JavaScript strings are UTF-16 so we can't round-trip binary reliably since some byte sequences are interpreted as multi-byte or otherwise non-printable characters. Instead we need to accept Uint8Arrays and send them over the wire as-is. This is a backwards compatible change as far as interop goes since protobuf `bytes` and `string` types are identical on the wire, but it's breaking for API consumers in that the lookup function now needs to accept a `Uint8Array` identifier instead of a `string`. Refs: libp2p/specs#656 BREAKING CHANGE: registered lookup functions now receive a Uint8Array identifier instead of a string
1 parent 60ccf1a commit b56d918

File tree

7 files changed

+91
-56
lines changed

7 files changed

+91
-56
lines changed

packages/integration-tests/test/fetch.spec.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { type Fetch, fetch } from '@libp2p/fetch'
44
import { expect } from 'aegir/chai'
55
import { createLibp2p } from 'libp2p'
6+
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
67
import { isWebWorker } from 'wherearewe'
78
import { createBaseOptions } from './fixtures/base-options.js'
89
import type { Libp2p } from '@libp2p/interface'
@@ -31,9 +32,9 @@ describe('fetch', () => {
3132
const DATA_B = { foobar: 'goodnight moon' }
3233

3334
const generateLookupFunction = function (prefix: string, data: Record<string, string>) {
34-
return async function (key: string): Promise<Uint8Array | undefined> {
35+
return async function (key: Uint8Array): Promise<Uint8Array | undefined> {
3536
key = key.slice(prefix.length) // strip prefix from key
36-
const val = data[key]
37+
const val = data[uint8ArrayToString(key)]
3738
if (val != null) {
3839
return (new TextEncoder()).encode(val)
3940
}

packages/protocol-fetch/README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,12 @@ const libp2p = await createLibp2p({
4141
}
4242
})
4343

44-
// Given a key (as a string) returns a value (as a Uint8Array), or undefined
45-
// if the key isn't found.
44+
// Given a key (as a Uint8Array) returns a value (as a Uint8Array), or
45+
// undefined if the key isn't found.
46+
//
4647
// All keys must be prefixed by the same prefix, which will be used to find
4748
// the appropriate key lookup function.
48-
async function my_subsystem_key_lookup (key: string): Promise<Uint8Array | undefined> {
49+
async function my_subsystem_key_lookup (key: Uint8Array): Promise<Uint8Array | undefined> {
4950
// app specific callback to lookup key-value pairs.
5051
return Uint8Array.from([0, 1, 2, 3, 4])
5152
}

packages/protocol-fetch/src/fetch.ts

Lines changed: 48 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export class Fetch implements Startable, FetchInterface {
4343
await data.stream.close()
4444
})
4545
.catch(err => {
46-
this.log.error(err)
46+
this.log.error('error handling message - %e', err)
4747
})
4848
}, {
4949
maxInboundStreams: this.init.maxInboundStreams,
@@ -64,8 +64,12 @@ export class Fetch implements Startable, FetchInterface {
6464
/**
6565
* Sends a request to fetch the value associated with the given key from the given peer
6666
*/
67-
async fetch (peer: PeerId, key: string, options: AbortOptions = {}): Promise<Uint8Array | undefined> {
68-
this.log('dialing %s to %p', this.protocol, peer)
67+
async fetch (peer: PeerId, key: string | Uint8Array, options: AbortOptions = {}): Promise<Uint8Array | undefined> {
68+
if (typeof key === 'string') {
69+
key = uint8arrayFromString(key)
70+
}
71+
72+
this.log.trace('dialing %s to %p', this.protocol, peer)
6973

7074
const connection = await this.components.connectionManager.openConnection(peer, options)
7175
let signal = options.signal
@@ -75,7 +79,7 @@ export class Fetch implements Startable, FetchInterface {
7579
// create a timeout if no abort signal passed
7680
if (signal == null) {
7781
const timeout = this.init.timeout ?? DEFAULT_TIMEOUT
78-
this.log('using default timeout of %d ms', timeout)
82+
this.log.trace('using default timeout of %d ms', timeout)
7983
signal = AbortSignal.timeout(timeout)
8084

8185
setMaxListeners(Infinity, signal)
@@ -93,7 +97,7 @@ export class Fetch implements Startable, FetchInterface {
9397
// make stream abortable
9498
signal.addEventListener('abort', onAbort, { once: true })
9599

96-
this.log('fetch %s', key)
100+
this.log.trace('fetch %m', key)
97101

98102
const pb = pbStream(stream)
99103
await pb.write({
@@ -105,20 +109,20 @@ export class Fetch implements Startable, FetchInterface {
105109

106110
switch (response.status) {
107111
case (FetchResponse.StatusCode.OK): {
108-
this.log('received status for %s ok', key)
112+
this.log.trace('received status OK for %m', key)
109113
return response.data
110114
}
111115
case (FetchResponse.StatusCode.NOT_FOUND): {
112-
this.log('received status for %s not found', key)
116+
this.log('received status NOT_FOUND for %m', key)
113117
return
114118
}
115119
case (FetchResponse.StatusCode.ERROR): {
116-
this.log('received status for %s error', key)
120+
this.log('received status ERROR for %m', key)
117121
const errmsg = uint8arrayToString(response.data)
118122
throw new ProtocolError('Error in fetch protocol response: ' + errmsg)
119123
}
120124
default: {
121-
this.log('received status for %s unknown', key)
125+
this.log('received status unknown for %m', key)
122126
throw new InvalidMessageError('Unknown response status')
123127
}
124128
}
@@ -149,21 +153,32 @@ export class Fetch implements Startable, FetchInterface {
149153
})
150154

151155
let response: FetchResponse
152-
const lookup = this._getLookupFunction(request.identifier)
153-
if (lookup != null) {
154-
this.log('look up data with identifier %s', request.identifier)
155-
const data = await lookup(request.identifier)
156-
if (data != null) {
157-
this.log('sending status for %s ok', request.identifier)
158-
response = { status: FetchResponse.StatusCode.OK, data }
159-
} else {
160-
this.log('sending status for %s not found', request.identifier)
161-
response = { status: FetchResponse.StatusCode.NOT_FOUND, data: new Uint8Array(0) }
162-
}
163-
} else {
164-
this.log('sending status for %s error', request.identifier)
165-
const errmsg = uint8arrayFromString(`No lookup function registered for key: ${request.identifier}`)
156+
const key = uint8arrayToString(request.identifier)
157+
158+
const lookup = this._getLookupFunction(key)
159+
160+
if (lookup == null) {
161+
this.log.trace('sending status ERROR for %m', request.identifier)
162+
const errmsg = uint8arrayFromString('No lookup function registered for key')
166163
response = { status: FetchResponse.StatusCode.ERROR, data: errmsg }
164+
} else {
165+
this.log.trace('lookup data with identifier %s', lookup.prefix)
166+
167+
try {
168+
const data = await lookup.fn(request.identifier)
169+
170+
if (data == null) {
171+
this.log.trace('sending status NOT_FOUND for %m', request.identifier)
172+
response = { status: FetchResponse.StatusCode.NOT_FOUND, data: new Uint8Array(0) }
173+
} else {
174+
this.log.trace('sending status OK for %m', request.identifier)
175+
response = { status: FetchResponse.StatusCode.OK, data }
176+
}
177+
} catch (err: any) {
178+
this.log.error('error during lookup of %m - %e', request.identifier, err)
179+
const errmsg = uint8arrayFromString(err.message)
180+
response = { status: FetchResponse.StatusCode.ERROR, data: errmsg }
181+
}
167182
}
168183

169184
await pb.write(response, FetchResponse, {
@@ -174,7 +189,7 @@ export class Fetch implements Startable, FetchInterface {
174189
signal
175190
})
176191
} catch (err: any) {
177-
this.log('error answering fetch request', err)
192+
this.log.error('error answering fetch request - %e', err)
178193
stream.abort(err)
179194
}
180195
}
@@ -183,10 +198,17 @@ export class Fetch implements Startable, FetchInterface {
183198
* Given a key, finds the appropriate function for looking up its corresponding value, based on
184199
* the key's prefix.
185200
*/
186-
_getLookupFunction (key: string): LookupFunction | undefined {
201+
_getLookupFunction (key: string): { fn: LookupFunction, prefix: string } | undefined {
187202
for (const prefix of this.lookupFunctions.keys()) {
188203
if (key.startsWith(prefix)) {
189-
return this.lookupFunctions.get(prefix)
204+
const fn = this.lookupFunctions.get(prefix)
205+
206+
if (fn != null) {
207+
return {
208+
fn,
209+
prefix
210+
}
211+
}
190212
}
191213
}
192214
}

packages/protocol-fetch/src/index.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@
1818
* }
1919
* })
2020
*
21-
* // Given a key (as a string) returns a value (as a Uint8Array), or undefined
22-
* // if the key isn't found.
21+
* // Given a key (as a Uint8Array) returns a value (as a Uint8Array), or
22+
* // undefined if the key isn't found.
23+
* //
2324
* // All keys must be prefixed by the same prefix, which will be used to find
2425
* // the appropriate key lookup function.
25-
* async function my_subsystem_key_lookup (key: string): Promise<Uint8Array | undefined> {
26+
* async function my_subsystem_key_lookup (key: Uint8Array): Promise<Uint8Array | undefined> {
2627
* // app specific callback to lookup key-value pairs.
2728
* return Uint8Array.from([0, 1, 2, 3, 4])
2829
* }
@@ -56,8 +57,15 @@ export interface FetchInit {
5657
timeout?: number
5758
}
5859

60+
/**
61+
* A lookup function is registered against a specific identifier prefix and is
62+
* invoked when a remote peer requests a value with that prefix
63+
*/
5964
export interface LookupFunction {
60-
(key: string): Promise<Uint8Array | undefined>
65+
/**
66+
* The key is the identifier requested by the remote peer
67+
*/
68+
(key: Uint8Array): Promise<Uint8Array | undefined>
6169
}
6270

6371
export interface FetchComponents {
@@ -70,7 +78,7 @@ export interface Fetch {
7078
/**
7179
* Sends a request to fetch the value associated with the given key from the given peer
7280
*/
73-
fetch(peer: PeerId, key: string, options?: AbortOptions): Promise<Uint8Array | undefined>
81+
fetch(peer: PeerId, key: string | Uint8Array, options?: AbortOptions): Promise<Uint8Array | undefined>
7482

7583
/**
7684
* Registers a new lookup callback that can map keys to values, for a given set of keys that
Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
syntax = "proto3";
22

33
message FetchRequest {
4-
string identifier = 1;
4+
bytes identifier = 1;
55
}
66

77
message FetchResponse {
8-
StatusCode status = 1;
9-
enum StatusCode {
10-
OK = 0;
11-
NOT_FOUND = 1;
12-
ERROR = 2;
13-
}
14-
bytes data = 2;
8+
enum StatusCode {
9+
OK = 0;
10+
NOT_FOUND = 1;
11+
ERROR = 2;
12+
}
13+
14+
StatusCode status = 1;
15+
bytes data = 2;
1516
}

packages/protocol-fetch/src/pb/proto.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { alloc as uint8ArrayAlloc } from 'uint8arrays/alloc'
99
import type { Uint8ArrayList } from 'uint8arraylist'
1010

1111
export interface FetchRequest {
12-
identifier: string
12+
identifier: Uint8Array
1313
}
1414

1515
export namespace FetchRequest {
@@ -22,17 +22,17 @@ export namespace FetchRequest {
2222
w.fork()
2323
}
2424

25-
if ((obj.identifier != null && obj.identifier !== '')) {
25+
if ((obj.identifier != null && obj.identifier.byteLength > 0)) {
2626
w.uint32(10)
27-
w.string(obj.identifier)
27+
w.bytes(obj.identifier)
2828
}
2929

3030
if (opts.lengthDelimited !== false) {
3131
w.ldelim()
3232
}
3333
}, (reader, length, opts = {}) => {
3434
const obj: any = {
35-
identifier: ''
35+
identifier: uint8ArrayAlloc(0)
3636
}
3737

3838
const end = length == null ? reader.len : reader.pos + length
@@ -42,7 +42,7 @@ export namespace FetchRequest {
4242

4343
switch (tag >>> 3) {
4444
case 1: {
45-
obj.identifier = reader.string()
45+
obj.identifier = reader.bytes()
4646
break
4747
}
4848
default: {

packages/protocol-fetch/test/index.spec.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import { duplexPair } from 'it-pair/duplex'
99
import { pbStream } from 'it-protobuf-stream'
1010
import sinon from 'sinon'
1111
import { stubInterface, type StubbedInstance } from 'sinon-ts'
12+
import { fromString as uint8arrayFromString } from 'uint8arrays/from-string'
13+
import { toString as uint8arrayToString } from 'uint8arrays/to-string'
1214
import { Fetch } from '../src/fetch.js'
1315
import { FetchRequest, FetchResponse } from '../src/pb/proto.js'
1416
import type { ComponentLogger, Connection, Stream, PeerId } from '@libp2p/interface'
@@ -89,7 +91,7 @@ describe('fetch', () => {
8991
const pb = pbStream(incomingStream)
9092
const request = await pb.read(FetchRequest)
9193

92-
expect(request.identifier).to.equal(key)
94+
expect(uint8arrayToString(request.identifier)).to.equal(key)
9395

9496
await pb.write({
9597
status: FetchResponse.StatusCode.OK,
@@ -112,7 +114,7 @@ describe('fetch', () => {
112114
const pb = pbStream(incomingStream)
113115
const request = await pb.read(FetchRequest)
114116

115-
expect(request.identifier).to.equal(key)
117+
expect(uint8arrayToString(request.identifier)).to.equal(key)
116118

117119
await pb.write({
118120
status: FetchResponse.StatusCode.NOT_FOUND
@@ -134,7 +136,7 @@ describe('fetch', () => {
134136
const pb = pbStream(incomingStream)
135137
const request = await pb.read(FetchRequest)
136138

137-
expect(request.identifier).to.equal(key)
139+
expect(uint8arrayToString(request.identifier)).to.equal(key)
138140

139141
await pb.write({
140142
status: FetchResponse.StatusCode.ERROR
@@ -177,7 +179,7 @@ describe('fetch', () => {
177179
} = createStreams(components)
178180

179181
fetch.registerLookupFunction('/test', async (k) => {
180-
expect(k).to.equal(key)
182+
expect(k).to.equalBytes(uint8arrayFromString(key))
181183
return value
182184
})
183185

@@ -189,7 +191,7 @@ describe('fetch', () => {
189191
const pb = pbStream(outgoingStream)
190192

191193
await pb.write({
192-
identifier: key
194+
identifier: uint8arrayFromString(key)
193195
}, FetchRequest)
194196

195197
const response = await pb.read(FetchResponse)
@@ -218,7 +220,7 @@ describe('fetch', () => {
218220
const pb = pbStream(outgoingStream)
219221

220222
await pb.write({
221-
identifier: key
223+
identifier: uint8arrayFromString(key)
222224
}, FetchRequest)
223225

224226
const response = await pb.read(FetchResponse)
@@ -242,7 +244,7 @@ describe('fetch', () => {
242244
const pb = pbStream(outgoingStream)
243245

244246
await pb.write({
245-
identifier: key
247+
identifier: uint8arrayFromString(key)
246248
}, FetchRequest)
247249

248250
const response = await pb.read(FetchResponse)

0 commit comments

Comments
 (0)