Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading