diff --git a/CHANGELOG.md b/CHANGELOG.md index 2196b1d31..30e6aadeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,9 @@ to these calls. - New RPC methods: - `createbatch` and `sendbatch` create batch transactions with any number of outputs with any combination of covenants. +- Updates related to nonces and blinds + - RPC method `importnonce` now returns an array of blinds instead of a single blind. + - HTTP endpoint `/wallet/:id/nonce/:name`'s response replaces 2 string fields (`nonce`, `blind`) with arrays of the same type (`nonces`, `blinds`) ## v4.0.0 diff --git a/lib/wallet/http.js b/lib/wallet/http.js index 367e8cad3..d73bc807f 100644 --- a/lib/wallet/http.js +++ b/lib/wallet/http.js @@ -1030,13 +1030,12 @@ class HTTP extends Server { const nameHash = rules.hashName(name); const nonces = await req.wallet.generateNonces(nameHash, address, bid); - const nonce = nonces[0]; - const blind = rules.blind(bid, nonce); + const blinds = nonces.map(nonce => rules.blind(bid, nonce)); return res.json(200, { address: address.toString(this.network), - blind: blind.toString('hex'), - nonce: nonce.toString('hex'), + blinds: blinds.map(blind => blind.toString('hex')), + nonces: nonces.map(nonce => nonce.toString('hex')), bid: bid, name: name, nameHash: nameHash.toString('hex') diff --git a/lib/wallet/rpc.js b/lib/wallet/rpc.js index 45e979a04..146d6d069 100644 --- a/lib/wallet/rpc.js +++ b/lib/wallet/rpc.js @@ -2669,7 +2669,7 @@ class RPC extends RPCBase { // Return only first blind (based on own key) // to stay backward-compatible - return blinds[0].toString('hex'); + return blinds.map(blind => blind.toString('hex')); } } diff --git a/lib/wallet/wallet.js b/lib/wallet/wallet.js index 35e96d3d5..e3f3ea60b 100644 --- a/lib/wallet/wallet.js +++ b/lib/wallet/wallet.js @@ -1217,6 +1217,9 @@ class Wallet extends EventEmitter { for (const accountKey of [account.accountKey, ...account.keys]) publicKeys.push(accountKey.derive(index).publicKey); + // Use smallest public key + publicKeys.sort(Buffer.compare); + return publicKeys; } @@ -1232,10 +1235,6 @@ class Wallet extends EventEmitter { async generateNonce(nameHash, address, value) { const publicKeys = await this._getNoncePublicKeys(address, value); - - // Use smallest public key - publicKeys.sort(Buffer.compare); - return blake2b.multi(address.hash, publicKeys[0], nameHash); } @@ -1252,7 +1251,6 @@ class Wallet extends EventEmitter { const publicKeys = await this._getNoncePublicKeys(address, value); // Generate nonces for all public keys - // Nonce based on own public key is always first const nonces = []; for (const publicKey of publicKeys) nonces.push(blake2b.multi(address.hash, publicKey, nameHash)); diff --git a/test/auction-rpc-test.js b/test/auction-rpc-test.js index fe60f6348..e152cf343 100644 --- a/test/auction-rpc-test.js +++ b/test/auction-rpc-test.js @@ -336,11 +336,11 @@ describe('Auction RPCs', function() { // that same value. Note that "loser" MUST remember their original // bid value. If this were a wallet recovery scenario, that value // would have to be entered by the user without data from the blockchain. - const importedBlind = await util.wrpc( + const importedBlinds = await util.wrpc( 'importnonce', [bidName, bidAddress, loserBid.bid] ); - assert.strictEqual(importedBlind, bidBlind); + assert.strictEqual(importedBlinds[0], bidBlind); }); it('should create REVEAL with signing paths', async () => { diff --git a/test/wallet-http-test.js b/test/wallet-http-test.js index 1b571d313..2ce910826 100644 --- a/test/wallet-http-test.js +++ b/test/wallet-http-test.js @@ -469,13 +469,13 @@ describe('Wallet HTTP', function() { const nameHash = rules.hashName(name); const primary = node.plugins.walletdb.wdb.primary; - const nonce = await primary.generateNonce(nameHash, address, bid); - const blind = rules.blind(bid, nonce); + const nonces = await primary.generateNonces(nameHash, address, bid); + const blinds = nonces.map(nonce => rules.blind(bid, nonce)); assert.deepStrictEqual(response, { address: address.toString(network.type), - blind: blind.toString('hex'), - nonce: nonce.toString('hex'), + blinds: blinds.map(blind => blind.toString('hex')), + nonces: nonces.map(nonce => nonce.toString('hex')), bid: bid, name: name, nameHash: nameHash.toString('hex') @@ -494,13 +494,13 @@ describe('Wallet HTTP', function() { const nameHash = rules.hashName(name); const primary = node.plugins.walletdb.wdb.primary; - const nonce = await primary.generateNonce(nameHash, address, bid); - const blind = rules.blind(bid, nonce); + const nonces = await primary.generateNonces(nameHash, address, bid); + const blinds = nonces.map(nonce => rules.blind(bid, nonce)); assert.deepStrictEqual(response, { address: address.toString(network.type), - blind: blind.toString('hex'), - nonce: nonce.toString('hex'), + blinds: blinds.map(blind => blind.toString('hex')), + nonces: nonces.map(nonce => nonce.toString('hex')), bid: bid, name: name, nameHash: nameHash.toString('hex') diff --git a/test/wallet-rpc-test.js b/test/wallet-rpc-test.js index 896a83952..2e6aec5f1 100644 --- a/test/wallet-rpc-test.js +++ b/test/wallet-rpc-test.js @@ -952,4 +952,119 @@ describe('Wallet RPC Methods', function() { await nclient.execute('generatetoaddress', [1, addr]); }); }); + + describe('Multisig Auction RPC', function() { + // wallet clients + let alice, bob; + + // auction + let name; + const bidValue = 5, blindValue = 5; + + before(async () => { + await wclient.createWallet('msAlice', { + type: 'multisig', + m: 2, + n: 2 + }); + await wclient.createWallet('msBob', { + type: 'multisig', + m: 2, + n: 2 + }); + + alice = wclient.wallet('msAlice'); + bob = wclient.wallet('msBob'); + + // Initialize both multisig wallets + const accountKeys = { + alice: (await alice.getAccount('default')).accountKey, + bob: (await bob.getAccount('default')).accountKey + }; + await alice.addSharedKey('default', accountKeys.bob); + await bob.addSharedKey('default', accountKeys.alice); + + // Fund Alice's wallet + await wclient.execute('selectwallet', ['msAlice']); + const addrAlice = await wclient.execute('getnewaddress', []); + await nclient.execute('generatetoaddress', [50, addrAlice]); + + // Fund Bob's wallet + await wclient.execute('selectwallet', ['msBob']); + const addrBob = await wclient.execute('getnewaddress', []); + await nclient.execute('generatetoaddress', [50, addrBob]); + }); + + it('(alice) should open name for auction', async () => { + await wclient.execute('selectwallet', ['msAlice']); + + // Create, sign, send OPEN + name = await nclient.execute('grindname', [5]); + const tx = await wclient.execute('createopen', [name]); + const txSigned = await signMultisigTx(tx, [alice, bob]); + await nclient.execute('sendrawtransaction', [txSigned.hex]); + + // confirm and advance to bidding phase + const addrAlice = await wclient.execute('getnewaddress', []); + await nclient.execute('generatetoaddress', [treeInterval + 1, addrAlice]); + }); + + it('(alice) should bid on name with blind', async () => { + await wclient.execute('selectwallet', ['msAlice']); + + // Create, sign, send BID + const tx = await wclient.execute('createbid', [name, bidValue, bidValue+blindValue]); + const txSigned = await signMultisigTx(tx, [alice, bob]); + await nclient.execute('sendrawtransaction', [txSigned.hex]); + + // confirm and advance to reveal phase + const addrAlice = await wclient.execute('getnewaddress', []); + await nclient.execute('generatetoaddress', [biddingPeriod + 1, addrAlice]); + }); + + it('(bob) should not be able to reveal bid', async () => { + // Alice can create reveal + await wclient.execute('selectwallet', ['msAlice']); + assert.doesNotReject(wclient.execute('createreveal', [name])); + + // Bob cannot. + await wclient.execute('selectwallet', ['msBob']); + assert.rejects(wclient.execute('createreveal', [name])); + }); + + it('(bob) should import nonce', async () => { + await wclient.execute('selectwallet', ['msBob']); + const bidsBob = await wclient.execute('getbids', [name, true, true]); + const address = bidsBob[0].address; + const blinds = await wclient.execute('importnonce', [name, address, 5]); + assert.strictEqual(blinds[0], bidsBob[0].blind); + }); + + it('(bob) should reveal bid', async () => { + await wclient.execute('selectwallet', ['msBob']); + + // Create, sign, send REVEAL + const tx = await wclient.execute('createreveal', [name]); + const txSigned = await signMultisigTx(tx, [alice, bob]); + await nclient.execute('sendrawtransaction', [txSigned.hex]); + + // confirm and advance to close auction + const addrAlice = await wclient.execute('getnewaddress', []); + await nclient.execute('generatetoaddress', [revealPeriod + 1, addrAlice]); + + // Ensure name is owned + const ownedNames = await wclient.execute('getnames', [true]); + assert.strictEqual(ownedNames.length, 1); + }); + }); }); + +async function signMultisigTx(tx, walletClients) { + assert(tx.hex, 'tx must be a json object with `hex`'); + assert(walletClients.length); + + for (const wclient of walletClients) + tx = await wclient.sign({tx: tx.hex}); + + return tx; +} diff --git a/test/wallet-test.js b/test/wallet-test.js index d167fea6f..d6d90f48c 100644 --- a/test/wallet-test.js +++ b/test/wallet-test.js @@ -3370,44 +3370,39 @@ describe('Wallet', function() { const aliceNonces = await alice.generateNonces(nameHash, addr, value); const bobNonces = await bob.generateNonces(nameHash, addr, value); - // Both alice and bob get N nonces + // Both alice and bob get the same N nonces + assert.deepStrictEqual(aliceNonces, bobNonces); assert.strictEqual(aliceNonces.length, 2); - assert.strictEqual(bobNonces.length, 2); - - // and are the same, but ordered differently. - // First nonce is always based on own key, - // Then the rest in order of public key. assert.deepStrictEqual( aliceNonces, [expectedNonces.alice, expectedNonces.bob] ); - assert.deepStrictEqual( - bobNonces, - [expectedNonces.bob, expectedNonces.alice] - ); // Generate Blind // -------------- // sanity check: no blinds saved as of this point - assert(!await bob.txdb.hasBlind(expectedBlinds.alice)); - assert(!await bob.txdb.hasBlind(expectedBlinds.bob)); + assert.strictEqual(await bob.txdb.hasBlind(expectedBlinds.alice), false); + assert.strictEqual(await bob.txdb.hasBlind(expectedBlinds.bob), false); // 1) Generate single blind: const bobBlind = await bob.generateBlind(nameHash, addr, value); // smallest public key (alice's) is used for blind assert.bufferEqual(bobBlind, expectedBlinds.alice); // bob's public key blind isn't stored - assert(await bob.txdb.hasBlind(expectedBlinds.alice)); - assert(!await bob.txdb.hasBlind(expectedBlinds.bob)); + assert.strictEqual(await bob.txdb.hasBlind(expectedBlinds.alice), true); + assert.strictEqual(await bob.txdb.hasBlind(expectedBlinds.bob), false); // 2) Generate all blinds: const bobBlinds = await bob.generateBlinds(nameHash, addr, value); - // own public key is used for blind (backwards-compatible) - assert.bufferEqual(bobBlinds[0], expectedBlinds.bob); - // but blinds for all key are saved in db - assert(await bob.txdb.hasBlind(expectedBlinds.alice)); - assert(await bob.txdb.hasBlind(expectedBlinds.bob)); + // smallest public key (alice's) is used for blind + assert.deepStrictEqual( + bobBlinds, + [expectedBlinds.alice, expectedBlinds.bob] + ); + // but blinds for all keys are saved in db + assert.strictEqual(await bob.txdb.hasBlind(expectedBlinds.alice), true); + assert.strictEqual(await bob.txdb.hasBlind(expectedBlinds.bob), true); }); }); });