Skip to content

Commit

Permalink
feat: custom hasher for chunks (#21)
Browse files Browse the repository at this point in the history
* fix: types path

* chore: produce same filenames between builds

* refactor: export Message type

* feat: hashFn for chunk

* fix: pass options to bmtRootHash on address calculation

* refactor: simplify code

* build: use sourcemap that works

* test: bit shifting does not work
  • Loading branch information
nugaon committed Jul 5, 2023
1 parent 10687d5 commit 2003efe
Show file tree
Hide file tree
Showing 5 changed files with 49 additions and 29 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
"name": "@fairdatasociety/bmt-js",
"version": "2.0.1",
"description": "Binary Merkle Tree operations on data",
"main": "dist/index.min.js",
"types": "dist/index.d.ts",
"main": "dist/index.js",
"types": "dist/src/index.d.ts",
"repository": {
"type": "git",
"url": "https://github.com/fairDataSociety/bmt-js.git"
Expand Down
60 changes: 40 additions & 20 deletions src/chunk.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { DEFAULT_SPAN_SIZE, makeSpan, Span } from './span'
import { assertFlexBytes, Bytes, keccak256Hash, Flavor, FlexBytes, serializeBytes } from './utils'
import { assertFlexBytes, Bytes, keccak256Hash, Flavor, FlexBytes, serializeBytes, Message } from './utils'

export const SEGMENT_SIZE = 32
const SEGMENT_PAIR_SIZE = 2 * SEGMENT_SIZE
export const DEFAULT_MAX_PAYLOAD_SIZE = 4096 as const
const HASH_SIZE = 32
export const DEFAULT_MIN_PAYLOAD_SIZE = 1 as const
export type ChunkAddress = Bytes<32>
export type ChunkAddress = Uint8Array
type ValidChunkData = Uint8Array & Flavor<'ValidChunkData'>
/** Available options at each Chunk function */
type Options = {
hashFn?: (...messages: Message[]) => Uint8Array
}

export interface Chunk<
MaxPayloadLength extends number = typeof DEFAULT_MAX_PAYLOAD_SIZE,
Expand Down Expand Up @@ -37,21 +41,22 @@ export function makeChunk<
maxPayloadSize?: MaxPayloadSize
spanLength?: SpanLength
startingSpanValue?: number
},
} & Options,
): Chunk<MaxPayloadSize, SpanLength> {
// assertion for the sizes are required because
// typescript does not recognise subset relation on union type definition
const maxPayloadLength = (options?.maxPayloadSize || DEFAULT_MAX_PAYLOAD_SIZE) as MaxPayloadSize
const spanLength = (options?.spanLength || DEFAULT_SPAN_SIZE) as SpanLength
const spanValue = options?.startingSpanValue || payloadBytes.length
const hashFn = options?.hashFn ? options.hashFn : keccak256Hash

assertFlexBytes(payloadBytes, 0, maxPayloadLength)
const paddingChunkLength = new Uint8Array(maxPayloadLength - payloadBytes.length)
const span = () => makeSpan(spanValue, spanLength)
const data = () => serializeBytes(payloadBytes, new Uint8Array(paddingChunkLength)) as ValidChunkData
const inclusionProof = (segmentIndex: number) => inclusionProofBottomUp(data(), segmentIndex)
const address = () => chunkAddress(payloadBytes, spanLength, span())
const bmtFn = () => bmt(data(), maxPayloadLength)
const inclusionProof = (segmentIndex: number) => inclusionProofBottomUp(data(), segmentIndex, { hashFn })
const address = () => chunkAddress(payloadBytes, spanLength, span(), { hashFn })
const bmtFn = () => bmt(data(), { hashFn })

return {
payload: payloadBytes,
Expand All @@ -68,10 +73,12 @@ export function makeChunk<
export function bmtRootHash(
payload: Uint8Array,
maxPayloadLength: number = DEFAULT_MAX_PAYLOAD_SIZE, // default 4096
options?: Options,
): Uint8Array {
if (payload.length > maxPayloadLength) {
throw new Error(`invalid data length ${payload}`)
}
const hashFn = options?.hashFn ? options.hashFn : keccak256Hash

// create an input buffer padded with zeros
let input = new Uint8Array([...payload, ...new Uint8Array(maxPayloadLength - payload.length)])
Expand All @@ -80,7 +87,7 @@ export function bmtRootHash(

// in each round we hash the segment pairs together
for (let offset = 0; offset < input.length; offset += SEGMENT_PAIR_SIZE) {
const hashNumbers = keccak256Hash(input.slice(offset, offset + SEGMENT_PAIR_SIZE))
const hashNumbers = hashFn(input.slice(offset, offset + SEGMENT_PAIR_SIZE))
output.set(hashNumbers, offset / 2)
}

Expand All @@ -95,10 +102,15 @@ export function bmtRootHash(
*
* @param payloadBytes chunk data initialised in Uint8Array object
* @param segmentIndex segment index in the data array that has to be proofed for inclusion
* @param options function configuraiton
* @returns Required segments for inclusion proof starting from the data level
* until the BMT root hash of the payload
*/
export function inclusionProofBottomUp(payloadBytes: Uint8Array, segmentIndex: number): Uint8Array[] {
export function inclusionProofBottomUp(
payloadBytes: Uint8Array,
segmentIndex: number,
options?: Options,
): Uint8Array[] {
if (segmentIndex * SEGMENT_SIZE >= payloadBytes.length) {
throw new Error(
`The given segment index ${segmentIndex} is greater than ${Math.floor(
Expand All @@ -107,7 +119,7 @@ export function inclusionProofBottomUp(payloadBytes: Uint8Array, segmentIndex: n
)
}

const tree = bmt(payloadBytes)
const tree = bmt(payloadBytes, options)
const sisterSegments: Array<Uint8Array> = []
const rootHashLevel = tree.length - 1
for (let level = 0; level < rootHashLevel; level++) {
Expand All @@ -130,13 +142,16 @@ export function rootHashFromInclusionProof(
proofSegments: Uint8Array[],
proveSegment: Uint8Array,
proveSegmentIndex: number,
options?: Options,
): Uint8Array {
const hashFn = options?.hashFn ? options.hashFn : keccak256Hash

let calculatedHash = proveSegment
for (const proofSegment of proofSegments) {
const mergeSegmentFromRight = proveSegmentIndex % 2 === 0 ? true : false
const mergeSegmentFromRight = proveSegmentIndex % 2 === 0
calculatedHash = mergeSegmentFromRight
? keccak256Hash(calculatedHash, proofSegment)
: keccak256Hash(proofSegment, calculatedHash)
? hashFn(calculatedHash, proofSegment)
: hashFn(proofSegment, calculatedHash)
proveSegmentIndex >>>= 1
}

Expand All @@ -147,24 +162,26 @@ export function rootHashFromInclusionProof(
* Gives back all level of the bmt of the payload
*
* @param payload any data in Uint8Array object
* @param options function configuraitons
* @returns array of the whole bmt hash level of the given data.
* First level is the data itself until the last level that is the root hash itself.
*/
function bmt(payload: Uint8Array, maxPayloadLength: number = DEFAULT_MAX_PAYLOAD_SIZE): Uint8Array[] {
if (payload.length > maxPayloadLength) {
function bmt(payload: Uint8Array, options?: Options): Uint8Array[] {
if (payload.length > DEFAULT_MAX_PAYLOAD_SIZE) {
throw new Error(`invalid data length ${payload.length}`)
}
const hashFn = options?.hashFn ? options.hashFn : keccak256Hash

// create an input buffer padded with zeros
let input = new Uint8Array([...payload, ...new Uint8Array(maxPayloadLength - payload.length)])
let input = new Uint8Array([...payload, ...new Uint8Array(DEFAULT_MAX_PAYLOAD_SIZE - payload.length)])
const tree: Uint8Array[] = []
while (input.length !== HASH_SIZE) {
tree.push(input)
const output = new Uint8Array(input.length / 2)

// in each round we hash the segment pairs together
for (let offset = 0; offset < input.length; offset += SEGMENT_PAIR_SIZE) {
const hashNumbers = keccak256Hash(input.slice(offset, offset + SEGMENT_PAIR_SIZE))
const hashNumbers = hashFn(input.slice(offset, offset + SEGMENT_PAIR_SIZE))
output.set(hashNumbers, offset / 2)
}

Expand All @@ -189,18 +206,21 @@ function bmt(payload: Uint8Array, maxPayloadLength: number = DEFAULT_MAX_PAYLOAD
* @param payload Chunk data Uint8Array
* @param spanLength dedicated byte length for serializing span value of chunk
* @param chunkSpan constucted Span uint8array object of the chunk
* @param options function configurations
*
* @returns the keccak256 hash in a byte array
* @returns the Chunk address in a byte array
*/
function chunkAddress<SpanLength extends number = typeof DEFAULT_SPAN_SIZE>(
payload: Uint8Array,
spanLength?: SpanLength,
chunkSpan?: Span<SpanLength>,
): Bytes<32> {
options?: Options,
): ChunkAddress {
const hashFn = options?.hashFn ? options.hashFn : keccak256Hash
const span = chunkSpan || makeSpan(payload.length, spanLength)
const rootHash = bmtRootHash(payload)
const rootHash = bmtRootHash(payload, DEFAULT_MAX_PAYLOAD_SIZE, options)
const chunkHashInput = new Uint8Array([...span, ...rootHash])
const chunkHash = keccak256Hash(chunkHashInput)
const chunkHash = hashFn(chunkHashInput)

return chunkHash
}
2 changes: 1 addition & 1 deletion src/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ export function fileAddressFromInclusionProof<SpanLength extends number = typeof
maxChunkPayloadByteLength,
)
for (const proofSegment of proveChunk.sisterSegments) {
const mergeSegmentFromRight = proveSegmentIndex % 2 === 0 ? true : false
const mergeSegmentFromRight = proveSegmentIndex % 2 === 0
calculatedHash = mergeSegmentFromRight
? keccak256Hash(calculatedHash, proofSegment)
: keccak256Hash(proofSegment, calculatedHash)
Expand Down
2 changes: 2 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,5 @@ export function equalBytes(a: Uint8Array, b: Uint8Array): boolean {

return a.every((byte, index) => b[index] === byte)
}

export { Message }
10 changes: 4 additions & 6 deletions webpack.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@ interface WebpackEnvParams {

const base = async (env?: Partial<WebpackEnvParams>): Promise<Configuration> => {
const isProduction = env?.mode === 'production'
const filename =
env?.fileName ||
['index', isProduction ? '.min' : null, '.js'].filter(Boolean).join('')
const filename = env?.fileName || 'index.js'
const entry = Path.resolve(__dirname, 'src')
const path = Path.resolve(__dirname, 'dist')
const target = 'web'
Expand All @@ -29,12 +27,12 @@ const base = async (env?: Partial<WebpackEnvParams>): Promise<Configuration> =>
return {
bail: Boolean(isProduction),
mode: env?.mode || 'development',
devtool: isProduction ? 'source-map' : 'cheap-module-source-map',
devtool: 'eval-source-map',
entry,
output: {
path,
filename,
sourceMapFilename: filename + '.map',
// sourceMapFilename: filename + '.map',
library: 'BmtJs',
libraryTarget: 'umd',
globalObject: 'this',
Expand Down Expand Up @@ -82,7 +80,7 @@ const base = async (env?: Partial<WebpackEnvParams>): Promise<Configuration> =>
ecma: 5,
comments: false,
},
sourceMap: true
// sourceMap: true,
},
// Use multi-process parallel running to improve the build speed
// Default number of concurrent runs: os.cpus().length - 1
Expand Down

0 comments on commit 2003efe

Please sign in to comment.