Skip to content

Commit 1ad2399

Browse files
authored
Merge pull request #1246 from lightninglabs/add-new-groupkeyreveal
Add `GroupKeyReveal` V1
2 parents 3600d5d + 0318fad commit 1ad2399

File tree

8 files changed

+1245
-71
lines changed

8 files changed

+1245
-71
lines changed

asset/asset.go

Lines changed: 508 additions & 10 deletions
Large diffs are not rendered by default.

asset/encoding.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -895,3 +895,55 @@ func AltLeavesDecoder(r io.Reader, val any, buf *[8]byte, l uint64) error {
895895
}
896896
return tlv.NewTypeForEncodingErr(val, "[]*AltLeaf")
897897
}
898+
899+
func GroupKeyRevealEncoder(w io.Writer, val any, _ *[8]byte) error {
900+
if t, ok := val.(*GroupKeyReveal); ok {
901+
if err := (*t).Encode(w); err != nil {
902+
return fmt.Errorf("unable to encode group key "+
903+
"reveal: %w", err)
904+
}
905+
906+
return nil
907+
}
908+
909+
return tlv.NewTypeForEncodingErr(val, "GroupKeyReveal")
910+
}
911+
912+
func GroupKeyRevealDecoder(r io.Reader, val any, buf *[8]byte, l uint64) error {
913+
// Return early if the val is not a pointer to a GroupKeyReveal.
914+
typ, ok := val.(*GroupKeyReveal)
915+
if !ok {
916+
return tlv.NewTypeForEncodingErr(val, "GroupKeyReveal")
917+
}
918+
919+
// If the length is less than or equal to the sum of the lengths of the
920+
// internal key and the tapscript root, then we'll attempt to decode it
921+
// as a GroupKeyRevealV0.
922+
internalKeyLen := uint64(btcec.PubKeyBytesLenCompressed)
923+
tapscriptRootLen := uint64(sha256.Size)
924+
925+
if l <= internalKeyLen+tapscriptRootLen {
926+
// Attempt decoding with GroupKeyRevealV0.
927+
var gkrV0 GroupKeyRevealV0
928+
929+
err := gkrV0.Decode(r, buf, l)
930+
if err != nil {
931+
return fmt.Errorf("group key reveal V0 decode "+
932+
"error: %w", err)
933+
}
934+
935+
*typ = &gkrV0
936+
return nil
937+
}
938+
939+
// Attempt decoding with GroupKeyRevealV1.
940+
var gkrV1 GroupKeyRevealV1
941+
942+
err := gkrV1.Decode(r, buf, l)
943+
if err != nil {
944+
return fmt.Errorf("group key reveal V1 decode error: %w", err)
945+
}
946+
947+
*typ = &gkrV1
948+
return nil
949+
}

asset/group_key_reveal_test.go

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
package asset
2+
3+
import (
4+
"bytes"
5+
"testing"
6+
7+
"github.com/btcsuite/btcd/btcec/v2"
8+
"github.com/btcsuite/btcd/chaincfg/chainhash"
9+
"github.com/btcsuite/btcd/txscript"
10+
"github.com/lightninglabs/taproot-assets/fn"
11+
"github.com/lightninglabs/taproot-assets/internal/test"
12+
"github.com/stretchr/testify/require"
13+
"pgregory.net/rapid"
14+
)
15+
16+
type testCaseGkrEncodeDecode struct {
17+
testName string
18+
19+
internalKey btcec.PublicKey
20+
genesisAssetID ID
21+
customSubtreeRoot fn.Option[chainhash.Hash]
22+
}
23+
24+
// GroupKeyReveal generates a GroupKeyReveal instance from the test case.
25+
func (tc testCaseGkrEncodeDecode) GroupKeyReveal() (GroupKeyReveal, error) {
26+
gkr, err := NewGroupKeyRevealV1(
27+
tc.internalKey, tc.genesisAssetID, tc.customSubtreeRoot,
28+
)
29+
30+
return &gkr, err
31+
}
32+
33+
// TestGroupKeyRevealEncodeDecode tests encoding and decoding of GroupKeyReveal.
34+
func TestGroupKeyRevealEncodeDecode(t *testing.T) {
35+
t.Parallel()
36+
37+
// Create a random internal public key.
38+
internalKey := *(test.RandPubKey(t))
39+
40+
// Create a random genesis asset ID.
41+
randomAssetIdBytes := test.RandBytes(32)
42+
genesisAssetID := ID(randomAssetIdBytes)
43+
44+
// Construct a custom user script leaf. This is used to validate any
45+
// control block.
46+
customScriptLeaf := txscript.NewBaseTapLeaf(
47+
[]byte("I'm a custom user script"),
48+
)
49+
customSubtreeRoot := fn.Some(customScriptLeaf.TapHash())
50+
51+
testCases := []testCaseGkrEncodeDecode{
52+
{
53+
testName: "no custom root",
54+
55+
internalKey: internalKey,
56+
genesisAssetID: genesisAssetID,
57+
customSubtreeRoot: fn.None[chainhash.Hash](),
58+
},
59+
{
60+
testName: "with custom root",
61+
62+
internalKey: internalKey,
63+
genesisAssetID: genesisAssetID,
64+
customSubtreeRoot: customSubtreeRoot,
65+
},
66+
}
67+
68+
for _, tc := range testCases {
69+
t.Run(tc.testName, func(tt *testing.T) {
70+
gkr, err := tc.GroupKeyReveal()
71+
require.NoError(tt, err)
72+
73+
groupPubKey, err := gkr.GroupPubKey(tc.genesisAssetID)
74+
require.NoError(tt, err)
75+
76+
// Encode the GroupKeyReveal into buffer.
77+
var buffer bytes.Buffer
78+
var scratchBuffEncode [8]byte
79+
err = GroupKeyRevealEncoder(
80+
&buffer, &gkr, &scratchBuffEncode,
81+
)
82+
require.NoError(tt, err)
83+
84+
// Decode the GroupKeyReveal from buffer.
85+
var gkrDecoded GroupKeyReveal
86+
var scratchBuffDecode [8]byte
87+
err = GroupKeyRevealDecoder(
88+
&buffer, &gkrDecoded, &scratchBuffDecode,
89+
uint64(buffer.Len()),
90+
)
91+
require.NoError(tt, err)
92+
93+
// Prepare the original GroupKeyReveal for comparison.
94+
// Remove fields which are not included in
95+
// encoding/decoding.
96+
gkrV1, ok := gkr.(*GroupKeyRevealV1)
97+
require.True(tt, ok)
98+
gkrV1.tapscript.customSubtreeInclusionProof = nil
99+
100+
// Compare decoded group key reveal with the original.
101+
require.Equal(tt, gkrV1, gkrDecoded)
102+
103+
// Ensure the decoded group public key matches the
104+
// original.
105+
groupPubKeyDecoded, err := gkrDecoded.GroupPubKey(
106+
tc.genesisAssetID,
107+
)
108+
require.NoError(tt, err)
109+
110+
require.Equal(
111+
tt, groupPubKey, groupPubKeyDecoded,
112+
"decoded GroupKeyReveal group pub key does "+
113+
"not match original",
114+
)
115+
116+
// If a custom subtree root is set, ensure the control
117+
// block is correct.
118+
if tc.customSubtreeRoot.IsSome() {
119+
gkrDecodedV1, ok :=
120+
gkrDecoded.(*GroupKeyRevealV1)
121+
require.True(tt, ok)
122+
123+
ctrlBlock, err :=
124+
gkrDecodedV1.ScriptSpendControlBlock(
125+
tc.genesisAssetID,
126+
)
127+
require.NoError(tt, err)
128+
129+
// Use the control block and the custom spend
130+
// script to compute the root hash.
131+
computedRoot := chainhash.Hash(
132+
ctrlBlock.RootHash(
133+
customScriptLeaf.Script,
134+
),
135+
)
136+
137+
// Ensure the computed root matches the custom
138+
// subtree root.
139+
require.Equal(
140+
tt, gkrDecodedV1.tapscript.root,
141+
computedRoot,
142+
)
143+
}
144+
})
145+
}
146+
}
147+
148+
// TestGroupKeyRevealEncodeDecodeRapid tests encoding and decoding of
149+
// GroupKeyReveal using rapid testing. The Rapid framework is used to generate
150+
// random test inputs.
151+
func TestGroupKeyRevealEncodeDecodeRapid(tt *testing.T) {
152+
tt.Parallel()
153+
154+
rapid.Check(tt, func(t *rapid.T) {
155+
// Generate random test inputs using rapid generators.
156+
//
157+
// Generate a random internal key.
158+
internalKeyBytes := rapid.SliceOfN(rapid.Byte(), 32, 32).
159+
Draw(t, "internal_key_bytes")
160+
_, publicKey := btcec.PrivKeyFromBytes(internalKeyBytes)
161+
internalKey := *publicKey
162+
163+
// Generate a random genesis asset ID.
164+
genesisAssetID := ID(rapid.SliceOfN(rapid.Byte(), 32, 32).
165+
Draw(t, "genesis_id"))
166+
167+
// Randomly decide whether to include a custom script.
168+
hasCustomScript := rapid.Bool().Draw(t, "has_custom_script")
169+
170+
// If a custom script is included, generate a random script leaf
171+
// and subtree root.
172+
var customSubtreeRoot fn.Option[chainhash.Hash]
173+
var customScriptLeaf *txscript.TapLeaf
174+
175+
if hasCustomScript {
176+
// Generate random script between 1-100 bytes.
177+
scriptSize := rapid.IntRange(1, 100).
178+
Draw(t, "script_size")
179+
customScript := rapid.SliceOfN(
180+
rapid.Byte(), scriptSize, scriptSize,
181+
).Draw(t, "custom_script")
182+
183+
leaf := txscript.NewBaseTapLeaf(customScript)
184+
customScriptLeaf = &leaf
185+
customSubtreeRoot = fn.Some(customScriptLeaf.TapHash())
186+
} else {
187+
customSubtreeRoot = fn.None[chainhash.Hash]()
188+
}
189+
190+
// Create a new GroupKeyReveal instance from the random test
191+
// inputs.
192+
gkrV1, err := NewGroupKeyRevealV1(
193+
internalKey,
194+
genesisAssetID,
195+
customSubtreeRoot,
196+
)
197+
require.NoError(t, err)
198+
199+
// Encode the GroupKeyReveal instance into a buffer.
200+
var buffer bytes.Buffer
201+
var scratchBuffEncode [8]byte
202+
gkr := GroupKeyReveal(&gkrV1)
203+
err = GroupKeyRevealEncoder(&buffer, &gkr, &scratchBuffEncode)
204+
require.NoError(t, err)
205+
206+
// Decode the GroupKeyReveal instance from the buffer.
207+
var gkrDecoded GroupKeyReveal
208+
var scratchBuffDecode [8]byte
209+
err = GroupKeyRevealDecoder(
210+
&buffer, &gkrDecoded, &scratchBuffDecode,
211+
uint64(buffer.Len()),
212+
)
213+
require.NoError(t, err)
214+
215+
// Prepare for comparison by removing non-encoded fields from
216+
// the original GroupKeyReveal.
217+
gkrV1.tapscript.customSubtreeInclusionProof = nil
218+
219+
// Compare decoded with original.
220+
require.Equal(t, &gkrV1, gkrDecoded)
221+
222+
// Verify decoded group public key.
223+
//
224+
// First derive a group public key from the original.
225+
groupPubKey, err := gkrV1.GroupPubKey(genesisAssetID)
226+
require.NoError(t, err)
227+
228+
// Then derive a group public key from the decoded.
229+
groupPubKeyDecoded, err := gkrDecoded.GroupPubKey(
230+
genesisAssetID,
231+
)
232+
require.NoError(t, err)
233+
234+
require.Equal(t, groupPubKey, groupPubKeyDecoded)
235+
236+
// If a custom subtree root is set on the decoded
237+
// GroupKeyReveal, ensure the derived control block is correct.
238+
if customSubtreeRoot.IsSome() && customScriptLeaf != nil {
239+
gkrDecodedV1, ok := gkrDecoded.(*GroupKeyRevealV1)
240+
require.True(t, ok)
241+
242+
ctrlBlock, err := gkrDecodedV1.ScriptSpendControlBlock(
243+
genesisAssetID,
244+
)
245+
require.NoError(t, err)
246+
247+
computedRoot := chainhash.Hash(
248+
ctrlBlock.RootHash(customScriptLeaf.Script),
249+
)
250+
251+
// Ensure the computed root matches the tapscript root
252+
// for both the original and decoded GroupKeyReveal.
253+
require.Equal(
254+
t, gkrV1.tapscript.root, computedRoot,
255+
)
256+
require.Equal(
257+
t, gkrDecodedV1.tapscript.root, computedRoot,
258+
)
259+
}
260+
})
261+
}

commitment/tap.go

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -427,15 +427,6 @@ func (c *TapCommitment) Downgrade() (*TapCommitment, error) {
427427
return NewTapCommitment(nil, newAssetCommitments...)
428428
}
429429

430-
// tapBranchHash takes the tap hashes of the left and right nodes and hashes
431-
// them into a branch.
432-
func tapBranchHash(l, r chainhash.Hash) chainhash.Hash {
433-
if bytes.Compare(l[:], r[:]) > 0 {
434-
l, r = r, l
435-
}
436-
return *chainhash.TaggedHash(chainhash.TagTapBranch, l[:], r[:])
437-
}
438-
439430
// IsTaprootAssetCommitmentScript returns true if the passed script is a valid
440431
// Taproot Asset commitment script.
441432
func IsTaprootAssetCommitmentScript(script []byte) bool {
@@ -472,7 +463,7 @@ func (c *TapCommitment) TapscriptRoot(sibling *chainhash.Hash) chainhash.Hash {
472463

473464
// The ordering of `commitmentLeaf` and `sibling` doesn't matter here as
474465
// TapBranch will sort them before hashing.
475-
return tapBranchHash(commitmentLeaf.TapHash(), *sibling)
466+
return asset.TapBranchHash(commitmentLeaf.TapHash(), *sibling)
476467
}
477468

478469
// Proof computes the full TapCommitment merkle proof for the asset leaf

0 commit comments

Comments
 (0)