From 84df0b286d0daf98231c96c7993ab8e7e5303b16 Mon Sep 17 00:00:00 2001
From: ffranr <robyn@lightning.engineering>
Date: Thu, 21 Nov 2024 15:03:33 +0000
Subject: [PATCH 1/3] tapgarden: refactor method isBatchSealed from sealBatch

Extract method `isBatchSealed` from the `sealBatch` method to improve
clarity and reusability.
---
 tapgarden/planter.go | 78 ++++++++++++++++++++++++++++++--------------
 1 file changed, 54 insertions(+), 24 deletions(-)

diff --git a/tapgarden/planter.go b/tapgarden/planter.go
index 6d785b754..5d08c0744 100644
--- a/tapgarden/planter.go
+++ b/tapgarden/planter.go
@@ -20,6 +20,7 @@ import (
 	"github.com/lightninglabs/taproot-assets/tapscript"
 	"github.com/lightninglabs/taproot-assets/tapsend"
 	"github.com/lightninglabs/taproot-assets/universe"
+	lfn "github.com/lightningnetwork/lnd/fn"
 	"github.com/lightningnetwork/lnd/lnwallet/chainfee"
 	"golang.org/x/exp/maps"
 )
@@ -1527,30 +1528,17 @@ func (c *ChainPlanter) fundBatch(ctx context.Context, params FundParams,
 	return nil
 }
 
-// sealBatch will verify that each grouped asset in the pending batch has an
-// asset group witness, and will attempt to create asset group witnesses when
-// possible if they are not provided. After all asset group witnesses have been
-// validated, they are saved to disk to be used by the caretaker during batch
-// finalization.
-func (c *ChainPlanter) sealBatch(ctx context.Context, params SealParams,
-	workingBatch *MintingBatch) (*MintingBatch, error) {
-
-	// A batch should exist with 1+ seedlings and be funded before being
-	// sealed.
-	if !workingBatch.HasSeedlings() {
-		return nil, fmt.Errorf("no seedlings in batch")
-	}
-
-	if !workingBatch.IsFunded() {
-		return nil, fmt.Errorf("batch is not funded")
-	}
+// isBatchSealed returns true if the minting batch has been sealed; otherwise,
+// it returns false.
+func (c *ChainPlanter) isBatchSealed(ctx context.Context,
+	workingBatch *MintingBatch) lfn.Result[bool] {
 
 	// Filter the batch seedlings to only consider those that will become
 	// grouped assets. If there are no such seedlings, then there is nothing
 	// to seal and no action is needed.
 	groupSeedlings, _ := filterSeedlingsWithGroup(workingBatch.Seedlings)
 	if len(groupSeedlings) == 0 {
-		return workingBatch, nil
+		return lfn.Ok[bool](true)
 	}
 
 	// Before we can build the group key requests for each seedling, we must
@@ -1575,17 +1563,59 @@ func (c *ChainPlanter) sealBatch(ctx context.Context, params SealParams,
 	existingGroups, err := c.cfg.Log.FetchSeedlingGroups(
 		ctx, genesisPoint, anchorOutputIndex, singleSeedling,
 	)
-
-	switch {
-	case len(existingGroups) != 0:
-		return nil, ErrBatchAlreadySealed
-	case err != nil:
+	if err != nil {
 		// The only expected error is for a missing asset genesis.
 		if !errors.Is(err, ErrNoGenesis) {
-			return nil, err
+			return lfn.Err[bool](err)
 		}
 	}
 
+	if len(existingGroups) != 0 {
+		// Asset genesis already stored on disk therefore the batch is
+		// already sealed.
+		return lfn.Ok[bool](true)
+	}
+
+	// If we reach this point then the batch hasn't been sealed.
+	return lfn.Ok[bool](false)
+}
+
+// sealBatch will verify that each grouped asset in the pending batch has an
+// asset group witness, and will attempt to create asset group witnesses when
+// possible if they are not provided. After all asset group witnesses have been
+// validated, they are saved to disk to be used by the caretaker during batch
+// finalization.
+func (c *ChainPlanter) sealBatch(ctx context.Context, params SealParams,
+	workingBatch *MintingBatch) (*MintingBatch, error) {
+
+	// Check if the batch meets the requirements for sealing.
+	if !workingBatch.HasSeedlings() {
+		return nil, fmt.Errorf("no seedlings in batch")
+	}
+
+	if !workingBatch.IsFunded() {
+		return nil, fmt.Errorf("batch is not funded")
+	}
+
+	// Return early if the batch is already sealed.
+	isSealed, err := c.isBatchSealed(ctx, workingBatch).Unpack()
+	if err != nil {
+		return nil, fmt.Errorf("failed to inspect batch seal status: "+
+			"%w", err)
+	}
+
+	if isSealed {
+		return nil, ErrBatchAlreadySealed
+	}
+
+	genesisPoint := extractGenesisOutpoint(
+		workingBatch.GenesisPacket.Pkt.UnsignedTx,
+	)
+	anchorOutputIndex := extractAnchorOutputIndex(
+		workingBatch.GenesisPacket,
+	)
+	groupSeedlings, _ := filterSeedlingsWithGroup(workingBatch.Seedlings)
+
 	// Construct the group key requests and group virtual TXs for each
 	// seedling. With these we can verify provided asset group witnesses,
 	// or attempt to derive asset group witnesses if needed.

From eae7bc4b88c87d38401b0557b8c9c8445f484902 Mon Sep 17 00:00:00 2001
From: ffranr <robyn@lightning.engineering>
Date: Thu, 21 Nov 2024 15:33:59 +0000
Subject: [PATCH 2/3] tapgarden: move sanity check into buildGroupReqs

Simplifies the code by ensuring the consistency check for group requests
and virtual transactions is handled within the buildGroupReqs function.
This removes redundant checks from multiple call sites.
---
 tapgarden/planter.go | 16 +++++++---------
 1 file changed, 7 insertions(+), 9 deletions(-)

diff --git a/tapgarden/planter.go b/tapgarden/planter.go
index 5d08c0744..eedd238f9 100644
--- a/tapgarden/planter.go
+++ b/tapgarden/planter.go
@@ -766,6 +766,13 @@ func buildGroupReqs(genesisPoint wire.OutPoint, assetOutputIndex uint32,
 		}
 	}
 
+	// Verify consistency: ensure the number of group key requests matches
+	// the number of group VM transactions.
+	if len(groupReqs) != len(genTXs) {
+		return nil, nil, fmt.Errorf("mismatched number of group " +
+			"requests and virtual TXs")
+	}
+
 	return groupReqs, genTXs, nil
 }
 
@@ -1053,11 +1060,6 @@ func listBatches(ctx context.Context, batchStore MintingStore,
 				"requests: %w", err)
 		}
 
-		if len(groupReqs) != len(genTXs) {
-			return nil, fmt.Errorf("mismatched number of group " +
-				"requests and virtual TXs")
-		}
-
 		// Copy existing seedlngs into the unsealed seedling map; we'll
 		// clear the batch seedlings after adding group information.
 		currentBatch.UnsealedSeedlings = make(
@@ -1627,10 +1629,6 @@ func (c *ChainPlanter) sealBatch(ctx context.Context, params SealParams,
 		return nil, fmt.Errorf("unable to build group requests: "+
 			"%w", err)
 	}
-	if len(groupReqs) != len(genTXs) {
-		return nil, fmt.Errorf("mismatched number of group requests " +
-			"and virtual TXs")
-	}
 
 	// Each provided group witness must have a corresponding seedling in the
 	// current batch.

From 7994c808ca76a344ee59934e5d8ae23bc5678f0b Mon Sep 17 00:00:00 2001
From: ffranr <robyn@lightning.engineering>
Date: Thu, 21 Nov 2024 16:46:40 +0000
Subject: [PATCH 3/3] WIP: tapgarden: add method querySealBatchPsbts to return
 an external seal package for a given batch

This package can be used for external asset group witness PSBT signing.
---
 tapgarden/planter.go | 68 ++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 68 insertions(+)

diff --git a/tapgarden/planter.go b/tapgarden/planter.go
index eedd238f9..ff27c6ef9 100644
--- a/tapgarden/planter.go
+++ b/tapgarden/planter.go
@@ -1582,6 +1582,74 @@ func (c *ChainPlanter) isBatchSealed(ctx context.Context,
 	return lfn.Ok[bool](false)
 }
 
+type SeedlingExternalSealPkg struct {
+	Psbt psbt.Packet
+
+	AssetGroupPubKey btcec.PublicKey
+}
+
+func (c *ChainPlanter) querySealBatchPsbts(ctx context.Context,
+	workingBatch *MintingBatch) ([]SeedlingExternalSealPkg, error) {
+
+	// Check if the batch meets the requirements for sealing.
+	if !workingBatch.HasSeedlings() {
+		return nil, fmt.Errorf("no seedlings in batch")
+	}
+
+	if !workingBatch.IsFunded() {
+		return nil, fmt.Errorf("batch is not funded")
+	}
+
+	// Return early if the batch is already sealed.
+	isSealed, err := c.isBatchSealed(ctx, workingBatch).Unpack()
+	if err != nil {
+		return nil, fmt.Errorf("failed to inspect batch seal status: "+
+			"%w", err)
+	}
+
+	if isSealed {
+		return nil, ErrBatchAlreadySealed
+	}
+
+	genesisPoint := extractGenesisOutpoint(
+		workingBatch.GenesisPacket.Pkt.UnsignedTx,
+	)
+	anchorOutputIndex := extractAnchorOutputIndex(
+		workingBatch.GenesisPacket,
+	)
+	groupSeedlings, _ := filterSeedlingsWithGroup(workingBatch.Seedlings)
+
+	// Construct the group key requests and group virtual TXs for each
+	// seedling. With these we can verify provided asset group witnesses,
+	// or attempt to derive asset group witnesses if needed.
+	_, genTXs, err := buildGroupReqs(
+		genesisPoint, anchorOutputIndex, c.cfg.GenTxBuilder,
+		groupSeedlings,
+	)
+	if err != nil {
+		return nil, fmt.Errorf("unable to build group requests: "+
+			"%w", err)
+	}
+
+	var res []SeedlingExternalSealPkg
+	for idx := range genTXs {
+		genTX := genTXs[idx]
+
+		psbtPkg, err := psbt.NewFromUnsignedTx(&genTX.Tx)
+		if err != nil {
+			return nil, fmt.Errorf("unable to create psbt from "+
+				"unsigned group witness VM tx: %w", err)
+		}
+
+		res = append(res, SeedlingExternalSealPkg{
+			Psbt:             *psbtPkg,
+			AssetGroupPubKey: genTX.TweakedKey,
+		})
+	}
+
+	return res, nil
+}
+
 // sealBatch will verify that each grouped asset in the pending batch has an
 // asset group witness, and will attempt to create asset group witnesses when
 // possible if they are not provided. After all asset group witnesses have been