Skip to content

Commit

Permalink
Merge pull request lightninglabs#1233 from lightninglabs/proof_alt_le…
Browse files Browse the repository at this point in the history
…aves

Add AltLeaf support to tapfreighter
  • Loading branch information
Roasbeef authored Dec 19, 2024
2 parents bd7a6c8 + 17f4e95 commit c1badfa
Show file tree
Hide file tree
Showing 38 changed files with 2,996 additions and 1,134 deletions.
109 changes: 89 additions & 20 deletions asset/asset.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ var (
// EmptyGenesis is the empty Genesis struct used for alt leaves.
EmptyGenesis Genesis

// EmptyGenesisID is the ID of the empty genesis struct.
EmptyGenesisID = EmptyGenesis.ID()

// NUMSBytes is the NUMs point we'll use for un-spendable script keys.
// It was generated via a try-and-increment approach using the phrase
// "taproot-assets" with SHA2-256. The code for the try-and-increment
Expand All @@ -128,6 +131,14 @@ var (
// ErrUnknownVersion is returned when an asset with an unknown asset
// version is being used.
ErrUnknownVersion = errors.New("asset: unknown asset version")

// ErrUnwrapAssetID is returned when an asset ID cannot be unwrapped
// from a Specifier.
ErrUnwrapAssetID = errors.New("unable to unwrap asset ID")

// ErrDuplicateAltLeafKey is returned when a slice of AltLeaves contains
// 2 or more AltLeaves with the same AssetCommitmentKey.
ErrDuplicateAltLeafKey = errors.New("duplicate alt leaf key")
)

const (
Expand Down Expand Up @@ -241,12 +252,6 @@ func DecodeGenesis(r io.Reader) (Genesis, error) {
return gen, err
}

var (
// ErrUnwrapAssetID is an error type which is returned when an asset ID
// cannot be unwrapped from a specifier.
ErrUnwrapAssetID = errors.New("unable to unwrap asset ID")
)

// Specifier is a type that can be used to specify an asset by its ID, its asset
// group public key, or both.
type Specifier struct {
Expand Down Expand Up @@ -2794,13 +2799,25 @@ type ChainAsset struct {
AnchorLeaseExpiry *time.Time
}

// LeafKeySet is a set of leaf keys.
type LeafKeySet = fn.Set[[32]byte]

// NewLeafKeySet creates a new leaf key set.
func NewLeafKeySet() LeafKeySet {
return fn.NewSet[[32]byte]()
}

// An AltLeaf is a type that is used to carry arbitrary data, and does not
// represent a Taproot asset. An AltLeaf can be used to anchor other protocols
// alongside Taproot Asset transactions.
type AltLeaf[T any] interface {
// Copyable asserts that the target type of this interface satisfies
// the Copyable interface.
fn.Copyable[T]
fn.Copyable[*T]

// AssetCommitmentKey is the key for an AltLeaf within an
// AssetCommitment.
AssetCommitmentKey() [32]byte

// ValidateAltLeaf ensures that an AltLeaf is valid.
ValidateAltLeaf() error
Expand Down Expand Up @@ -2834,18 +2851,17 @@ func NewAltLeaf(key ScriptKey, keyVersion ScriptVersion,
}, nil
}

// CopyAltLeaf performs a deep copy of an AltLeaf.
func CopyAltLeaf[T AltLeaf[T]](a AltLeaf[T]) AltLeaf[T] {
return a.Copy()
}

// CopyAltLeaves performs a deep copy of an AltLeaf slice.
func CopyAltLeaves[T AltLeaf[T]](a []AltLeaf[T]) []AltLeaf[T] {
return fn.Map(a, CopyAltLeaf[T])
func CopyAltLeaves(a []AltLeaf[Asset]) []AltLeaf[Asset] {
if len(a) == 0 {
return nil
}

return ToAltLeaves(fn.CopyAll(FromAltLeaves(a)))
}

// Validate checks that an Asset is a valid AltLeaf. An Asset used as an AltLeaf
// must meet these constraints:
// ValidateAltLeaf checks that an Asset is a valid AltLeaf. An Asset used as an
// AltLeaf must meet these constraints:
// - Version must be V0.
// - Genesis must be the empty Genesis.
// - Amount, LockTime, and RelativeLockTime must be 0.
Expand Down Expand Up @@ -2873,9 +2889,8 @@ func (a *Asset) ValidateAltLeaf() error {
}

if a.SplitCommitmentRoot != nil {
return fmt.Errorf(
"alt leaf split commitment root must be empty",
)
return fmt.Errorf("alt leaf split commitment root must be " +
"empty")
}

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

// ValidAltLeaves checks that a set of Assets are valid AltLeaves, and can be
// used to construct an AltCommitment. This requires that each AltLeaf has a
// unique AssetCommitmentKey.
func ValidAltLeaves(leaves []AltLeaf[Asset]) error {
leafKeys := NewLeafKeySet()
return AddLeafKeysVerifyUnique(leafKeys, leaves)
}

// AddLeafKeysVerifyUnique checks that a set of Assets are valid AltLeaves, and
// have unique AssetCommitmentKeys (unique among the given slice but also not
// colliding with any of the keys in the existingKeys set). If the leaves are
// valid, the function returns the updated set of keys.
func AddLeafKeysVerifyUnique(existingKeys LeafKeySet,
leaves []AltLeaf[Asset]) error {

for _, leaf := range leaves {
err := leaf.ValidateAltLeaf()
if err != nil {
return err
}

leafKey := leaf.AssetCommitmentKey()
if existingKeys.Contains(leafKey) {
return fmt.Errorf("%w: %x", ErrDuplicateAltLeafKey,
leafKey)
}

existingKeys.Add(leafKey)
}

return nil
}

// IsAltLeaf returns true if an Asset would be stored in the AltCommitment of
// a TapCommitment. It does not check if the Asset is a valid AltLeaf.
func (a *Asset) IsAltLeaf() bool {
return a.GroupKey == nil && a.Genesis == EmptyGenesis
}

// encodeAltLeafRecords determines the set of non-nil records to include when
// encoding an AltLeaf. Since the Genesis, Group Key, Amount, and Version fields
// are static, we can omit those fields.
Expand Down Expand Up @@ -2926,4 +2980,19 @@ func (a *Asset) DecodeAltLeaf(r io.Reader) error {
}

// Ensure Asset implements the AltLeaf interface.
var _ AltLeaf[*Asset] = (*Asset)(nil)
var _ AltLeaf[Asset] = (*Asset)(nil)

// ToAltLeaves casts []Asset to []AltLeafAsset, without checking that the assets
// are valid AltLeaves.
func ToAltLeaves(leaves []*Asset) []AltLeaf[Asset] {
return fn.Map(leaves, func(l *Asset) AltLeaf[Asset] {
return l
})
}

// FromAltLeaves casts []AltLeafAsset to []Asset, which is always safe.
func FromAltLeaves(leaves []AltLeaf[Asset]) []*Asset {
return fn.Map(leaves, func(l AltLeaf[Asset]) *Asset {
return l.(*Asset)
})
}
11 changes: 7 additions & 4 deletions asset/encoding.go
Original file line number Diff line number Diff line change
Expand Up @@ -809,7 +809,10 @@ func DecodeTapLeaf(leafData []byte) (*txscript.TapLeaf, error) {
}

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

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

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

leafKeys[leafKey] = struct{}{}
leaves = append(leaves, AltLeaf[*Asset](&leaf))
leaves = append(leaves, AltLeaf[Asset](&leaf))
}

*typ = leaves
Expand Down
58 changes: 58 additions & 0 deletions asset/mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"crypto/sha256"
"encoding/hex"
"fmt"
"slices"
"testing"

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

// SortFunc is used to sort assets lexicographically by their script keys.
func SortFunc(a, b *Asset) int {
return bytes.Compare(
a.ScriptKey.PubKey.SerializeCompressed(),
b.ScriptKey.PubKey.SerializeCompressed(),
)
}

// RandGenesis creates a random genesis for testing.
func RandGenesis(t testing.TB, assetType Type) Genesis {
t.Helper()
Expand Down Expand Up @@ -660,6 +669,55 @@ func RandAssetWithValues(t testing.TB, genesis Genesis, groupKey *GroupKey,
)
}

// RandAltLeaf generates a random Asset that is a valid AltLeaf.
func RandAltLeaf(t testing.TB) *Asset {
randWitness := []Witness{
{TxWitness: test.RandTxWitnesses(t)},
}
randKey := RandScriptKey(t)
randVersion := ScriptVersion(test.RandInt[uint16]())
randLeaf, err := NewAltLeaf(randKey, randVersion, randWitness)
require.NoError(t, err)
require.NoError(t, randLeaf.ValidateAltLeaf())

return randLeaf
}

// RandAltLeaves generates a random number of random alt leaves.
func RandAltLeaves(t testing.TB, nonZero bool) []*Asset {
// Limit the number of leaves to keep test vectors small.
maxLeaves := 4
numLeaves := test.RandIntn(maxLeaves)
if nonZero {
numLeaves += 1
}

if numLeaves == 0 {
return nil
}

altLeaves := make([]*Asset, numLeaves)
for idx := range numLeaves {
altLeaves[idx] = RandAltLeaf(t)
}

return altLeaves
}

// CompareAltLeaves compares two slices of AltLeafAssets for equality.
func CompareAltLeaves(t *testing.T, a, b []AltLeaf[Asset]) {
require.Equal(t, len(a), len(b))

aInner := FromAltLeaves(a)
bInner := FromAltLeaves(b)

slices.SortStableFunc(aInner, SortFunc)
slices.SortStableFunc(bInner, SortFunc)
for idx := range aInner {
require.True(t, aInner[idx].DeepEqual(bInner[idx]))
}
}

type ValidTestCase struct {
Asset *TestAsset `json:"asset"`
Expected string `json:"expected"`
Expand Down
86 changes: 85 additions & 1 deletion commitment/commitment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1164,7 +1164,7 @@ func TestUpdateTapCommitment(t *testing.T) {
groupKey1 := asset.RandGroupKey(t, genesis1, protoAsset1)
groupKey2 := asset.RandGroupKey(t, genesis2, protoAsset2)

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

// TestTapCommitmentAltLeaves asserts that we can properly fetch, trim, and
// merge alt leaves to and from a TapCommitment.
func TestTapCommitmentAltLeaves(t *testing.T) {
t.Parallel()

// Create two random assets, to populate our Tap commitment.
asset1 := asset.RandAsset(t, asset.Normal)
asset2 := asset.RandAsset(t, asset.Collectible)

// We'll create three AltLeaves. Leaves 1 and 2 are valid, and leaf 3
// will collide with leaf 1.
leaf1 := asset.RandAltLeaf(t)
leaf2 := asset.RandAltLeaf(t)
leaf3 := asset.RandAltLeaf(t)
leaf3.ScriptKey.PubKey = leaf1.ScriptKey.PubKey
leaf4 := asset.RandAltLeaf(t)

// Create our initial, asset-only, Tap commitment.
commitment, err := FromAssets(nil, asset1, asset2)
require.NoError(t, err)
assetOnlyTapLeaf := commitment.TapLeaf()

// If we try to trim any alt leaves, we should get none back.
_, altLeaves, err := TrimAltLeaves(commitment)
require.NoError(t, err)
require.Empty(t, altLeaves)

// Trying to merge colliding alt leaves should fail.
err = commitment.MergeAltLeaves([]asset.AltLeaf[asset.Asset]{
leaf1, leaf3,
})
require.ErrorIs(t, err, asset.ErrDuplicateAltLeafKey)

// Merging non-colliding, valid alt leaves should succeed. The new
// commitment should contain three AssetCommitments, since we've created
// an AltCommitment.
err = commitment.MergeAltLeaves([]asset.AltLeaf[asset.Asset]{
leaf1, leaf2,
})
require.NoError(t, err)
require.Len(t, commitment.assetCommitments, 3)

// Trying to merge an alt leaf that will collide with an existing leaf
// should also fail.
err = commitment.MergeAltLeaves([]asset.AltLeaf[asset.Asset]{leaf3})
require.ErrorIs(t, err, asset.ErrDuplicateAltLeafKey)

// Merging a valid, non-colliding, new alt leaf into an existing
// AltCommitment should succeed.
err = commitment.MergeAltLeaves([]asset.AltLeaf[asset.Asset]{leaf4})
require.NoError(t, err)

// If we fetch the alt leaves, they should not be removed from the
// commitment.
finalTapLeaf := commitment.TapLeaf()
fetchedAltLeaves, err := commitment.FetchAltLeaves()
require.NoError(t, err)
require.Equal(t, finalTapLeaf, commitment.TapLeaf())
insertedAltLeaves := []*asset.Asset{leaf1, leaf2, leaf4}

// The fetched leaves must be equal to the three leaves we successfully
// inserted.
asset.CompareAltLeaves(
t, asset.ToAltLeaves(insertedAltLeaves),
asset.ToAltLeaves(fetchedAltLeaves),
)

// Now, if we trim out the alt leaves, the AltCommitment should be fully
// removed.
originalCommitment, _, err := TrimAltLeaves(commitment)
require.NoError(t, err)

trimmedTapLeaf := originalCommitment.TapLeaf()
require.NotEqual(t, finalTapLeaf, trimmedTapLeaf)
require.Equal(t, assetOnlyTapLeaf, trimmedTapLeaf)

// The trimmed leaves should match the leaves we successfully merged
// into the commitment.
asset.CompareAltLeaves(
t, asset.ToAltLeaves(fetchedAltLeaves),
asset.ToAltLeaves(insertedAltLeaves),
)
}

// TestAssetCommitmentDeepCopy tests that we're able to properly perform a deep
// copy of a given asset commitment.
func TestAssetCommitmentDeepCopy(t *testing.T) {
Expand Down
Loading

0 comments on commit c1badfa

Please sign in to comment.