improve/fix p2sh. move input signing functionality to tx object.

This commit is contained in:
Christopher Jeffrey 2015-12-05 03:01:37 -08:00
parent 377e156874
commit 505aad8729
4 changed files with 229 additions and 84 deletions

View File

@ -22,6 +22,8 @@ exports.genesis = {
// address versions
exports.addr = {
normal: 0,
p2pkh: 0,
multisig: 0,
p2sh: 5
};
@ -157,6 +159,11 @@ exports.hashType = {
anyonecaypay: 0x80
};
exports.rhashType = Object.keys(exports.hashType).reduce(function(out, type) {
out[exports.hashType[type]] = type;
return out;
}, {});
exports.block = {
maxSize: 1000000,
maxSigops: 1000000 / 50,

View File

@ -151,6 +151,10 @@ script.execute = function execute(s, stack, tx) {
if (type !== 1)
return false;
// XXX Deal with different hashTypes besides `all`
// if (typeof tx === 'function')
// tx = tx(constants.rhashType[type]);
var res = bcoin.ecdsa.verify(tx, sig.slice(0, -1), pub);
if (o === 'checksigverify') {
if (!res)
@ -195,6 +199,10 @@ script.execute = function execute(s, stack, tx) {
if (type !== 1)
return false;
// XXX Deal with different hashTypes besides `all`
// if (typeof tx === 'function')
// tx = tx(constants.rhashType[type]);
var res = false;
for (; !res && j < n; j++)
res = bcoin.ecdsa.verify(tx, sig.slice(0, -1), keys[j]);
@ -221,6 +229,23 @@ script.execute = function execute(s, stack, tx) {
return true;
};
script.redemption = function(publicKeys, m, n) {
if (publicKeys.length !== m) {
throw new Error('wrong amount of pubkeys for redeem script');
}
var mcode = constants.opcodes['1'] + (m - 1);
var ncode = constants.opcodes['1'] + (n - 1);
var redemption = [];
redemption.push(mcode);
publicKeys.forEach(function(pubkey) {
redemption.push(pubkey.length);
redemption = redemption.concat(pubkey);
});
redemption.push(ncode);
redemption.push(constants.opcodes.checkmultisig);
return redemption;
};
script.isPubkeyhash = function isPubkeyhash(s, hash) {
if (s.length !== 5)
return false;
@ -296,14 +321,22 @@ script.isPubkeyhashInput = function isPubkeyhashInput(s) {
33 <= s[1].length && s[1].length <= 65;
};
script.isScripthash = function isScripthash(s) {
script.isScripthash = function isScripthash(s, hash) {
if (s.length !== 3)
return false;
return s[0] === 'hash160' &&
Array.isArray(s[1]) &&
s[1].length === 20 &&
s[2] === 'eq';
var ret = s[0] === 'hash160' &&
Array.isArray(s[1]) &&
s[1].length === 20 &&
s[2] === 'eq';
if (!ret)
return false;
if (hash)
return utils.isEqual(s[1], hash);
return true;
};
script.isNullData = function isNullData(s) {

View File

@ -3,6 +3,7 @@ var bn = require('bn.js');
var bcoin = require('../bcoin');
var utils = bcoin.utils;
var assert = utils.assert;
var constants = bcoin.protocol.constants;
function TX(data, block) {
if (!(this instanceof TX))
@ -111,6 +112,110 @@ TX.prototype._inputIndex = function _inputIndex(hash, index) {
return -1;
};
TX.prototype.signature = function(input, key) {
// Get the previous output's subscript
var s = input.out.tx.getSubscript(input.out.index);
// Get the hash of the current tx, minus the other inputs, plus the sighash.
var hash = this.subscriptHash(tx.inputs.indexOf(input), s, type);
// Sign the transaction with our one input
var signature = bcoin.ecdsa.sign(hash, key).toDER();
// Add the sighash as a single byte to the signature
signature = signature.concat(constants.hashType[type]);
return signature;
};
// Build the scriptSigs for inputs, excluding the signatures
TX.prototype.scriptInput = function(input, pub, nsigs) {
// Get the previous output's subscript
var s = input.out.tx.getSubscript(input.out.index);
// P2PKH and simple tx
if (bcoin.script.isPubkeyhash(s) || bcoin.script.isSimplePubkeyhash(s)) {
input.script = [ constants.opcodes['0'], pub ];
return;
}
// Multisig
// raw format: OP_FALSE [sig-1] [sig-2] ...
if (bcoin.script.isMultisig(s)) {
if (!nsigs) {
throw new Error('`nsigs` is required for multisig');
}
input.script = [ constants.opcodes['false'] ];
for (var i = 0; i < nsigs; i++) {
input.script[i + 1] = constants.opcodes['0'];
}
return;
}
// P2SH multisig
// p2sh format: OP_FALSE [sig-1] [sig-2] ... [redeem-script]
if (bcoin.script.isScripthash(s)) {
input.script = [ constants.opcodes['false'] ];
var m = pub[0] - constants.opcodes['1'] + 1;
for (var i = 0; i < m; i++) {
input.script[i + 1] = constants.opcodes['0'];
}
// P2SH requires the redeem script after signatures
if (bcoin.script.isScripthash(s)) {
input.script.push(pub);
}
return;
}
throw new Error('could not identify prev_out type');
};
// Sign the now-built scriptSigs
TX.prototype.signInput = function(input, key) {
// Get the previous output's subscript
var s = input.out.tx.getSubscript(input.out.index);
// Get the hash of the current tx, minus the other inputs, plus the sighash.
var hash = this.subscriptHash(tx.inputs.indexOf(input), s, type);
// Sign the transaction with our one input
var signature = bcoin.ecdsa.sign(hash, key).toDER();
// Add the sighash as a single byte to the signature
signature = signature.concat(constants.hashType[type]);
// P2PKH and simple tx
if (bcoin.script.isPubkeyhash(s) || bcoin.script.isSimplePubkeyhash(s)) {
input.script[0] = signature;
return;
}
// Multisig
// empty array == OP_FALSE == OP_0
// raw format: OP_FALSE [sig-1] [sig-2] ...
// p2sh format: OP_FALSE [sig-1] [sig-2] ... [redeem-script]
if (bcoin.script.isMultisig(s) || bcoin.script.isScripthash(s)) {
var l = input.script.length;
if (bcoin.script.isScripthash(s)) {
l--;
}
for (var i = 0; i < l; i++) {
input.script[i + 1] = signature;
}
}
};
// Build the scriptSig and sign it
TX.prototype.scriptSig = function(input, key, pub, nsigs) {
// Build script for input
tx.scriptInput(input, pub, nsigs);
// Sign input
tx.signInput(input, key);
return this.input.script;
};
TX.prototype.input = function input(i, index) {
this._input(i, index);
return this;
@ -155,7 +260,7 @@ TX.prototype.out = function out(output, value) {
// outputs: [ [ 2 ], 'key1', 'key2', [ 2 ], 'checkmultisig' ]
// in reality:
// outputs: [ [ 2 ], [0,1,...], [2,3,...], [ 2 ], 'checkmultisig' ]
} else if (bcoin.wallet.validAddress(output.address, 'p2sh')) {
} else if (bcoin.wallet.validateAddress(output.address, 'p2sh')) {
// p2sh transaction
// https://github.com/bitcoin/bips/blob/master/bip-0016.mediawiki
// hash160 [20-byte-redeemscript-hash] equal
@ -224,6 +329,9 @@ TX.prototype.verify = function verify(index, force) {
var subscript = input.out.tx.getSubscript(input.out.index);
var hash = this.subscriptHash(i, subscript, 'all');
// XXX Deal with different hashTypes besides `all`
// var hash = this.subscriptHash.bind(this, i, subscript);
var stack = [];
bcoin.script.execute(input.script, stack);
var prev = input.out.tx.outputs[input.out.index].script;

View File

@ -29,7 +29,7 @@ function Wallet(options, passphrase) {
this.key = null;
this.loaded = false;
this.lastTs = 0;
this.publicKeys = options.publicKeys;
this.sharedKeys = options.sharedKeys;
if (options.priv instanceof bcoin.hd.priv) {
this.hd = options.priv;
@ -58,24 +58,28 @@ function Wallet(options, passphrase) {
this.fee = 10000;
this.dust = 5460;
this.addressType = options.addressType || 'normal';
// Multisig
this.sharedKeys = (options.sharedKeys || []).map(utils.toKeyArray);
this.m = options.m || 1;
this.n = options.n || 1;
this.publicKeys = options.publicKeys;
if (this.publicKeys) {
if (this.publicKeys.length < this.n) {
this.publicKeys.push(this.getPublicKey());
}
// Use p2sh multisig by default
if (!options.addressType && this.sharedKeys.length) {
this.addressType = 'p2sh';
}
if (this.m < 1 || this.m > this.n) {
throw new Error('m ranges between 1 and n');
}
if (this.n < 1 || this.n > 7) {
throw new Error('n ranges between 1 and 7');
}
if (this.publicKeys.length !== this.n) {
throw new Error(this.n + ' public keys required');
}
if (this.m < 1 || this.m > this.n) {
throw new Error('m ranges between 1 and n');
}
if (this.n < 1 || this.n > 7) {
throw new Error('n ranges between 1 and 7');
}
if (this.sharedKeys.length !== this.m - 1) {
throw new Error(this.m + ' public keys required');
}
this._init();
@ -142,7 +146,12 @@ Wallet.prototype.getPrivateKey = function getPrivateKey(enc) {
};
Wallet.prototype.getPublicKey = function getPublicKey(enc) {
var pub = this.key.getPublic(this.compressed, 'array');
var pub;
if (this.addressType === 'p2sh')
pub = this.getRedemption();
else
pub = this.key.getPublic(this.compressed, 'array');
if (enc === 'base58')
return utils.toBase58(pub);
else if (enc === 'hex')
@ -162,7 +171,7 @@ Wallet.prototype.getAddress = function getAddress() {
Wallet.hash2addr = function hash2addr(hash, version) {
hash = utils.toArray(hash, 'hex');
version = constants.addr[version || 'normal'];
version = constants.addr[version || this.addressType];
hash = [ version ].concat(hash);
var addr = hash.concat(utils.checksum(hash));
@ -173,7 +182,7 @@ Wallet.addr2hash = function addr2hash(addr, version) {
if (!Array.isArray(addr))
addr = utils.fromBase58(addr);
version = constants.addr[version || 'normal'];
version = constants.addr[version || this.addressType];
if (addr.length !== 25)
return [];
@ -187,15 +196,8 @@ Wallet.addr2hash = function addr2hash(addr, version) {
return addr.slice(1, -4);
};
Wallet.validAddress = function validAddr(addr, version) {
if (!addr)
return false;
return !!Wallet.addr2hash(addr, version).length;
};
Wallet.prototype.validateAddress = function validateAddress(addr) {
var p = Wallet.addr2hash(addr);
Wallet.prototype.validateAddress = function validateAddress(addr, version) {
var p = Wallet.addr2hash(addr, version);
return p.length !== 0;
};
Wallet.validateAddress = Wallet.prototype.validateAddress;
@ -219,10 +221,8 @@ Wallet.prototype.ownOutput = function ownOutput(tx, index) {
if (bcoin.script.isMultisig(s, key))
return true;
if (bcoin.script.isScripthash(s)
&& utils.isEqual(s[1], this.getP2SHHash())) {
if (bcoin.script.isScripthash(s, hash))
return true;
}
return false;
}, this);
@ -254,10 +254,8 @@ Wallet.prototype.ownInput = function ownInput(tx, index) {
if (bcoin.script.isMultisig(s, key))
return true;
if (bcoin.script.isScripthash(s)
&& utils.isEqual(s[1], this.getP2SHHash())) {
if (bcoin.script.isScripthash(s, hash))
return true;
}
return false;
}, this);
@ -308,8 +306,6 @@ Wallet.prototype.sign = function sign(tx, type, inputs, off) {
// raw format: OP_FALSE [sig-1] [sig-2] ...
// p2sh format: OP_FALSE [sig-1] [sig-2] ... [redeem-script]
if (bcoin.script.isMultisig(s) || bcoin.script.isScripthash(s)) {
// XXX Check own?
// || (bcoin.script.isScripthash(s) && utils.isEqual(s[1], this.getP2SHHash())) {
if (!input.script || !input.script.length) {
input.script = [ [], signature ];
} else if (!~input.script.indexOf(signature)) {
@ -320,7 +316,7 @@ Wallet.prototype.sign = function sign(tx, type, inputs, off) {
// P2SH requires a redeem script after signatures
if (bcoin.script.isScripthash(s)) {
if (input.script.length - 1 === this.m) {
input.script.push(this.getP2SHRedemption());
input.script.push(this.getRedemption());
}
}
@ -330,6 +326,30 @@ Wallet.prototype.sign = function sign(tx, type, inputs, off) {
return inputs.length;
};
Wallet.prototype.sign = function sign(tx, type, inputs) {
if (!type)
type = 'all';
var pub = this.getPublicKey();
var key = this.key;
var nsigs = this.m;
inputs = inputs || tx.inputs;
// Add signature script to each input
inputs = inputs.filter(function(input) {
// Filter inputs that this wallet own
if (!input.out.tx || !this.ownOutput(input.out.tx))
return false;
tx.scriptSig(input, key, pub, nsigs);
return true;
}, this);
return inputs.length;
};
Wallet.prototype.addTX = function addTX(tx, block) {
return this.tx.add(tx);
};
@ -445,56 +465,29 @@ Wallet.prototype.getChange = function fill(tx) {
};
/**
* P2SH (and Multisig)
* P2SH+Multisig Redemption
*/
Wallet.prototype.getP2SHHash = function() {
return this.getP2SH().hash;
};
Wallet.prototype.getP2SHAddress = function() {
return this.getP2SH().address;
};
Wallet.prototype.getP2SHRedemption = function() {
return this.getP2SH().redemption;
};
Wallet.prototype.getP2SH = function(redeem) {
this.publicKeys = this.publicKeys.map(function(key) {
return utils.toKeyArray(key);
});
var redemption = redeem || this._createMultisigRedemption();
var hash = utils.ripasha(redemption);
return {
hash: hash,
address: Wallet.hash2addr(hash, 'p2sh'),
redemption: redemption
};
};
Wallet.prototype._createMultisigRedemption = function() {
var publicKeys = this.publicKeys;
var mcode = constants.opcodes['1'] + (this.m - 1);
var ncode = constants.opcodes['1'] + (this.n - 1);
var redemption = [];
redemption.push(mcode);
this.publicKeys.forEach(function(pubkey) {
redemption.push(pubkey.length);
redemption = redemption.concat(pubkey);
}, this);
redemption.push(ncode);
redemption.push(constants.opcodes.checkmultisig);
return redemption;
Wallet.prototype.getRedemption = function() {
var sharedKeys = this.sharedKeys.slice().map(utils.toKeyArray);
if (sharedKeys.length < this.m) {
var pub = this.key.getPublic(this.compressed, 'array');
sharedKeys.push(pub);
}
return bcoin.script.redemption(sharedKeys, m, n);
};
Wallet.prototype.toJSON = function toJSON() {
return {
v: 1,
type: 'wallet',
pub: this.getPublicKey('base58'),
pub: utils.toBase58(this.key.getPublic(this.compressed, 'array')),
priv: this.getPrivateKey('base58'),
tx: this.tx.toJSON()
tx: this.tx.toJSON(),
addressType: this.addressType,
sharedKeys: utils.toBase58(this.sharedKeys),
m: this.m,
n: this.n
};
};
@ -528,7 +521,11 @@ Wallet.fromJSON = function fromJSON(json) {
var w = new Wallet({
priv: priv,
pub: pub,
compressed: compressed
compressed: compressed,
addressType: json.addressType,
sharedKeys: json.sharedKeys,
m: json.m,
n: json.n
});
w.tx.fromJSON(json.tx);