diff --git a/internal/staging/stdscript/error.go b/internal/staging/stdscript/error.go index 5cd350f8..70221bd8 100644 --- a/internal/staging/stdscript/error.go +++ b/internal/staging/stdscript/error.go @@ -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. diff --git a/internal/staging/stdscript/error_test.go b/internal/staging/stdscript/error_test.go index 5285ed0a..9658a0a7 100644 --- a/internal/staging/stdscript/error_test.go +++ b/internal/staging/stdscript/error_test.go @@ -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, ""), diff --git a/internal/staging/stdscript/scriptv0.go b/internal/staging/stdscript/scriptv0.go index 3115e975..cf090f48 100644 --- a/internal/staging/stdscript/scriptv0.go +++ b/internal/staging/stdscript/scriptv0.go @@ -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() +} diff --git a/internal/staging/stdscript/scriptv0_test.go b/internal/staging/stdscript/scriptv0_test.go index 35432a4c..71aa41b3 100644 --- a/internal/staging/stdscript/scriptv0_test.go +++ b/internal/staging/stdscript/scriptv0_test.go @@ -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.