Skip to content

[wallet 1/3]: refactor in preparation for group key channel funding #1402

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

Merged
merged 16 commits into from
Feb 28, 2025
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
32 changes: 32 additions & 0 deletions fn/memory.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
package fn

import "errors"

var (
// ErrNilPointerDeference is returned when a nil pointer is
// dereferenced.
ErrNilPointerDeference = errors.New("nil pointer dereference")
)

// Ptr returns the pointer of the given value. This is useful in instances
// where a function returns the value, but a pointer is wanted. Without this,
// then an intermediate variable is needed.
Expand Down Expand Up @@ -32,7 +40,31 @@ func ToArray[T ByteArray](v []byte) T {
// CopySlice returns a copy of the given slice. Does a shallow copy of the
// slice itself, not the underlying elements.
func CopySlice[T any](slice []T) []T {
if slice == nil {
return nil
}

newSlice := make([]T, len(slice))
copy(newSlice, slice)
return newSlice
}

// Deref safely dereferences a pointer. If the pointer is nil, it returns the
// zero value of type T and an error.
func Deref[T any](ptr *T) (T, error) {
if ptr == nil {
var zero T
return zero, ErrNilPointerDeference
}

return *ptr, nil
}

// DerefPanic dereferences a pointer. If the pointer is nil, it panics.
func DerefPanic[T any](ptr *T) T {
if ptr == nil {
panic(ErrNilPointerDeference)
}

return *ptr
}
18 changes: 0 additions & 18 deletions key_ring.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"context"
"fmt"

"github.com/davecgh/go-spew/spew"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/taproot-assets/asset"
"github.com/lightninglabs/taproot-assets/tapgarden"
Expand Down Expand Up @@ -60,23 +59,6 @@ func (l *LndRpcKeyRing) DeriveNextTaprootAssetKey(
return *keyDesc, nil
}

// DeriveKey attempts to derive an arbitrary key specified by the passed
// KeyLocator. This may be used in several recovery scenarios, or when manually
// rotating something like our current default node key.
func (l *LndRpcKeyRing) DeriveKey(ctx context.Context,
keyLoc keychain.KeyLocator) (keychain.KeyDescriptor, error) {

tapdLog.Debugf("Deriving new key, key_loc=%v", spew.Sdump(keyLoc))

keyDesc, err := l.lnd.WalletKit.DeriveKey(ctx, &keyLoc)
if err != nil {
return keychain.KeyDescriptor{}, fmt.Errorf("unable to "+
"derive key ring: %w", err)
}

return *keyDesc, nil
}

// IsLocalKey returns true if the key is under the control of the wallet
// and can be derived by it.
func (l *LndRpcKeyRing) IsLocalKey(ctx context.Context,
Expand Down
11 changes: 8 additions & 3 deletions proof/archive.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,8 @@ type AnnotatedProof struct {
*AssetSnapshot
}

// Archiver is the main storage backend the ProofArchiver uses to store and
// query for proof files.
type Archiver interface {
// Exporter is used to fetch proofs by their unique identifier.
type Exporter interface {
// FetchProof fetches a proof for an asset uniquely identified by the
// passed ProofIdentifier.
//
Expand All @@ -144,6 +143,12 @@ type Archiver interface {
// locator then ErrMultipleProofs should be returned to indicate more
// specific fields need to be set in the Locator (e.g. the OutPoint).
FetchProof(ctx context.Context, id Locator) (Blob, error)
}

// Archiver is the main storage backend the ProofArchiver uses to store and
// query for proof files.
type Archiver interface {
Exporter

// FetchIssuanceProof fetches the issuance proof for an asset, given the
// anchor point of the issuance (NOT the genesis point for the asset).
Expand Down
130 changes: 98 additions & 32 deletions rpcserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -2235,8 +2235,7 @@ func (r *rpcServer) FundVirtualPsbt(ctx context.Context,
// Extract the passive assets that are needed for the fully RPC driven
// flow.
passivePackets, err := r.cfg.AssetWallet.CreatePassiveAssets(
ctx, []*tappsbt.VPacket{fundedVPkt.VPacket},
fundedVPkt.InputCommitments,
ctx, fundedVPkt.VPackets, fundedVPkt.InputCommitments,
)
if err != nil {
return nil, fmt.Errorf("error creating passive assets: %w", err)
Expand All @@ -2257,7 +2256,12 @@ func (r *rpcServer) FundVirtualPsbt(ctx context.Context,
}
}

response.FundedPsbt, err = serialize(fundedVPkt.VPacket)
// TODO(guggero): Remove this once we support multiple packets.
if len(fundedVPkt.VPackets) > 1 {
return nil, fmt.Errorf("only one packet supported")
}

response.FundedPsbt, err = serialize(fundedVPkt.VPackets[0])
if err != nil {
return nil, fmt.Errorf("error serializing packet: %w", err)
}
Expand Down Expand Up @@ -3387,11 +3391,13 @@ func (r *rpcServer) BurnAsset(ctx context.Context,
// We found the asset group, so we can use the group key to
// burn the asset.
groupKey = &assetGroup.GroupPubKey

case errors.Is(err, address.ErrAssetGroupUnknown):
// We don't know the asset group, so we'll try to burn the
// asset using the asset ID only.
rpcsLog.Debug("Asset group key not found, asset may not be " +
"part of a group")

case err != nil:
return nil, fmt.Errorf("error querying asset group: %w", err)
}
Expand Down Expand Up @@ -3419,16 +3425,22 @@ func (r *rpcServer) BurnAsset(ctx context.Context,
return nil, fmt.Errorf("error funding burn: %w", err)
}

// We don't support burning by group key yet, so we only expect a single
// vPacket (which implies a single asset ID is involved).
if len(fundResp.VPackets) > 1 {
return nil, fmt.Errorf("only one packet supported")
}

// Now we can sign the packet and send it to the chain.
_, err = r.cfg.AssetWallet.SignVirtualPacket(fundResp.VPacket)
vPkt := fundResp.VPackets[0]
_, err = r.cfg.AssetWallet.SignVirtualPacket(vPkt)
if err != nil {
return nil, fmt.Errorf("error signing packet: %w", err)
}

resp, err := r.cfg.ChainPorter.RequestShipment(
tapfreighter.NewPreSignedParcel(
[]*tappsbt.VPacket{fundResp.VPacket},
fundResp.InputCommitments, in.Note,
fundResp.VPackets, fundResp.InputCommitments, in.Note,
),
)
if err != nil {
Expand All @@ -3443,7 +3455,7 @@ func (r *rpcServer) BurnAsset(ctx context.Context,

var burnProof *taprpc.DecodedProof
for idx := range resp.Outputs {
vOut := fundResp.VPacket.Outputs[idx]
vOut := vPkt.Outputs[idx]
tOut := resp.Outputs[idx]
if vOut.Asset.IsBurn() {
p, err := proof.Decode(tOut.ProofSuffix)
Expand Down Expand Up @@ -6427,54 +6439,71 @@ func MarshalAssetFedSyncCfg(
}

// unmarshalAssetSpecifier unmarshals an asset specifier from the RPC form.
func unmarshalAssetSpecifier(req *rfqrpc.AssetSpecifier) (*asset.ID,
func unmarshalAssetSpecifier(s *rfqrpc.AssetSpecifier) (*asset.ID,
*btcec.PublicKey, error) {

if s == nil {
return nil, nil, fmt.Errorf("asset specifier must be specified")
}

return parseAssetSpecifier(
s.GetAssetId(), s.GetAssetIdStr(), s.GetGroupKey(),
s.GetGroupKeyStr(),
)
}

// parseAssetSpecifier parses an asset specifier from the RPC form.
func parseAssetSpecifier(reqAssetID []byte, reqAssetIDStr string,
reqGroupKey []byte, reqGroupKeyStr string) (*asset.ID, *btcec.PublicKey,
error) {

// Attempt to decode the asset specifier from the RPC request. In cases
// where both the asset ID and asset group key are provided, we will
// give precedence to the asset ID due to its higher level of
// specificity.
var (
assetID *asset.ID

groupKeyBytes []byte
groupKey *btcec.PublicKey

err error
assetID *asset.ID
groupKey *btcec.PublicKey
err error
)

switch {
// Parse the asset ID if it's set.
case len(req.GetAssetId()) > 0:
case len(reqAssetID) > 0:
if len(reqAssetID) != sha256.Size {
return nil, nil, fmt.Errorf("asset ID must be 32 bytes")
}

var assetIdBytes [32]byte
copy(assetIdBytes[:], req.GetAssetId())
copy(assetIdBytes[:], reqAssetID)
id := asset.ID(assetIdBytes)
assetID = &id

case len(req.GetAssetIdStr()) > 0:
assetIDBytes, err := hex.DecodeString(req.GetAssetIdStr())
case len(reqAssetIDStr) > 0:
assetIDBytes, err := hex.DecodeString(reqAssetIDStr)
if err != nil {
return nil, nil, fmt.Errorf("error decoding asset "+
"ID: %w", err)
}

if len(assetIDBytes) != sha256.Size {
return nil, nil, fmt.Errorf("asset ID must be 32 bytes")
}

var id asset.ID
copy(id[:], assetIDBytes)
assetID = &id

// Parse the group key if it's set.
case len(req.GetGroupKey()) > 0:
groupKeyBytes = req.GetGroupKey()
groupKey, err = btcec.ParsePubKey(groupKeyBytes)
case len(reqGroupKey) > 0:
groupKey, err = btcec.ParsePubKey(reqGroupKey)
if err != nil {
return nil, nil, fmt.Errorf("error parsing group "+
"key: %w", err)
}

case len(req.GetGroupKeyStr()) > 0:
groupKeyBytes, err := hex.DecodeString(
req.GetGroupKeyStr(),
)
case len(reqGroupKeyStr) > 0:
groupKeyBytes, err := hex.DecodeString(reqGroupKeyStr)
if err != nil {
return nil, nil, fmt.Errorf("error decoding group "+
"key: %w", err)
Expand Down Expand Up @@ -7055,8 +7084,21 @@ func (r *rpcServer) FundChannel(ctx context.Context,
return nil, fmt.Errorf("error parsing peer pubkey: %w", err)
}

if len(req.AssetId) != sha256.Size {
return nil, fmt.Errorf("asset ID must be 32 bytes")
assetID, groupKey, err := parseAssetSpecifier(
req.GetAssetId(), "", nil, "",
)
if err != nil {
return nil, fmt.Errorf("error parsing asset specifier: %w", err)
}

// For channel funding, we need to make sure that the group key is set
// if the asset is grouped.
assetSpecifier, err := r.specifierWithGroupKeyLookup(
ctx, assetID, groupKey,
)
if err != nil {
return nil, fmt.Errorf("error creating asset specifier: %w",
err)
}

if req.AssetAmount == 0 {
Expand All @@ -7067,12 +7109,12 @@ func (r *rpcServer) FundChannel(ctx context.Context,
}

fundReq := tapchannel.FundReq{
PeerPub: *peerPub,
AssetAmount: req.AssetAmount,
FeeRate: chainfee.SatPerVByte(req.FeeRateSatPerVbyte),
PushAmount: btcutil.Amount(req.PushSat),
PeerPub: *peerPub,
AssetSpecifier: assetSpecifier,
AssetAmount: req.AssetAmount,
FeeRate: chainfee.SatPerVByte(req.FeeRateSatPerVbyte),
PushAmount: btcutil.Amount(req.PushSat),
}
copy(fundReq.AssetID[:], req.AssetId)

chanPoint, err := r.cfg.AuxFundingController.FundChannel(ctx, fundReq)
if err != nil {
Expand All @@ -7085,6 +7127,30 @@ func (r *rpcServer) FundChannel(ctx context.Context,
}, nil
}

// specifierWithGroupKeyLookup returns an asset specifier that has the group key
// set if it's a grouped asset.
func (r *rpcServer) specifierWithGroupKeyLookup(ctx context.Context,
assetID *asset.ID, groupKey *btcec.PublicKey) (asset.Specifier, error) {

var result asset.Specifier

if assetID != nil && groupKey == nil {
dbGroupKey, err := r.cfg.TapAddrBook.QueryAssetGroup(
ctx, *assetID,
)
switch {
case err == nil && dbGroupKey.GroupKey != nil:
groupKey = &dbGroupKey.GroupPubKey

case err != nil:
return result, fmt.Errorf("unable to query asset "+
"group: %w", err)
}
}

return asset.NewSpecifier(assetID, groupKey, nil, true)
}

// EncodeCustomRecords allows RPC users to encode Taproot Asset channel related
// data into the TLV format that is used in the custom records of the lnd
// payment or other channel related RPCs. This RPC is completely stateless and
Expand Down
32 changes: 16 additions & 16 deletions tapcfg/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -556,22 +556,22 @@ func genServerConfig(cfg *Config, cfgLogger btclog.Logger,
ProofUpdates: proofArchive,
ErrChan: mainErrChan,
}),
AssetCustodian: tapgarden.NewCustodian(
&tapgarden.CustodianConfig{
ChainParams: &tapChainParams,
WalletAnchor: walletAnchor,
ChainBridge: chainBridge,
GroupVerifier: tapgarden.GenGroupVerifier(
context.Background(), assetMintingStore,
),
AddrBook: addrBook,
ProofArchive: proofArchive,
ProofNotifier: multiNotifier,
ErrChan: mainErrChan,
ProofCourierDispatcher: proofCourierDispatcher,
ProofRetrievalDelay: cfg.CustodianProofRetrievalDelay, ProofWatcher: reOrgWatcher,
},
),
// nolint: lll
AssetCustodian: tapgarden.NewCustodian(&tapgarden.CustodianConfig{
ChainParams: &tapChainParams,
WalletAnchor: walletAnchor,
ChainBridge: chainBridge,
GroupVerifier: tapgarden.GenGroupVerifier(
context.Background(), assetMintingStore,
),
AddrBook: addrBook,
ProofArchive: proofArchive,
ProofNotifier: multiNotifier,
ErrChan: mainErrChan,
ProofCourierDispatcher: proofCourierDispatcher,
ProofRetrievalDelay: cfg.CustodianProofRetrievalDelay,
ProofWatcher: reOrgWatcher,
}),
ChainBridge: chainBridge,
AddrBook: addrBook,
AddrBookDisableSyncer: cfg.AddrBook.DisableSyncer,
Expand Down
Loading
Loading