From 08e2d88e8a25544899e62e509d826b096f2e0190 Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Sun, 30 May 2021 02:55:38 -0500 Subject: [PATCH] stdscript: Add v0 stake sub p2pkh support. This adds support for detecting version 0 stake submission pay-to-pubkey-hash scripts as standard and extracting the associated public key hash. Full test coverage is included. This is part of a series of commits to fully implement the stdscript package. --- internal/staging/stdscript/script.go | 24 +++++++++++++ internal/staging/stdscript/script_test.go | 2 ++ internal/staging/stdscript/scriptv0.go | 35 ++++++++++++++++++ internal/staging/stdscript/scriptv0_test.go | 39 +++++++++++++++++++++ 4 files changed, 100 insertions(+) diff --git a/internal/staging/stdscript/script.go b/internal/staging/stdscript/script.go index fe9742db..4f0a5c70 100644 --- a/internal/staging/stdscript/script.go +++ b/internal/staging/stdscript/script.go @@ -81,6 +81,15 @@ const ( // prunable. STNullData + // STStakeSubmissionPubKeyHash identifies a script that is only valid when + // used as part of a ticket purchase transaction in the staking system and + // is used for imposing voting rights. + // + // It imposes an encumbrance that requires a secp256k1 public key that + // hashes to a specific value along with a valid ECDSA signature for that + // public key. + STStakeSubmissionPubKeyHash + // numScriptTypes is the maximum script type number used in tests. This // entry MUST be the last entry in the enum. numScriptTypes @@ -99,6 +108,7 @@ var scriptTypeToName = []string{ STScriptHash: "scripthash", STMultiSig: "multisig", STNullData: "nulldata", + STStakeSubmissionPubKeyHash: "stakesubmission-pubkeyhash", } // String returns the ScriptType as a human-readable name. @@ -256,6 +266,20 @@ func IsNullDataScript(scriptVersion uint16, script []byte) bool { return false } +// IsStakeSubmissionPubKeyHashScript returns whether or not the passed script is +// a standard stake submission pay-to-pubkey-hash script. +// +// NOTE: Version 0 scripts are the only currently supported version. It will +// always return false for other script versions. +func IsStakeSubmissionPubKeyHashScript(scriptVersion uint16, script []byte) bool { + switch scriptVersion { + case 0: + return IsStakeSubmissionPubKeyHashScriptV0(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 04b8f338..503549fe 100644 --- a/internal/staging/stdscript/script_test.go +++ b/internal/staging/stdscript/script_test.go @@ -26,6 +26,7 @@ func TestScriptTypeStringer(t *testing.T) { {STScriptHash, "scripthash"}, {STMultiSig, "multisig"}, {STNullData, "nulldata"}, + {STStakeSubmissionPubKeyHash, "stakesubmission-pubkeyhash"}, {0xff, "invalid"}, } @@ -143,6 +144,7 @@ func TestDetermineScriptType(t *testing.T) { testIsX(IsScriptHashScript, STScriptHash) testIsX(IsMultiSigScript, STMultiSig) testIsX(IsNullDataScript, STNullData) + testIsX(IsStakeSubmissionPubKeyHashScript, STStakeSubmissionPubKeyHash) // Ensure the special case of determining if a signature script appears // to be a signature script which consists of a pay-to-script-hash diff --git a/internal/staging/stdscript/scriptv0.go b/internal/staging/stdscript/scriptv0.go index 00cc8617..d04a15a9 100644 --- a/internal/staging/stdscript/scriptv0.go +++ b/internal/staging/stdscript/scriptv0.go @@ -428,6 +428,39 @@ func IsNullDataScriptV0(script []byte) bool { isCanonicalPushV0(tokenizer.Opcode(), tokenizer.Data()) } +// extractStakePubKeyHashV0 extracts the public key hash from the passed script +// if it is a standard version 0 stake-tagged pay-to-pubkey-hash script with the +// provided stake opcode. It will return nil otherwise. +func extractStakePubKeyHashV0(script []byte, stakeOpcode byte) []byte { + // A stake-tagged pay-to-pubkey-hash is of the form: + // + + // The script can't possibly be a stake-tagged pay-to-pubkey-hash if it + // doesn't start with the given stake opcode. Fail fast to avoid more work + // below. + if len(script) < 1 || script[0] != stakeOpcode { + return nil + } + + return ExtractPubKeyHashV0(script[1:]) +} + +// ExtractStakeSubmissionPubKeyHashV0 extracts the public key hash from +// the passed script if it is a standard version 0 stake submission +// pay-to-pubkey-hash script. It will return nil otherwise. +func ExtractStakeSubmissionPubKeyHashV0(script []byte) []byte { + // A stake submission pay-to-pubkey-hash script is of the form: + // OP_SSTX + const stakeOpcode = txscript.OP_SSTX + return extractStakePubKeyHashV0(script, stakeOpcode) +} + +// IsStakeSubmissionPubKeyHashScriptV0 returns whether or not the passed script +// is a standard version 0 stake submission pay-to-pubkey-hash script. +func IsStakeSubmissionPubKeyHashScriptV0(script []byte) bool { + return ExtractStakeSubmissionPubKeyHashV0(script) != nil +} + // 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. @@ -453,6 +486,8 @@ func DetermineScriptTypeV0(script []byte) ScriptType { return STMultiSig case IsNullDataScriptV0(script): return STNullData + case IsStakeSubmissionPubKeyHashScriptV0(script): + return STStakeSubmissionPubKeyHash } return STNonStandard diff --git a/internal/staging/stdscript/scriptv0_test.go b/internal/staging/stdscript/scriptv0_test.go index a67e6b08..92aa1ebd 100644 --- a/internal/staging/stdscript/scriptv0_test.go +++ b/internal/staging/stdscript/scriptv0_test.go @@ -520,6 +520,24 @@ var scriptV0Tests = func() []scriptTest { name: "v0 nulldata max standard push", script: p("RETURN PUSHDATA2 0x0001 0x01{256}"), wantType: STNullData, + }, { + // --------------------------------------------------------------------- + // Negative stake submission P2PKH tests. + // --------------------------------------------------------------------- + + name: "almost v0 stake sub p2pkh-ecdsa-secp256k1 -- wrong hash length", + script: p("SSTX DUP HASH160 DATA_21 0x00%s EQUALVERIFY CHECKSIG", + h160CE), + wantType: STNonStandard, + }, { + // --------------------------------------------------------------------- + // Positive stake submission P2PKH tests. + // --------------------------------------------------------------------- + + name: "v0 stake submission p2pkh-ecdsa-secp256k1", + script: p("SSTX DUP HASH160 DATA_20 0x%s EQUALVERIFY CHECKSIG", h160CE), + wantType: STStakeSubmissionPubKeyHash, + wantData: hexToBytes(h160CE), }} }() @@ -752,3 +770,24 @@ func TestMultiSigRedeemScriptFromScriptSigV0(t *testing.T) { } } } + +// 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. +func TestExtractStakeSubmissionPubKeyHashV0(t *testing.T) { + for _, test := range scriptV0Tests { + // Determine the expected data based on the expected script type and + // data specified in the test. + var want []byte + if test.wantType == STStakeSubmissionPubKeyHash { + want = asByteSlice(t, test) + } + + got := ExtractStakeSubmissionPubKeyHashV0(test.script) + if !bytes.Equal(got, want) { + t.Errorf("%q: unexpected pubkey hash -- got %x, want %x", test.name, + got, want) + continue + } + } +}