Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

mintrpc+tapgarden: add universe announcement flag to asset minting #1173

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 63 additions & 37 deletions rpcserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -408,18 +408,15 @@ func (r *rpcServer) GetInfo(ctx context.Context,
}, nil
}

// MintAsset attempts to mint the set of assets (async by default to ensure
// proper batching) specified in the request.
func (r *rpcServer) MintAsset(ctx context.Context,
req *mintrpc.MintAssetRequest) (*mintrpc.MintAssetResponse, error) {

// validateMintAssetRequest validates the given mint asset request.
func validateMintAssetRequest(req *mintrpc.MintAssetRequest) error {
if req.Asset == nil {
return nil, fmt.Errorf("asset cannot be nil")
return fmt.Errorf("asset cannot be nil")
}

err := asset.ValidateAssetName(req.Asset.Name)
if err != nil {
return nil, fmt.Errorf("invalid asset name: %w", err)
return fmt.Errorf("invalid asset name: %w", err)
}

specificGroupKey := len(req.Asset.GroupKey) != 0
Expand All @@ -431,64 +428,98 @@ func (r *rpcServer) MintAsset(ctx context.Context,
if groupTapscriptRootSize != 0 &&
groupTapscriptRootSize != sha256.Size {

return nil, fmt.Errorf("group tapscript root must be %d bytes",
return fmt.Errorf("group tapscript root must be %d bytes",
sha256.Size)
}

switch {
// New grouped asset and grouped asset cannot both be set.
case req.Asset.NewGroupedAsset && req.Asset.GroupedAsset:
return nil, fmt.Errorf("cannot set both new grouped asset " +
return fmt.Errorf("cannot set both new grouped asset " +
"and grouped asset",
)

// Using a specific group key or anchor implies disabling emission.
case req.Asset.NewGroupedAsset:
if specificGroupKey || specificGroupAnchor {
return nil, fmt.Errorf("must disable emission to " +
return fmt.Errorf("must disable emission to " +
"specify a group")
}

// A group tapscript root cannot be specified if emission is disabled.
case !req.Asset.NewGroupedAsset && groupTapscriptRootSize != 0:
return nil, fmt.Errorf("cannot specify a group tapscript root" +
return fmt.Errorf("cannot specify a group tapscript root" +
"with emission disabled")

// A group internal key cannot be specified if emission is disabled.
case !req.Asset.NewGroupedAsset && specificGroupInternalKey:
return nil, fmt.Errorf("cannot specify a group internal key" +
return fmt.Errorf("cannot specify a group internal key" +
"with emission disabled")

// If the asset is intended to be part of an existing group, a group key
// or anchor must be specified, but not both. Neither a group tapscript
// root nor group internal key can be specified.
case req.Asset.GroupedAsset:
if !specificGroupKey && !specificGroupAnchor {
return nil, fmt.Errorf("must specify a group key or" +
return fmt.Errorf("must specify a group key or" +
"group anchor")
}

if specificGroupKey && specificGroupAnchor {
return nil, fmt.Errorf("cannot specify both a group " +
return fmt.Errorf("cannot specify both a group " +
"key and a group anchor")
}

if groupTapscriptRootSize != 0 {
return nil, fmt.Errorf("cannot specify a group " +
return fmt.Errorf("cannot specify a group " +
"tapscript root with emission disabled")
}

if specificGroupInternalKey {
return nil, fmt.Errorf("cannot specify a group " +
return fmt.Errorf("cannot specify a group " +
"internal key with emission disabled")
}

// A group was specified without GroupedAsset being set.
case specificGroupKey || specificGroupAnchor:
return nil, fmt.Errorf("must set grouped asset to mint into " +
return fmt.Errorf("must set grouped asset to mint into " +
"a specific group")
}

// If a custom decimal display is set, the meta type must also be set to
// JSON.
if req.Asset.DecimalDisplay != 0 && req.Asset.AssetMeta == nil {
return fmt.Errorf("decimal display requires JSON asset " +
"metadata")
}

// Ensure that the universe announcement flag is used correctly.
if req.EnableUniAnnounce {
// If universe announcement is enabled, the asset must be a new
// grouped asset.
if !(specificGroupKey || specificGroupAnchor) {
return fmt.Errorf("universe announcement feature is "+
"only applicable for grouped assets ("+
"specific_group_key=%v,"+
"specific_group_anchor=%v)",
specificGroupKey, specificGroupAnchor)
}
}

return nil
}

// MintAsset attempts to mint the set of assets (async by default to ensure
// proper batching) specified in the request.
func (r *rpcServer) MintAsset(ctx context.Context,
req *mintrpc.MintAssetRequest) (*mintrpc.MintAssetResponse, error) {

// Validate the request.
err := validateMintAssetRequest(req)
if err != nil {
return nil, err
}

assetVersion, err := taprpc.UnmarshalAssetVersion(
req.Asset.AssetVersion,
)
Expand All @@ -497,14 +528,6 @@ func (r *rpcServer) MintAsset(ctx context.Context,
}

var seedlingMeta *proof.MetaReveal

// If a custom decimal display is set, the meta type must also be set to
// JSON.
if req.Asset.DecimalDisplay != 0 && req.Asset.AssetMeta == nil {
return nil, fmt.Errorf("decimal display requires JSON asset " +
"metadata")
}

if req.Asset.AssetMeta != nil {
// Ensure that the meta type is valid.
metaType, err := proof.IsValidMetaType(req.Asset.AssetMeta.Type)
Expand Down Expand Up @@ -574,7 +597,7 @@ func (r *rpcServer) MintAsset(ctx context.Context,
}
}

if specificGroupInternalKey {
if req.Asset.GroupInternalKey != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't have the same meaning as before. specificGroupInternalKey was true if the seedling is being minted into an existing group.

GroupInternalKey is only non-nil if you are creating a new group, and you want to use a specific internal key instead of whichever key tapd will generate automatically.

groupInternalKey, err = taprpc.UnmarshalKeyDescriptor(
req.Asset.GroupInternalKey,
)
Expand All @@ -583,28 +606,31 @@ func (r *rpcServer) MintAsset(ctx context.Context,
}
}

groupTapscriptRootSize := len(req.Asset.GroupTapscriptRoot)

if groupTapscriptRootSize != 0 {
groupTapscriptRoot = bytes.Clone(req.Asset.GroupTapscriptRoot)
}

seedling := &tapgarden.Seedling{
AssetVersion: assetVersion,
AssetType: asset.Type(req.Asset.AssetType),
AssetName: req.Asset.Name,
Amount: req.Asset.Amount,
EnableEmission: req.Asset.NewGroupedAsset,
Meta: seedlingMeta,
AssetVersion: assetVersion,
AssetType: asset.Type(req.Asset.AssetType),
AssetName: req.Asset.Name,
Amount: req.Asset.Amount,
EnableEmission: req.Asset.NewGroupedAsset,
EnableUniAnnounce: req.EnableUniAnnounce,
Meta: seedlingMeta,
}

rpcsLog.Infof("[MintAsset]: version=%v, type=%v, name=%v, amt=%v, "+
"issuance=%v", seedling.AssetVersion, seedling.AssetType,
"enable_emission=%v", seedling.AssetVersion, seedling.AssetType,
seedling.AssetName, seedling.Amount, seedling.EnableEmission)

if scriptKey != nil {
seedling.ScriptKey = *scriptKey
}

if specificGroupInternalKey {
if req.Asset.GroupInternalKey != nil {
seedling.GroupInternalKey = &groupInternalKey
}

Expand All @@ -615,7 +641,7 @@ func (r *rpcServer) MintAsset(ctx context.Context,
switch {
// If a group key is provided, parse the provided group public key
// before creating the asset seedling.
case specificGroupKey:
case len(req.Asset.GroupKey) != 0:
groupTweakedKey, err := btcec.ParsePubKey(req.Asset.GroupKey)
if err != nil {
return nil, fmt.Errorf("invalid group key: %w", err)
Expand All @@ -634,9 +660,9 @@ func (r *rpcServer) MintAsset(ctx context.Context,
},
}

// If a group anchor is provided, propoate the name to the seedling.
// If a group anchor is provided, propagate the name to the seedling.
// We cannot do any name validation from outside the minter.
case specificGroupAnchor:
case len(req.Asset.GroupAnchor) != 0:
seedling.GroupAnchor = &req.Asset.GroupAnchor
}

Expand Down
35 changes: 35 additions & 0 deletions tapgarden/batch.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ type MintingBatch struct {
// reveal for that asset, if it has one.
AssetMetas AssetMetas

// EnableUniAnnounce is a flag that indicates whether the minting
// event should support universe announcements. If set to true,
// the batch must include only assets that share the same asset group
// key, which must also be set.
EnableUniAnnounce bool

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

// ValidateSeedling checks if a seedling is valid for the batch.
func (m *MintingBatch) ValidateSeedling(newSeedling Seedling) error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIUC we want this to be a logical AND between this flag for each seedling, and the matching flag for the batch.

So the first seedling of the batch should update the batch flag, and every other seedling should match. This implies that every seedling would have EnableUniAnnounce set to true.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's what I'm going for. EnableUniAnnounce set to true on each seedling if it's true on the batch. They need to correspond basically. We can relax that sort of thing in the future. Let me know if you have thoughts on that sort of approach.

// Ensure that the seedling and batch agree on the enabled universe
// announcements.
if m.EnableUniAnnounce != newSeedling.EnableUniAnnounce {
return fmt.Errorf("batch and seedling do not agree on " +
"enabled universe announcements")
}

// If the seedling supports universe announcements, it must have a group
// anchor or the same group key as all the other seedlings in the batch.
if newSeedling.EnableUniAnnounce {
ffranr marked this conversation as resolved.
Show resolved Hide resolved
if newSeedling.GroupAnchor == nil &&
newSeedling.GroupInfo == nil {

return fmt.Errorf("universe announcement enabled for " +
"seedling but group info/anchor is absent")
}

if newSeedling.GroupInfo != nil {
// TODO(ffranr): Add check to ensure that this new
// seedling has the same group key as the other
// seedlings in the batch.
}
}

return nil
}

// ToMintingBatch creates a new MintingBatch from a VerboseBatch.
func (v *VerboseBatch) ToMintingBatch() *MintingBatch {
newBatch := v.MintingBatch.Copy()
Expand Down
31 changes: 26 additions & 5 deletions tapgarden/planter.go
Original file line number Diff line number Diff line change
Expand Up @@ -1179,6 +1179,10 @@ func (c *ChainPlanter) gardener() {
// After some basic validation, prepare the asset
// seedling (soon to be a sprout) by committing it to
// disk as part of the latest batch.
//
// This method will also include the seedling in any
// existing pending batch or create a new pending batch
// if necessary.
ctx, cancel := c.WithCtxQuit()
err := c.prepAssetSeedling(ctx, req)
cancel()
Expand Down Expand Up @@ -1755,8 +1759,8 @@ func (c *ChainPlanter) finalizeBatch(params FinalizeParams) (*BatchCaretaker,
return caretaker, nil
}

// PendingBatch returns the current pending batch. If there's no pending batch,
// then an error is returned.
// PendingBatch returns the current pending batch, or nil if no batch is
// pending.
func (c *ChainPlanter) PendingBatch() (*MintingBatch, error) {
req := newStateReq[*MintingBatch](reqTypePendingBatch)

Expand Down Expand Up @@ -1842,8 +1846,7 @@ func (c *ChainPlanter) CancelBatch() (*btcec.PublicKey, error) {
}

// prepAssetSeedling performs some basic validation for the Seedling, then
// either adds it to an existing pending batch or creates a new batch for it. A
// bool indicating if a new batch should immediately be created is returned.
// 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 {

Expand Down Expand Up @@ -1964,6 +1967,10 @@ func (c *ChainPlanter) prepAssetSeedling(ctx context.Context,

newBatch.Seedlings[req.AssetName] = req

// The batch enable universe announcement flag inherits from the
// first seedling added to the batch.
newBatch.EnableUniAnnounce = req.EnableUniAnnounce

ctx, cancel := c.WithCtxQuit()
defer cancel()
err = c.cfg.Log.CommitMintingBatch(ctx, newBatch)
Expand All @@ -1978,12 +1985,26 @@ func (c *ChainPlanter) prepAssetSeedling(ctx context.Context,
case c.pendingBatch != nil:
log.Infof("Adding %v to existing MintingBatch", req)

// The batch already exist and may contain seedlings. Before
// adding the seedling to the batch, we'll ensure that the
// batch will still be valid after adding the seedling.
err := c.pendingBatch.ValidateSeedling(*req)
if err != nil {
return fmt.Errorf("seedling can not be added to "+
"pending batch: %w", err)
}

c.pendingBatch.Seedlings[req.AssetName] = req

// The batch may already exist, but it may not have any
ffranr marked this conversation as resolved.
Show resolved Hide resolved
// seedlings. If this is the first seedling, we'll inherit the
// universe announcement flag from it.
c.pendingBatch.EnableUniAnnounce = req.EnableUniAnnounce

// Now that we know the seedling is ok, we'll write it to disk.
ctx, cancel := c.WithCtxQuit()
defer cancel()
err := c.cfg.Log.AddSeedlingsToBatch(
err = c.cfg.Log.AddSeedlingsToBatch(
ctx, c.pendingBatch.BatchKey.PubKey, req,
)
if err != nil {
Expand Down
6 changes: 6 additions & 0 deletions tapgarden/planter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1654,6 +1654,12 @@ func testFundSealBeforeFinalize(t *mintingTestHarness) {
// harness.
t.refreshChainPlanter()

// A pending batch should not exist yet. Therefore, `PendingBatch`
// should return nil and no error.
batch, err := t.planter.PendingBatch()
ffranr marked this conversation as resolved.
Show resolved Hide resolved
require.Nil(t, batch)
require.NoError(t, err)

var (
wg sync.WaitGroup
respChan = make(chan *FundBatchResp, 1)
Expand Down
7 changes: 7 additions & 0 deletions tapgarden/seedling.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,13 @@ type Seedling struct {
// for this asset meaning future assets linked to it can be created.
EnableEmission bool

// EnableUniAnnounce indicates whether the minting event which
// will be associated with the seedling supports universe announcements.
// If set to true, the seedling can only be included in a minting batch
// where all assets share the same asset group key, which must be
// specified.
EnableUniAnnounce bool

// 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.
Expand Down
Loading
Loading