Skip to content

Commit 6ddefb0

Browse files
feat: add sessions to trustless gateways (#459)
Implements blockstore sessions for trustless gateways. - Queries the Helia routing for block providers - Creates a set of trustless gateways from routing results - Uses only these gateways to fetch session blocks --------- Co-authored-by: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com>
1 parent 5cf216b commit 6ddefb0

File tree

6 files changed

+248
-14
lines changed

6 files changed

+248
-14
lines changed

packages/block-brokers/.aegir.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import cors from 'cors'
2+
import polka from 'polka'
3+
4+
/** @type {import('aegir').PartialOptions} */
5+
const options = {
6+
test: {
7+
async before (options) {
8+
const server = polka({
9+
port: 0,
10+
host: '127.0.0.1'
11+
})
12+
server.use(cors())
13+
server.all('/ipfs/bafkreiefnkxuhnq3536qo2i2w3tazvifek4mbbzb6zlq3ouhprjce5c3aq', (req, res) => {
14+
res.writeHead(200, {
15+
'content-type': 'application/octet-stream'
16+
})
17+
res.end(Uint8Array.from([0, 1, 2, 0]))
18+
})
19+
20+
await server.listen()
21+
const { port } = server.server.address()
22+
23+
return {
24+
server,
25+
env: {
26+
TRUSTLESS_GATEWAY: `http://127.0.0.1:${port}`
27+
}
28+
}
29+
},
30+
async after (options, before) {
31+
await before.server.server.close()
32+
}
33+
}
34+
}
35+
36+
export default options

packages/block-brokers/package.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,15 +55,24 @@
5555
"dependencies": {
5656
"@helia/interface": "^4.1.0",
5757
"@libp2p/interface": "^1.1.4",
58+
"@libp2p/utils": "^5.2.6",
59+
"@multiformats/multiaddr-matcher": "^1.2.0",
60+
"@multiformats/multiaddr-to-uri": "^10.0.1",
5861
"interface-blockstore": "^5.2.10",
5962
"ipfs-bitswap": "^20.0.2",
6063
"multiformats": "^13.1.0",
64+
"p-defer": "^4.0.0",
6165
"progress-events": "^1.0.0"
6266
},
6367
"devDependencies": {
6468
"@libp2p/logger": "^4.0.7",
69+
"@libp2p/peer-id-factory": "^4.0.7",
70+
"@multiformats/multiaddr": "^12.1.14",
71+
"@multiformats/uri-to-multiaddr": "^8.0.0",
6572
"@types/sinon": "^17.0.3",
6673
"aegir": "^42.2.5",
74+
"cors": "^2.8.5",
75+
"polka": "^0.5.2",
6776
"sinon": "^17.0.1",
6877
"sinon-ts": "^2.0.0"
6978
}

packages/block-brokers/src/trustless-gateway/broker.ts

Lines changed: 145 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,66 @@
1+
import { DEFAULT_SESSION_MIN_PROVIDERS, DEFAULT_SESSION_MAX_PROVIDERS, DEFAULT_SESSION_PROVIDER_QUERY_CONCURRENCY, DEFAULT_SESSION_PROVIDER_QUERY_TIMEOUT } from '@helia/interface'
2+
import { PeerQueue } from '@libp2p/utils/peer-queue'
3+
import { isPrivateIp } from '@libp2p/utils/private-ip'
4+
import { DNS, HTTP, HTTPS } from '@multiformats/multiaddr-matcher'
5+
import { multiaddrToUri } from '@multiformats/multiaddr-to-uri'
6+
import pDefer from 'p-defer'
17
import { TrustlessGateway } from './trustless-gateway.js'
28
import { DEFAULT_TRUSTLESS_GATEWAYS } from './index.js'
39
import type { TrustlessGatewayBlockBrokerInit, TrustlessGatewayComponents, TrustlessGatewayGetBlockProgressEvents } from './index.js'
4-
import type { BlockRetrievalOptions, BlockBroker } from '@helia/interface/blocks'
10+
import type { Routing, BlockRetrievalOptions, BlockBroker, CreateSessionOptions } from '@helia/interface'
511
import type { Logger } from '@libp2p/interface'
612
import type { CID } from 'multiformats/cid'
713

14+
export interface CreateTrustlessGatewaySessionOptions extends CreateSessionOptions<TrustlessGatewayGetBlockProgressEvents> {
15+
/**
16+
* Specify the cache control header to send to the remote. 'only-if-cached'
17+
* will prevent the gateway from fetching the content if they don't have it.
18+
*
19+
* @default only-if-cached
20+
*/
21+
cacheControl?: string
22+
23+
/**
24+
* By default we will only connect to peers with HTTPS addresses, pass true
25+
* to also connect to HTTP addresses.
26+
*
27+
* @default false
28+
*/
29+
allowInsecure?: boolean
30+
31+
/**
32+
* By default we will only connect to peers with public or DNS addresses, pass
33+
* true to also connect to private addresses.
34+
*
35+
* @default false
36+
*/
37+
allowLocal?: boolean
38+
}
39+
840
/**
941
* A class that accepts a list of trustless gateways that are queried
1042
* for blocks.
1143
*/
1244
export class TrustlessGatewayBlockBroker implements BlockBroker<TrustlessGatewayGetBlockProgressEvents> {
45+
private readonly components: TrustlessGatewayComponents
1346
private readonly gateways: TrustlessGateway[]
47+
private readonly routing: Routing
1448
private readonly log: Logger
1549

1650
constructor (components: TrustlessGatewayComponents, init: TrustlessGatewayBlockBrokerInit = {}) {
51+
this.components = components
1752
this.log = components.logger.forComponent('helia:trustless-gateway-block-broker')
53+
this.routing = components.routing
1854
this.gateways = (init.gateways ?? DEFAULT_TRUSTLESS_GATEWAYS)
1955
.map((gatewayOrUrl) => {
20-
return new TrustlessGateway(gatewayOrUrl)
56+
return new TrustlessGateway(gatewayOrUrl, components.logger)
2157
})
2258
}
2359

60+
addGateway (gatewayOrUrl: string): void {
61+
this.gateways.push(new TrustlessGateway(gatewayOrUrl, this.components.logger))
62+
}
63+
2464
async retrieve (cid: CID, options: BlockRetrievalOptions<TrustlessGatewayGetBlockProgressEvents> = {}): Promise<Uint8Array> {
2565
// Loop through the gateways until we get a block or run out of gateways
2666
// TODO: switch to toSorted when support is better
@@ -38,7 +78,7 @@ export class TrustlessGatewayBlockBroker implements BlockBroker<TrustlessGateway
3878
this.log.error('failed to validate block for %c from %s', cid, gateway.url, err)
3979
gateway.incrementInvalidBlocks()
4080

41-
throw new Error(`unable to validate block for CID ${cid} from gateway ${gateway.url}`)
81+
throw new Error(`Block for CID ${cid} from gateway ${gateway.url} failed validation`)
4282
}
4383

4484
return block
@@ -47,7 +87,7 @@ export class TrustlessGatewayBlockBroker implements BlockBroker<TrustlessGateway
4787
if (err instanceof Error) {
4888
aggregateErrors.push(err)
4989
} else {
50-
aggregateErrors.push(new Error(`unable to fetch raw block for CID ${cid} from gateway ${gateway.url}`))
90+
aggregateErrors.push(new Error(`Unable to fetch raw block for CID ${cid} from gateway ${gateway.url}`))
5191
}
5292
// if signal was aborted, exit the loop
5393
if (options.signal?.aborted === true) {
@@ -57,6 +97,106 @@ export class TrustlessGatewayBlockBroker implements BlockBroker<TrustlessGateway
5797
}
5898
}
5999

60-
throw new AggregateError(aggregateErrors, `unable to fetch raw block for CID ${cid} from any gateway`)
100+
if (aggregateErrors.length > 0) {
101+
throw new AggregateError(aggregateErrors, `Unable to fetch raw block for CID ${cid} from any gateway`)
102+
} else {
103+
throw new Error(`Unable to fetch raw block for CID ${cid} from any gateway`)
104+
}
105+
}
106+
107+
async createSession (root: CID, options: CreateTrustlessGatewaySessionOptions = {}): Promise<BlockBroker<TrustlessGatewayGetBlockProgressEvents>> {
108+
const gateways: string[] = []
109+
const minProviders = options.minProviders ?? DEFAULT_SESSION_MIN_PROVIDERS
110+
const maxProviders = options.minProviders ?? DEFAULT_SESSION_MAX_PROVIDERS
111+
const deferred = pDefer<BlockBroker<TrustlessGatewayGetBlockProgressEvents>>()
112+
const broker = new TrustlessGatewayBlockBroker(this.components, {
113+
gateways
114+
})
115+
116+
this.log('finding transport-ipfs-gateway-http providers for cid %c', root)
117+
118+
const queue = new PeerQueue({
119+
concurrency: options.providerQueryConcurrency ?? DEFAULT_SESSION_PROVIDER_QUERY_CONCURRENCY
120+
})
121+
122+
Promise.resolve().then(async () => {
123+
for await (const provider of this.routing.findProviders(root, options)) {
124+
const httpAddresses = provider.multiaddrs.filter(ma => {
125+
if (HTTPS.matches(ma) || (options.allowInsecure === true && HTTP.matches(ma))) {
126+
if (options.allowLocal === true) {
127+
return true
128+
}
129+
130+
if (DNS.matches(ma)) {
131+
return true
132+
}
133+
134+
return isPrivateIp(ma.toOptions().host) === false
135+
}
136+
137+
return false
138+
})
139+
140+
if (httpAddresses.length === 0) {
141+
continue
142+
}
143+
144+
this.log('found transport-ipfs-gateway-http provider %p for cid %c', provider.id, root)
145+
146+
void queue.add(async () => {
147+
for (const ma of httpAddresses) {
148+
let uri: string | undefined
149+
150+
try {
151+
// /ip4/x.x.x.x/tcp/31337/http
152+
// /ip4/x.x.x.x/tcp/31337/https
153+
// etc
154+
uri = multiaddrToUri(ma)
155+
156+
const resource = `${uri}/ipfs/${root.toString()}?format=raw`
157+
158+
// make sure the peer is available - HEAD support doesn't seem to
159+
// be very widely implemented so as long as the remote responds
160+
// we are happy they are valid
161+
// https://specs.ipfs.tech/http-gateways/trustless-gateway/#head-ipfs-cid-path-params
162+
163+
// in the future we should be able to request `${uri}/.well-known/libp2p-http
164+
// and discover an IPFS gateway from $.protocols['/ipfs/gateway'].path
165+
// in the response
166+
// https://github.com/libp2p/specs/pull/508/files
167+
const response = await fetch(resource, {
168+
method: 'HEAD',
169+
headers: {
170+
Accept: 'application/vnd.ipld.raw',
171+
'Cache-Control': options.cacheControl ?? 'only-if-cached'
172+
},
173+
signal: AbortSignal.timeout(options.providerQueryTimeout ?? DEFAULT_SESSION_PROVIDER_QUERY_TIMEOUT)
174+
})
175+
176+
this.log('HEAD %s %d', resource, response.status)
177+
gateways.push(uri)
178+
broker.addGateway(uri)
179+
180+
this.log('found %d transport-ipfs-gateway-http providers for cid %c', gateways.length, root)
181+
182+
if (gateways.length === minProviders) {
183+
deferred.resolve(broker)
184+
}
185+
186+
if (gateways.length === maxProviders) {
187+
queue.clear()
188+
}
189+
} catch (err: any) {
190+
this.log.error('could not fetch %c from %a', root, uri ?? ma, err)
191+
}
192+
}
193+
})
194+
}
195+
})
196+
.catch(err => {
197+
this.log.error('error creating session for %c', root, err)
198+
})
199+
200+
return deferred.promise
61201
}
62202
}

packages/block-brokers/src/trustless-gateway/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { TrustlessGatewayBlockBroker } from './broker.js'
2-
import type { BlockBroker } from '@helia/interface/src/blocks.js'
2+
import type { Routing, BlockBroker } from '@helia/interface'
33
import type { ComponentLogger } from '@libp2p/interface'
44
import type { ProgressEvent } from 'progress-events'
55

@@ -22,6 +22,7 @@ export interface TrustlessGatewayBlockBrokerInit {
2222
}
2323

2424
export interface TrustlessGatewayComponents {
25+
routing: Routing
2526
logger: ComponentLogger
2627
}
2728

packages/block-brokers/src/trustless-gateway/trustless-gateway.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { ComponentLogger, Logger } from '@libp2p/interface'
12
import type { CID } from 'multiformats/cid'
23

34
/**
@@ -36,8 +37,11 @@ export class TrustlessGateway {
3637
*/
3738
#successes = 0
3839

39-
constructor (url: URL | string) {
40+
private readonly log: Logger
41+
42+
constructor (url: URL | string, logger: ComponentLogger) {
4043
this.url = url instanceof URL ? url : new URL(url)
44+
this.log = logger.forComponent(`helia:trustless-gateway-block-broker:${this.url.hostname}`)
4145
}
4246

4347
/**
@@ -67,6 +71,9 @@ export class TrustlessGateway {
6771
},
6872
cache: 'force-cache'
6973
})
74+
75+
this.log('GET %s %d', gwUrl, res.status)
76+
7077
if (!res.ok) {
7178
this.#errors++
7279
throw new Error(`unable to fetch raw block for CID ${cid} from gateway ${this.url}`)

packages/block-brokers/test/trustless-gateway.spec.ts

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,24 @@
11
/* eslint-env mocha */
22

33
import { defaultLogger } from '@libp2p/logger'
4+
import { createEd25519PeerId } from '@libp2p/peer-id-factory'
5+
import { multiaddr } from '@multiformats/multiaddr'
6+
import { uriToMultiaddr } from '@multiformats/uri-to-multiaddr'
47
import { expect } from 'aegir/chai'
58
import * as raw from 'multiformats/codecs/raw'
69
import Sinon from 'sinon'
7-
import { type StubbedInstance, stubConstructor } from 'sinon-ts'
10+
import { type StubbedInstance, stubConstructor, stubInterface } from 'sinon-ts'
811
import { TrustlessGatewayBlockBroker } from '../src/trustless-gateway/broker.js'
912
import { TrustlessGateway } from '../src/trustless-gateway/trustless-gateway.js'
1013
import { createBlock } from './fixtures/create-block.js'
11-
import type { BlockBroker } from '@helia/interface/blocks'
14+
import type { Routing } from '@helia/interface'
1215
import type { CID } from 'multiformats/cid'
1316

1417
describe('trustless-gateway-block-broker', () => {
1518
let blocks: Array<{ cid: CID, block: Uint8Array }>
16-
let gatewayBlockBroker: BlockBroker
19+
let gatewayBlockBroker: TrustlessGatewayBlockBroker
1720
let gateways: Array<StubbedInstance<TrustlessGateway>>
21+
let routing: StubbedInstance<Routing>
1822

1923
// take a Record<gatewayIndex, (gateway: StubbedInstance<TrustlessGateway>) => void> and stub the gateways
2024
// Record.default is the default handler
@@ -29,19 +33,21 @@ describe('trustless-gateway-block-broker', () => {
2933
}
3034

3135
beforeEach(async () => {
36+
routing = stubInterface<Routing>()
3237
blocks = []
3338

3439
for (let i = 0; i < 10; i++) {
3540
blocks.push(await createBlock(raw.code, Uint8Array.from([0, 1, 2, i])))
3641
}
3742

3843
gateways = [
39-
stubConstructor(TrustlessGateway, 'http://localhost:8080'),
40-
stubConstructor(TrustlessGateway, 'http://localhost:8081'),
41-
stubConstructor(TrustlessGateway, 'http://localhost:8082'),
42-
stubConstructor(TrustlessGateway, 'http://localhost:8083')
44+
stubConstructor(TrustlessGateway, 'http://localhost:8080', defaultLogger()),
45+
stubConstructor(TrustlessGateway, 'http://localhost:8081', defaultLogger()),
46+
stubConstructor(TrustlessGateway, 'http://localhost:8082', defaultLogger()),
47+
stubConstructor(TrustlessGateway, 'http://localhost:8083', defaultLogger())
4348
]
4449
gatewayBlockBroker = new TrustlessGatewayBlockBroker({
50+
routing,
4551
logger: defaultLogger()
4652
})
4753
// must copy the array because the broker calls .sort which mutates in-place
@@ -150,4 +156,39 @@ describe('trustless-gateway-block-broker', () => {
150156
expect(gateways[1].getRawBlock.calledWith(cid1, Sinon.match.any)).to.be.false()
151157
expect(gateways[2].getRawBlock.calledWith(cid1, Sinon.match.any)).to.be.false()
152158
})
159+
160+
it('creates a session', async () => {
161+
routing.findProviders.returns(async function * () {
162+
// non-http provider
163+
yield {
164+
id: await createEd25519PeerId(),
165+
multiaddrs: [
166+
multiaddr('/ip4/132.32.25.6/tcp/1234')
167+
]
168+
}
169+
// expired peer info
170+
yield {
171+
id: await createEd25519PeerId(),
172+
multiaddrs: []
173+
}
174+
// http gateway
175+
yield {
176+
id: await createEd25519PeerId(),
177+
multiaddrs: [
178+
uriToMultiaddr(process.env.TRUSTLESS_GATEWAY ?? '')
179+
]
180+
}
181+
}())
182+
183+
const sessionBlockstore = await gatewayBlockBroker.createSession?.(blocks[0].cid, {
184+
minProviders: 1,
185+
providerQueryConcurrency: 1,
186+
allowInsecure: true,
187+
allowLocal: true
188+
})
189+
190+
expect(sessionBlockstore).to.be.ok()
191+
192+
await expect(sessionBlockstore?.retrieve?.(blocks[0].cid)).to.eventually.deep.equal(blocks[0].block)
193+
})
153194
})

0 commit comments

Comments
 (0)