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
JavaScript
;
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