UNPKG

thebigguy-contract

Version:

A library to generate P2SH scripts and create spend transactions for permissionless share-based distribution of UTXOs

516 lines 21.3 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.SCRIPT_NOPAY = void 0; exports.createScript = createScript; exports.quotient = quotient; exports.minUnitForShare = minUnitForShare; exports.minUnitForAllShares = minUnitForAllShares; const ecash_lib_1 = require("ecash-lib"); const xecaddr = __importStar(require("ecashaddrjs")); // // constants // exports.SCRIPT_NOPAY = ecash_lib_1.Script.fromOps([ecash_lib_1.OP_RETURN]); /** * Constructs a P2SH script that validates the transaction outputs to ensure * that each `party` receives it's assigned share of the input value (after * removing the transaction fee). * * The script expects the following pushes to the stack as inputs: * * - Serialized `prevouts`: to ensure that the script is only being used for * the first input * - Serialized `outputs`: to validate shares * - Schnorr signature for the preimage: to validate the preimage * - The BIP 143 preimage split into multiple pushes: All elements are pushed * separately except `nLocktime` and `sighash` which are pushed together, * because they are not needed * * The script supports multiple paths based solely on the input value. * * - If the value is bigger than 2147483647 (`0x7fffffff`) then the outputs * must be a 50/50 split with the receiver being the same P2SH script. Any * odd amount must have the extra sat on the second output. * - If the value is smaller than `fee + 1000` then a single empty `OP_RETURN` * is expected. Note that this will be the case even if additional standard * inputs are used, because only the value of the first input is considered. * - For the other cases the outputs must match the parties, in order, and have * the expected value of `floor(value - fee / 1000) * share`, or be ommited * if the value would be smaller thant the dust value of 546 sats. * * @param ecc An instance of `ecash-lib`'s ECC implementation for reusability * @param prvKey The private key that will be used to sign transactions * @param fee The value that will be discounted from the input value to pay for fees * @param parties The list of parties included in the script to validate the transaction outputs * * @returns A locking script that can be used to derive the P2SH address or * added to an unlocking script. */ function createScript(ecc, prvKey, fee, parties) { // validate number of parties if (parties.length < 2 || parties.length > 3) { // less than 2 is useless, more than 3 will break the 520 byte push limit // // The push limit is not broken by the pushes in the input script, or any // single push operation, but by the series of OP_CAT used to reconstruct // the preimage. throw new Error("The contract must have between 2 and 3 parties"); } // validate maximum fee to avoid problems with OP_NUM2BIN, after addition // // When the input value needs to be split the script will validate outputs by adding // the values and the fee. Since the maximum positive value that can be minimally encoded // is 0x7fffffff, and the addition is done on 24-bit numbers, then the fee cannot be // larger than 0x7fffffff - 0xffffff - 0xffffff. // if (fee > 2113929217) { throw new Error(`The fee cannot be larger than 2113929217 to avoid math overflows`); } // validate parties for (let i = 0; i < parties.length; i++) { const p = parties[i]; // avoid etoken and addresses with other purposes const decodedAddress = xecaddr.decodeCashAddress(p.address); if (!["ecash", "ecregtest"].includes(decodedAddress.prefix)) { throw new Error(`Only ecash addresses are acceptable, got ${decodedAddress.prefix} for ${p.address}`); } // make sure numbers are integer numbers const isIntShare = p.share % 1 === 0; if (!isIntShare || p.share <= 0) { throw new Error(`All shares must be a positive integer, got "${p.share}" for "${p.address}"`); } // make sure shares are in the expected range if (p.share < 1 || p.share > 999) { throw new Error(`Shares must be a value between 1 and 999, got "${p.share}" for "${p.address}"`); } } // validate total shares const totalShares = parties.reduce((v, p) => v + p.share, 0); if (totalShares != 1000) { throw new Error(`Shares must add to 1000, got ${totalShares}`); } // find the smallest unit from all shares (from the largest share) // // This is used to check when an OP_RETURN is expected because if the largest // cannot get a payment above token level then no other share will. It avoids // keeping state, while checking outputs, or allow an optional tail. const absoluteMinShare = minUnitForAllShares(parties); // extract public key to include in script, also validates that private key is acceptable const pubKey = ecc.derivePubkey(prvKey); // build script ops const ops = [ // // inputs // <prevouts> <outputs> <sig,sigflags> <preimage[1,2,3,4,5,6,7,8,9-10]> // // To work around the 520-push limits, the preimage will be provided as // multiple pushes. All preimage parts are pushed individually, except the // last two because they are not needed. // // Since the preimage is split, the verification of the multiple parts is // done first and all signature verifications are left to the end. // // // check prevouts SHA256d // <prevouts> <outputs> <sig,sigflags> <preimage[1]> <hashPrevouts> ... // ... <preimage[3,4,5,6,7,8,9-10]> // (0, ecash_lib_1.pushNumberOp)(11), ecash_lib_1.OP_ROLL, ecash_lib_1.OP_DUP, ecash_lib_1.OP_HASH256, (0, ecash_lib_1.pushNumberOp)(9), ecash_lib_1.OP_PICK, ecash_lib_1.OP_EQUALVERIFY, // // make sure that only one script input is used // <outputs> <sig,sigflags> <preimage[1,2,3]> <outpoint> ... // ... <preimage[5,6,7,8,9-10]> <prevouts> // // Ensuring a single script input is needed because a transaction could // be made with multiple coins of the same value. The input script for // each coin would validate the same outputs but only one coin would be // split and all the others would be givne to miners. // // By matching the preimage outpoint with the first prevout provided as // input we limit the number of scripts inputs while allowing for more // inputs that can add fees. This is important to make all coins spendable // because transactions will be bigger than 546 bytes. // (0, ecash_lib_1.pushNumberOp)(36), ecash_lib_1.OP_SPLIT, ecash_lib_1.OP_DROP, (0, ecash_lib_1.pushNumberOp)(6), ecash_lib_1.OP_PICK, ecash_lib_1.OP_EQUALVERIFY, // // check outputs SHA256d // <outputs> <sig,sigflags> <preimage[1,2,3,4,5,6,7]> <hashOutputs> ... // ... <preimage[9-10]> // ecash_lib_1.OP_OVER, (0, ecash_lib_1.pushNumberOp)(11), ecash_lib_1.OP_ROLL, ecash_lib_1.OP_DUP, ecash_lib_1.OP_HASH256, ecash_lib_1.OP_ROT, ecash_lib_1.OP_EQUALVERIFY, // // bring script and value to top to enable output verification // <sig,sigflags> <preimage[1,2,3,4]> <size,scriptCode> <value as bin> ... // ... <preimage[7,8,9-10]> <outputs> // (0, ecash_lib_1.pushNumberOp)(5), ecash_lib_1.OP_PICK, (0, ecash_lib_1.pushNumberOp)(5), ecash_lib_1.OP_PICK, // // check for input value overflow (5 bytes of more) // ... <outputs> <size,scriptCode> <value as bin> // // Script numbers are signed and may require a leading 0 byte to mark // the number as positive when the most significant bit is set. But // script math operations only accept minimally encoded numbers that fit // in 4 bytes. This means that shares can only be verified for values // no larger than 0x7fffffff (big-endian). // // To make all inputs spendable there are two paths. When the input // value is within range, the shares are computed as expected. But when // the input value is out of range, only two outputs, to the contract // address, are allowed and their values (plus the fee) must match the // input value. This can be done because 64-bit addition can be emulated // with lower-bit addition and carry over. Fo this, 24-bit addition is // used because any overflow will fit in 4 bytes. // ecash_lib_1.OP_DUP, (0, ecash_lib_1.pushBytesOp)((0, ecash_lib_1.fromHex)("00000080ffffffff")), ecash_lib_1.OP_AND, ecash_lib_1.OP_IF, // // compute the contract output script // ... <outputs> <size,scriptCode> <value as bin> // (0, ecash_lib_1.pushBytesOp)((0, ecash_lib_1.fromHex)("17a914")), // PUSH(23), HASH160, PUSH(20) ecash_lib_1.OP_ROT, (0, ecash_lib_1.pushNumberOp)(3), // assume length of compact size ecash_lib_1.OP_SPLIT, ecash_lib_1.OP_NIP, ecash_lib_1.OP_HASH160, (0, ecash_lib_1.pushBytesOp)((0, ecash_lib_1.fromHex)("87")), // OP_EQUAL ecash_lib_1.OP_CAT, ecash_lib_1.OP_CAT, // // validate output destinations, preserve output values // ... <outputs> <value as bin> <outputscript> // ecash_lib_1.OP_ROT, (0, ecash_lib_1.pushNumberOp)(8), ecash_lib_1.OP_SPLIT, (0, ecash_lib_1.pushNumberOp)(24), ecash_lib_1.OP_SPLIT, (0, ecash_lib_1.pushNumberOp)(8), ecash_lib_1.OP_SPLIT, ecash_lib_1.OP_ROT, // ... <value as bin> <outputscript> <value1 as bin> <value2 as bin> ... // ... <script2> <script1> ecash_lib_1.OP_DUP, (0, ecash_lib_1.pushNumberOp)(5), ecash_lib_1.OP_ROLL, ecash_lib_1.OP_EQUALVERIFY, // <script1> == <contractscript> ecash_lib_1.OP_EQUALVERIFY, // <script2> == <script1> // // validate that the output values plus fee equals input value // ... <value as bin> <outputscript> <value1 as bin> <value2 as bin> // (0, ecash_lib_1.pushNumberOp)(3), ecash_lib_1.OP_SPLIT, ecash_lib_1.OP_SWAP, (0, ecash_lib_1.pushBytesOp)((0, ecash_lib_1.fromHex)("00")), ecash_lib_1.OP_CAT, ecash_lib_1.OP_BIN2NUM, ecash_lib_1.OP_ROT, (0, ecash_lib_1.pushNumberOp)(3), ecash_lib_1.OP_SPLIT, ecash_lib_1.OP_SWAP, (0, ecash_lib_1.pushBytesOp)((0, ecash_lib_1.fromHex)("00")), ecash_lib_1.OP_CAT, ecash_lib_1.OP_BIN2NUM, ecash_lib_1.OP_ROT, ecash_lib_1.OP_ADD, // add bits 0 - 23 (0, ecash_lib_1.pushNumberOp)(fee), ecash_lib_1.OP_ADD, // add fee, can be at most 2113929217 (0, ecash_lib_1.pushNumberOp)(4), ecash_lib_1.OP_NUM2BIN, (0, ecash_lib_1.pushNumberOp)(3), ecash_lib_1.OP_SPLIT, ecash_lib_1.OP_BIN2NUM, // carryover for bits 24 - 47 ecash_lib_1.OP_2SWAP, (0, ecash_lib_1.pushNumberOp)(3), ecash_lib_1.OP_SPLIT, ecash_lib_1.OP_SWAP, (0, ecash_lib_1.pushBytesOp)((0, ecash_lib_1.fromHex)("00")), ecash_lib_1.OP_CAT, ecash_lib_1.OP_BIN2NUM, ecash_lib_1.OP_ROT, (0, ecash_lib_1.pushNumberOp)(3), ecash_lib_1.OP_SPLIT, ecash_lib_1.OP_SWAP, (0, ecash_lib_1.pushBytesOp)((0, ecash_lib_1.fromHex)("00")), ecash_lib_1.OP_CAT, ecash_lib_1.OP_BIN2NUM, ecash_lib_1.OP_ROT, ecash_lib_1.OP_ADD, // add bits 24 - 27 (0, ecash_lib_1.pushNumberOp)(3), ecash_lib_1.OP_ROLL, ecash_lib_1.OP_ADD, // add carryover for bits 24 - 47 (0, ecash_lib_1.pushNumberOp)(4), ecash_lib_1.OP_NUM2BIN, (0, ecash_lib_1.pushNumberOp)(3), ecash_lib_1.OP_SPLIT, ecash_lib_1.OP_BIN2NUM, // carryover for bits 48 - 63 ecash_lib_1.OP_2SWAP, ecash_lib_1.OP_BIN2NUM, // will always have a leading 0 ecash_lib_1.OP_SWAP, ecash_lib_1.OP_BIN2NUM, // will always have a leading 0 ecash_lib_1.OP_ADD, // add bits 48 - 63 ecash_lib_1.OP_ADD, // add carryover for bits 48 - 63 (0, ecash_lib_1.pushNumberOp)(2), ecash_lib_1.OP_NUM2BIN, ecash_lib_1.OP_CAT, // join bits 24 - 63 ecash_lib_1.OP_CAT, // join bits 0 - 63 ecash_lib_1.OP_EQUALVERIFY, ecash_lib_1.OP_ELSE, // // drop script code, not neded for output verification // ... <outputs> <size,scriptCode> <value as bin> // ecash_lib_1.OP_NIP, // // can validate shares, start by taking fixed fee from value // ... <outputs> <value as bin> // ecash_lib_1.OP_BIN2NUM, (0, ecash_lib_1.pushNumberOp)(fee), ecash_lib_1.OP_SUB, // // calculate 1/1000 unit from input value // ... <outputs> <value> // (0, ecash_lib_1.pushNumberOp)(1000), ecash_lib_1.OP_DIV, // // check if unit is below the minimum of all shares // ... <outputs> <unit> // // This means that, for all shares, unit times share is smaler than 546. // ecash_lib_1.OP_DUP, (0, ecash_lib_1.pushNumberOp)(absoluteMinShare), ecash_lib_1.OP_LESSTHAN, ecash_lib_1.OP_IF, // // drop unit, not longer needed for output verification // ... <outputs> <unit> // ecash_lib_1.OP_DROP, // // check for an empty OP_RETURN // ... <outputs> // // The OP_RETURN is enforced because there's no way to respect the // shares and any other distribution would be arbitrary. A future // version may allow consolidation to the contract address. // ecash_lib_1.OP_REVERSEBYTES, ecash_lib_1.OP_BIN2NUM, (0, ecash_lib_1.pushBytesOp)((0, ecash_lib_1.fromHex)("6a01")), ecash_lib_1.OP_EQUALVERIFY, ecash_lib_1.OP_ELSE, // // bring outputs to the top, to allow a dynamic number of shares // ... <outputs> <unit> // ecash_lib_1.OP_SWAP ]; // add repeatable logic for each party for (let i = 0; i < parties.length; i++) { const party = parties[i]; const outputScript = ecash_lib_1.Script.fromAddress(party.address); const minUnit = minUnitForShare(party.share); appendOps(ops, [ // // check if party1 must be present // ... <unit> <outputs[1-n]> // ecash_lib_1.OP_OVER, (0, ecash_lib_1.pushNumberOp)(minUnit), ecash_lib_1.OP_GREATERTHANOREQUAL, ecash_lib_1.OP_IF, // // extract value and output script (save remaining outputs) // ... <unit> <outputs[1-n]> // (0, ecash_lib_1.pushNumberOp)(8), ecash_lib_1.OP_SPLIT, (0, ecash_lib_1.pushNumberOp)(1), // first byte is the var size, always minimally encoded (size <= 25) ecash_lib_1.OP_SPLIT, ecash_lib_1.OP_SWAP, ecash_lib_1.OP_SPLIT, ecash_lib_1.OP_ROT, ecash_lib_1.OP_ROT, // // check output script, which includes address // ... <unit> <outputs[2-n]> <value1 as bin> <outputscript1> // (0, ecash_lib_1.pushBytesOp)(outputScript.bytecode), ecash_lib_1.OP_EQUALVERIFY, // // check that value/share == unit (the reverse of value = share * unit) // ... <unit> <outputs[2-n]> <value1 as bin> // ecash_lib_1.OP_BIN2NUM, (0, ecash_lib_1.pushNumberOp)(party.share), ecash_lib_1.OP_DIV, (0, ecash_lib_1.pushNumberOp)(2), ecash_lib_1.OP_PICK, ecash_lib_1.OP_EQUALVERIFY, ecash_lib_1.OP_ENDIF, ]); } // append common ending appendOps(ops, [ // // ensure that there are not more outputs // ... <unit> <outputs[3-n]> // // No extra outputs are allowed for safety and because the transaction // priority is relevant for the contract. By restricting outputs it's // possible to ensure that the fees are never less than the chosen amount. // // Without this it would be, in principle, possible to abuse high priority // contracts and redirect some of the fees to another output. // ecash_lib_1.OP_1ADD, // only a 0x00 tail is a valid number // // clean stack by dropping verified tail and unit // ... <unit> 1 // ecash_lib_1.OP_2DROP, ecash_lib_1.OP_ENDIF, ecash_lib_1.OP_ENDIF, // // reconstruct the preimage from its parts // <sig,sigflags> <preimage[1,2,3,4,5,6,7,8,9-10]> // ecash_lib_1.OP_CAT, ecash_lib_1.OP_CAT, ecash_lib_1.OP_CAT, ecash_lib_1.OP_CAT, ecash_lib_1.OP_CAT, ecash_lib_1.OP_CAT, ecash_lib_1.OP_CAT, ecash_lib_1.OP_CAT, // // add validation public key // <sig,sigflags> <preimage> // (0, ecash_lib_1.pushBytesOp)(pubKey), // // check transaction signature // <sig,sigflags> <preimage> <pubkey> // ecash_lib_1.OP_3DUP, ecash_lib_1.OP_NIP, ecash_lib_1.OP_CHECKSIGVERIFY, // // prepare signature for prehash validation by dropping sighas byte // <sig,sigflags> <preimage> <pubkey> // ecash_lib_1.OP_ROT, (0, ecash_lib_1.pushNumberOp)(64), ecash_lib_1.OP_SPLIT, ecash_lib_1.OP_DROP, // // prepare preimage for validation by hashing once // <preimage> <pubkey> <sig> // ecash_lib_1.OP_ROT, ecash_lib_1.OP_SHA256, // // validate preimage hash // <pubkey> <sig> <preimagehash> // ecash_lib_1.OP_ROT, ecash_lib_1.OP_CHECKDATASIG, // does the second sha256 before checking // // end // 1 // // All the validations done on prevouts and outputs rests on the preimage // being valid. That part is done by the OP_OP_CHECKDATASIG above, but // the validation is only meaningful after the previous OP_CHECKSIGVERIFY // with the same signature. That's what ensures that the preimage given as // input is the actual preimage for the utxo being spent. // ]); // return script from ops return ecash_lib_1.Script.fromOps(ops); } // // utility functions // function quotient(numerator, divisor) { // avoid Infinity if (divisor === 0) { throw new RangeError("Division by zero"); } // integer division, avoid working with floats return (numerator - numerator % divisor) / divisor; } function minUnitForShare(share) { // get how many units are needed to reach the dust level const shareUnit = quotient(546, share); // adjust units up if not perfectly divisible, basically Math.ceil(546/share) return Number(shareUnit) + (546 % share == 0 ? 0 : 1); } function minUnitForAllShares(parties) { const largestShare = parties.reduce((v, p) => Math.max(v, p.share), 0); return minUnitForShare(largestShare); } function appendOps(ops1, ops2) { ops2.forEach(op => ops1.push(op)); } //# sourceMappingURL=script.js.map