From ed2d8e2ce1806e8050a2072b1ab59a36d9d039f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Andr=C3=B3il?= Date: Tue, 22 Jul 2025 13:12:09 +0200 Subject: [PATCH 1/5] Update slashing_protection_common.nim --- .../validators/slashing_protection_common.nim | 79 ++++++------------- 1 file changed, 22 insertions(+), 57 deletions(-) diff --git a/beacon_chain/validators/slashing_protection_common.nim b/beacon_chain/validators/slashing_protection_common.nim index 665429ff3b..b512c01c3f 100644 --- a/beacon_chain/validators/slashing_protection_common.nim +++ b/beacon_chain/validators/slashing_protection_common.nim @@ -9,7 +9,7 @@ import # Stdlib - std/[typetraits, strutils, algorithm], + std/[typetraits, strutils], # Status stew/byteutils, results, @@ -305,15 +305,7 @@ proc importInterchangeV5Impl*( continue key.get() - # TODO: with minification sorting is unnecessary, cleanup - # Sort by ascending minimum slot so that we don't trigger MinSlotViolation - spdir.data[v].signed_blocks.sort do (a, b: SPDIR_SignedBlock) -> int: - result = cmp(a.slot.int, b.slot.int) - - spdir.data[v].signed_attestations.sort do (a, b: SPDIR_SignedAttestation) -> int: - result = cmp(a.source_epoch.int, b.source_epoch.int) - if result == 0: # Same epoch - result = cmp(a.target_epoch.int, b.target_epoch.int) + # --- Replace the original sorting + selection logic --- const ZeroDigest = Eth2Digest() @@ -321,67 +313,46 @@ proc importInterchangeV5Impl*( # Blocks # --------------------------------------------------- - # After import we need to prune the DB from everything - # besides the last imported block slot. - # This ensures that even if 2 slashing DB are imported in the wrong order - # (the last before the earliest) the minSlotViolation check stays consistent. var maxValidSlotSeen = -1 if dbSlot.isSome(): maxValidSlotSeen = int dbSlot.get() - if spdir.data[v].signed_blocks.len >= 1: - # Minification, to limit SQLite IO we only import the last block after sorting - template B: untyped = spdir.data[v].signed_blocks[^1] + if spdir.data[v].signed_blocks.len > 0: + # Efficient: Find the block with the highest slot without sorting + var latestBlock = spdir.data[v].signed_blocks[0] + for b in spdir.data[v].signed_blocks: + if b.slot.int > latestBlock.slot.int: + latestBlock = b + let signing_root = - if B.signing_root.isSome: - B.signing_root.get.Eth2Digest + if latestBlock.signing_root.isSome: + latestBlock.signing_root.get.Eth2Digest else: - # https://eips.ethereum.org/EIPS/eip-3076#advice-for-complete-databases - # "If your database records the signing roots of messages in - # addition to their slot/epochs, you should ensure that imported - # messages without signing roots are assigned a suitable dummy - # signing root internally. We suggest using a special "null" value - # which is distinct from all other signing roots, although a value - # like 0x0 may be used instead (as it is extremely unlikely to - # collide with any real signing root)." ZeroDigest - status = db.registerBlock(parsedKey, B.slot.Slot, signing_root) + status = db.registerBlock(parsedKey, latestBlock.slot.Slot, signing_root) + if status.isErr(): - # We might be importing a duplicate which EIP-3076 allows - # there is no reason during normal operation to integrate - # a duplicate so checkSlashableBlockProposal would have rejected it. - # We special-case that for imports. - # Note: rule 2 mentions repeat signing in the MinSlotViolation case - # having 2 blocks with the same signing root and different slots - # would break the blockchain so we only check for exact slot. if status.error.kind == DoubleProposal and signing_root != ZeroDigest and status.error.existingBlock == signing_root: warn "Block already exists in the DB", pubkey = spdir.data[v].pubkey.PubKeyBytes.toHex(), - candidateBlock = B + candidateBlock = latestBlock else: error "Slashable block. Skipping its import.", pubkey = spdir.data[v].pubkey.PubKeyBytes.toHex(), - candidateBlock = B, + candidateBlock = latestBlock, conflict = status.error() result = siPartial - if B.slot.int > maxValidSlotSeen: - maxValidSlotSeen = int B.slot + if latestBlock.slot.int > maxValidSlotSeen: + maxValidSlotSeen = int latestBlock.slot - # Now prune everything that predates - # this DB or interchange file max slot - # Even if the block is not imported, pruning will keep the latest one. db.pruneBlocks(parsedKey, Slot maxValidSlotSeen) # Attestations # --------------------------------------------------- - # After import we need to prune the DB from everything - # besides the last imported attestation source and target epochs. - # This ensures that even if 2 slashing DB are imported in the wrong order - # (the last before the earliest) the minEpochViolation check stays consistent. var maxValidSourceEpochSeen = -1 var maxValidTargetEpochSeen = -1 @@ -390,14 +361,11 @@ proc importInterchangeV5Impl*( if dbTarget.isSome(): maxValidTargetEpochSeen = int dbTarget.get() - # We do a first pass over the data to find the max source/target seen - for a in 0 ..< spdir.data[v].signed_attestations.len: - template A: untyped = spdir.data[v].signed_attestations[a] - - if A.source_epoch.int > maxValidSourceEpochSeen: - maxValidSourceEpochSeen = A.source_epoch.int - if A.target_epoch.int > maxValidTargetEpochSeen: - maxValidTargetEpochSeen = A.target_epoch.int + for a in spdir.data[v].signed_attestations: + if a.source_epoch.int > maxValidSourceEpochSeen: + maxValidSourceEpochSeen = a.source_epoch.int + if a.target_epoch.int > maxValidTargetEpochSeen: + maxValidTargetEpochSeen = a.target_epoch.int if maxValidSourceEpochSeen < 0 or maxValidTargetEpochSeen < 0: doAssert maxValidSourceEpochSeen == -1 and maxValidTargetEpochSeen == -1 @@ -405,11 +373,8 @@ proc importInterchangeV5Impl*( pubkey = spdir.data[v].pubkey.PubKeyBytes.toHex() continue - # See formal proof https://github.com/michaelsproul/slashing-proofs - # of synthetic attestation if not(maxValidSourceEpochSeen < maxValidTargetEpochSeen) and not(maxValidSourceEpochSeen == 0 and maxValidTargetEpochSeen == 0): - # Special-case genesis (Slashing prot is deactivated anyway) warn "Invalid attestation(s), source epochs should be less than target epochs, skipping import", pubkey = spdir.data[v].pubkey.PubKeyBytes.toHex(), maxValidSourceEpochSeen = maxValidSourceEpochSeen, From 1bdd6d61a81751ea98de2488d5bff26a8f303059 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Andr=C3=B3il?= Date: Mon, 25 Aug 2025 11:08:45 +0200 Subject: [PATCH 2/5] Update slashing_protection_common.nim --- .../validators/slashing_protection_common.nim | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/beacon_chain/validators/slashing_protection_common.nim b/beacon_chain/validators/slashing_protection_common.nim index b512c01c3f..e703d79355 100644 --- a/beacon_chain/validators/slashing_protection_common.nim +++ b/beacon_chain/validators/slashing_protection_common.nim @@ -305,8 +305,6 @@ proc importInterchangeV5Impl*( continue key.get() - # --- Replace the original sorting + selection logic --- - const ZeroDigest = Eth2Digest() let (dbSlot, dbSource, dbTarget) = db.retrieveLatestValidatorData(parsedKey) @@ -329,6 +327,7 @@ proc importInterchangeV5Impl*( if latestBlock.signing_root.isSome: latestBlock.signing_root.get.Eth2Digest else: + # https://eips.ethereum.org/EIPS/eip-3076#advice-for-complete-databases ZeroDigest status = db.registerBlock(parsedKey, latestBlock.slot.Slot, signing_root) @@ -349,10 +348,17 @@ proc importInterchangeV5Impl*( if latestBlock.slot.int > maxValidSlotSeen: maxValidSlotSeen = int latestBlock.slot + # Now prune everything that predates + # this DB or interchange file max slot + # Even if the block is not imported, pruning will keep the latest one. db.pruneBlocks(parsedKey, Slot maxValidSlotSeen) # Attestations # --------------------------------------------------- + # After import we need to prune the DB from everything + # besides the last imported attestation source and target epochs. + # This ensures that even if 2 slashing DB are imported in the wrong order + # (the last before the earliest) the minEpochViolation check stays consistent. var maxValidSourceEpochSeen = -1 var maxValidTargetEpochSeen = -1 @@ -373,6 +379,8 @@ proc importInterchangeV5Impl*( pubkey = spdir.data[v].pubkey.PubKeyBytes.toHex() continue + # See formal proof https://github.com/michaelsproul/slashing-proofs + # of synthetic attestation if not(maxValidSourceEpochSeen < maxValidTargetEpochSeen) and not(maxValidSourceEpochSeen == 0 and maxValidTargetEpochSeen == 0): warn "Invalid attestation(s), source epochs should be less than target epochs, skipping import", From d92ab1412c267935f41f0d600594b30df5f5a6d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Andr=C3=B3il?= Date: Mon, 25 Aug 2025 11:11:56 +0200 Subject: [PATCH 3/5] Update slashing_protection_common.nim --- beacon_chain/validators/slashing_protection_common.nim | 1 + 1 file changed, 1 insertion(+) diff --git a/beacon_chain/validators/slashing_protection_common.nim b/beacon_chain/validators/slashing_protection_common.nim index e703d79355..2df3a16c6a 100644 --- a/beacon_chain/validators/slashing_protection_common.nim +++ b/beacon_chain/validators/slashing_protection_common.nim @@ -383,6 +383,7 @@ proc importInterchangeV5Impl*( # of synthetic attestation if not(maxValidSourceEpochSeen < maxValidTargetEpochSeen) and not(maxValidSourceEpochSeen == 0 and maxValidTargetEpochSeen == 0): + # Special-case genesis (Slashing prot is deactivated anyway) warn "Invalid attestation(s), source epochs should be less than target epochs, skipping import", pubkey = spdir.data[v].pubkey.PubKeyBytes.toHex(), maxValidSourceEpochSeen = maxValidSourceEpochSeen, From 0798d30865dacda6c60ec5b6ae706943af290c97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Andr=C3=B3il?= Date: Mon, 25 Aug 2025 11:15:48 +0200 Subject: [PATCH 4/5] Update slashing_protection_common.nim --- beacon_chain/validators/slashing_protection_common.nim | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/beacon_chain/validators/slashing_protection_common.nim b/beacon_chain/validators/slashing_protection_common.nim index 2df3a16c6a..7421d45c27 100644 --- a/beacon_chain/validators/slashing_protection_common.nim +++ b/beacon_chain/validators/slashing_protection_common.nim @@ -272,6 +272,7 @@ chronicles.formatIt EpochString: it.Slot.shortLog chronicles.formatIt Eth2Digest0x: it.Eth2Digest.shortLog chronicles.formatIt SPDIR_SignedBlock: it.shortLog chronicles.formatIt SPDIR_SignedAttestation: it.shortLog +chronicles.formatIt PubKey0x: "0x" & it.PubKeyBytes.toHex # Interchange import # -------------------------------------------- @@ -331,6 +332,12 @@ proc importInterchangeV5Impl*( ZeroDigest status = db.registerBlock(parsedKey, latestBlock.slot.Slot, signing_root) + # We might be importing a duplicate which EIP-3076 allows: + # there is no reason during normal operation to integrate a duplicate + # (checkSlashableBlockProposal would have rejected it), but we special-case that for imports. + # Note: rule 2 mentions repeat signing in the MinSlotViolation case; having 2 blocks + # with the same signing root and different slots would break the chain, so we only + # check for exact slot here. if status.isErr(): if status.error.kind == DoubleProposal and signing_root != ZeroDigest and From ad07c16f0b8eacea6a55ff37032c93623ca2e477 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Andr=C3=B3il?= Date: Mon, 25 Aug 2025 11:19:28 +0200 Subject: [PATCH 5/5] Update slashing_protection_common.nim --- beacon_chain/validators/slashing_protection_common.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beacon_chain/validators/slashing_protection_common.nim b/beacon_chain/validators/slashing_protection_common.nim index 7421d45c27..a10a2ea974 100644 --- a/beacon_chain/validators/slashing_protection_common.nim +++ b/beacon_chain/validators/slashing_protection_common.nim @@ -272,7 +272,7 @@ chronicles.formatIt EpochString: it.Slot.shortLog chronicles.formatIt Eth2Digest0x: it.Eth2Digest.shortLog chronicles.formatIt SPDIR_SignedBlock: it.shortLog chronicles.formatIt SPDIR_SignedAttestation: it.shortLog -chronicles.formatIt PubKey0x: "0x" & it.PubKeyBytes.toHex +chronicles.formatIt PubKey0x: it.PubKeyBytes.to0xHex # Interchange import # --------------------------------------------