github.com/prysmaticlabs/prysm@v1.4.4/validator/slashing-protection/local/standard-protection-format/import.go (about)

     1  package interchangeformat
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"fmt"
     8  	"io"
     9  	"io/ioutil"
    10  
    11  	"github.com/pkg/errors"
    12  	types "github.com/prysmaticlabs/eth2-types"
    13  	ethpb "github.com/prysmaticlabs/prysm/proto/eth/v1alpha1"
    14  	"github.com/prysmaticlabs/prysm/shared/bytesutil"
    15  	"github.com/prysmaticlabs/prysm/shared/slashutil"
    16  	"github.com/prysmaticlabs/prysm/validator/db"
    17  	"github.com/prysmaticlabs/prysm/validator/db/kv"
    18  	"github.com/prysmaticlabs/prysm/validator/slashing-protection/local/standard-protection-format/format"
    19  )
    20  
    21  // ImportStandardProtectionJSON takes in EIP-3076 compliant JSON file used for slashing protection
    22  // by Ethereum validators and imports its data into Prysm's internal representation of slashing
    23  // protection in the validator client's database. For more information, see the EIP document here:
    24  // https://eips.ethereum.org/EIPS/eip-3076.
    25  func ImportStandardProtectionJSON(ctx context.Context, validatorDB db.Database, r io.Reader) error {
    26  	encodedJSON, err := ioutil.ReadAll(r)
    27  	if err != nil {
    28  		return errors.Wrap(err, "could not read slashing protection JSON file")
    29  	}
    30  	interchangeJSON := &format.EIPSlashingProtectionFormat{}
    31  	if err := json.Unmarshal(encodedJSON, interchangeJSON); err != nil {
    32  		return errors.Wrap(err, "could not unmarshal slashing protection JSON file")
    33  	}
    34  	if interchangeJSON.Data == nil {
    35  		log.Warn("No slashing protection data to import")
    36  		return nil
    37  	}
    38  
    39  	// We validate the `MetadataV0` field of the slashing protection JSON file.
    40  	if err := validateMetadata(ctx, validatorDB, interchangeJSON); err != nil {
    41  		return errors.Wrap(err, "slashing protection JSON metadata was incorrect")
    42  	}
    43  
    44  	// We need to handle duplicate public keys in the JSON file, with potentially
    45  	// different signing histories for both attestations and blocks.
    46  	signedBlocksByPubKey, err := parseBlocksForUniquePublicKeys(interchangeJSON.Data)
    47  	if err != nil {
    48  		return errors.Wrap(err, "could not parse unique entries for blocks by public key")
    49  	}
    50  	signedAttsByPubKey, err := parseAttestationsForUniquePublicKeys(interchangeJSON.Data)
    51  	if err != nil {
    52  		return errors.Wrap(err, "could not parse unique entries for attestations by public key")
    53  	}
    54  
    55  	attestingHistoryByPubKey := make(map[[48]byte][]*kv.AttestationRecord)
    56  	proposalHistoryByPubKey := make(map[[48]byte]kv.ProposalHistoryForPubkey)
    57  	for pubKey, signedBlocks := range signedBlocksByPubKey {
    58  		// Transform the processed signed blocks data from the JSON
    59  		// file into the internal Prysm representation of proposal history.
    60  		proposalHistory, err := transformSignedBlocks(ctx, signedBlocks)
    61  		if err != nil {
    62  			return errors.Wrapf(err, "could not parse signed blocks in JSON file for key %#x", pubKey)
    63  		}
    64  		proposalHistoryByPubKey[pubKey] = *proposalHistory
    65  	}
    66  
    67  	for pubKey, signedAtts := range signedAttsByPubKey {
    68  		// Transform the processed signed attestation data from the JSON
    69  		// file into the internal Prysm representation of attesting history.
    70  		historicalAtt, err := transformSignedAttestations(pubKey, signedAtts)
    71  		if err != nil {
    72  			return errors.Wrapf(err, "could not parse signed attestations in JSON file for key %#x", pubKey)
    73  		}
    74  		attestingHistoryByPubKey[pubKey] = historicalAtt
    75  	}
    76  
    77  	// We validate and filter out public keys parsed from JSON to ensure we are
    78  	// not importing those which are slashable with respect to other data within the same JSON.
    79  	slashableProposerKeys := filterSlashablePubKeysFromBlocks(ctx, proposalHistoryByPubKey)
    80  	slashableAttesterKeys, err := filterSlashablePubKeysFromAttestations(
    81  		ctx, validatorDB, attestingHistoryByPubKey,
    82  	)
    83  	if err != nil {
    84  		return errors.Wrap(err, "could not filter slashable attester public keys from JSON data")
    85  	}
    86  
    87  	slashablePublicKeys := make([][48]byte, 0, len(slashableAttesterKeys)+len(slashableProposerKeys))
    88  	for _, pubKey := range slashableProposerKeys {
    89  		delete(proposalHistoryByPubKey, pubKey)
    90  		slashablePublicKeys = append(slashablePublicKeys, pubKey)
    91  	}
    92  	for _, pubKey := range slashableAttesterKeys {
    93  		delete(attestingHistoryByPubKey, pubKey)
    94  		slashablePublicKeys = append(slashablePublicKeys, pubKey)
    95  	}
    96  
    97  	if err := validatorDB.SaveEIPImportBlacklistedPublicKeys(ctx, slashablePublicKeys); err != nil {
    98  		return errors.Wrap(err, "could not save slashable public keys to database")
    99  	}
   100  
   101  	// We save the histories to disk as atomic operations, ensuring that this only occurs
   102  	// until after we successfully parse all data from the JSON file. If there is any error
   103  	// in parsing the JSON proposal and attesting histories, we will not reach this point.
   104  	for pubKey, proposalHistory := range proposalHistoryByPubKey {
   105  		bar := initializeProgressBar(
   106  			len(proposalHistory.Proposals),
   107  			fmt.Sprintf("Importing proposals for validator public key %#x", bytesutil.Trunc(pubKey[:])),
   108  		)
   109  		for _, proposal := range proposalHistory.Proposals {
   110  			if err := bar.Add(1); err != nil {
   111  				log.WithError(err).Debug("Could not increase progress bar")
   112  			}
   113  			if err = validatorDB.SaveProposalHistoryForSlot(ctx, pubKey, proposal.Slot, proposal.SigningRoot); err != nil {
   114  				return errors.Wrap(err, "could not save proposal history from imported JSON to database")
   115  			}
   116  		}
   117  	}
   118  	bar := initializeProgressBar(
   119  		len(attestingHistoryByPubKey),
   120  		"Importing attesting history for validator public keys",
   121  	)
   122  	for pubKey, attestations := range attestingHistoryByPubKey {
   123  		if err := bar.Add(1); err != nil {
   124  			log.WithError(err).Debug("Could not increase progress bar")
   125  		}
   126  		indexedAtts := make([]*ethpb.IndexedAttestation, len(attestations))
   127  		signingRoots := make([][32]byte, len(attestations))
   128  		for i, att := range attestations {
   129  			indexedAtt := createAttestation(att.Source, att.Target)
   130  			indexedAtts[i] = indexedAtt
   131  			signingRoots[i] = att.SigningRoot
   132  		}
   133  		if err := validatorDB.SaveAttestationsForPubKey(ctx, pubKey, signingRoots, indexedAtts); err != nil {
   134  			return errors.Wrap(err, "could not save attestations from imported JSON to database")
   135  		}
   136  	}
   137  	return nil
   138  }
   139  
   140  func validateMetadata(ctx context.Context, validatorDB db.Database, interchangeJSON *format.EIPSlashingProtectionFormat) error {
   141  	// We need to ensure the version in the metadata field matches the one we support.
   142  	version := interchangeJSON.Metadata.InterchangeFormatVersion
   143  	if version != format.InterchangeFormatVersion {
   144  		return fmt.Errorf(
   145  			"slashing protection JSON version '%s' is not supported, wanted '%s'",
   146  			version,
   147  			format.InterchangeFormatVersion,
   148  		)
   149  	}
   150  
   151  	// We need to verify the genesis validators root matches that of our chain data, otherwise
   152  	// the imported slashing protection JSON was created on a different chain.
   153  	gvr, err := RootFromHex(interchangeJSON.Metadata.GenesisValidatorsRoot)
   154  	if err != nil {
   155  		return fmt.Errorf("%#x is not a valid root: %w", interchangeJSON.Metadata.GenesisValidatorsRoot, err)
   156  	}
   157  	dbGvr, err := validatorDB.GenesisValidatorsRoot(ctx)
   158  	if err != nil {
   159  		return errors.Wrap(err, "could not retrieve genesis validator root to db")
   160  	}
   161  	if dbGvr == nil {
   162  		if err = validatorDB.SaveGenesisValidatorsRoot(ctx, gvr[:]); err != nil {
   163  			return errors.Wrap(err, "could not save genesis validator root to db")
   164  		}
   165  		return nil
   166  	}
   167  	if !bytes.Equal(dbGvr, gvr[:]) {
   168  		return errors.New("genesis validator root doesnt match the one that is stored in slashing protection db. " +
   169  			"Please make sure you import the protection data that is relevant to the chain you are on")
   170  	}
   171  	return nil
   172  }
   173  
   174  // We create a map of pubKey -> []*SignedBlock. Then, for each public key we observe,
   175  // we append to this map. This allows us to handle valid input JSON data such as:
   176  //
   177  // "0x2932232930: {
   178  //   SignedBlocks: [Slot: 5, Slot: 6, Slot: 7],
   179  //  },
   180  // "0x2932232930: {
   181  //   SignedBlocks: [Slot: 5, Slot: 10, Slot: 11],
   182  //  }
   183  //
   184  // Which should be properly parsed as:
   185  //
   186  // "0x2932232930: {
   187  //   SignedBlocks: [Slot: 5, Slot: 5, Slot: 6, Slot: 7, Slot: 10, Slot: 11],
   188  //  }
   189  func parseBlocksForUniquePublicKeys(data []*format.ProtectionData) (map[[48]byte][]*format.SignedBlock, error) {
   190  	signedBlocksByPubKey := make(map[[48]byte][]*format.SignedBlock)
   191  	for _, validatorData := range data {
   192  		pubKey, err := PubKeyFromHex(validatorData.Pubkey)
   193  		if err != nil {
   194  			return nil, fmt.Errorf("%s is not a valid public key: %w", validatorData.Pubkey, err)
   195  		}
   196  		for _, sBlock := range validatorData.SignedBlocks {
   197  			if sBlock == nil {
   198  				continue
   199  			}
   200  			signedBlocksByPubKey[pubKey] = append(signedBlocksByPubKey[pubKey], sBlock)
   201  		}
   202  	}
   203  	return signedBlocksByPubKey, nil
   204  }
   205  
   206  // We create a map of pubKey -> []*SignedAttestation. Then, for each public key we observe,
   207  // we append to this map. This allows us to handle valid input JSON data such as:
   208  //
   209  // "0x2932232930: {
   210  //   SignedAttestations: [{Source: 5, Target: 6}, {Source: 6, Target: 7}],
   211  //  },
   212  // "0x2932232930: {
   213  //   SignedAttestations: [{Source: 5, Target: 6}],
   214  //  }
   215  //
   216  // Which should be properly parsed as:
   217  //
   218  // "0x2932232930: {
   219  //   SignedAttestations: [{Source: 5, Target: 6}, {Source: 5, Target: 6}, {Source: 6, Target: 7}],
   220  //  }
   221  func parseAttestationsForUniquePublicKeys(data []*format.ProtectionData) (map[[48]byte][]*format.SignedAttestation, error) {
   222  	signedAttestationsByPubKey := make(map[[48]byte][]*format.SignedAttestation)
   223  	for _, validatorData := range data {
   224  		pubKey, err := PubKeyFromHex(validatorData.Pubkey)
   225  		if err != nil {
   226  			return nil, fmt.Errorf("%s is not a valid public key: %w", validatorData.Pubkey, err)
   227  		}
   228  		for _, sAtt := range validatorData.SignedAttestations {
   229  			if sAtt == nil {
   230  				continue
   231  			}
   232  			signedAttestationsByPubKey[pubKey] = append(signedAttestationsByPubKey[pubKey], sAtt)
   233  		}
   234  	}
   235  	return signedAttestationsByPubKey, nil
   236  }
   237  
   238  func filterSlashablePubKeysFromBlocks(ctx context.Context, historyByPubKey map[[48]byte]kv.ProposalHistoryForPubkey) [][48]byte {
   239  	// Given signing roots are optional in the EIP standard, we behave as follows:
   240  	// For a given block:
   241  	//   If we have a previous block with the same slot in our history:
   242  	//     If signing root is nil, we consider that proposer public key as slashable
   243  	//     If signing root is not nil , then we compare signing roots. If they are different,
   244  	//     then we consider that proposer public key as slashable.
   245  	slashablePubKeys := make([][48]byte, 0)
   246  	for pubKey, proposals := range historyByPubKey {
   247  		seenSigningRootsBySlot := make(map[types.Slot][]byte)
   248  		for _, blk := range proposals.Proposals {
   249  			if signingRoot, ok := seenSigningRootsBySlot[blk.Slot]; ok {
   250  				if signingRoot == nil || !bytes.Equal(signingRoot, blk.SigningRoot) {
   251  					slashablePubKeys = append(slashablePubKeys, pubKey)
   252  					break
   253  				}
   254  			}
   255  			seenSigningRootsBySlot[blk.Slot] = blk.SigningRoot
   256  		}
   257  	}
   258  	return slashablePubKeys
   259  }
   260  
   261  func filterSlashablePubKeysFromAttestations(
   262  	ctx context.Context,
   263  	validatorDB db.Database,
   264  	signedAttsByPubKey map[[48]byte][]*kv.AttestationRecord,
   265  ) ([][48]byte, error) {
   266  	slashablePubKeys := make([][48]byte, 0)
   267  	// First we need to find attestations that are slashable with respect to other
   268  	// attestations within the same JSON import.
   269  	for pubKey, signedAtts := range signedAttsByPubKey {
   270  		signingRootsByTarget := make(map[types.Epoch][32]byte)
   271  		targetEpochsBySource := make(map[types.Epoch][]types.Epoch)
   272  	Loop:
   273  		for _, att := range signedAtts {
   274  			// Check for double votes.
   275  			if sr, ok := signingRootsByTarget[att.Target]; ok {
   276  				if slashutil.SigningRootsDiffer(sr, att.SigningRoot) {
   277  					slashablePubKeys = append(slashablePubKeys, pubKey)
   278  					break Loop
   279  				}
   280  			}
   281  			// Check for surround voting.
   282  			for source, targets := range targetEpochsBySource {
   283  				for _, target := range targets {
   284  					a := createAttestation(source, target)
   285  					b := createAttestation(att.Source, att.Target)
   286  					if slashutil.IsSurround(a, b) || slashutil.IsSurround(b, a) {
   287  						slashablePubKeys = append(slashablePubKeys, pubKey)
   288  						break Loop
   289  					}
   290  				}
   291  			}
   292  			signingRootsByTarget[att.Target] = att.SigningRoot
   293  			targetEpochsBySource[att.Source] = append(targetEpochsBySource[att.Source], att.Target)
   294  		}
   295  	}
   296  	// Then, we need to find attestations that are slashable with respect to our database.
   297  	for pubKey, signedAtts := range signedAttsByPubKey {
   298  		for _, att := range signedAtts {
   299  			indexedAtt := createAttestation(att.Source, att.Target)
   300  			slashable, err := validatorDB.CheckSlashableAttestation(ctx, pubKey, att.SigningRoot, indexedAtt)
   301  			if err != nil {
   302  				return nil, err
   303  			}
   304  			// Malformed data should not prevent us from completing this function.
   305  			if slashable != kv.NotSlashable {
   306  				slashablePubKeys = append(slashablePubKeys, pubKey)
   307  				break
   308  			}
   309  		}
   310  	}
   311  	return slashablePubKeys, nil
   312  }
   313  
   314  func transformSignedBlocks(ctx context.Context, signedBlocks []*format.SignedBlock) (*kv.ProposalHistoryForPubkey, error) {
   315  	proposals := make([]kv.Proposal, len(signedBlocks))
   316  	for i, proposal := range signedBlocks {
   317  		slot, err := SlotFromString(proposal.Slot)
   318  		if err != nil {
   319  			return nil, fmt.Errorf("%d is not a valid slot: %w", slot, err)
   320  		}
   321  		var signingRoot [32]byte
   322  		// Signing roots are optional in the standard JSON file.
   323  		if proposal.SigningRoot != "" {
   324  			signingRoot, err = RootFromHex(proposal.SigningRoot)
   325  			if err != nil {
   326  				return nil, fmt.Errorf("%#x is not a valid root: %w", signingRoot, err)
   327  			}
   328  		}
   329  		proposals[i] = kv.Proposal{
   330  			Slot:        slot,
   331  			SigningRoot: signingRoot[:],
   332  		}
   333  	}
   334  	return &kv.ProposalHistoryForPubkey{
   335  		Proposals: proposals,
   336  	}, nil
   337  }
   338  
   339  func transformSignedAttestations(pubKey [48]byte, atts []*format.SignedAttestation) ([]*kv.AttestationRecord, error) {
   340  	historicalAtts := make([]*kv.AttestationRecord, 0)
   341  	for _, attestation := range atts {
   342  		target, err := EpochFromString(attestation.TargetEpoch)
   343  		if err != nil {
   344  			return nil, fmt.Errorf("%d is not a valid epoch: %w", target, err)
   345  		}
   346  		source, err := EpochFromString(attestation.SourceEpoch)
   347  		if err != nil {
   348  			return nil, fmt.Errorf("%d is not a valid epoch: %w", source, err)
   349  		}
   350  		var signingRoot [32]byte
   351  		// Signing roots are optional in the standard JSON file.
   352  		if attestation.SigningRoot != "" {
   353  			signingRoot, err = RootFromHex(attestation.SigningRoot)
   354  			if err != nil {
   355  				return nil, fmt.Errorf("%#x is not a valid root: %w", signingRoot, err)
   356  			}
   357  		}
   358  		historicalAtts = append(historicalAtts, &kv.AttestationRecord{
   359  			PubKey:      pubKey,
   360  			Source:      source,
   361  			Target:      target,
   362  			SigningRoot: signingRoot,
   363  		})
   364  	}
   365  	return historicalAtts, nil
   366  }
   367  
   368  func createAttestation(source, target types.Epoch) *ethpb.IndexedAttestation {
   369  	return &ethpb.IndexedAttestation{
   370  		Data: &ethpb.AttestationData{
   371  			Source: &ethpb.Checkpoint{
   372  				Epoch: source,
   373  			},
   374  			Target: &ethpb.Checkpoint{
   375  				Epoch: target,
   376  			},
   377  		},
   378  	}
   379  }