PHP WebShell
Текущая директория: /opt/BitGoJS/modules/utxo-lib/src/bitgo
Просмотр файла: parseInput.ts
/* eslint no-redeclare: 0 */
import * as opcodes from 'bitcoin-ops';
import { script as bscript, TxInput } from 'bitcoinjs-lib';
import { isTriple } from './types';
import { isScriptType2Of3 } from './outputScripts';
export function isPlaceholderSignature(v: number | Buffer): boolean {
if (Buffer.isBuffer(v)) {
return v.length === 0;
}
return v === 0;
}
/**
* @return true iff P2TR script path's control block matches BitGo's need
*/
export function isValidControlBock(controlBlock: Buffer): boolean {
// The last stack element is called the control block c, and must have length 33 + 32m
return Buffer.isBuffer(controlBlock) && 33 <= controlBlock.length && controlBlock.length % 32 === 1;
}
/**
* @return script path level for P2TR control block
*/
export function calculateScriptPathLevel(controlBlock: Buffer): number {
if (!Buffer.isBuffer(controlBlock)) {
throw new Error('Invalid control block type.');
}
if (controlBlock.length === 65) {
return 1;
}
if (controlBlock.length === 97) {
return 2;
}
throw new Error('unexpected control block length.');
}
/**
* @return leaf version for P2TR control block.
*/
export function getLeafVersion(controlBlock: Buffer): number {
if (Buffer.isBuffer(controlBlock) && controlBlock.length > 0) {
return controlBlock[0] & 0xfe;
}
throw new Error('unexpected leafVersion.');
}
export type ParsedScriptType2Of3 =
| 'p2sh'
| 'p2shP2wsh'
| 'p2wsh'
| 'taprootKeyPathSpend' // only implemented for p2trMusig2
| 'taprootScriptPathSpend'; // can be for either p2tr or p2trMusig2 output script
export type ParsedScriptType = ParsedScriptType2Of3 | 'p2shP2pk';
export type ParsedPubScript = {
scriptType: ParsedScriptType;
};
export type ParsedSignatureScript = {
scriptType: ParsedScriptType;
};
export interface ParsedSignatureScriptP2shP2pk extends ParsedSignatureScript {
scriptType: 'p2shP2pk';
publicKeys: [Buffer];
signatures: [Buffer];
}
export interface ParsedPubScriptTaprootKeyPath extends ParsedPubScript {
scriptType: 'taprootKeyPathSpend';
// x-only tapOutputKey
publicKeys: [Buffer];
pubScript: Buffer;
}
export interface ParsedPubScriptTaprootScriptPath extends ParsedPubScript {
scriptType: 'taprootScriptPathSpend';
publicKeys: [Buffer, Buffer];
pubScript: Buffer;
}
export type ParsedPubScriptTaproot = ParsedPubScriptTaprootKeyPath | ParsedPubScriptTaprootScriptPath;
export interface ParsedPubScriptP2ms extends ParsedPubScript {
scriptType: 'p2sh' | 'p2shP2wsh' | 'p2wsh';
publicKeys: [Buffer, Buffer, Buffer];
pubScript: Buffer;
redeemScript: Buffer | undefined;
witnessScript: Buffer | undefined;
}
export interface ParsedPubScriptP2shP2pk extends ParsedPubScript {
scriptType: 'p2shP2pk';
publicKeys: [Buffer];
pubScript: Buffer;
redeemScript: Buffer;
}
export interface ParsedSignatureScriptP2ms extends ParsedSignatureScript {
scriptType: 'p2sh' | 'p2shP2wsh' | 'p2wsh';
publicKeys: [Buffer, Buffer, Buffer];
signatures:
| [Buffer, Buffer] // fully-signed transactions with signatures
/* Partially signed transactions with placeholder signatures.
For p2sh, the placeholder is OP_0 (number 0) */
| [Buffer | 0, Buffer | 0, Buffer | 0];
pubScript: Buffer;
redeemScript: Buffer | undefined;
witnessScript: Buffer | undefined;
}
/**
* Keypath spends only have a single signature
*/
export interface ParsedSignatureScriptTaprootKeyPath extends ParsedSignatureScript {
scriptType: 'taprootKeyPathSpend';
signatures: [Buffer];
}
/**
* Taproot Scriptpath spends are more similar to regular p2ms spends and have two public keys and
* two signatures
*/
export interface ParsedSignatureScriptTaprootScriptPath extends ParsedSignatureScript {
scriptType: 'taprootScriptPathSpend';
publicKeys: [Buffer, Buffer];
signatures: [Buffer, Buffer];
controlBlock: Buffer;
leafVersion: number;
/** Indicates the level inside the taptree. */
scriptPathLevel: number;
pubScript: Buffer;
}
export type ParsedSignatureScriptTaproot = ParsedSignatureScriptTaprootKeyPath | ParsedSignatureScriptTaprootScriptPath;
type DecompiledScript = Array<Buffer | number>;
/**
* Static script elements
*/
type ScriptPatternConstant =
| 'OP_0'
| 'OP_1'
| 'OP_2'
| 'OP_3'
| 'OP_CHECKMULTISIG'
| 'OP_CHECKSIG'
| 'OP_CHECKSIGVERIFY';
/**
* Script elements that can be captured
*/
type ScriptPatternCapture =
| ':pubkey'
| ':pubkey-xonly'
| ':signature'
| ':control-block'
| { ':script': ScriptPatternElement[] };
type ScriptPatternElement = ScriptPatternConstant | ScriptPatternCapture;
/**
* Result for a successful script match
*/
type MatchResult = {
':pubkey': Buffer[];
':pubkey-xonly': Buffer[];
':control-block': Buffer[];
':signature': (Buffer | 0)[];
':script': { buffer: Buffer; match: MatchResult }[];
};
function emptyMatchResult(): MatchResult {
return {
':pubkey': [],
':pubkey-xonly': [],
':control-block': [],
':signature': [],
':script': [],
};
}
class MatchError extends Error {
// this property is required to prohibit `return new Error()` when the return type demands `MatchError`
__type = 'MatchError';
constructor(message: string) {
super(message);
}
static forPatternElement(p: ScriptPatternElement): MatchError {
if (typeof p === 'object' && ':script' in p) {
return new MatchError(`error matching nested script`);
}
return new MatchError(`error matching ${p}`);
}
}
/**
* @param script
* @param pattern
* @return MatchResult if script matches pattern. The result will contain the matched values.
*/
function matchScript(script: DecompiledScript, pattern: ScriptPatternElement[]): MatchResult | MatchError {
/**
* Match a single script element with a ScriptPatternElement
*/
function matchElement(e: Buffer | number, p: ScriptPatternElement): MatchResult | boolean {
switch (p) {
case 'OP_0':
return e === opcodes.OP_0 || (Buffer.isBuffer(e) && e.length === 0);
case 'OP_1':
case 'OP_2':
case 'OP_3':
case 'OP_CHECKMULTISIG':
case 'OP_CHECKSIG':
case 'OP_CHECKSIGVERIFY':
return e === opcodes[p];
case ':pubkey':
return Buffer.isBuffer(e) && (e.length === 33 || e.length === 65);
case ':pubkey-xonly':
return Buffer.isBuffer(e) && e.length === 32;
case ':signature':
return Buffer.isBuffer(e) || isPlaceholderSignature(e);
case ':control-block':
return Buffer.isBuffer(e) && isValidControlBock(e);
default:
throw new Error(`unknown pattern element ${p}`);
}
}
if (script.length !== pattern.length) {
return new MatchError(`length mismatch`);
}
// Go over each pattern element.
// Collect captures into a result object.
return pattern.reduce((obj: MatchResult | MatchError, p, i): MatchResult | MatchError => {
// if we had a previous mismatch, short-circuit
if (obj instanceof MatchError) {
return obj;
}
const e = script[i];
// for ':script' pattern elements, decompile script element and recurse
if (typeof p === 'object' && ':script' in p) {
if (!Buffer.isBuffer(e)) {
return new MatchError(`expected buffer for :script`);
}
const dec = bscript.decompile(e);
if (!dec) {
return new MatchError(`error decompiling nested script`);
}
const match = matchScript(dec, p[':script']);
if (match instanceof MatchError) {
return match;
}
obj[':script'].push({
buffer: e,
match,
});
return obj;
}
const match = matchElement(e, p);
if (!match) {
return MatchError.forPatternElement(p);
}
// if pattern element is a capture, add it to the result obj
if (p === ':signature' && e === 0) {
obj[p].push(e);
} else if (p in obj) {
if (!Buffer.isBuffer(e)) {
throw new Error(`invalid capture value`);
}
obj[p].push(e);
}
return obj;
}, emptyMatchResult());
}
/**
* @param script
* @param patterns
* @return first match
*/
function matchScriptSome(script: DecompiledScript, patterns: ScriptPatternElement[][]): MatchResult | MatchError {
for (const p of patterns) {
const m = matchScript(script, p);
if (m instanceof MatchError) {
continue;
}
return m;
}
return new MatchError(`no match for script`);
}
type InputScripts<TScript, TWitness> = {
script: TScript;
witness: TWitness;
};
type InputScriptsLegacy = InputScripts<DecompiledScript, null>;
type InputScriptsWrappedSegwit = InputScripts<DecompiledScript, Buffer[]>;
type InputScriptsNativeSegwit = InputScripts<null, Buffer[]>;
type InputScriptsUnknown = InputScripts<DecompiledScript | null, Buffer[] | null>;
type InputParser<T extends ParsedSignatureScriptP2shP2pk | ParsedSignatureScriptP2ms | ParsedSignatureScriptTaproot> = (
p: InputScriptsUnknown
) => T | MatchError;
export type InputPubScript = Buffer;
type PubScriptParser<T extends ParsedPubScriptTaproot | ParsedPubScriptP2ms | ParsedPubScriptP2shP2pk> = (
p: InputPubScript,
t: ParsedScriptType
) => T | MatchError;
function isLegacy(p: InputScriptsUnknown): p is InputScriptsLegacy {
return Boolean(p.script && !p.witness);
}
function isWrappedSegwit(p: InputScriptsUnknown): p is InputScriptsWrappedSegwit {
return Boolean(p.script && p.witness);
}
function isNativeSegwit(p: InputScriptsUnknown): p is InputScriptsNativeSegwit {
return Boolean(!p.script && p.witness);
}
const parseP2shP2pk: InputParser<ParsedSignatureScriptP2shP2pk> = (p) => {
if (!isLegacy(p)) {
return new MatchError(`expected legacy input`);
}
const match = matchScript(p.script, [':signature', { ':script': [':pubkey', 'OP_CHECKSIG'] }]);
if (match instanceof MatchError) {
return match;
}
return {
scriptType: 'p2shP2pk',
publicKeys: match[':script'][0].match[':pubkey'] as [Buffer],
signatures: match[':signature'] as [Buffer],
};
};
function parseP2ms(
decScript: DecompiledScript,
scriptType: 'p2sh' | 'p2shP2wsh' | 'p2wsh'
): ParsedSignatureScriptP2ms | MatchError {
const pattern2Of3: ScriptPatternElement[] = ['OP_2', ':pubkey', ':pubkey', ':pubkey', 'OP_3', 'OP_CHECKMULTISIG'];
const match = matchScriptSome(decScript, [
/* full-signed, no placeholder signature */
['OP_0', ':signature', ':signature', { ':script': pattern2Of3 }],
/* half-signed, placeholder signatures */
['OP_0', ':signature', ':signature', ':signature', { ':script': pattern2Of3 }],
]);
if (match instanceof MatchError) {
return match;
}
const [redeemScript] = match[':script'];
if (!isTriple(redeemScript.match[':pubkey'])) {
throw new Error(`invalid pubkey count`);
}
return {
scriptType,
publicKeys: redeemScript.match[':pubkey'],
pubScript: redeemScript.buffer,
signatures: match[':signature'] as ParsedSignatureScriptP2ms['signatures'],
redeemScript: scriptType === 'p2sh' ? redeemScript.buffer : undefined,
witnessScript: scriptType === 'p2shP2wsh' || scriptType === 'p2wsh' ? redeemScript.buffer : undefined,
};
}
const parseP2sh2Of3: InputParser<ParsedSignatureScriptP2ms> = (p) => {
if (!isLegacy(p)) {
return new MatchError(`expected legacy input`);
}
return parseP2ms(p.script, 'p2sh');
};
const parseP2shP2wsh2Of3: InputParser<ParsedSignatureScriptP2ms> = (p) => {
if (!isWrappedSegwit(p)) {
return new MatchError(`expected wrapped segwit input`);
}
return { ...parseP2ms(p.witness, 'p2shP2wsh'), redeemScript: p.script[0] as Buffer };
};
const parseP2wsh2Of3: InputParser<ParsedSignatureScriptP2ms> = (p) => {
if (!isNativeSegwit(p)) {
return new MatchError(`expected native segwit`);
}
return parseP2ms(p.witness, 'p2wsh');
};
const parseTaprootKeyPath2Of3: InputParser<ParsedSignatureScriptTaprootKeyPath> = (p) => {
if (!isNativeSegwit(p)) {
return new MatchError(`expected native segwit`);
}
const match = matchScript(p.witness, [':signature']);
if (match instanceof MatchError) {
return match;
}
const signatures = match[':signature'] as [Buffer];
if (isPlaceholderSignature(signatures[0])) {
throw new Error(`invalid taproot key path signature`);
}
return {
scriptType: 'taprootKeyPathSpend',
signatures,
};
};
const parseTaprootScriptPath2Of3: InputParser<ParsedSignatureScriptTaproot> = (p) => {
if (!isNativeSegwit(p)) {
return new MatchError(`expected native segwit`);
}
// assumes no annex
const match = matchScript(p.witness, [
':signature',
':signature',
{ ':script': [':pubkey-xonly', 'OP_CHECKSIGVERIFY', ':pubkey-xonly', 'OP_CHECKSIG'] },
':control-block',
]);
if (match instanceof MatchError) {
return match;
}
const [controlBlock] = match[':control-block'];
const scriptPathLevel = calculateScriptPathLevel(controlBlock);
const leafVersion = getLeafVersion(controlBlock);
return {
scriptType: 'taprootScriptPathSpend',
pubScript: match[':script'][0].buffer,
publicKeys: match[':script'][0].match[':pubkey-xonly'] as [Buffer, Buffer],
signatures: match[':signature'] as [Buffer, Buffer],
controlBlock,
scriptPathLevel,
leafVersion,
};
};
/**
* Parse a transaction's signature script to obtain public keys, signatures, the sig script,
* and other properties.
*
* Only supports script types used in BitGo transactions.
*
* @param input
* @returns ParsedSignatureScript
*/
export function parseSignatureScript(
input: TxInput
): ParsedSignatureScriptP2shP2pk | ParsedSignatureScriptP2ms | ParsedSignatureScriptTaproot {
const decScript = bscript.decompile(input.script);
const parsers = [
parseP2sh2Of3,
parseP2shP2wsh2Of3,
parseP2wsh2Of3,
parseTaprootKeyPath2Of3,
parseTaprootScriptPath2Of3,
parseP2shP2pk,
] as const;
for (const f of parsers) {
const parsed = f({
script: decScript?.length === 0 ? null : decScript,
witness: input.witness.length === 0 ? null : input.witness,
});
if (parsed instanceof MatchError) {
continue;
}
return parsed;
}
throw new Error(`could not parse input`);
}
export function parseSignatureScript2Of3(input: TxInput): ParsedSignatureScriptP2ms | ParsedSignatureScriptTaproot {
const result = parseSignatureScript(input);
if (
!isScriptType2Of3(result.scriptType) &&
result.scriptType !== 'taprootKeyPathSpend' &&
result.scriptType !== 'taprootScriptPathSpend'
) {
throw new Error(`invalid script type`);
}
if (!result.signatures) {
throw new Error(`missing signatures`);
}
if (
result.scriptType !== 'taprootKeyPathSpend' &&
result.publicKeys.length !== 3 &&
(result.publicKeys.length !== 2 || result.scriptType !== 'taprootScriptPathSpend')
) {
throw new Error(`unexpected pubkey count`);
}
return result as ParsedSignatureScriptP2ms | ParsedSignatureScriptTaproot;
}
const parseP2shP2pkPubScript: PubScriptParser<ParsedPubScriptP2shP2pk> = (pubScript, scriptType) => {
if (scriptType !== 'p2shP2pk') {
throw new Error('invalid script type');
}
const match = matchScript([pubScript], [{ ':script': [':pubkey', 'OP_CHECKSIG'] }]);
if (match instanceof MatchError) {
return match;
}
const [script] = match[':script'];
return {
scriptType,
publicKeys: script.match[':pubkey'] as [Buffer],
pubScript: pubScript,
redeemScript: pubScript,
};
};
const parseP2msPubScript: PubScriptParser<ParsedPubScriptP2ms> = (pubScript, scriptType) => {
if (scriptType === 'taprootScriptPathSpend' || scriptType === 'taprootKeyPathSpend' || scriptType === 'p2shP2pk') {
throw new Error('invalid script type');
}
const match = matchScript(
[pubScript],
[{ ':script': ['OP_2', ':pubkey', ':pubkey', ':pubkey', 'OP_3', 'OP_CHECKMULTISIG'] }]
);
if (match instanceof MatchError) {
return match;
}
const [redeemScript] = match[':script'];
if (!isTriple(redeemScript.match[':pubkey'])) {
throw new Error('invalid pubkey count');
}
return {
scriptType,
publicKeys: redeemScript.match[':pubkey'],
pubScript: redeemScript.buffer,
redeemScript: scriptType === 'p2sh' ? redeemScript.buffer : undefined,
witnessScript: scriptType === 'p2shP2wsh' || scriptType === 'p2wsh' ? redeemScript.buffer : undefined,
};
};
const parseTaprootKeyPathPubScript: PubScriptParser<ParsedPubScriptTaprootKeyPath> = (pubScript, scriptType) => {
if (
scriptType === 'p2sh' ||
scriptType === 'p2wsh' ||
scriptType === 'p2shP2wsh' ||
scriptType === 'taprootScriptPathSpend' ||
scriptType === 'p2shP2pk'
) {
throw new Error('invalid script type');
}
const match = matchScript([pubScript], [{ ':script': ['OP_1', ':pubkey-xonly'] }]);
if (match instanceof MatchError) {
return match;
}
const [script] = match[':script'];
return {
scriptType: 'taprootKeyPathSpend',
publicKeys: script.match[':pubkey-xonly'] as [Buffer],
pubScript: pubScript,
};
};
const parseTaprootScriptPathPubScript: PubScriptParser<ParsedPubScriptTaprootScriptPath> = (pubScript, scriptType) => {
if (
scriptType === 'p2sh' ||
scriptType === 'p2wsh' ||
scriptType === 'p2shP2wsh' ||
scriptType === 'taprootKeyPathSpend' ||
scriptType === 'p2shP2pk'
) {
throw new Error('invalid script type');
}
const match = matchScript(
[pubScript],
[{ ':script': [':pubkey-xonly', 'OP_CHECKSIGVERIFY', ':pubkey-xonly', 'OP_CHECKSIG'] }]
);
if (match instanceof MatchError) {
return match;
}
return {
scriptType,
pubScript: match[':script'][0].buffer,
publicKeys: match[':script'][0].match[':pubkey-xonly'] as [Buffer, Buffer],
};
};
/**
* @return pubScript (scriptPubKey/redeemScript/witnessScript) is parsed.
* P2SH => scriptType, pubScript (redeemScript), redeemScript, public keys
* PW2SH => scriptType, pubScript (witnessScript), witnessScript, public keys.
* P2SH-PW2SH => scriptType, pubScript (witnessScript), witnessScript, public keys.
* taprootScriptPathSpend (P2TR and P2TRMUISG2 script path) => scriptType, pubScript, pub keys.
* taprootKeyPathSpend (P2TRMUISG2 key path) => scriptType, pubScript (34-byte output script), pub key (tapOutputKey).
*/
export function parsePubScript2Of3(
inputPubScript: InputPubScript,
scriptType: 'taprootKeyPathSpend'
): ParsedPubScriptTaprootKeyPath;
export function parsePubScript2Of3(
inputPubScript: InputPubScript,
scriptType: 'taprootScriptPathSpend'
): ParsedPubScriptTaprootScriptPath;
export function parsePubScript2Of3(
inputPubScript: InputPubScript,
scriptType: 'p2sh' | 'p2shP2wsh' | 'p2wsh'
): ParsedPubScriptP2ms;
export function parsePubScript2Of3(
inputPubScript: InputPubScript,
scriptType: ParsedScriptType2Of3
): ParsedPubScriptP2ms | ParsedPubScriptTaproot;
export function parsePubScript2Of3(
inputPubScript: InputPubScript,
scriptType: ParsedScriptType2Of3
): ParsedPubScriptP2ms | ParsedPubScriptTaproot {
const result =
scriptType === 'taprootKeyPathSpend'
? parseTaprootKeyPathPubScript(inputPubScript, scriptType)
: scriptType === 'taprootScriptPathSpend'
? parseTaprootScriptPathPubScript(inputPubScript, scriptType)
: parseP2msPubScript(inputPubScript, scriptType);
if (result instanceof MatchError) {
throw new Error(result.message);
}
if (
(result.scriptType === 'taprootKeyPathSpend' && result.publicKeys.length !== 1) ||
(result.scriptType === 'taprootScriptPathSpend' && result.publicKeys.length !== 2) ||
(isScriptType2Of3(result.scriptType) && result.publicKeys.length !== 3)
) {
throw new Error('unexpected pubkey count');
}
return result;
}
/**
* @return pubScript (scriptPubKey/redeemScript/witnessScript) is parsed.
* P2SH => scriptType, pubScript (redeemScript), redeemScript, public keys
* PW2SH => scriptType, pubScript (witnessScript), witnessScript, public keys.
* P2SH-PW2SH => scriptType, pubScript (witnessScript), witnessScript, public keys.
* taprootScriptPathSpend (P2TR and P2TRMUISG2 script path) => scriptType, pubScript, pub keys.
* taprootKeyPathSpend (P2TRMUISG2 key path) => scriptType, pubScript (34-byte output script), pub key (tapOutputKey).
* P2SH-P2PK => scriptType, pubScript, pub key, redeemScript.
*/
export function parsePubScript(
inputPubScript: InputPubScript,
scriptType: 'taprootKeyPathSpend'
): ParsedPubScriptTaprootKeyPath;
export function parsePubScript(
inputPubScript: InputPubScript,
scriptType: 'taprootScriptPathSpend'
): ParsedPubScriptTaprootScriptPath;
export function parsePubScript(inputPubScript: InputPubScript, scriptType: 'p2shP2pk'): ParsedPubScriptP2shP2pk;
export function parsePubScript(
inputPubScript: InputPubScript,
scriptType: 'p2sh' | 'p2shP2wsh' | 'p2wsh'
): ParsedPubScriptP2ms;
export function parsePubScript(
inputPubScript: InputPubScript,
scriptType: ParsedScriptType
): ParsedPubScriptP2ms | ParsedPubScriptTaproot | ParsedPubScriptP2shP2pk;
export function parsePubScript(
inputPubScript: InputPubScript,
scriptType: ParsedScriptType
): ParsedPubScriptP2ms | ParsedPubScriptTaproot | ParsedPubScriptP2shP2pk {
const result =
scriptType === 'p2shP2pk'
? parseP2shP2pkPubScript(inputPubScript, scriptType)
: parsePubScript2Of3(inputPubScript, scriptType);
if (result instanceof MatchError) {
throw new Error(result.message);
}
if (result.scriptType === 'p2shP2pk' && result.publicKeys.length !== 1) {
throw new Error('unexpected pubkey count');
}
return result;
}
Выполнить команду
Для локальной разработки. Не используйте в интернете!