diff --git a/lib/primitives/mtx.js b/lib/primitives/mtx.js index 53a54ec73..93ff1883a 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; } @@ -158,11 +164,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; } @@ -1122,12 +1136,31 @@ class MTX extends TX { assert(options, 'Options are required.'); assert(options.changeAddress, 'Change address is required.'); + // 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. + for (const input of this.inputs) { + const {prevout} = input; + if (this.view.hasEntry(prevout)) + continue; + + for (const coin of coins) { + if (prevout.hash.equals(coin.hash) && prevout.index === coin.index) { + 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); @@ -1585,7 +1618,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 +1720,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/interactive-swap-test.js b/test/interactive-swap-test.js index 0510ba2c0..70b53bfa2 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.addCoin(coin); + assert(coin.covenant.type === types.TRANSFER); const addr = new Address({ version: coin.covenant.items[2].readInt8(), @@ -221,17 +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(); - // 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); + await bob.fund(mtx); // Rearrange outputs. // Since we added a change output, the SINGELREVERSE is now broken: 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); + }); }); 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); + }); +}); 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);