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:
parent
299bdc987c
commit
9bcae49b9e
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user