import Transaction from "./transaction"; import Header from "./header"; import { BufferReader, BufferChunksReader, Hash } from "./utils"; import { isOutOfBoundsError } from "./utils/errors"; const VERSION_1 = Buffer.from([0, 0, 0, 1]); export interface BlockOptions { validate?: boolean; } export type TxIndex = { tx: Transaction; index: number; // Tx index in block offset: number; // Tx byte offset in block size: number; // Tx size in bytes }; export type BlockStream = { height?: number; size: number; bytesRead: number; bytesRemaining: number; txCount: number; txRead: number; txs: TxIndex[]; finished: boolean; started: boolean; header: Header; startDate: number; }; export default class Block { txRead: number; size: number; options: BlockOptions; merkleArray: Buffer[][]; header?: Header; txCount?: number; txPos?: number; buffer?: Buffer; transactions?: Transaction[]; computedMerkleRoot?: Buffer; br?: BufferChunksReader; height?: number; startDate: number; constructor(options: BlockOptions = {}) { this.txRead = 0; this.size = 0; this.options = options; this.merkleArray = [[]]; this.startDate = +new Date(); } static fromBuffer(buf: Buffer) { const br = new BufferReader(buf); const block = new Block(); block.header = Header.fromBufferReader(br); block.txCount = br.readVarintNum(); block.txPos = br.pos; block.size = buf.length; block.buffer = buf; return block; } static fromHex(hex: string) { const buf = Buffer.from(hex, "hex"); return Block.fromBuffer(buf); } getHash(): Buffer; getHash(hexStr: T): T extends true ? string : Buffer; getHash(hexStr = false) { if (!this.header) throw Error("Missing header"); const hash = this.header.getHash(); return hexStr ? hash.toString("hex") : hash; } getTransactions() { if (this.transactions) return this.transactions; this.transactions = []; const { txPos, txCount } = this; const buf = this.toBuffer(); const br = new BufferReader(buf); if (!txPos) throw Error("Missing txPos"); if (!txCount) throw Error("Missing txCount"); br.read(txPos); // Skip header and txCount for (let i = 0; i < txCount; i++) { const transaction = Transaction.fromBufferReader(br); this.transactions.push(transaction); this.txRead = i + 1; } return this.transactions; } getHeight() { // https://en.bitcoin.it/wiki/BIP_0034 if (!this.header) throw Error("Missing header"); if (!this.txPos) throw Error("Missing txPos"); if (Buffer.compare(VERSION_1, this.header.version) === 0) { throw Error("No height in v1 blocks"); } const buf = this.toBuffer(); const br = new BufferReader(buf); br.read(this.txPos); // Skip header and txCount const transaction = Transaction.fromBufferReader(br); return transaction.getCoinbaseHeight(); } validate() { if (this.computedMerkleRoot) { if (!this.header) throw Error("Missing header"); if ( Buffer.compare(this.computedMerkleRoot, this.header.merkleRoot) !== 0 ) { throw Error(`Invalid merkle root!`); } } else if (this.transactions) { let index = 0; for (const transaction of this.transactions) { this.addMerkleHash(index++, transaction.getHash()); } } else { throw Error(`Must call addMerkleHash on all transactions first`); } } addMerkleHash(index: number, hash: Buffer) { const { merkleArray, computedMerkleRoot, txCount } = this; if (computedMerkleRoot) return; merkleArray[0].push(Buffer.from(hash).reverse()); if (!txCount) throw Error("Missing txCount"); const finished = index + 1 >= txCount; const calculate = (height = 0) => { if ( finished && merkleArray[height].length === 1 && merkleArray.slice(height).length === 1 ) { this.computedMerkleRoot = merkleArray[height][0].reverse(); this.merkleArray = [[]]; this.validate(); return; } if (finished || merkleArray[height].length === 2) { const first = merkleArray[height].shift(); const second = merkleArray[height].shift() || first; if (first && second) { const concat = Buffer.concat([first, second]); const hash = Hash.sha256sha256(concat); if (!merkleArray[height + 1]) merkleArray.push([]); merkleArray[height + 1].push(hash); calculate(height + 1); } } }; calculate(); } async getTransactionsAsync( callback: (data: BlockStream) => Promise | void ) { const { txPos, txCount, size, header, options } = this; if (!header) throw Error("Missing header"); if (!txPos) throw Error("Missing txPos"); if (!txCount) throw Error("Missing txCount"); const startDate = +new Date(); const buf = this.toBuffer(); const br = new BufferReader(buf); br.read(txPos); // Skip header and txCount this.txRead = 0; for (let index = 0; index < txCount; index++) { const tx = Transaction.fromBufferReader(br); this.txRead = index + 1; if (options.validate) { this.addMerkleHash(index, tx.getHash()); } const offset = tx.bufStart; const txSize = tx.length; await callback({ txs: [{ index, tx, offset, size: txSize }], finished: this.finished(), started: index === 0, header, txCount, txRead: this.txRead, size, startDate, bytesRead: br.pos, bytesRemaining: buf.length - br.pos, }); } } toBuffer() { if (!this.buffer) throw Error("Missing buffer"); return this.buffer; } toHex() { return this.toBuffer().toString("hex"); } finished() { if (this.txCount && this.txRead > this.txCount) { throw Error(`Block is corrupted`); } return this.txCount !== undefined && this.txRead === this.txCount; } addBufferChunk(buf: Buffer): BlockStream { if (!this.br) { this.startDate = +new Date(); this.br = new BufferChunksReader(buf); } else { this.br.append(buf); } const startPos = this.br.pos; let started = false; if (!this.header) { if (this.br.length < 80) throw Error(`buffer too small`); this.header = Header.fromBufferReader(this.br); } if (this.txCount === undefined) { this.txCount = this.br.readVarintNum(); started = true; } const txs: TxIndex[] = []; let prePos = this.br.pos; try { for (let index = this.txRead; index < this.txCount; index++) { prePos = this.br.pos; const tx = Transaction.fromBufferReader(this.br); const offset = tx.bufStart; const size = tx.length; txs.push({ index, tx, offset, size }); if (this.options.validate) { this.addMerkleHash(index, tx.getHash()); } if ( index === 0 && Buffer.compare(VERSION_1, this.header.version) !== 0 ) { // https://en.bitcoin.it/wiki/BIP_0034 try { this.height = tx.getCoinbaseHeight(); } catch (err) {} } this.txRead = index + 1; } } catch (err) { if (isOutOfBoundsError(err)) { this.br.rewind(this.br.pos - prePos); } else { throw err; } } const finished = this.finished(); this.br.trim(); this.size = this.br.pos; return { startDate: this.startDate, size: this.size, header: this.header, height: this.height, txs, started, finished, bytesRead: this.br.pos - startPos, bytesRemaining: this.br.length - this.br.pos, txCount: this.txCount, txRead: this.txRead, }; } }