Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
58 changes: 48 additions & 10 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"@libp2p/utils": "^3.0.7",
"@libp2p/websockets": "^5.0.7",
"@libp2p/webtransport": "^1.0.11",
"@marcopolo_/libp2p-fetch": "^0.0.1",
"@multiformats/multiaddr": "^12.1.0",
"abortabort": "^0.0.1",
"blockstore-core": "^4.1.0",
Expand All @@ -43,6 +44,7 @@
"debug": "^4.3.4",
"file-type": "^16.5.4",
"helia": "next",
"interface-blockstore": "^5.2.0",
"ipns": "^6.0.0",
"kubo-rpc-client": "^3.0.1",
"libp2p": "^0.43.3",
Expand Down
14 changes: 13 additions & 1 deletion src/get-helia.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@ import type { Helia } from '@helia/interface'
import { MemoryBlockstore } from 'blockstore-core'
import { LevelDatastore } from 'datastore-level'
import { MemoryDatastore } from 'datastore-core'
import type { Blockstore } from 'interface-blockstore'
// import { CID } from 'multiformats/cid'

import type { LibP2pComponents, Libp2pConfigTypes } from './types.ts'
import { getLibp2p } from './getLibp2p.ts'
import { HttpBlockstore } from './http-blockstore.ts'
import { multiaddr } from '@multiformats/multiaddr'
import { peerIdFromString } from '@libp2p/peer-id'

// import debug from 'debug'
// debug.enable('libp2p:websockets,libp2p:webtransport,libp2p:kad-dht,libp2p:dialer*,libp2p:connection-manager')
Expand All @@ -28,7 +32,7 @@ const defaultOptions: GetHeliaOptions = {

export async function getHelia ({ usePersistentDatastore, libp2pConfigType }: GetHeliaOptions = defaultOptions): Promise<Helia> {
// the blockstore is where we store the blocks that make up files
const blockstore: HeliaInit['blockstore'] = new MemoryBlockstore() as unknown as HeliaInit['blockstore']
const memBlockstore = new MemoryBlockstore()

// application-specific data lives in the datastore
let datastore: LibP2pComponents['datastore']
Expand All @@ -44,6 +48,14 @@ export async function getHelia ({ usePersistentDatastore, libp2pConfigType }: Ge
// libp2p is the networking layer that underpins Helia
const libp2p = await getLibp2p({ datastore, type: libp2pConfigType })

// TODO we don't get the webtransport multiaddr after a provide. TODO debug.
const marcoServer = multiaddr('/ip4/34.221.29.193/udp/4001/quic-v1/webtransport/certhash/uEiC13IfwpbpLsAgaV3a-9JR9FDZPxPabgv-UqmAfQuHeVw/certhash/uEiAdK9M5b3NvBMINTaVbfLAjxsTMTY2x-pEQB6lPYQe-Tw/p2p/12D3KooWEKMXiNrBNi6LkNGdT7PxoGuTqFqAiQTosRHftk2vk4k7')
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where's the code that runs this server? Some out of curiosity and some out of wanting to be able to debug if there are issues 😅

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

await libp2p.peerStore.addressBook.add(peerIdFromString('12D3KooWEKMXiNrBNi6LkNGdT7PxoGuTqFqAiQTosRHftk2vk4k7'), [marcoServer])
// TODO, this is to bootstrap a single http over libp2p server. We should get these peers from the router.
await libp2p.dial(marcoServer)

const blockstore = new HttpBlockstore(memBlockstore, libp2p)

// create a Helia node
const helia = await createHelia({
datastore: datastore as unknown as HeliaInit['datastore'],
Expand Down
109 changes: 109 additions & 0 deletions src/http-blockstore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import type { Blockstore, Pair } from 'interface-blockstore'
import type { Connection } from '@libp2p/interface-connection'
import type {
AbortOptions,
Await,
AwaitIterable
} from 'interface-store'
import type { Libp2p } from 'libp2p'
import type { CID } from 'multiformats/cid'
import { fetchViaDuplex } from '@marcopolo_/libp2p-fetch'

// TODOS:
// - Validate the blocks we fetch
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to do this and it should be fairly straightforward to do (just grab the multihash, lookup an implementation of the hash function, run it over the bytes and see if they match)

// - Use format=car to get the whole dag at once rather than per block (probably faster?)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is almost certainly a separate PR once we've got the rest of this done.

// - Keep a list of HTTPS endpoints, not just libp2p peers.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say we should (if we have time) just make up some record data and chuck it into IPNI for our records where the gateways are non-recursive (i.e. only based on data we have locally similar to https://github.com/ipfs/kubo/blob/c58aadb887a5ca88256ac8f3a8fbf60b0263c978/docs/config.md#gatewaynofetch).

Happy to help here, it shouldn't be too bad as I've done some hacking here before. Basically we'd use this format ipni/specs#6 to avoid dealing with the global code table (and associated bikeshedding) as much as possible. It's a small amount of parsing in the code here, and then we can hook up some hacky IPNI record publishing.

For recursive gateways (e.g. dweb.link, ipfs.io, cf-ipfs.com) we could either hard code them, get them from https://ipfs.github.io/public-gateway-checker/ or have an IPNI rendezvous record.


If there's no time, then hard coding it is 😄

// - Keep a list of previous peers we were connected to, so even if we lose a webtransport connection, we can spin it up later.

export class HttpBlockstore implements Blockstore {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIUC this approach will cause problems when also enabling fetching over Bitswap (I think it might check the blockstore before doing a Bitswap request which would basically cause these lookups to be sequential).

An alternative way to do this would be (although @achingbrain might have more helia-friendly suggestions):

  1. Implement this as the Bitswap interface (i.e. want, unwant, cancel_wants)
  2. Make a third implementation of the Bitswap interface that basically asks both in parallel, but when one of them yields a block it cancels on the other one

Later we can basically do combined task prioritization with Bitswap whereby we hit peers (Bitswap or HTTP or HTTP over libp2p) based on how quickly they respond to us with good data.

private peersWithHttpOverLibp2p: Connection[] = []
constructor (private readonly innerBlockstore: Blockstore, libp2p: Libp2p) {
libp2p.addEventListener('peer:connect', (connEvent) => {
// TODO check if they support http over libp2p and the ipfs gateway api
const conn = connEvent.detail
this.peersWithHttpOverLibp2p.push(conn)
})
libp2p.addEventListener('peer:disconnect', (connEvent) => {
const conn = connEvent.detail
this.peersWithHttpOverLibp2p = this.peersWithHttpOverLibp2p.filter((c) => c !== conn)
})
}

getAll (): AwaitIterable<Pair> {
return this.innerBlockstore.getAll()
}

async has (key: CID, options?: AbortOptions): Promise<boolean> {
const innerHas = await this.innerBlockstore.has(key, options)
if (innerHas) {
return true
}

for (const connection of this.peersWithHttpOverLibp2p) {
// TODO unsure how to wait for identify to finish, just trying to see if it works
// const protos = await this.libp2p.peerStore.protoBook.get(connection.remotePeer)
// if (!protos.includes('/libp2p-http')) {
// // TODO remove from peersWithHttpOverLibp2p
// continue
// }

try {
const s = await connection.newStream('/libp2p-http')
const fetch = fetchViaDuplex(s)
const resp = await fetch(new Request(`https://example.com/ipfs/${key.toString()}/`, { method: 'HEAD' }))
if (resp.ok) {
console.log('HTTP Blockstore has it!', key.toString(), 'via libp2p')
return true
}
} catch (err) {
console.warn('http over libp2p err', err)
continue
}
}

return false
}

put (key: CID, val: Uint8Array, options?: AbortOptions): Await<CID> {
return this.innerBlockstore.put(key, val, options)
}

putMany (source: AwaitIterable<Pair>, options?: AbortOptions): AwaitIterable<CID> {
return this.innerBlockstore.putMany(source, options)
}

delete (key: CID, options?: AbortOptions): Await<void> {
return this.innerBlockstore.delete(key, options)
}

deleteMany (source: AwaitIterable<CID>, options?: AbortOptions): AwaitIterable<CID> {
return this.innerBlockstore.deleteMany(source, options)
}

async get (key: CID, options?: AbortOptions): Promise<Uint8Array> {
if (await this.innerBlockstore.has(key, options)) {
return await this.innerBlockstore.get(key, options)
}

for (const connection of this.peersWithHttpOverLibp2p) {
try {
const s = await connection.newStream('/libp2p-http')
const fetch = fetchViaDuplex(s)
const resp = await fetch(new Request(`https://example.com/ipfs/${key.toString()}/?format=raw`))
if (resp.ok) {
const ab = (await resp.arrayBuffer())
await this.innerBlockstore.put(key, new Uint8Array(ab))
return new Uint8Array(ab)
}
} catch (err) {
console.warn('http over libp2p err', err)
continue
}
}
return await this.innerBlockstore.get(key, options)
}

getMany (source: AwaitIterable<CID>, options?: AbortOptions): AwaitIterable<Pair> {
return this.innerBlockstore.getMany(source, options)
}
}
2 changes: 1 addition & 1 deletion src/lib/connectAndGetFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export async function connectAndGetFile ({ channel, localMultiaddr, fileCid, hel
color: COLORS.active
}
})
await helia.stop()
// await helia.stop()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happened here?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was client side stopping of the helia node when fetch was done. We chatted about it in slack a little bit.

However it's probably good to know that most of the stuff with channel.blah and postMessage is not used in the latest code on main. Its leftover from the more "debugging " version when we had the ui terminal output

channel.postMessage({
action: 'SHOW_STATUS',
data: {
Expand Down
2 changes: 2 additions & 0 deletions src/libp2pConfigs/getDhtLibp2pConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ export const getDhtLibp2pConfig = (): Libp2pOptions => ({
peerDiscovery: /** @type {import('libp2p').Libp2pOptions['peerDiscovery']} */([
bootstrap({
list: [
// Marco's random server
'/ip4/34.221.29.193/udp/4001/quic-v1/webtransport/certhash/uEiC13IfwpbpLsAgaV3a-9JR9FDZPxPabgv-UqmAfQuHeVw/certhash/uEiAdK9M5b3NvBMINTaVbfLAjxsTMTY2x-pEQB6lPYQe-Tw/p2p/12D3KooWEKMXiNrBNi6LkNGdT7PxoGuTqFqAiQTosRHftk2vk4k7',
'/dnsaddr/bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN',
'/dnsaddr/bootstrap.libp2p.io/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa',
'/dnsaddr/bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb',
Expand Down