From 483697a268cac3d4b631a5c1c74a6d2f5c0ef59e Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Thu, 1 Aug 2019 10:47:34 -0700 Subject: [PATCH] wallet: check account ownership of bid TX when making reveal TX Adds new method hasCoinByAccount(acct, hash, index) to txdb which returns true if the coin (hash, index) is owned by account (number). Adds this check to wallet.makeReveal() to prevent BIDs from other wallet accounts being included as inputs to a new REVEAL TX from a specific account. --- lib/wallet/txdb.js | 14 +++++ lib/wallet/wallet.js | 13 ++++- test/double-reveal-test.js | 103 +++++++++++++++++++++++++++++++++++++ 3 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 test/double-reveal-test.js diff --git a/lib/wallet/txdb.js b/lib/wallet/txdb.js index 3c45aacc7..2c783a909 100644 --- a/lib/wallet/txdb.js +++ b/lib/wallet/txdb.js @@ -2240,6 +2240,20 @@ class TXDB { }); } + /** + * Test whether an account owns a coin. + * @param {Number} acct + * @param {Hash} hash + * @param {Index} number + * @returns {Promise} - Returns Boolean. + */ + + hasCoinByAccount(acct, hash, index) { + assert(typeof acct === 'number'); + + return this.bucket.has(layout.C.encode(acct, hash, index)); + } + /** * Get hashes of all transactions in the database. * @param {Number} acct diff --git a/lib/wallet/wallet.js b/lib/wallet/wallet.js index 1a780c896..6c0c57537 100644 --- a/lib/wallet/wallet.js +++ b/lib/wallet/wallet.js @@ -1742,9 +1742,14 @@ class Wallet extends EventEmitter { * @returns {MTX} */ - async makeReveal(name) { + async makeReveal(name, acct) { assert(typeof name === 'string'); + if (acct != null) { + assert((acct >>> 0) === acct || typeof acct === 'string'); + acct = await this.getAccountIndex(acct); + } + if (!rules.verifyName(name)) throw new Error('Invalid name.'); @@ -1781,6 +1786,9 @@ class Wallet extends EventEmitter { if (!coin) continue; + if (acct != null && !await this.txdb.hasCoinByAccount(acct, hash, index)) + continue; + // Is local? if (coin.height < ns.height) continue; @@ -1820,7 +1828,8 @@ class Wallet extends EventEmitter { */ async _createReveal(name, options) { - const mtx = await this.makeReveal(name); + const acct = options ? options.account : null; + const mtx = await this.makeReveal(name, acct); await this.fill(mtx, options); return this.finalize(mtx, options); } diff --git a/test/double-reveal-test.js b/test/double-reveal-test.js new file mode 100644 index 000000000..466d130d2 --- /dev/null +++ b/test/double-reveal-test.js @@ -0,0 +1,103 @@ +/* eslint-env mocha */ +/* eslint prefer-arrow-callback: "off" */ + +'use strict'; + +const assert = require('bsert'); +const Network = require('../lib/protocol/network'); +const FullNode = require('../lib/node/fullnode'); +const Address = require('../lib/primitives/address'); +const rules = require('../lib/covenants/rules'); + +const network = Network.get('regtest'); + +const node = new FullNode({ + memory: true, + network: 'regtest', + plugins: [require('../lib/wallet/plugin')] +}); + +const {wdb} = node.require('walletdb'); +const name = rules.grindName(10, 20, network); + +let wallet, alice, bob, aliceMiner, bobMiner; + +async function mineBlocks(n, addr) { + addr = addr ? addr : new Address().toString('regtest'); + for (let i = 0; i < n; i++) { + const block = await node.miner.mineBlock(null, addr); + await node.chain.add(block); + } +} + +describe('One wallet, two accounts, one name', function() { + before(async () => { + await node.open(); + + wallet = await wdb.create(); + + alice = await wallet.createAccount({name: 'alice'}); + bob = await wallet.createAccount({name: 'bob'}); + + aliceMiner = await alice.receiveAddress(); + bobMiner = await bob.receiveAddress(); + }); + + after(async () => { + await node.close(); + }); + + it('should fund both accounts', async () => { + await mineBlocks(10, aliceMiner); + await mineBlocks(10, bobMiner); + + // Wallet rescan is an effective way to ensure that + // wallet and chain are synced before proceeding. + await wdb.rescan(0); + + const aliceBal = await wallet.getBalance('alice'); + const bobBal = await wallet.getBalance('bob'); + assert(aliceBal.confirmed === 2000 * 10 * 1e6); + assert(bobBal.confirmed === 2000 * 10 * 1e6); + }); + + it('should open an auction and proceed to REVEAL phase', async () => { + await wallet.sendOpen(name, false, {account: 'alice'}); + await mineBlocks(network.names.treeInterval + 2); + let ns = await node.chain.db.getNameStateByName(name); + assert(ns.isBidding(node.chain.height, network)); + + await wdb.rescan(0); + + await wallet.sendBid(name, 100000, 200000, {account: 'alice'}); + await wallet.sendBid(name, 50000, 200000, {account: 'bob'}); + await mineBlocks(network.names.biddingPeriod); + ns = await node.chain.db.getNameStateByName(name); + assert(ns.isReveal(node.chain.height, network)); + + await wdb.rescan(0); + + const walletBids = await wallet.getBidsByName(name); + assert.strictEqual(walletBids.length, 2); + + for (const bid of walletBids) + assert(bid.own); + }); + + it('should send REVEAL from one account at a time', async () => { + const tx1 = await wallet.sendReveal(name, {account: 'alice'}); + assert(tx1); + + const tx2 = await wallet.sendReveal(name, {account: 'bob'}); + assert(tx2); + + // Reset for next test + await wallet.abandon(tx1.hash()); + await wallet.abandon(tx2.hash()); + }); + + it('should send REVEAL from all accounts', async () => { + const tx = await wallet.sendRevealAll(); + assert(tx); + }); +});