Skip to content

Commit 88b0017

Browse files
committed
tapgarden: fix proof not found bug
This fixes a bug reported by a user running v0.3.3-rc1. Although the situation can only happen if the daemon is shut down in exactly the wrong moment (or, more likely, due to an otherwise inconsistent database state), it can happen that the multiverse reports a proof is available but it wasn't yet imported into the local archive. To make sure we can definitely rely on the proof being in the asset DB when trying to complete a receive event, we double check and re-import the proof if necessary.
1 parent e9042bc commit 88b0017

File tree

2 files changed

+201
-29
lines changed

2 files changed

+201
-29
lines changed

tapgarden/custodian.go

Lines changed: 51 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -647,12 +647,13 @@ func (c *Custodian) checkProofAvailable(event *address.Event) (bool, error) {
647647
// be a multi archiver that includes file based storage) to make sure
648648
// the proof is available in the relational database. If the proof is
649649
// not in the DB, we can't update the event.
650-
blob, err := c.cfg.ProofNotifier.FetchProof(ctxt, proof.Locator{
650+
locator := proof.Locator{
651651
AssetID: fn.Ptr(event.Addr.AssetID),
652652
GroupKey: event.Addr.GroupKey,
653653
ScriptKey: event.Addr.ScriptKey,
654654
OutPoint: &event.Outpoint,
655-
})
655+
}
656+
blob, err := c.cfg.ProofNotifier.FetchProof(ctxt, locator)
656657
switch {
657658
case errors.Is(err, proof.ErrProofNotFound):
658659
return false, nil
@@ -671,6 +672,19 @@ func (c *Custodian) checkProofAvailable(event *address.Event) (bool, error) {
671672
"but got something else")
672673
}
673674

675+
// In case we missed a notification from the local universe and didn't
676+
// previously import the proof (for example because we were shutting
677+
// down), we could be in a situation where the local database doesn't
678+
// have the proof yet. So we make sure to import it now.
679+
err = c.assertProofInLocalArchive(&proof.AnnotatedProof{
680+
Locator: locator,
681+
Blob: blob,
682+
})
683+
if err != nil {
684+
return false, fmt.Errorf("error asserting proof in local "+
685+
"archive: %w", err)
686+
}
687+
674688
file, err := blob.AsFile()
675689
if err != nil {
676690
return false, fmt.Errorf("error extracting proof file: %w", err)
@@ -755,29 +769,13 @@ func (c *Custodian) mapProofToEvent(p proof.Blob) error {
755769
// local universe instead of the local proof archive (which the
756770
// couriers use). This is mainly an optimization to make sure we
757771
// don't unnecessarily overwrite the proofs in our main archive.
758-
haveProof, err := c.cfg.ProofArchive.HasProof(ctxt, loc)
772+
err := c.assertProofInLocalArchive(&proof.AnnotatedProof{
773+
Locator: loc,
774+
Blob: proofBlob,
775+
})
759776
if err != nil {
760-
return fmt.Errorf("error checking if proof is "+
761-
"available: %w", err)
762-
}
763-
764-
// We don't have the proof yet, or not in all backends, so we
765-
// need to import it now.
766-
if !haveProof {
767-
headerVerifier := GenHeaderVerifier(
768-
ctxt, c.cfg.ChainBridge,
769-
)
770-
err = c.cfg.ProofArchive.ImportProofs(
771-
ctxt, headerVerifier, c.cfg.GroupVerifier,
772-
false, &proof.AnnotatedProof{
773-
Locator: loc,
774-
Blob: proofBlob,
775-
},
776-
)
777-
if err != nil {
778-
return fmt.Errorf("error importing proof "+
779-
"file into main archive: %w", err)
780-
}
777+
return fmt.Errorf("error asserting proof in local "+
778+
"archive: %w", err)
781779
}
782780
}
783781

@@ -828,6 +826,35 @@ func (c *Custodian) mapProofToEvent(p proof.Blob) error {
828826
return nil
829827
}
830828

829+
// assertProofInLocalArchive checks if the proof is already in the local proof
830+
// archive. If it isn't, it is imported now.
831+
func (c *Custodian) assertProofInLocalArchive(p *proof.AnnotatedProof) error {
832+
ctxt, cancel := c.WithCtxQuit()
833+
defer cancel()
834+
835+
haveProof, err := c.cfg.ProofArchive.HasProof(ctxt, p.Locator)
836+
if err != nil {
837+
return fmt.Errorf("error checking if proof is available: %w",
838+
err)
839+
}
840+
841+
// We don't have the proof yet, or not in all backends, so we
842+
// need to import it now.
843+
if !haveProof {
844+
headerVerifier := GenHeaderVerifier(ctxt, c.cfg.ChainBridge)
845+
err = c.cfg.ProofArchive.ImportProofs(
846+
ctxt, headerVerifier, c.cfg.GroupVerifier, false, p,
847+
)
848+
if err != nil {
849+
log.Errorf("ERROOOORRR: %v", err)
850+
return fmt.Errorf("error importing proof file into "+
851+
"main archive: %w", err)
852+
}
853+
}
854+
855+
return nil
856+
}
857+
831858
// setReceiveCompleted updates the address event in the database to mark it as
832859
// completed successfully and to link it to the proof we received.
833860
func (c *Custodian) setReceiveCompleted(event *address.Event,

tapgarden/custodian_test.go

Lines changed: 150 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"context"
66
"database/sql"
77
"fmt"
8+
"io"
89
"math/rand"
910
"net/url"
1011
"testing"
@@ -23,6 +24,7 @@ import (
2324
"github.com/lightninglabs/taproot-assets/tapdb"
2425
"github.com/lightninglabs/taproot-assets/tapgarden"
2526
"github.com/lightninglabs/taproot-assets/tapscript"
27+
"github.com/lightninglabs/taproot-assets/universe"
2628
"github.com/lightningnetwork/lnd/clock"
2729
"github.com/lightningnetwork/lnd/lnrpc"
2830
"github.com/lightningnetwork/lnd/lntest/wait"
@@ -59,9 +61,46 @@ func newAddrBookForDB(db *tapdb.BaseDB, keyRing *tapgarden.MockKeyRing,
5961
return book, tapdbBook
6062
}
6163

64+
type mockVerifier struct {
65+
t *testing.T
66+
}
67+
68+
func newMockVerifier(t *testing.T) *mockVerifier {
69+
return &mockVerifier{
70+
t: t,
71+
}
72+
}
73+
74+
func (m *mockVerifier) Verify(_ context.Context, r io.Reader,
75+
headerVerifier proof.HeaderVerifier,
76+
groupVerifier proof.GroupVerifier) (*proof.AssetSnapshot, error) {
77+
78+
f := &proof.File{}
79+
err := f.Decode(r)
80+
require.NoError(m.t, err)
81+
82+
lastProof, err := f.LastProof()
83+
require.NoError(m.t, err)
84+
85+
ac, err := commitment.NewAssetCommitment(&lastProof.Asset)
86+
require.NoError(m.t, err)
87+
tc, err := commitment.NewTapCommitment(ac)
88+
require.NoError(m.t, err)
89+
90+
return &proof.AssetSnapshot{
91+
Asset: &lastProof.Asset,
92+
OutPoint: lastProof.OutPoint(),
93+
OutputIndex: lastProof.InclusionProof.OutputIndex,
94+
AnchorBlockHash: lastProof.BlockHeader.BlockHash(),
95+
AnchorTx: &lastProof.AnchorTx,
96+
InternalKey: lastProof.InclusionProof.InternalKey,
97+
ScriptRoot: tc,
98+
}, nil
99+
}
100+
62101
// newProofArchive creates a new instance of the MultiArchiver.
63102
func newProofArchiveForDB(t *testing.T, db *tapdb.BaseDB) (*proof.MultiArchiver,
64-
*tapdb.AssetStore) {
103+
*tapdb.AssetStore, *tapdb.MultiverseStore) {
65104

66105
txCreator := func(tx *sql.Tx) tapdb.ActiveAssetsStore {
67106
return db.WithTx(tx)
@@ -78,7 +117,14 @@ func newProofArchiveForDB(t *testing.T, db *tapdb.BaseDB) (*proof.MultiArchiver,
78117
assetStore,
79118
)
80119

81-
return proofArchive, assetStore
120+
multiverseDB := tapdb.NewTransactionExecutor(
121+
db, func(tx *sql.Tx) tapdb.BaseMultiverseStore {
122+
return db.WithTx(tx)
123+
},
124+
)
125+
multiverse := tapdb.NewMultiverseStore(multiverseDB)
126+
127+
return proofArchive, assetStore, multiverse
82128
}
83129

84130
type custodianHarness struct {
@@ -93,6 +139,7 @@ type custodianHarness struct {
93139
addrBook *address.Book
94140
syncer *tapgarden.MockAssetSyncer
95141
assetDB *tapdb.AssetStore
142+
multiverse *tapdb.MultiverseStore
96143
courier *proof.MockProofCourier
97144
}
98145

@@ -192,6 +239,48 @@ func (h *custodianHarness) assertAddrsRegistered(
192239
}
193240
}
194241

242+
// addProofFileToMultiverse adds the given proof to the multiverse store.
243+
func (h *custodianHarness) addProofFileToMultiverse(p *proof.AnnotatedProof) {
244+
f := &proof.File{}
245+
err := f.Decode(bytes.NewReader(p.Blob))
246+
require.NoError(h.t, err)
247+
248+
ctx := context.Background()
249+
ctxt, cancel := context.WithTimeout(ctx, testTimeout)
250+
defer cancel()
251+
252+
for i := uint32(0); i < uint32(f.NumProofs()); i++ {
253+
transition, err := f.ProofAt(i)
254+
require.NoError(h.t, err)
255+
256+
rawTransition, err := f.RawProofAt(i)
257+
require.NoError(h.t, err)
258+
259+
id := universe.NewUniIDFromAsset(transition.Asset)
260+
key := universe.LeafKey{
261+
OutPoint: transition.OutPoint(),
262+
ScriptKey: fn.Ptr(asset.NewScriptKey(
263+
transition.Asset.ScriptKey.PubKey,
264+
)),
265+
}
266+
leaf := &universe.Leaf{
267+
GenesisWithGroup: universe.GenesisWithGroup{
268+
Genesis: transition.Asset.Genesis,
269+
GroupKey: transition.Asset.GroupKey,
270+
},
271+
RawProof: rawTransition,
272+
Asset: &transition.Asset,
273+
Amt: transition.Asset.Amount,
274+
}
275+
h.t.Logf("Importing proof with script key %x and outpoint %v "+
276+
"into multiverse",
277+
key.ScriptKey.PubKey.SerializeCompressed(),
278+
key.OutPoint)
279+
_, err = h.multiverse.UpsertProofLeaf(ctxt, id, key, leaf, nil)
280+
require.NoError(h.t, err)
281+
}
282+
}
283+
195284
func newHarness(t *testing.T,
196285
initialAddrs []*address.AddrWithKeyInfo) *custodianHarness {
197286

@@ -201,7 +290,10 @@ func newHarness(t *testing.T,
201290
syncer := tapgarden.NewMockAssetSyncer()
202291
db := tapdb.NewTestDB(t)
203292
addrBook, tapdbBook := newAddrBookForDB(db.BaseDB, keyRing, syncer)
204-
_, assetDB := newProofArchiveForDB(t, db.BaseDB)
293+
294+
_, assetDB, multiverse := newProofArchiveForDB(t, db.BaseDB)
295+
notifier := proof.NewMultiArchiveNotifier(assetDB, multiverse)
296+
205297
courier := proof.NewMockProofCourier()
206298
courierDispatch := &proof.MockProofCourierDispatcher{
207299
Courier: courier,
@@ -214,14 +306,18 @@ func newHarness(t *testing.T,
214306
require.NoError(t, err)
215307
}
216308

309+
archive := proof.NewMultiArchiver(
310+
newMockVerifier(t), testTimeout, assetDB,
311+
)
312+
217313
errChan := make(chan error, 1)
218314
cfg := &tapgarden.CustodianConfig{
219315
ChainParams: chainParams,
220316
ChainBridge: chainBridge,
221317
WalletAnchor: walletAnchor,
222318
AddrBook: addrBook,
223-
ProofArchive: assetDB,
224-
ProofNotifier: assetDB,
319+
ProofArchive: archive,
320+
ProofNotifier: notifier,
225321
ProofCourierDispatcher: courierDispatch,
226322
ProofWatcher: proofWatcher,
227323
ErrChan: errChan,
@@ -238,6 +334,7 @@ func newHarness(t *testing.T,
238334
addrBook: addrBook,
239335
syncer: syncer,
240336
assetDB: assetDB,
337+
multiverse: multiverse,
241338
courier: courier,
242339
}
243340
}
@@ -315,6 +412,11 @@ func randProof(t *testing.T, outputIndex int, tx *wire.MsgTx,
315412
Genesis: *genesis,
316413
Amount: addr.Amount,
317414
ScriptKey: asset.NewScriptKey(&addr.ScriptKey),
415+
PrevWitnesses: []asset.Witness{
416+
{
417+
PrevID: &asset.PrevID{},
418+
},
419+
},
318420
}
319421
if addr.GroupKey != nil {
320422
a.GroupKey = &asset.GroupKey{
@@ -653,6 +755,49 @@ func mustMakeAddr(t *testing.T,
653755
return addr
654756
}
655757

758+
// TestProofInMultiverseOnly tests that the custodian imports a proof correctly
759+
// into the local archive if it's only present in the multiverse.
760+
func TestProofInMultiverseOnly(t *testing.T) {
761+
h := newHarness(t, nil)
762+
763+
// Before we start the custodian, we create a random address and a
764+
// corresponding wallet transaction.
765+
ctx := context.Background()
766+
767+
addr, genesis := randAddr(h)
768+
err := h.tapdbBook.InsertAddrs(ctx, *addr)
769+
require.NoError(t, err)
770+
771+
// We now start the custodian and make sure it's started up correctly
772+
// and the pending event is registered.
773+
require.NoError(t, h.c.Start())
774+
h.assertStartup()
775+
h.assertAddrsRegistered(addr)
776+
777+
// Receiving a TX for it should create a pending event and cause the
778+
// proof courier to attempt to fetch it. But the courier won't find it.
779+
outputIdx, tx := randWalletTx(addr)
780+
h.walletAnchor.SubscribeTx <- *tx
781+
h.assertEventsPresent(1, address.StatusTransactionDetected)
782+
783+
// We now stop the custodian again.
784+
require.NoError(t, h.c.Stop())
785+
786+
// The proof is only in the multiverse, not in the local archive. And we
787+
// add the proof to the multiverse before starting the custodian, so the
788+
// notification for it doesn't trigger.
789+
mockProof := randProof(t, outputIdx, tx.Tx, genesis, addr)
790+
h.addProofFileToMultiverse(mockProof)
791+
792+
// And a new start should import the proof into the local archive.
793+
h.c = tapgarden.NewCustodian(h.cfg)
794+
require.NoError(t, h.c.Start())
795+
t.Cleanup(func() {
796+
require.NoError(t, h.c.Stop())
797+
})
798+
h.assertStartup()
799+
}
800+
656801
// TestAddrMatchesAsset tests that the AddrMatchesAsset function works
657802
// correctly.
658803
func TestAddrMatchesAsset(t *testing.T) {

0 commit comments

Comments
 (0)