-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
299 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()), | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |