From 1296bb2302ec82bc8b7f16ee94d1887edb425e8d Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Sun, 11 Dec 2016 12:19:18 -0800 Subject: [PATCH] serialization: add size calculation and static writer. --- lib/primitives/input.js | 9 + lib/primitives/outpoint.js | 9 + lib/primitives/output.js | 2 +- lib/script/opcode.js | 28 ++- lib/script/script.js | 35 ++- lib/script/witness.js | 34 ++- lib/utils/staticwriter.js | 429 +++++++++++++++++++++++++++++++++++++ 7 files changed, 541 insertions(+), 5 deletions(-) create mode 100644 lib/utils/staticwriter.js diff --git a/lib/primitives/input.js b/lib/primitives/input.js index 5b7b89d3..99783022 100644 --- a/lib/primitives/input.js +++ b/lib/primitives/input.js @@ -319,6 +319,15 @@ Input.fromJSON = function fromJSON(json) { return new Input().fromJSON(json); }; +/** + * Calculate size of serialized input. + * @returns {Number} + */ + +Input.prototype.getSize = function getSize() { + return 40 + this.script.getVarSize(); +}; + /** * Serialize the input. * @param {String?} enc - Encoding, can be `'hex'` or null. diff --git a/lib/primitives/outpoint.js b/lib/primitives/outpoint.js index a5719d6f..335560fd 100644 --- a/lib/primitives/outpoint.js +++ b/lib/primitives/outpoint.js @@ -109,6 +109,15 @@ Outpoint.prototype.toWriter = function toWriter(bw) { return bw; }; +/** + * Calculate size of outpoint. + * @returns {Number} + */ + +Outpoint.prototype.getSize = function getSize() { + return 36; +}; + /** * Serialize outpoint. * @returns {Buffer} diff --git a/lib/primitives/output.js b/lib/primitives/output.js index 651198f0..f4015add 100644 --- a/lib/primitives/output.js +++ b/lib/primitives/output.js @@ -192,7 +192,7 @@ Output.prototype.getDustThreshold = function getDustThreshold(rate) { */ Output.prototype.getSize = function getSize() { - return this.toWriter(new BufferWriter()).written; + return 8 + this.script.getVarSize(); }; /** diff --git a/lib/script/opcode.js b/lib/script/opcode.js index 696a0f69..dc11600d 100644 --- a/lib/script/opcode.js +++ b/lib/script/opcode.js @@ -14,6 +14,7 @@ var util = require('../utils/util'); var encoding = require('./encoding'); var BufferReader = require('../utils/reader'); var BufferWriter = require('../utils/writer'); +var StaticWriter = require('../utils/staticwriter'); var opcodes = constants.opcodes; /** @@ -85,7 +86,32 @@ Opcode.prototype.toWriter = function toWriter(bw) { */ Opcode.prototype.toRaw = function toRaw() { - return this.toWriter(new BufferWriter()).render(); + var size = this.getSize(); + return this.toWriter(new StaticWriter(size)).render(); +}; + +/** + * Calculate opcode size. + * @returns {Number} + */ + +Opcode.prototype.getSize = function getSize() { + if (!this.data) + return 1; + + if (this.value <= 0x4b) + return 1 + this.data.length; + + switch (this.value) { + case opcodes.OP_PUSHDATA1: + return 2 + this.data.length; + case opcodes.OP_PUSHDATA2: + return 3 + this.data.length; + case opcodes.OP_PUSHDATA4: + return 5 + this.data.length; + default: + throw new Error('Unknown pushdata opcode.'); + } }; /** diff --git a/lib/script/script.js b/lib/script/script.js index a3c24bfd..c5025997 100644 --- a/lib/script/script.js +++ b/lib/script/script.js @@ -14,6 +14,7 @@ var crypto = require('../crypto/crypto'); var assert = require('assert'); var BufferWriter = require('../utils/writer'); var BufferReader = require('../utils/reader'); +var StaticWriter = require('../utils/staticwriter'); var opcodes = constants.opcodes; var STACK_TRUE = new Buffer([1]); var STACK_FALSE = new Buffer(0); @@ -25,6 +26,7 @@ var Opcode = require('./opcode'); var Stack = require('./stack'); var sigcache = require('./sigcache'); var encoding = require('./encoding'); +var enc = require('../utils/encoding'); var ec = require('../crypto/ec'); var Address = require('../primitives/address'); @@ -251,7 +253,8 @@ Script.prototype.toASM = function toASM(decode) { */ Script.prototype.compile = function compile() { - var bw = new BufferWriter(); + var size = this.getCodeSize(); + var bw = new StaticWriter(size); var i, op; for (i = 0; i < this.code.length; i++) { @@ -1942,7 +1945,8 @@ Script.prototype.isStandard = function isStandard() { }; /** - * Calculate size of script excluding the varint size bytes. + * Calculate the size of the script + * excluding the varint size bytes. * @returns {Number} */ @@ -1950,6 +1954,33 @@ Script.prototype.getSize = function getSize() { return this.raw.length; }; +/** + * Calculate the size of the script + * including the varint size bytes. + * @returns {Number} + */ + +Script.prototype.getVarSize = function getVarSize() { + return enc.sizeVarint(this.raw.length) + this.raw.length; +}; + +/** + * Calculate size of code to be compiled. + * @returns {Number} + */ + +Script.prototype.getCodeSize = function getCodeSize() { + var size = 0; + var i, op; + + for (i = 0; i < this.code.length; i++) { + op = this.code[i]; + size += op.getSize(); + } + + return size; +}; + /** * "Guess" the address of the input script. * This method is not 100% reliable. diff --git a/lib/script/witness.js b/lib/script/witness.js index fd42a5b3..3b09ad6e 100644 --- a/lib/script/witness.js +++ b/lib/script/witness.js @@ -17,8 +17,10 @@ var STACK_NEGATE = new Buffer([0x81]); var scriptTypes = constants.scriptTypes; var Script = require('./script'); var encoding = require('./encoding'); +var enc = require('../utils/encoding'); var Opcode = require('./opcode'); var BufferWriter = require('../utils/writer'); +var StaticWriter = require('../utils/staticwriter'); var BufferReader = require('../utils/reader'); var Address = require('../primitives/address'); var Stack = require('./stack'); @@ -303,6 +305,35 @@ Witness.prototype.indexOf = function indexOf(data) { return util.indexOf(this.items, data); }; +/** + * Calculate size of the witness + * excluding the varint size bytes. + * @returns {Number} + */ + +Witness.prototype.getSize = function getSize() { + var size = 0; + var i, item; + + for (i = 0; i < this.items.length; i++) { + item = this.items[i]; + size += enc.sizeVarint(item.length); + size += item.length; + } + + return size; +}; + +/** + * Calculate size of the witness + * including the varint size bytes. + * @returns {Number} + */ + +Witness.prototype.getVarSize = function getVarSize() { + return enc.sizeVarint(this.items.length) + this.getSize(); +}; + /** * Write witness to a buffer writer. * @param {BufferWriter} bw @@ -326,7 +357,8 @@ Witness.prototype.toWriter = function toWriter(bw) { */ Witness.prototype.toRaw = function toRaw() { - return this.toWriter(new BufferWriter()).render(); + var size = this.getSize(); + return this.toWriter(new StaticWriter(size)).render(); }; /** diff --git a/lib/utils/staticwriter.js b/lib/utils/staticwriter.js new file mode 100644 index 00000000..aac3ee9c --- /dev/null +++ b/lib/utils/staticwriter.js @@ -0,0 +1,429 @@ +/*! + * writer.js - buffer writer for bcoin + * Copyright (c) 2014-2015, Fedor Indutny (MIT License) + * Copyright (c) 2014-2016, Christopher Jeffrey (MIT License). + * https://github.com/bcoin-org/bcoin + */ + +'use strict'; + +var assert = require('assert'); +var encoding = require('./encoding'); +var crypto = require('../crypto/crypto'); + +/** + * An object that allows writing of buffers in a + * sane manner. This buffer writer is extremely + * optimized since it does not actually write + * anything until `render` is called. It makes + * one allocation: at the end, once it knows the + * size of the buffer to be allocated. Because + * of this, it can also act as a size calculator + * which is useful for guaging block size + * without actually serializing any data. + * @exports StaticWriter + * @constructor + * @param {(StaticWriter|Object)?} options + */ + +function StaticWriter(size) { + if (!(this instanceof StaticWriter)) + return new StaticWriter(size); + + this.data = new Buffer(size); + this.written = 0; +} + +/** + * Allocate and render the final buffer. + * @param {Boolean?} keep - Do not destroy the writer. + * @returns {Buffer} Rendered buffer. + */ + +StaticWriter.prototype.render = function render(keep) { + var data = this.data; + + assert.equal(this.written, data.length); + + if (!keep) + this.destroy(); + + return data; +}; + +/** + * Get size of data written so far. + * @returns {Number} + */ + +StaticWriter.prototype.getSize = function getSize() { + return this.written; +}; + +/** + * Seek to relative offset. + * @param {Number} offset + */ + +StaticWriter.prototype.seek = function seek(offset) { + this.written += offset; +}; + +/** + * Destroy the buffer writer. + */ + +StaticWriter.prototype.destroy = function destroy() { + this.data = null; + this.written = null; +}; + +/** + * Write uint8. + * @param {Number} value + */ + +StaticWriter.prototype.writeU8 = function writeU8(value) { + this.written = this.data.writeUInt8(value, this.written, true); +}; + +/** + * Write uint16le. + * @param {Number} value + */ + +StaticWriter.prototype.writeU16 = function writeU16(value) { + this.written = this.data.writeUInt16LE(value, this.written, true); +}; + +/** + * Write uint16be. + * @param {Number} value + */ + +StaticWriter.prototype.writeU16BE = function writeU16BE(value) { + this.written = this.data.writeUInt16BE(value, this.written, true); +}; + +/** + * Write uint32le. + * @param {Number} value + */ + +StaticWriter.prototype.writeU32 = function writeU32(value) { + this.written = this.data.writeUInt32LE(value, this.written, true); +}; + +/** + * Write uint32be. + * @param {Number} value + */ + +StaticWriter.prototype.writeU32BE = function writeU32BE(value) { + this.written = this.data.writeUInt32BE(value, this.written, true); +}; + +/** + * Write uint64le. + * @param {Number} value + */ + +StaticWriter.prototype.writeU64 = function writeU64(value) { + this.written = encoding.writeU64(this.data, value, this.written); +}; + +/** + * Write uint64be. + * @param {Number} value + */ + +StaticWriter.prototype.writeU64BE = function writeU64BE(value) { + this.written = encoding.writeU64BE(this.data, value, this.written); +}; + +/** + * Write uint64le. + * @param {BN} value + */ + +StaticWriter.prototype.writeU64BN = function writeU64BN(value) { + assert(false, 'Not implemented.'); +}; + +/** + * Write uint64be. + * @param {BN} value + */ + +StaticWriter.prototype.writeU64BEBN = function writeU64BEBN(value) { + assert(false, 'Not implemented.'); +}; + +/** + * Write int8. + * @param {Number} value + */ + +StaticWriter.prototype.write8 = function write8(value) { + this.written = this.data.writeInt8(value, this.written, true); +}; + +/** + * Write int16le. + * @param {Number} value + */ + +StaticWriter.prototype.write16 = function write16(value) { + this.written = this.data.writeInt16LE(value, this.written, true); +}; + +/** + * Write int16be. + * @param {Number} value + */ + +StaticWriter.prototype.write16BE = function write16BE(value) { + this.written = this.data.writeInt16BE(value, this.written, true); +}; + +/** + * Write int32le. + * @param {Number} value + */ + +StaticWriter.prototype.write32 = function write32(value) { + this.written = this.data.writeInt32LE(value, this.written, true); +}; + +/** + * Write int32be. + * @param {Number} value + */ + +StaticWriter.prototype.write32BE = function write32BE(value) { + this.written = this.data.writeInt32BE(value, this.written, true); +}; + +/** + * Write int64le. + * @param {Number} value + */ + +StaticWriter.prototype.write64 = function write64(value) { + this.written = encoding.write64(this.data, value, this.written); +}; + +/** + * Write int64be. + * @param {Number} value + */ + +StaticWriter.prototype.write64BE = function write64BE(value) { + this.written = encoding.write64BE(this.data, value, this.written); +}; + +/** + * Write int64le. + * @param {BN} value + */ + +StaticWriter.prototype.write64BN = function write64BN(value) { + assert(false, 'Not implemented.'); +}; + +/** + * Write int64be. + * @param {BN} value + */ + +StaticWriter.prototype.write64BEBN = function write64BEBN(value) { + assert(false, 'Not implemented.'); +}; + +/** + * Write float le. + * @param {Number} value + */ + +StaticWriter.prototype.writeFloat = function writeFloat(value) { + this.written = this.data.writeFloatLE(value, this.written, true); +}; + +/** + * Write float be. + * @param {Number} value + */ + +StaticWriter.prototype.writeFloatBE = function writeFloatBE(value) { + this.written = this.data.writeFloatBE(value, this.written, true); +}; + +/** + * Write double le. + * @param {Number} value + */ + +StaticWriter.prototype.writeDouble = function writeDouble(value) { + this.written = this.data.writeDoubleLE(value, this.written, true); +}; + +/** + * Write double be. + * @param {Number} value + */ + +StaticWriter.prototype.writeDoubleBE = function writeDoubleBE(value) { + this.written = this.data.writeDoubleBE(value, this.written, true); +}; + +/** + * Write a varint. + * @param {Number} value + */ + +StaticWriter.prototype.writeVarint = function writeVarint(value) { + this.written = encoding.writeVarint(this.data, value, this.written); +}; + +/** + * Write a varint. + * @param {BN} value + */ + +StaticWriter.prototype.writeVarintBN = function writeVarintBN(value) { + assert(false, 'Not implemented.'); +}; + +/** + * Write a varint (type 2). + * @param {Number} value + */ + +StaticWriter.prototype.writeVarint2 = function writeVarint2(value) { + this.written = encoding.writeVarint2(this.data, value, this.written); +}; + +/** + * Write a varint (type 2). + * @param {BN} value + */ + +StaticWriter.prototype.writeVarint2BN = function writeVarint2BN(value) { + assert(false, 'Not implemented.'); +}; + +/** + * Write bytes. + * @param {Buffer} value + */ + +StaticWriter.prototype.writeBytes = function writeBytes(value) { + if (value.length === 0) + return; + + this.written += value.copy(this.data, this.written); +}; + +/** + * Write bytes with a varint length before them. + * @param {Buffer} value + */ + +StaticWriter.prototype.writeVarBytes = function writeVarBytes(value) { + this.writeVarint(value.length); + + if (value.length === 0) + return; + + this.writeBytes(value); +}; + +/** + * Write string to buffer. + * @param {String|Buffer} value + * @param {String?} enc - Any buffer-supported encoding. + */ + +StaticWriter.prototype.writeString = function writeString(value, enc) { + if (typeof value !== 'string') + return this.writeBytes(value); + + if (value.length === 0) + return; + + this.written += this.data.write(value, this.written, enc); +}; + +/** + * Write a hash/hex-string. + * @param {Hash|Buffer} + */ + +StaticWriter.prototype.writeHash = function writeHash(value) { + this.writeString(value, 'hex'); +}; + +/** + * Write a string with a varint length before it. + * @param {String|Buffer} + * @param {String?} enc - Any buffer-supported encoding. + */ + +StaticWriter.prototype.writeVarString = function writeVarString(value, enc) { + var size; + + if (typeof value !== 'string') + return this.writeVarBytes(value); + + size = Buffer.byteLength(value, enc); + + this.writeVarint(size); + + if (value.length === 0) + return; + + this.writeString(value, enc); +}; + +/** + * Write a null-terminated string. + * @param {String|Buffer} + * @param {String?} enc - Any buffer-supported encoding. + */ + +StaticWriter.prototype.writeNullString = function writeNullString(value, enc) { + this.writeString(value, enc); + this.writeU8(0); +}; + +/** + * Calculate and write a checksum for the data written so far. + */ + +StaticWriter.prototype.writeChecksum = function writeChecksum() { + var data = this.data.slice(0, this.written); + var hash = crypto.hash256(data); + this.written += hash.copy(this.data, this.written, 0, 4); +}; + +/** + * Fill N bytes with value. + * @param {Number} value + * @param {Number} size + */ + +StaticWriter.prototype.fill = function fill(value, size) { + assert(size >= 0); + + if (size === 0) + return; + + this.data.fill(value, this.written, this.written + size); + this.written += size; +}; + +/* + * Expose + */ + +module.exports = StaticWriter;