Repository: lightninglabs/taproot-assets

Issue:
Add `IgnoreAsset` RPC endpoint
Add `IgnoreAsset` RPC endpoint. This allows an asset issuer to mark an asset UTXO (identified by `PrevID`) as "ignored". Ignored assets will be included in the ignore tree of the next universe commitment transaction.

Hints from issue comments:

Solution patch:
diff --git a/cmd/commands/universe.go b/cmd/commands/universe.go
index 5b4f76da..53f8a3ea 100644
--- a/cmd/commands/universe.go
+++ b/cmd/commands/universe.go
@@ -1315,7 +1315,7 @@ func universeInfo(ctx *cli.Context) error {
 
 var universeStatsCommand = cli.Command{
 	Name:      "stats",
-	ShortName: "s",
+	ShortName: "st",
 	Usage:     "query for stats related to the active local Universe server",
 	Description: `
 	Query for a set of aggregate statistics related to the local Universe
diff --git a/go.mod b/go.mod
index 5b897bb9..037c8a81 100644
--- a/go.mod
+++ b/go.mod
@@ -30,7 +30,7 @@ require (
 	github.com/lightninglabs/lndclient v0.19.0-9
 	github.com/lightninglabs/neutrino/cache v1.1.2
 	github.com/lightninglabs/taproot-assets/taprpc v1.0.9
-	github.com/lightningnetwork/lnd v0.19.2-beta.rc2
+	github.com/lightningnetwork/lnd v0.19.2-beta
 	github.com/lightningnetwork/lnd/cert v1.2.2
 	github.com/lightningnetwork/lnd/clock v1.1.1
 	github.com/lightningnetwork/lnd/fn/v2 v2.0.8
diff --git a/go.sum b/go.sum
index 6c0bbbfa..7f4d4719 100644
--- a/go.sum
+++ b/go.sum
@@ -1149,8 +1149,8 @@ github.com/lightninglabs/protobuf-go-hex-display v1.34.2-hex-display h1:w7FM5LH9
 github.com/lightninglabs/protobuf-go-hex-display v1.34.2-hex-display/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
 github.com/lightningnetwork/lightning-onion v1.2.1-0.20240712235311-98bd56499dfb h1:yfM05S8DXKhuCBp5qSMZdtSwvJ+GFzl94KbXMNB1JDY=
 github.com/lightningnetwork/lightning-onion v1.2.1-0.20240712235311-98bd56499dfb/go.mod h1:c0kvRShutpj3l6B9WtTsNTBUtjSmjZXbJd9ZBRQOSKI=
-github.com/lightningnetwork/lnd v0.19.2-beta.rc2 h1:vPIMjQr8SWZJHn/j3QSFct4AbCVa0WA7k0j0lHqFDAA=
-github.com/lightningnetwork/lnd v0.19.2-beta.rc2/go.mod h1:+yKUfIGKKYRHGewgzQ6xi0S26DIfBiMv1zCdB3m6YxA=
+github.com/lightningnetwork/lnd v0.19.2-beta h1:3SKVrKYFY4IJLlrMf7cDzZcBeT+MxjI9Xy6YpY+EEX4=
+github.com/lightningnetwork/lnd v0.19.2-beta/go.mod h1:+yKUfIGKKYRHGewgzQ6xi0S26DIfBiMv1zCdB3m6YxA=
 github.com/lightningnetwork/lnd/cert v1.2.2 h1:71YK6hogeJtxSxw2teq3eGeuy4rHGKcFf0d0Uy4qBjI=
 github.com/lightningnetwork/lnd/cert v1.2.2/go.mod h1:jQmFn/Ez4zhDgq2hnYSw8r35bqGVxViXhX6Cd7HXM6U=
 github.com/lightningnetwork/lnd/clock v1.1.1 h1:OfR3/zcJd2RhH0RU+zX/77c0ZiOnIMsDIBjgjWdZgA0=
diff --git a/tapdb/sqlc/querier.go b/tapdb/sqlc/querier.go
index b474a051..c5b5958d 100644
--- a/tapdb/sqlc/querier.go
+++ b/tapdb/sqlc/querier.go
@@ -108,6 +108,8 @@ type Querier interface {
 	FetchUniverseRoot(ctx context.Context, namespace string) (FetchUniverseRootRow, error)
 	FetchUniverseSupplyRoot(ctx context.Context, namespaceRoot string) (FetchUniverseSupplyRootRow, error)
 	FetchUnknownTypeScriptKeys(ctx context.Context) ([]FetchUnknownTypeScriptKeysRow, error)
+	// Fetch unspent pre-commitment outputs. A pre-commitment output is a mint
+	// anchor transaction output which relates to the supply commitment feature.
 	FetchUnspentPrecommits(ctx context.Context, groupKey []byte) ([]FetchUnspentPrecommitsRow, error)
 	FinalizeSupplyCommitTransition(ctx context.Context, transitionID int64) error
 	GenesisAssets(ctx context.Context) ([]GenesisAsset, error)
diff --git a/tapdb/sqlc/queries/supply_commit.sql b/tapdb/sqlc/queries/supply_commit.sql
index e97b2d06..031e0792 100644
--- a/tapdb/sqlc/queries/supply_commit.sql
+++ b/tapdb/sqlc/queries/supply_commit.sql
@@ -137,9 +137,11 @@ DELETE FROM supply_update_events
 WHERE transition_id = @transition_id;
 
 -- name: FetchUnspentPrecommits :many
+-- Fetch unspent pre-commitment outputs. A pre-commitment output is a mint
+-- anchor transaction output which relates to the supply commitment feature.
 SELECT
     mac.tx_output_index,
-    ik.raw_key AS taproot_internal_key,
+    sqlc.embed(ik),
     mac.group_key,
     mint_txn.block_height,
     mint_txn.raw_tx
@@ -159,7 +161,7 @@ SELECT
     sc.commit_id,
     sc.output_index,
     sc.output_key,
-    ik.raw_key AS internal_key,
+    sqlc.embed(ik),
     txn.raw_tx,
     sc.supply_root_hash AS root_hash,
     sc.supply_root_sum AS root_sum 
diff --git a/tapdb/sqlc/supply_commit.sql.go b/tapdb/sqlc/supply_commit.sql.go
index 7d7ba890..56f25493 100644
--- a/tapdb/sqlc/supply_commit.sql.go
+++ b/tapdb/sqlc/supply_commit.sql.go
@@ -72,7 +72,7 @@ SELECT
     sc.commit_id,
     sc.output_index,
     sc.output_key,
-    ik.raw_key AS internal_key,
+    ik.key_id, ik.raw_key, ik.key_family, ik.key_index,
     txn.raw_tx,
     sc.supply_root_hash AS root_hash,
     sc.supply_root_sum AS root_sum 
@@ -92,7 +92,7 @@ type FetchSupplyCommitRow struct {
 	CommitID    int64
 	OutputIndex sql.NullInt32
 	OutputKey   []byte
-	InternalKey []byte
+	InternalKey InternalKey
 	RawTx       []byte
 	RootHash    []byte
 	RootSum     sql.NullInt64
@@ -105,7 +105,10 @@ func (q *Queries) FetchSupplyCommit(ctx context.Context, groupKey []byte) (Fetch
 		&i.CommitID,
 		&i.OutputIndex,
 		&i.OutputKey,
-		&i.InternalKey,
+		&i.InternalKey.KeyID,
+		&i.InternalKey.RawKey,
+		&i.InternalKey.KeyFamily,
+		&i.InternalKey.KeyIndex,
 		&i.RawTx,
 		&i.RootHash,
 		&i.RootSum,
@@ -116,7 +119,7 @@ func (q *Queries) FetchSupplyCommit(ctx context.Context, groupKey []byte) (Fetch
 const FetchUnspentPrecommits = `-- name: FetchUnspentPrecommits :many
 SELECT
     mac.tx_output_index,
-    ik.raw_key AS taproot_internal_key,
+    ik.key_id, ik.raw_key, ik.key_family, ik.key_index,
     mac.group_key,
     mint_txn.block_height,
     mint_txn.raw_tx
@@ -133,13 +136,15 @@ WHERE
 `
 
 type FetchUnspentPrecommitsRow struct {
-	TxOutputIndex      int32
-	TaprootInternalKey []byte
-	GroupKey           []byte
-	BlockHeight        sql.NullInt32
-	RawTx              []byte
+	TxOutputIndex int32
+	InternalKey   InternalKey
+	GroupKey      []byte
+	BlockHeight   sql.NullInt32
+	RawTx         []byte
 }
 
+// Fetch unspent pre-commitment outputs. A pre-commitment output is a mint
+// anchor transaction output which relates to the supply commitment feature.
 func (q *Queries) FetchUnspentPrecommits(ctx context.Context, groupKey []byte) ([]FetchUnspentPrecommitsRow, error) {
 	rows, err := q.db.QueryContext(ctx, FetchUnspentPrecommits, groupKey)
 	if err != nil {
@@ -151,7 +156,10 @@ func (q *Queries) FetchUnspentPrecommits(ctx context.Context, groupKey []byte) (
 		var i FetchUnspentPrecommitsRow
 		if err := rows.Scan(
 			&i.TxOutputIndex,
-			&i.TaprootInternalKey,
+			&i.InternalKey.KeyID,
+			&i.InternalKey.RawKey,
+			&i.InternalKey.KeyFamily,
+			&i.InternalKey.KeyIndex,
 			&i.GroupKey,
 			&i.BlockHeight,
 			&i.RawTx,
diff --git a/tapdb/supply_commit.go b/tapdb/supply_commit.go
index 13a56683..718792ce 100644
--- a/tapdb/supply_commit.go
+++ b/tapdb/supply_commit.go
@@ -245,12 +245,10 @@ func (s *SupplyCommitMachine) UnspentPrecommits(ctx context.Context,
 		// interface.
 		preCommits = make(supplycommit.PreCommits, 0, len(rows))
 		for _, row := range rows {
-			internalKey, err := btcec.ParsePubKey(
-				row.TaprootInternalKey,
-			)
+			internalKey, err := parseInternalKey(row.InternalKey)
 			if err != nil {
-				return fmt.Errorf("error parsing internal "+
-					"key: %w", err)
+				return fmt.Errorf("failed to parse "+
+					"pre-commitment internal key: %w", err)
 			}
 
 			groupPubKey, err := btcec.ParsePubKey(row.GroupKey)
@@ -272,7 +270,7 @@ func (s *SupplyCommitMachine) UnspentPrecommits(ctx context.Context,
 				),
 				MintingTxn:  &mintingTx,
 				OutIdx:      uint32(row.TxOutputIndex),
-				InternalKey: *internalKey,
+				InternalKey: internalKey,
 				GroupPubKey: *groupPubKey,
 			}
 			preCommits = append(preCommits, preCommit)
@@ -315,7 +313,7 @@ func (s *SupplyCommitMachine) SupplyCommit(ctx context.Context,
 				err)
 		}
 
-		internalKey, err := btcec.ParsePubKey(row.InternalKey)
+		internalKey, err := parseInternalKey(row.InternalKey)
 		if err != nil {
 			return fmt.Errorf("error parsing internal key: %w", err)
 		}
@@ -828,7 +826,11 @@ func fetchCommitment(ctx context.Context, db SupplyCommitStore,
 			"fetch internal key %d for commit %d: %w",
 			commit.InternalKeyID, commitID.Int64, err)
 	}
-	internalKey, err := btcec.ParsePubKey(internalKeyRow.RawKey)
+	internalKey, err := parseInternalKey(sqlc.InternalKey{
+		RawKey:    internalKeyRow.RawKey,
+		KeyFamily: internalKeyRow.KeyFamily,
+		KeyIndex:  internalKeyRow.KeyIndex,
+	})
 	if err != nil {
 		return noneRootCommit, noneChainInfo, fmt.Errorf("failed to "+
 			"parse internal key for commit %d: %w", commitID.Int64,
diff --git a/universe/supplycommit/env.go b/universe/supplycommit/env.go
index 2244fe76..c31c0cad 100644
--- a/universe/supplycommit/env.go
+++ b/universe/supplycommit/env.go
@@ -123,7 +123,7 @@ type PreCommitment struct {
 
 	// InternalKey is the Taproot internal public key associated with the
 	// pre-commitment output.
-	InternalKey btcec.PublicKey
+	InternalKey keychain.KeyDescriptor
 
 	// GroupPubKey is the asset group public key associated with this
 	// pre-commitment output.
@@ -154,7 +154,7 @@ type RootCommitment struct {
 	TxOutIdx uint32
 
 	// InternalKey is the internal key used to create the commitment output.
-	InternalKey *btcec.PublicKey
+	InternalKey keychain.KeyDescriptor
 
 	// Output key is the taproot output key used to create the commitment
 	// output.
@@ -182,11 +182,21 @@ func (r *RootCommitment) TxIn() *wire.TxIn {
 //
 // TODO(roasbeef): expand, add support for tapscript as well
 func (r *RootCommitment) TxOut() (*wire.TxOut, error) {
-	// First, obtain the root hash of the supply tree.
-	supplyRootHash := r.SupplyRoot.NodeHash()
+	txOut, _, err := RootCommitTxOut(
+		r.InternalKey.PubKey, r.OutputKey, r.SupplyRoot.NodeHash(),
+	)
+
+	return txOut, err
+}
 
-	var taprootKey *btcec.PublicKey
-	if r.OutputKey == nil {
+// RootCommitTxOut returns the transaction output that corresponds to the root
+// commitment. This is used to create a new commitment output.
+func RootCommitTxOut(internalKey *btcec.PublicKey,
+	tapOutKey *btcec.PublicKey, supplyRootHash mssmt.NodeHash) (*wire.TxOut,
+	*btcec.PublicKey, error) {
+
+	var taprootOutputKey *btcec.PublicKey
+	if tapOutKey == nil {
 		// We'll create a new unspendable output that contains a
 		// commitment to the root.
 		//
@@ -195,28 +205,32 @@ func (r *RootCommitment) TxOut() (*wire.TxOut, error) {
 			asset.PedersenVersion, supplyRootHash[:],
 		)
 		if err != nil {
-			return nil, fmt.Errorf("unable to create leaf: %w", err)
+			return nil, nil, fmt.Errorf("unable to create leaf: %w",
+				err)
 		}
 
 		tapscriptTree := txscript.AssembleTaprootScriptTree(tapLeaf)
 
 		rootHash := tapscriptTree.RootNode.TapHash()
-		taprootKey = txscript.ComputeTaprootOutputKey(
-			r.InternalKey, rootHash[:],
+		taprootOutputKey = txscript.ComputeTaprootOutputKey(
+			internalKey, rootHash[:],
 		)
 	} else {
-		taprootKey = r.OutputKey
+		taprootOutputKey = tapOutKey
 	}
 
-	pkScript, err := txscript.PayToTaprootScript(taprootKey)
+	pkScript, err := txscript.PayToTaprootScript(taprootOutputKey)
 	if err != nil {
-		return nil, fmt.Errorf("unable to create pk script: %w", err)
+		return nil, nil, fmt.Errorf("unable to create pk script: %w",
+			err)
 	}
 
-	return &wire.TxOut{
+	txOut := wire.TxOut{
 		Value:    int64(tapsend.DummyAmtSats),
 		PkScript: pkScript,
-	}, nil
+	}
+
+	return &txOut, taprootOutputKey, nil
 }
 
 // ChainProof stores the information needed to prove that a given supply commit
@@ -293,9 +307,8 @@ type Wallet interface {
 		feeRate chainfee.SatPerKWeight,
 		changeIdx int32) (*tapsend.FundedPsbt, error)
 
-	// SignAndFinalizePsbt fully signs and finalizes the target PSBT
-	// packet.
-	SignAndFinalizePsbt(context.Context, *psbt.Packet) (*psbt.Packet, error)
+	// SignPsbt fully signs the target PSBT packet.
+	SignPsbt(context.Context, *psbt.Packet) (*psbt.Packet, error)
 
 	// ImportTaprootOutput imports a new taproot output key into the wallet.
 	ImportTaprootOutput(context.Context, *btcec.PublicKey) (btcutil.Address,
diff --git a/universe/supplycommit/mock.go b/universe/supplycommit/mock.go
index b52e74d2..af517705 100644
--- a/universe/supplycommit/mock.go
+++ b/universe/supplycommit/mock.go
@@ -75,11 +75,9 @@ type fundPsbtMockFn func(
 	chainfee.SatPerKWeight, int32,
 ) (*tapsend.FundedPsbt, error)
 
-// signAndFinalizePsbtMockFn defines a type for the mock function used in
+// signPsbtMockFn defines a type for the mock function used in
 // SignAndFinalizePsbt, to simplify a long type assertion.
-type signAndFinalizePsbtMockFn func(
-	context.Context, *psbt.Packet,
-) (*psbt.Packet, error)
+type signPsbtMockFn func(context.Context, *psbt.Packet) (*psbt.Packet, error)
 
 // mockWallet is a mock implementation of the Wallet interface.
 type mockWallet struct {
@@ -107,7 +105,7 @@ func (m *mockWallet) FundPsbt(
 	return args.Get(0).(*tapsend.FundedPsbt), args.Error(1)
 }
 
-func (m *mockWallet) SignAndFinalizePsbt(ctx context.Context,
+func (m *mockWallet) SignPsbt(ctx context.Context,
 	packet *psbt.Packet) (*psbt.Packet, error) {
 
 	args := m.Called(ctx, packet)
@@ -116,7 +114,7 @@ func (m *mockWallet) SignAndFinalizePsbt(ctx context.Context,
 	// If so, this indicates a custom mock implementation that should be
 	// executed to get the actual return values.
 	arg0 := args.Get(0)
-	if fn, ok := arg0.(signAndFinalizePsbtMockFn); ok {
+	if fn, ok := arg0.(signPsbtMockFn); ok {
 		return fn(ctx, packet)
 	}
 
diff --git a/universe/supplycommit/transitions.go b/universe/supplycommit/transitions.go
index 614eee48..7a6f3385 100644
--- a/universe/supplycommit/transitions.go
+++ b/universe/supplycommit/transitions.go
@@ -5,15 +5,17 @@ import (
 	"context"
 	"fmt"
 
-	"github.com/btcsuite/btcd/btcec/v2"
 	"github.com/btcsuite/btcd/btcutil/psbt"
+	"github.com/btcsuite/btcd/chaincfg"
 	"github.com/btcsuite/btcd/wire"
 	"github.com/lightninglabs/taproot-assets/asset"
 	"github.com/lightninglabs/taproot-assets/mssmt"
 	"github.com/lightninglabs/taproot-assets/proof"
+	"github.com/lightninglabs/taproot-assets/tappsbt"
 	"github.com/lightninglabs/taproot-assets/tapsend"
 	"github.com/lightningnetwork/lnd/chainntnfs"
 	lfn "github.com/lightningnetwork/lnd/fn/v2"
+	"github.com/lightningnetwork/lnd/keychain"
 	"github.com/lightningnetwork/lnd/lnutils"
 	"github.com/lightningnetwork/lnd/protofsm"
 )
@@ -298,6 +300,10 @@ func (c *CommitTreeCreateState) ProcessEvent(event Event,
 					"sub-tree root: %w", err)
 			}
 
+			if subTreeRoot.NodeSum() == 0 {
+				continue
+			}
+
 			rootTreeLeaf := mssmt.NewLeafNode(
 				lnutils.ByteSlice(subTreeRoot.NodeHash()),
 				subTreeRoot.NodeSum(),
@@ -353,7 +359,8 @@ func (c *CommitTreeCreateState) ProcessEvent(event Event,
 func newRootCommitment(ctx context.Context,
 	oldCommitment lfn.Option[RootCommitment],
 	unspentPreCommits []PreCommitment, newSupplyRoot *mssmt.BranchNode,
-	wallet Wallet, keyRing KeyRing) lfn.Result[RootCommitment] {
+	wallet Wallet, keyRing KeyRing,
+	chainParams chaincfg.Params) (*RootCommitment, *psbt.Packet, error) {
 
 	newCommitTx := wire.NewMsgTx(2)
 
@@ -363,48 +370,118 @@ func newRootCommitment(ctx context.Context,
 	// TODO(roasbeef): need index map for PSBT??
 	//  * verify all inputs locked on restart, otherwise transition from
 	//  sign//broadcast back to this state?
+	packetPInputs := make([]psbt.PInput, 0, len(unspentPreCommits)+1)
+
 	for _, preCommit := range unspentPreCommits {
 		newCommitTx.AddTxIn(preCommit.TxIn())
+
+		bip32Derivation, trBip32Derivation :=
+			tappsbt.Bip32DerivationFromKeyDesc(
+				preCommit.InternalKey, chainParams.HDCoinType,
+			)
+
+		witnessUtxo := preCommit.MintingTxn.TxOut[preCommit.OutIdx]
+
+		packetPInputs = append(packetPInputs, psbt.PInput{
+			WitnessUtxo: witnessUtxo,
+			Bip32Derivation: []*psbt.Bip32Derivation{
+				bip32Derivation,
+			},
+			TaprootBip32Derivation: []*psbt.TaprootBip32Derivation{
+				trBip32Derivation,
+			},
+			TaprootInternalKey: trBip32Derivation.XOnlyPubKey,
+		})
 	}
 
-	// If there isn't an existing commitment in the chain, then we'll have
-	// one less input on the transaction.
+	// If all pre-commitments are spent, then we'll use the old commitment
+	// as an input to the new transaction. Pre-commitments are only present
+	// on mint transactions where as the old commitment is the last
+	// commitment that was broadcast.
 	oldCommitment.WhenSome(func(r RootCommitment) {
 		newCommitTx.AddTxIn(r.TxIn())
+
+		bip32Derivation, trBip32Derivation :=
+			tappsbt.Bip32DerivationFromKeyDesc(
+				r.InternalKey, chainParams.HDCoinType,
+			)
+
+		witnessUtxo := r.Txn.TxOut[r.TxOutIdx]
+
+		packetPInputs = append(packetPInputs, psbt.PInput{
+			WitnessUtxo: witnessUtxo,
+			Bip32Derivation: []*psbt.Bip32Derivation{
+				bip32Derivation,
+			},
+			TaprootBip32Derivation: []*psbt.TaprootBip32Derivation{
+				trBip32Derivation,
+			},
+			TaprootInternalKey: trBip32Derivation.XOnlyPubKey,
+		})
 	})
 
-	// If we have a prior root commitment, then we'll reuse the internal
-	// key. Otherwise, we'll generate a fresh one.
-	iKeyOpt := lfn.MapOption(func(r RootCommitment) *btcec.PublicKey {
+	// With the inputs available, derive the supply commitment output.
+	//
+	// Determine the internal key to use for this output.
+	// If a prior root commitment exists, reuse its internal key;
+	// otherwise, generate a new one.
+	iKeyOpt := lfn.MapOption(func(r RootCommitment) keychain.KeyDescriptor {
 		return r.InternalKey
 	})(oldCommitment)
+
 	commitInternalKey, err := iKeyOpt.UnwrapOrFuncErr(
-		func() (*btcec.PublicKey, error) {
+		func() (keychain.KeyDescriptor, error) {
+			var zero keychain.KeyDescriptor
+
 			newKey, err := keyRing.DeriveNextTaprootAssetKey(ctx)
 			if err != nil {
-				return nil, fmt.Errorf("unable to derive "+
+				return zero, fmt.Errorf("unable to derive "+
 					"next key: %w", err)
 			}
 
-			return newKey.PubKey, nil
+			return newKey, nil
 		},
 	)
 	if err != nil {
-		return lfn.Err[RootCommitment](err)
+		return nil, nil, err
 	}
 
+	// Derive the new commitment output, and add that to the update
+	// transaction.
+	supplyTxOut, tapOutKey, err := RootCommitTxOut(
+		commitInternalKey.PubKey, nil, newSupplyRoot.NodeHash(),
+	)
+	if err != nil {
+		return nil, nil, fmt.Errorf("unable to create commitment "+
+			"tx output: %w", err)
+	}
+	newCommitTx.AddTxOut(supplyTxOut)
+
 	// If we just generated a new key, then we'll import that into the
 	// wallet so it can track the sats for our commitment output.
 	if iKeyOpt.IsNone() {
-		_, err := wallet.ImportTaprootOutput(
-			ctx, commitInternalKey,
-		)
+		_, err := wallet.ImportTaprootOutput(ctx, tapOutKey)
 		if err != nil {
-			return lfn.Errf[RootCommitment]("unable to import "+
+			return nil, nil, fmt.Errorf("unable to import "+
 				"taproot output: %w", err)
 		}
 	}
 
+	bip32Derivation, trBip32Derivation :=
+		tappsbt.Bip32DerivationFromKeyDesc(
+			commitInternalKey, chainParams.HDCoinType,
+		)
+
+	packetPOutput := psbt.POutput{
+		Bip32Derivation: []*psbt.Bip32Derivation{
+			bip32Derivation,
+		},
+		TaprootBip32Derivation: []*psbt.TaprootBip32Derivation{
+			trBip32Derivation,
+		},
+		TaprootInternalKey: trBip32Derivation.XOnlyPubKey,
+	}
+
 	// With the inputs added, we'll now create our new commitment output. We
 	// don't know the index yet, so we'll set this to 0. We'll also re-use
 	// the current internal key for now.
@@ -414,19 +491,18 @@ func newRootCommitment(ctx context.Context,
 		Txn:         newCommitTx,
 		TxOutIdx:    0,
 		InternalKey: commitInternalKey,
+		OutputKey:   tapOutKey,
 		SupplyRoot:  newSupplyRoot,
 	}
 
-	// Finally, we'll derive the new commitment output, and add that to the
-	// update transaction.
-	supplyTxOut, err := newSupplyCommit.TxOut()
+	commitPkt, err := psbt.NewFromUnsignedTx(newCommitTx)
 	if err != nil {
-		return lfn.Errf[RootCommitment]("unable to create "+
-			"commitment output: %w", err)
+		return nil, nil, fmt.Errorf("unable to create PSBT: %w", err)
 	}
-	newCommitTx.AddTxOut(supplyTxOut)
+	commitPkt.Inputs = packetPInputs
+	commitPkt.Outputs = []psbt.POutput{packetPOutput}
 
-	return lfn.Ok(newSupplyCommit)
+	return &newSupplyCommit, commitPkt, nil
 }
 
 // fundSupplyCommitTx takes a newly created supply commitment transaction,
@@ -434,13 +510,7 @@ func newRootCommitment(ctx context.Context,
 // the index of the supply commitment output within the funded transaction. It
 // updates the TxOutIdx field of the passed supplyCommit directly.
 func fundSupplyCommitTx(ctx context.Context, supplyCommit *RootCommitment,
-	env *Environment) (*tapsend.FundedPsbt, error) {
-
-	// Create a new PSBT from the unsigned transaction.
-	commitPkt, err := psbt.NewFromUnsignedTx(supplyCommit.Txn)
-	if err != nil {
-		return nil, fmt.Errorf("unable to create PSBT: %w", err)
-	}
+	commitPkt *psbt.Packet, env *Environment) (*tapsend.FundedPsbt, error) {
 
 	// Estimate the required fee rate.
 	feeRate, err := env.Chain.EstimateFee(ctx, env.CommitConfTarget)
@@ -527,10 +597,10 @@ func (c *CommitTxCreateState) ProcessEvent(event Event,
 
 		// With all the inputs obtained, we'll create the new supply
 		// commitment.
-		newSupplyCommit, err := newRootCommitment(
+		newSupplyCommit, commitPkt, err := newRootCommitment(
 			ctx, oldCommitment, preCommits, newSupplyRoot,
-			env.Wallet, env.KeyRing,
-		).Unpack()
+			env.Wallet, env.KeyRing, env.ChainParams,
+		)
 		if err != nil {
 			return nil, fmt.Errorf("unable to create "+
 				"new root commitment: %w", err)
@@ -540,7 +610,7 @@ func (c *CommitTxCreateState) ProcessEvent(event Event,
 		// the TxOutIdx field within the newSupplyCommit struct passed
 		// to it.
 		fundedCommitPkt, err := fundSupplyCommitTx(
-			ctx, &newSupplyCommit, env,
+			ctx, newSupplyCommit, commitPkt, env,
 		)
 		if err != nil {
 			return nil, fmt.Errorf("unable to fund supply "+
@@ -554,7 +624,7 @@ func (c *CommitTxCreateState) ProcessEvent(event Event,
 			OldCommitment:     oldCommitment,
 			UnspentPreCommits: preCommits,
 			PendingUpdates:    c.SupplyTransition.PendingUpdates,
-			NewCommitment:     newSupplyCommit,
+			NewCommitment:     *newSupplyCommit,
 		}
 
 		return &StateTransition{
@@ -564,7 +634,7 @@ func (c *CommitTxCreateState) ProcessEvent(event Event,
 			NewEvents: lfn.Some(FsmEvent{
 				InternalEvent: []Event{&SignTxEvent{
 					CommitPkt:       fundedCommitPkt,
-					NewSupplyCommit: newSupplyCommit,
+					NewSupplyCommit: *newSupplyCommit,
 				}},
 			}),
 		}, nil
@@ -592,13 +662,21 @@ func (c *CommitTxSignState) ProcessEvent(event Event,
 		stateTransition := c.SupplyTransition
 
 		// After some initial validation, we'll now sign the PSBT.
-		signedPsbt, err := env.Wallet.SignAndFinalizePsbt(
+		log.Debug("Signing supply commitment PSBT")
+		signedPsbt, err := env.Wallet.SignPsbt(
 			ctx, newEvent.CommitPkt.Pkt,
 		)
 		if err != nil {
 			return nil, fmt.Errorf("unable to sign "+
 				"commitment tx: %w", err)
 		}
+
+		err = psbt.MaybeFinalizeAll(signedPsbt)
+		if err != nil {
+			return nil, fmt.Errorf("unable to finalize psbt: %w",
+				err)
+		}
+
 		commitTx, err := psbt.Extract(signedPsbt)
 		if err != nil {
 			return nil, fmt.Errorf("unable to extract "+
@@ -611,11 +689,12 @@ func (c *CommitTxSignState) ProcessEvent(event Event,
 		// state to disk, and also mark that that we'll transition to
 		// the CommitBroadcastState. We construct the SupplyCommitTxn
 		// struct with the details from the finalized commitment.
+		newCommit := &stateTransition.NewCommitment
 		commitTxnDetails := SupplyCommitTxn{
 			Txn:         commitTx,
-			InternalKey: stateTransition.NewCommitment.InternalKey,
-			OutputKey:   stateTransition.NewCommitment.OutputKey,
-			OutputIndex: stateTransition.NewCommitment.TxOutIdx,
+			InternalKey: newCommit.InternalKey.PubKey,
+			OutputKey:   newCommit.OutputKey,
+			OutputIndex: newCommit.TxOutIdx,
 		}
 		err = env.StateLog.InsertSignedCommitTx(
 			ctx, env.AssetSpec, commitTxnDetails,



Test patch:
diff --git a/cmd/commands/commands_test.go b/cmd/commands/commands_test.go
new file mode 100644
index 00000000..a279fbed
--- /dev/null
+++ b/cmd/commands/commands_test.go
@@ -0,0 +1,82 @@
+package commands
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+	"github.com/urfave/cli"
+)
+
+// TestCommandShortNamesUnique ensures that all command short names are unique
+// within their respective command groups at the same level to avoid conflicts.
+func TestCommandShortNamesUnique(t *testing.T) {
+	// Create a new app to get all commands.
+	app := NewApp()
+
+	// Helper function to check short names within a group of commands at
+	// the same level.
+	//
+	// Note that we define using var here to avoid recursion issues.
+	var checkLevel func(commands []cli.Command, groupPath string)
+	checkLevel = func(commands []cli.Command, groupPath string) {
+		shortNames := make(map[string][]string)
+
+		// Check short names only at this level (not recursively).
+		for _, cmd := range commands {
+			// Check if command has a short name.
+			if cmd.ShortName == "" {
+				continue
+			}
+
+			// Command has a short name, so we add it to the map.
+			commandPath := groupPath
+			if commandPath != "" {
+				commandPath += " "
+			}
+
+			commandPath += cmd.Name
+			shortNames[cmd.ShortName] = append(
+				shortNames[cmd.ShortName], commandPath,
+			)
+		}
+
+		// Check for duplicates at this level.
+		var duplicates []string
+		for shortName, paths := range shortNames {
+			if len(paths) > 1 {
+				duplicates = append(duplicates, shortName)
+			}
+		}
+
+		// Fail the test if any duplicates were found at this level.
+		require.Empty(t, duplicates, "Found duplicate short names at "+
+			"command level '%s'", groupPath)
+
+		// Log all short names for reference (only in verbose mode).
+		if testing.Verbose() {
+			t.Logf("Level '%s' has %d unique short names:",
+				groupPath, len(shortNames))
+			for shortName, paths := range shortNames {
+				t.Logf("  %s -> %s", shortName, paths[0])
+			}
+		}
+
+		// Recursively check subcommands at their respective levels.
+		for _, cmd := range commands {
+			if len(cmd.Subcommands) > 0 {
+				// Formulate subgroup path.
+				subGroupPath := groupPath
+				if subGroupPath != "" {
+					subGroupPath += " "
+				}
+				subGroupPath += cmd.Name
+
+				// Recursively check subcommands.
+				checkLevel(cmd.Subcommands, subGroupPath)
+			}
+		}
+	}
+
+	// Check top-level commands.
+	checkLevel(app.Commands, "")
+}
diff --git a/tapdb/supply_commit_test.go b/tapdb/supply_commit_test.go
index 85aeb7a4..8426ba23 100644
--- a/tapdb/supply_commit_test.go
+++ b/tapdb/supply_commit_test.go
@@ -681,7 +681,7 @@ func (h *supplyCommitTestHarness) performSingleTransition(
 	// Next, we'll generate a new "fake" commitment transaction along with
 	// sample internal and output keys.
 	commitTx := randTx(h.t, 1)
-	internalKey := test.RandPubKey(h.t)
+	internalKey, _ := test.RandKeyDesc(h.t)
 	outputKey := test.RandPubKey(h.t)
 
 	// We'll now simulate the next phase of the state transition where we
@@ -689,7 +689,7 @@ func (h *supplyCommitTestHarness) performSingleTransition(
 	// and commit that.
 	commitDetails := supplycommit.SupplyCommitTxn{
 		Txn:         commitTx,
-		InternalKey: internalKey,
+		InternalKey: internalKey.PubKey,
 		OutputKey:   outputKey,
 		OutputIndex: 1,
 	}
@@ -764,7 +764,7 @@ func (h *supplyCommitTestHarness) performSingleTransition(
 
 	return stateTransitionOutput{
 		appliedUpdates: updates,
-		internalKey:    internalKey,
+		internalKey:    internalKey.PubKey,
 		outputKey:      outputKey,
 		commitTx:       commitTx,
 		chainProof:     chainProof,
@@ -1116,7 +1116,7 @@ func (h *supplyCommitTestHarness) assertTransitionApplied(
 	)
 	require.Equal(
 		h.t, output.internalKey.SerializeCompressed(),
-		fetchedCommit.InternalKey.SerializeCompressed(),
+		fetchedCommit.InternalKey.PubKey.SerializeCompressed(),
 		"SupplyCommit returned wrong InternalKey",
 	)
 	require.Equal(
@@ -1321,7 +1321,7 @@ func TestSupplyCommitInsertSignedCommitTx(t *testing.T) {
 	)
 	require.Equal(
 		t, internalKey.SerializeCompressed(),
-		fetchedTransition.NewCommitment.InternalKey.SerializeCompressed(), //nolint:lll
+		fetchedTransition.NewCommitment.InternalKey.PubKey.SerializeCompressed(), //nolint:lll
 	)
 	require.Equal(
 		t, outputKey.SerializeCompressed(),
@@ -1714,7 +1714,7 @@ func TestSupplyCommitMachineFetch(t *testing.T) {
 	require.NoError(t, err)
 	require.Equal(
 		t, dbInternalKeyRow.RawKey,
-		rootCommit.InternalKey.SerializeCompressed(),
+		rootCommit.InternalKey.PubKey.SerializeCompressed(),
 	)
 	require.Equal(
 		t, dbCommit.OutputKey,
diff --git a/universe/supplycommit/state_machine_test.go b/universe/supplycommit/state_machine_test.go
index 14f7c1bd..84f7c6e8 100644
--- a/universe/supplycommit/state_machine_test.go
+++ b/universe/supplycommit/state_machine_test.go
@@ -418,7 +418,7 @@ func (h *supplyCommitTestHarness) expectPsbtFunding() {
 }
 
 func (h *supplyCommitTestHarness) expectPsbtSigning() {
-	signFn := signAndFinalizePsbtMockFn(func(
+	signFn := signPsbtMockFn(func(
 		_ context.Context, pkt *psbt.Packet,
 	) (*psbt.Packet, error) {
 		// The transaction from the PSBT is extracted and used to create
@@ -428,7 +428,7 @@ func (h *supplyCommitTestHarness) expectPsbtSigning() {
 		return newTestSignedPsbt(h.t, pkt.UnsignedTx), nil
 	})
 	h.mockWallet.On(
-		"SignAndFinalizePsbt", mock.Anything, mock.Anything,
+		"SignPsbt", mock.Anything, mock.Anything,
 	).Return(signFn, nil).Once()
 }
 
@@ -791,10 +791,13 @@ func TestSupplyCommitTxSignStateTransitions(t *testing.T) {
 	defaultAssetSpec := asset.NewSpecifierFromId(testAssetID)
 	dummyTx := wire.NewMsgTx(2)
 	dummyTx.AddTxOut(&wire.TxOut{PkScript: []byte("test"), Value: 1})
+
+	internalKey, _ := test.RandKeyDesc(t)
+
 	initialTransition := SupplyStateTransition{
 		NewCommitment: RootCommitment{
 			Txn:         dummyTx,
-			InternalKey: test.RandPubKey(t),
+			InternalKey: internalKey,
 			TxOutIdx:    0,
 		},
 	}