diff --git a/btcec/privkey.go b/btcec/privkey.go index 6f13990b..4efa806c 100644 --- a/btcec/privkey.go +++ b/btcec/privkey.go @@ -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 diff --git a/btcutil/go.sum b/btcutil/go.sum index 69c701ee..8617dd83 100644 --- a/btcutil/go.sum +++ b/btcutil/go.sum @@ -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= diff --git a/btcutil/psbt/go.mod b/btcutil/psbt/go.mod index 4aa9d394..059bdab5 100644 --- a/btcutil/psbt/go.mod +++ b/btcutil/psbt/go.mod @@ -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 ) diff --git a/btcutil/psbt/go.sum b/btcutil/psbt/go.sum index 40bb0c16..3056278f 100644 --- a/btcutil/psbt/go.sum +++ b/btcutil/psbt/go.sum @@ -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= diff --git a/go.mod b/go.mod index 69620803..732ed420 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 51aa0520..604e5054 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/txscript/sign.go b/txscript/sign.go index de829d7e..fc89312f 100644 --- a/txscript/sign.go +++ b/txscript/sign.go @@ -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, diff --git a/txscript/taproot.go b/txscript/taproot.go index 4f132e40..5bd7aef6 100644 --- a/txscript/taproot.go +++ b/txscript/taproot.go @@ -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 diff --git a/txscript/taproot_test.go b/txscript/taproot_test.go index fd5ec217..2a458551 100644 --- a/txscript/taproot_test.go +++ b/txscript/taproot_test.go @@ -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 +}