From bfd0f4a492d7d79030f884198dbabc54fd1592ed Mon Sep 17 00:00:00 2001 From: Brian Stafford Date: Mon, 1 Nov 2021 13:42:22 -0500 Subject: [PATCH] txscript: add taproot script type Add the WitnessV1TaprootTy script class and return it from GetScriptClass / typeOfScript. Bump the btcutil dep to leverage new taproot address type. --- txscript/pkscript.go | 11 +++- txscript/pkscript_test.go | 8 +-- txscript/script.go | 5 +- txscript/script_test.go | 3 +- txscript/standard.go | 132 ++++++++++++++++++++++++-------------- 5 files changed, 103 insertions(+), 56 deletions(-) diff --git a/txscript/pkscript.go b/txscript/pkscript.go index d89c244e..4998f97b 100644 --- a/txscript/pkscript.go +++ b/txscript/pkscript.go @@ -7,9 +7,9 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/ecdsa" + "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/wire" - "github.com/btcsuite/btcd/btcutil" "golang.org/x/crypto/ripemd160" ) @@ -48,6 +48,9 @@ const ( // witnessV0ScriptHashLen is the length of a P2WSH script. witnessV0ScriptHashLen = 34 + // witnessV1TaprootLen is the length of a P2TR script. + witnessV1TaprootLen = 34 + // maxLen is the maximum script length supported by ParsePkScript. maxLen = witnessV0ScriptHashLen ) @@ -100,7 +103,7 @@ func ParsePkScript(pkScript []byte) (PkScript, error) { func isSupportedScriptType(class ScriptClass) bool { switch class { case PubKeyHashTy, WitnessV0PubKeyHashTy, ScriptHashTy, - WitnessV0ScriptHashTy: + WitnessV0ScriptHashTy, WitnessV1TaprootTy: return true default: return false @@ -133,6 +136,10 @@ func (s PkScript) Script() []byte { script = make([]byte, witnessV0ScriptHashLen) copy(script, s.script[:witnessV0ScriptHashLen]) + case WitnessV1TaprootTy: + script = make([]byte, witnessV1TaprootLen) + copy(script, s.script[:witnessV1TaprootLen]) + default: // Unsupported script type. return nil diff --git a/txscript/pkscript_test.go b/txscript/pkscript_test.go index 49e2db8a..c82ab04e 100644 --- a/txscript/pkscript_test.go +++ b/txscript/pkscript_test.go @@ -429,12 +429,12 @@ func TestComputePkScript(t *testing.T) { } if pkScript.Class() != test.class { - t.Fatalf("expected pkScript of type %v, got %v", - test.class, pkScript.Class()) + t.Fatalf("%s: expected pkScript of type %v, got %v", + test.name, test.class, pkScript.Class()) } if !bytes.Equal(pkScript.Script(), test.pkScript) { - t.Fatalf("expected pkScript=%x, got pkScript=%x", - test.pkScript, pkScript.Script()) + t.Fatalf("%s: expected pkScript=%x, got pkScript=%x", + test.name, test.pkScript, pkScript.Script()) } }) } diff --git a/txscript/script.go b/txscript/script.go index 696bfe2d..1c60b1c5 100644 --- a/txscript/script.go +++ b/txscript/script.go @@ -651,7 +651,7 @@ func countSigOpsV0(script []byte, precise bool) int { // covering 1 through 16 pubkeys, which means this will count any // more than that value (e.g. 17, 18 19) as the maximum number of // allowed pubkeys. This is, unfortunately, now part of - // the Bitcion consensus rules, due to historical + // the Bitcoin consensus rules, due to historical // reasons. This could be made more correct with a new // script version, however, ideally all multisignaure // operations in new script versions should move to @@ -799,6 +799,9 @@ func getWitnessSigOps(pkScript []byte, witness wire.TxWitness) int { witnessScript := witness[len(witness)-1] return countSigOpsV0(witnessScript, true) } + case 1: + // https://github.com/bitcoin/bitcoin/blob/368831371d97a642beb54b5c4eb6eb0fedaa16b4/src/script/interpreter.cpp#L2090 + return 0 } return 0 diff --git a/txscript/script_test.go b/txscript/script_test.go index 86f94d84..a90e1940 100644 --- a/txscript/script_test.go +++ b/txscript/script_test.go @@ -173,6 +173,7 @@ func TestGetPreciseSigOps(t *testing.T) { // nested p2sh, and invalid variants are counted properly. func TestGetWitnessSigOpCount(t *testing.T) { t.Parallel() + tests := []struct { name string @@ -182,7 +183,7 @@ func TestGetWitnessSigOpCount(t *testing.T) { numSigOps int }{ - // A regualr p2wkh witness program. The output being spent + // A regular p2wkh witness program. The output being spent // should only have a single sig-op counted. { name: "p2wkh", diff --git a/txscript/standard.go b/txscript/standard.go index 343a9b27..1488cd28 100644 --- a/txscript/standard.go +++ b/txscript/standard.go @@ -7,9 +7,9 @@ package txscript import ( "fmt" + "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/wire" - "github.com/btcsuite/btcd/btcutil" ) const ( @@ -58,6 +58,7 @@ const ( WitnessV0ScriptHashTy // Pay to witness script hash. MultiSigTy // Multi signature. NullDataTy // Empty data-only (provably prunable). + WitnessV1TaprootTy // Taproot output WitnessUnknownTy // Witness unknown ) @@ -72,6 +73,7 @@ var scriptClassToName = []string{ WitnessV0ScriptHashTy: "witness_v0_scripthash", MultiSigTy: "multisig", NullDataTy: "nulldata", + WitnessV1TaprootTy: "witness_v1_taproot", WitnessUnknownTy: "witness_unknown", } @@ -349,11 +351,11 @@ func IsMultisigSigScript(script []byte) bool { func extractWitnessPubKeyHash(script []byte) []byte { // A pay-to-witness-pubkey-hash script is of the form: // OP_0 OP_DATA_20 <20-byte-hash> - if len(script) == 22 && + if len(script) == witnessV0PubKeyHashLen && script[0] == OP_0 && script[1] == OP_DATA_20 { - return script[2:22] + return script[2:witnessV0PubKeyHashLen] } return nil @@ -365,13 +367,13 @@ func isWitnessPubKeyHashScript(script []byte) bool { return extractWitnessPubKeyHash(script) != nil } -// extractWitnessScriptHash extracts the witness script hash from the passed +// extractWitnessV0ScriptHash extracts the witness script hash from the passed // script if it is standard pay-to-witness-script-hash script. It will return // nil otherwise. -func extractWitnessScriptHash(script []byte) []byte { +func extractWitnessV0ScriptHash(script []byte) []byte { // A pay-to-witness-script-hash script is of the form: // OP_0 OP_DATA_32 <32-byte-hash> - if len(script) == 34 && + if len(script) == witnessV0ScriptHashLen && script[0] == OP_0 && script[1] == OP_DATA_32 { @@ -381,10 +383,26 @@ func extractWitnessScriptHash(script []byte) []byte { return nil } +// extractWitnessV1ScriptHash extracts the witness script hash from the passed +// script if it is standard pay-to-witness-script-hash script. It will return +// nil otherwise. +func extractWitnessV1ScriptHash(script []byte) []byte { + // A pay-to-witness-script-hash script is of the form: + // OP_1 OP_DATA_32 <32-byte-hash> + if len(script) == witnessV1TaprootLen && + script[0] == OP_1 && + script[1] == OP_DATA_32 { + + return script[2:34] + } + + return nil +} + // isWitnessScriptHashScript returns whether or not the passed script is a // standard pay-to-witness-script-hash script. func isWitnessScriptHashScript(script []byte) bool { - return extractWitnessScriptHash(script) != nil + return extractWitnessV0ScriptHash(script) != nil } // extractWitnessProgramInfo returns the version and program if the passed @@ -435,15 +453,28 @@ func extractWitnessProgramInfo(script []byte) (int, []byte, bool) { // smallest program is the witness version, followed by a data push of // 2 bytes. The largest allowed witness program has a data push of // 40-bytes. -// -// NOTE: This function is only valid for version 0 scripts. Since the function -// does not accept a script version, the results are undefined for other script -// versions. func isWitnessProgramScript(script []byte) bool { _, _, valid := extractWitnessProgramInfo(script) return valid } +// isWitnessTaprootScript returns true if the passed script is for a +// pay-to-witness-taproot output, false otherwise. +func isWitnessTaprootScript(script []byte) bool { + return extractWitnessV1ScriptHash(script) != nil +} + +// isAnnexedWitness returns true if the passed witness has a final push +// that is a witness annex. +func isAnnexedWitness(witness [][]byte) bool { + const OP_ANNEX = 0x50 + if len(witness) < 2 { + return false + } + lastElement := witness[len(witness)-1] + return len(lastElement) > 0 && lastElement[0] == OP_ANNEX +} + // isNullDataScript returns whether or not the passed script is a standard // null data script. // @@ -480,41 +511,49 @@ func isNullDataScript(scriptVersion uint16, script []byte) bool { } // scriptType returns the type of the script being inspected from the known -// standard types. -// -// NOTE: All scripts that are not version 0 are currently considered non -// standard. +// standard types. The version version should be 0 if the script is segwit v0 +// or prior, and 1 for segwit v1 (taproot) scripts. func typeOfScript(scriptVersion uint16, script []byte) ScriptClass { - if scriptVersion != 0 { - return NonStandardTy - } - - switch { - case isPubKeyScript(script): - return PubKeyTy - case isPubKeyHashScript(script): - return PubKeyHashTy - case isScriptHashScript(script): - return ScriptHashTy - case isWitnessPubKeyHashScript(script): - return WitnessV0PubKeyHashTy - case isWitnessScriptHashScript(script): - return WitnessV0ScriptHashTy - case isMultisigScript(scriptVersion, script): - return MultiSigTy - case isNullDataScript(scriptVersion, script): - return NullDataTy - default: - return NonStandardTy + switch scriptVersion { + case 0: + switch { + case isPubKeyScript(script): + return PubKeyTy + case isPubKeyHashScript(script): + return PubKeyHashTy + case isScriptHashScript(script): + return ScriptHashTy + case isWitnessPubKeyHashScript(script): + return WitnessV0PubKeyHashTy + case isWitnessScriptHashScript(script): + return WitnessV0ScriptHashTy + case isMultisigScript(scriptVersion, script): + return MultiSigTy + case isNullDataScript(scriptVersion, script): + return NullDataTy + } + case 1: + switch { + case isWitnessTaprootScript(script): + return WitnessV1TaprootTy + } } + return NonStandardTy } // GetScriptClass returns the class of the script passed. // // NonStandardTy will be returned when the script does not parse. func GetScriptClass(script []byte) ScriptClass { - const scriptVersion = 0 - return typeOfScript(scriptVersion, script) + const scriptVersionSegWit = 0 + classSegWit := typeOfScript(scriptVersionSegWit, script) + + if classSegWit != NonStandardTy { + return classSegWit + } + + const scriptVersionTaproot = 1 + return typeOfScript(scriptVersionTaproot, script) } // NewScriptClass returns the ScriptClass corresponding to the string name @@ -561,6 +600,10 @@ func expectedInputs(script []byte, class ScriptClass) int { // Not including script. That is handled by the caller. return 1 + case WitnessV1TaprootTy: + // Not including script. That is handled by the caller. + return 1 + case MultiSigTy: // Standard multisig has a push a small number for the number // of sigs and number of keys. Check the first push instruction @@ -848,10 +891,6 @@ func MultiSigScript(pubkeys []*btcutil.AddressPubKey, nrequired int) ([]byte, er // PushedData returns an array of byte slices containing any pushed data found // in the passed script. This includes OP_0, but not OP_1 - OP_16. -// -// NOTE: This function is only valid for version 0 scripts. Since the function -// does not accept a script version, the results are undefined for other script -// versions. func PushedData(script []byte) ([][]byte, error) { const scriptVersion = 0 @@ -900,11 +939,8 @@ func scriptHashToAddrs(hash []byte, params *chaincfg.Params) []btcutil.Address { // signatures associated with the passed PkScript. Note that it only works for // 'standard' transaction script types. Any data such as public keys which are // invalid are omitted from the results. -// -// NOTE: This function only attempts to identify version 0 scripts. The return -// value will indicate a nonstandard script type for other script versions along -// with an invalid script version error. -func ExtractPkScriptAddrs(pkScript []byte, chainParams *chaincfg.Params) (ScriptClass, []btcutil.Address, int, error) { +func ExtractPkScriptAddrs(pkScript []byte, + chainParams *chaincfg.Params) (ScriptClass, []btcutil.Address, int, error) { // Check for pay-to-pubkey-hash script. if hash := extractPubKeyHash(pkScript); hash != nil { @@ -956,7 +992,7 @@ func ExtractPkScriptAddrs(pkScript []byte, chainParams *chaincfg.Params) (Script return WitnessV0PubKeyHashTy, addrs, 1, nil } - if hash := extractWitnessScriptHash(pkScript); hash != nil { + if hash := extractWitnessV0ScriptHash(pkScript); hash != nil { var addrs []btcutil.Address addr, err := btcutil.NewAddressWitnessScriptHash(hash, chainParams) if err == nil {