Skip to content

Commit 5d667f4

Browse files
authored
Merge pull request #1599 from lightninglabs/wip/asset-group-equality-checks
asset: tighten GroupKey equality checks and update tests
2 parents cc7c146 + 7643f33 commit 5d667f4

File tree

3 files changed

+167
-92
lines changed

3 files changed

+167
-92
lines changed

asset/group_key.go

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -975,6 +975,40 @@ func GroupPubKeyV0(rawKey *btcec.PublicKey, singleTweak, tapTweak []byte) (
975975
}
976976
}
977977

978+
// IsEqualCustomTapscriptRoot reports whether the receiver and the provided
979+
// GroupKey define the same custom tapscript root.
980+
//
981+
// Equality semantics:
982+
// 1. If neither key sets a custom tapscript root (both options are None),
983+
// the keys are considered equal.
984+
// 2. If both keys set a custom tapscript root (both options are Some), the
985+
// underlying root hashes must be identical.
986+
// 3. If the presence bits differ (one Some, the other None) the keys are
987+
// unequal.
988+
//
989+
// Fields other than CustomTapscriptRoot are intentionally ignored.
990+
func (g *GroupKey) IsEqualCustomTapscriptRoot(otherGroupKey *GroupKey) bool {
991+
// If the two presence flags differ, the roots cannot be equal.
992+
if g.CustomTapscriptRoot.IsSome() !=
993+
otherGroupKey.CustomTapscriptRoot.IsSome() {
994+
995+
return false
996+
}
997+
998+
// Both option flags are None -> neither key specifies a custom root;
999+
// they match trivially.
1000+
if g.CustomTapscriptRoot.IsNone() {
1001+
return true
1002+
}
1003+
1004+
// At this point both option flags are Some. Compare the underlying
1005+
// hashes.
1006+
root := g.CustomTapscriptRoot.UnwrapToPtr()
1007+
otherRoot := otherGroupKey.CustomTapscriptRoot.UnwrapToPtr()
1008+
1009+
return root.IsEqual(otherRoot)
1010+
}
1011+
9781012
// IsEqual returns true if this group key and signature are exactly equivalent
9791013
// to the passed other group key.
9801014
func (g *GroupKey) IsEqual(otherGroupKey *GroupKey) bool {
@@ -986,7 +1020,11 @@ func (g *GroupKey) IsEqual(otherGroupKey *GroupKey) bool {
9861020
return false
9871021
}
9881022

989-
equalGroup := g.IsEqualGroup(otherGroupKey)
1023+
if g.Version != otherGroupKey.Version {
1024+
return false
1025+
}
1026+
1027+
equalGroup := g.IsSameGroup(otherGroupKey)
9901028
if !equalGroup {
9911029
return false
9921030
}
@@ -995,16 +1033,20 @@ func (g *GroupKey) IsEqual(otherGroupKey *GroupKey) bool {
9951033
return false
9961034
}
9971035

1036+
if !g.IsEqualCustomTapscriptRoot(otherGroupKey) {
1037+
return false
1038+
}
1039+
9981040
if len(g.Witness) != len(otherGroupKey.Witness) {
9991041
return false
10001042
}
10011043

10021044
return slices.EqualFunc(g.Witness, otherGroupKey.Witness, bytes.Equal)
10031045
}
10041046

1005-
// IsEqualGroup returns true if this group key describes the same asset group
1006-
// as the passed other group key.
1007-
func (g *GroupKey) IsEqualGroup(otherGroupKey *GroupKey) bool {
1047+
// IsSameGroup returns true if this group key refers to the same asset group
1048+
// as the given group key.
1049+
func (g *GroupKey) IsSameGroup(otherGroupKey *GroupKey) bool {
10081050
// If this key is nil, the other must be nil too.
10091051
if g == nil {
10101052
return otherGroupKey == nil

commitment/asset.go

Lines changed: 35 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -89,42 +89,39 @@ type AssetCommitment struct {
8989
// parseCommon extracts the common fixed parameters of a set of assets to
9090
// include in the returned commitment.
9191
func parseCommon(assets ...*asset.Asset) (*AssetCommitment, error) {
92+
// Return early if no assets were provided. After this point, we
93+
// assume that at least one asset is present.
9294
if len(assets) == 0 {
9395
return nil, ErrNoAssets
9496
}
9597

98+
// Inspect the first asset to note properties which should be consistent
99+
// across all assets.
96100
var (
97-
assetType asset.Type
98-
tapCommitmentKey [32]byte
99-
maxVersion = asset.Version(0)
100-
firstAssetID = assets[0].Genesis.ID()
101-
assetGroupKey = assets[0].GroupKey
102-
assetsMap = make(CommittedAssets, len(assets))
101+
firstAsset = assets[0]
102+
103+
expectedAssetType = firstAsset.Type
104+
expectedTapCommitmentKey = firstAsset.TapCommitmentKey()
105+
expectedAssetID = firstAsset.Genesis.ID()
106+
expectedGroupKey = firstAsset.GroupKey
103107
)
104-
for idx, newAsset := range assets {
105-
// Inspect the first asset to note properties which should be
106-
// consistent across all assets.
107-
if idx == 0 {
108-
// Set the asset type from the first asset.
109-
assetType = newAsset.Type
110-
111-
// Set the expected tapCommitmentKey from the first
112-
// asset.
113-
tapCommitmentKey = newAsset.TapCommitmentKey()
114-
}
108+
109+
// Ensure all assets share the same properties as the first.
110+
for idx := range assets {
111+
newAsset := assets[idx]
115112

116113
// Return error if the asset type doesn't match the previously
117114
// encountered asset types.
118-
if assetType != newAsset.Type {
115+
if expectedAssetType != newAsset.Type {
119116
return nil, ErrAssetTypeMismatch
120117
}
121118

122119
switch {
123-
case !assetGroupKey.IsEqualGroup(newAsset.GroupKey):
120+
case !expectedGroupKey.IsSameGroup(newAsset.GroupKey):
124121
return nil, ErrAssetGroupKeyMismatch
125122

126-
case assetGroupKey == nil:
127-
if firstAssetID != newAsset.Genesis.ID() {
123+
case expectedGroupKey == nil:
124+
if expectedAssetID != newAsset.Genesis.ID() {
128125
return nil, ErrAssetGenesisMismatch
129126
}
130127
}
@@ -134,36 +131,49 @@ func parseCommon(assets ...*asset.Asset) (*AssetCommitment, error) {
134131
//
135132
// NOTE: This sanity check executes after the group key check
136133
// because it is a less specific check.
137-
if tapCommitmentKey != newAsset.TapCommitmentKey() {
134+
if expectedTapCommitmentKey != newAsset.TapCommitmentKey() {
138135
return nil, fmt.Errorf("inconsistent asset " +
139136
"TapCommitmentKey")
140137
}
138+
}
139+
140+
// Construct a map from AssetCommitmentKey to asset, validating that
141+
// each AssetCommitmentKey value is unique as the map is populated.
142+
assetsMap := make(CommittedAssets, len(assets))
143+
for idx := range assets {
144+
newAsset := assets[idx]
141145

142146
key := newAsset.AssetCommitmentKey()
143147
if _, ok := assetsMap[key]; ok {
144148
return nil, fmt.Errorf("%w: %x",
145149
ErrAssetDuplicateScriptKey, key[:])
146150
}
151+
assetsMap[key] = newAsset
152+
}
153+
154+
// Determine the maximum asset version among all assets in the set.
155+
maxVersion := asset.Version(0)
156+
for idx := range assets {
157+
newAsset := assets[idx]
147158
if newAsset.Version > maxVersion {
148159
maxVersion = newAsset.Version
149160
}
150-
assetsMap[key] = newAsset
151161
}
152162

153163
// The tapKey here is what will be used to place this asset commitment
154164
// into the top-level Taproot Asset commitment. For assets without a
155165
// group key, then this will be the normal asset ID. Otherwise, this'll
156166
// be the sha256 of the group key.
157167
assetSpecifier := asset.NewSpecifierOptionalGroupKey(
158-
firstAssetID, assetGroupKey,
168+
expectedAssetID, expectedGroupKey,
159169
)
160170

161171
tapKey := asset.TapCommitmentKey(assetSpecifier)
162172

163173
return &AssetCommitment{
164174
Version: maxVersion,
165175
TapKey: tapKey,
166-
AssetType: assetType,
176+
AssetType: expectedAssetType,
167177
assets: assetsMap,
168178
}, nil
169179
}

tapdb/assets_store_test.go

Lines changed: 86 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -2058,82 +2058,105 @@ func TestAssetGroupComplexWitness(t *testing.T) {
20582058
require.True(t, groupKey.IsEqual(storedGroup.GroupKey))
20592059
}
20602060

2061-
// TestAssetGroupV1 tests that we can store and fetch an asset group version 1.
2062-
func TestAssetGroupV1(t *testing.T) {
2061+
// TestStoreFetchAssetGroupV1 tests that we can store and fetch an asset group
2062+
// version 1.
2063+
func TestStoreFetchAssetGroupV1(t *testing.T) {
20632064
t.Parallel()
20642065

2065-
mintingStore, assetStore, db := newAssetStore(t)
2066-
ctx := context.Background()
2066+
testCases := []struct {
2067+
name string
2068+
customTapscriptRoot fn.Option[chainhash.Hash]
2069+
witnessData [][]byte
2070+
expectedError error
2071+
}{{
2072+
name: "group key with custom tapscript root",
2073+
customTapscriptRoot: fn.Some(test.RandHash()),
2074+
witnessData: [][]byte{
2075+
test.RandBytes(32),
2076+
test.RandBytes(64),
2077+
},
2078+
expectedError: nil,
2079+
}, {
2080+
name: "group key without custom tapscript " +
2081+
"root",
2082+
customTapscriptRoot: fn.None[chainhash.Hash](),
2083+
witnessData: [][]byte{
2084+
test.RandBytes(32),
2085+
test.RandBytes(64),
2086+
},
2087+
expectedError: nil,
2088+
}}
20672089

2068-
internalKey := test.RandPubKey(t)
2069-
groupAnchorGen := asset.RandGenesis(t, asset.RandAssetType(t))
2070-
groupAnchorGen.MetaHash = [32]byte{}
2071-
tapscriptRoot := test.RandBytes(32)
2072-
customTapscriptRoot := test.RandHash()
2073-
groupSig := test.RandBytes(64)
2090+
for idx := range testCases {
2091+
tc := testCases[idx]
20742092

2075-
// First, we'll insert all the required rows we need to satisfy the
2076-
// foreign key constraints needed to insert a new genesis witness.
2077-
genesisPointID, err := upsertGenesisPoint(
2078-
ctx, db, groupAnchorGen.FirstPrevOut,
2079-
)
2080-
require.NoError(t, err)
2093+
t.Run(tc.name, func(tt *testing.T) {
2094+
tt.Parallel()
20812095

2082-
genAssetID, err := upsertGenesis(
2083-
ctx, db, genesisPointID, groupAnchorGen,
2084-
)
2085-
require.NoError(t, err)
2096+
mintingStore, assetStore, db := newAssetStore(tt)
2097+
ctx := context.Background()
20862098

2087-
groupKey := asset.GroupKey{
2088-
Version: asset.GroupKeyV1,
2089-
RawKey: keychain.KeyDescriptor{
2090-
PubKey: internalKey,
2091-
},
2092-
GroupPubKey: *internalKey,
2093-
TapscriptRoot: tapscriptRoot,
2094-
CustomTapscriptRoot: fn.Some[chainhash.Hash](
2095-
customTapscriptRoot,
2096-
),
2097-
Witness: fn.MakeSlice(tapscriptRoot, groupSig),
2098-
}
2099+
internalKey := test.RandPubKey(tt)
2100+
groupAnchorGen := asset.RandGenesis(
2101+
tt, asset.RandAssetType(tt),
2102+
)
2103+
groupAnchorGen.MetaHash = [32]byte{}
2104+
tapscriptRoot := test.RandBytes(32)
2105+
2106+
// First, we'll insert all the required rows we need to
2107+
// satisfy the foreign key constraints needed to insert
2108+
// a new genesis witness.
2109+
genesisPointID, err := upsertGenesisPoint(
2110+
ctx, db, groupAnchorGen.FirstPrevOut,
2111+
)
2112+
require.NoError(tt, err)
20992113

2100-
// Upsert, fetch, and check the group key.
2101-
_, err = upsertGroupKey(
2102-
ctx, &groupKey, assetStore.db, genesisPointID, genAssetID,
2103-
)
2104-
require.NoError(t, err)
2114+
genAssetID, err := upsertGenesis(
2115+
ctx, db, genesisPointID, groupAnchorGen,
2116+
)
2117+
require.NoError(tt, err)
21052118

2106-
storedGroup, err := mintingStore.FetchGroupByGroupKey(ctx, internalKey)
2107-
require.NoError(t, err)
2119+
groupKey := asset.GroupKey{
2120+
Version: asset.GroupKeyV1,
2121+
RawKey: keychain.KeyDescriptor{
2122+
PubKey: internalKey,
2123+
},
2124+
GroupPubKey: *internalKey,
2125+
TapscriptRoot: tapscriptRoot,
2126+
CustomTapscriptRoot: tc.customTapscriptRoot,
2127+
Witness: tc.witnessData,
2128+
}
21082129

2109-
require.Equal(t, groupAnchorGen, *storedGroup.Genesis)
2110-
require.True(t, groupKey.IsEqual(storedGroup.GroupKey))
2130+
// Upsert the group key.
2131+
_, err = upsertGroupKey(
2132+
ctx, &groupKey, assetStore.db, genesisPointID,
2133+
genAssetID,
2134+
)
2135+
if tc.expectedError != nil {
2136+
require.ErrorIs(tt, err, tc.expectedError)
2137+
return
2138+
}
2139+
require.NoError(tt, err)
21112140

2112-
// Formulate a new group key where the custom tapscript root is None.
2113-
// Check that we can insert and fetch the group key.
2114-
groupKeyCustomRootNone := asset.GroupKey{
2115-
Version: asset.GroupKeyV1,
2116-
RawKey: keychain.KeyDescriptor{
2117-
PubKey: internalKey,
2118-
},
2119-
GroupPubKey: *internalKey,
2120-
TapscriptRoot: tapscriptRoot,
2121-
CustomTapscriptRoot: fn.None[chainhash.Hash](),
2122-
Witness: fn.MakeSlice(tapscriptRoot, groupSig),
2123-
}
2141+
// Fetch and verify the group key.
2142+
storedGroup, err := mintingStore.FetchGroupByGroupKey(
2143+
ctx, internalKey,
2144+
)
2145+
require.NoError(tt, err)
21242146

2125-
// Upsert, fetch, and check the group key.
2126-
_, err = upsertGroupKey(
2127-
ctx, &groupKeyCustomRootNone, assetStore.db, genesisPointID,
2128-
genAssetID,
2129-
)
2130-
require.NoError(t, err)
2147+
// Verify the genesis matches.
2148+
require.Equal(tt, groupAnchorGen, *storedGroup.Genesis)
21312149

2132-
storedGroup2, err := mintingStore.FetchGroupByGroupKey(ctx, internalKey)
2133-
require.NoError(t, err)
2150+
// Verify the group key matches.
2151+
require.True(tt, groupKey.IsEqual(storedGroup.GroupKey))
21342152

2135-
require.Equal(t, groupAnchorGen, *storedGroup2.Genesis)
2136-
require.True(t, groupKeyCustomRootNone.IsEqual(storedGroup2.GroupKey))
2153+
// Ensure that the group key version is set to V1.
2154+
require.Equal(
2155+
tt, asset.GroupKeyV1,
2156+
storedGroup.GroupKey.Version,
2157+
)
2158+
})
2159+
}
21372160
}
21382161

21392162
// TestAssetGroupKeyUpsert tests that if you try to insert another asset group

0 commit comments

Comments
 (0)