UNPKG

@node-dlc/bitcoin

Version:
404 lines 15.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Script = void 0; const bufio_1 = require("@node-dlc/bufio"); const bufio_2 = require("@node-dlc/bufio"); const bufio_3 = require("@node-dlc/bufio"); const crypto_1 = require("@node-dlc/crypto"); const BitcoinError_1 = require("./BitcoinError"); const BitcoinErrorCode_1 = require("./BitcoinErrorCode"); const OpCodes_1 = require("./OpCodes"); const SigHashType_1 = require("./SigHashType"); const Stack_1 = require("./Stack"); function asssertValidSig(sig) { const der = sig.slice(0, sig.length - 1); const hashtype = sig[sig.length - 1]; if (!(0, crypto_1.isDERSig)(der)) { throw new BitcoinError_1.BitcoinError(BitcoinErrorCode_1.BitcoinErrorCode.SigEncodingInvalid); } if (!(0, SigHashType_1.isSigHashTypeValid)(hashtype)) { throw new BitcoinError_1.BitcoinError(BitcoinErrorCode_1.BitcoinErrorCode.SigHashTypeInvalid, hashtype); } } function assertValidPubKey(pubkey) { if (!(0, crypto_1.validPublicKey)(pubkey)) { throw new BitcoinError_1.BitcoinError(BitcoinErrorCode_1.BitcoinErrorCode.PubKeyInvalid); } } /** * Bitcoin Script */ class Script { /** * * @param val */ static number(val) { // Zero is encoded as OP_0 if (val === 0 || val === BigInt(0)) { return OpCodes_1.OpCode.OP_0; } // 1-16 is encoded as OP_1 to OP_16 if (val >= 1 && val <= 16) { return 0x50 + Number(val); } // anything else is encoded as signed-magnitude value // and added to the script as raw push bytes return Stack_1.Stack.encodeNum(val); } /** * Creates a standard (though no longer used) pay-to-pubkey * scriptPubKey using the provided pubkey. * * P2PK format: * <pubkey> OP_CHECKSIG * * @param pubkey 33-byte compressed or 65-byte uncompressed SEC * encoded pubkey */ static p2pkLock(pubkey) { assertValidPubKey(pubkey); return new Script(pubkey, OpCodes_1.OpCode.OP_CHECKSIG); } /** * Creates a standard (though no longer used) pay-to-pubkey * scriptSig using the provided signature. * * P2PK format: * <sig> * * @param sig DER encoded signature + 1-byte sighash type */ static p2pkUnlock(sig) { asssertValidSig(sig); return new Script(sig); } /** * Creates a standard Pay-to-Public-Key-Hash scriptPubKey by accepting a * hash of a public key as input and generating the script in the standard * P2PKH script format: * OP_DUP OP_HASH160 <hash160pubkey> OP_EQUALVERIFY OP_CHECKSIG * * @param value either the 20-byte hash160 of a pubkey or an SEC * encoded compressed or uncompressed pubkey */ static p2pkhLock(value) { // if not a hash160, then it must be a valid pubkey if (value.length !== 20) { assertValidPubKey(value); } // either the hash value or a valid pubkey that needs to be hashed const hash160PubKey = value.length === 20 ? value : (0, crypto_1.hash160)(value); return new Script(OpCodes_1.OpCode.OP_DUP, OpCodes_1.OpCode.OP_HASH160, hash160PubKey, OpCodes_1.OpCode.OP_EQUALVERIFY, OpCodes_1.OpCode.OP_CHECKSIG); } /** * Creates a standard Pay-to-Public-Key-Hash scriptSig * @param sig BIP66 compliant DER encoded signuture + hash byte * @param pubkey SEC encoded public key */ static p2pkhUnlock(sig, pubkey) { asssertValidSig(sig); assertValidPubKey(pubkey); return new Script(sig, pubkey); } /** * Creates a standard Pay-to-MultiSig scriptPubKey by accepting m of n * public keys as inputs in the format: * OP_<m> <pubkey1> <pubkey2> <pubkey..m> OP_<n> OP_CHECKMULTISIG */ static p2msLock(m, ...pubkeys) { // assert all public keys are valid for (const pubkey of pubkeys) { assertValidPubKey(pubkey); } // ensure proper number of keys if (m < 1 || m > pubkeys.length || pubkeys.length === 0 || pubkeys.length > 20) { throw new BitcoinError_1.BitcoinError(BitcoinErrorCode_1.BitcoinErrorCode.MultiSigSetupInvalid); } return new Script(Script.number(m), ...pubkeys, Script.number(pubkeys.length), OpCodes_1.OpCode.OP_CHECKMULTISIG); // prettier-ignore } /** * Creates a standard Pay-to-MultiSig scripSig using the provided * signatures. The signatures must be in the order of the pub keys * used in the lock script. This function also correctly adds OP_0 * as the first element on the stack to ensure the p2ms off-by-one * error is correctly accounted for. * * Each signature must be DER encoded using BIP66 and * include a 1-byte sighash type at the end. The builder validates * the signatures. As such they will be 10 to 74 bytes. * * @param pubkeys */ static p2msUnlock(...sigs) { // assert all signatures for (const sig of sigs) { asssertValidSig(sig); } return new Script(OpCodes_1.OpCode.OP_0, ...sigs); // prettier-ignore } /** * Creates a standard Pay-to-Script-Hash scriptPubKey by accepting a hash of * the redeem script as input and generating the P2SH script: * OP_HASH160 <hashScript> OP_EQUAL * * Accepts the redeem script either as a Script object or as the * hash160 of the redeem script. When the hash160 Buffer is provided * it will throw if the Buffer is not 20-bytes. * * @param value can be either the redeem script as a Script type or * the hash160 as a 20-byte buffer */ static p2shLock(value) { const scriptHash160 = value instanceof Script ? value.hash160() : value; if (scriptHash160.length !== 20) { throw new BitcoinError_1.BitcoinError(BitcoinErrorCode_1.BitcoinErrorCode.Hash160Invalid, { got: scriptHash160.length, expected: 20, }); } return new Script(OpCodes_1.OpCode.OP_HASH160, scriptHash160, OpCodes_1.OpCode.OP_EQUAL); // prettier-ignore } static p2shUnlock(redeemScript, ...data) { if (data[0] instanceof Script) { return new Script(...data[0].cmds, redeemScript.serializeCmds()); } else { return new Script(...data, redeemScript.serializeCmds()); } } /** * Create a standard Pay-to-Witness-PubKey-Hash scriptPubKey by accepting * the hash160 of a compressed public key point as input. It is of the * format: * OP_0 <hash160_pubkey> * * @param value either a 20-byte pubkeyhash or a valid pubkey */ static p2wpkhLock(value) { // if not a hash160, then it must be a valid pubkey if (value.length !== 20) { assertValidPubKey(value); } // either the hash value or a valid pubkey that needs to be hashed const hash160PubKey = value.length === 20 ? value : (0, crypto_1.hash160)(value); return new Script(OpCodes_1.OpCode.OP_0, hash160PubKey); // prettier-ignore } /** * Create a standard Pay-to-Witness-Script-Hash scriptPubKey by accepting * the sha256 of the witness script as input. It is of the format: * OP_0 <sha256_redeem_script> */ static p2wshLock(value) { const sha256Script = value instanceof Buffer ? value : (0, crypto_1.sha256)(value.serializeCmds()); if (sha256Script.length !== 32) { throw new BitcoinError_1.BitcoinError(BitcoinErrorCode_1.BitcoinErrorCode.Hash256Invalid); } return new Script(OpCodes_1.OpCode.OP_0, sha256Script); // prettier-ignore } /** * Parses a stream of bytes representing a Script. The stream must start * with a Varint length of Script data. The Script data is then parsed into * data blocks or op_codes depending on the meaning of the bytes * @param stream */ static parse(reader) { // read the length const len = reader.readVarInt(); // read the length of bytes occupied by the script and then pass it // through the command parser. const buf = reader.readBytes(Number(len)); const cmds = Script.parseCmds(buf); // return the script object with the commands return new Script(...cmds); } /** * When supplied with a Buffer of cmds this method will parse the commands * into data blocks or op_codes depending on the meaning of the bytes * @param buf */ static parseCmds(buf) { const br = new bufio_3.BufferReader(buf); // commands that were read off the stack const cmds = []; // loop until all bytes have been read while (!br.eof) { // read the current command from the stream const op = br.readUInt8(); // data range between 1-75 bytes is OP_PUSHBYTES_xx and we simple // read the xx number of bytes off the script if (op >= 0x01 && op <= 0x4b) { const n = op; const bytes = br.readBytes(n); cmds.push(bytes); } // data range between 76 and 255 bytes uses OP_PUSHDATA1 and uses // the format with a single byte length and then the n bytes are // read from the script else if (op === OpCodes_1.OpCode.OP_PUSHDATA1) { const n = br.readUInt8(); cmds.push(br.readBytes(n)); } // data range between 256 and 520 uses OP_PUSHDATA2 and uses the // format with two bytes little-endian to determine the n bytes of // data of data that need to be read. else if (op === OpCodes_1.OpCode.OP_PUSHDATA2) { const n = br.readUInt16LE(); cmds.push(br.readBytes(n)); } // otherwise the value is an opcode that should be added to the cmds else { cmds.push(op); } } return cmds; } /** * Constructs a Script with the supplied ScriptCmd values * @param cmds */ constructor(...cmds) { this.cmds = cmds; } /** * Returns true if other script is an exact match of the current script. * This requires all data element sto be exact matches and all operations * to be exact matches. * @param other */ equals(other) { if (this.cmds.length !== other.cmds.length) return false; for (let i = 0; i < this.cmds.length; i++) { const l = this.cmds[i]; const r = other.cmds[i]; if (Buffer.isBuffer(l) && Buffer.isBuffer(r)) { if (!l.equals(r)) return false; } else { if (l !== r) return false; } } return true; } /** * Returns a string with the friendly name of the opcode. For data, * it returns the value in hexadecimal format. */ toString() { return this.cmds .map((cmd) => { if (Buffer.isBuffer(cmd)) { return cmd.toString('hex'); } else { return OpCodes_1.OpCode[cmd]; } }) .join(' '); } /** * Returns a JSON serialization of the Script. */ toJSON() { return this.toString(); } /** * Serializes the Script to a Buffer by serializing the cmds prefixed with * the overall length as a varint. Therefore the format of this method is * the format used when encoding a Script and is: * * [varint]: length * [length]: script_cmds */ serialize() { // first obtain the length of all commands in the script const cmdBuf = this.serializeCmds(); // capture the length of cmd buffer const len = (0, bufio_2.encodeVarInt)(cmdBuf.length); // return combined buffer return Buffer.concat([len, cmdBuf]); } /** * Serializes the commands to a buffer. This information is the raw * serialization and can be directly parsed with the `parseCmds` method. */ serializeCmds() { const results = []; for (const op of this.cmds) { // OP_CODES are just an integers and can just be pushed directly onto // the byte array after being converted into a single byte buffer if (typeof op === 'number') { const opBuf = Buffer.from([op]); results.push(opBuf); } // elements will be represented as a buffer of information and we will // use the length to determine how to encode it else if (op instanceof Buffer) { // between 1 and 75 bytes are OP_PUSHBYTES_XX // there is no op_code for these. We first need to push // the length of the buffer array though as the operation if (op.length >= 1 && op.length <= 75) { results.push(Buffer.from([op.length])); results.push(op); } // between 76 and 255 uses OP_PUSHDATA1 // this requires us to push the op_code, a single byte length // and finally push the 76-255 bytes of data else if (op.length >= 76 && op.length <= 255) { results.push(Buffer.from([OpCodes_1.OpCode.OP_PUSHDATA1])); // op_pushdata1 results.push(Buffer.from([op.length])); // single-byte results.push(op); } // between 256 and 520 uses OP_PUSHDATA2 // this requires us to push the op_code, a two-byte little-endian number // and finally push the 256-520 bytes of data else if (op.length >= 256 && op.length <= 520) { results.push(Buffer.from([OpCodes_1.OpCode.OP_PUSHDATA2])); // op_pushdata2 results.push((0, bufio_1.bigToBufLE)(BigInt(op.length), 2)); // two-bytes little-endian results.push(op); } // data longer than 520 is not supported else { throw new Error('Data too long'); } } } // combine all parts of data return Buffer.concat(results); } /** * Clone via deep copy */ clone() { return new Script(...this.cmds.map((cmd) => { if (Buffer.isBuffer(cmd)) { return Buffer.from(cmd); } else { return cmd; } })); } /** * Performs a hash160 on the serialized commands. This is useful for * turning a script into a P2SH redeem script. */ hash160() { return (0, crypto_1.hash160)(this.serializeCmds()); } /** * Performs a sha256 hash on the serialized commands. This is useful * fro turning a script into a P2WSH lock script. */ sha256() { return (0, crypto_1.sha256)(this.serializeCmds()); } } exports.Script = Script; //# sourceMappingURL=Script.js.map