Skip to content

Commit

Permalink
avoid packing attestations from other forks (#4273)
Browse files Browse the repository at this point in the history
* avoid packing attestations from other forks

Revisit #3893 using method based on Lighthouse (less heavy computation).

* fix comment
  • Loading branch information
etan-status authored Nov 1, 2022
1 parent 3ef09ff commit aff9147
Show file tree
Hide file tree
Showing 2 changed files with 125 additions and 0 deletions.
30 changes: 30 additions & 0 deletions beacon_chain/consensus_object_pools/attestation_pool.nim
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,30 @@ func score(
# Not found in cache - fresh vote meaning all attestations count
bitsScore

proc check_attestation_compatible*(
dag: ChainDAGRef,
state: ForkyBeaconState,
attestation: SomeAttestation): Result[void, cstring] =
let targetEpoch = attestation.data.target.epoch
if targetEpoch <= MIN_SEED_LOOKAHEAD:
return ok()

let
attestedBlck =
dag.getBlockRef(attestation.data.beacon_block_root).valueOr:
return err("Unknown `beacon_block_root`")

dependentSlot = (targetEpoch - MIN_SEED_LOOKAHEAD).start_slot - 1
dependentBid = dag.atSlot(attestedBlck.bid, dependentSlot).valueOr:
return err("Dependent root not found")

dependentRoot = dependentBid.bid.root
compatibleRoot = state.get_block_root_at_slot(dependentSlot)
if dependentRoot != compatibleRoot:
return err("Incompatible shuffling")

ok()

proc getAttestationsForBlock*(pool: var AttestationPool,
state: ForkyHashedBeaconState,
cache: var StateCache): seq[Attestation] =
Expand Down Expand Up @@ -579,6 +603,12 @@ proc getAttestationsForBlock*(pool: var AttestationPool,
let
attestation = entry.toAttestation(entry.aggregates[j])

# Filter out attestations that were created with a different shuffling.
# As we don't re-check signatures, this needs to be done separately
if not check_attestation_compatible(
pool.dag, state.data, attestation).isOk():
continue

# Attestations are checked based on the state that we're adding the
# attestation to - there might have been a fork between when we first
# saw the attestation and the time that we added it
Expand Down
95 changes: 95 additions & 0 deletions tests/test_attestation_pool.nim
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,101 @@ suite "Attestation pool processing" & preset():
dag.cfg, state[], getStateField(state[], slot) + 1, cache, info,
{}).isOk()

test "Attestation from different branch" & preset():
# Create two alternate histories with different shufflings
check process_slots(
dag.cfg, state[], (SLOTS_PER_EPOCH - 2).Slot, cache, info, {}).isOk
var state2 = newClone(state[])

const epoch = 3.Epoch
template fillToEpoch(
state: ref ForkedHashedBeaconState, cache: var StateCache) =
while getStateField(state[], slot).epoch <= epoch:
check process_slots(
dag.cfg, state[], getStateField(state[], slot) + 1, cache, info,
{}).isOk
let
parent_root = withState(state[]): forkyState.latest_block_root
attestations = makeFullAttestations(
state[], parent_root, getStateField(state[], slot), cache)
blck = addTestBlock(
state[], cache, attestations = attestations, cfg = dag.cfg)
check dag.addHeadBlock(
verifier, blck.phase0Data, OnPhase0BlockAdded(nil)).isOk

# History 1 contains all odd blocks
state.fillToEpoch(cache)

# History 2 contains all even blocks
var cache2 = StateCache()
check process_slots(
dag.cfg, state2[], getStateField(state2[], slot) + 1, cache2, info,
{}).isOk
state2.fillToEpoch(cache2)

# The shuffling for epoch 3 among both chains should now be different
let
dependent_root1 = withState(state[]): forkyState.attester_dependent_root
dependent_root2 = withState(state2[]): forkyState.attester_dependent_root
check dependent_root1 != dependent_root2

# Fill pool with attestations from both chains
let
cIndex = 0.CommitteeIndex
att1 = block:
let
slot = getStateField(state[], slot)
parent_root = withState(state[]): forkyState.latest_block_root
committee = get_beacon_committee(state[], slot, cIndex, cache)
makeAttestation(state[], parent_root, committee[0], cache)
att2 = block:
let
slot = getStateField(state2[], slot)
parent_root = withState(state2[]): forkyState.latest_block_root
committee = get_beacon_committee(state2[], slot, cIndex, cache2)
makeAttestation(state2[], parent_root, committee[0], cache2)
maxSlot = max(att1.data.slot, att2.data.slot)

# Advance time so attestations become valid
check:
process_slots(
dag.cfg, state[], maxSlot + MIN_ATTESTATION_INCLUSION_DELAY,
cache, info, {}).isOk
process_slots(
dag.cfg, state2[], maxSlot + MIN_ATTESTATION_INCLUSION_DELAY,
cache2, info, {}).isOk

# They should remain valid only within a compatible state
withState(state[]):
check:
check_attestation(forkyState.data, att1, {}, cache).isOk
check_attestation(forkyState.data, att2, {}, cache).isErr
withState(state2[]):
check:
check_attestation(forkyState.data, att1, {}, cache2).isErr
check_attestation(forkyState.data, att2, {}, cache2).isOk

# If signature checks are skipped, state incompatibility is not detected
let flags = {skipBlsValidation}
withState(state[]):
check:
check_attestation(forkyState.data, att1, flags, cache).isOk
check_attestation(forkyState.data, att2, flags, cache).isOk
withState(state2[]):
check:
check_attestation(forkyState.data, att1, flags, cache2).isOk
check_attestation(forkyState.data, att2, flags, cache2).isOk

# An additional compatibility check catches that (used in block production)
withState(state[]):
check:
check_attestation_compatible(dag, forkyState.data, att1).isOk
check_attestation_compatible(dag, forkyState.data, att2).isErr
withState(state2[]):
check:
check_attestation_compatible(dag, forkyState.data, att1).isErr
check_attestation_compatible(dag, forkyState.data, att2).isOk

test "Can add and retrieve simple attestations" & preset():
let
# Create an attestation for slot 1!
Expand Down

0 comments on commit aff9147

Please sign in to comment.