Skip to content

Commit 41be822

Browse files
authored
Merge pull request #1602 from lightninglabs/wip/improve-GenGroupAnchorVerifier
Improve robustness of `GenGroupAnchorVerifier` with anchor checks
2 parents b078b3e + 28422a7 commit 41be822

File tree

4 files changed

+244
-0
lines changed

4 files changed

+244
-0
lines changed

asset/asset.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -937,6 +937,12 @@ type AssetGroup struct {
937937
*GroupKey
938938
}
939939

940+
// IsGroupAnchor returns true if this genesis matches the group anchor asset ID,
941+
// meaning it corresponds to the first tranche that established the group.
942+
func (a *AssetGroup) IsGroupAnchor() (bool, error) {
943+
return a.GroupKey.IsGroupAnchor(a.Genesis.ID())
944+
}
945+
940946
// ExternalKey represents an external key used for deriving and managing
941947
// hierarchical deterministic (HD) wallet addresses according to BIP-86.
942948
type ExternalKey struct {

asset/group_key.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,38 @@ type GroupKey struct {
9292
Witness wire.TxWitness
9393
}
9494

95+
// IsGroupAnchor returns true if the provided asset ID matches the first asset
96+
// minted into the group. Each asset group key is derived from a single group
97+
// anchor asset ID.
98+
func (g *GroupKey) IsGroupAnchor(assetID ID) (bool, error) {
99+
var zero bool
100+
101+
// We will use the given asset ID and the parameters of the receiver
102+
// GroupKey to derive a group public key.
103+
//
104+
// We will then compare the derived key to the GroupPubKey in the
105+
// receiver. If they match, then the asset ID is the group anchor
106+
// asset ID from which the receiver group key is derived.
107+
expectedGroupPubKey := g.GroupPubKey
108+
109+
// The group key reveal logic derives the group public key.
110+
// Although we pass in the receiver’s GroupKey, its group public key is
111+
// not used. Instead, the key is re-derived from the associated
112+
// parameters.
113+
gkr, err := NewGroupKeyReveal(*g, assetID)
114+
if err != nil {
115+
return zero, err
116+
}
117+
118+
derivedGroupPubKey, err := gkr.GroupPubKey(assetID)
119+
if err != nil {
120+
return zero, fmt.Errorf("cannot derive group public key: %w",
121+
err)
122+
}
123+
124+
return expectedGroupPubKey.IsEqual(derivedGroupPubKey), nil
125+
}
126+
95127
// GroupKeyRequest contains the essential fields used to derive a group key.
96128
type GroupKeyRequest struct {
97129
// Version is the version of the group key construction.

asset/group_key_test.go

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -679,3 +679,195 @@ func TestGroupKeyDerivationInvalidAsset(t *testing.T) {
679679
require.NoError(t, err)
680680
require.NotNil(t, groupKey)
681681
}
682+
683+
// TestAssetIsGroupAnchor tests the GroupKey.IsGroupAnchor method with various
684+
// test cases covering both positive and negative scenarios.
685+
func TestAssetIsGroupAnchor(t *testing.T) {
686+
t.Parallel()
687+
688+
// Generate a sample asset ID to use as the group anchor asset ID.
689+
firstAssetID := RandID(t)
690+
691+
testCases := []struct {
692+
name string
693+
694+
// groupKeyVersion indicates the version of the group key
695+
// to be tested.
696+
groupKeyVersion GroupKeyVersion
697+
698+
// customTapscriptRoot is an optional custom tapscript root
699+
// that can be used for group key derivation.
700+
customTapscriptRoot fn.Option[chainhash.Hash]
701+
702+
// groupAnchorAssetID is the asset ID that the group key is
703+
// derived from.
704+
groupAnchorAssetID ID
705+
706+
// candidateAssetID is the asset ID being checked to determine
707+
// whether it is the group anchor asset ID.
708+
candidateAssetID ID
709+
710+
// expectedIsGroupAnchorRes indicates whether we expect the
711+
// IsGroupAnchor method to return true or false for the
712+
// candidate asset ID.
713+
expectedIsGroupAnchorRes bool
714+
}{{
715+
name: "v0 group key matching group anchor asset",
716+
717+
groupKeyVersion: GroupKeyV0,
718+
customTapscriptRoot: fn.None[chainhash.Hash](),
719+
groupAnchorAssetID: firstAssetID,
720+
candidateAssetID: firstAssetID,
721+
expectedIsGroupAnchorRes: true,
722+
}, {
723+
name: "v0 group key non-matching asset",
724+
725+
groupKeyVersion: GroupKeyV0,
726+
customTapscriptRoot: fn.None[chainhash.Hash](),
727+
groupAnchorAssetID: RandID(t),
728+
candidateAssetID: RandID(t),
729+
expectedIsGroupAnchorRes: false,
730+
}, {
731+
name: "v1 group key matching group anchor asset",
732+
733+
groupKeyVersion: GroupKeyV1,
734+
customTapscriptRoot: fn.None[chainhash.Hash](),
735+
groupAnchorAssetID: firstAssetID,
736+
candidateAssetID: firstAssetID,
737+
expectedIsGroupAnchorRes: true,
738+
}, {
739+
name: "v1 group key non-matching asset",
740+
groupKeyVersion: GroupKeyV1,
741+
customTapscriptRoot: fn.None[chainhash.Hash](),
742+
groupAnchorAssetID: RandID(t),
743+
candidateAssetID: RandID(t),
744+
expectedIsGroupAnchorRes: false,
745+
}, {
746+
name: "v1 group key with custom tapscript root matching " +
747+
"group anchor",
748+
749+
groupKeyVersion: GroupKeyV1,
750+
customTapscriptRoot: fn.Some(test.RandHash()),
751+
groupAnchorAssetID: firstAssetID,
752+
candidateAssetID: firstAssetID,
753+
expectedIsGroupAnchorRes: true,
754+
}, {
755+
name: "v1 group key with custom tapscript root non-matching",
756+
757+
groupKeyVersion: GroupKeyV1,
758+
customTapscriptRoot: fn.Some(test.RandHash()),
759+
groupAnchorAssetID: RandID(t),
760+
candidateAssetID: RandID(t),
761+
expectedIsGroupAnchorRes: false,
762+
}}
763+
764+
for idx := range testCases {
765+
tc := testCases[idx]
766+
767+
t.Run(tc.name, func(tt *testing.T) {
768+
tt.Parallel()
769+
770+
// Create a taproot internal key for use in constructing
771+
// the group key.
772+
internalKey := test.RandPubKey(tt)
773+
774+
// Create the group key using the group anchor asset ID
775+
// and other parameters (we don't use the candidate
776+
// asset ID here, as it is only used for the
777+
// IsGroupAnchor check).
778+
var groupKey *GroupKey
779+
switch tc.groupKeyVersion {
780+
case GroupKeyV0:
781+
// For V0, we need to derive the group key using
782+
// the genesis asset ID.
783+
rawKey := ToSerialized(internalKey)
784+
tapscriptRoot := test.RandBytes(32)
785+
gkr := NewGroupKeyRevealV0(
786+
rawKey, tapscriptRoot,
787+
)
788+
789+
groupPubKey, err := gkr.GroupPubKey(
790+
tc.groupAnchorAssetID,
791+
)
792+
require.NoError(tt, err)
793+
794+
groupKey = &GroupKey{
795+
Version: GroupKeyV0,
796+
RawKey: keychain.KeyDescriptor{
797+
PubKey: internalKey,
798+
},
799+
GroupPubKey: *groupPubKey,
800+
TapscriptRoot: tapscriptRoot,
801+
}
802+
803+
case GroupKeyV1:
804+
// For V1, we need to derive the group key using
805+
// the group anchor asset ID.
806+
gkr, err := NewGroupKeyRevealV1(
807+
PedersenVersion, *internalKey,
808+
tc.groupAnchorAssetID,
809+
tc.customTapscriptRoot,
810+
)
811+
require.NoError(tt, err)
812+
813+
groupPubKey, err := gkr.GroupPubKey(
814+
tc.groupAnchorAssetID,
815+
)
816+
require.NoError(tt, err)
817+
818+
groupKey = &GroupKey{
819+
Version: GroupKeyV1,
820+
RawKey: keychain.KeyDescriptor{
821+
PubKey: internalKey,
822+
},
823+
GroupPubKey: *groupPubKey,
824+
TapscriptRoot: gkr.tapscript.root[:],
825+
826+
// nolint: lll
827+
CustomTapscriptRoot: tc.customTapscriptRoot,
828+
}
829+
830+
default:
831+
t.Fatalf("unknown group key version: %d",
832+
tc.groupKeyVersion)
833+
}
834+
835+
// Call IsGroupAnchor with the candidate asset ID.
836+
result, err := groupKey.IsGroupAnchor(
837+
tc.candidateAssetID,
838+
)
839+
840+
// Verify the result matches expectations.
841+
require.NoError(tt, err)
842+
require.Equal(tt, tc.expectedIsGroupAnchorRes, result)
843+
844+
// Additional verification: if we're testing a matching
845+
// case, verify that the same asset ID returns true and
846+
// a different one returns false
847+
if tc.expectedIsGroupAnchorRes {
848+
// Test with the actual group anchor asset ID
849+
// (should return true).
850+
result, err := groupKey.IsGroupAnchor(
851+
tc.groupAnchorAssetID,
852+
)
853+
require.NoError(tt, err)
854+
require.True(tt, result)
855+
856+
// Test with a different asset ID (should return
857+
// false).
858+
differentAssetID := RandID(t)
859+
for differentAssetID == tc.groupAnchorAssetID {
860+
// This case should be rare, but we fail
861+
// explicitly if it occurs.
862+
t.Fatal("very unexpected asset ID " +
863+
"match")
864+
}
865+
result, err = groupKey.IsGroupAnchor(
866+
differentAssetID,
867+
)
868+
require.NoError(tt, err)
869+
require.False(tt, result)
870+
}
871+
})
872+
}
873+
}

tapgarden/caretaker.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1512,6 +1512,20 @@ func GenGroupAnchorVerifier(ctx context.Context,
15121512
ErrGroupKeyUnknown)
15131513
}
15141514

1515+
isGroupAnchor, err := storedGroup.IsGroupAnchor()
1516+
if err != nil {
1517+
return fmt.Errorf("%x: group anchor verifier: "+
1518+
"unable to check if genesis is "+
1519+
"group anchor: %w", assetGroupKey[:],
1520+
err)
1521+
}
1522+
1523+
if !isGroupAnchor {
1524+
return fmt.Errorf("%x: group anchor verifier: "+
1525+
"genesis is not a group anchor: %w",
1526+
assetGroupKey[:], err)
1527+
}
1528+
15151529
groupAnchor = newSingleValue(storedGroup.Genesis)
15161530

15171531
_, _ = groupAnchors.Put(assetGroupKey, groupAnchor)

0 commit comments

Comments
 (0)