@btc-vision/transaction
Version:
OPNet transaction library allows you to create and sign transactions for the OPNet network.
177 lines (152 loc) • 5.56 kB
text/typescript
import { IChallengeSolution, RawChallenge } from '../interfaces/IChallengeSolution.js';
import { ChallengeSolution } from '../ChallengeSolution.js';
import { crypto } from '@btc-vision/bitcoin';
export class EpochValidator {
private static readonly BLOCKS_PER_EPOCH: bigint = 5n;
/**
* Convert Buffer to Uint8Array
*/
public static bufferToUint8Array(buffer: Buffer): Uint8Array {
return new Uint8Array(buffer);
}
/**
* Convert Uint8Array to Buffer
*/
public static uint8ArrayToBuffer(array: Uint8Array): Buffer {
return Buffer.from(array);
}
/**
* Calculate SHA-1 hash
*/
public static sha1(data: Uint8Array | Buffer): Buffer {
return crypto.sha1(Buffer.isBuffer(data) ? data : Buffer.from(data));
}
/**
* Calculate mining preimage
*/
public static calculatePreimage(checksumRoot: Buffer, publicKey: Buffer, salt: Buffer): Buffer {
// Ensure all are 32 bytes
if (checksumRoot.length !== 32 || publicKey.length !== 32 || salt.length !== 32) {
throw new Error('All inputs must be 32 bytes');
}
const preimage = Buffer.alloc(32);
for (let i = 0; i < 32; i++) {
preimage[i] = checksumRoot[i] ^ publicKey[i] ^ salt[i];
}
return preimage;
}
/**
* Count matching bits between two hashes
*/
public static countMatchingBits(hash1: Buffer, hash2: Buffer): number {
let matchingBits = 0;
if (hash1.length !== hash2.length) {
throw new Error('Hashes must be of the same length');
}
const minLength = Math.min(hash1.length, hash2.length);
for (let i = 0; i < minLength; i++) {
const byte1 = hash1[i];
const byte2 = hash2[i];
if (byte1 === byte2) {
matchingBits += 8;
} else {
// Check individual bits
for (let bit = 7; bit >= 0; bit--) {
if (((byte1 >> bit) & 1) === ((byte2 >> bit) & 1)) {
matchingBits++;
} else {
return matchingBits;
}
}
}
}
return matchingBits;
}
/**
* Verify an epoch solution using IPreimage
*/
public static verifySolution(challenge: IChallengeSolution, log: boolean = false): boolean {
try {
const verification = challenge.verification;
const calculatedPreimage = this.calculatePreimage(
verification.targetChecksum,
challenge.publicKey.toBuffer(),
challenge.salt,
);
const computedSolution = this.sha1(calculatedPreimage);
const computedSolutionBuffer = this.uint8ArrayToBuffer(computedSolution);
if (!computedSolutionBuffer.equals(challenge.solution)) {
return false;
}
const matchingBits = this.countMatchingBits(
computedSolutionBuffer,
verification.targetHash,
);
if (matchingBits !== challenge.difficulty) {
return false;
}
const expectedStartBlock = challenge.epochNumber * this.BLOCKS_PER_EPOCH;
const expectedEndBlock = expectedStartBlock + this.BLOCKS_PER_EPOCH - 1n;
return !(
verification.startBlock !== expectedStartBlock ||
verification.endBlock !== expectedEndBlock
);
} catch (error) {
if (log) console.error('Verification error:', error);
return false;
}
}
/**
* Get the mining target block for an epoch
*/
public static getMiningTargetBlock(epochNumber: bigint): bigint | null {
if (epochNumber === 0n) {
return null; // Epoch 0 cannot be mined
}
// Last block of previous epoch
return epochNumber * this.BLOCKS_PER_EPOCH - 1n;
}
/**
* Validate epoch winner from raw data
*/
public static validateEpochWinner(epochData: RawChallenge): boolean {
const preimage = new ChallengeSolution(epochData);
return this.verifySolution(preimage);
}
/**
* Validate epoch winner from Preimage instance
*/
public static validateChallengeSolution(challenge: IChallengeSolution): boolean {
return this.verifySolution(challenge);
}
/**
* Calculate solution hash from preimage components
* @param targetChecksum The target checksum (32 bytes)
* @param publicKey The public key buffer (32 bytes)
* @param salt The salt buffer (32 bytes)
* @returns The SHA-1 hash of the preimage
*/
public static calculateSolution(
targetChecksum: Buffer,
publicKey: Buffer,
salt: Buffer,
): Buffer {
const preimage = this.calculatePreimage(targetChecksum, publicKey, salt);
const hash = this.sha1(this.bufferToUint8Array(preimage));
return this.uint8ArrayToBuffer(hash);
}
/**
* Check if a solution meets the minimum difficulty requirement
*/
public static checkDifficulty(
solution: Buffer,
targetHash: Buffer,
minDifficulty: number,
): { valid: boolean; difficulty: number } {
const difficulty = this.countMatchingBits(solution, targetHash);
return {
valid: difficulty >= minDifficulty,
difficulty,
};
}
}