txscript: add new RawTxInTapscriptSignature to generate tapsript sigs

In this commit, we add a new function `RawTxInTapscriptSignature` that
will be used to generate signatures in the _tapscript_ context. Note
that this differs from top-level taproot as a distinct sighash is used,
and we _always_ accept a root hash to perform the proper tweak.
This commit is contained in:
Olaoluwa Osuntokun 2022-01-06 18:14:49 -08:00
parent 37f8c8ba0a
commit 6fc4199ee4
No known key found for this signature in database
GPG key ID: 3BBD59E99B280306
9 changed files with 272 additions and 14 deletions

View file

@ -27,5 +27,11 @@ func NewPrivateKey() (*PrivateKey, error) {
return secp.GeneratePrivateKey()
}
// PrivKeyFromScalar instantiates a new private key from a scalar encoded as a
// big integer.
func PrivKeyFromScalar(key *ModNScalar) *PrivateKey {
return &PrivateKey{Key: *key}
}
// PrivKeyBytesLen defines the length in bytes of a serialized private key.
const PrivKeyBytesLen = 32

View file

@ -11,6 +11,7 @@ github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufo
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0=
@ -43,6 +44,11 @@ github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY=
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@ -79,3 +85,5 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -12,6 +12,7 @@ require (
require (
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect
github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 // indirect
)

View file

@ -8,6 +8,7 @@ github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufo
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0=
@ -38,6 +39,11 @@ github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
@ -73,3 +79,5 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

5
go.mod
View file

@ -9,9 +9,11 @@ require (
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792
github.com/btcsuite/winsvc v1.0.0
github.com/davecgh/go-spew v1.1.1
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1
github.com/decred/dcrd/lru v1.0.0
github.com/jessevdk/go-flags v1.4.0
github.com/jrick/logrotate v1.0.0
github.com/stretchr/testify v1.7.0
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
)
@ -19,9 +21,10 @@ require (
require (
github.com/aead/siphash v1.0.1 // indirect
github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
)
replace github.com/btcsuite/btcd/btcutil => ./btcutil

9
go.sum
View file

@ -15,6 +15,7 @@ github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
github.com/btcsuite/winsvc v1.0.0 h1:J9B4L7e3oqhXOcm+2IuNApwzQec85lE+QaikUcCs+dk=
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0=
@ -55,6 +56,11 @@ github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY=
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@ -91,6 +97,7 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
@ -99,3 +106,5 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -63,14 +63,12 @@ func WitnessSignature(tx *wire.MsgTx, sigHashes *TxSigHashes, idx int, amt int64
return wire.TxWitness{sig, pkData}, nil
}
// RawTxInWitnessSignature returns a valid schnorr signature required to
// perform a taproot key-spend of the specified input. An explicit sighash is
// always attached, which can be removed by a caller if they wish to
// implicitly use the "default" sighash with a 64-byte signature.
//
// TODO(roasbeef): also need a tapscript version of this as well
// RawTxInTaprootSignature returns a valid schnorr signature required to
// perform a taproot key-spend of the specified input. If SigHashDefault was
// specified, then the returned signature is 64-byte in length, as it omits the
// additional byte to denote the sighash type.
func RawTxInTaprootSignature(tx *wire.MsgTx, sigHashes *TxSigHashes, idx int,
amt int64, pkScript []byte, hashType SigHashType,
amt int64, pkScript []byte, tapScriptRootHash []byte, hashType SigHashType,
key *btcec.PrivateKey) ([]byte, error) {
// First, we'll start by compute the top-level taproot sighash.
@ -82,27 +80,48 @@ func RawTxInTaprootSignature(tx *wire.MsgTx, sigHashes *TxSigHashes, idx int,
return nil, err
}
// Before we sign the sighash, we'll need to apply the taptweak to the
// private key based on the tapScriptRootHash.
privKeyTweak := TweakTaprootPrivKey(key, tapScriptRootHash)
// With the sighash constructed, we can sign it with the specified
// private key.
signature, err := schnorr.Sign(key, sigHash)
signature, err := schnorr.Sign(privKeyTweak, sigHash)
if err != nil {
return nil, err
}
// Finally, append the sighash type to the final sig.
return append(signature.Serialize(), byte(hashType)), nil
sig := signature.Serialize()
// If this is sighash default, then we can just return the signature
// directly.
if hashType&SigHashDefault == SigHashDefault {
return sig, nil
}
// Otherwise, append the sighash type to the final sig.
return append(sig, byte(hashType)), nil
}
// TaprootWitnessSignature returns a valid witness stack that can be used to
// spend the key-spend path of a taproot input as specified in BIP 342.
// spend the key-spend path of a taproot input as specified in BIP 342 and BIP
// 86. This method assumes that the public key included in pkScript was
// generated using ComputeTaprootKeyNoScript that commits to a fake root
// tapscript hash. If not, then RawTxInTaprootSignature should be used with the
// actual committed contents.
//
// TODO(roasbeef): add support for annex even tho it's non-standard?
func TaprootWitnessSignature(tx *wire.MsgTx, sigHashes *TxSigHashes, idx int,
amt int64, pkScript []byte, hashType SigHashType,
key *btcec.PrivateKey) (wire.TxWitness, error) {
// As we're assuming this was a BIP 86 key, we use an empty root hash
// which means output key commits to just the public key.
fakeTapscriptRootHash := []byte{}
sig, err := RawTxInTaprootSignature(
tx, sigHashes, idx, amt, pkScript, hashType, key,
tx, sigHashes, idx, amt, pkScript, fakeTapscriptRootHash,
hashType, key,
)
if err != nil {
return nil, err
@ -114,6 +133,45 @@ func TaprootWitnessSignature(tx *wire.MsgTx, sigHashes *TxSigHashes, idx int,
return wire.TxWitness{sig}, nil
}
// RawTxInTapscriptSignature computes a raw schnorr signature for a signature
// generated from a tapscript leaf. This differs from the
// RawTxInTaprootSignature which is used to generate signatures for top-level
// taproot key spends.
//
// TODO(roasbeef): actually add code-sep to interface? not really used
// anywhere....
func RawTxInTapscriptSignature(tx *wire.MsgTx, sigHashes *TxSigHashes, idx int,
amt int64, pkScript []byte, tapLeaf TapLeaf, hashType SigHashType,
privKey *btcec.PrivateKey) ([]byte, error) {
// First, we'll start by compute the top-level taproot sighash.
tapLeafHash := tapLeaf.TapHash()
sigHash, err := calcTaprootSignatureHashRaw(
sigHashes, hashType, tx, idx,
NewCannedPrevOutputFetcher(pkScript, amt),
WithBaseTapscriptVersion(blankCodeSepValue, tapLeafHash[:]),
)
if err != nil {
return nil, err
}
// With the sighash constructed, we can sign it with the specified
// private key.
signature, err := schnorr.Sign(privKey, sigHash)
if err != nil {
return nil, err
}
// Finally, append the sighash type to the final sig if it's not the
// default sighash value (in which case appending it is disallowed).
if hashType != SigHashDefault {
return append(signature.Serialize(), byte(hashType)), nil
}
// The default sighash case where we'll return _just_ the signature.
return signature.Serialize(), nil
}
// RawTxInSignature returns the serialized ECDSA signature for the input idx of
// the given transaction, with hashType appended to it.
func RawTxInSignature(tx *wire.MsgTx, idx int, subScript []byte,

View file

@ -96,6 +96,9 @@ func VerifyTaprootKeySpend(witnessProgram []byte, rawSig []byte, tx *wire.MsgTx,
// ControlBlock houses the structured witness input for a taproot spend. This
// includes the internal taproot key, the leaf version, and finally a nearly
// complete merkle inclusion proof for the main taproot commitment.
//
// TODO(roasbeef): method to serialize control block that commits to even
// y-bit, which pops up everywhere even tho 32 byte keys
type ControlBlock struct {
// InternalKey is the internal public key in the taproot commitment.
InternalKey *secp.PublicKey
@ -272,6 +275,54 @@ func ComputeTaprootOutputKey(pubKey *btcec.PublicKey,
return btcec.NewPublicKey(&taprootKey.X, &taprootKey.Y)
}
// ComputeTaprootKeyNoScript calculates the top-level taproot output key given
// an internal key, and a desire that the only way an output can be spent is
// with the keyspend path. This is useful for normal wallet operations that
// don't need any other additional spending conditions.
func ComputeTaprootKeyNoScript(internalKey *btcec.PublicKey) *btcec.PublicKey {
// We'll compute a custom tap tweak hash that just commits to the key,
// rather than an actual root hash.
fakeScriptroot := []byte{}
return ComputeTaprootOutputKey(internalKey, fakeScriptroot)
}
// TweakTaprootPrivKey applies the same operation as ComputeTaprootOutputKey,
// but on the private key instead. The final key is derived as: privKey +
// h_tapTweak(internalKey || merkleRoot) % N, where N is the order of the
// secp256k1 curve, and merkleRoot is the root hash of the tapscript tree.
func TweakTaprootPrivKey(privKey *btcec.PrivateKey,
scriptRoot []byte) *btcec.PrivateKey {
// If the corresponding public key has an odd y coordinate, then we'll
// negate the private key as specified in BIP 341.
privKeyScalar := &privKey.Key
pubKeyBytes := privKey.PubKey().SerializeCompressed()
if pubKeyBytes[0] == btcec.PubKeyFormatCompressedOdd {
privKeyScalar.Negate()
}
// Next, we'll compute the tap tweak hash that commits to the internal
// key and the merkle script root. We'll snip off the extra parity byte
// from the compressed serialization and use that directly.
schnorrKeyBytes := pubKeyBytes[1:]
tapTweakHash := chainhash.TaggedHash(
chainhash.TagTapTweak, schnorrKeyBytes, scriptRoot,
)
// Map the private key to a ModNScalar which is needed to perform
// operation mod the curve order.
var tweakScalar btcec.ModNScalar
tweakScalar.SetBytes((*[32]byte)(tapTweakHash))
// Now that we have the private key in its may negated form, we'll add
// the script root as a tweak. As we're using a ModNScalar all
// operations are already normalized mod the curve order.
privTweak := privKeyScalar.Add(&tweakScalar)
return btcec.PrivKeyFromScalar(privTweak)
}
// VerifyTaprootLeafCommitment attempts to verify a taproot commitment of the
// revealed script within the taprootWitnessProgram (a schnorr public key)
// given the required information included in the control block. An error is

View file

@ -8,13 +8,40 @@ import (
"bytes"
"encoding/hex"
"testing"
"testing/quick"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/btcsuite/btcd/chaincfg"
secp "github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/stretchr/testify/require"
)
var (
testPubBytes, _ = hex.DecodeString("F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9")
testPubBytes, _ = hex.DecodeString("F9308A019258C31049344F85F89D5229B" +
"531C845836F99B08601F113BCE036F9")
// rootKey is the test root key defined in the test vectors:
// https://github.com/bitcoin/bips/blob/master/bip-0086.mediawiki
rootKey, _ = hdkeychain.NewKeyFromString(
"xprv9s21ZrQH143K3GJpoapnV8SFfukcVBSfeCficPSGfubmSFDxo1kuHnLi" +
"sriDvSnRRuL2Qrg5ggqHKNVpxR86QEC8w35uxmGoggxtQTPvfUu",
)
// accountPath is the base path for BIP86 (m/86'/0'/0').
accountPath = []uint32{
86 + hdkeychain.HardenedKeyStart, hdkeychain.HardenedKeyStart,
hdkeychain.HardenedKeyStart,
}
expectedExternalAddresses = []string{
"bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr",
"bc1p4qhjn9zdvkux4e44uhx8tc55attvtyu358kutcqkudyccelu0was9fqzwh",
}
expectedInternalAddresses = []string{
"bc1p3qkhfews2uk44qtvauqyr2ttdsw7svhkl9nkm9s9c3x4ax5h60wqwruhk7",
}
)
// TestControlBlockParsing tests that we're able to generate and parse a valid
@ -131,3 +158,90 @@ func TestControlBlockParsing(t *testing.T) {
}
}
}
// TestTaprootScriptSpendTweak tests that for any 32-byte hypothetical script
// root, the resulting tweaked public key is the same as tweaking the private
// key, then generating a public key from that. This test a quickcheck test to
// assert the following invariant:
//
// * taproot_tweak_pubkey(pubkey_gen(seckey), h)[1] ==
// pubkey_gen(taproot_tweak_seckey(seckey, h))
func TestTaprootScriptSpendTweak(t *testing.T) {
t.Parallel()
// Assert that if we use this x value as the hash of the script root,
// then if we generate a tweaked public key, it's the same key as if we
// used that key to generate the tweaked
// private key, and then generated the public key from that.
f := func(x [32]byte) bool {
privKey, err := btcec.NewPrivateKey()
if err != nil {
return false
}
// Generate the tweaked public key using the x value as the
// script root.
tweakedPub := ComputeTaprootOutputKey(privKey.PubKey(), x[:])
// Now we'll generate the corresponding tweaked private key.
tweakedPriv := TweakTaprootPrivKey(privKey, x[:])
// The public key for this private key should be the same as
// the tweaked public key we generate above.
return tweakedPub.IsEqual(tweakedPriv.PubKey()) &&
bytes.Equal(
schnorr.SerializePubKey(tweakedPub),
schnorr.SerializePubKey(tweakedPriv.PubKey()),
)
}
if err := quick.Check(f, nil); err != nil {
t.Fatalf("tweaked public/private key mapping is "+
"incorrect: %v", err)
}
}
// TestTaprootConstructKeyPath tests the key spend only taproot construction.
func TestTaprootConstructKeyPath(t *testing.T) {
checkPath := func(branch uint32, expectedAddresses []string) {
path, err := derivePath(rootKey, append(accountPath, branch))
require.NoError(t, err)
for index, expectedAddr := range expectedAddresses {
extendedKey, err := path.Derive(uint32(index))
require.NoError(t, err)
pubKey, err := extendedKey.ECPubKey()
require.NoError(t, err)
tapKey := ComputeTaprootKeyNoScript(pubKey)
addr, err := btcutil.NewAddressTaproot(
schnorr.SerializePubKey(tapKey),
&chaincfg.MainNetParams,
)
require.NoError(t, err)
require.Equal(t, expectedAddr, addr.String())
}
}
checkPath(0, expectedExternalAddresses)
checkPath(1, expectedInternalAddresses)
}
func derivePath(key *hdkeychain.ExtendedKey, path []uint32) (
*hdkeychain.ExtendedKey, error) {
var (
currentKey = key
err error
)
for _, pathPart := range path {
currentKey, err = currentKey.Derive(pathPart)
if err != nil {
return nil, err
}
}
return currentKey, nil
}