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  }