Skip to content

Commit

Permalink
feat: bee-js inspired feats
Browse files Browse the repository at this point in the history
  • Loading branch information
nugaon committed Feb 28, 2022
1 parent caf6695 commit 8cb3dd4
Show file tree
Hide file tree
Showing 5 changed files with 299 additions and 0 deletions.
92 changes: 92 additions & 0 deletions src/bmt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { keccak256 } from 'js-sha3'
import { Bytes, keccak256Hash } from './utils'

const MAX_CHUNK_PAYLOAD_SIZE = 4096
const SEGMENT_SIZE = 32
const SEGMENT_PAIR_SIZE = 2 * SEGMENT_SIZE
const HASH_SIZE = 32

/**
* Calculate a Binary Merkle Tree hash for a chunk
*
* The BMT chunk address is the hash of the 8 byte span and the root
* hash of a binary Merkle tree (BMT) built on the 32-byte segments
* of the underlying data.
*
* If the chunk content is less than 4k, the hash is calculated as
* if the chunk was padded with all zeros up to 4096 bytes.
*
* @param chunkContent Chunk data including span and payload as well
*
* @returns the keccak256 hash in a byte array
*/
export function bmtHash(chunkContent: Uint8Array): Bytes<32> {
const span = chunkContent.slice(0, 8)
const payload = chunkContent.slice(8)
const rootHash = bmtRootHash(payload)
const chunkHashInput = new Uint8Array([...span, ...rootHash])
const chunkHash = keccak256Hash(chunkHashInput)

return chunkHash
}

function bmtRootHash(payload: Uint8Array): Uint8Array {
if (payload.length > MAX_CHUNK_PAYLOAD_SIZE) {
throw new Error(`invalid data length ${payload}`)
}

// create an input buffer padded with zeros
let input = new Uint8Array([
...payload,
...new Uint8Array(MAX_CHUNK_PAYLOAD_SIZE - payload.length),
])
while (input.length !== HASH_SIZE) {
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 = keccak256.array(input.slice(offset, offset + SEGMENT_PAIR_SIZE))
output.set(hashNumbers, offset / 2)
}

input = output
}

return input
}

/**
* Gives back all level of the bmt of the payload
*
* @param payload any data in Uint8Array object
* @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 bmtTree(payload: Uint8Array): Uint8Array[] {
if (payload.length > MAX_CHUNK_PAYLOAD_SIZE) {
throw new Error(`invalid data length ${payload.length}`)
}

// create an input buffer padded with zeros
let input = new Uint8Array([
...payload,
...new Uint8Array(MAX_CHUNK_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 = keccak256.array(input.slice(offset, offset + SEGMENT_PAIR_SIZE))
output.set(hashNumbers, offset / 2)
}

input = output
}
//add the last "input" that is the bmt root hash of the application
tree.push(input)

return tree
}
55 changes: 55 additions & 0 deletions src/chunk.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { bmtHash } from './bmt'
import { DEFAULT_SPAN_SIZE, makeSpan } from './span'
import { assertFlexBytes, Bytes, Flavor, FlexBytes, serializeBytes } from './utils'

const DEFAULT_MAX_PAYLOAD_SIZE = 4096 as const
const DEFAULT_MIN_PAYLOAD_SIZE = 1 as const
export type ChunkAddress = Bytes<32>
type ValidChunkData = Uint8Array & Flavor<'ValidChunkData'>

export interface Chunk<
MaxLength extends number = typeof DEFAULT_MAX_PAYLOAD_SIZE,
MinLength extends number = typeof DEFAULT_MIN_PAYLOAD_SIZE,
SpanSize extends number = typeof DEFAULT_SPAN_SIZE,
> extends Flavor<'Chunk'> {
readonly payload: FlexBytes<MinLength, MaxLength>
data(): ValidChunkData
span(): Bytes<SpanSize>
address(): ChunkAddress
//TODO inclusionProofSegments
}

/**
* Creates a content addressed chunk and verifies the payload size.
*
* @param payloadBytes the data to be stored in the chunk
*/
export function makeChunk<
MaxPayloadSize extends number = typeof DEFAULT_MAX_PAYLOAD_SIZE,
MinPayloadSize extends number = typeof DEFAULT_MIN_PAYLOAD_SIZE,
SpanSize extends number = typeof DEFAULT_SPAN_SIZE,
>(
payloadBytes: Uint8Array,
options?: {
maxPayloadSize?: MaxPayloadSize
minPayloadSize?: MinPayloadSize
spanSize?: SpanSize
},
): Chunk<MaxPayloadSize, MinPayloadSize, SpanSize> {
// assertion for the sizes are required because
// typescript does not recognise subset relation on union type definition
const maxPayloadSize = (options?.maxPayloadSize || DEFAULT_MAX_PAYLOAD_SIZE) as MaxPayloadSize
const minPayloadSize = (options?.minPayloadSize || DEFAULT_MIN_PAYLOAD_SIZE) as MinPayloadSize
const spanSize = (options?.spanSize || DEFAULT_SPAN_SIZE) as SpanSize

assertFlexBytes(payloadBytes, minPayloadSize, maxPayloadSize)
const spanFn = () => makeSpan(payloadBytes.length, spanSize)
const dataFn = () => serializeBytes(spanFn(), payloadBytes) as ValidChunkData

return {
payload: payloadBytes,
data: dataFn,
span: spanFn,
address: () => bmtHash(dataFn()),
}
}
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './bmt'
export * from './chunk'
export * from './span'
export * as Utils from './utils'
38 changes: 38 additions & 0 deletions src/span.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Bytes, Flavor } from './utils'

export const DEFAULT_SPAN_SIZE = 8 as const

export interface Span<Length extends number = typeof DEFAULT_SPAN_SIZE>
extends Bytes<Length>,
Flavor<'Span'> {}

// we limit the maximum span size in 32 bits to avoid BigInt compatibility issues
const MAX_SPAN_LENGTH = 2 ** 32 - 1

/**
* Create a span for storing the length of the chunk
*
* The length is encoded in 64-bit little endian.
*
* @param value The length of the span
*/
export function makeSpan<Length extends number>(value: number, length?: Length): Span<Length> {
const spanLength = length || DEFAULT_SPAN_SIZE

if (value <= 0) {
throw new Error(`invalid length for span: ${value}`)
}

if (value > MAX_SPAN_LENGTH) {
throw new Error(`invalid length (> ${MAX_SPAN_LENGTH}) ${value}`)
}

const span = new Uint8Array(spanLength)
const dataView = new DataView(span.buffer)
const littleEndian = true
const lengthLower32 = value & 0xffffffff

dataView.setUint32(0, lengthLower32, littleEndian)

return span as Bytes<Length>
}
110 changes: 110 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { keccak256, Message } from 'js-sha3'

/** Used for FavorTypes */
export type Flavor<Name> = { __tag__?: Name }

/**
* Nominal type to represent hex strings WITHOUT '0x' prefix.
* For example for 32 bytes hex representation you have to use 64 length.
* TODO: Make Length mandatory: https://github.com/ethersphere/bee-js/issues/208
*/
export type HexString<Length extends number = number> = string & {
readonly length: Length
} & Flavor<'HexString'>

export interface Bytes<Length extends number> extends Uint8Array {
readonly length: Length
}

/**
* Helper type for dealing with flexible sized byte arrays.
*
* The actual min and and max values are not stored in runtime, they
* are only there to differentiate the type from the Uint8Array at
* compile time.
* @see BrandedType
*/
export interface FlexBytes<Min extends number, Max extends number> extends Uint8Array {
readonly __min__?: Min
readonly __max__?: Max
}

export function isFlexBytes<Min extends number, Max extends number = Min>(
b: unknown,
min: Min,
max: Max,
): b is FlexBytes<Min, Max> {
return b instanceof Uint8Array && b.length >= min && b.length <= max
}

/**
* Verifies if a byte array has a certain length between min and max
*
* @param b The byte array
* @param min Minimum size of the array
* @param max Maximum size of the array
*/
export function assertFlexBytes<Min extends number, Max extends number = Min>(
b: unknown,
min: Min,
max: Max,
): asserts b is FlexBytes<Min, Max> {
if (!isFlexBytes(b, min, max)) {
throw new TypeError(
`Parameter is not valid FlexBytes of min: ${min}, max: ${max}, length: ${
(b as Uint8Array).length
}`,
)
}
}

/**
* Helper function for serialize byte arrays
*
* @param arrays Any number of byte array arguments
*/
export function serializeBytes(...arrays: Uint8Array[]): Uint8Array {
const length = arrays.reduce((prev, curr) => prev + curr.length, 0)
const buffer = new Uint8Array(length)
let offset = 0
arrays.forEach(arr => {
buffer.set(arr, offset)
offset += arr.length
})

return buffer
}

/**
* Helper function for calculating the keccak256 hash with
* correct types.
*
* @param messages Any number of messages (strings, byte arrays etc.)
*/
export function keccak256Hash(...messages: Message[]): Bytes<32> {
const hasher = keccak256.create()

messages.forEach(bytes => hasher.update(bytes))

return Uint8Array.from(hasher.digest()) as Bytes<32>
}

/**
* Converts array of number or Uint8Array to HexString without prefix.
*
* @param bytes The input array
* @param len The length of the non prefixed HexString
*/
export function bytesToHex<Length extends number>(
bytes: Uint8Array,
len: Length,
): HexString<Length> {
const hexByte = (n: number) => n.toString(16).padStart(2, '0')
const hex = Array.from(bytes, hexByte).join('') as HexString<Length>

if (hex.length !== len) {
throw new TypeError(`Resulting HexString does not have expected length ${len}: ${hex}`)
}

return hex
}

0 comments on commit 8cb3dd4

Please sign in to comment.