stdscript: Add v0 ecdsa multisig redeem support.

This adds support for identifying if a signature script likely consists
of a pay-to-script-hash multi-signature redeem script for an original
pay-to-script-hash script as a convenience method for consumers of the
package.

Note that this is different in that it applies to the signature script
as opposed to the public key script and consequently there is no new
script type introduced nor does the overall script determination detect
it.  This is the case because 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.

Also of note is that the same change in semantics regarding compressed
public keys and the required number of signatures as described in the
commit which introduced support for detecting version 0 ecdsa
multisignature scripts applies here.

Full test coverage is included.

This is part of a series of commits to fully implement the stdscript
package.
This commit is contained in:
Dave Collins 2021-05-30 02:55:34 -05:00
parent 299bdc987c
commit 9bcae49b9e
No known key found for this signature in database
GPG Key ID: B8904D9D9C93D1F2
4 changed files with 157 additions and 0 deletions

View File

@ -217,6 +217,26 @@ func IsMultiSigScript(scriptVersion uint16, script []byte) bool {
return false
}
// 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: Version 0 scripts are the only currently supported version. It will
// always return false for other script versions.
func IsMultiSigSigScript(scriptVersion uint16, script []byte) bool {
switch scriptVersion {
case 0:
return IsMultiSigSigScriptV0(script)
}
return false
}
// DetermineScriptType returns the type of the script passed.
//
// NOTE: Version 0 scripts are the only currently supported version. It will

View File

@ -126,6 +126,10 @@ func TestDetermineScriptType(t *testing.T) {
t.Helper()
testIsXInternal(fn, false, wantType)
}
testIsSigX := func(fn isXFn, wantType ScriptType) {
t.Helper()
testIsXInternal(fn, true, wantType)
}
// Ensure the individual determination methods produce the expected
// results.
@ -137,5 +141,10 @@ func TestDetermineScriptType(t *testing.T) {
testIsX(IsPubKeyHashSchnorrSecp256k1Script, STPubKeyHashSchnorrSecp256k1)
testIsX(IsScriptHashScript, STScriptHash)
testIsX(IsMultiSigScript, STMultiSig)
// Ensure the special case of determining if a signature script appears
// to be a signature script which consists of a pay-to-script-hash
// multi-signature redeem script.
testIsSigX(IsMultiSigSigScript, STMultiSig)
}
}

View File

@ -312,6 +312,64 @@ func IsMultiSigScriptV0(script []byte) bool {
return details.Valid
}
// finalOpcodeDataV0 returns the data associated with the final opcode in the
// passed version 0 script. It will return nil if the script fails to parse.
func finalOpcodeDataV0(script []byte) []byte {
// Avoid unnecessary work.
if len(script) == 0 {
return nil
}
var data []byte
const scriptVersion = 0
tokenizer := txscript.MakeScriptTokenizer(scriptVersion, script)
for tokenizer.Next() {
data = tokenizer.Data()
}
if tokenizer.Err() != nil {
return nil
}
return data
}
// IsMultiSigSigScriptV0 returns whether or not the passed script appears to be
// a version 0 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.
func IsMultiSigSigScriptV0(script []byte) bool {
// 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] != txscript.OP_CHECKMULTISIG {
return false
}
// 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 := finalOpcodeDataV0(script)
if possibleRedeemScript == nil {
return false
}
// Finally, return if that possible redeem script is a multisig script.
return IsMultiSigScriptV0(possibleRedeemScript)
}
// MultiSigRedeemScriptFromScriptSigV0 attempts to extract a multi-signature
// redeem script from a version 0 P2SH-redeeming input. The script is expected
// to already have been checked to be a version 0 multisignature script prior to
// calling this function. The results are undefined for other script types.
func MultiSigRedeemScriptFromScriptSigV0(script []byte) []byte {
// The redeemScript is always the last item on the stack of the script sig.
return finalOpcodeDataV0(script)
}
// DetermineScriptTypeV0 returns the type of the passed version 0 script from
// the known standard types. This includes both types that are required by
// consensus as well as those which are not.

View File

@ -412,6 +412,42 @@ var scriptV0Tests = func() []scriptTest {
},
Valid: true,
},
}, {
// ---------------------------------------------------------------------
// Negative ECDSA multisig secp256k1 redeem script tests.
// ---------------------------------------------------------------------
name: "almost v0 multisig redeem script -- no req sigs",
script: p("DATA_3 0 0 CHECKMULTISIG"),
isSig: true,
wantType: STNonStandard,
}, {
name: "almost v0 multisig 1-of-1 redeem script -- trailing opcode",
script: p("DATA_38 1 DATA_33 0x%s 1 CHECKMULTISIG TRUE", pkCE),
isSig: true,
wantType: STNonStandard,
}, {
name: "almost v0 multisig 1-of-1 redeem script -- parse error",
script: p("DATA_38 1 DATA_33 0x%s 1 CHECKMULTISIG", pkCE),
isSig: true,
wantType: STNonStandard,
}, {
// ---------------------------------------------------------------------
// Positive ECDSA multisig secp256k1 redeem script tests.
// ---------------------------------------------------------------------
name: "v0 multisig 1-of-1 compressed pubkey redeem script",
script: p("DATA_37 1 DATA_33 0x%s 1 CHECKMULTISIG", pkCE),
isSig: true,
wantType: STMultiSig,
wantData: p("1 DATA_33 0x%s 1 CHECKMULTISIG", pkCE),
}, {
name: "v0 multisig 1-of-2 compressed pubkeys redeem script",
script: p("DATA_71 1 DATA_33 0x%s DATA_33 0x%s 2 CHECKMULTISIG", pkCE,
pkCE2),
isSig: true,
wantType: STMultiSig,
wantData: p("1 DATA_33 0x%s DATA_33 0x%s 2 CHECKMULTISIG", pkCE, pkCE2),
}}
}()
@ -610,3 +646,37 @@ func TestExtractMultiSigScriptDetailsV0(t *testing.T) {
}
}
}
// TestMultiSigRedeemScriptFromScriptSigV0 ensures extracting a version 0 ECDSA
// multisignature redeem script returns the expected scripts for the version 0
// test scripts that are actually multisignature redeem scripts.
func TestMultiSigRedeemScriptFromScriptSigV0(t *testing.T) {
// Add an additional test to ensure empty redeem scripts are handled
// correctly.
tests := []scriptTest{{
name: "v0 empty script",
script: nil,
isSig: true,
wantData: []byte(nil),
}}
for _, test := range scriptV0Tests {
// Per the documentation, unlike most of the extraction funcs, the
// multisig redeem script extraction function is only valid for scripts
// that have already been determined to be of the correct form.
if test.wantType != STMultiSig || !test.isSig {
continue
}
tests = append(tests, test)
}
for _, test := range tests {
want := asByteSlice(t, test)
got := MultiSigRedeemScriptFromScriptSigV0(test.script)
if !bytes.Equal(got, want) {
t.Errorf("%q: unexpected redeem script -- got %x, want %x",
test.name, got, want)
continue
}
}
}