diff --git a/lib/covenants/db.js b/lib/covenants/db.js index d48bc319..de24988e 100644 --- a/lib/covenants/db.js +++ b/lib/covenants/db.js @@ -211,7 +211,6 @@ class NameDB { const {prevout} = input; const coin = view.getOutput(prevout); - assert(coin); const uc = coin.covenant; const index = input.link; @@ -219,10 +218,12 @@ class NameDB { const {covenant} = output; if (uc.type === types.BID) { + assert(covenant.type === types.REVEAL); + const auction = await view.getAuctionFor(this, prevout); assert(auction); - if (auction.state(height, network) !== states.REVEAL) + if (auction.state(height, network) > states.REVEAL) return false; auction.removeBid(prevout.hash, prevout.index); @@ -233,6 +234,10 @@ class NameDB { } if (uc.type === types.REVEAL) { + assert(covenant.type === types.REDEEM + || covenant.type === types.UPDATE + || covenant.type === types.RELEASE); + const auction = await view.getAuctionFor(this, prevout); assert(auction); @@ -244,7 +249,7 @@ class NameDB { if (owner.isNull()) owner = await this.pickWinner(auction.nameHash); - if (covenant.type !== types.UPDATE) { + if (covenant.type === types.REDEEM) { // Must be the loser in order // to redeem the money now. if (prevout.equals(owner)) @@ -253,23 +258,29 @@ class NameDB { continue; } - assert(covenant.type === types.UPDATE); - // Must be the winner in order // to update the name record. if (!prevout.equals(owner)) return false; auction.removeReveal(prevout.hash, prevout.index); - auction.setOwner(hash, index); - auction.setData(covenant.items[1]); - auction.save(); - auction.queue(); + + if (covenant.type === types.UPDATE) { + auction.setOwner(hash, index); + auction.setData(covenant.items[1]); + auction.save(); + auction.queue(); + } else { + auction.remove(); + } continue; } if (uc.type === types.UPDATE) { + assert(covenant.type === types.UPDATE + || covenant.type === types.RELEASE); + const auction = await view.getAuctionFor(this, prevout); assert(auction); @@ -280,10 +291,15 @@ class NameDB { if (!prevout.equals(auction.owner)) return false; - auction.setOwner(hash, index); - auction.setData(covenant.items[1]); - auction.save(); - auction.queue(); + if (covenant.type === types.UPDATE) { + auction.setOwner(hash, index); + auction.setData(covenant.items[1]); + auction.save(); + auction.queue(); + } else { + auction.remove(); + auction.unqueue(); + } continue; } @@ -299,6 +315,9 @@ class NameDB { const auction = await view.ensureAuction(this, name, nameHash, height); + if (!auction.owner.isNull()) + return false; + if (auction.state(height, network) !== states.BIDDING) return false; @@ -365,9 +384,11 @@ class NameDB { const {covenant} = output; if (uc.type === types.BID) { + assert(covenant.type === types.REVEAL); + const auction = await view.getAuctionFor(this, prevout); assert(auction); - assert(auction.state(height, network) === states.REVEAL); + assert(auction.state(height, network) <= states.REVEAL); auction.removeReveal(hash, index); auction.save(); @@ -376,18 +397,26 @@ class NameDB { } if (uc.type === types.REVEAL) { + assert(covenant.type === types.REDEEM + || covenant.type === types.UPDATE + || covenant.type === types.RELEASE); + + // XXX Figure out what to do here: + // Add a `released` property to auction object! + assert(covenant.type !== types.RELEASE); + const auction = await view.getAuctionFor(this, prevout); assert(auction); assert(auction.state(height, network) === states.CLOSED); - if (covenant.type !== types.UPDATE) { + if (covenant.type === types.REDEEM) { assert(!prevout.equals(auction.owner)); auction.addReveal(prevout.hash, prevout.index, coin.value); auction.save(); continue; } - assert(!prevout.equals(auction.owner)); + assert(prevout.equals(auction.owner)); // Switch back to previous owner and data. auction.addReveal(prevout.hash, prevout.index, coin.value); @@ -400,7 +429,12 @@ class NameDB { } if (uc.type === types.UPDATE) { - assert(covenant.type === types.UPDATE); + assert(covenant.type === types.UPDATE + || covenant.type === types.RELEASE); + + // XXX Figure out what to do here: + // Add a `released` property to auction object! + assert(covenant.type !== types.RELEASE); const auction = await view.getAuctionFor(this, prevout); assert(auction); diff --git a/lib/covenants/rules.js b/lib/covenants/rules.js index 93fb4944..e77f1a91 100644 --- a/lib/covenants/rules.js +++ b/lib/covenants/rules.js @@ -4,13 +4,17 @@ const assert = require('assert'); const bio = require('bufio'); const blake2b = require('bcrypto/lib/blake2b'); +// TODO: +// locktime for transfer +// no duplicate names +// early reveals + const types = { NONE: 0, BID: 1, REVEAL: 2, UPDATE: 3, REDEEM: 4, - // locktime for transfer RELEASE: 5 }; @@ -19,7 +23,7 @@ exports.types = types; exports.MAX_NAME_SIZE = 255; exports.MAX_RECORD_SIZE = 512; exports.MAX_COVENANT_SIZE = 1 + exports.MAX_RECORD_SIZE; -exports.MAX_COVENANT_TYPE = types.UPDATE; +exports.MAX_COVENANT_TYPE = types.RELEASE; exports.BIDDING_PERIOD = 1; exports.REVEAL_PERIOD = 1; @@ -123,6 +127,9 @@ exports.hasSaneCovenants = function hasSaneCovenants(tx) { for (const {covenant} of tx.outputs) { if (covenant.type !== types.NONE) return false; + + if (covenant.items.length !== 0) + return false; } return true; @@ -148,14 +155,19 @@ exports.hasSaneCovenants = function hasSaneCovenants(tx) { const {covenant} = tx.outputs[i]; switch (covenant.type) { - case types.NONE: + case types.NONE: { // No inputs can be linked. if (links.has(i)) return false; + // Just a regular payment. // Can come from a payment or a reveal (loser). + if (covenant.items.length !== 0) + return false; + break; - case types.BID: + } + case types.BID: { // No inputs can be linked. if (links.has(i)) return false; @@ -173,7 +185,8 @@ exports.hasSaneCovenants = function hasSaneCovenants(tx) { return false; break; - case types.REVEAL: + } + case types.REVEAL: { // Has to come from a BID. if (!links.has(i)) return false; @@ -191,7 +204,8 @@ exports.hasSaneCovenants = function hasSaneCovenants(tx) { return false; break; - case types.UPDATE: + } + case types.UPDATE: { // Has to come from an UPDATE or REVEAL. if (!links.has(i)) return false; @@ -209,7 +223,8 @@ exports.hasSaneCovenants = function hasSaneCovenants(tx) { return false; break; - case types.REDEEM: + } + case types.REDEEM: { // Has to come from a REVEAL. if (!links.has(i)) return false; @@ -223,7 +238,8 @@ exports.hasSaneCovenants = function hasSaneCovenants(tx) { return false; break; - case types.RELEASE: + } + case types.RELEASE: { // Has to come from an UPDATE or REVEAL. if (!links.has(i)) return false; @@ -237,10 +253,12 @@ exports.hasSaneCovenants = function hasSaneCovenants(tx) { return false; break; - default: + } + default: { // Unknown covenant. // Don't enforce anything. break; + } } } @@ -259,27 +277,27 @@ exports.verifyCovenants = function verifyCovenants(tx, view) { const uc = coin.covenant; - // XXX More verification here? - if (input.link === 0xffffffff) - continue; - - assert(input.link < tx.outputs.length); - - // Output the covenant is linked to. - const output = tx.outputs[input.link]; - const {covenant} = output; + if (input.link !== 0xffffffff) + assert(input.link < tx.outputs.length); switch (uc.type) { - case types.NONE: { - // Payment has to go to either - // another payment, or a bid. - if (covenant.type !== types.NONE - && covenant.type !== types.BID) { + case types.NONE: + case types.REDEEM: { + // Cannot be linked. + if (input.link !== 0xffffffff) return false; - } + break; } case types.BID: { + // Must be be linked. + if (input.link === 0xffffffff) + return false; + + // Output the covenant is linked to. + const output = tx.outputs[input.link]; + const {covenant} = output; + // Bid has to go to a reveal. if (covenant.type !== types.REVEAL) return false; @@ -304,58 +322,76 @@ exports.verifyCovenants = function verifyCovenants(tx, view) { break; } case types.REVEAL: { + // Must be be linked. + if (input.link === 0xffffffff) + return false; + + // Output the covenant is linked to. + const output = tx.outputs[input.link]; + const {covenant} = output; + // Reveal has to go to an update, or // a redeem (in the case of the loser). - if (covenant.type !== types.UPDATE - && covenant.type !== types.REDEEM) { - return false; - } + switch (covenant.type) { + case types.UPDATE: + case types.RELEASE: { + // Names must match. + if (!covenant.items[0].equals(uc.items[0])) + return false; - // Money is now locked up forever. - if (covenant.type === types.UPDATE) { - // Names must match. - if (!covenant.items[0].equals(uc.items[0])) - return false; + // Money is now locked up forever. + if (output.value !== coin.value) + return false; - if (output.value !== coin.value) - return false; - } + break; + } + case types.REDEEM: { + // Names must match. + if (!covenant.items[0].equals(uc.items[0])) + return false; - if (covenant.type === types.REDEEM) { - // Names must match. - if (!covenant.items[0].equals(uc.items[0])) + break; + } + default: { return false; + } } break; } case types.UPDATE: { - // Names must match. - if (!covenant.items[0].equals(uc.items[0])) + // Must be be linked. + if (input.link === 0xffffffff) return false; - // Money is now locked up forever. - if (output.value !== coin.value) - return false; + // Output the covenant is linked to. + const output = tx.outputs[input.link]; + const {covenant} = output; - if (covenant.type !== types.UPDATE) - return false; + // Can only send to another update or release. + switch (covenant.type) { + case types.UPDATE: + case types.RELEASE: { + // Names must match. + if (!covenant.items[0].equals(uc.items[0])) + return false; - break; - } - case types.REDEEM: { - // Can go anywhere. - if (covenant.items.length !== 0) - return false; + // Money is now locked up forever. + if (output.value !== coin.value) + return false; + + break; + } + default: { + return false; + } + } break; } case types.RELEASE: { - // Can go anywhere. - if (covenant.items.length !== 0) - return false; - - break; + // Releases are perma-burned. + return false; } default: { // Unknown covenant. diff --git a/lib/primitives/mtx.js b/lib/primitives/mtx.js index 5acf2773..21d22e4d 100644 --- a/lib/primitives/mtx.js +++ b/lib/primitives/mtx.js @@ -1125,38 +1125,8 @@ class MTX extends TX { for (const coin of select.chosen) this.addCoin(coin); - // XXX - const map = new Map(); - - for (let i = 0; i < this.outputs.length; i++) { - const {covenant} = this.outputs[i]; - - if (covenant.items.length < 1) - continue; - - const name = covenant.string(0); - map.set(name, i); - } - - for (let i = 0; i < select.chosen.length; i++) { - if (map.size === 0) - break; - - const {covenant} = select.chosen[i]; - - if (covenant.items.length < 1) - continue; - - const name = covenant.string(0); - const link = map.get(name); - - if (link == null) - continue; - - map.delete(name); - - this.inputs[i].link = link; - } + if (select.prevout) + this.inputs[0].link = select.link; // Attempt to subtract fee. if (select.subtractFee) { @@ -1411,8 +1381,8 @@ class CoinSelector { this.maxFee = -1; this.round = false; this.changeAddress = null; - // XXX - this.map = new Map(); + this.prevout = null; + this.link = -1; // Needed for size estimation. this.estimate = null; @@ -1508,6 +1478,12 @@ class CoinSelector { this.estimate = options.estimate; } + if (options.covenant) { + assert(typeof options.covenant === 'object'); + this.prevout = options.covenant.prevout; + this.link = options.covenant.link; + } + return this; } @@ -1524,17 +1500,6 @@ class CoinSelector { this.change = 0; this.fee = CoinSelector.MIN_FEE; this.tx.inputs.length = 0; - this.map.clear(); - - // XXX - for (let i = 0; i < this.tx.outputs.length; i++) { - const {covenant} = this.tx.outputs[i]; - - if (covenant.items.length < 1) - continue; - - this.map.set(covenant.string(0), i); - } switch (this.selection) { case 'all': @@ -1624,6 +1589,22 @@ class CoinSelector { return Math.min(fee, CoinSelector.MAX_FEE); } + findCoin(prevout) { + for (let i = 0; i < this.coins.length; i++) { + const {hash, index} = this.coins[i]; + + if (index !== prevout.index) + continue; + + if (hash !== prevout.hash) + continue; + + return i; + } + + return -1; + } + /** * Fund the transaction with more * coins if the `output value + fee` @@ -1631,26 +1612,11 @@ class CoinSelector { */ fund() { - // XXX - for (let i = 0; i < this.coins.length; i++) { - if (this.map.size === 0) - break; - - const coin = this.coins[i]; - const {covenant} = coin; - - if (covenant.items.length < 1) - continue; - - const name = covenant.string(0); - - if (!this.map.has(name)) - continue; - - this.map.delete(name); - this.coins.splice(i, 1); - i -= 1; - + if (this.prevout && this.chosen.length === 0) { + const index = this.findCoin(this.prevout); + assert(index !== -1, 'Coin not found.'); + const coin = this.coins[index]; + this.coins.splice(index, 1); this.tx.addCoin(coin); this.chosen.push(coin); } diff --git a/test/util/memwallet.js b/test/util/memwallet.js index a7b8866c..b9bf45df 100644 --- a/test/util/memwallet.js +++ b/test/util/memwallet.js @@ -340,6 +340,29 @@ class MemWallet { this.auctions.set(name, [new Outpoint(hash, i), 3]); + break; + } + case 4: { + if (!path) + break; + + const name = covenant.string(0); + + // We lost. + this.auctions.delete(name); + this.bids.delete(name); + this.values.delete(name); + + break; + } + case 5: { + const name = covenant.string(0); + + // Someone released it. + this.auctions.delete(name); + this.bids.delete(name); + this.values.delete(name); + break; } } @@ -388,6 +411,75 @@ class MemWallet { const op = input.prevout.toKey(); const coin = this.getUndo(op); + switch (covenant.type) { + case 1: { + if (!coin) + break; + + const name = covenant.string(0); + + this.auctions.delete(name); + + break; + } + case 2: { + const name = covenant.string(0); + const nonce = covenant.items[1]; + + if (!this.auctions.has(name)) + break; + + if (!this.bids.has(name)) + break; + + const key = Outpoint.toKey(hash, i); + + const bids = this.bids.get(name); + + bids.delete(key); + + if (bids.size === 0) + this.bids.delete(name); + + if (!coin) + break; + + this.values.delete(name); + this.auctions.set(name, [new Outpoint(hash, i), 1]); + + break; + } + case 3: { + if (!coin) + break; + + const name = covenant.string(0); + + this.auctions.set(name, [new Outpoint(hash, i), 2]); + + break; + } + case 4: { + if (!coin) + break; + + const name = covenant.string(0); + + // We lost. + this.auctions.set(name, [new Outpoint(hash, i), 2]); + + break; + } + case 5: { + const name = covenant.string(0); + + // Someone released it. + this.auctions.set(name, [new Outpoint(hash, i), 3]); + + break; + } + } + if (!coin) continue; @@ -479,7 +571,10 @@ class MemWallet { const mtx = new MTX(); mtx.outputs.push(output); - return this._create(mtx, options, prevout); + return this._create(mtx, options, { + prevout, + link: 0 + }); } async registerName(name, data, options) { @@ -511,7 +606,10 @@ class MemWallet { const mtx = new MTX(); mtx.outputs.push(output); - return this._create(mtx, options, prevout); + return this._create(mtx, options, { + prevout, + link: 0 + }); } async redeemName(name, options) { @@ -531,7 +629,10 @@ class MemWallet { const mtx = new MTX(); mtx.outputs.push(output); - return this._create(mtx, options); + return this._create(mtx, options, { + prevout, + link: 0 + }); } isWinner(name) { @@ -556,7 +657,7 @@ class MemWallet { return this.coins.has(winner); } - fund(mtx, options) { + fund(mtx, options, covenant) { const coins = this.getCoins(); if (!options) @@ -571,7 +672,8 @@ class MemWallet { changeAddress: this.getChange(), height: -1, rate: options.rate, - maxFee: options.maxFee + maxFee: options.maxFee, + covenant }); } @@ -586,8 +688,8 @@ class MemWallet { mtx.sign(keys); } - async _create(mtx, options) { - await this.fund(mtx, options); + async _create(mtx, options, covenant) { + await this.fund(mtx, options, covenant); assert(mtx.getFee() <= MTX.Selector.MAX_FEE, 'TX exceeds MAX_FEE.');