Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add fetch protocol #1036

Merged
merged 26 commits into from
Jan 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
afb5188
rebuild protobufs on clean build
stbrody Nov 24, 2021
3ab5c0e
feat: Add fetch protocol
stbrody Nov 24, 2021
9559b2b
update readme
stbrody Nov 24, 2021
7ceb9c7
fix comment
stbrody Nov 29, 2021
5a9eea0
wip: use a class to have a stateful instance of fetch protocol
stbrody Nov 29, 2021
2686e5e
can register multiple lookup functions
stbrody Nov 29, 2021
dbb3315
update tests
stbrody Nov 29, 2021
3d73cc0
update docs/comments
stbrody Nov 29, 2021
d75ef08
Use error when missing key handler instead of just returning null
stbrody Nov 29, 2021
0126366
underscore prefix private methods/variables
stbrody Nov 29, 2021
839d349
small comment cleanup
stbrody Nov 29, 2021
daf7232
minor
stbrody Nov 29, 2021
d1627a9
revert generated protobuf code from other modules
stbrody Jan 7, 2022
9a13fb9
Merge remote-tracking branch 'origin' into feat/fetch-protocol
stbrody Jan 7, 2022
5882a0e
fix test setup
stbrody Jan 7, 2022
d658007
use error codes
stbrody Jan 7, 2022
db9990f
Merge remote-tracking branch 'origin/master' into feat/fetch-protocol
achingbrain Jan 24, 2022
ef2d860
chore: fix linting and tests
achingbrain Jan 24, 2022
bb61c9e
chore: refactor for consistency with identify service
achingbrain Jan 24, 2022
30f7e59
chore: make fetch command top level
achingbrain Jan 24, 2022
da00c4d
chore: document fetch method
achingbrain Jan 24, 2022
a005361
chore: document register/unregister
achingbrain Jan 24, 2022
839a487
chore: put prepare back
achingbrain Jan 24, 2022
24e1ecb
chore: reuse existing error code
achingbrain Jan 24, 2022
d03018b
chore: update fetch protocol name in line with spec
achingbrain Jan 24, 2022
bb04a19
chore: linting
achingbrain Jan 24, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions doc/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
* [`handle`](#handle)
* [`unhandle`](#unhandle)
* [`ping`](#ping)
* [`fetch`](#fetch)
* [`fetchService.registerLookupFunction`](#fetchserviceregisterlookupfunction)
* [`fetchService.unRegisterLookupFunction`](#fetchserviceunregisterlookupfunction)
* [`multiaddrs`](#multiaddrs)
* [`addressManager.getListenAddrs`](#addressmanagergetlistenaddrs)
* [`addressManager.getAnnounceAddrs`](#addressmanagergetannounceaddrs)
Expand Down Expand Up @@ -455,6 +458,72 @@ Pings a given peer and get the operation's latency.
const latency = await libp2p.ping(otherPeerId)
```

## fetch

Fetch a value from a remote node

`libp2p.fetch(peer, key)`

#### Parameters

| Name | Type | Description |
|------|------|-------------|
| peer | [`PeerId`][peer-id]\|[`Multiaddr`][multiaddr]\|`string` | peer to ping |
| key | `string` | A key that corresponds to a value on the remote node |

#### Returns

| Type | Description |
|------|-------------|
| `Promise<Uint8Array | null>` | The value for the key or null if it cannot be found |

#### Example

```js
// ...
const value = await libp2p.fetch(otherPeerId, '/some/key')
```

## fetchService.registerLookupFunction

Register a function to look up values requested by remote nodes

`libp2p.fetchService.registerLookupFunction(prefix, lookup)`

#### Parameters

| Name | Type | Description |
|------|------|-------------|
| prefix | `string` | All queries below this prefix will be passed to the lookup function |
| lookup | `(key: string) => Promise<Uint8Array | null>` | A function that takes a key and returns a Uint8Array or null |

#### Example

```js
// ...
const value = await libp2p.fetchService.registerLookupFunction('/prefix', (key) => { ... })
```

## fetchService.unregisterLookupFunction

Removes the passed lookup function or any function registered for the passed prefix

`libp2p.fetchService.unregisterLookupFunction(prefix, lookup)`

#### Parameters

| Name | Type | Description |
|------|------|-------------|
| prefix | `string` | All queries below this prefix will be passed to the lookup function |
| lookup | `(key: string) => Promise<Uint8Array | null>` | Optional: A function that takes a key and returns a Uint8Array or null |

#### Example

```js
// ...
libp2p.fetchService.unregisterLookupFunction('/prefix')
```

## multiaddrs

Gets the multiaddrs the libp2p node announces to the network. This computes the advertising multiaddrs
Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,17 @@
"scripts": {
"lint": "aegir lint",
"build": "aegir build",
"build:proto": "npm run build:proto:circuit && npm run build:proto:identify && npm run build:proto:plaintext && npm run build:proto:address-book && npm run build:proto:proto-book && npm run build:proto:peer && npm run build:proto:peer-record && npm run build:proto:envelope",
"build:proto": "npm run build:proto:circuit && npm run build:proto:fetch && npm run build:proto:identify && npm run build:proto:plaintext && npm run build:proto:address-book && npm run build:proto:proto-book && npm run build:proto:peer && npm run build:proto:peer-record && npm run build:proto:envelope",
"build:proto:circuit": "pbjs -t static-module -w commonjs -r libp2p-circuit --force-number --no-verify --no-delimited --no-create --no-beautify --no-defaults --lint eslint-disable -o src/circuit/protocol/index.js ./src/circuit/protocol/index.proto",
"build:proto:fetch": "pbjs -t static-module -w commonjs -r libp2p-fetch --force-number --no-verify --no-delimited --no-create --no-beautify --no-defaults --lint eslint-disable -o src/fetch/proto.js ./src/fetch/proto.proto",
"build:proto:identify": "pbjs -t static-module -w commonjs -r libp2p-identify --force-number --no-verify --no-delimited --no-create --no-beautify --no-defaults --lint eslint-disable -o src/identify/message.js ./src/identify/message.proto",
"build:proto:plaintext": "pbjs -t static-module -w commonjs -r libp2p-plaintext --force-number --no-verify --no-delimited --no-create --no-beautify --no-defaults --lint eslint-disable -o src/insecure/proto.js ./src/insecure/proto.proto",
"build:proto:peer": "pbjs -t static-module -w commonjs -r libp2p-peer --force-number --no-verify --no-delimited --no-create --no-beautify --no-defaults --lint eslint-disable -o src/peer-store/pb/peer.js ./src/peer-store/pb/peer.proto",
"build:proto:peer-record": "pbjs -t static-module -w commonjs -r libp2p-peer-record --force-number --no-verify --no-delimited --no-create --no-beautify --no-defaults --lint eslint-disable -o src/record/peer-record/peer-record.js ./src/record/peer-record/peer-record.proto",
"build:proto:envelope": "pbjs -t static-module -w commonjs -r libp2p-envelope --force-number --no-verify --no-delimited --no-create --no-beautify --no-defaults --lint eslint-disable -o src/record/envelope/envelope.js ./src/record/envelope/envelope.proto",
"build:proto-types": "npm run build:proto-types:circuit && npm run build:proto-types:identify && npm run build:proto-types:plaintext && npm run build:proto-types:address-book && npm run build:proto-types:proto-book && npm run build:proto-types:peer && npm run build:proto-types:peer-record && npm run build:proto-types:envelope",
"build:proto-types": "npm run build:proto-types:circuit && npm run build:proto-types:fetch && npm run build:proto-types:identify && npm run build:proto-types:plaintext && npm run build:proto-types:address-book && npm run build:proto-types:proto-book && npm run build:proto-types:peer && npm run build:proto-types:peer-record && npm run build:proto-types:envelope",
"build:proto-types:circuit": "pbts -o src/circuit/protocol/index.d.ts src/circuit/protocol/index.js",
"build:proto-types:fetch": "pbts -o src/fetch/proto.d.ts src/fetch/proto.js",
"build:proto-types:identify": "pbts -o src/identify/message.d.ts src/identify/message.js",
"build:proto-types:plaintext": "pbts -o src/insecure/proto.d.ts src/insecure/proto.js",
"build:proto-types:peer": "pbts -o src/peer-store/pb/peer.d.ts src/peer-store/pb/peer.js",
Expand Down
36 changes: 36 additions & 0 deletions src/fetch/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
libp2p-fetch JavaScript Implementation
=====================================

> Libp2p fetch protocol JavaScript implementation

## Overview

An implementation of the Fetch protocol as described here: https://github.com/libp2p/specs/tree/master/fetch

The fetch protocol is a simple protocol for requesting a value corresponding to a key from a peer.

## Usage

```javascript
const Libp2p = require('libp2p')

/**
* Given a key (as a string) returns a value (as a Uint8Array), or null if the key isn't found.
* All keys must be prefixed my the same prefix, which will be used to find the appropriate key
stbrody marked this conversation as resolved.
Show resolved Hide resolved
* lookup function.
* @param key - a string
* @returns value - a Uint8Array value that corresponds to the given key, or null if the key doesn't
* have a corresponding value.
*/
async function my_subsystem_key_lookup(key) {
// app specific callback to lookup key-value pairs.
}

// Enable this peer to respond to fetch requests for keys that begin with '/my_subsystem_key_prefix/'
const libp2p = Libp2p.create(...)
libp2p.fetchService.registerLookupFunction('/my_subsystem_key_prefix/', my_subsystem_key_lookup)

const key = '/my_subsystem_key_prefix/{...}'
const peerDst = PeerId.parse('Qmfoo...') // or Multiaddr instance
const value = await libp2p.fetch(peerDst, key)
```
6 changes: 6 additions & 0 deletions src/fetch/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
'use strict'

module.exports = {
// https://github.com/libp2p/specs/tree/master/fetch#wire-protocol
PROTOCOL: '/libp2p/fetch/0.0.1'
}
159 changes: 159 additions & 0 deletions src/fetch/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
'use strict'

const debug = require('debug')
const log = Object.assign(debug('libp2p:fetch'), {
error: debug('libp2p:fetch:err')
})
const errCode = require('err-code')
const { codes } = require('../errors')
const lp = require('it-length-prefixed')
const { FetchRequest, FetchResponse } = require('./proto')
// @ts-ignore it-handshake does not export types
const handshake = require('it-handshake')
const { PROTOCOL } = require('./constants')

/**
* @typedef {import('../')} Libp2p
* @typedef {import('multiaddr').Multiaddr} Multiaddr
* @typedef {import('peer-id')} PeerId
* @typedef {import('libp2p-interfaces/src/stream-muxer/types').MuxedStream} MuxedStream
* @typedef {(key: string) => Promise<Uint8Array | null>} LookupFunction
*/

/**
* A simple libp2p protocol for requesting a value corresponding to a key from a peer.
* Developers can register one or more lookup function for retrieving the value corresponding to
* a given key. Each lookup function must act on a distinct part of the overall key space, defined
* by a fixed prefix that all keys that should be routed to that lookup function will start with.
*/
class FetchProtocol {
/**
* @param {Libp2p} libp2p
*/
constructor (libp2p) {
this._lookupFunctions = new Map() // Maps key prefix to value lookup function
this._libp2p = libp2p
this.handleMessage = this.handleMessage.bind(this)
}

/**
* Sends a request to fetch the value associated with the given key from the given peer.
*
* @param {PeerId|Multiaddr} peer
* @param {string} key
* @returns {Promise<Uint8Array | null>}
*/
async fetch (peer, key) {
// @ts-ignore multiaddr might not have toB58String
log('dialing %s to %s', this._protocol, peer.toB58String ? peer.toB58String() : peer)

const connection = await this._libp2p.dial(peer)
const { stream } = await connection.newStream(FetchProtocol.PROTOCOL)
const shake = handshake(stream)

// send message
const request = new FetchRequest({ identifier: key })
shake.write(lp.encode.single(FetchRequest.encode(request).finish()))

// read response
const response = FetchResponse.decode((await lp.decode.fromReader(shake.reader).next()).value.slice())
switch (response.status) {
case (FetchResponse.StatusCode.OK): {
return response.data
}
case (FetchResponse.StatusCode.NOT_FOUND): {
return null
}
case (FetchResponse.StatusCode.ERROR): {
const errmsg = (new TextDecoder()).decode(response.data)
throw errCode(new Error('Error in fetch protocol response: ' + errmsg), codes.ERR_INVALID_PARAMETERS)
}
default: {
throw errCode(new Error('Unknown response status'), codes.ERR_INVALID_MESSAGE)
}
}
}

/**
* Invoked when a fetch request is received. Reads the request message off the given stream and
* responds based on looking up the key in the request via the lookup callback that corresponds
* to the key's prefix.
*
* @param {object} options
* @param {MuxedStream} options.stream
* @param {string} options.protocol
*/
async handleMessage (options) {
const { stream } = options
const shake = handshake(stream)
const request = FetchRequest.decode((await lp.decode.fromReader(shake.reader).next()).value.slice())

let response
const lookup = this._getLookupFunction(request.identifier)
if (lookup) {
const data = await lookup(request.identifier)
if (data) {
response = new FetchResponse({ status: FetchResponse.StatusCode.OK, data })
} else {
response = new FetchResponse({ status: FetchResponse.StatusCode.NOT_FOUND })
}
} else {
const errmsg = (new TextEncoder()).encode('No lookup function registered for key: ' + request.identifier)
response = new FetchResponse({ status: FetchResponse.StatusCode.ERROR, data: errmsg })
}

shake.write(lp.encode.single(FetchResponse.encode(response).finish()))
}

/**
* Given a key, finds the appropriate function for looking up its corresponding value, based on
* the key's prefix.
*
* @param {string} key
*/
_getLookupFunction (key) {
for (const prefix of this._lookupFunctions.keys()) {
if (key.startsWith(prefix)) {
return this._lookupFunctions.get(prefix)
}
}
return null
}

/**
* Registers a new lookup callback that can map keys to values, for a given set of keys that
* share the same prefix.
*
* @param {string} prefix
* @param {LookupFunction} lookup
*/
registerLookupFunction (prefix, lookup) {
if (this._lookupFunctions.has(prefix)) {
throw errCode(new Error("Fetch protocol handler for key prefix '" + prefix + "' already registered"), codes.ERR_KEY_ALREADY_EXISTS)
}
this._lookupFunctions.set(prefix, lookup)
}

/**
* Registers a new lookup callback that can map keys to values, for a given set of keys that
* share the same prefix.
*
* @param {string} prefix
* @param {LookupFunction} [lookup]
*/
unregisterLookupFunction (prefix, lookup) {
if (lookup != null) {
const existingLookup = this._lookupFunctions.get(prefix)

if (existingLookup !== lookup) {
return
}
}

this._lookupFunctions.delete(prefix)
}
}

FetchProtocol.PROTOCOL = PROTOCOL

exports = module.exports = FetchProtocol
Loading