Skip to content

Commit

Permalink
tapgarden: implement sealBatch
Browse files Browse the repository at this point in the history
In this commit, we add logic to the planter to produce asset group
witnesses for all seedlings in a batch associated with an asset group.
We also store the asset groups so they can be fetched by the caretaker
during batch finalization.
  • Loading branch information
jharveyb committed Apr 9, 2024
1 parent dabdacb commit 49f7b1c
Show file tree
Hide file tree
Showing 5 changed files with 273 additions and 5 deletions.
4 changes: 2 additions & 2 deletions fn/iter.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ func ForEach[T any](items []T, f func(T)) {
// ForEachMapItem is a generic implementation of a for-each (map with side
// effects). This can be used to ensure that any normal for-loop don't run into
// bugs due to loop variable scoping.
func ForEachMapItem[T any, K comparable](items map[K]T, f func(T)) {
func ForEachMapItem[T any, K comparable](items map[K]T, f func(K, T)) {
for i := range items {
f(items[i])
f(i, items[i])
}
}

Expand Down
4 changes: 4 additions & 0 deletions tapgarden/batch.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,3 +219,7 @@ func (m *MintingBatch) TapSibling() []byte {
func (m *MintingBatch) UpdateTapSibling(sibling *chainhash.Hash) {
m.tapSibling = sibling
}

func (m *MintingBatch) IsFunded() bool {
return m.GenesisPacket != nil
}
4 changes: 2 additions & 2 deletions tapgarden/caretaker.go
Original file line number Diff line number Diff line change
Expand Up @@ -1595,10 +1595,10 @@ func GenRawGroupAnchorVerifier(ctx context.Context) func(*asset.Genesis,
assetGroupKey := asset.ToSerialized(&groupKey.GroupPubKey)
groupAnchor, err := groupAnchors.Get(assetGroupKey)
if err != nil {
// TODO(jhb): add tapscript root support
singleTweak := gen.ID()
tweakedGroupKey, err := asset.GroupPubKey(
groupKey.RawKey.PubKey, singleTweak[:], nil,
groupKey.RawKey.PubKey, singleTweak[:],
groupKey.TapscriptRoot,
)
if err != nil {
return err
Expand Down
11 changes: 11 additions & 0 deletions tapgarden/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,17 @@ type MintingStore interface {
FetchMintingBatch(ctx context.Context,
batchKey *btcec.PublicKey) (*MintingBatch, error)

// AddSeedlingGroups stores the asset groups for seedlings associated
// with a batch.
AddSeedlingGroups(ctx context.Context, genesisOutpoint wire.OutPoint,
assetGroups []*asset.AssetGroup) error

// FetchSeedlingGroups is used to fetch the asset groups for seedlings
// associated with a funded batch.
FetchSeedlingGroups(ctx context.Context, genesisOutpoint wire.OutPoint,
anchorOutputIndex uint32,
seedlings []*Seedling) ([]*asset.AssetGroup, error)

// AddSproutsToBatch adds a new set of sprouts to the batch, along with
// a GenesisPacket, that once signed and broadcast with create the
// set of assets on chain.
Expand Down
255 changes: 254 additions & 1 deletion tapgarden/planter.go
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,167 @@ func (c *ChainPlanter) fundGenesisPsbt(ctx context.Context,
return fundedGenesisPkt, nil
}

// filterSeedlingsWithGroup separates a set of seedlings into two sets based on
// their relation to an asset group, which has not been constructed yet.
func filterSeedlingsWithGroup(
seedlings map[string]*Seedling) (map[string]*Seedling,
map[string]*Seedling) {

withGroup := make(map[string]*Seedling)
withoutGroup := make(map[string]*Seedling)
fn.ForEachMapItem(seedlings, func(name string, seedling *Seedling) {
switch {
case seedling.GroupInfo != nil || seedling.GroupAnchor != nil ||
seedling.EnableEmission:

withGroup[name] = seedling

default:
withoutGroup[name] = seedling
}
})

return withGroup, withoutGroup
}

// buildGroupReqs creates group key requests and asset group genesis TXs for
// seedlings that are part of a funded batch.
func (c *ChainPlanter) buildGroupReqs(genesisPoint wire.OutPoint,
assetOutputIndex uint32,
groupSeedlings map[string]*Seedling) ([]asset.GroupKeyRequest,
[]asset.GroupVirtualTx, error) {

// Seedlings that anchor a group may be referenced by other seedlings,
// and therefore need to be mapped to sprouts first so that we derive
// the initial tweaked group key early.
orderedSeedlings := SortSeedlings(maps.Values(groupSeedlings))
newGroups := make(map[string]*asset.AssetGroup)
groupReqs := make([]asset.GroupKeyRequest, 0, len(orderedSeedlings))
genTXs := make([]asset.GroupVirtualTx, 0, len(orderedSeedlings))

for _, seedlingName := range orderedSeedlings {
seedling := groupSeedlings[seedlingName]

assetGen := asset.Genesis{
FirstPrevOut: genesisPoint,
Tag: seedling.AssetName,
OutputIndex: assetOutputIndex,
Type: seedling.AssetType,
}

// If the seedling has a meta data reveal set, then we'll bind
// that by including the hash of the meta data in the asset
// genesis.
if seedling.Meta != nil {
assetGen.MetaHash = seedling.Meta.MetaHash()
}

var (
amount uint64
groupInfo *asset.AssetGroup
protoAsset *asset.Asset
err error
)

// Determine the amount for the actual asset.
switch seedling.AssetType {
case asset.Normal:
amount = seedling.Amount
case asset.Collectible:
amount = 1
}

// If the seedling has a group key specified,
// that group key was validated earlier. We need to
// sign the new genesis with that group key.
if seedling.HasGroupKey() {
groupInfo = seedling.GroupInfo
}

// If the seedling has a group anchor specified, that anchor
// was validated earlier and the corresponding group has already
// been created. We need to look up the group key and sign
// the asset genesis with that key.
if seedling.GroupAnchor != nil {
groupInfo = newGroups[*seedling.GroupAnchor]
}

// If a group witness needs to be produced, then we will need a
// partially filled asset as part of the signing process.
if groupInfo != nil || seedling.EnableEmission {
protoAsset, err = asset.New(
assetGen, amount, 0, 0, seedling.ScriptKey,
nil,
asset.WithAssetVersion(seedling.AssetVersion),
)
if err != nil {
return nil, nil, fmt.Errorf("unable to create "+
"asset for group key signing: %w", err)
}
}

if groupInfo != nil {
groupReq, err := asset.NewGroupKeyRequest(
groupInfo.GroupKey.RawKey, *groupInfo.Genesis,
protoAsset, groupInfo.GroupKey.TapscriptRoot,
)
if err != nil {
return nil, nil, fmt.Errorf("unable to "+
"request asset group membership: %w",
err)
}

genTx, err := groupReq.BuildGroupVirtualTx(
c.cfg.GenTxBuilder,
)
if err != nil {
return nil, nil, err
}

groupReqs = append(groupReqs, *groupReq)
genTXs = append(genTXs, *genTx)
}

// If emission is enabled, an internal key for the group should
// already be specified. Use that to derive the key group
// signature along with the tweaked key group.
if seedling.EnableEmission {
if seedling.GroupInternalKey == nil {
return nil, nil, fmt.Errorf("unable to " +
"derive group key")
}

groupReq, err := asset.NewGroupKeyRequest(
*seedling.GroupInternalKey, assetGen,
protoAsset, seedling.GroupTapscriptRoot,
)
if err != nil {
return nil, nil, fmt.Errorf("unable to "+
"request asset group creation: %w", err)
}

genTx, err := groupReq.BuildGroupVirtualTx(
c.cfg.GenTxBuilder,
)
if err != nil {
return nil, nil, err
}

groupReqs = append(groupReqs, *groupReq)
genTXs = append(genTXs, *genTx)

newGroups[seedlingName] = &asset.AssetGroup{
Genesis: &assetGen,
GroupKey: &asset.GroupKey{
RawKey: *seedling.GroupInternalKey,
},
}
}
}

return groupReqs, genTXs, nil
}

// freezeMintingBatch freezes a target minting batch which means that no new
// assets can be added to the batch.
func freezeMintingBatch(ctx context.Context, batchStore MintingStore,
Expand Down Expand Up @@ -924,7 +1085,99 @@ func (c *ChainPlanter) fundBatch(ctx context.Context, params FundParams) error {
return nil
}

func (c *ChainPlanter) sealBatch(params SealParams) error {
// sealBatch will verify that each grouped asset in the pending batch has an
// asset group witness, and will attempt to create asset group witnesses when
// possible if they are not provided. After all asset group witnesses have been
// validated, they are saved to disk to be used by the caretaker during batch
// finalization.
func (c *ChainPlanter) sealBatch(ctx context.Context, _ SealParams) error {
// A batch should exist with 1+ seedlings and be funded before being
// sealed.
if c.pendingBatch == nil {
return fmt.Errorf("no pending batch")
}

if len(c.pendingBatch.Seedlings) == 0 {
return fmt.Errorf("no seedlings in batch")
}

if !c.pendingBatch.IsFunded() {
return fmt.Errorf("batch is not funded")
}

// Filter the batch seedlings to only consider those that will become
// grouped assets. If there are no such seedlings, then there is nothing
// to seal and no action is needed.
groupSeedlings, _ := filterSeedlingsWithGroup(c.pendingBatch.Seedlings)
if len(groupSeedlings) == 0 {
return nil
}

// Before we can build the group key requests for each seedling, we must
// fetch the genesis point and anchor index for the batch.
anchorOutputIndex := uint32(0)
if c.pendingBatch.GenesisPacket.ChangeOutputIndex == 0 {
anchorOutputIndex = 1
}

genesisPoint := extractGenesisOutpoint(
c.pendingBatch.GenesisPacket.Pkt.UnsignedTx,
)

// Construct the group key requests and group virtual TXs for each
// seedling. With these we can verify provided asset group witnesses,
// or attempt to derive asset group witnesses if needed.
groupReqs, genTXs, err := c.buildGroupReqs(
genesisPoint, anchorOutputIndex, groupSeedlings,
)
if err != nil {
return fmt.Errorf("unable to build group requests: %w", err)
}

assetGroups := make([]*asset.AssetGroup, 0, len(groupReqs))
for i := 0; i < len(groupReqs); i++ {
// Derive the asset group witness.
groupKey, err := asset.DeriveGroupKey(
c.cfg.GenSigner, genTXs[i], groupReqs[i], nil,
)
if err != nil {
return err
}

// Recreate the asset with the populated group key and validate
// the asset group witness.
protoAsset := groupReqs[i].NewAsset
groupedAsset, err := asset.New(
protoAsset.Genesis, protoAsset.Amount,
protoAsset.LockTime, protoAsset.RelativeLockTime,
protoAsset.ScriptKey, groupKey,
asset.WithAssetVersion(protoAsset.Version),
)
if err != nil {
return err
}

err = c.cfg.TxValidator.Execute(groupedAsset, nil, nil)
if err != nil {
return fmt.Errorf("unable to verify asset "+
"group witness: %w", err)
}

newGroup := &asset.AssetGroup{
Genesis: &groupReqs[i].NewAsset.Genesis,
GroupKey: groupKey,
}

assetGroups = append(assetGroups, newGroup)
}

// With all the asset group witnesses validated, we can now save them
// to disk.
err = c.cfg.Log.AddSeedlingGroups(ctx, genesisPoint, assetGroups)
if err != nil {
return fmt.Errorf("unable to write seedling groups: %w", err)
}

return nil
}

Expand Down

0 comments on commit 49f7b1c

Please sign in to comment.