From d952bcb934e63ebe132d05b433266cc73d499b32 Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Tue, 6 Aug 2019 16:33:24 -0700 Subject: [PATCH] wallet: check account ownership of name before making update TX Uses new method txdb.hasCoinByAccount() to verify that not only is a name-owning coin owned by the wallet, but also by the specified account, when making an UPDATE TX. --- lib/wallet/wallet.js | 13 +- test/wallet-accounts-auction-test.js | 343 +++++++++++++++++++++++++++ test/wallet-accounts-reveal-test.js | 196 --------------- 3 files changed, 354 insertions(+), 198 deletions(-) create mode 100644 test/wallet-accounts-auction-test.js delete mode 100644 test/wallet-accounts-reveal-test.js diff --git a/lib/wallet/wallet.js b/lib/wallet/wallet.js index 6c0c57537..911d61a94 100644 --- a/lib/wallet/wallet.js +++ b/lib/wallet/wallet.js @@ -2329,13 +2329,18 @@ class Wallet extends EventEmitter { * @returns {MTX} */ - async makeUpdate(name, resource) { + async makeUpdate(name, resource, acct) { assert(typeof name === 'string'); assert(resource instanceof Resource); if (!rules.verifyName(name)) throw new Error('Invalid name.'); + if (acct != null) { + assert((acct >>> 0) === acct || typeof acct === 'string'); + acct = await this.getAccountIndex(acct); + } + const rawName = Buffer.from(name, 'ascii'); const nameHash = rules.hashName(rawName); const ns = await this.getNameState(nameHash); @@ -2351,6 +2356,9 @@ class Wallet extends EventEmitter { if (!coin) throw new Error(`Wallet does not own: "${name}".`); + if (acct != null && !await this.txdb.hasCoinByAccount(acct, hash, index)) + throw new Error(`Account does not own: "${name}".`); + if (coin.covenant.isReveal() || coin.covenant.isClaim()) return this._makeRegister(name, resource); @@ -2403,7 +2411,8 @@ class Wallet extends EventEmitter { */ async _createUpdate(name, resource, options) { - const mtx = await this.makeUpdate(name, resource); + const acct = options ? options.account : null; + const mtx = await this.makeUpdate(name, resource, acct); await this.fill(mtx, options); return this.finalize(mtx, options); } diff --git a/test/wallet-accounts-auction-test.js b/test/wallet-accounts-auction-test.js new file mode 100644 index 000000000..d2f1f5967 --- /dev/null +++ b/test/wallet-accounts-auction-test.js @@ -0,0 +1,343 @@ +/* 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 Resource = require('../lib/dns/resource'); +const {WalletClient} = require('hs-client'); + +const network = Network.get('regtest'); + +const node = new FullNode({ + memory: true, + network: 'regtest', + plugins: [require('../lib/wallet/plugin')] +}); + +// Prevent mempool from sending duplicate TXs back to the walletDB and txdb. +// This will prevent a race condition when we need to remove spent (but +// unconfirmed) outputs from the wallet so they can be reused in other tests. +node.mempool.emit = () => {}; + +const wclient = new WalletClient({ + port: network.walletPort +}); + +const {wdb} = node.require('walletdb'); + +const name = rules.grindName(5, 1, network); +let wallet, alice, bob, aliceReceive, bobReceive; + +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('Multiple accounts participating in same auction', function() { + before(async () => { + await node.open(); + await wclient.open(); + + wallet = await wdb.create(); + + // We'll use an account number for alice and a string for bob + // to ensure that both types work as options. + alice = await wallet.getAccount(0); + bob = await wallet.createAccount({name: 'bob'}); + + aliceReceive = await alice.receiveAddress(); + bobReceive = await bob.receiveAddress(); + }); + + after(async () => { + await wclient.close(); + await node.close(); + }); + + it('should fund both accounts', async () => { + await mineBlocks(2, aliceReceive); + await mineBlocks(2, bobReceive); + + // 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(0); + const bobBal = await wallet.getBalance('bob'); + assert(aliceBal.confirmed === 2000 * 2 * 1e6); + assert(bobBal.confirmed === 2000 * 2 * 1e6); + }); + + it('should open an auction and proceed to REVEAL phase', async () => { + await wallet.sendOpen(name, false, {account: 0}); + 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: 0}); + 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); + + assert.strictEqual(node.mempool.map.size, 0); + }); + + describe('REVEAL', function() { + describe('Library methods', function() { + it('one tx per account', async () => { + const tx1 = await wallet.sendReveal(name, {account: 0}); + 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()); + + assert.strictEqual(node.mempool.map.size, 2); + await node.mempool.reset(); + assert.strictEqual(node.mempool.map.size, 0); + }); + + it('all accounts in one tx', async () => { + const tx = await wallet.sendRevealAll(); + assert(tx); + + // Reset for next test + await wallet.abandon(tx.hash()); + + assert.strictEqual(node.mempool.map.size, 1); + await node.mempool.reset(); + assert.strictEqual(node.mempool.map.size, 0); + }); + }); + + describe('HTTP API', function () { + it('one tx per account', async () => { + const tx1 = await wclient.post(`/wallet/${wallet.id}/reveal`, { + name: name, + account: 'default' + }); + assert(tx1); + + const tx2 = await wclient.post(`/wallet/${wallet.id}/reveal`, { + name: name, + account: 'bob' + }); + assert(tx2); + + // Reset for next test + await wallet.abandon(Buffer.from(tx1.hash, 'hex')); + await wallet.abandon(Buffer.from(tx2.hash, 'hex')); + + assert.strictEqual(node.mempool.map.size, 2); + await node.mempool.reset(); + assert.strictEqual(node.mempool.map.size, 0); + }); + + it('all accounts in one tx', async () => { + const tx = await wclient.post(`/wallet/${wallet.id}/reveal`, { + name: name + }); + assert(tx); + + // Reset for next test + await wallet.abandon(Buffer.from(tx.hash, 'hex')); + + assert.strictEqual(node.mempool.map.size, 1); + await node.mempool.reset(); + assert.strictEqual(node.mempool.map.size, 0); + }); + }); + + describe('RPC API', function() { + it('one tx per account', async () => { + await wclient.execute('selectwallet', [wallet.id]); + + const tx1 = await wclient.execute('sendreveal', [name, 'default']); + assert(tx1); + + const tx2 = await wclient.execute('sendreveal', [name, 'bob']); + assert(tx2); + + // Reset for next test + await wallet.abandon(Buffer.from(tx1.hash, 'hex')); + await wallet.abandon(Buffer.from(tx2.hash, 'hex')); + + assert.strictEqual(node.mempool.map.size, 2); + await node.mempool.reset(); + assert.strictEqual(node.mempool.map.size, 0); + }); + + it('all accounts in one tx', async () => { + const tx = await wclient.execute('sendreveal', [name]); + assert(tx); + + // Do not reset for next test, time to move on to REGISTER + }); + }); + }); + + describe('UPDATE', function() { + const aliceResource = Resource.fromJSON({text: ['ALICE']}); + const bobResource = Resource.fromJSON({text: ['BOB']}); + + it('should advance auction to REGISTER phase', async () => { + await mineBlocks(network.names.revealPeriod); + const ns = await node.chain.db.getNameStateByName(name); + assert(ns.isClosed(node.chain.height, network)); + + await wdb.rescan(0); + + // Alice is the winner + const {hash, index} = ns.owner; + assert(await wallet.txdb.hasCoinByAccount(0, hash, index)); + + // ...not Bob (sanity check) + assert(!await wallet.txdb.hasCoinByAccount(1, hash, index)); + }); + + describe('Library methods', function() { + it('reject from wrongly specified account', async () => { + await assert.rejects(async () => { + await wallet.sendUpdate(name, bobResource, {account: 'bob'}); + }, { + name: 'Error', + message: `Account does not own: "${name}".` + }); + }); + + it('send from correctly specified account', async () => { + const tx = await wallet.sendUpdate(name, aliceResource, {account: 0}); + assert(tx); + + await wallet.abandon(tx.hash()); + + assert.strictEqual(node.mempool.map.size, 1); + await node.mempool.reset(); + assert.strictEqual(node.mempool.map.size, 0); + }); + + it('send from correct account automatically', async () => { + const tx = await wallet.sendUpdate(name, aliceResource); + assert(tx); + + await wallet.abandon(tx.hash()); + + assert.strictEqual(node.mempool.map.size, 1); + await node.mempool.reset(); + assert.strictEqual(node.mempool.map.size, 0); + }); + }); + + describe('HTTP API', function () { + it('reject from wrongly specified account', async () => { + await assert.rejects(async () => { + await wclient.post(`wallet/${wallet.id}/update`, { + name: name, + data: bobResource, + account: 'bob' + }); + }, { + name: 'Error', + message: `Account does not own: "${name}".` + }); + }); + + it('send from correctly specified account', async () => { + const tx = await wclient.post(`wallet/${wallet.id}/update`, { + name: name, + data: aliceResource, + account: 'default' + }); + assert(tx); + + await wallet.abandon(Buffer.from(tx.hash, 'hex')); + + assert.strictEqual(node.mempool.map.size, 1); + await node.mempool.reset(); + assert.strictEqual(node.mempool.map.size, 0); + }); + + it('send from correct account automatically', async () => { + const tx = await wclient.post(`wallet/${wallet.id}/update`, { + name: name, + data: aliceResource + }); + assert(tx); + + await wallet.abandon(Buffer.from(tx.hash, 'hex')); + + assert.strictEqual(node.mempool.map.size, 1); + await node.mempool.reset(); + assert.strictEqual(node.mempool.map.size, 0); + }); + }); + + describe('RPC API', function() { + it('reject from wrongly specified account', async () => { + await wclient.execute('selectwallet', [wallet.id]); + + await assert.rejects(async () => { + await wclient.execute('sendupdate', [ + name, + bobResource, + 'bob' + ]); + }, { + name: 'Error', + message: `Account does not own: "${name}".` + }); + }); + + it('send from correctly specified account', async () => { + const tx = await wclient.execute('sendupdate', [ + name, + aliceResource, + 'default' + ]); + assert(tx); + + await wallet.abandon(Buffer.from(tx.hash, 'hex')); + + assert.strictEqual(node.mempool.map.size, 1); + await node.mempool.reset(); + assert.strictEqual(node.mempool.map.size, 0); + }); + + it('send from correct account automatically', async () => { + const tx = await wclient.execute('sendupdate', [ + name, + aliceResource + ]); + assert(tx); + + await wallet.abandon(Buffer.from(tx.hash, 'hex')); + + assert.strictEqual(node.mempool.map.size, 1); + await node.mempool.reset(); + assert.strictEqual(node.mempool.map.size, 0); + }); + }); + }); +}); diff --git a/test/wallet-accounts-reveal-test.js b/test/wallet-accounts-reveal-test.js deleted file mode 100644 index 7adbc3611..000000000 --- a/test/wallet-accounts-reveal-test.js +++ /dev/null @@ -1,196 +0,0 @@ -/* 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 {WalletClient} = require('hs-client'); - -const network = Network.get('regtest'); - -const node = new FullNode({ - memory: true, - network: 'regtest', - plugins: [require('../lib/wallet/plugin')] -}); - -// Prevent mempool from sending duplicate TXs back to the walletDB and txdb. -// This will prevent a race condition when we need to remove spent (but -// unconfirmed) outputs from the wallet so they can be reused in other tests. -node.mempool.emit = () => {}; - -const wclient = new WalletClient({ - port: network.walletPort -}); - -const {wdb} = node.require('walletdb'); - -const name = rules.grindName(5, 1, network); -let wallet, alice, bob, aliceReceive, bobReceive; - -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(); - await wclient.open(); - - wallet = await wdb.create(); - - // We'll use an account number for alice and a string for bob - // to ensure that both types work as options. - alice = await wallet.getAccount(0); - bob = await wallet.createAccount({name: 'bob'}); - - aliceReceive = await alice.receiveAddress(); - bobReceive = await bob.receiveAddress(); - }); - - after(async () => { - await wclient.close(); - await node.close(); - }); - - it('should fund both accounts', async () => { - await mineBlocks(2, aliceReceive); - await mineBlocks(2, bobReceive); - - // 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(0); - const bobBal = await wallet.getBalance('bob'); - assert(aliceBal.confirmed === 2000 * 2 * 1e6); - assert(bobBal.confirmed === 2000 * 2 * 1e6); - }); - - it('should open an auction and proceed to REVEAL phase', async () => { - await wallet.sendOpen(name, false, {account: 0}); - 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: 0}); - 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); - - assert.strictEqual(node.mempool.map.size, 0); - }); - - it('should send REVEAL from one account at a time -- LIBRARY', async () => { - const tx1 = await wallet.sendReveal(name, {account: 0}); - 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()); - - assert.strictEqual(node.mempool.map.size, 2); - await node.mempool.reset(); - assert.strictEqual(node.mempool.map.size, 0); - }); - - it('should send REVEAL from all accounts -- LIBRARY', async () => { - const tx = await wallet.sendRevealAll(); - assert(tx); - - // Reset for next test - await wallet.abandon(tx.hash()); - - assert.strictEqual(node.mempool.map.size, 1); - await node.mempool.reset(); - assert.strictEqual(node.mempool.map.size, 0); - }); - - it('should send REVEAL from one account at a time -- HTTP', async () => { - const tx1 = await wclient.post(`/wallet/${wallet.id}/reveal`, { - name: name, - account: 'default' - }); - assert(tx1); - - const tx2 = await wclient.post(`/wallet/${wallet.id}/reveal`, { - name: name, - account: 'bob' - }); - assert(tx2); - - // Reset for next test - await wallet.abandon(Buffer.from(tx1.hash, 'hex')); - await wallet.abandon(Buffer.from(tx2.hash, 'hex')); - - assert.strictEqual(node.mempool.map.size, 2); - await node.mempool.reset(); - assert.strictEqual(node.mempool.map.size, 0); - }); - - it('should send REVEAL from all accounts -- HTTP', async () => { - const tx = await wclient.post(`/wallet/${wallet.id}/reveal`, { - name: name - }); - assert(tx); - - // Reset for next test - await wallet.abandon(Buffer.from(tx.hash, 'hex')); - - assert.strictEqual(node.mempool.map.size, 1); - await node.mempool.reset(); - assert.strictEqual(node.mempool.map.size, 0); - }); - - it('should send REVEAL from one account at a time -- RPC', async () => { - await wclient.execute('selectwallet', [wallet.id]); - - const tx1 = await wclient.execute('sendreveal', [name, 'default']); - assert(tx1); - - const tx2 = await wclient.execute('sendreveal', [name, 'bob']); - assert(tx2); - - // Reset for next test - await wallet.abandon(Buffer.from(tx1.hash, 'hex')); - await wallet.abandon(Buffer.from(tx2.hash, 'hex')); - - assert.strictEqual(node.mempool.map.size, 2); - await node.mempool.reset(); - assert.strictEqual(node.mempool.map.size, 0); - }); - - it('should send REVEAL from all accounts -- RPC', async () => { - const tx = await wclient.execute('sendreveal', [name]); - assert(tx); - - // Reset for next test - await wallet.abandon(Buffer.from(tx.hash, 'hex')); - - assert.strictEqual(node.mempool.map.size, 1); - await node.mempool.reset(); - assert.strictEqual(node.mempool.map.size, 0); - }); -});