11package proofscache
22
33import (
4+ "bytes"
5+ "sort"
46 "sync"
57
68 "github.com/multiversx/mx-chain-core-go/core/check"
79 "github.com/multiversx/mx-chain-core-go/data"
810)
911
12+ // maxProofsPerNonce bounds how many different-hash proofs are kept at one nonce (more than one is
13+ // equivocation evidence); on overflow the highest (round, hash) proof is evicted
14+ const maxProofsPerNonce = 4
15+
1016type proofsCache struct {
1117 mutProofsCache sync.RWMutex
1218 proofsByNonceBuckets map [uint64 ]* proofNonceBucket
@@ -34,54 +40,125 @@ func (pc *proofsCache) getProofByHash(headerHash []byte) (data.HeaderProofHandle
3440 return proof , nil
3541}
3642
43+ // getProofByNonce returns the canonical proof at the given nonce: the lowest round, with the
44+ // lowest hash as tie-break
3745func (pc * proofsCache ) getProofByNonce (headerNonce uint64 ) (data.HeaderProofHandler , error ) {
3846 pc .mutProofsCache .RLock ()
3947 defer pc .mutProofsCache .RUnlock ()
4048
41- bucketKey := pc .getBucketKey (headerNonce )
42- bucket , ok := pc .proofsByNonceBuckets [bucketKey ]
43- if ! ok {
49+ proof := pc .lowestProofAtNonce (headerNonce )
50+ if proof == nil {
4451 return nil , ErrMissingProof
4552 }
4653
47- proofHash , ok := bucket .proofsByNonce [headerNonce ]
54+ return proof , nil
55+ }
56+
57+ // lowestProofAtNonce must be called under mutex protection; allocation-free on purpose, it sits
58+ // on the hot read path
59+ func (pc * proofsCache ) lowestProofAtNonce (headerNonce uint64 ) data.HeaderProofHandler {
60+ bucket , ok := pc .proofsByNonceBuckets [pc .getBucketKey (headerNonce )]
4861 if ! ok {
49- return nil , ErrMissingProof
62+ return nil
63+ }
64+
65+ var lowest data.HeaderProofHandler
66+ for _ , headerHash := range bucket .hashesAt (headerNonce ) {
67+ proof , hasProof := pc .proofsByHash [headerHash ]
68+ if ! hasProof {
69+ continue
70+ }
71+ if lowest == nil || lessProof (proof , lowest ) {
72+ lowest = proof
73+ }
5074 }
5175
52- proof , ok := pc .proofsByHash [proofHash ]
76+ return lowest
77+ }
78+
79+ // lessProof reports whether a orders before b by (round, hash) ascending
80+ func lessProof (a , b data.HeaderProofHandler ) bool {
81+ if a .GetHeaderRound () != b .GetHeaderRound () {
82+ return a .GetHeaderRound () < b .GetHeaderRound ()
83+ }
84+ return bytes .Compare (a .GetHeaderHash (), b .GetHeaderHash ()) < 0
85+ }
86+
87+ // getProofsByNonce returns all proofs at the given nonce, ordered by (round, hash) ascending
88+ func (pc * proofsCache ) getProofsByNonce (headerNonce uint64 ) []data.HeaderProofHandler {
89+ pc .mutProofsCache .RLock ()
90+ defer pc .mutProofsCache .RUnlock ()
91+
92+ return pc .sortedProofsAtNonce (headerNonce )
93+ }
94+
95+ // sortedProofsAtNonce must be called under mutex protection
96+ func (pc * proofsCache ) sortedProofsAtNonce (headerNonce uint64 ) []data.HeaderProofHandler {
97+ bucket , ok := pc .proofsByNonceBuckets [pc .getBucketKey (headerNonce )]
5398 if ! ok {
54- return nil , ErrMissingProof
99+ return nil
55100 }
56101
57- return proof , nil
102+ hashes := bucket .hashesAt (headerNonce )
103+ proofs := make ([]data.HeaderProofHandler , 0 , len (hashes ))
104+ for _ , headerHash := range hashes {
105+ proof , hasProof := pc .proofsByHash [headerHash ]
106+ if hasProof {
107+ proofs = append (proofs , proof )
108+ }
109+ }
110+
111+ if len (proofs ) > 1 {
112+ sort .Slice (proofs , func (i , j int ) bool {
113+ return lessProof (proofs [i ], proofs [j ])
114+ })
115+ }
116+
117+ return proofs
58118}
59119
60- func (pc * proofsCache ) addProof (proof data.HeaderProofHandler ) {
120+ // addProof stores the proof, keeping different-hash proofs at the same nonce as equivocation
121+ // evidence; returns the competing proofs only on a newly stored hash, so equivocation reports once
122+ func (pc * proofsCache ) addProof (proof data.HeaderProofHandler ) []data.HeaderProofHandler {
61123 if check .IfNil (proof ) {
62- return
124+ return nil
63125 }
64126
65127 pc .mutProofsCache .Lock ()
66128 defer pc .mutProofsCache .Unlock ()
67129
68- oldHash := pc . insertProofByNonce ( proof )
130+ nonce := proof . GetHeaderNonce ( )
69131 newHash := string (proof .GetHeaderHash ())
132+ bucket := pc .getOrCreateBucket (nonce )
133+
134+ alreadyStored := false
135+ var competingProofs []data.HeaderProofHandler
136+ for _ , existingHash := range bucket .hashesAt (nonce ) {
137+ if existingHash == newHash {
138+ alreadyStored = true
139+ continue
140+ }
70141
71- // Delete the old hash from proofsByHash if it's different from the new hash
72- if len (oldHash ) != 0 && oldHash != newHash {
73- log .Warn ("proofsCache.addProof: overwrite proof by hash" ,
74- "oldHash" , oldHash ,
75- "newHash" , newHash ,
76- )
77- delete (pc .proofsByHash , oldHash )
142+ existingProof , hasProof := pc .proofsByHash [existingHash ]
143+ if hasProof {
144+ competingProofs = append (competingProofs , existingProof )
145+ }
78146 }
79147
148+ bucket .insert (nonce , newHash )
80149 pc .proofsByHash [newHash ] = proof
150+
151+ pc .evictExcessProofsAtNonce (bucket , nonce )
152+
153+ if alreadyStored {
154+ return nil
155+ }
156+
157+ return competingProofs
81158}
82159
83160// addProofIfNoneAtNonce adds the proof only if its nonce slot is free; an occupied slot (same or
84- // different hash) rejects the add and returns the pre-existing proof, never overwriting it
161+ // different hash) rejects the add and returns the pre-existing canonical proof, never overwriting it
85162func (pc * proofsCache ) addProofIfNoneAtNonce (proof data.HeaderProofHandler ) (bool , data.HeaderProofHandler ) {
86163 if check .IfNil (proof ) {
87164 return false , nil
@@ -90,39 +167,57 @@ func (pc *proofsCache) addProofIfNoneAtNonce(proof data.HeaderProofHandler) (boo
90167 pc .mutProofsCache .Lock ()
91168 defer pc .mutProofsCache .Unlock ()
92169
93- bucketKey := pc .getBucketKey (proof .GetHeaderNonce ())
94- bucket , ok := pc .proofsByNonceBuckets [bucketKey ]
95- if ok {
96- existingHash , hasNonce := bucket .proofsByNonce [proof .GetHeaderNonce ()]
97- if hasNonce {
98- existingProof , hasProof := pc .proofsByHash [existingHash ]
99- if hasProof {
100- return false , existingProof
101- }
102- }
170+ nonce := proof .GetHeaderNonce ()
171+ existingProof := pc .lowestProofAtNonce (nonce )
172+ if existingProof != nil {
173+ return false , existingProof
103174 }
104175
105- _ = pc .insertProofByNonce (proof )
176+ bucket := pc .getOrCreateBucket (nonce )
177+ bucket .insert (nonce , string (proof .GetHeaderHash ()))
106178 pc .proofsByHash [string (proof .GetHeaderHash ())] = proof
107179
108180 return true , nil
109181}
110182
183+ // evictExcessProofsAtNonce must be called under mutex protection
184+ func (pc * proofsCache ) evictExcessProofsAtNonce (bucket * proofNonceBucket , nonce uint64 ) {
185+ if len (bucket .hashesAt (nonce )) <= maxProofsPerNonce {
186+ return
187+ }
188+
189+ proofs := pc .sortedProofsAtNonce (nonce )
190+ for len (proofs ) > maxProofsPerNonce {
191+ evictedProof := proofs [len (proofs )- 1 ]
192+ proofs = proofs [:len (proofs )- 1 ]
193+ evictedHash := string (evictedProof .GetHeaderHash ())
194+ bucket .remove (nonce , evictedHash )
195+ delete (pc .proofsByHash , evictedHash )
196+
197+ log .Warn ("proofsCache: too many proofs at the same nonce, evicted the highest round one" ,
198+ "nonce" , nonce ,
199+ "evicted hash" , evictedProof .GetHeaderHash (),
200+ "evicted round" , evictedProof .GetHeaderRound (),
201+ )
202+ }
203+ }
204+
111205// getBucketKey will return bucket key as lower bound window value
112206func (pc * proofsCache ) getBucketKey (index uint64 ) uint64 {
113207 return (index / pc .bucketSize ) * pc .bucketSize
114208}
115209
116- func (pc * proofsCache ) insertProofByNonce (proof data.HeaderProofHandler ) string {
117- bucketKey := pc .getBucketKey (proof .GetHeaderNonce ())
210+ // getOrCreateBucket must be called under mutex protection
211+ func (pc * proofsCache ) getOrCreateBucket (nonce uint64 ) * proofNonceBucket {
212+ bucketKey := pc .getBucketKey (nonce )
118213
119214 bucket , ok := pc .proofsByNonceBuckets [bucketKey ]
120215 if ! ok {
121216 bucket = newProofBucket ()
122217 pc .proofsByNonceBuckets [bucketKey ] = bucket
123218 }
124219
125- return bucket . insert ( proof )
220+ return bucket
126221}
127222
128223func (pc * proofsCache ) cleanupProofsBehindNonce (nonce uint64 ) {
@@ -142,7 +237,9 @@ func (pc *proofsCache) cleanupProofsBehindNonce(nonce uint64) {
142237}
143238
144239func (pc * proofsCache ) cleanupProofsInBucket (bucket * proofNonceBucket ) {
145- for _ , headerHash := range bucket .proofsByNonce {
146- delete (pc .proofsByHash , headerHash )
240+ for _ , headerHashes := range bucket .proofsByNonce {
241+ for _ , headerHash := range headerHashes {
242+ delete (pc .proofsByHash , headerHash )
243+ }
147244 }
148245}
0 commit comments