diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 653b2900d..3a8a3ea14 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -56,6 +56,7 @@ pub struct BeaconChain { pub attestation_aggregator: RwLock, pub deposits_for_inclusion: RwLock>, pub exits_for_inclusion: RwLock>, + pub transfers_for_inclusion: RwLock>, pub proposer_slashings_for_inclusion: RwLock>, pub attester_slashings_for_inclusion: RwLock>, canonical_head: RwLock, @@ -126,6 +127,7 @@ where attestation_aggregator, deposits_for_inclusion: RwLock::new(vec![]), exits_for_inclusion: RwLock::new(vec![]), + transfers_for_inclusion: RwLock::new(vec![]), proposer_slashings_for_inclusion: RwLock::new(vec![]), attester_slashings_for_inclusion: RwLock::new(vec![]), state: RwLock::new(genesis_state), @@ -436,6 +438,44 @@ where } } + /// Accept some transfer and queue it for inclusion in an appropriate block. + pub fn receive_transfer_for_inclusion(&self, transfer: Transfer) { + // TODO: transfers are not checked for validity; check them. + // + // https://github.com/sigp/lighthouse/issues/276 + self.transfers_for_inclusion.write().push(transfer); + } + + /// Return a vec of transfers suitable for inclusion in some block. + pub fn get_transfers_for_block(&self) -> Vec { + // TODO: transfers are indiscriminately included; check them for validity. + // + // https://github.com/sigp/lighthouse/issues/275 + self.transfers_for_inclusion.read().clone() + } + + /// Takes a list of `Deposits` that were included in recent blocks and removes them from the + /// inclusion queue. + /// + /// This ensures that `Deposits` are not included twice in successive blocks. + pub fn set_transfers_as_included(&self, included_transfers: &[Transfer]) { + // TODO: method does not take forks into account; consider this. + let mut indices_to_delete = vec![]; + + for included in included_transfers { + for (i, for_inclusion) in self.transfers_for_inclusion.read().iter().enumerate() { + if included == for_inclusion { + indices_to_delete.push(i); + } + } + } + + let transfers_for_inclusion = &mut self.transfers_for_inclusion.write(); + for i in indices_to_delete { + transfers_for_inclusion.remove(i); + } + } + /// Accept some proposer slashing and queue it for inclusion in an appropriate block. pub fn receive_proposer_slashing_for_inclusion(&self, proposer_slashing: ProposerSlashing) { // TODO: proposer_slashings are not checked for validity; check them. @@ -664,6 +704,8 @@ where // Update the inclusion queues so they aren't re-submitted. self.set_deposits_as_included(&block.body.deposits[..]); + self.set_transfers_as_included(&block.body.transfers[..]); + self.set_exits_as_included(&block.body.voluntary_exits[..]); self.set_proposer_slashings_as_included(&block.body.proposer_slashings[..]); self.set_attester_slashings_as_included(&block.body.attester_slashings[..]); @@ -730,7 +772,7 @@ where attestations, deposits: self.get_deposits_for_block(), voluntary_exits: self.get_exits_for_block(), - transfers: vec![], + transfers: self.get_transfers_for_block(), }, }; diff --git a/beacon_node/beacon_chain/test_harness/specs/validator_registry.yaml b/beacon_node/beacon_chain/test_harness/specs/validator_registry.yaml index a6021dbe5..aea7dcf31 100644 --- a/beacon_node/beacon_chain/test_harness/specs/validator_registry.yaml +++ b/beacon_node/beacon_chain/test_harness/specs/validator_registry.yaml @@ -10,18 +10,23 @@ test_cases: num_slots: 64 skip_slots: [2, 3] deposits: - # At slot 1, create a new validator deposit of 32 ETH. + # At slot 1, create a new validator deposit of 5 ETH. - slot: 1 - amount: 32 + amount: 5000000000 # Trigger more deposits... - slot: 3 - amount: 32 + amount: 5000000000 - slot: 5 - amount: 32 + amount: 32000000000 exits: # At slot 10, submit an exit for validator #50. - slot: 10 validator_index: 50 + transfers: + - slot: 6 + from: 1000 + to: 1001 + amount: 5000000000 proposer_slashings: # At slot 2, trigger a proposer slashing for validator #42. - slot: 2 @@ -44,4 +49,11 @@ test_cases: slashed_validators: [11, 12, 13, 14, 42] exited_validators: [] exit_initiated_validators: [50] + balances: + - validator_index: 1000 + comparison: "eq" + balance: 0 + - validator_index: 1001 + comparison: "eq" + balance: 10000000000 diff --git a/beacon_node/beacon_chain/test_harness/src/beacon_chain_harness.rs b/beacon_node/beacon_chain/test_harness/src/beacon_chain_harness.rs index 38c41e38c..f220619ce 100644 --- a/beacon_node/beacon_chain/test_harness/src/beacon_chain_harness.rs +++ b/beacon_node/beacon_chain/test_harness/src/beacon_chain_harness.rs @@ -1,7 +1,7 @@ use super::ValidatorHarness; use beacon_chain::{BeaconChain, BlockProcessingOutcome}; pub use beacon_chain::{BeaconChainError, CheckPoint}; -use bls::create_proof_of_possession; +use bls::{create_proof_of_possession, get_withdrawal_credentials}; use db::{ stores::{BeaconBlockStore, BeaconStateStore}, MemoryDB, @@ -67,7 +67,13 @@ impl BeaconChainHarness { timestamp: genesis_time - 1, deposit_input: DepositInput { pubkey: keypair.pk.clone(), - withdrawal_credentials: Hash256::zero(), // Withdrawal not possible. + // Validator can withdraw using their main keypair. + withdrawal_credentials: Hash256::from_slice( + &get_withdrawal_credentials( + &keypair.pk, + spec.bls_withdrawal_prefix_byte, + )[..], + ), proof_of_possession: create_proof_of_possession(&keypair), }, }, @@ -286,6 +292,11 @@ impl BeaconChainHarness { self.beacon_chain.receive_exit_for_inclusion(exit); } + /// Submit an transfer to the `BeaconChain` for inclusion in some block. + pub fn add_transfer(&mut self, transfer: Transfer) { + self.beacon_chain.receive_transfer_for_inclusion(transfer); + } + /// Submit a proposer slashing to the `BeaconChain` for inclusion in some block. pub fn add_proposer_slashing(&mut self, proposer_slashing: ProposerSlashing) { self.beacon_chain diff --git a/beacon_node/beacon_chain/test_harness/src/test_case.rs b/beacon_node/beacon_chain/test_harness/src/test_case.rs index e3bcf740a..97b4bb3d4 100644 --- a/beacon_node/beacon_chain/test_harness/src/test_case.rs +++ b/beacon_node/beacon_chain/test_harness/src/test_case.rs @@ -3,7 +3,7 @@ use crate::beacon_chain_harness::BeaconChainHarness; use beacon_chain::CheckPoint; -use bls::create_proof_of_possession; +use bls::{create_proof_of_possession, get_withdrawal_credentials}; use log::{info, warn}; use ssz::SignedRoot; use types::*; @@ -83,8 +83,8 @@ impl TestCase { info!("Starting simulation across {} slots...", slots); - // -1 slots because genesis counts as a slot. - for slot_height in 0..slots - 1 { + // Start at 1 because genesis counts as a slot. + for slot_height in 1..slots { // Used to ensure that deposits in the same slot have incremental deposit indices. let mut deposit_index_offset = 0; @@ -144,6 +144,20 @@ impl TestCase { } } + // Feed transfers to the BeaconChain. + if let Some(ref transfers) = self.config.transfers { + for (slot, from, to, amount) in transfers { + if *slot == slot_height { + info!( + "Including transfer at slot height {} from validator {}.", + slot_height, from + ); + let transfer = build_transfer(&harness, *from, *to, *amount); + harness.add_transfer(transfer); + } + } + } + // Build a block or skip a slot. match self.config.skip_slots { Some(ref skip_slots) if skip_slots.contains(&slot_height) => { @@ -208,6 +222,30 @@ impl TestCase { } } +/// Builds a `Deposit` this is valid for the given `BeaconChainHarness` at its next slot. +fn build_transfer(harness: &BeaconChainHarness, from: u64, to: u64, amount: u64) -> Transfer { + let slot = harness.beacon_chain.state.read().slot + 1; + + let mut transfer = Transfer { + from, + to, + amount, + fee: 0, + slot, + pubkey: harness.validators[from as usize].keypair.pk.clone(), + signature: Signature::empty_signature(), + }; + + let message = transfer.signed_root(); + let epoch = slot.epoch(harness.spec.slots_per_epoch); + + transfer.signature = harness + .validator_sign(from as usize, &message[..], epoch, Domain::Transfer) + .expect("Unable to sign Transfer"); + + transfer +} + /// Builds a `Deposit` this is valid for the given `BeaconChainHarness`. /// /// `index_offset` is used to ensure that `deposit.index == state.index` when adding multiple @@ -220,8 +258,9 @@ fn build_deposit( let keypair = Keypair::random(); let proof_of_possession = create_proof_of_possession(&keypair); let index = harness.beacon_chain.state.read().deposit_index + index_offset; - - info!("index: {}, index_offset: {}", index, index_offset); + let withdrawal_credentials = Hash256::from_slice( + &get_withdrawal_credentials(&keypair.pk, harness.spec.bls_withdrawal_prefix_byte)[..], + ); let deposit = Deposit { // Note: `branch` and `index` will need to be updated once the spec defines their @@ -233,7 +272,7 @@ fn build_deposit( timestamp: 1, deposit_input: DepositInput { pubkey: keypair.pk.clone(), - withdrawal_credentials: Hash256::zero(), + withdrawal_credentials, proof_of_possession, }, }, diff --git a/beacon_node/beacon_chain/test_harness/src/test_case/config.rs b/beacon_node/beacon_chain/test_harness/src/test_case/config.rs index 140fc4128..f336b9d53 100644 --- a/beacon_node/beacon_chain/test_harness/src/test_case/config.rs +++ b/beacon_node/beacon_chain/test_harness/src/test_case/config.rs @@ -10,6 +10,8 @@ pub type DepositTuple = (SlotHeight, GweiAmount); pub type ExitTuple = (SlotHeight, ValidatorIndex); pub type ProposerSlashingTuple = (SlotHeight, ValidatorIndex); pub type AttesterSlashingTuple = (SlotHeight, ValidatorIndices); +/// (slot_height, from, to, amount) +pub type TransferTuple = (SlotHeight, ValidatorIndex, ValidatorIndex, GweiAmount); /// Defines the execution of a `BeaconStateHarness` across a series of slots. #[derive(Debug)] @@ -30,6 +32,8 @@ pub struct Config { pub attester_slashings: Option>, /// Exits to be including during execution. pub exits: Option>, + /// Transfers to be including during execution. + pub transfers: Option>, } impl Config { @@ -47,10 +51,27 @@ impl Config { proposer_slashings: parse_proposer_slashings(&yaml), attester_slashings: parse_attester_slashings(&yaml), exits: parse_exits(&yaml), + transfers: parse_transfers(&yaml), } } } +/// Parse the `transfers` section of the YAML document. +fn parse_transfers(yaml: &Yaml) -> Option> { + let mut tuples = vec![]; + + for exit in yaml["transfers"].as_vec()? { + let slot = as_u64(exit, "slot").expect("Incomplete transfer (slot)"); + let from = as_u64(exit, "from").expect("Incomplete transfer (from)"); + let to = as_u64(exit, "to").expect("Incomplete transfer (to)"); + let amount = as_u64(exit, "amount").expect("Incomplete transfer (amount)"); + + tuples.push((SlotHeight::from(slot), from, to, amount)); + } + + Some(tuples) +} + /// Parse the `attester_slashings` section of the YAML document. fn parse_exits(yaml: &Yaml) -> Option> { let mut tuples = vec![]; @@ -102,8 +123,7 @@ fn parse_deposits(yaml: &Yaml) -> Option> { for deposit in yaml["deposits"].as_vec()? { let slot = as_u64(deposit, "slot").expect("Incomplete deposit (slot)"); - let amount = - as_u64(deposit, "amount").expect("Incomplete deposit (amount)") * 1_000_000_000; + let amount = as_u64(deposit, "amount").expect("Incomplete deposit (amount)"); deposits.push((SlotHeight::from(slot), amount)) } diff --git a/beacon_node/beacon_chain/test_harness/src/test_case/state_check.rs b/beacon_node/beacon_chain/test_harness/src/test_case/state_check.rs index 6fa75364a..4d2bfd07d 100644 --- a/beacon_node/beacon_chain/test_harness/src/test_case/state_check.rs +++ b/beacon_node/beacon_chain/test_harness/src/test_case/state_check.rs @@ -3,6 +3,11 @@ use log::info; use types::*; use yaml_rust::Yaml; +type ValidatorIndex = u64; +type BalanceGwei = u64; + +type BalanceCheckTuple = (ValidatorIndex, String, BalanceGwei); + /// Tests to be conducted upon a `BeaconState` object generated during the execution of a /// `TestCase`. #[derive(Debug)] @@ -17,6 +22,8 @@ pub struct StateCheck { pub exited_validators: Option>, /// A list of validator indices which have had an exit initiated. Must be in ascending order. pub exit_initiated_validators: Option>, + /// A list of balances to check. + pub balances: Option>, } impl StateCheck { @@ -30,6 +37,7 @@ impl StateCheck { slashed_validators: as_vec_u64(&yaml, "slashed_validators"), exited_validators: as_vec_u64(&yaml, "exited_validators"), exit_initiated_validators: as_vec_u64(&yaml, "exit_initiated_validators"), + balances: parse_balances(&yaml), } } @@ -124,5 +132,47 @@ impl StateCheck { exit_initiated_validators ); } + + // Check validator balances. + if let Some(ref balances) = self.balances { + for (index, comparison, expected) in balances { + let actual = *state + .validator_balances + .get(*index as usize) + .expect("Balance check specifies unknown validator"); + + let result = match comparison.as_ref() { + "eq" => actual == *expected, + _ => panic!("Unknown balance comparison (use `eq`)"), + }; + assert!( + result, + format!( + "Validator balance for {}: {} !{} {}.", + index, actual, comparison, expected + ) + ); + info!("OK: validator balance for {:?}.", index); + } + } } } + +/// Parse the `transfers` section of the YAML document. +fn parse_balances(yaml: &Yaml) -> Option> { + let mut tuples = vec![]; + + for exit in yaml["balances"].as_vec()? { + let from = + as_u64(exit, "validator_index").expect("Incomplete balance check (validator_index)"); + let comparison = exit["comparison"] + .clone() + .into_string() + .expect("Incomplete balance check (amount)"); + let balance = as_u64(exit, "balance").expect("Incomplete balance check (balance)"); + + tuples.push((from, comparison, balance)); + } + + Some(tuples) +} diff --git a/eth2/state_processing/src/per_block_processing/errors.rs b/eth2/state_processing/src/per_block_processing/errors.rs index 59d3f2f80..36b0d4942 100644 --- a/eth2/state_processing/src/per_block_processing/errors.rs +++ b/eth2/state_processing/src/per_block_processing/errors.rs @@ -365,7 +365,9 @@ pub enum TransferInvalid { /// (from_validator) FromValidatorIneligableForTransfer(u64), /// The validators withdrawal credentials do not match `transfer.pubkey`. - WithdrawalCredentialsMismatch, + /// + /// (state_credentials, transfer_pubkey_credentials) + WithdrawalCredentialsMismatch(Hash256, Hash256), /// The deposit was not signed by `deposit.pubkey`. BadSignature, /// Overflow when adding to `transfer.to` balance. diff --git a/eth2/state_processing/src/per_block_processing/verify_transfer.rs b/eth2/state_processing/src/per_block_processing/verify_transfer.rs index 15ec17142..4746fc75c 100644 --- a/eth2/state_processing/src/per_block_processing/verify_transfer.rs +++ b/eth2/state_processing/src/per_block_processing/verify_transfer.rs @@ -64,7 +64,10 @@ pub fn verify_transfer( ); verify!( from_validator.withdrawal_credentials == transfer_withdrawal_credentials, - Invalid::WithdrawalCredentialsMismatch + Invalid::WithdrawalCredentialsMismatch( + from_validator.withdrawal_credentials, + transfer_withdrawal_credentials + ) ); let message = transfer.signed_root();