pragma cashscript ^0.13.0; // DeliveryEscrow — payment for a name that can only be collected by the // transaction that registers it to the buyer. // (Decentralized.DNS/DESIGN-trustless-purchase.md §4.) // // STATUS: chipnet draft. NOT REVIEWED. // // A buyer who cannot build a registration themselves (pay-by-QR, any wallet) // pays this script instead of paying a registrar. The money then has exactly // two ways out: // // deliver() anyone may spend it — in a transaction that mints the agreed // certificate to the buyer and publishes the agreed registration. // "Took the money and did not register" is not a risk the buyer // has to weigh; it is a transaction that cannot be built. // refund() the buyer takes it back with their signature once it is // `refundAfter` blocks old. // // What deliver() pins down, output by output: // 0 the certificate — a NEW token category created by this transaction // (its id is input 0's outpoint), an NFT with no capability, commitment = // 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 // 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 // both be collected against a single certificate. // // WHAT IT CANNOT PROMISE. Script sees this transaction, not the registry. If // the name was registered by someone else between payment and delivery, a // deliverer can still mint a certificate that satisfies every line here and // that no resolver will honour ("first valid registration wins" is a resolver // rule). The window is the buyer's to keep short: check the name is free, pay, // and refund if delivery does not come. contract DeliveryEscrow( bytes20 buyerPkh, bytes certLock, // where the certificate must go (the buyer's token address, or their covenant) bytes nameCommitment, // the on-chain name, as the certificate's commitment bytes payloadLock, // the full OP_RETURN locking bytecode of the agreed REG payload bytes beaconLock, int beaconDust, int refundAfter ) { function deliver() { require(this.activeInputIndex == 1); require(tx.inputs[0].outpointIndex == 0); bytes category = tx.inputs[0].outpointTransactionHash; require(tx.outputs[0].lockingBytecode == certLock); require(tx.outputs[0].tokenCategory == category); require(tx.outputs[0].nftCommitment == nameCommitment); require(tx.outputs[0].tokenAmount == 0); require(tx.outputs[1].lockingBytecode == payloadLock); require(tx.outputs[1].tokenCategory == 0x); require(tx.outputs[2].lockingBytecode == beaconLock); require(tx.outputs[2].value >= beaconDust); require(tx.outputs[2].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); } }