Finalize and extract done

This commit is contained in:
junderw 2019-07-03 15:13:36 +09:00
parent 343297a359
commit 813b84f91f
No known key found for this signature in database
GPG key ID: B256185D3A971908
3 changed files with 400 additions and 93 deletions

View file

@ -6,6 +6,7 @@ const crypto_1 = require('./crypto');
const payments = require('./payments'); const payments = require('./payments');
const bscript = require('./script'); const bscript = require('./script');
const transaction_1 = require('./transaction'); const transaction_1 = require('./transaction');
const varuint = require('varuint-bitcoin');
class Psbt extends bip174_1.Psbt { class Psbt extends bip174_1.Psbt {
// protected __TX: Transaction; // protected __TX: Transaction;
constructor(network) { constructor(network) {
@ -23,56 +24,75 @@ class Psbt extends bip174_1.Psbt {
// } // }
// }); // });
} }
canFinalize(inputIndex) { extractTransaction() {
if (!this.inputs.every(isFinalized)) throw new Error('Not finalized');
const tx = transaction_1.Transaction.fromBuffer(this.globalMap.unsignedTx);
this.inputs.forEach((input, idx) => {
if (input.finalScriptSig) tx.ins[idx].script = input.finalScriptSig;
if (input.finalScriptWitness) {
const decompiled = bscript.decompile(input.finalScriptWitness);
if (decompiled) tx.ins[idx].witness = bscript.toStack(decompiled);
}
});
return tx;
}
finalizeAllInputs() {
const inputResults = range(this.inputs.length).map(idx =>
this.finalizeInput(idx),
);
const result = inputResults.every(val => val === true);
return {
result,
inputResults,
};
}
finalizeInput(inputIndex) {
const input = utils_1.checkForInput(this.inputs, inputIndex); const input = utils_1.checkForInput(this.inputs, inputIndex);
const script = getScriptFromInput( const { script, isP2SH, isP2WSH, isSegwit } = getScriptFromInput(
inputIndex, inputIndex,
input, input,
this.globalMap.unsignedTx, this.globalMap.unsignedTx,
); );
if (!script) return false; if (!script) return false;
const scriptType = classifyScript(script); const scriptType = classifyScript(script);
const hasSigs = (neededSigs, partialSig) => { if (!canFinalize(input, script, scriptType)) return false;
if (!partialSig) return false; let finalScriptSig;
if (partialSig.length > neededSigs) let finalScriptWitness;
throw new Error('Too many signatures'); // Wow, the payments API is very handy
return partialSig.length === neededSigs; const payment = getPayment(script, scriptType, input.partialSig);
}; const p2wsh = !isP2WSH ? null : payments.p2wsh({ redeem: payment });
switch (scriptType) { const p2sh = !isP2SH ? null : payments.p2sh({ redeem: p2wsh || payment });
case 'pubkey': if (isSegwit) {
return hasSigs(1, input.partialSig); if (p2wsh) {
case 'pubkeyhash': finalScriptWitness = witnessStackToScriptWitness(p2wsh.witness);
return hasSigs(1, input.partialSig); } else {
case 'multisig': finalScriptWitness = witnessStackToScriptWitness(payment.witness);
const p2ms = payments.p2ms({ output: script }); }
return hasSigs(p2ms.m, input.partialSig); if (p2sh) {
case 'witnesspubkeyhash': finalScriptSig = bscript.compile([p2sh.redeem.output]);
return hasSigs(1, input.partialSig); }
default: } else {
return false; finalScriptSig = payment.input;
} }
if (finalScriptSig)
this.addFinalScriptSigToInput(inputIndex, finalScriptSig);
if (finalScriptWitness)
this.addFinalScriptWitnessToInput(inputIndex, finalScriptWitness);
if (!finalScriptSig && !finalScriptWitness) return false;
this.clearFinalizedInput(inputIndex);
return true;
} }
signInput(inputIndex, keyPair) { signInput(inputIndex, keyPair) {
const input = this.inputs[inputIndex]; const input = utils_1.checkForInput(this.inputs, inputIndex);
if (input === undefined) throw new Error(`No input #${inputIndex}`); if (!keyPair || !keyPair.publicKey)
throw new Error('Need Signer to sign input');
const { hash, sighashType, script } = getHashForSig( const { hash, sighashType, script } = getHashForSig(
inputIndex, inputIndex,
input, input,
this.globalMap.unsignedTx, this.globalMap.unsignedTx,
); );
const pubkey = keyPair.publicKey; const pubkey = keyPair.publicKey;
const pubkeyHash = crypto_1.hash160(keyPair.publicKey); checkScriptForPubkey(pubkey, script);
const decompiled = bscript.decompile(script);
if (decompiled === null) throw new Error('Unknown script error');
const hasKey = decompiled.some(element => {
if (typeof element === 'number') return false;
return element.equals(pubkey) || element.equals(pubkeyHash);
});
if (!hasKey) {
throw new Error(
`Can not sign for this input with the key ${pubkey.toString('hex')}`,
);
}
const partialSig = { const partialSig = {
pubkey, pubkey,
signature: bscript.signature.encode(keyPair.sign(hash), sighashType), signature: bscript.signature.encode(keyPair.sign(hash), sighashType),
@ -81,6 +101,77 @@ class Psbt extends bip174_1.Psbt {
} }
} }
exports.Psbt = Psbt; exports.Psbt = Psbt;
//
//
//
//
// Helper functions
//
//
//
//
function isFinalized(input) {
return !!input.finalScriptSig || !!input.finalScriptWitness;
}
function getPayment(script, scriptType, partialSig) {
let payment;
switch (scriptType) {
case 'multisig':
payment = payments.p2ms({
output: script,
signatures: partialSig.map(ps => ps.signature),
});
break;
case 'pubkey':
payment = payments.p2pk({
output: script,
signature: partialSig[0].signature,
});
break;
case 'pubkeyhash':
payment = payments.p2pkh({
output: script,
pubkey: partialSig[0].pubkey,
signature: partialSig[0].signature,
});
break;
case 'witnesspubkeyhash':
payment = payments.p2wpkh({
output: script,
pubkey: partialSig[0].pubkey,
signature: partialSig[0].signature,
});
break;
}
return payment;
}
function canFinalize(input, script, scriptType) {
switch (scriptType) {
case 'pubkey':
case 'pubkeyhash':
case 'witnesspubkeyhash':
return hasSigs(1, input.partialSig);
case 'multisig':
const p2ms = payments.p2ms({ output: script });
return hasSigs(p2ms.m, input.partialSig);
default:
return false;
}
}
function checkScriptForPubkey(pubkey, script) {
const pubkeyHash = crypto_1.hash160(pubkey);
const decompiled = bscript.decompile(script);
if (decompiled === null) throw new Error('Unknown script error');
const hasKey = decompiled.some(element => {
if (typeof element === 'number') return false;
return element.equals(pubkey) || element.equals(pubkeyHash);
});
if (!hasKey) {
throw new Error(
`Can not sign for this input with the key ${pubkey.toString('hex')}`,
);
}
}
const getHashForSig = (inputIndex, input, txBuf) => { const getHashForSig = (inputIndex, input, txBuf) => {
const unsignedTx = transaction_1.Transaction.fromBuffer(txBuf); const unsignedTx = transaction_1.Transaction.fromBuffer(txBuf);
const sighashType = const sighashType =
@ -202,29 +293,67 @@ const classifyScript = script => {
return 'nonstandard'; return 'nonstandard';
}; };
function getScriptFromInput(inputIndex, input, _unsignedTx) { function getScriptFromInput(inputIndex, input, _unsignedTx) {
let script; const res = {
script: null,
isSegwit: false,
isP2SH: false,
isP2WSH: false,
};
if (input.nonWitnessUtxo) { if (input.nonWitnessUtxo) {
if (input.redeemScript) { if (input.redeemScript) {
script = input.redeemScript; res.isP2SH = true;
res.script = input.redeemScript;
} else { } else {
const unsignedTx = transaction_1.Transaction.fromBuffer(_unsignedTx); const unsignedTx = transaction_1.Transaction.fromBuffer(_unsignedTx);
const nonWitnessUtxoTx = transaction_1.Transaction.fromBuffer( const nonWitnessUtxoTx = transaction_1.Transaction.fromBuffer(
input.nonWitnessUtxo, input.nonWitnessUtxo,
); );
const prevoutIndex = unsignedTx.ins[inputIndex].index; const prevoutIndex = unsignedTx.ins[inputIndex].index;
script = nonWitnessUtxoTx.outs[prevoutIndex].script; res.script = nonWitnessUtxoTx.outs[prevoutIndex].script;
} }
} else if (input.witnessUtxo) { } else if (input.witnessUtxo) {
res.isSegwit = true;
res.isP2SH = !!input.redeemScript;
res.isP2WSH = !!input.witnessScript;
if (input.witnessScript) { if (input.witnessScript) {
script = input.witnessScript; res.script = input.witnessScript;
} else if (input.redeemScript) { } else if (input.redeemScript) {
script = payments.p2pkh({ hash: input.redeemScript.slice(2) }).output; res.script = payments.p2pkh({
hash: input.redeemScript.slice(2),
}).output;
} else { } else {
script = payments.p2pkh({ hash: input.witnessUtxo.script.slice(2) }) res.script = payments.p2pkh({
.output; hash: input.witnessUtxo.script.slice(2),
}).output;
} }
} else {
return;
} }
return script; return res;
} }
const hasSigs = (neededSigs, partialSig) => {
if (!partialSig) return false;
if (partialSig.length > neededSigs) throw new Error('Too many signatures');
return partialSig.length === neededSigs;
};
function witnessStackToScriptWitness(witness) {
let buffer = Buffer.allocUnsafe(0);
function writeSlice(slice) {
buffer = Buffer.concat([buffer, Buffer.from(slice)]);
}
function writeVarInt(i) {
const currentLen = buffer.length;
const varintLen = varuint.encodingLength(i);
buffer = Buffer.concat([buffer, Buffer.allocUnsafe(varintLen)]);
varuint.encode(i, buffer, currentLen);
}
function writeVarSlice(slice) {
writeVarInt(slice.length);
writeSlice(slice);
}
function writeVector(vector) {
writeVarInt(vector.length);
vector.forEach(writeVarSlice);
}
writeVector(witness);
return buffer;
}
const range = n => [...Array(n).keys()];

View file

@ -1,5 +1,5 @@
import { Psbt as PsbtBase } from 'bip174'; import { Psbt as PsbtBase } from 'bip174';
import { PsbtInput } from 'bip174/src/lib/interfaces'; import { PartialSig, PsbtInput } from 'bip174/src/lib/interfaces';
import { checkForInput } from 'bip174/src/lib/utils'; import { checkForInput } from 'bip174/src/lib/utils';
import { hash160 } from './crypto'; import { hash160 } from './crypto';
import { Signer } from './ecpair'; import { Signer } from './ecpair';
@ -7,6 +7,7 @@ import { Network } from './networks';
import * as payments from './payments'; import * as payments from './payments';
import * as bscript from './script'; import * as bscript from './script';
import { Transaction } from './transaction'; import { Transaction } from './transaction';
const varuint = require('varuint-bitcoin');
export class Psbt extends PsbtBase { export class Psbt extends PsbtBase {
// protected __TX: Transaction; // protected __TX: Transaction;
@ -25,41 +26,84 @@ export class Psbt extends PsbtBase {
// }); // });
} }
canFinalize(inputIndex: number): boolean { extractTransaction(): Transaction {
if (!this.inputs.every(isFinalized)) throw new Error('Not finalized');
const tx = Transaction.fromBuffer(this.globalMap.unsignedTx!);
this.inputs.forEach((input, idx) => {
if (input.finalScriptSig) tx.ins[idx].script = input.finalScriptSig;
if (input.finalScriptWitness) {
const decompiled = bscript.decompile(input.finalScriptWitness);
if (decompiled) tx.ins[idx].witness = bscript.toStack(decompiled);
}
});
return tx;
}
finalizeAllInputs(): {
result: boolean;
inputResults: boolean[];
} {
const inputResults = range(this.inputs.length).map(idx =>
this.finalizeInput(idx),
);
const result = inputResults.every(val => val === true);
return {
result,
inputResults,
};
}
finalizeInput(inputIndex: number): boolean {
const input = checkForInput(this.inputs, inputIndex); const input = checkForInput(this.inputs, inputIndex);
const script = getScriptFromInput( const { script, isP2SH, isP2WSH, isSegwit } = getScriptFromInput(
inputIndex, inputIndex,
input, input,
this.globalMap.unsignedTx!, this.globalMap.unsignedTx!,
); );
if (!script) return false; if (!script) return false;
const scriptType = classifyScript(script); const scriptType = classifyScript(script);
if (!canFinalize(input, script, scriptType)) return false;
const hasSigs = (neededSigs: number, partialSig?: any[]): boolean => { let finalScriptSig: Buffer | undefined;
if (!partialSig) return false; let finalScriptWitness: Buffer | undefined;
if (partialSig.length > neededSigs)
throw new Error('Too many signatures');
return partialSig.length === neededSigs;
};
switch (scriptType) { // Wow, the payments API is very handy
case 'pubkey': const payment: payments.Payment = getPayment(
return hasSigs(1, input.partialSig); script,
case 'pubkeyhash': scriptType,
return hasSigs(1, input.partialSig); input.partialSig!,
case 'multisig': );
const p2ms = payments.p2ms({ output: script }); const p2wsh = !isP2WSH ? null : payments.p2wsh({ redeem: payment });
return hasSigs(p2ms.m!, input.partialSig); const p2sh = !isP2SH ? null : payments.p2sh({ redeem: p2wsh || payment });
case 'witnesspubkeyhash':
return hasSigs(1, input.partialSig); if (isSegwit) {
default: if (p2wsh) {
return false; finalScriptWitness = witnessStackToScriptWitness(p2wsh.witness!);
} else {
finalScriptWitness = witnessStackToScriptWitness(payment.witness!);
}
if (p2sh) {
finalScriptSig = bscript.compile([p2sh.redeem!.output!]);
}
} else {
finalScriptSig = payment.input;
} }
if (finalScriptSig)
this.addFinalScriptSigToInput(inputIndex, finalScriptSig);
if (finalScriptWitness)
this.addFinalScriptWitnessToInput(inputIndex, finalScriptWitness);
if (!finalScriptSig && !finalScriptWitness) return false;
this.clearFinalizedInput(inputIndex);
return true;
} }
signInput(inputIndex: number, keyPair: Signer): Psbt { signInput(inputIndex: number, keyPair: Signer): Psbt {
const input = this.inputs[inputIndex]; const input = checkForInput(this.inputs, inputIndex);
if (input === undefined) throw new Error(`No input #${inputIndex}`); if (!keyPair || !keyPair.publicKey)
throw new Error('Need Signer to sign input');
const { hash, sighashType, script } = getHashForSig( const { hash, sighashType, script } = getHashForSig(
inputIndex, inputIndex,
input, input,
@ -67,21 +111,8 @@ export class Psbt extends PsbtBase {
); );
const pubkey = keyPair.publicKey; const pubkey = keyPair.publicKey;
const pubkeyHash = hash160(keyPair.publicKey);
const decompiled = bscript.decompile(script); checkScriptForPubkey(pubkey, script);
if (decompiled === null) throw new Error('Unknown script error');
const hasKey = decompiled.some(element => {
if (typeof element === 'number') return false;
return element.equals(pubkey) || element.equals(pubkeyHash);
});
if (!hasKey) {
throw new Error(
`Can not sign for this input with the key ${pubkey.toString('hex')}`,
);
}
const partialSig = { const partialSig = {
pubkey, pubkey,
@ -92,6 +123,93 @@ export class Psbt extends PsbtBase {
} }
} }
//
//
//
//
// Helper functions
//
//
//
//
function isFinalized(input: PsbtInput): boolean {
return !!input.finalScriptSig || !!input.finalScriptWitness;
}
function getPayment(
script: Buffer,
scriptType: string,
partialSig: PartialSig[],
): payments.Payment {
let payment: payments.Payment;
switch (scriptType) {
case 'multisig':
payment = payments.p2ms({
output: script,
signatures: partialSig.map(ps => ps.signature),
});
break;
case 'pubkey':
payment = payments.p2pk({
output: script,
signature: partialSig[0].signature,
});
break;
case 'pubkeyhash':
payment = payments.p2pkh({
output: script,
pubkey: partialSig[0].pubkey,
signature: partialSig[0].signature,
});
break;
case 'witnesspubkeyhash':
payment = payments.p2wpkh({
output: script,
pubkey: partialSig[0].pubkey,
signature: partialSig[0].signature,
});
break;
}
return payment!;
}
function canFinalize(
input: PsbtInput,
script: Buffer,
scriptType: string,
): boolean {
switch (scriptType) {
case 'pubkey':
case 'pubkeyhash':
case 'witnesspubkeyhash':
return hasSigs(1, input.partialSig);
case 'multisig':
const p2ms = payments.p2ms({ output: script });
return hasSigs(p2ms.m!, input.partialSig);
default:
return false;
}
}
function checkScriptForPubkey(pubkey: Buffer, script: Buffer): void {
const pubkeyHash = hash160(pubkey);
const decompiled = bscript.decompile(script);
if (decompiled === null) throw new Error('Unknown script error');
const hasKey = decompiled.some(element => {
if (typeof element === 'number') return false;
return element.equals(pubkey) || element.equals(pubkeyHash);
});
if (!hasKey) {
throw new Error(
`Can not sign for this input with the key ${pubkey.toString('hex')}`,
);
}
}
interface HashForSigData { interface HashForSigData {
script: Buffer; script: Buffer;
hash: Buffer; hash: Buffer;
@ -239,32 +357,86 @@ const classifyScript = (script: Buffer): string => {
return 'nonstandard'; return 'nonstandard';
}; };
interface GetScriptReturn {
script: Buffer | null;
isSegwit: boolean;
isP2SH: boolean;
isP2WSH: boolean;
}
function getScriptFromInput( function getScriptFromInput(
inputIndex: number, inputIndex: number,
input: PsbtInput, input: PsbtInput,
_unsignedTx: Buffer, _unsignedTx: Buffer,
): Buffer | undefined { ): GetScriptReturn {
let script: Buffer; const res: GetScriptReturn = {
script: null,
isSegwit: false,
isP2SH: false,
isP2WSH: false,
};
if (input.nonWitnessUtxo) { if (input.nonWitnessUtxo) {
if (input.redeemScript) { if (input.redeemScript) {
script = input.redeemScript; res.isP2SH = true;
res.script = input.redeemScript;
} else { } else {
const unsignedTx = Transaction.fromBuffer(_unsignedTx); const unsignedTx = Transaction.fromBuffer(_unsignedTx);
const nonWitnessUtxoTx = Transaction.fromBuffer(input.nonWitnessUtxo); const nonWitnessUtxoTx = Transaction.fromBuffer(input.nonWitnessUtxo);
const prevoutIndex = unsignedTx.ins[inputIndex].index; const prevoutIndex = unsignedTx.ins[inputIndex].index;
script = nonWitnessUtxoTx.outs[prevoutIndex].script; res.script = nonWitnessUtxoTx.outs[prevoutIndex].script;
} }
} else if (input.witnessUtxo) { } else if (input.witnessUtxo) {
res.isSegwit = true;
res.isP2SH = !!input.redeemScript;
res.isP2WSH = !!input.witnessScript;
if (input.witnessScript) { if (input.witnessScript) {
script = input.witnessScript; res.script = input.witnessScript;
} else if (input.redeemScript) { } else if (input.redeemScript) {
script = payments.p2pkh({ hash: input.redeemScript.slice(2) }).output!; res.script = payments.p2pkh({
hash: input.redeemScript.slice(2),
}).output!;
} else { } else {
script = payments.p2pkh({ hash: input.witnessUtxo.script.slice(2) }) res.script = payments.p2pkh({
.output!; hash: input.witnessUtxo.script.slice(2),
}).output!;
} }
} else {
return;
} }
return script; return res;
} }
const hasSigs = (neededSigs: number, partialSig?: any[]): boolean => {
if (!partialSig) return false;
if (partialSig.length > neededSigs) throw new Error('Too many signatures');
return partialSig.length === neededSigs;
};
function witnessStackToScriptWitness(witness: Buffer[]): Buffer {
let buffer = Buffer.allocUnsafe(0);
function writeSlice(slice: Buffer): void {
buffer = Buffer.concat([buffer, Buffer.from(slice)]);
}
function writeVarInt(i: number): void {
const currentLen = buffer.length;
const varintLen = varuint.encodingLength(i);
buffer = Buffer.concat([buffer, Buffer.allocUnsafe(varintLen)]);
varuint.encode(i, buffer, currentLen);
}
function writeVarSlice(slice: Buffer): void {
writeVarInt(slice.length);
writeSlice(slice);
}
function writeVector(vector: Buffer[]): void {
writeVarInt(vector.length);
vector.forEach(writeVarSlice);
}
writeVector(witness);
return buffer;
}
const range = (n: number): number[] => [...Array(n).keys()];

8
types/psbt.d.ts vendored
View file

@ -1,9 +1,15 @@
import { Psbt as PsbtBase } from 'bip174'; import { Psbt as PsbtBase } from 'bip174';
import { Signer } from './ecpair'; import { Signer } from './ecpair';
import { Network } from './networks'; import { Network } from './networks';
import { Transaction } from './transaction';
export declare class Psbt extends PsbtBase { export declare class Psbt extends PsbtBase {
network?: Network | undefined; network?: Network | undefined;
constructor(network?: Network | undefined); constructor(network?: Network | undefined);
canFinalize(inputIndex: number): boolean; extractTransaction(): Transaction;
finalizeAllInputs(): {
result: boolean;
inputResults: boolean[];
};
finalizeInput(inputIndex: number): boolean;
signInput(inputIndex: number, keyPair: Signer): Psbt; signInput(inputIndex: number, keyPair: Signer): Psbt;
} }