Skip to content
This repository has been archived by the owner on Aug 11, 2021. It is now read-only.

feat: new IPLD Format API #39

Merged
merged 1 commit into from
May 8, 2019
Merged
Show file tree
Hide file tree
Changes from all 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
8 changes: 2 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,8 @@ Though it can also be used as a standalone module:
const IpldZcash = require('ipld-zcash')

// `zcashBlock` is some binary Zcash block
IpldZcash.util.deserialize(zcashBlock, (err, dagNode) => {
if (err) {
throw err
}
console.log(dagNode)
})
const dagNode = IpldZcash.util.deserialize(zcashBlock)
console.log(dagNode)
```

## Contribute
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,16 @@
},
"homepage": "https://github.com/ipld/js-ipld-zcash#readme",
"dependencies": {
"async": "^2.6.1",
"cids": "~0.6.0",
"multicodec": "~0.5.1",
"multihashes": "~0.4.12",
"multihashing-async": "~0.6.0",
"multihashing-async": "~0.7.0",
"zcash-bitcore-lib": "~0.13.20-rc3"
},
"devDependencies": {
"aegir": "^18.2.0",
"chai": "^4.1.2",
"chai-as-promised": "^7.1.1",
"dirty-chai": "^2.0.1"
},
"contributors": [
Expand Down
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@

exports.resolver = require('./resolver.js')
exports.util = require('./util.js')
exports.codec = exports.util.codec
exports.defaultHashAlg = exports.util.defaultHashAlg
159 changes: 43 additions & 116 deletions src/resolver.js
Original file line number Diff line number Diff line change
@@ -1,143 +1,70 @@
'use strict'

const CID = require('cids')

const util = require('./util')

/**
* @callback ResolveCallback
* @param {?Error} error - Error if path can't be resolved
* @param {Object} result - Result of the path it it was resolved successfully
* @param {*} result.value - Value the path resolves to
* @param {string} result.remainderPath - If the path resolves half-way to a
* link, then the `remainderPath` is the part after the link that can be used
* for further resolving.
*/
/**
* Resolves a path in a Zcash block.
* Resolves a path within a Zcash block.
*
* Returns the value or a link and the partial mising path. This way the
* IPLD Resolver can fetch the link and continue to resolve.
*
* @param {Buffer} binaryBlob - Binary representation of a Zcash block
* @param {string} [path='/'] - Path that should be resolved
* @param {ResolveCallback} callback - Callback that handles the return value
* @returns {void}
* @returns {Object} result - Result of the path it it was resolved successfully
* @returns {*} result.value - Value the path resolves to
* @returns {string} result.remainderPath - If the path resolves half-way to a
* link, then the `remainderPath` is the part after the link that can be used
* for further resolving
*/
const resolve = (binaryBlob, path, callback) => {
if (typeof path === 'function') {
callback = path
path = undefined
}
exports.resolve = (binaryBlob, path) => {
let node = util.deserialize(binaryBlob)

util.deserialize(binaryBlob, (err, dagNode) => {
if (err) {
return callback(err)
const parts = path.split('/').filter(Boolean)
while (parts.length) {
const key = parts.shift()
if (node[key] === undefined) {
throw new Error(`Object has no property '${key}'`)
}

// Return the deserialized block if no path is given
if (!path) {
return callback(null, {
value: dagNode,
remainderPath: ''
})
node = node[key]
if (CID.isCID(node)) {
return {
value: node,
remainderPath: parts.join('/')
}
}
}

const pathArray = path.split('/')
const value = resolveField(dagNode, pathArray[0])
if (value === null) {
return callback(new Error('No such path'), null)
}
return {
value: node,
remainderPath: ''
}
}

let remainderPath = pathArray.slice(1).join('/')
// It is a link, hence it may have a remainder
if (value['/'] !== undefined) {
return callback(null, {
value: value,
remainderPath: remainderPath
})
} else {
if (remainderPath.length > 0) {
return callback(new Error('No such path'), null)
} else {
return callback(null, {
value: value,
remainderPath: ''
})
}
}
})
const traverse = function * (node, path) {
// Traverse only objects and arrays
if (Buffer.isBuffer(node) || CID.isCID(node) || typeof node === 'string' ||
node === null) {
Copy link
Member

Choose a reason for hiding this comment

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

==

return
}
for (const item of Object.keys(node)) {
const nextpath = path === undefined ? item : path + '/' + item
yield nextpath
yield * traverse(node[item], nextpath)
}
}

/**
* @callback TreeCallback
* @param {?Error} error - Error if paths can't be retreived
* @param {string[] | Object.<string, *>[]} result - The result depends on
* `options.values`, whether it returns only the paths, or the paths with
* the corresponding values
*/
/**
* Return all available paths of a block.
*
* @generator
* @param {Buffer} binaryBlob - Binary representation of a Zcash block
* @param {Object} [options] - Possible options
* @param {boolean} [options.values=false] - Retun only the paths by default.
* If it is `true` also return the values
* @param {TreeCallback} callback - Callback that handles the return value
* @returns {void}
* @yields {string} - A single path
*/
const tree = (binaryBlob, options, callback) => {
if (typeof options === 'function') {
callback = options
options = undefined
}
options = options || {}

util.deserialize(binaryBlob, (err, dagNode) => {
if (err) {
return callback(err)
}

const paths = ['version', 'timestamp', 'difficulty', 'nonce',
'solution', 'reserved', 'parent', 'tx']

if (options.values === true) {
const pathValues = {}
for (let path of paths) {
pathValues[path] = resolveField(dagNode, path)
}
return callback(null, pathValues)
} else {
return callback(null, paths)
}
})
}

// Return top-level fields. Returns `null` if field doesn't exist
const resolveField = (header, field) => {
switch (field) {
case 'version':
return header.version
case 'timestamp':
return header.time
case 'difficulty':
return header.bits
case 'nonce':
return header.nonce
case 'solution':
return header.solution
case 'reserved':
return header.reserved
case 'parent':
return { '/': util.hashToCid(header.prevHash) }
case 'tx':
return { '/': util.hashToCid(header.merkleRoot) }
default:
return null
}
}
exports.tree = function * (binaryBlob) {
const node = util.deserialize(binaryBlob)

module.exports = {
multicodec: 'zcash-block',
defaultHashAlg: 'dbl-sha2-256',
resolve: resolve,
tree: tree
yield * traverse(node)
}
130 changes: 61 additions & 69 deletions src/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,104 +2,94 @@

const ZcashBitcoreBlockHeader = require('zcash-bitcore-lib').BlockHeader
const CID = require('cids')
const multicodec = require('multicodec')
const multihashes = require('multihashes')
const multihashing = require('multihashing-async')
const waterfall = require('async/waterfall')

const ZCASH_BLOCK_HEADER_SIZE = 1487
const CODEC = multicodec.ZCASH_BLOCK
const DEFAULT_HASH_ALG = multicodec.DBL_SHA2_256

/**
* @callback SerializeCallback
* @param {?Error} error - Error if serialization failed
* @param {?Buffer} binaryBlob - Binary Zcash block if serialization was
* successful
*/
/**
* Serialize internal representation into a binary Zcash block.
*
* @param {ZcashBlock} dagNode - Internal representation of a Zcash block
* @param {SerializeCallback} callback - Callback that handles the
* return value
* @returns {void}
* @returns {Buffer}
*/
const serialize = (dagNode, callback) => {
let err = null
let binaryBlob
try {
binaryBlob = dagNode.toBuffer()
} catch (serializeError) {
err = serializeError
} finally {
callback(err, binaryBlob)
}
const serialize = (dagNode) => {
return dagNode.toBuffer()
}

/**
* @callback DeserializeCallback
* @param {?Error} error - Error if deserialization failed
* @param {?ZcashBlock} dagNode - Internal representation of a Zcash block
* if deserialization was successful
*/
/**
* Deserialize Zcash block into the internal representation,
* Deserialize Zcash block into the internal representation.
*
* @param {Buffer} binaryBlob - Binary representation of a Zcash block
* @param {DeserializeCallback} callback - Callback that handles the
* return value
* @returns {void}
* @returns {ZcashBlock}
*/
const deserialize = (binaryBlob, callback) => {
const deserialize = (binaryBlob) => {
if (binaryBlob.length !== ZCASH_BLOCK_HEADER_SIZE) {
const err = new Error(
throw new Error(
`Zcash block header needs to be ${ZCASH_BLOCK_HEADER_SIZE} bytes`)
return callback(err)
}

const dagNode = ZcashBitcoreBlockHeader.fromBuffer(binaryBlob)
callback(null, dagNode)
const deserialized = ZcashBitcoreBlockHeader.fromBuffer(binaryBlob)

const getters = {
difficulty: function () {
return this.bits
},
parent: function () {
return hashToCid(this.prevHash)
},
tx: function () {
return hashToCid(this.merkleRoot)
}
}
Object.entries(getters).forEach(([name, fun]) => {
Object.defineProperty(deserialized, name, {
enumerable: true,
get: fun
})
})

const removeEnumberables = [
'bits',
'merkleRoot',
'prevHash',
'time'
]
removeEnumberables.forEach((field) => {
if (field in deserialized) {
Object.defineProperty(deserialized, field, { enumerable: false })
}
})

return deserialized
}

/**
* @callback CidCallback
* @param {?Error} error - Error if getting the CID failed
* @param {?CID} cid - CID if call was successful
*/
/**
* Get the CID of the DAG-Node.
* Calculate the CID of the binary blob.
*
* @param {ZcashBlock} dagNode - Internal representation of a Zcash block
* @param {Object} [options] - Options to create the CID
* @param {number} [options.version=1] - CID version number
* @param {string} [options.hashAlg='dbl-sha2-256'] - Hashing algorithm
* @param {CidCallback} callback - Callback that handles the return value
* @returns {void}
* @param {Object} binaryBlob - Encoded IPLD Node
* @param {Object} [userOptions] - Options to create the CID
* @param {number} [userOptions.cidVersion=1] - CID version number
* @param {string} [UserOptions.hashAlg] - Defaults to the defaultHashAlg of the format
* @returns {Promise.<CID>}
*/
const cid = (dagNode, options, callback) => {
if (typeof options === 'function') {
callback = options
options = {}
}
options = options || {}
// avoid deadly embrace between resolver and util
const hashAlg = options.hashAlg || require('./resolver').defaultHashAlg
const version = typeof options.version === 'undefined' ? 1 : options.version
waterfall([
(cb) => {
try {
multihashing(dagNode.toBuffer(), hashAlg, cb)
} catch (err) {
cb(err)
}
},
(mh, cb) => cb(null, new CID(version, 'zcash-block', mh))
], callback)
const cid = async (binaryBlob, userOptions) => {
const defaultOptions = { cidVersion: 1, hashAlg: DEFAULT_HASH_ALG }
const options = Object.assign(defaultOptions, userOptions)

const multihash = await multihashing(binaryBlob, options.hashAlg)
const codecName = multicodec.print[CODEC]
const cid = new CID(options.cidVersion, codecName, multihash)

return cid
}

// Convert a Zcash hash (as Buffer) to a CID
const hashToCid = (hash) => {
// avoid deadly embrace between resolver and util
const defaultHashAlg = require('./resolver').defaultHashAlg
const multihash = multihashes.encode(hash, defaultHashAlg)
const multihash = multihashes.encode(hash, DEFAULT_HASH_ALG)
const cidVersion = 1
const cid = new CID(cidVersion, 'zcash-block', multihash)
return cid
Expand All @@ -108,6 +98,8 @@ const hashToCid = (hash) => {
module.exports = {
hashToCid: hashToCid,
ZCASH_BLOCK_HEADER_SIZE: ZCASH_BLOCK_HEADER_SIZE,
codec: CODEC,
defaultHashAlg: DEFAULT_HASH_ALG,

// Public API
cid: cid,
Expand Down
Loading