Skip to content

Commit cf417f8

Browse files
authored
Merge pull request #1896 from lightninglabs/wip/re-org/ReplaceProofInFiles-refactor-test-coverage
proof: improve re-org proof replacement, strengthen error handling, expand test coverage
2 parents cfee1c8 + c7eb0cc commit cf417f8

File tree

5 files changed

+283
-88
lines changed

5 files changed

+283
-88
lines changed

proof/archive.go

Lines changed: 52 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,7 @@ var (
4545
// is valid.
4646
emptyKey btcec.PublicKey
4747

48-
// ErrProofNotFound is returned when a user attempts to look up a proof
49-
// based on a Locator, but we can't find it on disk.
48+
// ErrProofNotFound reports that no proof satisfies the lookup criteria.
5049
ErrProofNotFound = fmt.Errorf("unable to find proof")
5150

5251
// ErrInvalidLocatorID is returned when a specified has an invalid
@@ -1090,61 +1089,62 @@ func (m *MultiArchiver) RemoveSubscriber(
10901089
// NotifyArchiver interface.
10911090
var _ NotifyArchiver = (*MultiArchiver)(nil)
10921091

1093-
// ReplaceProofInBlob attempts to replace a proof in all proof files we have for
1094-
// assets of the same ID. This is useful when we want to update the proof with a
1095-
// new one after a re-org.
1096-
func ReplaceProofInBlob(ctx context.Context, p *Proof, archive Archiver,
1097-
vCtx VerifierCtx) error {
1098-
1099-
// This is a bit of a hacky part. If we have a chain of transactions
1100-
// that were re-organized, we can't verify the whole chain until all of
1101-
// the transactions were confirmed and all proofs were updated with the
1102-
// new blocks and merkle roots. So we'll skip the verification here
1103-
// since we don't know if the whole chain has been updated yet (the
1104-
// confirmations might come in out of order).
1105-
// TODO(guggero): Find a better way to do this.
1106-
vCtx.HeaderVerifier = func(wire.BlockHeader, uint32) error {
1107-
return nil
1108-
}
1092+
// ReplaceProofInFiles attempts to replace a proof in all provided proof files
1093+
// for assets of the same ID and returns the updated proof blobs. This is useful
1094+
// when we want to update the proof with a new one after a re-org.
1095+
func ReplaceProofInFiles(newProof *Proof,
1096+
proofFiles []*AnnotatedProof) ([]*AnnotatedProof, error) {
11091097

1110-
assetID := p.Asset.ID()
1111-
scriptPubKeyOfUpdate := p.Asset.ScriptKey.PubKey
1098+
var (
1099+
updatedProofs []*AnnotatedProof
11121100

1113-
// We now fetch all proofs of that same asset ID and filter out those
1114-
// that need updating.
1115-
proofs, err := archive.FetchProofs(ctx, assetID)
1116-
if err != nil {
1117-
return fmt.Errorf("unable to fetch all proofs for asset ID "+
1118-
"%x: %w", assetID[:], err)
1119-
}
1101+
scriptPubKeyOfUpdate = newProof.Asset.ScriptKey.PubKey
1102+
targetAssetID = newProof.Asset.ID()
1103+
)
11201104

1121-
for idx := range proofs {
1122-
existingProof := proofs[idx]
1105+
for idx := range proofFiles {
1106+
proofFile := proofFiles[idx]
1107+
1108+
// Sanity check: make sure the asset ID of the proof file
1109+
// matches the target asset ID.
1110+
if *proofFile.Locator.AssetID != targetAssetID {
1111+
return nil, fmt.Errorf("mismatched asset ID: "+
1112+
"expected %s, got %s",
1113+
proofFile.Locator.AssetID.String(),
1114+
targetAssetID.String())
1115+
}
11231116

11241117
f := &File{}
1125-
err := f.Decode(bytes.NewReader(existingProof.Blob))
1118+
err := f.Decode(bytes.NewReader(proofFile.Blob))
11261119
if err != nil {
1127-
return fmt.Errorf("unable to decode current proof: %w",
1128-
err)
1120+
return nil, fmt.Errorf("unable to decode current "+
1121+
"proof: %w", err)
11291122
}
11301123

11311124
// We only need to update proofs that contain this asset in the
11321125
// chain and haven't been updated yet (i.e. the block hash of
11331126
// the proof is different from the block hash of the proof we
11341127
// want to update).
11351128
_, indexToUpdate, err := f.LocateProof(func(fp *Proof) bool {
1136-
fileScriptKey := fp.Asset.ScriptKey.PubKey
1137-
fileTxHash := fp.AnchorTx.TxHash()
1138-
fileBlockHash := fp.BlockHeader.BlockHash()
1139-
return fileScriptKey.IsEqual(scriptPubKeyOfUpdate) &&
1140-
fileTxHash == p.AnchorTx.TxHash() &&
1141-
fileBlockHash != p.BlockHeader.BlockHash()
1129+
fScriptKey := fp.Asset.ScriptKey.PubKey
1130+
fTxHash := fp.AnchorTx.TxHash()
1131+
fBlockHash := fp.BlockHeader.BlockHash()
1132+
1133+
return fScriptKey.IsEqual(scriptPubKeyOfUpdate) &&
1134+
fTxHash == newProof.AnchorTx.TxHash() &&
1135+
fBlockHash != newProof.BlockHeader.BlockHash()
11421136
})
1143-
if err != nil {
1144-
// Either we failed to decode the proof for some reason,
1145-
// or we didn't find a proof that needs updating. In
1146-
// either case, we can skip this file.
1137+
switch {
1138+
// This proof file doesn't contain a proof that needs to be
1139+
// updated, so we can skip it.
1140+
case errors.Is(err, ErrProofNotFound):
11471141
continue
1142+
1143+
// Any other error during proof location is fatal and should be
1144+
// returned.
1145+
case err != nil:
1146+
return nil, fmt.Errorf("unable to locate "+
1147+
"proof to update: %w", err)
11481148
}
11491149

11501150
log.Debugf("Updating descendant proof at index %d "+
@@ -1154,29 +1154,25 @@ func ReplaceProofInBlob(ctx context.Context, p *Proof, archive Archiver,
11541154

11551155
// All good, we can now replace the proof in the file with the
11561156
// new one.
1157-
err = f.ReplaceProofAt(indexToUpdate, *p)
1157+
err = f.ReplaceProofAt(indexToUpdate, *newProof)
11581158
if err != nil {
1159-
return fmt.Errorf("unable to replace proof at index "+
1160-
"%d with updated one: %w", indexToUpdate, err)
1159+
return nil, fmt.Errorf("unable to replace proof at "+
1160+
"index %d with updated one: %w", indexToUpdate,
1161+
err)
11611162
}
11621163

11631164
var buf bytes.Buffer
11641165
if err := f.Encode(&buf); err != nil {
1165-
return fmt.Errorf("unable to encode updated proof: %w",
1166-
err)
1166+
return nil, fmt.Errorf("unable to encode updated "+
1167+
"proof: %w", err)
11671168
}
11681169

1169-
// We now update this direct proof in the archive.
1170-
directProof := &AnnotatedProof{
1171-
Locator: existingProof.Locator,
1170+
updatedProof := &AnnotatedProof{
1171+
Locator: proofFile.Locator,
11721172
Blob: buf.Bytes(),
11731173
}
1174-
err = archive.ImportProofs(ctx, vCtx, true, directProof)
1175-
if err != nil {
1176-
return fmt.Errorf("unable to import updated proof: %w",
1177-
err)
1178-
}
1174+
updatedProofs = append(updatedProofs, updatedProof)
11791175
}
11801176

1181-
return nil
1177+
return updatedProofs, nil
11821178
}

proof/archive_test.go

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,3 +333,129 @@ func TestMigrateOldFileNames(t *testing.T) {
333333
require.NoError(t, err)
334334
assertProofAtNewName(proof6)
335335
}
336+
337+
// TestReplaceProofInFilesUpdatesProof ensures that when a matching proof is
338+
// found, it is replaced with the updated version and returned.
339+
func TestReplaceProofInFilesUpdatesProof(t *testing.T) {
340+
t.Parallel()
341+
342+
encodeProof := func(p Proof) []byte {
343+
file, err := NewFile(V0, p)
344+
require.NoError(t, err)
345+
346+
var buf bytes.Buffer
347+
require.NoError(t, file.Encode(&buf))
348+
349+
return buf.Bytes()
350+
}
351+
352+
testBlocks := readTestData(t)
353+
354+
genesis := asset.RandGenesis(t, asset.Collectible)
355+
scriptKey := test.RandPubKey(t)
356+
357+
oldProof := RandProof(t, genesis, scriptKey, testBlocks[0], 0, 1)
358+
locator := Locator{
359+
AssetID: fn.Ptr(oldProof.Asset.ID()),
360+
ScriptKey: *oldProof.Asset.ScriptKey.PubKey,
361+
OutPoint: fn.Ptr(oldProof.OutPoint()),
362+
}
363+
oldProofAnnotated := &AnnotatedProof{
364+
Locator: locator,
365+
Blob: encodeProof(oldProof),
366+
}
367+
368+
newProof := oldProof
369+
newProof.BlockHeader = testBlocks[1].Header
370+
newProof.BlockHeight = oldProof.BlockHeight + 1
371+
372+
updated, err := ReplaceProofInFiles(
373+
&newProof, []*AnnotatedProof{oldProofAnnotated},
374+
)
375+
require.NoError(t, err)
376+
require.Len(t, updated, 1)
377+
require.Equal(t, locator, updated[0].Locator)
378+
379+
var updatedFile File
380+
err = updatedFile.Decode(bytes.NewReader(updated[0].Blob))
381+
require.NoError(t, err)
382+
383+
updatedProof, err := updatedFile.ProofAt(0)
384+
require.NoError(t, err)
385+
386+
require.Equal(
387+
t, newProof.BlockHeader.BlockHash(),
388+
updatedProof.BlockHeader.BlockHash(),
389+
)
390+
require.Equal(
391+
t, newProof.AnchorTx.TxHash(), updatedProof.AnchorTx.TxHash(),
392+
)
393+
}
394+
395+
// TestReplaceProofInFilesSkipsNonMatchingProof ensures that proofs that do not
396+
// match the update predicate are left untouched and not returned.
397+
func TestReplaceProofInFilesSkipsNonMatchingProof(t *testing.T) {
398+
t.Parallel()
399+
400+
testBlocks := readTestData(t)
401+
genesis := asset.RandGenesis(t, asset.Collectible)
402+
scriptKey := test.RandPubKey(t)
403+
404+
oldProof := RandProof(t, genesis, scriptKey, testBlocks[0], 0, 1)
405+
newProof := RandProof(t, genesis, scriptKey, testBlocks[0], 1, 1)
406+
407+
oldProofFile, err := NewFile(V0, oldProof)
408+
require.NoError(t, err)
409+
410+
var buf bytes.Buffer
411+
require.NoError(t, oldProofFile.Encode(&buf))
412+
413+
locator := Locator{
414+
AssetID: fn.Ptr(oldProof.Asset.ID()),
415+
ScriptKey: *oldProof.Asset.ScriptKey.PubKey,
416+
OutPoint: fn.Ptr(oldProof.OutPoint()),
417+
}
418+
oldProofAnnotated := &AnnotatedProof{
419+
Locator: locator,
420+
Blob: buf.Bytes(),
421+
}
422+
423+
updated, err := ReplaceProofInFiles(
424+
&newProof, []*AnnotatedProof{oldProofAnnotated},
425+
)
426+
require.NoError(t, err)
427+
require.Empty(t, updated)
428+
}
429+
430+
// TestReplaceProofInFilesMismatchedAsset ensures we error if a proof file has
431+
// a different asset ID than the proof being updated.
432+
func TestReplaceProofInFilesMismatchedAsset(t *testing.T) {
433+
t.Parallel()
434+
435+
testBlocks := readTestData(t)
436+
genesis := asset.RandGenesis(t, asset.Collectible)
437+
scriptKey := test.RandPubKey(t)
438+
439+
proof := RandProof(t, genesis, scriptKey, testBlocks[0], 0, 1)
440+
441+
file, err := NewFile(V0, proof)
442+
require.NoError(t, err)
443+
444+
var buf bytes.Buffer
445+
require.NoError(t, file.Encode(&buf))
446+
447+
mismatchedLocator := Locator{
448+
AssetID: fn.Ptr(asset.RandGenesis(t, asset.Collectible).ID()),
449+
ScriptKey: *proof.Asset.ScriptKey.PubKey,
450+
OutPoint: fn.Ptr(proof.OutPoint()),
451+
}
452+
453+
_, err = ReplaceProofInFiles(
454+
&proof, []*AnnotatedProof{{
455+
Locator: mismatchedLocator,
456+
Blob: buf.Bytes(),
457+
}},
458+
)
459+
require.Error(t, err)
460+
require.Contains(t, err.Error(), "mismatched asset ID")
461+
}

0 commit comments

Comments
 (0)