argus/contracts/name-registry.cash

181 lines
9.7 KiB
Text
Raw Normal View History

pragma cashscript ^0.13.0;
// NameRegistry v1 — the covenant that holds a name certificate and enforces
// its lifecycle (Decentralized.DNS/DESIGN-covenant-registry.md).
//
// STATUS: chipnet draft. NOT REVIEWED. A flaw here either locks every name
// forever or lets anyone take one; nothing built from this file may hold a
// mainnet name until someone who did not write it has reviewed it.
//
// The certificate (an immutable NFT whose commitment is the name) sits in an
// output locked by this script. Two values change over a name's life — who
// owns it and when it expires — and they are the LAST two constructor
// arguments, which CashScript pushes FIRST. The redeem script therefore begins
//
// 0x14 <ownerPkh:20> 0x04 <expiry:4> <everything else>
//
// and every path that keeps the name re-creates the covenant by replacing
// those 26 bytes and requiring output 0 to be locked to the result. One
// contract, one code hash per price tier; the state is in the address.
//
// Numbers must match Argus/src/lib/registry-lifecycle.js, which is the
// reference: integer arithmetic, multiply before dividing.
//
// What script cannot do, stated once: it can prove a block height has been
// REACHED (tx.time), never that one has NOT. So "the owner may reclaim only
// until the grace period ends" is not enforceable; an owner can renew for as
// long as nobody has taken the name over. The takeover price is safe against
// this: a taker can only claim a height that is already past, and a later
// height means a lower price they are entitled to anyway.
contract NameRegistry(
// ---- fixed for this covenant version and price tier ----
bytes fundLock, // locking bytecode the fund's payments must go to
pubkey stewardPk, // approves moving from this code to a newer version's (migrate)
bytes commitBoxCode, // redeem-script body of the CommitBox contract
int renewalSats, // price of one year, for the owner
int premiumSats, // takeover price at the moment of expiry
int floorSats, // takeover price once the grace period has run out
int graceBlocks, // length of the grace period
int blocksPerYear,
int ownerPercentIdle, // previous owner's share of a takeover during grace
// ---- state: keep these LAST, in this order ----
bytes4 expiry, // block height the paid period ends at
bytes20 ownerPkh
) {
// Keep the name: pay for `years` more. Also the reclaim path — an expired
// name renews exactly the same way, from its old expiry.
function renew(pubkey pk, sig s, int years) {
require(hash160(pk) == ownerPkh);
require(checkSig(s, pk));
require(years >= 1);
int newExpiry = int(expiry) + years * blocksPerYear;
// Stay a block height. At 500,000,000 a locktime is read as a
// timestamp, takeover could never satisfy its height check, and the
// name would be untakeable for ever.
require(newExpiry < 500000000);
bytes newState = 0x14 + ownerPkh + 0x04 + toPaddedBytes(newExpiry, 4);
bytes32 newHash = hash256(newState + this.activeBytecode.split(26)[1]);
require(tx.outputs[0].lockingBytecode == new LockingBytecodeP2SH32(newHash));
require(tx.outputs[0].tokenCategory == tx.inputs[this.activeInputIndex].tokenCategory);
require(tx.outputs[0].nftCommitment == tx.inputs[this.activeInputIndex].nftCommitment);
require(tx.outputs[0].value >= tx.inputs[this.activeInputIndex].value);
require(tx.outputs[1].lockingBytecode == fundLock);
require(tx.outputs[1].value >= renewalSats * years);
require(tx.outputs[1].tokenCategory == 0x);
}
// Hand the name to another key (a sale, a gift), or re-create it for the
// same key — which is how records are updated: the certificate has to move
// for a resolver to accept an UPD. Expiry is untouched.
function transfer(pubkey pk, sig s, bytes20 newOwnerPkh) {
require(hash160(pk) == ownerPkh);
require(checkSig(s, pk));
bytes newState = 0x14 + newOwnerPkh + 0x04 + expiry;
bytes32 newHash = hash256(newState + this.activeBytecode.split(26)[1]);
require(tx.outputs[0].lockingBytecode == new LockingBytecodeP2SH32(newHash));
require(tx.outputs[0].tokenCategory == tx.inputs[this.activeInputIndex].tokenCategory);
require(tx.outputs[0].nftCommitment == tx.inputs[this.activeInputIndex].nftCommitment);
require(tx.outputs[0].value >= tx.inputs[this.activeInputIndex].value);
}
// Take an expired name. No owner signature: anyone may, at a price that
// glides from premiumSats at expiry down to floorSats at the end of the
// grace period and stays there. During grace the previous owner is paid
// ownerPercentIdle of the price; afterwards the fund receives all of it.
//
// Input 1 must be a CommitBox the taker created at least a block earlier,
// committing to this name and to their own key. It hides which name is
// wanted until the takeover itself, so the transaction cannot be copied
// from the mempool by someone who had not already committed.
function takeover(bytes20 newOwnerPkh, bytes salt) {
int expiryHeight = int(expiry);
require(tx.time >= expiryHeight);
int height = tx.locktime;
require(height < 500000000); // a block height, not a timestamp
int idleEnd = expiryHeight + graceBlocks;
int price = floorSats;
int ownerSats = 0;
if (height < idleEnd) {
price = floorSats + (premiumSats - floorSats) * (idleEnd - height) / graceBlocks;
ownerSats = price * ownerPercentIdle / 100;
}
// the commit. The salt has a fixed size: the name and the salt are
// hashed end to end, so with a free-length salt a commit for "pay"
// would also be a commit for "payroll" (the rest of the name passed
// off as the start of the salt).
require(salt.length == 32);
bytes32 commitHash = hash256(tx.inputs[this.activeInputIndex].nftCommitment + salt);
bytes boxRedeem = 0x14 + newOwnerPkh + 0x20 + commitHash + commitBoxCode;
require(tx.inputs[1].lockingBytecode == new LockingBytecodeP2SH32(hash256(boxRedeem)));
// the name, under its new owner, for one fresh year
require(height + blocksPerYear < 500000000);
bytes newState = 0x14 + newOwnerPkh + 0x04 + toPaddedBytes(height + blocksPerYear, 4);
bytes32 newHash = hash256(newState + this.activeBytecode.split(26)[1]);
require(tx.outputs[0].lockingBytecode == new LockingBytecodeP2SH32(newHash));
require(tx.outputs[0].tokenCategory == tx.inputs[this.activeInputIndex].tokenCategory);
require(tx.outputs[0].nftCommitment == tx.inputs[this.activeInputIndex].nftCommitment);
require(tx.outputs[0].value >= tx.inputs[this.activeInputIndex].value);
// the money
require(tx.outputs[1].lockingBytecode == fundLock);
require(tx.outputs[1].value >= price - ownerSats);
require(tx.outputs[1].tokenCategory == 0x);
if (ownerSats > 0) {
require(tx.outputs[2].lockingBytecode == new LockingBytecodeP2PKH(ownerPkh));
require(tx.outputs[2].value >= ownerSats);
require(tx.outputs[2].tokenCategory == 0x);
}
}
// Give the name back: the certificate is destroyed. At most four outputs,
// none of which may carry it — the owner cannot use this path to walk the
// certificate out of the covenant into a plain wallet.
function release(pubkey pk, sig s) {
require(hash160(pk) == ownerPkh);
require(checkSig(s, pk));
bytes category = tx.inputs[this.activeInputIndex].tokenCategory;
require(tx.outputs.length <= 4);
require(tx.outputs[0].tokenCategory != category);
if (tx.outputs.length > 1) { require(tx.outputs[1].tokenCategory != category); }
if (tx.outputs.length > 2) { require(tx.outputs[2].tokenCategory != category); }
if (tx.outputs.length > 3) { require(tx.outputs[3].tokenCategory != category); }
}
// Move to a newer covenant version. The owner chooses to; the steward has
// approved moving from THIS code to the new code, once, for everyone in
// this covenant. Neither can act alone.
//
// The owner and the expiry carry over unchanged, in the same 26 bytes, in
// front of the new code. Nothing here is free for the owner to choose:
// · the state is rebuilt by the contract, so the owner cannot put a
// script of their own in front of the approved code (which would turn
// that code into inert data behind their own signature check), and
// cannot give themselves a later expiry on the way;
// · the approval is over (this code, new code), so a signature made for
// one covenant — another tier, another network's fund — is worthless
// in any other.
// A newer version must therefore keep this state layout.
function migrate(pubkey pk, sig s, datasig approval, bytes newCode) {
require(hash160(pk) == ownerPkh);
require(checkSig(s, pk));
bytes32 fromCode = hash256(this.activeBytecode.split(26)[1]);
require(checkDataSig(approval, hash256(fromCode + hash256(newCode)), stewardPk));
bytes32 newHash = hash256(0x14 + ownerPkh + 0x04 + expiry + newCode);
require(tx.outputs[0].lockingBytecode == new LockingBytecodeP2SH32(newHash));
require(tx.outputs[0].tokenCategory == tx.inputs[this.activeInputIndex].tokenCategory);
require(tx.outputs[0].nftCommitment == tx.inputs[this.activeInputIndex].nftCommitment);
require(tx.outputs[0].value >= tx.inputs[this.activeInputIndex].value);
}
}