Skip to content
This repository was archived by the owner on Feb 12, 2024. It is now read-only.

Commit 1110e96

Browse files
vasco-santosalanshaw
authored andcommitted
feat: ipns locally (#1400)
A working version of **IPNS working locally** is here! πŸš€ 😎 πŸ’ͺ Steps: - [x] Create a new repo (**js-ipns**) like it was made with go in the last week ([go-ipns](https://github.com/ipfs/go-ipns)) and port the related code from this PR to there - [x] Resolve IPNS names in publish, in order to verify if they exist (as it is being done for regular files) before being published - [x] Handle remaining parameters in publish and resolve (except ttl). - [x] Test interface core spec [interface-ipfs-core#327](ipfs-inactive/interface-js-ipfs-core#327) - [x] Test interoperability with go. [interop#26](ipfs/interop#26) - [x] Integrate logging - [x] Write unit tests - [x] Add support for the lifetime with nanoseconds precision - [x] Add Cache - [x] Add initializeKeyspace - [x] Republish Some notes, regarding the previous steps: - There is an optional parameter not implemented in this PR, which is `ttl`, since it is still experimental, we can add it in a separate PR. Finally, thanks @Stebalien for your help and time answering all my questions regarding the IPNS implementation in GO. Moreover, since there are no specs, and not that much documentation, I have been writing a document with all the IPNS workflow. It is a WIP by now, but it is currently available [here](ipfs/specs#184). Related PRs: - [x] [js-ipns#4](ipfs/js-ipns#4) - [x] [js-ipfs-repo#173](ipfs/js-ipfs-repo#173) - [x] [js-ipfs#1496](#1496) - [x] [interface-ipfs-core#327](ipfs-inactive/interface-js-ipfs-core#327) - [x] enable `interface-ipfs-core` tests for IPNS in `js-ipfs`
1 parent 61b91f8 commit 1110e96

31 files changed

+1653
-9
lines changed

β€ŽREADME.md

+9
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ You can check the development status at the [Waffle Board](https://waffle.io/ipf
7070
- [Core API](#core-api)
7171
- [Files](#files)
7272
- [Graph](#graph)
73+
- [Name](#name)
7374
- [Crypto and Key Management](#crypto-and-key-management)
7475
- [Network](#network)
7576
- [Node Management](#node-management)
@@ -545,6 +546,12 @@ The core API is grouped into several areas:
545546
- [`ipfs.pin.ls([hash], [options], [callback])`](https://github.com/ipfs/interface-ipfs-core/blob/master/SPEC/PIN.md#pinls)
546547
- [`ipfs.pin.rm(hash, [options], [callback])`](https://github.com/ipfs/interface-ipfs-core/blob/master/SPEC/PIN.md#pinrm)
547548

549+
### Name
550+
551+
- [name](https://github.com/ipfs/interface-ipfs-core/blob/master/SPEC/NAME.md)
552+
- [`ipfs.name.publish(value, [options, callback])`](https://github.com/ipfs/interface-ipfs-core/blob/master/SPEC/NAME.md#namepublish)
553+
- [`ipfs.name.resolve(value, [options, callback])`](https://github.com/ipfs/interface-ipfs-core/blob/master/SPEC/NAME.md#nameresolve)
554+
548555
#### Crypto and Key Management
549556

550557
- [key](https://github.com/ipfs/interface-ipfs-core/tree/master/SPEC/KEY.md)
@@ -837,6 +844,8 @@ Listing of the main packages used in the IPFS ecosystem. There are also three sp
837844
| [`ipld`](//github.com/ipld/js-ipld) | [![npm](https://img.shields.io/npm/v/ipld.svg?maxAge=86400&style=flat)](//github.com/ipld/js-ipld/releases) | [![Dep](https://david-dm.org/ipld/js-ipld.svg?style=flat)](https://david-dm.org/ipld/js-ipld) | [![Build Status](https://ci.ipfs.team/buildStatus/icon?job=ipld/js-ipld/master)](https://ci.ipfs.team/job/ipld/job/js-ipld/job/master/) | [![Coverage Status](https://codecov.io/gh/ipld/js-ipld/branch/master/graph/badge.svg)](https://codecov.io/gh/ipld/js-ipld) |
838845
| [`ipld-dag-pb`](//github.com/ipld/js-ipld-dag-pb) | [![npm](https://img.shields.io/npm/v/ipld-dag-pb.svg?maxAge=86400&style=flat)](//github.com/ipld/js-ipld-dag-pb/releases) | [![Dep](https://david-dm.org/ipld/js-ipld-dag-pb.svg?style=flat)](https://david-dm.org/ipld/js-ipld-dag-pb) | [![Build Status](https://ci.ipfs.team/buildStatus/icon?job=ipld/js-ipld-dag-pb/master)](https://ci.ipfs.team/job/ipld/job/js-ipld-dag-pb/job/master/) | [![Coverage Status](https://codecov.io/gh/ipld/js-ipld-dag-pb/branch/master/graph/badge.svg)](https://codecov.io/gh/ipld/js-ipld-dag-pb) |
839846
| [`ipld-dag-cbor`](//github.com/ipld/js-ipld-dag-cbor) | [![npm](https://img.shields.io/npm/v/ipld-dag-cbor.svg?maxAge=86400&style=flat)](//github.com/ipld/js-ipld-dag-cbor/releases) | [![Dep](https://david-dm.org/ipld/js-ipld-dag-cbor.svg?style=flat)](https://david-dm.org/ipld/js-ipld-dag-cbor) | [![Build Status](https://ci.ipfs.team/buildStatus/icon?job=ipld/js-ipld-dag-cbor/master)](https://ci.ipfs.team/job/ipld/job/js-ipld-dag-cbor/job/master/) | [![Coverage Status](https://codecov.io/gh/ipld/js-ipld-dag-cbor/branch/master/graph/badge.svg)](https://codecov.io/gh/ipld/js-ipld-dag-cbor) |
847+
| **Name** |
848+
| [`ipns`](//github.com/ipfs/js-ipns) | [![npm](https://img.shields.io/npm/v/ipns.svg?maxAge=86400&style=flat)](//github.com/ipfs/js-ipns/releases) | [![Dep](https://david-dm.org/ipfs/js-ipns.svg?style=flat-square)](https://david-dm.org/ipfs/js-ipns) | [![Build Status](https://ci.ipfs.team/buildStatus/icon?job=ipfs/js-ipns/master)](https://ci.ipfs.team/job/ipfs/job/js-ipns/job/master/) | [![Coverage Status](https://codecov.io/gh/ipfs/js-ipns/branch/master/graph/badge.svg)](https://codecov.io/gh/ipfs/js-ipns) |
840849
| **Repo** |
841850
| [`ipfs-repo`](//github.com/ipfs/js-ipfs-repo) | [![npm](https://img.shields.io/npm/v/ipfs-repo.svg?maxAge=86400&style=flat)](//github.com/ipfs/js-ipfs-repo/releases) | [![Dep](https://david-dm.org/ipfs/js-ipfs-repo.svg?style=flat)](https://david-dm.org/ipfs/js-ipfs-repo) | [![Build Status](https://ci.ipfs.team/buildStatus/icon?job=ipfs/js-ipfs-repo/master)](https://ci.ipfs.team/job/ipfs/job/js-ipfs-repo/job/master/) | [![Coverage Status](https://codecov.io/gh/ipfs/js-ipfs-repo/branch/master/graph/badge.svg)](https://codecov.io/gh/ipfs/js-ipfs-repo) |
842851
| **Exchange** |

β€Žpackage.json

+3
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@
118118
"ipld": "~0.17.3",
119119
"ipld-dag-cbor": "~0.12.1",
120120
"ipld-dag-pb": "~0.14.6",
121+
"ipns": "~0.1.3",
121122
"is-ipfs": "~0.4.2",
122123
"is-pull-stream": "~0.0.0",
123124
"is-stream": "^1.1.0",
@@ -132,6 +133,7 @@
132133
"libp2p-keychain": "~0.3.1",
133134
"libp2p-mdns": "~0.12.0",
134135
"libp2p-mplex": "~0.8.0",
136+
"libp2p-record": "~0.5.1",
135137
"libp2p-secio": "~0.10.0",
136138
"libp2p-tcp": "~0.12.0",
137139
"libp2p-webrtc-star": "~0.15.3",
@@ -164,6 +166,7 @@
164166
"pull-zip": "^2.0.1",
165167
"read-pkg-up": "^4.0.0",
166168
"readable-stream": "2.3.6",
169+
"receptacle": "^1.3.2",
167170
"stream-to-pull-stream": "^1.7.2",
168171
"tar-stream": "^1.6.1",
169172
"temp": "~0.8.3",

β€Žsrc/cli/bin.js

+5
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ const cli = yargs
3131
type: 'string',
3232
default: ''
3333
})
34+
.option('local', {
35+
desc: 'Run the command locally, instead of using the daemon',
36+
type: 'boolean',
37+
default: false
38+
})
3439
.epilog(utils.ipfsPathHelp)
3540
.demandCommand(1)
3641
.fail((msg, err, yargs) => {

β€Žsrc/cli/commands/name.js

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
'use strict'
2+
3+
/*
4+
IPNS is a PKI namespace, where names are the hashes of public keys, and
5+
the private key enables publishing new (signed) values. In both publish
6+
and resolve, the default name used is the node's own PeerID,
7+
which is the hash of its public key.
8+
*/
9+
module.exports = {
10+
command: 'name <command>',
11+
12+
description: 'Publish and resolve IPNS names.',
13+
14+
builder (yargs) {
15+
return yargs.commandDir('name')
16+
},
17+
18+
handler (argv) {
19+
}
20+
}

β€Žsrc/cli/commands/name/publish.js

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
'use strict'
2+
3+
const print = require('../../utils').print
4+
5+
module.exports = {
6+
command: 'publish <ipfsPath>',
7+
8+
describe: 'Publish IPNS names.',
9+
10+
builder: {
11+
resolve: {
12+
describe: 'Resolve given path before publishing. Default: true.',
13+
default: true
14+
},
15+
lifetime: {
16+
alias: 't',
17+
describe: 'Time duration that the record will be valid for. Default: 24h.',
18+
default: '24h'
19+
},
20+
key: {
21+
alias: 'k',
22+
describe: 'Name of the key to be used or a valid PeerID, as listed by "ipfs key list -l". Default: self.',
23+
default: 'self'
24+
},
25+
ttl: {
26+
describe: 'Time duration this record should be cached for (caution: experimental).',
27+
default: ''
28+
}
29+
},
30+
31+
handler (argv) {
32+
const opts = {
33+
resolve: argv.resolve,
34+
lifetime: argv.lifetime,
35+
key: argv.key,
36+
ttl: argv.ttl
37+
}
38+
39+
argv.ipfs.name.publish(argv.ipfsPath, opts, (err, result) => {
40+
if (err) {
41+
throw err
42+
}
43+
44+
print(`Published to ${result.name}: ${result.value}`)
45+
})
46+
}
47+
}

β€Žsrc/cli/commands/name/resolve.js

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
'use strict'
2+
3+
const print = require('../../utils').print
4+
5+
module.exports = {
6+
command: 'resolve [<name>]',
7+
8+
describe: 'Resolve IPNS names.',
9+
10+
builder: {
11+
nocache: {
12+
alias: 'n',
13+
describe: 'Do not use cached entries. Default: false.',
14+
default: false
15+
},
16+
recursive: {
17+
alias: 'r',
18+
recursive: 'Resolve until the result is not an IPNS name. Default: false.',
19+
default: false
20+
}
21+
},
22+
23+
handler (argv) {
24+
const opts = {
25+
nocache: argv.nocache,
26+
recursive: argv.recursive
27+
}
28+
29+
argv.ipfs.name.resolve(argv.name, opts, (err, result) => {
30+
if (err) {
31+
throw err
32+
}
33+
34+
if (result && result.path) {
35+
print(result.path)
36+
} else {
37+
print(result)
38+
}
39+
})
40+
}
41+
}

β€Žsrc/core/components/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,4 @@ exports.key = require('./key')
2828
exports.stats = require('./stats')
2929
exports.mfs = require('./mfs')
3030
exports.resolve = require('./resolve')
31+
exports.name = require('./name')

β€Žsrc/core/components/init.js

+12-6
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ module.exports = function init (self) {
5252
opts.log = opts.log || function () {}
5353
const config = defaultConfig()
5454
let privateKey
55+
5556
waterfall([
5657
// Verify repo does not yet exist.
5758
(cb) => self._repo.exists(cb),
@@ -75,14 +76,14 @@ module.exports = function init (self) {
7576
peerId.create({ bits: opts.bits }, cb)
7677
}
7778
},
78-
(keys, cb) => {
79+
(peerId, cb) => {
7980
self.log('identity generated')
8081
config.Identity = {
81-
PeerID: keys.toB58String(),
82-
PrivKey: keys.privKey.bytes.toString('base64')
82+
PeerID: peerId.toB58String(),
83+
PrivKey: peerId.privKey.bytes.toString('base64')
8384
}
85+
privateKey = peerId.privKey
8486
if (opts.pass) {
85-
privateKey = keys.privKey
8687
config.Keychain = Keychain.generateOptions()
8788
}
8889
opts.log('done')
@@ -102,14 +103,19 @@ module.exports = function init (self) {
102103
cb(null, true)
103104
}
104105
},
106+
// add empty unixfs dir object (go-ipfs assumes this exists)
105107
(_, cb) => {
106108
if (opts.emptyRepo) {
107109
return cb(null, true)
108110
}
109111

110112
const tasks = [
111-
// add empty unixfs dir object (go-ipfs assumes this exists)
112-
(cb) => self.object.new('unixfs-dir', cb)
113+
(cb) => {
114+
waterfall([
115+
(cb) => self.object.new('unixfs-dir', cb),
116+
(emptyDirNode, cb) => self._ipns.initializeKeyspace(privateKey, emptyDirNode.toJSON().multihash, cb)
117+
], cb)
118+
}
113119
]
114120

115121
if (typeof addDefaultAssets === 'function') {

β€Žsrc/core/components/name.js

+167
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
'use strict'
2+
3+
const debug = require('debug')
4+
const promisify = require('promisify-es6')
5+
const waterfall = require('async/waterfall')
6+
const parallel = require('async/parallel')
7+
const human = require('human-to-milliseconds')
8+
const crypto = require('libp2p-crypto')
9+
const errcode = require('err-code')
10+
11+
const log = debug('jsipfs:name')
12+
log.error = debug('jsipfs:name:error')
13+
14+
const utils = require('../utils')
15+
const path = require('../ipns/path')
16+
17+
const keyLookup = (ipfsNode, kname, callback) => {
18+
if (kname === 'self') {
19+
return callback(null, ipfsNode._peerInfo.id.privKey)
20+
}
21+
22+
const pass = ipfsNode._options.pass
23+
24+
waterfall([
25+
(cb) => ipfsNode._keychain.exportKey(kname, pass, cb),
26+
(pem, cb) => crypto.keys.import(pem, pass, cb)
27+
], (err, privateKey) => {
28+
if (err) {
29+
log.error(err)
30+
return callback(errcode(err, 'ERR_CANNOT_GET_KEY'))
31+
}
32+
33+
return callback(null, privateKey)
34+
})
35+
}
36+
37+
module.exports = function name (self) {
38+
return {
39+
/**
40+
* IPNS is a PKI namespace, where names are the hashes of public keys, and
41+
* the private key enables publishing new (signed) values. In both publish
42+
* and resolve, the default name used is the node's own PeerID,
43+
* which is the hash of its public key.
44+
*
45+
* @param {String} value ipfs path of the object to be published.
46+
* @param {Object} options ipfs publish options.
47+
* @param {boolean} options.resolve resolve given path before publishing.
48+
* @param {String} options.lifetime time duration that the record will be valid for.
49+
This accepts durations such as "300s", "1.5h" or "2h45m". Valid time units are
50+
"ns", "ms", "s", "m", "h". Default is 24h.
51+
* @param {String} options.ttl time duration this record should be cached for (NOT IMPLEMENTED YET).
52+
* This accepts durations such as "300s", "1.5h" or "2h45m". Valid time units are
53+
"ns", "ms", "s", "m", "h" (caution: experimental).
54+
* @param {String} options.key name of the key to be used or a valid PeerID, as listed by 'ipfs key list -l'.
55+
* @param {function(Error)} [callback]
56+
* @returns {Promise|void}
57+
*/
58+
publish: promisify((value, options, callback) => {
59+
if (typeof options === 'function') {
60+
callback = options
61+
options = {}
62+
}
63+
64+
options = options || {}
65+
const resolve = !(options.resolve === false)
66+
const lifetime = options.lifetime || '24h'
67+
const key = options.key || 'self'
68+
69+
if (!self.isOnline()) {
70+
const errMsg = utils.OFFLINE_ERROR
71+
72+
log.error(errMsg)
73+
return callback(errcode(errMsg, 'OFFLINE_ERROR'))
74+
}
75+
76+
// TODO: params related logic should be in the core implementation
77+
78+
// Normalize path value
79+
try {
80+
value = utils.normalizePath(value)
81+
} catch (err) {
82+
log.error(err)
83+
return callback(err)
84+
}
85+
86+
parallel([
87+
(cb) => human(lifetime, cb),
88+
// (cb) => ttl ? human(ttl, cb) : cb(),
89+
(cb) => keyLookup(self, key, cb),
90+
// verify if the path exists, if not, an error will stop the execution
91+
(cb) => resolve.toString() === 'true' ? path.resolvePath(self, value, cb) : cb()
92+
], (err, results) => {
93+
if (err) {
94+
log.error(err)
95+
return callback(err)
96+
}
97+
98+
// Calculate lifetime with nanoseconds precision
99+
const pubLifetime = results[0].toFixed(6)
100+
const privateKey = results[1]
101+
102+
// TODO IMPROVEMENT - Handle ttl for cache
103+
// const ttl = results[1]
104+
// const privateKey = results[2]
105+
106+
// Start publishing process
107+
self._ipns.publish(privateKey, value, pubLifetime, callback)
108+
})
109+
}),
110+
111+
/**
112+
* Given a key, query the DHT for its best value.
113+
*
114+
* @param {String} name ipns name to resolve. Defaults to your node's peerID.
115+
* @param {Object} options ipfs resolve options.
116+
* @param {boolean} options.nocache do not use cached entries.
117+
* @param {boolean} options.recursive resolve until the result is not an IPNS name.
118+
* @param {function(Error)} [callback]
119+
* @returns {Promise|void}
120+
*/
121+
resolve: promisify((name, options, callback) => {
122+
if (typeof options === 'function') {
123+
callback = options
124+
options = {}
125+
}
126+
127+
options = options || {}
128+
const nocache = options.nocache && options.nocache.toString() === 'true'
129+
const recursive = options.recursive && options.recursive.toString() === 'true'
130+
131+
const local = true // TODO ROUTING - use self._options.local
132+
133+
if (!self.isOnline() && !local) {
134+
const errMsg = utils.OFFLINE_ERROR
135+
136+
log.error(errMsg)
137+
return callback(errcode(errMsg, 'OFFLINE_ERROR'))
138+
}
139+
140+
// TODO: params related logic should be in the core implementation
141+
142+
if (local && nocache) {
143+
const error = 'cannot specify both local and nocache'
144+
145+
log.error(error)
146+
return callback(errcode(new Error(error), 'ERR_NOCACHE_AND_LOCAL'))
147+
}
148+
149+
// Set node id as name for being resolved, if it is not received
150+
if (!name) {
151+
name = self._peerInfo.id.toB58String()
152+
}
153+
154+
if (!name.startsWith('/ipns/')) {
155+
name = `/ipns/${name}`
156+
}
157+
158+
const resolveOptions = {
159+
nocache,
160+
recursive,
161+
local
162+
}
163+
164+
self._ipns.resolve(name, self._peerInfo.id, resolveOptions, callback)
165+
})
166+
}
167+
}

0 commit comments

Comments
Β (0)