Skip to content

Commit 13f6073

Browse files
committed
commitment: refactor tapscript preimage validation
In this commit, we update how tapscript preimages are validated before use. We define safe constructors for tapscript preimages, and require their use by making the raw preimage bytes private. We also add checks when decoding a preimage to further prevent improper construction.
1 parent 3e103a3 commit 13f6073

File tree

3 files changed

+230
-116
lines changed

3 files changed

+230
-116
lines changed

commitment/encoding.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -141,12 +141,12 @@ func TapscriptPreimageEncoder(w io.Writer, val any, buf *[8]byte) error {
141141
if t, ok := val.(**TapscriptPreimage); ok {
142142
// We'll encode the pre-image as 1 byte for the type of the
143143
// pre-image, and then the pre-image itself.
144-
siblingType := uint8((*t).SiblingType)
144+
siblingType := uint8((*t).siblingType)
145145
if err := tlv.EUint8(w, &siblingType, buf); err != nil {
146146
return err
147147
}
148148

149-
return tlv.EVarBytes(w, &(*t).SiblingPreimage, buf)
149+
return tlv.EVarBytes(w, &(*t).siblingPreimage, buf)
150150
}
151151

152152
return tlv.NewTypeForEncodingErr(val, "*TapscriptPreimage")
@@ -175,10 +175,10 @@ func TapscriptPreimageDecoder(r io.Reader, val any, buf *[8]byte,
175175
return err
176176
}
177177

178-
preimage.SiblingType = TapscriptPreimageType(siblingType)
178+
preimage.siblingType = TapscriptPreimageType(siblingType)
179179

180180
// Now we'll read out the pre-image itself.
181-
err = tlv.DVarBytes(r, &preimage.SiblingPreimage, buf, l-1)
181+
err = tlv.DVarBytes(r, &preimage.siblingPreimage, buf, l-1)
182182
if err != nil {
183183
return err
184184
}

commitment/taproot.go

Lines changed: 160 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"github.com/btcsuite/btcd/chaincfg/chainhash"
99
"github.com/btcsuite/btcd/txscript"
1010
"github.com/btcsuite/btcd/wire"
11+
"github.com/lightninglabs/taproot-assets/asset"
12+
"github.com/lightninglabs/taproot-assets/fn"
1113
)
1214

1315
const (
@@ -37,6 +39,12 @@ var (
3739
"invalid tapscript preimage length",
3840
)
3941

42+
// ErrInvalidTapscriptPreimageType is an error returned when a tapscript
43+
// preimage has an unknown type that is not leaf nor branch.
44+
ErrInvalidTapscriptPreimageType = errors.New(
45+
"unknown tapscript preimage type",
46+
)
47+
4048
// ErrPreimageIsTapCommitment is an error returned when a tapscript
4149
// preimage is a valid Taproot Asset commitment.
4250
ErrPreimageIsTapCommitment = errors.New(
@@ -66,51 +74,152 @@ func (t TapscriptPreimageType) String() string {
6674
return "BranchPreimage"
6775

6876
default:
69-
return fmt.Sprintf("UnKnownSiblingType(%d)", t)
77+
return fmt.Sprintf("UnknownSiblingType(%d)", t)
7078
}
7179
}
7280

7381
// TapscriptPreimage wraps a pre-image byte slice with a type byte that self
7482
// identifies what type of pre-image it is.
7583
type TapscriptPreimage struct {
76-
// SiblingPreimage is the pre-image itself. This will be either 32 or
77-
// 64 bytes.
78-
SiblingPreimage []byte
84+
// SiblingPreimage is the pre-image itself. This will be 64 bytes if
85+
// representing a TapBranch, or any size under 4 MBytes if representing
86+
// a TapLeaf.
87+
siblingPreimage []byte
7988

8089
// SiblingType is the type of the pre-image.
81-
SiblingType TapscriptPreimageType
90+
siblingType TapscriptPreimageType
8291
}
8392

8493
// NewPreimageFromLeaf creates a new TapscriptPreimage from a single tap leaf.
85-
func NewPreimageFromLeaf(leaf txscript.TapLeaf) *TapscriptPreimage {
86-
// The leaf encoding is: leafVersion || compactSizeof(script) ||
87-
// script, where compactSizeof returns the compact size needed to
88-
// encode the value.
94+
func NewPreimageFromLeaf(leaf txscript.TapLeaf) (*TapscriptPreimage, error) {
95+
// Check the leaf size and version, and assert that the leaf script is
96+
// not a Taproot Asset Commitment.
97+
err := asset.CheckTapLeafSanity(&leaf)
98+
if err != nil {
99+
return nil, err
100+
}
101+
102+
if IsTaprootAssetCommitmentScript(leaf.Script) {
103+
return nil, ErrPreimageIsTapCommitment
104+
}
105+
106+
// The leaf encoding is: leafVersion || compactSizeof(script) || script,
107+
// where compactSizeof returns the compact size needed to encode the
108+
// value.
89109
var encodedLeaf bytes.Buffer
90110

91111
_ = encodedLeaf.WriteByte(byte(leaf.LeafVersion))
92112
_ = wire.WriteVarBytes(&encodedLeaf, 0, leaf.Script)
93113

94114
return &TapscriptPreimage{
95-
SiblingPreimage: encodedLeaf.Bytes(),
96-
SiblingType: LeafPreimage,
115+
siblingPreimage: encodedLeaf.Bytes(),
116+
siblingType: LeafPreimage,
117+
}, nil
118+
}
119+
120+
// NewLeafFromPreimage sanity checks a TapscriptPreimage and decodes it into a
121+
// TapLeaf.
122+
func NewLeafFromPreimage(preimage TapscriptPreimage) (*txscript.TapLeaf,
123+
error) {
124+
125+
// The preimage must be a TapLeaf.
126+
if preimage.Type() != LeafPreimage {
127+
return nil, ErrInvalidTapscriptPreimageType
128+
}
129+
130+
// Remove the leaf version and script size prefix from the preimage.
131+
// The prefix is at least 2 bytes long, and if it's missing then this
132+
// preimage was not created correctly.
133+
if len(preimage.siblingPreimage) < 2 {
134+
return nil, ErrInvalidTapscriptPreimageLen
135+
}
136+
137+
// The script is encoded with a leading VarByte that indicates its total
138+
// length.
139+
version := txscript.TapscriptLeafVersion(preimage.siblingPreimage[0])
140+
remaining := preimage.siblingPreimage[1:]
141+
script, err := wire.ReadVarBytes(
142+
bytes.NewReader(remaining), 0, uint32(len(remaining)), "script",
143+
)
144+
if err != nil {
145+
return nil, fmt.Errorf("error decoding leaf pre-image: %w", err)
97146
}
147+
148+
// The script must not be a Taproot Asset Commitment.
149+
if IsTaprootAssetCommitmentScript(script) {
150+
return nil, ErrPreimageIsTapCommitment
151+
}
152+
153+
return fn.Ptr(txscript.NewTapLeaf(version, script)), nil
98154
}
99155

100156
// NewPreimageFromBranch creates a new TapscriptPreimage from a tap branch.
101-
func NewPreimageFromBranch(branch txscript.TapBranch) *TapscriptPreimage {
157+
func NewPreimageFromBranch(branch txscript.TapBranch) TapscriptPreimage {
158+
leftHash := branch.Left().TapHash()
159+
rightHash := branch.Right().TapHash()
160+
branchBytes := bytes.Join([][]byte{leftHash[:], rightHash[:]}, nil)
161+
162+
return TapscriptPreimage{
163+
siblingPreimage: branchBytes,
164+
siblingType: BranchPreimage,
165+
}
166+
}
167+
168+
// TapTreeToSibling constucts a taproot sibling hash from Tapscript tree nodes,
169+
// to be used with a TapCommitment tree root to derive a tapscript root. This
170+
// could be multiple TapLeaf objects, or a representation of a TapBranch.
171+
func NewPreimageFromTapscriptTreeNodes(
172+
tn asset.TapscriptTreeNodes) (*TapscriptPreimage, error) {
173+
102174
var (
103-
encodedBranch bytes.Buffer
104-
leftHash = branch.Left().TapHash()
105-
rightHash = branch.Right().TapHash()
175+
preimage *TapscriptPreimage
176+
preimageErr error
106177
)
107-
_, _ = encodedBranch.Write(leftHash[:])
108-
_, _ = encodedBranch.Write(rightHash[:])
109178

110-
return &TapscriptPreimage{
111-
SiblingPreimage: encodedBranch.Bytes(),
112-
SiblingType: BranchPreimage,
179+
asset.GetLeaves(tn).WhenSome(func(tln asset.TapLeafNodes) {
180+
leaves := asset.ToLeaves(tln)
181+
182+
// Check that none of the leaves are a Taproot Asset Commitment.
183+
badLeaves := fn.Any(leaves, func(leaf txscript.TapLeaf) bool {
184+
return IsTaprootAssetCommitmentScript(leaf.Script)
185+
})
186+
if badLeaves {
187+
preimageErr = ErrPreimageIsTapCommitment
188+
return
189+
}
190+
191+
switch len(leaves) {
192+
case 1:
193+
// If we only have one leaf, our preimage is just the
194+
// encoded leaf.
195+
preimage, preimageErr = NewPreimageFromLeaf(leaves[0])
196+
197+
default:
198+
// Make a branch from the leaves.
199+
tree := txscript.AssembleTaprootScriptTree(leaves...)
200+
branch := txscript.NewTapBranch(
201+
tree.RootNode.Left(), tree.RootNode.Right(),
202+
)
203+
preimage = fn.Ptr(NewPreimageFromBranch(branch))
204+
}
205+
})
206+
207+
asset.GetBranch(tn).WhenSome(func(tbn asset.TapBranchNodes) {
208+
branch := asset.ToBranch(tbn)
209+
preimage = &TapscriptPreimage{
210+
siblingPreimage: bytes.Join(branch, nil),
211+
siblingType: BranchPreimage,
212+
}
213+
})
214+
215+
if preimageErr != nil {
216+
return nil, preimageErr
113217
}
218+
if preimage == nil {
219+
return nil, fmt.Errorf("malformed tapscript tree nodes")
220+
}
221+
222+
return preimage, nil
114223
}
115224

116225
// IsEmpty returns true if the sibling pre-image is empty.
@@ -119,7 +228,12 @@ func (t *TapscriptPreimage) IsEmpty() bool {
119228
return true
120229
}
121230

122-
return len(t.SiblingPreimage) == 0
231+
return len(t.siblingPreimage) == 0
232+
}
233+
234+
// Type returns the preimage type.
235+
func (t *TapscriptPreimage) Type() TapscriptPreimageType {
236+
return TapscriptPreimageType(t.siblingType)
123237
}
124238

125239
// TapHash returns the tap hash of this preimage according to its type.
@@ -128,33 +242,35 @@ func (t *TapscriptPreimage) TapHash() (*chainhash.Hash, error) {
128242
return nil, ErrInvalidEmptyTapscriptPreimage
129243
}
130244

131-
switch t.SiblingType {
132-
// The sibling is actually a leaf pre-image, so we'll verify that it
133-
// isn't a Taproot Asset commitment, and then hash it with the
134-
// commitment to obtain our root.
245+
switch t.siblingType {
246+
// The sibling is a leaf pre-image, so we'll verify that it isn't a
247+
// Taproot Asset commitment, and then compute its TapHash.
135248
case LeafPreimage:
136-
return TapLeafHash(t.SiblingPreimage)
249+
leaf, err := NewLeafFromPreimage(*t)
250+
if err != nil {
251+
return nil, err
252+
}
137253

138-
// The sibling is actually a branch pre-image, so we'll verify that the
139-
// branch pre-image is 64-bytes (the two 32-byte hashes of the left
140-
// and right nodes), and then derive the key from that.
254+
return fn.Ptr(leaf.TapHash()), nil
255+
256+
// The sibling is a branch pre-image, so we'll verify that the pre-image
257+
// is 64-bytes (the two 32-byte hashes of the left and right nodes),
258+
// and then derive the TapHash from that.
141259
case BranchPreimage:
142-
return TapBranchHash(t.SiblingPreimage)
260+
if len(t.siblingPreimage) != tapBranchPreimageLen {
261+
return nil, ErrInvalidTapscriptPreimageLen
262+
}
143263

144-
default:
145-
return nil, fmt.Errorf("unknown sibling type: <%d>",
146-
t.SiblingType)
147-
}
148-
}
264+
var left, right chainhash.Hash
265+
left = (chainhash.Hash)(t.siblingPreimage[:chainhash.HashSize])
266+
right = (chainhash.Hash)(t.siblingPreimage[chainhash.HashSize:])
149267

150-
// VerifyNoCommitment verifies that the preimage is not a Taproot Asset
151-
// commitment.
152-
func (t *TapscriptPreimage) VerifyNoCommitment() error {
153-
if IsTaprootAssetCommitmentScript(t.SiblingPreimage) {
154-
return ErrPreimageIsTapCommitment
155-
}
268+
return fn.Ptr(asset.NewTapBranchHash(left, right)), nil
156269

157-
return nil
270+
default:
271+
return nil, fmt.Errorf("%w: %d",
272+
ErrInvalidTapscriptPreimageType, t.siblingType)
273+
}
158274
}
159275

160276
// MaybeDecodeTapscriptPreimage returns the decoded preimage and hash of the
@@ -179,6 +295,8 @@ func MaybeDecodeTapscriptPreimage(encoded []byte) (*TapscriptPreimage,
179295
"preimage: %w", err)
180296
}
181297

298+
// Validate the correctness of the decoded preimage and compute its
299+
// TapHash.
182300
tapHash, err := preimage.TapHash()
183301
if err != nil {
184302
return nil, nil, fmt.Errorf("error deriving tap hash "+
@@ -214,57 +332,3 @@ func MaybeEncodeTapscriptPreimage(t *TapscriptPreimage) ([]byte,
214332

215333
return b.Bytes(), tapHash, nil
216334
}
217-
218-
// NewTapBranchHash takes the raw tap hashes of the left and right nodes and
219-
// hashes them into a branch.
220-
func NewTapBranchHash(l, r chainhash.Hash) chainhash.Hash {
221-
if bytes.Compare(l[:], r[:]) > 0 {
222-
l, r = r, l
223-
}
224-
225-
return *chainhash.TaggedHash(chainhash.TagTapBranch, l[:], r[:])
226-
}
227-
228-
// TapBranchHash computes the TapHash of a TapBranch node from its preimage
229-
// if possible, otherwise an error is returned.
230-
func TapBranchHash(preimage []byte) (*chainhash.Hash, error) {
231-
// Empty preimage or leaf preimage, return typed error.
232-
if len(preimage) != tapBranchPreimageLen {
233-
return nil, ErrInvalidTapscriptPreimageLen
234-
}
235-
236-
left := (*chainhash.Hash)(preimage[:chainhash.HashSize])
237-
right := (*chainhash.Hash)(preimage[chainhash.HashSize:])
238-
h := NewTapBranchHash(*left, *right)
239-
240-
return &h, nil
241-
}
242-
243-
// TapLeafHash computes the TapHash of a TapLeaf node from its preimage
244-
// if possible, otherwise an error is returned.
245-
func TapLeafHash(preimage []byte) (*chainhash.Hash, error) {
246-
// Empty preimage.
247-
if len(preimage) == 0 {
248-
return nil, ErrInvalidEmptyTapscriptPreimage
249-
}
250-
251-
// Enforce that it is not including another Taproot Asset commitment.
252-
if bytes.Contains(preimage, TaprootAssetsMarker[:]) {
253-
return nil, ErrInvalidTaprootProof
254-
}
255-
256-
version := txscript.TapscriptLeafVersion(preimage[0])
257-
258-
// The script is encoded with a leading VarByte that indicates its total
259-
// length.
260-
remaining := preimage[1:]
261-
script, err := wire.ReadVarBytes(
262-
bytes.NewReader(remaining), 0, uint32(len(remaining)), "script",
263-
)
264-
if err != nil {
265-
return nil, fmt.Errorf("error decoding leaf pre-image: %w", err)
266-
}
267-
268-
h := txscript.NewTapLeaf(version, script).TapHash()
269-
return &h, nil
270-
}

0 commit comments

Comments
 (0)