# beacon_chain # Copyright (c) 2018-2024 Status Research & Development GmbH # Licensed and distributed under either of # * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). # * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). # at your option. This file may not be copied, modified, or distributed except according to those terms. {.push raises: [].} {.used.} import # Status lib unittest2, chronicles, chronos, stew/[byteutils, endians2], taskpools, # Internal ../beacon_chain/gossip_processing/[gossip_validation], ../beacon_chain/fork_choice/[fork_choice_types, fork_choice], ../beacon_chain/consensus_object_pools/[ block_quarantine, blockchain_dag, block_clearance, attestation_pool], ../beacon_chain/spec/[beaconstate, helpers, state_transition, validator], ../beacon_chain/beacon_clock, # Test utilities ./testutil, ./testdbutil, ./testblockutil, ./consensus_spec/fixtures_utils from std/sequtils import mapIt, toSeq from ./testbcutil import addHeadBlock func combine(tgt: var (phase0.Attestation | electra.Attestation), src: phase0.Attestation | electra.Attestation) = ## Combine the signature and participation bitfield, with the assumption that ## the same data is being signed - if the signatures overlap, they are not ## combined. doAssert tgt.data == src.data # In a BLS aggregate signature, one needs to count how many times a # particular public key has been added - since we use a single bit per key, we # can only it once, thus we can never combine signatures that overlap already! doAssert not tgt.aggregation_bits.overlaps(src.aggregation_bits) tgt.aggregation_bits.incl(src.aggregation_bits) var agg {.noinit.}: AggregateSignature agg.init(tgt.signature.load().get()) agg.aggregate(src.signature.load.get()) tgt.signature = agg.finish().toValidatorSig() func loadSig(a: phase0.Attestation): CookedSig = a.signature.load.get() func loadSig(a: electra.Attestation): CookedSig = a.signature.load.get() proc pruneAtFinalization(dag: ChainDAGRef, attPool: AttestationPool) = if dag.needStateCachesAndForkChoicePruning(): dag.pruneStateCachesDAG() # pool[].prune() # We test logic without attestation pool / fork choice pruning suite "Attestation pool processing" & preset(): ## For now just test that we can compile and execute block processing with ## mock data. setup: # Genesis state that results in 6 members per committee let rng = HmacDrbgContext.new() var validatorMonitor = newClone(ValidatorMonitor.init()) dag = init( ChainDAGRef, defaultRuntimeConfig, makeTestDB(SLOTS_PER_EPOCH * 6), validatorMonitor, {}) taskpool = Taskpool.new() verifier = BatchVerifier.init(rng, taskpool) quarantine = newClone(Quarantine.init()) pool = newClone(AttestationPool.init(dag, quarantine)) state = newClone(dag.headState) cache = StateCache() info = ForkedEpochInfo() # Slot 0 is a finalized slot - won't be making attestations for it.. check: process_slots( 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, att1).isOk check_attestation_compatible(dag, forkyState, att2).isErr withState(state2[]): check: check_attestation_compatible(dag, forkyState, att1).isErr check_attestation_compatible(dag, forkyState, att2).isOk test "Can add and retrieve simple attestations" & preset(): let # Create an attestation for slot 1! bc0 = get_beacon_committee( state[], getStateField(state[], slot), 0.CommitteeIndex, cache) attestation = makeAttestation( state[], state[].latest_block_root, bc0[0], cache) pool[].addAttestation( attestation, @[bc0[0]], attestation.loadSig, attestation.data.slot.start_beacon_time) check: # Added attestation, should get it back toSeq(pool[].attestations(Opt.none(Slot), Opt.none(CommitteeIndex))) == @[attestation] toSeq(pool[].attestations( Opt.some(attestation.data.slot), Opt.none(CommitteeIndex))) == @[attestation] toSeq(pool[].attestations( Opt.some(attestation.data.slot), Opt.some(attestation.data.index.CommitteeIndex))) == @[attestation] toSeq(pool[].attestations(Opt.none(Slot), Opt.some(attestation.data.index.CommitteeIndex))) == @[attestation] toSeq(pool[].attestations(Opt.some( attestation.data.slot + 1), Opt.none(CommitteeIndex))) == [] toSeq(pool[].attestations( Opt.none(Slot), Opt.some(CommitteeIndex(attestation.data.index + 1)))) == [] process_slots( defaultRuntimeConfig, state[], getStateField(state[], slot) + MIN_ATTESTATION_INCLUSION_DELAY, cache, info, {}).isOk() let attestations = pool[].getAttestationsForBlock(state[], cache) check: attestations.len == 1 pool[].getPhase0AggregatedAttestation(1.Slot, 0.CommitteeIndex).isSome() let root1 = addTestBlock( state[], cache, attestations = attestations, nextSlot = false).phase0Data.root bc1 = get_beacon_committee( state[], getStateField(state[], slot), 0.CommitteeIndex, cache) att1 = makeAttestation(state[], root1, bc1[0], cache) check: withState(state[]): forkyState.latest_block_root == root1 process_slots( defaultRuntimeConfig, state[], getStateField(state[], slot) + MIN_ATTESTATION_INCLUSION_DELAY, cache, info, {}).isOk() withState(state[]): forkyState.latest_block_root == root1 check: # shouldn't include already-included attestations pool[].getAttestationsForBlock(state[], cache) == [] pool[].addAttestation( att1, @[bc1[0]], att1.loadSig, att1.data.slot.start_beacon_time) check: # but new ones should go in pool[].getAttestationsForBlock(state[], cache).len() == 1 let att2 = makeAttestation(state[], root1, bc1[1], cache) pool[].addAttestation( att2, @[bc1[1]], att2.loadSig, att2.data.slot.start_beacon_time) let combined = pool[].getAttestationsForBlock(state[], cache) check: # New attestations should be combined with old attestations combined.len() == 1 combined[0].aggregation_bits.countOnes() == 2 pool[].addAttestation( combined[0], @[bc1[1], bc1[0]], combined[0].loadSig, combined[0].data.slot.start_beacon_time) check: # readding the combined attestation shouldn't have an effect pool[].getAttestationsForBlock(state[], cache).len() == 1 let # Someone votes for a different root att3 = makeAttestation(state[], ZERO_HASH, bc1[2], cache) pool[].addAttestation( att3, @[bc1[2]], att3.loadSig, att3.data.slot.start_beacon_time) check: # We should now get both attestations for the block, but the aggregate # should be the one with the most votes pool[].getAttestationsForBlock(state[], cache).len() == 2 pool[].getPhase0AggregatedAttestation(2.Slot, 0.CommitteeIndex). get().aggregation_bits.countOnes() == 2 pool[].getPhase0AggregatedAttestation(2.Slot, hash_tree_root(att2.data)). get().aggregation_bits.countOnes() == 2 let # Someone votes for a different root att4 = makeAttestation(state[], ZERO_HASH, bc1[2], cache) pool[].addAttestation( att4, @[bc1[2]], att3.loadSig, att3.data.slot.start_beacon_time) test "Working with aggregates" & preset(): let # Create an attestation for slot 1! bc0 = get_beacon_committee( state[], getStateField(state[], slot), 0.CommitteeIndex, cache) var att0 = makeAttestation( state[], state[].latest_block_root, bc0[0], cache) att0x = att0 att1 = makeAttestation( state[], state[].latest_block_root, bc0[1], cache) att2 = makeAttestation( state[], state[].latest_block_root, bc0[2], cache) att3 = makeAttestation( state[], state[].latest_block_root, bc0[3], cache) # Both attestations include member 2 but neither is a subset of the other att0.combine(att2) att1.combine(att2) check: not pool[].covers(att0.data, att0.aggregation_bits) pool[].addAttestation( att0, @[bc0[0], bc0[2]], att0.loadSig, att0.data.slot.start_beacon_time) pool[].addAttestation( att1, @[bc0[1], bc0[2]], att1.loadSig, att1.data.slot.start_beacon_time) check: process_slots( defaultRuntimeConfig, state[], getStateField(state[], slot) + MIN_ATTESTATION_INCLUSION_DELAY, cache, info, {}).isOk() check: pool[].covers(att0.data, att0.aggregation_bits) pool[].getAttestationsForBlock(state[], cache).len() == 2 # Can get either aggregate here, random! pool[].getPhase0AggregatedAttestation(1.Slot, 0.CommitteeIndex).isSome() # Add in attestation 3 - both aggregates should now have it added pool[].addAttestation( att3, @[bc0[3]], att3.loadSig, att3.data.slot.start_beacon_time) block: let attestations = pool[].getAttestationsForBlock(state[], cache) check: attestations.len() == 2 attestations[0].aggregation_bits.countOnes() == 3 # Can get either aggregate here, random! pool[].getPhase0AggregatedAttestation(1.Slot, 0.CommitteeIndex).isSome() # Add in attestation 0 as single - attestation 1 is now a superset of the # aggregates in the pool, so everything else should be removed pool[].addAttestation( att0x, @[bc0[0]], att0x.loadSig, att0x.data.slot.start_beacon_time) block: let attestations = pool[].getAttestationsForBlock(state[], cache) check: attestations.len() == 1 attestations[0].aggregation_bits.countOnes() == 4 pool[].getPhase0AggregatedAttestation(1.Slot, 0.CommitteeIndex).isSome() test "Everyone voting for something different" & preset(): var attestations: int for i in 0.. MAX_ATTESTATIONS, "6*SLOTS_PER_EPOCH validators > 128 mainnet MAX_ATTESTATIONS" check: # Fill block with attestations pool[].getAttestationsForBlock(state[], cache).lenu64() == MAX_ATTESTATIONS pool[].getPhase0AggregatedAttestation( getStateField(state[], slot) - 1, 0.CommitteeIndex).isSome() test "Attestations may arrive in any order" & preset(): var cache = StateCache() let # Create an attestation for slot 1! bc0 = get_beacon_committee( state[], getStateField(state[], slot), 0.CommitteeIndex, cache) attestation0 = makeAttestation( state[], state[].latest_block_root, bc0[0], cache) check: process_slots( defaultRuntimeConfig, state[], getStateField(state[], slot) + 1, cache, info, {}).isOk() let bc1 = get_beacon_committee(state[], getStateField(state[], slot), 0.CommitteeIndex, cache) attestation1 = makeAttestation( state[], state[].latest_block_root, bc1[0], cache) # test reverse order pool[].addAttestation( attestation1, @[bc1[0]], attestation1.loadSig, attestation1.data.slot.start_beacon_time) pool[].addAttestation( attestation0, @[bc0[0]], attestation0.loadSig, attestation0.data.slot.start_beacon_time) let attestations = pool[].getAttestationsForBlock(state[], cache) check: attestations.len == 1 test "Attestations should be combined" & preset(): var cache = StateCache() let # Create an attestation for slot 1! bc0 = get_beacon_committee( state[], getStateField(state[], slot), 0.CommitteeIndex, cache) attestation0 = makeAttestation(state[], state[].latest_block_root, bc0[0], cache) attestation1 = makeAttestation(state[], state[].latest_block_root, bc0[1], cache) pool[].addAttestation( attestation0, @[bc0[0]], attestation0.loadSig, attestation0.data.slot.start_beacon_time) pool[].addAttestation( attestation1, @[bc0[1]], attestation1.loadSig, attestation1.data.slot.start_beacon_time) check: process_slots( defaultRuntimeConfig, state[], MIN_ATTESTATION_INCLUSION_DELAY.Slot + 1, cache, info, {}).isOk() let attestations = pool[].getAttestationsForBlock(state[], cache) check: attestations.len == 1 test "Attestations may overlap, bigger first" & preset(): var cache = StateCache() var # Create an attestation for slot 1! bc0 = get_beacon_committee( state[], getStateField(state[], slot), 0.CommitteeIndex, cache) attestation0 = makeAttestation( state[], state[].latest_block_root, bc0[0], cache) attestation1 = makeAttestation( state[], state[].latest_block_root, bc0[1], cache) attestation0.combine(attestation1) pool[].addAttestation( attestation0, @[bc0[0]], attestation0.loadSig, attestation0.data.slot.start_beacon_time) pool[].addAttestation( attestation1, @[bc0[1]], attestation1.loadSig, attestation1.data.slot.start_beacon_time) check: process_slots( defaultRuntimeConfig, state[], MIN_ATTESTATION_INCLUSION_DELAY.Slot + 1, cache, info, {}).isOk() let attestations = pool[].getAttestationsForBlock(state[], cache) check: attestations.len == 1 test "Attestations may overlap, smaller first" & preset(): var cache = StateCache() var # Create an attestation for slot 1! bc0 = get_beacon_committee(state[], getStateField(state[], slot), 0.CommitteeIndex, cache) attestation0 = makeAttestation( state[], state[].latest_block_root, bc0[0], cache) attestation1 = makeAttestation( state[], state[].latest_block_root, bc0[1], cache) attestation0.combine(attestation1) pool[].addAttestation( attestation1, @[bc0[1]], attestation1.loadSig, attestation1.data.slot.start_beacon_time) pool[].addAttestation( attestation0, @[bc0[0]], attestation0.loadSig, attestation0.data.slot.start_beacon_time) check: process_slots( defaultRuntimeConfig, state[], MIN_ATTESTATION_INCLUSION_DELAY.Slot + 1, cache, info, {}).isOk() let attestations = pool[].getAttestationsForBlock(state[], cache) check: attestations.len == 1 test "Fork choice returns latest block with no attestations": var cache = StateCache() let b1 = addTestBlock(state[], cache).phase0Data b1Add = dag.addHeadBlock(verifier, b1) do ( blckRef: BlockRef, signedBlock: phase0.TrustedSignedBeaconBlock, epochRef: EpochRef, unrealized: FinalityCheckpoints): # Callback add to fork choice if valid pool[].addForkChoice( epochRef, blckRef, unrealized, signedBlock.message, blckRef.slot.start_beacon_time) let head = pool[].selectOptimisticHead(b1Add[].slot.start_beacon_time).get().blck check: head == b1Add[] let b2 = addTestBlock(state[], cache).phase0Data b2Add = dag.addHeadBlock(verifier, b2) do ( blckRef: BlockRef, signedBlock: phase0.TrustedSignedBeaconBlock, epochRef: EpochRef, unrealized: FinalityCheckpoints): # Callback add to fork choice if valid pool[].addForkChoice( epochRef, blckRef, unrealized, signedBlock.message, blckRef.slot.start_beacon_time) let head2 = pool[].selectOptimisticHead(b2Add[].slot.start_beacon_time).get().blck check: head2 == b2Add[] test "Fork choice returns block with attestation": var cache = StateCache() let b10 = makeTestBlock(state[], cache).phase0Data b10Add = dag.addHeadBlock(verifier, b10) do ( blckRef: BlockRef, signedBlock: phase0.TrustedSignedBeaconBlock, epochRef: EpochRef, unrealized: FinalityCheckpoints): # Callback add to fork choice if valid pool[].addForkChoice( epochRef, blckRef, unrealized, signedBlock.message, blckRef.slot.start_beacon_time) let head = pool[].selectOptimisticHead(b10Add[].slot.start_beacon_time).get().blck check: head == b10Add[] # Add a block too late to be timely enough to be proposer-boosted, which # would otherwise cause it to be selected as head let b11 = makeTestBlock(state[], cache, graffiti = GraffitiBytes [1'u8, 0, 0, 0 ,0 ,0 ,0 ,0 ,0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] ).phase0Data b11Add = dag.addHeadBlock(verifier, b11) do ( blckRef: BlockRef, signedBlock: phase0.TrustedSignedBeaconBlock, epochRef: EpochRef, unrealized: FinalityCheckpoints): # Callback add to fork choice if valid pool[].addForkChoice( epochRef, blckRef, unrealized, signedBlock.message, blckRef.slot.start_beacon_time + SECONDS_PER_SLOT.int64.seconds) bc1 = get_beacon_committee( state[], getStateField(state[], slot) - 1, 1.CommitteeIndex, cache) attestation0 = makeAttestation(state[], b10.root, bc1[0], cache) pool[].addAttestation( attestation0, @[bc1[0]], attestation0.loadSig, attestation0.data.slot.start_beacon_time) let head2 = pool[].selectOptimisticHead(b10Add[].slot.start_beacon_time).get().blck check: # Single vote for b10 and no votes for b11 head2 == b10Add[] let attestation1 = makeAttestation(state[], b11.root, bc1[1], cache) attestation2 = makeAttestation(state[], b11.root, bc1[2], cache) pool[].addAttestation( attestation1, @[bc1[1]], attestation1.loadSig, attestation1.data.slot.start_beacon_time) let head3 = pool[].selectOptimisticHead(b10Add[].slot.start_beacon_time).get().blck let bigger = if b11.root.data < b10.root.data: b10Add else: b11Add check: # Ties broken lexicographically in spec -> ? head3 == bigger[] pool[].addAttestation( attestation2, @[bc1[2]], attestation2.loadSig, attestation2.data.slot.start_beacon_time) let head4 = pool[].selectOptimisticHead(b11Add[].slot.start_beacon_time).get().blck check: # Two votes for b11 head4 == b11Add[] test "Trying to add a block twice tags the second as an error": var cache = StateCache() let b10 = makeTestBlock(state[], cache).phase0Data b10Add = dag.addHeadBlock(verifier, b10) do ( blckRef: BlockRef, signedBlock: phase0.TrustedSignedBeaconBlock, epochRef: EpochRef, unrealized: FinalityCheckpoints): # Callback add to fork choice if valid pool[].addForkChoice( epochRef, blckRef, unrealized, signedBlock.message, blckRef.slot.start_beacon_time) let head = pool[].selectOptimisticHead(b10Add[].slot.start_beacon_time).get().blck check: head == b10Add[] # ------------------------------------------------------------- # Add back the old block to ensure we have a duplicate error let b10_clone = b10 # Assumes deep copy let b10Add_clone = dag.addHeadBlock(verifier, b10_clone) do ( blckRef: BlockRef, signedBlock: phase0.TrustedSignedBeaconBlock, epochRef: EpochRef, unrealized: FinalityCheckpoints): # Callback add to fork choice if valid pool[].addForkChoice( epochRef, blckRef, unrealized, signedBlock.message, blckRef.slot.start_beacon_time) doAssert: b10Add_clone.error == VerifierError.Duplicate test "Trying to add a duplicate block from an old pruned epoch is tagged as an error": # Note: very sensitive to stack usage dag.updateFlags.incl {skipBlsValidation} var cache = StateCache() let b10 = addTestBlock(state[], cache).phase0Data b10Add = dag.addHeadBlock(verifier, b10) do ( blckRef: BlockRef, signedBlock: phase0.TrustedSignedBeaconBlock, epochRef: EpochRef, unrealized: FinalityCheckpoints): # Callback add to fork choice if valid pool[].addForkChoice( epochRef, blckRef, unrealized, signedBlock.message, blckRef.slot.start_beacon_time) let head = pool[].selectOptimisticHead(b10Add[].slot.start_beacon_time).get().blck doAssert: head == b10Add[] # ------------------------------------------------------------- let b10_clone = b10 # Assumes deep copy # ------------------------------------------------------------- # Pass an epoch var attestations: seq[phase0.Attestation] for epoch in 0 ..< 5: let start_slot = start_slot(Epoch epoch) let committees_per_slot = get_committee_count_per_slot(state[], Epoch epoch, cache) for slot in start_slot ..< start_slot + SLOTS_PER_EPOCH: let new_block = addTestBlock( state[], cache, attestations = attestations).phase0Data let blockRef = dag.addHeadBlock(verifier, new_block) do ( blckRef: BlockRef, signedBlock: phase0.TrustedSignedBeaconBlock, epochRef: EpochRef, unrealized: FinalityCheckpoints): # Callback add to fork choice if valid pool[].addForkChoice( epochRef, blckRef, unrealized, signedBlock.message, blckRef.slot.start_beacon_time) let head = pool[].selectOptimisticHead( blockRef[].slot.start_beacon_time).get().blck doAssert: head == blockRef[] dag.updateHead(head, quarantine[], []) pruneAtFinalization(dag, pool[]) attestations.setLen(0) for committee_index in get_committee_indices(committees_per_slot): let committee = get_beacon_committee( state[], getStateField(state[], slot), committee_index, cache) # Create a bitfield filled with the given count per attestation, # exactly on the right-most part of the committee field. var aggregation_bits = init(CommitteeValidatorsBits, committee.len) for v in 0 ..< committee.len * 2 div 3 + 1: aggregation_bits[v] = true attestations.add phase0.Attestation( aggregation_bits: aggregation_bits, data: makeAttestationData(state[], getStateField(state[], slot), committee_index, blockRef.get().root) # signature: ValidatorSig() ) cache = StateCache() # ------------------------------------------------------------- # Prune doAssert: dag.finalizedHead.slot != 0 pool[].prune() doAssert: b10.root notin pool.forkChoice.backend # Add back the old block to ensure we have a duplicate error let b10Add_clone = dag.addHeadBlock(verifier, b10_clone) do ( blckRef: BlockRef, signedBlock: phase0.TrustedSignedBeaconBlock, epochRef: EpochRef, unrealized: FinalityCheckpoints): # Callback add to fork choice if valid pool[].addForkChoice( epochRef, blckRef, unrealized, signedBlock.message, blckRef.slot.start_beacon_time) doAssert: b10Add_clone.error == VerifierError.Duplicate suite "Attestation pool electra processing" & preset(): ## For now just test that we can compile and execute block processing with ## mock data. setup: # Genesis state that results in 6 members per committee (2 committees total) const TOTAL_COMMITTEES = 2 let rng = HmacDrbgContext.new() var validatorMonitor = newClone(ValidatorMonitor.init()) cfg = genesisTestRuntimeConfig(ConsensusFork.Electra) dag = init( ChainDAGRef, cfg, makeTestDB( TOTAL_COMMITTEES * TARGET_COMMITTEE_SIZE * SLOTS_PER_EPOCH, cfg = cfg), validatorMonitor, {}) taskpool = Taskpool.new() verifier = BatchVerifier.init(rng, taskpool) quarantine = newClone(Quarantine.init()) pool = newClone(AttestationPool.init(dag, quarantine)) state = newClone(dag.headState) cache = StateCache() info = ForkedEpochInfo() # Slot 0 is a finalized slot - won't be making attestations for it.. check: process_slots( dag.cfg, state[], getStateField(state[], slot) + MIN_ATTESTATION_INCLUSION_DELAY, cache, info, {}).isOk() test "Can add and retrieve simple electra attestations" & preset(): let # Create an attestation for slot 1! bc0 = get_beacon_committee( state[], getStateField(state[], slot), 0.CommitteeIndex, cache) attestation = makeElectraAttestation( state[], state[].latest_block_root, bc0[0], cache) pool[].addAttestation( attestation, @[bc0[0]], attestation.loadSig, attestation.data.slot.start_beacon_time) check: process_slots( defaultRuntimeConfig, state[], getStateField(state[], slot) + MIN_ATTESTATION_INCLUSION_DELAY, cache, info, {}).isOk() let attestations = pool[].getElectraAttestationsForBlock(state[], cache) check: attestations.len == 1 let root1 = addTestBlock( state[], cache, electraAttestations = attestations, nextSlot = false).electraData.root bc1 = get_beacon_committee( state[], getStateField(state[], slot), 0.CommitteeIndex, cache) att1 = makeElectraAttestation(state[], root1, bc1[0], cache) check: withState(state[]): forkyState.latest_block_root == root1 process_slots( defaultRuntimeConfig, state[], getStateField(state[], slot) + MIN_ATTESTATION_INCLUSION_DELAY, cache, info, {}).isOk() withState(state[]): forkyState.latest_block_root == root1 check: # shouldn't include already-included attestations pool[].getElectraAttestationsForBlock(state[], cache) == [] pool[].addAttestation( att1, @[bc1[0]], att1.loadSig, att1.data.slot.start_beacon_time) check: # but new ones should go in pool[].getElectraAttestationsForBlock(state[], cache).len() == 1 let att2 = makeElectraAttestation(state[], root1, bc1[1], cache) pool[].addAttestation( att2, @[bc1[1]], att2.loadSig, att2.data.slot.start_beacon_time) let combined = pool[].getElectraAttestationsForBlock(state[], cache) check: # New attestations should be combined with old attestations combined.len() == 1 combined[0].aggregation_bits.countOnes() == 2 pool[].addAttestation( combined[0], @[bc1[1], bc1[0]], combined[0].loadSig, combined[0].data.slot.start_beacon_time) check: # readding the combined attestation shouldn't have an effect pool[].getElectraAttestationsForBlock(state[], cache).len() == 1 let # Someone votes for a different root att3 = makeElectraAttestation(state[], ZERO_HASH, bc1[2], cache) pool[].addAttestation( att3, @[bc1[2]], att3.loadSig, att3.data.slot.start_beacon_time) check: # We should now get both attestations for the block, but the aggregate # should be the one with the most votes pool[].getElectraAttestationsForBlock(state[], cache).len() == 2 pool[].getElectraAggregatedAttestation(2.Slot, hash_tree_root(combined[0].data), 0.CommitteeIndex).get().aggregation_bits.countOnes() == 2 pool[].getElectraAggregatedAttestation(2.Slot, hash_tree_root(att2.data), 0.CommitteeIndex). get().aggregation_bits.countOnes() == 2 # requests to get and aggregate from different committees should be empty pool[].getElectraAggregatedAttestation( 2.Slot, combined[0].data.beacon_block_root, 1.CommitteeIndex).isNone() test "Attestations with disjoint comittee bits and equal data into single on-chain aggregate" & preset(): let bc0 = get_beacon_committee( state[], getStateField(state[], slot), 0.CommitteeIndex, cache) bc1 = get_beacon_committee( state[], getStateField(state[], slot), 1.CommitteeIndex, cache) # atestation from committee 1 attestation_1 = makeElectraAttestation( state[], state[].latest_block_root, bc0[0], cache) # atestation from different committee with same data as # attestaton 1 attestation_2 = makeElectraAttestation( state[], state[].latest_block_root, bc1[1], cache) pool[].addAttestation( attestation_1, @[bc0[0]], attestation_1.loadSig, attestation_1.data.slot.start_beacon_time) pool[].addAttestation( attestation_2, @[bc0[1]], attestation_2.loadSig, attestation_2.data.slot.start_beacon_time) check: process_slots( defaultRuntimeConfig, state[], getStateField(state[], slot) + MIN_ATTESTATION_INCLUSION_DELAY, cache, info, {}).isOk() let attestations = pool[].getElectraAttestationsForBlock(state[], cache) check: # A single inal chain aggregated attestation should be created # with same data and joint committee,aggregation bits attestations.len == 1 attestations[0].aggregation_bits.countOnes() == 2 attestations[0].committee_bits.countOnes() == 2 test "Aggregated attestations with disjoint comittee bits into a single on-chain aggregate" & preset(): proc verifyAttestationSignature(attestation: electra.Attestation): bool = withState(state[]): when consensusFork == ConsensusFork.Electra: let fork = pool.dag.cfg.forkAtEpoch(forkyState.data.slot.epoch) attesting_indices = get_attesting_indices( forkyState.data, attestation.data, attestation.aggregation_bits, attestation.committee_bits, cache) verify_attestation_signature( fork, pool.dag.genesis_validators_root, attestation.data, attesting_indices.mapIt(forkyState.data.validators.item(it).pubkey), attestation.signature) else: raiseAssert "must be electra" let bc0 = get_beacon_committee( state[], getStateField(state[], slot), 0.CommitteeIndex, cache) bc1 = get_beacon_committee( state[], getStateField(state[], slot), 1.CommitteeIndex, cache) # attestation from first committee attestation_1 = makeElectraAttestation( state[], state[].latest_block_root, bc0[0], cache) # another attestation from first committee with same data attestation_2 = makeElectraAttestation( state[], state[].latest_block_root, bc0[1], cache) # attestation from different committee with same data as # attestation 1 attestation_3 = makeElectraAttestation( state[], state[].latest_block_root, bc1[1], cache) check: verifyAttestationSignature(attestation_1) verifyAttestationSignature(attestation_2) verifyAttestationSignature(attestation_3) pool[].addAttestation( attestation_1, @[bc0[0]], attestation_1.loadSig, attestation_1.data.slot.start_beacon_time) pool[].addAttestation( attestation_2, @[bc0[1]], attestation_2.loadSig, attestation_2.data.slot.start_beacon_time) pool[].addAttestation( attestation_3, @[bc1[1]], attestation_3.loadSig, attestation_3.data.slot.start_beacon_time) check: process_slots( defaultRuntimeConfig, state[], getStateField(state[], slot) + MIN_ATTESTATION_INCLUSION_DELAY, cache, info, {}).isOk() let attestations = pool[].getElectraAttestationsForBlock(state[], cache) check: verifyAttestationSignature(attestations[0]) check_attestation( state[].electraData.data, attestations[0], {}, cache, true).isOk # A single final chain aggregated attestation should be created # with same data, 2 committee bits and 3 aggregation bits attestations.len == 1 attestations[0].aggregation_bits.countOnes() == 3 attestations[0].committee_bits.countOnes() == 2 test "Working with electra aggregates" & preset(): let # Create an attestation for slot 1! bc0 = get_beacon_committee( state[], getStateField(state[], slot), 0.CommitteeIndex, cache) var att0 = makeElectraAttestation( state[], state[].latest_block_root, bc0[0], cache) att0x = att0 att1 = makeElectraAttestation( state[], state[].latest_block_root, bc0[1], cache) att2 = makeElectraAttestation( state[], state[].latest_block_root, bc0[2], cache) att3 = makeElectraAttestation( state[], state[].latest_block_root, bc0[3], cache) proc verifyAttestationSignature(attestation: electra.Attestation): bool = withState(state[]): when consensusFork == ConsensusFork.Electra: let fork = pool.dag.cfg.forkAtEpoch(forkyState.data.slot.epoch) attesting_indices = get_attesting_indices( forkyState.data, attestation.data, attestation.aggregation_bits, attestation.committee_bits, cache) verify_attestation_signature( fork, pool.dag.genesis_validators_root, attestation.data, attesting_indices.mapIt(forkyState.data.validators.item(it).pubkey), attestation.signature) else: raiseAssert "must be electra" check: verifyAttestationSignature(att0) verifyAttestationSignature(att0x) verifyAttestationSignature(att1) verifyAttestationSignature(att2) verifyAttestationSignature(att3) # Both attestations include member 2 but neither is a subset of the other att0.combine(att2) att1.combine(att2) check: verifyAttestationSignature(att0) verifyAttestationSignature(att1) not pool[].covers(att0.data, att0.aggregation_bits) pool[].addAttestation( att0, @[bc0[0], bc0[2]], att0.loadSig, att0.data.slot.start_beacon_time) pool[].addAttestation( att1, @[bc0[1], bc0[2]], att1.loadSig, att1.data.slot.start_beacon_time) for att in pool[].electraAttestations(Opt.none Slot, Opt.none CommitteeIndex): check: verifyAttestationSignature(att) check: process_slots( defaultRuntimeConfig, state[], getStateField(state[], slot) + MIN_ATTESTATION_INCLUSION_DELAY, cache, info, {}).isOk() for att in pool[].electraAttestations(Opt.none Slot, Opt.none CommitteeIndex): check: verifyAttestationSignature(att) check: pool[].getElectraAttestationsForBlock(state[], cache).len() == 1 # Can get either aggregate here, random! verifyAttestationSignature(pool[].getElectraAggregatedAttestation( 1.Slot, hash_tree_root(att0.data), 0.CommitteeIndex).get) # Add in attestation 3 - both aggregates should now have it added pool[].addAttestation( att3, @[bc0[3]], att3.loadSig, att3.data.slot.start_beacon_time) block: let attestations = pool[].getElectraAttestationsForBlock(state[], cache) check: attestations.len() == 1 attestations[0].aggregation_bits.countOnes() == 3 check_attestation( state[].electraData.data, attestations[0], {}, cache, true).isOk verifyAttestationSignature(attestations[0]) # Can get either aggregate here, random! verifyAttestationSignature(pool[].getElectraAggregatedAttestation( 1.Slot, hash_tree_root(attestations[0].data), 0.CommitteeIndex).get) # Add in attestation 0 as single - attestation 1 is now a superset of the # aggregates in the pool, so everything else should be removed pool[].addAttestation( att0x, @[bc0[0]], att0x.loadSig, att0x.data.slot.start_beacon_time) block: let attestations = pool[].getElectraAttestationsForBlock(state[], cache) check: attestations.len() == 1 attestations[0].aggregation_bits.countOnes() == 4 check_attestation( state[].electraData.data, attestations[0], {}, cache, true).isOk verifyAttestationSignature(attestations[0]) verifyAttestationSignature(pool[].getElectraAggregatedAttestation( 1.Slot, hash_tree_root(attestations[0].data), 0.CommitteeIndex).get) # Someone votes for a different root let att4 = makeElectraAttestation(state[], ZERO_HASH, bc0[4], cache) check: verifyAttestationSignature(att4) pool[].addAttestation( att4, @[bc0[4]], att4.loadSig, att4.data.slot.start_beacon_time) # Total aggregations size should be one for that root check: pool[].getElectraAggregatedAttestation(1.Slot, hash_tree_root(att4.data), 0.CommitteeIndex).get().aggregation_bits.countOnes() == 1