scripts: add allowIncomplete for multisig scripts
This commit is contained in:
parent
abf870fb37
commit
c35d4b46c5
4 changed files with 118 additions and 23 deletions
|
@ -1,6 +1,6 @@
|
|||
var assert = require('assert')
|
||||
var typeForce = require('typeforce')
|
||||
var ops = require('./opcodes')
|
||||
var typeForce = require('typeforce')
|
||||
|
||||
var ecurve = require('ecurve')
|
||||
var curve = ecurve.getCurveByName('secp256k1')
|
||||
|
@ -63,7 +63,7 @@ function isPubKeyOutput(script) {
|
|||
script.chunks[1] === ops.OP_CHECKSIG
|
||||
}
|
||||
|
||||
function isScriptHashInput(script) {
|
||||
function isScriptHashInput(script, allowIncomplete) {
|
||||
if (script.chunks.length < 2) return false
|
||||
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 scriptPubKey = Script.fromBuffer(lastChunk)
|
||||
|
||||
return classifyInput(scriptSig) === classifyOutput(scriptPubKey)
|
||||
return classifyInput(scriptSig, allowIncomplete) === classifyOutput(scriptPubKey)
|
||||
}
|
||||
|
||||
function isScriptHashOutput(script) {
|
||||
|
@ -83,9 +83,19 @@ function isScriptHashOutput(script) {
|
|||
script.chunks[2] === ops.OP_EQUAL
|
||||
}
|
||||
|
||||
function isMultisigInput(script) {
|
||||
return script.chunks[0] === ops.OP_0 &&
|
||||
script.chunks.slice(1).every(isCanonicalSignature)
|
||||
// allowIncomplete is to account for combining signatures
|
||||
// See https://github.com/bitcoin/bitcoin/blob/f425050546644a36b0b8e0eb2f6934a3e0f6f80f/src/script/sign.cpp#L195-L197
|
||||
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) {
|
||||
|
@ -134,15 +144,15 @@ function classifyOutput(script) {
|
|||
return 'nonstandard'
|
||||
}
|
||||
|
||||
function classifyInput(script) {
|
||||
function classifyInput(script, allowIncomplete) {
|
||||
typeForce('Script', script)
|
||||
|
||||
if (isPubKeyHashInput(script)) {
|
||||
return 'pubkeyhash'
|
||||
} else if (isScriptHashInput(script)) {
|
||||
return 'scripthash'
|
||||
} else if (isMultisigInput(script)) {
|
||||
} else if (isMultisigInput(script, allowIncomplete)) {
|
||||
return 'multisig'
|
||||
} else if (isScriptHashInput(script, allowIncomplete)) {
|
||||
return 'scripthash'
|
||||
} else if (isPubKeyInput(script)) {
|
||||
return 'pubkey'
|
||||
}
|
||||
|
@ -234,8 +244,13 @@ function multisigInput(signatures, scriptPubKey) {
|
|||
var m = mOp - (ops.OP_1 - 1)
|
||||
var n = nOp - (ops.OP_1 - 1)
|
||||
|
||||
assert(signatures.length >= m, 'Not enough signatures provided')
|
||||
assert(signatures.length <= n, 'Too many signatures provided')
|
||||
var count = 0
|
||||
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))
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
var assert = require('assert')
|
||||
var ops = require('./opcodes')
|
||||
var scripts = require('./scripts')
|
||||
|
||||
var ECPubKey = require('./ecpubkey')
|
||||
|
@ -44,23 +45,23 @@ TransactionBuilder.fromTransaction = function(transaction) {
|
|||
|
||||
var redeemScript
|
||||
var scriptSig = txIn.script
|
||||
var scriptType = scripts.classifyInput(scriptSig)
|
||||
var scriptType = scripts.classifyInput(scriptSig, true)
|
||||
|
||||
// Re-classify if P2SH
|
||||
if (scriptType === 'scripthash') {
|
||||
redeemScript = Script.fromBuffer(scriptSig.chunks.slice(-1)[0])
|
||||
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')
|
||||
}
|
||||
|
||||
// Extract hashType, pubKeys and signatures
|
||||
var hashType, pubKeys, signatures
|
||||
var hashType, parsed, pubKeys, signatures
|
||||
|
||||
switch (scriptType) {
|
||||
case 'pubkeyhash':
|
||||
var parsed = ECSignature.parseScriptSignature(scriptSig.chunks[0])
|
||||
parsed = ECSignature.parseScriptSignature(scriptSig.chunks[0])
|
||||
var pubKey = ECPubKey.fromBuffer(scriptSig.chunks[1])
|
||||
|
||||
hashType = parsed.hashType
|
||||
|
@ -70,10 +71,9 @@ TransactionBuilder.fromTransaction = function(transaction) {
|
|||
break
|
||||
|
||||
case 'multisig':
|
||||
var scriptSigs = scriptSig.chunks.slice(1) // ignore OP_0
|
||||
var parsed = scriptSigs.map(function(scriptSig) {
|
||||
return ECSignature.parseScriptSignature(scriptSig)
|
||||
})
|
||||
parsed = scriptSig.chunks.slice(1).filter(function(chunk) {
|
||||
return chunk !== ops.OP_0
|
||||
}).map(ECSignature.parseScriptSignature)
|
||||
|
||||
hashType = parsed[0].hashType
|
||||
pubKeys = []
|
||||
|
@ -82,7 +82,7 @@ TransactionBuilder.fromTransaction = function(transaction) {
|
|||
break
|
||||
|
||||
case 'pubkey':
|
||||
var parsed = ECSignature.parseScriptSignature(scriptSig.chunks[0])
|
||||
parsed = ECSignature.parseScriptSignature(scriptSig.chunks[0])
|
||||
|
||||
hashType = parsed.hashType
|
||||
pubKeys = []
|
||||
|
|
57
test/fixtures/scripts.json
vendored
57
test/fixtures/scripts.json
vendored
|
@ -53,6 +53,51 @@
|
|||
"type": "nulldata",
|
||||
"data": "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": {
|
||||
|
@ -121,6 +166,18 @@
|
|||
}
|
||||
],
|
||||
"multisigInput": [
|
||||
{
|
||||
"description": "Not enough signatures provided",
|
||||
"type": "multisig",
|
||||
"pubKeys": [
|
||||
"0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
|
||||
"02b80011a883a0fd621ad46dfc405df1e74bf075cbaf700fd4aebef6e96f848340"
|
||||
],
|
||||
"signatures": [
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
{
|
||||
"exception": "Not enough signatures provided",
|
||||
"pubKeys": [
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
var assert = require('assert')
|
||||
var ops = require('../src/opcodes')
|
||||
var scripts = require('../src/scripts')
|
||||
|
||||
var ECPubKey = require('../src/ecpubkey')
|
||||
|
@ -22,6 +23,18 @@ describe('Scripts', function() {
|
|||
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() {
|
||||
|
@ -51,6 +64,16 @@ describe('Scripts', function() {
|
|||
|
||||
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() {
|
||||
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)
|
||||
|
@ -145,7 +168,7 @@ describe('Scripts', function() {
|
|||
|
||||
it('throws on ' + f.exception, function() {
|
||||
var signatures = f.signatures.map(function(signature) {
|
||||
return new Buffer(signature, 'hex')
|
||||
return signature ? new Buffer(signature, 'hex') : ops.OP_0
|
||||
})
|
||||
|
||||
assert.throws(function() {
|
||||
|
|
Loading…
Reference in a new issue