Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add header encodings #132

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions src/lib/message/block-header-encoding.spec.ts
Original file line number Diff line number Diff line change
@@ -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")
})
147 changes: 147 additions & 0 deletions src/lib/message/block-header-encoding.ts
Original file line number Diff line number Diff line change
@@ -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<BlockHeader>} A parsed BlockHeader object if successful, or an error message if not.
*/
export const readHeader = (
position: ReadPosition,
): MaybeReadResult<BlockHeader> => {
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.`,
);
}

Check warning on line 125 in src/lib/message/block-header-encoding.ts

View check run for this annotation

Codecov / codecov/patch

src/lib/message/block-header-encoding.ts#L120-L125

Added lines #L120 - L125 were not covered by tests
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),
]);
Loading