Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add AltLeaf support to tapfreighter #1233

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 89 additions & 20 deletions asset/asset.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,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 @@ -127,6 +130,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 @@ -250,12 +261,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 @@ -2296,13 +2301,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 @@ -2336,18 +2353,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 @@ -2375,9 +2391,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 @@ -2391,6 +2406,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 @@ -2428,4 +2482,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
Loading