Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
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
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"bin": "src/bin.js",
"scripts": {
"lint": "aegir lint",
"prepare": "npm run build",
"build": "npm run build:proto && npm run build:proto-types && aegir build --no-types",
"build:proto": "pbjs -t static-module -w commonjs -r libp2p-peer-id --force-number --no-verify --no-delimited --no-create --no-beautify --no-defaults --lint eslint-disable -o src/proto.js ./src/proto.proto",
"build:proto-types": "pbts -o src/proto.d.ts src/proto.js",
Expand Down Expand Up @@ -41,14 +42,14 @@
"@types/dirty-chai": "^2.0.2",
"@types/mocha": "^8.2.0",
"aegir": "^33.0.0",
"multihashes": "^4.0.2",
"util": "^0.12.3"
},
"dependencies": {
"cids": "^1.1.5",
"class-is": "^1.1.0",
"libp2p-crypto": "^0.19.0",
"minimist": "^1.2.5",
"multihashes": "^4.0.2",
"multiformats": "^9.0.0",
"protobufjs": "^6.10.2",
"uint8arrays": "^2.0.5"
},
Expand Down
10 changes: 8 additions & 2 deletions src/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { PrivateKey, PublicKey, KeyType } from "libp2p-crypto";
import CID from 'cids'
import { CID } from 'multiformats/cid'

declare namespace PeerId {
/**
Expand Down Expand Up @@ -68,7 +68,7 @@ declare namespace PeerId {
* Create PeerId from CID.
* @param cid The CID.
*/
function createFromCID(cid: CID | Uint8Array | string | object): PeerId;
function createFromCID(cid: CID): PeerId;

/**
* Create PeerId from public key.
Expand All @@ -94,6 +94,12 @@ declare namespace PeerId {
* @param buf Protobuf bytes, as Uint8Array or hex-encoded string.
*/
function createFromProtobuf(buf: Uint8Array | string): Promise<PeerId>;

/**
* Create PeerId from Protobuf bytes.
* @param buf Protobuf bytes, as Uint8Array or hex-encoded string.
*/
function parse(str: string): Promise<PeerId>;
}

/**
Expand Down
76 changes: 57 additions & 19 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,22 @@

'use strict'

const mh = require('multihashes')
const CID = require('cids')
const { CID } = require('multiformats/cid')
const { base58btc } = require('multiformats/bases/base58')
const { base16 } = require('multiformats/bases/base16')
const Digest = require('multiformats/hashes/digest')
const cryptoKeys = require('libp2p-crypto/src/keys')
const withIs = require('class-is')
const { PeerIdProto } = require('./proto')
const uint8ArrayEquals = require('uint8arrays/equals')
const uint8ArrayFromString = require('uint8arrays/from-string')
const uint8ArrayToString = require('uint8arrays/to-string')

// these values are from https://github.com/multiformats/multicodec/blob/master/table.csv
const IDENTITY_CODE = 0x00
Copy link
Member

Choose a reason for hiding this comment

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

Any reason for not using multicodec here? I would like to just have these codes in one place as the source of truth

Copy link
Member Author

Choose a reason for hiding this comment

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

I think one of the design goals behind the new multiformats module was to not have the big lookup tables that multicodec is the guardian of.

Copy link
Member

Choose a reason for hiding this comment

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

Fair enough. Probably worth adding a reference to the table just to reference where they come from? Probably to https://github.com/multiformats/multicodec/blob/master/table.csv

Copy link
Member Author

Choose a reason for hiding this comment

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

In my journey through the codebase doing the multiformats stuff it seems that we only really use four or five of them. I think the only weirdness is when bitswap has to mess around with CID prefixes in incoming bitswap 1.1.0 messages.

Copy link
Member

Choose a reason for hiding this comment

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

I think just adding a comment above with the multicodec table is enough here

Copy link
Member Author

Choose a reason for hiding this comment

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

Done!

const DAG_PB_CODE = 0x70
const LIBP2P_KEY_CODE = 0x72

class PeerId {
constructor (id, privKey, pubKey) {
if (!(id instanceof Uint8Array)) {
Expand All @@ -24,7 +31,7 @@ class PeerId {
}

this._id = id
this._idB58String = mh.toB58String(this.id)
this._idB58String = base58btc.encode(this.id).substring(1)
this._privKey = privKey
this._pubKey = pubKey
}
Expand Down Expand Up @@ -55,9 +62,9 @@ class PeerId {
}

try {
const decoded = mh.decode(this.id)
const decoded = Digest.decode(this.id)

if (decoded.name === 'identity') {
if (decoded.code === IDENTITY_CODE) {
this._pubKey = cryptoKeys.unmarshalPublicKey(decoded.digest)
}
} catch (_) {
Expand Down Expand Up @@ -121,7 +128,7 @@ class PeerId {

// encode/decode functions
toHexString () {
return mh.toHexString(this.id)
return base16.encode(this.id).substring(1)
}

toBytes () {
Expand All @@ -136,10 +143,10 @@ class PeerId {
// in default format from RFC 0001: https://github.com/libp2p/specs/pull/209
toString () {
if (!this._idCIDString) {
const cid = new CID(1, 'libp2p-key', this.id, 'base32')
const cid = CID.createV1(LIBP2P_KEY_CODE, Digest.decode(this.id))

Object.defineProperty(this, '_idCIDString', {
value: cid.toBaseEncodedString('base32'),
value: cid.toString(),
enumerable: false
})
}
Expand Down Expand Up @@ -192,8 +199,9 @@ class PeerId {
*/
hasInlinePublicKey () {
try {
const decoded = mh.decode(this.id)
if (decoded.name === 'identity') {
const decoded = Digest.decode(this.id)

if (decoded.code === IDENTITY_CODE) {
return true
}
} catch (_) {
Expand All @@ -213,7 +221,7 @@ exports = module.exports = PeerIdWithIs

const computeDigest = (pubKey) => {
if (pubKey.bytes.length <= 42) {
return mh.encode(pubKey.bytes, 'identity')
return Digest.create(IDENTITY_CODE, pubKey.bytes).bytes
} else {
return pubKey.hash()
}
Expand All @@ -235,26 +243,46 @@ exports.create = async (opts) => {
}

exports.createFromHexString = (str) => {
return new PeerIdWithIs(mh.fromHexString(str))
return new PeerIdWithIs(base16.decode('f' + str))
}

exports.createFromBytes = (buf) => {
return new PeerIdWithIs(buf)
try {
const cid = CID.decode(buf)

if (!validMulticodec(cid)) {
throw new Error('Supplied PeerID CID is invalid')
}

return exports.createFromCID(cid)
} catch {
const digest = Digest.decode(buf)

if (digest.code !== IDENTITY_CODE) {
throw new Error('Supplied PeerID CID is invalid')
}

return new PeerIdWithIs(buf)
}
}

exports.createFromB58String = (str) => {
return exports.createFromCID(str) // B58String is CIDv0
return exports.createFromBytes(base58btc.decode('z' + str))
}

const validMulticodec = (cid) => {
// supported: 'libp2p-key' (CIDv1) and 'dag-pb' (CIDv0 converted to CIDv1)
return cid.codec === 'libp2p-key' || cid.codec === 'dag-pb'
return cid.code === LIBP2P_KEY_CODE || cid.code === DAG_PB_CODE
}

exports.createFromCID = (cid) => {
cid = CID.isCID(cid) ? cid : new CID(cid)
if (!validMulticodec(cid)) throw new Error('Supplied PeerID CID has invalid multicodec: ' + cid.codec)
return new PeerIdWithIs(cid.multihash)
cid = CID.asCID(cid)

if (!cid || !validMulticodec(cid)) {
throw new Error('Supplied PeerID CID is invalid')
}

return new PeerIdWithIs(cid.multihash.bytes)
}

// Public Key input will be a Uint8Array
Expand Down Expand Up @@ -288,7 +316,7 @@ exports.createFromPrivKey = async (key) => {
}

exports.createFromJSON = async (obj) => {
const id = mh.fromB58String(obj.id)
const id = base58btc.decode('z' + obj.id)
const rawPrivKey = obj.privKey && uint8ArrayFromString(obj.privKey, 'base64pad')
const rawPubKey = obj.pubKey && uint8ArrayFromString(obj.pubKey, 'base64pad')
const pub = rawPubKey && await cryptoKeys.unmarshalPublicKey(rawPubKey)
Expand Down Expand Up @@ -360,6 +388,16 @@ exports.createFromProtobuf = async (buf) => {
throw new Error('Protobuf did not contain any usable key material')
}

exports.parse = (str) => {
if (str.charAt(0) === '1') {
// base58btc encoded public key
return exports.createFromBytes(base58btc.decode(`z${str}`))
}

// try to parse it as a regular base58btc multihash or base32 encoded CID
return exports.createFromCID(CID.parse(str))
}

exports.isPeerId = (peerId) => {
return Boolean(typeof peerId === 'object' &&
peerId._id &&
Expand Down
80 changes: 51 additions & 29 deletions test/peer-id.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,26 @@
const { expect } = require('aegir/utils/chai')
const crypto = require('libp2p-crypto')
const mh = require('multihashes')
const CID = require('cids')
const { CID } = require('multiformats/cid')
const Digest = require('multiformats/hashes/digest')
const uint8ArrayFromString = require('uint8arrays/from-string')
const uint8ArrayToString = require('uint8arrays/to-string')

const PeerId = require('../src')

const util = require('util')

const DAG_PB_CODE = 0x70
const LIBP2P_KEY_CODE = 0x72
const RAW_CODE = 0x55

const testId = require('./fixtures/sample-id')
const testIdHex = testId.id
const testIdBytes = mh.fromHexString(testId.id)
const testIdDigest = Digest.decode(testIdBytes)
const testIdB58String = mh.toB58String(testIdBytes)
const testIdCID = new CID(1, 'libp2p-key', testIdBytes)
const testIdCIDString = testIdCID.toBaseEncodedString('base32')
const testIdCID = CID.createV1(LIBP2P_KEY_CODE, testIdDigest)
const testIdCIDString = testIdCID.toString()

const goId = require('./fixtures/go-private-key')

Expand Down Expand Up @@ -90,63 +96,55 @@ describe('PeerId', () => {
})

it('recreate from Base58 String (CIDv0))', () => {
const id = PeerId.createFromCID(testIdB58String)
const id = PeerId.createFromCID(CID.parse(testIdB58String))
expect(testIdCIDString).to.equal(id.toString())
expect(testIdBytes).to.deep.equal(id.toBytes())
})

it('recreate from CIDv1 Base32 (libp2p-key multicodec)', () => {
const cid = new CID(1, 'libp2p-key', testIdBytes)
const cidString = cid.toBaseEncodedString('base32')
const id = PeerId.createFromCID(cidString)
expect(cidString).to.equal(id.toString())
const cid = CID.createV1(LIBP2P_KEY_CODE, testIdDigest)
const id = PeerId.createFromCID(cid)
expect(cid.toString()).to.equal(id.toString())
expect(testIdBytes).to.deep.equal(id.toBytes())
})

it('recreate from CIDv1 Base32 (dag-pb multicodec)', () => {
const cid = new CID(1, 'dag-pb', testIdBytes)
const cidString = cid.toBaseEncodedString('base32')
const id = PeerId.createFromCID(cidString)
const cid = CID.createV1(DAG_PB_CODE, testIdDigest)
const id = PeerId.createFromCID(cid)
// toString should return CID with multicodec set to libp2p-key
expect(new CID(id.toString()).codec).to.equal('libp2p-key')
expect(CID.parse(id.toString()).code).to.equal(LIBP2P_KEY_CODE)
expect(testIdBytes).to.deep.equal(id.toBytes())
})

it('recreate from CID Uint8Array', () => {
const id = PeerId.createFromCID(testIdCID.bytes)
const id = PeerId.createFromBytes(testIdCID.bytes)
expect(testIdCIDString).to.equal(id.toString())
expect(testIdBytes).to.deep.equal(id.toBytes())
})

it('throws on invalid CID multicodec', () => {
// only libp2p and dag-pb are supported
const invalidCID = new CID(1, 'raw', testIdBytes).toBaseEncodedString('base32')
const invalidCID = CID.createV1(RAW_CODE, testIdDigest)
expect(() => {
PeerId.createFromCID(invalidCID)
}).to.throw(/Supplied PeerID CID has invalid multicodec: raw/)
}).to.throw(/invalid/i)
})

it('throws on invalid CID value', () => {
// using function code that does not represent valid hash function
it('throws on invalid multihash value', () => {
// using function code 0x50 that does not represent valid hash function
// https://github.com/multiformats/js-multihash/blob/b85999d5768bf06f1b0f16b926ef2cb6d9c14265/src/constants.js#L345
const invalidCID = 'QmaozNR7DZHQK1ZcU9p7QdrshMvXqWK6gpu5rmrkPdT3L'
const invalidMultihash = uint8ArrayToString(Uint8Array.from([0x50, 0x1, 0x0]), 'base58btc')
expect(() => {
PeerId.createFromCID(invalidCID)
}).to.throw(/multihash unknown function code: 0x50/)
PeerId.createFromB58String(invalidMultihash)
}).to.throw(/invalid/i)
})

it('throws on invalid CID object', () => {
const invalidCID = {}
expect(() => {
// @ts-expect-error invalid cid is invalid type
PeerId.createFromCID(invalidCID)
}).to.throw(/Invalid version, must be a number equal to 1 or 0/)
})

it('throws on invalid CID object', () => {
const invalidCID = {}
expect(() => {
PeerId.createFromCID(invalidCID)
}).to.throw(/Invalid version, must be a number equal to 1 or 0/)
}).to.throw(/invalid/i)
})

it('recreate from a Public Key', async () => {
Expand Down Expand Up @@ -174,6 +172,28 @@ describe('PeerId', () => {
expect(uint8ArrayToString(id.marshal(), 'base16')).to.deep.equal(testId.marshaled)
})

it('recreate from embedded ed25519 key', async () => {
const key = '12D3KooWRm8J3iL796zPFi2EtGGtUJn58AG67gcqzMFHZnnsTzqD'
const id = await PeerId.parse(key)
expect(id.toB58String()).to.equal(key)
const expB58 = mh.toB58String(mh.encode(id.pubKey.bytes, 'identity'))
expect(id.toB58String()).to.equal(expB58)
})

it('recreate from embedded secp256k1 key', async () => {
const key = '16Uiu2HAm5qw8UyXP2RLxQUx5KvtSN8DsTKz8quRGqGNC3SYiaB8E'
const id = await PeerId.parse(key)
expect(id.toB58String()).to.equal(key)
const expB58 = mh.toB58String(mh.encode(id.pubKey.bytes, 'identity'))
expect(id.toB58String()).to.equal(expB58)
})

it('recreate from string key', async () => {
const key = 'QmRsooYQasV5f5r834NSpdUtmejdQcpxXkK6qsozZWEihC'
const id = await PeerId.parse(key)
expect(id.toB58String()).to.equal(key)
})

it('can be created from a Secp256k1 public key', async () => {
const privKey = await crypto.keys.generateKeyPair('secp256k1', 256)
const id = await PeerId.createFromPubKey(privKey.public.bytes)
Expand Down Expand Up @@ -209,7 +229,8 @@ describe('PeerId', () => {

it('Pretty printing', async () => {
const id1 = await PeerId.create(testOpts)
const id2 = await PeerId.createFromPrivKey((id1.toJSON()).privKey)
const json = id1.toJSON()
const id2 = await PeerId.createFromPrivKey(json.privKey || 'invalid, should not happen')
expect(id1.toPrint()).to.be.eql(id2.toPrint())
expect(id1.toPrint()).to.equal('<peer.ID ' + id1.toB58String().substr(2, 6) + '>')
})
Expand Down Expand Up @@ -375,6 +396,7 @@ describe('PeerId', () => {
})

it('invalid id', () => {
// @ts-expect-error incorrect constructor arg type
expect(() => new PeerId('hello world')).to.throw(/invalid id/)
})
})
Expand Down