chaindb: switch to pertxout.

This commit is contained in:
Christopher Jeffrey 2017-07-02 18:10:05 -07:00
parent 396ba5ee5f
commit a1af3ab980
No known key found for this signature in database
GPG Key ID: 8962AB9DE6666BBD
12 changed files with 552 additions and 1012 deletions

View File

@ -25,6 +25,7 @@ const Outpoint = require('../primitives/outpoint');
const Address = require('../primitives/address');
const ChainEntry = require('./chainentry');
const TXMeta = require('../primitives/txmeta');
const CoinEntry = require('../coins/coinentry');
const U8 = encoding.U8;
const U32 = encoding.U32;
@ -83,7 +84,7 @@ ChainDB.prototype.open = async function open() {
this.logger.info('Opening ChainDB...');
await this.db.open();
await this.db.checkVersion('V', 2);
await this.db.checkVersion('V', 3);
state = await this.getState();
@ -839,6 +840,40 @@ ChainDB.prototype.getTips = function getTips() {
});
};
/**
* Get a coin (unspents only).
* @method
* @private
* @param {Outpoint} outpoint
* @returns {Promise} - Returns {@link CoinEntry}.
*/
ChainDB.prototype.readCoin = async function readCoin(outpoint) {
let state = this.state;
let key = outpoint.toKey();
let hash = outpoint.hash;
let index = outpoint.index;
let raw;
if (this.options.spv)
return;
raw = this.coinCache.get(key);
if (raw)
return CoinEntry.fromRaw(raw);
raw = await this.db.get(layout.c(hash, index));
if (!raw)
return;
if (state === this.state)
this.coinCache.set(key, raw);
return CoinEntry.fromRaw(raw);
};
/**
* Get a coin (unspents only).
* @method
@ -848,52 +883,9 @@ ChainDB.prototype.getTips = function getTips() {
*/
ChainDB.prototype.getCoin = async function getCoin(hash, index) {
let state = this.state;
let raw;
if (this.options.spv)
return;
raw = this.coinCache.get(hash);
if (raw)
return Coins.parseCoin(raw, hash, index);
raw = await this.db.get(layout.c(hash));
if (!raw)
return;
if (state === this.state)
this.coinCache.set(hash, raw);
return Coins.parseCoin(raw, hash, index);
};
/**
* Get coins (unspents only).
* @method
* @param {Hash} hash
* @returns {Promise} - Returns {@link Coins}.
*/
ChainDB.prototype.getCoins = async function getCoins(hash) {
let raw;
if (this.options.spv)
return;
raw = this.coinCache.get(hash);
if (raw)
return Coins.fromRaw(raw, hash);
raw = await this.db.get(layout.c(hash));
if (!raw)
return;
return Coins.fromRaw(raw, hash);
let prevout = new Outpoint(hash, index);
let coin = await this.readCoin(prevout);
return coin.toCoin(prevout);
};
/**
@ -903,8 +895,18 @@ ChainDB.prototype.getCoins = async function getCoins(hash) {
* @returns {Promise} - Returns Boolean.
*/
ChainDB.prototype.hasCoins = function hasCoins(hash) {
return this.db.has(layout.c(hash));
ChainDB.prototype.hasCoins = async function hasCoins(hash) {
let iter = this.db.iterator({
gte: layout.c(hash, 0x00000000),
lte: layout.c(hash, 0xffffffff)
});
if (!(await iter.next()))
return false;
await iter.end();
return true;
};
/**
@ -916,19 +918,17 @@ ChainDB.prototype.hasCoins = function hasCoins(hash) {
ChainDB.prototype.getCoinView = async function getCoinView(tx) {
let view = new CoinView();
let prevout = tx.getPrevout();
for (let hash of prevout) {
let coins = await this.getCoins(hash);
for (let {prevout} of tx.inputs) {
let coin = await this.readCoin(prevout);
if (!coins) {
coins = new Coins();
coins.hash = hash;
view.add(coins);
if (!coin) {
let coins = new Coins();
view.add(prevout.hash, coins);
continue;
}
view.add(coins);
view.addEntry(prevout, coin);
}
return view;
@ -944,13 +944,13 @@ ChainDB.prototype.getCoinView = async function getCoinView(tx) {
ChainDB.prototype.getSpentView = async function getSpentView(tx) {
let view = await this.getCoinView(tx);
for (let coins of view.map.values()) {
for (let [hash, coins] of view.map) {
let meta;
if (!coins.isEmpty())
continue;
meta = await this.getMeta(coins.hash);
meta = await this.getMeta(hash);
if (!meta)
continue;
@ -1031,19 +1031,6 @@ ChainDB.prototype.getBlockView = async function getBlockView(block) {
for (let j = tx.inputs.length - 1; j >= 0; j--) {
let input = tx.inputs[j];
let prev = input.prevout.hash;
if (!view.has(prev)) {
assert(!undo.isEmpty());
if (undo.top().height === -1) {
let coins = new Coins();
coins.hash = prev;
coins.coinbase = false;
view.add(coins);
}
}
undo.apply(view, input.prevout);
}
}
@ -1697,14 +1684,24 @@ ChainDB.prototype.removeBlock = async function removeBlock(entry) {
*/
ChainDB.prototype.saveView = function saveView(view) {
for (let coins of view.map.values()) {
if (coins.isEmpty()) {
this.del(layout.c(coins.hash));
this.coinCache.unpush(coins.hash);
} else {
let raw = coins.toRaw();
this.put(layout.c(coins.hash), raw);
this.coinCache.push(coins.hash, raw);
for (let [hash, coins] of view.map) {
for (let i = 0; i < coins.outputs.length; i++) {
let coin = coins.outputs[i];
let raw;
if (!coin)
continue;
if (coin.spent) {
this.del(layout.c(hash, i));
this.coinCache.unpush(hash + i);
continue;
}
raw = coin.toRaw();
this.put(layout.c(hash, i), raw);
this.coinCache.push(hash + i, raw);
}
}
};
@ -1786,8 +1783,6 @@ ChainDB.prototype.disconnectBlock = async function disconnectBlock(entry, block)
let tx = block.txs[i];
if (i > 0) {
await view.ensureInputs(this, tx);
for (let j = tx.inputs.length - 1; j >= 0; j--) {
let input = tx.inputs[j];
undo.apply(view, input.prevout);

View File

@ -36,8 +36,8 @@ const layout = {
t: function t(hash) {
return 't' + hex(hash);
},
c: function c(hash) {
return 'c' + hex(hash);
c: function c(hash, index) {
return 'c' + hex(hash) + pad32(index);
},
u: function u(hash) {
return 'u' + hex(hash);

View File

@ -53,8 +53,8 @@ const layout = {
t: function t(hash) {
return pair(0x74, hash);
},
c: function c(hash) {
return pair(0x63, hash);
c: function c(hash, index) {
return bpair(0x63, hash, index);
},
u: function u(hash) {
return pair(0x75, hash);
@ -160,6 +160,14 @@ function ipair(prefix, num) {
return key;
}
function bpair(prefix, hash, index) {
let key = Buffer.allocUnsafe(37);
key[0] = prefix;
write(key, hash, 1);
key.writeUInt32BE(index, 33, true);
return key;
}
/*
* Expose
*/

280
lib/coins/coinentry.js Normal file
View File

@ -0,0 +1,280 @@
/*!
* coinentry.js - coin entry object for bcoin
* Copyright (c) 2014-2017, Christopher Jeffrey (MIT License).
* https://github.com/bcoin-org/bcoin
*/
'use strict';
const assert = require('assert');
const Output = require('../primitives/output');
const BufferReader = require('../utils/reader');
const StaticWriter = require('../utils/staticwriter');
const encoding = require('../utils/encoding');
const Coin = require('../primitives/coin');
const {compress, decompress} = require('../coins/compress');
/**
* Represents an unspent output.
* @alias module:coins.CoinEntry
* @constructor
* @property {Number} version - Transaction version.
* @property {Number} height - Transaction height (-1 if unconfirmed).
* @property {Boolean} coinbase - Whether the containing
* transaction is a coinbase.
* @property {Output} output
* @property {Boolean} spent
* @property {Buffer} raw
*/
function CoinEntry() {
if (!(this instanceof CoinEntry))
return new CoinEntry();
this.version = 1;
this.height = -1;
this.coinbase = false;
this.output = new Output();
this.spent = false;
this.raw = null;
}
/**
* Convert coin entry to an output.
* @returns {Output}
*/
CoinEntry.prototype.toOutput = function toOutput() {
return this.output;
};
/**
* Convert coin entry to a coin.
* @param {Outpoint} prevout
* @returns {Coin}
*/
CoinEntry.prototype.toCoin = function toCoin(prevout) {
let coin = new Coin();
coin.version = this.version;
coin.height = this.height;
coin.coinbase = this.coinbase;
coin.script = this.output.script;
coin.value = this.output.value;
coin.hash = prevout.hash;
coin.index = prevout.index;
return coin;
};
/**
* Inject properties from TX.
* @param {TX} tx
* @param {Number} index
*/
CoinEntry.prototype.fromOutput = function fromOutput(output) {
this.output = output;
return this;
};
/**
* Instantiate a coin from a TX
* @param {TX} tx
* @param {Number} index - Output index.
* @returns {CoinEntry}
*/
CoinEntry.fromOutput = function fromOutput(output) {
return new CoinEntry().fromOutput(output);
};
/**
* Inject properties from TX.
* @param {TX} tx
* @param {Number} index
*/
CoinEntry.prototype.fromCoin = function fromCoin(coin) {
this.version = coin.version;
this.height = coin.height;
this.coinbase = coin.coinbase;
this.output.script = coin.script;
this.output.value = coin.value;
return this;
};
/**
* Instantiate a coin from a TX
* @param {TX} tx
* @param {Number} index - Output index.
* @returns {CoinEntry}
*/
CoinEntry.fromCoin = function fromCoin(coin) {
return new CoinEntry().fromCoin(coin);
};
/**
* Inject properties from TX.
* @param {TX} tx
* @param {Number} index
*/
CoinEntry.prototype.fromTX = function fromTX(tx, index, height) {
assert(typeof index === 'number');
assert(typeof height === 'number');
assert(index >= 0 && index < tx.outputs.length);
this.version = tx.version;
this.height = height;
this.coinbase = tx.isCoinbase();
this.output = tx.outputs[index];
return this;
};
/**
* Instantiate a coin from a TX
* @param {TX} tx
* @param {Number} index - Output index.
* @returns {CoinEntry}
*/
CoinEntry.fromTX = function fromTX(tx, index, height) {
return new CoinEntry().fromTX(tx, index, height);
};
/**
* Calculate size of coin.
* @returns {Number}
*/
CoinEntry.prototype.getSize = function getSize() {
let size;
if (this.raw)
return this.raw.length;
size = 0;
size += encoding.sizeVarint(this.version);
size += 4;
size += compress.size(this.output);
return size;
};
/**
* Write the coin to a buffer writer.
* @param {BufferWriter} bw
*/
CoinEntry.prototype.toWriter = function toWriter(bw) {
let flags = 0;
let height = this.height;
let field;
if (this.raw) {
bw.writeBytes(this.raw);
return bw;
}
// We save 29 bits for the height.
// This should be good for the next 4000 years.
if (height === -1)
height = 0x1fffffff;
if (this.coinbase)
flags |= 1;
field = (height * 8) | flags;
bw.writeVarint(this.version);
bw.writeU32(field);
compress.output(this.output, bw);
return bw;
};
/**
* Serialize the coin.
* @returns {Buffer}
*/
CoinEntry.prototype.toRaw = function toRaw() {
let size, bw;
if (this.raw)
return this.raw;
size = this.getSize();
bw = new StaticWriter(size);
this.toWriter(bw);
this.raw = bw.render();
return this.raw;
};
/**
* Inject properties from serialized buffer writer.
* @private
* @param {BufferReader} br
*/
CoinEntry.prototype.fromReader = function fromReader(br) {
let field, flags, height;
this.version = br.readVarint();
field = br.readU32();
height = field / 8 | 0;
flags = field & 0x07;
if (height === 0x1fffffff)
height = -1;
this.coinbase = (flags & 1) !== 0;
this.height = height;
decompress.output(this.output, br);
return this;
};
/**
* Instantiate a coin from a serialized Buffer.
* @param {Buffer} data
* @returns {CoinEntry}
*/
CoinEntry.fromReader = function fromReader(data) {
return new CoinEntry().fromReader(data);
};
/**
* Inject properties from serialized data.
* @private
* @param {Buffer} data
*/
CoinEntry.prototype.fromRaw = function fromRaw(data) {
this.fromReader(new BufferReader(data));
this.raw = data;
return this;
};
/**
* Instantiate a coin from a serialized Buffer.
* @param {Buffer} data
* @returns {CoinEntry}
*/
CoinEntry.fromRaw = function fromRaw(data) {
return new CoinEntry().fromRaw(data);
};
/*
* Expose
*/
module.exports = CoinEntry;

View File

@ -6,27 +6,14 @@
'use strict';
const util = require('../utils/util');
const assert = require('assert');
const Coin = require('../primitives/coin');
const Output = require('../primitives/output');
const BufferReader = require('../utils/reader');
const StaticWriter = require('../utils/staticwriter');
const encoding = require('../utils/encoding');
const compressor = require('./compress');
const compress = compressor.compress;
const decompress = compressor.decompress;
const CoinEntry = require('./coinentry');
/**
* Represents the outputs for a single transaction.
* @alias module:coins.Coins
* @constructor
* @param {Object?} options - Options object.
* @property {Hash} hash - Transaction hash.
* @property {Number} version - Transaction version.
* @property {Number} height - Transaction height (-1 if unconfirmed).
* @property {Boolean} coinbase - Whether the containing
* transaction is a coinbase.
* @property {CoinEntry[]} outputs - Coins.
*/
@ -34,69 +21,16 @@ function Coins(options) {
if (!(this instanceof Coins))
return new Coins(options);
this.version = 1;
this.hash = encoding.NULL_HASH;
this.height = -1;
this.coinbase = true;
this.outputs = [];
if (options)
this.fromOptions(options);
}
/**
* Inject properties from options object.
* @private
* @param {Object} options
*/
Coins.prototype.fromOptions = function fromOptions(options) {
if (options.version != null) {
assert(util.isUInt32(options.version));
this.version = options.version;
}
if (options.hash) {
assert(typeof options.hash === 'string');
this.hash = options.hash;
}
if (options.height != null) {
assert(util.isNumber(options.height));
this.height = options.height;
}
if (options.coinbase != null) {
assert(typeof options.coinbase === 'boolean');
this.coinbase = options.coinbase;
}
if (options.outputs) {
assert(Array.isArray(options.outputs));
this.outputs = options.outputs;
this.cleanup();
}
return this;
};
/**
* Instantiate coins from options object.
* @param {Object} options
* @returns {Coins}
*/
Coins.fromOptions = function fromOptions(options) {
return new Coins().fromOptions(options);
};
/**
* Add a single entry to the collection.
* @param {Number} index
* @param {CoinEntry} entry
* @param {CoinEntry} coin
*/
Coins.prototype.add = function add(index, entry) {
Coins.prototype.add = function add(index, coin) {
assert(index >= 0);
while (this.outputs.length <= index)
@ -104,7 +38,7 @@ Coins.prototype.add = function add(index, entry) {
assert(!this.outputs[index]);
this.outputs[index] = entry;
this.outputs[index] = coin;
};
/**
@ -142,21 +76,20 @@ Coins.prototype.has = function has(index) {
};
/**
* Test whether the collection
* has an unspent coin.
* Test whether the collection has an unspent coin.
* @param {Number} index
* @returns {Boolean}
*/
Coins.prototype.isUnspent = function isUnspent(index) {
let output;
let coin;
if (index >= this.outputs.length)
return false;
output = this.outputs[index];
coin = this.outputs[index];
if (!output || output.spent)
if (!coin || coin.spent)
return false;
return true;
@ -182,27 +115,23 @@ Coins.prototype.get = function get(index) {
*/
Coins.prototype.getOutput = function getOutput(index) {
let entry = this.get(index);
if (!entry)
if (index >= this.outputs.length)
return;
return entry.toOutput();
return this.outputs[index].output;
};
/**
* Get a coin.
* @param {Number} index
* @param {Outpoint} prevout
* @returns {Coin}
*/
Coins.prototype.getCoin = function getCoin(index) {
let entry = this.get(index);
if (!entry)
Coins.prototype.getCoin = function getCoin(prevout) {
if (prevout.index >= this.outputs.length)
return;
return entry.toCoin(this, index);
return this.outputs[prevout.index].toCoin(prevout);
};
/**
@ -212,14 +141,14 @@ Coins.prototype.getCoin = function getCoin(index) {
*/
Coins.prototype.spend = function spend(index) {
let entry = this.get(index);
let coin = this.get(index);
if (!entry || entry.spent)
if (!coin || coin.spent)
return;
entry.spent = true;
coin.spent = true;
return entry;
return coin;
};
/**
@ -229,15 +158,15 @@ Coins.prototype.spend = function spend(index) {
*/
Coins.prototype.remove = function remove(index) {
let entry = this.get(index);
let coin = this.get(index);
if (!entry)
if (!coin)
return false;
this.outputs[index] = null;
this.cleanup();
return entry;
return coin;
};
/**
@ -276,284 +205,6 @@ Coins.prototype.isEmpty = function isEmpty() {
return this.length() === 0;
};
/*
* Coins serialization:
* version: varint
* height: uint32
* header-code: varint
* bit 1: coinbase
* bit 2: first output unspent
* bit 3: second output unspent
* bit 4-32: spent-field size
* spent-field: bitfield (0=spent, 1=unspent)
* outputs (repeated):
* value: varint
* compressed-script:
* prefix: 0x00 = 20 byte pubkey hash
* 0x01 = 20 byte script hash
* 0x02-0x05 = 32 byte ec-key x-value
* 0x06-0x09 = reserved
* >=0x10 = varint-size + 10 | raw script
* data: script data, dictated by the prefix
*
* The compression below sacrifices some cpu in exchange
* for reduced size, but in some cases the use of varints
* actually increases speed (varint versions and values
* for example). We do as much compression as possible
* without sacrificing too much cpu. Value compression
* is intentionally excluded for now as it seems to be
* too much of a perf hit. Maybe when v8 optimizes
* non-smi arithmetic better we can enable it.
*/
/**
* Calculate header code.
* @param {Number} len
* @param {Number} size
* @returns {Number}
*/
Coins.prototype.header = function header(len, size) {
let first = this.isUnspent(0);
let second = this.isUnspent(1);
let offset = 0;
let code;
// Throw if we're fully spent.
assert(len !== 0, 'Cannot serialize fully-spent coins.');
// First and second bits
// have a double meaning.
if (!first && !second) {
assert(size !== 0);
offset = 1;
}
// Calculate header code.
code = 8 * (size - offset);
if (this.coinbase)
code += 1;
if (first)
code += 2;
if (second)
code += 4;
return code;
};
/**
* Serialize the coins object.
* @returns {Buffer}
*/
Coins.prototype.toRaw = function toRaw() {
let len = this.length();
let size = Math.floor((len + 5) / 8);
let code = this.header(len, size);
let total = this.getSize(len, size, code);
let bw = new StaticWriter(total);
// Write headers.
bw.writeVarint(this.version);
bw.writeU32(this.height);
bw.writeVarint(code);
// Write the spent field.
for (let i = 0; i < size; i++) {
let ch = 0;
for (let j = 0; j < 8 && 2 + i * 8 + j < len; j++) {
if (this.isUnspent(2 + i * 8 + j))
ch |= 1 << j;
}
bw.writeU8(ch);
}
// Write the compressed outputs.
for (let i = 0; i < len; i++) {
let output = this.outputs[i];
if (!output || output.spent)
continue;
output.toWriter(bw);
}
return bw.render();
};
/**
* Calculate coins size.
* @param {Number} code
* @param {Number} size
* @param {Number} len
* @returns {Number}
*/
Coins.prototype.getSize = function getSize(len, size, code) {
let total = 0;
total += encoding.sizeVarint(this.version);
total += 4;
total += encoding.sizeVarint(code);
total += size;
// Write the compressed outputs.
for (let i = 0; i < len; i++) {
let output = this.outputs[i];
if (!output || output.spent)
continue;
total += output.getSize();
}
return total;
};
/**
* Inject data from serialized coins.
* @private
* @param {Buffer} data
* @param {Hash} hash
* @returns {Coins}
*/
Coins.prototype.fromRaw = function fromRaw(data, hash) {
let br = new BufferReader(data);
let first = null;
let second = null;
let code, size, offset;
// Inject hash (passed by caller).
this.hash = hash;
// Read headers.
this.version = br.readVarint();
this.height = br.readU32();
code = br.readVarint();
this.coinbase = (code & 1) !== 0;
// Recalculate size.
size = code / 8 | 0;
if ((code & 6) === 0)
size += 1;
// Setup spent field.
offset = br.offset;
br.seek(size);
// Read first two outputs.
if ((code & 2) !== 0)
first = CoinEntry.fromReader(br);
if ((code & 4) !== 0)
second = CoinEntry.fromReader(br);
this.outputs.push(first);
this.outputs.push(second);
// Read outputs.
for (let i = 0; i < size; i++) {
let ch = br.data[offset++];
for (let j = 0; j < 8; j++) {
if ((ch & (1 << j)) === 0) {
this.outputs.push(null);
continue;
}
this.outputs.push(CoinEntry.fromReader(br));
}
}
this.cleanup();
return this;
};
/**
* Parse a single serialized coin.
* @param {Buffer} data
* @param {Hash} hash
* @param {Number} index
* @returns {Coin}
*/
Coins.parseCoin = function parseCoin(data, hash, index) {
let br = new BufferReader(data);
let coin = new Coin();
let code, size, offset;
// Inject outpoint (passed by caller).
coin.hash = hash;
coin.index = index;
// Read headers.
coin.version = br.readVarint();
coin.height = br.readU32();
code = br.readVarint();
coin.coinbase = (code & 1) !== 0;
// Recalculate size.
size = code / 8 | 0;
if ((code & 6) === 0)
size += 1;
if (index >= 2 + size * 8)
return;
// Setup spent field.
offset = br.offset;
br.seek(size);
// Read first two outputs.
for (let i = 0; i < 2; i++) {
if ((code & (2 << i)) !== 0) {
if (index === 0) {
decompress.coin(coin, br);
return coin;
}
decompress.skip(br);
} else {
if (index === 0)
return;
}
index -= 1;
}
// Read outputs.
for (let i = 0; i < size; i++) {
let ch = br.data[offset++];
for (let j = 0; j < 8; j++) {
if ((ch & (1 << j)) !== 0) {
if (index === 0) {
decompress.coin(coin, br);
return coin;
}
decompress.skip(br);
} else {
if (index === 0)
return;
}
index -= 1;
}
}
};
/**
* Instantiate coins from a buffer.
* @param {Buffer} data
* @param {Hash} hash - Transaction hash.
* @returns {Coins}
*/
Coins.fromRaw = function fromRaw(data, hash) {
return new Coins().fromRaw(data, hash);
};
/**
* Inject properties from tx.
* @private
@ -562,21 +213,17 @@ Coins.fromRaw = function fromRaw(data, hash) {
*/
Coins.prototype.fromTX = function fromTX(tx, height) {
let output;
assert(typeof height === 'number');
this.version = tx.version;
this.hash = tx.hash('hex');
this.height = height;
this.coinbase = tx.isCoinbase();
for (let i = 0; i < tx.outputs.length; i++) {
let output = tx.outputs[i];
for (output of tx.outputs) {
if (output.script.isUnspendable()) {
this.outputs.push(null);
continue;
}
this.outputs.push(CoinEntry.fromOutput(output));
this.outputs.push(CoinEntry.fromTX(tx, i, height));
}
this.cleanup();
@ -595,169 +242,8 @@ Coins.fromTX = function fromTX(tx, height) {
return new Coins().fromTX(tx, height);
};
/**
* A coin entry is an object which defers
* parsing of a coin. Say there is a transaction
* with 100 outputs. When a block comes in,
* there may only be _one_ input in that entire
* block which redeems an output from that
* transaction. When parsing the Coins, there
* is no sense to get _all_ of them into their
* abstract form. A coin entry is just a
* pointer to that coin in the Coins buffer, as
* well as a size. Parsing and decompression
* is done only if that coin is being redeemed.
* @alias module:coins.CoinEntry
* @constructor
* @property {Number} offset
* @property {Number} size
* @property {Buffer} raw
* @property {Output|null} output
* @property {Boolean} spent
*/
function CoinEntry() {
this.offset = 0;
this.size = 0;
this.raw = null;
this.output = null;
this.spent = false;
}
/**
* Instantiate a reader at the correct offset.
* @private
* @returns {BufferReader}
*/
CoinEntry.prototype.reader = function reader() {
let br;
assert(this.raw);
br = new BufferReader(this.raw);
br.offset = this.offset;
return br;
};
/**
* Parse the deferred data and return a coin.
* @param {Coins} coins
* @param {Number} index
* @returns {Coin}
*/
CoinEntry.prototype.toCoin = function toCoin(coins, index) {
let coin = new Coin();
let output = this.toOutput();
// Load in all necessary properties
// from the parent Coins object.
coin.version = coins.version;
coin.coinbase = coins.coinbase;
coin.height = coins.height;
coin.hash = coins.hash;
coin.index = index;
coin.script = output.script;
coin.value = output.value;
return coin;
};
/**
* Parse the deferred data and return an output.
* @returns {Output}
*/
CoinEntry.prototype.toOutput = function toOutput() {
if (!this.output) {
this.output = new Output();
decompress.output(this.output, this.reader());
}
return this.output;
};
/**
* Calculate coin entry size.
* @returns {Number}
*/
CoinEntry.prototype.getSize = function getSize() {
if (!this.raw)
return compress.size(this.output);
return this.size;
};
/**
* Slice off the part of the buffer
* relevant to this particular coin.
*/
CoinEntry.prototype.toWriter = function toWriter(bw) {
if (!this.raw) {
assert(this.output);
compress.output(this.output, bw);
return bw;
}
// If we read this coin from the db and
// didn't use it, it's still in its
// compressed form. Just write it back
// as a buffer for speed.
bw.copy(this.raw, this.offset, this.offset + this.size);
return bw;
};
/**
* Instantiate coin entry from reader.
* @param {BufferReader} br
* @returns {CoinEntry}
*/
CoinEntry.fromReader = function fromReader(br) {
let entry = new CoinEntry();
entry.offset = br.offset;
entry.size = decompress.skip(br);
entry.raw = br.data;
return entry;
};
/**
* Instantiate coin entry from output.
* @param {Output} output
* @returns {CoinEntry}
*/
CoinEntry.fromOutput = function fromOutput(output) {
let entry = new CoinEntry();
entry.output = output;
return entry;
};
/**
* Instantiate coin entry from coin.
* @param {Coin} coin
* @returns {CoinEntry}
*/
CoinEntry.fromCoin = function fromCoin(coin) {
let entry = new CoinEntry();
let output = new Output();
output.value = coin.value;
output.script = coin.script;
entry.output = output;
return entry;
};
/*
* Expose
*/
exports = Coins;
exports.Coins = Coins;
exports.CoinEntry = CoinEntry;
module.exports = exports;
module.exports = Coins;

View File

@ -6,10 +6,9 @@
'use strict';
const assert = require('assert');
const Coins = require('./coins');
const UndoCoins = require('./undocoins');
const CoinEntry = Coins.CoinEntry;
const CoinEntry = require('./coinentry');
/**
* Represents a coin viewpoint:
@ -50,11 +49,12 @@ CoinView.prototype.has = function has(hash) {
/**
* Add coins to the collection.
* @param {Hash} hash
* @param {Coins} coins
*/
CoinView.prototype.add = function add(coins) {
this.map.set(coins.hash, coins);
CoinView.prototype.add = function add(hash, coins) {
this.map.set(hash, coins);
return coins;
};
@ -81,7 +81,7 @@ CoinView.prototype.remove = function remove(hash) {
CoinView.prototype.addTX = function addTX(tx, height) {
let coins = Coins.fromTX(tx, height);
return this.add(coins);
return this.add(tx.hash('hex'), coins);
};
/**
@ -92,8 +92,35 @@ CoinView.prototype.addTX = function addTX(tx, height) {
CoinView.prototype.removeTX = function removeTX(tx, height) {
let coins = Coins.fromTX(tx, height);
coins.outputs.length = 0;
return this.add(coins);
for (let coin of coins.outputs)
coin.spent = true;
return this.add(tx.hash('hex'), coins);
};
/**
* Add an entry to the collection.
* @param {Outpoint} prevout
* @param {CoinEntry} coin
* @returns {Coins|null}
*/
CoinView.prototype.addEntry = function addEntry(prevout, coin) {
let coins = this.get(prevout.hash);
if (!coins) {
coins = new Coins();
this.add(prevout.hash, coins);
}
if (coin.output.script.isUnspendable())
return;
if (!coins.has(prevout.index))
coins.add(prevout.index, coin);
return coins;
};
/**
@ -106,10 +133,7 @@ CoinView.prototype.addCoin = function addCoin(coin) {
if (!coins) {
coins = new Coins();
coins.hash = coin.hash;
coins.height = coin.height;
coins.coinbase = coin.coinbase;
this.add(coins);
this.add(coin.hash, coins);
}
if (coin.script.isUnspendable())
@ -117,63 +141,59 @@ CoinView.prototype.addCoin = function addCoin(coin) {
if (!coins.has(coin.index))
coins.addCoin(coin);
return coins;
};
/**
* Add an output to the collection.
* @param {Hash} hash
* @param {Number} index
* @param {Outpoint} prevout
* @param {Output} output
*/
CoinView.prototype.addOutput = function addOutput(hash, index, output) {
let coins = this.get(hash);
CoinView.prototype.addOutput = function addOutput(prevout, output) {
let coins = this.get(prevout.hash);
if (!coins) {
coins = new Coins();
coins.hash = hash;
coins.height = -1;
coins.coinbase = false;
this.add(coins);
this.add(prevout.hash, coins);
}
if (output.script.isUnspendable())
return;
if (!coins.has(index))
coins.addOutput(index, output);
if (!coins.has(prevout.index))
coins.addOutput(prevout.index, output);
};
/**
* Spend an output.
* @param {Hash} hash
* @param {Number} index
* @param {Outpoint} prevout
* @returns {Boolean}
*/
CoinView.prototype.spendOutput = function spendOutput(hash, index) {
let coins = this.get(hash);
CoinView.prototype.spendOutput = function spendOutput(prevout) {
let coins = this.get(prevout.hash);
if (!coins)
return false;
return this.spendFrom(coins, index);
return this.spendFrom(coins, prevout.index);
};
/**
* Remove an output.
* @param {Hash} hash
* @param {Number} index
* @param {Outpoint} prevout
* @returns {Boolean}
*/
CoinView.prototype.removeOutput = function removeOutput(hash, index) {
let coins = this.get(hash);
CoinView.prototype.removeOutput = function removeOutput(prevout) {
let coins = this.get(prevout.hash);
if (!coins)
return false;
return coins.remove(index);
return coins.remove(prevout.index);
};
/**
@ -184,55 +204,16 @@ CoinView.prototype.removeOutput = function removeOutput(hash, index) {
*/
CoinView.prototype.spendFrom = function spendFrom(coins, index) {
let entry = coins.spend(index);
let undo;
let coin = coins.spend(index);
if (!entry)
if (!coin)
return false;
this.undo.push(entry);
if (coins.isEmpty()) {
undo = this.undo.top();
undo.height = coins.height;
undo.coinbase = coins.coinbase;
undo.version = coins.version;
assert(undo.height !== -1);
}
this.undo.push(coin);
return true;
};
/**
* Get a single coin by input.
* @param {Input} input
* @returns {Coin}
*/
CoinView.prototype.getCoin = function getCoin(input) {
let coins = this.get(input.prevout.hash);
if (!coins)
return;
return coins.getCoin(input.prevout.index);
};
/**
* Get a single output by input.
* @param {Input} input
* @returns {Output}
*/
CoinView.prototype.getOutput = function getOutput(input) {
let coins = this.get(input.prevout.hash);
if (!coins)
return;
return coins.getOutput(input.prevout.index);
};
/**
* Get a single entry by input.
* @param {Input} input
@ -248,6 +229,36 @@ CoinView.prototype.getEntry = function getEntry(input) {
return coins.get(input.prevout.index);
};
/**
* Get a single coin by input.
* @param {Input} input
* @returns {Coin}
*/
CoinView.prototype.getCoin = function getCoin(input) {
let coins = this.get(input.prevout.hash);
if (!coins)
return;
return coins.getCoin(input.prevout);
};
/**
* Get a single output by input.
* @param {Input} input
* @returns {Output}
*/
CoinView.prototype.getOutput = function getOutput(input) {
let coins = this.get(input.prevout.hash);
if (!coins)
return;
return coins.getOutput(input.prevout.index);
};
/**
* Test whether the view has an entry by input.
* @param {Input} input
@ -270,12 +281,12 @@ CoinView.prototype.hasEntry = function hasEntry(input) {
*/
CoinView.prototype.getHeight = function getHeight(input) {
let coins = this.get(input.prevout.hash);
let coin = this.getEntry(input);
if (!coins)
if (!coin)
return -1;
return coins.height;
return coin.height;
};
/**
@ -285,35 +296,36 @@ CoinView.prototype.getHeight = function getHeight(input) {
*/
CoinView.prototype.isCoinbase = function isCoinbase(input) {
let coins = this.get(input.prevout.hash);
let coin = this.getEntry(input);
if (!coins)
if (!coin)
return false;
return coins.coinbase;
return coin.coinbase;
};
/**
* Retrieve coins from database.
* @method
* @param {ChainDB} db
* @param {TX} tx
* @param {Input} input
* @returns {Promise} - Returns {@link Coins}.
*/
CoinView.prototype.readCoins = async function readCoins(db, hash) {
let coins = this.map.get(hash);
CoinView.prototype.readCoin = async function readCoin(db, input) {
let coin = this.hasEntry(input);
let prevout = input.prevout;
if (!coins) {
coins = await db.getCoins(hash);
if (!coin) {
coin = await db.readCoin(prevout);
if (!coins)
if (!coin)
return;
this.map.set(hash, coins);
return this.addEntry(prevout, coin);
}
return coins;
return this.get(prevout.hash);
};
/**
@ -328,7 +340,7 @@ CoinView.prototype.ensureInputs = async function ensureInputs(db, tx) {
let found = true;
for (let input of tx.inputs) {
if (!(await this.readCoins(db, input.prevout.hash)))
if (!(await this.readCoin(db, input)))
found = false;
}
@ -345,13 +357,12 @@ CoinView.prototype.ensureInputs = async function ensureInputs(db, tx) {
CoinView.prototype.spendInputs = async function spendInputs(db, tx) {
for (let input of tx.inputs) {
let prevout = input.prevout;
let coins = await this.readCoins(db, prevout.hash);
let coins = await this.readCoin(db, input);
if (!coins)
return false;
if (!this.spendFrom(coins, prevout.index))
if (!this.spendFrom(coins, input.prevout.index))
return false;
}
@ -383,12 +394,12 @@ CoinView.prototype.getSize = function getSize(tx) {
size += tx.inputs.length;
for (let input of tx.inputs) {
let entry = this.getEntry(input);
let coin = this.getEntry(input);
if (!entry)
if (!coin)
continue;
size += entry.getSize();
size += coin.getSize();
}
return size;
@ -403,24 +414,15 @@ CoinView.prototype.getSize = function getSize(tx) {
CoinView.prototype.toWriter = function toWriter(bw, tx) {
for (let input of tx.inputs) {
let prevout = input.prevout;
let coins = this.get(prevout.hash);
let entry;
let coin = this.getEntry(input);
if (!coins) {
bw.writeU8(0);
continue;
}
entry = coins.get(prevout.index);
if (!entry) {
if (!coin) {
bw.writeU8(0);
continue;
}
bw.writeU8(1);
entry.toWriter(bw);
coin.toWriter(bw);
}
return bw;
@ -435,24 +437,15 @@ CoinView.prototype.toWriter = function toWriter(bw, tx) {
*/
CoinView.prototype.fromReader = function fromReader(br, tx) {
for (let input of tx.inputs) {
let prevout = input.prevout;
let coins, entry;
for (let {prevout} of tx.inputs) {
let coin;
if (br.readU8() === 0)
continue;
coins = this.get(prevout.hash);
coin = CoinEntry.fromReader(br);
if (!coins) {
coins = new Coins();
coins.hash = prevout.hash;
coins.coinbase = false;
this.add(coins);
}
entry = CoinEntry.fromReader(br);
coins.add(prevout.index, entry);
this.addEntry(prevout, coin);
}
return this;

View File

@ -9,12 +9,7 @@
const assert = require('assert');
const BufferReader = require('../utils/reader');
const StaticWriter = require('../utils/staticwriter');
const encoding = require('../utils/encoding');
const Output = require('../primitives/output');
const Coins = require('./coins');
const compressor = require('./compress');
const compress = compressor.compress;
const decompress = compressor.decompress;
const CoinEntry = require('../coins/coinentry');
/**
* UndoCoins
@ -39,10 +34,8 @@ function UndoCoins() {
* @param {CoinEntry}
*/
UndoCoins.prototype.push = function push(entry) {
let undo = new UndoCoin();
undo.entry = entry;
this.items.push(undo);
UndoCoins.prototype.push = function push(coin) {
this.items.push(coin);
};
/**
@ -90,7 +83,7 @@ UndoCoins.prototype.fromRaw = function fromRaw(data) {
let count = br.readU32();
for (let i = 0; i < count; i++)
this.items.push(UndoCoin.fromReader(br));
this.items.push(CoinEntry.fromReader(br));
return this;
};
@ -137,200 +130,19 @@ UndoCoins.prototype.top = function top() {
/**
* Re-apply undo coins to a view, effectively unspending them.
* @param {CoinView} view
* @param {Outpoint} outpoint
* @param {Outpoint} prevout
*/
UndoCoins.prototype.apply = function apply(view, outpoint) {
UndoCoins.prototype.apply = function apply(view, prevout) {
let undo = this.items.pop();
let hash = outpoint.hash;
let index = outpoint.index;
let coins;
assert(undo);
if (undo.height !== -1) {
coins = new Coins();
assert(!view.map.has(hash));
view.map.set(hash, coins);
coins.hash = hash;
coins.coinbase = undo.coinbase;
coins.height = undo.height;
coins.version = undo.version;
} else {
coins = view.map.get(hash);
assert(coins);
}
coins.addOutput(index, undo.toOutput());
assert(coins.has(index));
};
/**
* UndoCoin
* @alias module:coins.UndoCoin
* @constructor
* @property {CoinEntry|null} entry
* @property {Output|null} output
* @property {Number} version
* @property {Number} height
* @property {Boolean} coinbase
*/
function UndoCoin() {
this.entry = null;
this.output = null;
this.version = -1;
this.height = -1;
this.coinbase = false;
}
/**
* Convert undo coin to an output.
* @returns {Output}
*/
UndoCoin.prototype.toOutput = function toOutput() {
if (!this.output) {
assert(this.entry);
return this.entry.toOutput();
}
return this.output;
};
/**
* Calculate undo coin size.
* @returns {Number}
*/
UndoCoin.prototype.getSize = function getSize() {
let height = this.height;
let size = 0;
if (height === -1)
height = 0;
size += encoding.sizeVarint(height * 2 + (this.coinbase ? 1 : 0));
if (this.height !== -1)
size += encoding.sizeVarint(this.version);
if (this.entry) {
// Cached from spend.
size += this.entry.getSize();
} else {
size += compress.size(this.output);
}
return size;
};
/**
* Write the undo coin to a buffer writer.
* @param {BufferWriter} bw
*/
UndoCoin.prototype.toWriter = function toWriter(bw) {
let height = this.height;
assert(height !== 0);
if (height === -1)
height = 0;
bw.writeVarint(height * 2 + (this.coinbase ? 1 : 0));
if (this.height !== -1) {
assert(this.version !== -1);
bw.writeVarint(this.version);
}
if (this.entry) {
// Cached from spend.
this.entry.toWriter(bw);
} else {
compress.output(this.output, bw);
}
return bw;
};
/**
* Serialize the undo coin.
* @returns {Buffer}
*/
UndoCoin.prototype.toRaw = function toRaw() {
let size = this.getSize();
return this.toWriter(new StaticWriter(size)).render();
};
/**
* Inject properties from buffer reader.
* @private
* @param {BufferReader} br
* @returns {UndoCoin}
*/
UndoCoin.prototype.fromReader = function fromReader(br) {
let code = br.readVarint();
this.output = new Output();
this.height = code / 2 | 0;
if (this.height === 0)
this.height = -1;
this.coinbase = (code & 1) !== 0;
if (this.height !== -1)
this.version = br.readVarint();
decompress.output(this.output, br);
return this;
};
/**
* Inject properties from serialized data.
* @private
* @param {Buffer} data
* @returns {UndoCoin}
*/
UndoCoin.prototype.fromRaw = function fromRaw(data) {
return this.fromReader(new BufferReader(data));
};
/**
* Instantiate undo coin from serialized data.
* @param {Buffer} data
* @returns {UndoCoin}
*/
UndoCoin.fromReader = function fromReader(br) {
return new UndoCoin().fromReader(br);
};
/**
* Instantiate undo coin from serialized data.
* @param {Buffer} data
* @returns {UndoCoin}
*/
UndoCoin.fromRaw = function fromRaw(data) {
return new UndoCoin().fromRaw(data);
view.addEntry(prevout, undo);
};
/*
* Expose
*/
exports = UndoCoins;
exports.UndoCoins = UndoCoins;
exports.UndoCoin = UndoCoin;
module.exports = exports;
module.exports = UndoCoins;

View File

@ -1657,27 +1657,25 @@ Mempool.prototype.getSpentView = async function getSpentView(tx) {
Mempool.prototype.getCoinView = async function getCoinView(tx) {
let view = new CoinView();
let prevout = tx.getPrevout();
for (let hash of prevout) {
let entry = this.getEntry(hash);
let coins;
for (let {prevout} of tx.inputs) {
let entry = this.getEntry(prevout.hash);
let coin;
if (entry) {
view.addTX(entry.tx, -1);
continue;
}
coins = await this.chain.db.getCoins(hash);
coin = await this.chain.db.readCoin(prevout);
if (!coins) {
coins = new Coins();
coins.hash = hash;
view.add(coins);
if (!coin) {
let coins = new Coins();
view.add(prevout.hash, coins);
continue;
}
view.add(coins);
view.addEntry(prevout, coin);
}
return view;

View File

@ -1731,22 +1731,19 @@ TX.prototype.checkInputs = function checkInputs(view, height) {
assert(typeof height === 'number');
for (let input of this.inputs) {
let coins = view.get(input.prevout.hash);
let coin;
if (!coins)
return [-1, 'bad-txns-inputs-missingorspent', 0];
if (coins.coinbase) {
if (height - coins.height < consensus.COINBASE_MATURITY)
return [-1, 'bad-txns-premature-spend-of-coinbase', 0];
}
coin = coins.getOutput(input.prevout.index);
let coin = view.getEntry(input);
if (!coin)
return [-1, 'bad-txns-inputs-missingorspent', 0];
if (coin.coinbase) {
if (height - coin.height < consensus.COINBASE_MATURITY)
return [-1, 'bad-txns-premature-spend-of-coinbase', 0];
}
coin = view.getOutput(input);
assert(coin);
if (coin.value < 0 || coin.value > consensus.MAX_MONEY)
return [-1, 'bad-txns-inputvalues-outofrange', 100];

View File

@ -8,7 +8,6 @@ const Headers = require('../lib/primitives/headers');
const MerkleBlock = require('../lib/primitives/merkleblock');
const CoinView = require('../lib/coins/coinview');
const Coin = require('../lib/primitives/coin');
const Coins = require('../lib/coins/coins');
const UndoCoins = require('../lib/coins/undocoins');
const consensus = require('../lib/protocol/consensus');
const Script = require('../lib/script/script');
@ -32,19 +31,6 @@ function applyUndo(block, undo) {
for (let j = tx.inputs.length - 1; j >= 0; j--) {
let input = tx.inputs[j];
let prev = input.prevout.hash;
if (!view.has(prev)) {
assert(!undo.isEmpty());
if (undo.top().height === -1) {
let coins = new Coins();
coins.hash = prev;
coins.coinbase = false;
view.add(coins);
}
}
undo.apply(view, input.prevout);
}
}

View File

@ -5,41 +5,26 @@ const Output = require('../lib/primitives/output');
const Input = require('../lib/primitives/input');
const Outpoint = require('../lib/primitives/outpoint');
const CoinView = require('../lib/coins/coinview');
const Coins = require('../lib/coins/coins');
const CoinEntry = require('../lib/coins/coinentry');
const StaticWriter = require('../lib/utils/staticwriter');
const BufferReader = require('../lib/utils/reader');
const parseTX = require('./util/common').parseTX;
let data = parseTX('data/tx1.hex');
let tx1 = data.tx;
const data = parseTX('data/tx1.hex');
const tx1 = data.tx;
function collect(coins) {
let outputs = [];
let i;
for (i = 0; i < coins.outputs.length; i++) {
if (!coins.isUnspent(i))
continue;
outputs.push(coins.getOutput(i));
}
return outputs;
}
function reserialize(coins) {
let raw = coins.toRaw();
return Coins.fromRaw(raw);
function reserialize(coin) {
let raw = coin.toRaw();
let entry = CoinEntry.fromRaw(raw);
entry.raw = null;
return CoinEntry.fromRaw(entry.toRaw());
}
function deepCoinsEqual(a, b) {
assert(a.outputs.length > 0);
assert(b.outputs.length > 0);
assert.strictEqual(a.version, b.version);
assert.strictEqual(a.height, b.height);
assert.strictEqual(a.coinbase, b.coinbase);
assert.strictEqual(a.length(), b.length());
assert.deepStrictEqual(collect(a), collect(b));
assert.deepStrictEqual(a.raw, b.raw);
}
describe('Coins', function() {
@ -53,18 +38,15 @@ describe('Coins', function() {
view.addTX(tx1, 1);
coins = view.get(hash);
assert.equal(coins.version, 1);
assert.equal(coins.height, 1);
assert.equal(coins.coinbase, false);
assert.equal(coins.outputs.length, tx1.outputs.length);
entry = coins.get(0);
assert(entry);
assert(!entry.spent);
assert.equal(entry.offset, 0);
assert.equal(entry.size, 0);
assert.equal(entry.version, 1);
assert.equal(entry.height, 1);
assert.equal(entry.coinbase, false);
assert.equal(entry.raw, null);
assert(entry.output instanceof Output);
assert.equal(entry.spent, false);
@ -72,7 +54,7 @@ describe('Coins', function() {
output = view.getOutput(input);
assert(output);
deepCoinsEqual(coins, reserialize(coins));
deepCoinsEqual(entry, reserialize(entry));
});
it('should spend an output', () => {
@ -86,7 +68,7 @@ describe('Coins', function() {
assert(coins);
length = coins.length();
view.spendOutput(hash, 0);
view.spendOutput(new Outpoint(hash, 0));
coins = view.get(hash);
assert(coins);
@ -95,7 +77,7 @@ describe('Coins', function() {
assert(entry);
assert(entry.spent);
deepCoinsEqual(coins, reserialize(coins));
deepCoinsEqual(entry, reserialize(entry));
assert.strictEqual(coins.length(), length);
assert.equal(view.undo.items.length, 1);
@ -120,6 +102,8 @@ describe('Coins', function() {
prev = tx1.inputs[0].prevout;
coins = res.get(prev.hash);
assert.deepStrictEqual(coins.get(0), reserialize(coins).get(0));
assert.strictEqual(coins.length(), 2);
assert.strictEqual(coins.get(0), null);
deepCoinsEqual(coins.get(1), reserialize(coins.get(1)));
});
});

View File

@ -8,6 +8,7 @@ const consensus = require('../lib/protocol/consensus');
const TX = require('../lib/primitives/tx');
const Coin = require('../lib/primitives/coin');
const Output = require('../lib/primitives/output');
const Outpoint = require('../lib/primitives/outpoint');
const Script = require('../lib/script/script');
const Witness = require('../lib/script/witness');
const Input = require('../lib/primitives/input');
@ -349,7 +350,7 @@ describe('TX', function() {
let hash = random.randomBytes(32).toString('hex');
let output = new Output();
output.value = value;
view.addOutput(hash, 0, output);
view.addOutput(new Outpoint(hash, 0), output);
return {
prevout: {
hash: hash,