792 lines
19 KiB
JavaScript
792 lines
19 KiB
JavaScript
/*!
|
|
* 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;
|