diff --git a/contracts/delivery-escrow.cash b/contracts/delivery-escrow.cash index 2cb74b3..368bade 100644 --- a/contracts/delivery-escrow.cash +++ b/contracts/delivery-escrow.cash @@ -23,9 +23,19 @@ pragma cashscript ^0.13.0; // the name, no fungible amount, locked to the buyer's lock // 1 the registration payload, byte for byte as agreed (OP_RETURN) // 2 the discovery beacon, at least its dust -// … and NO other output may carry a token. Without that, the deliverer could -// mint a second, minting-capable token of the same category alongside the -// certificate and later use it to update the buyer's name. +// 3 optional: the deliverer's change — no token, and not a data output +// and nothing else. Two things are being kept out: +// · a second token of the certificate's category, which its holder could +// later use to update the buyer's name; +// · a second data output. A transaction is read as carrying ONE protocol +// message; a later OP_RETURN would replace the agreed registration in +// every resolver, and the deliverer would be paid for a certificate +// that registers nothing. +// +// It cannot deliver under a TLD that requires its owner's co-signature: that +// rule wants the TLD's certificate in an output of the registration, and no +// extra token is allowed here. The library refuses to build an escrow for +// such a name (escrow-covenant.js assertTldAllowsEscrow). // // The escrow must be input 1 (input 0 is the genesis outpoint). That makes it // one escrow per transaction: two payments for the same name could otherwise @@ -63,15 +73,17 @@ contract DeliveryEscrow( require(tx.outputs[2].value >= beaconDust); require(tx.outputs[2].tokenCategory == 0x); - require(tx.outputs.length <= 6); - if (tx.outputs.length > 3) { require(tx.outputs[3].tokenCategory == 0x); } - if (tx.outputs.length > 4) { require(tx.outputs[4].tokenCategory == 0x); } - if (tx.outputs.length > 5) { require(tx.outputs[5].tokenCategory == 0x); } + require(tx.outputs.length <= 4); + if (tx.outputs.length > 3) { + require(tx.outputs[3].tokenCategory == 0x); + require(tx.outputs[3].lockingBytecode.split(1)[0] != 0x6a); + } } function refund(pubkey pk, sig s) { require(hash160(pk) == buyerPkh); require(checkSig(s, pk)); + require(within(refundAfter, 1, 65536)); // a 16-bit block count, see subscription.cash require(this.age >= refundAfter); } } diff --git a/contracts/delivery-escrow.json b/contracts/delivery-escrow.json index c375ab1..3ea34ef 100644 --- a/contracts/delivery-escrow.json +++ b/contracts/delivery-escrow.json @@ -19,32 +19,32 @@ ] } ], - "bytecode": "OP_7 OP_PICK OP_0 OP_NUMEQUAL OP_IF OP_INPUTINDEX OP_1 OP_NUMEQUALVERIFY OP_0 OP_OUTPOINTINDEX OP_0 OP_NUMEQUALVERIFY OP_0 OP_OUTPOINTTXHASH OP_0 OP_OUTPUTBYTECODE OP_3 OP_ROLL OP_EQUALVERIFY OP_0 OP_OUTPUTTOKENCATEGORY OP_EQUALVERIFY OP_0 OP_OUTPUTTOKENCOMMITMENT OP_ROT OP_EQUALVERIFY OP_0 OP_OUTPUTTOKENAMOUNT OP_0 OP_NUMEQUALVERIFY OP_1 OP_OUTPUTBYTECODE OP_ROT OP_EQUALVERIFY OP_1 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_2 OP_OUTPUTBYTECODE OP_ROT OP_EQUALVERIFY OP_2 OP_OUTPUTVALUE OP_ROT OP_GREATERTHANOREQUAL OP_VERIFY OP_2 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_TXOUTPUTCOUNT OP_6 OP_LESSTHANOREQUAL OP_VERIFY OP_TXOUTPUTCOUNT OP_3 OP_GREATERTHAN OP_IF OP_3 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_ENDIF OP_TXOUTPUTCOUNT OP_4 OP_GREATERTHAN OP_IF OP_4 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_ENDIF OP_TXOUTPUTCOUNT OP_5 OP_GREATERTHAN OP_IF OP_5 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_ENDIF OP_2DROP OP_DROP OP_1 OP_ELSE OP_7 OP_ROLL OP_1 OP_NUMEQUALVERIFY OP_7 OP_PICK OP_HASH160 OP_EQUALVERIFY OP_7 OP_ROLL OP_7 OP_ROLL OP_CHECKSIGVERIFY OP_5 OP_ROLL OP_CHECKSEQUENCEVERIFY OP_2DROP OP_2DROP OP_2DROP OP_1 OP_ENDIF", - "source": "pragma cashscript ^0.13.0;\n\n// DeliveryEscrow — payment for a name that can only be collected by the\n// transaction that registers it to the buyer.\n// (Decentralized.DNS/DESIGN-trustless-purchase.md §4.)\n//\n// STATUS: chipnet draft. NOT REVIEWED.\n//\n// A buyer who cannot build a registration themselves (pay-by-QR, any wallet)\n// pays this script instead of paying a registrar. The money then has exactly\n// two ways out:\n//\n// deliver() anyone may spend it — in a transaction that mints the agreed\n// certificate to the buyer and publishes the agreed registration.\n// \"Took the money and did not register\" is not a risk the buyer\n// has to weigh; it is a transaction that cannot be built.\n// refund() the buyer takes it back with their signature once it is\n// `refundAfter` blocks old.\n//\n// What deliver() pins down, output by output:\n// 0 the certificate — a NEW token category created by this transaction\n// (its id is input 0's outpoint), an NFT with no capability, commitment =\n// the name, no fungible amount, locked to the buyer's lock\n// 1 the registration payload, byte for byte as agreed (OP_RETURN)\n// 2 the discovery beacon, at least its dust\n// … and NO other output may carry a token. Without that, the deliverer could\n// mint a second, minting-capable token of the same category alongside the\n// certificate and later use it to update the buyer's name.\n//\n// The escrow must be input 1 (input 0 is the genesis outpoint). That makes it\n// one escrow per transaction: two payments for the same name could otherwise\n// both be collected against a single certificate.\n//\n// WHAT IT CANNOT PROMISE. Script sees this transaction, not the registry. If\n// the name was registered by someone else between payment and delivery, a\n// deliverer can still mint a certificate that satisfies every line here and\n// that no resolver will honour (\"first valid registration wins\" is a resolver\n// rule). The window is the buyer's to keep short: check the name is free, pay,\n// and refund if delivery does not come.\ncontract DeliveryEscrow(\n bytes20 buyerPkh,\n bytes certLock, // where the certificate must go (the buyer's token address, or their covenant)\n bytes nameCommitment, // the on-chain name, as the certificate's commitment\n bytes payloadLock, // the full OP_RETURN locking bytecode of the agreed REG payload\n bytes beaconLock,\n int beaconDust,\n int refundAfter\n) {\n function deliver() {\n require(this.activeInputIndex == 1);\n require(tx.inputs[0].outpointIndex == 0);\n bytes category = tx.inputs[0].outpointTransactionHash;\n\n require(tx.outputs[0].lockingBytecode == certLock);\n require(tx.outputs[0].tokenCategory == category);\n require(tx.outputs[0].nftCommitment == nameCommitment);\n require(tx.outputs[0].tokenAmount == 0);\n\n require(tx.outputs[1].lockingBytecode == payloadLock);\n require(tx.outputs[1].tokenCategory == 0x);\n\n require(tx.outputs[2].lockingBytecode == beaconLock);\n require(tx.outputs[2].value >= beaconDust);\n require(tx.outputs[2].tokenCategory == 0x);\n\n require(tx.outputs.length <= 6);\n if (tx.outputs.length > 3) { require(tx.outputs[3].tokenCategory == 0x); }\n if (tx.outputs.length > 4) { require(tx.outputs[4].tokenCategory == 0x); }\n if (tx.outputs.length > 5) { require(tx.outputs[5].tokenCategory == 0x); }\n }\n\n function refund(pubkey pk, sig s) {\n require(hash160(pk) == buyerPkh);\n require(checkSig(s, pk));\n require(this.age >= refundAfter);\n }\n}\n", - "fingerprint": "9c93f44bcd3875104d52642c9bd01751a2198b56dc00aaabd301d616f9047cc3", + "bytecode": "OP_7 OP_PICK OP_0 OP_NUMEQUAL OP_IF OP_INPUTINDEX OP_1 OP_NUMEQUALVERIFY OP_0 OP_OUTPOINTINDEX OP_0 OP_NUMEQUALVERIFY OP_0 OP_OUTPOINTTXHASH OP_0 OP_OUTPUTBYTECODE OP_3 OP_ROLL OP_EQUALVERIFY OP_0 OP_OUTPUTTOKENCATEGORY OP_EQUALVERIFY OP_0 OP_OUTPUTTOKENCOMMITMENT OP_ROT OP_EQUALVERIFY OP_0 OP_OUTPUTTOKENAMOUNT OP_0 OP_NUMEQUALVERIFY OP_1 OP_OUTPUTBYTECODE OP_ROT OP_EQUALVERIFY OP_1 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_2 OP_OUTPUTBYTECODE OP_ROT OP_EQUALVERIFY OP_2 OP_OUTPUTVALUE OP_ROT OP_GREATERTHANOREQUAL OP_VERIFY OP_2 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_TXOUTPUTCOUNT OP_4 OP_LESSTHANOREQUAL OP_VERIFY OP_TXOUTPUTCOUNT OP_3 OP_GREATERTHAN OP_IF OP_3 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_3 OP_OUTPUTBYTECODE OP_1 OP_SPLIT OP_DROP 6a OP_EQUAL OP_NOT OP_VERIFY OP_ENDIF OP_2DROP OP_DROP OP_1 OP_ELSE OP_7 OP_ROLL OP_1 OP_NUMEQUALVERIFY OP_7 OP_PICK OP_HASH160 OP_EQUALVERIFY OP_7 OP_ROLL OP_7 OP_ROLL OP_CHECKSIGVERIFY OP_5 OP_PICK OP_1 000001 OP_WITHIN OP_VERIFY OP_5 OP_ROLL OP_CHECKSEQUENCEVERIFY OP_2DROP OP_2DROP OP_2DROP OP_1 OP_ENDIF", + "source": "pragma cashscript ^0.13.0;\n\n// DeliveryEscrow — payment for a name that can only be collected by the\n// transaction that registers it to the buyer.\n// (Decentralized.DNS/DESIGN-trustless-purchase.md §4.)\n//\n// STATUS: chipnet draft. NOT REVIEWED.\n//\n// A buyer who cannot build a registration themselves (pay-by-QR, any wallet)\n// pays this script instead of paying a registrar. The money then has exactly\n// two ways out:\n//\n// deliver() anyone may spend it — in a transaction that mints the agreed\n// certificate to the buyer and publishes the agreed registration.\n// \"Took the money and did not register\" is not a risk the buyer\n// has to weigh; it is a transaction that cannot be built.\n// refund() the buyer takes it back with their signature once it is\n// `refundAfter` blocks old.\n//\n// What deliver() pins down, output by output:\n// 0 the certificate — a NEW token category created by this transaction\n// (its id is input 0's outpoint), an NFT with no capability, commitment =\n// the name, no fungible amount, locked to the buyer's lock\n// 1 the registration payload, byte for byte as agreed (OP_RETURN)\n// 2 the discovery beacon, at least its dust\n// 3 optional: the deliverer's change — no token, and not a data output\n// and nothing else. Two things are being kept out:\n// · a second token of the certificate's category, which its holder could\n// later use to update the buyer's name;\n// · a second data output. A transaction is read as carrying ONE protocol\n// message; a later OP_RETURN would replace the agreed registration in\n// every resolver, and the deliverer would be paid for a certificate\n// that registers nothing.\n//\n// It cannot deliver under a TLD that requires its owner's co-signature: that\n// rule wants the TLD's certificate in an output of the registration, and no\n// extra token is allowed here. The library refuses to build an escrow for\n// such a name (escrow-covenant.js assertTldAllowsEscrow).\n//\n// The escrow must be input 1 (input 0 is the genesis outpoint). That makes it\n// one escrow per transaction: two payments for the same name could otherwise\n// both be collected against a single certificate.\n//\n// WHAT IT CANNOT PROMISE. Script sees this transaction, not the registry. If\n// the name was registered by someone else between payment and delivery, a\n// deliverer can still mint a certificate that satisfies every line here and\n// that no resolver will honour (\"first valid registration wins\" is a resolver\n// rule). The window is the buyer's to keep short: check the name is free, pay,\n// and refund if delivery does not come.\ncontract DeliveryEscrow(\n bytes20 buyerPkh,\n bytes certLock, // where the certificate must go (the buyer's token address, or their covenant)\n bytes nameCommitment, // the on-chain name, as the certificate's commitment\n bytes payloadLock, // the full OP_RETURN locking bytecode of the agreed REG payload\n bytes beaconLock,\n int beaconDust,\n int refundAfter\n) {\n function deliver() {\n require(this.activeInputIndex == 1);\n require(tx.inputs[0].outpointIndex == 0);\n bytes category = tx.inputs[0].outpointTransactionHash;\n\n require(tx.outputs[0].lockingBytecode == certLock);\n require(tx.outputs[0].tokenCategory == category);\n require(tx.outputs[0].nftCommitment == nameCommitment);\n require(tx.outputs[0].tokenAmount == 0);\n\n require(tx.outputs[1].lockingBytecode == payloadLock);\n require(tx.outputs[1].tokenCategory == 0x);\n\n require(tx.outputs[2].lockingBytecode == beaconLock);\n require(tx.outputs[2].value >= beaconDust);\n require(tx.outputs[2].tokenCategory == 0x);\n\n require(tx.outputs.length <= 4);\n if (tx.outputs.length > 3) {\n require(tx.outputs[3].tokenCategory == 0x);\n require(tx.outputs[3].lockingBytecode.split(1)[0] != 0x6a);\n }\n }\n\n function refund(pubkey pk, sig s) {\n require(hash160(pk) == buyerPkh);\n require(checkSig(s, pk));\n require(within(refundAfter, 1, 65536)); // a 16-bit block count, see subscription.cash\n require(this.age >= refundAfter);\n }\n}\n", + "fingerprint": "4f43d98c69ef538b7c7588fda9b49c1f18c4f712a6ea3a5e98c7cc6d24a36b1f", "debug": { - "bytecode": "5779009c63c0519d00c9009d00c800cd537a8800d18800d27b8800d3009d51cd7b8851d1008852cd7b8852cc7ba26952d10088c456a169c453a06353d1008868c454a06354d1008868c455a06355d10088686d755167577a519d5779a988577a577aad557ab26d6d6d5168", - "sourceMap": "49:4:70:5;;;;;50:16:50:37;:41::42;:8::44:1;51:26:51:27:0;:16::42:1;:46::47:0;:8::49:1;52:35:52:36:0;:25::61:1;54:27:54:28:0;:16::45:1;:49::57:0;;:8::59:1;55:27:55:28:0;:16::43:1;:8::57;56:27:56:28:0;:16::43:1;:47::61:0;:8::63:1;57:27:57:28:0;:16::41:1;:45::46:0;:8::48:1;59:27:59:28:0;:16::45:1;:49::60:0;:8::62:1;60:27:60:28:0;:16::43:1;:47::49:0;:8::51:1;62:27:62:28:0;:16::45:1;:49::59:0;:8::61:1;63:27:63:28:0;:16::35:1;:39::49:0;:16:::1;:8::51;64:27:64:28:0;:16::43:1;:47::49:0;:8::51:1;66:16:66:33:0;:37::38;:16:::1;:8::40;67:12:67:29:0;:32::33;:12:::1;:35::82:0;:56::57;:45::72:1;:76::78:0;:37::80:1;:35::82;68:12:68:29:0;:32::33;:12:::1;:35::82:0;:56::57;:45::72:1;:76::78:0;:37::80:1;:35::82;69:12:69:29:0;:32::33;:12:::1;:35::82:0;:56::57;:45::72:1;:76::78:0;:37::80:1;:35::82;49:23:70:5;;;:4;72::76::0;;;;73:24:73:26;;:16::27:1;:8::41;74:25:74:26:0;;:28::30;;:8::33:1;75:28:75:39:0;;:8::41:1;72:38:76:5;;;;40:0:77:1", + "bytecode": "5779009c63c0519d00c9009d00c800cd537a8800d18800d27b8800d3009d51cd7b8851d1008852cd7b8852cc7ba26952d10088c454a169c453a06353d1008853cd517f75016a879169686d755167577a519d5779a988577a577aad55795103000001a569557ab26d6d6d5168", + "sourceMap": "59:4:81:5;;;;;60:16:60:37;:41::42;:8::44:1;61:26:61:27:0;:16::42:1;:46::47:0;:8::49:1;62:35:62:36:0;:25::61:1;64:27:64:28:0;:16::45:1;:49::57:0;;:8::59:1;65:27:65:28:0;:16::43:1;:8::57;66:27:66:28:0;:16::43:1;:47::61:0;:8::63:1;67:27:67:28:0;:16::41:1;:45::46:0;:8::48:1;69:27:69:28:0;:16::45:1;:49::60:0;:8::62:1;70:27:70:28:0;:16::43:1;:47::49:0;:8::51:1;72:27:72:28:0;:16::45:1;:49::59:0;:8::61:1;73:27:73:28:0;:16::35:1;:39::49:0;:16:::1;:8::51;74:27:74:28:0;:16::43:1;:47::49:0;:8::51:1;76:16:76:33:0;:37::38;:16:::1;:8::40;77:12:77:29:0;:32::33;:12:::1;:35:80:9:0;78:31:78:32;:20::47:1;:51::53:0;:12::55:1;79:31:79:32:0;:20::49:1;:56::57:0;:20::58:1;:::61;:65::69:0;:20:::1;;:12::71;77:35:80:9;59:23:81:5;;;:4;83::88::0;;;;84:24:84:26;;:16::27:1;:8::41;85:25:85:26:0;;:28::30;;:8::33:1;86:23:86:34:0;;:36::37;:39::44;:16::45:1;:8::47;87:28:87:39:0;;:8::41:1;83:38:88:5;;;;50:0:89:1", "logs": [], "requires": [ - { "ip": 14, "line": 50 }, - { "ip": 18, "line": 51 }, - { "ip": 25, "line": 54 }, - { "ip": 28, "line": 55 }, - { "ip": 32, "line": 56 }, - { "ip": 36, "line": 57 }, - { "ip": 40, "line": 59 }, - { "ip": 44, "line": 60 }, - { "ip": 48, "line": 62 }, - { "ip": 53, "line": 63 }, - { "ip": 57, "line": 64 }, - { "ip": 61, "line": 66 }, - { "ip": 69, "line": 67 }, - { "ip": 78, "line": 68 }, - { "ip": 87, "line": 69 }, - { "ip": 100, "line": 73 }, - { "ip": 105, "line": 74 }, - { "ip": 108, "line": 75 } + { "ip": 14, "line": 60 }, + { "ip": 18, "line": 61 }, + { "ip": 25, "line": 64 }, + { "ip": 28, "line": 65 }, + { "ip": 32, "line": 66 }, + { "ip": 36, "line": 67 }, + { "ip": 40, "line": 69 }, + { "ip": 44, "line": 70 }, + { "ip": 48, "line": 72 }, + { "ip": 53, "line": 73 }, + { "ip": 57, "line": 74 }, + { "ip": 61, "line": 76 }, + { "ip": 69, "line": 78 }, + { "ip": 78, "line": 79 }, + { "ip": 91, "line": 84 }, + { "ip": 96, "line": 85 }, + { "ip": 102, "line": 86 }, + { "ip": 105, "line": 87 } ] }, "compiler": { @@ -55,5 +55,5 @@ "enforceLocktimeGuard": true } }, - "updatedAt": "2026-10-04T14:02:01.200Z" + "updatedAt": "2026-10-04T14:37:29.033Z" } \ No newline at end of file diff --git a/contracts/name-registry.cash b/contracts/name-registry.cash index 09ddd52..9f72021 100644 --- a/contracts/name-registry.cash +++ b/contracts/name-registry.cash @@ -30,7 +30,7 @@ pragma cashscript ^0.13.0; contract NameRegistry( // ---- fixed for this covenant version and price tier ---- bytes fundLock, // locking bytecode the fund's payments must go to - pubkey stewardPk, // approves the CODE of a newer covenant version (migrate) + 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 @@ -50,6 +50,10 @@ contract NameRegistry( 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]); @@ -102,12 +106,17 @@ contract NameRegistry( ownerSats = price * ownerPercentIdle / 100; } - // the commit + // 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)); @@ -142,16 +151,27 @@ contract NameRegistry( } // Move to a newer covenant version. The owner chooses to; the steward has - // signed the hash of the new version's CODE, once, for everyone. Neither - // can act alone: the steward cannot move a name, and the owner cannot - // migrate to a script the steward has not published. `newState` is whatever - // the new version expects in front of its code. - function migrate(pubkey pk, sig s, datasig approval, bytes newCode, bytes newState) { + // 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)); - require(checkDataSig(approval, hash256(newCode), stewardPk)); - bytes32 newHash = hash256(newState + newCode); + 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); diff --git a/contracts/name-registry.json b/contracts/name-registry.json index 3f70f40..823b1b8 100644 --- a/contracts/name-registry.json +++ b/contracts/name-registry.json @@ -50,64 +50,66 @@ { "name": "pk", "type": "pubkey" }, { "name": "s", "type": "sig" }, { "name": "approval", "type": "datasig" }, - { "name": "newCode", "type": "bytes" }, - { "name": "newState", "type": "bytes" } + { "name": "newCode", "type": "bytes" } ] } ], - "bytecode": "OP_11 OP_PICK OP_0 OP_NUMEQUAL OP_IF OP_12 OP_PICK OP_HASH160 OP_11 OP_PICK OP_EQUALVERIFY OP_13 OP_ROLL OP_13 OP_ROLL OP_CHECKSIGVERIFY OP_12 OP_PICK OP_1 OP_GREATERTHANOREQUAL OP_VERIFY OP_9 OP_ROLL OP_BIN2NUM OP_12 OP_PICK OP_9 OP_ROLL OP_MUL OP_ADD 14 OP_10 OP_ROLL OP_CAT OP_4 OP_CAT OP_SWAP OP_4 OP_NUM2BIN OP_CAT OP_ACTIVEBYTECODE 1a OP_SPLIT OP_NIP OP_CAT OP_HASH256 OP_0 OP_OUTPUTBYTECODE aa20 OP_ROT OP_CAT 87 OP_CAT OP_EQUALVERIFY OP_0 OP_OUTPUTTOKENCATEGORY OP_INPUTINDEX OP_UTXOTOKENCATEGORY OP_EQUALVERIFY OP_0 OP_OUTPUTTOKENCOMMITMENT OP_INPUTINDEX OP_UTXOTOKENCOMMITMENT OP_EQUALVERIFY OP_0 OP_OUTPUTVALUE OP_INPUTINDEX OP_UTXOVALUE OP_GREATERTHANOREQUAL OP_VERIFY OP_1 OP_OUTPUTBYTECODE OP_EQUALVERIFY OP_1 OP_OUTPUTVALUE OP_3 OP_ROLL OP_9 OP_ROLL OP_MUL OP_GREATERTHANOREQUAL OP_VERIFY OP_1 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_2DROP OP_2DROP OP_2DROP OP_DROP OP_1 OP_ELSE OP_11 OP_PICK OP_1 OP_NUMEQUAL OP_IF OP_14 OP_ROLL OP_SIZE 14 OP_EQUALVERIFY OP_13 OP_PICK OP_HASH160 OP_12 OP_ROLL OP_EQUALVERIFY OP_13 OP_ROLL OP_13 OP_ROLL OP_CHECKSIGVERIFY 14 OP_SWAP OP_CAT OP_4 OP_CAT OP_10 OP_ROLL OP_CAT OP_ACTIVEBYTECODE 1a OP_SPLIT OP_NIP OP_CAT OP_HASH256 OP_0 OP_OUTPUTBYTECODE aa20 OP_ROT OP_CAT 87 OP_CAT OP_EQUALVERIFY OP_0 OP_OUTPUTTOKENCATEGORY OP_INPUTINDEX OP_UTXOTOKENCATEGORY OP_EQUALVERIFY OP_0 OP_OUTPUTTOKENCOMMITMENT OP_INPUTINDEX OP_UTXOTOKENCOMMITMENT OP_EQUALVERIFY OP_0 OP_OUTPUTVALUE OP_INPUTINDEX OP_UTXOVALUE OP_GREATERTHANOREQUAL OP_VERIFY OP_2DROP OP_2DROP OP_2DROP OP_2DROP OP_2DROP OP_1 OP_ELSE OP_11 OP_PICK OP_2 OP_NUMEQUAL OP_IF OP_12 OP_ROLL OP_SIZE 14 OP_EQUALVERIFY OP_10 OP_ROLL OP_BIN2NUM OP_DUP OP_CHECKLOCKTIMEVERIFY OP_DROP OP_TXLOCKTIME OP_DUP 0065cd1d OP_LESSTHAN OP_VERIFY OP_SWAP OP_9 OP_PICK OP_ADD OP_8 OP_PICK OP_0 OP_2OVER OP_LESSTHAN OP_IF OP_10 OP_PICK OP_10 OP_PICK OP_12 OP_PICK OP_SUB OP_4 OP_PICK OP_6 OP_PICK OP_SUB OP_MUL OP_13 OP_PICK OP_DIV OP_ADD OP_ROT OP_DROP OP_SWAP OP_OVER OP_14 OP_PICK OP_MUL 64 OP_DIV OP_NIP OP_ENDIF OP_INPUTINDEX OP_UTXOTOKENCOMMITMENT 11 OP_ROLL OP_CAT OP_HASH256 14 OP_6 OP_PICK OP_CAT 20 OP_CAT OP_SWAP OP_CAT OP_8 OP_ROLL OP_CAT OP_1 OP_UTXOBYTECODE aa20 OP_ROT OP_HASH256 OP_CAT 87 OP_CAT OP_EQUALVERIFY 14 OP_5 OP_ROLL OP_CAT OP_4 OP_CAT OP_4 OP_ROLL OP_11 OP_ROLL OP_ADD OP_4 OP_NUM2BIN OP_CAT OP_ACTIVEBYTECODE 1a OP_SPLIT OP_NIP OP_CAT OP_HASH256 OP_0 OP_OUTPUTBYTECODE aa20 OP_ROT OP_CAT 87 OP_CAT OP_EQUALVERIFY OP_0 OP_OUTPUTTOKENCATEGORY OP_INPUTINDEX OP_UTXOTOKENCATEGORY OP_EQUALVERIFY OP_0 OP_OUTPUTTOKENCOMMITMENT OP_INPUTINDEX OP_UTXOTOKENCOMMITMENT OP_EQUALVERIFY OP_0 OP_OUTPUTVALUE OP_INPUTINDEX OP_UTXOVALUE OP_GREATERTHANOREQUAL OP_VERIFY OP_1 OP_OUTPUTBYTECODE OP_4 OP_ROLL OP_EQUALVERIFY OP_1 OP_OUTPUTVALUE OP_ROT OP_2 OP_PICK OP_SUB OP_GREATERTHANOREQUAL OP_VERIFY OP_1 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_DUP OP_0 OP_GREATERTHAN OP_IF OP_2 OP_OUTPUTBYTECODE 76a914 OP_10 OP_PICK OP_CAT 88ac OP_CAT OP_EQUALVERIFY OP_2 OP_OUTPUTVALUE OP_OVER OP_GREATERTHANOREQUAL OP_VERIFY OP_2 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_ENDIF OP_2DROP OP_2DROP OP_2DROP OP_2DROP OP_2DROP OP_1 OP_ELSE OP_11 OP_PICK OP_3 OP_NUMEQUAL OP_IF OP_12 OP_PICK OP_HASH160 OP_11 OP_ROLL OP_EQUALVERIFY OP_12 OP_ROLL OP_12 OP_ROLL OP_CHECKSIGVERIFY OP_INPUTINDEX OP_UTXOTOKENCATEGORY OP_TXOUTPUTCOUNT OP_4 OP_LESSTHANOREQUAL OP_VERIFY OP_0 OP_OUTPUTTOKENCATEGORY OP_OVER OP_EQUAL OP_NOT OP_VERIFY OP_TXOUTPUTCOUNT OP_1 OP_GREATERTHAN OP_IF OP_1 OP_OUTPUTTOKENCATEGORY OP_OVER OP_EQUAL OP_NOT OP_VERIFY OP_ENDIF OP_TXOUTPUTCOUNT OP_2 OP_GREATERTHAN OP_IF OP_2 OP_OUTPUTTOKENCATEGORY OP_OVER OP_EQUAL OP_NOT OP_VERIFY OP_ENDIF OP_TXOUTPUTCOUNT OP_3 OP_GREATERTHAN OP_IF OP_3 OP_OUTPUTTOKENCATEGORY OP_OVER OP_EQUAL OP_NOT OP_VERIFY OP_ENDIF OP_2DROP OP_2DROP OP_2DROP OP_2DROP OP_2DROP OP_2DROP OP_1 OP_ELSE OP_11 OP_ROLL OP_4 OP_NUMEQUALVERIFY OP_11 OP_PICK OP_HASH160 OP_11 OP_ROLL OP_EQUALVERIFY OP_11 OP_ROLL OP_11 OP_ROLL OP_CHECKSIGVERIFY OP_10 OP_ROLL OP_11 OP_PICK OP_HASH256 OP_3 OP_ROLL OP_CHECKDATASIGVERIFY OP_10 OP_ROLL OP_10 OP_ROLL OP_CAT OP_HASH256 OP_0 OP_OUTPUTBYTECODE aa20 OP_ROT OP_CAT 87 OP_CAT OP_EQUALVERIFY OP_0 OP_OUTPUTTOKENCATEGORY OP_INPUTINDEX OP_UTXOTOKENCATEGORY OP_EQUALVERIFY OP_0 OP_OUTPUTTOKENCOMMITMENT OP_INPUTINDEX OP_UTXOTOKENCOMMITMENT OP_EQUALVERIFY OP_0 OP_OUTPUTVALUE OP_INPUTINDEX OP_UTXOVALUE OP_GREATERTHANOREQUAL OP_VERIFY OP_2DROP OP_2DROP OP_2DROP OP_2DROP OP_DROP OP_1 OP_ENDIF OP_ENDIF OP_ENDIF OP_ENDIF", - "source": "pragma cashscript ^0.13.0;\n\n// NameRegistry v1 — the covenant that holds a name certificate and enforces\n// its lifecycle (Decentralized.DNS/DESIGN-covenant-registry.md).\n//\n// STATUS: chipnet draft. NOT REVIEWED. A flaw here either locks every name\n// forever or lets anyone take one; nothing built from this file may hold a\n// mainnet name until someone who did not write it has reviewed it.\n//\n// The certificate (an immutable NFT whose commitment is the name) sits in an\n// output locked by this script. Two values change over a name's life — who\n// owns it and when it expires — and they are the LAST two constructor\n// arguments, which CashScript pushes FIRST. The redeem script therefore begins\n//\n// 0x14 0x04 \n//\n// and every path that keeps the name re-creates the covenant by replacing\n// those 26 bytes and requiring output 0 to be locked to the result. One\n// contract, one code hash per price tier; the state is in the address.\n//\n// Numbers must match Argus/src/lib/registry-lifecycle.js, which is the\n// reference: integer arithmetic, multiply before dividing.\n//\n// What script cannot do, stated once: it can prove a block height has been\n// REACHED (tx.time), never that one has NOT. So \"the owner may reclaim only\n// until the grace period ends\" is not enforceable; an owner can renew for as\n// long as nobody has taken the name over. The takeover price is safe against\n// this: a taker can only claim a height that is already past, and a later\n// height means a lower price they are entitled to anyway.\ncontract NameRegistry(\n // ---- fixed for this covenant version and price tier ----\n bytes fundLock, // locking bytecode the fund's payments must go to\n pubkey stewardPk, // approves the CODE of a newer covenant version (migrate)\n bytes commitBoxCode, // redeem-script body of the CommitBox contract\n int renewalSats, // price of one year, for the owner\n int premiumSats, // takeover price at the moment of expiry\n int floorSats, // takeover price once the grace period has run out\n int graceBlocks, // length of the grace period\n int blocksPerYear,\n int ownerPercentIdle, // previous owner's share of a takeover during grace\n // ---- state: keep these LAST, in this order ----\n bytes4 expiry, // block height the paid period ends at\n bytes20 ownerPkh\n) {\n // Keep the name: pay for `years` more. Also the reclaim path — an expired\n // name renews exactly the same way, from its old expiry.\n function renew(pubkey pk, sig s, int years) {\n require(hash160(pk) == ownerPkh);\n require(checkSig(s, pk));\n require(years >= 1);\n\n int newExpiry = int(expiry) + years * blocksPerYear;\n bytes newState = 0x14 + ownerPkh + 0x04 + toPaddedBytes(newExpiry, 4);\n bytes32 newHash = hash256(newState + this.activeBytecode.split(26)[1]);\n\n require(tx.outputs[0].lockingBytecode == new LockingBytecodeP2SH32(newHash));\n require(tx.outputs[0].tokenCategory == tx.inputs[this.activeInputIndex].tokenCategory);\n require(tx.outputs[0].nftCommitment == tx.inputs[this.activeInputIndex].nftCommitment);\n require(tx.outputs[0].value >= tx.inputs[this.activeInputIndex].value);\n\n require(tx.outputs[1].lockingBytecode == fundLock);\n require(tx.outputs[1].value >= renewalSats * years);\n require(tx.outputs[1].tokenCategory == 0x);\n }\n\n // Hand the name to another key (a sale, a gift), or re-create it for the\n // same key — which is how records are updated: the certificate has to move\n // for a resolver to accept an UPD. Expiry is untouched.\n function transfer(pubkey pk, sig s, bytes20 newOwnerPkh) {\n require(hash160(pk) == ownerPkh);\n require(checkSig(s, pk));\n\n bytes newState = 0x14 + newOwnerPkh + 0x04 + expiry;\n bytes32 newHash = hash256(newState + this.activeBytecode.split(26)[1]);\n\n require(tx.outputs[0].lockingBytecode == new LockingBytecodeP2SH32(newHash));\n require(tx.outputs[0].tokenCategory == tx.inputs[this.activeInputIndex].tokenCategory);\n require(tx.outputs[0].nftCommitment == tx.inputs[this.activeInputIndex].nftCommitment);\n require(tx.outputs[0].value >= tx.inputs[this.activeInputIndex].value);\n }\n\n // Take an expired name. No owner signature: anyone may, at a price that\n // glides from premiumSats at expiry down to floorSats at the end of the\n // grace period and stays there. During grace the previous owner is paid\n // ownerPercentIdle of the price; afterwards the fund receives all of it.\n //\n // Input 1 must be a CommitBox the taker created at least a block earlier,\n // committing to this name and to their own key. It hides which name is\n // wanted until the takeover itself, so the transaction cannot be copied\n // from the mempool by someone who had not already committed.\n function takeover(bytes20 newOwnerPkh, bytes salt) {\n int expiryHeight = int(expiry);\n require(tx.time >= expiryHeight);\n int height = tx.locktime;\n require(height < 500000000); // a block height, not a timestamp\n\n int idleEnd = expiryHeight + graceBlocks;\n int price = floorSats;\n int ownerSats = 0;\n if (height < idleEnd) {\n price = floorSats + (premiumSats - floorSats) * (idleEnd - height) / graceBlocks;\n ownerSats = price * ownerPercentIdle / 100;\n }\n\n // the commit\n bytes32 commitHash = hash256(tx.inputs[this.activeInputIndex].nftCommitment + salt);\n bytes boxRedeem = 0x14 + newOwnerPkh + 0x20 + commitHash + commitBoxCode;\n require(tx.inputs[1].lockingBytecode == new LockingBytecodeP2SH32(hash256(boxRedeem)));\n\n // the name, under its new owner, for one fresh year\n bytes newState = 0x14 + newOwnerPkh + 0x04 + toPaddedBytes(height + blocksPerYear, 4);\n bytes32 newHash = hash256(newState + this.activeBytecode.split(26)[1]);\n require(tx.outputs[0].lockingBytecode == new LockingBytecodeP2SH32(newHash));\n require(tx.outputs[0].tokenCategory == tx.inputs[this.activeInputIndex].tokenCategory);\n require(tx.outputs[0].nftCommitment == tx.inputs[this.activeInputIndex].nftCommitment);\n require(tx.outputs[0].value >= tx.inputs[this.activeInputIndex].value);\n\n // the money\n require(tx.outputs[1].lockingBytecode == fundLock);\n require(tx.outputs[1].value >= price - ownerSats);\n require(tx.outputs[1].tokenCategory == 0x);\n if (ownerSats > 0) {\n require(tx.outputs[2].lockingBytecode == new LockingBytecodeP2PKH(ownerPkh));\n require(tx.outputs[2].value >= ownerSats);\n require(tx.outputs[2].tokenCategory == 0x);\n }\n }\n\n // Give the name back: the certificate is destroyed. At most four outputs,\n // none of which may carry it — the owner cannot use this path to walk the\n // certificate out of the covenant into a plain wallet.\n function release(pubkey pk, sig s) {\n require(hash160(pk) == ownerPkh);\n require(checkSig(s, pk));\n\n bytes category = tx.inputs[this.activeInputIndex].tokenCategory;\n require(tx.outputs.length <= 4);\n require(tx.outputs[0].tokenCategory != category);\n if (tx.outputs.length > 1) { require(tx.outputs[1].tokenCategory != category); }\n if (tx.outputs.length > 2) { require(tx.outputs[2].tokenCategory != category); }\n if (tx.outputs.length > 3) { require(tx.outputs[3].tokenCategory != category); }\n }\n\n // Move to a newer covenant version. The owner chooses to; the steward has\n // signed the hash of the new version's CODE, once, for everyone. Neither\n // can act alone: the steward cannot move a name, and the owner cannot\n // migrate to a script the steward has not published. `newState` is whatever\n // the new version expects in front of its code.\n function migrate(pubkey pk, sig s, datasig approval, bytes newCode, bytes newState) {\n require(hash160(pk) == ownerPkh);\n require(checkSig(s, pk));\n require(checkDataSig(approval, hash256(newCode), stewardPk));\n\n bytes32 newHash = hash256(newState + newCode);\n require(tx.outputs[0].lockingBytecode == new LockingBytecodeP2SH32(newHash));\n require(tx.outputs[0].tokenCategory == tx.inputs[this.activeInputIndex].tokenCategory);\n require(tx.outputs[0].nftCommitment == tx.inputs[this.activeInputIndex].nftCommitment);\n require(tx.outputs[0].value >= tx.inputs[this.activeInputIndex].value);\n }\n}\n", - "fingerprint": "bd756d3c667bfe0a857b74d48d9ccea9a69ea51ffb2bd9a6de0f12f919719453", + "bytecode": "OP_11 OP_PICK OP_0 OP_NUMEQUAL OP_IF OP_12 OP_PICK OP_HASH160 OP_11 OP_PICK OP_EQUALVERIFY OP_13 OP_ROLL OP_13 OP_ROLL OP_CHECKSIGVERIFY OP_12 OP_PICK OP_1 OP_GREATERTHANOREQUAL OP_VERIFY OP_9 OP_ROLL OP_BIN2NUM OP_12 OP_PICK OP_9 OP_ROLL OP_MUL OP_ADD OP_DUP 0065cd1d OP_LESSTHAN OP_VERIFY 14 OP_10 OP_ROLL OP_CAT OP_4 OP_CAT OP_SWAP OP_4 OP_NUM2BIN OP_CAT OP_ACTIVEBYTECODE 1a OP_SPLIT OP_NIP OP_CAT OP_HASH256 OP_0 OP_OUTPUTBYTECODE aa20 OP_ROT OP_CAT 87 OP_CAT OP_EQUALVERIFY OP_0 OP_OUTPUTTOKENCATEGORY OP_INPUTINDEX OP_UTXOTOKENCATEGORY OP_EQUALVERIFY OP_0 OP_OUTPUTTOKENCOMMITMENT OP_INPUTINDEX OP_UTXOTOKENCOMMITMENT OP_EQUALVERIFY OP_0 OP_OUTPUTVALUE OP_INPUTINDEX OP_UTXOVALUE OP_GREATERTHANOREQUAL OP_VERIFY OP_1 OP_OUTPUTBYTECODE OP_EQUALVERIFY OP_1 OP_OUTPUTVALUE OP_3 OP_ROLL OP_9 OP_ROLL OP_MUL OP_GREATERTHANOREQUAL OP_VERIFY OP_1 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_2DROP OP_2DROP OP_2DROP OP_DROP OP_1 OP_ELSE OP_11 OP_PICK OP_1 OP_NUMEQUAL OP_IF OP_14 OP_ROLL OP_SIZE 14 OP_EQUALVERIFY OP_13 OP_PICK OP_HASH160 OP_12 OP_ROLL OP_EQUALVERIFY OP_13 OP_ROLL OP_13 OP_ROLL OP_CHECKSIGVERIFY 14 OP_SWAP OP_CAT OP_4 OP_CAT OP_10 OP_ROLL OP_CAT OP_ACTIVEBYTECODE 1a OP_SPLIT OP_NIP OP_CAT OP_HASH256 OP_0 OP_OUTPUTBYTECODE aa20 OP_ROT OP_CAT 87 OP_CAT OP_EQUALVERIFY OP_0 OP_OUTPUTTOKENCATEGORY OP_INPUTINDEX OP_UTXOTOKENCATEGORY OP_EQUALVERIFY OP_0 OP_OUTPUTTOKENCOMMITMENT OP_INPUTINDEX OP_UTXOTOKENCOMMITMENT OP_EQUALVERIFY OP_0 OP_OUTPUTVALUE OP_INPUTINDEX OP_UTXOVALUE OP_GREATERTHANOREQUAL OP_VERIFY OP_2DROP OP_2DROP OP_2DROP OP_2DROP OP_2DROP OP_1 OP_ELSE OP_11 OP_PICK OP_2 OP_NUMEQUAL OP_IF OP_12 OP_ROLL OP_SIZE 14 OP_EQUALVERIFY OP_10 OP_ROLL OP_BIN2NUM OP_DUP OP_CHECKLOCKTIMEVERIFY OP_DROP OP_TXLOCKTIME OP_DUP 0065cd1d OP_LESSTHAN OP_VERIFY OP_SWAP OP_9 OP_PICK OP_ADD OP_8 OP_PICK OP_0 OP_2OVER OP_LESSTHAN OP_IF OP_10 OP_PICK OP_10 OP_PICK OP_12 OP_PICK OP_SUB OP_4 OP_PICK OP_6 OP_PICK OP_SUB OP_MUL OP_13 OP_PICK OP_DIV OP_ADD OP_ROT OP_DROP OP_SWAP OP_OVER OP_14 OP_PICK OP_MUL 64 OP_DIV OP_NIP OP_ENDIF OP_16 OP_PICK OP_SIZE OP_NIP 20 OP_NUMEQUALVERIFY OP_INPUTINDEX OP_UTXOTOKENCOMMITMENT 11 OP_ROLL OP_CAT OP_HASH256 14 OP_6 OP_PICK OP_CAT 20 OP_CAT OP_SWAP OP_CAT OP_8 OP_ROLL OP_CAT OP_1 OP_UTXOBYTECODE aa20 OP_ROT OP_HASH256 OP_CAT 87 OP_CAT OP_EQUALVERIFY OP_3 OP_PICK OP_12 OP_PICK OP_ADD 0065cd1d OP_LESSTHAN OP_VERIFY 14 OP_5 OP_ROLL OP_CAT OP_4 OP_CAT OP_4 OP_ROLL OP_11 OP_ROLL OP_ADD OP_4 OP_NUM2BIN OP_CAT OP_ACTIVEBYTECODE 1a OP_SPLIT OP_NIP OP_CAT OP_HASH256 OP_0 OP_OUTPUTBYTECODE aa20 OP_ROT OP_CAT 87 OP_CAT OP_EQUALVERIFY OP_0 OP_OUTPUTTOKENCATEGORY OP_INPUTINDEX OP_UTXOTOKENCATEGORY OP_EQUALVERIFY OP_0 OP_OUTPUTTOKENCOMMITMENT OP_INPUTINDEX OP_UTXOTOKENCOMMITMENT OP_EQUALVERIFY OP_0 OP_OUTPUTVALUE OP_INPUTINDEX OP_UTXOVALUE OP_GREATERTHANOREQUAL OP_VERIFY OP_1 OP_OUTPUTBYTECODE OP_4 OP_ROLL OP_EQUALVERIFY OP_1 OP_OUTPUTVALUE OP_ROT OP_2 OP_PICK OP_SUB OP_GREATERTHANOREQUAL OP_VERIFY OP_1 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_DUP OP_0 OP_GREATERTHAN OP_IF OP_2 OP_OUTPUTBYTECODE 76a914 OP_10 OP_PICK OP_CAT 88ac OP_CAT OP_EQUALVERIFY OP_2 OP_OUTPUTVALUE OP_OVER OP_GREATERTHANOREQUAL OP_VERIFY OP_2 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_ENDIF OP_2DROP OP_2DROP OP_2DROP OP_2DROP OP_2DROP OP_1 OP_ELSE OP_11 OP_PICK OP_3 OP_NUMEQUAL OP_IF OP_12 OP_PICK OP_HASH160 OP_11 OP_ROLL OP_EQUALVERIFY OP_12 OP_ROLL OP_12 OP_ROLL OP_CHECKSIGVERIFY OP_INPUTINDEX OP_UTXOTOKENCATEGORY OP_TXOUTPUTCOUNT OP_4 OP_LESSTHANOREQUAL OP_VERIFY OP_0 OP_OUTPUTTOKENCATEGORY OP_OVER OP_EQUAL OP_NOT OP_VERIFY OP_TXOUTPUTCOUNT OP_1 OP_GREATERTHAN OP_IF OP_1 OP_OUTPUTTOKENCATEGORY OP_OVER OP_EQUAL OP_NOT OP_VERIFY OP_ENDIF OP_TXOUTPUTCOUNT OP_2 OP_GREATERTHAN OP_IF OP_2 OP_OUTPUTTOKENCATEGORY OP_OVER OP_EQUAL OP_NOT OP_VERIFY OP_ENDIF OP_TXOUTPUTCOUNT OP_3 OP_GREATERTHAN OP_IF OP_3 OP_OUTPUTTOKENCATEGORY OP_OVER OP_EQUAL OP_NOT OP_VERIFY OP_ENDIF OP_2DROP OP_2DROP OP_2DROP OP_2DROP OP_2DROP OP_2DROP OP_1 OP_ELSE OP_11 OP_ROLL OP_4 OP_NUMEQUALVERIFY OP_11 OP_PICK OP_HASH160 OP_11 OP_PICK OP_EQUALVERIFY OP_12 OP_ROLL OP_12 OP_ROLL OP_CHECKSIGVERIFY OP_ACTIVEBYTECODE 1a OP_SPLIT OP_NIP OP_HASH256 OP_12 OP_ROLL OP_SWAP OP_13 OP_PICK OP_HASH256 OP_CAT OP_HASH256 OP_3 OP_ROLL OP_CHECKDATASIGVERIFY 14 OP_10 OP_ROLL OP_CAT OP_4 OP_CAT OP_9 OP_ROLL OP_CAT OP_9 OP_ROLL OP_CAT OP_HASH256 OP_0 OP_OUTPUTBYTECODE aa20 OP_ROT OP_CAT 87 OP_CAT OP_EQUALVERIFY OP_0 OP_OUTPUTTOKENCATEGORY OP_INPUTINDEX OP_UTXOTOKENCATEGORY OP_EQUALVERIFY OP_0 OP_OUTPUTTOKENCOMMITMENT OP_INPUTINDEX OP_UTXOTOKENCOMMITMENT OP_EQUALVERIFY OP_0 OP_OUTPUTVALUE OP_INPUTINDEX OP_UTXOVALUE OP_GREATERTHANOREQUAL OP_VERIFY OP_2DROP OP_2DROP OP_2DROP OP_2DROP OP_1 OP_ENDIF OP_ENDIF OP_ENDIF OP_ENDIF", + "source": "pragma cashscript ^0.13.0;\n\n// NameRegistry v1 — the covenant that holds a name certificate and enforces\n// its lifecycle (Decentralized.DNS/DESIGN-covenant-registry.md).\n//\n// STATUS: chipnet draft. NOT REVIEWED. A flaw here either locks every name\n// forever or lets anyone take one; nothing built from this file may hold a\n// mainnet name until someone who did not write it has reviewed it.\n//\n// The certificate (an immutable NFT whose commitment is the name) sits in an\n// output locked by this script. Two values change over a name's life — who\n// owns it and when it expires — and they are the LAST two constructor\n// arguments, which CashScript pushes FIRST. The redeem script therefore begins\n//\n// 0x14 0x04 \n//\n// and every path that keeps the name re-creates the covenant by replacing\n// those 26 bytes and requiring output 0 to be locked to the result. One\n// contract, one code hash per price tier; the state is in the address.\n//\n// Numbers must match Argus/src/lib/registry-lifecycle.js, which is the\n// reference: integer arithmetic, multiply before dividing.\n//\n// What script cannot do, stated once: it can prove a block height has been\n// REACHED (tx.time), never that one has NOT. So \"the owner may reclaim only\n// until the grace period ends\" is not enforceable; an owner can renew for as\n// long as nobody has taken the name over. The takeover price is safe against\n// this: a taker can only claim a height that is already past, and a later\n// height means a lower price they are entitled to anyway.\ncontract NameRegistry(\n // ---- fixed for this covenant version and price tier ----\n bytes fundLock, // locking bytecode the fund's payments must go to\n pubkey stewardPk, // approves moving from this code to a newer version's (migrate)\n bytes commitBoxCode, // redeem-script body of the CommitBox contract\n int renewalSats, // price of one year, for the owner\n int premiumSats, // takeover price at the moment of expiry\n int floorSats, // takeover price once the grace period has run out\n int graceBlocks, // length of the grace period\n int blocksPerYear,\n int ownerPercentIdle, // previous owner's share of a takeover during grace\n // ---- state: keep these LAST, in this order ----\n bytes4 expiry, // block height the paid period ends at\n bytes20 ownerPkh\n) {\n // Keep the name: pay for `years` more. Also the reclaim path — an expired\n // name renews exactly the same way, from its old expiry.\n function renew(pubkey pk, sig s, int years) {\n require(hash160(pk) == ownerPkh);\n require(checkSig(s, pk));\n require(years >= 1);\n\n int newExpiry = int(expiry) + years * blocksPerYear;\n // Stay a block height. At 500,000,000 a locktime is read as a\n // timestamp, takeover could never satisfy its height check, and the\n // name would be untakeable for ever.\n require(newExpiry < 500000000);\n bytes newState = 0x14 + ownerPkh + 0x04 + toPaddedBytes(newExpiry, 4);\n bytes32 newHash = hash256(newState + this.activeBytecode.split(26)[1]);\n\n require(tx.outputs[0].lockingBytecode == new LockingBytecodeP2SH32(newHash));\n require(tx.outputs[0].tokenCategory == tx.inputs[this.activeInputIndex].tokenCategory);\n require(tx.outputs[0].nftCommitment == tx.inputs[this.activeInputIndex].nftCommitment);\n require(tx.outputs[0].value >= tx.inputs[this.activeInputIndex].value);\n\n require(tx.outputs[1].lockingBytecode == fundLock);\n require(tx.outputs[1].value >= renewalSats * years);\n require(tx.outputs[1].tokenCategory == 0x);\n }\n\n // Hand the name to another key (a sale, a gift), or re-create it for the\n // same key — which is how records are updated: the certificate has to move\n // for a resolver to accept an UPD. Expiry is untouched.\n function transfer(pubkey pk, sig s, bytes20 newOwnerPkh) {\n require(hash160(pk) == ownerPkh);\n require(checkSig(s, pk));\n\n bytes newState = 0x14 + newOwnerPkh + 0x04 + expiry;\n bytes32 newHash = hash256(newState + this.activeBytecode.split(26)[1]);\n\n require(tx.outputs[0].lockingBytecode == new LockingBytecodeP2SH32(newHash));\n require(tx.outputs[0].tokenCategory == tx.inputs[this.activeInputIndex].tokenCategory);\n require(tx.outputs[0].nftCommitment == tx.inputs[this.activeInputIndex].nftCommitment);\n require(tx.outputs[0].value >= tx.inputs[this.activeInputIndex].value);\n }\n\n // Take an expired name. No owner signature: anyone may, at a price that\n // glides from premiumSats at expiry down to floorSats at the end of the\n // grace period and stays there. During grace the previous owner is paid\n // ownerPercentIdle of the price; afterwards the fund receives all of it.\n //\n // Input 1 must be a CommitBox the taker created at least a block earlier,\n // committing to this name and to their own key. It hides which name is\n // wanted until the takeover itself, so the transaction cannot be copied\n // from the mempool by someone who had not already committed.\n function takeover(bytes20 newOwnerPkh, bytes salt) {\n int expiryHeight = int(expiry);\n require(tx.time >= expiryHeight);\n int height = tx.locktime;\n require(height < 500000000); // a block height, not a timestamp\n\n int idleEnd = expiryHeight + graceBlocks;\n int price = floorSats;\n int ownerSats = 0;\n if (height < idleEnd) {\n price = floorSats + (premiumSats - floorSats) * (idleEnd - height) / graceBlocks;\n ownerSats = price * ownerPercentIdle / 100;\n }\n\n // the commit. The salt has a fixed size: the name and the salt are\n // hashed end to end, so with a free-length salt a commit for \"pay\"\n // would also be a commit for \"payroll\" (the rest of the name passed\n // off as the start of the salt).\n require(salt.length == 32);\n bytes32 commitHash = hash256(tx.inputs[this.activeInputIndex].nftCommitment + salt);\n bytes boxRedeem = 0x14 + newOwnerPkh + 0x20 + commitHash + commitBoxCode;\n require(tx.inputs[1].lockingBytecode == new LockingBytecodeP2SH32(hash256(boxRedeem)));\n\n // the name, under its new owner, for one fresh year\n require(height + blocksPerYear < 500000000);\n bytes newState = 0x14 + newOwnerPkh + 0x04 + toPaddedBytes(height + blocksPerYear, 4);\n bytes32 newHash = hash256(newState + this.activeBytecode.split(26)[1]);\n require(tx.outputs[0].lockingBytecode == new LockingBytecodeP2SH32(newHash));\n require(tx.outputs[0].tokenCategory == tx.inputs[this.activeInputIndex].tokenCategory);\n require(tx.outputs[0].nftCommitment == tx.inputs[this.activeInputIndex].nftCommitment);\n require(tx.outputs[0].value >= tx.inputs[this.activeInputIndex].value);\n\n // the money\n require(tx.outputs[1].lockingBytecode == fundLock);\n require(tx.outputs[1].value >= price - ownerSats);\n require(tx.outputs[1].tokenCategory == 0x);\n if (ownerSats > 0) {\n require(tx.outputs[2].lockingBytecode == new LockingBytecodeP2PKH(ownerPkh));\n require(tx.outputs[2].value >= ownerSats);\n require(tx.outputs[2].tokenCategory == 0x);\n }\n }\n\n // Give the name back: the certificate is destroyed. At most four outputs,\n // none of which may carry it — the owner cannot use this path to walk the\n // certificate out of the covenant into a plain wallet.\n function release(pubkey pk, sig s) {\n require(hash160(pk) == ownerPkh);\n require(checkSig(s, pk));\n\n bytes category = tx.inputs[this.activeInputIndex].tokenCategory;\n require(tx.outputs.length <= 4);\n require(tx.outputs[0].tokenCategory != category);\n if (tx.outputs.length > 1) { require(tx.outputs[1].tokenCategory != category); }\n if (tx.outputs.length > 2) { require(tx.outputs[2].tokenCategory != category); }\n if (tx.outputs.length > 3) { require(tx.outputs[3].tokenCategory != category); }\n }\n\n // Move to a newer covenant version. The owner chooses to; the steward has\n // approved moving from THIS code to the new code, once, for everyone in\n // this covenant. Neither can act alone.\n //\n // The owner and the expiry carry over unchanged, in the same 26 bytes, in\n // front of the new code. Nothing here is free for the owner to choose:\n // · the state is rebuilt by the contract, so the owner cannot put a\n // script of their own in front of the approved code (which would turn\n // that code into inert data behind their own signature check), and\n // cannot give themselves a later expiry on the way;\n // · the approval is over (this code, new code), so a signature made for\n // one covenant — another tier, another network's fund — is worthless\n // in any other.\n // A newer version must therefore keep this state layout.\n function migrate(pubkey pk, sig s, datasig approval, bytes newCode) {\n require(hash160(pk) == ownerPkh);\n require(checkSig(s, pk));\n\n bytes32 fromCode = hash256(this.activeBytecode.split(26)[1]);\n require(checkDataSig(approval, hash256(fromCode + hash256(newCode)), stewardPk));\n\n bytes32 newHash = hash256(0x14 + ownerPkh + 0x04 + expiry + newCode);\n require(tx.outputs[0].lockingBytecode == new LockingBytecodeP2SH32(newHash));\n require(tx.outputs[0].tokenCategory == tx.inputs[this.activeInputIndex].tokenCategory);\n require(tx.outputs[0].nftCommitment == tx.inputs[this.activeInputIndex].nftCommitment);\n require(tx.outputs[0].value >= tx.inputs[this.activeInputIndex].value);\n }\n}\n", + "fingerprint": "c48440f8b9d46a8183c4c2326a691a281963febd642858667f3343fabc792736", "debug": { - "bytecode": "5b79009c635c79a95b79885d7a5d7aad5c7951a269597a815c79597a959301145a7a7e547e7c54807ec1011a7f777eaa00cd02aa207b7e01877e8800d1c0ce8800d2c0cf8800ccc0c6a26951cd8851cc537a597a95a26951d100886d6d6d7551675b79519c635e7a820114885d79a95c7a885d7a5d7aad01147c7e547e5a7a7ec1011a7f777eaa00cd02aa207b7e01877e8800d1c0ce8800d2c0cf8800ccc0c6a2696d6d6d6d6d51675b79529c635c7a820114885a7a8176b175c576040065cd1d9f697c597993587900709f635a795a795c79945479567994955d7996937b757c785e79950164967768c0cf01117a7eaa011456797e01207e7c7e587a7e51c702aa207baa7e01877e880114557a7e547e547a5b7a9354807ec1011a7f777eaa00cd02aa207b7e01877e8800d1c0ce8800d2c0cf8800ccc0c6a26951cd547a8851cc7b527994a26951d100887600a06352cd0376a9145a797e0288ac7e8852cc78a26952d10088686d6d6d6d6d51675b79539c635c79a95b7a885c7a5c7aadc0cec454a16900d178879169c451a06351d17887916968c452a06352d17887916968c453a06353d178879169686d6d6d6d6d6d51675b7a549d5b79a95b7a885b7a5b7aad5a7a5b79aa537abb5a7a5a7a7eaa00cd02aa207b7e01877e8800d1c0ce8800d2c0cf8800ccc0c6a2696d6d6d6d755168686868", - "sourceMap": "47:4:64:5;;;;;48:24:48:26;;:16::27:1;:31::39:0;;:8::41:1;49:25:49:26:0;;:28::30;;:8::33:1;50:16:50:21:0;;:25::26;:16:::1;:8::28;52:28:52:34:0;;:24::35:1;:38::43:0;;:46::59;;:38:::1;:24;53:25:53:29:0;:32::40;;:25:::1;:43::47:0;:25:::1;:64::73:0;:75::76;:50::77:1;:25;54:45:54:64:0;:71::73;:45::74:1;:::77;:34;:26::78;56:27:56:28:0;:16::45:1;:49::83:0;:75::82;:49::83:1;;;:8::85;57:27:57:28:0;:16::43:1;:57::78:0;:47::93:1;:8::95;58:27:58:28:0;:16::43:1;:57::78:0;:47::93:1;:8::95;59:27:59:28:0;:16::35:1;:49::70:0;:39::77:1;:16;:8::79;61:27:61:28:0;:16::45:1;:8::59;62:27:62:28:0;:16::35:1;:39::50:0;;:53::58;;:39:::1;:16;:8::60;63:27:63:28:0;:16::43:1;:47::49:0;:8::51:1;47:48:64:5;;;;;:4;69::80::0;;;;;:40:69:59;;;;;70:24:70:26;;:16::27:1;:31::39:0;;:8::41:1;71:25:71:26:0;;:28::30;;:8::33:1;73:25:73:29:0;:32::43;:25:::1;:46::50:0;:25:::1;:53::59:0;;:25:::1;74:45:74:64:0;:71::73;:45::74:1;:::77;:34;:26::78;76:27:76:28:0;:16::45:1;:49::83:0;:75::82;:49::83:1;;;:8::85;77:27:77:28:0;:16::43:1;:57::78:0;:47::93:1;:8::95;78:27:78:28:0;:16::43:1;:57::78:0;:47::93:1;:8::95;79:27:79:28:0;:16::35:1;:49::70:0;:39::77:1;:16;:8::79;69:61:80:5;;;;;;:4;91::127::0;;;;;:22:91:41;;;;;92:31:92:37;;:27::38:1;93::93:39:0;:8::41:1;;94:21:94:32:0;95:16:95:22;:25::34;:16:::1;:8::36;97:22:97:34:0;:37::48;;:22:::1;98:20:98:29:0;;99:24:99:25;100:12:100:28;::::1;:30:103:9:0;101:20:101:29;;:33::44;;:47::56;;:33:::1;:61::68:0;;:71::77;;:61:::1;:32::78;:81::92:0;;:32:::1;:20;:12::93;;;102:24:102:29:0;:32::48;;:24:::1;:51::54:0;:24:::1;:12::55;100:30:103:9;106:47:106:68:0;:37::83:1;:86::90:0;;:37:::1;:29::91;107:26:107:30:0;:33::44;;:26:::1;:47::51:0;:26:::1;:54::64:0;:26:::1;:67::80:0;;:26:::1;108::108:27:0;:16::44:1;:48::93:0;:82::91;:74::92:1;:48::93;;;:8::95;111:25:111:29:0;:32::43;;:25:::1;:46::50:0;:25:::1;:67::73:0;;:76::89;;:67:::1;:91::92:0;:53::93:1;:25;112:45:112:64:0;:71::73;:45::74:1;:::77;:34;:26::78;113:27:113:28:0;:16::45:1;:49::83:0;:75::82;:49::83:1;;;:8::85;114:27:114:28:0;:16::43:1;:57::78:0;:47::93:1;:8::95;115:27:115:28:0;:16::43:1;:57::78:0;:47::93:1;:8::95;116:27:116:28:0;:16::35:1;:49::70:0;:39::77:1;:16;:8::79;119:27:119:28:0;:16::45:1;:49::57:0;;:8::59:1;120:27:120:28:0;:16::35:1;:39::44:0;:47::56;;:39:::1;:16;:8::58;121:27:121:28:0;:16::43:1;:47::49:0;:8::51:1;122:12:122:21:0;:24::25;:12:::1;:27:126:9:0;123:31:123:32;:20::49:1;:53::87:0;:78::86;;:53::87:1;;;:12::89;124:31:124:32:0;:20::39:1;:43::52:0;:20:::1;:12::54;125:31:125:32:0;:20::47:1;:51::53:0;:12::55:1;122:27:126:9;91:55:127:5;;;;;;:4;132::142::0;;;;;133:24:133:26;;:16::27:1;:31::39:0;;:8::41:1;134:25:134:26:0;;:28::30;;:8::33:1;136:35:136:56:0;:25::71:1;137:16:137:33:0;:37::38;:16:::1;:8::40;138:27:138:28:0;:16::43:1;:47::55:0;:16:::1;;:8::57;139:12:139:29:0;:32::33;:12:::1;:35::88:0;:56::57;:45::72:1;:76::84:0;:45:::1;;:37::86;:35::88;140:12:140:29:0;:32::33;:12:::1;:35::88:0;:56::57;:45::72:1;:76::84:0;:45:::1;;:37::86;:35::88;141:12:141:29:0;:32::33;:12:::1;:35::88:0;:56::57;:45::72:1;:76::84:0;:45:::1;;:37::86;:35::88;132:39:142:5;;;;;;;:4;149::159::0;;;;150:24:150:26;;:16::27:1;:31::39:0;;:8::41:1;151:25:151:26:0;;:28::30;;:8::33:1;152:29:152:37:0;;:47::54;;:39::55:1;:57::66:0;;:8::69:1;154:34:154:42:0;;:45::52;;:34:::1;:26::53;155:27:155:28:0;:16::45:1;:49::83:0;:75::82;:49::83:1;;;:8::85;156:27:156:28:0;:16::43:1;:57::78:0;:47::93:1;:8::95;157:27:157:28:0;:16::43:1;:57::78:0;:47::93:1;:8::95;158:27:158:28:0;:16::35:1;:49::70:0;:39::77:1;:16;:8::79;149:88:159:5;;;;;;30:0:160:1;;;", + "bytecode": "5b79009c635c79a95b79885d7a5d7aad5c7951a269597a815c79597a959376040065cd1d9f6901145a7a7e547e7c54807ec1011a7f777eaa00cd02aa207b7e01877e8800d1c0ce8800d2c0cf8800ccc0c6a26951cd8851cc537a597a95a26951d100886d6d6d7551675b79519c635e7a820114885d79a95c7a885d7a5d7aad01147c7e547e5a7a7ec1011a7f777eaa00cd02aa207b7e01877e8800d1c0ce8800d2c0cf8800ccc0c6a2696d6d6d6d6d51675b79529c635c7a820114885a7a8176b175c576040065cd1d9f697c597993587900709f635a795a795c79945479567994955d7996937b757c785e799501649677686079827701209dc0cf01117a7eaa011456797e01207e7c7e587a7e51c702aa207baa7e01877e8853795c7993040065cd1d9f690114557a7e547e547a5b7a9354807ec1011a7f777eaa00cd02aa207b7e01877e8800d1c0ce8800d2c0cf8800ccc0c6a26951cd547a8851cc7b527994a26951d100887600a06352cd0376a9145a797e0288ac7e8852cc78a26952d10088686d6d6d6d6d51675b79539c635c79a95b7a885c7a5c7aadc0cec454a16900d178879169c451a06351d17887916968c452a06352d17887916968c453a06353d178879169686d6d6d6d6d6d51675b7a549d5b79a95b79885c7a5c7aadc1011a7f77aa5c7a7c5d79aa7eaa537abb01145a7a7e547e597a7e597a7eaa00cd02aa207b7e01877e8800d1c0ce8800d2c0cf8800ccc0c6a2696d6d6d6d5168686868", + "sourceMap": "47:4:68:5;;;;;48:24:48:26;;:16::27:1;:31::39:0;;:8::41:1;49:25:49:26:0;;:28::30;;:8::33:1;50:16:50:21:0;;:25::26;:16:::1;:8::28;52:28:52:34:0;;:24::35:1;:38::43:0;;:46::59;;:38:::1;:24;56:16:56:25:0;:28::37;:16:::1;:8::39;57:25:57:29:0;:32::40;;:25:::1;:43::47:0;:25:::1;:64::73:0;:75::76;:50::77:1;:25;58:45:58:64:0;:71::73;:45::74:1;:::77;:34;:26::78;60:27:60:28:0;:16::45:1;:49::83:0;:75::82;:49::83:1;;;:8::85;61:27:61:28:0;:16::43:1;:57::78:0;:47::93:1;:8::95;62:27:62:28:0;:16::43:1;:57::78:0;:47::93:1;:8::95;63:27:63:28:0;:16::35:1;:49::70:0;:39::77:1;:16;:8::79;65:27:65:28:0;:16::45:1;:8::59;66:27:66:28:0;:16::35:1;:39::50:0;;:53::58;;:39:::1;:16;:8::60;67:27:67:28:0;:16::43:1;:47::49:0;:8::51:1;47:48:68:5;;;;;:4;73::84::0;;;;;:40:73:59;;;;;74:24:74:26;;:16::27:1;:31::39:0;;:8::41:1;75:25:75:26:0;;:28::30;;:8::33:1;77:25:77:29:0;:32::43;:25:::1;:46::50:0;:25:::1;:53::59:0;;:25:::1;78:45:78:64:0;:71::73;:45::74:1;:::77;:34;:26::78;80:27:80:28:0;:16::45:1;:49::83:0;:75::82;:49::83:1;;;:8::85;81:27:81:28:0;:16::43:1;:57::78:0;:47::93:1;:8::95;82:27:82:28:0;:16::43:1;:57::78:0;:47::93:1;:8::95;83:27:83:28:0;:16::35:1;:49::70:0;:39::77:1;:16;:8::79;73:61:84:5;;;;;;:4;95::136::0;;;;;:22:95:41;;;;;96:31:96:37;;:27::38:1;97::97:39:0;:8::41:1;;98:21:98:32:0;99:16:99:22;:25::34;:16:::1;:8::36;101:22:101:34:0;:37::48;;:22:::1;102:20:102:29:0;;103:24:103:25;104:12:104:28;::::1;:30:107:9:0;105:20:105:29;;:33::44;;:47::56;;:33:::1;:61::68:0;;:71::77;;:61:::1;:32::78;:81::92:0;;:32:::1;:20;:12::93;;;106:24:106:29:0;:32::48;;:24:::1;:51::54:0;:24:::1;:12::55;104:30:107:9;113:16:113:20:0;;:::27:1;;:31::33:0;:8::35:1;114:47:114:68:0;:37::83:1;:86::90:0;;:37:::1;:29::91;115:26:115:30:0;:33::44;;:26:::1;:47::51:0;:26:::1;:54::64:0;:26:::1;:67::80:0;;:26:::1;116::116:27:0;:16::44:1;:48::93:0;:82::91;:74::92:1;:48::93;;;:8::95;119:16:119:22:0;;:25::38;;:16:::1;:41::50:0;:16:::1;:8::52;120:25:120:29:0;:32::43;;:25:::1;:46::50:0;:25:::1;:67::73:0;;:76::89;;:67:::1;:91::92:0;:53::93:1;:25;121:45:121:64:0;:71::73;:45::74:1;:::77;:34;:26::78;122:27:122:28:0;:16::45:1;:49::83:0;:75::82;:49::83:1;;;:8::85;123:27:123:28:0;:16::43:1;:57::78:0;:47::93:1;:8::95;124:27:124:28:0;:16::43:1;:57::78:0;:47::93:1;:8::95;125:27:125:28:0;:16::35:1;:49::70:0;:39::77:1;:16;:8::79;128:27:128:28:0;:16::45:1;:49::57:0;;:8::59:1;129:27:129:28:0;:16::35:1;:39::44:0;:47::56;;:39:::1;:16;:8::58;130:27:130:28:0;:16::43:1;:47::49:0;:8::51:1;131:12:131:21:0;:24::25;:12:::1;:27:135:9:0;132:31:132:32;:20::49:1;:53::87:0;:78::86;;:53::87:1;;;:12::89;133:31:133:32:0;:20::39:1;:43::52:0;:20:::1;:12::54;134:31:134:32:0;:20::47:1;:51::53:0;:12::55:1;131:27:135:9;95:55:136:5;;;;;;:4;141::151::0;;;;;142:24:142:26;;:16::27:1;:31::39:0;;:8::41:1;143:25:143:26:0;;:28::30;;:8::33:1;145:35:145:56:0;:25::71:1;146:16:146:33:0;:37::38;:16:::1;:8::40;147:27:147:28:0;:16::43:1;:47::55:0;:16:::1;;:8::57;148:12:148:29:0;:32::33;:12:::1;:35::88:0;:56::57;:45::72:1;:76::84:0;:45:::1;;:37::86;:35::88;149:12:149:29:0;:32::33;:12:::1;:35::88:0;:56::57;:45::72:1;:76::84:0;:45:::1;;:37::86;:35::88;150:12:150:29:0;:32::33;:12:::1;:35::88:0;:56::57;:45::72:1;:76::84:0;:45:::1;;:37::86;:35::88;141:39:151:5;;;;;;;:4;167::179::0;;;;168:24:168:26;;:16::27:1;:31::39:0;;:8::41:1;169:25:169:26:0;;:28::30;;:8::33:1;171:35:171:54:0;:61::63;:35::64:1;:::67;:27::68;172:29:172:37:0;;:47::55;:66::73;;:58::74:1;:47;:39::75;:77::86:0;;:8::89:1;174:34:174:38:0;:41::49;;:34:::1;:52::56:0;:34:::1;:59::65:0;;:34:::1;:68::75:0;;:34:::1;:26::76;175:27:175:28:0;:16::45:1;:49::83:0;:75::82;:49::83:1;;;:8::85;176:27:176:28:0;:16::43:1;:57::78:0;:47::93:1;:8::95;177:27:177:28:0;:16::43:1;:57::78:0;:47::93:1;:8::95;178:27:178:28:0;:16::35:1;:49::70:0;:39::77:1;:16;:8::79;167:72:179:5;;;;;30:0:180:1;;;", "logs": [], "requires": [ { "ip": 21, "line": 48 }, { "ip": 26, "line": 49 }, { "ip": 31, "line": 50 }, - { "ip": 64, "line": 56 }, - { "ip": 69, "line": 57 }, - { "ip": 74, "line": 58 }, - { "ip": 80, "line": 59 }, - { "ip": 83, "line": 61 }, - { "ip": 92, "line": 62 }, - { "ip": 96, "line": 63 }, - { "ip": 118, "line": 70 }, - { "ip": 123, "line": 71 }, - { "ip": 145, "line": 76 }, - { "ip": 150, "line": 77 }, - { "ip": 155, "line": 78 }, - { "ip": 161, "line": 79 }, - { "ip": 183, "line": 93 }, - { "ip": 189, "line": 95 }, - { "ip": 253, "line": 108 }, - { "ip": 281, "line": 113 }, - { "ip": 286, "line": 114 }, - { "ip": 291, "line": 115 }, - { "ip": 297, "line": 116 }, - { "ip": 302, "line": 119 }, - { "ip": 310, "line": 120 }, - { "ip": 314, "line": 121 }, - { "ip": 327, "line": 123 }, - { "ip": 332, "line": 124 }, - { "ip": 336, "line": 125 }, - { "ip": 355, "line": 133 }, - { "ip": 360, "line": 134 }, - { "ip": 366, "line": 137 }, - { "ip": 372, "line": 138 }, - { "ip": 382, "line": 139 }, - { "ip": 393, "line": 140 }, - { "ip": 404, "line": 141 }, - { "ip": 423, "line": 150 }, - { "ip": 428, "line": 151 }, - { "ip": 436, "line": 152 }, - { "ip": 450, "line": 155 }, - { "ip": 455, "line": 156 }, - { "ip": 460, "line": 157 }, - { "ip": 466, "line": 158 } + { "ip": 44, "line": 56 }, + { "ip": 68, "line": 60 }, + { "ip": 73, "line": 61 }, + { "ip": 78, "line": 62 }, + { "ip": 84, "line": 63 }, + { "ip": 87, "line": 65 }, + { "ip": 96, "line": 66 }, + { "ip": 100, "line": 67 }, + { "ip": 122, "line": 74 }, + { "ip": 127, "line": 75 }, + { "ip": 149, "line": 80 }, + { "ip": 154, "line": 81 }, + { "ip": 159, "line": 82 }, + { "ip": 165, "line": 83 }, + { "ip": 187, "line": 97 }, + { "ip": 193, "line": 99 }, + { "ip": 237, "line": 113 }, + { "ip": 263, "line": 116 }, + { "ip": 271, "line": 119 }, + { "ip": 299, "line": 122 }, + { "ip": 304, "line": 123 }, + { "ip": 309, "line": 124 }, + { "ip": 315, "line": 125 }, + { "ip": 320, "line": 128 }, + { "ip": 328, "line": 129 }, + { "ip": 332, "line": 130 }, + { "ip": 345, "line": 132 }, + { "ip": 350, "line": 133 }, + { "ip": 354, "line": 134 }, + { "ip": 373, "line": 142 }, + { "ip": 378, "line": 143 }, + { "ip": 384, "line": 146 }, + { "ip": 390, "line": 147 }, + { "ip": 400, "line": 148 }, + { "ip": 411, "line": 149 }, + { "ip": 422, "line": 150 }, + { "ip": 441, "line": 168 }, + { "ip": 446, "line": 169 }, + { "ip": 462, "line": 172 }, + { "ip": 483, "line": 175 }, + { "ip": 488, "line": 176 }, + { "ip": 493, "line": 177 }, + { "ip": 499, "line": 178 } ], - "sourceTags": "97:101:pv;163:167:pv" + "sourceTags": "101:105:pv;167:171:pv" }, "compiler": { "name": "cashc", @@ -117,5 +119,5 @@ "enforceLocktimeGuard": true } }, - "updatedAt": "2026-10-04T13:36:52.297Z" + "updatedAt": "2026-10-04T14:37:27.164Z" } \ No newline at end of file diff --git a/contracts/subscription.cash b/contracts/subscription.cash index 45d37d1..205ff88 100644 --- a/contracts/subscription.cash +++ b/contracts/subscription.cash @@ -12,12 +12,19 @@ pragma cashscript ^0.13.0; // · the balance stays theirs — cancel() needs only their signature, no // notice, no cooperation, and returns whatever is left; // · the host cannot take more than one period's price per period — pull() -// pays exactly the host's lock at least `priceSats`, sends the rest back to -// this same script, and can only run on an output that is `periodBlocks` -// old, which the re-created output is not; +// pays the host's lock exactly `priceSats`, sends the rest back to this +// same script, and can only run on an output that is `periodBlocks` old, +// which the re-created output is not; // · nobody else gains anything — pull() needs no signature, so the host does -// not have to be online with a key, but its only possible beneficiary is -// the host's lock. +// not have to be online with a key, but it has at most two outputs: the +// price to the host, exactly, and the rest back to this script. Whatever +// the fee allowance does not use goes to the miner, not to whoever built +// the transaction. +// +// ONE BALANCE, ONE OUTPUT. The once-per-period limit is a property of an +// output, not of an address. A buyer who pays the same address twice has two +// balances, and each can be drawn every period. To add funds: cancel and fund +// again, in one transaction if you like. // // A pull spends this input ALONE. With two inputs, one "price to the host" // output and one "rest back to the script" output could satisfy both, and the @@ -30,12 +37,17 @@ pragma cashscript ^0.13.0; contract Subscription(bytes20 buyerPkh, bytes hostLock, int priceSats, int periodBlocks, int maxFeeSats) { // One period's payment to the host. function pull() { + // A relative lock is a 16-bit block count. Anything outside that range + // would be read as something else entirely: only its low bits, a + // number of seconds, or no lock at all. + require(within(periodBlocks, 1, 65536)); require(this.age >= periodBlocks); require(tx.inputs.length == 1); + require(tx.outputs.length <= 2); int balance = tx.inputs[0].value; require(tx.outputs[0].lockingBytecode == hostLock); - require(tx.outputs[0].value >= priceSats); + require(tx.outputs[0].value == priceSats); require(tx.outputs[0].tokenCategory == 0x); // Everything else stays in the subscription, less a bounded fee. When @@ -45,6 +57,10 @@ contract Subscription(bytes20 buyerPkh, bytes hostLock, int priceSats, int perio require(tx.outputs[1].lockingBytecode == tx.inputs[0].lockingBytecode); require(tx.outputs[1].value >= rest); require(tx.outputs[1].tokenCategory == 0x); + } else { + // Nothing is returned, so there must be nothing else: the leftover + // is the fee, not a second output for whoever built the draw. + require(tx.outputs.length == 1); } } diff --git a/contracts/subscription.json b/contracts/subscription.json index 3024341..7baeae2 100644 --- a/contracts/subscription.json +++ b/contracts/subscription.json @@ -17,24 +17,27 @@ ] } ], - "bytecode": "OP_5 OP_PICK OP_0 OP_NUMEQUAL OP_IF OP_3 OP_ROLL OP_CHECKSEQUENCEVERIFY OP_DROP OP_TXINPUTCOUNT OP_1 OP_NUMEQUALVERIFY OP_0 OP_UTXOVALUE OP_0 OP_OUTPUTBYTECODE OP_3 OP_ROLL OP_EQUALVERIFY OP_0 OP_OUTPUTVALUE OP_3 OP_PICK OP_GREATERTHANOREQUAL OP_VERIFY OP_0 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_ROT OP_SUB OP_ROT OP_SUB OP_DUP 2202 OP_GREATERTHANOREQUAL OP_IF OP_1 OP_OUTPUTBYTECODE OP_0 OP_UTXOBYTECODE OP_EQUALVERIFY OP_1 OP_OUTPUTVALUE OP_OVER OP_GREATERTHANOREQUAL OP_VERIFY OP_1 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_ENDIF OP_2DROP OP_DROP OP_1 OP_ELSE OP_5 OP_ROLL OP_1 OP_NUMEQUALVERIFY OP_5 OP_PICK OP_HASH160 OP_EQUALVERIFY OP_2ROT OP_CHECKSIGVERIFY OP_2DROP OP_2DROP OP_1 OP_ENDIF", - "source": "pragma cashscript ^0.13.0;\n\n// Subscription — a prepaid balance the host can draw from once per period,\n// and the buyer can take back at any time.\n// (Argus/src/lib/plans.js SUBSCRIPTION_ROADMAP; ROADMAP-MAINNET.md §9 step 4.)\n//\n// STATUS: chipnet draft. NOT REVIEWED.\n//\n// The buyer funds this script. Three things then hold, and the buyer can check\n// each from the script alone:\n//\n// · the balance stays theirs — cancel() needs only their signature, no\n// notice, no cooperation, and returns whatever is left;\n// · the host cannot take more than one period's price per period — pull()\n// pays exactly the host's lock at least `priceSats`, sends the rest back to\n// this same script, and can only run on an output that is `periodBlocks`\n// old, which the re-created output is not;\n// · nobody else gains anything — pull() needs no signature, so the host does\n// not have to be online with a key, but its only possible beneficiary is\n// the host's lock.\n//\n// A pull spends this input ALONE. With two inputs, one \"price to the host\"\n// output and one \"rest back to the script\" output could satisfy both, and the\n// host would collect two balances for one payment. One input means the fee\n// comes out of the balance too, bounded by `maxFeeSats`.\n//\n// The first period is not special-cased: the balance is drawn one period\n// after it is funded. A host that wants payment up front takes the first\n// period in the funding transaction itself.\ncontract Subscription(bytes20 buyerPkh, bytes hostLock, int priceSats, int periodBlocks, int maxFeeSats) {\n // One period's payment to the host.\n function pull() {\n require(this.age >= periodBlocks);\n require(tx.inputs.length == 1);\n\n int balance = tx.inputs[0].value;\n require(tx.outputs[0].lockingBytecode == hostLock);\n require(tx.outputs[0].value >= priceSats);\n require(tx.outputs[0].tokenCategory == 0x);\n\n // Everything else stays in the subscription, less a bounded fee. When\n // what would remain is too small to be an output, it is the fee.\n int rest = balance - priceSats - maxFeeSats;\n if (rest >= 546) {\n require(tx.outputs[1].lockingBytecode == tx.inputs[0].lockingBytecode);\n require(tx.outputs[1].value >= rest);\n require(tx.outputs[1].tokenCategory == 0x);\n }\n }\n\n // The buyer ends it and takes the balance. No conditions on where it goes.\n function cancel(pubkey pk, sig s) {\n require(hash160(pk) == buyerPkh);\n require(checkSig(s, pk));\n }\n}\n", - "fingerprint": "ed3b45444d0d4a41357fcd312e039baf769a8bc2e8a2d66c19624cae5078f046", + "bytecode": "OP_5 OP_PICK OP_0 OP_NUMEQUAL OP_IF OP_3 OP_PICK OP_1 000001 OP_WITHIN OP_VERIFY OP_3 OP_ROLL OP_CHECKSEQUENCEVERIFY OP_DROP OP_TXINPUTCOUNT OP_1 OP_NUMEQUALVERIFY OP_TXOUTPUTCOUNT OP_2 OP_LESSTHANOREQUAL OP_VERIFY OP_0 OP_UTXOVALUE OP_0 OP_OUTPUTBYTECODE OP_3 OP_ROLL OP_EQUALVERIFY OP_0 OP_OUTPUTVALUE OP_3 OP_PICK OP_NUMEQUALVERIFY OP_0 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_ROT OP_SUB OP_ROT OP_SUB OP_DUP 2202 OP_GREATERTHANOREQUAL OP_IF OP_1 OP_OUTPUTBYTECODE OP_0 OP_UTXOBYTECODE OP_EQUALVERIFY OP_1 OP_OUTPUTVALUE OP_OVER OP_GREATERTHANOREQUAL OP_VERIFY OP_1 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_ELSE OP_TXOUTPUTCOUNT OP_1 OP_NUMEQUALVERIFY OP_ENDIF OP_2DROP OP_DROP OP_1 OP_ELSE OP_5 OP_ROLL OP_1 OP_NUMEQUALVERIFY OP_5 OP_PICK OP_HASH160 OP_EQUALVERIFY OP_2ROT OP_CHECKSIGVERIFY OP_2DROP OP_2DROP OP_1 OP_ENDIF", + "source": "pragma cashscript ^0.13.0;\n\n// Subscription — a prepaid balance the host can draw from once per period,\n// and the buyer can take back at any time.\n// (Argus/src/lib/plans.js SUBSCRIPTION_ROADMAP; ROADMAP-MAINNET.md §9 step 4.)\n//\n// STATUS: chipnet draft. NOT REVIEWED.\n//\n// The buyer funds this script. Three things then hold, and the buyer can check\n// each from the script alone:\n//\n// · the balance stays theirs — cancel() needs only their signature, no\n// notice, no cooperation, and returns whatever is left;\n// · the host cannot take more than one period's price per period — pull()\n// pays exactly the host's lock at least `priceSats`, sends the rest back to\n// this same script, and can only run on an output that is `periodBlocks`\n// old, which the re-created output is not;\n// · nobody else gains anything — pull() needs no signature, so the host does\n// not have to be online with a key, but it has at most two outputs: the\n// price to the host, exactly, and the rest back to this script. Whatever\n// the fee allowance does not use goes to the miner, not to whoever built\n// the transaction.\n//\n// ONE BALANCE, ONE OUTPUT. The once-per-period limit is a property of an\n// output, not of an address. A buyer who pays the same address twice has two\n// balances, and each can be drawn every period. To add funds: cancel and fund\n// again, in one transaction if you like.\n//\n// A pull spends this input ALONE. With two inputs, one \"price to the host\"\n// output and one \"rest back to the script\" output could satisfy both, and the\n// host would collect two balances for one payment. One input means the fee\n// comes out of the balance too, bounded by `maxFeeSats`.\n//\n// The first period is not special-cased: the balance is drawn one period\n// after it is funded. A host that wants payment up front takes the first\n// period in the funding transaction itself.\ncontract Subscription(bytes20 buyerPkh, bytes hostLock, int priceSats, int periodBlocks, int maxFeeSats) {\n // One period's payment to the host.\n function pull() {\n // A relative lock is a 16-bit block count. Anything outside that range\n // would be read as something else entirely: only its low bits, a\n // number of seconds, or no lock at all.\n require(within(periodBlocks, 1, 65536));\n require(this.age >= periodBlocks);\n require(tx.inputs.length == 1);\n require(tx.outputs.length <= 2);\n\n int balance = tx.inputs[0].value;\n require(tx.outputs[0].lockingBytecode == hostLock);\n require(tx.outputs[0].value == priceSats);\n require(tx.outputs[0].tokenCategory == 0x);\n\n // Everything else stays in the subscription, less a bounded fee. When\n // what would remain is too small to be an output, it is the fee.\n int rest = balance - priceSats - maxFeeSats;\n if (rest >= 546) {\n require(tx.outputs[1].lockingBytecode == tx.inputs[0].lockingBytecode);\n require(tx.outputs[1].value >= rest);\n require(tx.outputs[1].tokenCategory == 0x);\n } else {\n // Nothing is returned, so there must be nothing else: the leftover\n // is the fee, not a second output for whoever built the draw.\n require(tx.outputs.length == 1);\n }\n }\n\n // The buyer ends it and takes the balance. No conditions on where it goes.\n function cancel(pubkey pk, sig s) {\n require(hash160(pk) == buyerPkh);\n require(checkSig(s, pk));\n }\n}\n", + "fingerprint": "d5796aeddba44e7674c0caad36eb5c7ab658a0a4fe9c7ebf8701acb5cc8ba302", "debug": { - "bytecode": "5579009c63537ab275c3519d00c600cd537a8800cc5379a26900d100887b947b9476022202a26351cd00c78851cc78a26951d10088686d755167557a519d5579a98871ad6d6d5168", - "sourceMap": "32:4:49:5;;;;;33:28:33:40;;:8::42:1;;34:16:34:32:0;:36::37;:8::39:1;36:32:36:33:0;:22::40:1;37:27:37:28:0;:16::45:1;:49::57:0;;:8::59:1;38:27:38:28:0;:16::35:1;:39::48:0;;:16:::1;:8::50;39:27:39:28:0;:16::43:1;:47::49:0;:8::51:1;43:29:43:38:0;:19:::1;:41::51:0;:19:::1;44:12:44:16:0;:20::23;:12:::1;:25:48:9:0;45:31:45:32;:20::49:1;:63::64:0;:53::81:1;:12::83;46:31:46:32:0;:20::39:1;:43::47:0;:20:::1;:12::49;47:31:47:32:0;:20::47:1;:51::53:0;:12::55:1;44:25:48:9;32:20:49:5;;;:4;52::55::0;;;;53:24:53:26;;:16::27:1;:8::41;54:25:54:30:0;:8::33:1;52:38:55:5;;;30:0:56:1", + "bytecode": "5579009c6353795103000001a569537ab275c3519dc452a16900c600cd537a8800cc53799d00d100887b947b9476022202a26351cd00c78851cc78a26951d1008867c4519d686d755167557a519d5579a98871ad6d6d5168", + "sourceMap": "39:4:65:5;;;;;43:23:43:35;;:37::38;:40::45;:16::46:1;:8::48;44:28:44:40:0;;:8::42:1;;45:16:45:32:0;:36::37;:8::39:1;46:16:46:33:0;:37::38;:16:::1;:8::40;48:32:48:33:0;:22::40:1;49:27:49:28:0;:16::45:1;:49::57:0;;:8::59:1;50:27:50:28:0;:16::35:1;:39::48:0;;:8::50:1;51:27:51:28:0;:16::43:1;:47::49:0;:8::51:1;55:29:55:38:0;:19:::1;:41::51:0;:19:::1;56:12:56:16:0;:20::23;:12:::1;:25:60:9:0;57:31:57:32;:20::49:1;:63::64:0;:53::81:1;:12::83;58:31:58:32:0;:20::39:1;:43::47:0;:20:::1;:12::49;59:31:59:32:0;:20::47:1;:51::53:0;:12::55:1;60:15:64:9:0;63:20:63:37;:41::42;:12::44:1;60:15:64:9;39:20:65:5;;;:4;68::71::0;;;;69:24:69:26;;:16::27:1;:8::41;70:25:70:30:0;:8::33:1;68:38:71:5;;;37:0:72:1", "logs": [], "requires": [ - { "ip": 12, "line": 33 }, - { "ip": 16, "line": 34 }, - { "ip": 23, "line": 37 }, - { "ip": 29, "line": 38 }, - { "ip": 33, "line": 39 }, - { "ip": 46, "line": 45 }, - { "ip": 51, "line": 46 }, - { "ip": 55, "line": 47 }, - { "ip": 68, "line": 53 }, - { "ip": 70, "line": 54 } + { "ip": 15, "line": 43 }, + { "ip": 18, "line": 44 }, + { "ip": 22, "line": 45 }, + { "ip": 26, "line": 46 }, + { "ip": 33, "line": 49 }, + { "ip": 38, "line": 50 }, + { "ip": 42, "line": 51 }, + { "ip": 55, "line": 57 }, + { "ip": 60, "line": 58 }, + { "ip": 64, "line": 59 }, + { "ip": 68, "line": 63 }, + { "ip": 81, "line": 69 }, + { "ip": 83, "line": 70 } ] }, "compiler": { @@ -45,5 +48,5 @@ "enforceLocktimeGuard": true } }, - "updatedAt": "2026-10-04T14:02:00.557Z" + "updatedAt": "2026-10-04T14:39:23.518Z" } \ No newline at end of file diff --git a/src/lib/delivery-escrow-artifact.js b/src/lib/delivery-escrow-artifact.js index ffba9e2..6c2ce83 100644 --- a/src/lib/delivery-escrow-artifact.js +++ b/src/lib/delivery-escrow-artifact.js @@ -51,7 +51,7 @@ export const DELIVERY_ESCROW = { ] } ], - "bytecode": "OP_7 OP_PICK OP_0 OP_NUMEQUAL OP_IF OP_INPUTINDEX OP_1 OP_NUMEQUALVERIFY OP_0 OP_OUTPOINTINDEX OP_0 OP_NUMEQUALVERIFY OP_0 OP_OUTPOINTTXHASH OP_0 OP_OUTPUTBYTECODE OP_3 OP_ROLL OP_EQUALVERIFY OP_0 OP_OUTPUTTOKENCATEGORY OP_EQUALVERIFY OP_0 OP_OUTPUTTOKENCOMMITMENT OP_ROT OP_EQUALVERIFY OP_0 OP_OUTPUTTOKENAMOUNT OP_0 OP_NUMEQUALVERIFY OP_1 OP_OUTPUTBYTECODE OP_ROT OP_EQUALVERIFY OP_1 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_2 OP_OUTPUTBYTECODE OP_ROT OP_EQUALVERIFY OP_2 OP_OUTPUTVALUE OP_ROT OP_GREATERTHANOREQUAL OP_VERIFY OP_2 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_TXOUTPUTCOUNT OP_6 OP_LESSTHANOREQUAL OP_VERIFY OP_TXOUTPUTCOUNT OP_3 OP_GREATERTHAN OP_IF OP_3 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_ENDIF OP_TXOUTPUTCOUNT OP_4 OP_GREATERTHAN OP_IF OP_4 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_ENDIF OP_TXOUTPUTCOUNT OP_5 OP_GREATERTHAN OP_IF OP_5 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_ENDIF OP_2DROP OP_DROP OP_1 OP_ELSE OP_7 OP_ROLL OP_1 OP_NUMEQUALVERIFY OP_7 OP_PICK OP_HASH160 OP_EQUALVERIFY OP_7 OP_ROLL OP_7 OP_ROLL OP_CHECKSIGVERIFY OP_5 OP_ROLL OP_CHECKSEQUENCEVERIFY OP_2DROP OP_2DROP OP_2DROP OP_1 OP_ENDIF", + "bytecode": "OP_7 OP_PICK OP_0 OP_NUMEQUAL OP_IF OP_INPUTINDEX OP_1 OP_NUMEQUALVERIFY OP_0 OP_OUTPOINTINDEX OP_0 OP_NUMEQUALVERIFY OP_0 OP_OUTPOINTTXHASH OP_0 OP_OUTPUTBYTECODE OP_3 OP_ROLL OP_EQUALVERIFY OP_0 OP_OUTPUTTOKENCATEGORY OP_EQUALVERIFY OP_0 OP_OUTPUTTOKENCOMMITMENT OP_ROT OP_EQUALVERIFY OP_0 OP_OUTPUTTOKENAMOUNT OP_0 OP_NUMEQUALVERIFY OP_1 OP_OUTPUTBYTECODE OP_ROT OP_EQUALVERIFY OP_1 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_2 OP_OUTPUTBYTECODE OP_ROT OP_EQUALVERIFY OP_2 OP_OUTPUTVALUE OP_ROT OP_GREATERTHANOREQUAL OP_VERIFY OP_2 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_TXOUTPUTCOUNT OP_4 OP_LESSTHANOREQUAL OP_VERIFY OP_TXOUTPUTCOUNT OP_3 OP_GREATERTHAN OP_IF OP_3 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_3 OP_OUTPUTBYTECODE OP_1 OP_SPLIT OP_DROP 6a OP_EQUAL OP_NOT OP_VERIFY OP_ENDIF OP_2DROP OP_DROP OP_1 OP_ELSE OP_7 OP_ROLL OP_1 OP_NUMEQUALVERIFY OP_7 OP_PICK OP_HASH160 OP_EQUALVERIFY OP_7 OP_ROLL OP_7 OP_ROLL OP_CHECKSIGVERIFY OP_5 OP_PICK OP_1 000001 OP_WITHIN OP_VERIFY OP_5 OP_ROLL OP_CHECKSEQUENCEVERIFY OP_2DROP OP_2DROP OP_2DROP OP_1 OP_ENDIF", "compiler": { "name": "cashc", "version": "0.13.3", diff --git a/src/lib/escrow-covenant.js b/src/lib/escrow-covenant.js index d5046d6..3e1bde7 100644 --- a/src/lib/escrow-covenant.js +++ b/src/lib/escrow-covenant.js @@ -13,7 +13,7 @@ // both sides can recompute it and neither can change a term afterwards. import { binToHex, utf8ToBin, hexToBin } from "@bitauth/libauth"; -import { NETWORK } from "./resolver-web.js"; +import { NETWORK, cosignRequired } from "./resolver-web.js"; import { DELIVERY_ESCROW } from "./delivery-escrow-artifact.js"; import { asmToBytecode, selectorsOf, redeemScript, pushInt, push, bin, pkhLock, p2sh32Lock, lockToAddress, @@ -50,6 +50,18 @@ export function escrowTerms({ name, records = {}, buyerPkh, certLock, beaconLock refundAfter, }; } +/** + * An escrow cannot deliver under a TLD that requires its owner's co-signature: + * that rule wants the TLD's certificate in an output of the registration, and + * the contract allows no token but the name's own. The payment would still be + * collectable — for a registration no resolver accepts. Call this with the + * TLD's entry (fetchTldMap) before showing a buyer an escrow address. + */ +export function assertTldAllowsEscrow(tldEntry, height = 0, tld = "") { + if (tldEntry && cosignRequired(tldEntry, height)) { + throw new Error(`names under .${tld || "this TLD"} need the TLD owner's co-signature, which a delivery escrow cannot carry — register through the co-sign flow instead`); + } +} export const escrowRedeem = (t) => redeemScript(BODY, [ push(bin(t.buyerPkh)), push(t.certLock), push(t.nameCommitment), push(t.payloadLock), push(t.beaconLock), pushInt(t.beaconDust), pushInt(t.refundAfter), diff --git a/src/lib/name-registry-artifact.js b/src/lib/name-registry-artifact.js index 9768df3..f1a9612 100644 --- a/src/lib/name-registry-artifact.js +++ b/src/lib/name-registry-artifact.js @@ -127,15 +127,11 @@ export const NAME_REGISTRY = { { "name": "newCode", "type": "bytes" - }, - { - "name": "newState", - "type": "bytes" } ] } ], - "bytecode": "OP_11 OP_PICK OP_0 OP_NUMEQUAL OP_IF OP_12 OP_PICK OP_HASH160 OP_11 OP_PICK OP_EQUALVERIFY OP_13 OP_ROLL OP_13 OP_ROLL OP_CHECKSIGVERIFY OP_12 OP_PICK OP_1 OP_GREATERTHANOREQUAL OP_VERIFY OP_9 OP_ROLL OP_BIN2NUM OP_12 OP_PICK OP_9 OP_ROLL OP_MUL OP_ADD 14 OP_10 OP_ROLL OP_CAT OP_4 OP_CAT OP_SWAP OP_4 OP_NUM2BIN OP_CAT OP_ACTIVEBYTECODE 1a OP_SPLIT OP_NIP OP_CAT OP_HASH256 OP_0 OP_OUTPUTBYTECODE aa20 OP_ROT OP_CAT 87 OP_CAT OP_EQUALVERIFY OP_0 OP_OUTPUTTOKENCATEGORY OP_INPUTINDEX OP_UTXOTOKENCATEGORY OP_EQUALVERIFY OP_0 OP_OUTPUTTOKENCOMMITMENT OP_INPUTINDEX OP_UTXOTOKENCOMMITMENT OP_EQUALVERIFY OP_0 OP_OUTPUTVALUE OP_INPUTINDEX OP_UTXOVALUE OP_GREATERTHANOREQUAL OP_VERIFY OP_1 OP_OUTPUTBYTECODE OP_EQUALVERIFY OP_1 OP_OUTPUTVALUE OP_3 OP_ROLL OP_9 OP_ROLL OP_MUL OP_GREATERTHANOREQUAL OP_VERIFY OP_1 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_2DROP OP_2DROP OP_2DROP OP_DROP OP_1 OP_ELSE OP_11 OP_PICK OP_1 OP_NUMEQUAL OP_IF OP_14 OP_ROLL OP_SIZE 14 OP_EQUALVERIFY OP_13 OP_PICK OP_HASH160 OP_12 OP_ROLL OP_EQUALVERIFY OP_13 OP_ROLL OP_13 OP_ROLL OP_CHECKSIGVERIFY 14 OP_SWAP OP_CAT OP_4 OP_CAT OP_10 OP_ROLL OP_CAT OP_ACTIVEBYTECODE 1a OP_SPLIT OP_NIP OP_CAT OP_HASH256 OP_0 OP_OUTPUTBYTECODE aa20 OP_ROT OP_CAT 87 OP_CAT OP_EQUALVERIFY OP_0 OP_OUTPUTTOKENCATEGORY OP_INPUTINDEX OP_UTXOTOKENCATEGORY OP_EQUALVERIFY OP_0 OP_OUTPUTTOKENCOMMITMENT OP_INPUTINDEX OP_UTXOTOKENCOMMITMENT OP_EQUALVERIFY OP_0 OP_OUTPUTVALUE OP_INPUTINDEX OP_UTXOVALUE OP_GREATERTHANOREQUAL OP_VERIFY OP_2DROP OP_2DROP OP_2DROP OP_2DROP OP_2DROP OP_1 OP_ELSE OP_11 OP_PICK OP_2 OP_NUMEQUAL OP_IF OP_12 OP_ROLL OP_SIZE 14 OP_EQUALVERIFY OP_10 OP_ROLL OP_BIN2NUM OP_DUP OP_CHECKLOCKTIMEVERIFY OP_DROP OP_TXLOCKTIME OP_DUP 0065cd1d OP_LESSTHAN OP_VERIFY OP_SWAP OP_9 OP_PICK OP_ADD OP_8 OP_PICK OP_0 OP_2OVER OP_LESSTHAN OP_IF OP_10 OP_PICK OP_10 OP_PICK OP_12 OP_PICK OP_SUB OP_4 OP_PICK OP_6 OP_PICK OP_SUB OP_MUL OP_13 OP_PICK OP_DIV OP_ADD OP_ROT OP_DROP OP_SWAP OP_OVER OP_14 OP_PICK OP_MUL 64 OP_DIV OP_NIP OP_ENDIF OP_INPUTINDEX OP_UTXOTOKENCOMMITMENT 11 OP_ROLL OP_CAT OP_HASH256 14 OP_6 OP_PICK OP_CAT 20 OP_CAT OP_SWAP OP_CAT OP_8 OP_ROLL OP_CAT OP_1 OP_UTXOBYTECODE aa20 OP_ROT OP_HASH256 OP_CAT 87 OP_CAT OP_EQUALVERIFY 14 OP_5 OP_ROLL OP_CAT OP_4 OP_CAT OP_4 OP_ROLL OP_11 OP_ROLL OP_ADD OP_4 OP_NUM2BIN OP_CAT OP_ACTIVEBYTECODE 1a OP_SPLIT OP_NIP OP_CAT OP_HASH256 OP_0 OP_OUTPUTBYTECODE aa20 OP_ROT OP_CAT 87 OP_CAT OP_EQUALVERIFY OP_0 OP_OUTPUTTOKENCATEGORY OP_INPUTINDEX OP_UTXOTOKENCATEGORY OP_EQUALVERIFY OP_0 OP_OUTPUTTOKENCOMMITMENT OP_INPUTINDEX OP_UTXOTOKENCOMMITMENT OP_EQUALVERIFY OP_0 OP_OUTPUTVALUE OP_INPUTINDEX OP_UTXOVALUE OP_GREATERTHANOREQUAL OP_VERIFY OP_1 OP_OUTPUTBYTECODE OP_4 OP_ROLL OP_EQUALVERIFY OP_1 OP_OUTPUTVALUE OP_ROT OP_2 OP_PICK OP_SUB OP_GREATERTHANOREQUAL OP_VERIFY OP_1 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_DUP OP_0 OP_GREATERTHAN OP_IF OP_2 OP_OUTPUTBYTECODE 76a914 OP_10 OP_PICK OP_CAT 88ac OP_CAT OP_EQUALVERIFY OP_2 OP_OUTPUTVALUE OP_OVER OP_GREATERTHANOREQUAL OP_VERIFY OP_2 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_ENDIF OP_2DROP OP_2DROP OP_2DROP OP_2DROP OP_2DROP OP_1 OP_ELSE OP_11 OP_PICK OP_3 OP_NUMEQUAL OP_IF OP_12 OP_PICK OP_HASH160 OP_11 OP_ROLL OP_EQUALVERIFY OP_12 OP_ROLL OP_12 OP_ROLL OP_CHECKSIGVERIFY OP_INPUTINDEX OP_UTXOTOKENCATEGORY OP_TXOUTPUTCOUNT OP_4 OP_LESSTHANOREQUAL OP_VERIFY OP_0 OP_OUTPUTTOKENCATEGORY OP_OVER OP_EQUAL OP_NOT OP_VERIFY OP_TXOUTPUTCOUNT OP_1 OP_GREATERTHAN OP_IF OP_1 OP_OUTPUTTOKENCATEGORY OP_OVER OP_EQUAL OP_NOT OP_VERIFY OP_ENDIF OP_TXOUTPUTCOUNT OP_2 OP_GREATERTHAN OP_IF OP_2 OP_OUTPUTTOKENCATEGORY OP_OVER OP_EQUAL OP_NOT OP_VERIFY OP_ENDIF OP_TXOUTPUTCOUNT OP_3 OP_GREATERTHAN OP_IF OP_3 OP_OUTPUTTOKENCATEGORY OP_OVER OP_EQUAL OP_NOT OP_VERIFY OP_ENDIF OP_2DROP OP_2DROP OP_2DROP OP_2DROP OP_2DROP OP_2DROP OP_1 OP_ELSE OP_11 OP_ROLL OP_4 OP_NUMEQUALVERIFY OP_11 OP_PICK OP_HASH160 OP_11 OP_ROLL OP_EQUALVERIFY OP_11 OP_ROLL OP_11 OP_ROLL OP_CHECKSIGVERIFY OP_10 OP_ROLL OP_11 OP_PICK OP_HASH256 OP_3 OP_ROLL OP_CHECKDATASIGVERIFY OP_10 OP_ROLL OP_10 OP_ROLL OP_CAT OP_HASH256 OP_0 OP_OUTPUTBYTECODE aa20 OP_ROT OP_CAT 87 OP_CAT OP_EQUALVERIFY OP_0 OP_OUTPUTTOKENCATEGORY OP_INPUTINDEX OP_UTXOTOKENCATEGORY OP_EQUALVERIFY OP_0 OP_OUTPUTTOKENCOMMITMENT OP_INPUTINDEX OP_UTXOTOKENCOMMITMENT OP_EQUALVERIFY OP_0 OP_OUTPUTVALUE OP_INPUTINDEX OP_UTXOVALUE OP_GREATERTHANOREQUAL OP_VERIFY OP_2DROP OP_2DROP OP_2DROP OP_2DROP OP_DROP OP_1 OP_ENDIF OP_ENDIF OP_ENDIF OP_ENDIF", + "bytecode": "OP_11 OP_PICK OP_0 OP_NUMEQUAL OP_IF OP_12 OP_PICK OP_HASH160 OP_11 OP_PICK OP_EQUALVERIFY OP_13 OP_ROLL OP_13 OP_ROLL OP_CHECKSIGVERIFY OP_12 OP_PICK OP_1 OP_GREATERTHANOREQUAL OP_VERIFY OP_9 OP_ROLL OP_BIN2NUM OP_12 OP_PICK OP_9 OP_ROLL OP_MUL OP_ADD OP_DUP 0065cd1d OP_LESSTHAN OP_VERIFY 14 OP_10 OP_ROLL OP_CAT OP_4 OP_CAT OP_SWAP OP_4 OP_NUM2BIN OP_CAT OP_ACTIVEBYTECODE 1a OP_SPLIT OP_NIP OP_CAT OP_HASH256 OP_0 OP_OUTPUTBYTECODE aa20 OP_ROT OP_CAT 87 OP_CAT OP_EQUALVERIFY OP_0 OP_OUTPUTTOKENCATEGORY OP_INPUTINDEX OP_UTXOTOKENCATEGORY OP_EQUALVERIFY OP_0 OP_OUTPUTTOKENCOMMITMENT OP_INPUTINDEX OP_UTXOTOKENCOMMITMENT OP_EQUALVERIFY OP_0 OP_OUTPUTVALUE OP_INPUTINDEX OP_UTXOVALUE OP_GREATERTHANOREQUAL OP_VERIFY OP_1 OP_OUTPUTBYTECODE OP_EQUALVERIFY OP_1 OP_OUTPUTVALUE OP_3 OP_ROLL OP_9 OP_ROLL OP_MUL OP_GREATERTHANOREQUAL OP_VERIFY OP_1 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_2DROP OP_2DROP OP_2DROP OP_DROP OP_1 OP_ELSE OP_11 OP_PICK OP_1 OP_NUMEQUAL OP_IF OP_14 OP_ROLL OP_SIZE 14 OP_EQUALVERIFY OP_13 OP_PICK OP_HASH160 OP_12 OP_ROLL OP_EQUALVERIFY OP_13 OP_ROLL OP_13 OP_ROLL OP_CHECKSIGVERIFY 14 OP_SWAP OP_CAT OP_4 OP_CAT OP_10 OP_ROLL OP_CAT OP_ACTIVEBYTECODE 1a OP_SPLIT OP_NIP OP_CAT OP_HASH256 OP_0 OP_OUTPUTBYTECODE aa20 OP_ROT OP_CAT 87 OP_CAT OP_EQUALVERIFY OP_0 OP_OUTPUTTOKENCATEGORY OP_INPUTINDEX OP_UTXOTOKENCATEGORY OP_EQUALVERIFY OP_0 OP_OUTPUTTOKENCOMMITMENT OP_INPUTINDEX OP_UTXOTOKENCOMMITMENT OP_EQUALVERIFY OP_0 OP_OUTPUTVALUE OP_INPUTINDEX OP_UTXOVALUE OP_GREATERTHANOREQUAL OP_VERIFY OP_2DROP OP_2DROP OP_2DROP OP_2DROP OP_2DROP OP_1 OP_ELSE OP_11 OP_PICK OP_2 OP_NUMEQUAL OP_IF OP_12 OP_ROLL OP_SIZE 14 OP_EQUALVERIFY OP_10 OP_ROLL OP_BIN2NUM OP_DUP OP_CHECKLOCKTIMEVERIFY OP_DROP OP_TXLOCKTIME OP_DUP 0065cd1d OP_LESSTHAN OP_VERIFY OP_SWAP OP_9 OP_PICK OP_ADD OP_8 OP_PICK OP_0 OP_2OVER OP_LESSTHAN OP_IF OP_10 OP_PICK OP_10 OP_PICK OP_12 OP_PICK OP_SUB OP_4 OP_PICK OP_6 OP_PICK OP_SUB OP_MUL OP_13 OP_PICK OP_DIV OP_ADD OP_ROT OP_DROP OP_SWAP OP_OVER OP_14 OP_PICK OP_MUL 64 OP_DIV OP_NIP OP_ENDIF OP_16 OP_PICK OP_SIZE OP_NIP 20 OP_NUMEQUALVERIFY OP_INPUTINDEX OP_UTXOTOKENCOMMITMENT 11 OP_ROLL OP_CAT OP_HASH256 14 OP_6 OP_PICK OP_CAT 20 OP_CAT OP_SWAP OP_CAT OP_8 OP_ROLL OP_CAT OP_1 OP_UTXOBYTECODE aa20 OP_ROT OP_HASH256 OP_CAT 87 OP_CAT OP_EQUALVERIFY OP_3 OP_PICK OP_12 OP_PICK OP_ADD 0065cd1d OP_LESSTHAN OP_VERIFY 14 OP_5 OP_ROLL OP_CAT OP_4 OP_CAT OP_4 OP_ROLL OP_11 OP_ROLL OP_ADD OP_4 OP_NUM2BIN OP_CAT OP_ACTIVEBYTECODE 1a OP_SPLIT OP_NIP OP_CAT OP_HASH256 OP_0 OP_OUTPUTBYTECODE aa20 OP_ROT OP_CAT 87 OP_CAT OP_EQUALVERIFY OP_0 OP_OUTPUTTOKENCATEGORY OP_INPUTINDEX OP_UTXOTOKENCATEGORY OP_EQUALVERIFY OP_0 OP_OUTPUTTOKENCOMMITMENT OP_INPUTINDEX OP_UTXOTOKENCOMMITMENT OP_EQUALVERIFY OP_0 OP_OUTPUTVALUE OP_INPUTINDEX OP_UTXOVALUE OP_GREATERTHANOREQUAL OP_VERIFY OP_1 OP_OUTPUTBYTECODE OP_4 OP_ROLL OP_EQUALVERIFY OP_1 OP_OUTPUTVALUE OP_ROT OP_2 OP_PICK OP_SUB OP_GREATERTHANOREQUAL OP_VERIFY OP_1 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_DUP OP_0 OP_GREATERTHAN OP_IF OP_2 OP_OUTPUTBYTECODE 76a914 OP_10 OP_PICK OP_CAT 88ac OP_CAT OP_EQUALVERIFY OP_2 OP_OUTPUTVALUE OP_OVER OP_GREATERTHANOREQUAL OP_VERIFY OP_2 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_ENDIF OP_2DROP OP_2DROP OP_2DROP OP_2DROP OP_2DROP OP_1 OP_ELSE OP_11 OP_PICK OP_3 OP_NUMEQUAL OP_IF OP_12 OP_PICK OP_HASH160 OP_11 OP_ROLL OP_EQUALVERIFY OP_12 OP_ROLL OP_12 OP_ROLL OP_CHECKSIGVERIFY OP_INPUTINDEX OP_UTXOTOKENCATEGORY OP_TXOUTPUTCOUNT OP_4 OP_LESSTHANOREQUAL OP_VERIFY OP_0 OP_OUTPUTTOKENCATEGORY OP_OVER OP_EQUAL OP_NOT OP_VERIFY OP_TXOUTPUTCOUNT OP_1 OP_GREATERTHAN OP_IF OP_1 OP_OUTPUTTOKENCATEGORY OP_OVER OP_EQUAL OP_NOT OP_VERIFY OP_ENDIF OP_TXOUTPUTCOUNT OP_2 OP_GREATERTHAN OP_IF OP_2 OP_OUTPUTTOKENCATEGORY OP_OVER OP_EQUAL OP_NOT OP_VERIFY OP_ENDIF OP_TXOUTPUTCOUNT OP_3 OP_GREATERTHAN OP_IF OP_3 OP_OUTPUTTOKENCATEGORY OP_OVER OP_EQUAL OP_NOT OP_VERIFY OP_ENDIF OP_2DROP OP_2DROP OP_2DROP OP_2DROP OP_2DROP OP_2DROP OP_1 OP_ELSE OP_11 OP_ROLL OP_4 OP_NUMEQUALVERIFY OP_11 OP_PICK OP_HASH160 OP_11 OP_PICK OP_EQUALVERIFY OP_12 OP_ROLL OP_12 OP_ROLL OP_CHECKSIGVERIFY OP_ACTIVEBYTECODE 1a OP_SPLIT OP_NIP OP_HASH256 OP_12 OP_ROLL OP_SWAP OP_13 OP_PICK OP_HASH256 OP_CAT OP_HASH256 OP_3 OP_ROLL OP_CHECKDATASIGVERIFY 14 OP_10 OP_ROLL OP_CAT OP_4 OP_CAT OP_9 OP_ROLL OP_CAT OP_9 OP_ROLL OP_CAT OP_HASH256 OP_0 OP_OUTPUTBYTECODE aa20 OP_ROT OP_CAT 87 OP_CAT OP_EQUALVERIFY OP_0 OP_OUTPUTTOKENCATEGORY OP_INPUTINDEX OP_UTXOTOKENCATEGORY OP_EQUALVERIFY OP_0 OP_OUTPUTTOKENCOMMITMENT OP_INPUTINDEX OP_UTXOTOKENCOMMITMENT OP_EQUALVERIFY OP_0 OP_OUTPUTVALUE OP_INPUTINDEX OP_UTXOVALUE OP_GREATERTHANOREQUAL OP_VERIFY OP_2DROP OP_2DROP OP_2DROP OP_2DROP OP_1 OP_ENDIF OP_ENDIF OP_ENDIF OP_ENDIF", "compiler": { "name": "cashc", "version": "0.13.3", diff --git a/src/lib/registry-covenant.js b/src/lib/registry-covenant.js index bf96ed3..c0c36c0 100644 --- a/src/lib/registry-covenant.js +++ b/src/lib/registry-covenant.js @@ -100,18 +100,38 @@ export const registryLock = (terms, state) => p2sh32Lock(registryRedeem(terms, s export const registryAddress = (terms, state, prefix) => lockToAddress(registryLock(terms, state), prefix); export { p2sh32Lock, lockToAddress }; -/** What the steward signs, once, to approve a newer version's code: sha256 is applied by OP_CHECKDATASIG itself. */ -export const migrationMessage = (newCode) => hash256(bin(newCode)); -export function signMigrationApproval(stewardPrivateKey, newCode) { - const sig = secp256k1.signMessageHashSchnorr(stewardPrivateKey, sha256.hash(migrationMessage(newCode))); +/** + * What the steward signs to approve a migration: "names held by THIS code may + * move to THAT code". Both halves matter. An approval over the new code alone + * could be replayed in any covenant sharing the steward key — another tier, + * another network — so the source is part of the message. + * (sha256 is applied by OP_CHECKDATASIG itself.) + */ +export const migrationMessage = (fromCode, newCode) => + hash256(flattenBinArray([hash256(bin(fromCode)), hash256(bin(newCode))])); +/** @param terms the covenant the names are migrating OUT of */ +export function signMigrationApproval(stewardPrivateKey, terms, newCode) { + const sig = secp256k1.signMessageHashSchnorr(stewardPrivateKey, sha256.hash(migrationMessage(registryCode(terms), newCode))); if (typeof sig === "string") throw new Error(sig); return sig; } +/** Where a name lands after migrating: its unchanged state in front of the new code. */ +export const migratedLock = (state, newCode) => p2sh32Lock(flattenBinArray([registryState(state), bin(newCode)])); // ---------- commit box ---------- -/** commitHash = hash256(name ‖ salt): what the box's address hides until the takeover. */ -export const commitHashFor = (name, salt) => hash256(flattenBinArray([utf8ToBin(normalizeName(name)), bin(salt)])); +export const COMMIT_SALT_BYTES = 32; +/** + * commitHash = hash256(name ‖ salt): what the box's address hides until the + * takeover. The salt is exactly 32 bytes — the contract requires it — because + * name and salt are hashed end to end: with a free-length salt, a commit for + * "pay" would also be one for "payroll". + */ +export function commitHashFor(name, salt) { + const s = bin(salt); + if (s.length !== COMMIT_SALT_BYTES) throw new Error(`the commit salt must be exactly ${COMMIT_SALT_BYTES} bytes`); + return hash256(flattenBinArray([utf8ToBin(normalizeName(name)), s])); +} export const commitBoxRedeem = ({ ownerPkh, commitHash }) => flattenBinArray([Uint8Array.of(0x14), bin(ownerPkh), Uint8Array.of(0x20), bin(commitHash), COMMIT_BOX_CODE]); export const commitBoxLock = (o) => p2sh32Lock(commitBoxRedeem(o)); @@ -223,15 +243,15 @@ export function buildRegistryReleaseTx({ cov, terms, state, coins = [], changeLo } /** - * Migrate to a newer covenant version: `newCode` is its CODE (approved by the - * steward's signature `approval`), `newState` whatever it expects in front. + * Migrate to a newer covenant version: `newCode` is its CODE, approved for + * THIS covenant by the steward's signature `approval`. The owner and the + * expiry carry over unchanged; the contract rebuilds the state itself. */ -export function buildRegistryMigrateTx({ cov, terms, state, newCode, newState, approval, coins, changeLock, keyFor, extra = [], feeRate = 1, tamper = null }) { +export function buildRegistryMigrateTx({ cov, terms, state, newCode, approval, coins, changeLock, keyFor, extra = [], feeRate = 1, tamper = null }) { const redeem = registryRedeem(terms, state); - const newLock = p2sh32Lock(flattenBinArray([bin(newState), bin(newCode)])); return assemble({ - special: [registrySpecial(cov, redeem, "migrate", ownerKey(keyFor, state), [bin(approval), bin(newCode), bin(newState)])], - outputs: [certOut(newLock, cov)], + special: [registrySpecial(cov, redeem, "migrate", ownerKey(keyFor, state), [bin(approval), bin(newCode)])], + outputs: [certOut(migratedLock(state, newCode), cov)], extra, coins, changeLock, keyFor, feeRate, tamper, }); } diff --git a/src/lib/subscription-artifact.js b/src/lib/subscription-artifact.js index c2e18f7..aceb661 100644 --- a/src/lib/subscription-artifact.js +++ b/src/lib/subscription-artifact.js @@ -43,7 +43,7 @@ export const SUBSCRIPTION = { ] } ], - "bytecode": "OP_5 OP_PICK OP_0 OP_NUMEQUAL OP_IF OP_3 OP_ROLL OP_CHECKSEQUENCEVERIFY OP_DROP OP_TXINPUTCOUNT OP_1 OP_NUMEQUALVERIFY OP_0 OP_UTXOVALUE OP_0 OP_OUTPUTBYTECODE OP_3 OP_ROLL OP_EQUALVERIFY OP_0 OP_OUTPUTVALUE OP_3 OP_PICK OP_GREATERTHANOREQUAL OP_VERIFY OP_0 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_ROT OP_SUB OP_ROT OP_SUB OP_DUP 2202 OP_GREATERTHANOREQUAL OP_IF OP_1 OP_OUTPUTBYTECODE OP_0 OP_UTXOBYTECODE OP_EQUALVERIFY OP_1 OP_OUTPUTVALUE OP_OVER OP_GREATERTHANOREQUAL OP_VERIFY OP_1 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_ENDIF OP_2DROP OP_DROP OP_1 OP_ELSE OP_5 OP_ROLL OP_1 OP_NUMEQUALVERIFY OP_5 OP_PICK OP_HASH160 OP_EQUALVERIFY OP_2ROT OP_CHECKSIGVERIFY OP_2DROP OP_2DROP OP_1 OP_ENDIF", + "bytecode": "OP_5 OP_PICK OP_0 OP_NUMEQUAL OP_IF OP_3 OP_PICK OP_1 000001 OP_WITHIN OP_VERIFY OP_3 OP_ROLL OP_CHECKSEQUENCEVERIFY OP_DROP OP_TXINPUTCOUNT OP_1 OP_NUMEQUALVERIFY OP_TXOUTPUTCOUNT OP_2 OP_LESSTHANOREQUAL OP_VERIFY OP_0 OP_UTXOVALUE OP_0 OP_OUTPUTBYTECODE OP_3 OP_ROLL OP_EQUALVERIFY OP_0 OP_OUTPUTVALUE OP_3 OP_PICK OP_NUMEQUALVERIFY OP_0 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_ROT OP_SUB OP_ROT OP_SUB OP_DUP 2202 OP_GREATERTHANOREQUAL OP_IF OP_1 OP_OUTPUTBYTECODE OP_0 OP_UTXOBYTECODE OP_EQUALVERIFY OP_1 OP_OUTPUTVALUE OP_OVER OP_GREATERTHANOREQUAL OP_VERIFY OP_1 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_ELSE OP_TXOUTPUTCOUNT OP_1 OP_NUMEQUALVERIFY OP_ENDIF OP_2DROP OP_DROP OP_1 OP_ELSE OP_5 OP_ROLL OP_1 OP_NUMEQUALVERIFY OP_5 OP_PICK OP_HASH160 OP_EQUALVERIFY OP_2ROT OP_CHECKSIGVERIFY OP_2DROP OP_2DROP OP_1 OP_ENDIF", "compiler": { "name": "cashc", "version": "0.13.3", diff --git a/src/lib/subscription-covenant.js b/src/lib/subscription-covenant.js index 36c6148..cc0ed88 100644 --- a/src/lib/subscription-covenant.js +++ b/src/lib/subscription-covenant.js @@ -7,6 +7,10 @@ // anyone) may then send one period's price to the host once per period; the // buyer may take the balance back whenever they like. // +// One balance is one output. Paying the same address a second time creates a +// second balance, and each can be drawn once per period: to add funds, cancel +// and fund again. +// // The first draw is possible one period after funding. A host that wants the // first period up front takes it as a second output of the funding // transaction; the contract does not need to know. diff --git a/test/covenant-family.test.mjs b/test/covenant-family.test.mjs index 2c877a1..d768063 100644 --- a/test/covenant-family.test.mjs +++ b/test/covenant-family.test.mjs @@ -15,7 +15,7 @@ import { subscriptionTerms, subscriptionRedeem, subscriptionLock, subscriptionAddress, periodsCovered, buildSubscriptionPullTx, buildSubscriptionCancelTx, } from "../src/lib/subscription-covenant.js"; -import { escrowTerms, escrowLock, escrowAddress, buildEscrowDeliverTx, buildEscrowRefundTx } from "../src/lib/escrow-covenant.js"; +import { escrowTerms, escrowLock, escrowAddress, buildEscrowDeliverTx, buildEscrowRefundTx, assertTldAllowsEscrow } from "../src/lib/escrow-covenant.js"; import { parseOpReturn } from "../src/lib/bns.js"; import { buildIndexFromSnapshot } from "../src/lib/resolver-web.js"; @@ -110,6 +110,33 @@ test("subscription: the host cannot draw early, twice, or more than the price", rejects(() => pull({ tamper: setOut(0, () => ({ valueSatoshis: 49_999n })) }), "less than the price to the host"); }); +test("subscription: the fee allowance is nobody's to take", () => { + // The first draft required "at least" the price and did not limit outputs: + // the unused part of the fee allowance could be added to the host's output + // or sent to whoever built the transaction. Now it can only be the fee. + rejects(() => pull({ tamper: setOut(0, (o) => ({ valueSatoshis: o.valueSatoshis + 700n })) }), "the host's output topped up from the fee allowance"); + rejects(() => pull({ tamper: (tx) => ({ ...tx, outputs: [...tx.outputs, { lockingBytecode: stranger.lock, valueSatoshis: 700n }] }) }), "a third output paying whoever built the draw"); + // At the end of a balance, where nothing is returned, the leftover is still not theirs. + const last = (o = {}) => buildSubscriptionPullTx({ utxo: subUtxo(51_545n), terms: SUB, ...o }); + accepts(last(), "last draw"); + rejects(() => last({ tamper: (tx) => ({ ...tx, outputs: [tx.outputs[0], { lockingBytecode: stranger.lock, valueSatoshis: 1245n }] }) }), "the last 1,245 satoshis taken by a stranger"); +}); + +test("subscription: a period outside the range of a relative lock is refused, not misread", () => { + // The library refuses these terms; the contract must too, because an address + // built by someone else's code is only as safe as the contract. + const odd = (periodBlocks) => ({ ...SUB, periodBlocks }); + const pullWith = (terms, sequenceNumber) => () => { + const utxo = { txid: fakeTxid(1), vout: 0, satoshis: 200_000n, lockingBytecode: subscriptionLock(terms) }; + return buildSubscriptionPullTx({ utxo, terms, tamper: setIn(0, () => ({ sequenceNumber })) }); + }; + rejects(pullWith(odd(0x10001), 1), "0x10001 read as one block"); + rejects(pullWith(odd(0x10000), 0), "0x10000 read as no wait"); + rejects(pullWith(odd(0), 0), "a period of zero"); + rejects(pullWith(odd(0x400001), 0x400001), "a period read as seconds"); + rejects(pullWith(odd(0x80000001), 0xffffffff), "a period with the disable bit: no lock at all"); +}); + test("subscription: two balances cannot be drawn against one payment", () => { // The bug this guards against: with two inputs, ONE host output and ONE // return output would satisfy both scripts, and the host keeps a balance. @@ -200,10 +227,42 @@ test("escrow: the deliverer cannot keep a second token of the buyer's category", rejects(() => deliver({ tamper: setOut(i, () => ({ token: tokenOf(fakeTxid(9), "", "minting") })) }), `a minting token of the category kept in output ${i}`); } rejects(() => deliver({ tamper: (tx) => ({ ...tx, outputs: [...tx.outputs.slice(0, 3), { lockingBytecode: registrar.lock, valueSatoshis: 1000n, token: tokenOf(fakeTxid(9), "", "minting") }, ...tx.outputs.slice(3)] }) }), "…slipped in as an extra output"); - rejects(() => deliver({ tamper: (tx) => ({ ...tx, outputs: [...tx.outputs, ...Array(3).fill({ lockingBytecode: registrar.lock, valueSatoshis: 546n })] }) }), "seven outputs: one would go unchecked"); + rejects(() => deliver({ tamper: (tx) => ({ ...tx, outputs: [...tx.outputs, { lockingBytecode: registrar.lock, valueSatoshis: 546n }] }) }), "five outputs: one would go unchecked"); rejects(() => deliver({ tamper: setOut(2, (o) => ({ token: tokenOf(fakeTxid(9), "", "minting") })) }), "a token riding on the beacon output"); }); +test("escrow: a second data output cannot replace the agreed registration", () => { + // Found by review. Resolvers read one protocol message per transaction; the + // first draft pinned output 1 and let later outputs be anything token-free, + // so a later OP_RETURN decided what the resolver saw. The deliverer was paid + // and the buyer's name registered to nobody — or with other records. + const opReturn = (payload) => { const d = Buffer.from("BNS1" + JSON.stringify(payload)); return Uint8Array.from([0x6a, ...(d.length <= 75 ? [d.length] : [0x4c, d.length]), ...d]); }; + const hijacks = [ + ["other records for the same name", { o: "REG", n: NAME, r: { u: "https://deliverer.example" } }], + ["an UPD, so nothing registers", { o: "UPD", n: NAME, r: {} }], + ["a REG for another name", { o: "REG", n: "elsewhere.x", r: {} }], + ]; + for (const [what, payload] of hijacks) { + rejects(() => deliver({ tamper: setOut(3, () => ({ lockingBytecode: opReturn(payload), valueSatoshis: 0n })) }), `output 3 is a data output: ${what}`); + rejects(() => deliver({ tamper: (tx) => ({ ...tx, outputs: [...tx.outputs, { lockingBytecode: opReturn(payload), valueSatoshis: 0n }] }) }), `a fifth output: ${what}`); + } + rejects(() => deliver({ tamper: setOut(3, () => ({ lockingBytecode: Uint8Array.of(0x6a), valueSatoshis: 0n })) }), "a bare OP_RETURN in the change slot"); +}); + +test("escrow: a co-signed TLD is refused up front, and a bad refund wait is refused by the contract", () => { + const cosignTld = { category: "f".repeat(64), records: { policy: "cosign" }, updates: [{ height: 1, txid: "0".repeat(64), records: { policy: "cosign" } }] }; + assert.throws(() => assertTldAllowsEscrow(cosignTld, 100, "private"), /co-signature/); + assertTldAllowsEscrow({ category: "f".repeat(64), records: {}, updates: [{ height: 1, txid: "0".repeat(64), records: {} }] }, 100, "open"); + assertTldAllowsEscrow(null, 100, "unknown"); + + const bad = (refundAfter) => ({ ...ESC, refundAfter }); + const refundWith = (terms, sequenceNumber) => () => + buildEscrowRefundTx({ escrow: escUtxo(terms), terms, toLock: buyer.lock, keyFor: keyring(buyer), tamper: setIn(0, () => ({ sequenceNumber })) }); + rejects(refundWith(bad(0x10000), 0), "a wait of 0x10000 read as no wait"); + rejects(refundWith(bad(0x400001), 0x400001), "a wait read as seconds"); + rejects(refundWith(bad(0x80000001), 0xffffffff), "a wait with the disable bit"); +}); + test("escrow: the certificate must be new, and one delivery collects one payment", () => { rejects(() => deliver({ tamper: setIn(0, () => ({ outpointIndex: 1 })) }), "input 0 is not a genesis outpoint"); rejects(() => deliver({ tamper: (tx) => ({ ...tx, inputs: [tx.inputs[1], tx.inputs[0]] }) }), "the escrow is not input 1"); @@ -235,5 +294,5 @@ test("escrow: the buyer refunds after the wait, and only the buyer, and only aft test("every negative case was refused by the contract itself, not by the library", () => { assert.deepEqual(refusedByLibrary.sort(), ["library: genesis coin not at index 0"]); - assert.ok(refusedByVm >= 28, `expected the VM to refuse at least 28 tampered transactions, it refused ${refusedByVm}`); + assert.ok(refusedByVm >= 46, `expected the VM to refuse at least 46 tampered transactions, it refused ${refusedByVm}`); }); diff --git a/test/registry-covenant.test.mjs b/test/registry-covenant.test.mjs index 946f6e3..504ed06 100644 --- a/test/registry-covenant.test.mjs +++ b/test/registry-covenant.test.mjs @@ -17,12 +17,14 @@ import assert from "node:assert/strict"; import * as L from "@bitauth/libauth"; import { registryTerms, registryRedeem, registryLock, registryCode, registryState, parseRegistryState, STATE_BYTES, - commitBoxLock, commitHashFor, takeoverQuote, signMigrationApproval, p2sh32Lock, registryPhase, + commitBoxLock, commitBoxRedeem, commitHashFor, takeoverQuote, signMigrationApproval, migratedLock, registryPhase, COMMIT_SALT_BYTES, buildRegistryRenewTx, buildRegistryTransferTx, buildRegistryTakeoverTx, buildRegistryReleaseTx, buildRegistryMigrateTx, } from "../src/lib/registry-covenant.js"; import { BLOCKS_PER_YEAR, GRACE_BLOCKS, PHASE, takeoverPriceSats, splitTakeover, renewalPriceSats, phaseAt, } from "../src/lib/registry-lifecycle.js"; +import { unlockScript, assemble, p2sh32Lock, selectorsOf, PUBKEY, SIG } from "../src/lib/covenant.js"; +import { NAME_REGISTRY } from "../src/lib/name-registry-artifact.js"; const { hexToBin, binToHex, hash160, secp256k1, encodeLockingBytecodeP2pkh, generatePrivateKey, flattenBinArray } = L; @@ -65,7 +67,7 @@ const IDLE_END = EXPIRY + GRACE_BLOCKS; const state0 = { ownerPkh: owner.pkh, expiry: EXPIRY }; const certToken = (name = NAME) => ({ category: CATEGORY, amount: 0n, nft: { capability: "none", commitment: binToHex(new TextEncoder().encode(name)) } }); const covUtxo = (state = state0, name = NAME) => ({ txid: fakeTxid(1), vout: 0, satoshis: 1000n, lockingBytecode: registryLock(terms, state), token: certToken(name) }); -const SALT = hexToBin("00112233445566778899aabbccddeeff"); +const SALT = hexToBin("00112233445566778899aabbccddeeff".repeat(2)); // 32 bytes: the contract requires exactly that const boxUtxo = (who = taker, name = NAME, salt = SALT) => ({ txid: fakeTxid(7), vout: 0, satoshis: 1000n, lockingBytecode: commitBoxLock({ ownerPkh: who.pkh, commitHash: commitHashFor(name, salt) }) }); const outLockHex = (built, i) => binToHex(built.transaction.outputs[i].lockingBytecode); @@ -184,7 +186,7 @@ test("takeover needs the taker's own matured commit for this name", () => { accepts(takeover(EXPIRY + 10), "with the commit"); rejects(() => takeover(EXPIRY + 10, { box: { ...boxUtxo(), lockingBytecode: taker.lock } }), "input 1 is a plain coin, not a commit box"); rejects(() => takeover(EXPIRY + 10, { box: boxUtxo(taker, "other.x") }), "a commit made for another name"); - rejects(() => takeover(EXPIRY + 10, { box: boxUtxo(taker, NAME, hexToBin("ff".repeat(16))) }), "a commit made with another salt"); + rejects(() => takeover(EXPIRY + 10, { box: boxUtxo(taker, NAME, hexToBin("ff".repeat(32))) }), "a commit made with another salt"); rejects(() => takeover(EXPIRY + 10, { box: boxUtxo(stranger) }), "someone else's commit"); rejects(() => takeover(EXPIRY + 10, { tamper: (tx) => ({ ...tx, inputs: tx.inputs.map((inp, i) => (i === 1 ? { ...inp, sequenceNumber: 0 } : inp)) }) }), "a commit spent in the block it was made in"); rejects(() => takeover(EXPIRY + 10, { tamper: setOut(0, () => ({ lockingBytecode: registryLock(terms, { ownerPkh: stranger.pkh, expiry: EXPIRY + 10 + BLOCKS_PER_YEAR }) })) }), "the name goes to a key the commit was not made for"); @@ -213,26 +215,88 @@ test("release destroys the certificate; it cannot be used to take it out", () => // ---------- migrate ---------- -const NEW_CODE = flattenBinArray([Uint8Array.of(0x51), new TextEncoder().encode("registry v2 code")]); -const NEW_STATE = flattenBinArray([Uint8Array.of(0x14), hexToBin(owner.pkh)]); -const approval = signMigrationApproval(steward.priv, NEW_CODE); -const migrate = (o = {}) => buildRegistryMigrateTx({ cov: covUtxo(), terms, state: state0, newCode: NEW_CODE, newState: NEW_STATE, approval, coins: [coin(2, owner)], changeLock: owner.lock, keyFor: keyring(owner), ...o }); +// A stand-in for a newer version's CODE. It runs behind the 26 state bytes +// (two pushes), which it drops; then it is spendable by the owner's key. +const NEW_CODE = flattenBinArray([Uint8Array.of(0x6d), Uint8Array.of(0x21), owner.pub, Uint8Array.of(0xac)]); // OP_2DROP OP_CHECKSIG +const approval = signMigrationApproval(steward.priv, terms, NEW_CODE); +const migrate = (o = {}) => buildRegistryMigrateTx({ cov: covUtxo(), terms, state: state0, newCode: NEW_CODE, approval, coins: [coin(2, owner)], changeLock: owner.lock, keyFor: keyring(owner), ...o }); -test("migrate: owner AND steward — neither can move a name alone", () => { +test("migrate: owner AND steward — neither can move a name alone, and the state carries over", () => { const built = migrate(); accepts(built, "migrate"); - assert.equal(outLockHex(built, 0), binToHex(p2sh32Lock(flattenBinArray([NEW_STATE, NEW_CODE])))); - // the same approval serves any state: it covers the code, signed once for everyone - accepts(migrate({ newState: flattenBinArray([Uint8Array.of(0x14), hexToBin(buyer.pkh)]) }), "another state under the same approved code"); + assert.equal(outLockHex(built, 0), binToHex(migratedLock(state0, NEW_CODE)), "same owner, same expiry, in front of the approved code"); - rejects(() => migrate({ approval: signMigrationApproval(stranger.priv, NEW_CODE) }), "code approved by someone who is not the steward"); + rejects(() => migrate({ approval: signMigrationApproval(stranger.priv, terms, NEW_CODE) }), "code approved by someone who is not the steward"); rejects(() => migrate({ newCode: flattenBinArray([NEW_CODE, Uint8Array.of(0x00)]) }), "code the steward did not sign"); - rejects(() => migrate({ approval: signMigrationApproval(steward.priv, Uint8Array.of(0x51)) }), "an approval for different code"); - rejects(() => buildRegistryMigrateTx({ cov: covUtxo(), terms, state: state0, newCode: NEW_CODE, newState: NEW_STATE, approval, coins: [coin(2, stranger)], changeLock: stranger.lock, keyFor: () => ({ privateKey: stranger.priv, publicKey: stranger.pub }) }), "the steward's approval without the owner"); + rejects(() => migrate({ approval: signMigrationApproval(steward.priv, terms, Uint8Array.of(0x51)) }), "an approval for different code"); + rejects(() => buildRegistryMigrateTx({ cov: covUtxo(), terms, state: state0, newCode: NEW_CODE, approval, coins: [coin(2, stranger)], changeLock: stranger.lock, keyFor: () => ({ privateKey: stranger.priv, publicKey: stranger.pub }) }), "the steward's approval without the owner"); rejects(() => migrate({ tamper: setOut(0, () => ({ lockingBytecode: owner.lock })) }), "certificate sent to a plain address under cover of a migration"); rejects(() => migrate({ tamper: setOut(0, () => ({ token: undefined })) }), "certificate dropped"); }); +test("migrate: the owner chooses nothing — no script in front of the code, no new expiry, no new owner", () => { + // The first draft let the owner supply the bytes placed in front of the + // approved code. An owner could put their own signature check there and a + // push opcode that swallowed the approved code as data: the certificate left + // the covenant under any approval the steward had ever made. The contract + // now rebuilds the state itself, so every one of these targets is refused. + const push = (b) => (b.length <= 75 ? flattenBinArray([Uint8Array.of(b.length), b]) : flattenBinArray([Uint8Array.of(0x4c, b.length), b])); + const swallowed = flattenBinArray([Uint8Array.of(0x21), owner.pub, Uint8Array.of(0xad), push(NEW_CODE)]); // OP_CHECKSIGVERIFY PUSH(approved code) + rejects(() => migrate({ tamper: setOut(0, () => ({ lockingBytecode: p2sh32Lock(swallowed) })) }), "the approved code wrapped as data behind the owner's own signature check"); + rejects(() => migrate({ tamper: setOut(0, () => ({ lockingBytecode: migratedLock({ ownerPkh: owner.pkh, expiry: 0x7ffffffe }, NEW_CODE) })) }), "a migration that also grants itself forty thousand years"); + rejects(() => migrate({ tamper: setOut(0, () => ({ lockingBytecode: migratedLock({ ownerPkh: buyer.pkh, expiry: EXPIRY }, NEW_CODE) })) }), "a migration that also changes the owner"); +}); + +test("migrate: an approval is for one covenant and is worthless in another", () => { + // Same steward, same new code — but the approval was made for tier 4's covenant. + const otherTerms = registryTerms({ tier: 4, fundLock: fund.lock, stewardPk: steward.pub }); + rejects(() => migrate({ approval: signMigrationApproval(steward.priv, otherTerms, NEW_CODE) }), "an approval made for another tier's covenant"); + const otherFund = registryTerms({ tier: TIER, fundLock: stranger.lock, stewardPk: steward.pub }); + rejects(() => migrate({ approval: signMigrationApproval(steward.priv, otherFund, NEW_CODE) }), "an approval made for a covenant with another fund (another network's, say)"); +}); + +// ---------- bounds the review asked for ---------- + +test("renew cannot push the expiry past the range of block heights", () => { + const nearEnd = { ownerPkh: owner.pkh, expiry: 500_000_000 - BLOCKS_PER_YEAR }; + const cov = covUtxo(nearEnd); + // one more year would be exactly 500,000,000: read as a timestamp, never takeable again + rejects(() => buildRegistryRenewTx({ cov, terms, state: nearEnd, years: 1, coins: [coin(2, owner)], changeLock: owner.lock, keyFor: keyring(owner) }), "expiry of exactly 500,000,000"); + const justUnder = { ownerPkh: owner.pkh, expiry: 500_000_000 - BLOCKS_PER_YEAR - 1 }; + accepts(buildRegistryRenewTx({ cov: covUtxo(justUnder), terms, state: justUnder, years: 1, coins: [coin(2, owner)], changeLock: owner.lock, keyFor: keyring(owner) }), "expiry of 499,999,999"); +}); + +test("a commit is for one name: the salt has a fixed size, so nothing can slide between name and salt", () => { + assert.equal(COMMIT_SALT_BYTES, 32); + assert.throws(() => commitHashFor(NAME, hexToBin("00".repeat(16))), /exactly 32 bytes/); + + // The ambiguity, with real bytes: hash256("covenant" ‖ ".x" ‖ S) is one hash, + // whether you call the name "covenant" with salt ".x"‖S or "covenant.x" with salt S. + // Build a takeover of "covenant.x" from a box made as if for a 34-byte salt on a shorter name, + // by hand (the library refuses to), and confirm the CONTRACT refuses the odd-sized salt. + const longSalt = flattenBinArray([new TextEncoder().encode("xx"), SALT]); // 34 bytes + const { hash256 } = L; + const commitHash = hash256(flattenBinArray([new TextEncoder().encode(NAME), longSalt])); + const boxRedeem = commitBoxRedeem({ ownerPkh: taker.pkh, commitHash }); + const box = { txid: fakeTxid(7), vout: 0, satoshis: 1000n, lockingBytecode: p2sh32Lock(boxRedeem) }; + const height = IDLE_END, quote = takeoverQuote(terms, state0, height); + const redeem = registryRedeem(terms, state0); + const FN = selectorsOf(NAME_REGISTRY); + const takerKey = { privateKey: taker.priv, publicKey: taker.pub }; + const cov = covUtxo(); + rejects(() => assemble({ + special: [ + { utxo: cov, unlock: () => unlockScript({ selector: FN.takeover, redeem, args: [hexToBin(taker.pkh), longSalt] }) }, + { utxo: box, sequence: 1, unlock: (transaction, sourceOutputs, inputIndex, final) => unlockScript({ redeem: boxRedeem, transaction, sourceOutputs, inputIndex, key: final ? takerKey : null, args: [PUBKEY, SIG] }) }, + ], + outputs: [ + { lockingBytecode: registryLock(terms, { ownerPkh: taker.pkh, expiry: quote.newExpiry }), valueSatoshis: 1000n, token: { amount: 0n, category: hexToBin(CATEGORY), nft: { capability: "none", commitment: new TextEncoder().encode(NAME) } } }, + { lockingBytecode: fund.lock, valueSatoshis: quote.fundSats }, + ], + coins: [coin(3, taker, 50_000_000_000n)], changeLock: taker.lock, keyFor: keyring(taker), locktime: height, + }), "a takeover with a 34-byte salt, everything else in order"); +}); + // ---------- a life ---------- test("a name's life: renew, sell, lapse, be taken over, be renewed by its new owner", () => { @@ -262,5 +326,5 @@ test("a name's life: renew, sell, lapse, be taken over, be renewed by its new ow test("every negative case was refused by the contract itself, not by the library", () => { assert.deepEqual(refusedByLibrary.sort(), ["zero years"], "only these may be stopped before the VM runs"); - assert.ok(refusedByVm >= 45, `expected the VM to refuse at least 45 tampered transactions, it refused ${refusedByVm}`); + assert.ok(refusedByVm >= 54, `expected the VM to refuse at least 54 tampered transactions, it refused ${refusedByVm}`); });