Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lock Minting Anchor TX Change Output for Future Universe Commitment Support #1325

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
13 changes: 12 additions & 1 deletion cmd/commands/assets.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ var (
groupByGroupName = "by_group"
assetIDName = "asset_id"
shortResponseName = "short"
enableUniCommitmentName = "enable_uni_commitment"
feeRateName = "sat_per_vbyte"
assetAmountName = "amount"
burnOverrideConfirmationName = "override_confirmation_destroy_assets"
Expand Down Expand Up @@ -154,6 +155,15 @@ var mintAssetCommand = cli.Command{
Usage: "the master fingerprint of the key the xpub " +
"was derived from",
},
cli.BoolFlag{
Name: enableUniCommitmentName,
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{
Expand Down Expand Up @@ -382,7 +392,8 @@ func mintAsset(ctx *cli.Context) error {
AssetVersion: taprpc.AssetVersion(
ctx.Uint64(assetVersionName),
),
ExternalGroupKey: externalKey,
ExternalGroupKey: externalKey,
EnableUniCommitment: ctx.Bool(enableUniCommitmentName),
},
ShortResponse: ctx.Bool(shortResponseName),
})
Expand Down
6 changes: 6 additions & 0 deletions proof/meta.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"maps"
"math"

"github.com/btcsuite/btcd/btcec/v2"
"github.com/lightninglabs/taproot-assets/asset"
"github.com/lightninglabs/taproot-assets/fn"
"github.com/lightningnetwork/lnd/tlv"
Expand Down Expand Up @@ -108,6 +109,11 @@ type MetaReveal struct {
// protocol as it allows new odd (optional) types to be added without
// breaking old clients that don't yet fully understand them.
UnknownOddTypes tlv.TypeMap

// UniCommitTRKey is the Taproot output key of the minting anchor
// transaction's universe pre-commitment output. This field is optional.
// If specified, universe commitments are enabled for the minting event.
UniCommitTRKey fn.Option[btcec.PublicKey]
}

// SizableInteger is a subset of Integer that excludes int8, since we never use
Expand Down
13 changes: 7 additions & 6 deletions rpcserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -591,12 +591,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,
EnableUniCommitment: req.Asset.EnableUniCommitment,
}

rpcsLog.Infof("[MintAsset]: version=%v, type=%v, name=%v, amt=%v, "+
Expand Down
9 changes: 5 additions & 4 deletions tapcfg/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -529,16 +529,17 @@ func genServerConfig(cfg *Config, cfgLogger btclog.Logger,
"access status: %w", err)
}

chainParams := address.ParamsForChain(cfg.ActiveNetParams.Name)

return &tap.Config{
DebugLevel: cfg.DebugLevel,
RuntimeID: runtimeID,
EnableChannelFeatures: enableChannelFeatures,
Lnd: lndServices,
ChainParams: address.ParamsForChain(
cfg.ActiveNetParams.Name,
),
ReOrgWatcher: reOrgWatcher,
ChainParams: chainParams,
ReOrgWatcher: reOrgWatcher,
AssetMinter: tapgarden.NewChainPlanter(tapgarden.PlanterConfig{
ChainParams: chainParams,
GardenKit: tapgarden.GardenKit{
Wallet: walletAnchor,
ChainBridge: chainBridge,
Expand Down
119 changes: 119 additions & 0 deletions tapgarden/batch.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,15 @@ type MintingBatch struct {
// reveal for that asset, if it has one.
AssetMetas AssetMetas

// EnableUniCommitment 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, which must
// also be specified.
//
// Universe commitments are minter-controlled, on-chain anchored
// attestations regarding the state of the universe.
EnableUniCommitment bool

// mintingPubKey is the top-level Taproot output key that will be used
// to commit to the Taproot Asset commitment above.
mintingPubKey *btcec.PublicKey
Expand Down Expand Up @@ -313,6 +322,116 @@ func (m *MintingBatch) HasSeedlings() bool {
return len(m.Seedlings) != 0
}

// 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 newSeedling.EnableUniCommitment {
// 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.GenesisPacket != nil {
return fmt.Errorf("attempting to add " +
"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, it is confirmed that the batch contains seedlings, and
// 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.EnableUniCommitment != newSeedling.EnableUniCommitment {
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.EnableUniCommitment && !newSeedling.EnableUniCommitment {
return nil
}

// At this stage, the universe commitment flag is enabled for both the
// seedling and the batch, and the batch contains at least one seedling.
//
// As a result, the candidate seedling must have a group anchor that is
// already part of the batch.
if newSeedling.GroupAnchor == nil {
return fmt.Errorf("group anchor unspecified for seedling " +
"with universe commitment flag enabled")
}

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.EnableUniCommitment = newSeedling.EnableUniCommitment
}

// Add the seedling to the batch.
m.Seedlings[newSeedling.AssetName] = &newSeedling

return nil
}

// ToMintingBatch creates a new MintingBatch from a VerboseBatch.
func (v *VerboseBatch) ToMintingBatch() *MintingBatch {
newBatch := v.MintingBatch.Copy()
Expand Down
4 changes: 3 additions & 1 deletion tapgarden/mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -659,7 +659,9 @@ func (m *MockKeyRing) DeriveNextKey(ctx context.Context,
select {
case m.ReqKeys <- &desc:
case <-ctx.Done():
return keychain.KeyDescriptor{}, fmt.Errorf("shutting down")
// The context is done but we can still return the key
// descriptor because it was derived successfully.
return desc, nil
}

return desc, nil
Expand Down
75 changes: 66 additions & 9 deletions tapgarden/planter.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"github.com/btcsuite/btcd/chaincfg/chainhash"
"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"
Expand Down Expand Up @@ -81,6 +82,11 @@

// PlanterConfig is the main config for the ChainPlanter.
type PlanterConfig struct {
// ChainParams defines the chain parameters for the target blockchain
// network. It specifies whether the network is Bitcoin mainnet or
// testnet.
ChainParams address.ChainParams

GardenKit

// ProofUpdates is the storage backend for updated proofs.
Expand Down Expand Up @@ -656,6 +662,34 @@
return newBatch, nil
}

// setPreCommitmentOutput sets the
func (c *ChainPlanter) setPreCommitmentOutput(
fundedGenesisPkt *tapsend.FundedPsbt) error {

// Derive a new key to lock the change output.
ctx, cancel := c.WithCtxQuit()
defer cancel()

newKey, err := c.cfg.KeyRing.DeriveNextKey(
ctx, asset.TaprootAssetsKeyFamily,
)
if err != nil {
return fmt.Errorf("unable to derive pre-commitment output "+
"key: %w", err)
}
newKey = newKey

Check failure on line 680 in tapgarden/planter.go

View workflow job for this annotation

GitHub Actions / Lint check

assign: self-assignment of newKey to newKey (govet)

//bip32Derivation, trBip32Derivation := tappsbt.Bip32DerivationFromKeyDesc(

Check failure on line 682 in tapgarden/planter.go

View workflow job for this annotation

GitHub Actions / Lint check

commentFormatting: put a space between `//` and comment text (gocritic)
// newKey, c.cfg.ChainParams.HDCoinType,
//)
//bip32Derivation = bip32Derivation
//trBip32Derivation = trBip32Derivation

// Set in PSBT output and inner unsigned transaction.

return 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
Expand Down Expand Up @@ -739,6 +773,14 @@
log.Infof("Funded GenesisPacket for batch: %x", batchKey)
log.Tracef("GenesisPacket: %v", spew.Sdump(fundedGenesisPkt))

// TODO(ffranr): Lock change output here if EnableUniCommitment set for
// batch.
err = c.setPreCommitmentOutput(fundedGenesisPkt)
if err != nil {
return nil, fmt.Errorf("unable to lock pre-commitment "+
"output: %w", err)
}

return fundedGenesisPkt, nil
}

Expand Down Expand Up @@ -2279,6 +2321,12 @@
)
}

// If the universe commitment feature is enabled for the group
// genesis asset, we ensure it is also enabled for the seedling.
if anchorMeta.UniCommitTRKey.IsSome() {
req.EnableUniCommitment = true
}

err = req.validateGroupKey(*groupInfo, anchorMeta)
if err != nil {
return err
Expand Down Expand Up @@ -2351,35 +2399,44 @@
// No batch, so we'll create a new one with only this seedling as part
// of the batch.
case c.pendingBatch == nil:
newBatch, err := c.newBatch()
var err error
c.pendingBatch, err = c.newBatch()
if err != nil {
return err
}

log.Infof("Adding %v to new MintingBatch", req)
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 {
Expand Down
Loading
Loading