diff --git a/internal/staging/stdscript/script.go b/internal/staging/stdscript/script.go index 65654f01..0bb2b80c 100644 --- a/internal/staging/stdscript/script.go +++ b/internal/staging/stdscript/script.go @@ -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 diff --git a/internal/staging/stdscript/script_test.go b/internal/staging/stdscript/script_test.go index 9efef752..725910ce 100644 --- a/internal/staging/stdscript/script_test.go +++ b/internal/staging/stdscript/script_test.go @@ -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) } } diff --git a/internal/staging/stdscript/scriptv0.go b/internal/staging/stdscript/scriptv0.go index ee21b0e7..b1810814 100644 --- a/internal/staging/stdscript/scriptv0.go +++ b/internal/staging/stdscript/scriptv0.go @@ -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. diff --git a/internal/staging/stdscript/scriptv0_test.go b/internal/staging/stdscript/scriptv0_test.go index 535e7e54..7a7a9b07 100644 --- a/internal/staging/stdscript/scriptv0_test.go +++ b/internal/staging/stdscript/scriptv0_test.go @@ -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 + } + } +}