From b7a50a04e208166e15d34d7110599a097be1f245 Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Mon, 8 Feb 2021 19:56:15 -0500 Subject: [PATCH] namestate: refactor toStats() and check for expiration --- lib/covenants/namestate.js | 195 +++++++++++---------- test/namestate-test.js | 341 +++++++++++++++++++++++++++++++++++++ 2 files changed, 449 insertions(+), 87 deletions(-) create mode 100644 test/namestate-test.js diff --git a/lib/covenants/namestate.js b/lib/covenants/namestate.js index a4318132a1..5fbd7ca18f 100644 --- a/lib/covenants/namestate.js +++ b/lib/covenants/namestate.js @@ -209,7 +209,7 @@ class NameState extends bio.Struct { if (this.isClaimable(height, network)) return false; - // If we haven't been renewed in a year, start over. + // If we haven't been renewed in two years, start over. if (height >= this.renewal + network.names.renewalWindow) return true; @@ -740,96 +740,117 @@ class NameState extends bio.Struct { const stats = {}; - if (this.isOpening(height, network)) { - const start = this.height; - const end = this.height + openPeriod; - const blocks = end - height; - const hours = ((blocks * spacing) / 60 / 60); + let state = this.state(height, network); - stats.openPeriodStart = start; - stats.openPeriodEnd = end; - - stats.blocksUntilBidding = blocks; - stats.hoursUntilBidding = Number(hours.toFixed(2)); - } - - if (this.isLocked(height, network)) { - const start = this.height; - const end = this.height + lockupPeriod; - const blocks = end - height; - const hours = ((blocks * spacing) / 60 / 60); - - stats.lockupPeriodStart = start; - stats.lockupPeriodEnd = end; - - stats.blocksUntilClosed = blocks; - stats.hoursUntilClosed = Number(hours.toFixed(2)); - } - - if (this.isBidding(height, network)) { - const start = this.height + openPeriod; - const end = start + biddingPeriod; - const blocks = end - height; - const hours = ((blocks * spacing) / 60 / 60); - - stats.bidPeriodStart = start; - stats.bidPeriodEnd = end; - - stats.blocksUntilReveal = blocks; - stats.hoursUntilReveal = Number(hours.toFixed(2)); - } - - if (this.isReveal(height, network)) { - const start = this.height + openPeriod + biddingPeriod; - const end = start + revealPeriod; - const blocks = end - height; - const hours = ((blocks * spacing) / 60 / 60); - - stats.revealPeriodStart = start; - stats.revealPeriodEnd = end; - - stats.blocksUntilClose = blocks; - stats.hoursUntilClose = Number(hours.toFixed(2)); - } - - if (this.isClosed(height, network)) { - const start = this.renewal; - const end = start + renewalWindow; - const blocks = end - height; - const days = ((blocks * spacing) / 60 / 60 / 24); - - stats.renewalPeriodStart = start; - stats.renewalPeriodEnd = end; - - stats.blocksUntilExpire = blocks; - stats.daysUntilExpire = Number(days.toFixed(2)); - } - - if (this.isRevoked(height, network)) { - const start = this.revoked; - const end = start + auctionMaturity; - const blocks = end - height; - const hours = ((blocks * spacing) / 60 / 60); - - stats.revokePeriodStart = start; - stats.revokePeriodEnd = end; + // 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; - stats.blocksUntilReopen = blocks; - stats.hoursUntilReopen = Number(hours.toFixed(2)); + if (state !== states.REVOKED) + state = EXPIRED; } - // 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 * spacing) / 60 / 60); - - stats.transferLockupStart = start; - stats.transferLockupEnd = end; - - stats.blocksUntilValidFinalize = blocks; - stats.hoursUntilValidFinalize = Number(hours.toFixed(2)); + switch (state) { + case states.OPENING: { + const start = this.height; + const end = this.height + openPeriod; + const blocks = end - height; + const hours = ((blocks * spacing) / 60 / 60); + + 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 * spacing) / 60 / 60); + + 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 * spacing) / 60 / 60); + + 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 * spacing) / 60 / 60); + + stats.revealPeriodStart = start; + stats.revealPeriodEnd = end; + + stats.blocksUntilClose = blocks; + stats.hoursUntilClose = Number(hours.toFixed(2)); + break; + } + case states.CLOSED: { + const start = this.renewal; + const end = start + renewalWindow; + const blocks = end - height; + const days = ((blocks * spacing) / 60 / 60 / 24); + + stats.renewalPeriodStart = start; + stats.renewalPeriodEnd = end; + + stats.blocksUntilExpire = blocks; + 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 * spacing) / 60 / 60); + + 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 * spacing) / 60 / 60); + + stats.revokePeriodStart = start; + stats.revokePeriodEnd = end; + + stats.blocksUntilReopen = blocks; + stats.hoursUntilReopen = Number(hours.toFixed(2)); + break; + } + case EXPIRED: { + const expired = this.renewal + network.names.renewalWindow; + stats.blocksSinceExpired = height - expired; + break; + } } return stats; diff --git a/test/namestate-test.js b/test/namestate-test.js new file mode 100644 index 0000000000..32d030939d --- /dev/null +++ b/test/namestate-test.js @@ -0,0 +1,341 @@ +/* eslint-env mocha */ +/* eslint prefer-arrow-callback: "off" */ +/* eslint no-unused-vars: "off" */ + +'use strict'; + +const assert = require('bsert'); +const NameState = require('../lib/covenants/namestate'); +const rules = require('../lib/covenants/rules'); +const Network = require('../lib/protocol/network'); + +const network = Network.get('regtest'); + +const { + treeInterval, + biddingPeriod, + revealPeriod, + transferLockup, + renewalWindow, + claimPeriod, + auctionMaturity, + lockupPeriod +} = network.names; + +describe('Namestate', function() { + describe('open auction name', function() { + const name = 'handshake'; + const nameHash = rules.hashName(name); + let height = 0; + + const ns = new NameState(); + ns.nameHash = nameHash; + ns.set(Buffer.from(name, 'ascii'), height); + + it('should be OPENING', () => { + while (height < treeInterval + 1) { + const json = ns.getJSON(height, network); + + assert.strictEqual(json.state, 'OPENING'); + + const stats = Object.keys(json.stats); + assert.deepStrictEqual( + stats, + [ + 'openPeriodStart', + 'openPeriodEnd', + 'blocksUntilBidding', + 'hoursUntilBidding' + ] + ); + height++; + } + }); + + it('should be BIDDING', () => { + while (height < treeInterval + 1 + biddingPeriod) { + const json = ns.getJSON(height, network); + + assert.strictEqual(json.state, 'BIDDING'); + + const stats = Object.keys(json.stats); + assert.deepStrictEqual( + stats, + [ + 'bidPeriodStart', + 'bidPeriodEnd', + 'blocksUntilReveal', + 'hoursUntilReveal' + ] + ); + height++; + } + }); + + it('should be REVEALING', () => { + while (height < treeInterval + 1 + biddingPeriod + revealPeriod) { + const json = ns.getJSON(height, network); + + assert.strictEqual(json.state, 'REVEAL'); + + const stats = Object.keys(json.stats); + assert.deepStrictEqual( + stats, + [ + 'revealPeriodStart', + 'revealPeriodEnd', + 'blocksUntilClose', + 'hoursUntilClose' + ] + ); + height++; + } + }); + + it('should be CLOSED without owner', () => { + const json = ns.getJSON(height, network); + + assert.strictEqual(json.state, 'CLOSED'); + assert(json.stats === null); + }); + + it('should be CLOSED until expiration with owner', () => { + // Fork the timeline; + let heightWithOwner = height; + + // Someone won the name + ns.owner.hash = Buffer.alloc(32, 0x01); + ns.owner.index = 0; + + while (heightWithOwner < renewalWindow) { + const json = ns.getJSON(heightWithOwner, network); + + assert.strictEqual(json.state, 'CLOSED'); + + const stats = Object.keys(json.stats); + assert.deepStrictEqual( + stats, + [ + 'renewalPeriodStart', + 'renewalPeriodEnd', + 'blocksUntilExpire', + 'daysUntilExpire' + ] + ); + heightWithOwner++; + } + + // Expired without renewal + while (heightWithOwner < renewalWindow + 10) { + const json = ns.getJSON(heightWithOwner, network); + + assert.strictEqual(json.state, 'CLOSED'); + + const stats = Object.keys(json.stats); + assert.deepStrictEqual( + stats, + [ + 'blocksSinceExpired' + ] + ); + heightWithOwner++; + } + }); + + it('should be CLOSED with transfer statistics', () => { + // Fork the timeline; + let heightWithTransfer = height; + + // Someone won the name + ns.owner.hash = Buffer.alloc(32, 0x01); + ns.owner.index = 0; + + // Winner confirmed a TRANSFER + ns.transfer = heightWithTransfer; + + while (heightWithTransfer < renewalWindow) { + const json = ns.getJSON(heightWithTransfer, network); + + assert.strictEqual(json.state, 'CLOSED'); + + const stats = Object.keys(json.stats); + assert.deepStrictEqual( + stats, + [ + 'renewalPeriodStart', + 'renewalPeriodEnd', + 'blocksUntilExpire', + 'daysUntilExpire', + 'transferLockupStart', + 'transferLockupEnd', + 'blocksUntilValidFinalize', + 'hoursUntilValidFinalize' + ] + ); + heightWithTransfer++; + } + + // Expired before FINALIZE (which resets everything) + while (heightWithTransfer < renewalWindow + 10) { + const json = ns.getJSON(heightWithTransfer, network); + + assert.strictEqual(json.state, 'CLOSED'); + + const stats = Object.keys(json.stats); + assert.deepStrictEqual( + stats, + [ + 'blocksSinceExpired' + ] + ); + heightWithTransfer++; + } + }); + + it('should be REVOKED', () => { + // Fork the timeline; + let heightWithRevoke = height; + + // Someone won the name + ns.owner.hash = Buffer.alloc(32, 0x01); + ns.owner.index = 0; + + // Winner confirmed a TRANSFER + ns.transfer = heightWithRevoke; + + while (heightWithRevoke < height + 10) { + const json = ns.getJSON(heightWithRevoke, network); + + assert.strictEqual(json.state, 'CLOSED'); + + const stats = Object.keys(json.stats); + assert.deepStrictEqual( + stats, + [ + 'renewalPeriodStart', + 'renewalPeriodEnd', + 'blocksUntilExpire', + 'daysUntilExpire', + 'transferLockupStart', + 'transferLockupEnd', + 'blocksUntilValidFinalize', + 'hoursUntilValidFinalize' + ] + ); + heightWithRevoke++; + } + + // Winner REVOKEd before FINALIZE + ns.transfer = 0; + ns.revoked = heightWithRevoke; + const revokedHeight = heightWithRevoke; + + // Revoked stats remain until expired + while (heightWithRevoke < revokedHeight + renewalWindow) { + const json = ns.getJSON(heightWithRevoke, network); + + assert.strictEqual(json.state, 'REVOKED'); + + const stats = Object.keys(json.stats); + assert.deepStrictEqual( + stats, + [ + 'revokePeriodStart', + 'revokePeriodEnd', + 'blocksUntilReopen', + 'hoursUntilReopen' + ] + ); + heightWithRevoke++; + } + + while (heightWithRevoke < renewalWindow + 10) { + const json = ns.getJSON(heightWithRevoke, network); + + assert.strictEqual(json.state, 'CLOSED'); + + const stats = Object.keys(json.stats); + assert.deepStrictEqual( + stats, + [ + 'blocksSinceExpired' + ] + ); + heightWithRevoke++; + } + }); + }); + + describe('reserved name', function() { + const name = 'handshake'; + const nameHash = rules.hashName(name); + let height = 1; // ns.claimed can not be 0 + + const ns = new NameState(); + ns.nameHash = nameHash; + ns.set(Buffer.from(name, 'ascii'), height); + // Someone claimed the name + ns.owner.hash = Buffer.alloc(32, 0x01); + ns.owner.index = 0; + ns.claimed = height; + + it('should be LOCKED', () => { + while (height - 1 < lockupPeriod) { + const json = ns.getJSON(height, network); + + assert.strictEqual(json.state, 'LOCKED'); + + const stats = Object.keys(json.stats); + assert.deepStrictEqual( + stats, + [ + 'lockupPeriodStart', + 'lockupPeriodEnd', + 'blocksUntilClosed', + 'hoursUntilClosed' + ] + ); + height++; + } + }); + + it('should be CLOSED until claim period ends', () => { + // Fork the timeline; + let heightWithOwner = height; + + while (heightWithOwner < claimPeriod) { + const json = ns.getJSON(heightWithOwner, network); + + assert.strictEqual(json.state, 'CLOSED'); + + const stats = Object.keys(json.stats); + assert.deepStrictEqual( + stats, + [ + 'renewalPeriodStart', + 'renewalPeriodEnd', + 'blocksUntilExpire', + 'daysUntilExpire' + ] + ); + heightWithOwner++; + } + + // Expired without renewal + while (heightWithOwner < claimPeriod + 10) { + const json = ns.getJSON(heightWithOwner, network); + + assert.strictEqual(json.state, 'CLOSED'); + + const stats = Object.keys(json.stats); + assert.deepStrictEqual( + stats, + [ + 'blocksSinceExpired' + ] + ); + heightWithOwner++; + } + }); + }); +});