btcd/txscript/taproot_test.go
Olaoluwa Osuntokun 17e4609494
txscript: add AssembleTaprootScriptTree func for creating input witnesses
In this commit, we add a new AssembleTaprootScriptTree function that
given a list of tapscript leaves, generates a valid tapscript root,
along with the auxiliary proof data needed to spend each output.
2022-03-15 18:23:11 -07:00

363 lines
9.6 KiB
Go

// Copyright (c) 2013-2022 The btcsuite developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package txscript
import (
"bytes"
"encoding/hex"
"fmt"
prand "math/rand"
"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("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
// control block.
func TestControlBlockParsing(t *testing.T) {
t.Parallel()
var testCases = []struct {
controlBlockGen func() []byte
valid bool
}{
// An invalid control block, it's only 5 bytes and needs to be
// at least 33 bytes.
{
controlBlockGen: func() []byte {
return bytes.Repeat([]byte{0x00}, 5)
},
valid: false,
},
// An invalid control block, it's greater than the largest
// accepted control block.
{
controlBlockGen: func() []byte {
return bytes.Repeat([]byte{0x00}, ControlBlockMaxSize+1)
},
valid: false,
},
// An invalid control block, it isn't a multiple of 32 bytes
// enough though it has a valid starting byte length.
{
controlBlockGen: func() []byte {
return bytes.Repeat([]byte{0x00}, ControlBlockBaseSize+34)
},
valid: false,
},
// A valid control block, of the largest possible size.
{
controlBlockGen: func() []byte {
privKey, _ := btcec.NewPrivateKey()
pubKey := privKey.PubKey()
yIsOdd := (pubKey.SerializeCompressed()[0] ==
secp.PubKeyFormatCompressedOdd)
ctrl := ControlBlock{
InternalKey: pubKey,
OutputKeyYIsOdd: yIsOdd,
LeafVersion: BaseLeafVersion,
InclusionProof: bytes.Repeat(
[]byte{0x00},
ControlBlockMaxSize-ControlBlockBaseSize,
),
}
ctrlBytes, _ := ctrl.ToBytes()
return ctrlBytes
},
valid: true,
},
// A valid control block, only has a single element in the
// proof as the tree only has a single element.
{
controlBlockGen: func() []byte {
privKey, _ := btcec.NewPrivateKey()
pubKey := privKey.PubKey()
yIsOdd := (pubKey.SerializeCompressed()[0] ==
secp.PubKeyFormatCompressedOdd)
ctrl := ControlBlock{
InternalKey: pubKey,
OutputKeyYIsOdd: yIsOdd,
LeafVersion: BaseLeafVersion,
InclusionProof: bytes.Repeat(
[]byte{0x00}, ControlBlockNodeSize,
),
}
ctrlBytes, _ := ctrl.ToBytes()
return ctrlBytes
},
valid: true,
},
}
for i, testCase := range testCases {
ctrlBlockBytes := testCase.controlBlockGen()
ctrlBlock, err := ParseControlBlock(ctrlBlockBytes)
switch {
case testCase.valid && err != nil:
t.Fatalf("#%v: unable to parse valid control block: %v", i, err)
case !testCase.valid && err == nil:
t.Fatalf("#%v: invalid control block should have failed: %v", i, err)
}
if !testCase.valid {
continue
}
// If we serialize the control block, we should get the exact same
// set of bytes as the input.
ctrlBytes, err := ctrlBlock.ToBytes()
if err != nil {
t.Fatalf("#%v: unable to encode bytes: %v", i, err)
}
if !bytes.Equal(ctrlBytes, ctrlBlockBytes) {
t.Fatalf("#%v: encoding mismatch: expected %x, "+
"got %x", i, ctrlBlockBytes, ctrlBytes)
}
}
}
// 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
}
// TestTapscriptCommitmentVerification that given a valid control block, proof
// we're able to both generate and validate validate script tree leaf inclusion
// proofs.
func TestTapscriptCommitmentVerification(t *testing.T) {
t.Parallel()
// make from 0 to 1 leaf
// ensure verifies properly
testCases := []struct {
numLeaves int
valid bool
treeMutateFunc func(*IndexedTapScriptTree)
ctrlBlockMutateFunc func(*ControlBlock)
}{
// A valid merkle proof of a single leaf.
{
numLeaves: 1,
valid: true,
},
// A valid series of merkle proofs with an odd number of leaves.
{
numLeaves: 3,
valid: true,
},
// A valid series of merkle proofs with an even number of leaves.
{
numLeaves: 4,
valid: true,
},
// An invalid merkle proof, we modify the last byte of one of
// the leaves.
{
numLeaves: 4,
valid: false,
treeMutateFunc: func(t *IndexedTapScriptTree) {
for _, leafProof := range t.LeafMerkleProofs {
leafProof.InclusionProof[0] ^= 1
}
},
},
{
// An invalid series of proofs, we modify the control
// block to not match the parity of the final output
// key commitment.
numLeaves: 2,
valid: false,
ctrlBlockMutateFunc: func(c *ControlBlock) {
c.OutputKeyYIsOdd = !c.OutputKeyYIsOdd
},
},
}
for _, testCase := range testCases {
testName := fmt.Sprintf("num_leaves=%v, valid=%v, treeMutate=%v, "+
"ctrlBlockMutate=%v", testCase.numLeaves, testCase.valid,
testCase.treeMutateFunc == nil, testCase.ctrlBlockMutateFunc == nil)
t.Run(testName, func(t *testing.T) {
tapScriptLeaves := make([]TapLeaf, testCase.numLeaves)
for i := 0; i < len(tapScriptLeaves); i++ {
numLeafBytes := prand.Intn(1000)
scriptBytes := make([]byte, numLeafBytes)
if _, err := prand.Read(scriptBytes[:]); err != nil {
t.Fatalf("unable to read rand bytes: %v", err)
}
tapScriptLeaves[i] = NewBaseTapLeaf(scriptBytes)
}
scriptTree := AssembleTaprootScriptTree(tapScriptLeaves...)
if testCase.treeMutateFunc != nil {
testCase.treeMutateFunc(scriptTree)
}
internalKey, _ := btcec.NewPrivateKey()
rootHash := scriptTree.RootNode.TapHash()
outputKey := ComputeTaprootOutputKey(
internalKey.PubKey(), rootHash[:],
)
for _, leafProof := range scriptTree.LeafMerkleProofs {
ctrlBlock := leafProof.ToControlBlock(
internalKey.PubKey(),
)
if testCase.ctrlBlockMutateFunc != nil {
testCase.ctrlBlockMutateFunc(&ctrlBlock)
}
err := VerifyTaprootLeafCommitment(
&ctrlBlock, schnorr.SerializePubKey(outputKey),
leafProof.TapLeaf.Script,
)
valid := err == nil
if valid != testCase.valid {
t.Fatalf("test case mismatch: expected "+
"valid=%v, got valid=%v", testCase.valid,
valid)
}
}
// TODO(roasbeef): index correctness
})
}
}