Skip to content

Commit

Permalink
Merge pull request #1246 from lightninglabs/add-new-groupkeyreveal
Browse files Browse the repository at this point in the history
Add `GroupKeyReveal` V1
  • Loading branch information
guggero authored Dec 19, 2024
2 parents 3600d5d + 0318fad commit 1ad2399
Show file tree
Hide file tree
Showing 8 changed files with 1,245 additions and 71 deletions.
518 changes: 508 additions & 10 deletions asset/asset.go

Large diffs are not rendered by default.

52 changes: 52 additions & 0 deletions asset/encoding.go
Original file line number Diff line number Diff line change
Expand Up @@ -895,3 +895,55 @@ func AltLeavesDecoder(r io.Reader, val any, buf *[8]byte, l uint64) error {
}
return tlv.NewTypeForEncodingErr(val, "[]*AltLeaf")
}

func GroupKeyRevealEncoder(w io.Writer, val any, _ *[8]byte) error {
if t, ok := val.(*GroupKeyReveal); ok {
if err := (*t).Encode(w); err != nil {
return fmt.Errorf("unable to encode group key "+
"reveal: %w", err)
}

return nil
}

return tlv.NewTypeForEncodingErr(val, "GroupKeyReveal")
}

func GroupKeyRevealDecoder(r io.Reader, val any, buf *[8]byte, l uint64) error {
// Return early if the val is not a pointer to a GroupKeyReveal.
typ, ok := val.(*GroupKeyReveal)
if !ok {
return tlv.NewTypeForEncodingErr(val, "GroupKeyReveal")
}

// If the length is less than or equal to the sum of the lengths of the
// internal key and the tapscript root, then we'll attempt to decode it
// as a GroupKeyRevealV0.
internalKeyLen := uint64(btcec.PubKeyBytesLenCompressed)
tapscriptRootLen := uint64(sha256.Size)

if l <= internalKeyLen+tapscriptRootLen {
// Attempt decoding with GroupKeyRevealV0.
var gkrV0 GroupKeyRevealV0

err := gkrV0.Decode(r, buf, l)
if err != nil {
return fmt.Errorf("group key reveal V0 decode "+
"error: %w", err)
}

*typ = &gkrV0
return nil
}

// Attempt decoding with GroupKeyRevealV1.
var gkrV1 GroupKeyRevealV1

err := gkrV1.Decode(r, buf, l)
if err != nil {
return fmt.Errorf("group key reveal V1 decode error: %w", err)
}

*typ = &gkrV1
return nil
}
261 changes: 261 additions & 0 deletions asset/group_key_reveal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
package asset

import (
"bytes"
"testing"

"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/lightninglabs/taproot-assets/fn"
"github.com/lightninglabs/taproot-assets/internal/test"
"github.com/stretchr/testify/require"
"pgregory.net/rapid"
)

type testCaseGkrEncodeDecode struct {
testName string

internalKey btcec.PublicKey
genesisAssetID ID
customSubtreeRoot fn.Option[chainhash.Hash]
}

// GroupKeyReveal generates a GroupKeyReveal instance from the test case.
func (tc testCaseGkrEncodeDecode) GroupKeyReveal() (GroupKeyReveal, error) {
gkr, err := NewGroupKeyRevealV1(
tc.internalKey, tc.genesisAssetID, tc.customSubtreeRoot,
)

return &gkr, err
}

// TestGroupKeyRevealEncodeDecode tests encoding and decoding of GroupKeyReveal.
func TestGroupKeyRevealEncodeDecode(t *testing.T) {
t.Parallel()

// Create a random internal public key.
internalKey := *(test.RandPubKey(t))

// Create a random genesis asset ID.
randomAssetIdBytes := test.RandBytes(32)
genesisAssetID := ID(randomAssetIdBytes)

// Construct a custom user script leaf. This is used to validate any
// control block.
customScriptLeaf := txscript.NewBaseTapLeaf(
[]byte("I'm a custom user script"),
)
customSubtreeRoot := fn.Some(customScriptLeaf.TapHash())

testCases := []testCaseGkrEncodeDecode{
{
testName: "no custom root",

internalKey: internalKey,
genesisAssetID: genesisAssetID,
customSubtreeRoot: fn.None[chainhash.Hash](),
},
{
testName: "with custom root",

internalKey: internalKey,
genesisAssetID: genesisAssetID,
customSubtreeRoot: customSubtreeRoot,
},
}

for _, tc := range testCases {
t.Run(tc.testName, func(tt *testing.T) {
gkr, err := tc.GroupKeyReveal()
require.NoError(tt, err)

groupPubKey, err := gkr.GroupPubKey(tc.genesisAssetID)
require.NoError(tt, err)

// Encode the GroupKeyReveal into buffer.
var buffer bytes.Buffer
var scratchBuffEncode [8]byte
err = GroupKeyRevealEncoder(
&buffer, &gkr, &scratchBuffEncode,
)
require.NoError(tt, err)

// Decode the GroupKeyReveal from buffer.
var gkrDecoded GroupKeyReveal
var scratchBuffDecode [8]byte
err = GroupKeyRevealDecoder(
&buffer, &gkrDecoded, &scratchBuffDecode,
uint64(buffer.Len()),
)
require.NoError(tt, err)

// Prepare the original GroupKeyReveal for comparison.
// Remove fields which are not included in
// encoding/decoding.
gkrV1, ok := gkr.(*GroupKeyRevealV1)
require.True(tt, ok)
gkrV1.tapscript.customSubtreeInclusionProof = nil

// Compare decoded group key reveal with the original.
require.Equal(tt, gkrV1, gkrDecoded)

// Ensure the decoded group public key matches the
// original.
groupPubKeyDecoded, err := gkrDecoded.GroupPubKey(
tc.genesisAssetID,
)
require.NoError(tt, err)

require.Equal(
tt, groupPubKey, groupPubKeyDecoded,
"decoded GroupKeyReveal group pub key does "+
"not match original",
)

// If a custom subtree root is set, ensure the control
// block is correct.
if tc.customSubtreeRoot.IsSome() {
gkrDecodedV1, ok :=
gkrDecoded.(*GroupKeyRevealV1)
require.True(tt, ok)

ctrlBlock, err :=
gkrDecodedV1.ScriptSpendControlBlock(
tc.genesisAssetID,
)
require.NoError(tt, err)

// Use the control block and the custom spend
// script to compute the root hash.
computedRoot := chainhash.Hash(
ctrlBlock.RootHash(
customScriptLeaf.Script,
),
)

// Ensure the computed root matches the custom
// subtree root.
require.Equal(
tt, gkrDecodedV1.tapscript.root,
computedRoot,
)
}
})
}
}

// TestGroupKeyRevealEncodeDecodeRapid tests encoding and decoding of
// GroupKeyReveal using rapid testing. The Rapid framework is used to generate
// random test inputs.
func TestGroupKeyRevealEncodeDecodeRapid(tt *testing.T) {
tt.Parallel()

rapid.Check(tt, func(t *rapid.T) {
// Generate random test inputs using rapid generators.
//
// Generate a random internal key.
internalKeyBytes := rapid.SliceOfN(rapid.Byte(), 32, 32).
Draw(t, "internal_key_bytes")
_, publicKey := btcec.PrivKeyFromBytes(internalKeyBytes)
internalKey := *publicKey

// Generate a random genesis asset ID.
genesisAssetID := ID(rapid.SliceOfN(rapid.Byte(), 32, 32).
Draw(t, "genesis_id"))

// Randomly decide whether to include a custom script.
hasCustomScript := rapid.Bool().Draw(t, "has_custom_script")

// If a custom script is included, generate a random script leaf
// and subtree root.
var customSubtreeRoot fn.Option[chainhash.Hash]
var customScriptLeaf *txscript.TapLeaf

if hasCustomScript {
// Generate random script between 1-100 bytes.
scriptSize := rapid.IntRange(1, 100).
Draw(t, "script_size")
customScript := rapid.SliceOfN(
rapid.Byte(), scriptSize, scriptSize,
).Draw(t, "custom_script")

leaf := txscript.NewBaseTapLeaf(customScript)
customScriptLeaf = &leaf
customSubtreeRoot = fn.Some(customScriptLeaf.TapHash())
} else {
customSubtreeRoot = fn.None[chainhash.Hash]()
}

// Create a new GroupKeyReveal instance from the random test
// inputs.
gkrV1, err := NewGroupKeyRevealV1(
internalKey,
genesisAssetID,
customSubtreeRoot,
)
require.NoError(t, err)

// Encode the GroupKeyReveal instance into a buffer.
var buffer bytes.Buffer
var scratchBuffEncode [8]byte
gkr := GroupKeyReveal(&gkrV1)
err = GroupKeyRevealEncoder(&buffer, &gkr, &scratchBuffEncode)
require.NoError(t, err)

// Decode the GroupKeyReveal instance from the buffer.
var gkrDecoded GroupKeyReveal
var scratchBuffDecode [8]byte
err = GroupKeyRevealDecoder(
&buffer, &gkrDecoded, &scratchBuffDecode,
uint64(buffer.Len()),
)
require.NoError(t, err)

// Prepare for comparison by removing non-encoded fields from
// the original GroupKeyReveal.
gkrV1.tapscript.customSubtreeInclusionProof = nil

// Compare decoded with original.
require.Equal(t, &gkrV1, gkrDecoded)

// Verify decoded group public key.
//
// First derive a group public key from the original.
groupPubKey, err := gkrV1.GroupPubKey(genesisAssetID)
require.NoError(t, err)

// Then derive a group public key from the decoded.
groupPubKeyDecoded, err := gkrDecoded.GroupPubKey(
genesisAssetID,
)
require.NoError(t, err)

require.Equal(t, groupPubKey, groupPubKeyDecoded)

// If a custom subtree root is set on the decoded
// GroupKeyReveal, ensure the derived control block is correct.
if customSubtreeRoot.IsSome() && customScriptLeaf != nil {
gkrDecodedV1, ok := gkrDecoded.(*GroupKeyRevealV1)
require.True(t, ok)

ctrlBlock, err := gkrDecodedV1.ScriptSpendControlBlock(
genesisAssetID,
)
require.NoError(t, err)

computedRoot := chainhash.Hash(
ctrlBlock.RootHash(customScriptLeaf.Script),
)

// Ensure the computed root matches the tapscript root
// for both the original and decoded GroupKeyReveal.
require.Equal(
t, gkrV1.tapscript.root, computedRoot,
)
require.Equal(
t, gkrDecodedV1.tapscript.root, computedRoot,
)
}
})
}
11 changes: 1 addition & 10 deletions commitment/tap.go
Original file line number Diff line number Diff line change
Expand Up @@ -427,15 +427,6 @@ func (c *TapCommitment) Downgrade() (*TapCommitment, error) {
return NewTapCommitment(nil, newAssetCommitments...)
}

// tapBranchHash takes the tap hashes of the left and right nodes and hashes
// them into a branch.
func tapBranchHash(l, r chainhash.Hash) chainhash.Hash {
if bytes.Compare(l[:], r[:]) > 0 {
l, r = r, l
}
return *chainhash.TaggedHash(chainhash.TagTapBranch, l[:], r[:])
}

// IsTaprootAssetCommitmentScript returns true if the passed script is a valid
// Taproot Asset commitment script.
func IsTaprootAssetCommitmentScript(script []byte) bool {
Expand Down Expand Up @@ -472,7 +463,7 @@ func (c *TapCommitment) TapscriptRoot(sibling *chainhash.Hash) chainhash.Hash {

// The ordering of `commitmentLeaf` and `sibling` doesn't matter here as
// TapBranch will sort them before hashing.
return tapBranchHash(commitmentLeaf.TapHash(), *sibling)
return asset.TapBranchHash(commitmentLeaf.TapHash(), *sibling)
}

// Proof computes the full TapCommitment merkle proof for the asset leaf
Expand Down
Loading

0 comments on commit 1ad2399

Please sign in to comment.