diff --git a/src/lib/message/block-header-encoding.spec.ts b/src/lib/message/block-header-encoding.spec.ts new file mode 100644 index 00000000..b2fbc663 --- /dev/null +++ b/src/lib/message/block-header-encoding.spec.ts @@ -0,0 +1,59 @@ +import test from 'ava'; +import { hexToBin } from '../lib.js'; +import { decodeHeader, encodeHeader } from './block-header-encoding.js'; +import type { BlockHeader } from './block-header-encoding.js' + +export const uahfHeader = hexToBin( + "02000020e42980330b7294bef6527af576e5cfe2c97d55f9c19beb0000000000000000004a88016082f466735a0f4bc9e5e42725fbc3d0ac28d4ab9547bf18654f14655b1e7f80593547011816dd5975", +); + +const genesisHeader = hexToBin( + "0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a29ab5f49ffff001d1dac2b7c", +); +const genesisDecoded: BlockHeader = { + version: 1, + previousBlockHash: hexToBin("0000000000000000000000000000000000000000000000000000000000000000"), + merkleRootHash: hexToBin("4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b"), + time: 1231006505, + difficultyTarget: 486604799, + nonce: 2083236893, +}; +const uahfDecoded: BlockHeader = { + version: 536870914, + previousBlockHash: hexToBin("000000000000000000eb9bc1f9557dc9e2cfe576f57a52f6be94720b338029e4"), + merkleRootHash: hexToBin("5b65144f6518bf4795abd428acd0c3fb2527e4e5c94b0f5a7366f4826001884a"), + time: 1501593374, + difficultyTarget: 402736949, + nonce: 1968823574, +}; +test("decodeHeader genesis", (t) => { + t.deepEqual(decodeHeader(genesisHeader), genesisDecoded) +}) +test("encodeHeader genesis", (t) => { + t.deepEqual(encodeHeader(genesisDecoded), genesisHeader) +}) +test("decodeHeader uahf", (t) => { + t.deepEqual(decodeHeader(uahfHeader), uahfDecoded) +}) +test("encodeHeader uahf", (t) => { + t.deepEqual(encodeHeader(uahfDecoded), uahfHeader) +}) + +test("decodeHeader invalid version byte length", (t) => { + t.deepEqual(decodeHeader(Uint8Array.from([])), "Error reading header. Error reading Uint32LE: requires 4 bytes. Remaining bytes: 0") +}) +test("decodeHeader invalid previousHash byte length", (t) => { + t.deepEqual(decodeHeader(Uint8Array.from([0, 0, 0, 0, 0])), "Error reading header. Error reading bytes: insufficient length. Bytes requested: 32; remaining bytes: 1") +}) +test("decodeHeader invalid merkle byte length", (t) => { + t.deepEqual(decodeHeader(new Uint8Array(40)), "Error reading header. Error reading bytes: insufficient length. Bytes requested: 32; remaining bytes: 4") +}) +test("decodeHeader invalid time length", (t) => { + t.deepEqual(decodeHeader(new Uint8Array(68)), "Error reading header. Error reading Uint32LE: requires 4 bytes. Remaining bytes: 0") +}) +test("decodeHeader invalid target length", (t) => { + t.deepEqual(decodeHeader(new Uint8Array(72)), "Error reading header. Error reading Uint32LE: requires 4 bytes. Remaining bytes: 0") +}) +test("decodeHeader invalid nonce length", (t) => { + t.deepEqual(decodeHeader(new Uint8Array(76)), "Error reading header. Error reading Uint32LE: requires 4 bytes. Remaining bytes: 0") +}) diff --git a/src/lib/message/block-header-encoding.ts b/src/lib/message/block-header-encoding.ts new file mode 100644 index 00000000..7a7cd960 --- /dev/null +++ b/src/lib/message/block-header-encoding.ts @@ -0,0 +1,147 @@ +import { + flattenBinArray, + formatError, + numberToBinUint32LE, + readMultiple +} from "../lib.js"; +import { + type MaybeReadResult, + type ReadPosition, +} from "../lib.js" +import { + readBytes, + readUint32LE, +} from "./read-components.js"; + +const SHA256HASHLEN = 32; + +export enum HeaderDecodingError { + version = "Error reading version.", + previousBlock = "Error reading previous block.", + merkleRootHash = "Error reading merkle root hash", + time = "Error reading time", + difficultyTarget = "Error reading difficulty target", + nonce = "Error reading nonce", + generic = "Error reading header.", + endsWithUnexpectedBytes = "Error decoding header: the provided header includes unexpected bytes.", +} + +/** + * Represents the header of a block in a blockchain. + */ +export type BlockHeader = { + /** + * The version of the block. + */ + version: number; + + /** + * The hash of the previous block in the blockchain. + */ + previousBlockHash: Uint8Array; + + /** + * The hash of the Merkle root of the transactions in the block. + */ + merkleRootHash: Uint8Array; + + /** + * The Unix epoch time at which the block was created. + */ + time: number; + + /** + * The target value for the block's proof-of-work. + */ + difficultyTarget: number; + + /** + * A random value used in the proof-of-work calculation. + */ + nonce: number; +}; + +/** + * Attempts to read a BlockHeader from the provided binary data at the given position. + * + * @param {ReadPosition} position - The position in the binary data from which to start reading. + * @returns {MaybeReadResult} A parsed BlockHeader object if successful, or an error message if not. + */ +export const readHeader = ( + position: ReadPosition, +): MaybeReadResult => { + const headerRead = readMultiple(position, [ + readUint32LE, + readBytes(SHA256HASHLEN), // previous block hash + readBytes(SHA256HASHLEN), // merkle root + readUint32LE, // Unix epoch time + readUint32LE, // target difficulty A.K.A bits + readUint32LE, // nonce + ]); + if (typeof headerRead === "string") { + return formatError(HeaderDecodingError.generic, headerRead); + } + const { + position: nextPosition, + result: [ + version, + previousBlockHash, + merkleRootHash, + time, + difficultyTarget, + nonce, + ], + } = headerRead; + return { + position: nextPosition, + result: { + version, + previousBlockHash: previousBlockHash.reverse(), + merkleRootHash: merkleRootHash.reverse(), + time, + difficultyTarget, + nonce, + }, + }; +}; + +/** + * Decodes a BlockHeader from a given Uint8Array containing its binary representation. + * + * @param {Uint8Array} bin - The binary data containing the encoded BlockHeader. + * @returns {BlockHeader | string} A parsed BlockHeader object if successful, or an error message if not. + */ +export const decodeHeader = (bin: Uint8Array): BlockHeader | string => { + const headerRead = readHeader({ bin, index: 0 }); + if (typeof headerRead === "string") { + return headerRead; + } + if (headerRead.position.index !== bin.length) { + return formatError( + HeaderDecodingError.endsWithUnexpectedBytes, + `Encoded header ends at index ${headerRead.position.index - 1}, leaving ${bin.length - headerRead.position.index + } remaining bytes.`, + ); + } + return headerRead.result; +}; + +/** + * Encodes a BlockHeader object into its binary representation. + * + * This function takes a `BlockHeader` object and returns a new `Uint8Array` containing its + * serialized form. The encoding process follows the little-endian convention for all numerical + * values (version, time, difficultyTarget, and nonce). + * + * @param {BlockHeader} header - The BlockHeader object to encode. + * @returns {Uint8Array} A new Uint8Array containing the binary representation of the BlockHeader. + */ +export const encodeHeader = (header: BlockHeader) => + flattenBinArray([ + numberToBinUint32LE(header.version), + header.previousBlockHash.reverse(), + header.merkleRootHash.reverse(), + numberToBinUint32LE(header.time), + numberToBinUint32LE(header.difficultyTarget), + numberToBinUint32LE(header.nonce), + ]);