/*! * namedb.js - name database for bcoin * Copyright (c) 2014-2017, Christopher Jeffrey (MIT License). * https://github.com/bcoin-org/bcoin */ 'use strict'; const assert = require('assert'); const bdb = require('bdb'); const bio = require('bufio'); const blake2b = require('bcrypto/lib/blake2b'); const Trie = require('thc/trie/trie'); const Auction = require('./auction'); const Outpoint = require('../primitives/outpoint'); const consensus = require('../protocol/consensus'); const rules = require('./rules'); const {encoding} = bio; const {types, states} = Auction; /* * Database Layout: * c -> name bucket * t -> trie bucket * a[name-hash] -> auction data * r[name-hash][height][hash][index] -> reveal value (prevout by name hash) * u[hash][index] -> undo record for auction * k[hash][index] -> undo record for renewal height */ const layout = { c: bdb.key('c'), t: bdb.key('t'), a: bdb.key('a', ['hash256']), r: bdb.key('r', ['hash256', 'uint32', 'hash256', 'uint32']), u: bdb.key('u', ['hash256', 'uint32']), k: bdb.key('k', ['hash256', 'uint32']) }; /** * NameDB */ class NameDB { constructor(chaindb) { this.chaindb = chaindb; this.db = chaindb.db; this.network = chaindb.network; this.bucket = this.db.bucket(layout.c.build()); this.trieBucket = this.bucket.child(layout.t.build()); this.trie = new Trie(this.trieBucket); } async open() { await this.trie.open(); } async close() { await this.trie.close(); } async getAuction(nameHash) { assert(Buffer.isBuffer(nameHash)); const raw = await this.bucket.get(layout.a.build(nameHash)); if (!raw) return null; const auction = Auction.fromRaw(raw); auction.nameHash = nameHash; return auction; } async getAuctionByName(name) { return this.getAuction(blake2b.digest(name)); } async pickWinner(auction) { const {nameHash, height} = auction; const iter = this.bucket.iterator({ gte: layout.r.min(nameHash, height), lte: layout.r.max(nameHash, height), values: true }); let best = 0; let winner = null; await iter.each((key, data) => { const value = fromReveal(data); if (value >= best) { const [,, hash, index] = layout.r.parse(key); winner = new Outpoint(hash, index); best = value; } }); if (!winner) throw new Error('Could not find winner.'); return winner; } async getUndo(prevout) { assert(prevout instanceof Outpoint); const {hash, index} = prevout; const raw = await this.bucket.get(layout.u.build(hash, index)); if (!raw) return null; const auction = Auction.fromRaw(raw); auction.nameHash = blake2b.digest(auction.name); return auction; } async getUndoRenewal(prevout) { assert(prevout instanceof Outpoint); const {hash, index} = prevout; const raw = await this.bucket.get(layout.k.build(hash, index)); if (!raw) return null; return fromRenewal(raw); } root() { return this.trie.hash('hex'); } commitTrie(batch) { const b = this.trieBucket.wrap(batch); return this.trie.commit(b); } async prove(root, key) { return this.trie.prove(key); const trie = this.trie.snapshot(root); return trie.prove(key); } async saveView(batch, view) { const b = this.bucket.wrap(batch); for (const auction of view.auctions.values()) await this.saveAuction(b, auction, view); } async saveAuction(b, auction, view) { const {nameHash} = auction; for (const {type, params} of auction.ops) { switch (type) { case types.ADD_AUCTION: { b.put(layout.a.build(nameHash), auction.toRaw()); break; } case types.REMOVE_AUCTION: { b.del(layout.a.build(nameHash)); break; } case types.ADD_REVEAL: { const [height, {hash, index}, value] = params; b.put(layout.r.build(nameHash, height, hash, index), toReveal(value)); break; } case types.REMOVE_REVEAL: { const [height, {hash, index}] = params; b.del(layout.r.build(nameHash, height, hash, index)); break; } case types.COMMIT: { const [data] = params; const hash = blake2b.digest(data); await this.trie.insert(nameHash, hash); break; } case types.UNCOMMIT: { await this.trie.remove(nameHash); break; } case types.ADD_UNDO: { const [{hash, index}, raw] = params; b.put(layout.u.build(hash, index), raw); break; } case types.REMOVE_UNDO: { const [{hash, index}] = params; b.del(layout.u.build(hash, index)); break; } case types.ADD_RENEWAL: { const [{hash, index}, height] = params; b.put(layout.k.build(hash, index), toRenewal(height)); break; } case types.REMOVE_RENEWAL: { const [{hash, index}] = params; b.del(layout.k.build(hash, index)); break; } } } auction.ops.length = 0; } async getDataByName(name, height) { const key = blake2b.digest(name); return this.getData(key, height); } async getData(key, height) { const auction = await this.getAuction(key); if (!auction || auction.owner.isNull()) return null; return this.getDataFor(auction.owner, height); } async readCoin(prevout) { return this.chaindb.readCoin(prevout); } async getDataFor(prevout, height) { if (height == null) height = -1; const entry = await this.readCoin(prevout); assert(entry); // Not provable yet. if (height !== -1) { if (entry.height >= height) return null; } const {output} = entry; const {covenant} = output; return covenant.items[1]; } async verifyRenewal_(covenant, tip) { assert(covenant.items.length === 4); // We require renewals to commit to a block // within the past 6 months, to prove that // the user still owns the key. This prevents // people from presigning thousands years // worth of renewals. The block must be at // least 400 blocks back to prevent the // possibility of a reorg invalidating the // covenant. // Cannot renew yet. if (tip < rules.RENEWAL_MATURITY) return true; const height = covenant.items[3].readUInt32LE(0, true); // Make sure it's a mature block (unlikely to be reorgd). if (height > tip - rules.RENEWAL_MATURITY) return false; // Block committed to must be // no older than a 6 months. if (height < tip - rules.RENEWAL_PERIOD) return false; const entry = await this.chaindb.getEntry(height); if (!entry) return false; // XXX util.revHex // Must commit to last 8 bytes of the block hash. // Presigning a renewal would require 1.1151 exabytes of storage. const tail = covenant.items[4].toString('hex'); if (entry.slice(0, 16) !== tail) return false; return true; } async verifyRenewal(covenant, height) { assert(covenant.items.length === 3); const hash = covenant.items[2].toString('hex'); const entry = await this.chaindb.getEntry(hash); if (!entry) return false; // Must be main chain. if (!await this.chaindb.isMainChain(entry)) return false; // Cannot renew yet. if (height < consensus.COINBASE_MATURITY) return true; // Make sure it's a mature block (unlikely to be reorgd). if (entry.height > height - consensus.COINBASE_MATURITY) return false; // Block committed to must be // no older than a 6 months. if (entry.height < height - rules.RENEWAL_PERIOD) return false; return true; } async connect(tx, view, height) { if (tx.isCoinbase()) return true; const {types} = rules; const network = this.network; const hash = tx.hash('hex'); for (let i = 0; i < tx.inputs.length; i++) { const {prevout} = tx.inputs[i]; const entry = view.getEntry(prevout); const coin = entry.output; const uc = coin.covenant; if (uc.type === types.NONE) continue; if (uc.type > rules.MAX_COVENANT_TYPE) continue; assert(i < tx.outputs.length); const output = tx.outputs[i]; const {covenant} = output; const outpoint = new Outpoint(hash, i); const auction = await view.getAuctionByName(this, uc.items[0]); const state = auction.state(height, network); if (uc.type === types.BID) { if (state > states.REVEAL) return false; // A never revealed bid from a // previous auction is trying to // game us. if (auction.height > entry.height) return false; switch (covenant.type) { // bid -> reveal case types.REVEAL: { auction.bids -= 1; auction.addReveal(outpoint, output.value); auction.save(); break; } default: { assert(false); break; } } continue; } if (uc.type === types.REVEAL) { if (state !== states.CLOSED) return false; // A never redeemed reveal from a // previous auction is trying to // game us. if (auction.height > entry.height) return false; let winner = auction.owner; if (winner.isNull()) winner = await this.pickWinner(auction); switch (covenant.type) { // reveal -> redeem case types.REDEEM: { // Must be the loser in order // to redeem the money now. if (prevout.equals(winner)) return false; auction.removeReveal(prevout); break; } // reveal -> register case types.REGISTER: { // Must be the winner in order // to register the name record. if (!prevout.equals(winner)) return false; auction.removeReveal(prevout); auction.owner = outpoint; auction.renewal = height; auction.commit(covenant.items[1]); auction.save(); break; } default: { assert(false); break; } } continue; } if (uc.type === types.REGISTER) { if (state !== states.CLOSED) return false; if (auction.height > entry.height) return false; // Must be the owner to register. assert(prevout.equals(auction.owner)); switch (covenant.type) { // register -> register case types.REGISTER: { auction.owner = outpoint; auction.commit(covenant.items[1]); // Renewal! if (covenant.items.length === 3) { if (!await this.verifyRenewal(covenant, height)) return false; auction.addRenewal(prevout); auction.renewal = height; } auction.save(); break; } // register -> transfer case types.TRANSFER: { break; } default: { assert(false); break; } } continue; } if (uc.type === types.TRANSFER) { if (state !== states.CLOSED) return false; if (auction.height > entry.height) return false; switch (covenant.type) { // transfer -> register case types.REGISTER: { auction.addUndo(prevout); auction.owner = outpoint; auction.commit(covenant.items[1]); // Renewal! if (covenant.items.length === 3) { if (!await this.verifyRenewal(covenant, height)) return false; auction.renewal = height; } auction.save(); break; } // transfer -> revoke case types.REVOKE: { auction.addUndo(prevout); auction.setNull(); auction.save(); auction.uncommit(); break; } default: { assert(false); break; } } continue; } // Should not be a REVOKE. assert(false); } for (let i = 0; i < tx.outputs.length; i++) { const output = tx.outputs[i]; const {covenant} = output; if (covenant.type === types.BID) { const name = covenant.items[0]; const outpoint = new Outpoint(hash, i); // On mainnet, names are released on a // weekly basis for the first year. if (this.network.type === 'main') { const week = blake2b.digest(name)[0] % 52; const start = week * rules.ROLLOUT_INTERVAL; if (height < start) return false; } const auction = await view.ensureAuction(this, name, height); // If we haven't been renewed in a year, start over. if (height >= auction.renewal + rules.RENEWAL_WINDOW) { auction.addUndo(outpoint); if (!auction.owner.isNull()) auction.uncommit(); auction.owner = new Outpoint(); auction.height = height; auction.renewal = height; auction.bids = 0; } const state = auction.state(height, network); if (state !== states.BIDDING) return false; auction.bids += 1; auction.save(); continue; } } return true; } async disconnect(tx, view, height) { const {types} = rules; const network = this.network; const hash = tx.hash('hex'); for (let i = tx.outputs.length - 1; i >= 0; i--) { const output = tx.outputs[i]; const {covenant} = output; if (covenant.type === types.BID) { const outpoint = new Outpoint(hash, i); const auction = await view.getAuctionByName(this, covenant.items[0]); const state = auction.state(height, network); assert(state === states.BIDDING); auction.bids -= 1; if (auction.bids === 0) { const undo = await this.getUndo(outpoint); if (undo) { auction.removeUndo(prevout); auction.owner = undo.owner; auction.height = undo.height; auction.renewal = undo.renewal; auction.bids = undo.bids; // May have been in a reveal state. if (!auction.owner.isNull()) { const data = await view.getDataFor(this, auction.owner); assert(data); auction.commit(data); } auction.save(); } else { auction.remove(); } } else { auction.save(); } continue; } } for (let i = tx.inputs.length - 1; i >= 0; i--) { const {prevout} = tx.inputs[i]; const coin = view.getOutput(prevout); const uc = coin.covenant; if (uc.type === types.NONE) continue; if (uc.type > rules.MAX_COVENANT_TYPE) continue; assert(i < tx.outputs.length); const output = tx.outputs[i]; const {covenant} = output; const outpoint = new Outpoint(hash, i); const auction = await view.getAuctionByName(this, uc.items[0]); const state = auction.state(height, network); if (uc.type === types.BID) { assert(!auction.isNull()); assert(state <= states.REVEAL); switch (covenant.type) { // bid <- reveal case types.REVEAL: { auction.removeReveal(outpoint); break; } default: { assert(false); break; } } continue; } if (uc.type === types.REVEAL) { assert(!auction.isNull()); switch (covenant.type) { // reveal <- redeem case types.REDEEM: { assert(state === states.CLOSED); assert(!auction.owner.equals(outpoint)); auction.addReveal(prevout, coin.value); break; } // reveal <- register case types.REGISTER: { assert(state === states.CLOSED); assert(auction.owner.equals(outpoint)); auction.addReveal(prevout, coin.value); auction.owner = new Outpoint(); auction.renewal = auction.height; auction.uncommit(); auction.save(); break; } default: { assert(false); break; } } continue; } if (uc.type === types.REGISTER) { assert(!auction.isNull()); assert(state === states.CLOSED); switch (covenant.type) { // register <- register case types.REGISTER: { assert(auction.owner.equals(outpoint)); // Switch back to previous owner and data. auction.owner = prevout; auction.commit(uc.items[1]); // Renewal! if (uc.items.length === 3) { const undo = await this.getUndoRenewal(prevout); assert(undo !== -1); auction.removeRenewal(prevout); auction.renewal = undo; } auction.save(); break; } // register <- transfer case types.TRANSFER: { break; } default: { assert(false); break; } } continue; } if (uc.type === types.TRANSFER) { assert(state === states.CLOSED); switch (covenant.type) { // transfer <- register case types.REGISTER: { const undo = await this.getUndo(prevout); assert(!auction.isNull()); assert(auction.owner.equals(outpoint)); assert(undo); // Switch back to previous owner and data. auction.removeUndo(prevout); auction.owner = undo.owner; auction.renewal = undo.renewal; auction.commit(uc.data[1]); auction.save(); break; } // transfer <- revoke case types.REVOKE: { const undo = await this.getUndo(prevout); assert(auction.isNull()); assert(undo); // Switch back to previous owner and data. auction.removeUndo(prevout); auction.owner = undo.owner; auction.height = undo.height; auction.renewal = undo.renewal; auction.commit(uc.data[1]); auction.save(); break; } default: { assert(false); break; } } continue; } // Should not be a REVOKE. assert(false); } } } /* * Helpers */ function fromRenewal(data) { assert(data.length === 4); return data.readUInt32LE(0, true); } function toRenewal(value) { const data = Buffer.alloc(4); data.writeUInt32LE(value, 0, true); return data; } function fromReveal(data) { assert(data.length === 8); return encoding.readU64(data, 0); } function toReveal(value) { const data = Buffer.alloc(8); encoding.writeU64(data, value, 0); return data; } /* * Expose */ module.exports = NameDB;