stdscript: Add ecdsa multisig creation script.

This adds a convenience method to create a standard version 0 ecdsa
multisignature redemption script that requires a threshold of signatures
from a set of public keys.

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:59 -05:00
parent 2b099ba339
commit bf5e5b4040
No known key found for this signature in database
GPG Key ID: B8904D9D9C93D1F2
4 changed files with 142 additions and 0 deletions

View File

@ -12,6 +12,14 @@ const (
// ErrUnsupportedScriptVersion indicates that a given script version is not
// supported.
ErrUnsupportedScriptVersion = ErrorKind("ErrUnsupportedScriptVersion")
// ErrTooManyRequiredSigs is returned from MultiSigScript when the
// specified number of required signatures is larger than the number of
// provided public keys.
ErrTooManyRequiredSigs = ErrorKind("ErrTooManyRequiredSigs")
// ErrPubKeyType is returned when a script contains invalid public keys.
ErrPubKeyType = ErrorKind("ErrPubKeyType")
)
// Error satisfies the error interface and prints human-readable errors.

View File

@ -17,6 +17,8 @@ func TestErrorKindStringer(t *testing.T) {
want string
}{
{ErrUnsupportedScriptVersion, "ErrUnsupportedScriptVersion"},
{ErrTooManyRequiredSigs, "ErrTooManyRequiredSigs"},
{ErrPubKeyType, "ErrPubKeyType"},
}
for i, test := range tests {
@ -73,6 +75,30 @@ func TestErrorKindIsAs(t *testing.T) {
target: ErrUnsupportedScriptVersion,
wantMatch: true,
wantAs: ErrUnsupportedScriptVersion,
}, {
name: "ErrTooManyRequiredSigs != ErrPubKeyType",
err: ErrTooManyRequiredSigs,
target: ErrPubKeyType,
wantMatch: false,
wantAs: ErrTooManyRequiredSigs,
}, {
name: "Error.ErrTooManyRequiredSigs != ErrPubKeyType",
err: makeError(ErrTooManyRequiredSigs, ""),
target: ErrPubKeyType,
wantMatch: false,
wantAs: ErrTooManyRequiredSigs,
}, {
name: "ErrTooManyRequiredSigs != Error.ErrPubKeyType",
err: ErrTooManyRequiredSigs,
target: makeError(ErrPubKeyType, ""),
wantMatch: false,
wantAs: ErrTooManyRequiredSigs,
}, {
name: "Error.ErrTooManyRequiredSigs != Error.ErrPubKeyType",
err: makeError(ErrTooManyRequiredSigs, ""),
target: makeError(ErrPubKeyType, ""),
wantMatch: false,
wantAs: ErrTooManyRequiredSigs,
}, {
name: "Error.ErrUnsupportedScriptVersion != io.EOF",
err: makeError(ErrUnsupportedScriptVersion, ""),

View File

@ -5,6 +5,8 @@
package stdscript
import (
"fmt"
"github.com/decred/dcrd/dcrec"
"github.com/decred/dcrd/txscript/v4"
)
@ -679,3 +681,36 @@ func DetermineScriptTypeV0(script []byte) ScriptType {
return STNonStandard
}
// MultiSigScriptV0 returns a valid version 0 script for a multisignature
// redemption where the specified threshold number of the keys in the given
// public keys are required to have signed the transaction for success.
//
// The provided public keys must be serialized in the compressed format or an
// error with kind ErrPubKeyType will be returned.
//
// An Error with kind ErrTooManyRequiredSigs will be returned if the threshold
// is larger than the number of keys provided.
func MultiSigScriptV0(threshold int, pubKeys ...[]byte) ([]byte, error) {
if len(pubKeys) < threshold {
str := fmt.Sprintf("unable to generate multisig script with %d "+
"required signatures when there are only %d public keys available",
threshold, len(pubKeys))
return nil, makeError(ErrTooManyRequiredSigs, str)
}
builder := txscript.NewScriptBuilder().AddInt64(int64(threshold))
for _, pubKey := range pubKeys {
if !txscript.IsStrictCompressedPubKeyEncoding(pubKey) {
str := fmt.Sprintf("unable to generate multisig script with "+
"unsupported public key %x", pubKey)
return nil, makeError(ErrPubKeyType, str)
}
builder.AddData(pubKey)
}
builder.AddInt64(int64(len(pubKeys)))
builder.AddOp(txscript.OP_CHECKMULTISIG)
return builder.Script()
}

View File

@ -7,6 +7,7 @@ package stdscript
import (
"bytes"
"encoding/hex"
"errors"
"fmt"
"reflect"
"testing"
@ -952,6 +953,78 @@ func TestMultiSigRedeemScriptFromScriptSigV0(t *testing.T) {
}
}
// TestMultiSigScriptV0 ensures the version 0 ECDSA multisignature script
// creation function returns the expected scripts and errors.
func TestMultiSigScriptV0(t *testing.T) {
t.Parallel()
// mainnet p2pk 13CG6SJ3yHUXo4Cr2RY4THLLJrNFuG3gUg
p2pkCompressedMain := hexToBytes("02192d74d0cb94344c9569c2e77901573d8d790" +
"3c3ebec3a957724895dca52c6b4")
p2pkCompressed2Main := hexToBytes("03b0bd634234abbb1ba1e986e884185c61cf43" +
"e001f9137f23c2c409273eb16e65")
p2pkUncompressedMain := hexToBytes("0411db93e1dcdb8a016b49840f8c53bc1eb68" +
"a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f" +
"9d4c03f999b8643f656b412a3")
tests := []struct {
name string
threshold int
pubKeys [][]byte
expected string
err error
}{{
name: "normal 1-of-2",
threshold: 1,
pubKeys: [][]byte{p2pkCompressedMain, p2pkCompressed2Main},
expected: fmt.Sprintf("1 DATA_%d 0x%x DATA_%d 0x%x 2 CHECKMULTISIG",
len(p2pkCompressedMain), p2pkCompressedMain,
len(p2pkCompressed2Main), p2pkCompressed2Main),
}, {
name: "normal 2-of-2",
threshold: 2,
pubKeys: [][]byte{p2pkCompressedMain, p2pkCompressed2Main},
expected: fmt.Sprintf("2 DATA_%d 0x%x DATA_%d 0x%x 2 CHECKMULTISIG",
len(p2pkCompressedMain), p2pkCompressedMain,
len(p2pkCompressed2Main), p2pkCompressed2Main),
}, {
name: "threshold 3 > 2 pubkeys",
pubKeys: [][]byte{p2pkCompressedMain, p2pkCompressed2Main},
threshold: 3,
expected: "",
err: ErrTooManyRequiredSigs,
}, {
name: "threshold 2 > 1 pubkey",
pubKeys: [][]byte{p2pkCompressedMain},
threshold: 2,
expected: "",
err: ErrTooManyRequiredSigs,
}, {
name: "reject uncompressed pubkeys",
pubKeys: [][]byte{p2pkUncompressedMain},
threshold: 1,
expected: "",
err: ErrPubKeyType,
}}
for _, test := range tests {
script, err := MultiSigScriptV0(test.threshold, test.pubKeys...)
if !errors.Is(err, test.err) {
t.Errorf("%q: unexpected error - got %v, want %v", test.name, err,
test.err)
continue
}
const scriptVer = 0
expected := mustParseShortForm(scriptVer, test.expected)
if !bytes.Equal(script, expected) {
t.Errorf("%q: unexpected result -- got: %x\nwant: %x", test.name,
script, expected)
continue
}
}
}
// TestExtractStakeSubmissionPubKeyHashV0 ensures that extracting a public key
// hash from a version 0 stake submission pay-to-pubkey-hash script works as
// intended for all of the version 0 test scripts.