wallet: tests passing.

This commit is contained in:
Christopher Jeffrey 2017-10-16 21:09:49 -07:00
parent 55f5ff9493
commit c6d7c43485
No known key found for this signature in database
GPG Key ID: 8962AB9DE6666BBD
3 changed files with 138 additions and 294 deletions

View File

@ -199,10 +199,10 @@ TXDB.prototype.saveCredit = async function saveCredit(b, credit, path) {
const coin = credit.coin;
const raw = credit.toRaw();
await this.addOutpointMap(b, coin.hash, coin.index);
b.put(layout.c(coin.hash, coin.index), raw);
b.put(layout.C(path.account, coin.hash, coin.index), null);
return this.addOutpointMap(b, coin.hash, coin.index);
};
/**
@ -214,10 +214,10 @@ TXDB.prototype.saveCredit = async function saveCredit(b, credit, path) {
TXDB.prototype.removeCredit = async function removeCredit(b, credit, path) {
const coin = credit.coin;
await this.removeOutpointMap(b, coin.hash, coin.index);
b.del(layout.c(coin.hash, coin.index));
b.del(layout.C(path.account, coin.hash, coin.index));
return this.removeOutpointMap(b, coin.hash, coin.index);
};
/**
@ -247,94 +247,6 @@ TXDB.prototype.unspendCredit = function unspendCredit(b, tx, index) {
b.del(layout.d(spender.hash, spender.index));
};
/**
* Write input record.
* @param {TX} tx
* @param {Number} index
*/
TXDB.prototype.writeInput = function writeInput(b, tx, index) {
const prevout = tx.inputs[index].prevout;
const spender = Outpoint.fromTX(tx, index);
b.put(layout.s(prevout.hash, prevout.index), spender.toRaw());
};
/**
* Remove input record.
* @param {TX} tx
* @param {Number} index
*/
TXDB.prototype.removeInput = function removeInput(b, tx, index) {
const prevout = tx.inputs[index].prevout;
b.del(layout.s(prevout.hash, prevout.index));
};
/**
* Resolve orphan input.
* @param {TX} tx
* @param {Number} index
* @param {Number} height
* @param {Path} path
* @returns {Boolean}
*/
TXDB.prototype.resolveInput = async function resolveInput(b, state, tx, index, height, path, own) {
const hash = tx.hash('hex');
const spent = await this.getSpent(hash, index);
if (!spent)
return false;
// If we have an undo coin, we
// already knew about this input.
if (await this.hasSpentCoin(spent))
return false;
// Get the spending transaction so
// we can properly add the undo coin.
const stx = await this.getTX(spent.hash);
assert(stx);
// Crete the credit and add the undo coin.
const credit = Credit.fromTX(tx, index, height);
credit.own = own;
this.spendCredit(b, credit, stx.tx, spent.index);
// If the spender is unconfirmed, save
// the credit as well, and mark it as
// unspent in the mempool. This is the
// same behavior `insert` would have
// done for inputs. We're just doing
// it retroactively.
if (stx.height === -1) {
credit.spent = true;
await this.saveCredit(b, credit, path);
if (height !== -1)
state.confirmed += credit.coin.value;
}
return true;
};
/**
* Test an entire transaction to see
* if any of its outpoints are a double-spend.
* @param {TX} tx
* @returns {Promise} - Returns Boolean.
*/
TXDB.prototype.isDoubleSpend = async function isDoubleSpend(tx) {
for (const {prevout} of tx.inputs) {
const spent = await this.isSpent(prevout.hash, prevout.index);
if (spent)
return true;
}
return false;
};
/**
* Test a whether a coin has been spent.
* @param {Hash} hash
@ -461,23 +373,23 @@ TXDB.prototype.getBlock = async function getBlock(height) {
TXDB.prototype.addBlock = async function addBlock(b, hash, meta) {
const key = layout.b(meta.height);
let data = await this.get(key);
let block;
const data = await this.get(key);
if (!data) {
block = BlockRecord.fromMeta(meta);
data = block.toRaw();
const block = BlockRecord.fromMeta(meta);
block.add(hash);
b.put(key, block.toRaw());
return;
}
block = Buffer.allocUnsafe(data.length + 32);
data.copy(block, 0);
const raw = Buffer.allocUnsafe(data.length + 32);
data.copy(raw, 0);
const size = block.readUInt32LE(40, true);
block.writeUInt32LE(size + 1, 40, true);
hash.copy(block, data.length);
const size = raw.readUInt32LE(40, true);
raw.writeUInt32LE(size + 1, 40, true);
hash.copy(raw, data.length);
b.put(key, block);
b.put(key, raw);
};
/**
@ -504,10 +416,10 @@ TXDB.prototype.removeBlock = async function removeBlock(b, hash, height) {
return;
}
const block = data.slice(0, -32);
block.writeUInt32LE(size - 1, 40, true);
const raw = data.slice(0, -32);
raw.writeUInt32LE(size - 1, 40, true);
b.put(key, block);
b.put(key, raw);
};
/**
@ -607,8 +519,7 @@ TXDB.prototype.add = async function add(tx, block) {
TXDB.prototype.insert = async function insert(wtx, block) {
const b = this.bucket();
const state = this.state.clone();
const tx = wtx.tx;
const hash = wtx.hash;
const {tx, hash} = wtx;
const height = block ? block.height : -1;
const details = new Details(this, wtx, block);
const accounts = new Set();
@ -620,35 +531,13 @@ TXDB.prototype.insert = async function insert(wtx, block) {
// We need to potentially spend some coins here.
for (let i = 0; i < tx.inputs.length; i++) {
const input = tx.inputs[i];
const prevout = input.prevout;
const credit = await this.getCredit(prevout.hash, prevout.index);
const {hash, index} = input.prevout;
const credit = await this.getCredit(hash, index);
if (!credit) {
// Maintain an stxo list for every
// spent input (even ones we don't
// recognize). This is used for
// detecting double-spends (as best
// we can), as well as resolving
// inputs we didn't know were ours
// at the time. This built-in error
// correction is not technically
// necessary assuming no messages
// are ever missed from the mempool,
// but shit happens.
this.writeInput(b, tx, i);
if (!credit)
continue;
}
const coin = credit.coin;
// Do some verification.
if (!block) {
if (!await this.verifyInput(tx, i, coin)) {
this.clear();
return null;
}
}
const path = await this.getPath(coin);
assert(path);
@ -705,13 +594,6 @@ TXDB.prototype.insert = async function insert(wtx, block) {
details.setOutput(i, path);
accounts.add(path.account);
// Attempt to resolve an input we
// did not know was ours at the time.
if (await this.resolveInput(b, state, tx, i, height, path, own)) {
updated = true;
continue;
}
const credit = Credit.fromTX(tx, i, height);
credit.own = own;
@ -728,11 +610,8 @@ TXDB.prototype.insert = async function insert(wtx, block) {
// If this didn't update any coins,
// it's not our transaction.
if (!updated) {
// Clear the spent list inserts.
this.clear();
if (!updated)
return null;
}
// Save and index the transaction record.
b.put(layout.t(hash), wtx.toRaw());
@ -821,7 +700,7 @@ TXDB.prototype.confirm = async function confirm(hash, block) {
*/
TXDB.prototype._confirm = async function _confirm(wtx, block) {
const b = this.batch();
const b = this.bucket();
const state = this.state.clone();
const tx = wtx.tx;
const hash = wtx.hash;
@ -839,14 +718,14 @@ TXDB.prototype._confirm = async function _confirm(wtx, block) {
// from the utxo state.
for (let i = 0; i < tx.inputs.length; i++) {
const input = tx.inputs[i];
const prevout = input.prevout;
const {hash, index} = input.prevout;
let credit = credits[i];
// There may be new credits available
// that we haven't seen yet.
if (!credit) {
credit = await this.getCredit(prevout.hash, prevout.index);
credit = await this.getCredit(hash, index);
if (!credit)
continue;
@ -904,10 +783,8 @@ TXDB.prototype._confirm = async function _confirm(wtx, block) {
// Update coin height and confirmed
// balance. Save once again.
const coin = credit.coin;
coin.height = height;
state.confirmed += output.value;
credit.coin.height = height;
await this.saveCredit(b, credit, path);
}
@ -970,8 +847,7 @@ TXDB.prototype.remove = async function remove(hash) {
TXDB.prototype.erase = async function erase(wtx, block) {
const b = this.bucket();
const state = this.state.clone();
const tx = wtx.tx;
const hash = wtx.hash;
const {tx, hash} = wtx;
const height = block ? block.height : -1;
const details = new Details(this, wtx, block);
const accounts = new Set();
@ -985,13 +861,8 @@ TXDB.prototype.erase = async function erase(wtx, block) {
for (let i = 0; i < tx.inputs.length; i++) {
const credit = credits[i];
if (!credit) {
// This input never had an undo
// coin, but remove it from the
// stxo set.
this.removeInput(b, tx, i);
if (!credit)
continue;
}
const coin = credit.coin;
const path = await this.getPath(coin);
@ -1166,9 +1037,7 @@ TXDB.prototype.unconfirm = async function unconfirm(hash) {
TXDB.prototype.disconnect = async function disconnect(wtx, block) {
const b = this.bucket();
const state = this.state.clone();
const tx = wtx.tx;
const hash = wtx.hash;
const height = block.height;
const {tx, hash, height} = wtx;
const details = new Details(this, wtx, block);
const accounts = new Set();
@ -1265,7 +1134,7 @@ TXDB.prototype.disconnect = async function disconnect(wtx, block) {
this.state = state;
this.emit('unconfirmed', tx, details);
this.emit('balance', this.pending.toBalance(), details);
this.emit('balance', state.toBalance(), details);
return details;
};
@ -1352,24 +1221,6 @@ TXDB.prototype.removeConflicts = async function removeConflicts(tx, conf) {
return true;
};
/**
* Attempt to verify an input.
* @private
* @param {TX} tx
* @param {Number} index
* @param {Coin} coin
* @returns {Promise}
*/
TXDB.prototype.verifyInput = async function verifyInput(tx, index, coin) {
const flags = Script.flags.MANDATORY_VERIFY_FLAGS;
if (!this.options.verify)
return true;
return await tx.verifyInputAsync(index, coin, flags);
};
/**
* Lock all coins in a transaction.
* @param {TX} tx
@ -2173,7 +2024,7 @@ TXDB.prototype.hasSpentCoin = function hasSpentCoin(spent) {
* @returns {Promise}
*/
TXDB.prototype.updateSpentCoin = async function updateSpentCoin(tx, index, height) {
TXDB.prototype.updateSpentCoin = async function updateSpentCoin(b, tx, index, height) {
const prevout = Outpoint.fromTX(tx, index);
const spent = await this.getSpent(prevout.hash, prevout.index);
@ -2187,7 +2038,7 @@ TXDB.prototype.updateSpentCoin = async function updateSpentCoin(tx, index, heigh
coin.height = height;
this.put(layout.d(spent.hash, spent.index), coin.toRaw());
b.put(layout.d(spent.hash, spent.index), coin.toRaw());
};
/**

View File

@ -42,12 +42,6 @@ const U32 = encoding.U32;
* @alias module:wallet.WalletDB
* @constructor
* @param {Object} options
* @param {String?} options.name - Database name.
* @param {String?} options.location - Database file location.
* @param {String?} options.db - Database backend (`"leveldb"` by default).
* @param {Boolean?} options.verify - Verify transactions as they
* come in (note that this will not happen on the worker pool).
* @property {Boolean} loaded
*/
function WalletDB(options) {

View File

@ -19,6 +19,7 @@ const Input = require('../lib/primitives/input');
const Outpoint = require('../lib/primitives/outpoint');
const Script = require('../lib/script/script');
const HD = require('../lib/hd');
const U32 = encoding.U32;
const KEY1 = 'xprv9s21ZrQH143K3Aj6xQBymM31Zb4BVc7wxqfUhMZrzewdDVCt'
+ 'qUP9iWfcHgJofs25xbaUpCps9GDXj83NiWvQCAkWQhVj5J4CorfnpKX94AZ';
@ -26,15 +27,9 @@ const KEY1 = 'xprv9s21ZrQH143K3Aj6xQBymM31Zb4BVc7wxqfUhMZrzewdDVCt'
const KEY2 = 'xprv9s21ZrQH143K3mqiSThzPtWAabQ22Pjp3uSNnZ53A5bQ4udp'
+ 'faKekc2m4AChLYH1XDzANhrSdxHYWUeTWjYJwFwWFyHkTMnMeAcW4JyRCZa';
const workers = new WorkerPool({
enabled: true
});
const wdb = new WalletDB({
db: 'memory',
verify: true,
workers
});
const enabled = true;
const workers = new WorkerPool({ enabled });
const wdb = new WalletDB({ workers });
let currentWallet = null;
let importedWallet = null;
@ -42,27 +37,32 @@ let importedKey = null;
let doubleSpendWallet = null;
let doubleSpendCoin = null;
let globalTime = util.now();
let globalHeight = 1;
function prevBlock(wdb) {
assert(wdb.state.height > 0);
return fakeBlock(wdb.state.height - 1);
};
function nextBlock() {
const height = globalHeight++;
const time = globalTime++;
function curBlock(wdb) {
return fakeBlock(wdb.state.height);
};
const prevHead = encoding.U32(height - 1);
const prevHash = digest.hash256(prevHead);
function nextBlock(wdb) {
return fakeBlock(wdb.state.height + 1);
}
const head = encoding.U32(height);
const hash = digest.hash256(head);
function fakeBlock(height) {
const prev = digest.hash256(U32((height - 1) >>> 0));
const hash = digest.hash256(U32(height >>> 0));
const root = digest.hash256(U32((height | 0x80000000) >>> 0));
return {
hash: hash.toString('hex'),
height: height,
prevBlock: prevHash.toString('hex'),
time: time,
merkleRoot: encoding.NULL_HASH,
prevBlock: prev.toString('hex'),
merkleRoot: root.toString('hex'),
time: 500000000 + (height * (10 * 60)),
bits: 0,
nonce: 0,
bits: 0
height: height
};
}
@ -73,10 +73,7 @@ function dummyInput() {
async function testP2PKH(witness, nesting) {
const flags = Script.flags.STANDARD_VERIFY_FLAGS;
const wallet = await wdb.create({
witness
});
const wallet = await wdb.create({ witness });
const addr = Address.fromString(wallet.getAddress('string'));
@ -157,11 +154,9 @@ async function testP2SH(witness, nesting) {
fund.addOutput(nesting ? nestedAddr1 : addr1, 5460 * 10);
// Simulate a confirmation
const block = nextBlock();
assert.strictEqual(alice.account[receiveDepth], 1);
await wdb.addBlock(block, [fund.toTX()]);
await wdb.addBlock(nextBlock(wdb), [fund.toTX()]);
assert.strictEqual(alice.account[receiveDepth], 2);
assert.strictEqual(alice.account.changeDepth, 1);
@ -205,9 +200,7 @@ async function testP2SH(witness, nesting) {
// Simulate a confirmation
{
const block = nextBlock();
await wdb.addBlock(block, [tx]);
await wdb.addBlock(nextBlock(wdb), [tx]);
assert.strictEqual(alice.account[receiveDepth], 2);
assert.strictEqual(alice.account.changeDepth, 2);
@ -360,53 +353,39 @@ describe('Wallet', function() {
// balance: 11000
await alice.sign(f1);
const fake = new MTX();
fake.addTX(t1, 1); // 1000 (already redeemed)
fake.addOutput(alice.getAddress(), 500);
// Script inputs but do not sign
await alice.template(fake);
// Fake signature
const input = fake.inputs[0];
input.script.setData(0, encoding.ZERO_SIG);
input.script.compile();
// balance: 11000
// Fake TX should temporarily change output.
{
await wdb.addTX(fake.toTX());
await wdb.addTX(t4.toTX());
const balance = await alice.getBalance();
assert.strictEqual(balance.unconfirmed, 22500);
}
{
await wdb.addTX(t1.toTX());
const balance = await alice.getBalance();
assert.strictEqual(balance.unconfirmed, 72500);
}
{
await wdb.addTX(t2.toTX());
const balance = await alice.getBalance();
assert.strictEqual(balance.unconfirmed, 46500);
}
{
await wdb.addTX(t3.toTX());
const balance = await alice.getBalance();
assert.strictEqual(balance.unconfirmed, 22000);
}
{
await wdb.addTX(t1.toTX());
const balance = await alice.getBalance();
assert.strictEqual(balance.unconfirmed, 73000);
}
{
await wdb.addTX(t2.toTX());
const balance = await alice.getBalance();
assert.strictEqual(balance.unconfirmed, 71000);
}
{
await wdb.addTX(t3.toTX());
const balance = await alice.getBalance();
assert.strictEqual(balance.unconfirmed, 69000);
}
{
await wdb.addTX(f1.toTX());
const balance = await alice.getBalance();
assert.strictEqual(balance.unconfirmed, 11000);
assert.strictEqual(balance.unconfirmed, 58000);
const txs = await alice.getHistory();
assert(txs.some((wtx) => {
@ -423,11 +402,46 @@ describe('Wallet', function() {
return wtx.tx.hash('hex') === f1.hash('hex');
}));
}
// Should recover from missed txs on block.
await wdb.addBlock(nextBlock(wdb), [
t1.toTX(),
t2.toTX(),
t3.toTX(),
t4.toTX(),
f1.toTX()
]);
{
const balance = await alice.getBalance();
assert.strictEqual(balance.unconfirmed, 11000);
assert.strictEqual(balance.confirmed, 11000);
const txs = await alice.getHistory();
assert(txs.some((wtx) => {
return wtx.hash === f1.hash('hex');
}));
}
{
const balance = await bob.getBalance();
assert.strictEqual(balance.unconfirmed, 10000);
assert.strictEqual(balance.confirmed, 10000);
const txs = await bob.getHistory();
assert(txs.some((wtx) => {
return wtx.tx.hash('hex') === f1.hash('hex');
}));
}
});
it('should cleanup spenders after double-spend', async () => {
const wallet = doubleSpendWallet;
// Reorg and unconfirm all previous txs.
await wdb.removeBlock(curBlock(wdb));
{
const txs = await wallet.getHistory();
assert.strictEqual(txs.length, 5);
@ -441,6 +455,7 @@ describe('Wallet', function() {
{
const balance = await wallet.getBalance();
assert.strictEqual(balance.unconfirmed, 11000);
assert.strictEqual(balance.confirmed, 0);
}
{
@ -468,14 +483,6 @@ describe('Wallet', function() {
});
it('should handle missed txs without resolution', async () => {
const wdb = new WalletDB({
name: 'wallet-test',
db: 'memory',
verify: false
});
await wdb.open();
const alice = await wdb.create();
const bob = await wdb.create();
@ -534,20 +541,20 @@ describe('Wallet', function() {
{
await wdb.addTX(t2.toTX());
const balance = await alice.getBalance();
assert.strictEqual(balance.unconfirmed, 47000);
assert.strictEqual(balance.unconfirmed, 71000);
}
{
await wdb.addTX(t3.toTX());
const balance = await alice.getBalance();
assert.strictEqual(balance.unconfirmed, 22000);
assert.strictEqual(balance.unconfirmed, 69000);
}
{
await wdb.addTX(f1.toTX());
const balance = await alice.getBalance();
assert.strictEqual(balance.unconfirmed, 11000);
assert.strictEqual(balance.unconfirmed, 58000);
const txs = await alice.getHistory();
assert(txs.some((wtx) => {
@ -565,10 +572,14 @@ describe('Wallet', function() {
}));
}
await wdb.addTX(t2.toTX());
await wdb.addTX(t3.toTX());
await wdb.addTX(t4.toTX());
await wdb.addTX(f1.toTX());
// Should recover from missed txs on block.
await wdb.addBlock(nextBlock(wdb), [
t1.toTX(),
t2.toTX(),
t3.toTX(),
t4.toTX(),
f1.toTX()
]);
{
const balance = await alice.getBalance();
@ -1067,9 +1078,7 @@ describe('Wallet', function() {
t2.addOutput(alice.getAddress(), 5460);
t2.addOutput(alice.getAddress(), 5460);
const block = nextBlock();
await wdb.addBlock(block, [t2.toTX()]);
await wdb.addBlock(nextBlock(wdb), [t2.toTX()]);
{
const coins = await alice.getSmartCoins();
@ -1077,7 +1086,7 @@ describe('Wallet', function() {
for (let i = 0; i < coins.length; i++) {
const coin = coins[i];
assert.strictEqual(coin.height, block.height);
assert.strictEqual(coin.height, wdb.state.height);
}
}
@ -1104,7 +1113,7 @@ describe('Wallet', function() {
assert(coin.value < 5460);
found = true;
} else {
assert.strictEqual(coin.height, block.height);
assert.strictEqual(coin.height, wdb.state.height);
}
total += coin.value;
}
@ -1136,7 +1145,7 @@ describe('Wallet', function() {
assert(coin.value < 5460);
found = true;
} else {
assert.strictEqual(coin.height, block.height);
assert.strictEqual(coin.height, wdb.state.height);
}
}
@ -1337,12 +1346,7 @@ describe('Wallet', function() {
});
it('should recover from a missed tx', async () => {
const wdb = new WalletDB({
name: 'wallet-test',
db: 'memory',
verify: false
});
const wdb = new WalletDB({ workers });
await wdb.open();
const alice = await wdb.create({
@ -1393,21 +1397,16 @@ describe('Wallet', function() {
assert.strictEqual((await alice.getBalance()).unconfirmed, 30000);
// Bob sees t2 on the chain.
await bob.add(t2.toTX());
await bob.add(t2.toTX(), nextBlock(wdb));
// Bob sees t3 on the chain.
await bob.add(t3.toTX());
await bob.add(t3.toTX(), nextBlock(wdb));
assert.strictEqual((await bob.getBalance()).unconfirmed, 30000);
});
it('should recover from a missed tx and double spend', async () => {
const wdb = new WalletDB({
name: 'wallet-test',
db: 'memory',
verify: false
});
const wdb = new WalletDB({ workers });
await wdb.open();
const alice = await wdb.create({
@ -1468,10 +1467,10 @@ describe('Wallet', function() {
assert.strictEqual((await alice.getBalance()).unconfirmed, 30000);
// Bob sees t2a on the chain.
await bob.add(t2a.toTX());
await bob.add(t2a.toTX(), nextBlock(wdb));
// Bob sees t3 on the chain.
await bob.add(t3.toTX());
await bob.add(t3.toTX(), nextBlock(wdb));
assert.strictEqual((await bob.getBalance()).unconfirmed, 30000);
});