This commit is contained in:
Jim Hamill
2025-02-02 06:21:25 +00:00
parent 2def0383ec
commit 2e4e6a2761
5 changed files with 94 additions and 156 deletions
+24 -14
View File
@@ -125,12 +125,30 @@ export const blockHeader = {
], ],
to: [ to: [
{ {
label: 'To Raw', label: 'Hash (Hex)',
callback: (blockHeader: BlockHeader) => blockHeader.toRaw(), callback: (blockHeader: BlockHeader) => blockHeader.getHashHex(),
}, },
{ {
label: 'Version', label: 'Version (Hex)',
callback: (blockHeader: BlockHeader) => blockHeader.getVersion(), callback: (blockHeader: BlockHeader) => blockHeader.getVersionHex(),
},
{
label: 'Timestamp (Unix)',
callback: (blockHeader: BlockHeader) =>
blockHeader.getTimestamp().toString(),
},
{
label: 'Timestamp (ISO)',
callback: (blockHeader: BlockHeader) =>
blockHeader.getTimestampDate().toISOString(),
},
{
label: 'Bits (Hex)',
callback: (blockHeader: BlockHeader) => blockHeader.getBitsHex(),
},
{
label: 'Nonce (Hex)',
callback: (blockHeader: BlockHeader) => blockHeader.getNonceHex(),
}, },
{ {
label: 'Previous Block Hash', label: 'Previous Block Hash',
@@ -142,17 +160,9 @@ export const blockHeader = {
callback: (blockHeader: BlockHeader) => blockHeader.getMerkleRootHex(), callback: (blockHeader: BlockHeader) => blockHeader.getMerkleRootHex(),
}, },
{ {
label: 'Timestamp', label: 'To Raw',
callback: (blockHeader: BlockHeader) => callback: (blockHeader: BlockHeader) =>
blockHeader.getTimestamp().toString(), encodeExtendedJson(blockHeader.toRaw(), 2),
},
{
label: 'Bits',
callback: (blockHeader: BlockHeader) => blockHeader.getBits().toString(),
},
{
label: 'Nonce',
callback: (blockHeader: BlockHeader) => blockHeader.getNonce().toString(),
}, },
], ],
}; };
+57 -71
View File
@@ -2,15 +2,6 @@ import { Bytes } from './bytes.js';
import { hash256 } from '@bitauth/libauth'; import { hash256 } from '@bitauth/libauth';
export interface BlockHeaderRaw {
version: number;
previousBlockHash: Uint8Array;
merkleRoot: Uint8Array;
timestamp: number;
bits: number;
nonce: number;
}
/** /**
* Represents a Bitcoin Cash block header. * Represents a Bitcoin Cash block header.
* This class handles parsing and serialization of BCH block headers between different formats * This class handles parsing and serialization of BCH block headers between different formats
@@ -32,23 +23,16 @@ export interface BlockHeaderRaw {
* ``` * ```
*/ */
export class BlockHeader { export class BlockHeader {
// View of our data so that we can extract as numbers.
private view: DataView;
/** /**
* Creates a new BlockHeader instance. * Creates a new BlockHeader instance.
* @param version - The block version * @param bytes The bytes representing our Block Header (Big Endian)
* @param previousBlockHash - Hash of the previous block (32 bytes)
* @param merkleRoot - Merkle root of the block's transactions (32 bytes)
* @param timestamp - Block timestamp in seconds since Unix epoch
* @param bits - Compact representation of the target threshold
* @param nonce - Value modified during mining to make the block hash valid
*/ */
protected constructor( protected constructor(private readonly bytes: Uint8Array) {
private readonly version: number, this.view = new DataView(bytes.buffer);
private readonly previousBlockHash: Uint8Array, }
private readonly merkleRoot: Uint8Array,
private readonly timestamp: number,
private readonly bits: number,
private readonly nonce: number,
) {}
/** /**
* Instantiates a blockheader from either: * Instantiates a blockheader from either:
@@ -79,15 +63,7 @@ export class BlockHeader {
throw new Error('Invalid block header length'); throw new Error('Invalid block header length');
} }
const view = new DataView(bytes.buffer); return new BlockHeader(bytes);
return new BlockHeader(
view.getInt32(0, true),
bytes.slice(4, 36),
bytes.slice(36, 68),
view.getUint32(68, true),
view.getUint32(72, true),
view.getUint32(76, true),
);
} }
/** /**
@@ -108,15 +84,8 @@ export class BlockHeader {
* @param obj - Object containing the block header fields * @param obj - Object containing the block header fields
* @returns A new BlockHeader instance * @returns A new BlockHeader instance
*/ */
static fromRaw(obj: BlockHeaderRaw): BlockHeader { static fromRaw(bytes: Uint8Array): BlockHeader {
return new this( return new this(bytes);
obj.version,
obj.previousBlockHash,
obj.merkleRoot,
obj.timestamp,
obj.bits,
obj.nonce,
);
} }
/** /**
@@ -141,18 +110,7 @@ export class BlockHeader {
* @returns 80-byte array containing the serialized block header * @returns 80-byte array containing the serialized block header
*/ */
toBytes(): Uint8Array { toBytes(): Uint8Array {
const buffer = new ArrayBuffer(80); return this.bytes;
const view = new DataView(buffer);
const bytes = new Uint8Array(buffer);
view.setInt32(0, this.version, true);
bytes.set(this.previousBlockHash, 4);
bytes.set(this.merkleRoot, 36);
view.setUint32(68, this.timestamp, true);
view.setUint32(72, this.bits, true);
view.setUint32(76, this.nonce, true);
return bytes;
} }
/** /**
@@ -169,35 +127,63 @@ export class BlockHeader {
* *
* @returns Object containing the block header fields * @returns Object containing the block header fields
*/ */
toRaw(): BlockHeaderRaw { toRaw() {
return { return this.bytes;
version: this.version,
previousBlockHash: this.previousBlockHash,
merkleRoot: this.merkleRoot,
timestamp: this.timestamp,
bits: this.bits,
nonce: this.nonce,
};
} }
// Getters // Getters
getVersion(): number { getVersion(): number {
return this.version; return this.view.getUint32(0, true);
} }
getVersionBytes(): Uint8Array {
return this.bytes.slice(0, 4).toReversed();
}
getVersionHex(): string {
return Bytes.fromBytes(this.getVersionBytes()).toHex();
}
getPreviousBlockHash(): Uint8Array { getPreviousBlockHash(): Uint8Array {
return this.previousBlockHash; // NOTE: We want to return this as a Big Endian, so we reverse it.
return this.bytes.slice(4, 36).toReversed();
} }
getMerkleRoot(): Uint8Array { getMerkleRoot(): Uint8Array {
return this.merkleRoot; // NOTE: We want to return this as a Big Endian, so we reverse it.
return this.bytes.slice(36, 68).toReversed();
} }
getTimestamp(): number { getTimestamp(): number {
return this.timestamp; return this.view.getUint32(68, true);
} }
getTimestampDate(): Date {
return new Date(Math.round(this.getTimestamp() * 1000));
}
getBits(): number { getBits(): number {
return this.bits; return this.view.getUint32(72, true);
} }
getBitsBytes(): Uint8Array {
return this.bytes.slice(72, 76).toReversed();
}
getBitsHex(): string {
return Bytes.fromBytes(this.getBitsBytes()).toHex();
}
getNonce(): number { getNonce(): number {
return this.nonce; return this.view.getUint32(76, true);
}
getNonceBytes(): Uint8Array {
return this.bytes.slice(76, 80).toReversed();
}
getNonceHex(): string {
return Bytes.fromBytes(this.getNonceBytes()).toHex();
} }
/** /**
@@ -205,7 +191,7 @@ export class BlockHeader {
* @returns 64-character hex string * @returns 64-character hex string
*/ */
getPreviousBlockHashHex(): string { getPreviousBlockHashHex(): string {
return Bytes.fromBytes(this.previousBlockHash).toHex(); return Bytes.fromBytes(this.getPreviousBlockHash()).toHex();
} }
/** /**
@@ -213,7 +199,7 @@ export class BlockHeader {
* @returns 64-character hex string * @returns 64-character hex string
*/ */
getMerkleRootHex(): string { getMerkleRootHex(): string {
return Bytes.fromBytes(this.merkleRoot).toHex(); return Bytes.fromBytes(this.getMerkleRoot()).toHex();
} }
/** /**
@@ -221,7 +207,7 @@ export class BlockHeader {
* @returns 32-byte array containing the block hash * @returns 32-byte array containing the block hash
*/ */
getHash(): Uint8Array { getHash(): Uint8Array {
return hash256(this.toBytes()); return hash256(this.toBytes()).toReversed();
} }
/** /**
+4
View File
@@ -152,4 +152,8 @@ export class Bytes {
public toBytes(): Uint8Array { public toBytes(): Uint8Array {
return this.bytes; return this.bytes;
} }
public invert(): Bytes {
return new Bytes(Uint8Array.from([...this.bytes].reverse()));
}
} }
+6 -70
View File
@@ -5,14 +5,7 @@ import { Bytes } from '~/src/bytes';
describe('Success cases', () => { describe('Success cases', () => {
// Test cases with different block header representations // Test cases with different block header representations
type BlockHeaderCase = { type BlockHeaderCase = {
raw: { raw: Uint8Array;
version: number;
previousBlockHash: Uint8Array;
merkleRoot: Uint8Array;
timestamp: number;
bits: number;
nonce: number;
};
representations: { representations: {
bytes: Uint8Array; bytes: Uint8Array;
hex: string; hex: string;
@@ -21,23 +14,14 @@ describe('Success cases', () => {
const testCases: BlockHeaderCase[] = [ const testCases: BlockHeaderCase[] = [
{ {
raw: { raw: Bytes.fromHex(
version: 592633856, '0000602093ea70b294aeef69db85981bd387285824fc5af732e487010000000000000000135576e1ba9cae5509d60b4fbb9e66612f35b5ee8db264cb12612d46cc8db60b5e34a164d46b021864abbb1d',
previousBlockHash: Bytes.fromHex( ).toBytes(),
'df4da87b838b2dde9aaf26cb08380f15a57f32d99f2007000000000000000000',
).toBytes(),
merkleRoot: Bytes.fromHex(
'5e8a3ccc570da3eddb5c67853385e72aad4c830b2eedb4f23943d09513a0b476',
).toBytes(),
timestamp: 1737338908,
bits: 402807112,
nonce: 3478133258,
},
representations: { representations: {
bytes: Bytes.fromHex( bytes: Bytes.fromHex(
'00e05223df4da87b838b2dde9aaf26cb08380f15a57f32d99f20070000000000000000005e8a3ccc570da3eddb5c67853385e72aad4c830b2eedb4f23943d09513a0b4761cb08d67485902180a1a50cf', '0000602093ea70b294aeef69db85981bd387285824fc5af732e487010000000000000000135576e1ba9cae5509d60b4fbb9e66612f35b5ee8db264cb12612d46cc8db60b5e34a164d46b021864abbb1d',
).toBytes(), ).toBytes(),
hex: '00e05223df4da87b838b2dde9aaf26cb08380f15a57f32d99f20070000000000000000005e8a3ccc570da3eddb5c67853385e72aad4c830b2eedb4f23943d09513a0b4761cb08d67485902180a1a50cf', hex: '0000602093ea70b294aeef69db85981bd387285824fc5af732e487010000000000000000135576e1ba9cae5509d60b4fbb9e66612f35b5ee8db264cb12612d46cc8db60b5e34a164d46b021864abbb1d',
}, },
}, },
]; ];
@@ -78,54 +62,6 @@ describe('Success cases', () => {
expect(blockHeader[name]()).toStrictEqual(representations[key]); expect(blockHeader[name]()).toStrictEqual(representations[key]);
}); });
}); });
// Test getter methods
it.each(testCases)('getter methods return correct values', ({ raw }) => {
const blockHeader = BlockHeader.fromRaw(raw);
expect(blockHeader.getVersion()).toBe(raw.version);
expect(blockHeader.getPreviousBlockHash()).toStrictEqual(
raw.previousBlockHash,
);
expect(blockHeader.getMerkleRoot()).toStrictEqual(raw.merkleRoot);
expect(blockHeader.getTimestamp()).toBe(raw.timestamp);
expect(blockHeader.getBits()).toBe(raw.bits);
expect(blockHeader.getNonce()).toBe(raw.nonce);
});
// Test hex getter methods
it.each(testCases)('hex getter methods return correct values', ({ raw }) => {
const blockHeader = BlockHeader.fromRaw(raw);
expect(blockHeader.getPreviousBlockHashHex()).toBe(
Bytes.fromBytes(raw.previousBlockHash).toHex(),
);
expect(blockHeader.getMerkleRootHex()).toBe(
Bytes.fromBytes(raw.merkleRoot).toHex(),
);
});
// Test hash methods
it.each(testCases)('getHash() returns correct hash', ({ raw }) => {
const blockHeader = BlockHeader.fromRaw(raw);
const hash = blockHeader.getHash();
expect(hash).toBeInstanceOf(Uint8Array);
expect(hash.length).toBe(32);
});
it.each(testCases)('getHashHex() returns correct hash hex', ({ raw }) => {
const blockHeader = BlockHeader.fromRaw(raw);
const hashHex = blockHeader.getHashHex();
expect(typeof hashHex).toBe('string');
expect(hashHex.length).toBe(64);
});
// Test validation method
it.each(testCases)(
'isValid() returns true for valid inputs',
({ representations }) => {
expect(BlockHeader.isValid(representations.bytes)).toBe(true);
expect(BlockHeader.isValid(representations.hex)).toBe(true);
},
);
}); });
describe('Error cases', () => { describe('Error cases', () => {
+3 -1
View File
@@ -29,7 +29,9 @@ export class Encrypter {
algorithm: 'AES-GCM', algorithm: 'AES-GCM',
keyLength: 256, keyLength: 256,
salt: utf8ToBin(DEFAULT_XO_SALT), salt: utf8ToBin(DEFAULT_XO_SALT),
ivLength: 16, // NOTE: IV of length 96 bits (12 bytes) is recommended length as per https://csrc.nist.gov/pubs/sp/800/38/d/final.
// This is both secure - and avoids additional compute incurred by hashing.
ivLength: 12,
iterations: 100_000, iterations: 100_000,
...opts, ...opts,
}; };