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 support for webrtc and certhash (#261) #262

Merged
merged 7 commits into from
Aug 30, 2022
7 changes: 6 additions & 1 deletion src/convert.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

import * as ip from './ip.js'
import { getProtocol } from './protocols-table.js'
import { CID } from 'multiformats/cid'
Expand Down Expand Up @@ -26,6 +25,9 @@ export function convert (proto: string, a: string | Uint8Array) {
*/
export function convertToString (proto: number | string, buf: Uint8Array) {
const protocol = getProtocol(proto)
if (protocol.convertor) {
return protocol.convertor.bytesToString(buf);
}
switch (protocol.code) {
case 4: // ipv4
case 41: // ipv6
Expand Down Expand Up @@ -58,6 +60,9 @@ export function convertToString (proto: number | string, buf: Uint8Array) {

export function convertToBytes (proto: string | number, str: string) {
const protocol = getProtocol(proto)
if (protocol.convertor) {
return protocol.convertor.stringToBytes(str);
}
switch (protocol.code) {
case 4: // ipv4
return ip2bytes(str)
Expand Down
45 changes: 45 additions & 0 deletions src/default-certhash-codec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { ParseError } from './codec.js'
import type { Codec } from './protocols-table'
import { concat as uint8ArrayConcat } from 'uint8arrays/concat'
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
import varint from 'varint'
import { base16 } from 'multiformats/bases/base16'
import { base58btc } from 'multiformats/bases/base58'
import { base64url } from 'multiformats/bases/base64'
import type { BaseDecoder } from 'multiformats/bases/interface'

const encodings: {[prefix: string]: BaseDecoder} = {
'f': base16,
'u': base64url,
'z': base58btc,
};
Copy link
Member

Choose a reason for hiding this comment

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

I am not familiar with the JS multiformats stack. I am surprised that this mapping needs to be defined. I would expect this to be a general multihash and not a certhash specific mapping is.

Copy link
Member

Choose a reason for hiding this comment

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

You can compose a decoder from several other decoders, though the types are a little wonky.

I would expect this to be a general multihash

Do you mean not limited to the f, u or z prefixes? multiformats follows a pattern where you only include the codecs you need instead of the kitchen-sink approach.

You can, of course, compose your codec out of all available codecs.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@mxinden I believe my first pass supported all the encodings for a "general multihash", and I may have read too much into @achingbrain 's criticism of that (that the dependency I was using was too large because it supported too much). That's why I limited what I'm bringing in by default and provided the mechanism for the caller to dependency-inject a broader codec if wished.

Will look into using or to compose decoders. And to be clear, we're saying we want to support all of them, to take the draft spec as currently written literally, regardless of who is using this library? Except, perhaps, encodings with '/' in their alphabet?

Copy link
Member

Choose a reason for hiding this comment

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

I would expect this to be a general multihash

Do you mean not limited to the f, u or z prefixes? multiformats follows a pattern where you only include the codecs you need instead of the kitchen-sink approach.

I was not referring to supporting all multihash variants, but instead to the fact that the mapping between identifier (e.g. f) and its multihash variant (here base16) needs to be written by the user. That seems error prone to me. I might just be missing something.

And to be clear, we're saying we want to support all of them, to take the draft spec as currently written literally, regardless of who is using this library? Except, perhaps, encodings with '/' in their alphabet?

That is how we implement it in Rust multiformats/rust-multiaddr#59 and Golang multiformats/go-multiaddr#176. That said, both encode with Base64Url. Thus I think it is fine to support only a minimal set of bases for now.

We might require users to use sha256. In the same vein, we could as well require users to use Base64Url for now. Thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

the mapping between identifier (e.g. f) and its multihash variant (here base16) needs to be written by the user.
It doesn't and if you see the new version of the code I dropped that.

Base64Url. Thus I think it is fine to support only a minimal set of bases for now.
I'll do whichever you like. Right now it's back to supporting "all" of them.

I put all in quotes because of the '/' issue. I didn't see an easy way to detect the alphabets from the dictionary provided by the multiformats library so I didn't filter them out. I would just guess that a base64-encoded certhash that happens to have a / in it, if placed in a Multiaddr, would cause problems at runtime.

Copy link
Member

Choose a reason for hiding this comment

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

For the record libp2p/specs@7009f94 specifies the requirement of supporting sha256 and base64url. I don't have an opinion on whether js-multiaddr should support any other algorithms / encodings beyond that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Well-spotted. If the other side is only required to accept those two, then base58btc was a very poor choice of encoding for convertToString(). I'll fix that.

I think we can afford to leave the convertToBytes as supporting a bunch of 'em, since I'm not adding anything new to package.json to get it. But I'll certainly remove those others if advised to do so.


export class DefaultCerthashCodec implements Codec {
stringToBytes(stringRepresentation: string): Uint8Array {
if (stringRepresentation.length < 2) {
throw ParseError('Not enough length to be a multibase: ' + stringRepresentation);
}
let prefix = stringRepresentation[0];
let encoded = stringRepresentation.slice(1);
let encoding = encodings[prefix];
let decoded: Uint8Array;
if (encoding) {
decoded = encoding.baseDecode(encoded);
} else {
throw new Error('certhash is in a multibase encoding that is not supported by default. Please provide a custom decoder: '+stringRepresentation);
}
const size = Uint8Array.from(varint.encode(decoded.length))
return uint8ArrayConcat([size, decoded], size.length + decoded.length)
}
bytesToString(byteRepresentation: Uint8Array): string {
const size = varint.decode(byteRepresentation)
const hash = byteRepresentation.slice(varint.decode.bytes)

if (hash.length !== size) {
throw new Error('inconsistent lengths')
}

return 'z' + uint8ArrayToString(hash, 'base58btc')
}

}
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as codec from './codec.js'
import { getProtocol, names } from './protocols-table.js'
import { setProtocolCodec, getProtocol, names } from './protocols-table.js'
import varint from 'varint'
import { CID } from 'multiformats/cid'
import { base58btc } from 'multiformats/bases/base58'
Expand Down Expand Up @@ -647,4 +647,5 @@ export function multiaddr (addr: MultiaddrInput) {
}

export { getProtocol as protocols }
export { setProtocolCodec }
export { resolvers }
24 changes: 21 additions & 3 deletions src/protocols-table.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
import { DefaultCerthashCodec } from "./default-certhash-codec.js";

export interface Codec {
stringToBytes(stringRepresentation: string): Uint8Array;
bytesToString(byteRepresentation: Uint8Array): string;
}

export interface Protocol {
code: number
size: number
name: string
resolvable?: boolean
path?: boolean
convertor?: Codec
}

const V = -1
export const names: Record<string, Protocol> = {}
export const codes: Record<number, Protocol> = {}

export const table: Array<[number, number, string, boolean?, boolean?]> = [
export const table: Array<[number, number, string, boolean?, boolean?, Codec?]> = [
[4, 32, 'ip4'],
[6, 16, 'tcp'],
[33, 16, 'dccp'],
Expand All @@ -25,6 +33,7 @@ export const table: Array<[number, number, string, boolean?, boolean?]> = [
[275, 0, 'p2p-webrtc-star'],
[276, 0, 'p2p-webrtc-direct'],
[277, 0, 'p2p-stardust'],
[280, 0, 'webrtc'],
[290, 0, 'p2p-circuit'],
[301, 0, 'udt'],
[302, 0, 'utp'],
Expand All @@ -40,6 +49,7 @@ export const table: Array<[number, number, string, boolean?, boolean?]> = [
[445, 296, 'onion3'],
[446, V, 'garlic64'],
[460, 0, 'quic'],
[466, V, 'certhash', false, false, new DefaultCerthashCodec()],
[477, 0, 'ws'],
[478, 0, 'wss'],
[479, 0, 'p2p-websocket-star'],
Expand All @@ -54,13 +64,14 @@ table.forEach(row => {
names[proto.name] = proto
})

export function createProtocol (code: number, size: number, name: string, resolvable?: any, path?: any): Protocol {
export function createProtocol (code: number, size: number, name: string, resolvable?: any, path?: any, convertor?: Codec): Protocol {
return {
code,
size,
name,
resolvable: Boolean(resolvable),
path: Boolean(path)
path: Boolean(path),
convertor: convertor
}
}

Expand All @@ -81,3 +92,10 @@ export function getProtocol (proto: number | string) {

throw new Error(`invalid protocol id type: ${typeof proto}`)
}

export function setProtocolCodec(proto: number | string, codec: Codec) {
let protocol = getProtocol(proto)
protocol.convertor = codec
codes[protocol.code] = protocol
names[protocol.name] = protocol
}
14 changes: 14 additions & 0 deletions test/convert.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,18 @@ describe('convert', () => {
expect(convert.convertToString('sctp', buffer.subarray(5))).to.equal('1234')
})
})

it('can round-trip certhash, though encoding base may change', ()=>{
let myCertFingerprint = {
"algorithm": "sha-256",
"value": "f4:32:a0:45:34:62:85:e0:d8:d7:75:36:84:72:8e:b2:aa:9e:71:64:e4:eb:fe:06:51:64:42:64:fe:04:a8:d0"
};
let mb = 'f' + myCertFingerprint.value.replaceAll(':','');
let bytes = convert.convertToBytes('certhash',mb);
let outcome = convert.convertToString(466, bytes);
//Although I sent hex encoding in, base64 always comes out
expect(outcome).to.equal('zHSFFdP8jLt9o6HXnz5bBYoE7JACNqVcFNnCnDyd5Ynyu');
let bytesOut = convert.convertToBytes(466,outcome);
expect(bytesOut.toString()).to.equal(bytes.toString());
})
})
15 changes: 14 additions & 1 deletion test/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -540,12 +540,14 @@ describe('helpers', () => {
expect(new Multiaddr('/ip4/0.0.0.0/utp').protos())
.to.eql([{
code: 4,
convertor: undefined,
name: 'ip4',
path: false,
size: 32,
resolvable: false
resolvable: false,
}, {
code: 302,
convertor: undefined,
name: 'utp',
path: false,
size: 0,
Expand All @@ -558,18 +560,21 @@ describe('helpers', () => {
new Multiaddr('/ip4/0.0.0.0/utp/ipfs/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC').protos()
).to.be.eql([{
code: 4,
convertor: undefined,
name: 'ip4',
path: false,
size: 32,
resolvable: false
}, {
code: 302,
convertor: undefined,
name: 'utp',
path: false,
size: 0,
resolvable: false
}, {
code: 421,
convertor: undefined,
name: 'p2p',
path: false,
size: -1,
Expand All @@ -582,18 +587,21 @@ describe('helpers', () => {
new Multiaddr('/ip4/0.0.0.0/utp/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC').protos()
).to.be.eql([{
code: 4,
convertor: undefined,
name: 'ip4',
path: false,
size: 32,
resolvable: false
}, {
code: 302,
convertor: undefined,
name: 'utp',
path: false,
size: 0,
resolvable: false
}, {
code: 421,
convertor: undefined,
name: 'p2p',
path: false,
size: -1,
Expand All @@ -606,18 +614,21 @@ describe('helpers', () => {
new Multiaddr('/ip4/0.0.0.0/tcp/8000/unix/tmp/p2p.sock').protos()
).to.be.eql([{
code: 4,
convertor: undefined,
name: 'ip4',
path: false,
size: 32,
resolvable: false
}, {
code: 6,
convertor: undefined,
name: 'tcp',
path: false,
size: 16,
resolvable: false
}, {
code: 400,
convertor: undefined,
name: 'unix',
path: true,
size: -1,
Expand All @@ -630,12 +641,14 @@ describe('helpers', () => {
new Multiaddr('/memory/test/p2p/QmZR5a9AAXGqQF2ADqoDdGS8zvqv8n3Pag6TDDnTNMcFW6').protos()
).to.be.eql([{
code: 777,
convertor: undefined,
name: 'memory',
path: false,
size: -1,
resolvable: false
}, {
code: 421,
convertor: undefined,
name: 'p2p',
path: false,
size: -1,
Expand Down