Skip to content

Commit 64319ad

Browse files
jharveybguggero
authored andcommitted
commitment: support AltLeaf trimming and merging
In this commit, we add logic to handle adding and removing altLeaves from a TapCommitment. Unlike normal Assets, we never want to update an AltLeaf once inserted into the AltCommitment. This means we need to assert that AltLeaves being added to a TapCommitment don't collide with already committed AltLeaves.
1 parent 256404c commit 64319ad

File tree

2 files changed

+184
-1
lines changed

2 files changed

+184
-1
lines changed

commitment/commitment_test.go

Lines changed: 85 additions & 1 deletion
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) {

commitment/tap.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -662,3 +662,102 @@ func TrimSplitWitnesses(version *TapCommitmentVersion,
662662

663663
return tapCommitment, nil
664664
}
665+
666+
// TrimAltLeaves creates a new TapCommitment with any AltLeaves removed, if
667+
// present. The removed AltLeaves are returned separately.
668+
func TrimAltLeaves(c *TapCommitment) (*TapCommitment, []*asset.Asset, error) {
669+
altAssets, err := c.FetchAltLeaves()
670+
if err != nil {
671+
return nil, nil, fmt.Errorf("cannot trim: %w", err)
672+
}
673+
674+
// Remove the AltCommitment and reconstruct the Tap commitment.
675+
allCommitments := c.Commitments()
676+
delete(allCommitments, asset.EmptyGenesisID)
677+
678+
tapCommitment, err := NewTapCommitment(
679+
&c.Version, maps.Values(allCommitments)...,
680+
)
681+
if err != nil {
682+
return nil, nil, err
683+
}
684+
685+
return tapCommitment, altAssets, nil
686+
}
687+
688+
// FetchAltLeaves returns a copy of any AltLeaves present in the TapCommitment.
689+
func (c *TapCommitment) FetchAltLeaves() ([]*asset.Asset, error) {
690+
if c.assetCommitments == nil {
691+
return nil, errors.New("tap commitment has no leaves")
692+
}
693+
694+
altCommit := c.assetCommitments[asset.EmptyGenesisID]
695+
if altCommit == nil {
696+
return nil, nil
697+
}
698+
699+
return maps.Values(altCommit.Assets()), nil
700+
}
701+
702+
// MergeAltLeaves adds a set of AltLeaves to an existing TapCommitment. Merging
703+
// fails if the new AltLeaves collide with any existing AltLeaves.
704+
func (c *TapCommitment) MergeAltLeaves(
705+
altLeaves []asset.AltLeaf[asset.Asset]) error {
706+
707+
if len(altLeaves) == 0 {
708+
return nil
709+
}
710+
711+
// First, check that the given alt leaves have unique
712+
// AssetCommitmentKeys.
713+
newLeafKeys := asset.NewLeafKeySet()
714+
err := asset.AddLeafKeysVerifyUnique(newLeafKeys, altLeaves)
715+
if err != nil {
716+
return err
717+
}
718+
719+
// Check if any alt leaves are already present.
720+
var currentAltCommit *AssetCommitment
721+
if c.assetCommitments != nil {
722+
currentAltCommit = c.assetCommitments[asset.EmptyGenesisID]
723+
if currentAltCommit != nil {
724+
currentLeaves := currentAltCommit.Assets()
725+
726+
// If any alt leaves are already committed, new alt
727+
// leaves must not collide with existing alt leaves.
728+
for leafKey := range currentLeaves {
729+
if newLeafKeys.Contains(leafKey) {
730+
return fmt.Errorf("%w: existing alt "+
731+
"leaf: %x",
732+
asset.ErrDuplicateAltLeafKey,
733+
leafKey)
734+
}
735+
}
736+
}
737+
}
738+
739+
// None of the new or existing alt leaves collide; we can now update
740+
// the AltCommitment and Tap commitment.
741+
if currentAltCommit == nil {
742+
currentAltCommit, err = NewAssetCommitment(
743+
altLeaves[0].(*asset.Asset),
744+
)
745+
if err != nil {
746+
return err
747+
}
748+
}
749+
750+
for _, newLeaf := range altLeaves {
751+
err := currentAltCommit.Upsert(newLeaf.(*asset.Asset))
752+
if err != nil {
753+
return err
754+
}
755+
}
756+
757+
err = c.Upsert(currentAltCommit)
758+
if err != nil {
759+
return err
760+
}
761+
762+
return nil
763+
}

0 commit comments

Comments
 (0)