diff --git a/address/address.go b/address/address.go index b014171e6..a41a59253 100644 --- a/address/address.go +++ b/address/address.go @@ -276,7 +276,11 @@ func (a *Tap) AttachGenesis(gen asset.Genesis) { // TapCommitmentKey is the key that maps to the root commitment for the asset // group specified by a Taproot Asset address. func (a *Tap) TapCommitmentKey() [32]byte { - return asset.TapCommitmentKey(a.AssetID, a.GroupKey) + assetSpecifier := asset.NewSpecifierOptionalGroupPubKey( + a.AssetID, a.GroupKey, + ) + + return asset.TapCommitmentKey(assetSpecifier) } // AssetCommitmentKey is the key that maps to the asset leaf for the asset diff --git a/asset/asset.go b/asset/asset.go index 903828f6e..551a3d886 100644 --- a/asset/asset.go +++ b/asset/asset.go @@ -93,10 +93,10 @@ const ( type EncodeType uint8 const ( - // Encode normal is the normal encoding type for an asset. + // EncodeNormal normal is the normal encoding type for an asset. EncodeNormal EncodeType = iota - // EncodeSegwit denotes that the witness vector field is not be be + // EncodeSegwit denotes that the witness vector field is not to be // encoded. EncodeSegwit ) @@ -247,6 +247,124 @@ func DecodeGenesis(r io.Reader) (Genesis, error) { return gen, err } +var ( + // ErrUnwrapAssetID is an error type which is returned when an asset ID + // cannot be unwrapped from a specifier. + ErrUnwrapAssetID = errors.New("unable to unwrap asset ID") +) + +// Specifier is a type that can be used to specify an asset by its ID, its asset +// group public key, or both. +type Specifier struct { + // id is the asset ID. + id fn.Option[ID] + + // groupKey is the asset group public key. + groupKey fn.Option[btcec.PublicKey] +} + +// NewSpecifierOptionalGroupPubKey creates a new specifier that specifies an +// asset by its ID and an optional group public key. +func NewSpecifierOptionalGroupPubKey(id ID, + groupPubKey *btcec.PublicKey) Specifier { + + s := Specifier{ + id: fn.Some(id), + } + + if groupPubKey != nil { + s.groupKey = fn.Some(*groupPubKey) + } + + return s +} + +// NewSpecifierOptionalGroupKey creates a new specifier that specifies an +// asset by its ID and an optional group key. +func NewSpecifierOptionalGroupKey(id ID, groupKey *GroupKey) Specifier { + s := Specifier{ + id: fn.Some(id), + } + + if groupKey != nil { + s.groupKey = fn.Some(groupKey.GroupPubKey) + } + + return s +} + +// NewSpecifierFromId creates a new specifier that specifies an asset by its ID. +func NewSpecifierFromId(id ID) Specifier { + return Specifier{ + id: fn.Some(id), + } +} + +// NewSpecifierFromGroupKey creates a new specifier that specifies an asset by +// its group public key. +func NewSpecifierFromGroupKey(groupPubKey btcec.PublicKey) Specifier { + return Specifier{ + groupKey: fn.Some(groupPubKey), + } +} + +// AsBytes returns the asset ID and group public key as byte slices. +func (s *Specifier) AsBytes() ([]byte, []byte) { + var assetIDBytes, groupKeyBytes []byte + + s.WhenGroupPubKey(func(groupKey btcec.PublicKey) { + groupKeyBytes = groupKey.SerializeCompressed() + }) + + s.WhenId(func(id ID) { + assetIDBytes = id[:] + }) + + return assetIDBytes, groupKeyBytes +} + +// HasId returns true if the asset ID field is specified. +func (s *Specifier) HasId() bool { + return s.id.IsSome() +} + +// HasGroupPubKey returns true if the asset group public key field is specified. +func (s *Specifier) HasGroupPubKey() bool { + return s.groupKey.IsSome() +} + +// WhenId executes the given function if the ID field is specified. +func (s *Specifier) WhenId(f func(ID)) { + s.id.WhenSome(f) +} + +// WhenGroupPubKey executes the given function if asset group public key field +// is specified. +func (s *Specifier) WhenGroupPubKey(f func(btcec.PublicKey)) { + s.groupKey.WhenSome(f) +} + +// UnwrapIdOrErr unwraps the ID field or returns an error if it is not +// specified. +func (s *Specifier) UnwrapIdOrErr() (ID, error) { + id := s.id.UnwrapToPtr() + if id == nil { + return ID{}, ErrUnwrapAssetID + } + + return *id, nil +} + +// UnwrapIdToPtr unwraps the ID field to a pointer. +func (s *Specifier) UnwrapIdToPtr() *ID { + return s.id.UnwrapToPtr() +} + +// UnwrapGroupKeyToPtr unwraps the asset group public key field to a pointer. +func (s *Specifier) UnwrapGroupKeyToPtr() *btcec.PublicKey { + return s.groupKey.UnwrapToPtr() +} + // Type denotes the asset types supported by the Taproot Asset protocol. type Type uint8 @@ -1434,23 +1552,38 @@ func New(genesis Genesis, amount, locktime, relativeLocktime uint64, } // TapCommitmentKey is the key that maps to the root commitment for a specific -// asset group within a TapCommitment. +// asset within a TapCommitment. // // NOTE: This function is also used outside the asset package. -func TapCommitmentKey(assetID ID, groupKey *btcec.PublicKey) [32]byte { - if groupKey == nil { - return assetID +func TapCommitmentKey(assetSpecifier Specifier) [32]byte { + var commitmentKey [32]byte + + switch { + case assetSpecifier.HasGroupPubKey(): + assetSpecifier.WhenGroupPubKey(func(pubKey btcec.PublicKey) { + serializedPubKey := schnorr.SerializePubKey(&pubKey) + commitmentKey = sha256.Sum256(serializedPubKey) + }) + + case assetSpecifier.HasId(): + assetSpecifier.WhenId(func(id ID) { + commitmentKey = id + }) + + default: + // We should never reach this point as the asset specifier + // should always have either a group public key, an asset ID, or + // both. + panic("invalid asset specifier") } - return sha256.Sum256(schnorr.SerializePubKey(groupKey)) + + return commitmentKey } // TapCommitmentKey is the key that maps to the root commitment for a specific // asset group within a TapCommitment. func (a *Asset) TapCommitmentKey() [32]byte { - if a.GroupKey == nil { - return TapCommitmentKey(a.Genesis.ID(), nil) - } - return TapCommitmentKey(a.Genesis.ID(), &a.GroupKey.GroupPubKey) + return TapCommitmentKey(a.Specifier()) } // AssetCommitmentKey returns a key which can be used to locate an @@ -1893,6 +2026,12 @@ func (a *Asset) Leaf() (*mssmt.LeafNode, error) { return mssmt.NewLeafNode(buf.Bytes(), a.Amount), nil } +// Specifier returns the asset's specifier. +func (a *Asset) Specifier() Specifier { + id := a.Genesis.ID() + return NewSpecifierOptionalGroupKey(id, a.GroupKey) +} + // Validate ensures that an asset is valid. func (a *Asset) Validate() error { // TODO(ffranr): Add validation check for remaining fields. diff --git a/commitment/asset.go b/commitment/asset.go index f8fed4d14..43819fc82 100644 --- a/commitment/asset.go +++ b/commitment/asset.go @@ -8,7 +8,6 @@ import ( "errors" "fmt" - "github.com/btcsuite/btcd/btcec/v2" "github.com/lightninglabs/taproot-assets/asset" "github.com/lightninglabs/taproot-assets/fn" "github.com/lightninglabs/taproot-assets/mssmt" @@ -151,16 +150,15 @@ func parseCommon(assets ...*asset.Asset) (*AssetCommitment, error) { assetsMap[key] = newAsset } - var groupPubKey *btcec.PublicKey - if assetGroupKey != nil { - groupPubKey = &assetGroupKey.GroupPubKey - } - // The tapKey here is what will be used to place this asset commitment // into the top-level Taproot Asset commitment. For assets without a // group key, then this will be the normal asset ID. Otherwise, this'll // be the sha256 of the group key. - tapKey := asset.TapCommitmentKey(firstAssetID, groupPubKey) + assetSpecifier := asset.NewSpecifierOptionalGroupKey( + firstAssetID, assetGroupKey, + ) + + tapKey := asset.TapCommitmentKey(assetSpecifier) return &AssetCommitment{ Version: maxVersion, diff --git a/rpcserver.go b/rpcserver.go index 98914f1b0..e1ff1d403 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -3234,11 +3234,14 @@ func (r *rpcServer) BurnAsset(ctx context.Context, "burn_amount=%d)", assetID[:], serializedGroupKey, in.AmountToBurn) + assetSpecifier := asset.NewSpecifierOptionalGroupPubKey( + assetID, groupKey, + ) + fundResp, err := r.cfg.AssetWallet.FundBurn( ctx, &tapsend.FundingDescriptor{ - ID: assetID, - GroupKey: groupKey, - Amount: in.AmountToBurn, + AssetSpecifier: assetSpecifier, + Amount: in.AmountToBurn, }, ) if err != nil { diff --git a/tapdb/assets_store.go b/tapdb/assets_store.go index 300191464..59e8e5602 100644 --- a/tapdb/assets_store.go +++ b/tapdb/assets_store.go @@ -806,14 +806,14 @@ func (a *AssetStore) constraintsToDbFilter( query.MinAnchorHeight, ) } - if query.AssetID != nil { - assetID := query.AssetID[:] - assetFilter.AssetIDFilter = assetID - } - if query.GroupKey != nil { - groupKey := query.GroupKey.SerializeCompressed() - assetFilter.KeyGroupFilter = groupKey - } + + // Add asset ID bytes and group key bytes to the filter. These + // byte arrays are empty if the asset ID or group key is not + // specified in the query. + assetIDBytes, groupKeyBytes := query.AssetSpecifier.AsBytes() + assetFilter.AssetIDFilter = assetIDBytes + assetFilter.KeyGroupFilter = groupKeyBytes + // TODO(roasbeef): only want to allow asset ID or other and not // both? diff --git a/tapdb/assets_store_test.go b/tapdb/assets_store_test.go index 4273c38cb..3b814800e 100644 --- a/tapdb/assets_store_test.go +++ b/tapdb/assets_store_test.go @@ -280,7 +280,6 @@ func TestImportAssetProof(t *testing.T) { // Add a random asset and corresponding proof into the database. testAsset, testProof := dbHandle.AddRandomAssetProof(t) - assetID := testAsset.ID() initialBlob := testProof.Blob // We should now be able to retrieve the set of all assets inserted on @@ -314,11 +313,8 @@ func TestImportAssetProof(t *testing.T) { // We should also be able to fetch the created asset above based on // either the asset ID, or key group via the main coin selection // routine. - var assetConstraints tapfreighter.CommitmentConstraints - if testAsset.GroupKey != nil { - assetConstraints.GroupKey = &testAsset.GroupKey.GroupPubKey - } else { - assetConstraints.AssetID = &assetID + assetConstraints := tapfreighter.CommitmentConstraints{ + AssetSpecifier: testAsset.Specifier(), } selectedAssets, err := assetStore.ListEligibleCoins( ctxb, assetConstraints, @@ -660,16 +656,20 @@ func (a *assetGenerator) genAssets(t *testing.T, assetStore *AssetStore, } } -func (a *assetGenerator) bindAssetID(i int, op wire.OutPoint) *asset.ID { +func (a *assetGenerator) assetSpecifierAssetID(i int, + op wire.OutPoint) asset.Specifier { + gen := a.assetGens[i] gen.FirstPrevOut = op id := gen.ID() - return &id + return asset.NewSpecifierFromId(id) } -func (a *assetGenerator) bindGroupKey(i int, op wire.OutPoint) *btcec.PublicKey { +func (a *assetGenerator) assetSpecifierGroupKey(i int, + op wire.OutPoint) asset.Specifier { + gen := a.assetGens[i] gen.FirstPrevOut = op genTweak := gen.ID() @@ -678,8 +678,9 @@ func (a *assetGenerator) bindGroupKey(i int, op wire.OutPoint) *btcec.PublicKey internalPriv := input.TweakPrivKey(&groupPriv, genTweak[:]) tweakedPriv := txscript.TweakTaprootPrivKey(*internalPriv, nil) + groupPubKey := tweakedPriv.PubKey() - return tweakedPriv.PubKey() + return asset.NewSpecifierFromGroupKey(*groupPubKey) } // TestFetchAllAssets tests that the different AssetQueryFilters work as @@ -1001,7 +1002,7 @@ func TestSelectCommitment(t *testing.T) { }, }, constraints: tapfreighter.CommitmentConstraints{ - AssetID: assetGen.bindAssetID( + AssetSpecifier: assetGen.assetSpecifierAssetID( 0, assetGen.anchorPoints[0], ), MinAmt: 2, @@ -1023,7 +1024,7 @@ func TestSelectCommitment(t *testing.T) { }, }, constraints: tapfreighter.CommitmentConstraints{ - AssetID: assetGen.bindAssetID( + AssetSpecifier: assetGen.assetSpecifierAssetID( 0, assetGen.anchorPoints[0], ), MinAmt: 10, @@ -1044,7 +1045,7 @@ func TestSelectCommitment(t *testing.T) { }, }, constraints: tapfreighter.CommitmentConstraints{ - AssetID: assetGen.bindAssetID( + AssetSpecifier: assetGen.assetSpecifierAssetID( 1, assetGen.anchorPoints[1], ), MinAmt: 10, @@ -1075,7 +1076,7 @@ func TestSelectCommitment(t *testing.T) { }, }, constraints: tapfreighter.CommitmentConstraints{ - GroupKey: assetGen.bindGroupKey( + AssetSpecifier: assetGen.assetSpecifierGroupKey( 0, assetGen.anchorPoints[0], ), MinAmt: 1, @@ -1105,7 +1106,7 @@ func TestSelectCommitment(t *testing.T) { }, }, constraints: tapfreighter.CommitmentConstraints{ - AssetID: assetGen.bindAssetID( + AssetSpecifier: assetGen.assetSpecifierAssetID( 0, assetGen.anchorPoints[0], ), MinAmt: 2, @@ -1147,7 +1148,7 @@ func TestSelectCommitment(t *testing.T) { }, }, constraints: tapfreighter.CommitmentConstraints{ - GroupKey: assetGen.bindGroupKey( + AssetSpecifier: assetGen.assetSpecifierGroupKey( 0, assetGen.anchorPoints[0], ), MinAmt: 1, diff --git a/tapfreighter/coin_select.go b/tapfreighter/coin_select.go index dc49aff21..ac4235f3d 100644 --- a/tapfreighter/coin_select.go +++ b/tapfreighter/coin_select.go @@ -52,9 +52,8 @@ func (s *CoinSelect) SelectCoins(ctx context.Context, } listConstraints := CommitmentConstraints{ - GroupKey: constraints.GroupKey, - AssetID: constraints.AssetID, - MinAmt: 1, + AssetSpecifier: constraints.AssetSpecifier, + MinAmt: 1, } eligibleCommitments, err := s.coinLister.ListEligibleCoins( ctx, listConstraints, diff --git a/tapfreighter/interface.go b/tapfreighter/interface.go index cadca20bb..c769fc7e1 100644 --- a/tapfreighter/interface.go +++ b/tapfreighter/interface.go @@ -5,7 +5,6 @@ import ( "fmt" "time" - "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil/psbt" "github.com/btcsuite/btcd/chaincfg/chainhash" @@ -28,13 +27,8 @@ import ( // // NOTE: Only the GroupKey or the AssetID should be set. type CommitmentConstraints struct { - // GroupKey is the required group key. This is an optional field, if - // set then the asset returned may have a distinct asset ID to the one - // specified below. - GroupKey *btcec.PublicKey - - // AssetID is the asset ID that needs to be satisfied. - AssetID *asset.ID + // AssetSpecifier specifies the asset. + AssetSpecifier asset.Specifier // MinAmt is the minimum amount that an asset commitment needs to hold // to satisfy the constraints. @@ -47,13 +41,8 @@ type CommitmentConstraints struct { // String returns the string representation of the commitment constraints. func (c *CommitmentConstraints) String() string { - var groupKeyBytes, assetIDBytes []byte - if c.GroupKey != nil { - groupKeyBytes = c.GroupKey.SerializeCompressed() - } - if c.AssetID != nil { - assetIDBytes = c.AssetID[:] - } + assetIDBytes, groupKeyBytes := c.AssetSpecifier.AsBytes() + return fmt.Sprintf("group_key=%x, asset_id=%x, min_amt=%d", groupKeyBytes, assetIDBytes, c.MinAmt) } diff --git a/tapfreighter/wallet.go b/tapfreighter/wallet.go index bfacd77a2..31eceacef 100644 --- a/tapfreighter/wallet.go +++ b/tapfreighter/wallet.go @@ -366,8 +366,7 @@ func (f *AssetWallet) FundPacket(ctx context.Context, // send request. We'll map the address to a set of constraints, so we // can use that to do Taproot asset coin selection. constraints := CommitmentConstraints{ - GroupKey: fundDesc.GroupKey, - AssetID: &fundDesc.ID, + AssetSpecifier: fundDesc.AssetSpecifier, MinAmt: fundDesc.Amount, Bip86ScriptKeysOnly: true, } @@ -424,13 +423,18 @@ func (f *AssetWallet) FundPacket(ctx context.Context, func (f *AssetWallet) FundBurn(ctx context.Context, fundDesc *tapsend.FundingDescriptor) (*FundedVPacket, error) { + // Extract the asset ID and group key from the funding descriptor. + assetId, err := fundDesc.AssetSpecifier.UnwrapIdOrErr() + if err != nil { + return nil, err + } + // We need to find a commitment that has enough assets to satisfy this // send request. We'll map the address to a set of constraints, so we // can use that to do Taproot asset coin selection. constraints := CommitmentConstraints{ - GroupKey: fundDesc.GroupKey, - AssetID: &fundDesc.ID, - MinAmt: fundDesc.Amount, + AssetSpecifier: fundDesc.AssetSpecifier, + MinAmt: fundDesc.Amount, } selectedCommitments, err := f.cfg.CoinSelector.SelectCoins( ctx, constraints, PreferMaxAmount, commitment.TapCommitmentV2, @@ -461,7 +465,7 @@ func (f *AssetWallet) FundBurn(ctx context.Context, activeAssets := fn.Filter( selectedCommitments, func(c *AnchoredCommitment) bool { - return c.Asset.ID() == fundDesc.ID + return c.Asset.ID() == assetId }, ) @@ -495,7 +499,7 @@ func (f *AssetWallet) FundBurn(ctx context.Context, vPkt := &tappsbt.VPacket{ Inputs: []*tappsbt.VInput{{ PrevID: asset.PrevID{ - ID: fundDesc.ID, + ID: assetId, }, }}, Outputs: []*tappsbt.VOutput{{ @@ -582,8 +586,13 @@ func (f *AssetWallet) fundPacketWithInputs(ctx context.Context, fundDesc *tapsend.FundingDescriptor, vPkt *tappsbt.VPacket, selectedCommitments []*AnchoredCommitment) (*FundedVPacket, error) { + assetId, err := fundDesc.AssetSpecifier.UnwrapIdOrErr() + if err != nil { + return nil, err + } + log.Infof("Selected %v asset inputs for send of %d to %x", - len(selectedCommitments), fundDesc.Amount, fundDesc.ID[:]) + len(selectedCommitments), fundDesc.Amount, assetId[:]) assetType := selectedCommitments[0].Asset.Type diff --git a/tapsend/send.go b/tapsend/send.go index 7d293ffa2..27d9d43d4 100644 --- a/tapsend/send.go +++ b/tapsend/send.go @@ -165,11 +165,8 @@ type AssetGroupQuerier interface { // verify input assets in order to send to a specific recipient. It is a subset // of the information contained in a Taproot Asset address. type FundingDescriptor struct { - // ID is the asset ID of the asset being transferred. - ID asset.ID - - // GroupKey is the optional group key of the asset to transfer. - GroupKey *btcec.PublicKey + // AssetSpecifier is the asset specifier. + AssetSpecifier asset.Specifier // Amount is the amount of the asset to transfer. Amount uint64 @@ -178,7 +175,7 @@ type FundingDescriptor struct { // TapCommitmentKey is the key that maps to the root commitment for the asset // group specified by a recipient descriptor. func (r *FundingDescriptor) TapCommitmentKey() [32]byte { - return asset.TapCommitmentKey(r.ID, r.GroupKey) + return asset.TapCommitmentKey(r.AssetSpecifier) } // DescribeRecipients extracts the recipient descriptors from a Taproot Asset @@ -205,9 +202,12 @@ func DescribeRecipients(ctx context.Context, vPkt *tappsbt.VPacket, return nil, fmt.Errorf("unable to query asset group: %w", err) } + assetSpecifier := asset.NewSpecifierOptionalGroupPubKey( + firstInput.PrevID.ID, groupPubKey, + ) + desc := &FundingDescriptor{ - ID: firstInput.PrevID.ID, - GroupKey: groupPubKey, + AssetSpecifier: assetSpecifier, } for idx := range vPkt.Outputs { desc.Amount += vPkt.Outputs[idx].Amount @@ -224,9 +224,13 @@ func DescribeAddrs(addrs []*address.Tap) (*FundingDescriptor, error) { } firstAddr := addrs[0] + + assetSpecifier := asset.NewSpecifierOptionalGroupPubKey( + firstAddr.AssetID, firstAddr.GroupKey, + ) + desc := &FundingDescriptor{ - ID: firstAddr.AssetID, - GroupKey: firstAddr.GroupKey, + AssetSpecifier: assetSpecifier, } for idx := range addrs { desc.Amount += addrs[idx].Amount @@ -252,10 +256,18 @@ func AssetFromTapCommitment(tapCommitment *commitment.TapCommitment, ErrMissingInputAsset) } + // Determine whether issuance is disabled for the asset. + issuanceDisabled := !desc.AssetSpecifier.HasGroupPubKey() + + assetId, err := desc.AssetSpecifier.UnwrapIdOrErr() + if err != nil { + return nil, err + } + // The asset tree must have a non-empty Asset at the location // specified by the sender's script key. assetCommitmentKey := asset.AssetCommitmentKey( - desc.ID, &inputScriptKey, desc.GroupKey == nil, + assetId, &inputScriptKey, issuanceDisabled, ) inputAsset, ok := assetCommitment.Asset(assetCommitmentKey) if !ok { diff --git a/tapsend/send_test.go b/tapsend/send_test.go index e9bd66921..0955cbb1d 100644 --- a/tapsend/send_test.go +++ b/tapsend/send_test.go @@ -1825,10 +1825,13 @@ func TestAddressValidInput(t *testing.T) { } func addrToFundDesc(addr address.Tap) *tapsend.FundingDescriptor { + assetSpecifier := asset.NewSpecifierOptionalGroupPubKey( + addr.AssetID, addr.GroupKey, + ) + return &tapsend.FundingDescriptor{ - ID: addr.AssetID, - GroupKey: addr.GroupKey, - Amount: addr.Amount, + AssetSpecifier: assetSpecifier, + Amount: addr.Amount, } } @@ -2433,8 +2436,7 @@ func TestValidateAnchorOutputs(t *testing.T) { vOutCommitmentProof := proof.CommitmentProof{ Proof: commitment.Proof{ - TaprootAssetProof: commitment. - TaprootAssetProof{ + TaprootAssetProof: commitment.TaprootAssetProof{ Version: asset1Commitment.Version, }, },