Skip to content

Commit 5236dea

Browse files
authored
Merge pull request #992 from lightninglabs/marshal_batch_fixes
tapgarden: list batches correctly after asset transfer
2 parents 3e07a5b + 62b431e commit 5236dea

File tree

15 files changed

+869
-228
lines changed

15 files changed

+869
-228
lines changed

itest/assets_test.go

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"context"
66
"crypto/tls"
77
"net/http"
8+
"slices"
9+
"strings"
810
"time"
911

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

2932
var (
@@ -438,7 +441,6 @@ func testMintAssetsWithTapscriptSibling(t *harnessTest) {
438441
rpcIssuableAssets := MintAssetsConfirmBatch(
439442
t.t, t.lndHarness.Miner.Client, t.tapd, issuableAssets,
440443
)
441-
442444
AssertAssetBalances(t.t, t.tapd, rpcSimpleAssets, rpcIssuableAssets)
443445

444446
// Filter the managed UTXOs to select the genesis UTXO with the
@@ -528,3 +530,90 @@ func testMintAssetsWithTapscriptSibling(t *harnessTest) {
528530
t.lndHarness.MineBlocksAndAssertNumTxes(1, 1)
529531
t.lndHarness.AssertNumUTXOsWithConf(t.lndHarness.Bob, 1, 1, 1)
530532
}
533+
534+
// testMintBatchAndTransfer tests that we can mint a batch of assets, observe
535+
// the finalized batch state, and observe the same batch state after a transfer
536+
// of an asset from the batch.
537+
func testMintBatchAndTransfer(t *harnessTest) {
538+
ctxb := context.Background()
539+
rpcSimpleAssets := MintAssetsConfirmBatch(
540+
t.t, t.lndHarness.Miner.Client, t.tapd, simpleAssets,
541+
)
542+
543+
// List the batch right after minting.
544+
originalBatches, err := t.tapd.ListBatches(
545+
ctxb, &mintrpc.ListBatchRequest{},
546+
)
547+
require.NoError(t.t, err)
548+
549+
// We'll make a second node now that'll be the receiver of all the
550+
// assets made above.
551+
secondTapd := setupTapdHarness(
552+
t.t, t, t.lndHarness.Bob, t.universeServer,
553+
)
554+
defer func() {
555+
require.NoError(t.t, secondTapd.stop(!*noDelete))
556+
}()
557+
558+
// In order to force a split, we don't try to send the full first asset.
559+
a := rpcSimpleAssets[0]
560+
addr, events := NewAddrWithEventStream(
561+
t.t, secondTapd, &taprpc.NewAddrRequest{
562+
AssetId: a.AssetGenesis.AssetId,
563+
Amt: a.Amount - 1,
564+
AssetVersion: a.Version,
565+
},
566+
)
567+
568+
AssertAddrCreated(t.t, secondTapd, a, addr)
569+
570+
sendResp, sendEvents := sendAssetsToAddr(t, t.tapd, addr)
571+
sendRespJSON, err := formatProtoJSON(sendResp)
572+
require.NoError(t.t, err)
573+
574+
t.Logf("Got response from sending assets: %v", sendRespJSON)
575+
576+
// Make sure that eventually we see a single event for the
577+
// address.
578+
AssertAddrEvent(t.t, secondTapd, addr, 1, statusDetected)
579+
580+
// Mine a block to make sure the events are marked as confirmed.
581+
MineBlocks(t.t, t.lndHarness.Miner.Client, 1, 1)
582+
583+
// Eventually the event should be marked as confirmed.
584+
AssertAddrEvent(t.t, secondTapd, addr, 1, statusConfirmed)
585+
586+
// Make sure we have imported and finalized all proofs.
587+
AssertNonInteractiveRecvComplete(t.t, secondTapd, 1)
588+
AssertSendEventsComplete(t.t, addr.ScriptKey, sendEvents)
589+
590+
// Make sure the receiver has received all events in order for
591+
// the address.
592+
AssertReceiveEvents(t.t, addr, events)
593+
594+
afterBatches, err := t.tapd.ListBatches(
595+
ctxb, &mintrpc.ListBatchRequest{},
596+
)
597+
require.NoError(t.t, err)
598+
599+
// The batch listed after the transfer should be identical to the batch
600+
// listed before the transfer.
601+
require.Equal(
602+
t.t, len(originalBatches.Batches), len(afterBatches.Batches),
603+
)
604+
605+
originalBatch := originalBatches.Batches[0].Batch
606+
afterBatch := afterBatches.Batches[0].Batch
607+
608+
// Sort the assets from the listed batch before comparison.
609+
slices.SortFunc(originalBatch.Assets,
610+
func(a, b *mintrpc.PendingAsset) int {
611+
return strings.Compare(a.Name, b.Name)
612+
})
613+
slices.SortFunc(afterBatch.Assets,
614+
func(a, b *mintrpc.PendingAsset) int {
615+
return strings.Compare(a.Name, b.Name)
616+
})
617+
618+
require.True(t.t, proto.Equal(originalBatch, afterBatch))
619+
}

itest/test_list_on_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ var testCases = []*testCase{
1313
name: "mint batch resume",
1414
test: testMintBatchResume,
1515
},
16+
{
17+
name: "mint batch and transfer",
18+
test: testMintBatchAndTransfer,
19+
},
1620
{
1721
name: "asset meta validation",
1822
test: testAssetMeta,

proof/archive.go

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,13 @@ type Archiver interface {
145145
// specific fields need to be set in the Locator (e.g. the OutPoint).
146146
FetchProof(ctx context.Context, id Locator) (Blob, error)
147147

148+
// FetchIssuanceProof fetches the issuance proof for an asset, given the
149+
// anchor point of the issuance (NOT the genesis point for the asset).
150+
//
151+
// If a proof cannot be found, then ErrProofNotFound should be returned.
152+
FetchIssuanceProof(ctx context.Context, id asset.ID,
153+
anchorOutpoint wire.OutPoint) (Blob, error)
154+
148155
// HasProof returns true if the proof for the given locator exists. This
149156
// is intended to be a performance optimized lookup compared to fetching
150157
// a proof and checking for ErrProofNotFound.
@@ -385,6 +392,7 @@ func lookupProofFilePath(rootPath string, loc Locator) (string, error) {
385392
assetID := hex.EncodeToString(loc.AssetID[:])
386393
scriptKey := hex.EncodeToString(loc.ScriptKey.SerializeCompressed())
387394

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

540+
// FetchIssuanceProof fetches the issuance proof for an asset, given the
541+
// anchor point of the issuance (NOT the genesis point for the asset).
542+
//
543+
// If a proof cannot be found, then ErrProofNotFound should be returned.
544+
//
545+
// NOTE: This implements the Archiver interface.
546+
func (f *FileArchiver) FetchIssuanceProof(ctx context.Context, id asset.ID,
547+
anchorOutpoint wire.OutPoint) (Blob, error) {
548+
549+
// Construct a pattern to search for the issuance proof file. We'll
550+
// leave the script key unspecified, as we don't know what the script
551+
// key was at genesis.
552+
assetID := hex.EncodeToString(id[:])
553+
scriptKeyGlob := strings.Repeat("?", 2*btcec.PubKeyBytesLenCompressed)
554+
truncatedHash := anchorOutpoint.Hash.String()[:outpointTruncateLength]
555+
556+
fileName := fmt.Sprintf("%s-%s-%d.%s",
557+
scriptKeyGlob, truncatedHash, anchorOutpoint.Index,
558+
TaprootAssetsFileEnding)
559+
560+
searchPattern := filepath.Join(f.proofPath, assetID, fileName)
561+
matches, err := filepath.Glob(searchPattern)
562+
if err != nil {
563+
return nil, fmt.Errorf("error listing proof files: %w", err)
564+
}
565+
if len(matches) == 0 {
566+
return nil, ErrProofNotFound
567+
}
568+
569+
// We expect exactly one matching proof for a specific asset ID and
570+
// outpoint. However, the proof file path uses the truncated outpoint,
571+
// so an asset transfer with a collision in the first half of the TXID
572+
// could also match. We can filter out such proof files by size.
573+
proofFiles := make([]Blob, 0, len(matches))
574+
for _, path := range matches {
575+
proofFile, err := os.ReadFile(path)
576+
577+
switch {
578+
case os.IsNotExist(err):
579+
return nil, ErrProofNotFound
580+
581+
case err != nil:
582+
return nil, fmt.Errorf("unable to find proof: %w", err)
583+
}
584+
585+
proofFiles = append(proofFiles, proofFile)
586+
}
587+
588+
switch {
589+
// No proofs were read.
590+
case len(proofFiles) == 0:
591+
return nil, ErrProofNotFound
592+
593+
// Exactly one proof, we'll return it.
594+
case len(proofFiles) == 1:
595+
return proofFiles[0], nil
596+
597+
// Multiple proofs, return the smallest one.
598+
default:
599+
minProofIdx := 0
600+
minProofSize := len(proofFiles[minProofIdx])
601+
for idx, proof := range proofFiles {
602+
if len(proof) < minProofSize {
603+
minProofSize = len(proof)
604+
minProofIdx = idx
605+
}
606+
}
607+
608+
return proofFiles[minProofIdx], nil
609+
}
610+
}
611+
532612
// HasProof returns true if the proof for the given locator exists. This is
533613
// intended to be a performance optimized lookup compared to fetching a proof
534614
// and checking for ErrProofNotFound.
@@ -704,10 +784,13 @@ func (f *FileArchiver) RemoveSubscriber(
704784
return f.eventDistributor.RemoveSubscriber(subscriber)
705785
}
706786

707-
// A compile-time interface to ensure FileArchiver meets the NotifyArchiver
787+
// A compile-time assertion to ensure FileArchiver meets the NotifyArchiver
708788
// interface.
709789
var _ NotifyArchiver = (*FileArchiver)(nil)
710790

791+
// A compile-time assertion to ensure FileArchiver meets the Archiver interface.
792+
var _ Archiver = (*FileArchiver)(nil)
793+
711794
// MultiArchiver is an archive of archives. It contains several archives and
712795
// attempts to use them either as a look-aside cache, or a write through cache
713796
// for all incoming requests.
@@ -763,6 +846,33 @@ func (m *MultiArchiver) FetchProof(ctx context.Context,
763846
return nil, ErrProofNotFound
764847
}
765848

849+
// FetchIssuanceProof fetches the issuance proof for an asset, given the
850+
// anchor point of the issuance (NOT the genesis point for the asset).
851+
func (m *MultiArchiver) FetchIssuanceProof(ctx context.Context,
852+
id asset.ID, anchorOutpoint wire.OutPoint) (Blob, error) {
853+
854+
// Iterate through all our active backends and try to see if at least
855+
// one of them contains the proof. Either one of them will have the
856+
// proof, or we'll return an error back to the user.
857+
for _, archive := range m.backends {
858+
proof, err := archive.FetchIssuanceProof(
859+
ctx, id, anchorOutpoint,
860+
)
861+
862+
switch {
863+
case errors.Is(err, ErrProofNotFound):
864+
continue
865+
866+
case err != nil:
867+
return nil, err
868+
}
869+
870+
return proof, nil
871+
}
872+
873+
return nil, ErrProofNotFound
874+
}
875+
766876
// HasProof returns true if the proof for the given locator exists. This is
767877
// intended to be a performance optimized lookup compared to fetching a proof
768878
// and checking for ErrProofNotFound. The multi archiver only considers a proof

proof/courier_test.go

Lines changed: 5 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package proof
33
import (
44
"bytes"
55
"context"
6-
"fmt"
76
"testing"
87

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

15-
type mockProofArchive struct {
16-
proofs map[Locator]Blob
17-
}
18-
19-
func newMockProofArchive() *mockProofArchive {
20-
return &mockProofArchive{
21-
proofs: make(map[Locator]Blob),
22-
}
23-
}
24-
25-
func (m *mockProofArchive) FetchProof(ctx context.Context,
26-
id Locator) (Blob, error) {
27-
28-
proof, ok := m.proofs[id]
29-
if !ok {
30-
return nil, ErrProofNotFound
31-
}
32-
33-
return proof, nil
34-
}
35-
36-
func (m *mockProofArchive) HasProof(ctx context.Context,
37-
id Locator) (bool, error) {
38-
39-
_, ok := m.proofs[id]
40-
41-
return ok, nil
42-
}
43-
44-
func (m *mockProofArchive) FetchProofs(ctx context.Context,
45-
id asset.ID) ([]*AnnotatedProof, error) {
46-
47-
return nil, fmt.Errorf("not implemented")
48-
}
49-
50-
func (m *mockProofArchive) ImportProofs(context.Context, HeaderVerifier,
51-
MerkleVerifier, GroupVerifier, ChainLookupGenerator, bool,
52-
...*AnnotatedProof) error {
53-
54-
return fmt.Errorf("not implemented")
55-
}
56-
5714
// TestUniverseRpcCourierLocalArchiveShortCut tests that the local archive is
5815
// used as a shortcut to fetch a proof if it's available.
5916
func TestUniverseRpcCourierLocalArchiveShortCut(t *testing.T) {
60-
localArchive := newMockProofArchive()
17+
localArchive := NewMockProofArchive()
6118

6219
testBlocks := readTestData(t)
6320
oddTxBlock := testBlocks[0]
@@ -79,7 +36,10 @@ func TestUniverseRpcCourierLocalArchiveShortCut(t *testing.T) {
7936
ScriptKey: *proof.Asset.ScriptKey.PubKey,
8037
OutPoint: fn.Ptr(proof.OutPoint()),
8138
}
82-
localArchive.proofs[locator] = proofBlob
39+
locHash, err := locator.Hash()
40+
require.NoError(t, err)
41+
42+
localArchive.proofs.Store(locHash, proofBlob)
8343

8444
courier := &UniverseRpcCourier{
8545
recipient: Recipient{},

0 commit comments

Comments
 (0)