Skip to content

tapgarden: list batches correctly after asset transfer #992

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

Merged
merged 7 commits into from
Jul 10, 2024
Merged
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
91 changes: 90 additions & 1 deletion itest/assets_test.go
Original file line number Diff line number Diff line change
@@ -5,6 +5,8 @@ import (
"context"
"crypto/tls"
"net/http"
"slices"
"strings"
"time"

"github.com/btcsuite/btcd/btcec/v2"
@@ -24,6 +26,7 @@ import (
"github.com/stretchr/testify/require"
"golang.org/x/exp/maps"
"golang.org/x/net/http2"
"google.golang.org/protobuf/proto"
)

var (
@@ -438,7 +441,6 @@ func testMintAssetsWithTapscriptSibling(t *harnessTest) {
rpcIssuableAssets := MintAssetsConfirmBatch(
t.t, t.lndHarness.Miner.Client, t.tapd, issuableAssets,
)

AssertAssetBalances(t.t, t.tapd, rpcSimpleAssets, rpcIssuableAssets)

// Filter the managed UTXOs to select the genesis UTXO with the
@@ -528,3 +530,90 @@ func testMintAssetsWithTapscriptSibling(t *harnessTest) {
t.lndHarness.MineBlocksAndAssertNumTxes(1, 1)
t.lndHarness.AssertNumUTXOsWithConf(t.lndHarness.Bob, 1, 1, 1)
}

// testMintBatchAndTransfer tests that we can mint a batch of assets, observe
// the finalized batch state, and observe the same batch state after a transfer
// of an asset from the batch.
func testMintBatchAndTransfer(t *harnessTest) {
ctxb := context.Background()
rpcSimpleAssets := MintAssetsConfirmBatch(
t.t, t.lndHarness.Miner.Client, t.tapd, simpleAssets,
)

// List the batch right after minting.
originalBatches, err := t.tapd.ListBatches(
ctxb, &mintrpc.ListBatchRequest{},
)
require.NoError(t.t, err)

// We'll make a second node now that'll be the receiver of all the
// assets made above.
secondTapd := setupTapdHarness(
t.t, t, t.lndHarness.Bob, t.universeServer,
)
defer func() {
require.NoError(t.t, secondTapd.stop(!*noDelete))
}()

// In order to force a split, we don't try to send the full first asset.
a := rpcSimpleAssets[0]
addr, events := NewAddrWithEventStream(
t.t, secondTapd, &taprpc.NewAddrRequest{
AssetId: a.AssetGenesis.AssetId,
Amt: a.Amount - 1,
AssetVersion: a.Version,
},
)

AssertAddrCreated(t.t, secondTapd, a, addr)

sendResp, sendEvents := sendAssetsToAddr(t, t.tapd, addr)
sendRespJSON, err := formatProtoJSON(sendResp)
require.NoError(t.t, err)

t.Logf("Got response from sending assets: %v", sendRespJSON)

// Make sure that eventually we see a single event for the
// address.
AssertAddrEvent(t.t, secondTapd, addr, 1, statusDetected)

// Mine a block to make sure the events are marked as confirmed.
MineBlocks(t.t, t.lndHarness.Miner.Client, 1, 1)

// Eventually the event should be marked as confirmed.
AssertAddrEvent(t.t, secondTapd, addr, 1, statusConfirmed)

// Make sure we have imported and finalized all proofs.
AssertNonInteractiveRecvComplete(t.t, secondTapd, 1)
AssertSendEventsComplete(t.t, addr.ScriptKey, sendEvents)

// Make sure the receiver has received all events in order for
// the address.
AssertReceiveEvents(t.t, addr, events)

afterBatches, err := t.tapd.ListBatches(
ctxb, &mintrpc.ListBatchRequest{},
)
require.NoError(t.t, err)

// The batch listed after the transfer should be identical to the batch
// listed before the transfer.
require.Equal(
t.t, len(originalBatches.Batches), len(afterBatches.Batches),
)

originalBatch := originalBatches.Batches[0].Batch
afterBatch := afterBatches.Batches[0].Batch

// Sort the assets from the listed batch before comparison.
slices.SortFunc(originalBatch.Assets,
func(a, b *mintrpc.PendingAsset) int {
return strings.Compare(a.Name, b.Name)
})
slices.SortFunc(afterBatch.Assets,
func(a, b *mintrpc.PendingAsset) int {
return strings.Compare(a.Name, b.Name)
})

require.True(t.t, proto.Equal(originalBatch, afterBatch))
}
4 changes: 4 additions & 0 deletions itest/test_list_on_test.go
Original file line number Diff line number Diff line change
@@ -13,6 +13,10 @@ var testCases = []*testCase{
name: "mint batch resume",
test: testMintBatchResume,
},
{
name: "mint batch and transfer",
test: testMintBatchAndTransfer,
},
{
name: "asset meta validation",
test: testAssetMeta,
112 changes: 111 additions & 1 deletion proof/archive.go
Original file line number Diff line number Diff line change
@@ -145,6 +145,13 @@ type Archiver interface {
// specific fields need to be set in the Locator (e.g. the OutPoint).
FetchProof(ctx context.Context, id Locator) (Blob, error)

// FetchIssuanceProof fetches the issuance proof for an asset, given the
// anchor point of the issuance (NOT the genesis point for the asset).
//
// If a proof cannot be found, then ErrProofNotFound should be returned.
FetchIssuanceProof(ctx context.Context, id asset.ID,
anchorOutpoint wire.OutPoint) (Blob, error)

// HasProof returns true if the proof for the given locator exists. This
// is intended to be a performance optimized lookup compared to fetching
// a proof and checking for ErrProofNotFound.
@@ -385,6 +392,7 @@ func lookupProofFilePath(rootPath string, loc Locator) (string, error) {
assetID := hex.EncodeToString(loc.AssetID[:])
scriptKey := hex.EncodeToString(loc.ScriptKey.SerializeCompressed())

// TODO(jhb): Check for correct file suffix and truncated outpoint?
searchPattern := filepath.Join(rootPath, assetID, scriptKey+"*")
matches, err := filepath.Glob(searchPattern)
if err != nil {
@@ -529,6 +537,78 @@ func (f *FileArchiver) FetchProof(_ context.Context, id Locator) (Blob, error) {
return proofFile, nil
}

// FetchIssuanceProof fetches the issuance proof for an asset, given the
// anchor point of the issuance (NOT the genesis point for the asset).
//
// If a proof cannot be found, then ErrProofNotFound should be returned.
//
// NOTE: This implements the Archiver interface.
func (f *FileArchiver) FetchIssuanceProof(ctx context.Context, id asset.ID,
Copy link
Member

Choose a reason for hiding this comment

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

Can't this just use the universe archive only? The only thing you need to query for the issuance proof is the asset ID: https://github.com/lightninglabs/taproot-assets/blob/main/universe/interface.go#L329-L336

Copy link
Member

Choose a reason for hiding this comment

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

Discussed offline, and for maximal backwards compat, the file store may be the best option here as it existed before the universe store did.

anchorOutpoint wire.OutPoint) (Blob, error) {

// Construct a pattern to search for the issuance proof file. We'll
// leave the script key unspecified, as we don't know what the script
// key was at genesis.
assetID := hex.EncodeToString(id[:])
scriptKeyGlob := strings.Repeat("?", 2*btcec.PubKeyBytesLenCompressed)
truncatedHash := anchorOutpoint.Hash.String()[:outpointTruncateLength]

fileName := fmt.Sprintf("%s-%s-%d.%s",
scriptKeyGlob, truncatedHash, anchorOutpoint.Index,
TaprootAssetsFileEnding)

searchPattern := filepath.Join(f.proofPath, assetID, fileName)
matches, err := filepath.Glob(searchPattern)
if err != nil {
return nil, fmt.Errorf("error listing proof files: %w", err)
}
if len(matches) == 0 {
return nil, ErrProofNotFound
}

// We expect exactly one matching proof for a specific asset ID and
// outpoint. However, the proof file path uses the truncated outpoint,
// so an asset transfer with a collision in the first half of the TXID
// could also match. We can filter out such proof files by size.
proofFiles := make([]Blob, 0, len(matches))
for _, path := range matches {
proofFile, err := os.ReadFile(path)

switch {
case os.IsNotExist(err):
return nil, ErrProofNotFound

case err != nil:
return nil, fmt.Errorf("unable to find proof: %w", err)
}

proofFiles = append(proofFiles, proofFile)
}

switch {
// No proofs were read.
case len(proofFiles) == 0:
return nil, ErrProofNotFound

// Exactly one proof, we'll return it.
case len(proofFiles) == 1:
return proofFiles[0], nil

// Multiple proofs, return the smallest one.
default:
minProofIdx := 0
minProofSize := len(proofFiles[minProofIdx])
for idx, proof := range proofFiles {
if len(proof) < minProofSize {
minProofSize = len(proof)
minProofIdx = idx
}
}

return proofFiles[minProofIdx], nil
}
}

// HasProof returns true if the proof for the given locator exists. This is
// intended to be a performance optimized lookup compared to fetching a proof
// and checking for ErrProofNotFound.
@@ -704,10 +784,13 @@ func (f *FileArchiver) RemoveSubscriber(
return f.eventDistributor.RemoveSubscriber(subscriber)
}

// A compile-time interface to ensure FileArchiver meets the NotifyArchiver
// A compile-time assertion to ensure FileArchiver meets the NotifyArchiver
// interface.
var _ NotifyArchiver = (*FileArchiver)(nil)

// A compile-time assertion to ensure FileArchiver meets the Archiver interface.
var _ Archiver = (*FileArchiver)(nil)

// MultiArchiver is an archive of archives. It contains several archives and
// attempts to use them either as a look-aside cache, or a write through cache
// for all incoming requests.
@@ -763,6 +846,33 @@ func (m *MultiArchiver) FetchProof(ctx context.Context,
return nil, ErrProofNotFound
}

// FetchIssuanceProof fetches the issuance proof for an asset, given the
// anchor point of the issuance (NOT the genesis point for the asset).
func (m *MultiArchiver) FetchIssuanceProof(ctx context.Context,
id asset.ID, anchorOutpoint wire.OutPoint) (Blob, error) {

// Iterate through all our active backends and try to see if at least
// one of them contains the proof. Either one of them will have the
// proof, or we'll return an error back to the user.
for _, archive := range m.backends {
proof, err := archive.FetchIssuanceProof(
ctx, id, anchorOutpoint,
)

switch {
case errors.Is(err, ErrProofNotFound):
continue

case err != nil:
return nil, err
}

return proof, nil
}

return nil, ErrProofNotFound
}

// HasProof returns true if the proof for the given locator exists. This is
// intended to be a performance optimized lookup compared to fetching a proof
// and checking for ErrProofNotFound. The multi archiver only considers a proof
50 changes: 5 additions & 45 deletions proof/courier_test.go
Original file line number Diff line number Diff line change
@@ -3,7 +3,6 @@ package proof
import (
"bytes"
"context"
"fmt"
"testing"

"github.com/lightninglabs/taproot-assets/asset"
@@ -12,52 +11,10 @@ import (
"github.com/stretchr/testify/require"
)

type mockProofArchive struct {
proofs map[Locator]Blob
}

func newMockProofArchive() *mockProofArchive {
return &mockProofArchive{
proofs: make(map[Locator]Blob),
}
}

func (m *mockProofArchive) FetchProof(ctx context.Context,
id Locator) (Blob, error) {

proof, ok := m.proofs[id]
if !ok {
return nil, ErrProofNotFound
}

return proof, nil
}

func (m *mockProofArchive) HasProof(ctx context.Context,
id Locator) (bool, error) {

_, ok := m.proofs[id]

return ok, nil
}

func (m *mockProofArchive) FetchProofs(ctx context.Context,
id asset.ID) ([]*AnnotatedProof, error) {

return nil, fmt.Errorf("not implemented")
}

func (m *mockProofArchive) ImportProofs(context.Context, HeaderVerifier,
MerkleVerifier, GroupVerifier, ChainLookupGenerator, bool,
...*AnnotatedProof) error {

return fmt.Errorf("not implemented")
}

// TestUniverseRpcCourierLocalArchiveShortCut tests that the local archive is
// used as a shortcut to fetch a proof if it's available.
func TestUniverseRpcCourierLocalArchiveShortCut(t *testing.T) {
localArchive := newMockProofArchive()
localArchive := NewMockProofArchive()

testBlocks := readTestData(t)
oddTxBlock := testBlocks[0]
@@ -79,7 +36,10 @@ func TestUniverseRpcCourierLocalArchiveShortCut(t *testing.T) {
ScriptKey: *proof.Asset.ScriptKey.PubKey,
OutPoint: fn.Ptr(proof.OutPoint()),
}
localArchive.proofs[locator] = proofBlob
locHash, err := locator.Hash()
require.NoError(t, err)

localArchive.proofs.Store(locHash, proofBlob)

courier := &UniverseRpcCourier{
recipient: Recipient{},
181 changes: 181 additions & 0 deletions proof/mock.go
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ import (
"bytes"
"context"
"encoding/hex"
"fmt"
"io"
"net/url"
"sync"
@@ -20,6 +21,8 @@ import (
"github.com/lightninglabs/taproot-assets/fn"
"github.com/lightninglabs/taproot-assets/internal/test"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnutils"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/stretchr/testify/require"
)

@@ -254,6 +257,184 @@ func (m *mockChainLookup) GenProofChainLookup(*Proof) (asset.ChainLookup,
var _ asset.ChainLookup = (*mockChainLookup)(nil)
var _ ChainLookupGenerator = (*mockChainLookup)(nil)

// MockProofArchive is a map that implements the Archiver interface.
type MockProofArchive struct {
proofs lnutils.SyncMap[[32]byte, Blob]
locators lnutils.SyncMap[[132]byte, [32]byte]
}

// NewMockProofArchive creates a new mock proof archive.
func NewMockProofArchive() *MockProofArchive {
return &MockProofArchive{
proofs: lnutils.SyncMap[[32]byte, Blob]{},
locators: lnutils.SyncMap[[132]byte, [32]byte]{},
}
}

// storeLocator stores the locator as a byte array to allow for pattern matching
// over the locators for the stored proofs, similar to the FileArchiver
// implementation of FetchIssuanceProof.
func (m *MockProofArchive) storeLocator(id Locator) error {
var locBuf bytes.Buffer

if id.AssetID == nil {
return fmt.Errorf("missing asset ID")
}

locBuf.Write(id.AssetID[:])
if id.GroupKey != nil {
locBuf.Write(id.GroupKey.SerializeCompressed())
} else {
locBuf.Write(bytes.Repeat([]byte{0x00}, 33))
}

locBuf.Write(id.ScriptKey.SerializeCompressed())
if id.OutPoint != nil {
err := lnwire.WriteOutPoint(&locBuf, *id.OutPoint)
if err != nil {
return err
}
} else {
locBuf.Write(bytes.Repeat([]byte{0x00}, 34))
}

var locArray [132]byte
copy(locArray[:], locBuf.Bytes())

locHash, err := id.Hash()
if err != nil {
return err
}

m.locators.Store(locArray, locHash)

return nil
}

// FetchProof fetches a proof for an asset uniquely identified by the passed
// Locator. If a proof cannot be found, then ErrProofNotFound is returned.
func (m *MockProofArchive) FetchProof(_ context.Context,
id Locator) (Blob, error) {

idHash, err := id.Hash()
if err != nil {
return nil, err
}

proof, ok := m.proofs.Load(idHash)
if !ok {
return nil, ErrProofNotFound
}

return proof, nil
}

// FetchIssuanceProof fetches the issuance proof for an asset, given the
// anchor point of the issuance (NOT the genesis point for the asset).
//
// If a proof cannot be found, then ErrProofNotFound should be returned.
func (m *MockProofArchive) FetchIssuanceProof(_ context.Context,
id asset.ID, anchorOutpoint wire.OutPoint) (Blob, error) {

var outpointBuf bytes.Buffer
err := lnwire.WriteOutPoint(&outpointBuf, anchorOutpoint)
if err != nil {
return nil, err
}

// Mimic the pattern matching done with proof file paths in
// FileArchiver.FetchIssuanceProof().
matchingHashes := make([][32]byte, 0)
locMatcher := func(locBytes [132]byte, locHash [32]byte) error {
if bytes.Equal(locBytes[:32], id[:]) &&
bytes.Equal(locBytes[98:], outpointBuf.Bytes()) {

matchingHashes = append(matchingHashes, locHash)
}

return nil
}

m.locators.ForEach(locMatcher)
if len(matchingHashes) == 0 {
return nil, ErrProofNotFound
}

matchingProofs := make([]Blob, 0)
for _, locHash := range matchingHashes {
proof, ok := m.proofs.Load(locHash)
if !ok {
return nil, ErrProofNotFound
}

matchingProofs = append(matchingProofs, proof)
}

switch {
case len(matchingProofs) == 1:
return matchingProofs[0], nil

// Multiple proofs, return the smallest one.
default:
minProofIdx := 0
minProofSize := len(matchingProofs[minProofIdx])
for idx, proof := range matchingProofs {
if len(proof) < minProofSize {
minProofSize = len(proof)
minProofIdx = idx
}
}

return matchingProofs[minProofIdx], nil
}
}

// HasProof returns true if the proof for the given locator exists.
func (m *MockProofArchive) HasProof(_ context.Context,
id Locator) (bool, error) {

idHash, err := id.Hash()
if err != nil {
return false, err
}

_, ok := m.proofs.Load(idHash)

return ok, nil
}

// FetchProofs would fetch all proofs for a specific asset ID, but will always
// err for the mock proof archive.
func (m *MockProofArchive) FetchProofs(_ context.Context,
id asset.ID) ([]*AnnotatedProof, error) {

return nil, fmt.Errorf("not implemented")
}

// ImportProofs will store the given proofs, without performing any validation.
func (m *MockProofArchive) ImportProofs(_ context.Context, _ HeaderVerifier,
_ MerkleVerifier, _ GroupVerifier, _ ChainLookupGenerator, _ bool,
proofs ...*AnnotatedProof) error {

for _, proof := range proofs {
err := m.storeLocator(proof.Locator)
if err != nil {
return fmt.Errorf("mock archive failed: %w", err)
}

locHash, err := proof.Locator.Hash()
if err != nil {
return err
}

m.proofs.Store(locHash, proof.Blob)
}

return nil
}

var _ Archiver = (*MockProofArchive)(nil)

// MockProofCourierDispatcher is a mock proof courier dispatcher which returns
// the same courier for all requests.
type MockProofCourierDispatcher struct {
47 changes: 12 additions & 35 deletions tapdb/addrs.go
Original file line number Diff line number Diff line change
@@ -92,6 +92,10 @@ type AddrBook interface {
// asset groups related to them.
GroupStore

// FetchScriptKeyStore houses the methods related to fetching all
// information about a script key.
FetchScriptKeyStore

// FetchAddrs returns all the addresses based on the constraints of the
// passed AddrQuery.
FetchAddrs(ctx context.Context, arg AddrQuery) ([]Addresses, error)
@@ -153,11 +157,6 @@ type AddrBook interface {
FetchGenesisByAssetID(ctx context.Context,
assetID []byte) (sqlc.GenesisInfoView, error)

// FetchScriptKeyByTweakedKey attempts to fetch the script key and
// corresponding internal key from the database.
FetchScriptKeyByTweakedKey(ctx context.Context,
tweakedScriptKey []byte) (ScriptKey, error)

// FetchInternalKeyLocator fetches the key locator for an internal key.
FetchInternalKeyLocator(ctx context.Context, rawKey []byte) (KeyLocator,
error)
@@ -1158,43 +1157,21 @@ func (t *TapAddressBook) FetchScriptKey(ctx context.Context,
tweakedScriptKey *btcec.PublicKey) (*asset.TweakedScriptKey, error) {

var (
readOpts = NewAddrBookReadTx()
scriptKey *asset.TweakedScriptKey
err error
)
err := t.db.ExecTx(ctx, &readOpts, func(db AddrBook) error {
dbKey, err := db.FetchScriptKeyByTweakedKey(
ctx, tweakedScriptKey.SerializeCompressed(),
)
if err != nil {
return err
}

rawKey, err := btcec.ParsePubKey(dbKey.RawKey)
if err != nil {
return fmt.Errorf("unable to parse raw key: %w", err)
}

scriptKey = &asset.TweakedScriptKey{
Tweak: dbKey.Tweak,
RawKey: keychain.KeyDescriptor{
PubKey: rawKey,
KeyLocator: keychain.KeyLocator{
Family: keychain.KeyFamily(
dbKey.KeyFamily,
),
Index: uint32(dbKey.KeyIndex),
},
},
DeclaredKnown: dbKey.DeclaredKnown.Valid,
}

return nil
readOpts := NewAddrBookReadTx()
dbErr := t.db.ExecTx(ctx, &readOpts, func(db AddrBook) error {
scriptKey, err = fetchScriptKey(ctx, db, tweakedScriptKey)
return err
})

switch {
case errors.Is(err, sql.ErrNoRows):
case errors.Is(dbErr, sql.ErrNoRows):
return nil, address.ErrScriptKeyNotFound

case err != nil:
case dbErr != nil:
return nil, err
}

209 changes: 105 additions & 104 deletions tapdb/asset_minting.go
Original file line number Diff line number Diff line change
@@ -18,7 +18,6 @@ import (
"github.com/lightninglabs/taproot-assets/proof"
"github.com/lightninglabs/taproot-assets/tapdb/sqlc"
"github.com/lightninglabs/taproot-assets/tapgarden"
"github.com/lightninglabs/taproot-assets/tapscript"
"github.com/lightninglabs/taproot-assets/tapsend"
"github.com/lightningnetwork/lnd/keychain"
"golang.org/x/exp/maps"
@@ -141,6 +140,10 @@ type PendingAssetStore interface {
// GroupStore houses the methods related to querying asset groups.
GroupStore

// FetchScriptKeyStore houses the methods related to fetching all
// information about a script key.
FetchScriptKeyStore

// TapscriptTreeStore houses the methods related to storing, fetching,
// and deleting tapscript trees.
TapscriptTreeStore
@@ -772,7 +775,7 @@ func fetchAssetSprouts(ctx context.Context, q PendingAssetStore,

// We collect all the sprouts into fully grown assets, from which we'll
// then create asset and tap level commitments.
assetSprouts := make([]*asset.Asset, len(dbSprout))
sprouts := make([]*asset.Asset, len(dbSprout))
for i, sprout := range dbSprout {
// First, we'll decode the script key which very asset must
// specify, and populate the key locator information
@@ -897,64 +900,27 @@ func fetchAssetSprouts(ctx context.Context, q PendingAssetStore,

// TODO(roasbeef): need to update the above to set the
// witnesses of a valid asset
assetSprouts[i] = assetSprout
sprouts[i] = assetSprout
}

// Construct a TapCommitment from the batch sprouts, and verify that the
// version is correct by recomputing the genesis output script.
tapCommitment, err := commitment.FromAssets(
fn.Ptr(commitment.TapCommitmentV2), assetSprouts...,
)
if err != nil {
return nil, err
}

// If there are sprouts, let's find out whether they were created with
// a V2 commitment or not.
// Verify that we can reconstruct the genesis output script used in the
// anchor TX.
batchKey, err := btcec.ParsePubKey(rawBatchKey)
if err != nil {
return nil, err
}

var tapscriptSibling *chainhash.Hash
var tapSibling *chainhash.Hash
if len(batchSibling) != 0 {
tapscriptSibling, err = chainhash.NewHash(batchSibling)
tapSibling, err = chainhash.NewHash(batchSibling)
if err != nil {
return nil, err
}
}

computedScript, err := tapscript.PayToAddrScript(
*batchKey, tapscriptSibling, *tapCommitment,
return tapgarden.VerifyOutputScript(
batchKey, tapSibling, genScript, sprouts,
)
if err != nil {
return nil, err
}

if !bytes.Equal(genScript, computedScript) {
// The batch may have used a non-V2 commitment; check against a
// non-V2 commitment.
tapCommitment, err = commitment.FromAssets(
nil, assetSprouts...,
)
if err != nil {
return nil, err
}

computedScriptV0, err := tapscript.PayToAddrScript(
*batchKey, tapscriptSibling, *tapCommitment,
)
if err != nil {
return nil, err
}

if !bytes.Equal(genScript, computedScriptV0) {
return nil, fmt.Errorf("invalid commitment to asset "+
"sprouts: batch %x", rawBatchKey)
}
}

return tapCommitment, nil
}

// fetchAssetMetas attempts to fetch the asset meta reveal for each of the
@@ -1190,20 +1156,36 @@ func marshalMintingBatch(ctx context.Context, q PendingAssetStore,
}
}

// Depending on what state this batch is in, we'll
// either fetch the set of seedlings (asset
// descriptions w/ no real assets), or the set of
// sprouts (full defined assets, but not yet mined).
// Depending on what state this batch is in, we'll either return the set
// of seedlings (asset descriptions w/ no real assets), or the set of
// sprouts (full defined assets, but not yet mined). In all cases, we
// start by fetching the batch seedlings.
batchSeedlings, err := fetchAssetSeedlings(
ctx, q, dbBatch.RawKey,
)
if err != nil {
return nil, err
}

switch batchState {
// A batch in these states will only have seedlings.
case tapgarden.BatchStatePending,
tapgarden.BatchStateFrozen,
tapgarden.BatchStateSeedlingCancelled:

// In this case we can just fetch the set of
// descriptions of future assets to be.
batch.Seedlings, err = fetchAssetSeedlings(
ctx, q, dbBatch.RawKey,
)
batch.Seedlings = batchSeedlings

// For finalized batches, we need to fetch the assets from the proof
// archiver and not the DB. Set the batch seedlings here so they can be
// used later to fetch those proofs.
case tapgarden.BatchStateFinalized:
// A finalized batch must have a populated genesis packet.
if batch.GenesisPacket == nil {
return nil, fmt.Errorf("sprouted batch missing " +
"genesis packet")
}

batch.Seedlings = batchSeedlings

default:
if batch.GenesisPacket == nil {
@@ -1217,18 +1199,10 @@ func marshalMintingBatch(ctx context.Context, q PendingAssetStore,
}
genesisTx := batch.GenesisPacket.Pkt.UnsignedTx
genesisScript := genesisTx.TxOut[anchorOutputIndex].PkScript

var tapscriptSibling []byte
if len(batch.TapSibling()) != 0 {
tapscriptSibling = batch.TapSibling()
}

tapscriptSibling := batch.TapSibling()
batch.RootAssetCommitment, err = fetchAssetSprouts(
ctx, q, dbBatch.RawKey, tapscriptSibling, genesisScript,
)
if err != nil {
return nil, err
}

// Finally, for each asset contained in the root
// commitment above, we'll fetch the meta reveal for
@@ -1238,9 +1212,9 @@ func marshalMintingBatch(ctx context.Context, q PendingAssetStore,
batch.AssetMetas, err = fetchAssetMetas(
ctx, q, assetsInBatch,
)
}
if err != nil {
return nil, err
if err != nil {
return nil, err
}
}

return batch, nil
@@ -1363,57 +1337,57 @@ func (a *AssetMintingStore) AddSeedlingGroups(ctx context.Context,
// FetchSeedlingGroups is used to fetch the asset groups for seedlings
// associated with a funded batch.
func (a *AssetMintingStore) FetchSeedlingGroups(ctx context.Context,
genesisPoint wire.OutPoint, anchorOutputIndex uint32,
genPoint wire.OutPoint, anchorOutputIndex uint32,
seedlings []*tapgarden.Seedling) ([]*asset.AssetGroup, error) {

seedlingGroups := make([]*asset.AssetGroup, 0, len(seedlings))
seedlingGens := make([]*asset.Genesis, 0, len(seedlings))

// Compute meta hashes and geneses before reading from the DB.
fn.ForEach(seedlings, func(seedling *tapgarden.Seedling) {
gen := &asset.Genesis{
FirstPrevOut: genesisPoint,
Tag: seedling.AssetName,
OutputIndex: anchorOutputIndex,
Type: seedling.AssetType,
}

if seedling.Meta != nil {
gen.MetaHash = seedling.Meta.MetaHash()
}
var (
seedlingGroups []*asset.AssetGroup
err error
)

seedlingGens = append(seedlingGens, gen)
})
seedlingGens := fn.Map(seedlings,
func(s *tapgarden.Seedling) *asset.Genesis {
return fn.Ptr(s.Genesis(genPoint, anchorOutputIndex))
},
)

// Read geneses and asset groups.
readOpts := NewAssetStoreReadTx()
dbErr := a.db.ExecTx(ctx, &readOpts, func(q PendingAssetStore) error {
for i := range seedlingGens {
genID, err := fetchGenesisID(ctx, q, *seedlingGens[i])
if err != nil {
// Re-map the error about a missing asset
// genesis so it can be better handled in the
// planter.
if errors.Is(err, ErrFetchGenesisID) {
return tapgarden.ErrNoGenesis
}
seedlingGroups, err = fetchSeedlingGroups(ctx, q, seedlingGens)
return err
})
if dbErr != nil {
return nil, dbErr
}

return err
}
return seedlingGroups, nil
}

groupKey, err := fetchGroupByGenesis(ctx, q, genID)
if err != nil {
return err
// fetchSeedlingGroups fetches the asset groups for multiple geneses.
func fetchSeedlingGroups(ctx context.Context, q PendingAssetStore,
gens []*asset.Genesis) ([]*asset.AssetGroup, error) {

seedlingGroups := make([]*asset.AssetGroup, 0, len(gens))
for _, gen := range gens {
genID, err := fetchGenesisID(ctx, q, *gen)
if err != nil {
// Re-map the error about a missing asset
// genesis so it can be better handled in the
// planter.
if errors.Is(err, ErrFetchGenesisID) {
return nil, tapgarden.ErrNoGenesis
}

seedlingGroups = append(seedlingGroups, groupKey)
return nil, err
}

return nil
})
groupKey, err := fetchGroupByGenesis(ctx, q, genID)
if err != nil {
return nil, err
}

if dbErr != nil {
return nil, dbErr
seedlingGroups = append(seedlingGroups, groupKey)
}

return seedlingGroups, nil
@@ -1725,6 +1699,33 @@ func (a *AssetMintingStore) FetchGroupByGroupKey(ctx context.Context,
return dbGroup, nil
}

// FetchScriptKeyByTweakedKey fetches the populated script key given the tweaked
// script key.
func (a *AssetMintingStore) FetchScriptKeyByTweakedKey(ctx context.Context,
tweakedKey *btcec.PublicKey) (*asset.TweakedScriptKey, error) {

var (
scriptKey *asset.TweakedScriptKey
err error
)

readOpts := NewAssetStoreReadTx()
dbErr := a.db.ExecTx(ctx, &readOpts, func(q PendingAssetStore) error {
scriptKey, err = fetchScriptKey(ctx, q, tweakedKey)
return err
})

switch {
case errors.Is(dbErr, sql.ErrNoRows):
return nil, fmt.Errorf("script key not found")

case dbErr != nil:
return nil, err
}

return scriptKey, nil
}

// StoreTapscriptTree persists a Tapscript tree given a validated set of
// TapLeafs or a TapBranch. If the store succeeds, the root hash of the
// Tapscript tree is returned.
43 changes: 43 additions & 0 deletions tapdb/assets_common.go
Original file line number Diff line number Diff line change
@@ -422,6 +422,49 @@ func upsertScriptKey(ctx context.Context, scriptKey asset.ScriptKey,
return scriptKeyID, nil
}

// FetchScriptKeyStore houses the methods related to fetching all information
// about a script key.
type FetchScriptKeyStore interface {
// FetchScriptKeyByTweakedKey attempts to fetch the script key and
// corresponding internal key from the database.
FetchScriptKeyByTweakedKey(ctx context.Context,
tweakedScriptKey []byte) (ScriptKey, error)
}

// fetchScriptKey attempts to fetch the full tweaked script key struct
// (including the key descriptor) for the given tweaked script key.
func fetchScriptKey(ctx context.Context, q FetchScriptKeyStore,
tweakedScriptKey *btcec.PublicKey) (*asset.TweakedScriptKey, error) {

dbKey, err := q.FetchScriptKeyByTweakedKey(
ctx, tweakedScriptKey.SerializeCompressed(),
)
if err != nil {
return nil, err
}

rawKey, err := btcec.ParsePubKey(dbKey.RawKey)
if err != nil {
return nil, fmt.Errorf("unable to parse raw key: %w", err)
}

scriptKey := &asset.TweakedScriptKey{
Tweak: dbKey.Tweak,
RawKey: keychain.KeyDescriptor{
PubKey: rawKey,
KeyLocator: keychain.KeyLocator{
Family: keychain.KeyFamily(
dbKey.KeyFamily,
),
Index: uint32(dbKey.KeyIndex),
},
},
DeclaredKnown: dbKey.DeclaredKnown.Valid,
}

return scriptKey, nil
}

// FetchGenesisStore houses the methods related to fetching genesis assets.
type FetchGenesisStore interface {
// FetchGenesisByID returns a single genesis asset by its primary key
12 changes: 12 additions & 0 deletions tapdb/assets_store.go
Original file line number Diff line number Diff line change
@@ -1299,6 +1299,18 @@ func locatorToProofQuery(locator proof.Locator) (FetchAssetProof, error) {
return args, nil
}

// FetchIssuanceProof fetches the issuance proof for an asset, given the
// anchor point of the issuance (NOT the genesis point for the asset). For the
// AssetStore, we leave this unimplemented as we will only use this feature from
// the FileArchiver.
//
// NOTE: This implements the proof.Archiver interface.
func (a *AssetStore) FetchIssuanceProof(ctx context.Context, id asset.ID,
anchorOutpoint wire.OutPoint) (proof.Blob, error) {

return nil, proof.ErrProofNotFound
}

// HasProof returns true if the proof for the given locator exists. This is
// intended to be a performance optimized lookup compared to fetching a proof
// and checking for ErrProofNotFound.
61 changes: 61 additions & 0 deletions tapgarden/batch.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package tapgarden

import (
"bytes"
"fmt"
"sync/atomic"
"time"
@@ -193,6 +194,66 @@ func (m *MintingBatch) MintingOutputKey(sibling *commitment.TapscriptPreimage) (
return m.mintingPubKey, m.taprootAssetScriptRoot, nil
}

// VerifyOutputScript recomputes a batch genesis output script from a batch key,
// tapscript sibling, and set of assets. It checks multiple tap commitment
// versions to account for legacy batches.
func VerifyOutputScript(batchKey *btcec.PublicKey, tapSibling *chainhash.Hash,
genesisScript []byte, assets []*asset.Asset) (*commitment.TapCommitment,
error) {

// Construct a TapCommitment from the batch sprouts, and verify that the
// version is correct by recomputing the genesis output script.
buildTrimmedCommitment := func(vers *commitment.TapCommitmentVersion,
assets ...*asset.Asset) (*commitment.TapCommitment, error) {

tapCommitment, err := commitment.FromAssets(vers, assets...)
if err != nil {
return nil, err
}

return commitment.TrimSplitWitnesses(vers, tapCommitment)
}

tapCommitment, err := buildTrimmedCommitment(
fn.Ptr(commitment.TapCommitmentV2), assets...,
)
if err != nil {
return nil, err
}

computedScript, err := tapscript.PayToAddrScript(
*batchKey, tapSibling, *tapCommitment,
)
if err != nil {
return nil, err
}

if !bytes.Equal(genesisScript, computedScript) {
// The batch may have used a non-V2 commitment; check against a
// non-V2 commitment.
tapCommitment, err = buildTrimmedCommitment(nil, assets...)
if err != nil {
return nil, err
}

computedScriptV0, err := tapscript.PayToAddrScript(
*batchKey, tapSibling, *tapCommitment,
)
if err != nil {
return nil, err
}

if !bytes.Equal(genesisScript, computedScriptV0) {
return nil, fmt.Errorf("invalid commitment to asset "+
"sprouts: batch %x",
batchKey.SerializeCompressed(),
)
}
}

return tapCommitment, nil
}

// genesisScript returns the script that should be placed in the minting output
// within the genesis transaction.
func (m *MintingBatch) genesisScript(sibling *commitment.TapscriptPreimage) (
35 changes: 20 additions & 15 deletions tapgarden/caretaker.go
Original file line number Diff line number Diff line change
@@ -412,7 +412,21 @@ func (b *BatchCaretaker) assetCultivator() {
}
}

// extractGenesisOutpoint extracts the genesis point (the first output from the
// extractAnchorOutputIndex extracts the anchor output index from a funded
// genesis packet.
func extractAnchorOutputIndex(genesisPkt *tapsend.FundedPsbt) uint32 {
anchorOutputIndex := uint32(0)

// TODO(jhb): Does funding guarantee that minting TXs always have
// exactly two outputs? If not this func should be fallible.
if genesisPkt.ChangeOutputIndex == 0 {
Copy link
Member

Choose a reason for hiding this comment

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

Using the current FundPSBT call, yeah: we'll have the target output and a change output. Once we expose batch asset chan funding though, this'll change.

anchorOutputIndex = 1
}

return anchorOutputIndex
}

// extractGenesisOutpoint extracts the genesis point (the first input from the
// genesis transaction).
func extractGenesisOutpoint(tx *wire.MsgTx) wire.OutPoint {
return tx.TxIn[0].PreviousOutPoint
@@ -436,7 +450,6 @@ func (b *BatchCaretaker) seedlingsToAssetSprouts(ctx context.Context,
b.cfg.Batch.Seedlings,
)
groupedSeedlingCount := len(groupedSeedlings)

// load seedling asset groups and check for correct group count
seedlingGroups, err := b.cfg.Log.FetchSeedlingGroups(
ctx, genesisPoint, assetOutputIndex,
@@ -453,10 +466,9 @@ func (b *BatchCaretaker) seedlingsToAssetSprouts(ctx context.Context,
seedlingGroupCount)
}

for i := range seedlingGroups {
for _, seedlingGroup := range seedlingGroups {
// check that asset group has a witness, and that the group
// has a matching seedling
seedlingGroup := seedlingGroups[i]
if len(seedlingGroup.GroupKey.Witness) == 0 {
return nil, fmt.Errorf("not all seedling groups have " +
"witnesses")
@@ -496,12 +508,7 @@ func (b *BatchCaretaker) seedlingsToAssetSprouts(ctx context.Context,
// build assets for ungrouped seedlings
for seedlingName := range ungroupedSeedlings {
seedling := ungroupedSeedlings[seedlingName]
assetGen := asset.Genesis{
FirstPrevOut: genesisPoint,
Tag: seedling.AssetName,
OutputIndex: assetOutputIndex,
Type: seedling.AssetType,
}
assetGen := seedling.Genesis(genesisPoint, assetOutputIndex)

// 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
@@ -607,11 +614,9 @@ func (b *BatchCaretaker) stateStep(currentState BatchState) (BatchState, error)
// and vice versa.
// TODO(jhb): return the anchor index instead of change? or both
// so this works for N outputs
b.anchorOutputIndex = 0
if changeOutputIndex == 0 {
b.anchorOutputIndex = 1
}

b.anchorOutputIndex = extractAnchorOutputIndex(
b.cfg.Batch.GenesisPacket,
)
genesisPoint := extractGenesisOutpoint(genesisTxPkt.UnsignedTx)

// First, we'll turn all the seedlings into actual taproot assets.
5 changes: 5 additions & 0 deletions tapgarden/interface.go
Original file line number Diff line number Diff line change
@@ -262,6 +262,11 @@ type MintingStore interface {
FetchGroupByGroupKey(ctx context.Context,
groupKey *btcec.PublicKey) (*asset.AssetGroup, error)

// FetchScriptKeyByTweakedKey fetches the populated script key given the
// tweaked script key.
FetchScriptKeyByTweakedKey(ctx context.Context,
tweakedKey *btcec.PublicKey) (*asset.TweakedScriptKey, error)

// FetchAssetMeta fetches the meta reveal for an asset genesis.
FetchAssetMeta(ctx context.Context, ID asset.ID) (*proof.MetaReveal,
error)
223 changes: 198 additions & 25 deletions tapgarden/planter.go
Original file line number Diff line number Diff line change
@@ -5,6 +5,7 @@ import (
"context"
"errors"
"fmt"
"slices"
"sync"
"time"

@@ -587,13 +588,7 @@ func buildGroupReqs(genesisPoint wire.OutPoint, assetOutputIndex uint32,

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

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

// 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
@@ -730,10 +725,150 @@ func freezeMintingBatch(ctx context.Context, batchStore MintingStore,
)
}

// filterFinalizedBatches separates a set of batches into two sets based on
// their batch state.
func filterFinalizedBatches(batches []*MintingBatch) ([]*MintingBatch,
[]*MintingBatch) {

finalized := []*MintingBatch{}
nonFinalized := []*MintingBatch{}

fn.ForEach(batches, func(batch *MintingBatch) {
switch batch.State() {
case BatchStateFinalized:
finalized = append(finalized, batch)
default:
nonFinalized = append(nonFinalized, batch)
}
})

return finalized, nonFinalized
}

// fetchFinalizedBatch fetches the assets of a batch in their genesis state,
// given a batch populated with seedlings.
func fetchFinalizedBatch(ctx context.Context, batchStore MintingStore,
archiver proof.Archiver, batch *MintingBatch) (*MintingBatch, error) {

// Collect genesis TX information from the batch to build the proof
// locators.
anchorOutputIndex := extractAnchorOutputIndex(batch.GenesisPacket)
signedTx, err := psbt.Extract(batch.GenesisPacket.Pkt)
if err != nil {
return nil, err
}

genOutpoint := extractGenesisOutpoint(signedTx)
genScript := signedTx.TxOut[anchorOutputIndex].PkScript
anchorOutpoint := wire.OutPoint{
Hash: signedTx.TxHash(),
Index: anchorOutputIndex,
}

batchAssets := make([]*asset.Asset, 0, len(batch.Seedlings))
assetMetas := make(AssetMetas)
for _, seedling := range batch.Seedlings {
gen := seedling.Genesis(genOutpoint, anchorOutputIndex)
issuanceProof, err := archiver.FetchIssuanceProof(
ctx, gen.ID(), anchorOutpoint,
)
if err != nil {
return nil, err
}

proofFile, err := issuanceProof.AsFile()
if err != nil {
return nil, err
}

if proofFile.NumProofs() != 1 {
return nil, fmt.Errorf("expected single proof for " +
"issuance proof")
}

rawProof, err := proofFile.RawLastProof()
if err != nil {
return nil, err
}

// Decode the sprouted asset from the issuance proof.
var sproutedAsset asset.Asset
assetRecord := proof.AssetLeafRecord(&sproutedAsset)
err = proof.SparseDecode(bytes.NewReader(rawProof), assetRecord)
if err != nil {
return nil, fmt.Errorf("unable to decode issuance "+
"proof: %w", err)
}

if !sproutedAsset.IsGenesisAsset() {
return nil, fmt.Errorf("decoded asset is not a " +
"genesis asset")
}

// Populate the key info for the script key and group key.
if sproutedAsset.ScriptKey.PubKey == nil {
return nil, fmt.Errorf("decoded asset is missing " +
"script key")
}

tweakedScriptKey, err := batchStore.FetchScriptKeyByTweakedKey(
ctx, sproutedAsset.ScriptKey.PubKey,
)
if err != nil {
return nil, err
}

sproutedAsset.ScriptKey.TweakedScriptKey = tweakedScriptKey
if sproutedAsset.GroupKey != nil {
assetGroup, err := batchStore.FetchGroupByGroupKey(
ctx, &sproutedAsset.GroupKey.GroupPubKey,
)
if err != nil {
return nil, err
}

sproutedAsset.GroupKey = assetGroup.GroupKey
}

batchAssets = append(batchAssets, &sproutedAsset)
scriptKey := asset.ToSerialized(sproutedAsset.ScriptKey.PubKey)
assetMetas[scriptKey] = seedling.Meta
}

// Verify that we can reconstruct the genesis output script used in the
// anchor TX.
batchSibling := batch.TapSibling()
var tapSibling *chainhash.Hash
if len(batchSibling) != 0 {
var err error
tapSibling, err = chainhash.NewHash(batchSibling)
if err != nil {
return nil, err
}
}

tapCommitment, err := VerifyOutputScript(
batch.BatchKey.PubKey, tapSibling, genScript, batchAssets,
)

if err != nil {
return nil, err
}

// With the batch assets validated, construct the populated finalized
// batch.
batch.Seedlings = nil
finalizedBatch := batch.Copy()
finalizedBatch.RootAssetCommitment = tapCommitment
finalizedBatch.AssetMetas = assetMetas

return finalizedBatch, nil
}

// ListBatches returns the single batch specified by the batch key, or the set
// of batches not yet finalized on disk.
func listBatches(ctx context.Context, batchStore MintingStore,
genBuilder asset.GenesisTxBuilder,
archiver proof.Archiver, genBuilder asset.GenesisTxBuilder,
params ListBatchesParams) ([]*VerboseBatch, error) {

var (
@@ -753,12 +888,54 @@ func listBatches(ctx context.Context, batchStore MintingStore,
return nil, err
}

verboseBatches := fn.Map(batches, func(b *MintingBatch) *VerboseBatch {
return &VerboseBatch{
MintingBatch: b,
UnsealedSeedlings: nil,
var (
finalBatches, nonFinalBatches = filterFinalizedBatches(batches)
verboseBatches []*VerboseBatch
)

switch {
case len(finalBatches) == 0:
verboseBatches = fn.Map(batches,
func(b *MintingBatch) *VerboseBatch {
return &VerboseBatch{
MintingBatch: b,
UnsealedSeedlings: nil,
}
},
)

// For finalized batches, we need to fetch the assets from the proof
// archiver, not the DB.
default:
finalizedBatches := make([]*MintingBatch, 0, len(finalBatches))
for _, batch := range finalBatches {
finalizedBatch, err := fetchFinalizedBatch(
ctx, batchStore, archiver, batch,
)
if err != nil {
return nil, err
}

finalizedBatches = append(
finalizedBatches, finalizedBatch,
)
}
})

// Re-sort the batches by creation time for consistent display.
allBatches := append(nonFinalBatches, finalizedBatches...)
slices.SortFunc(allBatches, func(a, b *MintingBatch) int {
return a.CreationTime.Compare(b.CreationTime)
})

verboseBatches = fn.Map(allBatches,
func(b *MintingBatch) *VerboseBatch {
return &VerboseBatch{
MintingBatch: b,
UnsealedSeedlings: nil,
}
},
)
}

// Return the batches without any extra asset group info.
if !params.Verbose {
@@ -793,11 +970,9 @@ func listBatches(ctx context.Context, batchStore MintingStore,
// 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 currentBatch.GenesisPacket.ChangeOutputIndex == 0 {
anchorOutputIndex = 1
}

anchorOutputIndex := extractAnchorOutputIndex(
currentBatch.GenesisPacket,
)
genesisPoint := extractGenesisOutpoint(
currentBatch.GenesisPacket.Pkt.UnsignedTx,
)
@@ -1036,8 +1211,8 @@ func (c *ChainPlanter) gardener() {

ctx, cancel := c.WithCtxQuit()
batches, err := listBatches(
ctx, c.cfg.Log, c.cfg.GenTxBuilder,
*listBatchesParams,
ctx, c.cfg.Log, c.cfg.ProofFiles,
c.cfg.GenTxBuilder, *listBatchesParams,
)
cancel()
if err != nil {
@@ -1317,11 +1492,9 @@ 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 := uint32(0)
if workingBatch.GenesisPacket.ChangeOutputIndex == 0 {
anchorOutputIndex = 1
}

anchorOutputIndex := extractAnchorOutputIndex(
workingBatch.GenesisPacket,
)
genesisPoint := extractGenesisOutpoint(
workingBatch.GenesisPacket.Pkt.UnsignedTx,
)
5 changes: 3 additions & 2 deletions tapgarden/planter_test.go
Original file line number Diff line number Diff line change
@@ -99,7 +99,7 @@ type mintingTestHarness struct {

planter *tapgarden.ChainPlanter

proofFiles *tapgarden.MockProofArchive
proofFiles *proof.MockProofArchive

proofWatcher *tapgarden.MockProofWatcher

@@ -116,14 +116,15 @@ func newMintingTestHarness(t *testing.T,
keyRing := tapgarden.NewMockKeyRing()
genSigner := tapgarden.NewMockGenSigner(keyRing)
treeMgr := tapgarden.NewFallibleTapscriptTreeMgr(store)
archiver := proof.NewMockProofArchive()

return &mintingTestHarness{
T: t,
store: store,
treeStore: &treeMgr,
wallet: tapgarden.NewMockWalletAnchor(),
chain: tapgarden.NewMockChainBridge(),
proofFiles: &tapgarden.MockProofArchive{},
proofFiles: archiver,
proofWatcher: &tapgarden.MockProofWatcher{},
keyRing: keyRing,
genSigner: genSigner,
19 changes: 19 additions & 0 deletions tapgarden/seedling.go
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ import (
"crypto/sha256"
"fmt"

"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/taproot-assets/asset"
"github.com/lightninglabs/taproot-assets/proof"
"github.com/lightningnetwork/lnd/keychain"
@@ -199,6 +200,24 @@ func (c Seedling) validateGroupKey(group asset.AssetGroup,
return nil
}

// Genesis reconstructs the asset genesis for a seedling.
func (c Seedling) Genesis(genOutpoint wire.OutPoint,
genIndex uint32) asset.Genesis {

gen := asset.Genesis{
FirstPrevOut: genOutpoint,
Tag: c.AssetName,
OutputIndex: genIndex,
Type: c.AssetType,
}

if c.Meta != nil {
gen.MetaHash = c.Meta.MetaHash()
}

return gen
}

// HasGroupKey checks if a seedling specifies a particular group key.
func (c Seedling) HasGroupKey() bool {
return c.GroupInfo != nil && c.GroupInfo.GroupKey != nil