Skip to content

Commit c1badfa

Browse files
authored
Merge pull request #1233 from lightninglabs/proof_alt_leaves
Add AltLeaf support to tapfreighter
2 parents bd7a6c8 + 17f4e95 commit c1badfa

38 files changed

+2996
-1134
lines changed

asset/asset.go

+89-20
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,9 @@ var (
110110
// EmptyGenesis is the empty Genesis struct used for alt leaves.
111111
EmptyGenesis Genesis
112112

113+
// EmptyGenesisID is the ID of the empty genesis struct.
114+
EmptyGenesisID = EmptyGenesis.ID()
115+
113116
// NUMSBytes is the NUMs point we'll use for un-spendable script keys.
114117
// It was generated via a try-and-increment approach using the phrase
115118
// "taproot-assets" with SHA2-256. The code for the try-and-increment
@@ -128,6 +131,14 @@ var (
128131
// ErrUnknownVersion is returned when an asset with an unknown asset
129132
// version is being used.
130133
ErrUnknownVersion = errors.New("asset: unknown asset version")
134+
135+
// ErrUnwrapAssetID is returned when an asset ID cannot be unwrapped
136+
// from a Specifier.
137+
ErrUnwrapAssetID = errors.New("unable to unwrap asset ID")
138+
139+
// ErrDuplicateAltLeafKey is returned when a slice of AltLeaves contains
140+
// 2 or more AltLeaves with the same AssetCommitmentKey.
141+
ErrDuplicateAltLeafKey = errors.New("duplicate alt leaf key")
131142
)
132143

133144
const (
@@ -241,12 +252,6 @@ func DecodeGenesis(r io.Reader) (Genesis, error) {
241252
return gen, err
242253
}
243254

244-
var (
245-
// ErrUnwrapAssetID is an error type which is returned when an asset ID
246-
// cannot be unwrapped from a specifier.
247-
ErrUnwrapAssetID = errors.New("unable to unwrap asset ID")
248-
)
249-
250255
// Specifier is a type that can be used to specify an asset by its ID, its asset
251256
// group public key, or both.
252257
type Specifier struct {
@@ -2794,13 +2799,25 @@ type ChainAsset struct {
27942799
AnchorLeaseExpiry *time.Time
27952800
}
27962801

2802+
// LeafKeySet is a set of leaf keys.
2803+
type LeafKeySet = fn.Set[[32]byte]
2804+
2805+
// NewLeafKeySet creates a new leaf key set.
2806+
func NewLeafKeySet() LeafKeySet {
2807+
return fn.NewSet[[32]byte]()
2808+
}
2809+
27972810
// An AltLeaf is a type that is used to carry arbitrary data, and does not
27982811
// represent a Taproot asset. An AltLeaf can be used to anchor other protocols
27992812
// alongside Taproot Asset transactions.
28002813
type AltLeaf[T any] interface {
28012814
// Copyable asserts that the target type of this interface satisfies
28022815
// the Copyable interface.
2803-
fn.Copyable[T]
2816+
fn.Copyable[*T]
2817+
2818+
// AssetCommitmentKey is the key for an AltLeaf within an
2819+
// AssetCommitment.
2820+
AssetCommitmentKey() [32]byte
28042821

28052822
// ValidateAltLeaf ensures that an AltLeaf is valid.
28062823
ValidateAltLeaf() error
@@ -2834,18 +2851,17 @@ func NewAltLeaf(key ScriptKey, keyVersion ScriptVersion,
28342851
}, nil
28352852
}
28362853

2837-
// CopyAltLeaf performs a deep copy of an AltLeaf.
2838-
func CopyAltLeaf[T AltLeaf[T]](a AltLeaf[T]) AltLeaf[T] {
2839-
return a.Copy()
2840-
}
2841-
28422854
// CopyAltLeaves performs a deep copy of an AltLeaf slice.
2843-
func CopyAltLeaves[T AltLeaf[T]](a []AltLeaf[T]) []AltLeaf[T] {
2844-
return fn.Map(a, CopyAltLeaf[T])
2855+
func CopyAltLeaves(a []AltLeaf[Asset]) []AltLeaf[Asset] {
2856+
if len(a) == 0 {
2857+
return nil
2858+
}
2859+
2860+
return ToAltLeaves(fn.CopyAll(FromAltLeaves(a)))
28452861
}
28462862

2847-
// Validate checks that an Asset is a valid AltLeaf. An Asset used as an AltLeaf
2848-
// must meet these constraints:
2863+
// ValidateAltLeaf checks that an Asset is a valid AltLeaf. An Asset used as an
2864+
// AltLeaf must meet these constraints:
28492865
// - Version must be V0.
28502866
// - Genesis must be the empty Genesis.
28512867
// - Amount, LockTime, and RelativeLockTime must be 0.
@@ -2873,9 +2889,8 @@ func (a *Asset) ValidateAltLeaf() error {
28732889
}
28742890

28752891
if a.SplitCommitmentRoot != nil {
2876-
return fmt.Errorf(
2877-
"alt leaf split commitment root must be empty",
2878-
)
2892+
return fmt.Errorf("alt leaf split commitment root must be " +
2893+
"empty")
28792894
}
28802895

28812896
if a.GroupKey != nil {
@@ -2889,6 +2904,45 @@ func (a *Asset) ValidateAltLeaf() error {
28892904
return nil
28902905
}
28912906

2907+
// ValidAltLeaves checks that a set of Assets are valid AltLeaves, and can be
2908+
// used to construct an AltCommitment. This requires that each AltLeaf has a
2909+
// unique AssetCommitmentKey.
2910+
func ValidAltLeaves(leaves []AltLeaf[Asset]) error {
2911+
leafKeys := NewLeafKeySet()
2912+
return AddLeafKeysVerifyUnique(leafKeys, leaves)
2913+
}
2914+
2915+
// AddLeafKeysVerifyUnique checks that a set of Assets are valid AltLeaves, and
2916+
// have unique AssetCommitmentKeys (unique among the given slice but also not
2917+
// colliding with any of the keys in the existingKeys set). If the leaves are
2918+
// valid, the function returns the updated set of keys.
2919+
func AddLeafKeysVerifyUnique(existingKeys LeafKeySet,
2920+
leaves []AltLeaf[Asset]) error {
2921+
2922+
for _, leaf := range leaves {
2923+
err := leaf.ValidateAltLeaf()
2924+
if err != nil {
2925+
return err
2926+
}
2927+
2928+
leafKey := leaf.AssetCommitmentKey()
2929+
if existingKeys.Contains(leafKey) {
2930+
return fmt.Errorf("%w: %x", ErrDuplicateAltLeafKey,
2931+
leafKey)
2932+
}
2933+
2934+
existingKeys.Add(leafKey)
2935+
}
2936+
2937+
return nil
2938+
}
2939+
2940+
// IsAltLeaf returns true if an Asset would be stored in the AltCommitment of
2941+
// a TapCommitment. It does not check if the Asset is a valid AltLeaf.
2942+
func (a *Asset) IsAltLeaf() bool {
2943+
return a.GroupKey == nil && a.Genesis == EmptyGenesis
2944+
}
2945+
28922946
// encodeAltLeafRecords determines the set of non-nil records to include when
28932947
// encoding an AltLeaf. Since the Genesis, Group Key, Amount, and Version fields
28942948
// are static, we can omit those fields.
@@ -2926,4 +2980,19 @@ func (a *Asset) DecodeAltLeaf(r io.Reader) error {
29262980
}
29272981

29282982
// Ensure Asset implements the AltLeaf interface.
2929-
var _ AltLeaf[*Asset] = (*Asset)(nil)
2983+
var _ AltLeaf[Asset] = (*Asset)(nil)
2984+
2985+
// ToAltLeaves casts []Asset to []AltLeafAsset, without checking that the assets
2986+
// are valid AltLeaves.
2987+
func ToAltLeaves(leaves []*Asset) []AltLeaf[Asset] {
2988+
return fn.Map(leaves, func(l *Asset) AltLeaf[Asset] {
2989+
return l
2990+
})
2991+
}
2992+
2993+
// FromAltLeaves casts []AltLeafAsset to []Asset, which is always safe.
2994+
func FromAltLeaves(leaves []AltLeaf[Asset]) []*Asset {
2995+
return fn.Map(leaves, func(l AltLeaf[Asset]) *Asset {
2996+
return l.(*Asset)
2997+
})
2998+
}

asset/encoding.go

+7-4
Original file line numberDiff line numberDiff line change
@@ -809,7 +809,10 @@ func DecodeTapLeaf(leafData []byte) (*txscript.TapLeaf, error) {
809809
}
810810

811811
func AltLeavesEncoder(w io.Writer, val any, buf *[8]byte) error {
812-
if t, ok := val.(*[]AltLeaf[*Asset]); ok {
812+
if t, ok := val.(*[]AltLeaf[Asset]); ok {
813+
// If the AltLeaves slice is empty, we will still encode its
814+
// length here (as 0). Callers should avoid encoding empty
815+
// AltLeaves slices.
813816
if err := tlv.WriteVarInt(w, uint64(len(*t)), buf); err != nil {
814817
return err
815818
}
@@ -852,7 +855,7 @@ func AltLeavesDecoder(r io.Reader, val any, buf *[8]byte, l uint64) error {
852855
return tlv.ErrRecordTooLarge
853856
}
854857

855-
if typ, ok := val.(*[]AltLeaf[*Asset]); ok {
858+
if typ, ok := val.(*[]AltLeaf[Asset]); ok {
856859
// Each alt leaf is at least 42 bytes, which limits the total
857860
// number of aux leaves. So we don't need to enforce a strict
858861
// limit here.
@@ -861,7 +864,7 @@ func AltLeavesDecoder(r io.Reader, val any, buf *[8]byte, l uint64) error {
861864
return err
862865
}
863866

864-
leaves := make([]AltLeaf[*Asset], 0, numItems)
867+
leaves := make([]AltLeaf[Asset], 0, numItems)
865868
leafKeys := make(map[SerializedKey]struct{})
866869
for i := uint64(0); i < numItems; i++ {
867870
var streamBytes []byte
@@ -887,7 +890,7 @@ func AltLeavesDecoder(r io.Reader, val any, buf *[8]byte, l uint64) error {
887890
}
888891

889892
leafKeys[leafKey] = struct{}{}
890-
leaves = append(leaves, AltLeaf[*Asset](&leaf))
893+
leaves = append(leaves, AltLeaf[Asset](&leaf))
891894
}
892895

893896
*typ = leaves

asset/mock.go

+58
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"crypto/sha256"
77
"encoding/hex"
88
"fmt"
9+
"slices"
910
"testing"
1011

1112
"github.com/btcsuite/btcd/btcec/v2"
@@ -151,6 +152,14 @@ func CheckAssetAsserts(a *Asset, checks ...AssetAssert) error {
151152
return nil
152153
}
153154

155+
// SortFunc is used to sort assets lexicographically by their script keys.
156+
func SortFunc(a, b *Asset) int {
157+
return bytes.Compare(
158+
a.ScriptKey.PubKey.SerializeCompressed(),
159+
b.ScriptKey.PubKey.SerializeCompressed(),
160+
)
161+
}
162+
154163
// RandGenesis creates a random genesis for testing.
155164
func RandGenesis(t testing.TB, assetType Type) Genesis {
156165
t.Helper()
@@ -660,6 +669,55 @@ func RandAssetWithValues(t testing.TB, genesis Genesis, groupKey *GroupKey,
660669
)
661670
}
662671

672+
// RandAltLeaf generates a random Asset that is a valid AltLeaf.
673+
func RandAltLeaf(t testing.TB) *Asset {
674+
randWitness := []Witness{
675+
{TxWitness: test.RandTxWitnesses(t)},
676+
}
677+
randKey := RandScriptKey(t)
678+
randVersion := ScriptVersion(test.RandInt[uint16]())
679+
randLeaf, err := NewAltLeaf(randKey, randVersion, randWitness)
680+
require.NoError(t, err)
681+
require.NoError(t, randLeaf.ValidateAltLeaf())
682+
683+
return randLeaf
684+
}
685+
686+
// RandAltLeaves generates a random number of random alt leaves.
687+
func RandAltLeaves(t testing.TB, nonZero bool) []*Asset {
688+
// Limit the number of leaves to keep test vectors small.
689+
maxLeaves := 4
690+
numLeaves := test.RandIntn(maxLeaves)
691+
if nonZero {
692+
numLeaves += 1
693+
}
694+
695+
if numLeaves == 0 {
696+
return nil
697+
}
698+
699+
altLeaves := make([]*Asset, numLeaves)
700+
for idx := range numLeaves {
701+
altLeaves[idx] = RandAltLeaf(t)
702+
}
703+
704+
return altLeaves
705+
}
706+
707+
// CompareAltLeaves compares two slices of AltLeafAssets for equality.
708+
func CompareAltLeaves(t *testing.T, a, b []AltLeaf[Asset]) {
709+
require.Equal(t, len(a), len(b))
710+
711+
aInner := FromAltLeaves(a)
712+
bInner := FromAltLeaves(b)
713+
714+
slices.SortStableFunc(aInner, SortFunc)
715+
slices.SortStableFunc(bInner, SortFunc)
716+
for idx := range aInner {
717+
require.True(t, aInner[idx].DeepEqual(bInner[idx]))
718+
}
719+
}
720+
663721
type ValidTestCase struct {
664722
Asset *TestAsset `json:"asset"`
665723
Expected string `json:"expected"`

commitment/commitment_test.go

+85-1
Original file line numberDiff line numberDiff line change
@@ -1164,7 +1164,7 @@ func TestUpdateTapCommitment(t *testing.T) {
11641164
groupKey1 := asset.RandGroupKey(t, genesis1, protoAsset1)
11651165
groupKey2 := asset.RandGroupKey(t, genesis2, protoAsset2)
11661166

1167-
// We also create a thirds asset which is in the same group as the first
1167+
// We also create a third asset which is in the same group as the first
11681168
// one, to ensure that we can properly create Taproot Asset commitments
11691169
// from asset commitments of the same group.
11701170
genesis3 := asset.RandGenesis(t, asset.Normal)
@@ -1316,6 +1316,90 @@ func TestUpdateTapCommitment(t *testing.T) {
13161316
)
13171317
}
13181318

1319+
// TestTapCommitmentAltLeaves asserts that we can properly fetch, trim, and
1320+
// merge alt leaves to and from a TapCommitment.
1321+
func TestTapCommitmentAltLeaves(t *testing.T) {
1322+
t.Parallel()
1323+
1324+
// Create two random assets, to populate our Tap commitment.
1325+
asset1 := asset.RandAsset(t, asset.Normal)
1326+
asset2 := asset.RandAsset(t, asset.Collectible)
1327+
1328+
// We'll create three AltLeaves. Leaves 1 and 2 are valid, and leaf 3
1329+
// will collide with leaf 1.
1330+
leaf1 := asset.RandAltLeaf(t)
1331+
leaf2 := asset.RandAltLeaf(t)
1332+
leaf3 := asset.RandAltLeaf(t)
1333+
leaf3.ScriptKey.PubKey = leaf1.ScriptKey.PubKey
1334+
leaf4 := asset.RandAltLeaf(t)
1335+
1336+
// Create our initial, asset-only, Tap commitment.
1337+
commitment, err := FromAssets(nil, asset1, asset2)
1338+
require.NoError(t, err)
1339+
assetOnlyTapLeaf := commitment.TapLeaf()
1340+
1341+
// If we try to trim any alt leaves, we should get none back.
1342+
_, altLeaves, err := TrimAltLeaves(commitment)
1343+
require.NoError(t, err)
1344+
require.Empty(t, altLeaves)
1345+
1346+
// Trying to merge colliding alt leaves should fail.
1347+
err = commitment.MergeAltLeaves([]asset.AltLeaf[asset.Asset]{
1348+
leaf1, leaf3,
1349+
})
1350+
require.ErrorIs(t, err, asset.ErrDuplicateAltLeafKey)
1351+
1352+
// Merging non-colliding, valid alt leaves should succeed. The new
1353+
// commitment should contain three AssetCommitments, since we've created
1354+
// an AltCommitment.
1355+
err = commitment.MergeAltLeaves([]asset.AltLeaf[asset.Asset]{
1356+
leaf1, leaf2,
1357+
})
1358+
require.NoError(t, err)
1359+
require.Len(t, commitment.assetCommitments, 3)
1360+
1361+
// Trying to merge an alt leaf that will collide with an existing leaf
1362+
// should also fail.
1363+
err = commitment.MergeAltLeaves([]asset.AltLeaf[asset.Asset]{leaf3})
1364+
require.ErrorIs(t, err, asset.ErrDuplicateAltLeafKey)
1365+
1366+
// Merging a valid, non-colliding, new alt leaf into an existing
1367+
// AltCommitment should succeed.
1368+
err = commitment.MergeAltLeaves([]asset.AltLeaf[asset.Asset]{leaf4})
1369+
require.NoError(t, err)
1370+
1371+
// If we fetch the alt leaves, they should not be removed from the
1372+
// commitment.
1373+
finalTapLeaf := commitment.TapLeaf()
1374+
fetchedAltLeaves, err := commitment.FetchAltLeaves()
1375+
require.NoError(t, err)
1376+
require.Equal(t, finalTapLeaf, commitment.TapLeaf())
1377+
insertedAltLeaves := []*asset.Asset{leaf1, leaf2, leaf4}
1378+
1379+
// The fetched leaves must be equal to the three leaves we successfully
1380+
// inserted.
1381+
asset.CompareAltLeaves(
1382+
t, asset.ToAltLeaves(insertedAltLeaves),
1383+
asset.ToAltLeaves(fetchedAltLeaves),
1384+
)
1385+
1386+
// Now, if we trim out the alt leaves, the AltCommitment should be fully
1387+
// removed.
1388+
originalCommitment, _, err := TrimAltLeaves(commitment)
1389+
require.NoError(t, err)
1390+
1391+
trimmedTapLeaf := originalCommitment.TapLeaf()
1392+
require.NotEqual(t, finalTapLeaf, trimmedTapLeaf)
1393+
require.Equal(t, assetOnlyTapLeaf, trimmedTapLeaf)
1394+
1395+
// The trimmed leaves should match the leaves we successfully merged
1396+
// into the commitment.
1397+
asset.CompareAltLeaves(
1398+
t, asset.ToAltLeaves(fetchedAltLeaves),
1399+
asset.ToAltLeaves(insertedAltLeaves),
1400+
)
1401+
}
1402+
13191403
// TestAssetCommitmentDeepCopy tests that we're able to properly perform a deep
13201404
// copy of a given asset commitment.
13211405
func TestAssetCommitmentDeepCopy(t *testing.T) {

0 commit comments

Comments
 (0)