diff --git a/modules/ecash-lib/src/index.ts b/modules/ecash-lib/src/index.ts index e1b9f941e..97f614add 100644 --- a/modules/ecash-lib/src/index.ts +++ b/modules/ecash-lib/src/index.ts @@ -1,15 +1,17 @@ // Copyright (c) 2024 The Bitcoin developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. export * from './ecc.js'; export * from './hash.js'; +export * from './op.js'; +export * from './opcode.js'; export * from './script.js'; export * from './tx.js'; export * from './io/bytes.js'; export * from './io/hex.js'; export * from './io/int.js'; export * from './io/varsize.js'; export * from './io/writer.js'; export * from './io/writerbytes.js'; export * from './io/writerlength.js'; diff --git a/modules/ecash-lib/src/op.test.ts b/modules/ecash-lib/src/op.test.ts new file mode 100644 index 000000000..44a315311 --- /dev/null +++ b/modules/ecash-lib/src/op.test.ts @@ -0,0 +1,226 @@ +// Copyright (c) 2024 The Bitcoin developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +import { expect } from 'chai'; + +import { + OP_0, + OP_1, + OP_CHECKSIG, + OP_CODESEPARATOR, + OP_DUP, + OP_EQUAL, + OP_EQUALVERIFY, + OP_HASH160, + OP_PUSHDATA1, +} from './opcode.js'; +import { isPushOp, readOp, writeOp } from './op.js'; +import { Bytes, WriterBytes, fromHex, toHex } from './index.js'; + +const wrote = (size: number, fn: (writer: WriterBytes) => void) => { + const writer = new WriterBytes(size); + fn(writer); + return writer.data; +}; + +describe('Op', () => { + it('isPushOp', () => { + expect(isPushOp(null)).to.equal(false); + expect(isPushOp(OP_CODESEPARATOR)).to.equal(false); + expect(isPushOp(10)).to.equal(false); + expect(isPushOp({})).to.equal(false); + expect(isPushOp({ data: null })).to.equal(false); + expect(isPushOp({ opcode: null })).to.equal(false); + expect(isPushOp({ opcode: null, data: null })).to.equal(false); + expect(isPushOp({ opcode: 1, data: null })).to.equal(false); + expect(isPushOp({ opcode: null, data: new Uint8Array(1) })).to.equal( + false, + ); + expect(isPushOp({ opcode: 1, data: new Uint8Array(1) })).to.equal(true); + }); + it('Op.readOp all opcodes 0x00-0xff', () => { + // 00 is returned as just a single opcode, not as a PushOp + expect(readOp(new Bytes(fromHex('00')))).to.equal(OP_0); + + // Test all single-byte pushops + for (let opcode = 1; opcode <= 0x4b; ++opcode) { + const encoded = new Uint8Array([opcode, ...Array(opcode).keys()]); + const data = new Uint8Array([...Array(opcode).keys()]); + expect(readOp(new Bytes(encoded))).to.deep.equal({ + opcode, + data, + }); + } + + // OP_PUSHDATAn pushops + expect(readOp(new Bytes(fromHex('4c03456789')))).to.deep.equal({ + opcode: 0x4c, + data: fromHex('456789'), + }); + expect(readOp(new Bytes(fromHex('4d0300456789')))).to.deep.equal({ + opcode: 0x4d, + data: fromHex('456789'), + }); + expect(readOp(new Bytes(fromHex('4e03000000456789')))).to.deep.equal({ + opcode: 0x4e, + data: fromHex('456789'), + }); + + // Non-pushop opcodes + for (let opcode = 0x4f; opcode <= 0xff; ++opcode) { + expect(readOp(new Bytes(new Uint8Array([opcode])))).to.equal( + opcode, + ); + } + }); + it('Op.readOp P2PKH', () => { + const bytes = new Bytes( + fromHex('76a914012345678901234567890123456789012345678988ac'), + ); + expect(readOp(bytes)).to.equal(OP_DUP); + expect(readOp(bytes)).to.equal(OP_HASH160); + expect(readOp(bytes)).to.deep.equal({ + opcode: 20, + data: fromHex('0123456789012345678901234567890123456789'), + }); + expect(readOp(bytes)).to.equal(OP_EQUALVERIFY); + expect(readOp(bytes)).to.equal(OP_CHECKSIG); + }); + it('Op.readOp P2SH', () => { + const bytes = new Bytes( + fromHex('a914abcdeabcdeabcdeabcdeabcdeabcdeabcdeabcde87'), + ); + expect(readOp(bytes)).to.equal(OP_HASH160); + expect(readOp(bytes)).to.deep.equal({ + opcode: 20, + data: fromHex('abcdeabcdeabcdeabcdeabcdeabcdeabcdeabcde'), + }); + expect(readOp(bytes)).to.equal(OP_EQUAL); + }); + it('Op.readOp failure', () => { + expect(() => readOp(new Bytes(fromHex('01')))).to.throw( + 'Not enough bytes: Tried reading 1 byte(s), but there are only 0 byte(s) left', + ); + expect(() => readOp(new Bytes(fromHex('0200')))).to.throw( + 'Not enough bytes: Tried reading 2 byte(s), but there are only 1 byte(s) left', + ); + expect(() => readOp(new Bytes(fromHex('4c')))).to.throw( + 'Not enough bytes: Tried reading 1 byte(s), but there are only 0 byte(s) left', + ); + expect(() => readOp(new Bytes(fromHex('4d00')))).to.throw( + 'Not enough bytes: Tried reading 2 byte(s), but there are only 1 byte(s) left', + ); + }); + it('Op.writeOp all opcodes 0x00-0xff', () => { + expect(wrote(1, writer => writeOp(OP_0, writer))).to.deep.equal( + fromHex('00'), + ); + expect( + wrote(1, writer => + writeOp({ opcode: 0, data: new Uint8Array() }, writer), + ), + ).to.deep.equal(fromHex('00')); + + // Test all single-byte pushops + for (let opcode = 1; opcode <= 0x4b; ++opcode) { + const encoded = new Uint8Array([opcode, ...Array(opcode).keys()]); + const data = new Uint8Array([...Array(opcode).keys()]); + expect( + wrote(opcode + 1, writer => writeOp({ opcode, data }, writer)), + ).to.deep.equal(encoded); + } + + // OP_PUSHDATAn pushops + expect( + wrote(5, writer => + writeOp( + { + opcode: 0x4c, + data: fromHex('456789'), + }, + writer, + ), + ), + ).to.deep.equal(fromHex('4c03456789')); + expect( + wrote(6, writer => + writeOp( + { + opcode: 0x4d, + data: fromHex('456789'), + }, + writer, + ), + ), + ).to.deep.equal(fromHex('4d0300456789')); + expect( + wrote(8, writer => + writeOp( + { + opcode: 0x4e, + data: fromHex('456789'), + }, + writer, + ), + ), + ).to.deep.equal(fromHex('4e03000000456789')); + + // Non-pushop opcodes + for (let opcode = 0x4f; opcode <= 0xff; ++opcode) { + expect(wrote(1, writer => writeOp(opcode, writer))).to.deep.equal( + new Uint8Array([opcode]), + ); + } + }); + it('Op.writeOp P2PKH', () => { + const writer = new WriterBytes(25); + writeOp(OP_DUP, writer); + writeOp(OP_HASH160, writer); + writeOp({ opcode: 20, data: new Uint8Array(20) }, writer); + writeOp(OP_EQUALVERIFY, writer); + writeOp(OP_CHECKSIG, writer); + expect(toHex(writer.data)).to.be.equal( + '76a914000000000000000000000000000000000000000088ac', + ); + }); + it('Op.writeOp P2SH', () => { + const writer = new WriterBytes(23); + writeOp(OP_HASH160, writer); + writeOp({ opcode: 20, data: new Uint8Array(20) }, writer); + writeOp(OP_EQUAL, writer); + expect(toHex(writer.data)).to.be.equal( + 'a914000000000000000000000000000000000000000087', + ); + }); + it('Op.writeOp failure', () => { + expect(() => writeOp(OP_1, new WriterBytes(0))).to.throw( + 'Not enough bytes: Tried writing 1 byte(s), but only 0 byte(s) have been pre-allocated', + ); + + expect(() => + writeOp( + { opcode: OP_PUSHDATA1, data: new Uint8Array() }, + new WriterBytes(0), + ), + ).to.throw( + 'Not enough bytes: Tried writing 1 byte(s), but only 0 byte(s) have been pre-allocated', + ); + + expect(() => + writeOp( + { opcode: OP_CHECKSIG, data: new Uint8Array() }, + new WriterBytes(100), + ), + ).to.throw('Not a pushop opcode: 0xac'); + + expect(() => + writeOp( + { opcode: 2, data: new Uint8Array() }, + new WriterBytes(100), + ), + ).to.throw( + 'Inconsistent PushOp, claims to push 2 bytes but actually has 0 bytes attached', + ); + }); +}); diff --git a/modules/ecash-lib/src/op.ts b/modules/ecash-lib/src/op.ts new file mode 100644 index 000000000..c3dae2f6d --- /dev/null +++ b/modules/ecash-lib/src/op.ts @@ -0,0 +1,92 @@ +// Copyright (c) 2024 The Bitcoin developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +import { Bytes } from './io/bytes.js'; +import { Writer } from './io/writer.js'; +import { OP_PUSHDATA1, OP_PUSHDATA2, OP_PUSHDATA4, Opcode } from './opcode.js'; + +/** + * A single operation in Bitcoin script, either a singular non-pushop code or + * a `PushOp` with an opcode and data attached. + **/ +export type Op = Opcode | PushOp; + +/** + * An Op that pushes some data onto the stack, will use `opcode` to push the + * data + **/ +export interface PushOp { + opcode: Opcode; + data: Uint8Array; +} + +/** Returns true if the given object is a `PushOp` */ +export function isPushOp(op: any): op is PushOp { + if (!op || typeof op !== 'object') { + return false; + } + if (!op.hasOwnProperty('opcode') || !op.hasOwnProperty('data')) { + return false; + } + return typeof op.opcode === 'number' && op.data instanceof Uint8Array; +} + +/** Read a single Script operation from the bytes */ +export function readOp(bytes: Bytes): Op { + const opcode = bytes.readU8(); + let numBytes: number; + switch (opcode) { + case OP_PUSHDATA1: + numBytes = bytes.readU8(); + break; + case OP_PUSHDATA2: + numBytes = bytes.readU16(); + break; + case OP_PUSHDATA4: + numBytes = bytes.readU32(); + break; + default: + if (opcode < 0x01 || opcode > 0x4b) { + // Non-push opcode + return opcode; + } + numBytes = opcode; + } + const data = bytes.readBytes(numBytes); + return { opcode, data }; +} + +/** Write a Script operation to the writer */ +export function writeOp(op: Op, writer: Writer) { + if (typeof op == 'number') { + writer.putU8(op); + return; + } + if (!isPushOp(op)) { + throw `Unexpected op: ${op}`; + } + writer.putU8(op.opcode); + switch (op.opcode) { + case OP_PUSHDATA1: + writer.putU8(op.data.length); + break; + case OP_PUSHDATA2: + writer.putU16(op.data.length); + break; + case OP_PUSHDATA4: + writer.putU32(op.data.length); + break; + default: + if (op.opcode < 0 || op.opcode > 0x4b) { + throw `Not a pushop opcode: 0x${op.opcode.toString(16)}`; + } + if (op.opcode != op.data.length) { + throw ( + `Inconsistent PushOp, claims to push ${op.opcode} bytes ` + + `but actually has ${op.data.length} bytes attached` + ); + } + } + writer.putBytes(op.data); +} diff --git a/modules/ecash-lib/src/opcode.ts b/modules/ecash-lib/src/opcode.ts new file mode 100644 index 000000000..132dc8dea --- /dev/null +++ b/modules/ecash-lib/src/opcode.ts @@ -0,0 +1,154 @@ +// Copyright (c) 2024 The Bitcoin developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +// Script opcodes, from /src/script/script.h + +export type Opcode = number; + +// push value +export const OP_0 = 0x00; +export const OP_FALSE = OP_0; +export const OP_PUSHDATA1 = 0x4c; +export const OP_PUSHDATA2 = 0x4d; +export const OP_PUSHDATA4 = 0x4e; +export const OP_1NEGATE = 0x4f; +export const OP_RESERVED = 0x50; +export const OP_1 = 0x51; +export const OP_TRUE = OP_1; +export const OP_2 = 0x52; +export const OP_3 = 0x53; +export const OP_4 = 0x54; +export const OP_5 = 0x55; +export const OP_6 = 0x56; +export const OP_7 = 0x57; +export const OP_8 = 0x58; +export const OP_9 = 0x59; +export const OP_10 = 0x5a; +export const OP_11 = 0x5b; +export const OP_12 = 0x5c; +export const OP_13 = 0x5d; +export const OP_14 = 0x5e; +export const OP_15 = 0x5f; +export const OP_16 = 0x60; + +// control +export const OP_NOP = 0x61; +export const OP_VER = 0x62; +export const OP_IF = 0x63; +export const OP_NOTIF = 0x64; +export const OP_VERIF = 0x65; +export const OP_VERNOTIF = 0x66; +export const OP_ELSE = 0x67; +export const OP_ENDIF = 0x68; +export const OP_VERIFY = 0x69; +export const OP_RETURN = 0x6a; + +// stack ops +export const OP_TOALTSTACK = 0x6b; +export const OP_FROMALTSTACK = 0x6c; +export const OP_2DROP = 0x6d; +export const OP_2DUP = 0x6e; +export const OP_3DUP = 0x6f; +export const OP_2OVER = 0x70; +export const OP_2ROT = 0x71; +export const OP_2SWAP = 0x72; +export const OP_IFDUP = 0x73; +export const OP_DEPTH = 0x74; +export const OP_DROP = 0x75; +export const OP_DUP = 0x76; +export const OP_NIP = 0x77; +export const OP_OVER = 0x78; +export const OP_PICK = 0x79; +export const OP_ROLL = 0x7a; +export const OP_ROT = 0x7b; +export const OP_SWAP = 0x7c; +export const OP_TUCK = 0x7d; + +// splice ops +export const OP_CAT = 0x7e; +export const OP_SPLIT = 0x7f; // after monolith upgrade (May 2018) +export const OP_NUM2BIN = 0x80; // after monolith upgrade (May 2018) +export const OP_BIN2NUM = 0x81; // after monolith upgrade (May 2018) +export const OP_SIZE = 0x82; + +// bit logic +export const OP_INVERT = 0x83; +export const OP_AND = 0x84; +export const OP_OR = 0x85; +export const OP_XOR = 0x86; +export const OP_EQUAL = 0x87; +export const OP_EQUALVERIFY = 0x88; +export const OP_RESERVED1 = 0x89; +export const OP_RESERVED2 = 0x8a; + +// numeric +export const OP_1ADD = 0x8b; +export const OP_1SUB = 0x8c; +export const OP_2MUL = 0x8d; +export const OP_2DIV = 0x8e; +export const OP_NEGATE = 0x8f; +export const OP_ABS = 0x90; +export const OP_NOT = 0x91; +export const OP_0NOTEQUAL = 0x92; + +export const OP_ADD = 0x93; +export const OP_SUB = 0x94; +export const OP_MUL = 0x95; +export const OP_DIV = 0x96; +export const OP_MOD = 0x97; +export const OP_LSHIFT = 0x98; +export const OP_RSHIFT = 0x99; + +export const OP_BOOLAND = 0x9a; +export const OP_BOOLOR = 0x9b; +export const OP_NUMEQUAL = 0x9c; +export const OP_NUMEQUALVERIFY = 0x9d; +export const OP_NUMNOTEQUAL = 0x9e; +export const OP_LESSTHAN = 0x9f; +export const OP_GREATERTHAN = 0xa0; +export const OP_LESSTHANOREQUAL = 0xa1; +export const OP_GREATERTHANOREQUAL = 0xa2; +export const OP_MIN = 0xa3; +export const OP_MAX = 0xa4; + +export const OP_WITHIN = 0xa5; + +// crypto +export const OP_RIPEMD160 = 0xa6; +export const OP_SHA1 = 0xa7; +export const OP_SHA256 = 0xa8; +export const OP_HASH160 = 0xa9; +export const OP_HASH256 = 0xaa; +export const OP_CODESEPARATOR = 0xab; +export const OP_CHECKSIG = 0xac; +export const OP_CHECKSIGVERIFY = 0xad; +export const OP_CHECKMULTISIG = 0xae; +export const OP_CHECKMULTISIGVERIFY = 0xaf; + +// expansion +export const OP_NOP1 = 0xb0; +export const OP_CHECKLOCKTIMEVERIFY = 0xb1; +export const OP_NOP2 = OP_CHECKLOCKTIMEVERIFY; +export const OP_CHECKSEQUENCEVERIFY = 0xb2; +export const OP_NOP3 = OP_CHECKSEQUENCEVERIFY; +export const OP_NOP4 = 0xb3; +export const OP_NOP5 = 0xb4; +export const OP_NOP6 = 0xb5; +export const OP_NOP7 = 0xb6; +export const OP_NOP8 = 0xb7; +export const OP_NOP9 = 0xb8; +export const OP_NOP10 = 0xb9; + +// More crypto +export const OP_CHECKDATASIG = 0xba; +export const OP_CHECKDATASIGVERIFY = 0xbb; + +// additional byte string operations +export const OP_REVERSEBYTES = 0xbc; + +// multi-byte opcodes +export const OP_PREFIX_BEGIN = 0xf0; +export const OP_PREFIX_END = 0xf7; + +export const OP_INVALIDOPCODE = 0xff;