Skip to content

Commit

Permalink
asset+tapgarden: add ExternalKey field to Seedling and GroupKeyRequest
Browse files Browse the repository at this point in the history
Introduce a new `ExternalKey` type and add external key fields of this
type to `Seedling` and `GroupKeyRequest`.
  • Loading branch information
ffranr committed Jan 9, 2025
1 parent 76a5b0e commit 74decd6
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 6 deletions.
97 changes: 96 additions & 1 deletion asset/asset.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ import (
"github.com/btcsuite/btcd/blockchain"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/btcsuite/btcd/btcutil/psbt"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/taproot-assets/fn"
Expand Down Expand Up @@ -847,6 +849,87 @@ type AssetGroup struct {
*GroupKey
}

// ExternalKey represents an external key used for deriving and managing
// hierarchical deterministic (HD) wallet addresses according to BIP-86.
type ExternalKey struct {
// XPub is the extended public key derived at depth 3 of the BIP-86
// hierarchy (e.g., m/86'/0'/0'). This key serves as the parent key for
// deriving child public keys and addresses.
XPub hdkeychain.ExtendedKey

// MasterFingerprint is the fingerprint of the master key, derived from
// the first 4 bytes of the hash160 of the master public key. It is used
// to identify the master key in BIP-86 derivation schemes.
MasterFingerprint uint32

// DerivationPath specifies the extended BIP-86 derivation path used to
// derive a child key from the XPub. Starting from the base path of the
// XPub (e.g., m/86'/0'/0'), this path must contain exactly 5 components
// in total (e.g., m/86'/0'/0'/0/0), with the additional components
// defining specific child keys, such as individual addresses.
DerivationPath []uint32
}

// Validate ensures that the ExternalKey's fields conform to the expected
// requirements for a BIP-86 key structure.
func (e *ExternalKey) Validate() error {
if e.XPub.IsPrivate() {
return fmt.Errorf("xpub must be public key only")
}

if e.XPub.Depth() != 3 {
return fmt.Errorf("xpub must be derived at depth 3")
}

if len(e.DerivationPath) != 5 {
return fmt.Errorf("derivation path must have exactly 5 " +
"componenets")
}

bip86Purpose := waddrmgr.KeyScopeBIP0086.Purpose +
hdkeychain.HardenedKeyStart
if e.DerivationPath[0] != bip86Purpose {
return fmt.Errorf("xpub must be derived from BIP-0086 " +
"(Taproot) derivation path")
}

return nil
}

// PubKey derives and returns the public key corresponding to the final index in
// the ExternalKey's BIP-86 derivation path.
//
// The method assumes the ExternalKey's XPub was derived at depth 3
// (e.g., m/86'/0'/0') and uses the fourth and fifth components of the
// DerivationPath to derive a child key, typically representing an address or
// output key.
func (e *ExternalKey) PubKey() (btcec.PublicKey, error) {
err := e.Validate()
if err != nil {
return btcec.PublicKey{}, err
}

internalExternalFlag := e.DerivationPath[3]
index := e.DerivationPath[4]

changeKey, err := e.XPub.Derive(internalExternalFlag)
if err != nil {
return btcec.PublicKey{}, err
}

indexKey, err := changeKey.Derive(index)
if err != nil {
return btcec.PublicKey{}, err
}

pubKey, err := indexKey.ECPubKey()
if err != nil {
return btcec.PublicKey{}, err
}

return *pubKey, nil
}

// GroupKey is the tweaked public key that is used to associate assets together
// across distinct asset IDs, allowing further issuance of the asset to be made
// possible.
Expand Down Expand Up @@ -883,6 +966,10 @@ type GroupKeyRequest struct {
// has been applied.
RawKey keychain.KeyDescriptor

// ExternalKey specifies a public key that, when provided, is used to
// externally sign the group virtual transaction outside of tapd.
ExternalKey fn.Option[ExternalKey]

// 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.
Expand Down Expand Up @@ -1857,11 +1944,13 @@ func NewScriptKeyBip86(rawKey keychain.KeyDescriptor) ScriptKey {
}

// NewGroupKeyRequest constructs and validates a group key request.
func NewGroupKeyRequest(internalKey keychain.KeyDescriptor, anchorGen Genesis,
func NewGroupKeyRequest(internalKey keychain.KeyDescriptor,
externalKey fn.Option[ExternalKey], anchorGen Genesis,
newAsset *Asset, scriptRoot []byte) (*GroupKeyRequest, error) {

req := &GroupKeyRequest{
RawKey: internalKey,
ExternalKey: externalKey,
AnchorGen: anchorGen,
NewAsset: newAsset,
TapscriptRoot: scriptRoot,
Expand Down Expand Up @@ -1990,6 +2079,12 @@ func DeriveGroupKey(genSigner GenesisSigner, genTx GroupVirtualTx,
req GroupKeyRequest, tapLeaf *psbt.TaprootTapLeafScript) (*GroupKey,
error) {

// Cannot derive the group key witness for an external key.
if req.ExternalKey.IsSome() {
return nil, fmt.Errorf("cannot derive group key witness for " +
"group key with external key")
}

// Populate the signing descriptor needed to sign the virtual minting
// transaction.
signDesc := &lndclient.SignDescriptor{
Expand Down
5 changes: 4 additions & 1 deletion asset/mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/taproot-assets/fn"
"github.com/lightninglabs/taproot-assets/internal/test"
"github.com/lightninglabs/taproot-assets/mssmt"
"github.com/lightningnetwork/lnd/input"
Expand Down Expand Up @@ -617,7 +618,9 @@ func NewAssetNoErr(t testing.TB, gen Genesis, amt, locktime, relocktime uint64,
func NewGroupKeyRequestNoErr(t testing.TB, internalKey keychain.KeyDescriptor,
gen Genesis, newAsset *Asset, scriptRoot []byte) *GroupKeyRequest {

req, err := NewGroupKeyRequest(internalKey, gen, newAsset, scriptRoot)
req, err := NewGroupKeyRequest(
internalKey, fn.None[ExternalKey](), gen, newAsset, scriptRoot,
)
require.NoError(t, err)

return req
Expand Down
8 changes: 5 additions & 3 deletions tapgarden/planter.go
Original file line number Diff line number Diff line change
Expand Up @@ -716,8 +716,9 @@ func buildGroupReqs(genesisPoint wire.OutPoint, assetOutputIndex uint32,
// virtual transaction.
if groupInfo != nil {
groupReq, err := asset.NewGroupKeyRequest(
groupInfo.GroupKey.RawKey, *groupInfo.Genesis,
protoAsset, groupInfo.GroupKey.TapscriptRoot,
groupInfo.GroupKey.RawKey, seedling.ExternalKey,
*groupInfo.Genesis, protoAsset,
groupInfo.GroupKey.TapscriptRoot,
)
if err != nil {
return nil, nil, fmt.Errorf("unable to "+
Expand Down Expand Up @@ -750,7 +751,8 @@ func buildGroupReqs(genesisPoint wire.OutPoint, assetOutputIndex uint32,
}

groupReq, err := asset.NewGroupKeyRequest(
*seedling.GroupInternalKey, assetGen,
*seedling.GroupInternalKey,
seedling.ExternalKey, assetGen,
protoAsset, seedling.GroupTapscriptRoot,
)
if err != nil {
Expand Down
9 changes: 8 additions & 1 deletion tapgarden/seedling.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/taproot-assets/asset"
"github.com/lightninglabs/taproot-assets/fn"
"github.com/lightninglabs/taproot-assets/proof"
"github.com/lightningnetwork/lnd/keychain"
)
Expand Down Expand Up @@ -112,8 +113,14 @@ type Seedling struct {
// all script spend conditions for the group key. Instead of spending an
// asset, these scripts are used to define witnesses more complex than
// a Schnorr signature for reissuing assets. A group key with an empty
// Tapscript root can only authorize reissuance with a signature.
// Tapscript root can only authorize re-issuance with a signature.
GroupTapscriptRoot []byte

// ExternalKey is an optional field that allows specifying an external
// signing key for the group virtual transaction during minting. This
// key enables signing operations to be performed externally, outside
// the daemon.
ExternalKey fn.Option[asset.ExternalKey]
}

// validateFields attempts to validate the set of input fields for the passed
Expand Down

0 comments on commit 74decd6

Please sign in to comment.