Skip to content

Commit 543306c

Browse files
authored
Merge pull request #7912 from multiversx/multi-proof-storage
multi proof pool
2 parents 5c36291 + 4521f01 commit 543306c

21 files changed

Lines changed: 1012 additions & 111 deletions

common/constants.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,10 @@ const MetricNumShardHeadersProcessed = "erd_num_shard_headers_processed"
259259
// MetricNumTimesInForkChoice is the metric that counts how many times a node was in fork choice
260260
const MetricNumTimesInForkChoice = "erd_fork_choice_count"
261261

262+
// MetricNumEquivocationProofs is the metric that counts the equivocation events observed by the
263+
// proofs pool (different-hash proofs received for the same header nonce)
264+
const MetricNumEquivocationProofs = "erd_num_equivocation_proofs"
265+
262266
// MetricHighestFinalBlock is the metric for the nonce of the highest final block
263267
const MetricHighestFinalBlock = "erd_highest_final_nonce"
264268

dataRetriever/dataPool/proofsCache/export_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ package proofscache
22

33
import "github.com/multiversx/mx-chain-core-go/data"
44

5+
// MaxProofsPerNonce -
6+
const MaxProofsPerNonce = maxProofsPerNonce
7+
58
// NewProofsCache -
69
func NewProofsCache(bucketSize int) *proofsCache {
710
return newProofsCache(bucketSize)
Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,50 @@
11
package proofscache
22

3-
import "github.com/multiversx/mx-chain-core-go/data"
4-
53
type proofNonceBucket struct {
64
maxNonce uint64
7-
proofsByNonce map[uint64]string
5+
proofsByNonce map[uint64][]string
86
}
97

108
func newProofBucket() *proofNonceBucket {
119
return &proofNonceBucket{
12-
proofsByNonce: make(map[uint64]string),
10+
proofsByNonce: make(map[uint64][]string),
1311
}
1412
}
1513

1614
func (p *proofNonceBucket) size() int {
17-
return len(p.proofsByNonce)
15+
size := 0
16+
for _, hashes := range p.proofsByNonce {
17+
size += len(hashes)
18+
}
19+
20+
return size
1821
}
1922

20-
func (p *proofNonceBucket) insert(proof data.HeaderProofHandler) string {
21-
nonce := proof.GetHeaderNonce()
22-
newHash := string(proof.GetHeaderHash())
23+
func (p *proofNonceBucket) hashesAt(nonce uint64) []string {
24+
return p.proofsByNonce[nonce]
25+
}
2326

24-
oldHash, existed := p.proofsByNonce[nonce]
25-
p.proofsByNonce[nonce] = newHash
27+
// insert adds the hash at the given nonce, keeping any different hashes already stored there
28+
func (p *proofNonceBucket) insert(nonce uint64, hash string) {
29+
for _, existingHash := range p.proofsByNonce[nonce] {
30+
if existingHash == hash {
31+
return
32+
}
33+
}
34+
35+
p.proofsByNonce[nonce] = append(p.proofsByNonce[nonce], hash)
2636

2737
if nonce > p.maxNonce {
2838
p.maxNonce = nonce
2939
}
40+
}
3041

31-
if existed {
32-
return oldHash
42+
func (p *proofNonceBucket) remove(nonce uint64, hash string) {
43+
hashes := p.proofsByNonce[nonce]
44+
for i, existingHash := range hashes {
45+
if existingHash == hash {
46+
p.proofsByNonce[nonce] = append(hashes[:i], hashes[i+1:]...)
47+
return
48+
}
3349
}
34-
return ""
3550
}
Lines changed: 132 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
package proofscache
22

33
import (
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+
1016
type 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
3745
func (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
85162
func (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
112206
func (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

128223
func (pc *proofsCache) cleanupProofsBehindNonce(nonce uint64) {
@@ -142,7 +237,9 @@ func (pc *proofsCache) cleanupProofsBehindNonce(nonce uint64) {
142237
}
143238

144239
func (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

Comments
 (0)