UNPKG

hsd

Version:
900 lines (680 loc) 19.7 kB
/*! * namestate.js - name states for hsd * Copyright (c) 2017-2018, Christopher Jeffrey (MIT License). * https://github.com/handshake-org/hsd */ 'use strict'; const assert = require('bsert'); const bio = require('bufio'); const Network = require('../protocol/network'); const Outpoint = require('../primitives/outpoint'); const {ZERO_HASH} = require('../protocol/consensus'); const NameDelta = require('./namedelta'); const {encoding} = bio; /* * Constants */ const states = { OPENING: 0, LOCKED: 1, BIDDING: 2, REVEAL: 3, CLOSED: 4, REVOKED: 5 }; const statesByVal = { [states.OPENING]: 'OPENING', [states.LOCKED]: 'LOCKED', [states.BIDDING]: 'BIDDING', [states.REVEAL]: 'REVEAL', [states.CLOSED]: 'CLOSED', [states.REVOKED]: 'REVOKED' }; const EMPTY = Buffer.alloc(0); /** * NameState * @extends {bio.Struct} */ class NameState extends bio.Struct { constructor() { super(); this.name = EMPTY; this.nameHash = ZERO_HASH; this.height = 0; this.renewal = 0; this.owner = new Outpoint(); this.value = 0; this.highest = 0; this.data = EMPTY; this.transfer = 0; this.revoked = 0; this.claimed = 0; this.renewals = 0; this.registered = false; this.expired = false; this.weak = false; // Not serialized. this._delta = null; } get delta() { if (!this._delta) this._delta = new NameDelta(); return this._delta; } set delta(delta) { this._delta = delta; } inject(ns) { assert(ns instanceof this.constructor); this.name = ns.name; this.nameHash = ns.nameHash; this.height = ns.height; this.renewal = ns.renewal; this.owner = ns.owner; this.value = ns.value; this.highest = ns.highest; this.data = ns.data; this.transfer = ns.transfer; this.revoked = ns.revoked; this.claimed = ns.claimed; this.renewals = ns.renewals; this.registered = ns.registered; this.expired = ns.expired; this.weak = ns.weak; return this; } clear() { this._delta = null; return this; } isNull() { return this.height === 0 && this.renewal === 0 && this.owner.isNull() && this.value === 0 && this.highest === 0 && this.data.length === 0 && this.transfer === 0 && this.revoked === 0 && this.claimed === 0 && this.renewals === 0 && this.registered === false && this.expired === false && this.weak === false; } hasDelta() { return this._delta && !this._delta.isNull(); } state(height, network) { assert((height >>> 0) === height); assert(network && network.names); const { treeInterval, lockupPeriod, biddingPeriod, revealPeriod } = network.names; const openPeriod = treeInterval + 1; if (this.revoked !== 0) return states.REVOKED; if (this.claimed !== 0) { if (height < this.height + lockupPeriod) return states.LOCKED; return states.CLOSED; } if (height < this.height + openPeriod) return states.OPENING; if (height < this.height + openPeriod + biddingPeriod) return states.BIDDING; if (height < this.height + openPeriod + biddingPeriod + revealPeriod) return states.REVEAL; return states.CLOSED; } isOpening(height, network) { return this.state(height, network) === states.OPENING; } isLocked(height, network) { return this.state(height, network) === states.LOCKED; } isBidding(height, network) { return this.state(height, network) === states.BIDDING; } isReveal(height, network) { return this.state(height, network) === states.REVEAL; } isClosed(height, network) { return this.state(height, network) === states.CLOSED; } isRevoked(height, network) { return this.state(height, network) === states.REVOKED; } isRedeemable(height, network) { return this.state(height, network) >= states.CLOSED; } isClaimable(height, network) { assert((height >>> 0) === height); assert(network && network.names); return this.claimed !== 0 && !network.names.noReserved && height < network.names.claimPeriod; } isExpired(height, network) { assert((height >>> 0) === height); assert(network && network.names); if (this.revoked !== 0) { if (height < this.revoked + network.names.auctionMaturity) return false; return true; } // Can only renew once we reach the closed state. if (!this.isClosed(height, network)) return false; // Claimed names can only expire once the claim period is over. if (this.isClaimable(height, network)) return false; // If we haven't been renewed in two years, start over. if (height >= this.renewal + network.names.renewalWindow) return true; // If nobody revealed their bids, start over. if (this.owner.isNull()) return true; return false; } maybeExpire(height, network) { if (this.isExpired(height, network)) { const {data} = this; // Note: we keep the name data even // after expiration (but not revocation). this.reset(height); this.setExpired(true); this.setData(data); return true; } return false; } reset(height) { assert((height >>> 0) === height); this.setHeight(height); this.setRenewal(height); this.setOwner(new Outpoint()); this.setValue(0); this.setHighest(0); this.setData(null); this.setTransfer(0); this.setRevoked(0); this.setClaimed(0); this.setRenewals(0); this.setRegistered(false); this.setExpired(false); this.setWeak(false); return this; } set(name, height) { assert(Buffer.isBuffer(name)); this.name = name; this.reset(height); return this; } setHeight(height) { assert((height >>> 0) === height); if (height === this.height) return this; if (this.delta.height === null) this.delta.height = this.height; this.height = height; return this; } setRenewal(renewal) { assert((renewal >>> 0) === renewal); if (renewal === this.renewal) return this; if (this.delta.renewal === null) this.delta.renewal = this.renewal; this.renewal = renewal; return this; } setOwner(owner) { assert(owner instanceof Outpoint); if (owner.equals(this.owner)) return this; if (this.delta.owner === null) this.delta.owner = this.owner; this.owner = owner; return this; } setValue(value) { assert(Number.isSafeInteger(value) && value >= 0); if (value === this.value) return this; if (this.delta.value === null) this.delta.value = this.value; this.value = value; return this; } setHighest(highest) { assert(Number.isSafeInteger(highest) && highest >= 0); if (highest === this.highest) return this; if (this.delta.highest === null) this.delta.highest = this.highest; this.highest = highest; return this; } setData(data) { if (data === null) data = EMPTY; assert(Buffer.isBuffer(data)); if (this.data.equals(data)) return this; if (this.delta.data === null) this.delta.data = this.data; this.data = data; return this; } setTransfer(transfer) { assert((transfer >>> 0) === transfer); if (transfer === this.transfer) return this; if (this.delta.transfer === null) this.delta.transfer = this.transfer; this.transfer = transfer; return this; } setRevoked(revoked) { assert((revoked >>> 0) === revoked); if (revoked === this.revoked) return this; if (this.delta.revoked === null) this.delta.revoked = this.revoked; this.revoked = revoked; return this; } setClaimed(claimed) { assert((claimed >>> 0) === claimed); if (claimed === this.claimed) return this; if (this.delta.claimed === null) this.delta.claimed = this.claimed; this.claimed = claimed; return this; } setRenewals(renewals) { assert((renewals >>> 0) === renewals); if (renewals === this.renewals) return this; if (this.delta.renewals === null) this.delta.renewals = this.renewals; this.renewals = renewals; return this; } setRegistered(registered) { assert(typeof registered === 'boolean'); if (registered === this.registered) return this; if (this.delta.registered === null) this.delta.registered = this.registered; this.registered = registered; return this; } setExpired(expired) { assert(typeof expired === 'boolean'); if (expired === this.expired) return this; if (this.delta.expired === null) this.delta.expired = this.expired; this.expired = expired; return this; } setWeak(weak) { assert(typeof weak === 'boolean'); if (weak === this.weak) return this; if (this.delta.weak === null) this.delta.weak = this.weak; this.weak = weak; return this; } applyState(delta) { assert(delta instanceof NameDelta); if (delta.height !== null) this.height = delta.height; if (delta.renewal !== null) this.renewal = delta.renewal; if (delta.owner !== null) this.owner = delta.owner; if (delta.value !== null) this.value = delta.value; if (delta.highest !== null) this.highest = delta.highest; if (delta.data !== null) this.data = delta.data; if (delta.transfer !== null) this.transfer = delta.transfer; if (delta.revoked !== null) this.revoked = delta.revoked; if (delta.claimed !== null) this.claimed = delta.claimed; if (delta.renewals !== null) this.renewals = delta.renewals; if (delta.registered !== null) this.registered = delta.registered; if (delta.expired !== null) this.expired = delta.expired; if (delta.weak !== null) this.weak = delta.weak; return this; } getSize() { let size = 0; size += 1; size += this.name.length; size += 2; size += this.data.length; size += 4; size += 4; size += 2; if (!this.owner.isNull()) size += 32 + encoding.sizeVarint(this.owner.index); if (this.value !== 0) size += encoding.sizeVarint(this.value); if (this.highest !== 0) size += encoding.sizeVarint(this.highest); if (this.transfer !== 0) size += 4; if (this.revoked !== 0) size += 4; if (this.claimed !== 0) size += 4; if (this.renewals !== 0) size += encoding.sizeVarint(this.renewals); return size; } getField() { let field = 0; if (!this.owner.isNull()) field |= 1 << 0; if (this.value !== 0) field |= 1 << 1; if (this.highest !== 0) field |= 1 << 2; if (this.transfer !== 0) field |= 1 << 3; if (this.revoked !== 0) field |= 1 << 4; if (this.claimed !== 0) field |= 1 << 5; if (this.renewals !== 0) field |= 1 << 6; if (this.registered) field |= 1 << 7; if (this.expired) field |= 1 << 8; if (this.weak) field |= 1 << 9; return field; } write(bw) { bw.writeU8(this.name.length); bw.writeBytes(this.name); bw.writeU16(this.data.length); bw.writeBytes(this.data); bw.writeU32(this.height); bw.writeU32(this.renewal); bw.writeU16(this.getField()); if (!this.owner.isNull()) { bw.writeHash(this.owner.hash); bw.writeVarint(this.owner.index); } if (this.value !== 0) bw.writeVarint(this.value); if (this.highest !== 0) bw.writeVarint(this.highest); if (this.transfer !== 0) bw.writeU32(this.transfer); if (this.revoked !== 0) bw.writeU32(this.revoked); if (this.claimed !== 0) bw.writeU32(this.claimed); if (this.renewals !== 0) bw.writeVarint(this.renewals); return bw; } read(br) { this.name = br.readBytes(br.readU8()); this.data = br.readBytes(br.readU16()); this.height = br.readU32(); this.renewal = br.readU32(); const field = br.readU16(); if (field & (1 << 0)) { this.owner.hash = br.readHash(); this.owner.index = br.readVarint(); } if (field & (1 << 1)) this.value = br.readVarint(); if (field & (1 << 2)) this.highest = br.readVarint(); if (field & (1 << 3)) this.transfer = br.readU32(); if (field & (1 << 4)) this.revoked = br.readU32(); if (field & (1 << 5)) this.claimed = br.readU32(); if (field & (1 << 6)) this.renewals = br.readVarint(); if (field & (1 << 7)) this.registered = true; if (field & (1 << 8)) this.expired = true; if (field & (1 << 9)) this.weak = true; return this; } getJSON(height, network) { let state = undefined; let stats = undefined; if (height != null) { network = Network.get(network); state = this.state(height, network); state = statesByVal[state]; stats = this.toStats(height, network); } return { name: this.name.toString('binary'), nameHash: this.nameHash.toString('hex'), state: state, height: this.height, renewal: this.renewal, owner: this.owner.toJSON(), value: this.value, highest: this.highest, data: this.data.toString('hex'), transfer: this.transfer, revoked: this.revoked, claimed: this.claimed, renewals: this.renewals, registered: this.registered, expired: this.expired, weak: this.weak, stats: stats }; } fromJSON(json) { assert(json && typeof json === 'object'); assert(typeof json.name === 'string'); assert(json.name.length >= 0 && json.name.length <= 63); assert(typeof json.nameHash === 'string'); assert(json.nameHash.length === 64); assert((json.height >>> 0) === json.height); assert((json.renewal >>> 0) === json.renewal); assert(json.owner && typeof json.owner === 'object'); assert(Number.isSafeInteger(json.value) && json.value >= 0); assert(Number.isSafeInteger(json.highest) && json.highest >= 0); assert(typeof json.data === 'string'); assert((json.data.length & 1) === 0); assert((json.transfer >>> 0) === json.transfer); assert((json.revoked >>> 0) === json.revoked); assert((json.claimed >>> 0) === json.claimed); assert((json.renewals >>> 0) === json.renewals); assert(typeof json.registered === 'boolean'); assert(typeof json.expired === 'boolean'); assert(typeof json.weak === 'boolean'); this.name = Buffer.from(json.name, 'binary'); this.nameHash = Buffer.from(json.nameHash, 'hex'); this.height = json.height; this.renewal = json.renewal; this.owner = Outpoint.fromJSON(json.owner); this.value = json.value; this.highest = json.highest; this.data = Buffer.from(json.data, 'hex'); this.transfer = json.transfer; this.revoked = json.revoked; this.claimed = json.claimed; this.renewals = json.renewals; this.registered = json.registered; this.expired = json.expired; this.weak = json.weak; return this; } toStats(height, network) { assert((height >>> 0) === height); assert(network && network.names); const {blocksPerDay} = network.pow; const blocksPerHour = blocksPerDay / 24; const { treeInterval, lockupPeriod, biddingPeriod, revealPeriod, renewalWindow, auctionMaturity, transferLockup, claimPeriod } = network.names; const openPeriod = treeInterval + 1; const stats = {}; let state = this.state(height, network); // Special case for a state that is not a state: // EXPIRED but not revoked. const EXPIRED = -1; if (this.isExpired(height, network)) { if (this.owner.isNull()) return null; if (state !== states.REVOKED) state = EXPIRED; } switch (state) { case states.OPENING: { const start = this.height; const end = this.height + openPeriod; const blocks = end - height; const hours = blocks / blocksPerHour; stats.openPeriodStart = start; stats.openPeriodEnd = end; stats.blocksUntilBidding = blocks; stats.hoursUntilBidding = Number(hours.toFixed(2)); break; } case states.LOCKED: { const start = this.height; const end = this.height + lockupPeriod; const blocks = end - height; const hours = blocks / blocksPerHour; stats.lockupPeriodStart = start; stats.lockupPeriodEnd = end; stats.blocksUntilClosed = blocks; stats.hoursUntilClosed = Number(hours.toFixed(2)); break; } case states.BIDDING: { const start = this.height + openPeriod; const end = start + biddingPeriod; const blocks = end - height; const hours = blocks / blocksPerHour; stats.bidPeriodStart = start; stats.bidPeriodEnd = end; stats.blocksUntilReveal = blocks; stats.hoursUntilReveal = Number(hours.toFixed(2)); break; } case states.REVEAL: { const start = this.height + openPeriod + biddingPeriod; const end = start + revealPeriod; const blocks = end - height; const hours = blocks / blocksPerHour; stats.revealPeriodStart = start; stats.revealPeriodEnd = end; stats.blocksUntilClose = blocks; stats.hoursUntilClose = Number(hours.toFixed(2)); break; } case states.CLOSED: { const start = this.renewal; const normalEnd = start + renewalWindow; const end = this.claimed ? Math.max(claimPeriod, normalEnd) : normalEnd; const blocks = end - height; const days = blocks / blocksPerDay; stats.renewalPeriodStart = start; stats.renewalPeriodEnd = end; stats.blocksUntilExpire = blocks; assert(stats.blocksUntilExpire >= 0); stats.daysUntilExpire = Number(days.toFixed(2)); // Add these details if name is in mid-transfer if (this.transfer !== 0) { const start = this.transfer; const end = start + transferLockup; const blocks = end - height; const hours = blocks / blocksPerHour; stats.transferLockupStart = start; stats.transferLockupEnd = end; stats.blocksUntilValidFinalize = blocks; stats.hoursUntilValidFinalize = Number(hours.toFixed(2)); } break; } case states.REVOKED: { const start = this.revoked; const end = start + auctionMaturity; const blocks = end - height; const hours = blocks / blocksPerHour; stats.revokePeriodStart = start; stats.revokePeriodEnd = end; stats.blocksUntilReopen = blocks; stats.hoursUntilReopen = Number(hours.toFixed(2)); break; } case EXPIRED: { const expired = this.renewal + renewalWindow; stats.blocksSinceExpired = height - expired; break; } } return stats; } format(height, network) { return this.getJSON(height, network); } } /* * Static */ NameState.states = states; NameState.statesByVal = statesByVal; // Max size: 668 NameState.MAX_SIZE = (0 + 1 + 63 + 2 + 512 + 4 + 4 + 2 + 32 + 9 + 9 + 9 + 4 + 4 + 4 + 9); /* * Expose */ module.exports = NameState;