ecash-agora
Version:
Library for interacting with the eCash Agora protocol
211 lines • 10.5 kB
JavaScript
;
// 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