From 961645a297897debeae85c0825769fc981264232 Mon Sep 17 00:00:00 2001 From: Anunay Jain Date: Sat, 2 Oct 2021 10:09:12 +0530 Subject: [PATCH 1/4] 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/4] 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 6118f8efbf56f9747ad958624124065fe9f56fd8 Mon Sep 17 00:00:00 2001 From: Anunay Jain Date: Tue, 26 Oct 2021 05:10:54 +0530 Subject: [PATCH 3/4] Improved Anyone Can Renew to use a mature block --- lib/blockchain/chain.js | 10 +++++++++- test/anyone-can-renew-test.js | 7 +++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/lib/blockchain/chain.js b/lib/blockchain/chain.js index e096dce7f..2daabecf4 100644 --- a/lib/blockchain/chain.js +++ b/lib/blockchain/chain.js @@ -845,7 +845,15 @@ class Chain extends AsyncEmitter { assert(Buffer.isBuffer(hash)); assert((height >>> 0) === height); - // Cannot renew yet. + // Orignal comment: Cannot renew yet. + // This basically allows you to renew domains + // without having a mature enough block hash in the + // first 30 days of blockchain (on mainnet) + // and first 50 blocks on regtest. Considering + // names could not have been bought in that period, + // and the orignal comment, this seems to be a mistake + // and was supposed be return false; + if (height < this.network.names.renewalMaturity) return true; diff --git a/test/anyone-can-renew-test.js b/test/anyone-can-renew-test.js index cf912f111..eb75e2d8b 100644 --- a/test/anyone-can-renew-test.js +++ b/test/anyone-can-renew-test.js @@ -67,6 +67,9 @@ describe('Anyone-can-renew address', function() { aliceReceive = await alice.receiveAddress(); bobReceive = await bob.receiveAddress(); + + // Skip past first 50 blocks so renewal is possible. + await mineBlocks(50); }); after(async () => { @@ -275,7 +278,7 @@ describe('Anyone-can-renew address', function() { mtx.output(0).covenant.type = rules.types.RENEW; mtx.output(0).covenant.pushHash(nameHash); mtx.output(0).covenant.pushU32(heightBeforeOpen + 1); - mtx.output(0).covenant.pushHash(node.chain.tip.hash); + mtx.output(0).covenant.pushHash(await wdb.getRenewalBlock()); await alice.fund(mtx, {coins: [coin]}); await alice.finalize(mtx, {coins: [coin]}); @@ -300,7 +303,7 @@ describe('Anyone-can-renew address', function() { mtx.output(0).covenant.type = rules.types.RENEW; mtx.output(0).covenant.pushHash(nameHash); mtx.output(0).covenant.pushU32(heightBeforeOpen + 1); - mtx.output(0).covenant.pushHash(node.chain.tip.hash); + mtx.output(0).covenant.pushHash(await wdb.getRenewalBlock()); await bob.fund(mtx, {coins: [coin]}); await bob.finalize(mtx, {coins: [coin]}); From 723c014867dc94cc236b6f391e45fb3cbcac2fde Mon Sep 17 00:00:00 2001 From: Anunay Jain Date: Tue, 26 Oct 2021 05:14:05 +0530 Subject: [PATCH 4/4] WIP: Split Domain Management Example script to have seperate keys for UPDATEing and TRANSFERing domains + anyone can renew Co-authored-by: Matthew Zipkin --- test/split-domain-management-test.js | 566 +++++++++++++++++++++++++++ 1 file changed, 566 insertions(+) create mode 100644 test/split-domain-management-test.js diff --git a/test/split-domain-management-test.js b/test/split-domain-management-test.js new file mode 100644 index 000000000..03bc46edc --- /dev/null +++ b/test/split-domain-management-test.js @@ -0,0 +1,566 @@ +/* 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 MTX = require('../lib/primitives/mtx'); +const Address = require('../lib/primitives/address'); +const Output = require('../lib/primitives/output'); +const {Script, Opcode, Stack} = require('../lib/script'); +const rules = require('../lib/covenants/rules'); +const {Resource} = require('../lib/dns/resource'); +const {forValue} = require('./util/common'); +const common = require('../lib/script/common.js'); + +// Split Domain Management: +// +// Script: +// OP_TYPE +// types.RENEW +// OP_EQUAL +// OP_IF +// OP_TRUE +// OP_ELSE +// OP_DUP +// +// OP_EQUAL +// OP_IF +// OP_TYPE +// types.UPDATE +// OP_EQUALVERIFY +// OP_CHECKSIG +// OP_ELSE +// OP_DUP +// +// OP_EQUALVERIFY +// OP_CHECKSIG +// OP_ENDIF +// OP_ENDIF + +const createScript = function (pubKeyhot,pubKeycold) { + return new Script([ + Opcode.fromSymbol('type'), + Opcode.fromInt(rules.types.RENEW), + Opcode.fromSymbol('equal'), + Opcode.fromSymbol('if'), + Opcode.fromBool(true), + Opcode.fromSymbol('else'), + Opcode.fromSymbol('dup'), + Opcode.fromPush(pubKeyhot), + Opcode.fromSymbol('equal'), + Opcode.fromSymbol('if'), + Opcode.fromSymbol('type'), + Opcode.fromInt(rules.types.UPDATE), + Opcode.fromSymbol('equalverify'), + Opcode.fromSymbol('checksig'), + Opcode.fromSymbol('else'), + Opcode.fromSymbol('dup'), + Opcode.fromPush(pubKeycold), + Opcode.fromSymbol('equalverify'), + Opcode.fromSymbol('checksig'), + Opcode.fromSymbol('endif'), + Opcode.fromSymbol('endif') + ]); +}; + +const network = Network.get('regtest'); + +const node = new FullNode({ + memory: true, + network: 'regtest', + plugins: [require('../lib/wallet/plugin')] +}); + +const {wdb} = node.require('walletdb'); + +let alice, aliceReceive; +let bob, bobReceive; +let faythe, faytheReceive; + +let pubKeyhot, pubKeycold, privKeyhot, privKeycold; +let script, address; + +const name = rules.grindName(5, 1, network); +const nameHash = rules.hashName(name); +let heightBeforeOpen, heightBeforeRegister, heightBeforeFinalize; +let coin; + +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('Split Domain Management', function() { + before(async () => { + await node.open(); + + alice = await wdb.create(); // Bought and retains ownership of domain + bob = await wdb.create(); // Has ability to UPDATE + faythe = await wdb.create(); // Watchtower which can renew domain. + + aliceReceive = await alice.receiveAddress(); + bobReceive = await bob.receiveAddress(); + faytheReceive = await faythe.receiveAddress(); + + // TODO: HIP-0009 + // pubKeyhot = bob.deriveSomething(); + // pubKeycold = alice.deriveSomething(); + + // I'm Lazy right now so imma just do that for now + pubKeyhot = (await bob.getKey(bobReceive)).publicKey; + pubKeycold = (await alice.getKey(aliceReceive)).publicKey; + + privKeyhot = (await bob.getPrivateKey(bobReceive)).privateKey; + privKeycold = (await alice.getPrivateKey(aliceReceive)).privateKey; + + script = createScript(pubKeyhot,pubKeycold); + }); + + after(async () => { + await node.close(); + }); + + it('should create address from script', () => { + address = new Address().fromScript(script); + }); + + it('should fund all wallets', async () => { + await mineBlocks(2, aliceReceive); + await mineBlocks(2, bobReceive); + await mineBlocks(2, faytheReceive); + + await forValue(wdb, 'height', node.chain.height); + + const aliceBal = await alice.getBalance(); + const bobBal = await bob.getBalance(); + const faytheBal = await faythe.getBalance(); + + assert(aliceBal.confirmed === 2000 * 2 * 1e6); + assert(bobBal.confirmed === 2000 * 2 * 1e6); + assert(faytheBal.confirmed === 2000 * 2 * 1e6); + }); + + it('should win name with Alice\'s wallet', async () => { + heightBeforeOpen = node.chain.height; + + await alice.sendOpen(name, false); + await mineBlocks(network.names.treeInterval + 1); + + await alice.sendBid(name, 100000, 200000); + await mineBlocks(network.names.biddingPeriod); + + await alice.sendReveal(name); + await mineBlocks(network.names.revealPeriod + 1); + + let ns = await node.getNameStatus(nameHash); + assert(ns); + const owner = ns.owner; + const coin = await alice.getCoin(owner.hash, owner.index); + assert(coin); + const json = ns.getJSON(node.chain.height, node.network); + assert(json.state === 'CLOSED'); + + heightBeforeRegister = node.chain.height; + + const resource = Resource.fromJSON({ + records: [{type: 'TXT', txt: ['This name is managed by multiple keys']}] + }); + await alice.sendUpdate(name, resource); + await mineBlocks(network.names.treeInterval); + + ns = await node.getNameStatus(nameHash); + assert.strictEqual(ns.height, heightBeforeOpen + 1); + assert.strictEqual(ns.renewal, heightBeforeRegister + 1); + }); + + it('should TRANSFER/FINALIZE to Split domain management address', async () => { + const heightBeforeTransfer = node.chain.height; + + await alice.sendTransfer(name, address); + await mineBlocks(network.names.transferLockup); + + let ns = await node.getNameStatus(nameHash); + assert.strictEqual(ns.transfer, heightBeforeTransfer + 1); + + heightBeforeFinalize = node.chain.height; + + await alice.sendFinalize(name); + await mineBlocks(1); + + // FINALIZE resets transfer and renewal + ns = await node.getNameStatus(nameHash); + assert.strictEqual(ns.transfer, 0); + assert.strictEqual(ns.height, heightBeforeOpen + 1); + assert.strictEqual(ns.renewal, heightBeforeFinalize + 1); + + const {hash, index} = ns.owner; + coin = await node.getCoin(hash, index); + assert.deepStrictEqual(coin.address, address); + }); + + it('should not be owned by either wallet', async () => { + assert.rejects( + alice.sendTransfer(name, aliceReceive), + {message: `Wallet does not own: "${name}".`} + ); + + assert.rejects( + bob.sendTransfer(name, bobReceive), + {message: 'Auction not found.'} + ); + }); + + it('should advance chain to avoid premature renewal', async () => { + await mineBlocks(network.names.treeInterval); + }); + + it('should fail to spend without correct script', async () => { + const mtx = new MTX(); + mtx.addCoin(coin); + + const witness = new Stack(); + witness.pushData(Buffer.from('deadbeef', 'hex')); + mtx.inputs[0].witness.fromStack(witness); + + mtx.addOutput(new Output({ + value: coin.value, + address: coin.address + })); + mtx.output(0).covenant.type = rules.types.RENEW; + mtx.output(0).covenant.pushHash(nameHash); + mtx.output(0).covenant.pushU32(heightBeforeOpen + 1); + mtx.output(0).covenant.pushHash(node.chain.tip.hash); + + await alice.fund(mtx, {coins: [coin]}); + await alice.finalize(mtx, {coins: [coin]}); + await alice.sign(mtx); + + assert.throws( + () => mtx.check(), + {message: 'WITNESS_PROGRAM_MISMATCH'} + ); + }); + + it('should spend with correct action type: RENEW - Faythe', async () => { + const mtx = new MTX(); + mtx.addCoin(coin); + const witness = new Stack(); + witness.pushData(script.encode()); + mtx.inputs[0].witness.fromStack(witness); + + mtx.addOutput(new Output({ + value: coin.value, + address: coin.address + })); + mtx.output(0).covenant.type = rules.types.RENEW; + mtx.output(0).covenant.pushHash(nameHash); + mtx.output(0).covenant.pushU32(heightBeforeOpen + 1); + mtx.output(0).covenant.pushHash(node.chain.tip.hash); + + await faythe.fund(mtx, {coins: [coin]}); + await faythe.finalize(mtx, {coins: [coin]}); + await faythe.sign(mtx); + + mtx.check(); + await mineBlocks(50); + + // faythe (aka "anyone") will broadcast + const heightBeforeRenewal = node.chain.height; + await node.sendTX(mtx.toTX()); + + await mineBlocks(1); + + const ns = await node.getNameStatus(nameHash); + assert.strictEqual(ns.transfer, 0); + assert.strictEqual(ns.height, heightBeforeOpen + 1); + assert.strictEqual(ns.renewal, heightBeforeRenewal + 1); + + const {hash, index} = ns.owner; + coin = await node.getCoin(hash, index); + assert.deepStrictEqual(coin.address, address); + assert.bufferEqual(hash, mtx.hash()); + + // Urkel tree data is preserved + const res = Resource.decode(ns.data); + assert.strictEqual(res.records[0].txt[0], 'This name is managed by multiple keys'); + }); + + it('should fail to spend without correct signature: UPDATE', async () => { + const mtx = new MTX(); + mtx.addCoin(coin); + + mtx.addOutput(new Output({ + value: coin.value, + address: coin.address + })); + + const resource = Resource.fromJSON({ + records: [ + { + type: 'TXT', + txt: ['This name is managed by multiple keys and was just updated by Bob'] + } + ] + }); + + mtx.output(0).covenant.type = rules.types.UPDATE; + mtx.output(0).covenant.pushHash(nameHash); + mtx.output(0).covenant.pushU32(heightBeforeOpen + 1); + mtx.output(0).covenant.push(resource.encode()); + + await bob.fund(mtx, {coins: [coin]}); + await bob.finalize(mtx, {coins: [coin]}); + + const witness = new Stack(); + witness.pushData(Buffer.from([])); + witness.pushData(pubKeyhot); + witness.pushData(script.encode()); + mtx.inputs[0].witness.fromStack(witness); + + await bob.sign(mtx); + assert.throws( + () => mtx.check(), + {message: 'EVAL_FALSE'} + ); + }); + + it('should fail to spend without correct signature: TRANSFER', async () => { + const mtx = new MTX(); + mtx.addCoin(coin); + + mtx.addOutput(new Output({ + value: coin.value, + address: coin.address + })); + + mtx.output(0).covenant.type = rules.types.TRANSFER; + mtx.output(0).covenant.pushHash(nameHash); + mtx.output(0).covenant.pushU32(heightBeforeOpen + 1); + mtx.output(0).covenant.pushU8(coin.address.version); + // Let's just try transfer to same address whatever + mtx.output(0).covenant.push(coin.address.hash); + + await bob.fund(mtx, {coins: [coin]}); + await bob.finalize(mtx, {coins: [coin]}); + + // Sign after all the funding stuff is done + // Attempt signing from hot key while trying transfer + const sig = mtx.signature(0, script, coin.value, privKeyhot, common.hashType.ALL); + const witness = new Stack(); + witness.pushData(sig); + witness.pushData(privKeyhot); + witness.pushData(script.encode()); + mtx.inputs[0].witness.fromStack(witness); + + await bob.sign(mtx); + assert.throws( + () => mtx.check(), + {message: 'EQUALVERIFY (op=OP_EQUALVERIFY, ip=17)'} + ); + }); + + it('should spend with correct action type and signature: UPDATE - Bob', async () => { + const mtx = new MTX(); + mtx.addCoin(coin); + + mtx.addOutput(new Output({ + value: coin.value, + address: coin.address + })); + + const resource = Resource.fromJSON({ + records: [ + { + type: 'TXT', + txt: ['This name is managed by multiple keys and was just updated by Bob'] + } + ] + }); + + mtx.output(0).covenant.type = rules.types.UPDATE; + mtx.output(0).covenant.pushHash(nameHash); + mtx.output(0).covenant.pushU32(heightBeforeOpen + 1); + mtx.output(0).covenant.push(resource.encode()); + + await bob.fund(mtx, {coins: [coin]}); + await bob.finalize(mtx, {coins: [coin]}); + + // Sign after all the funding stuff is done + const sig = mtx.signature(0, script, coin.value, privKeyhot, common.hashType.ALL); + const witness = new Stack(); + witness.pushData(sig); + witness.pushData(pubKeyhot); + witness.pushData(script.encode()); + mtx.inputs[0].witness.fromStack(witness); + + await bob.sign(mtx); + + mtx.check(); + + await node.sendTX(mtx.toTX()); + await mineBlocks(1); + + const ns = await node.getNameStatus(nameHash); + const {hash, index} = ns.owner; + coin = await node.getCoin(hash, index); + assert.deepStrictEqual(coin.address, address); + assert.bufferEqual(hash, mtx.hash()); + + const res = Resource.decode(ns.data); + assert.strictEqual( + res.records[0].txt[0], + 'This name is managed by multiple keys and was just updated by Bob' + ); + }); + + it('should spend with correct action type and signature: UPDATE - Alice', async () => { + const mtx = new MTX(); + mtx.addCoin(coin); + + mtx.addOutput(new Output({ + value: coin.value, + address: coin.address + })); + + const resource = Resource.fromJSON({ + records: [ + { + type: 'TXT', + txt: ['This name is managed by multiple keys and was just updated by Alice'] + } + ] + }); + + mtx.output(0).covenant.type = rules.types.UPDATE; + mtx.output(0).covenant.pushHash(nameHash); + mtx.output(0).covenant.pushU32(heightBeforeOpen + 1); + mtx.output(0).covenant.push(resource.encode()); + + await alice.fund(mtx, {coins: [coin]}); + await alice.finalize(mtx, {coins: [coin]}); + + // Sign after all the funding stuff is done + const sig = mtx.signature(0, script, coin.value, privKeycold, common.hashType.ALL); + const witness = new Stack(); + witness.pushData(sig); + witness.pushData(pubKeycold); + witness.pushData(script.encode()); + mtx.inputs[0].witness.fromStack(witness); + + await alice.sign(mtx); + + mtx.check(); + + await node.sendTX(mtx.toTX()); + await mineBlocks(1); + + const ns = await node.getNameStatus(nameHash); + const {hash, index} = ns.owner; + coin = await node.getCoin(hash, index); + assert.deepStrictEqual(coin.address, address); + assert.bufferEqual(hash, mtx.hash()); + + const res = Resource.decode(ns.data); + assert.strictEqual( + res.records[0].txt[0], + 'This name is managed by multiple keys and was just updated by Alice' + ); + }); + + it('should spend with correct action type and signature: TRANSFER - Alice', async () => { + const mtx = new MTX(); + mtx.addCoin(coin); + // Anyone-can-renew address, just for testing + const address = Address.fromString('rs1qu3nrzrjkd783ftpk7l4hvpa96aazx5dddw66hgs2zuukckcchrqs570axm'); + + mtx.addOutput(new Output({ + value: coin.value, + address: coin.address + })); + + mtx.output(0).covenant.type = rules.types.TRANSFER; + mtx.output(0).covenant.pushHash(nameHash); + mtx.output(0).covenant.pushU32(heightBeforeOpen + 1); + mtx.output(0).covenant.pushU8(address.version); + mtx.output(0).covenant.push(address.hash); + + await alice.fund(mtx, {coins: [coin]}); + await alice.finalize(mtx, {coins: [coin]}); + + // Sign after all the funding stuff is done + const sig = mtx.signature(0, script, coin.value, privKeycold, common.hashType.ALL); + const witness = new Stack(); + witness.pushData(sig); + witness.pushData(pubKeycold); + witness.pushData(script.encode()); + mtx.inputs[0].witness.fromStack(witness); + + await alice.sign(mtx); + mtx.check(); + await node.sendTX(mtx.toTX()); + await mineBlocks(1); + + // Confirm tx got confirmed + const ns = await node.getNameStatus(nameHash); + const {hash, index} = ns.owner; + coin = await node.getCoin(hash, index); + assert.bufferEqual(hash, mtx.hash()); + }); + + it('should spend with correct action type and signature: TRANSFER - Alice', async () => { + // Mine blocks to pass the transfer window + await mineBlocks(50); + const mtx = new MTX(); + mtx.addCoin(coin); + // Anyone-can-renew address, just for testing + const address = Address.fromString('rs1qu3nrzrjkd783ftpk7l4hvpa96aazx5dddw66hgs2zuukckcchrqs570axm'); + mtx.addOutput(new Output({ + value: coin.value, + address: address + })); + + let ns = await node.getNameStatus(nameHash); + let flags = 0; + if (ns.weak) + flags |= 1; + + mtx.output(0).covenant.type = rules.types.FINALIZE; + mtx.output(0).covenant.pushHash(nameHash); + mtx.output(0).covenant.pushU32(ns.height); + mtx.output(0).covenant.push(Buffer.from(name, 'ascii')); + mtx.output(0).covenant.pushU8(flags); + mtx.output(0).covenant.pushU32(ns.claimed); + mtx.output(0).covenant.pushU32(ns.renewals); + mtx.output(0).covenant.pushHash(await wdb.getRenewalBlock()); + + await alice.fund(mtx, {coins: [coin]}); + await alice.finalize(mtx, {coins: [coin]}); + + // Sign after all the funding stuff is done + const sig = mtx.signature(0, script, coin.value, privKeycold, common.hashType.ALL); + const witness = new Stack(); + witness.pushData(sig); + witness.pushData(pubKeycold); + witness.pushData(script.encode()); + mtx.inputs[0].witness.fromStack(witness); + + await alice.sign(mtx); + mtx.check(); + await node.sendTX(mtx.toTX()); + await mineBlocks(1); + + // Confirm tx got confirmed + ns = await node.getNameStatus(nameHash); + const {hash, index} = ns.owner; + coin = await node.getCoin(hash, index); + // Confirm Name got transferred to new address + assert.deepStrictEqual(coin.address, address); + assert.bufferEqual(hash, mtx.hash()); + }); +});