From 0eaae2663ba9955de61a2c6bf52b008dd40cc7f6 Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Wed, 13 Mar 2019 01:11:16 -0500 Subject: [PATCH] txscript: Optimize IsMultisigScript. This converts the IsMultisigScript function to make use of the new tokenizer instead of the far less efficient parseScript thereby significantly optimizing the function. In order to accomplish this, it introduces two new functions. The first one is named extractMultisigScriptDetails and works with the raw script bytes to simultaneously determine if the script is a multisignature script, and in the case it is, extract and return the relevant details. The second new function is named isMultisigScript and is defined in terms of the former. The extract function accepts the script version, raw script bytes, and a flag to determine whether or not the public keys should also be extracted. The flag is provided because extracting pubkeys results in an allocation that the caller might wish to avoid. The extract function approach was chosen because it is common for callers to want to only extract relevant details from a script if the script is of the specific type. Extracting those details requires performing the exact same checks to ensure the script is of the correct type, so it is more efficient to combine the two into one and define the type determination in terms of the result so long as the extraction does not require allocations. It is important to note that this new implementation intentionally has a semantic difference from the existing implementation in that it will now correctly identify a multisig script with zero pubkeys whereas previously it incorrectly required at least one pubkey. This change is acceptable because the function only deals with standardness rather than consensus rules. Finally, this also deprecates the isMultiSig function that requires opcodes in favor of the new functions and deprecates the error return on the export IsMultisigScript function since it really does not make sense given the purpose of the function. The following is a before and after comparison of analyzing both a large script that is not a multisig script and a 1-of-2 multisig public key script: benchmark old ns/op new ns/op delta BenchmarkIsMultisigScriptLarge-8 64166 5.52 -99.99% BenchmarkIsMultisigScript-8 630 59.4 -90.57% benchmark old allocs new allocs delta BenchmarkIsMultisigScriptLarge-8 1 0 -100.00% BenchmarkIsMultisigScript-8 1 0 -100.00% benchmark old bytes new bytes delta BenchmarkIsMultisigScriptLarge-8 311299 0 -100.00% BenchmarkIsMultisigScript-8 2304 0 -100.00% --- txscript/engine.go | 21 ++++++++ txscript/standard.go | 116 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 127 insertions(+), 10 deletions(-) diff --git a/txscript/engine.go b/txscript/engine.go index f2d7b303..3c76b747 100644 --- a/txscript/engine.go +++ b/txscript/engine.go @@ -580,6 +580,27 @@ func (vm *Engine) checkHashTypeEncoding(hashType SigHashType) error { return nil } +// isStrictPubKeyEncoding returns whether or not the passed public key adheres +// to the strict encoding requirements. +func isStrictPubKeyEncoding(pubKey []byte) bool { + if len(pubKey) == 33 && (pubKey[0] == 0x02 || pubKey[0] == 0x03) { + // Compressed + return true + } + if len(pubKey) == 65 { + switch pubKey[0] { + case 0x04: + // Uncompressed + return true + + case 0x06, 0x07: + // Hybrid + return true + } + } + return false +} + // checkPubKeyEncoding returns whether or not the passed public key adheres to // the strict encoding requirements if enabled. func (vm *Engine) checkPubKeyEncoding(pubKey []byte) error { diff --git a/txscript/standard.go b/txscript/standard.go index 25483aeb..096de4e5 100644 --- a/txscript/standard.go +++ b/txscript/standard.go @@ -216,6 +216,8 @@ func isPubkeyHash(pops []parsedOpcode) bool { // isMultiSig returns true if the passed script is a multisig transaction, false // otherwise. +// +// DEPECATED. Use isMultisigScript or extractMultisigScriptDetails instead. func isMultiSig(pops []parsedOpcode) bool { // The absolute minimum is 1 pubkey: // OP_0/OP_1-16 OP_1 OP_CHECKMULTISIG @@ -248,6 +250,108 @@ func isMultiSig(pops []parsedOpcode) bool { return true } +// multiSigDetails houses details extracted from a standard multisig script. +type multiSigDetails struct { + requiredSigs int + numPubKeys int + pubKeys [][]byte + valid bool +} + +// extractMultisigScriptDetails attempts to extract details from the passed +// script if it is a standard multisig script. The returned details struct will +// have the valid flag set to false otherwise. +// +// The extract pubkeys flag indicates whether or not the pubkeys themselves +// should also be extracted and is provided because extracting them results in +// an allocation that the caller might wish to avoid. The pubKeys member of +// the returned details struct will be nil when the flag is false. +// +// NOTE: This function is only valid for version 0 scripts. The returned +// details struct will always be empty and have the valid flag set to false for +// other script versions. +func extractMultisigScriptDetails(scriptVersion uint16, script []byte, extractPubKeys bool) multiSigDetails { + // The only currently supported script version is 0. + if scriptVersion != 0 { + return multiSigDetails{} + } + + // A multi-signature script is of the form: + // NUM_SIGS PUBKEY PUBKEY PUBKEY ... NUM_PUBKEYS OP_CHECKMULTISIG + + // The script can't possibly be a multisig script if it doesn't end with + // OP_CHECKMULTISIG or have at least two small integer pushes preceding it. + // Fail fast to avoid more work below. + if len(script) < 3 || script[len(script)-1] != OP_CHECKMULTISIG { + return multiSigDetails{} + } + + // The first opcode must be a small integer specifying the number of + // signatures required. + tokenizer := MakeScriptTokenizer(scriptVersion, script) + if !tokenizer.Next() || !isSmallInt(tokenizer.Opcode()) { + return multiSigDetails{} + } + requiredSigs := asSmallInt(tokenizer.Opcode()) + + // The next series of opcodes must either push public keys or be a small + // integer specifying the number of public keys. + var numPubKeys int + var pubKeys [][]byte + if extractPubKeys { + pubKeys = make([][]byte, 0, MaxPubKeysPerMultiSig) + } + for tokenizer.Next() { + if isSmallInt(tokenizer.Opcode()) { + break + } + + data := tokenizer.Data() + numPubKeys++ + if !isStrictPubKeyEncoding(data) { + continue + } + if extractPubKeys { + pubKeys = append(pubKeys, data) + } + } + if tokenizer.Done() { + return multiSigDetails{} + } + + // The next opcode must be a small integer specifying the number of public + // keys required. + op := tokenizer.Opcode() + if !isSmallInt(op) || asSmallInt(op) != numPubKeys { + return multiSigDetails{} + } + + // There must only be a single opcode left unparsed which will be + // OP_CHECKMULTISIG per the check above. + if int32(len(tokenizer.Script()))-tokenizer.ByteIndex() != 1 { + return multiSigDetails{} + } + + return multiSigDetails{ + requiredSigs: requiredSigs, + numPubKeys: numPubKeys, + pubKeys: pubKeys, + valid: true, + } +} + +// isMultisigScript returns whether or not the passed script is a standard +// multisig script. +// +// NOTE: This function is only valid for version 0 scripts. It will always +// return false for other script versions. +func isMultisigScript(scriptVersion uint16, script []byte) bool { + // Since this is only checking the form of the script, don't extract the + // public keys to avoid the allocation. + details := extractMultisigScriptDetails(scriptVersion, script, false) + return details.valid +} + // IsMultisigScript returns whether or not the passed script is a standard // multisignature script. // @@ -257,16 +361,8 @@ func isMultiSig(pops []parsedOpcode) bool { // // The error is DEPRECATED and will be removed in the major version bump. func IsMultisigScript(script []byte) (bool, error) { - if len(script) == 0 || script == nil { - return false, nil - } - - pops, err := parseScript(script) - if err != nil { - return false, err - } - - return isMultiSig(pops), nil + const scriptVersion = 0 + return isMultisigScript(scriptVersion, script), nil } // isNullData returns true if the passed script is a null data transaction,