Skip to content

Commit 1cfbe7c

Browse files
Roasbeefguggero
authored andcommitted
proof: add new IgnoreChecker proof rejection cache
In this commit, we add a new interface, the IgnoreChecker. This is to be used as a proof rejection cache, which allows us to ensure that we don't continue to validate proofs that we already know to be invalid. The underlying cache implementation is also intended to support its own invalidation to handle re-org edge cases. A concrete implementation will be added in an upcoming PR.
1 parent 30d92d2 commit 1cfbe7c

File tree

3 files changed

+139
-5
lines changed

3 files changed

+139
-5
lines changed

proof/mock.go

+18
Original file line numberDiff line numberDiff line change
@@ -1028,3 +1028,21 @@ func (tmr *TestMetaReveal) ToMetaReveal(t testing.TB) *MetaReveal {
10281028
UnknownOddTypes: tmr.UnknownOddTypes,
10291029
}
10301030
}
1031+
1032+
type mockIgnoreChecker struct {
1033+
ignoredAssetPoints fn.Set[AssetPoint]
1034+
ignoreAll bool
1035+
}
1036+
1037+
func newMockIgnoreChecker(ignoreAll bool,
1038+
ignorePoints ...AssetPoint) *mockIgnoreChecker {
1039+
1040+
return &mockIgnoreChecker{
1041+
ignoredAssetPoints: fn.NewSet(ignorePoints...),
1042+
ignoreAll: ignoreAll,
1043+
}
1044+
}
1045+
1046+
func (m *mockIgnoreChecker) IsIgnored(assetPoint AssetPoint) bool {
1047+
return m.ignoreAll || m.ignoredAssetPoints.Contains(assetPoint)
1048+
}

proof/proof_test.go

+63
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import (
1313
"testing"
1414
"time"
1515

16+
lfn "github.com/lightningnetwork/lnd/fn"
17+
1618
"github.com/btcsuite/btcd/blockchain"
1719
"github.com/btcsuite/btcd/btcec/v2"
1820
"github.com/btcsuite/btcd/btcutil"
@@ -29,6 +31,7 @@ import (
2931
"github.com/lightningnetwork/lnd/build"
3032
"github.com/lightningnetwork/lnd/tlv"
3133
"github.com/stretchr/testify/require"
34+
"pgregory.net/rapid"
3235
)
3336

3437
var (
@@ -1002,6 +1005,66 @@ func TestProofVerification(t *testing.T) {
10021005
require.ErrorIs(t, err, ErrUnknownVersion)
10031006
}
10041007

1008+
// TestProofFileVerificationIgnoreChecker tests that the ignore checker can be
1009+
// used as a proof rejection cache.
1010+
func TestProofFileVerificationIgnoreChecker(t *testing.T) {
1011+
proofHex, err := os.ReadFile(proofFileHexFileName)
1012+
require.NoError(t, err)
1013+
1014+
proofBytes, err := hex.DecodeString(
1015+
strings.Trim(string(proofHex), "\n"),
1016+
)
1017+
require.NoError(t, err)
1018+
1019+
proofFile := &File{}
1020+
err = proofFile.Decode(bytes.NewReader(proofBytes))
1021+
require.NoError(t, err)
1022+
1023+
numProofs := proofFile.NumProofs()
1024+
1025+
rapid.Check(t, func(t *rapid.T) {
1026+
// Pick an invalid proof index in the range. -1 means that no
1027+
// proofs are invalid.
1028+
invalidIdx := rapid.IntRange(-1, numProofs-1).Draw(
1029+
t, "invalidIdx",
1030+
)
1031+
1032+
vCtx := MockVerifierCtx
1033+
1034+
reject := invalidIdx >= 0
1035+
1036+
if reject {
1037+
p, err := proofFile.ProofAt(uint32(invalidIdx))
1038+
require.NoError(t, err)
1039+
1040+
assetPoint := AssetPoint{
1041+
OutPoint: wire.OutPoint{
1042+
Hash: p.AnchorTx.TxHash(),
1043+
Index: p.InclusionProof.OutputIndex,
1044+
},
1045+
ID: p.Asset.ID(),
1046+
ScriptKey: asset.ToSerialized(
1047+
p.Asset.ScriptKey.PubKey,
1048+
),
1049+
}
1050+
1051+
ignoreChecker := newMockIgnoreChecker(
1052+
false, assetPoint,
1053+
)
1054+
vCtx.IgnoreChecker = lfn.Some[IgnoreChecker](
1055+
ignoreChecker,
1056+
)
1057+
}
1058+
1059+
_, err = proofFile.Verify(context.Background(), vCtx)
1060+
if reject {
1061+
require.ErrorIs(t, err, ErrProofInvalid)
1062+
} else {
1063+
require.NoError(t, err)
1064+
}
1065+
})
1066+
}
1067+
10051068
// TestOwnershipProofVerification ensures that the ownership proof encoding and
10061069
// decoding as well as the verification works as expected.
10071070
func TestOwnershipProofVerification(t *testing.T) {

proof/verifier.go

+58-5
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/lightninglabs/taproot-assets/commitment"
1818
"github.com/lightninglabs/taproot-assets/fn"
1919
"github.com/lightninglabs/taproot-assets/vm"
20+
lfn "github.com/lightningnetwork/lnd/fn"
2021
"golang.org/x/exp/maps"
2122
"golang.org/x/sync/errgroup"
2223
)
@@ -33,6 +34,21 @@ type ChainLookupGenerator interface {
3334
GenProofChainLookup(p *Proof) (asset.ChainLookup, error)
3435
}
3536

37+
// AssetPoint is similar to PrevID but is meant to be used for created asset
38+
// outputs rather than those that are spent. This is similar to the concept of
39+
// an outpoint in normal Bitcoin.
40+
type AssetPoint = asset.PrevID
41+
42+
// IgnoreChecker is used during proof validation to optionally fail validation
43+
// if a proof is known to be invalid. This can be used as a caching mechanism to
44+
// avoid expensive validation for already known invalid proofs.
45+
type IgnoreChecker interface {
46+
// IsIgnored returns true if the given prevID is known to be invalid. A
47+
// prevID is used here, but the check should be tested against a proof
48+
// result, or produced output.
49+
IsIgnored(prevID AssetPoint) bool
50+
}
51+
3652
// VerifierCtx is a context struct that is used to pass in various interfaces
3753
// needed during proof verification.
3854
type VerifierCtx struct {
@@ -45,6 +61,8 @@ type VerifierCtx struct {
4561
GroupAnchorVerifier GroupAnchorVerifier
4662

4763
ChainLookupGen ChainLookupGenerator
64+
65+
IgnoreChecker lfn.Option[IgnoreChecker]
4866
}
4967

5068
// Verifier abstracts away from the task of verifying a proof file blob.
@@ -559,6 +577,23 @@ func (p *Proof) Verify(ctx context.Context, prev *AssetSnapshot,
559577
"%w", err)
560578
}
561579

580+
assetPoint := AssetPoint{
581+
OutPoint: p.OutPoint(),
582+
ID: p.Asset.ID(),
583+
ScriptKey: asset.ToSerialized(p.Asset.ScriptKey.PubKey),
584+
}
585+
586+
// Before we do any other validation, we'll check to see if we can halt
587+
// validation here, as the proof is already known to be invalid. This
588+
// can be used as a rejection caching mechanism.
589+
fail := lfn.MapOptionZ(vCtx.IgnoreChecker, func(c IgnoreChecker) bool {
590+
return c.IsIgnored(assetPoint)
591+
})
592+
if fail {
593+
return prev, fmt.Errorf("%w: asset_point=%v is ignored",
594+
ErrProofInvalid, assetPoint)
595+
}
596+
562597
// 1. A transaction that spends the previous asset output has a valid
563598
// merkle proof within a block in the chain.
564599
if prev != nil && p.PrevOut != prev.OutPoint {
@@ -683,11 +718,8 @@ func (p *Proof) Verify(ctx context.Context, prev *AssetSnapshot,
683718
// TODO(roasbeef): need tx index as well
684719

685720
return &AssetSnapshot{
686-
Asset: &p.Asset,
687-
OutPoint: wire.OutPoint{
688-
Hash: p.AnchorTx.TxHash(),
689-
Index: p.InclusionProof.OutputIndex,
690-
},
721+
Asset: &p.Asset,
722+
OutPoint: p.OutPoint(),
691723
AnchorBlockHash: p.BlockHeader.BlockHash(),
692724
AnchorBlockHeight: p.BlockHeight,
693725
AnchorTx: &p.AnchorTx,
@@ -790,6 +822,27 @@ func (f *File) Verify(ctx context.Context,
790822
if err != nil {
791823
return nil, err
792824
}
825+
826+
// At this point, we'll check to see if we can halt validation
827+
// here, as the proof is already known to be invalid. This can
828+
// be used as a rejection caching mechanism.
829+
fail := lfn.MapOptionZ(
830+
vCtx.IgnoreChecker, func(checker IgnoreChecker) bool {
831+
assetPoint := AssetPoint{
832+
OutPoint: result.OutPoint,
833+
ID: result.Asset.ID(),
834+
ScriptKey: asset.ToSerialized(
835+
result.Asset.ScriptKey.PubKey,
836+
),
837+
}
838+
839+
return checker.IsIgnored(assetPoint)
840+
},
841+
)
842+
if fail {
843+
return prev, ErrProofFileInvalid
844+
}
845+
793846
prev = result
794847
}
795848

0 commit comments

Comments
 (0)