Skip to content

Commit

Permalink
wallet: check account ownership of name before making update TX
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
pinheadmz committed Mar 17, 2020
1 parent 420842b commit 5c09c70
Show file tree
Hide file tree
Showing 3 changed files with 366 additions and 198 deletions.
13 changes: 11 additions & 2 deletions lib/wallet/wallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -2337,13 +2337,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);
Expand All @@ -2359,6 +2364,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);

Expand Down Expand Up @@ -2411,7 +2419,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);
}
Expand Down
355 changes: 355 additions & 0 deletions test/wallet-accounts-auction-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,355 @@
/* 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.Resource.fromJSON({
records: [
{
type: 'TXT',
txt: ['ALICE']
}
]});
const bobResource = Resource.Resource.fromJSON({
records: [
{
type: 'TXT',
txt: ['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);
});
});
});
});
Loading

0 comments on commit 5c09c70

Please sign in to comment.