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%
This commit is contained in:
Dave Collins 2019-03-13 01:11:16 -05:00 committed by Olaoluwa Osuntokun
parent 4d31d1599d
commit 0eaae2663b
No known key found for this signature in database
GPG key ID: 3BBD59E99B280306
2 changed files with 127 additions and 10 deletions

View file

@ -580,6 +580,27 @@ func (vm *Engine) checkHashTypeEncoding(hashType SigHashType) error {
return nil 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 // checkPubKeyEncoding returns whether or not the passed public key adheres to
// the strict encoding requirements if enabled. // the strict encoding requirements if enabled.
func (vm *Engine) checkPubKeyEncoding(pubKey []byte) error { func (vm *Engine) checkPubKeyEncoding(pubKey []byte) error {

View file

@ -216,6 +216,8 @@ func isPubkeyHash(pops []parsedOpcode) bool {
// isMultiSig returns true if the passed script is a multisig transaction, false // isMultiSig returns true if the passed script is a multisig transaction, false
// otherwise. // otherwise.
//
// DEPECATED. Use isMultisigScript or extractMultisigScriptDetails instead.
func isMultiSig(pops []parsedOpcode) bool { func isMultiSig(pops []parsedOpcode) bool {
// The absolute minimum is 1 pubkey: // The absolute minimum is 1 pubkey:
// OP_0/OP_1-16 <pubkey> OP_1 OP_CHECKMULTISIG // OP_0/OP_1-16 <pubkey> OP_1 OP_CHECKMULTISIG
@ -248,6 +250,108 @@ func isMultiSig(pops []parsedOpcode) bool {
return true 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 // IsMultisigScript returns whether or not the passed script is a standard
// multisignature script. // multisignature script.
// //
@ -257,16 +361,8 @@ func isMultiSig(pops []parsedOpcode) bool {
// //
// The error is DEPRECATED and will be removed in the major version bump. // The error is DEPRECATED and will be removed in the major version bump.
func IsMultisigScript(script []byte) (bool, error) { func IsMultisigScript(script []byte) (bool, error) {
if len(script) == 0 || script == nil { const scriptVersion = 0
return false, nil return isMultisigScript(scriptVersion, script), nil
}
pops, err := parseScript(script)
if err != nil {
return false, err
}
return isMultiSig(pops), nil
} }
// isNullData returns true if the passed script is a null data transaction, // isNullData returns true if the passed script is a null data transaction,