github.com/prysmaticlabs/prysm@v1.4.4/slasher/detection/attestations/spanner.go (about) 1 // Package attestations defines an implementation of a 2 // slashable attestation detector using min-max surround vote checking. 3 package attestations 4 5 import ( 6 "context" 7 "fmt" 8 9 "github.com/pkg/errors" 10 "github.com/prometheus/client_golang/prometheus" 11 "github.com/prometheus/client_golang/prometheus/promauto" 12 types "github.com/prysmaticlabs/eth2-types" 13 ethpb "github.com/prysmaticlabs/prysm/proto/eth/v1alpha1" 14 "github.com/prysmaticlabs/prysm/shared/featureconfig" 15 "github.com/prysmaticlabs/prysm/shared/params" 16 "github.com/prysmaticlabs/prysm/slasher/db" 17 dbtypes "github.com/prysmaticlabs/prysm/slasher/db/types" 18 "github.com/prysmaticlabs/prysm/slasher/detection/attestations/iface" 19 slashertypes "github.com/prysmaticlabs/prysm/slasher/detection/attestations/types" 20 "go.opencensus.io/trace" 21 ) 22 23 var ( 24 latestMinSpanDistanceObserved = promauto.NewGauge(prometheus.GaugeOpts{ 25 Name: "latest_min_span_distance_observed", 26 Help: "The latest distance between target - source observed for min spans", 27 }) 28 latestMaxSpanDistanceObserved = promauto.NewGauge(prometheus.GaugeOpts{ 29 Name: "latest_max_span_distance_observed", 30 Help: "The latest distance between target - source observed for max spans", 31 }) 32 sourceLargerThenTargetObserved = promauto.NewCounter(prometheus.CounterOpts{ 33 Name: "attestation_source_larger_then_target", 34 Help: "The number of attestation data source epoch that aren larger then target epoch.", 35 }) 36 ) 37 38 // We look back 128 epochs when updating min/max spans 39 // for incoming attestations. 40 // TODO(#5040): Remove lookback and handle min spans properly. 41 const epochLookback = types.Epoch(128) 42 43 var _ iface.SpanDetector = (*SpanDetector)(nil) 44 45 // SpanDetector defines a struct which can detect slashable 46 // attestation offenses by tracking validator min-max 47 // spans from validators and attestation data roots. 48 type SpanDetector struct { 49 slasherDB db.Database 50 } 51 52 // NewSpanDetector creates a new instance of a struct tracking 53 // several epochs of min-max spans for each validator in 54 // the beacon state. 55 func NewSpanDetector(db db.Database) *SpanDetector { 56 return &SpanDetector{ 57 slasherDB: db, 58 } 59 } 60 61 // DetectSlashingsForAttestation uses a validator index and its corresponding 62 // min-max spans during an epoch to detect an epoch in which the validator 63 // committed a slashable attestation. 64 func (s *SpanDetector) DetectSlashingsForAttestation( 65 ctx context.Context, 66 att *ethpb.IndexedAttestation, 67 ) ([]*slashertypes.DetectionResult, error) { 68 ctx, traceSpan := trace.StartSpan(ctx, "spanner.DetectSlashingsForAttestation") 69 defer traceSpan.End() 70 sourceEpoch := att.Data.Source.Epoch 71 targetEpoch := att.Data.Target.Epoch 72 dis := targetEpoch - sourceEpoch 73 74 if sourceEpoch > targetEpoch { // Prevent underflow and handle source > target slashable cases. 75 dis = sourceEpoch - targetEpoch 76 sourceEpoch, targetEpoch = targetEpoch, sourceEpoch 77 sourceLargerThenTargetObserved.Inc() 78 } 79 80 if dis > params.BeaconConfig().WeakSubjectivityPeriod { 81 return nil, fmt.Errorf( 82 "attestation span was greater than weak subjectivity period %d, received: %d", 83 params.BeaconConfig().WeakSubjectivityPeriod, 84 dis, 85 ) 86 } 87 88 spanMap, err := s.slasherDB.EpochSpans(ctx, sourceEpoch, dbtypes.UseCache) 89 if err != nil { 90 return nil, err 91 } 92 targetSpanMap, err := s.slasherDB.EpochSpans(ctx, targetEpoch, dbtypes.UseCache) 93 if err != nil { 94 return nil, err 95 } 96 97 var detections []*slashertypes.DetectionResult 98 distance := uint16(dis) 99 for _, idx := range att.AttestingIndices { 100 if ctx.Err() != nil { 101 return nil, errors.Wrap(ctx.Err(), "could not detect slashings") 102 } 103 span, err := spanMap.GetValidatorSpan(idx) 104 if err != nil { 105 return nil, err 106 } 107 minSpan := span.MinSpan 108 if minSpan > 0 && minSpan < distance { 109 slashableEpoch := sourceEpoch + types.Epoch(minSpan) 110 targetSpans, err := s.slasherDB.EpochSpans(ctx, slashableEpoch, dbtypes.UseCache) 111 if err != nil { 112 return nil, err 113 } 114 valSpan, err := targetSpans.GetValidatorSpan(idx) 115 if err != nil { 116 return nil, err 117 } 118 detections = append(detections, &slashertypes.DetectionResult{ 119 ValidatorIndex: idx, 120 Kind: slashertypes.SurroundVote, 121 SlashableEpoch: slashableEpoch, 122 SigBytes: valSpan.SigBytes, 123 }) 124 continue 125 } 126 127 maxSpan := span.MaxSpan 128 if maxSpan > distance { 129 slashableEpoch := sourceEpoch + types.Epoch(maxSpan) 130 targetSpans, err := s.slasherDB.EpochSpans(ctx, slashableEpoch, dbtypes.UseCache) 131 if err != nil { 132 return nil, err 133 } 134 valSpan, err := targetSpans.GetValidatorSpan(idx) 135 if err != nil { 136 return nil, err 137 } 138 detections = append(detections, &slashertypes.DetectionResult{ 139 ValidatorIndex: idx, 140 Kind: slashertypes.SurroundVote, 141 SlashableEpoch: slashableEpoch, 142 SigBytes: valSpan.SigBytes, 143 }) 144 continue 145 } 146 147 targetSpan, err := targetSpanMap.GetValidatorSpan(idx) 148 if err != nil { 149 return nil, err 150 } 151 // Check if the validator has attested for this epoch or not. 152 if targetSpan.HasAttested { 153 detections = append(detections, &slashertypes.DetectionResult{ 154 ValidatorIndex: idx, 155 Kind: slashertypes.DoubleVote, 156 SlashableEpoch: targetEpoch, 157 SigBytes: targetSpan.SigBytes, 158 }) 159 continue 160 } 161 } 162 163 return detections, nil 164 } 165 166 // UpdateSpans given an indexed attestation for all of its attesting indices. 167 func (s *SpanDetector) UpdateSpans(ctx context.Context, att *ethpb.IndexedAttestation) error { 168 ctx, span := trace.StartSpan(ctx, "spanner.UpdateSpans") 169 defer span.End() 170 // Save the signature for the received attestation so we can have more detail to find it in the DB. 171 if err := s.saveSigBytes(ctx, att); err != nil { 172 return err 173 } 174 // Update min and max spans. 175 if err := s.updateMinSpan(ctx, att); err != nil { 176 return err 177 } 178 return s.updateMaxSpan(ctx, att) 179 } 180 181 // saveSigBytes saves the first 2 bytes of the signature for the att we're updating the spans to. 182 // Later used to help us find the violating attestation in the DB. 183 func (s *SpanDetector) saveSigBytes(ctx context.Context, att *ethpb.IndexedAttestation) error { 184 ctx, traceSpan := trace.StartSpan(ctx, "spanner.saveSigBytes") 185 defer traceSpan.End() 186 target := att.Data.Target.Epoch 187 source := att.Data.Source.Epoch 188 // handle source > target well 189 if source > target { 190 target = source 191 } 192 spanMap, err := s.slasherDB.EpochSpans(ctx, target, dbtypes.UseCache) 193 if err != nil { 194 return err 195 } 196 197 // We loop through the indices, instead of constantly locking/unlocking the cache for equivalent accesses. 198 for _, idx := range att.AttestingIndices { 199 if ctx.Err() != nil { 200 return errors.Wrap(ctx.Err(), "could not save signature bytes") 201 } 202 span, err := spanMap.GetValidatorSpan(idx) 203 if err != nil { 204 return err 205 } 206 // If the validator has already attested for this target epoch, 207 // then we do not need to update the values of the span sig bytes. 208 if span.HasAttested { 209 return nil 210 } 211 212 sigBytes := [2]byte{0, 0} 213 if len(att.Signature) > 1 { 214 sigBytes = [2]byte{att.Signature[0], att.Signature[1]} 215 } 216 // Save the signature bytes into the span for this epoch. 217 newSpan := slashertypes.Span{ 218 MinSpan: span.MinSpan, 219 MaxSpan: span.MaxSpan, 220 HasAttested: true, 221 SigBytes: sigBytes, 222 } 223 spanMap, err = spanMap.SetValidatorSpan(idx, newSpan) 224 if err != nil { 225 return err 226 } 227 } 228 return s.slasherDB.SaveEpochSpans(ctx, target, spanMap, dbtypes.UseCache) 229 } 230 231 // Updates a min span for a validator index given a source and target epoch 232 // for an attestation produced by the validator. Used for catching surrounding votes. 233 func (s *SpanDetector) updateMinSpan(ctx context.Context, att *ethpb.IndexedAttestation) error { 234 ctx, traceSpan := trace.StartSpan(ctx, "spanner.updateMinSpan") 235 defer traceSpan.End() 236 source := att.Data.Source.Epoch 237 target := att.Data.Target.Epoch 238 if source < 1 { 239 return nil 240 } 241 // handle source > target well 242 if source > target { 243 source, target = target, source 244 } 245 valIndices := make([]uint64, len(att.AttestingIndices)) 246 copy(valIndices, att.AttestingIndices) 247 latestMinSpanDistanceObserved.Set(float64(att.Data.Target.Epoch - att.Data.Source.Epoch)) 248 249 // the for loop tries to update min span using cache for as long as there 250 // is a relevant cached epoch. when there is no such epoch in cache batch 251 // db read and write is used. 252 var spanMap *slashertypes.EpochStore 253 epoch := source - 1 254 lookbackEpoch := epoch - epochLookback 255 // prevent underflow 256 if epoch < epochLookback { 257 lookbackEpoch = 0 258 } 259 untilEpoch := lookbackEpoch 260 if featureconfig.Get().DisableLookback { 261 untilEpoch = 0 262 } 263 var err error 264 dbOrCache := dbtypes.UseCache 265 for ; epoch >= untilEpoch; epoch-- { 266 if ctx.Err() != nil { 267 return errors.Wrap(ctx.Err(), "could not update min spans") 268 } 269 spanMap, err = s.slasherDB.EpochSpans(ctx, epoch, dbtypes.UseCache) 270 if err != nil { 271 return err 272 } 273 indices := valIndices[:0] 274 for _, idx := range valIndices { 275 span, err := spanMap.GetValidatorSpan(idx) 276 if err != nil { 277 return err 278 } 279 newMinSpan := uint16(target - epoch) 280 if span.MinSpan == 0 || span.MinSpan > newMinSpan { 281 span = slashertypes.Span{ 282 MinSpan: newMinSpan, 283 MaxSpan: span.MaxSpan, 284 SigBytes: span.SigBytes, 285 HasAttested: span.HasAttested, 286 } 287 spanMap, err = spanMap.SetValidatorSpan(idx, span) 288 if err != nil { 289 return err 290 } 291 indices = append(indices, idx) 292 } 293 } 294 if epoch <= lookbackEpoch && dbOrCache == dbtypes.UseCache { 295 dbOrCache = dbtypes.UseDB 296 } 297 if err := s.slasherDB.SaveEpochSpans(ctx, epoch, spanMap, dbOrCache); err != nil { 298 return err 299 } 300 if len(indices) == 0 { 301 break 302 } 303 if epoch == 0 { 304 break 305 } 306 } 307 return nil 308 } 309 310 // Updates a max span for a validator index given a source and target epoch 311 // for an attestation produced by the validator. Used for catching surrounded votes. 312 func (s *SpanDetector) updateMaxSpan(ctx context.Context, att *ethpb.IndexedAttestation) error { 313 ctx, traceSpan := trace.StartSpan(ctx, "spanner.updateMaxSpan") 314 defer traceSpan.End() 315 source := att.Data.Source.Epoch 316 target := att.Data.Target.Epoch 317 // handle source > target well 318 if source > target { 319 source, target = target, source 320 } 321 latestMaxSpanDistanceObserved.Set(float64(target - source)) 322 valIndices := make([]uint64, len(att.AttestingIndices)) 323 copy(valIndices, att.AttestingIndices) 324 for epoch := source + 1; epoch < target; epoch++ { 325 if ctx.Err() != nil { 326 return errors.Wrap(ctx.Err(), "could not update max spans") 327 } 328 spanMap, err := s.slasherDB.EpochSpans(ctx, epoch, dbtypes.UseCache) 329 if err != nil { 330 return err 331 } 332 indices := valIndices[:0] 333 for _, idx := range valIndices { 334 span, err := spanMap.GetValidatorSpan(idx) 335 if err != nil { 336 return err 337 } 338 newMaxSpan := uint16(target - epoch) 339 if newMaxSpan > span.MaxSpan { 340 span = slashertypes.Span{ 341 MinSpan: span.MinSpan, 342 MaxSpan: newMaxSpan, 343 SigBytes: span.SigBytes, 344 HasAttested: span.HasAttested, 345 } 346 spanMap, err = spanMap.SetValidatorSpan(idx, span) 347 if err != nil { 348 return err 349 } 350 indices = append(indices, idx) 351 } 352 } 353 if err := s.slasherDB.SaveEpochSpans(ctx, epoch, spanMap, dbtypes.UseCache); err != nil { 354 return err 355 } 356 if len(indices) == 0 { 357 break 358 } 359 } 360 return nil 361 }