Skip to content

Commit

Permalink
add taproot script type
Browse files Browse the repository at this point in the history
Add the WitnessV1TaprootTy script class and return it from GetScriptClass
/ typeOfScript.

Fix ComputePkScript, which was returning incorrect results for taproot
(an probably other) scripts. ComputePkScript is now essentially useless
for both v0 and v1 output witnesses.

Bump the btcutil dep to leverage new taproot address type.
  • Loading branch information
buck54321 committed Nov 1, 2021
1 parent 0ec4bdc commit b9a34e5
Show file tree
Hide file tree
Showing 8 changed files with 170 additions and 65 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module github.com/btcsuite/btcd

require (
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f
github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce
github.com/btcsuite/btcutil v1.0.3-0.20210929233259-9cdf59f60c51
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd
github.com/btcsuite/goleveldb v1.0.0
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d h1:yJzD/yFppdVCf6
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce h1:YtWJF7RHm2pYCvA5t0RPmAaLUhREsKuKd+SLhxFbFeQ=
github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce/go.mod h1:0DVlHczLPewLcPGEIeUEzfOJhqGPQ0mJJRDBtD307+o=
github.com/btcsuite/btcutil v1.0.3-0.20210929233259-9cdf59f60c51 h1:6XGSs4BMDRlNR9k+tpr1g2S6ZfQhKQl/Xr276yAEfP0=
github.com/btcsuite/btcutil v1.0.3-0.20210929233259-9cdf59f60c51/go.mod h1:0DVlHczLPewLcPGEIeUEzfOJhqGPQ0mJJRDBtD307+o=
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd h1:R/opQEbFEy9JGkIguV40SvRY1uliPX8ifOvi6ICsFCw=
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd h1:qdGvebPBDuYDPGi1WCPjy1tGyMpmDK8IEapSsszn7HE=
Expand Down
48 changes: 22 additions & 26 deletions txscript/pkscript.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,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
)
Expand Down Expand Up @@ -99,7 +102,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
Expand Down Expand Up @@ -132,6 +135,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
Expand Down Expand Up @@ -231,35 +238,24 @@ func computeNonWitnessPkScript(sigScript []byte) (PkScript, error) {

// computeWitnessPkScript computes the script of an output by looking at the
// spending input's witness.
// IMPORTANT: With the addition of taproot, we can no longer say for certain
// what kind of script the witness is in most cases. The only case in which we
// can say for sure is when the witness data has an annex as the last push. In
// that case, we can identify the script type, but we lack the ability to
// reconstruct the script itself.
func computeWitnessPkScript(witness wire.TxWitness) (PkScript, error) {
// We'll use the last item of the witness stack to determine the proper
// witness type.
lastWitnessItem := witness[len(witness)-1]

var pkScript PkScript
switch {
// If the witness stack has a size of 2 and its last item is a
// compressed public key, then this is a P2WPKH witness.
case len(witness) == 2 && len(lastWitnessItem) == compressedPubKeyLen:
pubKeyHash := hash160(lastWitnessItem)
script, err := payToWitnessPubKeyHashScript(pubKeyHash)
if err != nil {
return pkScript, err
}

pkScript.class = WitnessV0PubKeyHashTy
copy(pkScript.script[:], script)

// For any other witnesses, we'll assume it's a P2WSH witness.
// If the last push starts with the annex flag, this is a taproot spend.
// We can set the script class, but we can't say what the pubkey script
// looks like with just the witness data.
case isAnnexedWitness(witness):
pkScript.class = WitnessV1TaprootTy

// For any other witnesses, we can't say for certain what type it is or what
// the pubkey script will be.
default:
scriptHash := sha256.Sum256(lastWitnessItem)
script, err := payToWitnessScriptHashScript(scriptHash[:])
if err != nil {
return pkScript, err
}

pkScript.class = WitnessV0ScriptHashTy
copy(pkScript.script[:], script)
pkScript.class = WitnessUnknownTy
}

return pkScript, nil
Expand Down
40 changes: 12 additions & 28 deletions txscript/pkscript_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ func TestComputePkScript(t *testing.T) {
pkScript: nil,
},
{
name: "P2WSH witness",
name: "witness unknown",
sigScript: nil,
witness: [][]byte{
{},
Expand All @@ -348,20 +348,14 @@ func TestComputePkScript(t *testing.T) {
0x42, 0x59, 0x90, 0xac, 0xac,
},
},
class: WitnessV0ScriptHashTy,
pkScript: []byte{
// OP_0
0x00,
// OP_DATA_32
0x20,
// <32-byte script hash>
0x01, 0xd5, 0xd9, 0x2e, 0xff, 0xa6, 0xff, 0xba,
0x3e, 0xfa, 0x37, 0x9f, 0x98, 0x30, 0xd0, 0xf7,
0x56, 0x18, 0xb1, 0x33, 0x93, 0x82, 0x71, 0x52,
0xd2, 0x6e, 0x43, 0x09, 0x00, 0x0e, 0x88, 0xb1,
},
// We can't say for sure what kind of pubkey script this is. Before
// taproot, it would have been p2sh
class: WitnessUnknownTy,
},
{
// Before taproot, we could tell this script type based on its
// structure. But now this particular structure matches rules for
// both witness_v0_keyhash and witness_v1_taproot.
name: "P2WPKH witness",
sigScript: nil,
witness: [][]byte{
Expand All @@ -378,17 +372,7 @@ func TestComputePkScript(t *testing.T) {
0x59, 0x90, 0xac,
},
},
class: WitnessV0PubKeyHashTy,
pkScript: []byte{
// OP_0
0x00,
// OP_DATA_20
0x14,
// <20-byte pubkey hash>
0x1d, 0x7c, 0xd6, 0xc7, 0x5c, 0x2e, 0x86, 0xf4,
0xcb, 0xf9, 0x8e, 0xae, 0xd2, 0x21, 0xb3, 0x0b,
0xd9, 0xa0, 0xb9, 0x28,
},
class: WitnessUnknownTy,
},
// Invalid v0 P2WPKH - same as above but missing a byte on the
// public key.
Expand Down Expand Up @@ -429,12 +413,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())
}
})
}
Expand Down
37 changes: 35 additions & 2 deletions txscript/script.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,33 @@ func IsPayToScriptHash(script []byte) bool {
return isScriptHash(pops)
}

// isWitnessScriptHash returns true if the passed script is a
// pay-to-witness-script-hash transaction, false otherwise.
// isWitnessScriptHash returns true if the passed script is for a
// pay-to-witness-script-hash output, false otherwise.
func isWitnessScriptHash(pops []parsedOpcode) bool {
return len(pops) == 2 &&
pops[0].opcode.value == OP_0 &&
pops[1].opcode.value == OP_DATA_32
}

// isWitnessTaproot returns true if the passed script is for a
// pay-to-witness-taproot output, false otherwise.
func isWitnessTaproot(pops []parsedOpcode) bool {
return len(pops) == 2 &&
pops[0].opcode.value == OP_1 &&
pops[1].opcode.value == OP_DATA_32
}

// 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
}

// IsPayToWitnessScriptHash returns true if the is in the standard
// pay-to-witness-script-hash (P2WSH) format, false otherwise.
func IsPayToWitnessScriptHash(script []byte) bool {
Expand Down Expand Up @@ -814,6 +833,20 @@ func getWitnessSigOps(pkScript []byte, witness wire.TxWitness) int {
pops, _ := parseScript(witnessScript)
return getSigOpCount(pops, true)
}
case 1:
deAnnexedWitness := witness
if isAnnexedWitness(witness) {
deAnnexedWitness = witness[:len(witness)-1]
}
switch {
case len(deAnnexedWitness) == 1:
return 1
case len(deAnnexedWitness) >= 2:
// Last element is the control block. Second to last is the script.
witnessScript := deAnnexedWitness[len(deAnnexedWitness)-2]
pops, _ := parseScript(witnessScript)
return getSigOpCount(pops, true)
}
}

return 0
Expand Down
33 changes: 32 additions & 1 deletion txscript/script_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3850,6 +3850,7 @@ func TestGetPreciseSigOps(t *testing.T) {
// nested p2sh, and invalid variants are counted properly.
func TestGetWitnessSigOpCount(t *testing.T) {
t.Parallel()
const OP_ANNEX = 0x50
tests := []struct {
name string

Expand All @@ -3859,7 +3860,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",
Expand Down Expand Up @@ -3921,6 +3922,36 @@ func TestGetWitnessSigOpCount(t *testing.T) {
" EQUALVERIFY CHECKSIG DATA_20 0x91"),
},
},

{
name: "taproot key-path spend",
numSigOps: 1,
pkScript: hexToBytes("512058ce9d16c3384731a1727e512530620d031ee7b12f42aade70c9f976a905a74b"),
witness: wire.TxWitness{
hexToBytes("DCBA"),
},
},
{
name: "taproot script-path spend without annex",
numSigOps: 2,
pkScript: hexToBytes("512058ce9d16c3384731a1727e512530620d031ee7b12f42aade70c9f976a905a74b"),
witness: wire.TxWitness{
[]byte("signature"),
mustParseShortForm("CHECKSIG CHECKSIG"),
[]byte("control block"),
},
},
{
name: "taproot script-path spend with annex",
numSigOps: 1,
pkScript: hexToBytes("512058ce9d16c3384731a1727e512530620d031ee7b12f42aade70c9f976a905a74b"),
witness: wire.TxWitness{
[]byte("stuff"),
mustParseShortForm("CHECKSIG"),
[]byte("control block"),
[]byte{OP_ANNEX},
},
},
}

for _, test := range tests {
Expand Down
57 changes: 50 additions & 7 deletions txscript/standard.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
)

Expand All @@ -72,6 +73,7 @@ var scriptClassToName = []string{
WitnessV0ScriptHashTy: "witness_v0_scripthash",
MultiSigTy: "multisig",
NullDataTy: "nulldata",
WitnessV1TaprootTy: "witness_v1_taproot",
WitnessUnknownTy: "witness_unknown",
}

Expand Down Expand Up @@ -161,19 +163,22 @@ func isNullData(pops []parsedOpcode) bool {
// scriptType returns the type of the script being inspected from the known
// standard types.
func typeOfScript(pops []parsedOpcode) ScriptClass {
if isPubkey(pops) {
switch {
case isPubkey(pops):
return PubKeyTy
} else if isPubkeyHash(pops) {
case isPubkeyHash(pops):
return PubKeyHashTy
} else if isWitnessPubKeyHash(pops) {
case isWitnessPubKeyHash(pops):
return WitnessV0PubKeyHashTy
} else if isScriptHash(pops) {
case isScriptHash(pops):
return ScriptHashTy
} else if isWitnessScriptHash(pops) {
case isWitnessScriptHash(pops):
return WitnessV0ScriptHashTy
} else if isMultiSig(pops) {
case isWitnessTaproot(pops):
return WitnessV1TaprootTy
case isMultiSig(pops):
return MultiSigTy
} else if isNullData(pops) {
case isNullData(pops):
return NullDataTy
}
return NonStandardTy
Expand Down Expand Up @@ -230,6 +235,10 @@ func expectedInputs(pops []parsedOpcode, 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
Expand Down Expand Up @@ -363,6 +372,32 @@ func CalcScriptInfo(sigScript, pkScript []byte, witness wire.TxWitness,
si.SigOps = GetWitnessSigOpCount(sigScript, pkScript, witness)
si.NumInputs = len(witness)

// If segwit is active, and this is a p2tr output, then how we parse script
// info depends on the witness size after de-annexing.
case si.PkScriptClass == WitnessV1TaprootTy && segwit:
deAnnexedWitness := witness
if isAnnexedWitness(witness) {
deAnnexedWitness = witness[:len(witness)-1]
}
switch len(deAnnexedWitness) {
case 1:
case 0:
return nil, fmt.Errorf("length 0 v1 witness output not allowed")
default:
witnessScript := deAnnexedWitness[len(deAnnexedWitness)-2]
pops, _ := parseScript(witnessScript)

shInputs := expectedInputs(pops, typeOfScript(pops))
if shInputs == -1 {
si.ExpectedInputs = -1
} else {
si.ExpectedInputs += shInputs
}
}

si.SigOps = GetWitnessSigOpCount(sigScript, pkScript, witness)
si.NumInputs = len(witness)

default:
si.SigOps = getSigOpCount(pkPops, true)

Expand Down Expand Up @@ -611,6 +646,14 @@ func ExtractPkScriptAddrs(pkScript []byte, chainParams *chaincfg.Params) (Script
addrs = append(addrs, addr)
}

case WitnessV1TaprootTy:
requiredSigs = 1
addr, err := btcutil.NewAddressTaproot(pops[1].data,
chainParams)
if err == nil {
addrs = append(addrs, addr)
}

case MultiSigTy:
// A multi-signature script is of the form:
// <numsigs> <pubkey> <pubkey> <pubkey>... <numpubkeys> OP_CHECKMULTISIG
Expand Down

0 comments on commit b9a34e5

Please sign in to comment.