Skip to content

Commit 1594ea5

Browse files
committed
add header encodings
1 parent ade0151 commit 1594ea5

File tree

2 files changed

+206
-0
lines changed

2 files changed

+206
-0
lines changed
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import test from 'ava';
2+
import { hexToBin } from '../lib.js';
3+
import { decodeHeader, encodeHeader } from './block-header-encoding.js';
4+
import type { BlockHeader } from './block-header-encoding.js'
5+
6+
export const uahfHeader = hexToBin(
7+
"02000020e42980330b7294bef6527af576e5cfe2c97d55f9c19beb0000000000000000004a88016082f466735a0f4bc9e5e42725fbc3d0ac28d4ab9547bf18654f14655b1e7f80593547011816dd5975",
8+
);
9+
10+
const genesisHeader = hexToBin(
11+
"0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a29ab5f49ffff001d1dac2b7c",
12+
);
13+
const genesisDecoded: BlockHeader = {
14+
version: 1,
15+
previousBlockHash: hexToBin("0000000000000000000000000000000000000000000000000000000000000000"),
16+
merkleRootHash: hexToBin("4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b"),
17+
time: 1231006505,
18+
difficultyTarget: 486604799,
19+
nonce: 2083236893,
20+
};
21+
const uahfDecoded: BlockHeader = {
22+
version: 536870914,
23+
previousBlockHash: hexToBin("000000000000000000eb9bc1f9557dc9e2cfe576f57a52f6be94720b338029e4"),
24+
merkleRootHash: hexToBin("5b65144f6518bf4795abd428acd0c3fb2527e4e5c94b0f5a7366f4826001884a"),
25+
time: 1501593374,
26+
difficultyTarget: 402736949,
27+
nonce: 1968823574,
28+
};
29+
test("decodeHeader genesis", (t) => {
30+
t.deepEqual(decodeHeader(genesisHeader), genesisDecoded)
31+
})
32+
test("encodeHeader genesis", (t) => {
33+
t.deepEqual(encodeHeader(genesisDecoded), genesisHeader)
34+
})
35+
test("decodeHeader uahf", (t) => {
36+
t.deepEqual(decodeHeader(uahfHeader), uahfDecoded)
37+
})
38+
test("encodeHeader uahf", (t) => {
39+
t.deepEqual(encodeHeader(uahfDecoded), uahfHeader)
40+
})
41+
42+
test("decodeHeader invalid version byte length", (t) => {
43+
t.deepEqual(decodeHeader(Uint8Array.from([])), "Error reading header. Error reading Uint32LE: requires 4 bytes. Remaining bytes: 0")
44+
})
45+
test("decodeHeader invalid previousHash byte length", (t) => {
46+
t.deepEqual(decodeHeader(Uint8Array.from([0, 0, 0, 0, 0])), "Error reading header. Error reading bytes: insufficient length. Bytes requested: 32; remaining bytes: 1")
47+
})
48+
test("decodeHeader invalid merkle byte length", (t) => {
49+
t.deepEqual(decodeHeader(new Uint8Array(40)), "Error reading header. Error reading bytes: insufficient length. Bytes requested: 32; remaining bytes: 4")
50+
})
51+
test("decodeHeader invalid time length", (t) => {
52+
t.deepEqual(decodeHeader(new Uint8Array(68)), "Error reading header. Error reading Uint32LE: requires 4 bytes. Remaining bytes: 0")
53+
})
54+
test("decodeHeader invalid target length", (t) => {
55+
t.deepEqual(decodeHeader(new Uint8Array(72)), "Error reading header. Error reading Uint32LE: requires 4 bytes. Remaining bytes: 0")
56+
})
57+
test("decodeHeader invalid nonce length", (t) => {
58+
t.deepEqual(decodeHeader(new Uint8Array(76)), "Error reading header. Error reading Uint32LE: requires 4 bytes. Remaining bytes: 0")
59+
})
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import {
2+
flattenBinArray,
3+
formatError,
4+
numberToBinUint32LE,
5+
readMultiple
6+
} from "../lib.js";
7+
import {
8+
type MaybeReadResult,
9+
type ReadPosition,
10+
} from "../lib.js"
11+
import {
12+
readBytes,
13+
readUint32LE,
14+
} from "./read-components.js";
15+
16+
const SHA256HASHLEN = 32;
17+
18+
export enum HeaderDecodingError {
19+
version = "Error reading version.",
20+
previousBlock = "Error reading previous block.",
21+
merkleRootHash = "Error reading merkle root hash",
22+
time = "Error reading time",
23+
difficultyTarget = "Error reading difficulty target",
24+
nonce = "Error reading nonce",
25+
generic = "Error reading header.",
26+
endsWithUnexpectedBytes = "Error decoding header: the provided header includes unexpected bytes.",
27+
}
28+
29+
/**
30+
* Represents the header of a block in a blockchain.
31+
*/
32+
export type BlockHeader = {
33+
/**
34+
* The version of the block.
35+
*/
36+
version: number;
37+
38+
/**
39+
* The hash of the previous block in the blockchain.
40+
*/
41+
previousBlockHash: Uint8Array;
42+
43+
/**
44+
* The hash of the Merkle root of the transactions in the block.
45+
*/
46+
merkleRootHash: Uint8Array;
47+
48+
/**
49+
* The Unix epoch time at which the block was created.
50+
*/
51+
time: number;
52+
53+
/**
54+
* The target value for the block's proof-of-work.
55+
*/
56+
difficultyTarget: number;
57+
58+
/**
59+
* A random value used in the proof-of-work calculation.
60+
*/
61+
nonce: number;
62+
};
63+
64+
/**
65+
* Attempts to read a BlockHeader from the provided binary data at the given position.
66+
*
67+
* @param {ReadPosition} position - The position in the binary data from which to start reading.
68+
* @returns {MaybeReadResult<BlockHeader>} A parsed BlockHeader object if successful, or an error message if not.
69+
*/
70+
export const readHeader = (
71+
position: ReadPosition,
72+
): MaybeReadResult<BlockHeader> => {
73+
const headerRead = readMultiple(position, [
74+
readUint32LE,
75+
readBytes(SHA256HASHLEN), // previous block hash
76+
readBytes(SHA256HASHLEN), // merkle root
77+
readUint32LE, // Unix epoch time
78+
readUint32LE, // target difficulty A.K.A bits
79+
readUint32LE, // nonce
80+
]);
81+
if (typeof headerRead === "string") {
82+
return formatError(HeaderDecodingError.generic, headerRead);
83+
}
84+
const {
85+
position: nextPosition,
86+
result: [
87+
version,
88+
previousBlockHash,
89+
merkleRootHash,
90+
time,
91+
difficultyTarget,
92+
nonce,
93+
],
94+
} = headerRead;
95+
return {
96+
position: nextPosition,
97+
result: {
98+
version,
99+
previousBlockHash: previousBlockHash.reverse(),
100+
merkleRootHash: merkleRootHash.reverse(),
101+
time,
102+
difficultyTarget,
103+
nonce,
104+
},
105+
};
106+
};
107+
108+
/**
109+
* Decodes a BlockHeader from a given Uint8Array containing its binary representation.
110+
*
111+
* @param {Uint8Array} bin - The binary data containing the encoded BlockHeader.
112+
* @returns {BlockHeader | string} A parsed BlockHeader object if successful, or an error message if not.
113+
*/
114+
export const decodeHeader = (bin: Uint8Array): BlockHeader | string => {
115+
const headerRead = readHeader({ bin, index: 0 });
116+
if (typeof headerRead === "string") {
117+
return headerRead;
118+
}
119+
if (headerRead.position.index !== bin.length) {
120+
return formatError(
121+
HeaderDecodingError.endsWithUnexpectedBytes,
122+
`Encoded header ends at index ${headerRead.position.index - 1}, leaving ${bin.length - headerRead.position.index
123+
} remaining bytes.`,
124+
);
125+
}
126+
return headerRead.result;
127+
};
128+
129+
/**
130+
* Encodes a BlockHeader object into its binary representation.
131+
*
132+
* This function takes a `BlockHeader` object and returns a new `Uint8Array` containing its
133+
* serialized form. The encoding process follows the little-endian convention for all numerical
134+
* values (version, time, difficultyTarget, and nonce).
135+
*
136+
* @param {BlockHeader} header - The BlockHeader object to encode.
137+
* @returns {Uint8Array} A new Uint8Array containing the binary representation of the BlockHeader.
138+
*/
139+
export const encodeHeader = (header: BlockHeader) =>
140+
flattenBinArray([
141+
numberToBinUint32LE(header.version),
142+
header.previousBlockHash.reverse(),
143+
header.merkleRootHash.reverse(),
144+
numberToBinUint32LE(header.time),
145+
numberToBinUint32LE(header.difficultyTarget),
146+
numberToBinUint32LE(header.nonce),
147+
]);

0 commit comments

Comments
 (0)