UNPKG

@node-dlc/bitcoin

Version:
510 lines (451 loc) 15.4 kB
import { bigToBufLE } from '@node-dlc/bufio'; import { encodeVarInt } from '@node-dlc/bufio'; import { BufferReader } from '@node-dlc/bufio'; import { StreamReader } from '@node-dlc/bufio'; import { hash160, isDERSig, sha256, validPublicKey } from '@node-dlc/crypto'; import { BitcoinError } from './BitcoinError'; import { BitcoinErrorCode } from './BitcoinErrorCode'; import { ICloneable } from './ICloneable'; import { OpCode } from './OpCodes'; import { ScriptCmd } from './ScriptCmd'; import { isSigHashTypeValid } from './SigHashType'; import { Stack } from './Stack'; function asssertValidSig(sig: Buffer) { const der = sig.slice(0, sig.length - 1); const hashtype = sig[sig.length - 1]; if (!isDERSig(der)) { throw new BitcoinError(BitcoinErrorCode.SigEncodingInvalid); } if (!isSigHashTypeValid(hashtype)) { throw new BitcoinError(BitcoinErrorCode.SigHashTypeInvalid, hashtype); } } function assertValidPubKey(pubkey: Buffer) { if (!validPublicKey(pubkey)) { throw new BitcoinError(BitcoinErrorCode.PubKeyInvalid); } } /** * Bitcoin Script */ export class Script implements ICloneable<Script> { /** * * @param val */ public static number(val: number | bigint): ScriptCmd { // Zero is encoded as OP_0 if (val === 0 || val === BigInt(0)) { return 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.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 */ public static p2pkLock(pubkey: Buffer): Script { assertValidPubKey(pubkey); return new Script(pubkey, 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 */ public static p2pkUnlock(sig: Buffer): Script { 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 */ public static p2pkhLock(value: Buffer): Script { // 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 : hash160(value); return new Script( OpCode.OP_DUP, OpCode.OP_HASH160, hash160PubKey, OpCode.OP_EQUALVERIFY, 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 */ public static p2pkhUnlock(sig: Buffer, pubkey: Buffer): Script { 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 */ public static p2msLock(m: number, ...pubkeys: Buffer[]): Script { // 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(BitcoinErrorCode.MultiSigSetupInvalid); } return new Script( Script.number(m), ...pubkeys, Script.number(pubkeys.length), 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 */ public static p2msUnlock(...sigs: Buffer[]): Script { // assert all signatures for (const sig of sigs) { asssertValidSig(sig); } return new Script( 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 */ public static p2shLock(value: Script | Buffer): Script { const scriptHash160 = value instanceof Script ? value.hash160() : value; if (scriptHash160.length !== 20) { throw new BitcoinError(BitcoinErrorCode.Hash160Invalid, { got: scriptHash160.length, expected: 20, }); } return new Script( OpCode.OP_HASH160, scriptHash160, OpCode.OP_EQUAL, ); // prettier-ignore } /** * Creates a p2sh unlock script for use in a transaction input * scriptSig value. The redeem script, which is the preimage of the * of the script hash used to lock the p2sh output, must be provided * along with any additional data required to unlock the script. * * @param redeemScript preimage of the script hash * @param data script commands will be added as unlock data */ public static p2shUnlock(redeemScript: Script, data: Script): Script; /** * Creates a p2sh unlock script for use in a transaction input * scriptSig value. The redeem script, which is the preimage of the * of the script hash used to lock the p2sh output, must be provided * along with any additional data required to unlock the script. * * @param redeemScript preimage of the script hash * @param data ScriptCmd data used to unlock the script */ public static p2shUnlock(redeemScript: Script, ...data: ScriptCmd[]): Script; public static p2shUnlock( redeemScript: Script, ...data: Script[] | ScriptCmd[] ): Script { if (data[0] instanceof Script) { return new Script(...data[0].cmds, redeemScript.serializeCmds()); } else { return new Script(...(data as ScriptCmd[]), 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 */ public static p2wpkhLock(value: Buffer): Script { // 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 : hash160(value); return new Script( 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> */ public static p2wshLock(value: Buffer | Script): Script { const sha256Script = value instanceof Buffer ? value : sha256(value.serializeCmds()); if (sha256Script.length !== 32) { throw new BitcoinError(BitcoinErrorCode.Hash256Invalid); } return new Script( 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 */ public static parse(reader: StreamReader): Script { // 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 */ public static parseCmds(buf: Buffer): ScriptCmd[] { const br = new BufferReader(buf); // commands that were read off the stack const cmds: ScriptCmd[] = []; // 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 === 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 === 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; } /** * Commands belonging to the script */ public readonly cmds: ScriptCmd[]; /** * Constructs a Script with the supplied ScriptCmd values * @param cmds */ constructor(...cmds: ScriptCmd[]) { 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 */ public equals(other: Script): boolean { 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. */ public toString(): string { return this.cmds .map((cmd) => { if (Buffer.isBuffer(cmd)) { return cmd.toString('hex'); } else { return OpCode[cmd as OpCode]; } }) .join(' '); } /** * Returns a JSON serialization of the Script. */ public toJSON(): string { 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 */ public serialize(): Buffer { // first obtain the length of all commands in the script const cmdBuf = this.serializeCmds(); // capture the length of cmd buffer const len = 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. */ public serializeCmds(): Buffer { const results: Buffer[] = []; 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([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([OpCode.OP_PUSHDATA2])); // op_pushdata2 results.push(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 */ public clone(): Script { 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. */ public hash160(): Buffer { return hash160(this.serializeCmds()); } /** * Performs a sha256 hash on the serialized commands. This is useful * fro turning a script into a P2WSH lock script. */ public sha256(): Buffer { return sha256(this.serializeCmds()); } }