Skip to content
This repository has been archived by the owner on Dec 6, 2022. It is now read-only.

Commit

Permalink
feat: new IPLD Format API
Browse files Browse the repository at this point in the history
BREAKING CHANGE: The API is now async/await based

There are numerous changes, the most significant one is that the API
is no longer callback based, but it using async/await.

For the full new API please see the [IPLD Formats spec].

[IPLD Formats spec]: https://github.com/ipld/interface-ipld-format
  • Loading branch information
vmx committed May 8, 2019
1 parent 9dc7bb7 commit 1a799aa
Show file tree
Hide file tree
Showing 8 changed files with 272 additions and 450 deletions.
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 IpldBitcoin = require('ipld-bitcoin')

// `bitcoinBlock` is some binary Bitcoin block
IpldBitcoin.util.deserialize(bitcoinBlock, (err, dagNode) => {
if (err) {
throw err
}
console.log(dagNode)
})
const dagNode = IpldBitcoin.util.deserialize(bitcoinBlock)
console.log(dagNode)
```

## Contribute
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@
},
"homepage": "https://github.com/ipld/js-ipld-bitcoin#readme",
"dependencies": {
"async": "^2.6.1",
"bitcoinjs-lib": "^5.0.0",
"cids": "~0.6.0",
"multicodec": "~0.5.1",
"multihashes": "~0.4.14",
"multihashing-async": "~0.6.0"
"multihashing-async": "~0.7.0"
},
"devDependencies": {
"aegir": "^18.2.0",
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
155 changes: 43 additions & 112 deletions src/resolver.js
Original file line number Diff line number Diff line change
@@ -1,139 +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 Bitcoin block.
* Resolves a path within a Bitcoin 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 Bitcoin 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) {
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 Bitcoin 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',
'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 = (dagNode, field) => {
switch (field) {
case 'version':
return dagNode.version
case 'timestamp':
return dagNode.timestamp
case 'difficulty':
return dagNode.bits
case 'nonce':
return dagNode.nonce
case 'parent':
return { '/': util.hashToCid(dagNode.prevHash) }
case 'tx':
return { '/': util.hashToCid(dagNode.merkleRoot) }
default:
return null
}
}
exports.tree = function * (binaryBlob) {
const node = util.deserialize(binaryBlob)

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

const BitcoinjsBlock = require('bitcoinjs-lib').Block
const CID = require('cids')
const multicodec = require('multicodec')
const multihashes = require('multihashes')
const multihashing = require('multihashing-async')
const waterfall = require('async/waterfall')

const BITCOIN_BLOCK_HEADER_SIZE = 80
const CODEC = multicodec.BITCOIN_BLOCK
const DEFAULT_HASH_ALG = multicodec.DBL_SHA2_256

/**
* @callback SerializeCallback
* @param {?Error} error - Error if serialization failed
* @param {?Buffer} binaryBlob - Binary Bitcoin block if serialization was
* successful
*/
/**
* Serialize internal representation into a binary Bitcoin block.
*
* @param {BitcoinBlock} dagNode - Internal representation of a Bitcoin 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(true)
} catch (serializeError) {
err = serializeError
} finally {
callback(err, binaryBlob)
}
const serialize = (dagNode) => {
return dagNode.toBuffer(true)
}

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

const dagNode = BitcoinjsBlock.fromBuffer(binaryBlob)
callback(null, dagNode)
const deserialized = BitcoinjsBlock.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',
'transactions',
'witnessCommit'
]
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 {BitcoinBlock} dagNode - Internal representation of a Bitcoin 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(true), hashAlg, cb)
} catch (err) {
cb(err)
}
},
(mh, cb) => cb(null, new CID(version, 'bitcoin-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 Bitcoin 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, 'bitcoin-block', multihash)
return cid
Expand All @@ -108,6 +99,8 @@ const hashToCid = (hash) => {
module.exports = {
hashToCid: hashToCid,
BITCOIN_BLOCK_HEADER_SIZE: BITCOIN_BLOCK_HEADER_SIZE,
codec: CODEC,
defaultHashAlg: DEFAULT_HASH_ALG,

// Public API
cid: cid,
Expand Down
Loading

0 comments on commit 1a799aa

Please sign in to comment.