scripts: add allowIncomplete for multisig scripts

This commit is contained in:
Daniel Cousens 2015-02-11 14:01:20 +11:00
parent abf870fb37
commit c35d4b46c5
4 changed files with 118 additions and 23 deletions

View file

@ -1,6 +1,6 @@
var assert = require('assert') var assert = require('assert')
var typeForce = require('typeforce')
var ops = require('./opcodes') var ops = require('./opcodes')
var typeForce = require('typeforce')
var ecurve = require('ecurve') var ecurve = require('ecurve')
var curve = ecurve.getCurveByName('secp256k1') var curve = ecurve.getCurveByName('secp256k1')
@ -63,7 +63,7 @@ function isPubKeyOutput(script) {
script.chunks[1] === ops.OP_CHECKSIG script.chunks[1] === ops.OP_CHECKSIG
} }
function isScriptHashInput(script) { function isScriptHashInput(script, allowIncomplete) {
if (script.chunks.length < 2) return false if (script.chunks.length < 2) return false
var lastChunk = script.chunks[script.chunks.length - 1] var lastChunk = script.chunks[script.chunks.length - 1]
@ -72,7 +72,7 @@ function isScriptHashInput(script) {
var scriptSig = Script.fromChunks(script.chunks.slice(0, -1)) var scriptSig = Script.fromChunks(script.chunks.slice(0, -1))
var scriptPubKey = Script.fromBuffer(lastChunk) var scriptPubKey = Script.fromBuffer(lastChunk)
return classifyInput(scriptSig) === classifyOutput(scriptPubKey) return classifyInput(scriptSig, allowIncomplete) === classifyOutput(scriptPubKey)
} }
function isScriptHashOutput(script) { function isScriptHashOutput(script) {
@ -83,9 +83,19 @@ function isScriptHashOutput(script) {
script.chunks[2] === ops.OP_EQUAL script.chunks[2] === ops.OP_EQUAL
} }
function isMultisigInput(script) { // allowIncomplete is to account for combining signatures
return script.chunks[0] === ops.OP_0 && // See https://github.com/bitcoin/bitcoin/blob/f425050546644a36b0b8e0eb2f6934a3e0f6f80f/src/script/sign.cpp#L195-L197
script.chunks.slice(1).every(isCanonicalSignature) function isMultisigInput(script, allowIncomplete) {
if (script.chunks.length < 2) return false
if (script.chunks[0] !== ops.OP_0) return false
if (allowIncomplete) {
return script.chunks.slice(1).every(function(chunk) {
return chunk === ops.OP_0 || isCanonicalSignature(chunk)
})
}
return script.chunks.slice(1).every(isCanonicalSignature)
} }
function isMultisigOutput(script) { function isMultisigOutput(script) {
@ -134,15 +144,15 @@ function classifyOutput(script) {
return 'nonstandard' return 'nonstandard'
} }
function classifyInput(script) { function classifyInput(script, allowIncomplete) {
typeForce('Script', script) typeForce('Script', script)
if (isPubKeyHashInput(script)) { if (isPubKeyHashInput(script)) {
return 'pubkeyhash' return 'pubkeyhash'
} else if (isScriptHashInput(script)) { } else if (isMultisigInput(script, allowIncomplete)) {
return 'scripthash'
} else if (isMultisigInput(script)) {
return 'multisig' return 'multisig'
} else if (isScriptHashInput(script, allowIncomplete)) {
return 'scripthash'
} else if (isPubKeyInput(script)) { } else if (isPubKeyInput(script)) {
return 'pubkey' return 'pubkey'
} }
@ -234,8 +244,13 @@ function multisigInput(signatures, scriptPubKey) {
var m = mOp - (ops.OP_1 - 1) var m = mOp - (ops.OP_1 - 1)
var n = nOp - (ops.OP_1 - 1) var n = nOp - (ops.OP_1 - 1)
assert(signatures.length >= m, 'Not enough signatures provided') var count = 0
assert(signatures.length <= n, 'Too many signatures provided') signatures.forEach(function(signature) {
count += (signature !== ops.OP_0)
})
assert(count >= m, 'Not enough signatures provided')
assert(count <= n, 'Too many signatures provided')
} }
return Script.fromChunks([].concat(ops.OP_0, signatures)) return Script.fromChunks([].concat(ops.OP_0, signatures))

View file

@ -1,4 +1,5 @@
var assert = require('assert') var assert = require('assert')
var ops = require('./opcodes')
var scripts = require('./scripts') var scripts = require('./scripts')
var ECPubKey = require('./ecpubkey') var ECPubKey = require('./ecpubkey')
@ -44,23 +45,23 @@ TransactionBuilder.fromTransaction = function(transaction) {
var redeemScript var redeemScript
var scriptSig = txIn.script var scriptSig = txIn.script
var scriptType = scripts.classifyInput(scriptSig) var scriptType = scripts.classifyInput(scriptSig, true)
// Re-classify if P2SH // Re-classify if P2SH
if (scriptType === 'scripthash') { if (scriptType === 'scripthash') {
redeemScript = Script.fromBuffer(scriptSig.chunks.slice(-1)[0]) redeemScript = Script.fromBuffer(scriptSig.chunks.slice(-1)[0])
scriptSig = Script.fromChunks(scriptSig.chunks.slice(0, -1)) scriptSig = Script.fromChunks(scriptSig.chunks.slice(0, -1))
scriptType = scripts.classifyInput(scriptSig) scriptType = scripts.classifyInput(scriptSig, true)
assert.equal(scripts.classifyOutput(redeemScript), scriptType, 'Non-matching scriptSig and scriptPubKey in input') assert.equal(scripts.classifyOutput(redeemScript), scriptType, 'Non-matching scriptSig and scriptPubKey in input')
} }
// Extract hashType, pubKeys and signatures // Extract hashType, pubKeys and signatures
var hashType, pubKeys, signatures var hashType, parsed, pubKeys, signatures
switch (scriptType) { switch (scriptType) {
case 'pubkeyhash': case 'pubkeyhash':
var parsed = ECSignature.parseScriptSignature(scriptSig.chunks[0]) parsed = ECSignature.parseScriptSignature(scriptSig.chunks[0])
var pubKey = ECPubKey.fromBuffer(scriptSig.chunks[1]) var pubKey = ECPubKey.fromBuffer(scriptSig.chunks[1])
hashType = parsed.hashType hashType = parsed.hashType
@ -70,10 +71,9 @@ TransactionBuilder.fromTransaction = function(transaction) {
break break
case 'multisig': case 'multisig':
var scriptSigs = scriptSig.chunks.slice(1) // ignore OP_0 parsed = scriptSig.chunks.slice(1).filter(function(chunk) {
var parsed = scriptSigs.map(function(scriptSig) { return chunk !== ops.OP_0
return ECSignature.parseScriptSignature(scriptSig) }).map(ECSignature.parseScriptSignature)
})
hashType = parsed[0].hashType hashType = parsed[0].hashType
pubKeys = [] pubKeys = []
@ -82,7 +82,7 @@ TransactionBuilder.fromTransaction = function(transaction) {
break break
case 'pubkey': case 'pubkey':
var parsed = ECSignature.parseScriptSignature(scriptSig.chunks[0]) parsed = ECSignature.parseScriptSignature(scriptSig.chunks[0])
hashType = parsed.hashType hashType = parsed.hashType
pubKeys = [] pubKeys = []

View file

@ -53,6 +53,51 @@
"type": "nulldata", "type": "nulldata",
"data": "deadffffffffffffffffffffffffffffffffbeef", "data": "deadffffffffffffffffffffffffffffffffbeef",
"scriptPubKey": "OP_RETURN deadffffffffffffffffffffffffffffffffbeef" "scriptPubKey": "OP_RETURN deadffffffffffffffffffffffffffffffffbeef"
},
{
"type": "nonstandard",
"typeIncomplete": "multisig",
"pubKeys": [
"0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
"02b80011a883a0fd621ad46dfc405df1e74bf075cbaf700fd4aebef6e96f848340",
"024289801366bcee6172b771cf5a7f13aaecd237a0b9a1ff9d769cabc2e6b70a34"
],
"signatures": [
null,
"3044022001ab168e80b863fdec694350b587339bb72a37108ac3c989849251444d13ebba02201811272023e3c1038478eb972a82d3ad431bfc2408e88e4da990f1a7ecbb263901",
"3045022100aaeb7204c17eee2f2c4ff1c9f8b39b79e75e7fbf33e92cc67ac51be8f15b75f90220659eee314a4943a6384d2b154fa5821ef7a084814d7ee2c6f9f7f0ffb53be34b01"
],
"scriptSig": "OP_0 OP_0 3044022001ab168e80b863fdec694350b587339bb72a37108ac3c989849251444d13ebba02201811272023e3c1038478eb972a82d3ad431bfc2408e88e4da990f1a7ecbb263901 3045022100aaeb7204c17eee2f2c4ff1c9f8b39b79e75e7fbf33e92cc67ac51be8f15b75f90220659eee314a4943a6384d2b154fa5821ef7a084814d7ee2c6f9f7f0ffb53be34b01"
},
{
"type": "nonstandard",
"typeIncomplete": "multisig",
"pubKeys": [
"0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
"02b80011a883a0fd621ad46dfc405df1e74bf075cbaf700fd4aebef6e96f848340",
"024289801366bcee6172b771cf5a7f13aaecd237a0b9a1ff9d769cabc2e6b70a34"
],
"signatures": [
null,
null,
null
],
"scriptSig": "OP_0 OP_0 OP_0 OP_0"
},
{
"type": "nonstandard",
"typeIncomplete": "scripthash",
"pubKeys": [
"0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8",
"04c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee51ae168fea63dc339a3c58419466ceaeef7f632653266d0e1236431a950cfe52a"
],
"signatures": [
null,
"30450221009c92c1ae1767ac04e424da7f6db045d979b08cde86b1ddba48621d59a109d818022004f5bb21ad72255177270abaeb2d7940ac18f1e5ca1f53db4f3fd1045647a8a801"
],
"redeemScript": "OP_2 0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8 04c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee51ae168fea63dc339a3c58419466ceaeef7f632653266d0e1236431a950cfe52a OP_2 OP_CHECKMULTISIG",
"redeemScriptSig": "OP_0 OP_0 30450221009c92c1ae1767ac04e424da7f6db045d979b08cde86b1ddba48621d59a109d818022004f5bb21ad72255177270abaeb2d7940ac18f1e5ca1f53db4f3fd1045647a8a801",
"scriptSig": "OP_0 OP_0 30450221009c92c1ae1767ac04e424da7f6db045d979b08cde86b1ddba48621d59a109d818022004f5bb21ad72255177270abaeb2d7940ac18f1e5ca1f53db4f3fd1045647a8a801 52410479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b84104c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee51ae168fea63dc339a3c58419466ceaeef7f632653266d0e1236431a950cfe52a52ae"
} }
], ],
"invalid": { "invalid": {
@ -121,6 +166,18 @@
} }
], ],
"multisigInput": [ "multisigInput": [
{
"description": "Not enough signatures provided",
"type": "multisig",
"pubKeys": [
"0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
"02b80011a883a0fd621ad46dfc405df1e74bf075cbaf700fd4aebef6e96f848340"
],
"signatures": [
null,
null
]
},
{ {
"exception": "Not enough signatures provided", "exception": "Not enough signatures provided",
"pubKeys": [ "pubKeys": [

View file

@ -1,4 +1,5 @@
var assert = require('assert') var assert = require('assert')
var ops = require('../src/opcodes')
var scripts = require('../src/scripts') var scripts = require('../src/scripts')
var ECPubKey = require('../src/ecpubkey') var ECPubKey = require('../src/ecpubkey')
@ -22,6 +23,18 @@ describe('Scripts', function() {
assert.equal(type, f.type) assert.equal(type, f.type)
}) })
}) })
fixtures.valid.forEach(function(f) {
if (!f.scriptSig) return
if (!f.typeIncomplete) return
it('classifies incomplete ' + f.scriptSig + ' as ' + f.typeIncomplete, function() {
var script = Script.fromASM(f.scriptSig)
var type = scripts.classifyInput(script, true)
assert.equal(type, f.typeIncomplete)
})
})
}) })
describe('classifyOutput', function() { describe('classifyOutput', function() {
@ -51,6 +64,16 @@ describe('Scripts', function() {
assert.equal(inputFn(script), expected) assert.equal(inputFn(script), expected)
}) })
if (f.typeIncomplete) {
var expectedIncomplete = type.toLowerCase() === f.typeIncomplete
it('returns ' + expected + ' for ' + f.scriptSig, function() {
var script = Script.fromASM(f.scriptSig)
assert.equal(inputFn(script, true), expectedIncomplete)
})
}
} }
}) })
}) })
@ -131,7 +154,7 @@ describe('Scripts', function() {
it('returns ' + f.scriptSig, function() { it('returns ' + f.scriptSig, function() {
var signatures = f.signatures.map(function(signature) { var signatures = f.signatures.map(function(signature) {
return new Buffer(signature, 'hex') return signature ? new Buffer(signature, 'hex') : ops.OP_0
}) })
var scriptSig = scripts.multisigInput(signatures) var scriptSig = scripts.multisigInput(signatures)
@ -145,7 +168,7 @@ describe('Scripts', function() {
it('throws on ' + f.exception, function() { it('throws on ' + f.exception, function() {
var signatures = f.signatures.map(function(signature) { var signatures = f.signatures.map(function(signature) {
return new Buffer(signature, 'hex') return signature ? new Buffer(signature, 'hex') : ops.OP_0
}) })
assert.throws(function() { assert.throws(function() {