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.
This commit is contained in:
Olaoluwa Osuntokun 2022-01-06 18:16:09 -08:00
parent 6fc4199ee4
commit 17e4609494
No known key found for this signature in database
GPG Key ID: 3BBD59E99B280306
2 changed files with 390 additions and 8 deletions

View File

@ -9,10 +9,10 @@ import (
"fmt"
"github.com/btcsuite/btcd/btcec/v2"
secp "github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
secp "github.com/decred/dcrd/dcrec/secp256k1/v4"
)
// TapscriptLeafVersion represents the various possible versions of a tapscript
@ -101,7 +101,7 @@ func VerifyTaprootKeySpend(witnessProgram []byte, rawSig []byte, tx *wire.MsgTx,
// 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
InternalKey *btcec.PublicKey
// OutputKeyYIsOdd denotes if the y coordinate of the output key (the
// key placed in the actual taproot output is odd.
@ -298,7 +298,7 @@ func TweakTaprootPrivKey(privKey *btcec.PrivateKey,
// negate the private key as specified in BIP 341.
privKeyScalar := &privKey.Key
pubKeyBytes := privKey.PubKey().SerializeCompressed()
if pubKeyBytes[0] == btcec.PubKeyFormatCompressedOdd {
if pubKeyBytes[0] == secp.PubKeyFormatCompressedOdd {
privKeyScalar.Negate()
}
@ -350,6 +350,14 @@ func VerifyTaprootLeafCommitment(controlBlock *ControlBlock,
return fmt.Errorf("invalid witness commitment")
}
// Finally, we'll verify that the parity of the y coordinate of the
// public key we've derived matches the control block.
derivedYIsOdd := (taprootKey.SerializeCompressed()[0] ==
secp.PubKeyFormatCompressedOdd)
if controlBlock.OutputKeyYIsOdd != derivedYIsOdd {
return fmt.Errorf("invalid witness commitment")
}
// Otherwise, if we reach here, the commitment opening is valid and
// execution can continue.
return nil
@ -415,6 +423,9 @@ func NewTapLeaf(leafVersion TapscriptLeafVersion, script []byte) TapLeaf {
// digest is computed as: h_tapleaf(leafVersion || compactSizeof(script) ||
// script).
func (t TapLeaf) TapHash() chainhash.Hash {
// TODO(roasbeef): cache these and the branch due to the recursive
// call, so memoize
// The leaf encoding is: leafVersion || compactSizeof(script) ||
// script, where compactSizeof returns the compact size needed to
// encode the value.
@ -469,8 +480,7 @@ func (t TapBranch) TapHash() chainhash.Hash {
// hashes them into a branch. See The TapBranch method for the specifics.
func tapBranchHash(l, r []byte) chainhash.Hash {
if bytes.Compare(l[:], r[:]) > 0 {
l = r
r = l
l, r = r, l
}
return *chainhash.TaggedHash(
@ -484,6 +494,10 @@ type TapscriptProof struct {
// TapLeaf is the leaf that we want to prove inclusion for.
TapLeaf
// RootNode is the root of the tapscript tree, this will be used to
// compute what the final output key looks like.
RootNode TapNode
// InclusionProof is the tail end of the control block that contains
// the series of hashes (the sibling hashes up the tree), that when
// hashed together allow us to re-derive the top level taproot output.
@ -493,9 +507,261 @@ type TapscriptProof struct {
// ToControlBlock maps the tapscript proof into a fully valid control block
// that can be used as a witness item for a tapscript spend.
func (t *TapscriptProof) ToControlBlock(internalKey *btcec.PublicKey) ControlBlock {
// Compute the total level output commitment based on the populated
// root node.
rootHash := t.RootNode.TapHash()
taprootKey := ComputeTaprootOutputKey(
internalKey, rootHash[:],
)
// With the commitment computed we can obtain the bit that denotes if
// the resulting key has an odd y coordinate or not.
var outputKeyYIsOdd bool
if taprootKey.SerializeCompressed()[0] ==
secp.PubKeyFormatCompressedOdd {
outputKeyYIsOdd = true
}
return ControlBlock{
InternalKey: internalKey,
LeafVersion: t.TapLeaf.LeafVersion,
InclusionProof: t.InclusionProof,
InternalKey: internalKey,
OutputKeyYIsOdd: outputKeyYIsOdd,
LeafVersion: t.TapLeaf.LeafVersion,
InclusionProof: t.InclusionProof,
}
}
// IndexedTapScriptTree reprints a fully contracted tapscript tree. The
// RootNode can be used to traverse down the full tree. In addition, complete
// inclusion proofs for each leaf are included as well, with an index into the
// slice of proof based on the tap leaf hash of a given leaf.
type IndexedTapScriptTree struct {
// RootNode is the root of the tapscript tree. RootNode.TapHash() can
// be used to extract the hash needed to derive the taptweak committed
// to in the taproot output.
RootNode TapNode
// LeafMerkleProofs is a slice that houses the series of merkle
// inclusion proofs for each leaf based on the input order of the
// leaves.
LeafMerkleProofs []TapscriptProof
// LeafProofIndex maps the TapHash() of a given leaf node to the index
// within the LeafMerkleProofs array above. This can be used to
// retrieve the inclusion proof for a given script when constructing
// the witness stack and control block for spending a tapscript path.
LeafProofIndex map[chainhash.Hash]int
}
// NewIndexedTapScriptTree creates a new empty tapscript tree that has enough
// space to hold information for the specified amount of leaves.
func NewIndexedTapScriptTree(numLeaves int) *IndexedTapScriptTree {
return &IndexedTapScriptTree{
LeafMerkleProofs: make([]TapscriptProof, numLeaves),
LeafProofIndex: make(map[chainhash.Hash]int, numLeaves),
}
}
// hashTapNodes takes a left and right now, and returns the left and right tap
// hashes, along with the new combined node. If both nodes are nil, nil
// pointers are returned. If the right now is nil, then the left node is passed
// in, which effectively will "lift" the node up in the tree as long as it
// doesn't have any siblings.
func hashTapNodes(left, right TapNode) (*chainhash.Hash, *chainhash.Hash, TapNode) {
switch {
// If there's no left child, then this is a "nil" portion of the array
// tree, so well thread thru nil.
case left == nil:
return nil, nil, nil
// If there's no right child, then this is a single node that'll be
// passed all the way up the tree as it has no children.
case right == nil:
return nil, nil, left
}
// The result of hashing two nodes will always be a branch, so we start
// with that.
leftHash := left.TapHash()
rightHash := right.TapHash()
return &leftHash, &rightHash, NewTapBranch(left, right)
}
// leafDescendants is a recursive algorithm that returns all the leaf nodes
// that are a decedents of this tree. This is used to collect the series of
// nodes we need to extend the inclusion proof of each time we go up in the
// tree.
func leafDescendants(node TapNode) []TapNode {
// A leaf node has no decedents, so we just return it directly.
if node.Left() == nil && node.Right() == nil {
return []TapNode{node}
}
// Otherwise, get the descendants of the left and right sub-trees to
// return.
leftLeaves := leafDescendants(node.Left())
rightLeaves := leafDescendants(node.Right())
return append(leftLeaves, rightLeaves...)
}
// AssembleTaprootScriptTree constructs a new fully indexed tapscript tree
// given a series of leaf nodes. A combination of a recursive data structure,
// and an array-based representation are used to both generate the tree and
// also accumulate all the necessary inclusion proofs in the same path. See the
// comment of blockchain.BuildMerkleTreeStore for further details.
func AssembleTaprootScriptTree(leaves ...TapLeaf) *IndexedTapScriptTree {
// If there's only a single leaf, then that becomes our root.
if len(leaves) == 1 {
// A lone leaf has no additional inclusion proof, as a verifier
// will just hash the leaf as the sole branch.
leaf := leaves[0]
return &IndexedTapScriptTree{
RootNode: leaf,
LeafProofIndex: map[chainhash.Hash]int{
leaf.TapHash(): 0,
},
LeafMerkleProofs: []TapscriptProof{
{
TapLeaf: leaf,
RootNode: leaf,
InclusionProof: nil,
},
},
}
}
// We'll start out by populating the leaf index which maps a leave's
// taphash to its index within the tree.
scriptTree := NewIndexedTapScriptTree(len(leaves))
for i, leaf := range leaves {
leafHash := leaf.TapHash()
scriptTree.LeafProofIndex[leafHash] = i
}
var branches []TapBranch
for i := 0; i < len(leaves); i += 2 {
// If there's only a single leaf left, then we'll merge this
// with the last branch we have.
if i == len(leaves)-1 {
branchToMerge := branches[len(branches)-1]
leaf := leaves[i]
newBranch := NewTapBranch(branchToMerge, leaf)
branches[len(branches)-1] = newBranch
// The leaf includes the existing branch within its
// inclusion proof.
branchHash := branchToMerge.TapHash()
scriptTree.LeafMerkleProofs[i].TapLeaf = leaf
scriptTree.LeafMerkleProofs[i].InclusionProof = append(
scriptTree.LeafMerkleProofs[i].InclusionProof,
branchHash[:]...,
)
// We'll also add this right hash to the inclusion of
// the left and right nodes of the branch.
lastLeafHash := leaf.TapHash()
leftLeafHash := branchToMerge.Left().TapHash()
leftLeafIndex := scriptTree.LeafProofIndex[leftLeafHash]
scriptTree.LeafMerkleProofs[leftLeafIndex].InclusionProof = append(
scriptTree.LeafMerkleProofs[leftLeafIndex].InclusionProof,
lastLeafHash[:]...,
)
rightLeafHash := branchToMerge.Right().TapHash()
rightLeafIndex := scriptTree.LeafProofIndex[rightLeafHash]
scriptTree.LeafMerkleProofs[rightLeafIndex].InclusionProof = append(
scriptTree.LeafMerkleProofs[rightLeafIndex].InclusionProof,
lastLeafHash[:]...,
)
continue
}
// While we still have leaves left, we'll combine two of them
// into a new branch node.
left, right := leaves[i], leaves[i+1]
nextBranch := NewTapBranch(left, right)
branches = append(branches, nextBranch)
// The left node will use the right node as part of its
// inclusion proof, and vice versa.
leftHash := left.TapHash()
rightHash := right.TapHash()
scriptTree.LeafMerkleProofs[i].TapLeaf = left
scriptTree.LeafMerkleProofs[i].InclusionProof = append(
scriptTree.LeafMerkleProofs[i].InclusionProof,
rightHash[:]...,
)
scriptTree.LeafMerkleProofs[i+1].TapLeaf = right
scriptTree.LeafMerkleProofs[i+1].InclusionProof = append(
scriptTree.LeafMerkleProofs[i+1].InclusionProof,
leftHash[:]...,
)
}
// In this second phase, we'll merge all the leaf branches we have one
// by one until we have our final root.
var rootNode TapNode
for len(branches) != 0 {
// When we only have a single branch left, then that becomes
// our root.
if len(branches) == 1 {
rootNode = branches[0]
break
}
left, right := branches[0], branches[1]
newBranch := NewTapBranch(left, right)
branches = branches[2:]
branches = append(branches, newBranch)
// Accumulate the sibling hash of this new branch for all the
// leaves that are its children.
leftLeafDescendants := leafDescendants(left)
rightLeafDescendants := leafDescendants(right)
leftHash, rightHash := left.TapHash(), right.TapHash()
// For each left hash that's a leaf descendants, well add the
// right sibling as that sibling is needed to construct the new
// internal branch we just created. We also do the same for the
// siblings of the right node.
for _, leftLeaf := range leftLeafDescendants {
leafHash := leftLeaf.TapHash()
leafIndex := scriptTree.LeafProofIndex[leafHash]
scriptTree.LeafMerkleProofs[leafIndex].InclusionProof = append(
scriptTree.LeafMerkleProofs[leafIndex].InclusionProof,
rightHash[:]...,
)
}
for _, rightLeaf := range rightLeafDescendants {
leafHash := rightLeaf.TapHash()
leafIndex := scriptTree.LeafProofIndex[leafHash]
scriptTree.LeafMerkleProofs[leafIndex].InclusionProof = append(
scriptTree.LeafMerkleProofs[leafIndex].InclusionProof,
leftHash[:]...,
)
}
}
// Populate the top level root node pointer, as well as the pointer in
// each proof.
scriptTree.RootNode = rootNode
for i := range scriptTree.LeafMerkleProofs {
scriptTree.LeafMerkleProofs[i].RootNode = rootNode
}
return scriptTree
}

View File

@ -7,6 +7,8 @@ package txscript
import (
"bytes"
"encoding/hex"
"fmt"
prand "math/rand"
"testing"
"testing/quick"
@ -245,3 +247,117 @@ func derivePath(key *hdkeychain.ExtendedKey, path []uint32) (
}
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
})
}
}