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 0x04 // // 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); } }