Skip to content

Commit 49f7b1c

Browse files
committed
tapgarden: implement sealBatch
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.
1 parent dabdacb commit 49f7b1c

File tree

5 files changed

+273
-5
lines changed

5 files changed

+273
-5
lines changed

fn/iter.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ func ForEach[T any](items []T, f func(T)) {
2828
// ForEachMapItem is a generic implementation of a for-each (map with side
2929
// effects). This can be used to ensure that any normal for-loop don't run into
3030
// bugs due to loop variable scoping.
31-
func ForEachMapItem[T any, K comparable](items map[K]T, f func(T)) {
31+
func ForEachMapItem[T any, K comparable](items map[K]T, f func(K, T)) {
3232
for i := range items {
33-
f(items[i])
33+
f(i, items[i])
3434
}
3535
}
3636

tapgarden/batch.go

+4
Original file line numberDiff line numberDiff line change
@@ -219,3 +219,7 @@ func (m *MintingBatch) TapSibling() []byte {
219219
func (m *MintingBatch) UpdateTapSibling(sibling *chainhash.Hash) {
220220
m.tapSibling = sibling
221221
}
222+
223+
func (m *MintingBatch) IsFunded() bool {
224+
return m.GenesisPacket != nil
225+
}

tapgarden/caretaker.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -1595,10 +1595,10 @@ func GenRawGroupAnchorVerifier(ctx context.Context) func(*asset.Genesis,
15951595
assetGroupKey := asset.ToSerialized(&groupKey.GroupPubKey)
15961596
groupAnchor, err := groupAnchors.Get(assetGroupKey)
15971597
if err != nil {
1598-
// TODO(jhb): add tapscript root support
15991598
singleTweak := gen.ID()
16001599
tweakedGroupKey, err := asset.GroupPubKey(
1601-
groupKey.RawKey.PubKey, singleTweak[:], nil,
1600+
groupKey.RawKey.PubKey, singleTweak[:],
1601+
groupKey.TapscriptRoot,
16021602
)
16031603
if err != nil {
16041604
return err

tapgarden/interface.go

+11
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,17 @@ type MintingStore interface {
209209
FetchMintingBatch(ctx context.Context,
210210
batchKey *btcec.PublicKey) (*MintingBatch, error)
211211

212+
// AddSeedlingGroups stores the asset groups for seedlings associated
213+
// with a batch.
214+
AddSeedlingGroups(ctx context.Context, genesisOutpoint wire.OutPoint,
215+
assetGroups []*asset.AssetGroup) error
216+
217+
// FetchSeedlingGroups is used to fetch the asset groups for seedlings
218+
// associated with a funded batch.
219+
FetchSeedlingGroups(ctx context.Context, genesisOutpoint wire.OutPoint,
220+
anchorOutputIndex uint32,
221+
seedlings []*Seedling) ([]*asset.AssetGroup, error)
222+
212223
// AddSproutsToBatch adds a new set of sprouts to the batch, along with
213224
// a GenesisPacket, that once signed and broadcast with create the
214225
// set of assets on chain.

tapgarden/planter.go

+254-1
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,167 @@ func (c *ChainPlanter) fundGenesisPsbt(ctx context.Context,
495495
return fundedGenesisPkt, nil
496496
}
497497

498+
// filterSeedlingsWithGroup separates a set of seedlings into two sets based on
499+
// their relation to an asset group, which has not been constructed yet.
500+
func filterSeedlingsWithGroup(
501+
seedlings map[string]*Seedling) (map[string]*Seedling,
502+
map[string]*Seedling) {
503+
504+
withGroup := make(map[string]*Seedling)
505+
withoutGroup := make(map[string]*Seedling)
506+
fn.ForEachMapItem(seedlings, func(name string, seedling *Seedling) {
507+
switch {
508+
case seedling.GroupInfo != nil || seedling.GroupAnchor != nil ||
509+
seedling.EnableEmission:
510+
511+
withGroup[name] = seedling
512+
513+
default:
514+
withoutGroup[name] = seedling
515+
}
516+
})
517+
518+
return withGroup, withoutGroup
519+
}
520+
521+
// buildGroupReqs creates group key requests and asset group genesis TXs for
522+
// seedlings that are part of a funded batch.
523+
func (c *ChainPlanter) buildGroupReqs(genesisPoint wire.OutPoint,
524+
assetOutputIndex uint32,
525+
groupSeedlings map[string]*Seedling) ([]asset.GroupKeyRequest,
526+
[]asset.GroupVirtualTx, error) {
527+
528+
// Seedlings that anchor a group may be referenced by other seedlings,
529+
// and therefore need to be mapped to sprouts first so that we derive
530+
// the initial tweaked group key early.
531+
orderedSeedlings := SortSeedlings(maps.Values(groupSeedlings))
532+
newGroups := make(map[string]*asset.AssetGroup)
533+
groupReqs := make([]asset.GroupKeyRequest, 0, len(orderedSeedlings))
534+
genTXs := make([]asset.GroupVirtualTx, 0, len(orderedSeedlings))
535+
536+
for _, seedlingName := range orderedSeedlings {
537+
seedling := groupSeedlings[seedlingName]
538+
539+
assetGen := asset.Genesis{
540+
FirstPrevOut: genesisPoint,
541+
Tag: seedling.AssetName,
542+
OutputIndex: assetOutputIndex,
543+
Type: seedling.AssetType,
544+
}
545+
546+
// If the seedling has a meta data reveal set, then we'll bind
547+
// that by including the hash of the meta data in the asset
548+
// genesis.
549+
if seedling.Meta != nil {
550+
assetGen.MetaHash = seedling.Meta.MetaHash()
551+
}
552+
553+
var (
554+
amount uint64
555+
groupInfo *asset.AssetGroup
556+
protoAsset *asset.Asset
557+
err error
558+
)
559+
560+
// Determine the amount for the actual asset.
561+
switch seedling.AssetType {
562+
case asset.Normal:
563+
amount = seedling.Amount
564+
case asset.Collectible:
565+
amount = 1
566+
}
567+
568+
// If the seedling has a group key specified,
569+
// that group key was validated earlier. We need to
570+
// sign the new genesis with that group key.
571+
if seedling.HasGroupKey() {
572+
groupInfo = seedling.GroupInfo
573+
}
574+
575+
// If the seedling has a group anchor specified, that anchor
576+
// was validated earlier and the corresponding group has already
577+
// been created. We need to look up the group key and sign
578+
// the asset genesis with that key.
579+
if seedling.GroupAnchor != nil {
580+
groupInfo = newGroups[*seedling.GroupAnchor]
581+
}
582+
583+
// If a group witness needs to be produced, then we will need a
584+
// partially filled asset as part of the signing process.
585+
if groupInfo != nil || seedling.EnableEmission {
586+
protoAsset, err = asset.New(
587+
assetGen, amount, 0, 0, seedling.ScriptKey,
588+
nil,
589+
asset.WithAssetVersion(seedling.AssetVersion),
590+
)
591+
if err != nil {
592+
return nil, nil, fmt.Errorf("unable to create "+
593+
"asset for group key signing: %w", err)
594+
}
595+
}
596+
597+
if groupInfo != nil {
598+
groupReq, err := asset.NewGroupKeyRequest(
599+
groupInfo.GroupKey.RawKey, *groupInfo.Genesis,
600+
protoAsset, groupInfo.GroupKey.TapscriptRoot,
601+
)
602+
if err != nil {
603+
return nil, nil, fmt.Errorf("unable to "+
604+
"request asset group membership: %w",
605+
err)
606+
}
607+
608+
genTx, err := groupReq.BuildGroupVirtualTx(
609+
c.cfg.GenTxBuilder,
610+
)
611+
if err != nil {
612+
return nil, nil, err
613+
}
614+
615+
groupReqs = append(groupReqs, *groupReq)
616+
genTXs = append(genTXs, *genTx)
617+
}
618+
619+
// If emission is enabled, an internal key for the group should
620+
// already be specified. Use that to derive the key group
621+
// signature along with the tweaked key group.
622+
if seedling.EnableEmission {
623+
if seedling.GroupInternalKey == nil {
624+
return nil, nil, fmt.Errorf("unable to " +
625+
"derive group key")
626+
}
627+
628+
groupReq, err := asset.NewGroupKeyRequest(
629+
*seedling.GroupInternalKey, assetGen,
630+
protoAsset, seedling.GroupTapscriptRoot,
631+
)
632+
if err != nil {
633+
return nil, nil, fmt.Errorf("unable to "+
634+
"request asset group creation: %w", err)
635+
}
636+
637+
genTx, err := groupReq.BuildGroupVirtualTx(
638+
c.cfg.GenTxBuilder,
639+
)
640+
if err != nil {
641+
return nil, nil, err
642+
}
643+
644+
groupReqs = append(groupReqs, *groupReq)
645+
genTXs = append(genTXs, *genTx)
646+
647+
newGroups[seedlingName] = &asset.AssetGroup{
648+
Genesis: &assetGen,
649+
GroupKey: &asset.GroupKey{
650+
RawKey: *seedling.GroupInternalKey,
651+
},
652+
}
653+
}
654+
}
655+
656+
return groupReqs, genTXs, nil
657+
}
658+
498659
// freezeMintingBatch freezes a target minting batch which means that no new
499660
// assets can be added to the batch.
500661
func freezeMintingBatch(ctx context.Context, batchStore MintingStore,
@@ -924,7 +1085,99 @@ func (c *ChainPlanter) fundBatch(ctx context.Context, params FundParams) error {
9241085
return nil
9251086
}
9261087

927-
func (c *ChainPlanter) sealBatch(params SealParams) error {
1088+
// sealBatch will verify that each grouped asset in the pending batch has an
1089+
// asset group witness, and will attempt to create asset group witnesses when
1090+
// possible if they are not provided. After all asset group witnesses have been
1091+
// validated, they are saved to disk to be used by the caretaker during batch
1092+
// finalization.
1093+
func (c *ChainPlanter) sealBatch(ctx context.Context, _ SealParams) error {
1094+
// A batch should exist with 1+ seedlings and be funded before being
1095+
// sealed.
1096+
if c.pendingBatch == nil {
1097+
return fmt.Errorf("no pending batch")
1098+
}
1099+
1100+
if len(c.pendingBatch.Seedlings) == 0 {
1101+
return fmt.Errorf("no seedlings in batch")
1102+
}
1103+
1104+
if !c.pendingBatch.IsFunded() {
1105+
return fmt.Errorf("batch is not funded")
1106+
}
1107+
1108+
// Filter the batch seedlings to only consider those that will become
1109+
// grouped assets. If there are no such seedlings, then there is nothing
1110+
// to seal and no action is needed.
1111+
groupSeedlings, _ := filterSeedlingsWithGroup(c.pendingBatch.Seedlings)
1112+
if len(groupSeedlings) == 0 {
1113+
return nil
1114+
}
1115+
1116+
// Before we can build the group key requests for each seedling, we must
1117+
// fetch the genesis point and anchor index for the batch.
1118+
anchorOutputIndex := uint32(0)
1119+
if c.pendingBatch.GenesisPacket.ChangeOutputIndex == 0 {
1120+
anchorOutputIndex = 1
1121+
}
1122+
1123+
genesisPoint := extractGenesisOutpoint(
1124+
c.pendingBatch.GenesisPacket.Pkt.UnsignedTx,
1125+
)
1126+
1127+
// Construct the group key requests and group virtual TXs for each
1128+
// seedling. With these we can verify provided asset group witnesses,
1129+
// or attempt to derive asset group witnesses if needed.
1130+
groupReqs, genTXs, err := c.buildGroupReqs(
1131+
genesisPoint, anchorOutputIndex, groupSeedlings,
1132+
)
1133+
if err != nil {
1134+
return fmt.Errorf("unable to build group requests: %w", err)
1135+
}
1136+
1137+
assetGroups := make([]*asset.AssetGroup, 0, len(groupReqs))
1138+
for i := 0; i < len(groupReqs); i++ {
1139+
// Derive the asset group witness.
1140+
groupKey, err := asset.DeriveGroupKey(
1141+
c.cfg.GenSigner, genTXs[i], groupReqs[i], nil,
1142+
)
1143+
if err != nil {
1144+
return err
1145+
}
1146+
1147+
// Recreate the asset with the populated group key and validate
1148+
// the asset group witness.
1149+
protoAsset := groupReqs[i].NewAsset
1150+
groupedAsset, err := asset.New(
1151+
protoAsset.Genesis, protoAsset.Amount,
1152+
protoAsset.LockTime, protoAsset.RelativeLockTime,
1153+
protoAsset.ScriptKey, groupKey,
1154+
asset.WithAssetVersion(protoAsset.Version),
1155+
)
1156+
if err != nil {
1157+
return err
1158+
}
1159+
1160+
err = c.cfg.TxValidator.Execute(groupedAsset, nil, nil)
1161+
if err != nil {
1162+
return fmt.Errorf("unable to verify asset "+
1163+
"group witness: %w", err)
1164+
}
1165+
1166+
newGroup := &asset.AssetGroup{
1167+
Genesis: &groupReqs[i].NewAsset.Genesis,
1168+
GroupKey: groupKey,
1169+
}
1170+
1171+
assetGroups = append(assetGroups, newGroup)
1172+
}
1173+
1174+
// With all the asset group witnesses validated, we can now save them
1175+
// to disk.
1176+
err = c.cfg.Log.AddSeedlingGroups(ctx, genesisPoint, assetGroups)
1177+
if err != nil {
1178+
return fmt.Errorf("unable to write seedling groups: %w", err)
1179+
}
1180+
9281181
return nil
9291182
}
9301183

0 commit comments

Comments
 (0)