diff --git a/eth2/state_processing/src/block_processable.rs b/eth2/state_processing/src/block_processable.rs index ee8bc7c33..6f4d7268e 100644 --- a/eth2/state_processing/src/block_processable.rs +++ b/eth2/state_processing/src/block_processable.rs @@ -5,7 +5,11 @@ use int_to_bytes::int_to_bytes32; use log::{debug, trace}; use ssz::{ssz_encode, TreeHash}; use types::*; +use validate_attestation::validate_attestations; +pub use validate_attestation::{validate_attestation, validate_attestation_without_signature}; + +mod validate_attestation; mod verify_slashable_attestation; const PHASE_0_CUSTODY_BIT: bool = false; @@ -41,7 +45,6 @@ pub enum Error { BeaconStateError(BeaconStateError), SlotProcessingError(SlotProcessingError), } - #[derive(Debug, PartialEq)] pub enum AttestationValidationError { IncludedTooEarly, @@ -221,19 +224,10 @@ fn per_block_processing_signature_optional( verify_slashable_attestation(&mut state, &attester_slashing, spec)?; } - /* - * Attestations - */ - ensure!( - block.body.attestations.len() as u64 <= spec.max_attestations, - Error::MaxAttestationsExceeded - ); - - debug!("Verifying {} attestations.", block.body.attestations.len()); + validate_attestations(&mut state, &block, spec); + // Convert each attestation into a `PendingAttestation` and insert into the state. for attestation in &block.body.attestations { - validate_attestation(&state, attestation, spec)?; - let pending_attestation = PendingAttestation { data: attestation.data.clone(), aggregation_bitfield: attestation.aggregation_bitfield.clone(), @@ -293,7 +287,7 @@ fn per_block_processing_signature_optional( ); ensure!(state.current_epoch(spec) >= exit.epoch, Error::BadExit); let exit_message = { - let exit_struct = Exit { + let exit_struct = VoluntaryExit { epoch: exit.epoch, validator_index: exit.validator_index, signature: spec.empty_signature.clone(), @@ -317,110 +311,6 @@ fn per_block_processing_signature_optional( Ok(()) } -pub fn validate_attestation( - state: &BeaconState, - attestation: &Attestation, - spec: &ChainSpec, -) -> Result<(), AttestationValidationError> { - validate_attestation_signature_optional(state, attestation, spec, true) -} - -pub fn validate_attestation_without_signature( - state: &BeaconState, - attestation: &Attestation, - spec: &ChainSpec, -) -> Result<(), AttestationValidationError> { - validate_attestation_signature_optional(state, attestation, spec, false) -} - -fn validate_attestation_signature_optional( - state: &BeaconState, - attestation: &Attestation, - spec: &ChainSpec, - verify_signature: bool, -) -> Result<(), AttestationValidationError> { - trace!( - "validate_attestation_signature_optional: attestation epoch: {}", - attestation.data.slot.epoch(spec.slots_per_epoch) - ); - ensure!( - attestation.data.slot + spec.min_attestation_inclusion_delay <= state.slot, - AttestationValidationError::IncludedTooEarly - ); - ensure!( - attestation.data.slot + spec.slots_per_epoch >= state.slot, - AttestationValidationError::IncludedTooLate - ); - if attestation.data.slot >= state.current_epoch_start_slot(spec) { - ensure!( - attestation.data.justified_epoch == state.justified_epoch, - AttestationValidationError::WrongJustifiedSlot - ); - } else { - ensure!( - attestation.data.justified_epoch == state.previous_justified_epoch, - AttestationValidationError::WrongJustifiedSlot - ); - } - ensure!( - attestation.data.justified_block_root - == *state - .get_block_root( - attestation - .data - .justified_epoch - .start_slot(spec.slots_per_epoch), - &spec - ) - .ok_or(AttestationValidationError::NoBlockRoot)?, - AttestationValidationError::WrongJustifiedRoot - ); - let potential_crosslink = Crosslink { - shard_block_root: attestation.data.shard_block_root, - epoch: attestation.data.slot.epoch(spec.slots_per_epoch), - }; - ensure!( - (attestation.data.latest_crosslink - == state.latest_crosslinks[attestation.data.shard as usize]) - | (attestation.data.latest_crosslink == potential_crosslink), - AttestationValidationError::BadLatestCrosslinkRoot - ); - if verify_signature { - let participants = state.get_attestation_participants( - &attestation.data, - &attestation.aggregation_bitfield, - spec, - )?; - trace!( - "slot: {}, shard: {}, participants: {:?}", - attestation.data.slot, - attestation.data.shard, - participants - ); - let mut group_public_key = AggregatePublicKey::new(); - for participant in participants { - group_public_key.add(&state.validator_registry[participant as usize].pubkey) - } - ensure!( - attestation.verify_signature( - &group_public_key, - PHASE_0_CUSTODY_BIT, - get_domain( - &state.fork, - attestation.data.slot.epoch(spec.slots_per_epoch), - spec.domain_attestation, - ) - ), - AttestationValidationError::BadSignature - ); - } - ensure!( - attestation.data.shard_block_root == spec.zero_hash, - AttestationValidationError::ShardBlockRootNotZero - ); - Ok(()) -} - fn get_domain(fork: &Fork, epoch: Epoch, domain_type: u64) -> u64 { fork.get_domain(epoch, domain_type) } diff --git a/eth2/state_processing/src/block_processable/validate_attestation.rs b/eth2/state_processing/src/block_processable/validate_attestation.rs new file mode 100644 index 000000000..1b422711c --- /dev/null +++ b/eth2/state_processing/src/block_processable/validate_attestation.rs @@ -0,0 +1,238 @@ +use crate::errors::{AttestationInvalid as Invalid, AttestationValidationError as Error}; +use ssz::TreeHash; +use types::beacon_state::helpers::*; +use types::*; + +/// Validate the attestations in some block, converting each into a `PendingAttestation` which is +/// then added to `state.latest_attestations`. +/// +/// Spec v0.4.0 +pub fn validate_attestations( + state: &BeaconState, + block: &BeaconBlock, + spec: &ChainSpec, +) -> Result<(), Error> { + ensure!( + block.body.attestations.len() as u64 <= spec.max_attestations, + MaxAttestationsExceeded + ); + + for attestation in &block.body.attestations { + validate_attestation(&state, attestation, spec)?; + } + + Ok(()) +} + +/// Validate an attestation, checking the aggregate signature. +/// +/// Spec v0.4.0 +pub fn validate_attestation( + state: &BeaconState, + attestation: &Attestation, + spec: &ChainSpec, +) -> Result<(), Error> { + validate_attestation_signature_optional(state, attestation, spec, true) +} + +/// Validate an attestation, without checking the aggregate signature. +/// +/// Spec v0.4.0 +pub fn validate_attestation_without_signature( + state: &BeaconState, + attestation: &Attestation, + spec: &ChainSpec, +) -> Result<(), Error> { + validate_attestation_signature_optional(state, attestation, spec, false) +} + +/// Validate an attestation, optionally checking the aggregate signature. +/// +/// Spec v0.2.0 +fn validate_attestation_signature_optional( + state: &BeaconState, + attestation: &Attestation, + spec: &ChainSpec, + verify_signature: bool, +) -> Result<(), Error> { + // Verify that `attestation.data.slot >= GENESIS_SLOT`. + ensure!(attestation.data.slot >= spec.genesis_slot, PreGenesis); + + // Verify that `attestation.data.slot + MIN_ATTESTATION_INCLUSION_DELAY <= state.slot`. + ensure!( + attestation.data.slot + spec.min_attestation_inclusion_delay <= state.slot, + IncludedTooEarly + ); + + // Verify that `state.slot < attestation.data.slot + SLOTS_PER_EPOCH`. + ensure!( + state.slot < attestation.data.slot + spec.slots_per_epoch, + IncludedTooLate + ); + + // Verify that `attestation.data.justified_epoch` is equal to `state.justified_epoch` if + // `slot_to_epoch(attestation.data.slot + 1) >= get_current_epoch(state) else + // state.previous_justified_epoch`. + if (attestation.data.slot + 1).epoch(spec.slots_per_epoch) >= state.current_epoch(spec) { + ensure!( + attestation.data.justified_epoch == state.justified_epoch, + WrongJustifiedSlot + ); + } else { + ensure!( + attestation.data.justified_epoch == state.previous_justified_epoch, + WrongJustifiedSlot + ); + } + + // Verify that `attestation.data.justified_block_root` is equal to `get_block_root(state, + // get_epoch_start_slot(attestation.data.justified_epoch))`. + ensure!( + attestation.data.justified_block_root + == *state + .get_block_root( + attestation + .data + .justified_epoch + .start_slot(spec.slots_per_epoch), + &spec + ) + .ok_or(BeaconStateError::InsufficientBlockRoots)?, + WrongJustifiedRoot + ); + + // Verify that either: + // + // (i)`state.latest_crosslinks[attestation.data.shard] == attestation.data.latest_crosslink`, + // + // (ii) `state.latest_crosslinks[attestation.data.shard] == + // Crosslink(crosslink_data_root=attestation.data.crosslink_data_root, + // epoch=slot_to_epoch(attestation.data.slot))`. + let potential_crosslink = Crosslink { + crosslink_data_root: attestation.data.crosslink_data_root, + epoch: attestation.data.slot.epoch(spec.slots_per_epoch), + }; + ensure!( + (attestation.data.latest_crosslink + == state.latest_crosslinks[attestation.data.shard as usize]) + | (state.latest_crosslinks[attestation.data.shard as usize] == potential_crosslink), + BadLatestCrosslinkRoot + ); + + // Get the committee for this attestation + let (committee, _shard) = state + .get_crosslink_committees_at_slot(attestation.data.slot, spec)? + .iter() + .find(|(committee, shard)| *shard == attestation.data.shard) + .ok_or_else(|| Error::Invalid(Invalid::NoCommitteeForShard))?; + + // Custody bitfield is all zeros (phase 0 requirement). + ensure!( + attestation.custody_bitfield.num_set_bits() == 0, + CustodyBitfieldHasSetBits + ); + // Custody bitfield length is correct. + ensure!( + verify_bitfield_length(&attestation.aggregation_bitfield, committee.len()), + BadCustodyBitfieldLength + ); + // Aggregation bitfield isn't empty. + ensure!( + attestation.aggregation_bitfield.num_set_bits() != 0, + AggregationBitfieldIsEmpty + ); + // Aggregation bitfield length is correct. + ensure!( + verify_bitfield_length(&attestation.aggregation_bitfield, committee.len()), + BadAggregationBitfieldLength + ); + + if verify_signature { + ensure!( + verify_attestation_signature( + state, + committee, + &attestation.custody_bitfield, + &attestation.data, + &attestation.aggregate_signature, + spec + ), + BadSignature + ); + } + + // [TO BE REMOVED IN PHASE 1] Verify that `attestation.data.crosslink_data_root == ZERO_HASH`. + ensure!( + attestation.data.crosslink_data_root == spec.zero_hash, + ShardBlockRootNotZero + ); + + Ok(()) +} + +/// Verifies an aggregate signature for some given `AttestationData`, returning `true` if the +/// `aggregate_signature` is valid. +/// +/// Returns `false` if: +/// - `aggregate_signature` was not signed correctly. +/// - `custody_bitfield` does not have a bit for each index of `committee`. +/// - A `validator_index` in `committee` is not in `state.validator_registry`. +fn verify_attestation_signature( + state: &BeaconState, + committee: &[usize], + custody_bitfield: &Bitfield, + attestation_data: &AttestationData, + aggregate_signature: &AggregateSignature, + spec: &ChainSpec, +) -> bool { + let mut aggregate_pubs = vec![AggregatePublicKey::new(); 2]; + let mut message_exists = vec![false; 2]; + + for (i, v) in committee.iter().enumerate() { + let custody_bit = match custody_bitfield.get(i) { + Ok(bit) => bit, + // Invalidate signature if custody_bitfield.len() < committee + Err(_) => return false, + }; + + message_exists[custody_bit as usize] = true; + + match state.validator_registry.get(*v as usize) { + Some(validator) => { + aggregate_pubs[custody_bit as usize].add(&validator.pubkey); + } + // Invalidate signature if validator index is unknown. + None => return false, + }; + } + + // Message when custody bitfield is `false` + let message_0 = AttestationDataAndCustodyBit { + data: attestation_data.clone(), + custody_bit: false, + } + .hash_tree_root(); + + // Message when custody bitfield is `true` + let message_1 = AttestationDataAndCustodyBit { + data: attestation_data.clone(), + custody_bit: true, + } + .hash_tree_root(); + + let mut messages = vec![]; + let mut keys = vec![]; + + // If any validator signed a message with a `false` custody bit. + if message_exists[0] { + messages.push(&message_0[..]); + keys.push(&aggregate_pubs[0]); + } + // If any validator signed a message with a `true` custody bit. + if message_exists[1] { + messages.push(&message_1[..]); + keys.push(&aggregate_pubs[1]); + } + + aggregate_signature.verify_multiple(&messages[..], spec.domain_attestation, &keys[..]) +} diff --git a/eth2/state_processing/src/errors.rs b/eth2/state_processing/src/errors.rs new file mode 100644 index 000000000..5eceb0be0 --- /dev/null +++ b/eth2/state_processing/src/errors.rs @@ -0,0 +1,30 @@ +use types::BeaconStateError; + +#[derive(Debug, PartialEq)] +pub enum AttestationValidationError { + Invalid(AttestationInvalid), + ProcessingError(BeaconStateError), +} + +#[derive(Debug, PartialEq)] +pub enum AttestationInvalid { + PreGenesis, + IncludedTooEarly, + IncludedTooLate, + WrongJustifiedSlot, + WrongJustifiedRoot, + BadLatestCrosslinkRoot, + CustodyBitfieldHasSetBits, + AggregationBitfieldIsEmpty, + BadAggregationBitfieldLength, + BadCustodyBitfieldLength, + NoCommitteeForShard, + BadSignature, + ShardBlockRootNotZero, +} + +impl From for AttestationValidationError { + fn from(e: BeaconStateError) -> AttestationValidationError { + AttestationValidationError::ProcessingError(e) + } +} diff --git a/eth2/state_processing/src/lib.rs b/eth2/state_processing/src/lib.rs index 18d1f7554..0a4343e00 100644 --- a/eth2/state_processing/src/lib.rs +++ b/eth2/state_processing/src/lib.rs @@ -1,10 +1,13 @@ +#[macro_use] +mod macros; mod block_processable; -mod epoch_processable; -mod slot_processable; +// mod epoch_processable; +mod errors; +// mod slot_processable; pub use block_processable::{ validate_attestation, validate_attestation_without_signature, BlockProcessable, Error as BlockProcessingError, }; -pub use epoch_processable::{EpochProcessable, Error as EpochProcessingError}; -pub use slot_processable::{Error as SlotProcessingError, SlotProcessable}; +// pub use epoch_processable::{EpochProcessable, Error as EpochProcessingError}; +// pub use slot_processable::{Error as SlotProcessingError, SlotProcessable}; diff --git a/eth2/state_processing/src/macros.rs b/eth2/state_processing/src/macros.rs new file mode 100644 index 000000000..6ccc54ba9 --- /dev/null +++ b/eth2/state_processing/src/macros.rs @@ -0,0 +1,8 @@ +#[macro_use] +macro_rules! ensure { + ($condition: expr, $result: ident) => { + if !$condition { + return Err(Error::Invalid(Invalid::$result)); + } + }; +}