Repository: lightninglabs/taproot-assets

Issue:
[feature]: Minimal Code Changes for Future Universe Commitment Transaction Support
We need to make minimal changes now to enable seamless support for fully-featured universe commitment transactions in the future. The following steps outline the required adjustments:

1. **Taproot Locking for Minting Change Output**  
   Modify the minting transaction so that the change output is locked in a way that it can only be spent via `tapd`. This will involve re-using the existing Pedersen commitment work to commit to the genesis asset ID in the tapscript of the output. The output will use a Taproot format, with the internal key temporarily set to an LND hot wallet key. This setup ensures we can later sign the universe commitment transaction spending this output using an external cold signing key.

2. **User-Enabled Universe Commitment Support**  
   Add a user-facing option to enable universe commitment during minting. If enabled:
   - Warn the user that the change output will remain unspendable until full universe commitment support is implemented.
   - Restrict the minting batch to include only a single asset group for events with universe commitment enabled.

3. **Metadata Update for Minting Proofs**
   Update the asset minting proof metadata to indicate whether universe commitment is enabled for the minting event.

This change should will allow retroactive application of the feature to past minting events which have uni commitment support enabled.

Hints from issue comments:
@Roasbeef: > This will involve re-using the existing Pedersen commitment work to commit to the genesis asset ID in the tapscript of the output.

Why do we need to commit to the genesis ID here? IIRC, we never finalized the design of _what_ would actually go into this new change output. 

> The output will use a Taproot format, with the internal key temporarily set to an LND hot wallet key. This setup ensures we can later sign the universe commitment transaction spending this output using an external cold signing key.


These two statements appear to contradict each other. 

In an earlier design, the group key was intended to be used for the internal key of this commitment/change output. At that point, we were still tweaking the group key directly with the asset ID, so we would've had to deal with the double tweak issues. With GKR v1, the internal key is "bare", so I think we should be able to just re-use it for the internal key of this Bitcoin commitment anchor output. 

One issue with the above idea is that then if the group key is actually a cold key, user interaction may be needed to update the on chain commitment. But perhaps that'll just be a part of the normal minting process?

> Warn the user that the change output will remain unspendable until full universe commitment support is implemented.


Seems like something that requires a UI to be truly effective? 

> Restrict the minting batch to include only a single asset group for events with universe commitment enabled.


So you can't do a re-issue event along with other batch mints? Do we see that as a fundamental limitation, or is this an initial  simplification? Seems feasible, along with other re-issuance events that also have a canonical universe comment, with the tradeoff that we'd need multiple of these change outputs? @Roasbeef: One other thing that comes to mind: do we need a rule that restricts the _location_ of this change address, for the initial mint, and subsequent spends (to refresh the on chain universe commitment)? 

Validation can be simplified it clients always know to look at the nth/first output. Here's an example:
  * Minting happens, the commitment output is index 0. 
  * That gets spent to update the on-chain commitment with the initial unified commitment tree.
     * There's a change address, as a wallet input was needed to pay for fees. 
  * Validators know to watch just the output at index zero for subsequent spends. @Roasbeef: I think we have a good idea of how we'll aim to update the commitment for issuance, but what about burns and ignored assets? 

## Burns 

### The Two Types of Burns

Thinking about it a bit further, there're two classes of burns: first party burns and third party burns. A first party burn is a burn by the issuer (reducing available supply). A third party burn is a provable burn by a user that happens to hold the asset. 

#### First Party Burn Support
For first party burns, the issuer can spend the existing universe commitment output (the latest unspent one), and then update the commitment. With only knowledge of first party burns, users won't arrive at the most accurate supply figure when they "run the numbers". As a result, it is desirable to also support third party burns for the unified commitment. 

#### Third Party Burn Support

One idea to support third party burns is:
  * The sub-system that handle managing the on-chain commitment also watches the issuance tree for said asset. 
  * Periodically, the sub-system does a diff of the leaves it knows about, and the leaves that are reported from the Universe API. 
  * It'll then take this set of new keys/leave, fetch the burn proof, then start to update its local burn tree (that includes 1st party burns). 
  * When the next ticker fires, it _spends_ the on-chain commitment output, to account for this new information. 

## Ignored Assets 

Ignored assets differ from burns and mints, in that they don't have an actual on-chain footprint. Minimally, one can identify an asset on-chain based on a tuple similar to the prevID: `(outpoint, asset_id, group_key, script_key, asset_amt)`. For simplicity, lets assume we're committing this value (alternatively, we can commit the existence proof for the asset in question). 

What we need from here is a way to have the group key sign this ignore tuple. One idea is that we re-use the existing virtual tx re-issuance structure. Instead of the virtual output being the newly created asset, we craft something that commits to the asset to be ignored. 

Alternatively, we could keep it simple, and just have it be a normal schnorr signature over the tuple. One benefit of re-using the existing group key spending capabilities is that any of the scripts can be satisfied, so it can be a multi-sig script or w/e. @ffranr: > > This will involve re-using the existing Pedersen commitment work to commit to the genesis asset ID in the tapscript of the output.
> 
> Why do we need to commit to the genesis ID here? IIRC, we never finalized the design of _what_ would actually go into this new change output.

By committing to the genesis ID in the output TapScript, we could potentially enable universe commitments for batches involving multiple asset groups in the future. This approach would allow us to include additional outputs, with each output corresponding to a specific asset group.

If an output is tagged with the genesis asset ID in this way, and the commitment output internal key is known to validators, they would be able to identify which output to observe for commitment updates or minting transactions. This would hold true even if, in the future, the batch includes multiple asset groups with universe commitment enabled.

> > The output will use a Taproot format, with the internal key temporarily set to an LND hot wallet key. This setup ensures we can later sign the universe commitment transaction spending this output using an external cold signing key.
> 
> These two statements appear to contradict each other.
> 
> In an earlier design, the group key was intended to be used for the internal key of this commitment/change output. At that point, we were still tweaking the group key directly with the asset ID, so we would've had to deal with the double tweak issues. With GKR v1, the internal key is "bare", so I think we should be able to just re-use it for the internal key of this Bitcoin commitment anchor output.
> 
> One issue with the above idea is that then if the group key is actually a cold key, user interaction may be needed to update the on chain commitment. But perhaps that'll just be a part of the normal minting process?

I discussed this with Oli, and our idea was to use the LND wallet key as the internal key for now, avoiding the need for an additional round of external key signing. This approach would still leave the option open for supporting external keys in the future.

> > Warn the user that the change output will remain unspendable until full universe commitment support is implemented.
> 
> Seems like something that requires a UI to be truly effective?

Yes, I agree. However, that situation should only be temporary until we can create universe commitment transactions. Ideally, the universe commitment transaction would be broadcast as soon as possible and include change outputs that the user can spend normally.

> > Restrict the minting batch to include only a single asset group for events with universe commitment enabled.
> 
> So you can't do a re-issue event along with other batch mints? Do we see that as a fundamental limitation, or is this an initial simplification? Seems feasible, along with other re-issuance events that also have a canonical universe comment, with the tradeoff that we'd need multiple of these change outputs?

I see this as an initial simplification. In my opinion, we should prioritize supporting asset group minting with universe commitments for a single asset group. This approach is likely to address the majority of the current reissuance minter user demand.@Roasbeef: > By committing to the genesis ID in the output TapScript, we could potentially enable universe commitments for batches involving multiple asset groups in the future

But what about the case where we spend several of these change outputs, thereby batching updates to create a new Universe commitment? 

I don't think it's defined in any of the other issues _yet_, so can we lay out clearly here:
  * The structure of taproot output key of the change output (internal key + scripts)
  * The structure of the on-chain universe commitment which spends the change output to serialize updates (internal key + scripts)

> If an output is tagged with the genesis asset ID in this way, and the commitment output internal key is known to validators, they would be able to identify which output to observe for commitment updates or minting transactions.

Hmm, I don't think this quite matches my mental model. The output that the validators are watching is the _commitment_ output itself, not this pre-commitment change output. After the first mint, a minting event is what triggers spending that old commitment output, to the new one, which includes all the latest data. 

Pertaining to the validators knowing which pre-commitment output to watch: won't that just be included in the issuance proof for the newly created assets? My mental model of the commitment output transactions it that we'd use an ordering rule so validators know which output to watch for a spend (the first one, the second is there is just a change output). 


> I discussed this with Oli, and our idea was to use the LND wallet key as the internal key for now, avoiding the need for an additional round of external key signing. This approach would still leave the option open for supporting external keys in the future.

Perhaps instead we should build in the concept of a delegated key for commitment updates? This can be specified in the very first issuance proof that creates a new asset group. Using a normal `lnd` wallet key there is at odds with one of the original requirements that the only ppl that can update the commitment are the ones that can issue new assets. @ffranr: PR https://github.com/lightninglabs/taproot-assets/pull/1325 was merged which completes this issue. Other issues exist to account for the remaining universe commitment work, for example:

https://github.com/lightninglabs/taproot-assets/issues/1451
https://github.com/lightninglabs/taproot-assets/issues/1463

Solution patch:
diff --git a/cmd/commands/assets.go b/cmd/commands/assets.go
index caf1eb26..fd486f37 100644
--- a/cmd/commands/assets.go
+++ b/cmd/commands/assets.go
@@ -59,6 +59,7 @@ var (
 	groupByGroupName             = "by_group"
 	assetIDName                  = "asset_id"
 	shortResponseName            = "short"
+	universeCommitmentsName      = "universe_commitments"
 	feeRateName                  = "sat_per_vbyte"
 	assetAmountName              = "amount"
 	burnOverrideConfirmationName = "override_confirmation_destroy_assets"
@@ -154,6 +155,15 @@ var mintAssetCommand = cli.Command{
 			Usage: "the master fingerprint of the key the xpub " +
 				"was derived from",
 		},
+		cli.BoolFlag{
+			Name: universeCommitmentsName,
+			Usage: "if set, the asset group will be minted with " +
+				"universe commitments enabled " +
+				"(minter-controlled, on-chain attestations " +
+				"that anchor and verify the state of an " +
+				"asset group); this option restricts the " +
+				"minting batch to a single asset group",
+		},
 	},
 	Action: mintAsset,
 	Subcommands: []cli.Command{
@@ -382,7 +392,8 @@ func mintAsset(ctx *cli.Context) error {
 			AssetVersion: taprpc.AssetVersion(
 				ctx.Uint64(assetVersionName),
 			),
-			ExternalGroupKey: externalKey,
+			ExternalGroupKey:    externalKey,
+			UniverseCommitments: ctx.Bool(universeCommitmentsName),
 		},
 		ShortResponse: ctx.Bool(shortResponseName),
 	})
diff --git a/proof/meta.go b/proof/meta.go
index 63ed6113..10aba50d 100644
--- a/proof/meta.go
+++ b/proof/meta.go
@@ -174,12 +174,6 @@ type MetaReveal struct {
 	UnknownOddTypes tlv.TypeMap
 }
 
-// SizableInteger is a subset of Integer that excludes int8, since we never use
-// it in practice.
-type SizableInteger interface {
-	constraints.Unsigned | ~int | ~int16 | ~int32 | ~int64
-}
-
 // Validate validates the meta reveal.
 func (m *MetaReveal) Validate() error {
 	// A meta reveal is allowed to be nil.
@@ -234,6 +228,13 @@ func (m *MetaReveal) Validate() error {
 		return err
 	}
 
+	// The asset metadata is invalid when the universe commitments feature
+	// is enabled but no delegation key is specified.
+	if m.UniverseCommitments && m.DelegationKey.IsNone() {
+		return fmt.Errorf("universe commitments enabled in asset " +
+			"metadata but delegation key is unspecified")
+	}
+
 	return fn.MapOptionZ(m.DelegationKey, func(key btcec.PublicKey) error {
 		if key == emptyKey {
 			return ErrDelegationKeyEmpty
@@ -247,6 +248,12 @@ func (m *MetaReveal) Validate() error {
 	})
 }
 
+// SizableInteger is a subset of Integer that excludes int8, since we never use
+// it in practice.
+type SizableInteger interface {
+	constraints.Unsigned | ~int | ~int16 | ~int32 | ~int64
+}
+
 // IsValidMetaType checks if the passed value is a valid meta type.
 func IsValidMetaType[T SizableInteger](num T) (MetaType, error) {
 	switch {
@@ -288,6 +295,8 @@ func IsValidDecDisplay(decDisplay uint32) error {
 
 // DecodeMetaJSON decodes bytes as a JSON object, after checking that the bytes
 // could be valid metadata.
+//
+// TODO(ffranr): Add unit test for `jBytes := []byte{}`.
 func DecodeMetaJSON(jBytes []byte) (map[string]interface{}, error) {
 	jMeta := make(map[string]interface{})
 
diff --git a/rpcserver.go b/rpcserver.go
index 0b7ff955..35e896c9 100644
--- a/rpcserver.go
+++ b/rpcserver.go
@@ -508,7 +508,10 @@ func (r *rpcServer) MintAsset(ctx context.Context,
 			"collectibles")
 	}
 
-	var seedlingMeta *proof.MetaReveal
+	// TODO(ffranr): Move seedling MetaReveal construction into
+	//  ChainPlanter. This will allow us to simplify delegation key
+	//  management.
+	var seedlingMeta proof.MetaReveal
 	switch {
 	// If we have an explicit asset meta field, we parse the content.
 	case req.Asset.AssetMeta != nil:
@@ -520,7 +523,7 @@ func (r *rpcServer) MintAsset(ctx context.Context,
 
 		// If the asset meta field was specified, then the data inside
 		// must be valid. Let's check that now.
-		seedlingMeta = &proof.MetaReveal{
+		seedlingMeta = proof.MetaReveal{
 			Data: req.Asset.AssetMeta.Data,
 			Type: metaType,
 		}
@@ -542,15 +545,10 @@ func (r *rpcServer) MintAsset(ctx context.Context,
 			return nil, err
 		}
 
-		err = seedlingMeta.Validate()
-		if err != nil {
-			return nil, err
-		}
-
 	// If no asset meta field was specified, we create a default meta
 	// reveal with the decimal display set.
 	default:
-		seedlingMeta = &proof.MetaReveal{
+		seedlingMeta = proof.MetaReveal{
 			Type: proof.MetaOpaque,
 		}
 
@@ -560,11 +558,6 @@ func (r *rpcServer) MintAsset(ctx context.Context,
 		if err != nil {
 			return nil, err
 		}
-
-		err = seedlingMeta.Validate()
-		if err != nil {
-			return nil, err
-		}
 	}
 
 	// Parse the optional script key and group internal key. The group
@@ -602,12 +595,13 @@ func (r *rpcServer) MintAsset(ctx context.Context,
 	}
 
 	seedling := &tapgarden.Seedling{
-		AssetVersion:   assetVersion,
-		AssetType:      asset.Type(req.Asset.AssetType),
-		AssetName:      req.Asset.Name,
-		Amount:         req.Asset.Amount,
-		EnableEmission: req.Asset.NewGroupedAsset,
-		Meta:           seedlingMeta,
+		AssetVersion:        assetVersion,
+		AssetType:           asset.Type(req.Asset.AssetType),
+		AssetName:           req.Asset.Name,
+		Amount:              req.Asset.Amount,
+		EnableEmission:      req.Asset.NewGroupedAsset,
+		Meta:                &seedlingMeta,
+		UniverseCommitments: req.Asset.UniverseCommitments,
 	}
 
 	rpcsLog.Infof("[MintAsset]: version=%v, type=%v, name=%v, amt=%v, "+
diff --git a/tapcfg/server.go b/tapcfg/server.go
index c149dd55..90ab208a 100644
--- a/tapcfg/server.go
+++ b/tapcfg/server.go
@@ -534,10 +534,8 @@ func genServerConfig(cfg *Config, cfgLogger btclog.Logger,
 		RuntimeID:             runtimeID,
 		EnableChannelFeatures: enableChannelFeatures,
 		Lnd:                   lndServices,
-		ChainParams: address.ParamsForChain(
-			cfg.ActiveNetParams.Name,
-		),
-		ReOrgWatcher: reOrgWatcher,
+		ChainParams:           tapChainParams,
+		ReOrgWatcher:          reOrgWatcher,
 		AssetMinter: tapgarden.NewChainPlanter(tapgarden.PlanterConfig{
 			GardenKit: tapgarden.GardenKit{
 				Wallet:                walletAnchor,
@@ -553,6 +551,7 @@ func genServerConfig(cfg *Config, cfgLogger btclog.Logger,
 				ProofWatcher:          reOrgWatcher,
 				UniversePushBatchSize: defaultUniverseSyncBatchSize,
 			},
+			ChainParams:  tapChainParams,
 			ProofUpdates: proofArchive,
 			ErrChan:      mainErrChan,
 		}),
diff --git a/tapdb/asset_minting.go b/tapdb/asset_minting.go
index a0d54c24..075ebc0c 100644
--- a/tapdb/asset_minting.go
+++ b/tapdb/asset_minting.go
@@ -127,6 +127,10 @@ type (
 	// NewAssetMeta wraps the params needed to insert a new asset meta on
 	// disk.
 	NewAssetMeta = sqlc.UpsertAssetMetaParams
+
+	// MintAnchorUniCommitParams wraps the params needed to insert a new
+	// mint anchor uni commitment on disk.
+	MintAnchorUniCommitParams = sqlc.UpsertMintAnchorUniCommitmentParams
 )
 
 // PendingAssetStore is a sub-set of the main sqlc.Querier interface that
@@ -199,7 +203,8 @@ type PendingAssetStore interface {
 
 	// BindMintingBatchWithTx adds the minting transaction to an existing
 	// batch.
-	BindMintingBatchWithTx(ctx context.Context, arg BatchChainUpdate) error
+	BindMintingBatchWithTx(ctx context.Context,
+		arg BatchChainUpdate) (int64, error)
 
 	// UpdateBatchGenesisTx updates the batch tx attached to an existing
 	// batch.
@@ -238,6 +243,16 @@ type PendingAssetStore interface {
 	// FetchAssetMetaForAsset fetches the asset meta for a given asset.
 	FetchAssetMetaForAsset(ctx context.Context,
 		assetID []byte) (sqlc.FetchAssetMetaForAssetRow, error)
+
+	// FetchMintAnchorUniCommitment fetches the mint anchor uni commitment
+	// for a given batch.
+	FetchMintAnchorUniCommitment(ctx context.Context,
+		batchID int32) (sqlc.MintAnchorUniCommitment, error)
+
+	// UpsertMintAnchorUniCommitment inserts a new or updates an existing
+	// mint anchor uni commitment on disk.
+	UpsertMintAnchorUniCommitment(ctx context.Context,
+		arg MintAnchorUniCommitParams) (int64, error)
 }
 
 var (
@@ -311,6 +326,77 @@ type OptionalSeedlingFields struct {
 	GroupAnchorID      sql.NullInt64
 }
 
+// insertMintAnchorTx inserts a mint anchor transaction into the database.
+func insertMintAnchorTx(ctx context.Context, q PendingAssetStore,
+	anchorPackage tapgarden.FundedMintAnchorPsbt,
+	batchKey btcec.PublicKey, genesisOutpoint wire.OutPoint) error {
+
+	// Ensure that the genesis point is in the database.
+	genesisPointDbID, err := upsertGenesisPoint(
+		ctx, q, genesisOutpoint,
+	)
+	if err != nil {
+		return fmt.Errorf("%w: %w", ErrUpsertGenesisPoint, err)
+	}
+
+	var psbtBuf bytes.Buffer
+	err = anchorPackage.Pkt.Serialize(&psbtBuf)
+	if err != nil {
+		return fmt.Errorf("%w: %w", ErrEncodePsbt, err)
+	}
+
+	rawBatchKey := batchKey.SerializeCompressed()
+	enableUniverseCommitments := anchorPackage.PreCommitmentOutput.IsSome()
+
+	batchID, err := q.BindMintingBatchWithTx(ctx, BatchChainUpdate{
+		RawKey:              rawBatchKey,
+		MintingTxPsbt:       psbtBuf.Bytes(),
+		ChangeOutputIndex:   sqlInt32(anchorPackage.ChangeOutputIndex),
+		AssetsOutputIndex:   sqlInt32(anchorPackage.AssetAnchorOutIdx),
+		GenesisID:           sqlInt64(genesisPointDbID),
+		UniverseCommitments: enableUniverseCommitments,
+	})
+	if err != nil {
+		return fmt.Errorf("%w: %w", ErrBindBatchTx, err)
+	}
+
+	// If universe commitments are not enabled for this batch, we can
+	// return early.
+	if !enableUniverseCommitments {
+		return nil
+	}
+
+	// At this point, universe commitments are enabled for this batch, so
+	// we'll insert the mint anchor uni commitment record.
+	preCommitOut, err := anchorPackage.PreCommitmentOutput.UnwrapOrErr(
+		fmt.Errorf("pre-commitment outpoint bundle not set"),
+	)
+	if err != nil {
+		return err
+	}
+
+	// Serialize internal key.
+	internalKey := preCommitOut.InternalKey.SerializeCompressed()
+
+	// Serialize group key.
+	groupPubKey := preCommitOut.GroupPubKey.SerializeCompressed()
+
+	_, err = q.UpsertMintAnchorUniCommitment(
+		ctx, MintAnchorUniCommitParams{
+			BatchID:            int32(batchID),
+			TxOutputIndex:      int32(preCommitOut.OutIdx),
+			TaprootInternalKey: internalKey,
+			GroupKey:           groupPubKey,
+		},
+	)
+	if err != nil {
+		return fmt.Errorf("unable to insert mint anchor uni "+
+			"commitment: %w", err)
+	}
+
+	return nil
+}
+
 // CommitMintingBatch commits a new minting batch to disk along with any
 // seedlings specified as part of the batch. A new internal key is also
 // created, with the batch referencing that internal key. This internal key
@@ -365,31 +451,16 @@ func (a *AssetMintingStore) CommitMintingBatch(ctx context.Context,
 		if newBatch.GenesisPacket != nil {
 			genesisPacket := newBatch.GenesisPacket
 			genesisTx := genesisPacket.Pkt.UnsignedTx
-			changeIdx := genesisPacket.ChangeOutputIndex
 			genesisOutpoint := genesisTx.TxIn[0].PreviousOutPoint
 
-			var psbtBuf bytes.Buffer
-			err := genesisPacket.Pkt.Serialize(&psbtBuf)
-			if err != nil {
-				return fmt.Errorf("%w: %w", ErrEncodePsbt, err)
-			}
-
-			genesisPointID, err := upsertGenesisPoint(
-				ctx, q, genesisOutpoint,
+			// Insert the batch transaction.
+			err = insertMintAnchorTx(
+				ctx, q, *genesisPacket,
+				*newBatch.BatchKey.PubKey, genesisOutpoint,
 			)
 			if err != nil {
-				return fmt.Errorf("%w: %w",
-					ErrUpsertGenesisPoint, err)
-			}
-
-			err = q.BindMintingBatchWithTx(ctx, BatchChainUpdate{
-				RawKey:            rawBatchKey,
-				MintingTxPsbt:     psbtBuf.Bytes(),
-				ChangeOutputIndex: sqlInt32(changeIdx),
-				GenesisID:         sqlInt64(genesisPointID),
-			})
-			if err != nil {
-				return fmt.Errorf("%w: %w", ErrBindBatchTx, err)
+				return fmt.Errorf("unable to insert mint "+
+					"anchor tx: %w", err)
 			}
 		}
 
@@ -1163,11 +1234,59 @@ func marshalMintingBatch(ctx context.Context, q PendingAssetStore,
 		if err != nil {
 			return nil, err
 		}
-		batch.GenesisPacket = &tapsend.FundedPsbt{
-			Pkt: genesisPkt,
-			ChangeOutputIndex: extractSqlInt32[int32](
-				dbBatch.ChangeOutputIndex,
-			),
+
+		if !dbBatch.AssetsOutputIndex.Valid {
+			return nil, fmt.Errorf("missing asset anchor output " +
+				"index")
+		}
+		assetAnchorOutIdx := dbBatch.AssetsOutputIndex.Int32
+
+		// If the batch has universe commitments, we will retrieve
+		// the pre-commitment output index from the database.
+		var preCommitOut fn.Option[tapgarden.PreCommitmentOutput]
+		if dbBatch.UniverseCommitments {
+			res, err := q.FetchMintAnchorUniCommitment(
+				ctx, int32(dbBatch.BatchID),
+			)
+			if err != nil {
+				return nil, fmt.Errorf("unable to fetch mint "+
+					"anchor uni commitment: %w", err)
+			}
+
+			// Parse the internal key from the database.
+			internalKey, err := btcec.ParsePubKey(
+				res.TaprootInternalKey,
+			)
+			if err != nil {
+				return nil, fmt.Errorf("error parsing "+
+					"taproot internal key: %w", err)
+			}
+
+			// Parse the group public key from the database.
+			groupPubKey, err := btcec.ParsePubKey(res.GroupKey)
+			if err != nil {
+				return nil, fmt.Errorf("error parsing "+
+					"group public key: %w", err)
+			}
+
+			preCommitOut = fn.Some(
+				tapgarden.PreCommitmentOutput{
+					OutIdx:      uint32(res.TxOutputIndex),
+					InternalKey: *internalKey,
+					GroupPubKey: *groupPubKey,
+				},
+			)
+		}
+
+		batch.GenesisPacket = &tapgarden.FundedMintAnchorPsbt{
+			FundedPsbt: tapsend.FundedPsbt{
+				Pkt: genesisPkt,
+				ChangeOutputIndex: extractSqlInt32[int32](
+					dbBatch.ChangeOutputIndex,
+				),
+			},
+			AssetAnchorOutIdx:   uint32(assetAnchorOutIdx),
+			PreCommitmentOutput: preCommitOut,
 		}
 	}
 
@@ -1282,33 +1401,23 @@ func encodeOutpoint(outPoint wire.OutPoint) ([]byte, error) {
 // CommitBatchTx updates the genesis transaction of a batch based on the batch
 // key.
 func (a *AssetMintingStore) CommitBatchTx(ctx context.Context,
-	batchKey *btcec.PublicKey, genesisPacket *tapsend.FundedPsbt) error {
+	batchKey *btcec.PublicKey,
+	genesisPacket tapgarden.FundedMintAnchorPsbt) error {
 
 	genesisOutpoint := genesisPacket.Pkt.UnsignedTx.TxIn[0].PreviousOutPoint
-	rawBatchKey := batchKey.SerializeCompressed()
-
-	var psbtBuf bytes.Buffer
-	if err := genesisPacket.Pkt.Serialize(&psbtBuf); err != nil {
-		return fmt.Errorf("%w: %w", ErrEncodePsbt, err)
-	}
 
 	var writeTxOpts AssetStoreTxOptions
 	return a.db.ExecTx(ctx, &writeTxOpts, func(q PendingAssetStore) error {
-		genesisPointID, err := upsertGenesisPoint(
-			ctx, q, genesisOutpoint,
+		// Insert the batch transaction.
+		err := insertMintAnchorTx(
+			ctx, q, genesisPacket, *batchKey, genesisOutpoint,
 		)
 		if err != nil {
-			return fmt.Errorf("%w: %w", ErrUpsertGenesisPoint, err)
+			return fmt.Errorf("unable to insert mint anchor "+
+				"tx: %w", err)
 		}
 
-		return q.BindMintingBatchWithTx(ctx, BatchChainUpdate{
-			RawKey:        rawBatchKey,
-			MintingTxPsbt: psbtBuf.Bytes(),
-			ChangeOutputIndex: sqlInt32(
-				genesisPacket.ChangeOutputIndex,
-			),
-			GenesisID: sqlInt64(genesisPointID),
-		})
+		return nil
 	})
 }
 
@@ -1415,7 +1524,8 @@ func fetchSeedlingGroups(ctx context.Context, q PendingAssetStore,
 // binds the genesis transaction (which will create the set of assets in the
 // batch) to the batch itself.
 func (a *AssetMintingStore) AddSproutsToBatch(ctx context.Context,
-	batchKey *btcec.PublicKey, genesisPacket *tapsend.FundedPsbt,
+	batchKey *btcec.PublicKey,
+	genesisPacket *tapgarden.FundedMintAnchorPsbt,
 	assetRoot *commitment.TapCommitment) error {
 
 	// Before we open the DB transaction below, we'll fetch the set of
@@ -1444,7 +1554,8 @@ func (a *AssetMintingStore) AddSproutsToBatch(ctx context.Context,
 
 	var writeTxOpts AssetStoreTxOptions
 	return a.db.ExecTx(ctx, &writeTxOpts, func(q PendingAssetStore) error {
-		genesisPointID, _, err := upsertAssetsWithGenesis(
+		// Upsert the assets with genesis.
+		_, _, err := upsertAssetsWithGenesis(
 			ctx, q, genesisOutpoint, sortedAssets, nil,
 		)
 		if err != nil {
@@ -1452,23 +1563,13 @@ func (a *AssetMintingStore) AddSproutsToBatch(ctx context.Context,
 				"genesis: %w", err)
 		}
 
-		// With all the assets inserted, we'll now update the
-		// corresponding batch that references all these assets with
-		// the genesis packet, and genesis point information.
-		var psbtBuf bytes.Buffer
-		if err := genesisPacket.Pkt.Serialize(&psbtBuf); err != nil {
-			return fmt.Errorf("%w: %w", ErrEncodePsbt, err)
-		}
-		err = q.BindMintingBatchWithTx(ctx, BatchChainUpdate{
-			RawKey:        rawBatchKey,
-			MintingTxPsbt: psbtBuf.Bytes(),
-			ChangeOutputIndex: sqlInt32(
-				genesisPacket.ChangeOutputIndex,
-			),
-			GenesisID: sqlInt64(genesisPointID),
-		})
+		// Insert the batch transaction.
+		err = insertMintAnchorTx(
+			ctx, q, *genesisPacket, *batchKey, genesisOutpoint,
+		)
 		if err != nil {
-			return fmt.Errorf("%w: %w", ErrBindBatchTx, err)
+			return fmt.Errorf("unable to insert mint anchor "+
+				"tx: %w", err)
 		}
 
 		// Finally, update the batch state to BatchStateCommitted.
diff --git a/tapdb/migrations.go b/tapdb/migrations.go
index abc2b340..fbe40715 100644
--- a/tapdb/migrations.go
+++ b/tapdb/migrations.go
@@ -22,7 +22,7 @@ const (
 	// daemon.
 	//
 	// NOTE: This MUST be updated when a new migration is added.
-	LatestMigrationVersion = 29
+	LatestMigrationVersion = 30
 )
 
 // MigrationTarget is a functional option that can be passed to applyMigrations
diff --git a/tapdb/sqlc/assets.sql.go b/tapdb/sqlc/assets.sql.go
index 769c1ad7..5d46b321 100644
--- a/tapdb/sqlc/assets.sql.go
+++ b/tapdb/sqlc/assets.sql.go
@@ -87,25 +87,27 @@ func (q *Queries) AllInternalKeys(ctx context.Context) ([]InternalKey, error) {
 }
 
 const AllMintingBatches = `-- name: AllMintingBatches :many
-SELECT batch_id, batch_state, minting_tx_psbt, change_output_index, genesis_id, height_hint, creation_time_unix, tapscript_sibling, key_id, raw_key, key_family, key_index 
+SELECT batch_id, batch_state, minting_tx_psbt, change_output_index, genesis_id, height_hint, creation_time_unix, tapscript_sibling, assets_output_index, universe_commitments, key_id, raw_key, key_family, key_index 
 FROM asset_minting_batches
 JOIN internal_keys 
 ON asset_minting_batches.batch_id = internal_keys.key_id
 `
 
 type AllMintingBatchesRow struct {
-	BatchID           int64
-	BatchState        int16
-	MintingTxPsbt     []byte
-	ChangeOutputIndex sql.NullInt32
-	GenesisID         sql.NullInt64
-	HeightHint        int32
-	CreationTimeUnix  time.Time
-	TapscriptSibling  []byte
-	KeyID             int64
-	RawKey            []byte
-	KeyFamily         int32
-	KeyIndex          int32
+	BatchID             int64
+	BatchState          int16
+	MintingTxPsbt       []byte
+	ChangeOutputIndex   sql.NullInt32
+	GenesisID           sql.NullInt64
+	HeightHint          int32
+	CreationTimeUnix    time.Time
+	TapscriptSibling    []byte
+	AssetsOutputIndex   sql.NullInt32
+	UniverseCommitments bool
+	KeyID               int64
+	RawKey              []byte
+	KeyFamily           int32
+	KeyIndex            int32
 }
 
 func (q *Queries) AllMintingBatches(ctx context.Context) ([]AllMintingBatchesRow, error) {
@@ -126,6 +128,8 @@ func (q *Queries) AllMintingBatches(ctx context.Context) ([]AllMintingBatchesRow
 			&i.HeightHint,
 			&i.CreationTimeUnix,
 			&i.TapscriptSibling,
+			&i.AssetsOutputIndex,
+			&i.UniverseCommitments,
 			&i.KeyID,
 			&i.RawKey,
 			&i.KeyFamily,
@@ -353,7 +357,7 @@ func (q *Queries) BindMintingBatchWithTapSibling(ctx context.Context, arg BindMi
 	return err
 }
 
-const BindMintingBatchWithTx = `-- name: BindMintingBatchWithTx :exec
+const BindMintingBatchWithTx = `-- name: BindMintingBatchWithTx :one
 WITH target_batch AS (
     SELECT batch_id
     FROM asset_minting_batches batches
@@ -361,26 +365,34 @@ WITH target_batch AS (
         ON batches.batch_id = keys.key_id
     WHERE keys.raw_key = $1
 )
-UPDATE asset_minting_batches 
-SET minting_tx_psbt = $2, change_output_index = $3, genesis_id = $4
+UPDATE asset_minting_batches
+SET minting_tx_psbt = $2, change_output_index = $3, assets_output_index = $4,
+    genesis_id = $5, universe_commitments = $6
 WHERE batch_id IN (SELECT batch_id FROM target_batch)
+RETURNING batch_id
 `
 
 type BindMintingBatchWithTxParams struct {
-	RawKey            []byte
-	MintingTxPsbt     []byte
-	ChangeOutputIndex sql.NullInt32
-	GenesisID         sql.NullInt64
+	RawKey              []byte
+	MintingTxPsbt       []byte
+	ChangeOutputIndex   sql.NullInt32
+	AssetsOutputIndex   sql.NullInt32
+	GenesisID           sql.NullInt64
+	UniverseCommitments bool
 }
 
-func (q *Queries) BindMintingBatchWithTx(ctx context.Context, arg BindMintingBatchWithTxParams) error {
-	_, err := q.db.ExecContext(ctx, BindMintingBatchWithTx,
+func (q *Queries) BindMintingBatchWithTx(ctx context.Context, arg BindMintingBatchWithTxParams) (int64, error) {
+	row := q.db.QueryRowContext(ctx, BindMintingBatchWithTx,
 		arg.RawKey,
 		arg.MintingTxPsbt,
 		arg.ChangeOutputIndex,
+		arg.AssetsOutputIndex,
 		arg.GenesisID,
+		arg.UniverseCommitments,
 	)
-	return err
+	var batch_id int64
+	err := row.Scan(&batch_id)
+	return batch_id, err
 }
 
 const ConfirmChainAnchorTx = `-- name: ConfirmChainAnchorTx :exec
@@ -1502,6 +1514,26 @@ func (q *Queries) FetchManagedUTXOs(ctx context.Context) ([]FetchManagedUTXOsRow
 	return items, nil
 }
 
+const FetchMintAnchorUniCommitment = `-- name: FetchMintAnchorUniCommitment :one
+SELECT id, batch_id, tx_output_index, taproot_internal_key, group_key
+FROM mint_anchor_uni_commitments
+WHERE batch_id = $1
+`
+
+// Fetch a record from the mint_anchor_uni_commitments table by id.
+func (q *Queries) FetchMintAnchorUniCommitment(ctx context.Context, batchID int32) (MintAnchorUniCommitment, error) {
+	row := q.db.QueryRowContext(ctx, FetchMintAnchorUniCommitment, batchID)
+	var i MintAnchorUniCommitment
+	err := row.Scan(
+		&i.ID,
+		&i.BatchID,
+		&i.TxOutputIndex,
+		&i.TaprootInternalKey,
+		&i.GroupKey,
+	)
+	return i, err
+}
+
 const FetchMintingBatch = `-- name: FetchMintingBatch :one
 WITH target_batch AS (
     -- This CTE is used to fetch the ID of a batch, based on the serialized
@@ -1514,7 +1546,7 @@ WITH target_batch AS (
         ON batches.batch_id = keys.key_id
     WHERE keys.raw_key = $1
 )
-SELECT batch_id, batch_state, minting_tx_psbt, change_output_index, genesis_id, height_hint, creation_time_unix, tapscript_sibling, key_id, raw_key, key_family, key_index
+SELECT batch_id, batch_state, minting_tx_psbt, change_output_index, genesis_id, height_hint, creation_time_unix, tapscript_sibling, assets_output_index, universe_commitments, key_id, raw_key, key_family, key_index
 FROM asset_minting_batches batches
 JOIN internal_keys keys
     ON batches.batch_id = keys.key_id
@@ -1522,18 +1554,20 @@ WHERE batch_id in (SELECT batch_id FROM target_batch)
 `
 
 type FetchMintingBatchRow struct {
-	BatchID           int64
-	BatchState        int16
-	MintingTxPsbt     []byte
-	ChangeOutputIndex sql.NullInt32
-	GenesisID         sql.NullInt64
-	HeightHint        int32
-	CreationTimeUnix  time.Time
-	TapscriptSibling  []byte
-	KeyID             int64
-	RawKey            []byte
-	KeyFamily         int32
-	KeyIndex          int32
+	BatchID             int64
+	BatchState          int16
+	MintingTxPsbt       []byte
+	ChangeOutputIndex   sql.NullInt32
+	GenesisID           sql.NullInt64
+	HeightHint          int32
+	CreationTimeUnix    time.Time
+	TapscriptSibling    []byte
+	AssetsOutputIndex   sql.NullInt32
+	UniverseCommitments bool
+	KeyID               int64
+	RawKey              []byte
+	KeyFamily           int32
+	KeyIndex            int32
 }
 
 func (q *Queries) FetchMintingBatch(ctx context.Context, rawKey []byte) (FetchMintingBatchRow, error) {
@@ -1548,6 +1582,8 @@ func (q *Queries) FetchMintingBatch(ctx context.Context, rawKey []byte) (FetchMi
 		&i.HeightHint,
 		&i.CreationTimeUnix,
 		&i.TapscriptSibling,
+		&i.AssetsOutputIndex,
+		&i.UniverseCommitments,
 		&i.KeyID,
 		&i.RawKey,
 		&i.KeyFamily,
@@ -1557,7 +1593,7 @@ func (q *Queries) FetchMintingBatch(ctx context.Context, rawKey []byte) (FetchMi
 }
 
 const FetchMintingBatchesByInverseState = `-- name: FetchMintingBatchesByInverseState :many
-SELECT batch_id, batch_state, minting_tx_psbt, change_output_index, genesis_id, height_hint, creation_time_unix, tapscript_sibling, key_id, raw_key, key_family, key_index
+SELECT batch_id, batch_state, minting_tx_psbt, change_output_index, genesis_id, height_hint, creation_time_unix, tapscript_sibling, assets_output_index, universe_commitments, key_id, raw_key, key_family, key_index
 FROM asset_minting_batches batches
 JOIN internal_keys keys
     ON batches.batch_id = keys.key_id
@@ -1565,18 +1601,20 @@ WHERE batches.batch_state != $1
 `
 
 type FetchMintingBatchesByInverseStateRow struct {
-	BatchID           int64
-	BatchState        int16
-	MintingTxPsbt     []byte
-	ChangeOutputIndex sql.NullInt32
-	GenesisID         sql.NullInt64
-	HeightHint        int32
-	CreationTimeUnix  time.Time
-	TapscriptSibling  []byte
-	KeyID             int64
-	RawKey            []byte
-	KeyFamily         int32
-	KeyIndex          int32
+	BatchID             int64
+	BatchState          int16
+	MintingTxPsbt       []byte
+	ChangeOutputIndex   sql.NullInt32
+	GenesisID           sql.NullInt64
+	HeightHint          int32
+	CreationTimeUnix    time.Time
+	TapscriptSibling    []byte
+	AssetsOutputIndex   sql.NullInt32
+	UniverseCommitments bool
+	KeyID               int64
+	RawKey              []byte
+	KeyFamily           int32
+	KeyIndex            int32
 }
 
 func (q *Queries) FetchMintingBatchesByInverseState(ctx context.Context, batchState int16) ([]FetchMintingBatchesByInverseStateRow, error) {
@@ -1597,6 +1635,8 @@ func (q *Queries) FetchMintingBatchesByInverseState(ctx context.Context, batchSt
 			&i.HeightHint,
 			&i.CreationTimeUnix,
 			&i.TapscriptSibling,
+			&i.AssetsOutputIndex,
+			&i.UniverseCommitments,
 			&i.KeyID,
 			&i.RawKey,
 			&i.KeyFamily,
@@ -2936,6 +2976,42 @@ func (q *Queries) UpsertManagedUTXO(ctx context.Context, arg UpsertManagedUTXOPa
 	return utxo_id, err
 }
 
+const UpsertMintAnchorUniCommitment = `-- name: UpsertMintAnchorUniCommitment :one
+INSERT INTO mint_anchor_uni_commitments (
+    id, batch_id, tx_output_index, taproot_internal_key, group_key
+)
+VALUES ($1, $2, $3, $4, $5)
+ON CONFLICT(batch_id, tx_output_index) DO UPDATE SET
+    -- The following fields are updated if a conflict occurs.
+    taproot_internal_key = EXCLUDED.taproot_internal_key,
+    group_key = EXCLUDED.group_key
+RETURNING id
+`
+
+type UpsertMintAnchorUniCommitmentParams struct {
+	ID                 int64
+	BatchID            int32
+	TxOutputIndex      int32
+	TaprootInternalKey []byte
+	GroupKey           []byte
+}
+
+// Upsert a record into the mint_anchor_uni_commitments table.
+// If a record with the same batch_id and group_key already exists, update the
+// existing record. Otherwise, insert a new record.
+func (q *Queries) UpsertMintAnchorUniCommitment(ctx context.Context, arg UpsertMintAnchorUniCommitmentParams) (int64, error) {
+	row := q.db.QueryRowContext(ctx, UpsertMintAnchorUniCommitment,
+		arg.ID,
+		arg.BatchID,
+		arg.TxOutputIndex,
+		arg.TaprootInternalKey,
+		arg.GroupKey,
+	)
+	var id int64
+	err := row.Scan(&id)
+	return id, err
+}
+
 const UpsertScriptKey = `-- name: UpsertScriptKey :one
 INSERT INTO script_keys (
     internal_key_id, tweaked_script_key, tweak, declared_known
diff --git a/tapdb/sqlc/migrations/000030_mint_anchor_uni_commitments.down.sql b/tapdb/sqlc/migrations/000030_mint_anchor_uni_commitments.down.sql
new file mode 100644
index 00000000..09d43dd6
--- /dev/null
+++ b/tapdb/sqlc/migrations/000030_mint_anchor_uni_commitments.down.sql
@@ -0,0 +1,11 @@
+-- Drop the mint_anchor_uni_commitments table and its unique index.
+DROP INDEX IF EXISTS mint_anchor_uni_commitments_unique;
+
+-- Drop the table mint_anchor_uni_commitments.
+DROP TABLE IF EXISTS mint_anchor_uni_commitments;
+
+-- Drop the universe_commitments column from the asset_minting_batches table.
+ALTER TABLE asset_minting_batches DROP COLUMN universe_commitments;
+
+-- Drop the assets output index column from the asset_minting_batches table.
+ALTER TABLE asset_minting_batches DROP COLUMN assets_output_index;
\ No newline at end of file
diff --git a/tapdb/sqlc/migrations/000030_mint_anchor_uni_commitments.up.sql b/tapdb/sqlc/migrations/000030_mint_anchor_uni_commitments.up.sql
new file mode 100644
index 00000000..512d1105
--- /dev/null
+++ b/tapdb/sqlc/migrations/000030_mint_anchor_uni_commitments.up.sql
@@ -0,0 +1,44 @@
+-- Add a column to the asset_minting_batches table which stores the output index
+-- of the asset anchor transaction output.
+ALTER TABLE asset_minting_batches ADD COLUMN assets_output_index INTEGER;
+
+-- Existing minting anchor transactions have exactly two outputs: the asset
+-- commitment and the change output. We can therefore infer the asset anchor
+-- output index from the change output index.
+UPDATE asset_minting_batches
+SET assets_output_index = CASE
+    WHEN change_output_index = 1 THEN 0
+    WHEN change_output_index = 0 THEN 1
+    -- If change_output_index is neither 0 nor 1, just set the asset anchor
+    -- output index to NULL.
+    ELSE NULL
+END;
+
+-- Add a flag column which indicates if the universe commitments are enabled for
+-- this minting batch. This should default to false for all existing minting
+-- batches.
+ALTER TABLE asset_minting_batches
+    ADD COLUMN universe_commitments BOOLEAN NOT NULL DEFAULT FALSE;
+
+-- Create a table to relate a mint batch anchor transaction to its universe
+-- commitments.
+CREATE TABLE IF NOT EXISTS mint_anchor_uni_commitments (
+    id INTEGER PRIMARY KEY,
+
+    -- The ID of the minting batch this universe commitment relates to.
+    batch_id INTEGER NOT NULL REFERENCES asset_minting_batches(batch_id),
+
+    -- The index of the mint batch anchor transaction pre-commitment output.
+    tx_output_index INTEGER NOT NULL,
+
+    -- The Taproot output internal key for the pre-commitment output.
+    taproot_internal_key BLOB,
+
+    -- The asset group key associated with the universe commitment.
+    group_key BLOB
+);
+
+-- Create a unique index on the mint_anchor_uni_commitments table to enforce the
+-- uniqueness of (batch_id, tx_output_index) pairs.
+CREATE UNIQUE INDEX mint_anchor_uni_commitments_unique
+    ON mint_anchor_uni_commitments (batch_id, tx_output_index);
diff --git a/tapdb/sqlc/models.go b/tapdb/sqlc/models.go
index a0ffe55f..17555cec 100644
--- a/tapdb/sqlc/models.go
+++ b/tapdb/sqlc/models.go
@@ -81,14 +81,16 @@ type AssetGroupWitness struct {
 }
 
 type AssetMintingBatch struct {
-	BatchID           int64
-	BatchState        int16
-	MintingTxPsbt     []byte
-	ChangeOutputIndex sql.NullInt32
-	GenesisID         sql.NullInt64
-	HeightHint        int32
-	CreationTimeUnix  time.Time
-	TapscriptSibling  []byte
+	BatchID             int64
+	BatchState          int16
+	MintingTxPsbt       []byte
+	ChangeOutputIndex   sql.NullInt32
+	GenesisID           sql.NullInt64
+	HeightHint          int32
+	CreationTimeUnix    time.Time
+	TapscriptSibling    []byte
+	AssetsOutputIndex   sql.NullInt32
+	UniverseCommitments bool
 }
 
 type AssetProof struct {
@@ -276,6 +278,14 @@ type ManagedUtxo struct {
 	RootVersion      sql.NullInt16
 }
 
+type MintAnchorUniCommitment struct {
+	ID                 int64
+	BatchID            int32
+	TxOutputIndex      int32
+	TaprootInternalKey []byte
+	GroupKey           []byte
+}
+
 type MssmtNode struct {
 	HashKey   []byte
 	LHashKey  []byte
diff --git a/tapdb/sqlc/querier.go b/tapdb/sqlc/querier.go
index ef76b396..de2823b2 100644
--- a/tapdb/sqlc/querier.go
+++ b/tapdb/sqlc/querier.go
@@ -22,7 +22,7 @@ type Querier interface {
 	AssetsDBSizeSqlite(ctx context.Context) (int32, error)
 	AssetsInBatch(ctx context.Context, rawKey []byte) ([]AssetsInBatchRow, error)
 	BindMintingBatchWithTapSibling(ctx context.Context, arg BindMintingBatchWithTapSiblingParams) error
-	BindMintingBatchWithTx(ctx context.Context, arg BindMintingBatchWithTxParams) error
+	BindMintingBatchWithTx(ctx context.Context, arg BindMintingBatchWithTxParams) (int64, error)
 	ConfirmChainAnchorTx(ctx context.Context, arg ConfirmChainAnchorTxParams) error
 	ConfirmChainTx(ctx context.Context, arg ConfirmChainTxParams) error
 	DeleteAllNodes(ctx context.Context, namespace string) (int64, error)
@@ -76,6 +76,8 @@ type Querier interface {
 	FetchInternalKeyLocator(ctx context.Context, rawKey []byte) (FetchInternalKeyLocatorRow, error)
 	FetchManagedUTXO(ctx context.Context, arg FetchManagedUTXOParams) (FetchManagedUTXORow, error)
 	FetchManagedUTXOs(ctx context.Context) ([]FetchManagedUTXOsRow, error)
+	// Fetch a record from the mint_anchor_uni_commitments table by id.
+	FetchMintAnchorUniCommitment(ctx context.Context, batchID int32) (MintAnchorUniCommitment, error)
 	FetchMintingBatch(ctx context.Context, rawKey []byte) (FetchMintingBatchRow, error)
 	FetchMintingBatchesByInverseState(ctx context.Context, batchState int16) ([]FetchMintingBatchesByInverseStateRow, error)
 	FetchMultiverseRoot(ctx context.Context, namespaceRoot string) (FetchMultiverseRootRow, error)
@@ -172,6 +174,10 @@ type Querier interface {
 	UpsertGenesisPoint(ctx context.Context, prevOut []byte) (int64, error)
 	UpsertInternalKey(ctx context.Context, arg UpsertInternalKeyParams) (int64, error)
 	UpsertManagedUTXO(ctx context.Context, arg UpsertManagedUTXOParams) (int64, error)
+	// Upsert a record into the mint_anchor_uni_commitments table.
+	// If a record with the same batch_id and group_key already exists, update the
+	// existing record. Otherwise, insert a new record.
+	UpsertMintAnchorUniCommitment(ctx context.Context, arg UpsertMintAnchorUniCommitmentParams) (int64, error)
 	UpsertMultiverseLeaf(ctx context.Context, arg UpsertMultiverseLeafParams) (int64, error)
 	UpsertMultiverseRoot(ctx context.Context, arg UpsertMultiverseRootParams) (int64, error)
 	UpsertRootNode(ctx context.Context, arg UpsertRootNodeParams) error
diff --git a/tapdb/sqlc/queries/assets.sql b/tapdb/sqlc/queries/assets.sql
index 1b7a03d3..c3252bab 100644
--- a/tapdb/sqlc/queries/assets.sql
+++ b/tapdb/sqlc/queries/assets.sql
@@ -530,7 +530,7 @@ JOIN internal_keys keys
     ON keys.key_id = batches.batch_id
 WHERE keys.raw_key = $1;
 
--- name: BindMintingBatchWithTx :exec
+-- name: BindMintingBatchWithTx :one
 WITH target_batch AS (
     SELECT batch_id
     FROM asset_minting_batches batches
@@ -538,9 +538,11 @@ WITH target_batch AS (
         ON batches.batch_id = keys.key_id
     WHERE keys.raw_key = $1
 )
-UPDATE asset_minting_batches 
-SET minting_tx_psbt = $2, change_output_index = $3, genesis_id = $4
-WHERE batch_id IN (SELECT batch_id FROM target_batch);
+UPDATE asset_minting_batches
+SET minting_tx_psbt = $2, change_output_index = $3, assets_output_index = $4,
+    genesis_id = $5, universe_commitments = $6
+WHERE batch_id IN (SELECT batch_id FROM target_batch)
+RETURNING batch_id;
 
 -- name: BindMintingBatchWithTapSibling :exec
 WITH target_batch AS (
@@ -1015,3 +1017,23 @@ FROM genesis_assets assets
 JOIN assets_meta
     ON assets.meta_data_id = assets_meta.meta_id
 WHERE assets.asset_id = $1;
+
+-- Upsert a record into the mint_anchor_uni_commitments table.
+-- If a record with the same batch_id and group_key already exists, update the
+-- existing record. Otherwise, insert a new record.
+-- name: UpsertMintAnchorUniCommitment :one
+INSERT INTO mint_anchor_uni_commitments (
+    id, batch_id, tx_output_index, taproot_internal_key, group_key
+)
+VALUES ($1, $2, $3, $4, $5)
+ON CONFLICT(batch_id, tx_output_index) DO UPDATE SET
+    -- The following fields are updated if a conflict occurs.
+    taproot_internal_key = EXCLUDED.taproot_internal_key,
+    group_key = EXCLUDED.group_key
+RETURNING id;
+
+-- Fetch a record from the mint_anchor_uni_commitments table by id.
+-- name: FetchMintAnchorUniCommitment :one
+SELECT id, batch_id, tx_output_index, taproot_internal_key, group_key
+FROM mint_anchor_uni_commitments
+WHERE batch_id = $1;
\ No newline at end of file
diff --git a/tapdb/sqlc/schemas/generated_schema.sql b/tapdb/sqlc/schemas/generated_schema.sql
index ceb81fea..c9701eef 100644
--- a/tapdb/sqlc/schemas/generated_schema.sql
+++ b/tapdb/sqlc/schemas/generated_schema.sql
@@ -152,7 +152,7 @@ CREATE TABLE asset_minting_batches (
     height_hint INTEGER NOT NULL,
 
     creation_time_unix TIMESTAMP NOT NULL
-, tapscript_sibling BLOB);
+, tapscript_sibling BLOB, assets_output_index INTEGER, universe_commitments BOOLEAN NOT NULL DEFAULT FALSE);
 
 CREATE TABLE asset_proofs (
     proof_id INTEGER PRIMARY KEY,
@@ -510,6 +510,22 @@ CREATE TABLE managed_utxos (
     lease_expiry TIMESTAMP
 , root_version SMALLINT);
 
+CREATE TABLE mint_anchor_uni_commitments (
+    id INTEGER PRIMARY KEY,
+
+    -- The ID of the minting batch this universe commitment relates to.
+    batch_id INTEGER NOT NULL REFERENCES asset_minting_batches(batch_id),
+
+    -- The index of the mint batch anchor transaction pre-commitment output.
+    tx_output_index INTEGER NOT NULL,
+
+    -- The Taproot output internal key for the pre-commitment output.
+    taproot_internal_key BLOB,
+
+    -- The asset group key associated with the universe commitment.
+    group_key BLOB
+);
+
 CREATE TABLE mssmt_nodes (
     -- hash_key is the hash key by which we reference all nodes.
     hash_key BLOB NOT NULL,
diff --git a/tapgarden/batch.go b/tapgarden/batch.go
index 7aa0c95c..2d626f87 100644
--- a/tapgarden/batch.go
+++ b/tapgarden/batch.go
@@ -14,7 +14,6 @@ import (
 	"github.com/lightninglabs/taproot-assets/fn"
 	"github.com/lightninglabs/taproot-assets/proof"
 	"github.com/lightninglabs/taproot-assets/tapscript"
-	"github.com/lightninglabs/taproot-assets/tapsend"
 	"github.com/lightningnetwork/lnd/keychain"
 )
 
@@ -51,7 +50,7 @@ type MintingBatch struct {
 	// GenesisPacket is the funded genesis packet that may or may not be
 	// fully signed. When broadcast, this will create all assets stored
 	// within this batch.
-	GenesisPacket *tapsend.FundedPsbt
+	GenesisPacket *FundedMintAnchorPsbt
 
 	// RootAssetCommitment is the root Taproot Asset commitment for all the
 	// assets contained in this batch.
@@ -64,6 +63,14 @@ type MintingBatch struct {
 	// reveal for that asset, if it has one.
 	AssetMetas AssetMetas
 
+	// UniverseCommitments is a flag that determines whether the minting
+	// event supports universe commitments. When set to true, the batch must
+	// include only assets that share the same asset group key.
+	//
+	// Universe commitments are minter-controlled, on-chain anchored
+	// attestations regarding the state of the universe.
+	UniverseCommitments bool
+
 	// mintingPubKey is the top-level Taproot output key that will be used
 	// to commit to the Taproot Asset commitment above.
 	mintingPubKey *btcec.PublicKey
@@ -301,6 +308,233 @@ func (m *MintingBatch) HasSeedlings() bool {
 	return len(m.Seedlings) != 0
 }
 
+// validateDelegationKey ensures that the delegation key is valid for a seedling
+// being considered for inclusion in the batch.
+func (m *MintingBatch) validateDelegationKey(newSeedling Seedling) error {
+	// If the universe commitment flag is disabled, then the delegation key
+	// should not be set.
+	if !newSeedling.UniverseCommitments {
+		if newSeedling.DelegationKey.IsSome() {
+			return fmt.Errorf("delegation key must not be set " +
+				"for seedling without universe commitments")
+		}
+
+		// If the universe commitment flag is disabled and the
+		// delegation key is correctly unset, no further checks are
+		// needed.
+		return nil
+	}
+
+	// At this point, we know that the universe commitment flag is enabled
+	// for the seedling. Therefore, the delegation key must be set.
+	delegationKey, err := newSeedling.DelegationKey.UnwrapOrErr(
+		fmt.Errorf("delegation key must be set for seedling with " +
+			"universe commitments"),
+	)
+	if err != nil {
+		return err
+	}
+
+	// validateKeyDesc is a helper function to validate a key descriptor.
+	validateKeyDesc := func(keyDesc keychain.KeyDescriptor) error {
+		if keyDesc.PubKey == nil {
+			return fmt.Errorf("pubkey is nil")
+		}
+
+		if !keyDesc.PubKey.IsOnCurve() {
+			return fmt.Errorf("pubkey is not on curve")
+		}
+
+		return nil
+	}
+
+	// Ensure that the delegation key is valid.
+	err = validateKeyDesc(delegationKey)
+	if err != nil {
+		return fmt.Errorf("candidate seedling delegation "+
+			"key validation failed: %w", err)
+	}
+
+	// Ensure that the delegation key is the same for all seedlings in the
+	// batch.
+	for _, seedling := range m.Seedlings {
+		// Ensure that the delegation key matches that of the candidate
+		// seedling.
+		keyDesc, err := seedling.DelegationKey.UnwrapOrErr(
+			fmt.Errorf("delegation key must be set for seedling " +
+				"with universe commitments"),
+		)
+		if err != nil {
+			return err
+		}
+
+		if !delegationKey.PubKey.IsEqual(keyDesc.PubKey) {
+			return fmt.Errorf("delegation key mismatch")
+		}
+	}
+
+	return nil
+}
+
+// validateUniCommitment verifies that the seedling adheres to the universe
+// commitment feature restrictions in the context of the current batch state.
+func (m *MintingBatch) validateUniCommitment(newSeedling Seedling) error {
+	// If the batch is empty, the first seedling will set the universe
+	// commitment flag for the batch.
+	if !m.HasSeedlings() {
+		// If there are no seedlings in the batch, and the first
+		// (subject) seedling doesn't enable universe commitment, we can
+		// accept it without further checks.
+		if !newSeedling.UniverseCommitments {
+			return nil
+		}
+
+		// At this point, the given seedling is the first to be added to
+		// the batch, and it has the universe commitment flag enabled.
+		//
+		// The minting batch funding step records the genesis
+		// transaction in the database. Additionally, the uni-commitment
+		// feature requires the change output to be locked, ensuring it
+		// can only be spent by `tapd`. Therefore, to leverage the
+		// uni-commitment feature, the batch must be populated with
+		// seedlings, with the uni-commitment flag correctly set before
+		// any funding attempt is made.
+		//
+		// As such, when adding the first seedling with uni-commitment
+		// support to the batch, it is essential to verify that the
+		// batch has not yet been funded.
+		if m.IsFunded() {
+			return fmt.Errorf("attempting to add first seedling " +
+				"with universe commitment flag enabled to " +
+				"funded batch")
+		}
+
+		// At this point, we know the batch is empty, and the candidate
+		// seedling will be the first to be added. Consequently, if the
+		// seedling has the universe commitment flag enabled, it must
+		// specify a re-issuable asset group key.
+		if !newSeedling.EnableEmission {
+			return fmt.Errorf("the emission flag must be enabled " +
+				"for the first asset in a batch with the " +
+				"universe commitment flag enabled")
+		}
+
+		if !newSeedling.HasGroupKey() {
+			return fmt.Errorf("a group key must be specified " +
+				"for the first seedling in the batch when " +
+				"the universe commitment flag is enabled")
+		}
+
+		// No further checks are required for the first seedling in the
+		// batch.
+		return nil
+	}
+
+	// At this stage, we know that the batch contains seedlings.
+	// Furthermore, the universe commitment flag for the batch should have
+	// been correctly updated when the existing seedlings were added.
+	//
+	// Therefore, when evaluating this new candidate seedling for inclusion
+	// in the batch, we must ensure that its universe commitment flag state
+	// matches the flag state of the batch.
+	if m.UniverseCommitments != newSeedling.UniverseCommitments {
+		return fmt.Errorf("seedling universe commitment flag does " +
+			"not match batch")
+	}
+
+	// If the universe commitment flag is disabled for both the seedling and
+	// the batch, no additional checks are required.
+	if !m.UniverseCommitments && !newSeedling.UniverseCommitments {
+		return nil
+	}
+
+	// Logically, by this point, the following must be true:
+	// * the universe commitment flag is enabled for both the seedling and
+	//   the batch
+	// * the batch contains at least one seedling.
+	//
+	// For clarity, we will assert these conditions now.
+	if !m.UniverseCommitments || !newSeedling.UniverseCommitments ||
+		!m.HasSeedlings() {
+
+		return fmt.Errorf("unexpected code path reached")
+	}
+
+	// At this point, the candidate seedling (with uni commitments enabled)
+	// must have a group anchor that is already part of the batch. The group
+	// anchor must have been added to the batch before the candidate
+	// seedling.
+	if newSeedling.GroupAnchor == nil {
+		return fmt.Errorf("non-empty batch with uni commit enabled " +
+			"but candidate seedling does not have group anchor " +
+			"specified")
+	}
+
+	// For clarity, we will assert that the candidate seedling refers to a
+	// group anchor that is already part of the batch.
+	if _, ok := m.Seedlings[*newSeedling.GroupAnchor]; !ok {
+		return fmt.Errorf("group anchor for candidate seedling not " +
+			"present in batch")
+	}
+
+	// Next, we will also assert that there is only one group anchor in the
+	// batch.
+	var anchorCount int
+	for _, seedling := range m.Seedlings {
+		if seedling.GroupAnchor != nil {
+			anchorCount++
+		}
+	}
+	if anchorCount > 1 {
+		return fmt.Errorf("multiple group anchors present in batch " +
+			"with universe commitments enabled")
+	}
+
+	// Ensure that the group anchor for the candidate seedling is already
+	// present in the batch.
+	err := m.validateGroupAnchor(&newSeedling)
+	if err != nil {
+		return fmt.Errorf("group anchor validation failed: %w", err)
+	}
+
+	return nil
+}
+
+// AddSeedling adds a new seedling to the batch.
+func (m *MintingBatch) AddSeedling(newSeedling Seedling) error {
+	// Ensure that the seedling adheres to the universe commitment feature
+	// restrictions in relation to the current batch state.
+	err := m.validateUniCommitment(newSeedling)
+	if err != nil {
+		return fmt.Errorf("seedling does not comply with universe "+
+			"commitment feature: %w", err)
+	}
+
+	// At this stage, the seedling has been confirmed to comply with the
+	// universe commitment feature restrictions. If this is the first
+	// seedling being added to the batch, the batch universe commitment flag
+	// can be set to match the seedling's flag state.
+	if !m.HasSeedlings() {
+		m.UniverseCommitments = newSeedling.UniverseCommitments
+	}
+
+	// Ensure that the delegation key is valid for the seedling being
+	// considered for inclusion in the batch.
+	err = m.validateDelegationKey(newSeedling)
+	if err != nil {
+		return fmt.Errorf("delegation key validation failed: %w", err)
+	}
+
+	// Add the seedling to the batch.
+	if m.Seedlings == nil {
+		m.Seedlings = make(map[string]*Seedling)
+	}
+
+	m.Seedlings[newSeedling.AssetName] = &newSeedling
+
+	return nil
+}
+
 // ToMintingBatch creates a new MintingBatch from a VerboseBatch.
 func (v *VerboseBatch) ToMintingBatch() *MintingBatch {
 	newBatch := v.MintingBatch.Copy()
diff --git a/tapgarden/caretaker.go b/tapgarden/caretaker.go
index 08e738af..7e17b79c 100644
--- a/tapgarden/caretaker.go
+++ b/tapgarden/caretaker.go
@@ -412,24 +412,6 @@ func (b *BatchCaretaker) assetCultivator() {
 	}
 }
 
-// extractAnchorOutputIndex extracts the anchor output index from a funded
-// genesis packet.
-func extractAnchorOutputIndex(genesisPkt *tapsend.FundedPsbt) (uint32, error) {
-	if len(genesisPkt.Pkt.UnsignedTx.TxOut) != 2 {
-		return 0, fmt.Errorf("funded genesis packet has unexpected "+
-			"number of outputs, expected 2 (txout_len=%d)",
-			len(genesisPkt.Pkt.UnsignedTx.TxOut))
-	}
-
-	anchorOutputIndex := uint32(0)
-
-	if genesisPkt.ChangeOutputIndex == 0 {
-		anchorOutputIndex = 1
-	}
-
-	return anchorOutputIndex, nil
-}
-
 // extractGenesisOutpoint extracts the genesis point (the first input from the
 // genesis transaction).
 func extractGenesisOutpoint(tx *wire.MsgTx) wire.OutPoint {
@@ -612,18 +594,12 @@ func (b *BatchCaretaker) stateStep(currentState BatchState) (BatchState, error)
 			return 0, fmt.Errorf("unable to deserialize genesis "+
 				"PSBT: %w", err)
 		}
-		changeOutputIndex := b.cfg.Batch.GenesisPacket.ChangeOutputIndex
-
-		// If the change output is first, then our commitment is second,
-		// and vice versa.
-		// TODO(jhb): return the anchor index instead of change? or both
-		// so this works for N outputs
-		b.anchorOutputIndex, err = extractAnchorOutputIndex(
-			b.cfg.Batch.GenesisPacket,
-		)
-		if err != nil {
-			return 0, err
-		}
+
+		// Unpack output indexes.
+		genesisPacket := b.cfg.Batch.GenesisPacket
+
+		changeOutputIndex := genesisPacket.ChangeOutputIndex
+		b.anchorOutputIndex = genesisPacket.AssetAnchorOutIdx
 
 		genesisPoint := extractGenesisOutpoint(genesisTxPkt.UnsignedTx)
 
@@ -671,10 +647,15 @@ func (b *BatchCaretaker) stateStep(currentState BatchState) (BatchState, error)
 		log.Infof("BatchCaretaker(%x): committing sprouts to disk",
 			b.batchKey[:])
 
-		fundedGenesisPsbt := tapsend.FundedPsbt{
-			Pkt:               genesisTxPkt,
-			ChangeOutputIndex: changeOutputIndex,
+		fundedGenesisPsbt := FundedMintAnchorPsbt{
+			FundedPsbt: tapsend.FundedPsbt{
+				Pkt:               genesisTxPkt,
+				ChangeOutputIndex: changeOutputIndex,
+			},
+			AssetAnchorOutIdx:   b.anchorOutputIndex,
+			PreCommitmentOutput: genesisPacket.PreCommitmentOutput,
 		}
+
 		// With all our commitments created, we'll commit them to disk,
 		// replacing the existing seedlings we had created for each of
 		// these assets.
@@ -801,8 +782,9 @@ func (b *BatchCaretaker) stateStep(currentState BatchState) (BatchState, error)
 
 		err = b.cfg.Log.CommitSignedGenesisTx(
 			ctx, b.cfg.Batch.BatchKey.PubKey,
-			b.cfg.Batch.GenesisPacket, b.anchorOutputIndex,
-			merkleRoot, tapCommitmentRoot[:], siblingBytes,
+			&b.cfg.Batch.GenesisPacket.FundedPsbt,
+			b.anchorOutputIndex, merkleRoot, tapCommitmentRoot[:],
+			siblingBytes,
 		)
 		if err != nil {
 			return 0, fmt.Errorf("unable to commit genesis "+
diff --git a/tapgarden/interface.go b/tapgarden/interface.go
index 70d77cf9..b714e174 100644
--- a/tapgarden/interface.go
+++ b/tapgarden/interface.go
@@ -234,7 +234,7 @@ type MintingStore interface {
 	// NOTE: The BatchState should transition to BatchStateCommitted upon a
 	// successful call.
 	AddSproutsToBatch(ctx context.Context, batchKey *btcec.PublicKey,
-		genesisPacket *tapsend.FundedPsbt,
+		genesisPacket *FundedMintAnchorPsbt,
 		assets *commitment.TapCommitment) error
 
 	// CommitSignedGenesisTx adds a fully signed genesis transaction to the
@@ -288,7 +288,7 @@ type MintingStore interface {
 	// CommitBatchTx adds a funded transaction to the batch, which also sets
 	// the genesis point for the batch.
 	CommitBatchTx(ctx context.Context, batchKey *btcec.PublicKey,
-		genesisTx *tapsend.FundedPsbt) error
+		genesisTx FundedMintAnchorPsbt) error
 }
 
 // ChainBridge is our bridge to the target chain. It's used to get confirmation
diff --git a/tapgarden/mock.go b/tapgarden/mock.go
index ea53f23c..94f9f00a 100644
--- a/tapgarden/mock.go
+++ b/tapgarden/mock.go
@@ -22,6 +22,7 @@ import (
 	"github.com/btcsuite/btcwallet/waddrmgr"
 	"github.com/lightninglabs/lndclient"
 	"github.com/lightninglabs/taproot-assets/asset"
+	"github.com/lightninglabs/taproot-assets/fn"
 	"github.com/lightninglabs/taproot-assets/internal/test"
 	"github.com/lightninglabs/taproot-assets/proof"
 	"github.com/lightninglabs/taproot-assets/tapscript"
@@ -34,6 +35,232 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
+// RandGroupAnchorSeedling generates a random seedling for a group anchor asset.
+func RandGroupAnchorSeedling(t testing.TB, groupAnchorName string,
+	uniCommitments bool) Seedling {
+
+	scriptKey, _ := test.RandKeyDesc(t)
+
+	// For now, we only test the v0 and v1 versions.
+	assetVersion := asset.Version(test.RandIntn(2))
+	assetType := asset.Normal
+
+	assetGenesis := asset.RandGenesis(t, assetType)
+
+	// Create asset group key.
+	groupPrivateDesc, groupPrivateKey := test.RandKeyDesc(t)
+
+	// Generate the signature for our group genesis asset.
+	genSigner := asset.NewMockGenesisSigner(groupPrivateKey)
+	genTxBuilder := asset.MockGroupTxBuilder{}
+
+	genProtoAsset := asset.RandAssetWithValues(
+		t, assetGenesis, nil, asset.RandScriptKey(t),
+	)
+	groupKeyRequest := asset.NewGroupKeyRequestNoErr(
+		t, groupPrivateDesc, fn.None[asset.ExternalKey](), assetGenesis,
+		genProtoAsset, nil, fn.None[chainhash.Hash](),
+	)
+	genTx, err := groupKeyRequest.BuildGroupVirtualTx(&genTxBuilder)
+	require.NoError(t, err)
+
+	groupKey, err := asset.DeriveGroupKey(
+		genSigner, *genTx, *groupKeyRequest, nil,
+	)
+	require.NoError(t, err)
+
+	// Generate a delegation key if we're using universe commitments.
+	var delegationKey fn.Option[keychain.KeyDescriptor]
+	if uniCommitments {
+		keyDesc, _ := test.RandKeyDesc(t)
+		delegationKey = fn.Some[keychain.KeyDescriptor](keyDesc)
+	}
+
+	return Seedling{
+		AssetVersion: assetVersion,
+		AssetType:    assetType,
+		AssetName:    groupAnchorName,
+		Meta: &proof.MetaReveal{
+			Data: test.RandBytes(32),
+		},
+		Amount: uint64(test.RandInt[uint32]()),
+		GroupInfo: &asset.AssetGroup{
+			Genesis:  &assetGenesis,
+			GroupKey: groupKey,
+		},
+		ScriptKey:           asset.NewScriptKeyBip86(scriptKey),
+		EnableEmission:      true,
+		UniverseCommitments: uniCommitments,
+		DelegationKey:       delegationKey,
+	}
+}
+
+// RandNonAnchorGroupSeedling generates a random seedling for a non-anchor asset
+// in an asset group.
+func RandNonAnchorGroupSeedling(t testing.TB, assetVersion asset.Version,
+	assetType asset.Type, anchorName string, metaDataBlob []byte,
+	delegationKey fn.Option[keychain.KeyDescriptor],
+	uniCommitments bool) Seedling {
+
+	seedlingName := hex.EncodeToString(test.RandBytes(32))
+	scriptKey, _ := test.RandKeyDesc(t)
+
+	seedling := Seedling{
+		AssetVersion: assetVersion,
+		AssetType:    assetType,
+		AssetName:    seedlingName,
+		GroupAnchor:  &anchorName,
+		Meta: &proof.MetaReveal{
+			Data: metaDataBlob,
+		},
+		Amount:              uint64(test.RandInt[uint32]()),
+		ScriptKey:           asset.NewScriptKeyBip86(scriptKey),
+		EnableEmission:      true,
+		UniverseCommitments: uniCommitments,
+		DelegationKey:       delegationKey,
+	}
+	return seedling
+}
+
+// RandGroupSeedlings generates a random set of seedlings which all belong to a
+// single asset group.
+func RandGroupSeedlings(t testing.TB, numSeedlings int,
+	uniCommitments bool) []Seedling {
+
+	// Formulate group anchor seedling.
+	anchorName := hex.EncodeToString(test.RandBytes(32))
+
+	anchor := RandGroupAnchorSeedling(t, anchorName, uniCommitments)
+	seedlings := []Seedling{
+		anchor,
+	}
+
+	// Formulate non-anchor group seedlings.
+	for i := 0; i < numSeedlings-1; i++ {
+		seedling := RandNonAnchorGroupSeedling(
+			t, anchor.AssetVersion, anchor.AssetType, anchorName,
+			anchor.Meta.Data, anchor.DelegationKey, uniCommitments,
+		)
+		seedlings = append(seedlings, seedling)
+	}
+
+	return seedlings
+}
+
+// MintBatchOptions is a set of options for creating a new minting batch.
+type MintBatchOptions struct {
+	// totalSeedlings specifies the number of seedlings to generate in this
+	// minting batch. The seedlings are randomly assigned as grouped or
+	// ungrouped.
+	totalSeedlings int
+
+	// totalGroups specifies the number of asset groups to generate in this
+	// minting batch. Each element in the slice specifies the number of
+	// seedlings to generate for the corresponding asset group.
+	totalGroups []int
+
+	// universeCommitments specifies whether to generate universe
+	// commitments for the asset groups in this minting batch.
+	universeCommitments bool
+}
+
+// MintBatchOption is a functional option for creating a new minting batch.
+type MintBatchOption func(*MintBatchOptions)
+
+// DefaultMintBatchOptions returns a new set of default minting batch options.
+func DefaultMintBatchOptions() MintBatchOptions {
+	return MintBatchOptions{}
+}
+
+// WithTotalSeedlings sets the total number of seedlings to populate in the
+// minting batch.
+func WithTotalSeedlings(count int) MintBatchOption {
+	return func(options *MintBatchOptions) {
+		options.totalSeedlings = count
+	}
+}
+
+// WithTotalGroups sets the total number of asset groups to populate in the
+// minting batch. Each element in the slice specifies the number of seedlings
+// to generate for the corresponding asset group.
+func WithTotalGroups(counts []int) MintBatchOption {
+	return func(options *MintBatchOptions) {
+		options.totalGroups = counts
+	}
+}
+
+// WithUniverseCommitments specifies whether to generate universe commitments
+// for the asset groups in the minting batch.
+func WithUniverseCommitments(enabled bool) MintBatchOption {
+	return func(options *MintBatchOptions) {
+		options.universeCommitments = enabled
+	}
+}
+
+// RandMintingBatch creates a new minting batch with only random seedlings
+// populated for testing.
+func RandMintingBatch(t testing.TB, opts ...MintBatchOption) *MintingBatch {
+	// Construct options.
+	options := DefaultMintBatchOptions()
+	for _, opt := range opts {
+		opt(&options)
+	}
+
+	// If the total number of seedlings is unset, we set using the total
+	// number of seedlings in the asset groups.
+	if options.totalSeedlings == 0 {
+		for _, count := range options.totalGroups {
+			options.totalSeedlings += count
+		}
+	}
+
+	// Create an empty minting batch.
+	batchKey, _ := test.RandKeyDesc(t)
+	batch := &MintingBatch{
+		BatchKey:            batchKey,
+		HeightHint:          test.RandInt[uint32](),
+		CreationTime:        time.Now(),
+		UniverseCommitments: options.universeCommitments,
+	}
+
+	// Generate seedlings for each asset group.
+	for idx := range options.totalGroups {
+		countSeedlingsInGroup := options.totalGroups[idx]
+
+		groupSeedlings := RandGroupSeedlings(
+			t, countSeedlingsInGroup, options.universeCommitments,
+		)
+
+		// Add the seedlings to the total seedlings map.
+		for _, seedling := range groupSeedlings {
+			err := batch.AddSeedling(seedling)
+			require.NoError(t, err)
+		}
+	}
+
+	// If the total number of seedlings generated so far is less than the
+	// total number of seedlings requested, we generate the remaining
+	// seedlings at random.
+	if len(batch.Seedlings) < options.totalSeedlings {
+		remaining := options.totalSeedlings - len(batch.Seedlings)
+		randSeedlings := RandSeedlings(t, remaining)
+
+		// Add the seedlings to the total seedlings map.
+		for _, seedling := range randSeedlings {
+			err := batch.AddSeedling(*seedling)
+			require.NoError(t, err)
+		}
+	}
+
+	// Randomly generating seedlings may result in overlaps with existing
+	// ones, leading to fewer seedlings than intended. Sanity check to
+	// ensure that the total number of seedlings generated matches the
+	// requested amount. This check might help debug flakes in tests.
+	require.Equal(t, options.totalSeedlings, len(batch.Seedlings))
+
+	return batch
+}
+
 // RandSeedlings creates a new set of random seedlings for testing.
 func RandSeedlings(t testing.TB, numSeedlings int) map[string]*Seedling {
 	seedlings := make(map[string]*Seedling)
@@ -60,6 +287,9 @@ func RandSeedlings(t testing.TB, numSeedlings int) map[string]*Seedling {
 
 // RandSeedlingMintingBatch creates a new minting batch with only random
 // seedlings populated for testing.
+//
+// TODO(ffranr): Replace this function with RandMintingBatch. Note also function
+// addRandGroupToBatch.
 func RandSeedlingMintingBatch(t testing.TB, numSeedlings int) *MintingBatch {
 	genesisTx := NewGenesisTx(t, chainfee.FeePerKwFloor)
 	BatchKey, _ := test.RandKeyDesc(t)
@@ -68,9 +298,12 @@ func RandSeedlingMintingBatch(t testing.TB, numSeedlings int) *MintingBatch {
 		Seedlings:    RandSeedlings(t, numSeedlings),
 		HeightHint:   test.RandInt[uint32](),
 		CreationTime: time.Now(),
-		GenesisPacket: &tapsend.FundedPsbt{
-			Pkt:               &genesisTx,
-			ChangeOutputIndex: 1,
+		GenesisPacket: &FundedMintAnchorPsbt{
+			FundedPsbt: tapsend.FundedPsbt{
+				Pkt:               &genesisTx,
+				ChangeOutputIndex: 1,
+			},
+			AssetAnchorOutIdx: 0,
 		},
 	}
 }
@@ -184,6 +417,10 @@ func (m *MockWalletAnchor) FundPsbt(_ context.Context, packet *psbt.Packet,
 	packet.UnsignedTx.AddTxOut(&changeOutput)
 	packet.Outputs = append(packet.Outputs, psbt.POutput{})
 
+	// The change output was added last, so it will be the last output in
+	// the list. Update the change index to reflect this.
+	changeIdx = int32(len(packet.Outputs) - 1)
+
 	// We always have the change output be the second output, so this means
 	// the Taproot Asset commitment will live in the first output.
 	pkt := &tapsend.FundedPsbt{
diff --git a/tapgarden/planter.go b/tapgarden/planter.go
index 981b2202..ca4ead21 100644
--- a/tapgarden/planter.go
+++ b/tapgarden/planter.go
@@ -14,8 +14,10 @@ import (
 	"github.com/btcsuite/btcd/btcutil/psbt"
 	"github.com/btcsuite/btcd/chaincfg"
 	"github.com/btcsuite/btcd/chaincfg/chainhash"
+	"github.com/btcsuite/btcd/txscript"
 	"github.com/btcsuite/btcd/wire"
 	"github.com/davecgh/go-spew/spew"
+	"github.com/lightninglabs/taproot-assets/address"
 	"github.com/lightninglabs/taproot-assets/asset"
 	"github.com/lightninglabs/taproot-assets/fn"
 	"github.com/lightninglabs/taproot-assets/proof"
@@ -23,6 +25,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"
 )
@@ -83,6 +86,11 @@ type GardenKit struct {
 type PlanterConfig struct {
 	GardenKit
 
+	// ChainParams defines the chain parameters for the target blockchain
+	// network. It specifies whether the network is Bitcoin mainnet or
+	// testnet.
+	ChainParams address.ChainParams
+
 	// ProofUpdates is the storage backend for updated proofs.
 	ProofUpdates proof.Archiver
 
@@ -663,6 +671,203 @@ func (c *ChainPlanter) newBatch() (*MintingBatch, error) {
 	return newBatch, nil
 }
 
+// preCommitmentOutput creates the pre-commitment output for a batch that uses
+// universe commitments.
+func preCommitmentOutput(pendingBatch *MintingBatch) (PreCommitmentOutput,
+	error) {
+
+	var zero PreCommitmentOutput
+
+	// Ensure that a pending batch is provided.
+	if pendingBatch == nil {
+		return zero, fmt.Errorf("no pending batch provided when " +
+			"creating pre-commitment output")
+	}
+
+	// Ensure that the universe commitments feature is enabled for the
+	// batch.
+	if !pendingBatch.UniverseCommitments {
+		return zero, fmt.Errorf("code error: universe commitments " +
+			"should be enabled before calling " +
+			"preCommitmentOutput")
+	}
+
+	// Ensure that the batch has at least one seedling.
+	if len(pendingBatch.Seedlings) == 0 {
+		return zero, fmt.Errorf("uni commitment enabled for funded " +
+			"batch but no seedlings in batch")
+	}
+
+	// Retrieve batch anchor seedling.
+	var groupAnchorSeedling *Seedling
+	for _, seedling := range pendingBatch.Seedlings {
+		if seedling.GroupAnchor == nil {
+			groupAnchorSeedling = seedling
+			break
+		}
+
+		groupAnchorSeedling =
+			pendingBatch.Seedlings[*seedling.GroupAnchor]
+		break
+	}
+
+	// Ensure that the group anchor seedling is found.
+	if groupAnchorSeedling == nil {
+		return zero, fmt.Errorf("no group anchor seedling found")
+	}
+
+	// Extract delegated key from the group anchor seedling.
+	delegationKey, err := groupAnchorSeedling.DelegationKey.UnwrapOrErr(
+		fmt.Errorf("no delegation key found in seedling"),
+	)
+	if err != nil {
+		return zero, err
+	}
+
+	// Extract group pub key from group anchor seedling.
+	if groupAnchorSeedling.GroupInfo == nil {
+		return zero, fmt.Errorf("no group info found in seedling")
+	}
+	groupPubKey := groupAnchorSeedling.GroupInfo.GroupPubKey
+
+	// Use a placeholder output index for the pre-commitment output. This
+	// will be revised after funding.
+	var placeholderOutIdx uint32 = 0
+
+	// Formulate the pre-commitment output bundle.
+	preCommitOut := NewPreCommitmentOutput(
+		placeholderOutIdx, *delegationKey.PubKey, groupPubKey,
+	)
+	return preCommitOut, nil
+}
+
+// unfundedAnchorPsbt creates an unfunded PSBT packet for the minting anchor
+// transaction.
+func unfundedAnchorPsbt(preCommitmentTxOut fn.Option[wire.TxOut]) (psbt.Packet,
+	error) {
+
+	var zero psbt.Packet
+
+	// Construct a template transaction for our minting anchor transaction.
+	txTemplate := wire.NewMsgTx(2)
+
+	// Add one output to anchor all assets which are being minted.
+	txTemplate.AddTxOut(tapsend.CreateDummyOutput())
+
+	// If universe commitments are enabled, we add an output to the
+	// transaction which will be used as the pre-commitment output.
+	// This output is spent by the universe commitment transaction.
+	preCommitmentTxOut.WhenSome(func(txOut wire.TxOut) {
+		txTemplate.AddTxOut(&txOut)
+	})
+
+	// Formulate the PSBT packet from the template transaction.
+	genesisPkt, err := psbt.NewFromUnsignedTx(txTemplate)
+	if err != nil {
+		return zero, fmt.Errorf("unable to make psbt packet: %w", err)
+	}
+
+	return *genesisPkt, nil
+}
+
+// AnchorTxOutputIndexes specifies the output indexes of the batch mint anchor
+// transaction.
+type AnchorTxOutputIndexes struct {
+	// AssetAnchorOutIdx is the index of the asset anchor output in the
+	// transaction.
+	AssetAnchorOutIdx uint32
+
+	// ChangeOutIdx is the index of the change output in the transaction.
+	ChangeOutIdx uint32
+
+	// PreCommitOutIdx is the index of the pre-commitment output in the
+	// transaction. This field is only set if universe commitments are
+	// enabled for the batch.
+	PreCommitOutIdx fn.Option[uint32]
+}
+
+// anchorTxOutputIndexes specifies the output indexes of the anchor transaction.
+func anchorTxOutputIndexes(fundedPsbt tapsend.FundedPsbt,
+	preCommitmentTxOut fn.Option[wire.TxOut]) (AnchorTxOutputIndexes,
+	error) {
+
+	var (
+		zero AnchorTxOutputIndexes
+
+		// assetAnchorOutIdxOpt will contain the index of the asset
+		// anchor output in the transaction.
+		assetAnchorOutIdxOpt fn.Option[uint32]
+
+		// preCommitOutIdx will contain the index of the pre-commitment
+		// output in the transaction. This field is only
+		// set if universe commitments are enabled for the batch.
+		preCommitOutIdx fn.Option[uint32]
+	)
+
+	// Formulate the expected asset anchor output that we will use to
+	// identify the asset anchor output in the transaction.
+	expectedAssetAnchorOutput := tapsend.CreateDummyOutput()
+	expectedAssetAnchorPkScript := expectedAssetAnchorOutput.PkScript
+
+	// Inspect each output in the transaction to determine the output
+	// indexes.
+	for idx := range fundedPsbt.Pkt.UnsignedTx.TxOut {
+		// Skip the change output based on its index.
+		if int32(idx) == fundedPsbt.ChangeOutputIndex {
+			continue
+		}
+
+		// We will inspect the output script pubkey to determine whether
+		// it is the asset anchor output or the pre-commitment output.
+		txOut := fundedPsbt.Pkt.UnsignedTx.TxOut[idx]
+
+		// If the output script pubkey matches the expected asset anchor
+		// output script pubkey, we have found the asset anchor output.
+		if bytes.Equal(txOut.PkScript, expectedAssetAnchorPkScript) {
+			assetAnchorOutIdxOpt = fn.Some(uint32(idx))
+			continue
+		}
+
+		// If universe commitments are enabled, we will inspect the
+		// output script pubkey to determine whether it is the
+		// pre-commitment output.
+		preCommitmentTxOut.WhenSome(
+			func(preCommitTxOut wire.TxOut) {
+				// If the output script pubkey matches the
+				// pre-commitment output script pubkey, we have
+				// found the pre-commitment output.
+				outputMatch := bytes.Equal(
+					txOut.PkScript, preCommitTxOut.PkScript,
+				)
+				if outputMatch {
+					preCommitOutIdx = fn.Some(uint32(idx))
+				}
+			},
+		)
+	}
+
+	// Unpack the asset anchor output index. Return an error if the output
+	// index is not found.
+	assetAnchorOutIdx, err := assetAnchorOutIdxOpt.UnwrapOrErr(
+		fmt.Errorf("asset anchor output index not found"),
+	)
+	if err != nil {
+		return zero, err
+	}
+
+	// If the pre-commitment output is expected, but not found, we return an
+	// error.
+	if preCommitmentTxOut.IsSome() && !preCommitOutIdx.IsSome() {
+		return zero, fmt.Errorf("pre-commitment output index not found")
+	}
+
+	return AnchorTxOutputIndexes{
+		AssetAnchorOutIdx: assetAnchorOutIdx,
+		ChangeOutIdx:      uint32(fundedPsbt.ChangeOutputIndex),
+		PreCommitOutIdx:   preCommitOutIdx,
+	}, nil
+}
+
 // fundGenesisPsbt generates a PSBT packet we'll use to create an asset.  In
 // order to be able to create an asset, we need an initial genesis outpoint. To
 // obtain this we'll ask the wallet to fund a PSBT template for GenesisAmtSats
@@ -671,22 +876,50 @@ func (c *ChainPlanter) newBatch() (*MintingBatch, error) {
 // that's dependent on the genesis outpoint.
 func (c *ChainPlanter) fundGenesisPsbt(ctx context.Context,
 	batchKey asset.SerializedKey,
-	manualFeeRate *chainfee.SatPerKWeight) (*tapsend.FundedPsbt, error) {
+	manualFeeRate *chainfee.SatPerKWeight) (FundedMintAnchorPsbt, error) {
 
+	var zero FundedMintAnchorPsbt
 	log.Infof("Attempting to fund batch: %x", batchKey)
 
-	// Construct a 1-output TX as a template for our genesis TX, which the
-	// backing wallet will fund.
-	txTemplate := wire.NewMsgTx(2)
-	txTemplate.AddTxOut(tapsend.CreateDummyOutput())
-	genesisPkt, err := psbt.NewFromUnsignedTx(txTemplate)
-	if err != nil {
-		return nil, fmt.Errorf("unable to make psbt packet: %w", err)
+	// If universe commitments are enabled, we formulate a pre-commitment
+	// output. This output is spent by the universe commitment transaction.
+	var preCommitmentOut fn.Option[PreCommitmentOutput]
+	if c.pendingBatch != nil && c.pendingBatch.UniverseCommitments {
+		out, err := preCommitmentOutput(c.pendingBatch)
+		if err != nil {
+			return zero, fmt.Errorf("unable to create "+
+				"pre-commitment output: %w", err)
+		}
+
+		preCommitmentOut = fn.Some(out)
 	}
 
-	log.Infof("creating skeleton PSBT for batch: %x", batchKey)
-	log.Tracef("PSBT: %v", spew.Sdump(genesisPkt))
+	// Derive wire.TxOut from the pre-commitment output, if available.
+	var preCommitmentTxOut fn.Option[wire.TxOut]
+	if preCommitmentOut.IsSome() {
+		txOut, err := fn.MapOptionZ(
+			preCommitmentOut,
+			func(p PreCommitmentOutput) lfn.Result[wire.TxOut] {
+				return lfn.NewResult(p.TxOut())
+			},
+		).Unpack()
+		if err != nil {
+			return zero, err
+		}
+
+		preCommitmentTxOut = fn.Some(txOut)
+	}
+
+	// Construct an unfunded anchor PSBT which will eventually become a
+	// funded minting anchor transaction.
+	genesisPkt, err := unfundedAnchorPsbt(preCommitmentTxOut)
+	if err != nil {
+		return zero, fmt.Errorf("unable to create anchor template tx: "+
+			"%w", err)
+	}
+	log.Tracef("Unfunded batch anchor PSBT: %v", spew.Sdump(genesisPkt))
 
+	// Compute the anchor transaction fee rate.
 	var feeRate chainfee.SatPerKWeight
 	switch {
 	// If a fee rate was manually assigned for this batch, use that instead
@@ -702,7 +935,7 @@ func (c *ChainPlanter) fundGenesisPsbt(ctx context.Context,
 			ctx, GenesisConfTarget,
 		)
 		if err != nil {
-			return nil, fmt.Errorf("unable to estimate fee: %w",
+			return zero, fmt.Errorf("unable to estimate fee: %w",
 				err)
 		}
 
@@ -712,7 +945,7 @@ func (c *ChainPlanter) fundGenesisPsbt(ctx context.Context,
 
 	minRelayFee, err := c.cfg.Wallet.MinRelayFee(ctx)
 	if err != nil {
-		return nil, fmt.Errorf("unable to obtain minrelayfee: %w", err)
+		return zero, fmt.Errorf("unable to obtain minrelayfee: %w", err)
 	}
 
 	// If the fee rate is below the minimum relay fee, we'll
@@ -725,7 +958,7 @@ func (c *ChainPlanter) fundGenesisPsbt(ctx context.Context,
 			// This case should already have been handled by the
 			// `checkFeeRateSanity` of `rpcserver.go`. We check here
 			// again to be safe.
-			return nil, fmt.Errorf("feerate does not meet "+
+			return zero, fmt.Errorf("feerate does not meet "+
 				"minrelayfee: (fee_rate=%s, minrelayfee=%s)",
 				feeRate.String(), minRelayFee.String())
 		default:
@@ -737,16 +970,68 @@ func (c *ChainPlanter) fundGenesisPsbt(ctx context.Context,
 	}
 
 	fundedGenesisPkt, err := c.cfg.Wallet.FundPsbt(
-		ctx, genesisPkt, 1, feeRate, -1,
+		ctx, &genesisPkt, 1, feeRate, -1,
 	)
 	if err != nil {
-		return nil, fmt.Errorf("unable to fund psbt: %w", err)
+		return zero, fmt.Errorf("unable to fund psbt: %w", err)
+	}
+
+	// Sanity check the funded PSBT.
+	if fundedGenesisPkt.ChangeOutputIndex == -1 {
+		return zero, fmt.Errorf("undefined change output index in " +
+			"funded anchor transaction")
 	}
 
 	log.Infof("Funded GenesisPacket for batch: %x", batchKey)
 	log.Tracef("GenesisPacket: %v", spew.Sdump(fundedGenesisPkt))
 
-	return fundedGenesisPkt, nil
+	// Classify anchor transaction output indexes.
+	anchorOutIndexes, err := anchorTxOutputIndexes(
+		*fundedGenesisPkt, preCommitmentTxOut,
+	)
+	if err != nil {
+		return zero, fmt.Errorf("unable to determine output indexes: "+
+			"%w", err)
+	}
+
+	// Sanity check that the pre-commitment output index was found if
+	// expected.
+	if preCommitmentOut.IsSome() &&
+		anchorOutIndexes.PreCommitOutIdx.IsNone() {
+
+		return zero, fmt.Errorf("pre-commitment output index not found")
+	}
+
+	// If pre-commitment output is some, assign the output index to the
+	// pre-commitment output.
+	if preCommitmentOut.IsSome() {
+		// Ensure that a pre-commitment output index is found.
+		outIdx, err := anchorOutIndexes.PreCommitOutIdx.UnwrapOrErr(
+			fmt.Errorf("pre-commitment output index not found"),
+		)
+		if err != nil {
+			return zero, err
+		}
+
+		// Assign output index to the pre-commitment output.
+		preCommitmentOut = fn.MapOption(
+			func(out PreCommitmentOutput) PreCommitmentOutput {
+				out.OutIdx = outIdx
+				return out
+			},
+		)(preCommitmentOut)
+	}
+
+	// Formulate a funded minting anchor PSBT from the funded PSBT.
+	fundedMintAnchorPsbt, err := NewFundedMintAnchorPsbt(
+		*fundedGenesisPkt, anchorOutIndexes, preCommitmentOut,
+	)
+	if err != nil {
+		return zero, fmt.Errorf("unable to create funded minting "+
+			"anchor PSBT: %w", err)
+	}
+
+	return fundedMintAnchorPsbt, nil
 }
 
 // filterSeedlingsWithGroup separates a set of seedlings into two sets based on
@@ -1037,12 +1322,13 @@ func filterFinalizedBatches(batches []*MintingBatch) ([]*MintingBatch,
 func fetchFinalizedBatch(ctx context.Context, batchStore MintingStore,
 	archiver proof.Archiver, batch *MintingBatch) (*MintingBatch, error) {
 
+	if batch.GenesisPacket == nil {
+		return nil, fmt.Errorf("batch is missing anchor tx packet")
+	}
+
 	// Collect genesis TX information from the batch to build the proof
 	// locators.
-	anchorOutputIndex, err := extractAnchorOutputIndex(batch.GenesisPacket)
-	if err != nil {
-		return nil, err
-	}
+	anchorOutputIndex := batch.GenesisPacket.AssetAnchorOutIdx
 
 	signedTx, err := psbt.Extract(batch.GenesisPacket.Pkt)
 	if err != nil {
@@ -1279,12 +1565,7 @@ func newVerboseBatch(currentBatch *MintingBatch,
 
 	// Before we can build the group key requests for each seedling, we must
 	// fetch the genesis point and anchor index for the batch.
-	anchorOutputIndex, err := extractAnchorOutputIndex(
-		currentBatch.GenesisPacket,
-	)
-	if err != nil {
-		return nil, err
-	}
+	anchorOutputIndex := currentBatch.GenesisPacket.AssetAnchorOutIdx
 
 	genesisPoint := extractGenesisOutpoint(
 		currentBatch.GenesisPacket.Pkt.UnsignedTx,
@@ -1727,13 +2008,13 @@ func (c *ChainPlanter) fundBatch(ctx context.Context, params FundParams,
 
 		// Fund the batch with the specified fee rate.
 		batchKey := asset.ToSerialized(batch.BatchKey.PubKey)
-		batchTX, err := c.fundGenesisPsbt(ctx, batchKey, feeRate)
+		mintAnchorTx, err := c.fundGenesisPsbt(ctx, batchKey, feeRate)
 		if err != nil {
 			return fmt.Errorf("unable to fund minting PSBT for "+
 				"batch: %x %w", batchKey[:], err)
 		}
 
-		batch.GenesisPacket = batchTX
+		batch.GenesisPacket = &mintAnchorTx
 
 		return nil
 	}
@@ -1781,7 +2062,7 @@ func (c *ChainPlanter) fundBatch(ctx context.Context, params FundParams,
 	}
 
 	err = c.cfg.Log.CommitBatchTx(
-		ctx, workingBatch.BatchKey.PubKey, workingBatch.GenesisPacket,
+		ctx, workingBatch.BatchKey.PubKey, *workingBatch.GenesisPacket,
 	)
 	if err != nil {
 		return err
@@ -1862,12 +2143,7 @@ func (c *ChainPlanter) sealBatch(ctx context.Context, params SealParams,
 
 	// Before we can build the group key requests for each seedling, we must
 	// fetch the genesis point and anchor index for the batch.
-	anchorOutputIndex, err := extractAnchorOutputIndex(
-		workingBatch.GenesisPacket,
-	)
-	if err != nil {
-		return nil, err
-	}
+	anchorOutputIndex := workingBatch.GenesisPacket.AssetAnchorOutIdx
 
 	genesisPoint := extractGenesisOutpoint(
 		workingBatch.GenesisPacket.Pkt.UnsignedTx,
@@ -2245,12 +2521,93 @@ func (c *ChainPlanter) CancelBatch() (*btcec.PublicKey, error) {
 	return <-req.resp, <-req.err
 }
 
+// prepSeedlingDelegationKey finalizes the seedling delegation key.
+func (c *ChainPlanter) prepSeedlingDelegationKey(ctx context.Context,
+	req *Seedling) error {
+
+	// If the universe commitments feature is disabled for this seedling,
+	// we can skip any further delegation key considerations.
+	if !req.UniverseCommitments {
+		return nil
+	}
+
+	// At this point, we know that the universe commitments feature is
+	// enabled for the seedling. If a group anchor seedling is specified
+	// we will use its delegation key.
+	if req.GroupAnchor != nil {
+		// Retrieve the group anchor seedling from the pending batch.
+		anchorSeedlingName := *req.GroupAnchor
+
+		anchor, ok := c.pendingBatch.Seedlings[anchorSeedlingName]
+		if anchor == nil || !ok {
+			return fmt.Errorf("group anchor seedling not present "+
+				"in batch (anchor_seedling_name=%s)",
+				anchorSeedlingName)
+		}
+
+		if anchor.DelegationKey.IsNone() {
+			return fmt.Errorf("group anchor seedling has no "+
+				"delegation key (anchor_seedling_name=%s)",
+				anchorSeedlingName)
+		}
+
+		// Set the delegation key for the seedling to the delegation key
+		// of the group anchor seedling.
+		req.DelegationKey = anchor.DelegationKey
+
+		// Return early, no further seedling prep required for universe
+		// commitments feature.
+		return nil
+	}
+
+	// On the other hand, if we're handling the group anchor seedling, we
+	// and the delegation key is unset, we must generate a new one.
+	if req.EnableEmission && req.GroupAnchor == nil {
+		newKey, err := c.cfg.KeyRing.DeriveNextKey(
+			ctx, asset.TaprootAssetsKeyFamily,
+		)
+		if err != nil {
+			return fmt.Errorf("unable to derive pre-commitment "+
+				"output key: %w", err)
+		}
+
+		req.DelegationKey = fn.Some(newKey)
+	}
+
+	return nil
+}
+
 // prepAssetSeedling performs some basic validation for the Seedling, then
 // either adds it to an existing pending batch or creates a new batch for it.
 func (c *ChainPlanter) prepAssetSeedling(ctx context.Context,
 	req *Seedling) error {
 
-	// First, we'll perform some basic validation for the seedling.
+	// Finalise the seedling delegation key.
+	err := c.prepSeedlingDelegationKey(ctx, req)
+	if err != nil {
+		return err
+	}
+
+	// Set seedling asset metadata fields.
+	req.Meta.UniverseCommitments = req.UniverseCommitments
+
+	if req.DelegationKey.IsSome() {
+		keyDesc, err := req.DelegationKey.UnwrapOrErr(
+			fmt.Errorf("delegation key is not set"),
+		)
+		if err != nil {
+			return err
+		}
+
+		if keyDesc.PubKey == nil {
+			return fmt.Errorf("delegation key has no public key")
+		}
+
+		req.Meta.DelegationKey = fn.Some(*keyDesc.PubKey)
+	}
+
+	// We will perform basic validation on the seedling, including metadata
+	// validation.
 	if err := req.validateFields(); err != nil {
 		return err
 	}
@@ -2363,30 +2720,40 @@ func (c *ChainPlanter) prepAssetSeedling(ctx context.Context,
 			return err
 		}
 
-		log.Infof("Adding %v to new MintingBatch", req)
+		c.pendingBatch = newBatch
+
+		log.Infof("Attempting to add a seedling to a new batch "+
+			"(seedling=%v)", req)
 
-		newBatch.Seedlings[req.AssetName] = req
+		err = c.pendingBatch.AddSeedling(*req)
+		if err != nil {
+			return fmt.Errorf("failed to add seedling to batch: %w",
+				err)
+		}
 
 		ctx, cancel := c.WithCtxQuit()
 		defer cancel()
-		err = c.cfg.Log.CommitMintingBatch(ctx, newBatch)
+		err = c.cfg.Log.CommitMintingBatch(ctx, c.pendingBatch)
 		if err != nil {
 			return err
 		}
 
-		c.pendingBatch = newBatch
-
 	// A batch already exists, so we'll add this seedling to the batch,
 	// committing it to disk fully before we move on.
 	case c.pendingBatch != nil:
-		log.Infof("Adding %v to existing MintingBatch", req)
+		log.Infof("Attempting to add a seedling to batch (seedling=%v)",
+			req)
 
-		c.pendingBatch.Seedlings[req.AssetName] = req
+		err := c.pendingBatch.AddSeedling(*req)
+		if err != nil {
+			return fmt.Errorf("failed to add seedling to batch: %w",
+				err)
+		}
 
 		// Now that we know the seedling is ok, we'll write it to disk.
 		ctx, cancel := c.WithCtxQuit()
 		defer cancel()
-		err := c.cfg.Log.AddSeedlingsToBatch(
+		err = c.cfg.Log.AddSeedlingsToBatch(
 			ctx, c.pendingBatch.BatchKey.PubKey, req,
 		)
 		if err != nil {
@@ -2569,3 +2936,117 @@ var _ Planter = (*ChainPlanter)(nil)
 // A compile-time assertion to make sure BatchCaretaker satisfies the
 // fn.EventPublisher interface.
 var _ fn.EventPublisher[fn.Event, bool] = (*ChainPlanter)(nil)
+
+// PreCommitmentOutput provides metadata related to the pre-commitment output
+// of a mint anchor transaction. This output serves as an intermediate step
+// before being spent by the universe commitment transaction.
+type PreCommitmentOutput struct {
+	// OutIdx specifies the index of the pre-commitment output within the
+	// batch mint anchor transaction.
+	OutIdx uint32
+
+	// InternalKey is the Taproot internal public key associated with the
+	// pre-commitment output.
+	InternalKey btcec.PublicKey
+
+	// GroupPubKey is the asset group public key associated with this
+	// pre-commitment output.
+	GroupPubKey btcec.PublicKey
+}
+
+// NewPreCommitmentOutput creates a new PreCommitmentOutput instance.
+func NewPreCommitmentOutput(outIdx uint32, internalKey,
+	groupPubKey btcec.PublicKey) PreCommitmentOutput {
+
+	return PreCommitmentOutput{
+		OutIdx:      outIdx,
+		InternalKey: internalKey,
+		GroupPubKey: groupPubKey,
+	}
+}
+
+// TxOut returns the pre-commitment output as a wire.TxOut instance.
+func (p *PreCommitmentOutput) TxOut() (wire.TxOut, error) {
+	var zero wire.TxOut
+
+	// Formulate a taproot output key from the taproot internal key.
+	taprootOutputKey := txscript.ComputeTaprootKeyNoScript(&p.InternalKey)
+
+	// Create a new pay-to-taproot pk script from the taproot output key.
+	pkScript, err := txscript.PayToTaprootScript(taprootOutputKey)
+	if err != nil {
+		return zero, fmt.Errorf("unable to create pre-commitment "+
+			"output pk script: %w", err)
+	}
+
+	// Return the minting anchor transaction pre-commitment output.
+	return wire.TxOut{
+		Value:    int64(tapsend.DummyAmtSats),
+		PkScript: pkScript,
+	}, nil
+}
+
+// FundedMintAnchorPsbt is a struct that contains a funded minting anchor
+// transaction PSBT.
+type FundedMintAnchorPsbt struct {
+	// FundedPsbt is the PSBT packet that has been funded by the wallet.
+	tapsend.FundedPsbt
+
+	// AssetAnchorOutIdx is the index of the asset anchor output in the
+	// transaction.
+	AssetAnchorOutIdx uint32
+
+	// PreCommitmentOutput contains metadata describing the pre-commitment
+	// output.
+	//
+	// This field is set only if the pre-commitment output exists in the
+	// transaction. The pre-commitment output is later spent by the universe
+	// commitment transaction.
+	PreCommitmentOutput fn.Option[PreCommitmentOutput]
+}
+
+// NewFundedMintAnchorPsbt creates a new funded minting anchor PSBT package from
+// a funded PSBT.
+func NewFundedMintAnchorPsbt(
+	fundedPsbt tapsend.FundedPsbt, anchorOutIndexes AnchorTxOutputIndexes,
+	preCommitOut fn.Option[PreCommitmentOutput]) (FundedMintAnchorPsbt,
+	error) {
+
+	var zero FundedMintAnchorPsbt
+
+	// Sanity check pre-commitment output arguments.
+	if anchorOutIndexes.PreCommitOutIdx.IsSome() != preCommitOut.IsSome() {
+		return zero, fmt.Errorf("pre-commitment output index and " +
+			"pre-commitment output must be both set or both unset")
+	}
+
+	return FundedMintAnchorPsbt{
+		FundedPsbt:          fundedPsbt,
+		AssetAnchorOutIdx:   anchorOutIndexes.AssetAnchorOutIdx,
+		PreCommitmentOutput: preCommitOut,
+	}, nil
+}
+
+// Copy creates a deep copy of FundedMintAnchorPsbt.
+func (f *FundedMintAnchorPsbt) Copy() *FundedMintAnchorPsbt {
+	newMintAnchorPsbt := &FundedMintAnchorPsbt{
+		FundedPsbt: tapsend.FundedPsbt{
+			ChangeOutputIndex: f.ChangeOutputIndex,
+			ChainFees:         f.ChainFees,
+			LockedUTXOs:       fn.CopySlice(f.LockedUTXOs),
+		},
+		AssetAnchorOutIdx:   f.AssetAnchorOutIdx,
+		PreCommitmentOutput: f.PreCommitmentOutput,
+	}
+
+	if f.Pkt != nil {
+		newMintAnchorPsbt.Pkt = &psbt.Packet{
+			UnsignedTx: f.Pkt.UnsignedTx.Copy(),
+			Inputs:     fn.CopySlice(f.Pkt.Inputs),
+			Outputs:    fn.CopySlice(f.Pkt.Outputs),
+			Unknowns:   fn.CopySlice(f.Pkt.Unknowns),
+		}
+	}
+
+	return newMintAnchorPsbt
+}
diff --git a/tapgarden/seedling.go b/tapgarden/seedling.go
index 82f295f0..214e742f 100644
--- a/tapgarden/seedling.go
+++ b/tapgarden/seedling.go
@@ -92,6 +92,20 @@ type Seedling struct {
 	// for this asset meaning future assets linked to it can be created.
 	EnableEmission bool
 
+	// UniverseCommitments indicates whether the minting event which
+	// will be associated with the seedling supports universe commitments.
+	// If set to true, the seedling can only be included in a minting batch
+	// where all assets share the same asset group key, which must be
+	// specified.
+	//
+	// Universe commitments are minter-controlled, on-chain anchored
+	// attestations regarding the state of the universe.
+	UniverseCommitments bool
+
+	// DelegationKey is the public key that is used to verify universe
+	// commitment related on-chain outputs and proofs.
+	DelegationKey fn.Option[keychain.KeyDescriptor]
+
 	// GroupAnchor is the name of another seedling in the pending batch that
 	// will anchor an asset group. This seedling will be minted with the
 	// same group key as the anchor asset.
@@ -223,15 +237,16 @@ func validateAnchorMeta(seedlingMeta *proof.MetaReveal,
 		anchorUniverseCommitments = true
 	}
 
-	if seedlingUniverseCommitments != anchorUniverseCommitments {
-		return fmt.Errorf("seedling universe commitments flag does "+
-			"not match group anchor: %v, %v",
-			seedlingUniverseCommitments, anchorUniverseCommitments)
+	// If the anchor asset has universe commitment feature turned on, then
+	// the same must be true for the seedling.
+	if anchorUniverseCommitments && !seedlingUniverseCommitments {
+		return fmt.Errorf("seedling universe commitments flag is " +
+			"false but must be true since the group anchor's " +
+			"flag is true")
 	}
 
 	// For now, we simply require a delegation key to be set when universe
-	// commitments are turned on. In the future, we could allow this to be
-	// empty and the group internal key to be used for signing.
+	// commitments are turned on.
 	if seedlingUniverseCommitments && seedlingMeta.DelegationKey.IsNone() {
 		return fmt.Errorf("delegation key must be set for universe " +
 			"commitments flag")
diff --git a/taprpc/mintrpc/mint.pb.go b/taprpc/mintrpc/mint.pb.go
index 5e3cfb72..ac272dd8 100644
--- a/taprpc/mintrpc/mint.pb.go
+++ b/taprpc/mintrpc/mint.pb.go
@@ -374,6 +374,18 @@ type MintAsset struct {
 	//
 	// If this field is set then group_internal_key must be unset, and vice versa.
 	ExternalGroupKey *taprpc.ExternalKey `protobuf:"bytes,14,opt,name=external_group_key,json=externalGroupKey,proto3" json:"external_group_key,omitempty"`
+	// Enabling this flag allows the creation of universe commitments for a new
+	// asset group.
+	//
+	// Universe commitments are minter-controlled, on-chain attestations that
+	// anchor and verify the state of an asset group.
+	//
+	// This flag imposes restrictions on the minting process by limiting the batch
+	// to assets within the same universe, meaning they share the same group key.
+	// This option is applicable only for the creation of a new asset group
+	// (i.e., the first minting tranche of an asset group) and ensures that the
+	// batch is limited to a single asset group.
+	UniverseCommitments bool `protobuf:"varint,15,opt,name=universe_commitments,json=universeCommitments,proto3" json:"universe_commitments,omitempty"`
 }
 
 func (x *MintAsset) Reset() {
@@ -506,6 +518,13 @@ func (x *MintAsset) GetExternalGroupKey() *taprpc.ExternalKey {
 	return nil
 }
 
+func (x *MintAsset) GetUniverseCommitments() bool {
+	if x != nil {
+		return x.UniverseCommitments
+	}
+	return false
+}
+
 type MintAssetRequest struct {
 	state         protoimpl.MessageState
 	sizeCache     protoimpl.SizeCache
@@ -1617,7 +1636,7 @@ var file_mintrpc_mint_proto_rawDesc = []byte{
 	0x56, 0x69, 0x72, 0x74, 0x75, 0x61, 0x6c, 0x54, 0x78, 0x12, 0x2c, 0x0a, 0x12, 0x67, 0x72, 0x6f,
 	0x75, 0x70, 0x5f, 0x76, 0x69, 0x72, 0x74, 0x75, 0x61, 0x6c, 0x5f, 0x70, 0x73, 0x62, 0x74, 0x18,
 	0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x56, 0x69, 0x72, 0x74,
-	0x75, 0x61, 0x6c, 0x50, 0x73, 0x62, 0x74, 0x22, 0xfc, 0x04, 0x0a, 0x09, 0x4d, 0x69, 0x6e, 0x74,
+	0x75, 0x61, 0x6c, 0x50, 0x73, 0x62, 0x74, 0x22, 0xaf, 0x05, 0x0a, 0x09, 0x4d, 0x69, 0x6e, 0x74,
 	0x41, 0x73, 0x73, 0x65, 0x74, 0x12, 0x39, 0x0a, 0x0d, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x76,
 	0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x14, 0x2e, 0x74,
 	0x61, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x73, 0x73, 0x65, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69,
@@ -1657,177 +1676,181 @@ var file_mintrpc_mint_proto_rawDesc = []byte{
 	0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x0b, 0x32,
 	0x13, 0x2e, 0x74, 0x61, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61,
 	0x6c, 0x4b, 0x65, 0x79, 0x52, 0x10, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x47, 0x72,
-	0x6f, 0x75, 0x70, 0x4b, 0x65, 0x79, 0x22, 0x63, 0x0a, 0x10, 0x4d, 0x69, 0x6e, 0x74, 0x41, 0x73,
-	0x73, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x28, 0x0a, 0x05, 0x61, 0x73,
-	0x73, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6d, 0x69, 0x6e, 0x74,
-	0x72, 0x70, 0x63, 0x2e, 0x4d, 0x69, 0x6e, 0x74, 0x41, 0x73, 0x73, 0x65, 0x74, 0x52, 0x05, 0x61,
-	0x73, 0x73, 0x65, 0x74, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x68, 0x6f, 0x72, 0x74, 0x5f, 0x72, 0x65,
-	0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x73, 0x68,
-	0x6f, 0x72, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x4f, 0x0a, 0x11, 0x4d,
-	0x69, 0x6e, 0x74, 0x41, 0x73, 0x73, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
-	0x12, 0x3a, 0x0a, 0x0d, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x62, 0x61, 0x74, 0x63,
-	0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x69, 0x6e, 0x74, 0x72, 0x70,
-	0x63, 0x2e, 0x4d, 0x69, 0x6e, 0x74, 0x69, 0x6e, 0x67, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x0c,
-	0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x42, 0x61, 0x74, 0x63, 0x68, 0x22, 0x83, 0x02, 0x0a,
-	0x0c, 0x4d, 0x69, 0x6e, 0x74, 0x69, 0x6e, 0x67, 0x42, 0x61, 0x74, 0x63, 0x68, 0x12, 0x1b, 0x0a,
-	0x09, 0x62, 0x61, 0x74, 0x63, 0x68, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c,
-	0x52, 0x08, 0x62, 0x61, 0x74, 0x63, 0x68, 0x4b, 0x65, 0x79, 0x12, 0x1d, 0x0a, 0x0a, 0x62, 0x61,
-	0x74, 0x63, 0x68, 0x5f, 0x74, 0x78, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09,
-	0x62, 0x61, 0x74, 0x63, 0x68, 0x54, 0x78, 0x69, 0x64, 0x12, 0x29, 0x0a, 0x05, 0x73, 0x74, 0x61,
-	0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x13, 0x2e, 0x6d, 0x69, 0x6e, 0x74, 0x72,
-	0x70, 0x63, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x73,
-	0x74, 0x61, 0x74, 0x65, 0x12, 0x2d, 0x0a, 0x06, 0x61, 0x73, 0x73, 0x65, 0x74, 0x73, 0x18, 0x04,
-	0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x69, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x50,
-	0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x41, 0x73, 0x73, 0x65, 0x74, 0x52, 0x06, 0x61, 0x73, 0x73,
-	0x65, 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61,
-	0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64,
-	0x41, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x5f, 0x68, 0x69, 0x6e,
-	0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x48,
-	0x69, 0x6e, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x62, 0x61, 0x74, 0x63, 0x68, 0x5f, 0x70, 0x73, 0x62,
-	0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x62, 0x61, 0x74, 0x63, 0x68, 0x50, 0x73,
-	0x62, 0x74, 0x22, 0x7c, 0x0a, 0x0c, 0x56, 0x65, 0x72, 0x62, 0x6f, 0x73, 0x65, 0x42, 0x61, 0x74,
-	0x63, 0x68, 0x12, 0x2b, 0x0a, 0x05, 0x62, 0x61, 0x74, 0x63, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28,
-	0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x69, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4d, 0x69, 0x6e, 0x74,
-	0x69, 0x6e, 0x67, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x05, 0x62, 0x61, 0x74, 0x63, 0x68, 0x12,
-	0x3f, 0x0a, 0x0f, 0x75, 0x6e, 0x73, 0x65, 0x61, 0x6c, 0x65, 0x64, 0x5f, 0x61, 0x73, 0x73, 0x65,
-	0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x69, 0x6e, 0x74, 0x72,
-	0x70, 0x63, 0x2e, 0x55, 0x6e, 0x73, 0x65, 0x61, 0x6c, 0x65, 0x64, 0x41, 0x73, 0x73, 0x65, 0x74,
-	0x52, 0x0e, 0x75, 0x6e, 0x73, 0x65, 0x61, 0x6c, 0x65, 0x64, 0x41, 0x73, 0x73, 0x65, 0x74, 0x73,
-	0x22, 0xcc, 0x01, 0x0a, 0x10, 0x46, 0x75, 0x6e, 0x64, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65,
-	0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x68, 0x6f, 0x72, 0x74, 0x5f, 0x72,
-	0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x73,
-	0x68, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x19, 0x0a, 0x08,
-	0x66, 0x65, 0x65, 0x5f, 0x72, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07,
-	0x66, 0x65, 0x65, 0x52, 0x61, 0x74, 0x65, 0x12, 0x38, 0x0a, 0x09, 0x66, 0x75, 0x6c, 0x6c, 0x5f,
-	0x74, 0x72, 0x65, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x74, 0x61, 0x70,
-	0x72, 0x70, 0x63, 0x2e, 0x54, 0x61, 0x70, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x46, 0x75, 0x6c,
-	0x6c, 0x54, 0x72, 0x65, 0x65, 0x48, 0x00, 0x52, 0x08, 0x66, 0x75, 0x6c, 0x6c, 0x54, 0x72, 0x65,
-	0x65, 0x12, 0x2b, 0x0a, 0x06, 0x62, 0x72, 0x61, 0x6e, 0x63, 0x68, 0x18, 0x04, 0x20, 0x01, 0x28,
-	0x0b, 0x32, 0x11, 0x2e, 0x74, 0x61, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x54, 0x61, 0x70, 0x42, 0x72,
-	0x61, 0x6e, 0x63, 0x68, 0x48, 0x00, 0x52, 0x06, 0x62, 0x72, 0x61, 0x6e, 0x63, 0x68, 0x42, 0x0f,
-	0x0a, 0x0d, 0x62, 0x61, 0x74, 0x63, 0x68, 0x5f, 0x73, 0x69, 0x62, 0x6c, 0x69, 0x6e, 0x67, 0x22,
-	0x40, 0x0a, 0x11, 0x46, 0x75, 0x6e, 0x64, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70,
-	0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x05, 0x62, 0x61, 0x74, 0x63, 0x68, 0x18, 0x01, 0x20,
-	0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x69, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x56, 0x65,
-	0x72, 0x62, 0x6f, 0x73, 0x65, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x05, 0x62, 0x61, 0x74, 0x63,
-	0x68, 0x22, 0xb5, 0x01, 0x0a, 0x10, 0x53, 0x65, 0x61, 0x6c, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52,
-	0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x68, 0x6f, 0x72, 0x74, 0x5f,
-	0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d,
-	0x73, 0x68, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3d, 0x0a,
-	0x0f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x77, 0x69, 0x74, 0x6e, 0x65, 0x73, 0x73, 0x65, 0x73,
-	0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x74, 0x61, 0x70, 0x72, 0x70, 0x63, 0x2e,
-	0x47, 0x72, 0x6f, 0x75, 0x70, 0x57, 0x69, 0x74, 0x6e, 0x65, 0x73, 0x73, 0x52, 0x0e, 0x67, 0x72,
-	0x6f, 0x75, 0x70, 0x57, 0x69, 0x74, 0x6e, 0x65, 0x73, 0x73, 0x65, 0x73, 0x12, 0x3b, 0x0a, 0x1a,
-	0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x76, 0x69, 0x72,
-	0x74, 0x75, 0x61, 0x6c, 0x5f, 0x70, 0x73, 0x62, 0x74, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09,
-	0x52, 0x17, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x56, 0x69, 0x72,
-	0x74, 0x75, 0x61, 0x6c, 0x50, 0x73, 0x62, 0x74, 0x73, 0x22, 0x40, 0x0a, 0x11, 0x53, 0x65, 0x61,
-	0x6c, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b,
-	0x0a, 0x05, 0x62, 0x61, 0x74, 0x63, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e,
-	0x6d, 0x69, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4d, 0x69, 0x6e, 0x74, 0x69, 0x6e, 0x67, 0x42,
-	0x61, 0x74, 0x63, 0x68, 0x52, 0x05, 0x62, 0x61, 0x74, 0x63, 0x68, 0x22, 0xd0, 0x01, 0x0a, 0x14,
-	0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71,
-	0x75, 0x65, 0x73, 0x74, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x68, 0x6f, 0x72, 0x74, 0x5f, 0x72, 0x65,
-	0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x73, 0x68,
-	0x6f, 0x72, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x66,
-	0x65, 0x65, 0x5f, 0x72, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x66,
-	0x65, 0x65, 0x52, 0x61, 0x74, 0x65, 0x12, 0x38, 0x0a, 0x09, 0x66, 0x75, 0x6c, 0x6c, 0x5f, 0x74,
-	0x72, 0x65, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x74, 0x61, 0x70, 0x72,
-	0x70, 0x63, 0x2e, 0x54, 0x61, 0x70, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x46, 0x75, 0x6c, 0x6c,
-	0x54, 0x72, 0x65, 0x65, 0x48, 0x00, 0x52, 0x08, 0x66, 0x75, 0x6c, 0x6c, 0x54, 0x72, 0x65, 0x65,
-	0x12, 0x2b, 0x0a, 0x06, 0x62, 0x72, 0x61, 0x6e, 0x63, 0x68, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b,
-	0x32, 0x11, 0x2e, 0x74, 0x61, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x54, 0x61, 0x70, 0x42, 0x72, 0x61,
-	0x6e, 0x63, 0x68, 0x48, 0x00, 0x52, 0x06, 0x62, 0x72, 0x61, 0x6e, 0x63, 0x68, 0x42, 0x0f, 0x0a,
-	0x0d, 0x62, 0x61, 0x74, 0x63, 0x68, 0x5f, 0x73, 0x69, 0x62, 0x6c, 0x69, 0x6e, 0x67, 0x22, 0x44,
-	0x0a, 0x15, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52,
+	0x6f, 0x75, 0x70, 0x4b, 0x65, 0x79, 0x12, 0x31, 0x0a, 0x14, 0x75, 0x6e, 0x69, 0x76, 0x65, 0x72,
+	0x73, 0x65, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x0f,
+	0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x75, 0x6e, 0x69, 0x76, 0x65, 0x72, 0x73, 0x65, 0x43, 0x6f,
+	0x6d, 0x6d, 0x69, 0x74, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x22, 0x63, 0x0a, 0x10, 0x4d, 0x69, 0x6e,
+	0x74, 0x41, 0x73, 0x73, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x28, 0x0a,
+	0x05, 0x61, 0x73, 0x73, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6d,
+	0x69, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4d, 0x69, 0x6e, 0x74, 0x41, 0x73, 0x73, 0x65, 0x74,
+	0x52, 0x05, 0x61, 0x73, 0x73, 0x65, 0x74, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x68, 0x6f, 0x72, 0x74,
+	0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52,
+	0x0d, 0x73, 0x68, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x4f,
+	0x0a, 0x11, 0x4d, 0x69, 0x6e, 0x74, 0x41, 0x73, 0x73, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f,
+	0x6e, 0x73, 0x65, 0x12, 0x3a, 0x0a, 0x0d, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x62,
+	0x61, 0x74, 0x63, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x69, 0x6e,
+	0x74, 0x72, 0x70, 0x63, 0x2e, 0x4d, 0x69, 0x6e, 0x74, 0x69, 0x6e, 0x67, 0x42, 0x61, 0x74, 0x63,
+	0x68, 0x52, 0x0c, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x42, 0x61, 0x74, 0x63, 0x68, 0x22,
+	0x83, 0x02, 0x0a, 0x0c, 0x4d, 0x69, 0x6e, 0x74, 0x69, 0x6e, 0x67, 0x42, 0x61, 0x74, 0x63, 0x68,
+	0x12, 0x1b, 0x0a, 0x09, 0x62, 0x61, 0x74, 0x63, 0x68, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20,
+	0x01, 0x28, 0x0c, 0x52, 0x08, 0x62, 0x61, 0x74, 0x63, 0x68, 0x4b, 0x65, 0x79, 0x12, 0x1d, 0x0a,
+	0x0a, 0x62, 0x61, 0x74, 0x63, 0x68, 0x5f, 0x74, 0x78, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28,
+	0x09, 0x52, 0x09, 0x62, 0x61, 0x74, 0x63, 0x68, 0x54, 0x78, 0x69, 0x64, 0x12, 0x29, 0x0a, 0x05,
+	0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x13, 0x2e, 0x6d, 0x69,
+	0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x53, 0x74, 0x61, 0x74, 0x65,
+	0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x2d, 0x0a, 0x06, 0x61, 0x73, 0x73, 0x65, 0x74,
+	0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x69, 0x6e, 0x74, 0x72, 0x70,
+	0x63, 0x2e, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x41, 0x73, 0x73, 0x65, 0x74, 0x52, 0x06,
+	0x61, 0x73, 0x73, 0x65, 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65,
+	0x64, 0x5f, 0x61, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61,
+	0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x5f,
+	0x68, 0x69, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x68, 0x65, 0x69, 0x67,
+	0x68, 0x74, 0x48, 0x69, 0x6e, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x62, 0x61, 0x74, 0x63, 0x68, 0x5f,
+	0x70, 0x73, 0x62, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x62, 0x61, 0x74, 0x63,
+	0x68, 0x50, 0x73, 0x62, 0x74, 0x22, 0x7c, 0x0a, 0x0c, 0x56, 0x65, 0x72, 0x62, 0x6f, 0x73, 0x65,
+	0x42, 0x61, 0x74, 0x63, 0x68, 0x12, 0x2b, 0x0a, 0x05, 0x62, 0x61, 0x74, 0x63, 0x68, 0x18, 0x01,
+	0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x69, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4d,
+	0x69, 0x6e, 0x74, 0x69, 0x6e, 0x67, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x05, 0x62, 0x61, 0x74,
+	0x63, 0x68, 0x12, 0x3f, 0x0a, 0x0f, 0x75, 0x6e, 0x73, 0x65, 0x61, 0x6c, 0x65, 0x64, 0x5f, 0x61,
+	0x73, 0x73, 0x65, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x69,
+	0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x55, 0x6e, 0x73, 0x65, 0x61, 0x6c, 0x65, 0x64, 0x41, 0x73,
+	0x73, 0x65, 0x74, 0x52, 0x0e, 0x75, 0x6e, 0x73, 0x65, 0x61, 0x6c, 0x65, 0x64, 0x41, 0x73, 0x73,
+	0x65, 0x74, 0x73, 0x22, 0xcc, 0x01, 0x0a, 0x10, 0x46, 0x75, 0x6e, 0x64, 0x42, 0x61, 0x74, 0x63,
+	0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x68, 0x6f, 0x72,
+	0x74, 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08,
+	0x52, 0x0d, 0x73, 0x68, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
+	0x19, 0x0a, 0x08, 0x66, 0x65, 0x65, 0x5f, 0x72, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28,
+	0x0d, 0x52, 0x07, 0x66, 0x65, 0x65, 0x52, 0x61, 0x74, 0x65, 0x12, 0x38, 0x0a, 0x09, 0x66, 0x75,
+	0x6c, 0x6c, 0x5f, 0x74, 0x72, 0x65, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e,
+	0x74, 0x61, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x54, 0x61, 0x70, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74,
+	0x46, 0x75, 0x6c, 0x6c, 0x54, 0x72, 0x65, 0x65, 0x48, 0x00, 0x52, 0x08, 0x66, 0x75, 0x6c, 0x6c,
+	0x54, 0x72, 0x65, 0x65, 0x12, 0x2b, 0x0a, 0x06, 0x62, 0x72, 0x61, 0x6e, 0x63, 0x68, 0x18, 0x04,
+	0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x74, 0x61, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x54, 0x61,
+	0x70, 0x42, 0x72, 0x61, 0x6e, 0x63, 0x68, 0x48, 0x00, 0x52, 0x06, 0x62, 0x72, 0x61, 0x6e, 0x63,
+	0x68, 0x42, 0x0f, 0x0a, 0x0d, 0x62, 0x61, 0x74, 0x63, 0x68, 0x5f, 0x73, 0x69, 0x62, 0x6c, 0x69,
+	0x6e, 0x67, 0x22, 0x40, 0x0a, 0x11, 0x46, 0x75, 0x6e, 0x64, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52,
 	0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x05, 0x62, 0x61, 0x74, 0x63, 0x68,
 	0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x69, 0x6e, 0x74, 0x72, 0x70, 0x63,
-	0x2e, 0x4d, 0x69, 0x6e, 0x74, 0x69, 0x6e, 0x67, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x05, 0x62,
-	0x61, 0x74, 0x63, 0x68, 0x22, 0x14, 0x0a, 0x12, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x42, 0x61,
-	0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x32, 0x0a, 0x13, 0x43, 0x61,
-	0x6e, 0x63, 0x65, 0x6c, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
-	0x65, 0x12, 0x1b, 0x0a, 0x09, 0x62, 0x61, 0x74, 0x63, 0x68, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01,
-	0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x62, 0x61, 0x74, 0x63, 0x68, 0x4b, 0x65, 0x79, 0x22, 0x7b,
-	0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65,
-	0x73, 0x74, 0x12, 0x1d, 0x0a, 0x09, 0x62, 0x61, 0x74, 0x63, 0x68, 0x5f, 0x6b, 0x65, 0x79, 0x18,
-	0x01, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x00, 0x52, 0x08, 0x62, 0x61, 0x74, 0x63, 0x68, 0x4b, 0x65,
-	0x79, 0x12, 0x24, 0x0a, 0x0d, 0x62, 0x61, 0x74, 0x63, 0x68, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x73,
-	0x74, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0b, 0x62, 0x61, 0x74, 0x63,
-	0x68, 0x4b, 0x65, 0x79, 0x53, 0x74, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x62, 0x6f,
-	0x73, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x76, 0x65, 0x72, 0x62, 0x6f, 0x73,
-	0x65, 0x42, 0x08, 0x0a, 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x22, 0x44, 0x0a, 0x11, 0x4c,
-	0x69, 0x73, 0x74, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
-	0x12, 0x2f, 0x0a, 0x07, 0x62, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28,
-	0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x69, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x56, 0x65, 0x72, 0x62,
-	0x6f, 0x73, 0x65, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x07, 0x62, 0x61, 0x74, 0x63, 0x68, 0x65,
-	0x73, 0x22, 0x43, 0x0a, 0x1a, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x4d, 0x69,
-	0x6e, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
-	0x25, 0x0a, 0x0e, 0x73, 0x68, 0x6f, 0x72, 0x74, 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
-	0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x73, 0x68, 0x6f, 0x72, 0x74, 0x52, 0x65,
-	0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xa2, 0x01, 0x0a, 0x09, 0x4d, 0x69, 0x6e, 0x74, 0x45,
-	0x76, 0x65, 0x6e, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d,
-	0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61,
-	0x6d, 0x70, 0x12, 0x34, 0x0a, 0x0b, 0x62, 0x61, 0x74, 0x63, 0x68, 0x5f, 0x73, 0x74, 0x61, 0x74,
-	0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x13, 0x2e, 0x6d, 0x69, 0x6e, 0x74, 0x72, 0x70,
-	0x63, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0a, 0x62, 0x61,
-	0x74, 0x63, 0x68, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x2b, 0x0a, 0x05, 0x62, 0x61, 0x74, 0x63,
-	0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x69, 0x6e, 0x74, 0x72, 0x70,
-	0x63, 0x2e, 0x4d, 0x69, 0x6e, 0x74, 0x69, 0x6e, 0x67, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x05,
-	0x62, 0x61, 0x74, 0x63, 0x68, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x04,
-	0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x2a, 0x88, 0x02, 0x0a, 0x0a,
-	0x42, 0x61, 0x74, 0x63, 0x68, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x17, 0x0a, 0x13, 0x42, 0x41,
-	0x54, 0x43, 0x48, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57,
-	0x4e, 0x10, 0x00, 0x12, 0x17, 0x0a, 0x13, 0x42, 0x41, 0x54, 0x43, 0x48, 0x5f, 0x53, 0x54, 0x41,
-	0x54, 0x45, 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x16, 0x0a, 0x12,
-	0x42, 0x41, 0x54, 0x43, 0x48, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x46, 0x52, 0x4f, 0x5a,
-	0x45, 0x4e, 0x10, 0x02, 0x12, 0x19, 0x0a, 0x15, 0x42, 0x41, 0x54, 0x43, 0x48, 0x5f, 0x53, 0x54,
-	0x41, 0x54, 0x45, 0x5f, 0x43, 0x4f, 0x4d, 0x4d, 0x49, 0x54, 0x54, 0x45, 0x44, 0x10, 0x03, 0x12,
-	0x19, 0x0a, 0x15, 0x42, 0x41, 0x54, 0x43, 0x48, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x42,
-	0x52, 0x4f, 0x41, 0x44, 0x43, 0x41, 0x53, 0x54, 0x10, 0x04, 0x12, 0x19, 0x0a, 0x15, 0x42, 0x41,
-	0x54, 0x43, 0x48, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x49, 0x52,
-	0x4d, 0x45, 0x44, 0x10, 0x05, 0x12, 0x19, 0x0a, 0x15, 0x42, 0x41, 0x54, 0x43, 0x48, 0x5f, 0x53,
-	0x54, 0x41, 0x54, 0x45, 0x5f, 0x46, 0x49, 0x4e, 0x41, 0x4c, 0x49, 0x5a, 0x45, 0x44, 0x10, 0x06,
-	0x12, 0x22, 0x0a, 0x1e, 0x42, 0x41, 0x54, 0x43, 0x48, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f,
-	0x53, 0x45, 0x45, 0x44, 0x4c, 0x49, 0x4e, 0x47, 0x5f, 0x43, 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x4c,
-	0x45, 0x44, 0x10, 0x07, 0x12, 0x20, 0x0a, 0x1c, 0x42, 0x41, 0x54, 0x43, 0x48, 0x5f, 0x53, 0x54,
-	0x41, 0x54, 0x45, 0x5f, 0x53, 0x50, 0x52, 0x4f, 0x55, 0x54, 0x5f, 0x43, 0x41, 0x4e, 0x43, 0x45,
-	0x4c, 0x4c, 0x45, 0x44, 0x10, 0x08, 0x32, 0x84, 0x04, 0x0a, 0x04, 0x4d, 0x69, 0x6e, 0x74, 0x12,
-	0x42, 0x0a, 0x09, 0x4d, 0x69, 0x6e, 0x74, 0x41, 0x73, 0x73, 0x65, 0x74, 0x12, 0x19, 0x2e, 0x6d,
-	0x69, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4d, 0x69, 0x6e, 0x74, 0x41, 0x73, 0x73, 0x65, 0x74,
-	0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x6d, 0x69, 0x6e, 0x74, 0x72, 0x70,
-	0x63, 0x2e, 0x4d, 0x69, 0x6e, 0x74, 0x41, 0x73, 0x73, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f,
-	0x6e, 0x73, 0x65, 0x12, 0x42, 0x0a, 0x09, 0x46, 0x75, 0x6e, 0x64, 0x42, 0x61, 0x74, 0x63, 0x68,
-	0x12, 0x19, 0x2e, 0x6d, 0x69, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x75, 0x6e, 0x64, 0x42,
-	0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x6d, 0x69,
-	0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x75, 0x6e, 0x64, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52,
-	0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x42, 0x0a, 0x09, 0x53, 0x65, 0x61, 0x6c, 0x42,
-	0x61, 0x74, 0x63, 0x68, 0x12, 0x19, 0x2e, 0x6d, 0x69, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x53,
-	0x65, 0x61, 0x6c, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
-	0x1a, 0x2e, 0x6d, 0x69, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x61, 0x6c, 0x42, 0x61,
-	0x74, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4e, 0x0a, 0x0d, 0x46,
-	0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x42, 0x61, 0x74, 0x63, 0x68, 0x12, 0x1d, 0x2e, 0x6d,
-	0x69, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x42,
-	0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x6d, 0x69,
-	0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x42, 0x61,
-	0x74, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x48, 0x0a, 0x0b, 0x43,
-	0x61, 0x6e, 0x63, 0x65, 0x6c, 0x42, 0x61, 0x74, 0x63, 0x68, 0x12, 0x1b, 0x2e, 0x6d, 0x69, 0x6e,
+	0x2e, 0x56, 0x65, 0x72, 0x62, 0x6f, 0x73, 0x65, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x05, 0x62,
+	0x61, 0x74, 0x63, 0x68, 0x22, 0xb5, 0x01, 0x0a, 0x10, 0x53, 0x65, 0x61, 0x6c, 0x42, 0x61, 0x74,
+	0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x68, 0x6f,
+	0x72, 0x74, 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
+	0x08, 0x52, 0x0d, 0x73, 0x68, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
+	0x12, 0x3d, 0x0a, 0x0f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x77, 0x69, 0x74, 0x6e, 0x65, 0x73,
+	0x73, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x74, 0x61, 0x70, 0x72,
+	0x70, 0x63, 0x2e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x57, 0x69, 0x74, 0x6e, 0x65, 0x73, 0x73, 0x52,
+	0x0e, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x57, 0x69, 0x74, 0x6e, 0x65, 0x73, 0x73, 0x65, 0x73, 0x12,
+	0x3b, 0x0a, 0x1a, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f,
+	0x76, 0x69, 0x72, 0x74, 0x75, 0x61, 0x6c, 0x5f, 0x70, 0x73, 0x62, 0x74, 0x73, 0x18, 0x03, 0x20,
+	0x03, 0x28, 0x09, 0x52, 0x17, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x47, 0x72, 0x6f, 0x75, 0x70,
+	0x56, 0x69, 0x72, 0x74, 0x75, 0x61, 0x6c, 0x50, 0x73, 0x62, 0x74, 0x73, 0x22, 0x40, 0x0a, 0x11,
+	0x53, 0x65, 0x61, 0x6c, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
+	0x65, 0x12, 0x2b, 0x0a, 0x05, 0x62, 0x61, 0x74, 0x63, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b,
+	0x32, 0x15, 0x2e, 0x6d, 0x69, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4d, 0x69, 0x6e, 0x74, 0x69,
+	0x6e, 0x67, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x05, 0x62, 0x61, 0x74, 0x63, 0x68, 0x22, 0xd0,
+	0x01, 0x0a, 0x14, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x42, 0x61, 0x74, 0x63, 0x68,
+	0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x68, 0x6f, 0x72, 0x74,
+	0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52,
+	0x0d, 0x73, 0x68, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x19,
+	0x0a, 0x08, 0x66, 0x65, 0x65, 0x5f, 0x72, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d,
+	0x52, 0x07, 0x66, 0x65, 0x65, 0x52, 0x61, 0x74, 0x65, 0x12, 0x38, 0x0a, 0x09, 0x66, 0x75, 0x6c,
+	0x6c, 0x5f, 0x74, 0x72, 0x65, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x74,
+	0x61, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x54, 0x61, 0x70, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x46,
+	0x75, 0x6c, 0x6c, 0x54, 0x72, 0x65, 0x65, 0x48, 0x00, 0x52, 0x08, 0x66, 0x75, 0x6c, 0x6c, 0x54,
+	0x72, 0x65, 0x65, 0x12, 0x2b, 0x0a, 0x06, 0x62, 0x72, 0x61, 0x6e, 0x63, 0x68, 0x18, 0x04, 0x20,
+	0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x74, 0x61, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x54, 0x61, 0x70,
+	0x42, 0x72, 0x61, 0x6e, 0x63, 0x68, 0x48, 0x00, 0x52, 0x06, 0x62, 0x72, 0x61, 0x6e, 0x63, 0x68,
+	0x42, 0x0f, 0x0a, 0x0d, 0x62, 0x61, 0x74, 0x63, 0x68, 0x5f, 0x73, 0x69, 0x62, 0x6c, 0x69, 0x6e,
+	0x67, 0x22, 0x44, 0x0a, 0x15, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x42, 0x61, 0x74,
+	0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x05, 0x62, 0x61,
+	0x74, 0x63, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x69, 0x6e, 0x74,
+	0x72, 0x70, 0x63, 0x2e, 0x4d, 0x69, 0x6e, 0x74, 0x69, 0x6e, 0x67, 0x42, 0x61, 0x74, 0x63, 0x68,
+	0x52, 0x05, 0x62, 0x61, 0x74, 0x63, 0x68, 0x22, 0x14, 0x0a, 0x12, 0x43, 0x61, 0x6e, 0x63, 0x65,
+	0x6c, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x32, 0x0a,
+	0x13, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70,
+	0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x62, 0x61, 0x74, 0x63, 0x68, 0x5f, 0x6b, 0x65,
+	0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x62, 0x61, 0x74, 0x63, 0x68, 0x4b, 0x65,
+	0x79, 0x22, 0x7b, 0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65,
+	0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x09, 0x62, 0x61, 0x74, 0x63, 0x68, 0x5f, 0x6b,
+	0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x00, 0x52, 0x08, 0x62, 0x61, 0x74, 0x63,
+	0x68, 0x4b, 0x65, 0x79, 0x12, 0x24, 0x0a, 0x0d, 0x62, 0x61, 0x74, 0x63, 0x68, 0x5f, 0x6b, 0x65,
+	0x79, 0x5f, 0x73, 0x74, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0b, 0x62,
+	0x61, 0x74, 0x63, 0x68, 0x4b, 0x65, 0x79, 0x53, 0x74, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65,
+	0x72, 0x62, 0x6f, 0x73, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x76, 0x65, 0x72,
+	0x62, 0x6f, 0x73, 0x65, 0x42, 0x08, 0x0a, 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x22, 0x44,
+	0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f,
+	0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x07, 0x62, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x18, 0x01,
+	0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x69, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x56,
+	0x65, 0x72, 0x62, 0x6f, 0x73, 0x65, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x07, 0x62, 0x61, 0x74,
+	0x63, 0x68, 0x65, 0x73, 0x22, 0x43, 0x0a, 0x1a, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62,
+	0x65, 0x4d, 0x69, 0x6e, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65,
+	0x73, 0x74, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x68, 0x6f, 0x72, 0x74, 0x5f, 0x72, 0x65, 0x73, 0x70,
+	0x6f, 0x6e, 0x73, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x73, 0x68, 0x6f, 0x72,
+	0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xa2, 0x01, 0x0a, 0x09, 0x4d, 0x69,
+	0x6e, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73,
+	0x74, 0x61, 0x6d, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65,
+	0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x34, 0x0a, 0x0b, 0x62, 0x61, 0x74, 0x63, 0x68, 0x5f, 0x73,
+	0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x13, 0x2e, 0x6d, 0x69, 0x6e,
+	0x74, 0x72, 0x70, 0x63, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52,
+	0x0a, 0x62, 0x61, 0x74, 0x63, 0x68, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x2b, 0x0a, 0x05, 0x62,
+	0x61, 0x74, 0x63, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x69, 0x6e,
+	0x74, 0x72, 0x70, 0x63, 0x2e, 0x4d, 0x69, 0x6e, 0x74, 0x69, 0x6e, 0x67, 0x42, 0x61, 0x74, 0x63,
+	0x68, 0x52, 0x05, 0x62, 0x61, 0x74, 0x63, 0x68, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f,
+	0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x2a, 0x88,
+	0x02, 0x0a, 0x0a, 0x42, 0x61, 0x74, 0x63, 0x68, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x17, 0x0a,
+	0x13, 0x42, 0x41, 0x54, 0x43, 0x48, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x55, 0x4e, 0x4b,
+	0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x17, 0x0a, 0x13, 0x42, 0x41, 0x54, 0x43, 0x48, 0x5f,
+	0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12,
+	0x16, 0x0a, 0x12, 0x42, 0x41, 0x54, 0x43, 0x48, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x46,
+	0x52, 0x4f, 0x5a, 0x45, 0x4e, 0x10, 0x02, 0x12, 0x19, 0x0a, 0x15, 0x42, 0x41, 0x54, 0x43, 0x48,
+	0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x43, 0x4f, 0x4d, 0x4d, 0x49, 0x54, 0x54, 0x45, 0x44,
+	0x10, 0x03, 0x12, 0x19, 0x0a, 0x15, 0x42, 0x41, 0x54, 0x43, 0x48, 0x5f, 0x53, 0x54, 0x41, 0x54,
+	0x45, 0x5f, 0x42, 0x52, 0x4f, 0x41, 0x44, 0x43, 0x41, 0x53, 0x54, 0x10, 0x04, 0x12, 0x19, 0x0a,
+	0x15, 0x42, 0x41, 0x54, 0x43, 0x48, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x43, 0x4f, 0x4e,
+	0x46, 0x49, 0x52, 0x4d, 0x45, 0x44, 0x10, 0x05, 0x12, 0x19, 0x0a, 0x15, 0x42, 0x41, 0x54, 0x43,
+	0x48, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x46, 0x49, 0x4e, 0x41, 0x4c, 0x49, 0x5a, 0x45,
+	0x44, 0x10, 0x06, 0x12, 0x22, 0x0a, 0x1e, 0x42, 0x41, 0x54, 0x43, 0x48, 0x5f, 0x53, 0x54, 0x41,
+	0x54, 0x45, 0x5f, 0x53, 0x45, 0x45, 0x44, 0x4c, 0x49, 0x4e, 0x47, 0x5f, 0x43, 0x41, 0x4e, 0x43,
+	0x45, 0x4c, 0x4c, 0x45, 0x44, 0x10, 0x07, 0x12, 0x20, 0x0a, 0x1c, 0x42, 0x41, 0x54, 0x43, 0x48,
+	0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x53, 0x50, 0x52, 0x4f, 0x55, 0x54, 0x5f, 0x43, 0x41,
+	0x4e, 0x43, 0x45, 0x4c, 0x4c, 0x45, 0x44, 0x10, 0x08, 0x32, 0x84, 0x04, 0x0a, 0x04, 0x4d, 0x69,
+	0x6e, 0x74, 0x12, 0x42, 0x0a, 0x09, 0x4d, 0x69, 0x6e, 0x74, 0x41, 0x73, 0x73, 0x65, 0x74, 0x12,
+	0x19, 0x2e, 0x6d, 0x69, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4d, 0x69, 0x6e, 0x74, 0x41, 0x73,
+	0x73, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x6d, 0x69, 0x6e,
+	0x74, 0x72, 0x70, 0x63, 0x2e, 0x4d, 0x69, 0x6e, 0x74, 0x41, 0x73, 0x73, 0x65, 0x74, 0x52, 0x65,
+	0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x42, 0x0a, 0x09, 0x46, 0x75, 0x6e, 0x64, 0x42, 0x61,
+	0x74, 0x63, 0x68, 0x12, 0x19, 0x2e, 0x6d, 0x69, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x75,
+	0x6e, 0x64, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a,
+	0x2e, 0x6d, 0x69, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x75, 0x6e, 0x64, 0x42, 0x61, 0x74,
+	0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x42, 0x0a, 0x09, 0x53, 0x65,
+	0x61, 0x6c, 0x42, 0x61, 0x74, 0x63, 0x68, 0x12, 0x19, 0x2e, 0x6d, 0x69, 0x6e, 0x74, 0x72, 0x70,
+	0x63, 0x2e, 0x53, 0x65, 0x61, 0x6c, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65,
+	0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x6d, 0x69, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x61,
+	0x6c, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4e,
+	0x0a, 0x0d, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x42, 0x61, 0x74, 0x63, 0x68, 0x12,
+	0x1d, 0x2e, 0x6d, 0x69, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69,
+	0x7a, 0x65, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e,
+	0x2e, 0x6d, 0x69, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a,
+	0x65, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x48,
+	0x0a, 0x0b, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x42, 0x61, 0x74, 0x63, 0x68, 0x12, 0x1b, 0x2e,
+	0x6d, 0x69, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x42, 0x61,
+	0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x6d, 0x69, 0x6e,
 	0x74, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x42, 0x61, 0x74, 0x63, 0x68,
-	0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x6d, 0x69, 0x6e, 0x74, 0x72, 0x70,
-	0x63, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x73,
-	0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x42, 0x61, 0x74,
-	0x63, 0x68, 0x65, 0x73, 0x12, 0x19, 0x2e, 0x6d, 0x69, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4c,
-	0x69, 0x73, 0x74, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
-	0x1a, 0x2e, 0x6d, 0x69, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x42, 0x61,
-	0x74, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x50, 0x0a, 0x13, 0x53,
-	0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x4d, 0x69, 0x6e, 0x74, 0x45, 0x76, 0x65, 0x6e,
-	0x74, 0x73, 0x12, 0x23, 0x2e, 0x6d, 0x69, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x75, 0x62,
-	0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x4d, 0x69, 0x6e, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73,
-	0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x6d, 0x69, 0x6e, 0x74, 0x72, 0x70,
-	0x63, 0x2e, 0x4d, 0x69, 0x6e, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x30, 0x01, 0x42, 0x38, 0x5a,
-	0x36, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x69, 0x67, 0x68,
-	0x74, 0x6e, 0x69, 0x6e, 0x67, 0x6c, 0x61, 0x62, 0x73, 0x2f, 0x74, 0x61, 0x70, 0x72, 0x6f, 0x6f,
-	0x74, 0x2d, 0x61, 0x73, 0x73, 0x65, 0x74, 0x73, 0x2f, 0x74, 0x61, 0x70, 0x72, 0x70, 0x63, 0x2f,
-	0x6d, 0x69, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74,
+	0x42, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x12, 0x19, 0x2e, 0x6d, 0x69, 0x6e, 0x74, 0x72, 0x70,
+	0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65,
+	0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x6d, 0x69, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73,
+	0x74, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x50,
+	0x0a, 0x13, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x4d, 0x69, 0x6e, 0x74, 0x45,
+	0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x23, 0x2e, 0x6d, 0x69, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e,
+	0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x4d, 0x69, 0x6e, 0x74, 0x45, 0x76, 0x65,
+	0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x6d, 0x69, 0x6e,
+	0x74, 0x72, 0x70, 0x63, 0x2e, 0x4d, 0x69, 0x6e, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x30, 0x01,
+	0x42, 0x38, 0x5a, 0x36, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c,
+	0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x6c, 0x61, 0x62, 0x73, 0x2f, 0x74, 0x61, 0x70,
+	0x72, 0x6f, 0x6f, 0x74, 0x2d, 0x61, 0x73, 0x73, 0x65, 0x74, 0x73, 0x2f, 0x74, 0x61, 0x70, 0x72,
+	0x70, 0x63, 0x2f, 0x6d, 0x69, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
+	0x6f, 0x33,
 }
 
 var (
diff --git a/taprpc/mintrpc/mint.proto b/taprpc/mintrpc/mint.proto
index 50f96451..8190d132 100644
--- a/taprpc/mintrpc/mint.proto
+++ b/taprpc/mintrpc/mint.proto
@@ -223,6 +223,21 @@ message MintAsset {
     If this field is set then group_internal_key must be unset, and vice versa.
     */
     taprpc.ExternalKey external_group_key = 14;
+
+    /*
+    Enabling this flag allows the creation of universe commitments for a new
+    asset group.
+
+    Universe commitments are minter-controlled, on-chain attestations that
+    anchor and verify the state of an asset group.
+
+    This flag imposes restrictions on the minting process by limiting the batch
+    to assets within the same universe, meaning they share the same group key.
+    This option is applicable only for the creation of a new asset group
+    (i.e., the first minting tranche of an asset group) and ensures that the
+    batch is limited to a single asset group.
+    */
+    bool universe_commitments = 15;
 }
 
 message MintAssetRequest {
diff --git a/taprpc/mintrpc/mint.swagger.json b/taprpc/mintrpc/mint.swagger.json
index 7de73cfc..dd3f653e 100644
--- a/taprpc/mintrpc/mint.swagger.json
+++ b/taprpc/mintrpc/mint.swagger.json
@@ -436,6 +436,10 @@
         "external_group_key": {
           "$ref": "#/definitions/taprpcExternalKey",
           "description": "The external group key is an optional field that allows specifying an\nexternal signing key for the group virtual transaction during minting.\nThis key enables signing operations to be performed externally, outside\nthe daemon.\n\nIf this field is set then group_internal_key must be unset, and vice versa."
+        },
+        "universe_commitments": {
+          "type": "boolean",
+          "description": "Enabling this flag allows the creation of universe commitments for a new\nasset group.\n\nUniverse commitments are minter-controlled, on-chain attestations that\nanchor and verify the state of an asset group.\n\nThis flag imposes restrictions on the minting process by limiting the batch\nto assets within the same universe, meaning they share the same group key.\nThis option is applicable only for the creation of a new asset group\n(i.e., the first minting tranche of an asset group) and ensures that the\nbatch is limited to a single asset group."
         }
       }
     },



Test patch:
diff --git a/tapdb/asset_minting_test.go b/tapdb/asset_minting_test.go
index 8d84f4d4..acbedcad 100644
--- a/tapdb/asset_minting_test.go
+++ b/tapdb/asset_minting_test.go
@@ -88,7 +88,9 @@ func assertBatchEqual(t *testing.T, a, b *tapgarden.MintingBatch) {
 	require.Equal(t, a.TapSibling(), b.TapSibling())
 	require.Equal(t, a.BatchKey, b.BatchKey)
 	require.Equal(t, a.Seedlings, b.Seedlings)
-	assertPsbtEqual(t, a.GenesisPacket, b.GenesisPacket)
+	assertPsbtEqual(
+		t, &a.GenesisPacket.FundedPsbt, &b.GenesisPacket.FundedPsbt,
+	)
 	require.Equal(t, a.RootAssetCommitment, b.RootAssetCommitment)
 }
 
@@ -842,7 +844,10 @@ func TestAddSproutsToBatch(t *testing.T) {
 	// state.
 	assertSeedlingBatchLen(t, mintingBatches, 1, 0)
 	assertBatchState(t, mintingBatches[0], tapgarden.BatchStateCommitted)
-	assertPsbtEqual(t, genesisPacket, mintingBatches[0].GenesisPacket)
+	assertPsbtEqual(
+		t, &genesisPacket.FundedPsbt,
+		&mintingBatches[0].GenesisPacket.FundedPsbt,
+	)
 	assertAssetsEqual(t, assetRoot, mintingBatches[0].RootAssetCommitment)
 
 	// We also expect that for each of the assets we created above, we're
@@ -930,7 +935,7 @@ func addRandAssets(t *testing.T, ctx context.Context,
 		batchKey:        batchKey,
 		groupKey:        &group.GroupKey.GroupPubKey,
 		groupGenAmt:     genAmt,
-		genesisPkt:      genesisPacket,
+		genesisPkt:      &genesisPacket.FundedPsbt,
 		assetRoot:       assetRoot,
 		merkleRoot:      merkleRoot[:],
 		scriptRoot:      scriptRoot[:],
@@ -981,7 +986,8 @@ func TestCommitBatchChainActions(t *testing.T) {
 		t, mintingBatches[0], tapgarden.BatchStateBroadcast,
 	)
 	assertPsbtEqual(
-		t, randAssetCtx.genesisPkt, mintingBatches[0].GenesisPacket,
+		t, randAssetCtx.genesisPkt,
+		&mintingBatches[0].GenesisPacket.FundedPsbt,
 	)
 	assertBatchSibling(t, mintingBatches[0], randAssetCtx.tapSiblingHash)
 
@@ -1486,7 +1492,10 @@ func TestGroupAnchors(t *testing.T) {
 	// state.
 	assertSeedlingBatchLen(t, mintingBatches, 1, 0)
 	assertBatchState(t, mintingBatches[0], tapgarden.BatchStateCommitted)
-	assertPsbtEqual(t, genesisPacket, mintingBatches[0].GenesisPacket)
+	assertPsbtEqual(
+		t, &genesisPacket.FundedPsbt,
+		&mintingBatches[0].GenesisPacket.FundedPsbt,
+	)
 	assertAssetsEqual(t, assetRoot, mintingBatches[0].RootAssetCommitment)
 
 	// Check that the number of group anchors and members matches the batch
@@ -1761,6 +1770,144 @@ func TestTapscriptTreeManager(t *testing.T) {
 	loadTapscriptTreeChecked(t, ctx, assetStore, tree5, tree5Hash)
 }
 
+// storeMintAnchorUniCommitment stores a mint anchor commitment in the DB.
+func storeMintAnchorUniCommitment(t *testing.T, assetStore AssetMintingStore,
+	batchID int32, txOutputIndex int32, taprootInternalKey []byte,
+	groupKey []byte) {
+
+	ctx := context.Background()
+
+	var writeTxOpts AssetStoreTxOptions
+	upsertMintAnchorPreCommit := func(q PendingAssetStore) error {
+		_, err := q.UpsertMintAnchorUniCommitment(
+			ctx, sqlc.UpsertMintAnchorUniCommitmentParams{
+				BatchID:            batchID,
+				TxOutputIndex:      txOutputIndex,
+				TaprootInternalKey: taprootInternalKey,
+				GroupKey:           groupKey,
+			},
+		)
+		require.NoError(t, err)
+
+		return nil
+	}
+	_ = assetStore.db.ExecTx(ctx, &writeTxOpts, upsertMintAnchorPreCommit)
+}
+
+// assertMintAnchorUniCommitment is a helper function that reads a mint anchor
+// commitment from the DB and asserts that it matches the expected values.
+func assertMintAnchorUniCommitment(t *testing.T, assetStore AssetMintingStore,
+	batchID int32, txOutputIndex int32, preCommitInternalKeyBytes,
+	groupPubKeyBytes []byte) {
+
+	ctx := context.Background()
+	readOpts := NewAssetStoreReadTx()
+
+	var mintAnchorCommitment *sqlc.MintAnchorUniCommitment
+	readMintAnchorCommitment := func(q PendingAssetStore) error {
+		res, err := q.FetchMintAnchorUniCommitment(ctx, batchID)
+		require.NoError(t, err)
+
+		mintAnchorCommitment = &res
+		return nil
+	}
+	_ = assetStore.db.ExecTx(ctx, &readOpts, readMintAnchorCommitment)
+
+	// Ensure the mint anchor commitment matches the one we inserted.
+	require.NotNil(t, mintAnchorCommitment)
+	require.Equal(t, batchID, mintAnchorCommitment.BatchID)
+	require.Equal(t, txOutputIndex, mintAnchorCommitment.TxOutputIndex)
+	require.Equal(
+		t, preCommitInternalKeyBytes,
+		mintAnchorCommitment.TaprootInternalKey,
+	)
+	require.Equal(t, groupPubKeyBytes, mintAnchorCommitment.GroupKey)
+}
+
+// TestUpsertMintAnchorUniCommitment tests the UpsertMintAnchorUniCommitment
+// FetchMintAnchorUniCommitment and SQL queries. In particular, it tests that
+// upsert works correctly.
+func TestUpsertMintAnchorUniCommitment(t *testing.T) {
+	t.Parallel()
+
+	ctx := context.Background()
+	assetStore, _, _ := newAssetStore(t)
+
+	// Create a new batch with one asset group seedling.
+	mintingBatch := tapgarden.RandSeedlingMintingBatch(t, 1)
+	mintingBatch.UniverseCommitments = true
+
+	_, _, group := addRandGroupToBatch(
+		t, assetStore, ctx, mintingBatch.Seedlings,
+	)
+
+	// Commit batch.
+	require.NoError(t, assetStore.CommitMintingBatch(ctx, mintingBatch))
+
+	// Retrieve the batch ID of the batch we just inserted.
+	var batchID int32
+	readOpts := NewAssetStoreReadTx()
+	_ = assetStore.db.ExecTx(
+		ctx, &readOpts, func(q PendingAssetStore) error {
+			batches, err := q.AllMintingBatches(ctx)
+			require.NoError(t, err)
+			require.Len(t, batches, 1)
+
+			batchID = int32(batches[0].BatchID)
+			return nil
+		},
+	)
+
+	// Serialize keys into bytes for easier handling.
+	preCommitInternalKey := test.RandPubKey(t)
+	preCommitInternalKeyBytes := preCommitInternalKey.SerializeCompressed()
+
+	groupPubKeyBytes := group.GroupPubKey.SerializeCompressed()
+
+	// Upsert a mint anchor commitment for the batch.
+	txOutputIndex := int32(2)
+	storeMintAnchorUniCommitment(
+		t, *assetStore, batchID, txOutputIndex,
+		preCommitInternalKeyBytes, groupPubKeyBytes,
+	)
+
+	// Retrieve and inspect the mint anchor commitment we just inserted.
+	assertMintAnchorUniCommitment(
+		t, *assetStore, batchID, txOutputIndex,
+		preCommitInternalKeyBytes, groupPubKeyBytes,
+	)
+
+	// Upsert-ing a new taproot internal key for the same batch should
+	// overwrite the existing one.
+	internalKey2 := test.RandPubKey(t)
+	internalKey2Bytes := internalKey2.SerializeCompressed()
+
+	storeMintAnchorUniCommitment(
+		t, *assetStore, batchID, txOutputIndex, internalKey2Bytes,
+		groupPubKeyBytes,
+	)
+
+	assertMintAnchorUniCommitment(
+		t, *assetStore, batchID, txOutputIndex, internalKey2Bytes,
+		groupPubKeyBytes,
+	)
+
+	// Upsert-ing a new group key for the same batch should overwrite the
+	// existing one.
+	groupPubKey2 := test.RandPubKey(t)
+	groupPubKey2Bytes := groupPubKey2.SerializeCompressed()
+
+	storeMintAnchorUniCommitment(
+		t, *assetStore, batchID, txOutputIndex, internalKey2Bytes,
+		groupPubKey2Bytes,
+	)
+
+	assertMintAnchorUniCommitment(
+		t, *assetStore, batchID, txOutputIndex, internalKey2Bytes,
+		groupPubKey2Bytes,
+	)
+}
+
 func init() {
 	rand.Seed(time.Now().Unix())
 
diff --git a/tapgarden/batch_test.go b/tapgarden/batch_test.go
new file mode 100644
index 00000000..0c3c3da3
--- /dev/null
+++ b/tapgarden/batch_test.go
@@ -0,0 +1,213 @@
+package tapgarden
+
+import (
+	"testing"
+
+	"github.com/lightninglabs/taproot-assets/asset"
+	"github.com/lightninglabs/taproot-assets/fn"
+	"github.com/lightningnetwork/lnd/keychain"
+	"github.com/stretchr/testify/require"
+)
+
+// TestValidateUniCommitment tests the mint batch method validateUniCommitment.
+func TestValidateUniCommitment(t *testing.T) {
+	t.Parallel()
+
+	// Define test cases.
+	type TestCase struct {
+		name              string
+		candidateSeedling Seedling
+		batch             *MintingBatch
+		expectErr         bool
+	}
+
+	testCases := []TestCase{
+		{
+			// Verifies that a group anchor candidate seedling with
+			// universe commitments cannot be added to a batch with
+			// the universe commitments feature enabled
+			// if the batch already contains a group anchor
+			// seedling.
+			name: "populated batch with universe commitments; " +
+				"batch already includes group anchor; " +
+				"candidate seedling is also group anchor; " +
+				"is not valid",
+
+			candidateSeedling: RandGroupAnchorSeedling(
+				t, "new-group-anchor", true,
+			),
+			batch: RandMintingBatch(
+				t, WithTotalGroups([]int{2}),
+				WithUniverseCommitments(true),
+			),
+			expectErr: true,
+		},
+		{
+			// Ensures that a group anchor candidate seedling with
+			// universe commitments cannot be added to a batch that
+			// already contains seedlings and has the universe
+			// commitments feature disabled.
+			name: "populated batch without universe commitments; " +
+				"batch already includes group anchor; " +
+				"candidate seedling is also group anchor; " +
+				"is not valid",
+
+			candidateSeedling: RandGroupAnchorSeedling(
+				t, "new-group-anchor", true,
+			),
+			batch: RandMintingBatch(
+				t, WithTotalGroups([]int{2}),
+				WithUniverseCommitments(false),
+			),
+			expectErr: true,
+		},
+		{
+			// Ensures that a group candidate seedling (non-anchor)
+			// with universe commitments cannot be added to a batch
+			// that already contains seedlings and has the universe
+			// commitments feature disabled.
+			name: "populated batch without universe commitments; " +
+				"candidate seedling is non-anchor with " +
+				"universe commitments; is not valid",
+
+			candidateSeedling: RandNonAnchorGroupSeedling(
+				t, asset.V1, asset.Normal, "some-anchor-name",
+				[]byte{}, fn.None[keychain.KeyDescriptor](),
+				true,
+			),
+			batch: RandMintingBatch(
+				t, WithTotalGroups([]int{2}),
+				WithUniverseCommitments(false),
+			),
+			expectErr: true,
+		},
+		{
+			// Ensures that a group candidate seedling (non-anchor)
+			// with universe commitments cannot be added to a batch
+			// that already contains seedlings and has the universe
+			// commitments feature enabled.
+			//
+			// This is because the anchor seedling for the candidate
+			// seedling is not already present in the batch.
+			name: "populated batch with universe commitments; " +
+				"non-anchor candidate seedling with universe " +
+				"commitments; anchor not in batch; " +
+				"is not valid",
+
+			candidateSeedling: RandNonAnchorGroupSeedling(
+				t, asset.V1, asset.Normal, "some-anchor-name",
+				[]byte{}, fn.None[keychain.KeyDescriptor](),
+				true,
+			),
+			batch: RandMintingBatch(
+				t, WithTotalGroups([]int{2}),
+				WithUniverseCommitments(true),
+			),
+			expectErr: true,
+		},
+		{
+			// Ensures that a group anchor candidate seedling
+			// with universe commitments can be added to an empty
+			// batch.
+			name: "empty batch; candidate seedling is group " +
+				"anchor; is valid",
+
+			candidateSeedling: RandGroupAnchorSeedling(
+				t, "some-anchor-name", true,
+			),
+			batch:     RandMintingBatch(t),
+			expectErr: false,
+		},
+	}
+
+	// Construct a test case where a mint batch with universe commitments
+	// is populated with seedlings and a group anchor seedling is present.
+	// The candidate seedling is a non-anchor group seedling with
+	// universe commitments which specifies the batch group anchor seedling
+	// as its anchor. The candidate seedling should be deemed valid.
+	batch := RandMintingBatch(
+		t, WithTotalGroups([]int{2}), WithUniverseCommitments(true),
+	)
+
+	// Identify the anchor seedling in the batch.
+	var anchorSeedling *Seedling
+	for idx := range batch.Seedlings {
+		seedling := batch.Seedlings[idx]
+		if seedling.GroupAnchor == nil {
+			anchorSeedling = seedling
+			break
+		}
+	}
+
+	// Construct a candidate seedling that is a non-anchor group seedling
+	// with universe commitments and specifies the batch group anchor
+	// seedling as its anchor.
+	candidateSeedling := RandNonAnchorGroupSeedling(
+		t, anchorSeedling.AssetVersion, anchorSeedling.AssetType,
+		anchorSeedling.AssetName, anchorSeedling.Meta.Data,
+		anchorSeedling.DelegationKey,
+		anchorSeedling.UniverseCommitments,
+	)
+
+	testCases = append(testCases, TestCase{
+		name: "populated batch with universe commitments; " +
+			"candidate seedling is non-anchor group seedling " +
+			"with universe commitments; anchor in batch; " +
+			"is valid",
+		candidateSeedling: candidateSeedling,
+		batch:             batch,
+		expectErr:         false,
+	})
+
+	// Construct a test case where an empty but funded mint batch is
+	// populated with a group anchor seedling. The candidate seedling has
+	// universe commitments enabled. We expect the candidate seedling to be
+	// deemed invalid. This is because the batch is already funded and
+	// therefore the universe commitments feature cannot be enabled.
+	fundedEmptyBatch := RandMintingBatch(t)
+
+	// Set the genesis packet of the empty batch to simulate funding.
+	fundedEmptyBatch.GenesisPacket = &FundedMintAnchorPsbt{}
+
+	testCases = append(testCases, TestCase{
+		name: "empty funded batch; candidate seedling is anchor " +
+			"group seedling with universe commitments; is valid",
+		candidateSeedling: RandGroupAnchorSeedling(
+			t, "some-anchor-name", true,
+		),
+		batch:     fundedEmptyBatch,
+		expectErr: true,
+	})
+
+	// Add a test case where the batch is funded and empty but universe
+	// commitments is not enabled for the candidate seedling. The candidate
+	// seedling should be deemed valid. The universe commitment feature
+	// restriction does not apply in this case.
+	testCases = append(testCases, TestCase{
+		name: "empty funded batch; candidate seedling is anchor " +
+			"group seedling with universe commitments; is valid",
+		candidateSeedling: RandGroupAnchorSeedling(
+			t, "some-anchor-name", false,
+		),
+		batch:     fundedEmptyBatch,
+		expectErr: false,
+	})
+
+	// Execute test cases.
+	for idx := range testCases {
+		tc := testCases[idx]
+
+		t.Run(tc.name, func(t *testing.T) {
+			err := tc.batch.validateUniCommitment(
+				tc.candidateSeedling,
+			)
+
+			if tc.expectErr {
+				require.Error(t, err)
+				return
+			}
+
+			require.NoError(t, err)
+		})
+	}
+}
diff --git a/tapgarden/planter_test.go b/tapgarden/planter_test.go
index b1442f64..027c83b2 100644
--- a/tapgarden/planter_test.go
+++ b/tapgarden/planter_test.go
@@ -156,6 +156,7 @@ func (t *mintingTestHarness) refreshChainPlanter() {
 			ProofFiles:   t.proofFiles,
 			ProofWatcher: t.proofWatcher,
 		},
+		ChainParams:  *chainParams,
 		ProofUpdates: t.proofFiles,
 		ErrChan:      t.errChan,
 	})
@@ -1108,14 +1109,14 @@ func testBasicAssetCreation(t *mintingTestHarness) {
 	// Now that the planter is back up, a single caretaker should have been
 	// launched as well. The batch should already be funded.
 	batch := t.fetchSingleBatch(nil)
-	t.assertBatchGenesisTx(batch.GenesisPacket)
+	t.assertBatchGenesisTx(&batch.GenesisPacket.FundedPsbt)
 	t.assertNumCaretakersActive(1)
 
 	// We'll now force yet another restart to ensure correctness of the
 	// state machine. We expect the PSBT packet to still be funded.
 	t.refreshChainPlanter()
 	batch = t.fetchSingleBatch(nil)
-	t.assertBatchGenesisTx(batch.GenesisPacket)
+	t.assertBatchGenesisTx(&batch.GenesisPacket.FundedPsbt)
 
 	// Now that the batch has been ticked, and the caretaker started, there
 	// should no longer be a pending batch.
@@ -1211,7 +1212,7 @@ func testMintingTicker(t *mintingTestHarness) {
 	// that the batch is already funded.
 	t.assertBatchProgressing()
 	currentBatch := t.fetchLastBatch()
-	t.assertBatchGenesisTx(currentBatch.GenesisPacket)
+	t.assertBatchGenesisTx(&currentBatch.GenesisPacket.FundedPsbt)
 
 	// Now that the batch has been ticked, and the caretaker started, there
 	// should no longer be a pending batch.
@@ -1312,7 +1313,7 @@ func testMintingCancelFinalize(t *mintingTestHarness) {
 
 	t.assertBatchProgressing()
 	thirdBatch = t.fetchLastBatch()
-	t.assertBatchGenesisTx(thirdBatch.GenesisPacket)
+	t.assertBatchGenesisTx(&thirdBatch.GenesisPacket.FundedPsbt)
 
 	// Now that the batch has been ticked, and the caretaker started, there
 	// should no longer be a pending batch.
@@ -1720,7 +1721,7 @@ func testFundSealBeforeFinalize(t *mintingTestHarness) {
 	fundedEmptyBatch := fundedBatches[0]
 	require.Len(t, fundedEmptyBatch.Seedlings, 0)
 	require.NotNil(t, fundedEmptyBatch.GenesisPacket)
-	t.assertBatchGenesisTx(fundedEmptyBatch.GenesisPacket)
+	t.assertBatchGenesisTx(&fundedEmptyBatch.GenesisPacket.FundedPsbt)
 	require.Equal(t, defaultTapHash[:], fundedEmptyBatch.TapSibling())
 	require.True(t, fundedEmptyBatch.State() == tapgarden.BatchStatePending)