diff --git a/itest/assets_test.go b/itest/assets_test.go index d30c3b0e8..d4648c9f8 100644 --- a/itest/assets_test.go +++ b/itest/assets_test.go @@ -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)) +} diff --git a/itest/test_list_on_test.go b/itest/test_list_on_test.go index e3b3585f5..7f6c2c731 100644 --- a/itest/test_list_on_test.go +++ b/itest/test_list_on_test.go @@ -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, diff --git a/proof/archive.go b/proof/archive.go index 3cd90e29c..8431117a8 100644 --- a/proof/archive.go +++ b/proof/archive.go @@ -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, + 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 diff --git a/proof/courier_test.go b/proof/courier_test.go index 079346bab..8af08fadb 100644 --- a/proof/courier_test.go +++ b/proof/courier_test.go @@ -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{}, diff --git a/proof/mock.go b/proof/mock.go index b355220f1..081f6d6e3 100644 --- a/proof/mock.go +++ b/proof/mock.go @@ -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 { diff --git a/tapdb/addrs.go b/tapdb/addrs.go index 7b03e4790..1270f2d7b 100644 --- a/tapdb/addrs.go +++ b/tapdb/addrs.go @@ -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 } diff --git a/tapdb/asset_minting.go b/tapdb/asset_minting.go index e7f5fbd41..5c22a36dc 100644 --- a/tapdb/asset_minting.go +++ b/tapdb/asset_minting.go @@ -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. diff --git a/tapdb/assets_common.go b/tapdb/assets_common.go index f84a27d6f..0e66b363b 100644 --- a/tapdb/assets_common.go +++ b/tapdb/assets_common.go @@ -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 diff --git a/tapdb/assets_store.go b/tapdb/assets_store.go index bc86fd3a2..78c29bf86 100644 --- a/tapdb/assets_store.go +++ b/tapdb/assets_store.go @@ -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. diff --git a/tapgarden/batch.go b/tapgarden/batch.go index de6103b36..635e2bce6 100644 --- a/tapgarden/batch.go +++ b/tapgarden/batch.go @@ -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) ( diff --git a/tapgarden/caretaker.go b/tapgarden/caretaker.go index 23fe71eed..a3434f3a1 100644 --- a/tapgarden/caretaker.go +++ b/tapgarden/caretaker.go @@ -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 { + 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. diff --git a/tapgarden/interface.go b/tapgarden/interface.go index 266389db5..cfe9b71ff 100644 --- a/tapgarden/interface.go +++ b/tapgarden/interface.go @@ -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) diff --git a/tapgarden/planter.go b/tapgarden/planter.go index 71db92d07..59cde5c85 100644 --- a/tapgarden/planter.go +++ b/tapgarden/planter.go @@ -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, ) diff --git a/tapgarden/planter_test.go b/tapgarden/planter_test.go index 3bb98ae17..c9da40d2b 100644 --- a/tapgarden/planter_test.go +++ b/tapgarden/planter_test.go @@ -99,7 +99,7 @@ type mintingTestHarness struct { planter *tapgarden.ChainPlanter - proofFiles *tapgarden.MockProofArchive + proofFiles *proof.MockProofArchive proofWatcher *tapgarden.MockProofWatcher @@ -116,6 +116,7 @@ func newMintingTestHarness(t *testing.T, keyRing := tapgarden.NewMockKeyRing() genSigner := tapgarden.NewMockGenSigner(keyRing) treeMgr := tapgarden.NewFallibleTapscriptTreeMgr(store) + archiver := proof.NewMockProofArchive() return &mintingTestHarness{ T: t, @@ -123,7 +124,7 @@ func newMintingTestHarness(t *testing.T, treeStore: &treeMgr, wallet: tapgarden.NewMockWalletAnchor(), chain: tapgarden.NewMockChainBridge(), - proofFiles: &tapgarden.MockProofArchive{}, + proofFiles: archiver, proofWatcher: &tapgarden.MockProofWatcher{}, keyRing: keyRing, genSigner: genSigner, diff --git a/tapgarden/seedling.go b/tapgarden/seedling.go index 4829605f4..974beb3d5 100644 --- a/tapgarden/seedling.go +++ b/tapgarden/seedling.go @@ -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