Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Increase group witness test coverage #549

Merged
merged 8 commits into from
Jan 23, 2024
Merged
12 changes: 7 additions & 5 deletions address/mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,21 @@ func RandAddr(t testing.TB, params *ChainParams,
}

var (
assetVersion asset.Version
groupInfo *asset.GroupKey
groupPubKey *btcec.PublicKey
groupWitness wire.TxWitness
tapscriptSibling *commitment.TapscriptPreimage
)

if test.RandInt[uint32]()%2 == 0 {
assetVersion = asset.V1
}

if test.RandInt[uint32]()%2 == 0 {
protoAsset := asset.NewAssetNoErr(
t, genesis, amount, 0, 0, scriptKey, nil,
asset.WithAssetVersion(assetVersion),
)
groupInfo = asset.RandGroupKey(t, genesis, protoAsset)
groupPubKey = &groupInfo.GroupPubKey
Expand All @@ -68,11 +75,6 @@ func RandAddr(t testing.TB, params *ChainParams,
)
}

var assetVersion asset.Version
if test.RandInt[uint32]()%2 == 0 {
assetVersion = asset.V1
}

tapAddr, err := New(
V0, genesis, groupPubKey, groupWitness, *scriptKey.PubKey,
*internalKey.PubKey(), amount, tapscriptSibling, params,
Expand Down
214 changes: 194 additions & 20 deletions asset/asset.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/btcsuite/btcd/blockchain"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcutil/psbt"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/lndclient"
Expand Down Expand Up @@ -491,6 +492,28 @@ type GroupKey struct {
Witness wire.TxWitness
}

// GroupKeyRequest contains the essential fields used to derive a group key.
type GroupKeyRequest struct {
// RawKey is the raw group key before the tweak with the genesis point
// has been applied.
RawKey keychain.KeyDescriptor

// AnchorGen is the genesis of the group anchor, which is the asset used
// to derive the single tweak for the group key. For a new group key,
// this will be the genesis of the new asset.
AnchorGen Genesis

// TapscriptRoot is the root of a Tapscript tree that includes script
// spend conditions for the group key. A group key with an empty
// Tapscript root can only authorize reissuance with a signature.
TapscriptRoot []byte

// NewAsset is the asset which we are requesting group membership for.
// A successful request will produce a witness that authorizes this
// to be a member of this asset group.
NewAsset *Asset
}

// GroupKeyReveal is a 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 Expand Up @@ -798,39 +821,79 @@ func NewScriptKeyBip86(rawKey keychain.KeyDescriptor) ScriptKey {
}
}

// DeriveGroupKey derives an asset's group key based on an internal public
// key descriptor, the original group asset genesis, and the asset's genesis.
func DeriveGroupKey(genSigner GenesisSigner, genBuilder GenesisTxBuilder,
rawKey keychain.KeyDescriptor, initialGen Genesis,
newAsset *Asset) (*GroupKey, error) {
// NewGroupKeyRequest constructs and validates a group key request.
func NewGroupKeyRequest(internalKey keychain.KeyDescriptor, anchorGen Genesis,
newAsset *Asset, scriptRoot []byte) (*GroupKeyRequest, error) {

// First, perform the final checks on the asset being authorized for
// group membership.
if newAsset == nil {
return nil, fmt.Errorf("grouped asset cannot be nil")
req := &GroupKeyRequest{
RawKey: internalKey,
AnchorGen: anchorGen,
NewAsset: newAsset,
TapscriptRoot: scriptRoot,
}

err := req.Validate()
if err != nil {
return nil, err
}

return req, nil
}

// ValidateGroupKeyRequest ensures that the asset intended to be a member of an
// asset group is well-formed.
func (req *GroupKeyRequest) Validate() error {
// Perform the final checks on the asset being authorized for group
// membership.
if req.NewAsset == nil {
return fmt.Errorf("grouped asset cannot be nil")
}

// The asset in the request must have the default genesis asset witness,
// and no group key. Those fields can only be populated after group
// witness creation.
if !req.NewAsset.HasGenesisWitness() {
jharveyb marked this conversation as resolved.
Show resolved Hide resolved
return fmt.Errorf("asset is not a genesis asset")
}

if req.NewAsset.GroupKey != nil {
return fmt.Errorf("asset already has group key")
}

if !newAsset.HasGenesisWitness() {
return nil, fmt.Errorf("asset is not a genesis asset")
if req.AnchorGen.Type != req.NewAsset.Type {
return fmt.Errorf("asset group type mismatch")
}

if newAsset.GroupKey != nil {
return nil, fmt.Errorf("asset already has group key")
if req.RawKey.PubKey == nil {
return fmt.Errorf("missing group internal key")
}

if initialGen.Type != newAsset.Type {
return nil, fmt.Errorf("asset group type mismatch")
return nil
}

// DeriveGroupKey derives an asset's group key based on an internal public
// key descriptor, the original group asset genesis, and the asset's genesis.
func DeriveGroupKey(genSigner GenesisSigner, genBuilder GenesisTxBuilder,
req GroupKeyRequest) (*GroupKey, error) {

// First, perform the final checks on the asset being authorized for
// group membership.
err := req.Validate()
if err != nil {
return nil, err
}

// Compute the tweaked group key and set it in the asset before
// creating the virtual minting transaction.
genesisTweak := initialGen.ID()
tweakedGroupKey, err := GroupPubKey(rawKey.PubKey, genesisTweak[:], nil)
genesisTweak := req.AnchorGen.ID()
tweakedGroupKey, err := GroupPubKey(
req.RawKey.PubKey, genesisTweak[:], nil,
)
if err != nil {
return nil, fmt.Errorf("cannot tweak group key: %w", err)
}

assetWithGroup := newAsset.Copy()
assetWithGroup := req.NewAsset.Copy()
assetWithGroup.GroupKey = &GroupKey{
GroupPubKey: *tweakedGroupKey,
}
Expand All @@ -846,7 +909,7 @@ func DeriveGroupKey(genSigner GenesisSigner, genBuilder GenesisTxBuilder,
// minting transaction. This is restricted to group keys with an empty
// tapscript root and key path spends.
signDesc := &lndclient.SignDescriptor{
KeyDesc: rawKey,
KeyDesc: req.RawKey,
SingleTweak: genesisTweak[:],
SignMethod: input.TaprootKeySpendBIP0086SignMethod,
Output: prevOut,
Expand All @@ -859,12 +922,123 @@ func DeriveGroupKey(genSigner GenesisSigner, genBuilder GenesisTxBuilder,
}

return &GroupKey{
RawKey: rawKey,
RawKey: req.RawKey,
GroupPubKey: *tweakedGroupKey,
Witness: wire.TxWitness{sig.Serialize()},
}, nil
}

// DeriveCustomGroupKey derives an asset's group key based on a signing
// descriptor, the original group asset genesis, and the asset's genesis.
func DeriveCustomGroupKey(genSigner GenesisSigner, genBuilder GenesisTxBuilder,
req GroupKeyRequest, tapLeaf *psbt.TaprootTapLeafScript,
scriptWitness []byte) (*GroupKey, error) {

// First, perform the final checks on the asset being authorized for
// group membership.
err := req.Validate()
if err != nil {
return nil, err
}

// Compute the tweaked group key and set it in the asset before
// creating the virtual minting transaction.
genesisTweak := req.AnchorGen.ID()
tweakedGroupKey, err := GroupPubKey(
req.RawKey.PubKey, genesisTweak[:], req.TapscriptRoot,
)
if err != nil {
return nil, fmt.Errorf("cannot tweak group key: %w", err)
}

assetWithGroup := req.NewAsset.Copy()
assetWithGroup.GroupKey = &GroupKey{
GroupPubKey: *tweakedGroupKey,
}

// Exit early if a group witness is already given, since we don't need
// to construct a virtual TX nor produce a signature.
if scriptWitness != nil {
if tapLeaf == nil {
return nil, fmt.Errorf("need tap leaf with group " +
"script witness")
}

witness := wire.TxWitness{
scriptWitness, tapLeaf.Script, tapLeaf.ControlBlock,
}

return &GroupKey{
RawKey: req.RawKey,
GroupPubKey: *tweakedGroupKey,
TapscriptRoot: req.TapscriptRoot,
Witness: witness,
}, nil
}

// Build the virtual transaction that represents the minting of the new
// asset, which will be signed to generate the group witness.
genesisTx, prevOut, err := genBuilder.BuildGenesisTx(assetWithGroup)
if err != nil {
return nil, fmt.Errorf("cannot build virtual tx: %w", err)
}

// Populate the signing descriptor needed to sign the virtual minting
// transaction.
signDesc := &lndclient.SignDescriptor{
KeyDesc: req.RawKey,
SingleTweak: genesisTweak[:],
TapTweak: req.TapscriptRoot,
Output: prevOut,
HashType: txscript.SigHashDefault,
InputIndex: 0,
}

// There are three possible signing cases: BIP-0086 key spend path, key
// spend path with a script root, and script spend path.
switch {
// If there is no tapscript root, we're doing a BIP-0086 key spend.
case len(signDesc.TapTweak) == 0:
signDesc.SignMethod = input.TaprootKeySpendBIP0086SignMethod

// No leaf means we're not signing a specific script, so this is the key
// spend path with a tapscript root.
case len(signDesc.TapTweak) != 0 && tapLeaf == nil:
signDesc.SignMethod = input.TaprootKeySpendSignMethod

// One leaf hash and a merkle root means we're signing a specific
// script.
case len(signDesc.TapTweak) != 0 && tapLeaf != nil:
signDesc.SignMethod = input.TaprootScriptSpendSignMethod
signDesc.WitnessScript = tapLeaf.Script

default:
return nil, fmt.Errorf("bad sign descriptor for group key")
}

sig, err := genSigner.SignVirtualTx(signDesc, genesisTx, 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 {
witness = append(witness, signDesc.WitnessScript)
witness = append(witness, tapLeaf.ControlBlock)
}

return &GroupKey{
RawKey: signDesc.KeyDesc,
GroupPubKey: *tweakedGroupKey,
TapscriptRoot: signDesc.TapTweak,
Witness: witness,
}, nil
}

// Asset represents a Taproot asset.
type Asset struct {
// Version is the Taproot Asset version of the asset.
Expand Down
66 changes: 44 additions & 22 deletions asset/asset_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -632,8 +632,25 @@ func TestAssetGroupKey(t *testing.T) {
// TweakTaprootPrivKey modifies the private key that is passed in! We
// need to provide a copy to arrive at the same result.
protoAsset := NewAssetNoErr(t, g, 1, 0, 0, fakeScriptKey, nil)
keyGroup, err := DeriveGroupKey(
genSigner, &genBuilder, fakeKeyDesc, g, protoAsset,
groupReq := NewGroupKeyRequestNoErr(t, fakeKeyDesc, g, protoAsset, nil)
keyGroup, err := DeriveGroupKey(genSigner, &genBuilder, *groupReq)
require.NoError(t, err)

require.Equal(
t, schnorr.SerializePubKey(tweakedKey.PubKey()),
schnorr.SerializePubKey(&keyGroup.GroupPubKey),
)

// We should also be able to reproduce the correct tweak with a non-nil
// tapscript root.
tapTweak := test.RandBytes(32)
tweakedKey = txscript.TweakTaprootPrivKey(*internalKey, tapTweak)

groupReq = NewGroupKeyRequestNoErr(
t, test.PubToKeyDesc(privKey.PubKey()), g, protoAsset, tapTweak,
)
keyGroup, err = DeriveCustomGroupKey(
genSigner, &genBuilder, *groupReq, nil, nil,
)
require.NoError(t, err)

Expand Down Expand Up @@ -683,35 +700,40 @@ func TestDeriveGroupKey(t *testing.T) {
groupedProtoAsset.GroupKey = &GroupKey{
GroupPubKey: *groupPub,
}
groupReq := GroupKeyRequest{
RawKey: groupKeyDesc,
AnchorGen: baseGen,
}

// A prototype asset is required for building the genesis virtual TX.
_, err := DeriveGroupKey(
genSigner, &genBuilder, groupKeyDesc, baseGen, nil,
)
require.Error(t, err)
_, err := DeriveGroupKey(genSigner, &genBuilder, groupReq)
require.ErrorContains(t, err, "grouped asset cannot be nil")

// The prototype asset must have a genesis witness.
_, err = DeriveGroupKey(
genSigner, &genBuilder, groupKeyDesc, baseGen, nonGenProtoAsset,
)
require.Error(t, err)
groupReq.NewAsset = nonGenProtoAsset
_, err = DeriveGroupKey(genSigner, &genBuilder, groupReq)
require.ErrorContains(t, err, "asset is not a genesis asset")

// The prototype asset must not have a group key set.
_, err = DeriveGroupKey(
genSigner, &genBuilder, groupKeyDesc, baseGen, groupedProtoAsset,
)
require.Error(t, err)
groupReq.NewAsset = groupedProtoAsset
_, err = DeriveGroupKey(genSigner, &genBuilder, groupReq)
require.ErrorContains(t, err, "asset already has group key")

// The anchor genesis used for signing must have the same asset type
// as the prototype asset being signed.
_, err = DeriveGroupKey(
genSigner, &genBuilder, groupKeyDesc, collectGen, protoAsset,
)
require.Error(t, err)

groupKey, err := DeriveGroupKey(
genSigner, &genBuilder, groupKeyDesc, baseGen, protoAsset,
)
groupReq.AnchorGen = collectGen
groupReq.NewAsset = protoAsset
_, err = DeriveGroupKey(genSigner, &genBuilder, groupReq)
require.ErrorContains(t, err, "asset group type mismatch")

// The group key request must include an internal key.
groupReq.AnchorGen = baseGen
groupReq.RawKey.PubKey = nil
_, err = DeriveGroupKey(genSigner, &genBuilder, groupReq)
require.ErrorContains(t, err, "missing group internal key")

groupReq.RawKey = groupKeyDesc
groupKey, err := DeriveGroupKey(genSigner, &genBuilder, groupReq)
require.NoError(t, err)
require.NotNil(t, groupKey)
}
Expand Down
Loading