UNPKG

ecash-agora

Version:

Library for interacting with the eCash Agora protocol

211 lines 10.5 kB
"use strict"; // Copyright (c) 2024 The Bitcoin developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. Object.defineProperty(exports, "__esModule", { value: true }); exports.AgoraOneshotAdSignatory = exports.AgoraOneshotCancelSignatory = exports.AgoraOneshotSignatory = exports.AgoraOneshot = void 0; const ecash_lib_1 = require("ecash-lib"); const consts_js_1 = require("./consts.js"); /** * Agora offer that has to be accepted in "one shot", i.e. all or nothing. * This is useful for offers that offer exactly 1 token, especially NFTs. * * The covenant is reasonably simple, see * https://read.cash/@pein/bch-covenants-with-spedn-4a980ed3 for an explanation of the * covenant mechanism, but uses two optimizations: * 1. It uses ANYONECANPAY as sighash for the "accept" path, which makes the sighash * preimage start with `1000....00000`, which can be created with * `OP_1 68 OP_NUM2BIN`, saving around 64 bytes. * 2. It uses OP_CODESEPARATOR before the OP_CHECKSIG, which cuts out the entire script * code, leaving only the OP_CHECKSIG behind. The scriptCode part in the BIP143 * sighash now just becomes `01ac`, which is both easier to deal with in the OP_SPLIT * and also saves 100 bytes or so (depending on the enforced outputs). **/ class AgoraOneshot { constructor({ enforcedOutputs, cancelPk, }) { this.enforcedOutputs = enforcedOutputs; this.cancelPk = cancelPk; } /** Build the Script enforcing the Agora offer covenant. */ script() { const serEnforcedOutputs = (writer) => { for (const output of this.enforcedOutputs) { (0, ecash_lib_1.writeTxOutput)(output, writer); } }; const writerLength = new ecash_lib_1.WriterLength(); serEnforcedOutputs(writerLength); const writer = new ecash_lib_1.WriterBytes(writerLength.length); serEnforcedOutputs(writer); const enforcedOutputsSer = writer.data; return ecash_lib_1.Script.fromOps([ ecash_lib_1.OP_IF, // if is_accept (0, ecash_lib_1.pushBytesOp)(enforcedOutputsSer), // push enforced_outputs ecash_lib_1.OP_SWAP, // swap buyer_outputs, enforced_outputs ecash_lib_1.OP_CAT, // outputs = OP_CAT(enforced_outputs, buyer_outputs) ecash_lib_1.OP_HASH256, // expected_hash_outputs = OP_HASH256(outputs) ecash_lib_1.OP_OVER, // duplicate preimage_4_10, // push hash_outputs_idx: (0, ecash_lib_1.pushBytesOp)(new Uint8Array([ 36 + // 4. outpoint 2 + // 5. scriptCode, truncated to 01ac via OP_CODESEPARATOR 8 + // 6. value 4, // 7. sequence ])), ecash_lib_1.OP_SPLIT, // split into preimage_4_7 and preimage_8_10 ecash_lib_1.OP_NIP, // remove preimage_4_7 (0, ecash_lib_1.pushBytesOp)(new Uint8Array([32])), // push 32 onto the stack ecash_lib_1.OP_SPLIT, // split into actual_hash_outputs and preimage_9_10 ecash_lib_1.OP_DROP, // drop preimage_9_10 ecash_lib_1.OP_EQUALVERIFY, // expected_hash_outputs === actual_hash_outputs ecash_lib_1.OP_2, // push tx version // length of BIP143 preimage parts 1 to 3 (0, ecash_lib_1.pushBytesOp)(new Uint8Array([4 + 32 + 32])), // build BIP143 preimage parts 1 to 3 for ANYONECANPAY using OP_NUM2BIN ecash_lib_1.OP_NUM2BIN, ecash_lib_1.OP_SWAP, // swap preimage_4_10 and preimage_1_3 ecash_lib_1.OP_CAT, // preimage = OP_CAT(preimage_1_3, preimage_4_10) ecash_lib_1.OP_SHA256, // preimage_sha256 = OP_SHA256(preimage) ecash_lib_1.OP_3DUP, // OP_3DUP(covenant_pk, covenant_sig, preimage_sha256) ecash_lib_1.OP_ROT, // -> covenant_sig | preimage_sha256 | covenant_pk ecash_lib_1.OP_CHECKDATASIGVERIFY, // verify preimage matches covenant_sig ecash_lib_1.OP_DROP, // drop preimage_sha256 // push ALL|ANYONECANPAY|BIP143 onto the stack (0, ecash_lib_1.pushBytesOp)(new Uint8Array([ecash_lib_1.ALL_ANYONECANPAY_BIP143.toInt()])), ecash_lib_1.OP_CAT, // append sighash flags onto covenant_sig ecash_lib_1.OP_SWAP, // swap covenant_pk, covenant_sig_flagged ecash_lib_1.OP_ELSE, // cancel path (0, ecash_lib_1.pushBytesOp)(this.cancelPk), // pubkey that can cancel the covenant ecash_lib_1.OP_ENDIF, // cut out everything except the OP_CHECKSIG from the BIP143 scriptCode ecash_lib_1.OP_CODESEPARATOR, ecash_lib_1.OP_CHECKSIG, ]); } static fromRedeemScript(redeemScript, opreturnScript) { const ops = redeemScript.ops(); const outputsSerOp = ops.next(); if (!(0, ecash_lib_1.isPushOp)(outputsSerOp)) { throw new Error('Op 0 expected to be pushop for outputsSer'); } if (ops.next() !== ecash_lib_1.OP_DROP) { throw new Error('Op 1 expected to be OP_DROP'); } const cancelPkOp = ops.next(); if (!(0, ecash_lib_1.isPushOp)(cancelPkOp)) { throw new Error('Op 2 expected to be pushop for cancelPk'); } if (cancelPkOp.data.length != 33) { throw new Error(`Expected cancelPk to be 33 bytes`); } if (ops.next() !== ecash_lib_1.OP_CHECKSIGVERIFY) { throw new Error('Op 3 expected to be OP_CHECKSIGVERIFY'); } const covenantVariantOp = ops.next(); if (!(0, ecash_lib_1.isPushOp)(covenantVariantOp)) { throw new Error('Op 4 expected to be pushop for covenantVariant'); } if (ops.next() !== ecash_lib_1.OP_EQUALVERIFY) { throw new Error('Op 5 expected to be OP_EQUALVERIFY'); } const lokadIdOp = ops.next(); if (!(0, ecash_lib_1.isPushOp)(lokadIdOp)) { throw new Error('Op 6 expected to be pushop for LOKAD ID'); } const outputsSerBytes = new ecash_lib_1.Bytes(outputsSerOp.data); const enforcedOutputs = [ { sats: 0n, script: opreturnScript, }, ]; while (outputsSerBytes.data.length > outputsSerBytes.idx) { enforcedOutputs.push((0, ecash_lib_1.readTxOutput)(outputsSerBytes)); } return new AgoraOneshot({ enforcedOutputs, cancelPk: cancelPkOp.data, }); } adScript() { const serOutputs = (writer) => { for (const output of this.enforcedOutputs.slice(1)) { (0, ecash_lib_1.writeTxOutput)(output, writer); } }; const writerLength = new ecash_lib_1.WriterLength(); serOutputs(writerLength); const writer = new ecash_lib_1.WriterBytes(writerLength.length); serOutputs(writer); const outputsSer = writer.data; return ecash_lib_1.Script.fromOps([ (0, ecash_lib_1.pushBytesOp)(outputsSer), ecash_lib_1.OP_DROP, (0, ecash_lib_1.pushBytesOp)(this.cancelPk), ecash_lib_1.OP_CHECKSIGVERIFY, (0, ecash_lib_1.pushBytesOp)((0, ecash_lib_1.strToBytes)(AgoraOneshot.COVENANT_VARIANT)), ecash_lib_1.OP_EQUALVERIFY, (0, ecash_lib_1.pushBytesOp)(consts_js_1.AGORA_LOKAD_ID), ecash_lib_1.OP_EQUAL, ]); } askedSats() { return this.enforcedOutputs.reduce((prev, output) => prev + output.sats, 0n); } } exports.AgoraOneshot = AgoraOneshot; AgoraOneshot.COVENANT_VARIANT = 'ONESHOT'; const AgoraOneshotSignatory = (covenantSk, covenantPk, numEnforcedOutputs) => { return (ecc, input) => { const preimage = input.sigHashPreimage(ecash_lib_1.ALL_ANYONECANPAY_BIP143, 0); const sighash = (0, ecash_lib_1.sha256d)(preimage.bytes); const covenantSig = ecc.schnorrSign(covenantSk, sighash); const serBuyerOutputs = (writer) => { for (const output of input.unsignedTx.tx.outputs.slice(numEnforcedOutputs)) { (0, ecash_lib_1.writeTxOutput)(output, writer); } }; const writerLength = new ecash_lib_1.WriterLength(); serBuyerOutputs(writerLength); const writer = new ecash_lib_1.WriterBytes(writerLength.length); serBuyerOutputs(writer); const buyerOutputsSer = writer.data; return ecash_lib_1.Script.fromOps([ (0, ecash_lib_1.pushBytesOp)(covenantPk), (0, ecash_lib_1.pushBytesOp)(covenantSig), (0, ecash_lib_1.pushBytesOp)(preimage.bytes.slice(4 + 32 + 32)), // preimage_4_10 (0, ecash_lib_1.pushBytesOp)(buyerOutputsSer), ecash_lib_1.OP_1, // is_accept = true (0, ecash_lib_1.pushBytesOp)(preimage.redeemScript.bytecode), ]); }; }; exports.AgoraOneshotSignatory = AgoraOneshotSignatory; const AgoraOneshotCancelSignatory = (cancelSk) => { return (ecc, input) => { const preimage = input.sigHashPreimage(ecash_lib_1.ALL_BIP143, 0); const sighash = (0, ecash_lib_1.sha256d)(preimage.bytes); const cancelSig = (0, ecash_lib_1.flagSignature)(ecc.schnorrSign(cancelSk, sighash), ecash_lib_1.ALL_BIP143); return ecash_lib_1.Script.fromOps([ (0, ecash_lib_1.pushBytesOp)(cancelSig), ecash_lib_1.OP_0, // is_accept = false (0, ecash_lib_1.pushBytesOp)(preimage.redeemScript.bytecode), ]); }; }; exports.AgoraOneshotCancelSignatory = AgoraOneshotCancelSignatory; const AgoraOneshotAdSignatory = (cancelSk) => { return (ecc, input) => { const preimage = input.sigHashPreimage(ecash_lib_1.ALL_BIP143); const sighash = (0, ecash_lib_1.sha256d)(preimage.bytes); const cancelSig = (0, ecash_lib_1.flagSignature)(ecc.schnorrSign(cancelSk, sighash), ecash_lib_1.ALL_BIP143); return ecash_lib_1.Script.fromOps([ (0, ecash_lib_1.pushBytesOp)(consts_js_1.AGORA_LOKAD_ID), (0, ecash_lib_1.pushBytesOp)((0, ecash_lib_1.strToBytes)(AgoraOneshot.COVENANT_VARIANT)), (0, ecash_lib_1.pushBytesOp)(cancelSig), (0, ecash_lib_1.pushBytesOp)(preimage.redeemScript.bytecode), ]); }; }; exports.AgoraOneshotAdSignatory = AgoraOneshotAdSignatory; //# sourceMappingURL=oneshot.js.map