From 2e4e6a27616a356fd3c446139b7ed34acaa53b7b Mon Sep 17 00:00:00 2001 From: Jim Hamill Date: Sun, 2 Feb 2025 06:21:25 +0000 Subject: [PATCH] Fixes --- .../docs/src/tools/primitive-functions.ts | 38 ++++-- packages/primitives/src/block-header.ts | 128 ++++++++---------- packages/primitives/src/bytes.ts | 4 + .../primitives/tests/block-header.spec.ts | 76 +---------- packages/stack/src/utils/encrypter.ts | 4 +- 5 files changed, 94 insertions(+), 156 deletions(-) diff --git a/packages/docs/src/tools/primitive-functions.ts b/packages/docs/src/tools/primitive-functions.ts index 5d5ee0b..c5ee46f 100644 --- a/packages/docs/src/tools/primitive-functions.ts +++ b/packages/docs/src/tools/primitive-functions.ts @@ -125,12 +125,30 @@ export const blockHeader = { ], to: [ { - label: 'To Raw', - callback: (blockHeader: BlockHeader) => blockHeader.toRaw(), + label: 'Hash (Hex)', + callback: (blockHeader: BlockHeader) => blockHeader.getHashHex(), }, { - label: 'Version', - callback: (blockHeader: BlockHeader) => blockHeader.getVersion(), + label: 'Version (Hex)', + 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', @@ -142,17 +160,9 @@ export const blockHeader = { callback: (blockHeader: BlockHeader) => blockHeader.getMerkleRootHex(), }, { - label: 'Timestamp', + label: 'To Raw', callback: (blockHeader: BlockHeader) => - blockHeader.getTimestamp().toString(), - }, - { - label: 'Bits', - callback: (blockHeader: BlockHeader) => blockHeader.getBits().toString(), - }, - { - label: 'Nonce', - callback: (blockHeader: BlockHeader) => blockHeader.getNonce().toString(), + encodeExtendedJson(blockHeader.toRaw(), 2), }, ], }; diff --git a/packages/primitives/src/block-header.ts b/packages/primitives/src/block-header.ts index ed63709..7cd8f84 100644 --- a/packages/primitives/src/block-header.ts +++ b/packages/primitives/src/block-header.ts @@ -2,15 +2,6 @@ import { Bytes } from './bytes.js'; 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. * This class handles parsing and serialization of BCH block headers between different formats @@ -32,23 +23,16 @@ export interface BlockHeaderRaw { * ``` */ export class BlockHeader { + // View of our data so that we can extract as numbers. + private view: DataView; + /** * Creates a new BlockHeader instance. - * @param version - The block version - * @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 + * @param bytes The bytes representing our Block Header (Big Endian) */ - protected constructor( - private readonly version: number, - private readonly previousBlockHash: Uint8Array, - private readonly merkleRoot: Uint8Array, - private readonly timestamp: number, - private readonly bits: number, - private readonly nonce: number, - ) {} + protected constructor(private readonly bytes: Uint8Array) { + this.view = new DataView(bytes.buffer); + } /** * Instantiates a blockheader from either: @@ -79,15 +63,7 @@ export class BlockHeader { throw new Error('Invalid block header length'); } - const view = new DataView(bytes.buffer); - 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), - ); + return new BlockHeader(bytes); } /** @@ -108,15 +84,8 @@ export class BlockHeader { * @param obj - Object containing the block header fields * @returns A new BlockHeader instance */ - static fromRaw(obj: BlockHeaderRaw): BlockHeader { - return new this( - obj.version, - obj.previousBlockHash, - obj.merkleRoot, - obj.timestamp, - obj.bits, - obj.nonce, - ); + static fromRaw(bytes: Uint8Array): BlockHeader { + return new this(bytes); } /** @@ -141,18 +110,7 @@ export class BlockHeader { * @returns 80-byte array containing the serialized block header */ toBytes(): Uint8Array { - const buffer = new ArrayBuffer(80); - 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; + return this.bytes; } /** @@ -169,35 +127,63 @@ export class BlockHeader { * * @returns Object containing the block header fields */ - toRaw(): BlockHeaderRaw { - return { - version: this.version, - previousBlockHash: this.previousBlockHash, - merkleRoot: this.merkleRoot, - timestamp: this.timestamp, - bits: this.bits, - nonce: this.nonce, - }; + toRaw() { + return this.bytes; } // Getters 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 { - 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 { - 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 { - return this.timestamp; + return this.view.getUint32(68, true); } + + getTimestampDate(): Date { + return new Date(Math.round(this.getTimestamp() * 1000)); + } + 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 { - 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 */ 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 */ 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 */ getHash(): Uint8Array { - return hash256(this.toBytes()); + return hash256(this.toBytes()).toReversed(); } /** diff --git a/packages/primitives/src/bytes.ts b/packages/primitives/src/bytes.ts index c350ed4..7d8c6a5 100644 --- a/packages/primitives/src/bytes.ts +++ b/packages/primitives/src/bytes.ts @@ -152,4 +152,8 @@ export class Bytes { public toBytes(): Uint8Array { return this.bytes; } + + public invert(): Bytes { + return new Bytes(Uint8Array.from([...this.bytes].reverse())); + } } diff --git a/packages/primitives/tests/block-header.spec.ts b/packages/primitives/tests/block-header.spec.ts index a48bda7..e330049 100644 --- a/packages/primitives/tests/block-header.spec.ts +++ b/packages/primitives/tests/block-header.spec.ts @@ -5,14 +5,7 @@ import { Bytes } from '~/src/bytes'; describe('Success cases', () => { // Test cases with different block header representations type BlockHeaderCase = { - raw: { - version: number; - previousBlockHash: Uint8Array; - merkleRoot: Uint8Array; - timestamp: number; - bits: number; - nonce: number; - }; + raw: Uint8Array; representations: { bytes: Uint8Array; hex: string; @@ -21,23 +14,14 @@ describe('Success cases', () => { const testCases: BlockHeaderCase[] = [ { - raw: { - version: 592633856, - previousBlockHash: Bytes.fromHex( - 'df4da87b838b2dde9aaf26cb08380f15a57f32d99f2007000000000000000000', - ).toBytes(), - merkleRoot: Bytes.fromHex( - '5e8a3ccc570da3eddb5c67853385e72aad4c830b2eedb4f23943d09513a0b476', - ).toBytes(), - timestamp: 1737338908, - bits: 402807112, - nonce: 3478133258, - }, + raw: Bytes.fromHex( + '0000602093ea70b294aeef69db85981bd387285824fc5af732e487010000000000000000135576e1ba9cae5509d60b4fbb9e66612f35b5ee8db264cb12612d46cc8db60b5e34a164d46b021864abbb1d', + ).toBytes(), representations: { bytes: Bytes.fromHex( - '00e05223df4da87b838b2dde9aaf26cb08380f15a57f32d99f20070000000000000000005e8a3ccc570da3eddb5c67853385e72aad4c830b2eedb4f23943d09513a0b4761cb08d67485902180a1a50cf', + '0000602093ea70b294aeef69db85981bd387285824fc5af732e487010000000000000000135576e1ba9cae5509d60b4fbb9e66612f35b5ee8db264cb12612d46cc8db60b5e34a164d46b021864abbb1d', ).toBytes(), - hex: '00e05223df4da87b838b2dde9aaf26cb08380f15a57f32d99f20070000000000000000005e8a3ccc570da3eddb5c67853385e72aad4c830b2eedb4f23943d09513a0b4761cb08d67485902180a1a50cf', + hex: '0000602093ea70b294aeef69db85981bd387285824fc5af732e487010000000000000000135576e1ba9cae5509d60b4fbb9e66612f35b5ee8db264cb12612d46cc8db60b5e34a164d46b021864abbb1d', }, }, ]; @@ -78,54 +62,6 @@ describe('Success cases', () => { 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', () => { diff --git a/packages/stack/src/utils/encrypter.ts b/packages/stack/src/utils/encrypter.ts index fb82ac8..07d44b9 100644 --- a/packages/stack/src/utils/encrypter.ts +++ b/packages/stack/src/utils/encrypter.ts @@ -29,7 +29,9 @@ export class Encrypter { algorithm: 'AES-GCM', keyLength: 256, 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, ...opts, };