Skip to content

Commit

Permalink
WIP: asset: add GroupKeyRevealV1
Browse files Browse the repository at this point in the history
  • Loading branch information
ffranr committed Dec 9, 2024
1 parent 1a11feb commit a5e08bb
Show file tree
Hide file tree
Showing 2 changed files with 293 additions and 5 deletions.
187 changes: 187 additions & 0 deletions asset/asset.go
Original file line number Diff line number Diff line change
Expand Up @@ -957,6 +957,193 @@ type GroupKeyReveal interface {
GroupPubKey(assetID ID) (*btcec.PublicKey, error)
}

// GroupKeyRevealTapscript represents the data structure used to derive the
// tweaked tapscript root, which is subsequently used to compute the asset
// group key.
//
// The tapscript tree ensures that the derived asset group key is unique
// to a specific genesis asset ID. This design prevents asset group keys from
// being reused across different genesis assets or non-compliant asset minting
// tranches (e.g., tranches of a different asset type).
//
// The uniqueness of the asset group key to a specific genesis asset ID is
// guaranteed by including the genesis asset ID as a leaf in the tapscript tree.
// Additionally, we ensure that the tapscript tree cannot include multiple
// recognizable genesis asset IDs. To achieve this, we enforce that the genesis
// asset ID must appear in the first leaf layer of the tapscript tree. At this
// tree level, there are exactly two leaf positions. We use the remaining
// revealed structure of the tree to prove that the genesis asset ID is
// correctly committed and that no conflicting or duplicate genesis asset IDs
// are committed.
//
// The tapscript tree can include a custom tapscript subtree, enabling users
// to incorporate custom script spend paths. However, in the simplest case,
// if a custom tapscript tree is not included, the tapscript tree adopts the
// following structure:
//
// [tapscript root]
// / \
// [genesis asset ID] [internal key hash]
//
// Here, the tapscript tree consists of two leaf nodes: the genesis asset ID
// and the hash of the taproot internal key. In this case, we can prove that
// the genesis asset ID is committed to the tree root by deriving the tapscript
// root hash using the hash of the internal key. Further, the internal key hash
// is not a valid asset ID because its pre-image is 33 bytes long (the
// serialized compressed public key), making it cryptographically improbable
// to serve as a valid pre-image for an asset ID (which requires a longer
// pre-image).
//
// When a custom tapscript tree is provided, the tapscript tree adopts the
// following structure:
//
// [tapscript root]
// / \
// [genesis asset ID] [tweaked custom tree root]
// / \
// [internal key hash] [custom tree root]
//
// In this scenario, we can prove that the root hash commits to the genesis
// asset ID by deriving the tapscript root from the tweaked custom tree root
// and the genesis asset ID. Specifically, the tapscript root hash is
// reconstructed by concatenating the genesis asset ID and the tweaked custom
// tree root, and then hashing them.
//
// Additionally, we can prove that the tweaked custom tree root is not a valid
// asset ID because it is derived from the internal key hash and the custom
// tree root. By including the internal key hash as a leaf, the structure
// ensures that the tweaked custom tree root cannot be misinterpreted as a
// valid genesis asset ID.
//
// Note that the scripts in the tapscript tree will be made non-executable using
// OP_RETURN, except for the custom tree root.
type GroupKeyRevealTapscript struct {
// finalRoot is the final tapscript root after all tapscript tweaks have
// been applied. The asset group key is derived from this root and the
// internal key.
finalRoot chainhash.Hash

// customTapscriptRoot is the optional root of a custom tapscript tree
// that includes script spend conditions for the group key.
customTapscriptRoot fn.Option[chainhash.Hash]
}

// Validate checks that the group key reveal tapscript is well-formed and
// compliant.
func (g *GroupKeyRevealTapscript) Validate(assetID ID,
internalKey btcec.PublicKey) error {

var emptyHash chainhash.Hash

// Ensure that the final root is not empty.
if g.finalRoot == emptyHash {
return fmt.Errorf("group key reveal final tapscript root is " +
"empty")
}

// If an exclusion proof is specified, ensure that it is not empty.
err := fn.MapOptionZ(
g.customTapscriptRoot,
func(root chainhash.Hash) error {
if root == emptyHash {
return fmt.Errorf("group key reveal " +
"tapscript root asset ID exclusion " +
"proof is specified but empty")
}
return nil
},
)
if err != nil {
return err
}

// TODO(ffranr): Verify that the inclusion and exclusion proofs are
// valid using the internal key and asset ID.

return nil
}

// GroupKeyRevealV1 is a version 1 group key reveal type for representing the
// data used to derive and verify the tweaked key used to identify an asset
// group.
type GroupKeyRevealV1 struct {
// tapInternalKey refers to the internal key used to derive the asset
// group key. Typically, this internal key is the user's signing public
// key.
tapInternalKey SerializedKey

// tapscriptTree is the tapscript tree that commits to the genesis asset
// ID and any script spend conditions for the group key.
tapscriptTree GroupKeyRevealTapscript
}

// Ensure that GroupKeyRevealV1 implements the GroupKeyReveal interface.
var _ GroupKeyReveal = (*GroupKeyRevealV1)(nil)

// NewGroupKeyRevealV1 creates a new version 1 group key reveal instance.
func NewGroupKeyRevealV1(tapInternalKey SerializedKey,
finalRoot chainhash.Hash,
customTapscriptRoot fn.Option[chainhash.Hash]) GroupKeyReveal {

return &GroupKeyRevealV1{
tapInternalKey: tapInternalKey,
tapscriptTree: GroupKeyRevealTapscript{
finalRoot: finalRoot,
customTapscriptRoot: customTapscriptRoot,
},
}
}

// RawKey returns the raw key of the group key reveal.
func (g *GroupKeyRevealV1) RawKey() SerializedKey {
return g.tapInternalKey
}

// SetRawKey sets the raw key of the group key reveal.
func (g *GroupKeyRevealV1) SetRawKey(rawKey SerializedKey) {
g.tapInternalKey = rawKey
}

// TapscriptRoot returns the tapscript root of the group key reveal.
func (g *GroupKeyRevealV1) TapscriptRoot() []byte {
return g.tapscriptTree.finalRoot[:]
}

// SetTapscriptRoot sets the tapscript root of the group key reveal.
func (g *GroupKeyRevealV1) SetTapscriptRoot(tapscriptRootBytes []byte) {
var tapscriptRoot chainhash.Hash
copy(tapscriptRoot[:], tapscriptRootBytes)

g.tapscriptTree.finalRoot = tapscriptRoot
}

// GroupPubKey returns the group public key derived from the group key reveal.
func (g *GroupKeyRevealV1) GroupPubKey(assetID ID) (*btcec.PublicKey, error) {
internalKey, err := g.RawKey().ToPubKey()
if err != nil {
return nil, fmt.Errorf("group reveal raw key invalid: %w", err)
}

return GroupPubKeyV1(internalKey, assetID, g.tapscriptTree)
}

// GroupPubKeyV1 derives a version 1 asset group key from a signing public key
// and a tapscript tree.
func GroupPubKeyV1(internalKey *btcec.PublicKey, assetID ID,
tapscriptTree GroupKeyRevealTapscript) (*btcec.PublicKey, error) {

err := tapscriptTree.Validate(assetID, *internalKey)
if err != nil {
return nil, fmt.Errorf("group key reveal tapscript tree "+
"invalid: %w", err)
}

tapOutputKey := txscript.ComputeTaprootOutputKey(
internalKey, tapscriptTree.finalRoot[:],
)
return tapOutputKey, nil
}

// GroupKeyRevealV0 is a version 0 group key reveal type for representing the
// data used to derive the tweaked key used to identify an asset group. The
// final tweaked key is the result of: TapTweak(groupInternalKey, tapscriptRoot)
Expand Down
111 changes: 106 additions & 5 deletions proof/encoding.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import (

"github.com/btcsuite/btcd/blockchain"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/taproot-assets/asset"
"github.com/lightninglabs/taproot-assets/fn"
"github.com/lightningnetwork/lnd/tlv"
)

Expand Down Expand Up @@ -466,6 +468,11 @@ func GenesisRevealDecoder(r io.Reader, val any, buf *[8]byte, l uint64) error {
}

func GroupKeyRevealEncoder(w io.Writer, val any, buf *[8]byte) error {
// TODO(ffranr): When encoding V1 and onwards, we must fill rawKey,
// tapscriptRoot, and version. Ensuring these fields are populated will
// mean that older tapd nodes will reject the group key reveal cleanly
// (and not try to erroneously parse an unsupported group key reveal).

if t, ok := val.(*asset.GroupKeyReveal); ok {
key := (*t).RawKey()
if err := asset.SerializedKeyEncoder(w, &key, buf); err != nil {
Expand All @@ -492,14 +499,108 @@ func GroupKeyRevealDecoder(r io.Reader, val any, buf *[8]byte, l uint64) error {
if err != nil {
return err
}

// Compute remaining bytes. This calculation will not underflow
// because we have already verified that the length is at least
// the size of the compressed public key.
remaining := l - btcec.PubKeyBytesLenCompressed
var tapscriptRoot []byte
err = tlv.DVarBytes(r, &tapscriptRoot, buf, remaining)
if err != nil {
return err

// Attempt to read the tapscript root bytes if they are present.
var tapscriptRootBytes []byte
if remaining >= 32 {
// At this point, there are at least 32 bytes remaining
// which means that there is a tapscript root present.
// Read the tapscript root bytes.
err = tlv.DVarBytes(r, &tapscriptRootBytes, buf, 32)
if err != nil {
return err
}

// Update the remaining bytes length counter.
remaining -= 32
}

// Set a nil taproot root to an empty slice. This ensures that
// the encoding/decoding round trip is consistent.
if tapscriptRootBytes == nil {
tapscriptRootBytes = []byte{}
}

*typ = asset.NewGroupKeyRevealV0(rawKey, tapscriptRoot)
// If there are still bytes remaining, then the next byte should
// be the group key reveal version.
var version asset.GroupKeyRevealVersion
if remaining > 0 {
var v uint64
err = tlv.DUint8(r, &v, buf, 1)
if err != nil {
return err
}

version = asset.GroupKeyRevealVersion(v)

// Update the remaining bytes length counter.
remaining -= 1
}

// If the parsed version is greater the latest group key reveal
// version, then we reject the group key reveal.
//
// It is important to cleanly reject future versions of group
// key reveals that are not supported by this version of tapd.
// This safeguards compatibility for future upgrades to the
// group key reveal format.
if version > asset.LatestGroupKeyRevealVersion {
return fmt.Errorf("unsupported group key reveal "+
"version %d", version)
}

// If this is a version 0 group key reveal, then we can return
// the group key reveal now.
if version == asset.GroupKeyRevealVersion0 {
*typ = asset.NewGroupKeyRevealV0(
rawKey, tapscriptRootBytes,
)
return nil
}

// At this point, we know this is a version 1 group key reveal.
// Future versions could parse different fields from this point
// on.
//
// For clarity and robustness, we explicitly check for version
// 1.
if version != asset.GroupKeyRevealVersion1 {
return fmt.Errorf("code error: expected group reveal "+
"version 1, got %d", version)
}

// We can now cast the tapscript root bytes to a hash.
var tapscriptRoot chainhash.Hash
copy(tapscriptRoot[:], tapscriptRootBytes)

// The remaining bytes should constitute the custom tapscript
// tree root.
var customTapscriptRoot fn.Option[chainhash.Hash]
if remaining >= chainhash.HashSize {
var rootBytes [32]byte
err = tlv.DBytes32(
r, &rootBytes, buf,
chainhash.HashSize,
)
if err != nil {
return fmt.Errorf("unable to read custom "+
"tapscript tree root: %w", err)
}

var root chainhash.Hash
copy(root[:], rootBytes[:])
customTapscriptRoot = fn.Some(root)
}

*typ = asset.NewGroupKeyRevealV1(
rawKey, tapscriptRoot, customTapscriptRoot,
)

return nil
}
return tlv.NewTypeForEncodingErr(val, "GroupKeyReveal")
Expand Down

0 comments on commit a5e08bb

Please sign in to comment.