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 ðpb.IndexedAttestation{ 370 Data: ðpb.AttestationData{ 371 Source: ðpb.Checkpoint{ 372 Epoch: source, 373 }, 374 Target: ðpb.Checkpoint{ 375 Epoch: target, 376 }, 377 }, 378 } 379 }