From 34ebf0f32f4729eb5f37133cae9f9f2b4fdc7033 Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Wed, 13 Mar 2019 01:11:18 -0500 Subject: [PATCH] txscript: Optimize IsMultisigSigScript. This converts the IsMultisigSigScript function to analyze the raw script and make use of the new tokenizer instead of the far less efficient parseScript thereby significantly optimizing the function. In order to accomplish this, it first rejects scripts that can't possibly fit the bill due to the final byte of what would be the redeem script not being the appropriate opcode or the overall script not having enough bytes. Then, it uses a new function that is introduced named finalOpcodeData that uses the tokenizer to return any data associated with the final opcode in the signature script (which will be nil for non-push opcodes or if the script fails to parse) and analyzes it as if it were a redeem script when it is non nil. It is also worth noting that this new implementation intentionally has the same semantic difference from the existing implementation as the updated IsMultisigScript function in regards to allowing zero pubkeys whereas previously it incorrectly required at least one pubkey. Finally, the comment is modified to explicitly call out the script version semantics. The following is a before and after comparison of analyzing a large script that is not a multisig script and both a 1-of-2 multisig public key script (which should be false) and a signature script comprised of a pay-to-script-hash 1-of-2 multisig redeem script (which should be true): benchmark old ns/op new ns/op delta BenchmarkIsMultisigSigScriptLarge-8 69328 2.93 -100.00% BenchmarkIsMultisigSigScript-8 2375 146 -93.85% benchmark old allocs new allocs delta BenchmarkIsMultisigSigScriptLarge-8 5 0 -100.00% BenchmarkIsMultisigSigScript-8 3 0 -100.00% benchmark old bytes new bytes delta BenchmarkIsMultisigSigScriptLarge-8 330035 0 -100.00% BenchmarkIsMultisigSigScript-8 9472 0 -100.00% --- txscript/script.go | 19 +++++++++++++++++++ txscript/standard.go | 41 +++++++++++++++++++++++++++++------------ 2 files changed, 48 insertions(+), 12 deletions(-) diff --git a/txscript/script.go b/txscript/script.go index a8cfd166..eb5be8b7 100644 --- a/txscript/script.go +++ b/txscript/script.go @@ -769,6 +769,25 @@ func GetSigOpCount(script []byte) int { return getSigOpCount(pops, false) } +// finalOpcodeData returns the data associated with the final opcode in the +// script. It will return nil if the script fails to parse. +func finalOpcodeData(scriptVersion uint16, script []byte) []byte { + // Avoid unnecessary work. + if len(script) == 0 { + return nil + } + + var data []byte + tokenizer := MakeScriptTokenizer(scriptVersion, script) + for tokenizer.Next() { + data = tokenizer.Data() + } + if tokenizer.Err() != nil { + return nil + } + return data +} + // GetPreciseSigOpCount returns the number of signature operations in // scriptPubKey. If bip16 is true then scriptSig may be searched for the // Pay-To-Script-Hash script in order to find the precise number of signature diff --git a/txscript/standard.go b/txscript/standard.go index 51dcbfb9..272b42b8 100644 --- a/txscript/standard.go +++ b/txscript/standard.go @@ -365,22 +365,39 @@ func IsMultisigScript(script []byte) (bool, error) { return isMultisigScript(scriptVersion, script), nil } -// IsMultisigSigScript takes a script, parses it, then returns whether or -// not it is a multisignature script. +// IsMultisigSigScript returns whether or not the passed script appears to be a +// signature script which consists of a pay-to-script-hash multi-signature +// redeem script. Determining if a signature script is actually a redemption of +// pay-to-script-hash requires the associated public key script which is often +// expensive to obtain. Therefore, this makes a fast best effort guess that has +// a high probability of being correct by checking if the signature script ends +// with a data push and treating that data push as if it were a p2sh redeem +// script +// +// NOTE: This function is only valid for version 0 scripts. Since the function +// does not accept a script version, the results are undefined for other script +// versions. func IsMultisigSigScript(script []byte) bool { - if len(script) == 0 || script == nil { - return false - } - pops, err := parseScript(script) - if err != nil { - return false - } - subPops, err := parseScript(pops[len(pops)-1].data) - if err != nil { + const scriptVersion = 0 + + // The script can't possibly be a multisig signature script if it doesn't + // end with OP_CHECKMULTISIG in the redeem script or have at least two small + // integers preceding it, and the redeem script itself must be preceded by + // at least a data push opcode. Fail fast to avoid more work below. + if len(script) < 4 || script[len(script)-1] != OP_CHECKMULTISIG { return false } - return isMultiSig(subPops) + // Parse through the script to find the last opcode and any data it might + // push and treat it as a p2sh redeem script even though it might not + // actually be one. + possibleRedeemScript := finalOpcodeData(scriptVersion, script) + if possibleRedeemScript == nil { + return false + } + + // Finally, return if that possible redeem script is a multisig script. + return isMultisigScript(scriptVersion, possibleRedeemScript) } // isNullData returns true if the passed script is a null data transaction,