bmultisig
Version:
Bcoin wallet plugin for multi signature transaction proposals
430 lines (327 loc) • 9.92 kB
JavaScript
/*!
* mtx.js - MTX extended for multisig
* Copyright (c) 2018, The Bcoin Developers (MIT License).
* https://github.com/bcoin-org/bmultisig
*/
'use strict';
const assert = require('bsert');
const {enforce} = assert;
const MTX = require('bcoin/lib/primitives/mtx');
const Coin = require('bcoin/lib/primitives/coin');
const Script = require('bcoin/lib/script/script');
const Witness = require('bcoin/lib/script/witness');
/**
* Multisig MTX
* TODO: throw correct errors.
* @alias module:primitives.MultisigMTX
*/
class MultisigMTX extends MTX {
constructor(options) {
super(options);
}
/**
* Check if coin we are spending
* is witness.
* TODO: Belongs to Coin.
* @param {Coin} coin
* @param {KeyRing} ring
* @returns {Boolean} - true if it's witness program,
* false if it is normal or ring does not own coin.
*/
isWitnessCoin(coin, ring) {
assert(ring.ownOutput(coin), 'Coin does not belong to the ring.');
const prev = coin.script;
if (prev.isProgram())
return true;
const sh = prev.getScripthash();
if (sh) {
const redeem = ring.getRedeem(sh);
// should not happen
if (!redeem)
throw new Error('redeem script not found.');
if (redeem.isProgram())
return true;
}
return false;
}
/**
* Get script to sign.
* TODO: Belongs to Coin.
* @param {Coin} coin
* @param {KeyRing} ring
* @returns {Script?}
*/
getPrevScript(coin, ring) {
const prev = coin.script;
const sh = prev.getScripthash();
if (sh) {
const redeem = ring.getRedeem(sh);
if (!redeem)
return null;
// Regular P2SH.
if (!redeem.isProgram())
return redeem.clone();
// nested P2WSH
const wsh = redeem.getWitnessScripthash();
if (wsh) {
const wredeem = ring.getRedeem(wsh);
if (!wredeem)
return null;
return wredeem.clone();
}
// nested P2WPKH
const wpkh = redeem.getWitnessPubkeyhash();
if (wpkh)
return Script.fromPubkeyhash(wpkh);
// Unknown witness program.
return null;
}
// normal output.
if (!prev.isProgram())
return prev.clone();
// P2WSH
const wsh = prev.getWitnessScripthash();
if (wsh) {
const wredeem = ring.getRedeem(wsh);
if (!wredeem)
return null;
return wredeem.clone();
}
// P2WPKH
const wpkh = prev.getWitnessPubkeyhash();
if (wpkh)
return Script.fromPubkeyhash(wpkh);
return null;
}
/**
* Verify signature without modifying transaction
* TODO: move to TX
* TODO: verifySignature
* @param {Number} index - index of input being verified.
* @param {Coin} coin
* @param {KeyRing} ring
* @param {Buffer} signature
* @param {SighashType} type
* @return {Boolean}
*/
checkSignature(index, coin, ring, signature) {
const input = this.inputs[index];
const value = coin.value;
assert(input, 'Input does not exist.');
enforce(Coin.isCoin(coin), 'coin', 'Coin');
enforce(Buffer.isBuffer(signature), 'signature', 'buffer');
const prev = this.getPrevScript(coin, ring);
const version = this.isWitnessCoin(coin, ring) ? 1 : 0;
const key = ring.publicKey;
return this.checksig(index, prev, value, signature, key, version);
}
/**
* Sign and return signature
* TODO: move to TX
* TODO: Rename
* @param {Number} index - index of input being signed.
* @param {Coin|Output} coin
* @param {KeyRing} ring
* @param {SighashType} type
* @returns {Buffer}
*/
getInputSignature(index, coin, ring, type) {
const input = this.inputs[index];
assert(input, 'Input does not exist.');
assert(coin, 'Input does not exist.');
assert(ring.privateKey, 'No private key available.');
const key = ring.privateKey;
const value = coin.value;
const version = this.isWitnessCoin(coin, ring) ? 1 : 0;
const prev = this.getPrevScript(coin, ring);
return this.signature(index, prev, value, key, type, version);
}
/**
* Sign and return input signatures for a ring.
* TODO: move to TX
* TODO: rename
* @param {KeyRing[]} rings - Keyring
* @param {SighashType} type - Sighash type
* @returns {Buffer[]}
*/
getSignatures(rings, type) {
assert(rings.length === this.inputs.length);
const signatures = new Array(this.inputs.length);
for (let i = 0; i < rings.length; i++) {
const ring = rings[i];
if (!ring)
continue;
const {prevout} = this.inputs[i];
const coin = this.view.getOutput(prevout);
if (!coin)
continue;
if (!ring.ownOutput(coin))
throw new Error('Input does not belong to the key.');
signatures[i] = this.getInputSignature(i, coin, ring, type);
}
return signatures;
}
/**
* Apply signature without validating signature
* @param {Number} index - index of input being signed.
* @param {Coin|Output} coin
* @param {KeyRing} ring
* @param {Buffer} signature
* @param {Boolean} valid - verify signature
* @returns {Boolean} whether the signature was applied.
*/
applySignature(index, coin, ring, signature, valid) {
const input = this.inputs[index];
const key = ring.publicKey;
assert(input, 'Input does not exist.');
assert(coin, 'No coin passed.');
assert(signature, 'No signature passed.');
// Get the previous output's script
const value = coin.value;
let prev = coin.script;
let vector = input.script;
let version = 0;
let redeem = false;
// Grab regular p2sh redeem script.
if (prev.isScripthash()) {
prev = input.script.getRedeem();
if (!prev)
throw new Error('Input has not been templated.');
redeem = true;
}
// If the output script is a witness program,
// we have to switch the vector to the witness
// and potentially alter the length. Note that
// witnesses are stack items, so the `dummy`
// _has_ to be an empty buffer (what OP_0
// pushes onto the stack).
if (prev.isWitnessScripthash()) {
prev = input.witness.getRedeem();
if (!prev)
throw new Error('Input has not been templated.');
vector = input.witness;
redeem = true;
version = 1;
} else {
const wpkh = prev.getWitnessPubkeyhash();
if (wpkh) {
prev = Script.fromPubkeyhash(wpkh);
vector = input.witness;
redeem = false;
version = 1;
}
}
if (valid && !this.checksig(index, prev, value, signature, key, version))
return false;
if (redeem) {
const stack = vector.toStack();
const redeem = stack.pop();
const result = this.signVector(prev, stack, signature, ring);
if (!result)
return false;
result.push(redeem);
vector.fromStack(result);
return true;
}
const stack = vector.toStack();
const result = this.signVector(prev, stack, signature, ring);
if (!result)
return false;
vector.fromStack(result);
return true;
}
/**
* Apply signatures without validating signatures.
* TODO: partially applied?
* @param {KeyRing[]} rings - ring per signature
* @param {Buffer[]} signatures - signatures
* @param {Boolean} verify - should we verify applied signature(s)
* @return {Boolean} whether the signature was applied.
*/
applySignatures(rings, signatures, verify) {
assert(signatures.length === this.inputs.length);
assert(signatures.length === rings.length);
for (let i = 0; i < signatures.length; i++) {
const signature = signatures[i];
const ring = rings[i];
if (!signature)
continue;
if (!ring)
throw new Error('Could not find key.');
const {prevout} = this.inputs[i];
const coin = this.view.getOutput(prevout);
if (!coin)
throw new Error('Could not find coin.');
if (!ring.ownOutput(coin))
throw new Error('Coin does not belong to the key.');
// Build script for input
if (!this.scriptInput(i, coin, ring))
continue;
if (!this.applySignature(i, coin, ring, signature, verify))
return false;
}
return true;
}
/**
* Check signatures
* @param {KeyRing[]} rings - ring per signature
* @param {Buffer[]} signatures
* @returns {Number} number of valid signatures
*/
checkSignatures(rings, signatures) {
assert(signatures.length === this.inputs.length);
let valid = 0;
for (let i = 0; i < signatures.length; i++) {
const signature = signatures[i];
const ring = rings[i];
if (!signature)
continue;
if (!ring)
throw new Error('Could not find key.');
const {prevout} = this.inputs[i];
const coin = this.view.getCoin(prevout);
if (!coin)
throw new Error('Could not find coin.');
if (!ring.ownOutput(coin))
throw new Error('Coin does not belong to the key.');
if (this.checkSignature(i, coin, ring, signature))
valid += 1;
}
return valid;
}
/**
* Empties input scripts and witness
*/
emptyInputs() {
for (const input of this.inputs) {
const {script, witness} = input;
if (script.length > 0)
input.script = new Script();
if (witness.length > 0)
input.witness = new Witness();
}
}
toMTX() {
return new MTX().inject(this);
}
/**
* Instantiate MultisigMTX from MTX.
* @param {MTX} mtx
* @returns {MultisigMTX}
*/
static fromMTX(mtx) {
return new this().inject(mtx);
}
/**
* Test whether an object is a MultisigMTX.
* @param {Object} obj
* @returns {Boolean}
*/
static isMultisigMTX(obj) {
return obj instanceof MultisigMTX;
}
}
/*
* Expose
*/
module.exports = MultisigMTX;