argus/test/registry-lifecycle.test.mjs

262 lines
11 KiB
JavaScript
Raw Normal View History

// Lifecycle, pricing and the payment split.
//
// These are the rules the covenant will encode, so this file doubles as the
// fixture the CashScript contract must reproduce exactly. Several tests assert
// economic properties rather than arithmetic — no surplus cliff, the owner always
// holds the cheap option, the split never loses a satoshi — because those are the
// ones that quietly stop being true when a constant gets edited.
import test from "node:test";
import assert from "node:assert/strict";
import {
PHASE, phaseAt, expiryFor, idleEndFor,
BLOCKS_PER_YEAR, GRACE_BLOCKS, GRACE_MONTHS,
TIERS, tierForName, tierConfig,
renewalPriceSats, idlePremiumSats, openFloorSats,
takeoverPriceSats, reclaimPriceSats,
SPLIT, splitTakeover,
COMMIT, commitIsMature,
FUND, describeLifecycle,
IDLE_PREMIUM_MULTIPLE, OPEN_FLOOR_MULTIPLE,
} from "../src/lib/registry-lifecycle.js";
const START = 900_000;
const EXPIRY = expiryFor(START, 1);
const IDLE_END = idleEndFor(EXPIRY);
const TIER = 3; // the tier the design's $20 / $1000 / $100 figures describe
// ---------- phases ----------
test("phase boundaries are exact, not approximate", () => {
assert.equal(phaseAt({ height: EXPIRY - 1, expiryHeight: EXPIRY, idleEndHeight: IDLE_END }), PHASE.active);
assert.equal(phaseAt({ height: EXPIRY, expiryHeight: EXPIRY, idleEndHeight: IDLE_END }), PHASE.idle);
assert.equal(phaseAt({ height: IDLE_END - 1, expiryHeight: EXPIRY, idleEndHeight: IDLE_END }), PHASE.idle);
assert.equal(phaseAt({ height: IDLE_END, expiryHeight: EXPIRY, idleEndHeight: IDLE_END }), PHASE.open);
});
test("grace is 6 months, not 12 — the namespace has to actually recycle", () => {
assert.equal(GRACE_MONTHS, 6);
assert.equal(IDLE_END - EXPIRY, GRACE_BLOCKS);
// A 1-year name is unavailable for 18 months after last use, not 24.
assert.equal(IDLE_END - START, BLOCKS_PER_YEAR + GRACE_BLOCKS);
assert.ok(IDLE_END - START < BLOCKS_PER_YEAR * 2);
});
// ---------- tiers ----------
test("tier follows label length and ignores the TLD", () => {
assert.equal(tierForName("ab"), 1);
assert.equal(tierForName("abcd"), 2);
assert.equal(tierForName("abcdefg"), 3);
assert.equal(tierForName("abcdefgh"), 4);
assert.equal(tierForName("sirius.nav"), 3, "the TLD must not shift the tier");
assert.equal(tierForName("sirius"), tierForName("sirius.nav"));
});
test("tier 3 lands on the design's $20 / $1000 / $100 figures", () => {
assert.equal(renewalPriceSats(TIER), 200_000n);
assert.equal(idlePremiumSats(TIER), 10_000_000n); // 50x
assert.equal(openFloorSats(TIER), 1_000_000n); // 5x
assert.equal(IDLE_PREMIUM_MULTIPLE, 50n);
assert.equal(OPEN_FLOOR_MULTIPLE, 5n);
});
test("shorter names cost more, monotonically", () => {
const prices = TIERS.map((t) => t.renewalSats);
for (let i = 1; i < prices.length; i++) {
assert.ok(prices[i] < prices[i - 1], `tier ${i + 1} must be cheaper than tier ${i}`);
}
});
test("multi-year renewal is a straight multiple", () => {
assert.equal(renewalPriceSats(TIER, 3), renewalPriceSats(TIER) * 3n);
});
// ---------- the decay curve ----------
const priceAt = (height) => takeoverPriceSats({ height, expiryHeight: EXPIRY, idleEndHeight: IDLE_END, tier: TIER });
test("an ACTIVE name is not for sale at any price", () => {
assert.equal(priceAt(EXPIRY - 1), null);
assert.equal(priceAt(START), null);
});
test("takeover starts at the premium and ends at the floor", () => {
assert.equal(priceAt(EXPIRY), idlePremiumSats(TIER));
assert.equal(priceAt(IDLE_END), openFloorSats(TIER));
assert.equal(priceAt(IDLE_END + 100_000), openFloorSats(TIER), "the floor holds indefinitely");
});
test("the price only ever falls — never rises, never plateaus mid-glide", () => {
let previous = priceAt(EXPIRY);
for (let h = EXPIRY + 100; h < IDLE_END; h += 500) {
const p = priceAt(h);
assert.ok(p < previous, `price rose or stalled at height ${h}`);
previous = p;
}
});
test("NO SURPLUS CLIFF at the IDLE→OPEN boundary", () => {
// The whole reason for a glide instead of a step. If the price fell sharply at
// one block, every bidder would submit in that block and the winner would be
// whoever paid the biggest miner fee — the outcome the pricing exists to avoid.
const justBefore = priceAt(IDLE_END - 1);
const atBoundary = priceAt(IDLE_END);
const gap = justBefore - atBoundary;
const floor = openFloorSats(TIER);
assert.ok(gap >= 0n);
assert.ok(
gap * 1000n < floor,
`boundary drop of ${gap} sat against a ${floor} sat floor is a cliff, not a glide`,
);
});
test("the glide is roughly linear across the window", () => {
const mid = priceAt(EXPIRY + Number(GRACE_BLOCKS) / 2);
const expected = (idlePremiumSats(TIER) + openFloorSats(TIER)) / 2n;
const drift = mid > expected ? mid - expected : expected - mid;
assert.ok(drift * 100n < expected, "midpoint should sit near the average of the endpoints");
});
test("price is never below the floor at any height", () => {
const floor = openFloorSats(TIER);
for (let h = EXPIRY; h < IDLE_END + 10_000; h += 977) {
assert.ok(priceAt(h) >= floor, `price dipped under the floor at ${h}`);
}
});
// ---------- the owner's advantage ----------
test("the owner's option is tens of times cheaper throughout IDLE", () => {
// This asymmetry — not the length of the grace window — is what protects a
// present owner. Renewing always beats being bought out.
const reclaim = reclaimPriceSats({ tier: TIER });
for (const h of [EXPIRY, EXPIRY + 1000, IDLE_END - 1]) {
assert.ok(priceAt(h) > reclaim * 4n, `takeover at ${h} is not meaningfully dearer than reclaiming`);
}
assert.equal(idlePremiumSats(TIER) / reclaim, 50n, "50x at the start of IDLE");
});
test("reclaim price ignores phase and demand entirely", () => {
// An owner must never be priceable out of a name they already hold.
assert.equal(reclaimPriceSats({ tier: TIER }), renewalPriceSats(TIER));
assert.equal(reclaimPriceSats({ tier: TIER, years: 2 }), renewalPriceSats(TIER, 2));
});
// ---------- the split ----------
test("IDLE takeover splits 30 / 0 / 70", () => {
const s = splitTakeover({ amountSats: 10_000_000n, phase: PHASE.idle });
assert.equal(s.ownerSats, 3_000_000n);
assert.equal(s.minerFeeSats, 0n);
assert.equal(s.fundSats, 7_000_000n);
});
test("OPEN takeover pays the absent owner nothing", () => {
// The 30% is a buy-out while they are still entitled to reclaim. Once OPEN,
// that entitlement has lapsed and so has the justification.
const s = splitTakeover({ amountSats: 1_000_000n, phase: PHASE.open });
assert.equal(s.ownerSats, 0n);
assert.equal(s.fundSats, 1_000_000n);
});
test("the split never loses or conjures a satoshi, at any amount", () => {
// Integer division truncates; the fund takes the remainder so the parts always
// reconcile. A covenant that is one satoshi off simply will not unlock.
for (const amount of [1n, 3n, 7n, 99n, 101n, 999_999n, 10_000_001n, 123_456_789n]) {
for (const phase of [PHASE.idle, PHASE.open]) {
const s = splitTakeover({ amountSats: amount, phase });
assert.equal(
s.ownerSats + s.minerFeeSats + s.fundSats, amount,
`${amount} sat in ${phase} did not reconcile`,
);
assert.ok(s.ownerSats >= 0n && s.fundSats >= 0n && s.minerFeeSats >= 0n);
}
}
});
test("no miner share by default", () => {
assert.equal(SPLIT.minerPercent, 0n, "15% is a discount for the one party that also orders blocks");
});
test("restoring the miner share still reconciles exactly", () => {
// Proving the 30/15/55 variant is a one-value change, not a rewrite.
const original = SPLIT.minerPercent;
try {
SPLIT.minerPercent = 15n;
const s = splitTakeover({ amountSats: 10_000_000n, phase: PHASE.idle });
assert.equal(s.ownerSats, 3_000_000n);
assert.equal(s.minerFeeSats, 1_500_000n);
assert.equal(s.fundSats, 5_500_000n);
assert.equal(s.ownerSats + s.minerFeeSats + s.fundSats, 10_000_000n);
} finally {
SPLIT.minerPercent = original;
}
});
test("selling an ACTIVE name is refused outright", () => {
assert.throws(() => splitTakeover({ amountSats: 1000n, phase: PHASE.active }), /not for sale/);
});
test("a zero or negative takeover is refused", () => {
assert.throws(() => splitTakeover({ amountSats: 0n, phase: PHASE.idle }), /must be positive/);
});
// ---------- commit–reveal ----------
test("commit maturity window is one confirmation to one week", () => {
assert.equal(COMMIT.minConfirmations, 1);
assert.equal(COMMIT.maxConfirmations, 1008);
assert.equal(COMMIT.saltBytes, 16);
assert.equal(commitIsMature(1000, 1000), false, "same block is not a confirmation");
assert.equal(commitIsMature(1000, 1001), true, "one confirmation is enough to fix ordering");
assert.equal(commitIsMature(1000, 2008), true);
assert.equal(commitIsMature(1000, 2009), false, "stale commits must not be hoardable");
});
// ---------- fund ----------
test("the fund is unset until deliberately configured", () => {
// A covenant bakes this in permanently. Shipping a placeholder would send
// every takeover to whatever address happened to be in the file.
assert.equal(FUND.address, null);
assert.equal(FUND.multisig, "2-of-3");
assert.equal(FUND.voluntary, true, "the answer to the IFP comparison rests on this");
});
// ---------- the UI summary ----------
test("describeLifecycle gives a UI everything it needs to warn in time", () => {
const active = describeLifecycle({ height: EXPIRY - 5000, expiryHeight: EXPIRY, idleEndHeight: IDLE_END, tier: TIER });
assert.equal(active.phase, PHASE.active);
assert.equal(active.blocksUntilExpiry, 5000);
assert.equal(active.takeoverPriceSats, null);
assert.equal(active.ownerCanReclaim, true);
const idle = describeLifecycle({ height: EXPIRY + 10, expiryHeight: EXPIRY, idleEndHeight: IDLE_END, tier: TIER });
assert.equal(idle.phase, PHASE.idle);
assert.equal(idle.blocksUntilExpiry, 0);
assert.ok(idle.blocksUntilOpen > 0);
assert.ok(idle.ownerAdvantage >= 40, `owner advantage of ${idle.ownerAdvantage}x is too thin`);
const open = describeLifecycle({ height: IDLE_END + 1, expiryHeight: EXPIRY, idleEndHeight: IDLE_END, tier: TIER });
assert.equal(open.phase, PHASE.open);
assert.equal(open.ownerCanReclaim, false);
assert.equal(open.takeoverPriceSats, openFloorSats(TIER));
});
test("every tier survives a full lifecycle without a cliff or a rise", () => {
for (const { tier } of TIERS) {
const floor = openFloorSats(tier);
let previous = takeoverPriceSats({ height: EXPIRY, expiryHeight: EXPIRY, idleEndHeight: IDLE_END, tier });
assert.equal(previous, idlePremiumSats(tier));
for (let h = EXPIRY + 1000; h <= IDLE_END; h += 2000) {
const p = takeoverPriceSats({ height: h, expiryHeight: EXPIRY, idleEndHeight: IDLE_END, tier });
assert.ok(p <= previous, `tier ${tier} price rose at ${h}`);
assert.ok(p >= floor, `tier ${tier} dipped below floor at ${h}`);
previous = p;
}
}
});