From 961645a297897debeae85c0825769fc981264232 Mon Sep 17 00:00:00 2001 From: Anunay Jain Date: Sat, 2 Oct 2021 10:09:12 +0530 Subject: [PATCH 1/7] Fixed MTX cloning not carrying over CoinView MTX clone() would not carry over coinview, in order to get around this the CoinSelector would remove all inputs and add them back which made working with partially signed transactions a pain, this should fix that issue. --- lib/primitives/mtx.js | 49 ++++++++++++++++-------------------------- lib/wallet/wallet.js | 25 +++++++++++---------- test/util/memwallet.js | 18 ++++++++-------- 3 files changed, 39 insertions(+), 53 deletions(-) diff --git a/lib/primitives/mtx.js b/lib/primitives/mtx.js index 53a54ec73..c72f9a0a0 100644 --- a/lib/primitives/mtx.js +++ b/lib/primitives/mtx.js @@ -1122,12 +1122,20 @@ class MTX extends TX { assert(options, 'Options are required.'); assert(options.changeAddress, 'Change address is required.'); + // Hack in place to ensure backward compataibility with previous hack + if (this.inputs.length > 0) { + const inputPrevoutKeys = this.inputs.map(input => input.prevout.toKey()); + for (const coin of coins) { + const {hash, index} = coin; + const key = Outpoint.toKey(hash, index); + if ( inputPrevoutKeys.some(prevout => prevout.equals(key)) ) { + this.view.addCoin(coin); + } + } + } // Select necessary coins. const select = await this.selectCoins(coins, options); - // Make sure we empty the input array. - this.inputs.length = 0; - // Add coins to transaction. for (const coin of select.chosen) this.addCoin(coin); @@ -1424,6 +1432,7 @@ class CoinSelector { */ constructor(tx, options) { + this.parent = tx; this.tx = tx.clone(); this.coins = []; this.outputValue = 0; @@ -1569,6 +1578,12 @@ class CoinSelector { for (let i = 0; i < this.tx.inputs.length; i++) { const {prevout} = this.tx.inputs[i]; this.inputs.set(prevout.toKey(), i); + const coin = this.parent.view.getCoin(prevout); + // If the coin is not in the view, it's value cannot be known, + // therefore it can't be funded correctly. + if(!coin) + throw new Error('Could not resolve input coin value'); + this.tx.view.addCoin(coin); } } } @@ -1585,7 +1600,6 @@ class CoinSelector { this.chosen = []; this.change = 0; this.fee = CoinSelector.MIN_FEE; - this.tx.inputs.length = 0; switch (this.selection) { case 'all': @@ -1688,33 +1702,6 @@ class CoinSelector { */ fund() { - // Ensure all preferred inputs first. - if (this.inputs.size > 0) { - const coins = []; - - for (let i = 0; i < this.inputs.size; i++) - coins.push(null); - - for (const coin of this.coins) { - const {hash, index} = coin; - const key = Outpoint.toKey(hash, index); - const i = this.inputs.get(key); - - if (i != null) { - coins[i] = coin; - this.inputs.delete(key); - } - } - - if (this.inputs.size > 0) - throw new Error('Could not resolve preferred inputs.'); - - for (const coin of coins) { - this.tx.addCoin(coin); - this.chosen.push(coin); - } - } - if (this.isFull()) return; diff --git a/lib/wallet/wallet.js b/lib/wallet/wallet.js index e7abb803e..0ef790f13 100644 --- a/lib/wallet/wallet.js +++ b/lib/wallet/wallet.js @@ -38,7 +38,6 @@ const {types} = rules; const {Mnemonic} = HD; const {BufferSet} = require('buffer-map'); const Coin = require('../primitives/coin'); -const Outpoint = require('../primitives/outpoint'); /* * Constants @@ -1839,7 +1838,7 @@ class Wallet extends EventEmitter { output.covenant.pushHash(nameHash); output.covenant.pushU32(height); output.covenant.pushHash(nonce); - reveal.addOutpoint(Outpoint.fromTX(bid, bidOuputIndex)); + reveal.addCoin(bidCoin); reveal.outputs.push(output); await this.fill(reveal, { ...options, coins: coins }); @@ -1926,7 +1925,7 @@ class Wallet extends EventEmitter { output.covenant.pushU32(ns.height); output.covenant.pushHash(nonce); - mtx.addOutpoint(prevout); + mtx.addCoin(coin); mtx.outputs.push(output); } @@ -2051,7 +2050,7 @@ class Wallet extends EventEmitter { output.covenant.pushU32(ns.height); output.covenant.pushHash(nonce); - mtx.addOutpoint(prevout); + mtx.addCoin(coin); mtx.outputs.push(output); } @@ -2176,7 +2175,7 @@ class Wallet extends EventEmitter { if (coin.height < ns.height) continue; - mtx.addOutpoint(prevout); + mtx.addCoin(coin); const output = new Output(); output.address = coin.address; @@ -2298,7 +2297,7 @@ class Wallet extends EventEmitter { if (coin.height < ns.height) continue; - mtx.addOutpoint(prevout); + mtx.addCoin(coin); const output = new Output(); output.address = coin.address; @@ -2444,7 +2443,7 @@ class Wallet extends EventEmitter { output.covenant.pushHash(await this.wdb.getRenewalBlock()); const mtx = new MTX(); - mtx.addOutpoint(ns.owner); + mtx.addCoin(coin); mtx.outputs.push(output); return mtx; @@ -2524,7 +2523,7 @@ class Wallet extends EventEmitter { output.covenant.push(raw); const mtx = new MTX(); - mtx.addOutpoint(ns.owner); + mtx.addCoin(coin); mtx.outputs.push(output); return mtx; @@ -2663,7 +2662,7 @@ class Wallet extends EventEmitter { output.covenant.pushHash(await this.wdb.getRenewalBlock()); const mtx = new MTX(); - mtx.addOutpoint(ns.owner); + mtx.addCoin(coin); mtx.outputs.push(output); return mtx; @@ -2797,7 +2796,7 @@ class Wallet extends EventEmitter { output.covenant.push(address.hash); const mtx = new MTX(); - mtx.addOutpoint(ns.owner); + mtx.addCoin(coin); mtx.outputs.push(output); return mtx; @@ -2933,7 +2932,7 @@ class Wallet extends EventEmitter { output.covenant.push(EMPTY); const mtx = new MTX(); - mtx.addOutpoint(ns.owner); + mtx.addCoin(coin); mtx.outputs.push(output); return mtx; @@ -3077,7 +3076,7 @@ class Wallet extends EventEmitter { output.covenant.pushHash(await this.wdb.getRenewalBlock()); const mtx = new MTX(); - mtx.addOutpoint(ns.owner); + mtx.addCoin(coin); mtx.outputs.push(output); return mtx; @@ -3208,7 +3207,7 @@ class Wallet extends EventEmitter { output.covenant.pushU32(ns.height); const mtx = new MTX(); - mtx.addOutpoint(ns.owner); + mtx.addCoin(coin); mtx.outputs.push(output); return mtx; diff --git a/test/util/memwallet.js b/test/util/memwallet.js index 39d400435..82fd25731 100644 --- a/test/util/memwallet.js +++ b/test/util/memwallet.js @@ -1155,7 +1155,7 @@ class MemWallet { output.covenant.pushU32(ns.height); output.covenant.pushHash(nonce); - mtx.addOutpoint(prevout); + mtx.addCoin(coin); mtx.outputs.push(output); } @@ -1207,7 +1207,7 @@ class MemWallet { if (coin.height < ns.height) continue; - mtx.addOutpoint(prevout); + mtx.addCoin(coin); const output = new Output(); output.address = coin.address; @@ -1286,7 +1286,7 @@ class MemWallet { output.covenant.pushHash(this.getRenewalBlock()); const mtx = new MTX(); - mtx.addOutpoint(ns.owner); + mtx.addCoin(coin); mtx.outputs.push(output); return this._create(mtx, options); @@ -1348,7 +1348,7 @@ class MemWallet { output.covenant.push(resource); const mtx = new MTX(); - mtx.addOutpoint(ns.owner); + mtx.addCoin(coin); mtx.outputs.push(output); return this._create(mtx, options); @@ -1405,7 +1405,7 @@ class MemWallet { output.covenant.pushHash(this.getRenewalBlock()); const mtx = new MTX(); - mtx.addOutpoint(ns.owner); + mtx.addCoin(coin); mtx.outputs.push(output); return this._create(mtx, options); @@ -1461,7 +1461,7 @@ class MemWallet { output.covenant.push(address.hash); const mtx = new MTX(); - mtx.addOutpoint(ns.owner); + mtx.addCoin(coin); mtx.outputs.push(output); return this._create(mtx, options); @@ -1516,7 +1516,7 @@ class MemWallet { output.covenant.push(EMPTY); const mtx = new MTX(); - mtx.addOutpoint(ns.owner); + mtx.addCoin(coin); mtx.outputs.push(output); return this._create(mtx, options); @@ -1582,7 +1582,7 @@ class MemWallet { output.covenant.pushHash(this.getRenewalBlock()); const mtx = new MTX(); - mtx.addOutpoint(ns.owner); + mtx.addCoin(coin); mtx.outputs.push(output); return this._create(mtx, options); @@ -1635,7 +1635,7 @@ class MemWallet { output.covenant.pushU32(ns.height); const mtx = new MTX(); - mtx.addOutpoint(ns.owner); + mtx.addCoin(coin); mtx.outputs.push(output); return this._create(mtx, options); From c9d0d8d31894bfdd6d60b2d1c23d48ced3b6a22c Mon Sep 17 00:00:00 2001 From: Anunay Jain Date: Sat, 2 Oct 2021 10:11:44 +0530 Subject: [PATCH 2/7] Improved interactive-name-swap test Previous version of interactive-name-swap used a ugly hack to get around limitations on wallet funding, since wallet.fund() no longer wipes previous inputs, the previous implementation is not longer the recommended way. --- test/interactive-swap-test.js | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/test/interactive-swap-test.js b/test/interactive-swap-test.js index 0510ba2c0..d55b7c8f1 100644 --- a/test/interactive-swap-test.js +++ b/test/interactive-swap-test.js @@ -208,11 +208,12 @@ describe('Interactive name swap', function() { // Bob should verify all the data in the MTX to ensure everything is valid, // but this is the minimum. - const input0 = mtx.input(0).clone(); // copy input with Alice's signature - const coinEntry = await node.chain.db.readCoin(input0.prevout); + const coinEntry = await node.chain.db.readCoin(mtx.input(0).prevout); assert(coinEntry); // ensures that coin exists and is still unspent - const coin = coinEntry.toCoin(input0.prevout); + const coin = coinEntry.toCoin(mtx.input(0).prevout); + mtx.view.addCoin(coin); + assert(coin.covenant.type === types.TRANSFER); const addr = new Address({ version: coin.covenant.items[2].readInt8(), @@ -226,12 +227,8 @@ describe('Interactive name swap', function() { const changeAddress = await bob.changeAddress(); const rate = await wdb.estimateFee(); const coins = await bob.getSmartCoins(); - // Add the external coin to the coin selector so we don't fail assertions - coins.push(coin); + await mtx.fund(coins, {changeAddress, rate}); - // The funding mechanism starts by wiping out existing inputs - // which for us includes Alice's signature. Replace it from our backup. - mtx.inputs[0].inject(input0); // Rearrange outputs. // Since we added a change output, the SINGELREVERSE is now broken: From 3506db564e101c3ce7d6fd1fe53192f289326cda Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Wed, 1 Dec 2021 12:47:29 -0500 Subject: [PATCH 3/7] mtx: nits and comments about preferred inputs and coins --- lib/primitives/mtx.js | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/lib/primitives/mtx.js b/lib/primitives/mtx.js index c72f9a0a0..51ad4f624 100644 --- a/lib/primitives/mtx.js +++ b/lib/primitives/mtx.js @@ -1122,17 +1122,26 @@ class MTX extends TX { assert(options, 'Options are required.'); assert(options.changeAddress, 'Change address is required.'); - // Hack in place to ensure backward compataibility with previous hack + // Ensure backward compatibility with the old API: + // Inputs to MTX were added with mtx.addOutpoint() and then later + // CoinSelector.fund() was expected to match those outpoints with coins + // from the wallet. The problem with this is that when CoinSelector + // initialized, it wiped out all the inputs, including some that may have + // already been signed outside the wallet for custom scripts or CoinJoins. + // With the new API, mtx.addCoin() can be used to either fund existing + // (pre-signed) inputs OR add new "preferred" inputs for covenants. + // The logic below was moved here from CoinSelector.fund() in case + // the old method was used and "preferred" coins are not yet in the view. if (this.inputs.length > 0) { const inputPrevoutKeys = this.inputs.map(input => input.prevout.toKey()); for (const coin of coins) { - const {hash, index} = coin; - const key = Outpoint.toKey(hash, index); - if ( inputPrevoutKeys.some(prevout => prevout.equals(key)) ) { + const key = coin.toKey(); + if (inputPrevoutKeys.some(prevout => prevout.equals(key))) { this.view.addCoin(coin); } } } + // Select necessary coins. const select = await this.selectCoins(coins, options); @@ -1580,9 +1589,9 @@ class CoinSelector { this.inputs.set(prevout.toKey(), i); const coin = this.parent.view.getCoin(prevout); // If the coin is not in the view, it's value cannot be known, - // therefore it can't be funded correctly. - if(!coin) - throw new Error('Could not resolve input coin value'); + // therefore the TX can't be funded correctly. + if (!coin) + throw new Error('Could not resolve input coin value.'); this.tx.view.addCoin(coin); } } From 76b06e2530c4a40da3b04f0401c9a808919e4bcd Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Wed, 1 Dec 2021 12:48:02 -0500 Subject: [PATCH 4/7] mtx: do not duplicate inputs if adding duplicate coin --- lib/primitives/mtx.js | 12 ++++++++++-- test/interactive-swap-test.js | 10 ++-------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/lib/primitives/mtx.js b/lib/primitives/mtx.js index 51ad4f624..f95969d9e 100644 --- a/lib/primitives/mtx.js +++ b/lib/primitives/mtx.js @@ -158,11 +158,19 @@ class MTX extends TX { addCoin(coin) { assert(coin instanceof Coin, 'Cannot add non-coin.'); - const input = Input.fromCoin(coin); + // Avoid duplicate inputs + for (const input of this.inputs) { + const {hash, index} = input.prevout; + if (hash.equals(coin.hash) && index === coin.index) { + this.view.addCoin(coin); + return input; + } + } + // Coin represents a new input. + const input = Input.fromCoin(coin); this.inputs.push(input); this.view.addCoin(coin); - return input; } diff --git a/test/interactive-swap-test.js b/test/interactive-swap-test.js index d55b7c8f1..70b53bfa2 100644 --- a/test/interactive-swap-test.js +++ b/test/interactive-swap-test.js @@ -212,7 +212,7 @@ describe('Interactive name swap', function() { assert(coinEntry); // ensures that coin exists and is still unspent const coin = coinEntry.toCoin(mtx.input(0).prevout); - mtx.view.addCoin(coin); + mtx.addCoin(coin); assert(coin.covenant.type === types.TRANSFER); const addr = new Address({ @@ -222,13 +222,7 @@ describe('Interactive name swap', function() { assert.deepStrictEqual(addr, bobReceive); // transfer is to Bob's address // Fund the TX. - // The hsd wallet is not designed to handle partially-signed TXs - // or coins from outside the wallet, so a little hacking is needed. - const changeAddress = await bob.changeAddress(); - const rate = await wdb.estimateFee(); - const coins = await bob.getSmartCoins(); - - await mtx.fund(coins, {changeAddress, rate}); + await bob.fund(mtx); // Rearrange outputs. // Since we added a change output, the SINGELREVERSE is now broken: From 43bf26cf2ecd87759ea9e89f667c3b2f99b0d07b Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Wed, 1 Dec 2021 13:51:31 -0500 Subject: [PATCH 5/7] mtx: inject spent coins from view when cloning --- lib/primitives/mtx.js | 17 ++++++------- test/mtx-test.js | 57 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 9 deletions(-) diff --git a/lib/primitives/mtx.js b/lib/primitives/mtx.js index f95969d9e..5a2b7b1c8 100644 --- a/lib/primitives/mtx.js +++ b/lib/primitives/mtx.js @@ -100,8 +100,7 @@ class MTX extends TX { } /** - * Clone the transaction. Note that - * this will not carry over the view. + * Clone the transaction and it's input coins. * @returns {MTX} */ @@ -109,6 +108,13 @@ class MTX extends TX { assert(mtx instanceof this.constructor); super.inject(mtx); this.changeIndex = mtx.changeIndex; + + // CoinView has other properties like UndoCoins and BitView + // that an MTX doesn't care about. We just want a copy of the + // coins spent by the MTX. + for (const [key, value] of mtx.view.map.entries()) + this.view.map.set(key, value); + return this; } @@ -1449,7 +1455,6 @@ class CoinSelector { */ constructor(tx, options) { - this.parent = tx; this.tx = tx.clone(); this.coins = []; this.outputValue = 0; @@ -1595,12 +1600,6 @@ class CoinSelector { for (let i = 0; i < this.tx.inputs.length; i++) { const {prevout} = this.tx.inputs[i]; this.inputs.set(prevout.toKey(), i); - const coin = this.parent.view.getCoin(prevout); - // If the coin is not in the view, it's value cannot be known, - // therefore the TX can't be funded correctly. - if (!coin) - throw new Error('Could not resolve input coin value.'); - this.tx.view.addCoin(coin); } } } diff --git a/test/mtx-test.js b/test/mtx-test.js index e5dd39ae2..fbbfee82e 100644 --- a/test/mtx-test.js +++ b/test/mtx-test.js @@ -9,6 +9,8 @@ const bio = require('bufio'); const CoinView = require('../lib/coins/coinview'); const WalletCoinView = require('../lib/wallet/walletcoinview'); const MTX = require('../lib/primitives/mtx'); +const Address = require('../lib/primitives/address'); +const Coin = require('../lib/primitives/coin'); const Path = require('../lib/wallet/path'); const common = require('./util/common'); @@ -62,4 +64,59 @@ describe('MTX', function() { assert.ok(view instanceof CoinView); assert.deepStrictEqual(got, want); }); + + it('should clone MTX including view', () => { + const coin1 = new Coin({ + version: 1, + value: 1000001, + hash: Buffer.alloc(32, 0x01), + index: 1 + }); + + const coin1alt = new Coin({ + version: 1, + value: 9999999, + hash: Buffer.alloc(32, 0x01), + index: 1 + }); + + const coin2 = new Coin({ + version: 1, + value: 2000002, + hash: Buffer.alloc(32, 0x02), + index: 2 + }); + + const addr = new Address({ + version: 0, + hash: Buffer.alloc(20, 0xdb) + }); + + const value = coin1.value + coin2.value; + + const mtx1 = new MTX(); + mtx1.addCoin(coin1); + mtx1.addCoin(coin2); + mtx1.addOutput(addr, value); + + // Verify clone including view + const mtx2 = mtx1.clone(); + assert.deepStrictEqual(mtx1.toJSON(), mtx2.toJSON()); + assert.strictEqual(mtx1.getInputValue(), mtx2.getInputValue()); + assert.strictEqual(mtx1.view.map.size, 2); + assert.strictEqual(mtx2.view.map.size, 2); + + // Sanity check: verify deep clone by modifying original data + mtx1.view.remove(coin1.hash); + assert.notDeepStrictEqual(mtx1.toJSON(), mtx2.toJSON()); + assert.notDeepStrictEqual(mtx1.getInputValue(), mtx2.getInputValue()); + assert.strictEqual(mtx1.view.map.size, 1); + assert.strictEqual(mtx2.view.map.size, 2); + + mtx1.view.addCoin(coin1alt); + assert.notDeepStrictEqual(mtx1.toJSON(), mtx2.toJSON()); + assert.notStrictEqual(mtx1.getInputValue(), mtx2.getInputValue()); + assert.strictEqual(mtx1.view.map.size, 2); + assert.strictEqual(mtx2.view.map.size, 2); + }); }); From edc1f253b37aa228ee458ecb70d9b8d41fff39f0 Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Wed, 1 Dec 2021 14:03:43 -0500 Subject: [PATCH 6/7] mtx: do not search for preferred coins we already have in view --- lib/primitives/mtx.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/primitives/mtx.js b/lib/primitives/mtx.js index 5a2b7b1c8..93ff1883a 100644 --- a/lib/primitives/mtx.js +++ b/lib/primitives/mtx.js @@ -1146,11 +1146,13 @@ class MTX extends TX { // (pre-signed) inputs OR add new "preferred" inputs for covenants. // The logic below was moved here from CoinSelector.fund() in case // the old method was used and "preferred" coins are not yet in the view. - if (this.inputs.length > 0) { - const inputPrevoutKeys = this.inputs.map(input => input.prevout.toKey()); + for (const input of this.inputs) { + const {prevout} = input; + if (this.view.hasEntry(prevout)) + continue; + for (const coin of coins) { - const key = coin.toKey(); - if (inputPrevoutKeys.some(prevout => prevout.equals(key))) { + if (prevout.hash.equals(coin.hash) && prevout.index === coin.index) { this.view.addCoin(coin); } } From 54578efa4b4226d7a6aa555a01f7f679e417a15a Mon Sep 17 00:00:00 2001 From: Anunay Jain Date: Fri, 29 Sep 2023 04:25:03 +0530 Subject: [PATCH 7/7] WIP: blind staking --- test/turbo-blinds-test.js | 709 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 709 insertions(+) create mode 100644 test/turbo-blinds-test.js diff --git a/test/turbo-blinds-test.js b/test/turbo-blinds-test.js new file mode 100644 index 000000000..a47e42015 --- /dev/null +++ b/test/turbo-blinds-test.js @@ -0,0 +1,709 @@ +'use strict'; + +const assert = require('bsert'); +const Network = require('../lib/protocol/network'); +const FullNode = require('../lib/node/fullnode'); +const { MTX } = require('../lib/primitives/mtx'); +const Address = require('../lib/primitives/address'); +const Output = require('../lib/primitives/output'); +const Coin = require('../lib/primitives/coin'); +const {Resource} = require('../lib/dns/resource'); + +const {Script, Opcode, Stack} = require('../lib/script'); +const rules = require('../lib/covenants/rules'); +const {types} = rules; +const {WalletClient} = require('hs-client'); +const crypto = require('crypto'); +const common = require('../lib/script/common'); +const bio = require('bufio'); +const policy = require('../lib/protocol/policy'); +const { Outpoint } = require('../lib/primitives'); + +const network = Network.get('regtest'); + +const ports = { + p2p: 14331, + node: 14332, + wallet: 14333 +}; +const node = new FullNode({ + memory: true, + network: 'regtest', + plugins: [require('../lib/wallet/plugin')], + env: { + 'HSD_WALLET_HTTP_PORT': ports.wallet.toString() + } +}); + +const wclient = new WalletClient({ + port: ports.wallet +}); + +const {wdb} = node.require('walletdb'); + +// Alice wants to Bid 10 HNS +// Bob is willing to lend 90 HNS for a fee of 5 HNS +let alice, bob, aliceReceive, bobReceive; +let bobKey, aliceKey1, aliceKey2; + +// Amount lost to TX fee +let aliceLoss = 0; +let bobLoss = 0; + +// Here we only create one watchonly wallet, but we'll assume both parties have one. +let watchOnly; + +let presign; +let glob; + +const { + ALL, + NOINPUT, + ANYONECANPAY +} = common.hashType; + +// These are data that will be communicated between Alice and Bob +const name = rules.grindName(5, 1, network); +const nameHash = rules.hashName(name); +let ns; +const config = { + bid : 10 * 1e6, // 10 HNS + blind : 90 * 1e6, // 90 HNS + fee : 5 * 1e6 // 5 HNS, this fee should cover any transaction fees incurred by Bob too. +}; + +let bobRevealTX; + +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); + } +} + +function creatTimeLockedMultisig(stakerPubkey,bidderPubkey,height) { + // Script: + // OP_IF + // OP_CHECKSIGVERIFY + // OP_ELSE + // OP_CHECKLOCKTIMEVERIFY OP_DROP + // OP_ENDIF + // OP_CHECKSIG + + const script = new Script([ + Opcode.fromSymbol('if'), + Opcode.fromPush(stakerPubkey), + Opcode.fromSymbol('checksigverify'), + Opcode.fromSymbol('else'), + Opcode.fromInt(height), + Opcode.fromSymbol('checklocktimeverify'), + Opcode.fromSymbol('drop'), + Opcode.fromSymbol('endif'), + Opcode.fromPush(bidderPubkey), + Opcode.fromSymbol('checksig') + ]); + return script; +} + +describe('turbo blinds', function() { + before(async () => { + await node.open(); + await wclient.open(); + + alice = await wdb.create(); + bob = await wdb.create(); + + watchOnly = await wdb.create({ + accountKey: (await bob.getAccount('default')).accountKey, + watchOnly: true + }); + + aliceReceive = await alice.receiveAddress(); + bobReceive = await bob.receiveAddress(); + + aliceKey1 = alice.master.key.derive(5305,true).derive(0); + aliceKey2 = alice.master.key.derive(5305,true).derive(1); + bobKey = alice.master.key.derive(5305,true).derive(0); + }); + + after(async () => { + await wclient.close(); + await node.close(); + }); + + it('should fund both wallets', 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 alice.getBalance(); + const bobBal = await bob.getBalance(); + assert(aliceBal.confirmed === 2000 * 2 * 1e6); + assert(bobBal.confirmed === 2000 * 2 * 1e6); + }); + + // This step can be done by anyone + it('should send open', async () => { + const tx = await alice.sendOpen(name, false); + await mineBlocks(network.names.treeInterval + 1); + + const view = await alice.getSpentView(tx); + aliceLoss += tx.getFee(view); + }); + + it('should create presigned transactions - Bob', async () => { + ns = await alice.getNameStateByName(name); + + // Verify that we have enough time to bid, 5 blocks is enough. + assert(ns.isBidding(wdb.height + 5, network)); + + const revealPeriodEnd = ns.height + network.names.biddingPeriod + network.names.revealPeriod; + const bidScript = creatTimeLockedMultisig(bobKey.publicKey,aliceKey1.publicKey,revealPeriodEnd); + const fundingScript = creatTimeLockedMultisig(bobKey.publicKey,aliceKey2.publicKey,revealPeriodEnd); + + const nonce = crypto.randomBytes(32); + + // Bob creates presigned reveal (Alice can do this step too) + const revealTX = new MTX(); + + const output0 = new Output(); + const address = new Address().fromScript(bidScript); + output0.address = address; + output0.value = config.bid; + output0.covenant.type = types.REVEAL; + output0.covenant.pushHash(nameHash); + output0.covenant.pushU32(ns.height); + output0.covenant.pushHash(nonce); + + const output1 = new Output(); + output1.address = bobReceive; + output1.value = config.blind + config.bid + config.fee; + + revealTX.outputs.push(output0); + revealTX.outputs.push(output1); + + // Just a hack to set the appropriate coin values in coinview. + const coin1 = new Coin({ + height: 0, + value: config.bid + config.blind, + address: new Address().fromScript(bidScript), + hash: Buffer.alloc(32, 0x00), + index: 0 + }); + + const coin2 = new Coin({ + height: 0, + value: config.bid + config.fee, + address: new Address().fromScript(fundingScript), + hash: Buffer.alloc(32, 0x00), + index: 1 + }); + + revealTX.addCoin(coin1); + revealTX.addCoin(coin2); + + const feeRate = await wdb.estimateFee(1); // + const size = await revealTX.estimateSize(); // TODO: Pass in a custom size estimator for witness items + const fee = policy.getRoundFee(size,feeRate); // txfee + + // Modify coinview to contain appropriate value. + coin2.value += fee; + revealTX.view.addCoin(coin2); + + const sig0 = revealTX.signature(0, bidScript, config.bid + config.blind, bobKey.privateKey, ALL | NOINPUT | ANYONECANPAY); + const sig1 = revealTX.signature(1, fundingScript, config.bid + config.fee + fee, bobKey.privateKey, ALL | NOINPUT | ANYONECANPAY); + + bobRevealTX = revealTX; + // Bob Communicates the following information to alice, + // technically you could encode most of this in the tx itself, but this feels better + presign = { + tx: revealTX.encode().toString('hex'), + nonce: nonce.toString('hex'), + txFee: fee, + signaturesBob: [sig0.toString('hex'), sig1.toString('hex')], + signaturesAlice : [], + pubkeyBob: bobKey.publicKey.toString('hex'), + bidScript: bidScript.encode().toString('hex'), + fundingScript: fundingScript.encode().toString('hex') + }; + glob = { + bidScript: bidScript, + fundingScript: fundingScript + }; + }); + + it('should verify and sign transaction sent by Bob - Alice', async () => { + // Verify that we have enough time to bid, 5 blocks is enough. + assert(ns.isBidding(wdb.height + 5, network)); + + const revealPeriodEnd = ns.height + network.names.biddingPeriod + network.names.revealPeriod; + + // Create contracts using information provided + const pubkeyBob = Buffer.from(presign.pubkeyBob, 'hex'); + const bidScript = creatTimeLockedMultisig(pubkeyBob,aliceKey1.publicKey,revealPeriodEnd); + const fundingScript = creatTimeLockedMultisig(pubkeyBob,aliceKey2.publicKey,revealPeriodEnd); + + // Verify given contracts + assert.bufferEqual(fundingScript.encode(),Buffer.from(presign.fundingScript, 'hex')); + assert.bufferEqual(bidScript.encode(),Buffer.from(presign.bidScript, 'hex')); + + /** @type {MTX} */ + const revealTX = MTX.decode(Buffer.from(presign.tx, 'hex')); + const address = new Address().fromScript(bidScript); + + // Verify Outputs + // Verify Bid reveal output + assert.bufferEqual(revealTX.output(0).address.getHash(), address.getHash()); + assert(revealTX.output(0).value === config.bid); + assert(revealTX.output(0).covenant.type === types.REVEAL); + + // Verify convenants + const convenants = revealTX.output(0).covenant.toArray(); + + assert.bufferEqual(convenants[0], nameHash); + assert(bio.readU32(convenants[1], 0) === ns.height); + assert.bufferEqual(convenants[2], presign.nonce); + + // We really do not care what happens with the other outputs, + // Alice can split them into multiple coins, donate it to miners + const bobSigs = presign.signaturesBob.map(s => Buffer.from(s, 'hex')); + + // Technically these are malleable, but there is no need for other person + // to mess with these. + assert(revealTX.input(0).sequence === 0xffffffff); + assert(revealTX.input(1).sequence === 0xffffffff); + assert(revealTX.locktime === 0); + + // Ensure fee is high enough for reveal to go through, worst case scenario, + // since ANYONECANPAY is used, we can sacrifice some low value coin to pay the fee. + const feeRate = await wdb.estimateFee(3); // + const size = await revealTX.estimateSize(); // TODO: Pass in a custom size estimator for witness items + const minFee = policy.getRoundFee(size,feeRate); + + assert(presign.txFee >= minFee); + + // Sign the inputs, now it may seem like a bad idea signing this before verifying, + // but we'll do that shortly and these are presigns + const sig0 = revealTX.signature(0, bidScript, config.bid + config.blind, aliceKey1.privateKey, ALL | NOINPUT | ANYONECANPAY); + const sig1 = revealTX.signature(1, fundingScript, config.bid + config.fee + presign.txFee, aliceKey2.privateKey, ALL | NOINPUT | ANYONECANPAY); + + // Push signatures into witness records + const witness0 = new Stack(); + witness0.pushData(sig0); // Put Alice's Signature here + witness0.pushData(bobSigs[0]); + witness0.pushBool(true); + witness0.pushData(bidScript.encode()); + + const witness1 = new Stack(); + witness1.pushData(sig1); + witness1.pushData(bobSigs[1]); + witness1.pushBool(true); + witness1.pushData(fundingScript.encode()); + + // Placeholder coins + const coin1 = new Coin({ + height: 0, + value: config.bid + config.blind, + address: new Address().fromScript(bidScript), + hash: Buffer.alloc(32, 0x00), + index: 0 + }); + + const coin2 = new Coin({ + height: 0, + value: config.bid + config.fee + presign.txFee, + address: new Address().fromScript(fundingScript), + hash: Buffer.alloc(32, 0x00), + index: 1 + }); + + revealTX.view.addCoin(coin1); + revealTX.view.addCoin(coin2); + + revealTX.inputs[0].witness.fromStack(witness0); + revealTX.inputs[1].witness.fromStack(witness1); + + // Ensure Fee is high enough + assert(revealTX.getFee() === presign.txFee); + // Verify the transaction, this will take care of signatures too! + assert(revealTX.verify()); + + glob.revealTX = revealTX; + presign.signaturesAlice = [sig0.toString('hex'), sig1.toString('hex')]; + // All that needs to be set is appropriate prevouts, send signatures to Bob + }); + + it('should verify signatures sent by Alice - Bob', async () => { + const bobSigs = presign.signaturesBob.map(s => Buffer.from(s, 'hex')); + const aliceSigs = presign.signaturesAlice.map(s => Buffer.from(s, 'hex')); + + const witness0 = new Stack(); + witness0.pushData(aliceSigs[0]); // Put Alice's Signature here + witness0.pushData(bobSigs[0]); + witness0.pushBool(true); + witness0.pushData(Buffer.from(presign.bidScript, 'hex')); + + const witness1 = new Stack(); + witness1.pushData(aliceSigs[1]); + witness1.pushData(bobSigs[1]); + witness1.pushBool(true); + witness1.pushData(Buffer.from(presign.fundingScript, 'hex')); + + bobRevealTX.inputs[0].witness.fromStack(witness0); + bobRevealTX.inputs[1].witness.fromStack(witness1); + assert(bobRevealTX.verify()); + }); + + it('should start watching both funding and bid address', async () => { + glob.fundingAddress = new Address().fromScript(glob.fundingScript); + glob.bidAddress = new Address().fromScript(glob.bidScript); + + watchOnly.importAddress('default', glob.fundingAddress); + watchOnly.importAddress('default', glob.bidAddress); + }); + + it('should fund the funding script - Alice', async () => { + const out = new Output({ + address: glob.fundingAddress, + value: config.bid + config.fee + presign.txFee + }); + + // Optional NullAddress to backup Bob's pubkey and maybe a index; + // Compressed public keys are ~33 bytes + + // const out2 = new Output({ + // address: new Address().fromNulldata(bobKey.publicKey), + // value: 0, + // }) + + const tx = await alice.send({ + outputs: [out] + }); + // Tell Bob we funded the funding script :) + glob.fundingTX = tx; + const view = await alice.getSpentView(tx); + aliceLoss += tx.getFee(view); + }); + + it('should verify funding transaction - Bob', async () => { + // Bob waits 3 blocks for funding transaction to confirm + await mineBlocks(4); + // Verify fundingTX + const tx = await watchOnly.getTX(glob.fundingTX.hash()); + + // Verify fund has atleast 3 confirmations + // This is very important cause a double spend + // can cause bob to lose all his funds + const currentHeight = node.chain.tip.height; + assert(tx.height + 3 <= currentHeight); + + // Fund the funding output + let fundingOutput = null; + let fundingOutputIndex; + for(const index in tx.tx.outputs) { + const output = tx.tx.outputs[index]; + if(output.address.toString() === glob.fundingAddress.toString()) { + fundingOutput = output; + fundingOutputIndex = parseInt(index); + } + } + assert(fundingOutput, 'Funding Transaction does not fund the address'); + assert(fundingOutput.value === config.bid + config.fee + presign.txFee); + assert(fundingOutput.covenant.type === types.NONE); + // You never know :P + assert(fundingOutput.address.toString() === glob.fundingAddress.toString()); + + glob.fundingPrevout = { + tx: tx, + hash: tx.tx.hash(), + index: fundingOutputIndex + }; + }); + + it('should BID on name - Bob', async () => { + const start = ns.height; + // Generate blind from nonce + const blind = rules.blind(config.bid, Buffer.from(presign.nonce,'hex')); + const rawName = Buffer.from(name, 'ascii'); + + const output = new Output(); + output.address = glob.bidAddress; + output.value = config.bid + config.blind; + output.covenant.type = types.BID; + output.covenant.pushHash(nameHash); + output.covenant.pushU32(start); + output.covenant.push(rawName); + output.covenant.pushHash(blind); + + const mtx = new MTX(); + mtx.outputs.push(output); + + const unlock = await bob.fundLock.lock(); + let tx; + try { + await bob.fill(mtx); + await bob.finalize(mtx); + tx = await bob.sendMTX(mtx); // Verifies, signs and broadcasts; + } finally { + unlock(); + } + assert(tx); + glob.bidTX = tx; + const view = await bob.getSpentView(tx); + bobLoss += tx.getFee(view); + }); + + it('Verify bid by bob - Alice', async () => { + await mineBlocks(1); + // Verify bidTX, this will also be done by the trusted third party + // to decide wheteher to punish Bob or not. + const tx = await watchOnly.getTX(glob.bidTX.hash()); + assert(tx); + + const rawName = Buffer.from(name, 'ascii'); + const blind = rules.blind(config.bid, Buffer.from(presign.nonce,'hex')); + + let bidOutput = null; + let bidOutputIndex = -1; + for(const index in tx.tx.outputs) { + const output = tx.tx.outputs[index]; + if(output.address.toString() === glob.bidAddress.toString()) { + bidOutput = output; + bidOutputIndex = parseInt(index); + } + } + + glob.bidPrevout = { + tx: tx, + hash: tx.tx.hash(), + index: bidOutputIndex + }; + + assert(bidOutput); + assert(bidOutput.value = config.bid + config.blind); + + const covenants = bidOutput.covenant.toArray(); + assert(bidOutput.covenant.type === types.BID); + assert.bufferEqual(covenants[0], nameHash); + assert(bio.readU32(covenants[1], 0) === ns.height); + assert.bufferEqual(covenants[2], rawName); + assert.bufferEqual(covenants[3], blind); + }); + + it('should wait for reveal phase to start and reveal - Bob or Alice', async () => { + // Either parties can reveal, but Alice has little incentive to reveal + // so bob has to ensure that he reveals on time + const currentHeight = node.chain.tip.height; + const { + treeInterval, + biddingPeriod + } = network.names; + + const openPeriod = treeInterval + 1; + const revealHeight = ns.height + openPeriod + biddingPeriod; + + if(currentHeight < revealHeight) { + await mineBlocks(revealHeight - currentHeight); + } + + assert(ns.isReveal(node.chain.tip.height, network)); + + // Just a hack to add entries to the view so we can verify the tx + const coin0 = new Coin().fromTX(glob.bidPrevout.tx.tx, glob.bidPrevout.index, glob.bidPrevout.tx.height); + const coin1 = new Coin().fromTX(glob.fundingPrevout.tx.tx, glob.fundingPrevout.index, glob.fundingPrevout.tx.height); + + // Bob can reveal now + const mtx = glob.revealTX; + mtx.input(0).prevout = new Outpoint(glob.bidPrevout.hash, glob.bidPrevout.index); + mtx.input(1).prevout = new Outpoint(glob.fundingPrevout.hash, glob.fundingPrevout.index); + mtx.view.addCoin(coin0); + mtx.view.addCoin(coin1); + + assert(mtx.verify()); + await node.sendTX(mtx.toTX()); + await mineBlocks(1); + }); + + it('should verify everyone recieved funds correctly', async () => { + const aliceBal = await alice.getBalance(); + const bobBal = await bob.getBalance(); + + // Alice pays for + // TX fee for funding + // The bid and staker's fee + // and TX fee for the reveal + // (optionally) TX fee for open + aliceLoss += config.bid + config.fee + presign.txFee; + + assert(aliceBal.confirmed === 4000 * 1e6 - aliceLoss); + + // Bob gains his fee (subtract tx fee for the bid) + // His fee should be high enough to cover the tx fees + const bobGain = config.fee - bobLoss; + assert(bobGain > 0); + assert(bobBal.confirmed === 4000 * 1e6 + bobGain); + }); + + it('should register name', async () => { + // Skip past the reveal phase + const currentHeight = node.chain.tip.height; + const { + treeInterval, + biddingPeriod, + revealPeriod + } = network.names; + const revealEnd = ns.height + treeInterval + 1 + biddingPeriod + revealPeriod; + + if(currentHeight < revealEnd) { + await mineBlocks(revealEnd - currentHeight); + } + ns = await alice.getNameStateByName(name); + assert(ns.isClosed(node.chain.tip.height, network)); + assert.bufferEqual(ns.owner.hash, glob.revealTX.hash()); + assert(ns.owner.index === 0); + + // Alice should be able to register the name + const mtx = new MTX(); + + const output = new Output(); + output.address = glob.bidAddress; + output.value = ns.value; + + output.covenant.type = types.REGISTER; + output.covenant.pushHash(nameHash); + output.covenant.pushU32(ns.height); + output.covenant.push(Buffer.alloc(0)); + output.covenant.pushHash(await wdb.getRenewalBlock()); + mtx.outputs.push(output); + + const tx = await watchOnly.getTX(glob.revealTX.hash()); + const nameCoin = Coin.fromTX(tx.tx, 0, tx.height); + mtx.addCoin(nameCoin); + + mtx.setLocktime(node.chain.tip.height); + // Now time to sign it + await alice.fund(mtx); + await alice.sign(mtx); + const sig = mtx.signature(0, glob.bidScript, nameCoin.value, aliceKey1.privateKey, ALL); + + const witness = new Stack(); + witness.pushData(sig); + witness.pushBool(false); + witness.pushData(glob.bidScript.encode()); + + mtx.inputs[0].witness.fromStack(witness); + assert(mtx.verify()); + await node.sendTX(mtx.toTX()); + await mineBlocks(1); + + ns = await alice.getNameStateByName(name); + assert(ns.registered); + }); + + it('should transfer name', async () => { + ns = await alice.getNameStateByName(name); + + const mtx = new MTX(); + + const {hash, index} = ns.owner; + const nameCoin = await node.getCoin(hash, index); + mtx.addCoin(nameCoin); + + const output = new Output(); + output.address = glob.bidAddress; + output.value = ns.value; + + const address = await alice.receiveAddress(); + output.covenant.type = types.TRANSFER; + output.covenant.pushHash(nameHash); + output.covenant.pushU32(ns.height); + output.covenant.pushU8(address.version); + output.covenant.push(address.hash); + mtx.outputs.push(output); + + mtx.setLocktime(node.chain.tip.height); + // Now time to sign it + await alice.fund(mtx); + await alice.sign(mtx); + const sig = mtx.signature(0, glob.bidScript, nameCoin.value, aliceKey1.privateKey, ALL); + + const witness = new Stack(); + witness.pushData(sig); + witness.pushBool(false); + witness.pushData(glob.bidScript.encode()); + + mtx.inputs[0].witness.fromStack(witness); + assert(mtx.verify()); + await node.sendTX(mtx.toTX()); + await mineBlocks(1); + }); + + it('should finalize name transfer', async () => { + // We wait out the transfer lockup time + await mineBlocks(network.names.transferLockup + 1); + + ns = await alice.getNameStateByName(name); + + const {hash, index} = ns.owner; + const nameCoin = await node.getCoin(hash, index); + + const version = nameCoin.covenant.getU8(2); + const addr = nameCoin.covenant.get(3); + const address = Address.fromHash(addr, version); + const rawName = Buffer.from(name, 'ascii'); + + // Alice should be able to register the name + const mtx = new MTX(); + mtx.addCoin(nameCoin); + + const output = new Output(); + output.address = address; + output.value = ns.value; + + output.covenant.type = types.FINALIZE; + output.covenant.pushHash(nameHash); + output.covenant.pushU32(ns.height); + output.covenant.push(rawName); + output.covenant.pushU8(0); // this name was part of a bid so + output.covenant.pushU32(ns.claimed); + output.covenant.pushU32(ns.renewals); + output.covenant.pushHash(await wdb.getRenewalBlock()); + mtx.outputs.push(output); + + mtx.setLocktime(node.chain.tip.height); + // Now time to sign it + await alice.fund(mtx); + await alice.sign(mtx); + + const sig = mtx.signature(0, glob.bidScript, nameCoin.value, aliceKey1.privateKey, ALL); + + const witness = new Stack(); + witness.pushData(sig); + witness.pushBool(false); + witness.pushData(glob.bidScript.encode()); + + mtx.inputs[0].witness.fromStack(witness); + assert(mtx.verify()); + await node.sendTX(mtx.toTX()); + await mineBlocks(1); + }); + + it('should verify name got transferred', async () => { + const ns = await node.getNameStatus(nameHash); + const owner = ns.owner; + const coin = await alice.getCoin(owner.hash, owner.index); + assert(coin); + + const resource = Resource.fromJSON({ + records: [{type: 'TXT', txt: ['Thanks Bob! -- Alice']}] + }); + await alice.sendUpdate(name, resource); + await mineBlocks(network.names.treeInterval); + const actual = await node.chain.db.getNameState(nameHash); + assert.bufferEqual(resource.encode(), actual.data); + }); +});