From 490b669428227b511a9d78b2bf295b83c6a6dfea Mon Sep 17 00:00:00 2001 From: ffranr Date: Sun, 15 Dec 2024 14:25:33 +0000 Subject: [PATCH 01/12] asset: remove unused method Genesis.GroupKeyTweak Removed to make the code clearer. --- asset/asset.go | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/asset/asset.go b/asset/asset.go index 661267f2c..ee92b0af8 100644 --- a/asset/asset.go +++ b/asset/asset.go @@ -224,16 +224,6 @@ func (g Genesis) ID() ID { return *(*ID)(h.Sum(nil)) } -// GroupKeyTweak returns the tweak bytes that commit to the previous outpoint, -// output index and type of the genesis. -func (g Genesis) GroupKeyTweak() []byte { - var keyGroupBytes bytes.Buffer - _ = wire.WriteOutPoint(&keyGroupBytes, 0, 0, &g.FirstPrevOut) - _ = binary.Write(&keyGroupBytes, binary.BigEndian, g.OutputIndex) - _ = binary.Write(&keyGroupBytes, binary.BigEndian, g.Type) - return keyGroupBytes.Bytes() -} - // Encode encodes an asset genesis. func (g Genesis) Encode(w io.Writer) error { var buf [8]byte From 6e27160930097d9d82ae8fa1920b1ea7c42309d0 Mon Sep 17 00:00:00 2001 From: ffranr Date: Tue, 10 Dec 2024 15:29:22 +0000 Subject: [PATCH 02/12] asset+proof: add `Encode` method to `GroupKeyReveal` interface Introduce an `Encode` method to the `GroupKeyReveal` interface, enabling encoding to be delegated to the appropriate `GroupKeyReveal` version. --- asset/asset.go | 21 +++++++++++++++++++++ proof/encoding.go | 12 ++++++------ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/asset/asset.go b/asset/asset.go index ee92b0af8..98a6d89d2 100644 --- a/asset/asset.go +++ b/asset/asset.go @@ -911,6 +911,9 @@ type GroupVirtualTx struct { // GroupKeyReveal represents the data used to derive the adjusted key that // uniquely identifies an asset group. type GroupKeyReveal interface { + // Encode encodes the group key reveal into a writer. + Encode(w io.Writer) error + // RawKey returns the raw key of the group key reveal. RawKey() SerializedKey @@ -959,6 +962,24 @@ func NewGroupKeyRevealV0(rawKey SerializedKey, } } +// Encode encodes the group key reveal into the writer. +func (g *GroupKeyRevealV0) Encode(w io.Writer) error { + // Define a placeholder scratch buffer which won't be used. + var buf [8]byte + + // Encode the raw key into the writer. + if err := SerializedKeyEncoder(w, &g.rawKey, &buf); err != nil { + return err + } + + // Encode the tapscript root into the writer. + if err := tlv.EVarBytes(w, &g.tapscriptRoot, &buf); err != nil { + return err + } + + return nil +} + // RawKey returns the raw key of the group key reveal. func (g *GroupKeyRevealV0) RawKey() SerializedKey { return g.rawKey diff --git a/proof/encoding.go b/proof/encoding.go index 342e3a6a3..bd20bb210 100644 --- a/proof/encoding.go +++ b/proof/encoding.go @@ -466,14 +466,14 @@ func GenesisRevealDecoder(r io.Reader, val any, buf *[8]byte, l uint64) error { return tlv.NewTypeForEncodingErr(val, "GenesisReveal") } -func GroupKeyRevealEncoder(w io.Writer, val any, buf *[8]byte) error { +func GroupKeyRevealEncoder(w io.Writer, val any, _ *[8]byte) error { if t, ok := val.(*asset.GroupKeyReveal); ok { - key := (*t).RawKey() - if err := asset.SerializedKeyEncoder(w, &key, buf); err != nil { - return err + if err := (*t).Encode(w); err != nil { + return fmt.Errorf("unable to encode group key "+ + "reveal: %w", err) } - root := (*t).TapscriptRoot() - return tlv.EVarBytes(w, &root, buf) + + return nil } return tlv.NewTypeForEncodingErr(val, "GroupKeyReveal") From c3961272823b68f6bd97647b73da61cee38a86cc Mon Sep 17 00:00:00 2001 From: ffranr Date: Tue, 10 Dec 2024 17:00:09 +0000 Subject: [PATCH 03/12] asset+proof: add `Decode` method to `GroupKeyReveal` interface Introduce an `Decode` method to the `GroupKeyReveal` interface, enabling decoding to be delegated to the appropriate `GroupKeyReveal` version. --- asset/asset.go | 38 ++++++++++++++++++++++++++++++++++++++ proof/encoding.go | 37 +++++++++++-------------------------- 2 files changed, 49 insertions(+), 26 deletions(-) diff --git a/asset/asset.go b/asset/asset.go index 98a6d89d2..18b7357a5 100644 --- a/asset/asset.go +++ b/asset/asset.go @@ -914,6 +914,9 @@ type GroupKeyReveal interface { // Encode encodes the group key reveal into a writer. Encode(w io.Writer) error + // Decode decodes the group key reveal from a reader. + Decode(r io.Reader, buf *[8]byte, l uint64) error + // RawKey returns the raw key of the group key reveal. RawKey() SerializedKey @@ -980,6 +983,41 @@ func (g *GroupKeyRevealV0) Encode(w io.Writer) error { return nil } +// Decode decodes the group key reveal from the reader. +func (g *GroupKeyRevealV0) Decode(r io.Reader, buf *[8]byte, l uint64) error { + // Verify that the group key reveal is not excessively long. This check + // is essential to prevent misinterpreting V1 and later group key + // reveals as V0. + if l > btcec.PubKeyBytesLenCompressed+sha256.Size { + return tlv.ErrRecordTooLarge + } + + if l < btcec.PubKeyBytesLenCompressed { + return fmt.Errorf("group key reveal too short") + } + + var rawKey SerializedKey + err := SerializedKeyDecoder( + r, &rawKey, buf, btcec.PubKeyBytesLenCompressed, + ) + if err != nil { + return err + } + + remaining := l - btcec.PubKeyBytesLenCompressed + var tapscriptRoot []byte + err = tlv.DVarBytes(r, &tapscriptRoot, buf, remaining) + if err != nil { + return err + } + + // Set fields now that decoding is complete. + g.rawKey = rawKey + g.tapscriptRoot = tapscriptRoot + + return nil +} + // RawKey returns the raw key of the group key reveal. func (g *GroupKeyRevealV0) RawKey() SerializedKey { return g.rawKey diff --git a/proof/encoding.go b/proof/encoding.go index bd20bb210..b5de89a8b 100644 --- a/proof/encoding.go +++ b/proof/encoding.go @@ -2,13 +2,11 @@ package proof import ( "bytes" - "crypto/sha256" "fmt" "io" "math" "github.com/btcsuite/btcd/blockchain" - "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/wire" "github.com/lightninglabs/taproot-assets/asset" "github.com/lightningnetwork/lnd/tlv" @@ -480,32 +478,19 @@ func GroupKeyRevealEncoder(w io.Writer, val any, _ *[8]byte) error { } func GroupKeyRevealDecoder(r io.Reader, val any, buf *[8]byte, l uint64) error { - if l > btcec.PubKeyBytesLenCompressed+sha256.Size { - return tlv.ErrRecordTooLarge + typ, ok := val.(*asset.GroupKeyReveal) + if !ok { + return tlv.NewTypeForEncodingErr(val, "GroupKeyReveal") } - if l < btcec.PubKeyBytesLenCompressed { - return fmt.Errorf("%w: group key reveal too short", - ErrProofInvalid) + // Attempt decoding with GroupKeyRevealV0. + var gkrV0 asset.GroupKeyRevealV0 + err := gkrV0.Decode(r, buf, l) + if err != nil { + return fmt.Errorf("group key reveal V0 decode error: %w", err) } - if typ, ok := val.(*asset.GroupKeyReveal); ok { - var rawKey asset.SerializedKey - err := asset.SerializedKeyDecoder( - r, &rawKey, buf, btcec.PubKeyBytesLenCompressed, - ) - if err != nil { - return err - } - remaining := l - btcec.PubKeyBytesLenCompressed - var tapscriptRoot []byte - err = tlv.DVarBytes(r, &tapscriptRoot, buf, remaining) - if err != nil { - return err - } - - *typ = asset.NewGroupKeyRevealV0(rawKey, tapscriptRoot) - return nil - } - return tlv.NewTypeForEncodingErr(val, "GroupKeyReveal") + // If the decoding was successful, set the value and return. + *typ = &gkrV0 + return nil } From bdf88bcfa234708b703b9e0d3f5e1e229d66dcd7 Mon Sep 17 00:00:00 2001 From: ffranr Date: Thu, 12 Dec 2024 16:10:42 +0000 Subject: [PATCH 04/12] asset: move TapBranchHash from `commitment` to `asset` package --- asset/asset.go | 9 +++++++++ commitment/tap.go | 11 +---------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/asset/asset.go b/asset/asset.go index 18b7357a5..3369f2319 100644 --- a/asset/asset.go +++ b/asset/asset.go @@ -719,6 +719,15 @@ func CheckTapLeafSanity(leaf *txscript.TapLeaf) error { return nil } +// 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[:]) +} + // TapLeafNodes represents an ordered list of TapLeaf objects, that have been // checked for their script version and size. These leaves can be stored to and // loaded from the DB. diff --git a/commitment/tap.go b/commitment/tap.go index d8ed80e5c..0ce4d478b 100644 --- a/commitment/tap.go +++ b/commitment/tap.go @@ -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 { @@ -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 From 399d6eba8330c25e1ce439756cdad3a99347adbb Mon Sep 17 00:00:00 2001 From: ffranr Date: Tue, 10 Dec 2024 14:09:31 +0000 Subject: [PATCH 05/12] asset: add GroupKeyRevealV1 Introduce GroupKeyRevealV1, which avoids tweaking the internal key with the asset ID. Instead, the asset ID is used to tweak the tapscript tree. This enables an external PSBT signer to sign with the internal key without requiring knowledge of the tweak. For more information, see the in-code documentation. --- asset/asset.go | 442 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 438 insertions(+), 4 deletions(-) diff --git a/asset/asset.go b/asset/asset.go index 3369f2319..1fffbbf8b 100644 --- a/asset/asset.go +++ b/asset/asset.go @@ -22,6 +22,7 @@ import ( "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" + "github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/lightninglabs/lndclient" "github.com/lightninglabs/taproot-assets/fn" "github.com/lightninglabs/taproot-assets/mssmt" @@ -943,6 +944,440 @@ type GroupKeyReveal interface { GroupPubKey(assetID ID) (*btcec.PublicKey, error) } +// NewNonSpendableScriptLeaf creates a new non-spendable tapscript script leaf +// that includes the specified data. If the data is nil, the leaf will not +// contain any data but will still be a valid non-spendable script leaf. +// +// The script leaf is made non-spendable by including an OP_RETURN opcode at the +// start of the script. While the script can still be executed, it will always +// fail and cannot be used to spend funds. +func NewNonSpendableScriptLeaf(data []byte) (txscript.TapLeaf, error) { + // Construct a script builder and add the OP_RETURN opcode to the start + // of the script to ensure that the script is non-executable. + scriptBuilder := txscript.NewScriptBuilder().AddOp(txscript.OP_RETURN) + + // Add the data to the script if it is provided. + if data != nil { + scriptBuilder.AddData(data) + } + + // Construct script from the script builder. + script, err := scriptBuilder.Script() + if err != nil { + return txscript.TapLeaf{}, fmt.Errorf("failed to construct "+ + "non-spendable script: %w", err) + } + + // Create a new tapscript leaf from the script. + leaf := txscript.NewBaseTapLeaf(script) + return leaf, nil +} + +// GroupKeyRevealTlvType represents the different TLV types for GroupKeyReveal +// TLV records. +type GroupKeyRevealTlvType = tlv.Type + +const ( + GKRVersion GroupKeyRevealTlvType = 0 + GKRInternalKey GroupKeyRevealTlvType = 2 + GKRTapscriptRoot GroupKeyRevealTlvType = 4 + GKRCustomSubtreeRoot GroupKeyRevealTlvType = 7 +) + +func NewGKRVersionRecord(version *uint8) tlv.Record { + return tlv.MakePrimitiveRecord(GKRVersion, version) +} + +func NewGKRInternalKeyRecord(internalKey *SerializedKey) tlv.Record { + return tlv.MakePrimitiveRecord(GKRInternalKey, (*[33]byte)(internalKey)) +} + +func NewGKRTapscriptRootRecord(root *chainhash.Hash) tlv.Record { + return tlv.MakePrimitiveRecord(GKRTapscriptRoot, (*[32]byte)(root)) +} + +func NewGKRCustomSubtreeRootRecord(root *chainhash.Hash) tlv.Record { + return tlv.MakePrimitiveRecord(GKRCustomSubtreeRoot, (*[32]byte)(root)) +} + +// GroupKeyRevealTapscript holds data used to derive the tapscript root, which +// is then used to calculate the asset group key. +// +// More broadly, the asset group key is the Taproot output key, derived using +// the standard formula: +// +// outputKey = internalKey + TapTweak(internalKey || tapscriptRoot) * G +// +// This formula demonstrates that the asset group key (Taproot output key) +// commits to both the internal key and the tapscript tree root hash. +// +// By design, the tapscript root commits to a single genesis asset ID, which +// ensures that the asset group key also commits to the same unique genesis +// asset ID. This 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 tapscript tree is formulated to guarantee that only one recognizable +// genesis asset ID can exist in the tree. The ID is uniquely placed in the +// first leaf layer, which contains exactly two nodes: the ID leaf and its +// sibling. The sibling node is deliberately constructed to ensure it cannot be +// mistaken for a genesis asset ID leaf. +// +// The sibling node, `[tweaked_custom_branch]`, of the genesis asset ID leaf is +// a branch node by design. It serves two purposes: +// 1. It ensures that only one genesis asset ID leaf can exist in the first +// layer, as it is not a valid genesis asset ID leaf. +// 2. It optionally supports user-defined script spending leaves, enabling +// flexibility for custom tapscript subtrees. +// +// User-defined script spending leaves are nested under +// `[tweaked_custom_branch]` as a single node hash, `custom_root_hash`. This +// hash may represent either a single leaf or the root hash of an entire +// subtree. +// +// If `custom_root_hash` is not provided, it defaults to a 32-byte zero-filled +// array. In this case, no valid script spending path can correspond to the +// custom subtree root hash due to the pre-image resistance of SHA-256. +// +// A sibling node is included alongside the `custom_root_hash` node. This +// sibling is a non-spendable script leaf containing `[OP_RETURN]`. Its +// presence ensures that one of the two positions in the first layer of the +// tapscript tree is occupied by a branch node. Due to the pre-image +// resistance of SHA-256, this prevents the existence of a second recognizable +// genesis asset ID leaf. +// +// The final tapscript tree adopts the following structure: +// +// [tapscript_root] +// / \ +// [OP_RETURN ] [tweaked_custom_branch] +// / \ +// [OP_RETURN] +// +// Where: +// - [tapscript_root] is the root of the final tapscript tree. +// - [OP_RETURN ] is a first-layer non-spendable script +// leaf that commits to the genesis asset ID. +// - [tweaked_custom_branch] is a branch node that serves two purposes: +// 1. It cannot be misinterpreted as a genesis asset ID leaf. +// 2. It optionally includes user-defined script spending leaves. +// - is the root hash of the custom tapscript subtree. +// If not specified, it defaults to a 32-byte zero-filled array. +// - [OP_RETURN] is a non-spendable script leaf containing the script +// `OP_RETURN`. Its presence ensures that [tweaked_custom_branch] remains +// a branch node and cannot be a valid genesis asset ID leaf. +type GroupKeyRevealTapscript struct { + // root 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. + root chainhash.Hash + + // customSubtreeRoot is an optional root hash representing a + // user-defined tapscript subtree that is integrated into the final + // tapscript tree. This subtree may define script spending conditions + // associated with the group key. + customSubtreeRoot fn.Option[chainhash.Hash] + + // customSubtreeInclusionProof provides the inclusion proof for the + // custom tapscript subtree. It is required to spend the custom + // tapscript leaves within the tree. + // + // NOTE: This field should not be serialized as part of the group key + // reveal. It is included here to ensure it is constructed concurrently + // with the tapscript root, maintaining consistency and minimizing + // errors. + customSubtreeInclusionProof []byte +} + +// NewGroupKeyTapscriptRoot computes the final tapscript root hash +// which is used to derive the asset group key. The final tapscript root +// hash is computed from the genesis asset ID and an optional custom tapscript +// subtree root hash. +// +// nolint: lll +func NewGroupKeyTapscriptRoot(genesisAssetID ID, + customRoot fn.Option[chainhash.Hash]) (GroupKeyRevealTapscript, error) { + + // First, we compute the tweaked custom branch hash. This hash is + // derived by combining the hash of a non-spendable leaf and the root + // hash of the custom tapscript subtree. + // + // If a custom tapscript subtree root hash is provided, we use it. + // Otherwise, we default to an empty hash (a zero-filled byte array). + emptyNonSpendLeaf, err := NewNonSpendableScriptLeaf(nil) + if err != nil { + return GroupKeyRevealTapscript{}, err + } + + // Compute the tweaked custom branch hash. + tweakedCustomBranchHash := TapBranchHash( + emptyNonSpendLeaf.TapHash(), + customRoot.UnwrapOr(chainhash.Hash{}), + ) + + // Next, we'll combine the tweaked custom branch hash with the genesis + // asset ID leaf hash to compute the final tapscript root hash. + // + // Construct a non-spendable tapscript leaf for the genesis asset ID. + assetIDLeaf, err := NewNonSpendableScriptLeaf(genesisAssetID[:]) + if err != nil { + return GroupKeyRevealTapscript{}, err + } + + // Compute final tapscript root hash. This is the root hash of the + // tapscript tree that is used to derive the asset group key. + rootHash := TapBranchHash( + assetIDLeaf.TapHash(), tweakedCustomBranchHash, + ) + + // Construct the custom subtree inclusion proof. This proof is required + // to spend custom tapscript leaves in the tapscript tree. + emptyNonSpendLeafHash := emptyNonSpendLeaf.TapHash() + assetIDLeafHash := assetIDLeaf.TapHash() + + customSubtreeInclusionProof := bytes.Join( + [][]byte{ + emptyNonSpendLeafHash[:], + assetIDLeafHash[:], + }, nil, + ) + + return GroupKeyRevealTapscript{ + root: rootHash, + customSubtreeRoot: customRoot, + customSubtreeInclusionProof: customSubtreeInclusionProof, + }, nil +} + +// Validate checks that the group key reveal tapscript is well-formed and +// compliant. +func (g *GroupKeyRevealTapscript) Validate(assetID ID) error { + // Compute the final tapscript root hash from the genesis asset ID and + // the custom tapscript subtree root hash. + tapscript, err := NewGroupKeyTapscriptRoot(assetID, g.customSubtreeRoot) + if err != nil { + return fmt.Errorf("failed to compute tapscript root hash: %w", + err) + } + + // Ensure that the final tapscript root hash matches the computed root + // hash. + customRoot := g.customSubtreeRoot.UnwrapOr(chainhash.Hash{}) + + if !g.root.IsEqual(&tapscript.root) { + return fmt.Errorf("failed to derive tapscript root from "+ + "internal key, genesis asset ID, and "+ + "custom subtree root (expected_root=%s, "+ + "computed_root=%s, custom_subtree_root=%s, "+ + "genesis_asset_id=%x)", + g.root, tapscript.root, customRoot, assetID[:]) + } + + 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 { + // version is the version of the group key reveal. + version uint8 + + // internalKey refers to the internal key used to derive the asset + // group key. Typically, this internal key is the user's signing public + // key. + internalKey SerializedKey + + // tapscript is the tapscript tree that commits to the genesis asset ID + // and any script spend conditions for the group key. + tapscript GroupKeyRevealTapscript +} + +// Ensure that GroupKeyRevealV1 implements the GroupKeyReveal interface. +var _ GroupKeyReveal = (*GroupKeyRevealV1)(nil) + +// NewGroupKeyRevealV1 creates a new version 1 group key reveal instance. +func NewGroupKeyRevealV1(internalKey btcec.PublicKey, + genesisAssetID ID, + customRoot fn.Option[chainhash.Hash]) (GroupKeyRevealV1, error) { + + // Compute the final tapscript root. + gkrTapscript, err := NewGroupKeyTapscriptRoot( + genesisAssetID, customRoot, + ) + if err != nil { + return GroupKeyRevealV1{}, fmt.Errorf("failed to generate "+ + "group key reveal tapscript: %w", err) + } + + return GroupKeyRevealV1{ + version: 1, + internalKey: ToSerialized(&internalKey), + tapscript: gkrTapscript, + }, nil +} + +// ScriptSpendControlBlock returns the control block for the script spending +// path in the custom tapscript subtree. +// +// nolint: lll +func (g *GroupKeyRevealV1) ScriptSpendControlBlock( + genesisAssetID ID) (txscript.ControlBlock, error) { + + internalKey, err := btcec.ParsePubKey(g.internalKey[:]) + if err != nil { + return txscript.ControlBlock{}, fmt.Errorf("failed to parse "+ + "internal key: %w", err) + } + + outputKey := txscript.ComputeTaprootOutputKey( + internalKey, g.tapscript.root[:], + ) + outputKeyIsOdd := outputKey.SerializeCompressed()[0] == + secp256k1.PubKeyFormatCompressedOdd + + // If the custom subtree inclusion proof is nil, it may not have been + // set during decoding. Compute it, set it on the group key reveal, and + // validate both the computed tapscript root against the expected + // root. + if len(g.tapscript.customSubtreeInclusionProof) == 0 { + gkrTapscript, err := NewGroupKeyTapscriptRoot( + genesisAssetID, g.tapscript.customSubtreeRoot, + ) + if err != nil { + return txscript.ControlBlock{}, fmt.Errorf("failed to "+ + "generate tapscript artifacts: %w", err) + } + + // Ensure that the computed tapscript root matches the expected + // root. + if !gkrTapscript.root.IsEqual(&g.tapscript.root) { + return txscript.ControlBlock{}, fmt.Errorf("tapscript "+ + "root mismatch (expected=%s, computed=%s)", + g.tapscript.root, gkrTapscript.root) + } + + // Set the custom subtree inclusion proof on the group key + // reveal. + g.tapscript.customSubtreeInclusionProof = + gkrTapscript.customSubtreeInclusionProof + } + + return txscript.ControlBlock{ + InternalKey: internalKey, + OutputKeyYIsOdd: outputKeyIsOdd, + LeafVersion: txscript.BaseLeafVersion, + InclusionProof: g.tapscript.customSubtreeInclusionProof, + }, nil +} + +// Encode encodes the group key reveal into a writer. +// +// This encoding routine must ensure the resulting serialized bytes are +// sufficiently long to prevent the decoding routine from mistakenly using the +// wrong group key reveal version. Specifically, the raw key, tapscript root, +// and version fields must be properly populated. +func (g *GroupKeyRevealV1) Encode(w io.Writer) error { + records := []tlv.Record{ + NewGKRVersionRecord(&g.version), + NewGKRInternalKeyRecord(&g.internalKey), + NewGKRTapscriptRootRecord(&g.tapscript.root), + } + + // Add encode record for the custom tapscript root, if present. + g.tapscript.customSubtreeRoot.WhenSome(func(hash chainhash.Hash) { + records = append(records, NewGKRCustomSubtreeRootRecord(&hash)) + }) + + stream, err := tlv.NewStream(records...) + if err != nil { + return err + } + return stream.Encode(w) +} + +// Decode decodes the group key reveal from a reader. +func (g *GroupKeyRevealV1) Decode(r io.Reader, buf *[8]byte, l uint64) error { + var customSubtreeRoot chainhash.Hash + + tlvStream, err := tlv.NewStream( + NewGKRVersionRecord(&g.version), + NewGKRInternalKeyRecord(&g.internalKey), + NewGKRTapscriptRootRecord(&g.tapscript.root), + NewGKRCustomSubtreeRootRecord(&customSubtreeRoot), + ) + if err != nil { + return err + } + + // Decode the reader's contents into the tlv stream. + _, err = tlvStream.DecodeWithParsedTypes(r) + if err != nil { + return err + } + + // If the custom subtree root is not zero, set it on the group key + // reveal. + var zeroHash chainhash.Hash + if customSubtreeRoot != zeroHash { + g.tapscript.customSubtreeRoot = + fn.Some[chainhash.Hash](customSubtreeRoot) + } + + return nil +} + +// RawKey returns the raw key of the group key reveal. +func (g *GroupKeyRevealV1) RawKey() SerializedKey { + return g.internalKey +} + +// SetRawKey sets the raw key of the group key reveal. +func (g *GroupKeyRevealV1) SetRawKey(rawKey SerializedKey) { + g.internalKey = rawKey +} + +// TapscriptRoot returns the tapscript root of the group key reveal. +func (g *GroupKeyRevealV1) TapscriptRoot() []byte { + return g.tapscript.root[:] +} + +// SetTapscriptRoot sets the tapscript root of the group key reveal. +func (g *GroupKeyRevealV1) SetTapscriptRoot(tapscriptRootBytes []byte) { + var tapscriptRoot chainhash.Hash + copy(tapscriptRoot[:], tapscriptRootBytes) + + g.tapscript.root = 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, g.tapscript, assetID) +} + +// GroupPubKeyV1 derives a version 1 asset group key from a signing public key +// and a tapscript tree. +func GroupPubKeyV1(internalKey *btcec.PublicKey, + tapscriptTree GroupKeyRevealTapscript, assetID ID) (*btcec.PublicKey, + error) { + + err := tapscriptTree.Validate(assetID) + if err != nil { + return nil, fmt.Errorf("group key reveal tapscript tree "+ + "invalid: %w", err) + } + + tapOutputKey := txscript.ComputeTaprootOutputKey( + internalKey, tapscriptTree.root[:], + ) + 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) @@ -997,11 +1432,10 @@ func (g *GroupKeyRevealV0) Decode(r io.Reader, buf *[8]byte, l uint64) error { // Verify that the group key reveal is not excessively long. This check // is essential to prevent misinterpreting V1 and later group key // reveals as V0. - if l > btcec.PubKeyBytesLenCompressed+sha256.Size { + switch { + case l > btcec.PubKeyBytesLenCompressed+sha256.Size: return tlv.ErrRecordTooLarge - } - - if l < btcec.PubKeyBytesLenCompressed { + case l < btcec.PubKeyBytesLenCompressed: return fmt.Errorf("group key reveal too short") } From 11f91599be9d8b2f11bd844eaa8737349dcbe3f1 Mon Sep 17 00:00:00 2001 From: ffranr Date: Wed, 11 Dec 2024 13:01:19 +0000 Subject: [PATCH 06/12] asset+proof: update GroupKeyRevealDecoder to support multiple versions Modify GroupKeyRevealDecoder to handle decoding of both GroupKeyRevealV0 and GroupKeyRevealV1 formats. --- proof/encoding.go | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/proof/encoding.go b/proof/encoding.go index b5de89a8b..10e60817f 100644 --- a/proof/encoding.go +++ b/proof/encoding.go @@ -2,11 +2,13 @@ package proof import ( "bytes" + "crypto/sha256" "fmt" "io" "math" "github.com/btcsuite/btcd/blockchain" + "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/wire" "github.com/lightninglabs/taproot-assets/asset" "github.com/lightningnetwork/lnd/tlv" @@ -478,19 +480,40 @@ func GroupKeyRevealEncoder(w io.Writer, val any, _ *[8]byte) error { } 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.(*asset.GroupKeyReveal) if !ok { return tlv.NewTypeForEncodingErr(val, "GroupKeyReveal") } - // Attempt decoding with GroupKeyRevealV0. - var gkrV0 asset.GroupKeyRevealV0 - err := gkrV0.Decode(r, buf, l) + // 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 asset.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 asset.GroupKeyRevealV1 + + err := gkrV1.Decode(r, buf, l) if err != nil { - return fmt.Errorf("group key reveal V0 decode error: %w", err) + return fmt.Errorf("group key reveal V1 decode error: %w", err) } - // If the decoding was successful, set the value and return. - *typ = &gkrV0 + *typ = &gkrV1 return nil } From 6112ca1c9f9c222b4930beecce222b9ba396fd1d Mon Sep 17 00:00:00 2001 From: ffranr Date: Tue, 17 Dec 2024 18:49:47 +0000 Subject: [PATCH 07/12] asset: move GroupKeyRevealEncoder and Decoder from proof package Relocate GroupKeyRevealEncoder and GroupKeyRevealDecoder to the asset package to allow direct interrogation in unit tests. --- asset/encoding.go | 52 +++++++++++++++++++++++++++++++++++++++++++++ proof/encoding.go | 54 ----------------------------------------------- proof/records.go | 4 ++-- 3 files changed, 54 insertions(+), 56 deletions(-) diff --git a/asset/encoding.go b/asset/encoding.go index c28b01db8..6e99656aa 100644 --- a/asset/encoding.go +++ b/asset/encoding.go @@ -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 +} diff --git a/proof/encoding.go b/proof/encoding.go index 10e60817f..1b371dc65 100644 --- a/proof/encoding.go +++ b/proof/encoding.go @@ -2,13 +2,11 @@ package proof import ( "bytes" - "crypto/sha256" "fmt" "io" "math" "github.com/btcsuite/btcd/blockchain" - "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/wire" "github.com/lightninglabs/taproot-assets/asset" "github.com/lightningnetwork/lnd/tlv" @@ -465,55 +463,3 @@ func GenesisRevealDecoder(r io.Reader, val any, buf *[8]byte, l uint64) error { return tlv.NewTypeForEncodingErr(val, "GenesisReveal") } - -func GroupKeyRevealEncoder(w io.Writer, val any, _ *[8]byte) error { - if t, ok := val.(*asset.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.(*asset.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 asset.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 asset.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 -} diff --git a/proof/records.go b/proof/records.go index 1bd5e7584..eeeeb2342 100644 --- a/proof/records.go +++ b/proof/records.go @@ -389,7 +389,7 @@ func GroupKeyRevealRecord(reveal *asset.GroupKeyReveal) tlv.Record { ) } return tlv.MakeDynamicRecord( - GroupKeyRevealType, reveal, recordSize, GroupKeyRevealEncoder, - GroupKeyRevealDecoder, + GroupKeyRevealType, reveal, recordSize, + asset.GroupKeyRevealEncoder, asset.GroupKeyRevealDecoder, ) } From 4208122b90bebe8a0a5020f65f1f69ca58c20ded Mon Sep 17 00:00:00 2001 From: ffranr Date: Tue, 17 Dec 2024 13:51:34 +0000 Subject: [PATCH 08/12] asset: add GroupKeyRevealV1 encoding/decoding tests Tests also validate spend script control block. --- asset/group_key_reveal_test.go | 261 +++++++++++++++++++++++++++++++++ 1 file changed, 261 insertions(+) create mode 100644 asset/group_key_reveal_test.go diff --git a/asset/group_key_reveal_test.go b/asset/group_key_reveal_test.go new file mode 100644 index 000000000..4d09b2760 --- /dev/null +++ b/asset/group_key_reveal_test.go @@ -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, + ) + } + }) +} From fac93e6c0d76f4f3ae2e50f4a7b357483a476946 Mon Sep 17 00:00:00 2001 From: ffranr Date: Thu, 19 Dec 2024 01:26:25 +0000 Subject: [PATCH 09/12] tapgarden: add unit tests for GKR spend paths with custom scripts Add unit tests to verify the different possible spend paths for a group key reveal witness when custom scripts are used. These tests were authored by @guggero. --- tapgarden/planter_test.go | 254 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 254 insertions(+) diff --git a/tapgarden/planter_test.go b/tapgarden/planter_test.go index 1ae6262a8..e20102350 100644 --- a/tapgarden/planter_test.go +++ b/tapgarden/planter_test.go @@ -21,6 +21,7 @@ import ( "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/davecgh/go-spew/spew" + "github.com/lightninglabs/lndclient" tap "github.com/lightninglabs/taproot-assets" "github.com/lightninglabs/taproot-assets/asset" "github.com/lightninglabs/taproot-assets/commitment" @@ -2058,6 +2059,259 @@ func TestBatchedAssetIssuance(t *testing.T) { } } +// TestGroupKeyRevealV1WitnessWithCustomRoot tests the different possible spend +// paths for a group key reveal witness if there are custom scripts. +func TestGroupKeyRevealV1WitnessWithCustomRoot(t *testing.T) { + var ( + ctx = context.Background() + mockKeyRing = tapgarden.NewMockKeyRing() + mockSigner = tapgarden.NewMockGenSigner(mockKeyRing) + txBuilder = &tapscript.GroupTxBuilder{} + txValidator = &tap.ValidatorV0{} + hashLockPreimage = []byte("foobar") + ) + + // We expect two keys to be derived from the mock. + go func() { + <-mockKeyRing.ReqKeys + <-mockKeyRing.ReqKeys + }() + + // The internal key is for the actual internal key of the group. + internalKeyDesc, err := mockKeyRing.DeriveNextTaprootAssetKey(ctx) + require.NoError(t, err) + + // The second key is used for a signature spend within a tapscript leaf + // of the custom tapscript tree. + secondKeyDesc, err := mockKeyRing.DeriveNextTaprootAssetKey(ctx) + require.NoError(t, err) + + hashLockLeaf := test.ScriptHashLock(t, hashLockPreimage) + schnorrSigLeaf := test.ScriptSchnorrSig(t, secondKeyDesc.PubKey) + + userRoot := txscript.AssembleTaprootScriptTree( + hashLockLeaf, schnorrSigLeaf, + ).RootNode.TapHash() + + spendTestCases := []struct { + name string + genWitness func(*testing.T, *asset.Asset, + asset.GroupKeyRevealV1) wire.TxWitness + }{{ + name: "key spend", + genWitness: func(t *testing.T, a *asset.Asset, + gkr asset.GroupKeyRevealV1) wire.TxWitness { + + genTx, prevOut, err := txBuilder.BuildGenesisTx(a) + require.NoError(t, err) + + witness, err := signGroupKeyV1( + internalKeyDesc, gkr, genTx, prevOut, + mockSigner, nil, + ) + require.NoError(t, err) + + return witness + }, + }, { + name: "script spend with preimage", + genWitness: func(t *testing.T, a *asset.Asset, + gkr asset.GroupKeyRevealV1) wire.TxWitness { + + controlBlock, err := gkr.ScriptSpendControlBlock( + a.ID(), + ) + require.NoError(t, err) + + controlBlock.InclusionProof = bytes.Join([][]byte{ + fn.ByteSlice(schnorrSigLeaf.TapHash()), + controlBlock.InclusionProof, + }, nil) + controlBlockBytes, err := controlBlock.ToBytes() + require.NoError(t, err) + + // Witness is just the preimage, the script and the + // control block. + return wire.TxWitness{ + hashLockPreimage, + hashLockLeaf.Script, + controlBlockBytes, + } + }, + }, { + name: "script spend with signature", + genWitness: func(t *testing.T, a *asset.Asset, + gkr asset.GroupKeyRevealV1) wire.TxWitness { + + genTx, prevOut, err := txBuilder.BuildGenesisTx(a) + require.NoError(t, err) + + controlBlock, err := gkr.ScriptSpendControlBlock( + a.ID(), + ) + require.NoError(t, err) + + controlBlock.InclusionProof = bytes.Join([][]byte{ + fn.ByteSlice(hashLockLeaf.TapHash()), + controlBlock.InclusionProof, + }, nil) + controlBlockBytes, err := controlBlock.ToBytes() + require.NoError(t, err) + + leafToSign := &psbt.TaprootTapLeafScript{ + ControlBlock: controlBlockBytes, + Script: schnorrSigLeaf.Script, + LeafVersion: txscript.BaseLeafVersion, + } + + witness, err := signGroupKeyV1( + secondKeyDesc, gkr, genTx, prevOut, mockSigner, + leafToSign, + ) + require.NoError(t, err) + + return witness + }, + }} + + for _, tc := range spendTestCases { + t.Run(tc.name, func(tt *testing.T) { + randAsset := asset.RandAsset(tt, asset.Normal) + genAssetID := randAsset.ID() + groupKeyReveal, err := asset.NewGroupKeyRevealV1( + *internalKeyDesc.PubKey, genAssetID, + fn.Some(userRoot), + ) + require.NoError(tt, err) + + // Set the group key on the asset, since it's a randomly + // created group key otherwise. + groupPubKey, err := groupKeyReveal.GroupPubKey( + genAssetID, + ) + require.NoError(tt, err) + randAsset.GroupKey = &asset.GroupKey{ + RawKey: internalKeyDesc, + GroupPubKey: *groupPubKey, + TapscriptRoot: groupKeyReveal.TapscriptRoot(), + } + randAsset.PrevWitnesses = []asset.Witness{ + { + PrevID: &asset.PrevID{}, + }, + } + + witness := tc.genWitness(tt, randAsset, groupKeyReveal) + randAsset.PrevWitnesses[0].TxWitness = witness + + err = txValidator.Execute( + randAsset, nil, nil, proof.MockChainLookup, + ) + require.NoError(tt, err) + }) + } +} + +// TestGroupKeyRevealV1WitnessNoScripts tests the key spend path for a group key +// reveal witness if there are no custom scripts. +func TestGroupKeyRevealV1WitnessNoScripts(t *testing.T) { + var ( + ctx = context.Background() + mockKeyRing = tapgarden.NewMockKeyRing() + mockSigner = tapgarden.NewMockGenSigner(mockKeyRing) + txBuilder = &tapscript.GroupTxBuilder{} + txValidator = &tap.ValidatorV0{} + ) + + // We expect just one key to be derived from the mock. + go func() { + <-mockKeyRing.ReqKeys + }() + + // The internal key is for the actual internal key of the group. + internalKeyDesc, err := mockKeyRing.DeriveNextTaprootAssetKey(ctx) + require.NoError(t, err) + + randAsset := asset.RandAsset(t, asset.Normal) + genAssetID := randAsset.ID() + groupKeyReveal, err := asset.NewGroupKeyRevealV1( + *internalKeyDesc.PubKey, genAssetID, fn.None[chainhash.Hash](), + ) + require.NoError(t, err) + + // Set the group key on the asset, since it's a randomly created group + // key otherwise. + groupPubKey, err := groupKeyReveal.GroupPubKey(genAssetID) + require.NoError(t, err) + randAsset.GroupKey = &asset.GroupKey{ + RawKey: internalKeyDesc, + GroupPubKey: *groupPubKey, + TapscriptRoot: groupKeyReveal.TapscriptRoot(), + } + randAsset.PrevWitnesses = []asset.Witness{ + { + PrevID: &asset.PrevID{}, + }, + } + + genTx, prevOut, err := txBuilder.BuildGenesisTx(randAsset) + require.NoError(t, err) + + witness, err := signGroupKeyV1( + internalKeyDesc, groupKeyReveal, genTx, prevOut, mockSigner, + nil, + ) + require.NoError(t, err) + + randAsset.PrevWitnesses[0].TxWitness = witness + + err = txValidator.Execute( + randAsset, nil, nil, proof.MockChainLookup, + ) + require.NoError(t, err) +} + +// signGroupKeyV1 is the equivalent for asset.DeriveGroupKey but for a V1 key. +func signGroupKeyV1(keyDesc keychain.KeyDescriptor, gk asset.GroupKeyRevealV1, + genTx *wire.MsgTx, prevOut *wire.TxOut, signer asset.GenesisSigner, + leafToSign *psbt.TaprootTapLeafScript) (wire.TxWitness, error) { + + signDesc := &lndclient.SignDescriptor{ + KeyDesc: keyDesc, + TapTweak: gk.TapscriptRoot(), + Output: prevOut, + HashType: txscript.SigHashDefault, + InputIndex: 0, + SignMethod: input.TaprootKeySpendSignMethod, + } + + if leafToSign != nil { + signDesc.SignMethod = input.TaprootScriptSpendSignMethod + signDesc.WitnessScript = leafToSign.Script + } + + sig, err := signer.SignVirtualTx(signDesc, genTx, prevOut) + if err != nil { + return nil, err + } + + witness := wire.TxWitness{sig.Serialize()} + + // If this was a script spend, we also have to add the script itself and + // the control block to the witness, otherwise the verifier will reject + // the generated witness. + if signDesc.SignMethod == input.TaprootScriptSpendSignMethod && + leafToSign != nil { + + witness = append( + witness, signDesc.WitnessScript, + leafToSign.ControlBlock, + ) + } + + return witness, nil +} + func init() { rand.Seed(time.Now().Unix()) From a7707d534c72a44086c08f81c9ba50737424e12e Mon Sep 17 00:00:00 2001 From: ffranr Date: Wed, 18 Dec 2024 22:11:14 +0000 Subject: [PATCH 10/12] proof: update GroupKeyRevealRecord size calc for V0 and V1 handling Refactored the GroupKeyRevealRecord size calculation to support both GroupKeyRevealV0 and GroupKeyRevealV1 formats. --- proof/records.go | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/proof/records.go b/proof/records.go index eeeeb2342..3d570f0ee 100644 --- a/proof/records.go +++ b/proof/records.go @@ -379,15 +379,25 @@ func GenesisRevealRecord(genesis **asset.Genesis) tlv.Record { } func GroupKeyRevealRecord(reveal *asset.GroupKeyReveal) tlv.Record { + // recordSize returns the size of the record in bytes. This is used to + // determine the size of the record when encoding it. recordSize := func() uint64 { if reveal == nil || *reveal == nil { return 0 } - r := *reveal - return uint64( - btcec.PubKeyBytesLenCompressed + len(r.TapscriptRoot()), + + var ( + b bytes.Buffer + buf [8]byte ) + err := asset.GroupKeyRevealEncoder(&b, reveal, &buf) + if err != nil { + panic(err) + } + + return uint64(len(b.Bytes())) } + return tlv.MakeDynamicRecord( GroupKeyRevealType, reveal, recordSize, asset.GroupKeyRevealEncoder, asset.GroupKeyRevealDecoder, From 976531452c74942f95ca9bbc45110068d122dfdc Mon Sep 17 00:00:00 2001 From: ffranr Date: Thu, 19 Dec 2024 02:34:46 +0000 Subject: [PATCH 11/12] proof: add test helpers to assert equality of group key reveal types Introduce helper functions to simplify testing by asserting equality between instances of group key reveal types. --- asset/asset.go | 6 +++ proof/proof_test.go | 118 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+) diff --git a/asset/asset.go b/asset/asset.go index 1fffbbf8b..efa7ffacd 100644 --- a/asset/asset.go +++ b/asset/asset.go @@ -1350,6 +1350,12 @@ func (g *GroupKeyRevealV1) SetTapscriptRoot(tapscriptRootBytes []byte) { g.tapscript.root = tapscriptRoot } +// CustomSubtreeRoot returns the custom subtree root hash of the group key +// reveal. +func (g *GroupKeyRevealV1) CustomSubtreeRoot() fn.Option[chainhash.Hash] { + return g.tapscript.customSubtreeRoot +} + // 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() diff --git a/proof/proof_test.go b/proof/proof_test.go index d1ce5c2c8..3bc9298f4 100644 --- a/proof/proof_test.go +++ b/proof/proof_test.go @@ -86,6 +86,119 @@ func assertEqualTaprootProof(t *testing.T, expected, actual *TaprootProof) { } } +// assertEqualGroupKeyRevealV0 asserts that the expected and actual group key +// reveal V0 are equal. +func assertEqualGroupKeyRevealV0(t *testing.T, expected, + actual asset.GroupKeyRevealV0, expectedGenesisAssetID asset.ID) { + + t.Helper() + + require.Equal(t, expected.RawKey(), actual.RawKey()) + + // Compare the tapscript root. Normalize nil to empty slice for + // comparison. + expectedRoot := expected.TapscriptRoot() + if expectedRoot == nil { + expectedRoot = []byte{} + } + + actualRoot := actual.TapscriptRoot() + if actualRoot == nil { + actualRoot = []byte{} + } + + require.Equal(t, expectedRoot, actualRoot) + + // Assert that the asset group pub key is equal for the expected genesis + // asset ID. + expectedGroupPubKey, err := expected.GroupPubKey(expectedGenesisAssetID) + require.NoError(t, err) + + actualGroupPubKey, err := actual.GroupPubKey(expectedGenesisAssetID) + require.NoError(t, err) + + require.Equal(t, expectedGroupPubKey, actualGroupPubKey) +} + +// assertEqualGroupKeyRevealV1 asserts that the expected and actual group key +// reveal V1 are equal. +func assertEqualGroupKeyRevealV1(t *testing.T, expected, + actual asset.GroupKeyRevealV1, expectedGenesisAssetID asset.ID) { + + require.Equal(t, expected.RawKey(), actual.RawKey()) + + // Compare the tapscript root. Normalize nil to empty slice for + // comparison. + expectedRoot := expected.TapscriptRoot() + if expectedRoot == nil { + expectedRoot = []byte{} + } + + actualRoot := actual.TapscriptRoot() + if actualRoot == nil { + actualRoot = []byte{} + } + + require.Equal(t, expectedRoot, actualRoot) + + // Assert that the asset group pub key is equal for the expected genesis + // asset ID. + expectedGroupPubKey, err := expected.GroupPubKey(expectedGenesisAssetID) + require.NoError(t, err) + + actualGroupPubKey, err := actual.GroupPubKey(expectedGenesisAssetID) + require.NoError(t, err) + + require.Equal(t, expectedGroupPubKey, actualGroupPubKey) + + // Compare the custom subtree root. + require.Equal( + t, expected.CustomSubtreeRoot(), actual.CustomSubtreeRoot(), + ) +} + +// assertEqualGroupKeyReveal asserts that the expected and actual group key +// reveal are equal. +func assertEqualGroupKeyReveal(t *testing.T, expected, + actual asset.GroupKeyReveal, expectedGenesisAssetID asset.ID) { + + t.Helper() + + // Handle nil cases. + if expected == nil { + require.Nil(t, actual) + return + } + require.NotNil(t, actual) + + // Dispatch to group key reveal version 0 assertion. + if expectedV0, ok := expected.(*asset.GroupKeyRevealV0); ok { + // If expected is V0 then actual should be V0. + actualV0, actualOk := actual.(*asset.GroupKeyRevealV0) + require.True(t, actualOk) + + assertEqualGroupKeyRevealV0( + t, *expectedV0, *actualV0, expectedGenesisAssetID, + ) + return + } + + // Dispatch to group key reveal version 1 assertion. + if expectedV1, ok := expected.(*asset.GroupKeyRevealV1); ok { + // If expected is V1 then actual should be V1. + actualV1, actualOk := actual.(*asset.GroupKeyRevealV1) + require.True(t, actualOk) + + assertEqualGroupKeyRevealV1( + t, *expectedV1, *actualV1, expectedGenesisAssetID, + ) + return + } + + // Unexpected group key reveal type. + require.FailNow(t, "unexpected group key reveal type") +} + func assertEqualProof(t *testing.T, expected, actual *Proof) { t.Helper() @@ -136,6 +249,11 @@ func assertEqualProof(t *testing.T, expected, actual *Proof) { } require.Equal(t, expected.ChallengeWitness, actual.ChallengeWitness) + + assertEqualGroupKeyReveal( + t, expected.GroupKeyReveal, actual.GroupKeyReveal, + expected.Asset.Genesis.ID(), + ) } func TestProofEncoding(t *testing.T) { From 0318fade6ec4387ce0e347346df7a21b20a62b69 Mon Sep 17 00:00:00 2001 From: ffranr Date: Thu, 19 Dec 2024 02:38:16 +0000 Subject: [PATCH 12/12] proof: add a test for encoding/decoding of v1 group key reveal Add a unit test to verify that encoding and decoding work correctly when the proof group key reveal is of version 1 type. --- proof/proof_test.go | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/proof/proof_test.go b/proof/proof_test.go index 3bc9298f4..b91acb7ea 100644 --- a/proof/proof_test.go +++ b/proof/proof_test.go @@ -256,6 +256,42 @@ func assertEqualProof(t *testing.T, expected, actual *Proof) { ) } +// TestProofEncodingGroupKeyRevealV1 tests encoding and decoding a proof with a +// group key reveal V1. +func TestProofEncodingGroupKeyRevealV1(t *testing.T) { + t.Parallel() + + testBlocks := readTestData(t) + oddTxBlock := testBlocks[0] + + genesis := asset.RandGenesis(t, asset.Normal) + scriptKey := test.RandPubKey(t) + proof := RandProof(t, genesis, scriptKey, oddTxBlock, 0, 1) + + // Override the group key reveal with a V1 reveal. + internalKey := test.RandPubKey(t) + customRoot := chainhash.Hash(test.RandBytes(32)) + groupKeyReveal, err := asset.NewGroupKeyRevealV1( + *internalKey, genesis.ID(), fn.Some(customRoot), + ) + require.NoError(t, err) + + proof.GroupKeyReveal = &groupKeyReveal + + file, err := NewFile(V0, proof, proof) + require.NoError(t, err) + proof.AdditionalInputs = []File{*file, *file} + + var proofBuf bytes.Buffer + require.NoError(t, proof.Encode(&proofBuf)) + proofBytes := proofBuf.Bytes() + + var decodedProof Proof + require.NoError(t, decodedProof.Decode(bytes.NewReader(proofBytes))) + + assertEqualProof(t, &proof, &decodedProof) +} + func TestProofEncoding(t *testing.T) { t.Parallel()