Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 137 additions & 1 deletion src/primitives/TransactionSignature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,122 @@ import BigNumber from './BigNumber.js'
import * as Hash from './Hash.js'
import { toArray, Writer } from './utils.js'
import Script from '../script/Script.js'
import OP from '../script/OP.js'
import TransactionInput from '../transaction/TransactionInput.js'
import TransactionOutput from '../transaction/TransactionOutput.js'

export default class TransactionSignature extends Signature {
public static readonly SIGHASH_ALL = 0x00000001
public static readonly SIGHASH_NONE = 0x00000002
public static readonly SIGHASH_SINGLE = 0x00000003
public static readonly SIGHASH_CHRONICLE = 0x00000020
public static readonly SIGHASH_FORKID = 0x00000040
public static readonly SIGHASH_ANYONECANPAY = 0x00000080

scope: number

static format (params: {
/**
* Implements the original bitcoin transaction signature digest preimage algorithm (OTDA).
* @param params
* @returns preimage as a byte array
*/
static formatOTDA (params: {
sourceTXID: string
sourceOutputIndex: number
sourceSatoshis: number
transactionVersion: number
otherInputs: TransactionInput[]
outputs: TransactionOutput[]
inputIndex: number
subscript: Script
inputSequence: number
lockTime: number
scope: number
}): number[] {
const isAnyoneCanPay = (params.scope & TransactionSignature.SIGHASH_ANYONECANPAY) === TransactionSignature.SIGHASH_ANYONECANPAY
const isSingle = (params.scope & 31) === TransactionSignature.SIGHASH_SINGLE
const isNone = (params.scope & 31) === TransactionSignature.SIGHASH_NONE
const isAll = (params.scope & 31) === TransactionSignature.SIGHASH_ALL || (!isSingle && !isNone)

const subscript = new Script([...params.subscript.chunks])
subscript.findAndDelete(new Script().writeOpCode(OP.OP_CODESEPARATOR))

const currentInput = {
sourceTXID: params.sourceTXID,
sourceOutputIndex: params.sourceOutputIndex,
sequence: params.inputSequence,
script: subscript.toBinary()
}

const writer = new Writer()

function writeInputs (inputs: Array<{ sourceTXID: string, sourceOutputIndex: number, sequence: number, script: number[] }>): void {
writer.writeVarIntNum(inputs.length)
for (const input of inputs) {
writer.writeReverse(toArray(input.sourceTXID, 'hex'))
writer.writeUInt32LE(input.sourceOutputIndex)
writer.writeVarIntNum(input.script.length)
writer.write(input.script)
writer.writeUInt32LE(input.sequence)
}
}

function writeOutputs (outputs: Array<{ satoshis: number, script: number[] }>): void {
writer.writeVarIntNum(outputs.length)
for (const output of outputs) {
writer.writeUInt64LE(output.satoshis)
writer.writeVarIntNum(output.script.length)
writer.write(output.script)
}
}

// Version
writer.writeInt32LE(params.transactionVersion)

const emptyScript = new Script().toBinary()

if (!isAnyoneCanPay) {
const inputs = params.otherInputs.map(input => ({
sourceTXID: input.sourceTXID ?? input.sourceTransaction?.id('hex') ?? '',
sourceOutputIndex: input.sourceOutputIndex,
sequence: (isSingle || isNone) ? 0 : (input.sequence ?? 0xffffffff), // Default to max sequence number
script: emptyScript
}))
inputs.splice(params.inputIndex, 0, currentInput)
writeInputs(inputs)
} else if (isAnyoneCanPay) {
writeInputs([currentInput])
}

if (isAll) {
const outputs = params.outputs.map(output => ({
satoshis: output.satoshis ?? 0, // Default to 0 if undefined
script: output.lockingScript.toBinary()
}))
writeOutputs(outputs)
} else if (isSingle) {
const outputs: Array<{ satoshis: number, script: number[] }> = []
for (let i = 0; i < params.inputIndex; i++) outputs.push({ satoshis: -1, script: emptyScript })
const o = params.outputs[params.inputIndex]
if (o !== undefined) { outputs.push({ satoshis: o.satoshis ?? 0, script: o.lockingScript.toBinary() }) }
writeOutputs(outputs)
} else if (isNone) {
writeOutputs([])
}

// Locktime
writer.writeUInt32LE(params.lockTime)

// sighashType
writer.writeUInt32LE(params.scope >>> 0)

const buf = writer.toArray()
// const preimage = toHex(buf)
// const sighash = toHex(Hash.hash256(buf))
return buf
}

static formatBip143 (params: {
sourceTXID: string
sourceOutputIndex: number
sourceSatoshis: number
Expand Down Expand Up @@ -164,9 +267,42 @@ export default class TransactionSignature extends Signature {
writer.writeUInt32LE(params.scope >>> 0)

const buf = writer.toArray()
// const preimage = toHex(buf)
// const sighash = toHex(Hash.hash256(buf))
return buf
}

static format (params: {
sourceTXID: string
sourceOutputIndex: number
sourceSatoshis: number
transactionVersion: number
otherInputs: TransactionInput[]
outputs: TransactionOutput[]
inputIndex: number
subscript: Script
inputSequence: number
lockTime: number
scope: number
/**
* Supports running bitcoin-abc test vectors which reuses the CHRONICLE bit.
*/
ignoreChronicle?: boolean
}): number[] {
const hasForkId = (params.scope & TransactionSignature.SIGHASH_FORKID) !== 0
const hasChronicle = params.ignoreChronicle !== true && (params.scope & TransactionSignature.SIGHASH_CHRONICLE) !== 0

if (hasForkId && !hasChronicle) {
return TransactionSignature.formatBip143(params)
}

if (!hasForkId || hasChronicle) {
return TransactionSignature.formatOTDA(params)
}

return []
}

// The format used in a tx
static fromChecksigFormat (buf: number[]): TransactionSignature {
if (buf.length === 0) {
Expand Down
13 changes: 11 additions & 2 deletions src/primitives/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,10 @@ export class Writer {
return ret
}

toHex (): string {
return this.toArray().map((n) => n.toString(16).padStart(2, '0')).join('')
}

write (buf: number[]): this {
this.bufs.push(buf)
this.length += buf.length
Expand Down Expand Up @@ -506,8 +510,13 @@ export class Writer {
}

writeUInt64LE (n: number): this {
const buf = new BigNumber(n).toArray('be', 8)
this.writeReverse(buf)
if (n === -1) {
// This value is used as a dummy satoshis value when serializing OTDA placeholder output for SIGHASH_SINGLE
this.write(new Array(8).fill(0xff))
} else {
const buf = new BigNumber(n).toArray('be', 8)
this.writeReverse(buf)
}
return this
}

Expand Down
22 changes: 11 additions & 11 deletions src/script/OP.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,9 @@ const OP = {

// data manipulation ops
OP_CAT: 0x7e,
OP_SUBSTR: 0x7f, // Replaced in BSV
OP_SPLIT: 0x7f,
OP_LEFT: 0x80, // Replaced in BSV
OP_NUM2BIN: 0x80,
OP_RIGHT: 0x81, // Replaced in BSV
OP_BIN2NUM: 0x81,
OP_SPLIT: 0x7f, // after monolith upgrade (May 2018)
OP_NUM2BIN: 0x80, // after monolith upgrade (May 2018)
OP_BIN2NUM: 0x81, // after monolith upgrade (May 2018)
OP_SIZE: 0x82,

// bit logic
Expand Down Expand Up @@ -127,11 +124,14 @@ const OP = {

// expansion
OP_NOP1: 0xb0,
OP_NOP2: 0xb1,
OP_NOP3: 0xb2,
OP_NOP4: 0xb3,
OP_NOP5: 0xb4,
OP_NOP6: 0xb5,
OP_NOP2: 0xb1, // Used on BTC for OP_CHECKLOCKTIMEVERIFY
OP_NOP3: 0xb2, // Used on BTC for OP_CHECKSEQUENCEVERIFY
OP_NOP4: 0xb3, // OP_NOP4 allocated to restore OP_SUBSTR in 2025 CHRONICLE upgrade
OP_SUBSTR: 0xb3, // OP_NOP4 allocated to restore OP_SUBSTR in 2025 CHRONICLE upgrade
OP_NOP5: 0xb4, // OP_NOP5 allocated to restore OP_LEFT in 2025 CHRONICLE upgrade
OP_LEFT: 0xb4, // OP_NOP5 allocated to restore OP_LEFT in 2025 CHRONICLE upgrade
OP_NOP6: 0xb5, // OP_NOP6 allocated to restore OP_RIGHT in 2025 CHRONICLE upgrade
OP_RIGHT: 0xb5, // OP_NOP6 allocated to restore OP_RIGHT in 2025 CHRONICLE upgrade
OP_NOP7: 0xb6,
OP_NOP8: 0xb7,
OP_NOP9: 0xb8,
Expand Down
14 changes: 9 additions & 5 deletions src/script/Script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,12 @@ export default class Script {
* @method fromHex
* Static method to construct a Script instance from a hexadecimal string.
* @param hex - The script in hexadecimal format.
* @param legacyData - If true, arbitrary data following an OP_RETURN is parsed as a single data chunk.
* @returns A new Script instance.
* @example
* const script = Script.fromHex("76a9...");
*/
static fromHex (hex: string): Script {
static fromHex (hex: string, legacyData?: boolean): Script {
if (hex.length === 0) return Script.fromBinary([])
if (hex.length % 2 !== 0) {
throw new Error(
Expand All @@ -110,18 +111,19 @@ export default class Script {
if (!/^[0-9a-fA-F]+$/.test(hex)) {
throw new Error('Some elements in this string are not hex encoded.')
}
return Script.fromBinary(toArray(hex, 'hex'))
return Script.fromBinary(toArray(hex, 'hex'), legacyData)
}

/**
* @method fromBinary
* Static method to construct a Script instance from a binary array.
* @param bin - The script in binary array format.
* @param legacyData - If true, arbitrary data following an OP_RETURN is parsed as a single data chunk.
* @returns A new Script instance.
* @example
* const script = Script.fromBinary([0x76, 0xa9, ...])
*/
static fromBinary (bin: number[]): Script {
static fromBinary (bin: number[], legacyData?: boolean): Script {
bin = [...bin]
const chunks: ScriptChunk[] = []

Expand All @@ -133,7 +135,7 @@ export default class Script {

// if OP_RETURN and not in a conditional block, do not parse the rest of the data,
// rather just return the last chunk as data without prefixing with data length.
if (op === OP.OP_RETURN && inConditionalBlock === 0) {
if (legacyData === true && op === OP.OP_RETURN && inConditionalBlock === 0) {
chunks.push({
op,
data: br.read()
Expand Down Expand Up @@ -391,11 +393,13 @@ export default class Script {
*/
findAndDelete (script: Script): Script {
const buf = script.toHex()
for (let i = 0; i < this.chunks.length; i++) {
for (let i = 0; i < this.chunks.length;) {
const script2 = new Script([this.chunks[i]])
const buf2 = script2.toHex()
if (buf === buf2) {
this.chunks.splice(i, 1)
} else {
i++
}
}
return this
Expand Down
Loading