From 70c1d2f268cfbb33ea8e077c6e11cdb2b63dcf64 Mon Sep 17 00:00:00 2001 From: ffranr Date: Thu, 23 Jan 2025 23:21:32 +0000 Subject: [PATCH 01/26] tapgarden: add MintingBatch.AddSeedling method Introduce MintingBatch.AddSeedling, a method for attempting to add seedlings to a batch. Future commits will extend this method with validation logic to ensure compatibility of the seedling with others already in the batch. --- tapgarden/batch.go | 7 +++++++ tapgarden/planter.go | 26 ++++++++++++++++++-------- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/tapgarden/batch.go b/tapgarden/batch.go index 7aa0c95cc..919ef076d 100644 --- a/tapgarden/batch.go +++ b/tapgarden/batch.go @@ -301,6 +301,13 @@ func (m *MintingBatch) HasSeedlings() bool { return len(m.Seedlings) != 0 } +// AddSeedling adds a new seedling to the batch. +func (m *MintingBatch) AddSeedling(newSeedling Seedling) error { + 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/planter.go b/tapgarden/planter.go index 435ecbb01..7e72f6034 100644 --- a/tapgarden/planter.go +++ b/tapgarden/planter.go @@ -2363,30 +2363,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 { From d65d319cab2d9491c0ec3db0899894d02bb7e57e Mon Sep 17 00:00:00 2001 From: ffranr Date: Thu, 23 Jan 2025 23:53:00 +0000 Subject: [PATCH 02/26] tapgarden: add UniverseCommitments to MintingBatch with validation Add the UniverseCommitments field to the MintingBatch structure, populating it when a seedling is added to the batch. Include validation logic to ensure that for a batch with uni commitment enabled, all seedlings share the same asset group key. --- tapgarden/batch.go | 116 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/tapgarden/batch.go b/tapgarden/batch.go index 919ef076d..3c580904d 100644 --- a/tapgarden/batch.go +++ b/tapgarden/batch.go @@ -64,6 +64,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,8 +309,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 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, 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.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 + } + + // 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. The group anchor must have been added to + // the batch before the candidate seedling. + 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.UniverseCommitments = newSeedling.UniverseCommitments + } + + // Add the seedling to the batch. m.Seedlings[newSeedling.AssetName] = &newSeedling return nil From 7d8a471a684f483544351012bb0feed086b64d01 Mon Sep 17 00:00:00 2001 From: ffranr Date: Thu, 20 Feb 2025 17:00:02 +0000 Subject: [PATCH 03/26] tapgarden: set seedling delegation key during preparation Set the seedling's delegation key as part of the preparation process before batch inclusion. --- tapgarden/planter.go | 63 ++++++++++++++++++++++++++++++++++++++++++- tapgarden/seedling.go | 4 +++ 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/tapgarden/planter.go b/tapgarden/planter.go index 7e72f6034..83ed9413d 100644 --- a/tapgarden/planter.go +++ b/tapgarden/planter.go @@ -2245,12 +2245,73 @@ 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 + } + if err := req.validateFields(); err != nil { return err } diff --git a/tapgarden/seedling.go b/tapgarden/seedling.go index 6394234ef..5457621ac 100644 --- a/tapgarden/seedling.go +++ b/tapgarden/seedling.go @@ -102,6 +102,10 @@ type Seedling struct { // 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. From b71b1441e437cf839cbef32bb8051608dd3b427e Mon Sep 17 00:00:00 2001 From: ffranr Date: Thu, 20 Feb 2025 17:02:05 +0000 Subject: [PATCH 04/26] tapgarden: set seedling metadata during preparation Modify `prepAssetSeedling` to set the seedling metadata fields `UniverseCommitments` and `DelegationKey`. This is handled here instead of in rpcserver. --- tapgarden/planter.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tapgarden/planter.go b/tapgarden/planter.go index 83ed9413d..f8807ecab 100644 --- a/tapgarden/planter.go +++ b/tapgarden/planter.go @@ -2312,6 +2312,26 @@ func (c *ChainPlanter) prepAssetSeedling(ctx context.Context, 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 } From c460a69955298a49286658949f498062578a42fe Mon Sep 17 00:00:00 2001 From: ffranr Date: Wed, 19 Feb 2025 23:29:29 +0000 Subject: [PATCH 05/26] tapgarden: enforce batch-level delegation key validation Ensure that all candidate seedlings in a batch comply with universe commitment feature restrictions. If the feature is enabled, all seedlings must have a delegation key, and the key must be identical across the batch. --- tapgarden/batch.go | 75 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/tapgarden/batch.go b/tapgarden/batch.go index 3c580904d..e2e1589be 100644 --- a/tapgarden/batch.go +++ b/tapgarden/batch.go @@ -309,6 +309,74 @@ 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 { @@ -418,6 +486,13 @@ func (m *MintingBatch) AddSeedling(newSeedling Seedling) error { 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. m.Seedlings[newSeedling.AssetName] = &newSeedling From 6af4e280c2252309665e0967a194fa2382e9ab23 Mon Sep 17 00:00:00 2001 From: ffranr Date: Thu, 13 Feb 2025 02:12:53 +0000 Subject: [PATCH 06/26] tapgarden: allow universe commitments for new minting tranches Modified seedling validation to permit enabling the universe commitments feature for new minting tranches. --- tapgarden/seedling.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tapgarden/seedling.go b/tapgarden/seedling.go index 5457621ac..363860e94 100644 --- a/tapgarden/seedling.go +++ b/tapgarden/seedling.go @@ -237,10 +237,12 @@ 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 From 53c6a96655002e5c97db2ca958999487c125b492 Mon Sep 17 00:00:00 2001 From: ffranr Date: Thu, 13 Feb 2025 16:06:55 +0000 Subject: [PATCH 07/26] tapgarden: fix doc: group internal key is not the default delegation key Clarified that the group internal key is not used as the delegation key by default. --- tapgarden/seedling.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tapgarden/seedling.go b/tapgarden/seedling.go index 363860e94..214e742fe 100644 --- a/tapgarden/seedling.go +++ b/tapgarden/seedling.go @@ -246,8 +246,7 @@ func validateAnchorMeta(seedlingMeta *proof.MetaReveal, } // 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") From 2fa7fb82c5c47b390c4b8ae91b7a934883b27ba3 Mon Sep 17 00:00:00 2001 From: ffranr Date: Thu, 13 Feb 2025 23:52:36 +0000 Subject: [PATCH 08/26] proof: move SizableInteger closer to use Improve code health/readability. --- proof/meta.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/proof/meta.go b/proof/meta.go index 63ed61130..aebe7b5f5 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. @@ -247,6 +241,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 { From c87637bc6f3f55238e99e95c046f2f0633941c5b Mon Sep 17 00:00:00 2001 From: ffranr Date: Mon, 17 Feb 2025 12:23:15 +0000 Subject: [PATCH 09/26] rpcserver: remove seedling metadata validation Seedling metadata validation is now handled in ChainPlanter.prepAssetSeedling, so it is no longer needed in rpcserver. --- rpcserver.go | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/rpcserver.go b/rpcserver.go index 685dfa402..50a24d238 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 @@ -607,7 +600,7 @@ func (r *rpcServer) MintAsset(ctx context.Context, AssetName: req.Asset.Name, Amount: req.Asset.Amount, EnableEmission: req.Asset.NewGroupedAsset, - Meta: seedlingMeta, + Meta: &seedlingMeta, UniverseCommitments: req.Asset.UniverseCommitments, } From 6694babe894fa0486fceda8681d70fdcf65b3e5a Mon Sep 17 00:00:00 2001 From: ffranr Date: Mon, 17 Feb 2025 12:50:57 +0000 Subject: [PATCH 10/26] proof: MetaReveal.Validate accounts for UniverseCommitments --- proof/meta.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/proof/meta.go b/proof/meta.go index aebe7b5f5..10aba50d0 100644 --- a/proof/meta.go +++ b/proof/meta.go @@ -228,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 @@ -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{}) From f7a3c9a2e5f02b07f12d68e2a59c263d8ebf5726 Mon Sep 17 00:00:00 2001 From: ffranr Date: Tue, 4 Feb 2025 15:51:57 +0000 Subject: [PATCH 11/26] tapgarden+tapcfg: populate chain params in ChainPlanter This allows deriving a pre-commitment output script key when the universe commitment feature is enabled. --- tapcfg/server.go | 7 +++---- tapgarden/planter.go | 6 ++++++ tapgarden/planter_test.go | 1 + 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/tapcfg/server.go b/tapcfg/server.go index a91d51a75..11ee0311e 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/tapgarden/planter.go b/tapgarden/planter.go index f8807ecab..a408b49d3 100644 --- a/tapgarden/planter.go +++ b/tapgarden/planter.go @@ -16,6 +16,7 @@ import ( "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" @@ -83,6 +84,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 diff --git a/tapgarden/planter_test.go b/tapgarden/planter_test.go index a5c0905c0..edcd10226 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, }) From 2c069fcc0b277818c64c44ecf30da769749901fd Mon Sep 17 00:00:00 2001 From: ffranr Date: Fri, 21 Feb 2025 13:22:13 +0000 Subject: [PATCH 12/26] tapgarden: ensure change output index is set after anchor funding Add a check to verify that the change output index is defined after anchor funding. Since the funding request guarantees a change output, the index should be known. Modify the `MockWalletAnchor.FundPsbt` method to compute a reasonable change output index. --- tapgarden/mock.go | 4 ++++ tapgarden/planter.go | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/tapgarden/mock.go b/tapgarden/mock.go index 94cfa9712..8275ce58d 100644 --- a/tapgarden/mock.go +++ b/tapgarden/mock.go @@ -182,6 +182,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 a408b49d3..a8b8f7fb5 100644 --- a/tapgarden/planter.go +++ b/tapgarden/planter.go @@ -749,6 +749,12 @@ func (c *ChainPlanter) fundGenesisPsbt(ctx context.Context, return nil, fmt.Errorf("unable to fund psbt: %w", err) } + // Sanity check the funded PSBT. + if fundedGenesisPkt.ChangeOutputIndex == -1 { + return nil, 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)) From c8d053af9eb5986860af88f3bb2a7ec718eb4acf Mon Sep 17 00:00:00 2001 From: ffranr Date: Fri, 21 Feb 2025 15:28:04 +0000 Subject: [PATCH 13/26] tapgarden: refactor fundGenesisPsbt to extract unfundedAnchorPsbt method --- tapgarden/planter.go | 38 ++++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/tapgarden/planter.go b/tapgarden/planter.go index a8b8f7fb5..470d13158 100644 --- a/tapgarden/planter.go +++ b/tapgarden/planter.go @@ -669,6 +669,26 @@ func (c *ChainPlanter) newBatch() (*MintingBatch, error) { return newBatch, nil } +// unfundedAnchorPsbt creates an unfunded PSBT packet for the minting anchor +// transaction. +func unfundedAnchorPsbt() (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()) + + // 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 +} + // 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 @@ -681,18 +701,16 @@ func (c *ChainPlanter) fundGenesisPsbt(ctx context.Context, 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) + // Construct an unfunded anchor PSBT which will eventually become a + // funded minting anchor transaction. + genesisPkt, err := unfundedAnchorPsbt() if err != nil { - return nil, fmt.Errorf("unable to make psbt packet: %w", err) + return nil, fmt.Errorf("unable to create anchor template tx: "+ + "%w", err) } + log.Tracef("Unfunded batch anchor PSBT: %v", spew.Sdump(genesisPkt)) - log.Infof("creating skeleton PSBT for batch: %x", batchKey) - log.Tracef("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 @@ -743,7 +761,7 @@ 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) From 3625751125c56cfff687bcae6e3cdaf8a2e433f9 Mon Sep 17 00:00:00 2001 From: ffranr Date: Fri, 21 Feb 2025 16:17:43 +0000 Subject: [PATCH 14/26] tapgarden: add pre-commitment output to mint anchor transaction Modify anchor transaction construction to include a pre-commitment output when the universe commitments feature is enabled. Add a new type to encapsulate the pre-commitment output metadata along with the funded mint anchor transaction. This allows us to store and retrieve pre-commitment-related information from the database when storing or retrieving the batch mint transaction. --- tapgarden/planter.go | 397 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 385 insertions(+), 12 deletions(-) diff --git a/tapgarden/planter.go b/tapgarden/planter.go index 470d13158..4b6005ef0 100644 --- a/tapgarden/planter.go +++ b/tapgarden/planter.go @@ -14,6 +14,7 @@ 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" @@ -24,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" ) @@ -669,9 +671,81 @@ 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() (psbt.Packet, error) { +func unfundedAnchorPsbt(preCommitmentTxOut fn.Option[wire.TxOut]) (psbt.Packet, + error) { + var zero psbt.Packet // Construct a template transaction for our minting anchor transaction. @@ -680,6 +754,13 @@ func unfundedAnchorPsbt() (psbt.Packet, error) { // 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 { @@ -689,6 +770,104 @@ func unfundedAnchorPsbt() (psbt.Packet, error) { 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 @@ -697,15 +876,46 @@ func unfundedAnchorPsbt() (psbt.Packet, 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) + // 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) + } + + // 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] { + txOut, err := p.TxOut() + return lfn.NewResult(txOut, err) + }, + ).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() + genesisPkt, err := unfundedAnchorPsbt(preCommitmentTxOut) if err != nil { - return nil, fmt.Errorf("unable to create anchor template tx: "+ + return zero, fmt.Errorf("unable to create anchor template tx: "+ "%w", err) } log.Tracef("Unfunded batch anchor PSBT: %v", spew.Sdump(genesisPkt)) @@ -726,7 +936,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) } @@ -736,7 +946,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 @@ -749,7 +959,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: @@ -764,19 +974,65 @@ func (c *ChainPlanter) fundGenesisPsbt(ctx context.Context, 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 nil, fmt.Errorf("undefined change output index in " + + 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 @@ -1757,13 +2013,16 @@ 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 + // TODO(ffranr): In a future commit, we will replace the + // GenesisPacket field type so as to carry along the + // pre-commitment output info. + batch.GenesisPacket = &mintAnchorTx.FundedPsbt return nil } @@ -2680,3 +2939,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 +} From e372c2edadcd5c2a339893cda0489ef641193023 Mon Sep 17 00:00:00 2001 From: ffranr Date: Mon, 24 Feb 2025 22:27:03 +0000 Subject: [PATCH 15/26] tapdb: modify BindMintingBatchWithTx to return batch ID Update the SQL query in BindMintingBatchWithTx to return the batch ID. This ID will be used for populating the universe commitment anchor rows in the database. --- tapdb/asset_minting.go | 10 ++++++---- tapdb/sqlc/assets.sql.go | 13 ++++++++----- tapdb/sqlc/querier.go | 2 +- tapdb/sqlc/queries/assets.sql | 7 ++++--- 4 files changed, 19 insertions(+), 13 deletions(-) diff --git a/tapdb/asset_minting.go b/tapdb/asset_minting.go index a0d54c244..e6846df2c 100644 --- a/tapdb/asset_minting.go +++ b/tapdb/asset_minting.go @@ -199,7 +199,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. @@ -382,7 +383,7 @@ func (a *AssetMintingStore) CommitMintingBatch(ctx context.Context, ErrUpsertGenesisPoint, err) } - err = q.BindMintingBatchWithTx(ctx, BatchChainUpdate{ + _, err = q.BindMintingBatchWithTx(ctx, BatchChainUpdate{ RawKey: rawBatchKey, MintingTxPsbt: psbtBuf.Bytes(), ChangeOutputIndex: sqlInt32(changeIdx), @@ -1301,7 +1302,7 @@ func (a *AssetMintingStore) CommitBatchTx(ctx context.Context, return fmt.Errorf("%w: %w", ErrUpsertGenesisPoint, err) } - return q.BindMintingBatchWithTx(ctx, BatchChainUpdate{ + _, err = q.BindMintingBatchWithTx(ctx, BatchChainUpdate{ RawKey: rawBatchKey, MintingTxPsbt: psbtBuf.Bytes(), ChangeOutputIndex: sqlInt32( @@ -1309,6 +1310,7 @@ func (a *AssetMintingStore) CommitBatchTx(ctx context.Context, ), GenesisID: sqlInt64(genesisPointID), }) + return err }) } @@ -1459,7 +1461,7 @@ func (a *AssetMintingStore) AddSproutsToBatch(ctx context.Context, if err := genesisPacket.Pkt.Serialize(&psbtBuf); err != nil { return fmt.Errorf("%w: %w", ErrEncodePsbt, err) } - err = q.BindMintingBatchWithTx(ctx, BatchChainUpdate{ + _, err = q.BindMintingBatchWithTx(ctx, BatchChainUpdate{ RawKey: rawBatchKey, MintingTxPsbt: psbtBuf.Bytes(), ChangeOutputIndex: sqlInt32( diff --git a/tapdb/sqlc/assets.sql.go b/tapdb/sqlc/assets.sql.go index 769c1ad7b..fa4212800 100644 --- a/tapdb/sqlc/assets.sql.go +++ b/tapdb/sqlc/assets.sql.go @@ -353,7 +353,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,9 +361,10 @@ WITH target_batch AS ( ON batches.batch_id = keys.key_id WHERE keys.raw_key = $1 ) -UPDATE asset_minting_batches +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) +RETURNING batch_id ` type BindMintingBatchWithTxParams struct { @@ -373,14 +374,16 @@ type BindMintingBatchWithTxParams struct { GenesisID sql.NullInt64 } -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.GenesisID, ) - return err + var batch_id int64 + err := row.Scan(&batch_id) + return batch_id, err } const ConfirmChainAnchorTx = `-- name: ConfirmChainAnchorTx :exec diff --git a/tapdb/sqlc/querier.go b/tapdb/sqlc/querier.go index ef76b396c..eeeca62c0 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) diff --git a/tapdb/sqlc/queries/assets.sql b/tapdb/sqlc/queries/assets.sql index 1b7a03d3c..37d1a91bd 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,10 @@ WITH target_batch AS ( ON batches.batch_id = keys.key_id WHERE keys.raw_key = $1 ) -UPDATE asset_minting_batches +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); +WHERE batch_id IN (SELECT batch_id FROM target_batch) +RETURNING batch_id; -- name: BindMintingBatchWithTapSibling :exec WITH target_batch AS ( From cecaeea81bc06abec35631c63abd9d35c6a2a53e Mon Sep 17 00:00:00 2001 From: ffranr Date: Mon, 24 Feb 2025 22:42:38 +0000 Subject: [PATCH 16/26] tapdb: add mint asset anchor output idx and universe commitments support Add a new `assets_output_index` column to the `asset_minting_batches` table to store the output index of the asset anchor transaction. Populate this column for existing entries based on the `change_output_index`. Introduce a `universe_commitments` flag column to indicate whether universe commitments are enabled for a minting batch, defaulting to `FALSE`. Create the `mint_anchor_uni_commitments` table to associate a mint batch anchor transaction with its universe commitments, storing the pre-commitment output index, Taproot internal key, and asset group pub key. --- tapdb/migrations.go | 2 +- tapdb/sqlc/assets.sql.go | 161 +++++++++++++----- ...00030_mint_anchor_uni_commitments.down.sql | 11 ++ .../000030_mint_anchor_uni_commitments.up.sql | 44 +++++ tapdb/sqlc/models.go | 26 ++- tapdb/sqlc/querier.go | 6 + tapdb/sqlc/queries/assets.sql | 23 ++- tapdb/sqlc/schemas/generated_schema.sql | 18 +- 8 files changed, 236 insertions(+), 55 deletions(-) create mode 100644 tapdb/sqlc/migrations/000030_mint_anchor_uni_commitments.down.sql create mode 100644 tapdb/sqlc/migrations/000030_mint_anchor_uni_commitments.up.sql diff --git a/tapdb/migrations.go b/tapdb/migrations.go index abc2b3400..fbe40715f 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 fa4212800..5d46b3212 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, @@ -362,16 +366,19 @@ WITH target_batch AS ( WHERE keys.raw_key = $1 ) UPDATE asset_minting_batches -SET minting_tx_psbt = $2, change_output_index = $3, genesis_id = $4 +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) (int64, error) { @@ -379,7 +386,9 @@ func (q *Queries) BindMintingBatchWithTx(ctx context.Context, arg BindMintingBat arg.RawKey, arg.MintingTxPsbt, arg.ChangeOutputIndex, + arg.AssetsOutputIndex, arg.GenesisID, + arg.UniverseCommitments, ) var batch_id int64 err := row.Scan(&batch_id) @@ -1505,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 @@ -1517,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 @@ -1525,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) { @@ -1551,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, @@ -1560,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 @@ -1568,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) { @@ -1600,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, @@ -2939,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 000000000..09d43dd60 --- /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 000000000..512d11056 --- /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 a0ffe55fe..17555cec5 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 eeeca62c0..de2823b2d 100644 --- a/tapdb/sqlc/querier.go +++ b/tapdb/sqlc/querier.go @@ -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 37d1a91bd..c3252bab8 100644 --- a/tapdb/sqlc/queries/assets.sql +++ b/tapdb/sqlc/queries/assets.sql @@ -539,7 +539,8 @@ WITH target_batch AS ( WHERE keys.raw_key = $1 ) UPDATE asset_minting_batches -SET minting_tx_psbt = $2, change_output_index = $3, genesis_id = $4 +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; @@ -1016,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 ceb81fea4..c9701eef9 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, From 5fb69eb7f994fb88a52760116cc22f45e8244bb1 Mon Sep 17 00:00:00 2001 From: ffranr Date: Sat, 22 Feb 2025 00:15:42 +0000 Subject: [PATCH 17/26] tapgarden+tapdb: MintingBatch.GenesisPacket to FundedMintAnchorPsbt type Update the MintingBatch.GenesisPacket field to use the FundedMintAnchorPsbt type to include pre-commitment output information. --- tapdb/asset_minting.go | 69 +++++++++++++++++++++++++++++++++---- tapdb/asset_minting_test.go | 19 +++++++--- tapgarden/batch.go | 3 +- tapgarden/caretaker.go | 52 +++++++++------------------- tapgarden/interface.go | 4 +-- tapgarden/mock.go | 9 +++-- tapgarden/planter.go | 30 +++++----------- tapgarden/planter_test.go | 10 +++--- 8 files changed, 116 insertions(+), 80 deletions(-) diff --git a/tapdb/asset_minting.go b/tapdb/asset_minting.go index e6846df2c..9a9700ac3 100644 --- a/tapdb/asset_minting.go +++ b/tapdb/asset_minting.go @@ -239,6 +239,11 @@ 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) } var ( @@ -1164,11 +1169,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, } } @@ -1283,7 +1336,8 @@ 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() @@ -1417,7 +1471,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 diff --git a/tapdb/asset_minting_test.go b/tapdb/asset_minting_test.go index 8d84f4d4d..789bc499c 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 diff --git a/tapgarden/batch.go b/tapgarden/batch.go index e2e1589be..a2a6563fd 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. diff --git a/tapgarden/caretaker.go b/tapgarden/caretaker.go index 633c958e6..a12159ea9 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 d2d3fbb8a..9a972fe67 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 8275ce58d..ff25febb3 100644 --- a/tapgarden/mock.go +++ b/tapgarden/mock.go @@ -66,9 +66,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, }, } } diff --git a/tapgarden/planter.go b/tapgarden/planter.go index 4b6005ef0..b9a241028 100644 --- a/tapgarden/planter.go +++ b/tapgarden/planter.go @@ -1323,12 +1323,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 { @@ -1565,12 +1566,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, @@ -2019,10 +2015,7 @@ func (c *ChainPlanter) fundBatch(ctx context.Context, params FundParams, "batch: %x %w", batchKey[:], err) } - // TODO(ffranr): In a future commit, we will replace the - // GenesisPacket field type so as to carry along the - // pre-commitment output info. - batch.GenesisPacket = &mintAnchorTx.FundedPsbt + batch.GenesisPacket = &mintAnchorTx return nil } @@ -2070,7 +2063,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 @@ -2151,12 +2144,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, diff --git a/tapgarden/planter_test.go b/tapgarden/planter_test.go index edcd10226..52275699f 100644 --- a/tapgarden/planter_test.go +++ b/tapgarden/planter_test.go @@ -1148,14 +1148,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. @@ -1251,7 +1251,7 @@ func testMintingTicker(t *mintingTestHarness) { // that the batch is already funded. t.assertBatchProgressing() currentBatch := t.fetchLastBatch() - t.assertBatchGenesisTx(currentBatch.GenesisPacket) + t.assertBatchGenesisTx(¤tBatch.GenesisPacket.FundedPsbt) // Now that the batch has been ticked, and the caretaker started, there // should no longer be a pending batch. @@ -1352,7 +1352,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. @@ -1761,7 +1761,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) From 22bb6e3233bec759c8d37b13f0f8563a12096859 Mon Sep 17 00:00:00 2001 From: ffranr Date: Tue, 25 Feb 2025 17:47:19 +0000 Subject: [PATCH 18/26] tapdb: refactor BindMintingBatchWithTx into insertMintAnchorTx Refactor BindMintingBatchWithTx into insertMintAnchorTx, which will also handle inserting pre-commitment information into the database. insertMintAnchorTx will also ensure that the genesis point is in the database. --- tapdb/asset_minting.go | 156 ++++++++++++++++++++++++++--------------- 1 file changed, 100 insertions(+), 56 deletions(-) diff --git a/tapdb/asset_minting.go b/tapdb/asset_minting.go index 9a9700ac3..075ebc0c7 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 @@ -244,6 +248,11 @@ type PendingAssetStore interface { // 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 ( @@ -317,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 @@ -371,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) } } @@ -1340,31 +1405,19 @@ func (a *AssetMintingStore) CommitBatchTx(ctx context.Context, 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) } - _, err = q.BindMintingBatchWithTx(ctx, BatchChainUpdate{ - RawKey: rawBatchKey, - MintingTxPsbt: psbtBuf.Bytes(), - ChangeOutputIndex: sqlInt32( - genesisPacket.ChangeOutputIndex, - ), - GenesisID: sqlInt64(genesisPointID), - }) - return err + return nil }) } @@ -1501,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 { @@ -1509,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. From d49576e66cca389bfb5dd29237fb171e66228fa3 Mon Sep 17 00:00:00 2001 From: ffranr Date: Fri, 28 Feb 2025 15:26:37 +0000 Subject: [PATCH 19/26] tapdb: add unit test for mint anchor uni commitment queries Add unit tests for the SQL queries `UpsertMintAnchorUniCommitment` and `FetchMintAnchorUniCommitment`. --- tapdb/asset_minting_test.go | 138 ++++++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) diff --git a/tapdb/asset_minting_test.go b/tapdb/asset_minting_test.go index 789bc499c..acbedcad2 100644 --- a/tapdb/asset_minting_test.go +++ b/tapdb/asset_minting_test.go @@ -1770,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()) From 74bd5d8f2aa6052c3855414b99fd330215c32976 Mon Sep 17 00:00:00 2001 From: ffranr Date: Mon, 3 Mar 2025 12:57:01 +0000 Subject: [PATCH 20/26] tapgarden: refactor fee rate calculation from fundGenesisPsbt Separate the mint anchor transaction fee rate calculation from fundGenesisPsbt into anchorTxFeeRate. This refactor is part of a broader effort to simplify calling fundGenesisPsbt from unit tests. --- tapgarden/planter.go | 125 ++++++++++++++++++++++++------------------- 1 file changed, 70 insertions(+), 55 deletions(-) diff --git a/tapgarden/planter.go b/tapgarden/planter.go index b9a241028..a5b81ea57 100644 --- a/tapgarden/planter.go +++ b/tapgarden/planter.go @@ -868,6 +868,65 @@ func anchorTxOutputIndexes(fundedPsbt tapsend.FundedPsbt, }, nil } +// anchorTxFeeRate computes the fee rate for the anchor transaction. If a fee +// rate is manually assigned for the batch, it is used. Otherwise, the fee rate +// is estimated based on the current network conditions. +func (c *ChainPlanter) anchorTxFeeRate(ctx context.Context, + manualFeeRate *chainfee.SatPerKWeight) (chainfee.SatPerKWeight, error) { + + // Compute the anchor transaction fee rate. + var feeRate chainfee.SatPerKWeight + switch { + // If a fee rate was manually assigned for this batch, use that instead + // of a fee rate estimate. + case manualFeeRate != nil: + feeRate = *manualFeeRate + log.Infof("using manual fee rate for batch: %s, %d sat/vB", + feeRate.String(), + feeRate.FeePerKVByte()/1000) + + default: + feeRate, err := c.cfg.ChainBridge.EstimateFee( + ctx, GenesisConfTarget, + ) + if err != nil { + return 0, fmt.Errorf("unable to estimate fee: %w", + err) + } + + log.Infof("estimated fee rate for batch: %s", + feeRate.FeePerKVByte().String()) + } + + minRelayFee, err := c.cfg.Wallet.MinRelayFee(ctx) + if err != nil { + return 0, fmt.Errorf("unable to obtain minrelayfee: %w", err) + } + + // If the fee rate is below the minimum relay fee, we'll + // bump it up. + if feeRate < minRelayFee { + switch { + // If a fee rate was manually assigned for this batch, we err + // out, otherwise we silently bump the feerate. + case manualFeeRate != nil: + // This case should already have been handled by the + // `checkFeeRateSanity` of `rpcserver.go`. We check here + // again to be safe. + return 0, fmt.Errorf("feerate does not meet "+ + "minrelayfee: (fee_rate=%s, minrelayfee=%s)", + feeRate.String(), minRelayFee.String()) + default: + log.Infof("Bump fee rate for batch to meet "+ + "minrelayfee from %s to %s", + feeRate.String(), minRelayFee.String()) + feeRate = minRelayFee + } + } + + return feeRate, 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 @@ -876,7 +935,7 @@ func anchorTxOutputIndexes(fundedPsbt tapsend.FundedPsbt, // that's dependent on the genesis outpoint. func (c *ChainPlanter) fundGenesisPsbt(ctx context.Context, batchKey asset.SerializedKey, - manualFeeRate *chainfee.SatPerKWeight) (FundedMintAnchorPsbt, error) { + feeRate chainfee.SatPerKWeight) (FundedMintAnchorPsbt, error) { var zero FundedMintAnchorPsbt log.Infof("Attempting to fund batch: %x", batchKey) @@ -920,56 +979,6 @@ func (c *ChainPlanter) fundGenesisPsbt(ctx context.Context, } 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 - // of a fee rate estimate. - case manualFeeRate != nil: - feeRate = *manualFeeRate - log.Infof("using manual fee rate for batch: %x, %s, %d sat/vB", - batchKey[:], feeRate.String(), - feeRate.FeePerKVByte()/1000) - - default: - feeRate, err = c.cfg.ChainBridge.EstimateFee( - ctx, GenesisConfTarget, - ) - if err != nil { - return zero, fmt.Errorf("unable to estimate fee: %w", - err) - } - - log.Infof("estimated fee rate for batch: %x, %s", - batchKey[:], feeRate.FeePerKVByte().String()) - } - - minRelayFee, err := c.cfg.Wallet.MinRelayFee(ctx) - if err != nil { - return zero, fmt.Errorf("unable to obtain minrelayfee: %w", err) - } - - // If the fee rate is below the minimum relay fee, we'll - // bump it up. - if feeRate < minRelayFee { - switch { - // If a fee rate was manually assigned for this batch, we err - // out, otherwise we silently bump the feerate. - case manualFeeRate != nil: - // This case should already have been handled by the - // `checkFeeRateSanity` of `rpcserver.go`. We check here - // again to be safe. - return zero, fmt.Errorf("feerate does not meet "+ - "minrelayfee: (fee_rate=%s, minrelayfee=%s)", - feeRate.String(), minRelayFee.String()) - default: - log.Infof("Bump fee rate for batch %x to meet "+ - "minrelayfee from %s to %s", batchKey[:], - feeRate.String(), minRelayFee.String()) - feeRate = minRelayFee - } - } - fundedGenesisPkt, err := c.cfg.Wallet.FundPsbt( ctx, &genesisPkt, 1, feeRate, -1, ) @@ -1982,15 +1991,15 @@ func (c *ChainPlanter) fundBatch(ctx context.Context, params FundParams, workingBatch *MintingBatch) error { var ( - feeRate *chainfee.SatPerKWeight - rootHash *chainhash.Hash - err error + manualFeeRate *chainfee.SatPerKWeight + rootHash *chainhash.Hash + err error ) // If a tapscript tree was specified for this batch, we'll store it on // disk. The caretaker we start for this batch will use it when deriving // the final Taproot output key. - feeRate = params.FeeRate.UnwrapToPtr() + manualFeeRate = params.FeeRate.UnwrapToPtr() params.SiblingTapTree.WhenSome(func(tn asset.TapscriptTreeNodes) { rootHash, err = c.cfg.TreeStore.StoreTapscriptTree(ctx, tn) }) @@ -2008,6 +2017,12 @@ func (c *ChainPlanter) fundBatch(ctx context.Context, params FundParams, } // Fund the batch with the specified fee rate. + feeRate, err := c.anchorTxFeeRate(ctx, manualFeeRate) + if err != nil { + return fmt.Errorf("unable to determine anchor TX "+ + "fee rate: %w", err) + } + batchKey := asset.ToSerialized(batch.BatchKey.PubKey) mintAnchorTx, err := c.fundGenesisPsbt(ctx, batchKey, feeRate) if err != nil { From fbc9e296b157110582705d41d83d8611a5389151 Mon Sep 17 00:00:00 2001 From: ffranr Date: Mon, 3 Mar 2025 13:11:24 +0000 Subject: [PATCH 21/26] tapgarden: refactor fundGenesisPsbt to pass wallet funding as closure Extract the wallet funding call into a closure that is passed as an argument. This prepares fundGenesisPsbt to become a standalone function, making it easier to call in unit tests. --- tapgarden/planter.go | 36 +++++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/tapgarden/planter.go b/tapgarden/planter.go index a5b81ea57..9722e70a4 100644 --- a/tapgarden/planter.go +++ b/tapgarden/planter.go @@ -927,6 +927,10 @@ func (c *ChainPlanter) anchorTxFeeRate(ctx context.Context, return feeRate, nil } +// WalletFundPsbt is a function that funds a PSBT packet. +type WalletFundPsbt = func(ctx context.Context, + anchorPkt psbt.Packet) (tapsend.FundedPsbt, error) + // 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 @@ -935,7 +939,7 @@ func (c *ChainPlanter) anchorTxFeeRate(ctx context.Context, // that's dependent on the genesis outpoint. func (c *ChainPlanter) fundGenesisPsbt(ctx context.Context, batchKey asset.SerializedKey, - feeRate chainfee.SatPerKWeight) (FundedMintAnchorPsbt, error) { + walletFundPsbt WalletFundPsbt) (FundedMintAnchorPsbt, error) { var zero FundedMintAnchorPsbt log.Infof("Attempting to fund batch: %x", batchKey) @@ -979,9 +983,7 @@ func (c *ChainPlanter) fundGenesisPsbt(ctx context.Context, } log.Tracef("Unfunded batch anchor PSBT: %v", spew.Sdump(genesisPkt)) - fundedGenesisPkt, err := c.cfg.Wallet.FundPsbt( - ctx, &genesisPkt, 1, feeRate, -1, - ) + fundedGenesisPkt, err := walletFundPsbt(ctx, genesisPkt) if err != nil { return zero, fmt.Errorf("unable to fund psbt: %w", err) } @@ -997,7 +999,7 @@ func (c *ChainPlanter) fundGenesisPsbt(ctx context.Context, // Classify anchor transaction output indexes. anchorOutIndexes, err := anchorTxOutputIndexes( - *fundedGenesisPkt, preCommitmentTxOut, + fundedGenesisPkt, preCommitmentTxOut, ) if err != nil { return zero, fmt.Errorf("unable to determine output indexes: "+ @@ -1034,7 +1036,7 @@ func (c *ChainPlanter) fundGenesisPsbt(ctx context.Context, // Formulate a funded minting anchor PSBT from the funded PSBT. fundedMintAnchorPsbt, err := NewFundedMintAnchorPsbt( - *fundedGenesisPkt, anchorOutIndexes, preCommitmentOut, + fundedGenesisPkt, anchorOutIndexes, preCommitmentOut, ) if err != nil { return zero, fmt.Errorf("unable to create funded minting "+ @@ -2024,7 +2026,27 @@ func (c *ChainPlanter) fundBatch(ctx context.Context, params FundParams, } batchKey := asset.ToSerialized(batch.BatchKey.PubKey) - mintAnchorTx, err := c.fundGenesisPsbt(ctx, batchKey, feeRate) + + // walletFundPsbt is a closure that will be used to fund the + // batch with the specified fee rate. + walletFundPsbt := func(ctx context.Context, + anchorPkt psbt.Packet) (tapsend.FundedPsbt, error) { + + var zero tapsend.FundedPsbt + + fundedPkt, err := c.cfg.Wallet.FundPsbt( + ctx, &anchorPkt, 1, feeRate, -1, + ) + if err != nil { + return zero, err + } + + return *fundedPkt, nil + } + + mintAnchorTx, err := c.fundGenesisPsbt( + ctx, batchKey, walletFundPsbt, + ) if err != nil { return fmt.Errorf("unable to fund minting PSBT for "+ "batch: %x %w", batchKey[:], err) From d5ac7d0384f42653ef19166f1a536c0df6b5c1ff Mon Sep 17 00:00:00 2001 From: ffranr Date: Mon, 3 Mar 2025 13:19:57 +0000 Subject: [PATCH 22/26] tapgarden: refactor fundGenesisPsbt to pass in pending batch Pass the pending batch into fundGenesisPsbt and convert it into a standalone function rather than a method on ChainPlanter. This change makes it easier to call fundGenesisPsbt from unit tests. --- tapgarden/planter.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tapgarden/planter.go b/tapgarden/planter.go index 9722e70a4..56798da64 100644 --- a/tapgarden/planter.go +++ b/tapgarden/planter.go @@ -937,8 +937,8 @@ type WalletFundPsbt = func(ctx context.Context, // (all outputs need to hold some BTC to not be dust), and with a dummy script. // We need to use a dummy script as we can't know the actual script key since // that's dependent on the genesis outpoint. -func (c *ChainPlanter) fundGenesisPsbt(ctx context.Context, - batchKey asset.SerializedKey, +func fundGenesisPsbt(ctx context.Context, + pendingBatch *MintingBatch, batchKey asset.SerializedKey, walletFundPsbt WalletFundPsbt) (FundedMintAnchorPsbt, error) { var zero FundedMintAnchorPsbt @@ -947,8 +947,8 @@ func (c *ChainPlanter) fundGenesisPsbt(ctx context.Context, // 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 pendingBatch != nil && pendingBatch.UniverseCommitments { + out, err := preCommitmentOutput(pendingBatch) if err != nil { return zero, fmt.Errorf("unable to create "+ "pre-commitment output: %w", err) @@ -2044,8 +2044,8 @@ func (c *ChainPlanter) fundBatch(ctx context.Context, params FundParams, return *fundedPkt, nil } - mintAnchorTx, err := c.fundGenesisPsbt( - ctx, batchKey, walletFundPsbt, + mintAnchorTx, err := fundGenesisPsbt( + ctx, c.pendingBatch, batchKey, walletFundPsbt, ) if err != nil { return fmt.Errorf("unable to fund minting PSBT for "+ From f62e65eb26d52933368f363ffea5a2bb26f78fcd Mon Sep 17 00:00:00 2001 From: ffranr Date: Mon, 3 Mar 2025 13:24:13 +0000 Subject: [PATCH 23/26] tapgarden: remove batch key argument from fundGenesisPsbt The batch key was only used for logging. This commit moves the log messages outside fundGenesisPsbt, simplifying the function for better code health. --- tapgarden/planter.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tapgarden/planter.go b/tapgarden/planter.go index 56798da64..d9b4fa442 100644 --- a/tapgarden/planter.go +++ b/tapgarden/planter.go @@ -937,12 +937,10 @@ type WalletFundPsbt = func(ctx context.Context, // (all outputs need to hold some BTC to not be dust), and with a dummy script. // We need to use a dummy script as we can't know the actual script key since // that's dependent on the genesis outpoint. -func fundGenesisPsbt(ctx context.Context, - pendingBatch *MintingBatch, batchKey asset.SerializedKey, +func fundGenesisPsbt(ctx context.Context, pendingBatch *MintingBatch, walletFundPsbt WalletFundPsbt) (FundedMintAnchorPsbt, error) { var zero FundedMintAnchorPsbt - log.Infof("Attempting to fund batch: %x", batchKey) // If universe commitments are enabled, we formulate a pre-commitment // output. This output is spent by the universe commitment transaction. @@ -994,7 +992,6 @@ func fundGenesisPsbt(ctx context.Context, "funded anchor transaction") } - log.Infof("Funded GenesisPacket for batch: %x", batchKey) log.Tracef("GenesisPacket: %v", spew.Sdump(fundedGenesisPkt)) // Classify anchor transaction output indexes. @@ -2044,14 +2041,16 @@ func (c *ChainPlanter) fundBatch(ctx context.Context, params FundParams, return *fundedPkt, nil } + log.Infof("Attempting to fund batch: %x", batchKey) mintAnchorTx, err := fundGenesisPsbt( - ctx, c.pendingBatch, batchKey, walletFundPsbt, + ctx, c.pendingBatch, walletFundPsbt, ) if err != nil { return fmt.Errorf("unable to fund minting PSBT for "+ "batch: %x %w", batchKey[:], err) } + log.Infof("Funded GenesisPacket for batch: %x", batchKey) batch.GenesisPacket = &mintAnchorTx return nil From 01f23a0a21e204f12a0013ff04d6c645c331d958 Mon Sep 17 00:00:00 2001 From: ffranr Date: Mon, 3 Mar 2025 14:41:03 +0000 Subject: [PATCH 24/26] tapgarden: call fundGenesisPsbt from RandSeedlingMintingBatch Use the standalone function fundGenesisPsbt when generating a mock batch genesis (mint anchor tx) funded PSBT. This change will allow us to seamlessly include pre-commitment outputs in the mock process later. --- tapgarden/mock.go | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/tapgarden/mock.go b/tapgarden/mock.go index ff25febb3..7293f31b7 100644 --- a/tapgarden/mock.go +++ b/tapgarden/mock.go @@ -59,21 +59,32 @@ func RandSeedlings(t testing.TB, numSeedlings int) map[string]*Seedling { // RandSeedlingMintingBatch creates a new minting batch with only random // seedlings populated for testing. func RandSeedlingMintingBatch(t testing.TB, numSeedlings int) *MintingBatch { - genesisTx := NewGenesisTx(t, chainfee.FeePerKwFloor) - BatchKey, _ := test.RandKeyDesc(t) - return &MintingBatch{ - BatchKey: BatchKey, + batchKey, _ := test.RandKeyDesc(t) + batch := MintingBatch{ + BatchKey: batchKey, Seedlings: RandSeedlings(t, numSeedlings), HeightHint: test.RandInt[uint32](), CreationTime: time.Now(), - GenesisPacket: &FundedMintAnchorPsbt{ - FundedPsbt: tapsend.FundedPsbt{ - Pkt: &genesisTx, - ChangeOutputIndex: 1, - }, - AssetAnchorOutIdx: 0, - }, } + + walletFundPsbt := func(ctx context.Context, + anchorPkt psbt.Packet) (tapsend.FundedPsbt, error) { + + FundGenesisTx(&anchorPkt, chainfee.FeePerKwFloor) + + return tapsend.FundedPsbt{ + Pkt: &anchorPkt, + ChangeOutputIndex: 1, + }, nil + } + + // Fund genesis packet. + ctx := context.Background() + fundedPsbt, err := fundGenesisPsbt(ctx, &batch, walletFundPsbt) + require.NoError(t, err) + + batch.GenesisPacket = &fundedPsbt + return &batch } type MockWalletAnchor struct { From 8c70b0b6efff8f7b020bea9936f4dabfd6158064 Mon Sep 17 00:00:00 2001 From: ffranr Date: Tue, 4 Mar 2025 16:12:05 +0000 Subject: [PATCH 25/26] tapdb+tapgarden: refactor RandSeedlingMintingBatch Refactor RandSeedlingMintingBatch for generalization: - Rename to RandMintingBatch. - Change argument to `options...` pattern for extendability. The next commit will introduce additional options, enabling the creation of more specific mint batches. --- tapdb/asset_minting_test.go | 24 +++++++++++++++++------ tapgarden/mock.go | 38 +++++++++++++++++++++++++++++++++---- 2 files changed, 52 insertions(+), 10 deletions(-) diff --git a/tapdb/asset_minting_test.go b/tapdb/asset_minting_test.go index acbedcad2..8fef5ce59 100644 --- a/tapdb/asset_minting_test.go +++ b/tapdb/asset_minting_test.go @@ -499,7 +499,9 @@ func TestCommitMintingBatchSeedlings(t *testing.T) { // First, we'll write a new minting batch to disk, including an // internal key and a set of seedlings. One random seedling will // be a reissuance into a specific group. - mintingBatch := tapgarden.RandSeedlingMintingBatch(t, numSeedlings) + mintingBatch := tapgarden.RandMintingBatch( + t, tapgarden.WithTotalSeedlings(numSeedlings), + ) _, randGroup, _ := addRandGroupToBatch( t, assetStore, ctx, mintingBatch.Seedlings, ) @@ -600,7 +602,9 @@ func TestCommitMintingBatchSeedlings(t *testing.T) { // Insert another normal batch into the database. We should get this // batch back if we query for the set of non-final batches. - mintingBatch = tapgarden.RandSeedlingMintingBatch(t, numSeedlings) + mintingBatch = tapgarden.RandMintingBatch( + t, tapgarden.WithTotalSeedlings(numSeedlings), + ) err = assetStore.CommitMintingBatch(ctx, mintingBatch) require.NoError(t, err) mintingBatches = noError1(t, assetStore.FetchNonFinalBatches, ctx) @@ -791,7 +795,9 @@ func TestAddSproutsToBatch(t *testing.T) { // First, we'll create a new batch, then add some sample seedlings. // One random seedling will be a reissuance into a specific group. - mintingBatch := tapgarden.RandSeedlingMintingBatch(t, numSeedlings) + mintingBatch := tapgarden.RandMintingBatch( + t, tapgarden.WithTotalSeedlings(numSeedlings), + ) _, seedlingGroups, _ := addRandGroupToBatch( t, assetStore, ctx, mintingBatch.Seedlings, ) @@ -889,7 +895,9 @@ type randAssetCtx struct { func addRandAssets(t *testing.T, ctx context.Context, assetStore *AssetMintingStore, numAssets int) randAssetCtx { - mintingBatch := tapgarden.RandSeedlingMintingBatch(t, numAssets) + mintingBatch := tapgarden.RandMintingBatch( + t, tapgarden.WithTotalSeedlings(numAssets), + ) genAmt, seedlingGroups, group := addRandGroupToBatch( t, assetStore, ctx, mintingBatch.Seedlings, ) @@ -1397,7 +1405,9 @@ func TestGroupAnchors(t *testing.T) { // internal key and a set of seedlings. One random seedling will // be a reissuance into a specific group. Two other seedlings will form // a multi-asset group. - mintingBatch := tapgarden.RandSeedlingMintingBatch(t, numSeedlings) + mintingBatch := tapgarden.RandMintingBatch( + t, tapgarden.WithTotalSeedlings(numSeedlings), + ) _, seedlingGroups, _ := addRandGroupToBatch( t, assetStore, ctx, mintingBatch.Seedlings, ) @@ -1834,7 +1844,9 @@ func TestUpsertMintAnchorUniCommitment(t *testing.T) { assetStore, _, _ := newAssetStore(t) // Create a new batch with one asset group seedling. - mintingBatch := tapgarden.RandSeedlingMintingBatch(t, 1) + mintingBatch := tapgarden.RandMintingBatch( + t, tapgarden.WithTotalSeedlings(1), + ) mintingBatch.UniverseCommitments = true _, _, group := addRandGroupToBatch( diff --git a/tapgarden/mock.go b/tapgarden/mock.go index 7293f31b7..af8a23ffd 100644 --- a/tapgarden/mock.go +++ b/tapgarden/mock.go @@ -56,13 +56,43 @@ func RandSeedlings(t testing.TB, numSeedlings int) map[string]*Seedling { return seedlings } -// RandSeedlingMintingBatch creates a new minting batch with only random -// seedlings populated for testing. -func RandSeedlingMintingBatch(t testing.TB, numSeedlings int) *MintingBatch { +// 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 +} + +// 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 + } +} + +// 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) + } + batchKey, _ := test.RandKeyDesc(t) batch := MintingBatch{ BatchKey: batchKey, - Seedlings: RandSeedlings(t, numSeedlings), + Seedlings: RandSeedlings(t, options.totalSeedlings), HeightHint: test.RandInt[uint32](), CreationTime: time.Now(), } From 83ba31c193b1f981a937e716026ed23d8adb1549 Mon Sep 17 00:00:00 2001 From: ffranr Date: Wed, 5 Mar 2025 11:40:15 +0000 Subject: [PATCH 26/26] WIP --- tapdb/asset_minting_test.go | 27 ++++++ tapgarden/mock.go | 161 +++++++++++++++++++++++++++++++++++- 2 files changed, 184 insertions(+), 4 deletions(-) diff --git a/tapdb/asset_minting_test.go b/tapdb/asset_minting_test.go index 8fef5ce59..b4493210a 100644 --- a/tapdb/asset_minting_test.go +++ b/tapdb/asset_minting_test.go @@ -1920,6 +1920,33 @@ func TestUpsertMintAnchorUniCommitment(t *testing.T) { ) } +// TestCommitMintingBatchSeedlings tests that we're able to properly write and +// read a base minting batch on disk. This test covers the state when a batch +// only has seedlings, without any fully formed assets. +func TestBlah(t *testing.T) { + t.Parallel() + + assetStore, _, _ := newAssetStore(t) + + ctx := context.Background() + const numSeedlings = 5 + + // First, we'll write a new minting batch to disk, including an + // internal key and a set of seedlings. One random seedling will + // be a reissuance into a specific group. + mintingBatch := tapgarden.RandMintingBatch( + t, tapgarden.WithTotalSeedlings(1), + tapgarden.WithTotalGroups([]int{1}), + tapgarden.WithUniverseCommitments(true), + ) + //_, randGroup, _ := addRandGroupToBatch( + // t, assetStore, ctx, mintingBatch.Seedlings, + //) + //_, randSiblingHash := addRandSiblingToBatch(t, mintingBatch) + err := assetStore.CommitMintingBatch(ctx, mintingBatch) + require.NoError(t, err) +} + func init() { rand.Seed(time.Now().Unix()) diff --git a/tapgarden/mock.go b/tapgarden/mock.go index af8a23ffd..2de935973 100644 --- a/tapgarden/mock.go +++ b/tapgarden/mock.go @@ -21,6 +21,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" @@ -56,12 +57,110 @@ func RandSeedlings(t testing.TB, numSeedlings int) map[string]*Seedling { return seedlings } +// RandGroupSeedlings generates a random set of seedlings for a single asset +// group. +func RandGroupSeedlings(t testing.TB, numSeedlings int, + uniCommitments bool) map[string]*Seedling { + + seedlings := make(map[string]*Seedling) + + // Formulate group anchor seedling. + metaBlob := test.RandBytes(32) + groupAnchorName := hex.EncodeToString(test.RandBytes(32)) + assetType := asset.Normal + + // For now, we only test the v0 and v1 versions. + assetVersion := asset.Version(test.RandIntn(2)) + + scriptKey, _ := test.RandKeyDesc(t) + + // If universe commitments are enabled, we generate a random key + // descriptor to use as the delegation key. + var delegationKey fn.Option[keychain.KeyDescriptor] + if uniCommitments { + keyDesc, _ := test.RandKeyDesc(t) + delegationKey = fn.Some(keyDesc) + } + + 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) + + seedlings[groupAnchorName] = &Seedling{ + AssetVersion: assetVersion, + AssetType: assetType, + AssetName: groupAnchorName, + Meta: &proof.MetaReveal{ + Data: metaBlob, + }, + Amount: uint64(test.RandInt[uint32]()), + GroupInfo: &asset.AssetGroup{ + Genesis: &assetGenesis, + GroupKey: groupKey, + }, + ScriptKey: asset.NewScriptKeyBip86(scriptKey), + EnableEmission: true, + UniverseCommitments: uniCommitments, + DelegationKey: delegationKey, + } + + // Formulate non-anchor group seedlings. + for i := 0; i < numSeedlings-1; i++ { + seedlingName := hex.EncodeToString(test.RandBytes(32)) + + seedlings[groupAnchorName] = &Seedling{ + AssetVersion: assetVersion, + AssetType: assetType, + AssetName: seedlingName, + GroupAnchor: &groupAnchorName, + Meta: &proof.MetaReveal{ + Data: metaBlob, + }, + Amount: uint64(test.RandInt[uint32]()), + ScriptKey: asset.NewScriptKeyBip86(scriptKey), + EnableEmission: true, + UniverseCommitments: uniCommitments, + } + } + + 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. @@ -80,6 +179,23 @@ func WithTotalSeedlings(count int) MintBatchOption { } } +// 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 { @@ -89,12 +205,49 @@ func RandMintingBatch(t testing.TB, opts ...MintBatchOption) *MintingBatch { opt(&options) } + // Formulate batch seedlings. + seedlings := make(map[string]*Seedling) + + // 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 name, seedling := range groupSeedlings { + seedlings[name] = seedling + } + } + + // 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(seedlings) < options.totalSeedlings { + remaining := options.totalSeedlings - len(seedlings) + randSeedlings := RandSeedlings(t, remaining) + + // Add the seedlings to the total seedlings map. + for name, seedling := range randSeedlings { + seedlings[name] = seedling + } + } + + // 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(seedlings)) + batchKey, _ := test.RandKeyDesc(t) batch := MintingBatch{ - BatchKey: batchKey, - Seedlings: RandSeedlings(t, options.totalSeedlings), - HeightHint: test.RandInt[uint32](), - CreationTime: time.Now(), + BatchKey: batchKey, + Seedlings: seedlings, + HeightHint: test.RandInt[uint32](), + CreationTime: time.Now(), + UniverseCommitments: options.universeCommitments, } walletFundPsbt := func(ctx context.Context,