From cec5fb53571cc3ca8283b9d315d56675c933a511 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Tue, 14 Jan 2020 09:53:32 +0100 Subject: [PATCH 1/6] Extract BufferWriter class Move various write methods to a class `BufferWriter`. This allows increased code reuse for libraries that want to implement different serialization formats. Also de-duplicates some code paths and increases test coverage. Original concept by idea by https://github.com/argjv: https://github.com/BitGo/bitgo-utxo-lib/blob/master/src/bufferWriter.js --- src/buffer_writer.js | 44 +++++++ src/transaction.js | 129 ++++++++------------- test/buffer_writer.spec.ts | 231 +++++++++++++++++++++++++++++++++++++ ts_src/buffer_writer.ts | 54 +++++++++ ts_src/transaction.ts | 139 +++++++--------------- types/buffer_writer.d.ts | 16 +++ 6 files changed, 432 insertions(+), 181 deletions(-) create mode 100644 src/buffer_writer.js create mode 100644 test/buffer_writer.spec.ts create mode 100644 ts_src/buffer_writer.ts create mode 100644 types/buffer_writer.d.ts diff --git a/src/buffer_writer.js b/src/buffer_writer.js new file mode 100644 index 0000000..19a2332 --- /dev/null +++ b/src/buffer_writer.js @@ -0,0 +1,44 @@ +'use strict'; +Object.defineProperty(exports, '__esModule', { value: true }); +const bufferutils = require('./bufferutils'); +const types = require('./types'); +const typeforce = require('typeforce'); +const varuint = require('varuint-bitcoin'); +/** + * Helper class for serialization of bitcoin data types into a pre-allocated buffer. + */ +class BufferWriter { + constructor(buffer, offset = 0) { + typeforce(types.tuple(types.Buffer, types.UInt32), [buffer, offset]); + this.buffer = buffer; + this.offset = offset; + } + writeUInt8(i) { + this.offset = this.buffer.writeUInt8(i, this.offset); + } + writeInt32(i) { + this.offset = this.buffer.writeInt32LE(i, this.offset); + } + writeUInt32(i) { + this.offset = this.buffer.writeUInt32LE(i, this.offset); + } + writeUInt64(i) { + this.offset = bufferutils.writeUInt64LE(this.buffer, i, this.offset); + } + writeVarInt(i) { + varuint.encode(i, this.buffer, this.offset); + this.offset += varuint.encode.bytes; + } + writeSlice(slice) { + this.offset += slice.copy(this.buffer, this.offset); + } + writeVarSlice(slice) { + this.writeVarInt(slice.length); + this.writeSlice(slice); + } + writeVector(vector) { + this.writeVarInt(vector.length); + vector.forEach(buf => this.writeVarSlice(buf)); + } +} +exports.BufferWriter = BufferWriter; diff --git a/src/transaction.js b/src/transaction.js index e2d6a6b..fc36a63 100644 --- a/src/transaction.js +++ b/src/transaction.js @@ -1,5 +1,6 @@ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); +const buffer_writer_1 = require('./buffer_writer'); const bufferutils = require('./bufferutils'); const bufferutils_1 = require('./bufferutils'); const bcrypto = require('./crypto'); @@ -296,33 +297,16 @@ class Transaction { arguments, ); let tbuffer = Buffer.from([]); - let toffset = 0; - function writeSlice(slice) { - toffset += slice.copy(tbuffer, toffset); - } - function writeUInt32(i) { - toffset = tbuffer.writeUInt32LE(i, toffset); - } - function writeUInt64(i) { - toffset = bufferutils.writeUInt64LE(tbuffer, i, toffset); - } - function writeVarInt(i) { - varuint.encode(i, tbuffer, toffset); - toffset += varuint.encode.bytes; - } - function writeVarSlice(slice) { - writeVarInt(slice.length); - writeSlice(slice); - } + let bufferWriter; let hashOutputs = ZERO; let hashPrevouts = ZERO; let hashSequence = ZERO; if (!(hashType & Transaction.SIGHASH_ANYONECANPAY)) { tbuffer = Buffer.allocUnsafe(36 * this.ins.length); - toffset = 0; + bufferWriter = new buffer_writer_1.BufferWriter(tbuffer, 0); this.ins.forEach(txIn => { - writeSlice(txIn.hash); - writeUInt32(txIn.index); + bufferWriter.writeSlice(txIn.hash); + bufferWriter.writeUInt32(txIn.index); }); hashPrevouts = bcrypto.hash256(tbuffer); } @@ -332,9 +316,9 @@ class Transaction { (hashType & 0x1f) !== Transaction.SIGHASH_NONE ) { tbuffer = Buffer.allocUnsafe(4 * this.ins.length); - toffset = 0; + bufferWriter = new buffer_writer_1.BufferWriter(tbuffer, 0); this.ins.forEach(txIn => { - writeUInt32(txIn.sequence); + bufferWriter.writeUInt32(txIn.sequence); }); hashSequence = bcrypto.hash256(tbuffer); } @@ -346,10 +330,10 @@ class Transaction { return sum + 8 + varSliceSize(output.script); }, 0); tbuffer = Buffer.allocUnsafe(txOutsSize); - toffset = 0; + bufferWriter = new buffer_writer_1.BufferWriter(tbuffer, 0); this.outs.forEach(out => { - writeUInt64(out.value); - writeVarSlice(out.script); + bufferWriter.writeUInt64(out.value); + bufferWriter.writeVarSlice(out.script); }); hashOutputs = bcrypto.hash256(tbuffer); } else if ( @@ -358,25 +342,25 @@ class Transaction { ) { const output = this.outs[inIndex]; tbuffer = Buffer.allocUnsafe(8 + varSliceSize(output.script)); - toffset = 0; - writeUInt64(output.value); - writeVarSlice(output.script); + bufferWriter = new buffer_writer_1.BufferWriter(tbuffer, 0); + bufferWriter.writeUInt64(output.value); + bufferWriter.writeVarSlice(output.script); hashOutputs = bcrypto.hash256(tbuffer); } tbuffer = Buffer.allocUnsafe(156 + varSliceSize(prevOutScript)); - toffset = 0; + bufferWriter = new buffer_writer_1.BufferWriter(tbuffer, 0); const input = this.ins[inIndex]; - writeUInt32(this.version); - writeSlice(hashPrevouts); - writeSlice(hashSequence); - writeSlice(input.hash); - writeUInt32(input.index); - writeVarSlice(prevOutScript); - writeUInt64(value); - writeUInt32(input.sequence); - writeSlice(hashOutputs); - writeUInt32(this.locktime); - writeUInt32(hashType); + bufferWriter.writeUInt32(this.version); + bufferWriter.writeSlice(hashPrevouts); + bufferWriter.writeSlice(hashSequence); + bufferWriter.writeSlice(input.hash); + bufferWriter.writeUInt32(input.index); + bufferWriter.writeVarSlice(prevOutScript); + bufferWriter.writeUInt64(value); + bufferWriter.writeUInt32(input.sequence); + bufferWriter.writeSlice(hashOutputs); + bufferWriter.writeUInt32(this.locktime); + bufferWriter.writeUInt32(hashType); return bcrypto.hash256(tbuffer); } getHash(forWitness) { @@ -404,64 +388,41 @@ class Transaction { } __toBuffer(buffer, initialOffset, _ALLOW_WITNESS = false) { if (!buffer) buffer = Buffer.allocUnsafe(this.byteLength(_ALLOW_WITNESS)); - let offset = initialOffset || 0; - function writeSlice(slice) { - offset += slice.copy(buffer, offset); - } - function writeUInt8(i) { - offset = buffer.writeUInt8(i, offset); - } - function writeUInt32(i) { - offset = buffer.writeUInt32LE(i, offset); - } - function writeInt32(i) { - offset = buffer.writeInt32LE(i, offset); - } - function writeUInt64(i) { - offset = bufferutils.writeUInt64LE(buffer, i, offset); - } - function writeVarInt(i) { - varuint.encode(i, buffer, offset); - offset += varuint.encode.bytes; - } - function writeVarSlice(slice) { - writeVarInt(slice.length); - writeSlice(slice); - } - function writeVector(vector) { - writeVarInt(vector.length); - vector.forEach(writeVarSlice); - } - writeInt32(this.version); + const bufferWriter = new buffer_writer_1.BufferWriter( + buffer, + initialOffset || 0, + ); + bufferWriter.writeInt32(this.version); const hasWitnesses = _ALLOW_WITNESS && this.hasWitnesses(); if (hasWitnesses) { - writeUInt8(Transaction.ADVANCED_TRANSACTION_MARKER); - writeUInt8(Transaction.ADVANCED_TRANSACTION_FLAG); + bufferWriter.writeUInt8(Transaction.ADVANCED_TRANSACTION_MARKER); + bufferWriter.writeUInt8(Transaction.ADVANCED_TRANSACTION_FLAG); } - writeVarInt(this.ins.length); + bufferWriter.writeVarInt(this.ins.length); this.ins.forEach(txIn => { - writeSlice(txIn.hash); - writeUInt32(txIn.index); - writeVarSlice(txIn.script); - writeUInt32(txIn.sequence); + bufferWriter.writeSlice(txIn.hash); + bufferWriter.writeUInt32(txIn.index); + bufferWriter.writeVarSlice(txIn.script); + bufferWriter.writeUInt32(txIn.sequence); }); - writeVarInt(this.outs.length); + bufferWriter.writeVarInt(this.outs.length); this.outs.forEach(txOut => { if (isOutput(txOut)) { - writeUInt64(txOut.value); + bufferWriter.writeUInt64(txOut.value); } else { - writeSlice(txOut.valueBuffer); + bufferWriter.writeSlice(txOut.valueBuffer); } - writeVarSlice(txOut.script); + bufferWriter.writeVarSlice(txOut.script); }); if (hasWitnesses) { this.ins.forEach(input => { - writeVector(input.witness); + bufferWriter.writeVector(input.witness); }); } - writeUInt32(this.locktime); + bufferWriter.writeUInt32(this.locktime); // avoid slicing unless necessary - if (initialOffset !== undefined) return buffer.slice(initialOffset, offset); + if (initialOffset !== undefined) + return buffer.slice(initialOffset, bufferWriter.offset); return buffer; } } diff --git a/test/buffer_writer.spec.ts b/test/buffer_writer.spec.ts new file mode 100644 index 0000000..8b33e2e --- /dev/null +++ b/test/buffer_writer.spec.ts @@ -0,0 +1,231 @@ +import * as assert from 'assert'; +import { describe, it } from 'mocha'; +import { BufferWriter } from '../src/buffer_writer'; + +const varuint = require('varuint-bitcoin'); + +describe('BufferWriter', () => { + function concatToBuffer(values: number[][]): Buffer { + return Buffer.concat(values.map(data => Buffer.from(data))); + } + + function testBuffer( + bufferWriter: BufferWriter, + expectedBuffer: Buffer, + expectedOffset: number = expectedBuffer.length, + ): void { + assert.strictEqual(bufferWriter.offset, expectedOffset); + assert.deepStrictEqual( + bufferWriter.buffer.slice(0, expectedOffset), + expectedBuffer.slice(0, expectedOffset), + ); + } + + it('writeUint8', () => { + const values = [0, 1, 254, 255]; + const expectedBuffer = Buffer.from([0, 1, 0xfe, 0xff]); + const bufferWriter = new BufferWriter( + Buffer.allocUnsafe(expectedBuffer.length), + ); + values.forEach((v: number) => { + const expectedOffset = bufferWriter.offset + 1; + bufferWriter.writeUInt8(v); + testBuffer(bufferWriter, expectedBuffer, expectedOffset); + }); + testBuffer(bufferWriter, expectedBuffer); + }); + + it('writeInt32', () => { + const values = [ + 0, + 1, + Math.pow(2, 31) - 2, + Math.pow(2, 31) - 1, + -1, + -Math.pow(2, 31), + ]; + const expectedBuffer = concatToBuffer([ + [0, 0, 0, 0], + [1, 0, 0, 0], + [0xfe, 0xff, 0xff, 0x7f], + [0xff, 0xff, 0xff, 0x7f], + [0xff, 0xff, 0xff, 0xff], + [0x00, 0x00, 0x00, 0x80], + ]); + const bufferWriter = new BufferWriter( + Buffer.allocUnsafe(expectedBuffer.length), + ); + values.forEach((value: number) => { + const expectedOffset = bufferWriter.offset + 4; + bufferWriter.writeInt32(value); + testBuffer(bufferWriter, expectedBuffer, expectedOffset); + }); + testBuffer(bufferWriter, expectedBuffer); + }); + + it('writeUInt32', () => { + const maxUInt32 = Math.pow(2, 32) - 1; + const values = [0, 1, Math.pow(2, 16), maxUInt32]; + const expectedBuffer = concatToBuffer([ + [0, 0, 0, 0], + [1, 0, 0, 0], + [0, 0, 1, 0], + [0xff, 0xff, 0xff, 0xff], + ]); + const bufferWriter = new BufferWriter( + Buffer.allocUnsafe(expectedBuffer.length), + ); + values.forEach((value: number) => { + const expectedOffset = bufferWriter.offset + 4; + bufferWriter.writeUInt32(value); + testBuffer(bufferWriter, expectedBuffer, expectedOffset); + }); + testBuffer(bufferWriter, expectedBuffer); + }); + + it('writeUInt64', () => { + const values = [ + 0, + 1, + Math.pow(2, 32), + Number.MAX_SAFE_INTEGER /* 2^53 - 1 */, + ]; + const expectedBuffer = concatToBuffer([ + [0, 0, 0, 0, 0, 0, 0, 0], + [1, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0], + [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x1f, 0x00], + ]); + const bufferWriter = new BufferWriter( + Buffer.allocUnsafe(expectedBuffer.length), + ); + values.forEach((value: number) => { + const expectedOffset = bufferWriter.offset + 8; + bufferWriter.writeUInt64(value); + testBuffer(bufferWriter, expectedBuffer, expectedOffset); + }); + testBuffer(bufferWriter, expectedBuffer); + }); + + it('writeVarInt', () => { + const values = [ + 0, + 1, + 252, + 253, + 254, + 255, + 256, + Math.pow(2, 16) - 2, + Math.pow(2, 16) - 1, + Math.pow(2, 16), + Math.pow(2, 32) - 2, + Math.pow(2, 32) - 1, + Math.pow(2, 32), + Number.MAX_SAFE_INTEGER, + ]; + const expectedBuffer = concatToBuffer([ + [0x00], + [0x01], + [0xfc], + [0xfd, 0xfd, 0x00], + [0xfd, 0xfe, 0x00], + [0xfd, 0xff, 0x00], + [0xfd, 0x00, 0x01], + [0xfd, 0xfe, 0xff], + [0xfd, 0xff, 0xff], + [0xfe, 0x00, 0x00, 0x01, 0x00], + [0xfe, 0xfe, 0xff, 0xff, 0xff], + [0xfe, 0xff, 0xff, 0xff, 0xff], + [0xff, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00], + [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x1f, 0x00], + ]); + const bufferWriter = new BufferWriter( + Buffer.allocUnsafe(expectedBuffer.length), + ); + values.forEach((value: number) => { + const expectedOffset = + bufferWriter.offset + varuint.encodingLength(value); + bufferWriter.writeVarInt(value); + testBuffer(bufferWriter, expectedBuffer, expectedOffset); + }); + testBuffer(bufferWriter, expectedBuffer); + }); + + it('writeSlice', () => { + const values = [[], [1], [1, 2, 3, 4], [254, 255]]; + const expectedBuffer = concatToBuffer(values); + const bufferWriter = new BufferWriter( + Buffer.allocUnsafe(expectedBuffer.length), + ); + values.forEach((v: number[]) => { + const expectedOffset = bufferWriter.offset + v.length; + bufferWriter.writeSlice(Buffer.from(v)); + testBuffer(bufferWriter, expectedBuffer, expectedOffset); + }); + testBuffer(bufferWriter, expectedBuffer); + }); + + it('writeVarSlice', () => { + const values = [ + Buffer.alloc(1, 1), + Buffer.alloc(252, 2), + Buffer.alloc(253, 3), + ]; + const expectedBuffer = Buffer.concat([ + Buffer.from([0x01, 0x01]), + Buffer.from([0xfc]), + Buffer.alloc(252, 0x02), + Buffer.from([0xfd, 0xfd, 0x00]), + Buffer.alloc(253, 0x03), + ]); + const bufferWriter = new BufferWriter( + Buffer.allocUnsafe(expectedBuffer.length), + ); + values.forEach((value: Buffer) => { + const expectedOffset = + bufferWriter.offset + + varuint.encodingLength(value.length) + + value.length; + bufferWriter.writeVarSlice(value); + testBuffer(bufferWriter, expectedBuffer, expectedOffset); + }); + testBuffer(bufferWriter, expectedBuffer); + }); + + it('writeVector', () => { + const values = [ + [Buffer.alloc(1, 4), Buffer.alloc(253, 5)], + Array(253).fill(Buffer.alloc(1, 6)), + ]; + const expectedBuffer = Buffer.concat([ + Buffer.from([0x02]), + Buffer.from([0x01, 0x04]), + Buffer.from([0xfd, 0xfd, 0x00]), + Buffer.alloc(253, 5), + + Buffer.from([0xfd, 0xfd, 0x00]), + Buffer.concat( + Array(253) + .fill(0) + .map(() => Buffer.from([0x01, 0x06])), + ), + ]); + + const bufferWriter = new BufferWriter( + Buffer.allocUnsafe(expectedBuffer.length), + ); + values.forEach((value: Buffer[]) => { + const expectedOffset = + bufferWriter.offset + + varuint.encodingLength(value.length) + + value.reduce( + (sum: number, v) => sum + varuint.encodingLength(v.length) + v.length, + 0, + ); + bufferWriter.writeVector(value); + testBuffer(bufferWriter, expectedBuffer, expectedOffset); + }); + testBuffer(bufferWriter, expectedBuffer); + }); +}); diff --git a/ts_src/buffer_writer.ts b/ts_src/buffer_writer.ts new file mode 100644 index 0000000..541457d --- /dev/null +++ b/ts_src/buffer_writer.ts @@ -0,0 +1,54 @@ +import * as bufferutils from './bufferutils'; +import * as types from './types'; + +const typeforce = require('typeforce'); +const varuint = require('varuint-bitcoin'); + +/** + * Helper class for serialization of bitcoin data types into a pre-allocated buffer. + */ +export class BufferWriter { + buffer: Buffer; + offset: number; + + constructor(buffer: Buffer, offset: number = 0) { + typeforce(types.tuple(types.Buffer, types.UInt32), [buffer, offset]); + this.buffer = buffer; + this.offset = offset; + } + + writeUInt8(i: number): void { + this.offset = this.buffer.writeUInt8(i, this.offset); + } + + writeInt32(i: number): void { + this.offset = this.buffer.writeInt32LE(i, this.offset); + } + + writeUInt32(i: number): void { + this.offset = this.buffer.writeUInt32LE(i, this.offset); + } + + writeUInt64(i: number): void { + this.offset = bufferutils.writeUInt64LE(this.buffer, i, this.offset); + } + + writeVarInt(i: number): void { + varuint.encode(i, this.buffer, this.offset); + this.offset += varuint.encode.bytes; + } + + writeSlice(slice: Buffer): void { + this.offset += slice.copy(this.buffer, this.offset); + } + + writeVarSlice(slice: Buffer): void { + this.writeVarInt(slice.length); + this.writeSlice(slice); + } + + writeVector(vector: Buffer[]): void { + this.writeVarInt(vector.length); + vector.forEach((buf: Buffer) => this.writeVarSlice(buf)); + } +} diff --git a/ts_src/transaction.ts b/ts_src/transaction.ts index 88e9242..ac5aea4 100644 --- a/ts_src/transaction.ts +++ b/ts_src/transaction.ts @@ -1,3 +1,4 @@ +import { BufferWriter } from './buffer_writer'; import * as bufferutils from './bufferutils'; import { reverseBuffer } from './bufferutils'; import * as bcrypto from './crypto'; @@ -388,29 +389,7 @@ export class Transaction { ); let tbuffer: Buffer = Buffer.from([]); - let toffset: number = 0; - - function writeSlice(slice: Buffer): void { - toffset += slice.copy(tbuffer, toffset); - } - - function writeUInt32(i: number): void { - toffset = tbuffer.writeUInt32LE(i, toffset); - } - - function writeUInt64(i: number): void { - toffset = bufferutils.writeUInt64LE(tbuffer, i, toffset); - } - - function writeVarInt(i: number): void { - varuint.encode(i, tbuffer, toffset); - toffset += varuint.encode.bytes; - } - - function writeVarSlice(slice: Buffer): void { - writeVarInt(slice.length); - writeSlice(slice); - } + let bufferWriter: BufferWriter; let hashOutputs = ZERO; let hashPrevouts = ZERO; @@ -418,11 +397,11 @@ export class Transaction { if (!(hashType & Transaction.SIGHASH_ANYONECANPAY)) { tbuffer = Buffer.allocUnsafe(36 * this.ins.length); - toffset = 0; + bufferWriter = new BufferWriter(tbuffer, 0); this.ins.forEach(txIn => { - writeSlice(txIn.hash); - writeUInt32(txIn.index); + bufferWriter.writeSlice(txIn.hash); + bufferWriter.writeUInt32(txIn.index); }); hashPrevouts = bcrypto.hash256(tbuffer); @@ -434,10 +413,10 @@ export class Transaction { (hashType & 0x1f) !== Transaction.SIGHASH_NONE ) { tbuffer = Buffer.allocUnsafe(4 * this.ins.length); - toffset = 0; + bufferWriter = new BufferWriter(tbuffer, 0); this.ins.forEach(txIn => { - writeUInt32(txIn.sequence); + bufferWriter.writeUInt32(txIn.sequence); }); hashSequence = bcrypto.hash256(tbuffer); @@ -452,11 +431,11 @@ export class Transaction { }, 0); tbuffer = Buffer.allocUnsafe(txOutsSize); - toffset = 0; + bufferWriter = new BufferWriter(tbuffer, 0); this.outs.forEach(out => { - writeUInt64(out.value); - writeVarSlice(out.script); + bufferWriter.writeUInt64(out.value); + bufferWriter.writeVarSlice(out.script); }); hashOutputs = bcrypto.hash256(tbuffer); @@ -467,28 +446,28 @@ export class Transaction { const output = this.outs[inIndex]; tbuffer = Buffer.allocUnsafe(8 + varSliceSize(output.script)); - toffset = 0; - writeUInt64(output.value); - writeVarSlice(output.script); + bufferWriter = new BufferWriter(tbuffer, 0); + bufferWriter.writeUInt64(output.value); + bufferWriter.writeVarSlice(output.script); hashOutputs = bcrypto.hash256(tbuffer); } tbuffer = Buffer.allocUnsafe(156 + varSliceSize(prevOutScript)); - toffset = 0; + bufferWriter = new BufferWriter(tbuffer, 0); const input = this.ins[inIndex]; - writeUInt32(this.version); - writeSlice(hashPrevouts); - writeSlice(hashSequence); - writeSlice(input.hash); - writeUInt32(input.index); - writeVarSlice(prevOutScript); - writeUInt64(value); - writeUInt32(input.sequence); - writeSlice(hashOutputs); - writeUInt32(this.locktime); - writeUInt32(hashType); + bufferWriter.writeUInt32(this.version); + bufferWriter.writeSlice(hashPrevouts); + bufferWriter.writeSlice(hashSequence); + bufferWriter.writeSlice(input.hash); + bufferWriter.writeUInt32(input.index); + bufferWriter.writeVarSlice(prevOutScript); + bufferWriter.writeUInt64(value); + bufferWriter.writeUInt32(input.sequence); + bufferWriter.writeSlice(hashOutputs); + bufferWriter.writeUInt32(this.locktime); + bufferWriter.writeUInt32(hashType); return bcrypto.hash256(tbuffer); } @@ -531,82 +510,48 @@ export class Transaction { if (!buffer) buffer = Buffer.allocUnsafe(this.byteLength(_ALLOW_WITNESS)) as Buffer; - let offset = initialOffset || 0; - - function writeSlice(slice: Buffer): void { - offset += slice.copy(buffer!, offset); - } - - function writeUInt8(i: number): void { - offset = buffer!.writeUInt8(i, offset); - } - - function writeUInt32(i: number): void { - offset = buffer!.writeUInt32LE(i, offset); - } - - function writeInt32(i: number): void { - offset = buffer!.writeInt32LE(i, offset); - } - - function writeUInt64(i: number): void { - offset = bufferutils.writeUInt64LE(buffer!, i, offset); - } - - function writeVarInt(i: number): void { - varuint.encode(i, buffer, offset); - offset += varuint.encode.bytes; - } - - function writeVarSlice(slice: Buffer): void { - writeVarInt(slice.length); - writeSlice(slice); - } - - function writeVector(vector: Buffer[]): void { - writeVarInt(vector.length); - vector.forEach(writeVarSlice); - } + const bufferWriter = new BufferWriter(buffer, initialOffset || 0); - writeInt32(this.version); + bufferWriter.writeInt32(this.version); const hasWitnesses = _ALLOW_WITNESS && this.hasWitnesses(); if (hasWitnesses) { - writeUInt8(Transaction.ADVANCED_TRANSACTION_MARKER); - writeUInt8(Transaction.ADVANCED_TRANSACTION_FLAG); + bufferWriter.writeUInt8(Transaction.ADVANCED_TRANSACTION_MARKER); + bufferWriter.writeUInt8(Transaction.ADVANCED_TRANSACTION_FLAG); } - writeVarInt(this.ins.length); + bufferWriter.writeVarInt(this.ins.length); this.ins.forEach(txIn => { - writeSlice(txIn.hash); - writeUInt32(txIn.index); - writeVarSlice(txIn.script); - writeUInt32(txIn.sequence); + bufferWriter.writeSlice(txIn.hash); + bufferWriter.writeUInt32(txIn.index); + bufferWriter.writeVarSlice(txIn.script); + bufferWriter.writeUInt32(txIn.sequence); }); - writeVarInt(this.outs.length); + bufferWriter.writeVarInt(this.outs.length); this.outs.forEach(txOut => { if (isOutput(txOut)) { - writeUInt64(txOut.value); + bufferWriter.writeUInt64(txOut.value); } else { - writeSlice((txOut as any).valueBuffer); + bufferWriter.writeSlice((txOut as any).valueBuffer); } - writeVarSlice(txOut.script); + bufferWriter.writeVarSlice(txOut.script); }); if (hasWitnesses) { this.ins.forEach(input => { - writeVector(input.witness); + bufferWriter.writeVector(input.witness); }); } - writeUInt32(this.locktime); + bufferWriter.writeUInt32(this.locktime); // avoid slicing unless necessary - if (initialOffset !== undefined) return buffer.slice(initialOffset, offset); + if (initialOffset !== undefined) + return buffer.slice(initialOffset, bufferWriter.offset); return buffer; } } diff --git a/types/buffer_writer.d.ts b/types/buffer_writer.d.ts new file mode 100644 index 0000000..5ff07f9 --- /dev/null +++ b/types/buffer_writer.d.ts @@ -0,0 +1,16 @@ +/** + * Helper class for serialization of bitcoin data types into a pre-allocated buffer. + */ +export declare class BufferWriter { + buffer: Buffer; + offset: number; + constructor(buffer: Buffer, offset?: number); + writeUInt8(i: number): void; + writeInt32(i: number): void; + writeUInt32(i: number): void; + writeUInt64(i: number): void; + writeVarInt(i: number): void; + writeSlice(slice: Buffer): void; + writeVarSlice(slice: Buffer): void; + writeVector(vector: Buffer[]): void; +} From 91e6c8abc3517571eca08fefe13adc433394d2bb Mon Sep 17 00:00:00 2001 From: junderw Date: Wed, 15 Jan 2020 10:44:29 +0900 Subject: [PATCH 2/6] Move to bufferutils and add BufferReader --- src/buffer_writer.js | 44 ----------------- src/bufferutils.js | 90 +++++++++++++++++++++++++++++++++ src/transaction.js | 13 +++-- ts_src/buffer_writer.ts | 54 -------------------- ts_src/bufferutils.ts | 104 +++++++++++++++++++++++++++++++++++++++ ts_src/transaction.ts | 3 +- types/buffer_writer.d.ts | 16 ------ types/bufferutils.d.ts | 32 ++++++++++++ 8 files changed, 233 insertions(+), 123 deletions(-) delete mode 100644 src/buffer_writer.js delete mode 100644 ts_src/buffer_writer.ts delete mode 100644 types/buffer_writer.d.ts diff --git a/src/buffer_writer.js b/src/buffer_writer.js deleted file mode 100644 index 19a2332..0000000 --- a/src/buffer_writer.js +++ /dev/null @@ -1,44 +0,0 @@ -'use strict'; -Object.defineProperty(exports, '__esModule', { value: true }); -const bufferutils = require('./bufferutils'); -const types = require('./types'); -const typeforce = require('typeforce'); -const varuint = require('varuint-bitcoin'); -/** - * Helper class for serialization of bitcoin data types into a pre-allocated buffer. - */ -class BufferWriter { - constructor(buffer, offset = 0) { - typeforce(types.tuple(types.Buffer, types.UInt32), [buffer, offset]); - this.buffer = buffer; - this.offset = offset; - } - writeUInt8(i) { - this.offset = this.buffer.writeUInt8(i, this.offset); - } - writeInt32(i) { - this.offset = this.buffer.writeInt32LE(i, this.offset); - } - writeUInt32(i) { - this.offset = this.buffer.writeUInt32LE(i, this.offset); - } - writeUInt64(i) { - this.offset = bufferutils.writeUInt64LE(this.buffer, i, this.offset); - } - writeVarInt(i) { - varuint.encode(i, this.buffer, this.offset); - this.offset += varuint.encode.bytes; - } - writeSlice(slice) { - this.offset += slice.copy(this.buffer, this.offset); - } - writeVarSlice(slice) { - this.writeVarInt(slice.length); - this.writeSlice(slice); - } - writeVector(vector) { - this.writeVarInt(vector.length); - vector.forEach(buf => this.writeVarSlice(buf)); - } -} -exports.BufferWriter = BufferWriter; diff --git a/src/bufferutils.js b/src/bufferutils.js index 54ce1c9..19e8763 100644 --- a/src/bufferutils.js +++ b/src/bufferutils.js @@ -1,5 +1,8 @@ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); +const types = require('./types'); +const typeforce = require('typeforce'); +const varuint = require('varuint-bitcoin'); // https://github.com/feross/buffer/blob/master/index.js#L1127 function verifuint(value, max) { if (typeof value !== 'number') @@ -38,3 +41,90 @@ function reverseBuffer(buffer) { return buffer; } exports.reverseBuffer = reverseBuffer; +/** + * Helper class for serialization of bitcoin data types into a pre-allocated buffer. + */ +class BufferWriter { + constructor(buffer, offset = 0) { + this.buffer = buffer; + this.offset = offset; + typeforce(types.tuple(types.Buffer, types.UInt32), [buffer, offset]); + } + writeUInt8(i) { + this.offset = this.buffer.writeUInt8(i, this.offset); + } + writeInt32(i) { + this.offset = this.buffer.writeInt32LE(i, this.offset); + } + writeUInt32(i) { + this.offset = this.buffer.writeUInt32LE(i, this.offset); + } + writeUInt64(i) { + this.offset = writeUInt64LE(this.buffer, i, this.offset); + } + writeVarInt(i) { + varuint.encode(i, this.buffer, this.offset); + this.offset += varuint.encode.bytes; + } + writeSlice(slice) { + this.offset += slice.copy(this.buffer, this.offset); + } + writeVarSlice(slice) { + this.writeVarInt(slice.length); + this.writeSlice(slice); + } + writeVector(vector) { + this.writeVarInt(vector.length); + vector.forEach(buf => this.writeVarSlice(buf)); + } +} +exports.BufferWriter = BufferWriter; +/** + * Helper class for serialization of bitcoin data types into a pre-allocated buffer. + */ +class BufferReader { + constructor(buffer, offset = 0) { + this.buffer = buffer; + this.offset = offset; + typeforce(types.tuple(types.Buffer, types.UInt32), [buffer, offset]); + } + readUInt8() { + const result = this.buffer.readUInt8(this.offset); + this.offset++; + return result; + } + readInt32() { + const result = this.buffer.readInt32LE(this.offset); + this.offset += 4; + return result; + } + readUInt32() { + const result = this.buffer.readUInt32LE(this.offset); + this.offset += 4; + return result; + } + readUInt64() { + const result = readUInt64LE(this.buffer, this.offset); + this.offset += 8; + return result; + } + readVarInt() { + const vi = varuint.decode(this.buffer, this.offset); + this.offset += varuint.decode.bytes; + return vi; + } + readSlice(n) { + this.offset += n; + return this.buffer.slice(this.offset - n, this.offset); + } + readVarSlice() { + return this.readSlice(this.readVarInt()); + } + readVector() { + const count = this.readVarInt(); + const vector = []; + for (let i = 0; i < count; i++) vector.push(this.readVarSlice()); + return vector; + } +} +exports.BufferReader = BufferReader; diff --git a/src/transaction.js b/src/transaction.js index fc36a63..ef97900 100644 --- a/src/transaction.js +++ b/src/transaction.js @@ -1,6 +1,5 @@ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); -const buffer_writer_1 = require('./buffer_writer'); const bufferutils = require('./bufferutils'); const bufferutils_1 = require('./bufferutils'); const bcrypto = require('./crypto'); @@ -303,7 +302,7 @@ class Transaction { let hashSequence = ZERO; if (!(hashType & Transaction.SIGHASH_ANYONECANPAY)) { tbuffer = Buffer.allocUnsafe(36 * this.ins.length); - bufferWriter = new buffer_writer_1.BufferWriter(tbuffer, 0); + bufferWriter = new bufferutils_1.BufferWriter(tbuffer, 0); this.ins.forEach(txIn => { bufferWriter.writeSlice(txIn.hash); bufferWriter.writeUInt32(txIn.index); @@ -316,7 +315,7 @@ class Transaction { (hashType & 0x1f) !== Transaction.SIGHASH_NONE ) { tbuffer = Buffer.allocUnsafe(4 * this.ins.length); - bufferWriter = new buffer_writer_1.BufferWriter(tbuffer, 0); + bufferWriter = new bufferutils_1.BufferWriter(tbuffer, 0); this.ins.forEach(txIn => { bufferWriter.writeUInt32(txIn.sequence); }); @@ -330,7 +329,7 @@ class Transaction { return sum + 8 + varSliceSize(output.script); }, 0); tbuffer = Buffer.allocUnsafe(txOutsSize); - bufferWriter = new buffer_writer_1.BufferWriter(tbuffer, 0); + bufferWriter = new bufferutils_1.BufferWriter(tbuffer, 0); this.outs.forEach(out => { bufferWriter.writeUInt64(out.value); bufferWriter.writeVarSlice(out.script); @@ -342,13 +341,13 @@ class Transaction { ) { const output = this.outs[inIndex]; tbuffer = Buffer.allocUnsafe(8 + varSliceSize(output.script)); - bufferWriter = new buffer_writer_1.BufferWriter(tbuffer, 0); + bufferWriter = new bufferutils_1.BufferWriter(tbuffer, 0); bufferWriter.writeUInt64(output.value); bufferWriter.writeVarSlice(output.script); hashOutputs = bcrypto.hash256(tbuffer); } tbuffer = Buffer.allocUnsafe(156 + varSliceSize(prevOutScript)); - bufferWriter = new buffer_writer_1.BufferWriter(tbuffer, 0); + bufferWriter = new bufferutils_1.BufferWriter(tbuffer, 0); const input = this.ins[inIndex]; bufferWriter.writeUInt32(this.version); bufferWriter.writeSlice(hashPrevouts); @@ -388,7 +387,7 @@ class Transaction { } __toBuffer(buffer, initialOffset, _ALLOW_WITNESS = false) { if (!buffer) buffer = Buffer.allocUnsafe(this.byteLength(_ALLOW_WITNESS)); - const bufferWriter = new buffer_writer_1.BufferWriter( + const bufferWriter = new bufferutils_1.BufferWriter( buffer, initialOffset || 0, ); diff --git a/ts_src/buffer_writer.ts b/ts_src/buffer_writer.ts deleted file mode 100644 index 541457d..0000000 --- a/ts_src/buffer_writer.ts +++ /dev/null @@ -1,54 +0,0 @@ -import * as bufferutils from './bufferutils'; -import * as types from './types'; - -const typeforce = require('typeforce'); -const varuint = require('varuint-bitcoin'); - -/** - * Helper class for serialization of bitcoin data types into a pre-allocated buffer. - */ -export class BufferWriter { - buffer: Buffer; - offset: number; - - constructor(buffer: Buffer, offset: number = 0) { - typeforce(types.tuple(types.Buffer, types.UInt32), [buffer, offset]); - this.buffer = buffer; - this.offset = offset; - } - - writeUInt8(i: number): void { - this.offset = this.buffer.writeUInt8(i, this.offset); - } - - writeInt32(i: number): void { - this.offset = this.buffer.writeInt32LE(i, this.offset); - } - - writeUInt32(i: number): void { - this.offset = this.buffer.writeUInt32LE(i, this.offset); - } - - writeUInt64(i: number): void { - this.offset = bufferutils.writeUInt64LE(this.buffer, i, this.offset); - } - - writeVarInt(i: number): void { - varuint.encode(i, this.buffer, this.offset); - this.offset += varuint.encode.bytes; - } - - writeSlice(slice: Buffer): void { - this.offset += slice.copy(this.buffer, this.offset); - } - - writeVarSlice(slice: Buffer): void { - this.writeVarInt(slice.length); - this.writeSlice(slice); - } - - writeVector(vector: Buffer[]): void { - this.writeVarInt(vector.length); - vector.forEach((buf: Buffer) => this.writeVarSlice(buf)); - } -} diff --git a/ts_src/bufferutils.ts b/ts_src/bufferutils.ts index adb6060..6904b73 100644 --- a/ts_src/bufferutils.ts +++ b/ts_src/bufferutils.ts @@ -1,3 +1,8 @@ +import * as types from './types'; + +const typeforce = require('typeforce'); +const varuint = require('varuint-bitcoin'); + // https://github.com/feross/buffer/blob/master/index.js#L1127 function verifuint(value: number, max: number): void { if (typeof value !== 'number') @@ -42,3 +47,102 @@ export function reverseBuffer(buffer: Buffer): Buffer { } return buffer; } + +/** + * Helper class for serialization of bitcoin data types into a pre-allocated buffer. + */ +export class BufferWriter { + constructor(private buffer: Buffer, public offset: number = 0) { + typeforce(types.tuple(types.Buffer, types.UInt32), [buffer, offset]); + } + + writeUInt8(i: number): void { + this.offset = this.buffer.writeUInt8(i, this.offset); + } + + writeInt32(i: number): void { + this.offset = this.buffer.writeInt32LE(i, this.offset); + } + + writeUInt32(i: number): void { + this.offset = this.buffer.writeUInt32LE(i, this.offset); + } + + writeUInt64(i: number): void { + this.offset = writeUInt64LE(this.buffer, i, this.offset); + } + + writeVarInt(i: number): void { + varuint.encode(i, this.buffer, this.offset); + this.offset += varuint.encode.bytes; + } + + writeSlice(slice: Buffer): void { + this.offset += slice.copy(this.buffer, this.offset); + } + + writeVarSlice(slice: Buffer): void { + this.writeVarInt(slice.length); + this.writeSlice(slice); + } + + writeVector(vector: Buffer[]): void { + this.writeVarInt(vector.length); + vector.forEach((buf: Buffer) => this.writeVarSlice(buf)); + } +} + +/** + * Helper class for serialization of bitcoin data types into a pre-allocated buffer. + */ +export class BufferReader { + constructor(private buffer: Buffer, public offset: number = 0) { + typeforce(types.tuple(types.Buffer, types.UInt32), [buffer, offset]); + } + + readUInt8(): number { + const result = this.buffer.readUInt8(this.offset); + this.offset++; + return result; + } + + readInt32(): number { + const result = this.buffer.readInt32LE(this.offset); + this.offset += 4; + return result; + } + + readUInt32(): number { + const result = this.buffer.readUInt32LE(this.offset); + this.offset += 4; + return result; + } + + readUInt64(): number { + const result = readUInt64LE(this.buffer, this.offset); + this.offset += 8; + return result; + } + + readVarInt(): number { + const vi = varuint.decode(this.buffer, this.offset); + this.offset += varuint.decode.bytes; + return vi; + } + + readSlice(n: number): Buffer { + this.offset += n; + return this.buffer.slice(this.offset - n, this.offset); + } + + readVarSlice(): Buffer { + return this.readSlice(this.readVarInt()); + } + + readVector(): Buffer[] { + const count = this.readVarInt(); + const vector: Buffer[] = []; + for (let i = 0; i < count; i++) vector.push(this.readVarSlice()); + return vector; + } +} diff --git a/ts_src/transaction.ts b/ts_src/transaction.ts index ac5aea4..3b792ec 100644 --- a/ts_src/transaction.ts +++ b/ts_src/transaction.ts @@ -1,6 +1,5 @@ -import { BufferWriter } from './buffer_writer'; import * as bufferutils from './bufferutils'; -import { reverseBuffer } from './bufferutils'; +import { BufferWriter, reverseBuffer } from './bufferutils'; import * as bcrypto from './crypto'; import * as bscript from './script'; import { OPS as opcodes } from './script'; diff --git a/types/buffer_writer.d.ts b/types/buffer_writer.d.ts deleted file mode 100644 index 5ff07f9..0000000 --- a/types/buffer_writer.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Helper class for serialization of bitcoin data types into a pre-allocated buffer. - */ -export declare class BufferWriter { - buffer: Buffer; - offset: number; - constructor(buffer: Buffer, offset?: number); - writeUInt8(i: number): void; - writeInt32(i: number): void; - writeUInt32(i: number): void; - writeUInt64(i: number): void; - writeVarInt(i: number): void; - writeSlice(slice: Buffer): void; - writeVarSlice(slice: Buffer): void; - writeVector(vector: Buffer[]): void; -} diff --git a/types/bufferutils.d.ts b/types/bufferutils.d.ts index 1eff78d..d9e6dc0 100644 --- a/types/bufferutils.d.ts +++ b/types/bufferutils.d.ts @@ -1,3 +1,35 @@ export declare function readUInt64LE(buffer: Buffer, offset: number): number; export declare function writeUInt64LE(buffer: Buffer, value: number, offset: number): number; export declare function reverseBuffer(buffer: Buffer): Buffer; +/** + * Helper class for serialization of bitcoin data types into a pre-allocated buffer. + */ +export declare class BufferWriter { + private buffer; + offset: number; + constructor(buffer: Buffer, offset?: number); + writeUInt8(i: number): void; + writeInt32(i: number): void; + writeUInt32(i: number): void; + writeUInt64(i: number): void; + writeVarInt(i: number): void; + writeSlice(slice: Buffer): void; + writeVarSlice(slice: Buffer): void; + writeVector(vector: Buffer[]): void; +} +/** + * Helper class for serialization of bitcoin data types into a pre-allocated buffer. + */ +export declare class BufferReader { + private buffer; + offset: number; + constructor(buffer: Buffer, offset?: number); + readUInt8(): number; + readInt32(): number; + readUInt32(): number; + readUInt64(): number; + readVarInt(): number; + readSlice(n: number): Buffer; + readVarSlice(): Buffer; + readVector(): Buffer[]; +} From 06674b19fe9e146fa547d94b9ef67bd2425ad71b Mon Sep 17 00:00:00 2001 From: junderw Date: Wed, 15 Jan 2020 10:50:33 +0900 Subject: [PATCH 3/6] Add BufferReader to Transaction and fix tests --- src/transaction.js | 67 +++++++++------------------------ test/buffer_writer.spec.ts | 2 +- ts_src/bufferutils.ts | 4 +- ts_src/transaction.ts | 76 +++++++++----------------------------- types/bufferutils.d.ts | 4 +- 5 files changed, 40 insertions(+), 113 deletions(-) diff --git a/src/transaction.js b/src/transaction.js index ef97900..b47cfe9 100644 --- a/src/transaction.js +++ b/src/transaction.js @@ -1,6 +1,5 @@ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); -const bufferutils = require('./bufferutils'); const bufferutils_1 = require('./bufferutils'); const bcrypto = require('./crypto'); const bscript = require('./script'); @@ -47,80 +46,48 @@ class Transaction { this.outs = []; } static fromBuffer(buffer, _NO_STRICT) { - let offset = 0; - function readSlice(n) { - offset += n; - return buffer.slice(offset - n, offset); - } - function readUInt32() { - const i = buffer.readUInt32LE(offset); - offset += 4; - return i; - } - function readInt32() { - const i = buffer.readInt32LE(offset); - offset += 4; - return i; - } - function readUInt64() { - const i = bufferutils.readUInt64LE(buffer, offset); - offset += 8; - return i; - } - function readVarInt() { - const vi = varuint.decode(buffer, offset); - offset += varuint.decode.bytes; - return vi; - } - function readVarSlice() { - return readSlice(readVarInt()); - } - function readVector() { - const count = readVarInt(); - const vector = []; - for (let i = 0; i < count; i++) vector.push(readVarSlice()); - return vector; - } + const bufferReader = new bufferutils_1.BufferReader(buffer); const tx = new Transaction(); - tx.version = readInt32(); - const marker = buffer.readUInt8(offset); - const flag = buffer.readUInt8(offset + 1); + tx.version = bufferReader.readInt32(); + const marker = bufferReader.readUInt8(); + const flag = bufferReader.readUInt8(); let hasWitnesses = false; if ( marker === Transaction.ADVANCED_TRANSACTION_MARKER && flag === Transaction.ADVANCED_TRANSACTION_FLAG ) { - offset += 2; hasWitnesses = true; + } else { + bufferReader.offset -= 2; } - const vinLen = readVarInt(); + const vinLen = bufferReader.readVarInt(); for (let i = 0; i < vinLen; ++i) { tx.ins.push({ - hash: readSlice(32), - index: readUInt32(), - script: readVarSlice(), - sequence: readUInt32(), + hash: bufferReader.readSlice(32), + index: bufferReader.readUInt32(), + script: bufferReader.readVarSlice(), + sequence: bufferReader.readUInt32(), witness: EMPTY_WITNESS, }); } - const voutLen = readVarInt(); + const voutLen = bufferReader.readVarInt(); for (let i = 0; i < voutLen; ++i) { tx.outs.push({ - value: readUInt64(), - script: readVarSlice(), + value: bufferReader.readUInt64(), + script: bufferReader.readVarSlice(), }); } if (hasWitnesses) { for (let i = 0; i < vinLen; ++i) { - tx.ins[i].witness = readVector(); + tx.ins[i].witness = bufferReader.readVector(); } // was this pointless? if (!tx.hasWitnesses()) throw new Error('Transaction has superfluous witness data'); } - tx.locktime = readUInt32(); + tx.locktime = bufferReader.readUInt32(); if (_NO_STRICT) return tx; - if (offset !== buffer.length) + if (bufferReader.offset !== buffer.length) throw new Error('Transaction has unexpected data'); return tx; } diff --git a/test/buffer_writer.spec.ts b/test/buffer_writer.spec.ts index 8b33e2e..af35cba 100644 --- a/test/buffer_writer.spec.ts +++ b/test/buffer_writer.spec.ts @@ -1,6 +1,6 @@ import * as assert from 'assert'; import { describe, it } from 'mocha'; -import { BufferWriter } from '../src/buffer_writer'; +import { BufferWriter } from '../src/bufferutils'; const varuint = require('varuint-bitcoin'); diff --git a/ts_src/bufferutils.ts b/ts_src/bufferutils.ts index 6904b73..8a5c839 100644 --- a/ts_src/bufferutils.ts +++ b/ts_src/bufferutils.ts @@ -52,7 +52,7 @@ export function reverseBuffer(buffer: Buffer): Buffer { * Helper class for serialization of bitcoin data types into a pre-allocated buffer. */ export class BufferWriter { - constructor(private buffer: Buffer, public offset: number = 0) { + constructor(public buffer: Buffer, public offset: number = 0) { typeforce(types.tuple(types.Buffer, types.UInt32), [buffer, offset]); } @@ -96,7 +96,7 @@ export class BufferWriter { * Helper class for serialization of bitcoin data types into a pre-allocated buffer. */ export class BufferReader { - constructor(private buffer: Buffer, public offset: number = 0) { + constructor(public buffer: Buffer, public offset: number = 0) { typeforce(types.tuple(types.Buffer, types.UInt32), [buffer, offset]); } diff --git a/ts_src/transaction.ts b/ts_src/transaction.ts index 3b792ec..561ee8a 100644 --- a/ts_src/transaction.ts +++ b/ts_src/transaction.ts @@ -1,5 +1,4 @@ -import * as bufferutils from './bufferutils'; -import { BufferWriter, reverseBuffer } from './bufferutils'; +import { BufferReader, BufferWriter, reverseBuffer } from './bufferutils'; import * as bcrypto from './crypto'; import * as bscript from './script'; import { OPS as opcodes } from './script'; @@ -68,85 +67,46 @@ export class Transaction { static readonly ADVANCED_TRANSACTION_FLAG = 0x01; static fromBuffer(buffer: Buffer, _NO_STRICT?: boolean): Transaction { - let offset: number = 0; - - function readSlice(n: number): Buffer { - offset += n; - return buffer.slice(offset - n, offset); - } - - function readUInt32(): number { - const i = buffer.readUInt32LE(offset); - offset += 4; - return i; - } - - function readInt32(): number { - const i = buffer.readInt32LE(offset); - offset += 4; - return i; - } - - function readUInt64(): number { - const i = bufferutils.readUInt64LE(buffer, offset); - offset += 8; - return i; - } - - function readVarInt(): number { - const vi = varuint.decode(buffer, offset); - offset += varuint.decode.bytes; - return vi; - } - - function readVarSlice(): Buffer { - return readSlice(readVarInt()); - } - - function readVector(): Buffer[] { - const count = readVarInt(); - const vector: Buffer[] = []; - for (let i = 0; i < count; i++) vector.push(readVarSlice()); - return vector; - } + const bufferReader = new BufferReader(buffer); const tx = new Transaction(); - tx.version = readInt32(); + tx.version = bufferReader.readInt32(); - const marker = buffer.readUInt8(offset); - const flag = buffer.readUInt8(offset + 1); + const marker = bufferReader.readUInt8(); + const flag = bufferReader.readUInt8(); let hasWitnesses = false; if ( marker === Transaction.ADVANCED_TRANSACTION_MARKER && flag === Transaction.ADVANCED_TRANSACTION_FLAG ) { - offset += 2; hasWitnesses = true; + } else { + bufferReader.offset -= 2; } - const vinLen = readVarInt(); + const vinLen = bufferReader.readVarInt(); for (let i = 0; i < vinLen; ++i) { tx.ins.push({ - hash: readSlice(32), - index: readUInt32(), - script: readVarSlice(), - sequence: readUInt32(), + hash: bufferReader.readSlice(32), + index: bufferReader.readUInt32(), + script: bufferReader.readVarSlice(), + sequence: bufferReader.readUInt32(), witness: EMPTY_WITNESS, }); } - const voutLen = readVarInt(); + const voutLen = bufferReader.readVarInt(); for (let i = 0; i < voutLen; ++i) { tx.outs.push({ - value: readUInt64(), - script: readVarSlice(), + value: bufferReader.readUInt64(), + script: bufferReader.readVarSlice(), }); } if (hasWitnesses) { for (let i = 0; i < vinLen; ++i) { - tx.ins[i].witness = readVector(); + tx.ins[i].witness = bufferReader.readVector(); } // was this pointless? @@ -154,10 +114,10 @@ export class Transaction { throw new Error('Transaction has superfluous witness data'); } - tx.locktime = readUInt32(); + tx.locktime = bufferReader.readUInt32(); if (_NO_STRICT) return tx; - if (offset !== buffer.length) + if (bufferReader.offset !== buffer.length) throw new Error('Transaction has unexpected data'); return tx; diff --git a/types/bufferutils.d.ts b/types/bufferutils.d.ts index d9e6dc0..424bbaf 100644 --- a/types/bufferutils.d.ts +++ b/types/bufferutils.d.ts @@ -5,7 +5,7 @@ export declare function reverseBuffer(buffer: Buffer): Buffer; * Helper class for serialization of bitcoin data types into a pre-allocated buffer. */ export declare class BufferWriter { - private buffer; + buffer: Buffer; offset: number; constructor(buffer: Buffer, offset?: number); writeUInt8(i: number): void; @@ -21,7 +21,7 @@ export declare class BufferWriter { * Helper class for serialization of bitcoin data types into a pre-allocated buffer. */ export declare class BufferReader { - private buffer; + buffer: Buffer; offset: number; constructor(buffer: Buffer, offset?: number); readUInt8(): number; From c8fdfae9558b208ef68da358e196f660bd89a46b Mon Sep 17 00:00:00 2001 From: junderw Date: Wed, 15 Jan 2020 11:25:53 +0900 Subject: [PATCH 4/6] Add BufferReader tests --- src/block.js | 73 ++---- test/buffer_writer.spec.ts | 231 ------------------- test/bufferutils.spec.ts | 440 +++++++++++++++++++++++++++++++++++++ ts_src/block.ts | 82 +++---- 4 files changed, 486 insertions(+), 340 deletions(-) delete mode 100644 test/buffer_writer.spec.ts diff --git a/src/block.js b/src/block.js index 8ec6c29..cb3ee4b 100644 --- a/src/block.js +++ b/src/block.js @@ -26,43 +26,24 @@ class Block { } static fromBuffer(buffer) { if (buffer.length < 80) throw new Error('Buffer too small (< 80 bytes)'); - let offset = 0; - const readSlice = n => { - offset += n; - return buffer.slice(offset - n, offset); - }; - const readUInt32 = () => { - const i = buffer.readUInt32LE(offset); - offset += 4; - return i; - }; - const readInt32 = () => { - const i = buffer.readInt32LE(offset); - offset += 4; - return i; - }; + const bufferReader = new bufferutils_1.BufferReader(buffer); const block = new Block(); - block.version = readInt32(); - block.prevHash = readSlice(32); - block.merkleRoot = readSlice(32); - block.timestamp = readUInt32(); - block.bits = readUInt32(); - block.nonce = readUInt32(); + block.version = bufferReader.readInt32(); + block.prevHash = bufferReader.readSlice(32); + block.merkleRoot = bufferReader.readSlice(32); + block.timestamp = bufferReader.readUInt32(); + block.bits = bufferReader.readUInt32(); + block.nonce = bufferReader.readUInt32(); if (buffer.length === 80) return block; - const readVarInt = () => { - const vi = varuint.decode(buffer, offset); - offset += varuint.decode.bytes; - return vi; - }; const readTransaction = () => { const tx = transaction_1.Transaction.fromBuffer( - buffer.slice(offset), + bufferReader.buffer.slice(bufferReader.offset), true, ); - offset += tx.byteLength(); + bufferReader.offset += tx.byteLength(); return tx; }; - const nTransactions = readVarInt(); + const nTransactions = bufferReader.readVarInt(); block.transactions = []; for (let i = 0; i < nTransactions; ++i) { const tx = readTransaction(); @@ -154,32 +135,20 @@ class Block { // TODO: buffer, offset compatibility toBuffer(headersOnly) { const buffer = Buffer.allocUnsafe(this.byteLength(headersOnly)); - let offset = 0; - const writeSlice = slice => { - slice.copy(buffer, offset); - offset += slice.length; - }; - const writeInt32 = i => { - buffer.writeInt32LE(i, offset); - offset += 4; - }; - const writeUInt32 = i => { - buffer.writeUInt32LE(i, offset); - offset += 4; - }; - writeInt32(this.version); - writeSlice(this.prevHash); - writeSlice(this.merkleRoot); - writeUInt32(this.timestamp); - writeUInt32(this.bits); - writeUInt32(this.nonce); + const bufferWriter = new bufferutils_1.BufferWriter(buffer); + bufferWriter.writeInt32(this.version); + bufferWriter.writeSlice(this.prevHash); + bufferWriter.writeSlice(this.merkleRoot); + bufferWriter.writeUInt32(this.timestamp); + bufferWriter.writeUInt32(this.bits); + bufferWriter.writeUInt32(this.nonce); if (headersOnly || !this.transactions) return buffer; - varuint.encode(this.transactions.length, buffer, offset); - offset += varuint.encode.bytes; + varuint.encode(this.transactions.length, buffer, bufferWriter.offset); + bufferWriter.offset += varuint.encode.bytes; this.transactions.forEach(tx => { const txSize = tx.byteLength(); // TODO: extract from toBuffer? - tx.toBuffer(buffer, offset); - offset += txSize; + tx.toBuffer(buffer, bufferWriter.offset); + bufferWriter.offset += txSize; }); return buffer; } diff --git a/test/buffer_writer.spec.ts b/test/buffer_writer.spec.ts deleted file mode 100644 index af35cba..0000000 --- a/test/buffer_writer.spec.ts +++ /dev/null @@ -1,231 +0,0 @@ -import * as assert from 'assert'; -import { describe, it } from 'mocha'; -import { BufferWriter } from '../src/bufferutils'; - -const varuint = require('varuint-bitcoin'); - -describe('BufferWriter', () => { - function concatToBuffer(values: number[][]): Buffer { - return Buffer.concat(values.map(data => Buffer.from(data))); - } - - function testBuffer( - bufferWriter: BufferWriter, - expectedBuffer: Buffer, - expectedOffset: number = expectedBuffer.length, - ): void { - assert.strictEqual(bufferWriter.offset, expectedOffset); - assert.deepStrictEqual( - bufferWriter.buffer.slice(0, expectedOffset), - expectedBuffer.slice(0, expectedOffset), - ); - } - - it('writeUint8', () => { - const values = [0, 1, 254, 255]; - const expectedBuffer = Buffer.from([0, 1, 0xfe, 0xff]); - const bufferWriter = new BufferWriter( - Buffer.allocUnsafe(expectedBuffer.length), - ); - values.forEach((v: number) => { - const expectedOffset = bufferWriter.offset + 1; - bufferWriter.writeUInt8(v); - testBuffer(bufferWriter, expectedBuffer, expectedOffset); - }); - testBuffer(bufferWriter, expectedBuffer); - }); - - it('writeInt32', () => { - const values = [ - 0, - 1, - Math.pow(2, 31) - 2, - Math.pow(2, 31) - 1, - -1, - -Math.pow(2, 31), - ]; - const expectedBuffer = concatToBuffer([ - [0, 0, 0, 0], - [1, 0, 0, 0], - [0xfe, 0xff, 0xff, 0x7f], - [0xff, 0xff, 0xff, 0x7f], - [0xff, 0xff, 0xff, 0xff], - [0x00, 0x00, 0x00, 0x80], - ]); - const bufferWriter = new BufferWriter( - Buffer.allocUnsafe(expectedBuffer.length), - ); - values.forEach((value: number) => { - const expectedOffset = bufferWriter.offset + 4; - bufferWriter.writeInt32(value); - testBuffer(bufferWriter, expectedBuffer, expectedOffset); - }); - testBuffer(bufferWriter, expectedBuffer); - }); - - it('writeUInt32', () => { - const maxUInt32 = Math.pow(2, 32) - 1; - const values = [0, 1, Math.pow(2, 16), maxUInt32]; - const expectedBuffer = concatToBuffer([ - [0, 0, 0, 0], - [1, 0, 0, 0], - [0, 0, 1, 0], - [0xff, 0xff, 0xff, 0xff], - ]); - const bufferWriter = new BufferWriter( - Buffer.allocUnsafe(expectedBuffer.length), - ); - values.forEach((value: number) => { - const expectedOffset = bufferWriter.offset + 4; - bufferWriter.writeUInt32(value); - testBuffer(bufferWriter, expectedBuffer, expectedOffset); - }); - testBuffer(bufferWriter, expectedBuffer); - }); - - it('writeUInt64', () => { - const values = [ - 0, - 1, - Math.pow(2, 32), - Number.MAX_SAFE_INTEGER /* 2^53 - 1 */, - ]; - const expectedBuffer = concatToBuffer([ - [0, 0, 0, 0, 0, 0, 0, 0], - [1, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 1, 0, 0, 0], - [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x1f, 0x00], - ]); - const bufferWriter = new BufferWriter( - Buffer.allocUnsafe(expectedBuffer.length), - ); - values.forEach((value: number) => { - const expectedOffset = bufferWriter.offset + 8; - bufferWriter.writeUInt64(value); - testBuffer(bufferWriter, expectedBuffer, expectedOffset); - }); - testBuffer(bufferWriter, expectedBuffer); - }); - - it('writeVarInt', () => { - const values = [ - 0, - 1, - 252, - 253, - 254, - 255, - 256, - Math.pow(2, 16) - 2, - Math.pow(2, 16) - 1, - Math.pow(2, 16), - Math.pow(2, 32) - 2, - Math.pow(2, 32) - 1, - Math.pow(2, 32), - Number.MAX_SAFE_INTEGER, - ]; - const expectedBuffer = concatToBuffer([ - [0x00], - [0x01], - [0xfc], - [0xfd, 0xfd, 0x00], - [0xfd, 0xfe, 0x00], - [0xfd, 0xff, 0x00], - [0xfd, 0x00, 0x01], - [0xfd, 0xfe, 0xff], - [0xfd, 0xff, 0xff], - [0xfe, 0x00, 0x00, 0x01, 0x00], - [0xfe, 0xfe, 0xff, 0xff, 0xff], - [0xfe, 0xff, 0xff, 0xff, 0xff], - [0xff, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00], - [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x1f, 0x00], - ]); - const bufferWriter = new BufferWriter( - Buffer.allocUnsafe(expectedBuffer.length), - ); - values.forEach((value: number) => { - const expectedOffset = - bufferWriter.offset + varuint.encodingLength(value); - bufferWriter.writeVarInt(value); - testBuffer(bufferWriter, expectedBuffer, expectedOffset); - }); - testBuffer(bufferWriter, expectedBuffer); - }); - - it('writeSlice', () => { - const values = [[], [1], [1, 2, 3, 4], [254, 255]]; - const expectedBuffer = concatToBuffer(values); - const bufferWriter = new BufferWriter( - Buffer.allocUnsafe(expectedBuffer.length), - ); - values.forEach((v: number[]) => { - const expectedOffset = bufferWriter.offset + v.length; - bufferWriter.writeSlice(Buffer.from(v)); - testBuffer(bufferWriter, expectedBuffer, expectedOffset); - }); - testBuffer(bufferWriter, expectedBuffer); - }); - - it('writeVarSlice', () => { - const values = [ - Buffer.alloc(1, 1), - Buffer.alloc(252, 2), - Buffer.alloc(253, 3), - ]; - const expectedBuffer = Buffer.concat([ - Buffer.from([0x01, 0x01]), - Buffer.from([0xfc]), - Buffer.alloc(252, 0x02), - Buffer.from([0xfd, 0xfd, 0x00]), - Buffer.alloc(253, 0x03), - ]); - const bufferWriter = new BufferWriter( - Buffer.allocUnsafe(expectedBuffer.length), - ); - values.forEach((value: Buffer) => { - const expectedOffset = - bufferWriter.offset + - varuint.encodingLength(value.length) + - value.length; - bufferWriter.writeVarSlice(value); - testBuffer(bufferWriter, expectedBuffer, expectedOffset); - }); - testBuffer(bufferWriter, expectedBuffer); - }); - - it('writeVector', () => { - const values = [ - [Buffer.alloc(1, 4), Buffer.alloc(253, 5)], - Array(253).fill(Buffer.alloc(1, 6)), - ]; - const expectedBuffer = Buffer.concat([ - Buffer.from([0x02]), - Buffer.from([0x01, 0x04]), - Buffer.from([0xfd, 0xfd, 0x00]), - Buffer.alloc(253, 5), - - Buffer.from([0xfd, 0xfd, 0x00]), - Buffer.concat( - Array(253) - .fill(0) - .map(() => Buffer.from([0x01, 0x06])), - ), - ]); - - const bufferWriter = new BufferWriter( - Buffer.allocUnsafe(expectedBuffer.length), - ); - values.forEach((value: Buffer[]) => { - const expectedOffset = - bufferWriter.offset + - varuint.encodingLength(value.length) + - value.reduce( - (sum: number, v) => sum + varuint.encodingLength(v.length) + v.length, - 0, - ); - bufferWriter.writeVector(value); - testBuffer(bufferWriter, expectedBuffer, expectedOffset); - }); - testBuffer(bufferWriter, expectedBuffer); - }); -}); diff --git a/test/bufferutils.spec.ts b/test/bufferutils.spec.ts index 33ad8f5..2ed80e6 100644 --- a/test/bufferutils.spec.ts +++ b/test/bufferutils.spec.ts @@ -1,10 +1,16 @@ import * as assert from 'assert'; import { describe, it } from 'mocha'; import * as bufferutils from '../src/bufferutils'; +import { BufferReader, BufferWriter } from '../src/bufferutils'; import * as fixtures from './fixtures/bufferutils.json'; +const varuint = require('varuint-bitcoin'); describe('bufferutils', () => { + function concatToBuffer(values: number[][]): Buffer { + return Buffer.concat(values.map(data => Buffer.from(data))); + } + describe('readUInt64LE', () => { fixtures.valid.forEach(f => { it('decodes ' + f.hex, () => { @@ -46,4 +52,438 @@ describe('bufferutils', () => { }); }); }); + + describe('BufferWriter', () => { + function testBuffer( + bufferWriter: BufferWriter, + expectedBuffer: Buffer, + expectedOffset: number = expectedBuffer.length, + ): void { + assert.strictEqual(bufferWriter.offset, expectedOffset); + assert.deepStrictEqual( + bufferWriter.buffer.slice(0, expectedOffset), + expectedBuffer.slice(0, expectedOffset), + ); + } + + it('writeUint8', () => { + const values = [0, 1, 254, 255]; + const expectedBuffer = Buffer.from([0, 1, 0xfe, 0xff]); + const bufferWriter = new BufferWriter( + Buffer.allocUnsafe(expectedBuffer.length), + ); + values.forEach((v: number) => { + const expectedOffset = bufferWriter.offset + 1; + bufferWriter.writeUInt8(v); + testBuffer(bufferWriter, expectedBuffer, expectedOffset); + }); + testBuffer(bufferWriter, expectedBuffer); + }); + + it('writeInt32', () => { + const values = [ + 0, + 1, + Math.pow(2, 31) - 2, + Math.pow(2, 31) - 1, + -1, + -Math.pow(2, 31), + ]; + const expectedBuffer = concatToBuffer([ + [0, 0, 0, 0], + [1, 0, 0, 0], + [0xfe, 0xff, 0xff, 0x7f], + [0xff, 0xff, 0xff, 0x7f], + [0xff, 0xff, 0xff, 0xff], + [0x00, 0x00, 0x00, 0x80], + ]); + const bufferWriter = new BufferWriter( + Buffer.allocUnsafe(expectedBuffer.length), + ); + values.forEach((value: number) => { + const expectedOffset = bufferWriter.offset + 4; + bufferWriter.writeInt32(value); + testBuffer(bufferWriter, expectedBuffer, expectedOffset); + }); + testBuffer(bufferWriter, expectedBuffer); + }); + + it('writeUInt32', () => { + const maxUInt32 = Math.pow(2, 32) - 1; + const values = [0, 1, Math.pow(2, 16), maxUInt32]; + const expectedBuffer = concatToBuffer([ + [0, 0, 0, 0], + [1, 0, 0, 0], + [0, 0, 1, 0], + [0xff, 0xff, 0xff, 0xff], + ]); + const bufferWriter = new BufferWriter( + Buffer.allocUnsafe(expectedBuffer.length), + ); + values.forEach((value: number) => { + const expectedOffset = bufferWriter.offset + 4; + bufferWriter.writeUInt32(value); + testBuffer(bufferWriter, expectedBuffer, expectedOffset); + }); + testBuffer(bufferWriter, expectedBuffer); + }); + + it('writeUInt64', () => { + const values = [ + 0, + 1, + Math.pow(2, 32), + Number.MAX_SAFE_INTEGER /* 2^53 - 1 */, + ]; + const expectedBuffer = concatToBuffer([ + [0, 0, 0, 0, 0, 0, 0, 0], + [1, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0], + [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x1f, 0x00], + ]); + const bufferWriter = new BufferWriter( + Buffer.allocUnsafe(expectedBuffer.length), + ); + values.forEach((value: number) => { + const expectedOffset = bufferWriter.offset + 8; + bufferWriter.writeUInt64(value); + testBuffer(bufferWriter, expectedBuffer, expectedOffset); + }); + testBuffer(bufferWriter, expectedBuffer); + }); + + it('writeVarInt', () => { + const values = [ + 0, + 1, + 252, + 253, + 254, + 255, + 256, + Math.pow(2, 16) - 2, + Math.pow(2, 16) - 1, + Math.pow(2, 16), + Math.pow(2, 32) - 2, + Math.pow(2, 32) - 1, + Math.pow(2, 32), + Number.MAX_SAFE_INTEGER, + ]; + const expectedBuffer = concatToBuffer([ + [0x00], + [0x01], + [0xfc], + [0xfd, 0xfd, 0x00], + [0xfd, 0xfe, 0x00], + [0xfd, 0xff, 0x00], + [0xfd, 0x00, 0x01], + [0xfd, 0xfe, 0xff], + [0xfd, 0xff, 0xff], + [0xfe, 0x00, 0x00, 0x01, 0x00], + [0xfe, 0xfe, 0xff, 0xff, 0xff], + [0xfe, 0xff, 0xff, 0xff, 0xff], + [0xff, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00], + [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x1f, 0x00], + ]); + const bufferWriter = new BufferWriter( + Buffer.allocUnsafe(expectedBuffer.length), + ); + values.forEach((value: number) => { + const expectedOffset = + bufferWriter.offset + varuint.encodingLength(value); + bufferWriter.writeVarInt(value); + testBuffer(bufferWriter, expectedBuffer, expectedOffset); + }); + testBuffer(bufferWriter, expectedBuffer); + }); + + it('writeSlice', () => { + const values = [[], [1], [1, 2, 3, 4], [254, 255]]; + const expectedBuffer = concatToBuffer(values); + const bufferWriter = new BufferWriter( + Buffer.allocUnsafe(expectedBuffer.length), + ); + values.forEach((v: number[]) => { + const expectedOffset = bufferWriter.offset + v.length; + bufferWriter.writeSlice(Buffer.from(v)); + testBuffer(bufferWriter, expectedBuffer, expectedOffset); + }); + testBuffer(bufferWriter, expectedBuffer); + }); + + it('writeVarSlice', () => { + const values = [ + Buffer.alloc(1, 1), + Buffer.alloc(252, 2), + Buffer.alloc(253, 3), + ]; + const expectedBuffer = Buffer.concat([ + Buffer.from([0x01, 0x01]), + Buffer.from([0xfc]), + Buffer.alloc(252, 0x02), + Buffer.from([0xfd, 0xfd, 0x00]), + Buffer.alloc(253, 0x03), + ]); + const bufferWriter = new BufferWriter( + Buffer.allocUnsafe(expectedBuffer.length), + ); + values.forEach((value: Buffer) => { + const expectedOffset = + bufferWriter.offset + + varuint.encodingLength(value.length) + + value.length; + bufferWriter.writeVarSlice(value); + testBuffer(bufferWriter, expectedBuffer, expectedOffset); + }); + testBuffer(bufferWriter, expectedBuffer); + }); + + it('writeVector', () => { + const values = [ + [Buffer.alloc(1, 4), Buffer.alloc(253, 5)], + Array(253).fill(Buffer.alloc(1, 6)), + ]; + const expectedBuffer = Buffer.concat([ + Buffer.from([0x02]), + Buffer.from([0x01, 0x04]), + Buffer.from([0xfd, 0xfd, 0x00]), + Buffer.alloc(253, 5), + + Buffer.from([0xfd, 0xfd, 0x00]), + Buffer.concat( + Array(253) + .fill(0) + .map(() => Buffer.from([0x01, 0x06])), + ), + ]); + + const bufferWriter = new BufferWriter( + Buffer.allocUnsafe(expectedBuffer.length), + ); + values.forEach((value: Buffer[]) => { + const expectedOffset = + bufferWriter.offset + + varuint.encodingLength(value.length) + + value.reduce( + (sum: number, v) => + sum + varuint.encodingLength(v.length) + v.length, + 0, + ); + bufferWriter.writeVector(value); + testBuffer(bufferWriter, expectedBuffer, expectedOffset); + }); + testBuffer(bufferWriter, expectedBuffer); + }); + }); + + describe('BufferReader', () => { + function testValue( + bufferReader: BufferReader, + value: Buffer | number, + expectedValue: Buffer | number, + expectedOffset: number = Buffer.isBuffer(expectedValue) + ? expectedValue.length + : 0, + ): void { + assert.strictEqual(bufferReader.offset, expectedOffset); + if (Buffer.isBuffer(expectedValue)) { + assert.deepStrictEqual( + (value as Buffer).slice(0, expectedOffset), + expectedValue.slice(0, expectedOffset), + ); + } else { + assert.strictEqual(value as number, expectedValue); + } + } + + it('readUint8', () => { + const values = [0, 1, 0xfe, 0xff]; + const buffer = Buffer.from([0, 1, 0xfe, 0xff]); + const bufferReader = new BufferReader(buffer); + values.forEach((v: number) => { + const expectedOffset = bufferReader.offset + 1; + const val = bufferReader.readUInt8(); + testValue(bufferReader, val, v, expectedOffset); + }); + }); + + it('readInt32', () => { + const values = [ + 0, + 1, + Math.pow(2, 31) - 2, + Math.pow(2, 31) - 1, + -1, + -Math.pow(2, 31), + ]; + const buffer = concatToBuffer([ + [0, 0, 0, 0], + [1, 0, 0, 0], + [0xfe, 0xff, 0xff, 0x7f], + [0xff, 0xff, 0xff, 0x7f], + [0xff, 0xff, 0xff, 0xff], + [0x00, 0x00, 0x00, 0x80], + ]); + const bufferReader = new BufferReader(buffer); + values.forEach((value: number) => { + const expectedOffset = bufferReader.offset + 4; + const val = bufferReader.readInt32(); + testValue(bufferReader, val, value, expectedOffset); + }); + }); + + it('readUInt32', () => { + const maxUInt32 = Math.pow(2, 32) - 1; + const values = [0, 1, Math.pow(2, 16), maxUInt32]; + const buffer = concatToBuffer([ + [0, 0, 0, 0], + [1, 0, 0, 0], + [0, 0, 1, 0], + [0xff, 0xff, 0xff, 0xff], + ]); + const bufferReader = new BufferReader(buffer); + values.forEach((value: number) => { + const expectedOffset = bufferReader.offset + 4; + const val = bufferReader.readUInt32(); + testValue(bufferReader, val, value, expectedOffset); + }); + }); + + it('readUInt64', () => { + const values = [ + 0, + 1, + Math.pow(2, 32), + Number.MAX_SAFE_INTEGER /* 2^53 - 1 */, + ]; + const buffer = concatToBuffer([ + [0, 0, 0, 0, 0, 0, 0, 0], + [1, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0], + [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x1f, 0x00], + ]); + const bufferReader = new BufferReader(buffer); + values.forEach((value: number) => { + const expectedOffset = bufferReader.offset + 8; + const val = bufferReader.readUInt64(); + testValue(bufferReader, val, value, expectedOffset); + }); + }); + + it('readVarInt', () => { + const values = [ + 0, + 1, + 252, + 253, + 254, + 255, + 256, + Math.pow(2, 16) - 2, + Math.pow(2, 16) - 1, + Math.pow(2, 16), + Math.pow(2, 32) - 2, + Math.pow(2, 32) - 1, + Math.pow(2, 32), + Number.MAX_SAFE_INTEGER, + ]; + const buffer = concatToBuffer([ + [0x00], + [0x01], + [0xfc], + [0xfd, 0xfd, 0x00], + [0xfd, 0xfe, 0x00], + [0xfd, 0xff, 0x00], + [0xfd, 0x00, 0x01], + [0xfd, 0xfe, 0xff], + [0xfd, 0xff, 0xff], + [0xfe, 0x00, 0x00, 0x01, 0x00], + [0xfe, 0xfe, 0xff, 0xff, 0xff], + [0xfe, 0xff, 0xff, 0xff, 0xff], + [0xff, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00], + [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x1f, 0x00], + ]); + const bufferReader = new BufferReader(buffer); + values.forEach((value: number) => { + const expectedOffset = + bufferReader.offset + varuint.encodingLength(value); + const val = bufferReader.readVarInt(); + testValue(bufferReader, val, value, expectedOffset); + }); + }); + + it('readSlice', () => { + const values = [[1], [1, 2, 3, 4], [254, 255]]; + const buffer = concatToBuffer(values); + const bufferReader = new BufferReader(buffer); + values.forEach((v: number[]) => { + const expectedOffset = bufferReader.offset + v.length; + const val = bufferReader.readSlice(v.length); + testValue(bufferReader, val, Buffer.from(v), expectedOffset); + }); + }); + + it('readVarSlice', () => { + const values = [ + Buffer.alloc(1, 1), + Buffer.alloc(252, 2), + Buffer.alloc(253, 3), + ]; + const buffer = Buffer.concat([ + Buffer.from([0x01, 0x01]), + Buffer.from([0xfc]), + Buffer.alloc(252, 0x02), + Buffer.from([0xfd, 0xfd, 0x00]), + Buffer.alloc(253, 0x03), + ]); + const bufferReader = new BufferReader(buffer); + values.forEach((value: Buffer) => { + const expectedOffset = + bufferReader.offset + + varuint.encodingLength(value.length) + + value.length; + const val = bufferReader.readVarSlice(); + testValue(bufferReader, val, value, expectedOffset); + }); + }); + + it('readVector', () => { + const values = [ + [Buffer.alloc(1, 4), Buffer.alloc(253, 5)], + Array(253).fill(Buffer.alloc(1, 6)), + ]; + const buffer = Buffer.concat([ + Buffer.from([0x02]), + Buffer.from([0x01, 0x04]), + Buffer.from([0xfd, 0xfd, 0x00]), + Buffer.alloc(253, 5), + + Buffer.from([0xfd, 0xfd, 0x00]), + Buffer.concat( + Array(253) + .fill(0) + .map(() => Buffer.from([0x01, 0x06])), + ), + ]); + + const bufferReader = new BufferReader(buffer); + values.forEach((value: Buffer[]) => { + const expectedOffset = + bufferReader.offset + + varuint.encodingLength(value.length) + + value.reduce( + (sum: number, v) => + sum + varuint.encodingLength(v.length) + v.length, + 0, + ); + const val = bufferReader.readVector(); + testValue( + bufferReader, + Buffer.concat(val), + Buffer.concat(value), + expectedOffset, + ); + }); + }); + }); }); diff --git a/ts_src/block.ts b/ts_src/block.ts index 9a4d675..2760e5e 100644 --- a/ts_src/block.ts +++ b/ts_src/block.ts @@ -1,4 +1,4 @@ -import { reverseBuffer } from './bufferutils'; +import { BufferReader, BufferWriter, reverseBuffer } from './bufferutils'; import * as bcrypto from './crypto'; import { Transaction } from './transaction'; import * as types from './types'; @@ -18,47 +18,28 @@ export class Block { static fromBuffer(buffer: Buffer): Block { if (buffer.length < 80) throw new Error('Buffer too small (< 80 bytes)'); - let offset: number = 0; - const readSlice = (n: number): Buffer => { - offset += n; - return buffer.slice(offset - n, offset); - }; - - const readUInt32 = (): number => { - const i = buffer.readUInt32LE(offset); - offset += 4; - return i; - }; - - const readInt32 = (): number => { - const i = buffer.readInt32LE(offset); - offset += 4; - return i; - }; + const bufferReader = new BufferReader(buffer); const block = new Block(); - block.version = readInt32(); - block.prevHash = readSlice(32); - block.merkleRoot = readSlice(32); - block.timestamp = readUInt32(); - block.bits = readUInt32(); - block.nonce = readUInt32(); + block.version = bufferReader.readInt32(); + block.prevHash = bufferReader.readSlice(32); + block.merkleRoot = bufferReader.readSlice(32); + block.timestamp = bufferReader.readUInt32(); + block.bits = bufferReader.readUInt32(); + block.nonce = bufferReader.readUInt32(); if (buffer.length === 80) return block; - const readVarInt = (): number => { - const vi = varuint.decode(buffer, offset); - offset += varuint.decode.bytes; - return vi; - }; - const readTransaction = (): any => { - const tx = Transaction.fromBuffer(buffer.slice(offset), true); - offset += tx.byteLength(); + const tx = Transaction.fromBuffer( + bufferReader.buffer.slice(bufferReader.offset), + true, + ); + bufferReader.offset += tx.byteLength(); return tx; }; - const nTransactions = readVarInt(); + const nTransactions = bufferReader.readVarInt(); block.transactions = []; for (let i = 0; i < nTransactions; ++i) { @@ -183,37 +164,24 @@ export class Block { toBuffer(headersOnly?: boolean): Buffer { const buffer: Buffer = Buffer.allocUnsafe(this.byteLength(headersOnly)); - let offset: number = 0; - const writeSlice = (slice: Buffer): void => { - slice.copy(buffer, offset); - offset += slice.length; - }; - - const writeInt32 = (i: number): void => { - buffer.writeInt32LE(i, offset); - offset += 4; - }; - const writeUInt32 = (i: number): void => { - buffer.writeUInt32LE(i, offset); - offset += 4; - }; + const bufferWriter = new BufferWriter(buffer); - writeInt32(this.version); - writeSlice(this.prevHash!); - writeSlice(this.merkleRoot!); - writeUInt32(this.timestamp); - writeUInt32(this.bits); - writeUInt32(this.nonce); + bufferWriter.writeInt32(this.version); + bufferWriter.writeSlice(this.prevHash!); + bufferWriter.writeSlice(this.merkleRoot!); + bufferWriter.writeUInt32(this.timestamp); + bufferWriter.writeUInt32(this.bits); + bufferWriter.writeUInt32(this.nonce); if (headersOnly || !this.transactions) return buffer; - varuint.encode(this.transactions.length, buffer, offset); - offset += varuint.encode.bytes; + varuint.encode(this.transactions.length, buffer, bufferWriter.offset); + bufferWriter.offset += varuint.encode.bytes; this.transactions.forEach(tx => { const txSize = tx.byteLength(); // TODO: extract from toBuffer? - tx.toBuffer(buffer, offset); - offset += txSize; + tx.toBuffer(buffer, bufferWriter.offset); + bufferWriter.offset += txSize; }); return buffer; From 717166e66833ae16d4575beac7fbe6a8462f6baf Mon Sep 17 00:00:00 2001 From: junderw Date: Wed, 15 Jan 2020 11:28:56 +0900 Subject: [PATCH 5/6] Fix comment --- src/bufferutils.js | 2 +- ts_src/bufferutils.ts | 2 +- types/bufferutils.d.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/bufferutils.js b/src/bufferutils.js index 19e8763..98adbfb 100644 --- a/src/bufferutils.js +++ b/src/bufferutils.js @@ -80,7 +80,7 @@ class BufferWriter { } exports.BufferWriter = BufferWriter; /** - * Helper class for serialization of bitcoin data types into a pre-allocated buffer. + * Helper class for reading of bitcoin data types from a buffer. */ class BufferReader { constructor(buffer, offset = 0) { diff --git a/ts_src/bufferutils.ts b/ts_src/bufferutils.ts index 8a5c839..502745d 100644 --- a/ts_src/bufferutils.ts +++ b/ts_src/bufferutils.ts @@ -93,7 +93,7 @@ export class BufferWriter { } /** - * Helper class for serialization of bitcoin data types into a pre-allocated buffer. + * Helper class for reading of bitcoin data types from a buffer. */ export class BufferReader { constructor(public buffer: Buffer, public offset: number = 0) { diff --git a/types/bufferutils.d.ts b/types/bufferutils.d.ts index 424bbaf..36e31fe 100644 --- a/types/bufferutils.d.ts +++ b/types/bufferutils.d.ts @@ -18,7 +18,7 @@ export declare class BufferWriter { writeVector(vector: Buffer[]): void; } /** - * Helper class for serialization of bitcoin data types into a pre-allocated buffer. + * Helper class for reading of bitcoin data types from a buffer. */ export declare class BufferReader { buffer: Buffer; From 5679a4b455dcb8e584daaf92d37e1dbab43da53d Mon Sep 17 00:00:00 2001 From: junderw Date: Wed, 15 Jan 2020 14:14:02 +0900 Subject: [PATCH 6/6] Check write/read Slice out of bounds --- src/bufferutils.js | 9 ++++++++- test/bufferutils.spec.ts | 6 ++++++ ts_src/bufferutils.ts | 9 ++++++++- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/bufferutils.js b/src/bufferutils.js index 98adbfb..103ca5e 100644 --- a/src/bufferutils.js +++ b/src/bufferutils.js @@ -67,6 +67,9 @@ class BufferWriter { this.offset += varuint.encode.bytes; } writeSlice(slice) { + if (this.buffer.length < this.offset + slice.length) { + throw new Error('Cannot write slice out of bounds'); + } this.offset += slice.copy(this.buffer, this.offset); } writeVarSlice(slice) { @@ -114,8 +117,12 @@ class BufferReader { return vi; } readSlice(n) { + if (this.buffer.length < this.offset + n) { + throw new Error('Cannot read slice out of bounds'); + } + const result = this.buffer.slice(this.offset, this.offset + n); this.offset += n; - return this.buffer.slice(this.offset - n, this.offset); + return result; } readVarSlice() { return this.readSlice(this.readVarInt()); diff --git a/test/bufferutils.spec.ts b/test/bufferutils.spec.ts index 2ed80e6..b8f86ab 100644 --- a/test/bufferutils.spec.ts +++ b/test/bufferutils.spec.ts @@ -209,6 +209,9 @@ describe('bufferutils', () => { testBuffer(bufferWriter, expectedBuffer, expectedOffset); }); testBuffer(bufferWriter, expectedBuffer); + assert.throws(() => { + bufferWriter.writeSlice(Buffer.from([0, 0])); + }, /^Error: Cannot write slice out of bounds$/); }); it('writeVarSlice', () => { @@ -421,6 +424,9 @@ describe('bufferutils', () => { const val = bufferReader.readSlice(v.length); testValue(bufferReader, val, Buffer.from(v), expectedOffset); }); + assert.throws(() => { + bufferReader.readSlice(2); + }, /^Error: Cannot read slice out of bounds$/); }); it('readVarSlice', () => { diff --git a/ts_src/bufferutils.ts b/ts_src/bufferutils.ts index 502745d..23e1c0d 100644 --- a/ts_src/bufferutils.ts +++ b/ts_src/bufferutils.ts @@ -78,6 +78,9 @@ export class BufferWriter { } writeSlice(slice: Buffer): void { + if (this.buffer.length < this.offset + slice.length) { + throw new Error('Cannot write slice out of bounds'); + } this.offset += slice.copy(this.buffer, this.offset); } @@ -131,8 +134,12 @@ export class BufferReader { } readSlice(n: number): Buffer { + if (this.buffer.length < this.offset + n) { + throw new Error('Cannot read slice out of bounds'); + } + const result = this.buffer.slice(this.offset, this.offset + n); this.offset += n; - return this.buffer.slice(this.offset - n, this.offset); + return result; } readVarSlice(): Buffer {