improve/fix p2sh. move input signing functionality to tx object.
This commit is contained in:
parent
377e156874
commit
505aad8729
@ -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,
|
||||
|
||||
@ -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) {
|
||||
|
||||
110
lib/bcoin/tx.js
110
lib/bcoin/tx.js
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user