@node-dlc/bitcoin
Version:
404 lines • 15.5 kB
JavaScript
;
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