mirror of
https://gitlab.com/pulsechaincom/lighthouse-pulse.git
synced 2024-12-30 16:06:30 +00:00
Merge pull request #264 from sigp/yaml-chain-tests
YAML-defined test_harness routines
This commit is contained in:
commit
0e1a14a628
@ -15,9 +15,7 @@ use state_processing::{
|
||||
use std::sync::Arc;
|
||||
use types::{
|
||||
readers::{BeaconBlockReader, BeaconStateReader},
|
||||
AttestationData, BeaconBlock, BeaconBlockBody, BeaconState, BeaconStateError, ChainSpec,
|
||||
Crosslink, Deposit, Epoch, Eth1Data, FreeAttestation, Hash256, PublicKey, RelativeEpoch,
|
||||
Signature, Slot,
|
||||
*,
|
||||
};
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
@ -66,6 +64,9 @@ pub struct BeaconChain<T: ClientDB + Sized, U: SlotClock, F: ForkChoice> {
|
||||
pub state_store: Arc<BeaconStateStore<T>>,
|
||||
pub slot_clock: U,
|
||||
pub attestation_aggregator: RwLock<AttestationAggregator>,
|
||||
pub deposits_for_inclusion: RwLock<Vec<Deposit>>,
|
||||
pub proposer_slashings_for_inclusion: RwLock<Vec<ProposerSlashing>>,
|
||||
pub attester_slashings_for_inclusion: RwLock<Vec<AttesterSlashing>>,
|
||||
canonical_head: RwLock<CheckPoint>,
|
||||
finalized_head: RwLock<CheckPoint>,
|
||||
pub state: RwLock<BeaconState>,
|
||||
@ -132,6 +133,9 @@ where
|
||||
state_store,
|
||||
slot_clock,
|
||||
attestation_aggregator,
|
||||
deposits_for_inclusion: RwLock::new(vec![]),
|
||||
proposer_slashings_for_inclusion: RwLock::new(vec![]),
|
||||
attester_slashings_for_inclusion: RwLock::new(vec![]),
|
||||
state: RwLock::new(genesis_state),
|
||||
finalized_head,
|
||||
canonical_head,
|
||||
@ -364,6 +368,128 @@ where
|
||||
Ok(aggregation_outcome)
|
||||
}
|
||||
|
||||
/// Accept some deposit and queue it for inclusion in an appropriate block.
|
||||
pub fn receive_deposit_for_inclusion(&self, deposit: Deposit) {
|
||||
// TODO: deposits are not check for validity; check them.
|
||||
self.deposits_for_inclusion.write().push(deposit);
|
||||
}
|
||||
|
||||
/// Return a vec of deposits suitable for inclusion in some block.
|
||||
pub fn get_deposits_for_block(&self) -> Vec<Deposit> {
|
||||
// TODO: deposits are indiscriminately included; check them for validity.
|
||||
self.deposits_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_deposits_as_included(&self, included_deposits: &[Deposit]) {
|
||||
// TODO: method does not take forks into account; consider this.
|
||||
let mut indices_to_delete = vec![];
|
||||
|
||||
for included in included_deposits {
|
||||
for (i, for_inclusion) in self.deposits_for_inclusion.read().iter().enumerate() {
|
||||
if included == for_inclusion {
|
||||
indices_to_delete.push(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let deposits_for_inclusion = &mut self.deposits_for_inclusion.write();
|
||||
for i in indices_to_delete {
|
||||
deposits_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 check for validity; check them.
|
||||
self.proposer_slashings_for_inclusion
|
||||
.write()
|
||||
.push(proposer_slashing);
|
||||
}
|
||||
|
||||
/// Return a vec of proposer slashings suitable for inclusion in some block.
|
||||
pub fn get_proposer_slashings_for_block(&self) -> Vec<ProposerSlashing> {
|
||||
// TODO: proposer_slashings are indiscriminately included; check them for validity.
|
||||
self.proposer_slashings_for_inclusion.read().clone()
|
||||
}
|
||||
|
||||
/// Takes a list of `ProposerSlashings` that were included in recent blocks and removes them
|
||||
/// from the inclusion queue.
|
||||
///
|
||||
/// This ensures that `ProposerSlashings` are not included twice in successive blocks.
|
||||
pub fn set_proposer_slashings_as_included(
|
||||
&self,
|
||||
included_proposer_slashings: &[ProposerSlashing],
|
||||
) {
|
||||
// TODO: method does not take forks into account; consider this.
|
||||
let mut indices_to_delete = vec![];
|
||||
|
||||
for included in included_proposer_slashings {
|
||||
for (i, for_inclusion) in self
|
||||
.proposer_slashings_for_inclusion
|
||||
.read()
|
||||
.iter()
|
||||
.enumerate()
|
||||
{
|
||||
if included == for_inclusion {
|
||||
indices_to_delete.push(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let proposer_slashings_for_inclusion = &mut self.proposer_slashings_for_inclusion.write();
|
||||
for i in indices_to_delete {
|
||||
proposer_slashings_for_inclusion.remove(i);
|
||||
}
|
||||
}
|
||||
|
||||
/// Accept some attester slashing and queue it for inclusion in an appropriate block.
|
||||
pub fn receive_attester_slashing_for_inclusion(&self, attester_slashing: AttesterSlashing) {
|
||||
// TODO: attester_slashings are not check for validity; check them.
|
||||
self.attester_slashings_for_inclusion
|
||||
.write()
|
||||
.push(attester_slashing);
|
||||
}
|
||||
|
||||
/// Return a vec of attester slashings suitable for inclusion in some block.
|
||||
pub fn get_attester_slashings_for_block(&self) -> Vec<AttesterSlashing> {
|
||||
// TODO: attester_slashings are indiscriminately included; check them for validity.
|
||||
self.attester_slashings_for_inclusion.read().clone()
|
||||
}
|
||||
|
||||
/// Takes a list of `AttesterSlashings` that were included in recent blocks and removes them
|
||||
/// from the inclusion queue.
|
||||
///
|
||||
/// This ensures that `AttesterSlashings` are not included twice in successive blocks.
|
||||
pub fn set_attester_slashings_as_included(
|
||||
&self,
|
||||
included_attester_slashings: &[AttesterSlashing],
|
||||
) {
|
||||
// TODO: method does not take forks into account; consider this.
|
||||
let mut indices_to_delete = vec![];
|
||||
|
||||
for included in included_attester_slashings {
|
||||
for (i, for_inclusion) in self
|
||||
.attester_slashings_for_inclusion
|
||||
.read()
|
||||
.iter()
|
||||
.enumerate()
|
||||
{
|
||||
if included == for_inclusion {
|
||||
indices_to_delete.push(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let attester_slashings_for_inclusion = &mut self.attester_slashings_for_inclusion.write();
|
||||
for i in indices_to_delete {
|
||||
attester_slashings_for_inclusion.remove(i);
|
||||
}
|
||||
}
|
||||
|
||||
/// Dumps the entire canonical chain, from the head to genesis to a vector for analysis.
|
||||
///
|
||||
/// This could be a very expensive operation and should only be done in testing/analysis
|
||||
@ -412,6 +538,8 @@ where
|
||||
last_slot = slot;
|
||||
}
|
||||
|
||||
dump.reverse();
|
||||
|
||||
Ok(dump)
|
||||
}
|
||||
|
||||
@ -488,6 +616,11 @@ where
|
||||
self.block_store.put(&block_root, &ssz_encode(&block)[..])?;
|
||||
self.state_store.put(&state_root, &ssz_encode(&state)[..])?;
|
||||
|
||||
// Update the inclusion queues so they aren't re-submitted.
|
||||
self.set_deposits_as_included(&block.body.deposits[..]);
|
||||
self.set_proposer_slashings_as_included(&block.body.proposer_slashings[..]);
|
||||
self.set_attester_slashings_as_included(&block.body.attester_slashings[..]);
|
||||
|
||||
// run the fork_choice add_block logic
|
||||
self.fork_choice
|
||||
.write()
|
||||
@ -500,7 +633,7 @@ where
|
||||
if self.head().beacon_block_root == parent_block_root {
|
||||
self.update_canonical_head(block.clone(), block_root, state.clone(), state_root);
|
||||
// Update the local state variable.
|
||||
*self.state.write() = state.clone();
|
||||
*self.state.write() = state;
|
||||
}
|
||||
|
||||
Ok(BlockProcessingOutcome::ValidBlock(ValidBlock::Processed))
|
||||
@ -541,10 +674,10 @@ where
|
||||
},
|
||||
signature: self.spec.empty_signature.clone(), // To be completed by a validator.
|
||||
body: BeaconBlockBody {
|
||||
proposer_slashings: vec![],
|
||||
attester_slashings: vec![],
|
||||
proposer_slashings: self.get_proposer_slashings_for_block(),
|
||||
attester_slashings: self.get_attester_slashings_for_block(),
|
||||
attestations,
|
||||
deposits: vec![],
|
||||
deposits: self.get_deposits_for_block(),
|
||||
exits: vec![],
|
||||
},
|
||||
};
|
||||
@ -553,7 +686,7 @@ where
|
||||
|
||||
let result =
|
||||
state.per_block_processing_without_verifying_block_signature(&block, &self.spec);
|
||||
trace!(
|
||||
debug!(
|
||||
"BeaconNode::produce_block: state processing result: {:?}",
|
||||
result
|
||||
);
|
||||
|
@ -4,6 +4,14 @@ version = "0.1.0"
|
||||
authors = ["Paul Hauner <paul@paulhauner.com>"]
|
||||
edition = "2018"
|
||||
|
||||
[[bin]]
|
||||
name = "test_harness"
|
||||
path = "src/bin.rs"
|
||||
|
||||
[lib]
|
||||
name = "test_harness"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[bench]]
|
||||
name = "state_transition"
|
||||
harness = false
|
||||
@ -18,6 +26,7 @@ beacon_chain = { path = "../../beacon_chain" }
|
||||
block_proposer = { path = "../../../eth2/block_proposer" }
|
||||
bls = { path = "../../../eth2/utils/bls" }
|
||||
boolean-bitfield = { path = "../../../eth2/utils/boolean-bitfield" }
|
||||
clap = "2.32.0"
|
||||
db = { path = "../../db" }
|
||||
parking_lot = "0.7"
|
||||
failure = "0.1"
|
||||
@ -33,3 +42,4 @@ serde_json = "1.0"
|
||||
slot_clock = { path = "../../../eth2/utils/slot_clock" }
|
||||
ssz = { path = "../../../eth2/utils/ssz" }
|
||||
types = { path = "../../../eth2/types" }
|
||||
yaml-rust = "0.4.2"
|
||||
|
150
beacon_node/beacon_chain/test_harness/README.md
Normal file
150
beacon_node/beacon_chain/test_harness/README.md
Normal file
@ -0,0 +1,150 @@
|
||||
# Test Harness
|
||||
|
||||
Provides a testing environment for the `BeaconChain`, `Attester` and `BlockProposer` objects.
|
||||
|
||||
This environment bypasses networking and client run-times and connects the `Attester` and `Proposer`
|
||||
directly to the `BeaconChain` via an `Arc`.
|
||||
|
||||
The `BeaconChainHarness` contains a single `BeaconChain` instance and many `ValidatorHarness`
|
||||
instances. All of the `ValidatorHarness` instances work to advance the `BeaconChain` by
|
||||
producing blocks and attestations.
|
||||
|
||||
The crate consists of a library and binary, examples for using both are
|
||||
described below.
|
||||
|
||||
## YAML
|
||||
|
||||
Both the library and the binary are capable of parsing tests from a YAML file,
|
||||
in fact this is the sole purpose of the binary.
|
||||
|
||||
You can find YAML test cases [here](specs/). An example is included below:
|
||||
|
||||
```yaml
|
||||
title: Validator Registry Tests
|
||||
summary: Tests deposit and slashing effects on validator registry.
|
||||
test_suite: validator_registry
|
||||
fork: tchaikovsky
|
||||
version: 1.0
|
||||
test_cases:
|
||||
- config:
|
||||
epoch_length: 64
|
||||
deposits_for_chain_start: 1000
|
||||
num_slots: 64
|
||||
skip_slots: [2, 3]
|
||||
deposits:
|
||||
# At slot 1, create a new validator deposit of 32 ETH.
|
||||
- slot: 1
|
||||
amount: 32
|
||||
# Trigger more deposits...
|
||||
- slot: 3
|
||||
amount: 32
|
||||
- slot: 5
|
||||
amount: 32
|
||||
proposer_slashings:
|
||||
# At slot 2, trigger a proposer slashing for validator #42.
|
||||
- slot: 2
|
||||
validator_index: 42
|
||||
# Trigger another slashing...
|
||||
- slot: 8
|
||||
validator_index: 13
|
||||
attester_slashings:
|
||||
# At slot 2, trigger an attester slashing for validators #11 and #12.
|
||||
- slot: 2
|
||||
validator_indices: [11, 12]
|
||||
# Trigger another slashing...
|
||||
- slot: 5
|
||||
validator_indices: [14]
|
||||
results:
|
||||
num_skipped_slots: 2
|
||||
states:
|
||||
- slot: 63
|
||||
num_validators: 1003
|
||||
slashed_validators: [11, 12, 13, 14, 42]
|
||||
exited_validators: []
|
||||
|
||||
```
|
||||
|
||||
Thanks to [prsym](http://github.com/prysmaticlabs/prysm) for coming up with the
|
||||
base YAML format.
|
||||
|
||||
### Notes
|
||||
|
||||
Wherever `slot` is used, it is actually the "slot height", or slots since
|
||||
genesis. This allows the tests to disregard the `GENESIS_EPOCH`.
|
||||
|
||||
### Differences from Prysmatic's format
|
||||
|
||||
1. The detail for `deposits`, `proposer_slashings` and `attester_slashings` is
|
||||
ommitted from the test specification. It assumed they should be valid
|
||||
objects.
|
||||
2. There is a `states` list in `results` that runs checks against any state
|
||||
specified by a `slot` number. This is in contrast to the variables in
|
||||
`results` that assume the last (highest) state should be inspected.
|
||||
|
||||
#### Reasoning
|
||||
|
||||
Respective reasonings for above changes:
|
||||
|
||||
1. This removes the concerns of the actual object structure from the tests.
|
||||
This allows for more variation in the deposits/slashings objects without
|
||||
needing to update the tests. Also, it makes it makes it easier to create
|
||||
tests.
|
||||
2. This gives more fine-grained control over the tests. It allows for checking
|
||||
that certain events happened at certain times whilst making the tests only
|
||||
slightly more verbose.
|
||||
|
||||
_Notes: it may be useful to add an extra field to each slashing type to
|
||||
indicate if it should be valid or not. It also may be useful to add an option
|
||||
for double-vote/surround-vote attester slashings. The `amount` field was left
|
||||
on `deposits` as it changes the behaviour of state significantly._
|
||||
|
||||
## Binary Usage Example
|
||||
|
||||
Follow these steps to run as a binary:
|
||||
|
||||
1. Navigate to the root of this crate (where this readme is located)
|
||||
2. Run `$ cargo run --release -- --yaml examples/validator_registry.yaml`
|
||||
|
||||
_Note: the `--release` flag builds the binary without all the debugging
|
||||
instrumentation. The test is much faster built using `--release`. As is
|
||||
customary in cargo, the flags before `--` are passed to cargo and the flags
|
||||
after are passed to the binary._
|
||||
|
||||
### CLI Options
|
||||
|
||||
```
|
||||
Lighthouse Test Harness Runner 0.0.1
|
||||
Sigma Prime <contact@sigmaprime.io>
|
||||
Runs `test_harness` using a YAML test_case.
|
||||
|
||||
USAGE:
|
||||
test_harness --log-level <LOG_LEVEL> --yaml <FILE>
|
||||
|
||||
FLAGS:
|
||||
-h, --help Prints help information
|
||||
-V, --version Prints version information
|
||||
|
||||
OPTIONS:
|
||||
--log-level <LOG_LEVEL> Logging level. [default: debug] [possible values: error, warn, info, debug, trace]
|
||||
--yaml <FILE> YAML file test_case.
|
||||
```
|
||||
|
||||
|
||||
## Library Usage Example
|
||||
|
||||
```rust
|
||||
use test_harness::BeaconChainHarness;
|
||||
use types::ChainSpec;
|
||||
|
||||
let validator_count = 8;
|
||||
let spec = ChainSpec::few_validators();
|
||||
|
||||
let mut harness = BeaconChainHarness::new(spec, validator_count);
|
||||
|
||||
harness.advance_chain_with_block();
|
||||
|
||||
let chain = harness.chain_dump().unwrap();
|
||||
|
||||
// One block should have been built on top of the genesis block.
|
||||
assert_eq!(chain.len(), 2);
|
||||
```
|
@ -0,0 +1,42 @@
|
||||
title: Validator Registry Tests
|
||||
summary: Tests deposit and slashing effects on validator registry.
|
||||
test_suite: validator_registry
|
||||
fork: tchaikovsky
|
||||
version: 1.0
|
||||
test_cases:
|
||||
- config:
|
||||
epoch_length: 64
|
||||
deposits_for_chain_start: 1000
|
||||
num_slots: 64
|
||||
skip_slots: [2, 3]
|
||||
deposits:
|
||||
# At slot 1, create a new validator deposit of 32 ETH.
|
||||
- slot: 1
|
||||
amount: 32
|
||||
# Trigger more deposits...
|
||||
- slot: 3
|
||||
amount: 32
|
||||
- slot: 5
|
||||
amount: 32
|
||||
proposer_slashings:
|
||||
# At slot 2, trigger a proposer slashing for validator #42.
|
||||
- slot: 2
|
||||
validator_index: 42
|
||||
# Trigger another slashing...
|
||||
- slot: 8
|
||||
validator_index: 13
|
||||
attester_slashings:
|
||||
# At slot 2, trigger an attester slashing for validators #11 and #12.
|
||||
- slot: 2
|
||||
validator_indices: [11, 12]
|
||||
# Trigger another slashing...
|
||||
- slot: 5
|
||||
validator_indices: [14]
|
||||
results:
|
||||
num_skipped_slots: 2
|
||||
states:
|
||||
- slot: 63
|
||||
num_validators: 1003
|
||||
slashed_validators: [11, 12, 13, 14, 42]
|
||||
exited_validators: []
|
||||
|
@ -11,14 +11,9 @@ use log::debug;
|
||||
use rayon::prelude::*;
|
||||
use slot_clock::TestingSlotClock;
|
||||
use std::collections::HashSet;
|
||||
use std::fs::File;
|
||||
use std::io::prelude::*;
|
||||
use std::iter::FromIterator;
|
||||
use std::sync::Arc;
|
||||
use types::{
|
||||
BeaconBlock, ChainSpec, Deposit, DepositData, DepositInput, Eth1Data, FreeAttestation, Hash256,
|
||||
Keypair, Slot,
|
||||
};
|
||||
use types::*;
|
||||
|
||||
/// The beacon chain harness simulates a single beacon node with `validator_count` validators connected
|
||||
/// to it. Each validator is provided a borrow to the beacon chain, where it may read
|
||||
@ -245,6 +240,59 @@ impl BeaconChainHarness {
|
||||
debug!("Free attestations processed.");
|
||||
}
|
||||
|
||||
/// Signs a message using some validators secret key with the `Fork` info from the latest state
|
||||
/// of the `BeaconChain`.
|
||||
///
|
||||
/// Useful for producing slashable messages and other objects that `BeaconChainHarness` does
|
||||
/// not produce naturally.
|
||||
pub fn validator_sign(
|
||||
&self,
|
||||
validator_index: usize,
|
||||
message: &[u8],
|
||||
epoch: Epoch,
|
||||
domain_type: u64,
|
||||
) -> Option<Signature> {
|
||||
let validator = self.validators.get(validator_index)?;
|
||||
|
||||
let domain = self
|
||||
.beacon_chain
|
||||
.state
|
||||
.read()
|
||||
.fork
|
||||
.get_domain(epoch, domain_type);
|
||||
|
||||
Some(Signature::new(message, domain, &validator.keypair.sk))
|
||||
}
|
||||
|
||||
/// Submit a deposit to the `BeaconChain` and, if given a keypair, create a new
|
||||
/// `ValidatorHarness` instance for this validator.
|
||||
///
|
||||
/// If a new `ValidatorHarness` was created, the validator should become fully operational as
|
||||
/// if the validator were created during `BeaconChainHarness` instantiation.
|
||||
pub fn add_deposit(&mut self, deposit: Deposit, keypair: Option<Keypair>) {
|
||||
self.beacon_chain.receive_deposit_for_inclusion(deposit);
|
||||
|
||||
// If a keypair is present, add a new `ValidatorHarness` to the rig.
|
||||
if let Some(keypair) = keypair {
|
||||
let validator =
|
||||
ValidatorHarness::new(keypair, self.beacon_chain.clone(), self.spec.clone());
|
||||
self.validators.push(validator);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
.receive_proposer_slashing_for_inclusion(proposer_slashing);
|
||||
}
|
||||
|
||||
/// Submit an attester slashing to the `BeaconChain` for inclusion in some block.
|
||||
pub fn add_attester_slashing(&mut self, attester_slashing: AttesterSlashing) {
|
||||
self.beacon_chain
|
||||
.receive_attester_slashing_for_inclusion(attester_slashing);
|
||||
}
|
||||
|
||||
/// Executes the fork choice rule on the `BeaconChain`, selecting a new canonical head.
|
||||
pub fn run_fork_choice(&mut self) {
|
||||
self.beacon_chain.fork_choice().unwrap()
|
||||
}
|
||||
@ -253,12 +301,4 @@ impl BeaconChainHarness {
|
||||
pub fn chain_dump(&self) -> Result<Vec<CheckPoint>, BeaconChainError> {
|
||||
self.beacon_chain.chain_dump()
|
||||
}
|
||||
|
||||
/// Write the output of `chain_dump` to a JSON file.
|
||||
pub fn dump_to_file(&self, filename: String, chain_dump: &[CheckPoint]) {
|
||||
let json = serde_json::to_string(chain_dump).unwrap();
|
||||
let mut file = File::create(filename).unwrap();
|
||||
file.write_all(json.as_bytes())
|
||||
.expect("Failed writing dump to file.");
|
||||
}
|
||||
}
|
||||
|
69
beacon_node/beacon_chain/test_harness/src/bin.rs
Normal file
69
beacon_node/beacon_chain/test_harness/src/bin.rs
Normal file
@ -0,0 +1,69 @@
|
||||
use clap::{App, Arg};
|
||||
use env_logger::{Builder, Env};
|
||||
use std::{fs::File, io::prelude::*};
|
||||
use test_case::TestCase;
|
||||
use yaml_rust::YamlLoader;
|
||||
|
||||
mod beacon_chain_harness;
|
||||
mod test_case;
|
||||
mod validator_harness;
|
||||
|
||||
use validator_harness::ValidatorHarness;
|
||||
|
||||
fn main() {
|
||||
let matches = App::new("Lighthouse Test Harness Runner")
|
||||
.version("0.0.1")
|
||||
.author("Sigma Prime <contact@sigmaprime.io>")
|
||||
.about("Runs `test_harness` using a YAML test_case.")
|
||||
.arg(
|
||||
Arg::with_name("yaml")
|
||||
.long("yaml")
|
||||
.value_name("FILE")
|
||||
.help("YAML file test_case.")
|
||||
.required(true),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("log")
|
||||
.long("log-level")
|
||||
.value_name("LOG_LEVEL")
|
||||
.help("Logging level.")
|
||||
.possible_values(&["error", "warn", "info", "debug", "trace"])
|
||||
.default_value("debug")
|
||||
.required(true),
|
||||
)
|
||||
.get_matches();
|
||||
|
||||
if let Some(log_level) = matches.value_of("log") {
|
||||
Builder::from_env(Env::default().default_filter_or(log_level)).init();
|
||||
}
|
||||
|
||||
if let Some(yaml_file) = matches.value_of("yaml") {
|
||||
let docs = {
|
||||
let mut file = File::open(yaml_file).unwrap();
|
||||
|
||||
let mut yaml_str = String::new();
|
||||
file.read_to_string(&mut yaml_str).unwrap();
|
||||
|
||||
YamlLoader::load_from_str(&yaml_str).unwrap()
|
||||
};
|
||||
|
||||
for doc in &docs {
|
||||
// For each `test_cases` YAML in the document, build a `TestCase`, execute it and
|
||||
// assert that the execution result matches the test_case description.
|
||||
//
|
||||
// In effect, for each `test_case` a new `BeaconChainHarness` is created from genesis
|
||||
// and a new `BeaconChain` is built as per the test_case.
|
||||
//
|
||||
// After the `BeaconChain` has been built out as per the test_case, a dump of all blocks
|
||||
// and states in the chain is obtained and checked against the `results` specified in
|
||||
// the `test_case`.
|
||||
//
|
||||
// If any of the expectations in the results are not met, the process
|
||||
// panics with a message.
|
||||
for test_case in doc["test_cases"].as_vec().unwrap() {
|
||||
let test_case = TestCase::from_yaml(test_case);
|
||||
test_case.assert_result_valid(test_case.execute())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,32 @@
|
||||
//! Provides a testing environment for the `BeaconChain`, `Attester` and `BlockProposer` objects.
|
||||
//!
|
||||
//! This environment bypasses networking and client run-times and connects the `Attester` and `Proposer`
|
||||
//! directly to the `BeaconChain` via an `Arc`.
|
||||
//!
|
||||
//! The `BeaconChainHarness` contains a single `BeaconChain` instance and many `ValidatorHarness`
|
||||
//! instances. All of the `ValidatorHarness` instances work to advance the `BeaconChain` by
|
||||
//! producing blocks and attestations.
|
||||
//!
|
||||
//! Example:
|
||||
//! ```
|
||||
//! use test_harness::BeaconChainHarness;
|
||||
//! use types::ChainSpec;
|
||||
//!
|
||||
//! let validator_count = 8;
|
||||
//! let spec = ChainSpec::few_validators();
|
||||
//!
|
||||
//! let mut harness = BeaconChainHarness::new(spec, validator_count);
|
||||
//!
|
||||
//! harness.advance_chain_with_block();
|
||||
//!
|
||||
//! let chain = harness.chain_dump().unwrap();
|
||||
//!
|
||||
//! // One block should have been built on top of the genesis block.
|
||||
//! assert_eq!(chain.len(), 2);
|
||||
//! ```
|
||||
|
||||
mod beacon_chain_harness;
|
||||
pub mod test_case;
|
||||
mod validator_harness;
|
||||
|
||||
pub use self::beacon_chain_harness::BeaconChainHarness;
|
||||
|
109
beacon_node/beacon_chain/test_harness/src/test_case/config.rs
Normal file
109
beacon_node/beacon_chain/test_harness/src/test_case/config.rs
Normal file
@ -0,0 +1,109 @@
|
||||
use super::yaml_helpers::{as_u64, as_usize, as_vec_u64};
|
||||
use bls::create_proof_of_possession;
|
||||
use types::*;
|
||||
use yaml_rust::Yaml;
|
||||
|
||||
pub type DepositTuple = (u64, Deposit, Keypair);
|
||||
pub type ProposerSlashingTuple = (u64, u64);
|
||||
pub type AttesterSlashingTuple = (u64, Vec<u64>);
|
||||
|
||||
/// Defines the execution of a `BeaconStateHarness` across a series of slots.
|
||||
#[derive(Debug)]
|
||||
pub struct Config {
|
||||
/// Initial validators.
|
||||
pub deposits_for_chain_start: usize,
|
||||
/// Number of slots in an epoch.
|
||||
pub epoch_length: Option<u64>,
|
||||
/// Number of slots to build before ending execution.
|
||||
pub num_slots: u64,
|
||||
/// Number of slots that should be skipped due to inactive validator.
|
||||
pub skip_slots: Option<Vec<u64>>,
|
||||
/// Deposits to be included during execution.
|
||||
pub deposits: Option<Vec<DepositTuple>>,
|
||||
/// Proposer slashings to be included during execution.
|
||||
pub proposer_slashings: Option<Vec<ProposerSlashingTuple>>,
|
||||
/// Attester slashings to be including during execution.
|
||||
pub attester_slashings: Option<Vec<AttesterSlashingTuple>>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Load from a YAML document.
|
||||
///
|
||||
/// Expects to receive the `config` section of the document.
|
||||
pub fn from_yaml(yaml: &Yaml) -> Self {
|
||||
Self {
|
||||
deposits_for_chain_start: as_usize(&yaml, "deposits_for_chain_start")
|
||||
.expect("Must specify validator count"),
|
||||
epoch_length: as_u64(&yaml, "epoch_length"),
|
||||
num_slots: as_u64(&yaml, "num_slots").expect("Must specify `config.num_slots`"),
|
||||
skip_slots: as_vec_u64(yaml, "skip_slots"),
|
||||
deposits: parse_deposits(&yaml),
|
||||
proposer_slashings: parse_proposer_slashings(&yaml),
|
||||
attester_slashings: parse_attester_slashings(&yaml),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse the `attester_slashings` section of the YAML document.
|
||||
fn parse_attester_slashings(yaml: &Yaml) -> Option<Vec<AttesterSlashingTuple>> {
|
||||
let mut slashings = vec![];
|
||||
|
||||
for slashing in yaml["attester_slashings"].as_vec()? {
|
||||
let slot = as_u64(slashing, "slot").expect("Incomplete attester_slashing (slot)");
|
||||
let validator_indices = as_vec_u64(slashing, "validator_indices")
|
||||
.expect("Incomplete attester_slashing (validator_indices)");
|
||||
|
||||
slashings.push((slot, validator_indices));
|
||||
}
|
||||
|
||||
Some(slashings)
|
||||
}
|
||||
|
||||
/// Parse the `proposer_slashings` section of the YAML document.
|
||||
fn parse_proposer_slashings(yaml: &Yaml) -> Option<Vec<ProposerSlashingTuple>> {
|
||||
let mut slashings = vec![];
|
||||
|
||||
for slashing in yaml["proposer_slashings"].as_vec()? {
|
||||
let slot = as_u64(slashing, "slot").expect("Incomplete proposer slashing (slot)_");
|
||||
let validator_index = as_u64(slashing, "validator_index")
|
||||
.expect("Incomplete proposer slashing (validator_index)");
|
||||
|
||||
slashings.push((slot, validator_index));
|
||||
}
|
||||
|
||||
Some(slashings)
|
||||
}
|
||||
|
||||
/// Parse the `deposits` section of the YAML document.
|
||||
fn parse_deposits(yaml: &Yaml) -> Option<Vec<DepositTuple>> {
|
||||
let mut deposits = vec![];
|
||||
|
||||
for deposit in yaml["deposits"].as_vec()? {
|
||||
let keypair = Keypair::random();
|
||||
let proof_of_possession = create_proof_of_possession(&keypair);
|
||||
|
||||
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 deposit = Deposit {
|
||||
// Note: `branch` and `index` will need to be updated once the spec defines their
|
||||
// validity.
|
||||
branch: vec![],
|
||||
index: 0,
|
||||
deposit_data: DepositData {
|
||||
amount,
|
||||
timestamp: 1,
|
||||
deposit_input: DepositInput {
|
||||
pubkey: keypair.pk.clone(),
|
||||
withdrawal_credentials: Hash256::zero(),
|
||||
proof_of_possession,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
deposits.push((slot, deposit, keypair));
|
||||
}
|
||||
|
||||
Some(deposits)
|
||||
}
|
215
beacon_node/beacon_chain/test_harness/src/test_case/mod.rs
Normal file
215
beacon_node/beacon_chain/test_harness/src/test_case/mod.rs
Normal file
@ -0,0 +1,215 @@
|
||||
//! Defines execution and testing specs for a `BeaconChainHarness` instance. Supports loading from
|
||||
//! a YAML file.
|
||||
|
||||
use crate::beacon_chain_harness::BeaconChainHarness;
|
||||
use beacon_chain::CheckPoint;
|
||||
use log::{info, warn};
|
||||
use types::*;
|
||||
use types::{
|
||||
attester_slashing::AttesterSlashingBuilder, proposer_slashing::ProposerSlashingBuilder,
|
||||
};
|
||||
use yaml_rust::Yaml;
|
||||
|
||||
mod config;
|
||||
mod results;
|
||||
mod state_check;
|
||||
mod yaml_helpers;
|
||||
|
||||
pub use config::Config;
|
||||
pub use results::Results;
|
||||
pub use state_check::StateCheck;
|
||||
|
||||
/// Defines the execution and testing of a `BeaconChainHarness` instantiation.
|
||||
///
|
||||
/// Typical workflow is:
|
||||
///
|
||||
/// 1. Instantiate the `TestCase` from YAML: `let test_case = TestCase::from_yaml(&my_yaml);`
|
||||
/// 2. Execute the test_case: `let result = test_case.execute();`
|
||||
/// 3. Test the results against the test_case: `test_case.assert_result_valid(result);`
|
||||
#[derive(Debug)]
|
||||
pub struct TestCase {
|
||||
/// Defines the execution.
|
||||
pub config: Config,
|
||||
/// Defines tests to run against the execution result.
|
||||
pub results: Results,
|
||||
}
|
||||
|
||||
/// The result of executing a `TestCase`.
|
||||
///
|
||||
pub struct ExecutionResult {
|
||||
/// The canonical beacon chain generated from the execution.
|
||||
pub chain: Vec<CheckPoint>,
|
||||
/// The spec used for execution.
|
||||
pub spec: ChainSpec,
|
||||
}
|
||||
|
||||
impl TestCase {
|
||||
/// Load the test case from a YAML document.
|
||||
pub fn from_yaml(test_case: &Yaml) -> Self {
|
||||
Self {
|
||||
results: Results::from_yaml(&test_case["results"]),
|
||||
config: Config::from_yaml(&test_case["config"]),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a `ChainSpec::foundation()`.
|
||||
///
|
||||
/// If specified in `config`, returns it with a modified `epoch_length`.
|
||||
fn spec(&self) -> ChainSpec {
|
||||
let mut spec = ChainSpec::foundation();
|
||||
|
||||
if let Some(n) = self.config.epoch_length {
|
||||
spec.epoch_length = n;
|
||||
}
|
||||
|
||||
spec
|
||||
}
|
||||
|
||||
/// Executes the test case, returning an `ExecutionResult`.
|
||||
pub fn execute(&self) -> ExecutionResult {
|
||||
let spec = self.spec();
|
||||
let validator_count = self.config.deposits_for_chain_start;
|
||||
let slots = self.config.num_slots;
|
||||
|
||||
info!(
|
||||
"Building BeaconChainHarness with {} validators...",
|
||||
validator_count
|
||||
);
|
||||
|
||||
let mut harness = BeaconChainHarness::new(spec, validator_count);
|
||||
|
||||
info!("Starting simulation across {} slots...", slots);
|
||||
|
||||
// -1 slots because genesis counts as a slot.
|
||||
for slot_height in 0..slots - 1 {
|
||||
// Feed deposits to the BeaconChain.
|
||||
if let Some(ref deposits) = self.config.deposits {
|
||||
for (slot, deposit, keypair) in deposits {
|
||||
if *slot == slot_height {
|
||||
info!("Including deposit at slot height {}.", slot_height);
|
||||
harness.add_deposit(deposit.clone(), Some(keypair.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Feed proposer slashings to the BeaconChain.
|
||||
if let Some(ref slashings) = self.config.proposer_slashings {
|
||||
for (slot, validator_index) in slashings {
|
||||
if *slot == slot_height {
|
||||
info!(
|
||||
"Including proposer slashing at slot height {} for validator #{}.",
|
||||
slot_height, validator_index
|
||||
);
|
||||
let slashing = build_proposer_slashing(&harness, *validator_index);
|
||||
harness.add_proposer_slashing(slashing);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Feed attester slashings to the BeaconChain.
|
||||
if let Some(ref slashings) = self.config.attester_slashings {
|
||||
for (slot, validator_indices) in slashings {
|
||||
if *slot == slot_height {
|
||||
info!(
|
||||
"Including attester slashing at slot height {} for validators {:?}.",
|
||||
slot_height, validator_indices
|
||||
);
|
||||
let slashing =
|
||||
build_double_vote_attester_slashing(&harness, &validator_indices[..]);
|
||||
harness.add_attester_slashing(slashing);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build a block or skip a slot.
|
||||
match self.config.skip_slots {
|
||||
Some(ref skip_slots) if skip_slots.contains(&slot_height) => {
|
||||
warn!("Skipping slot at height {}.", slot_height);
|
||||
harness.increment_beacon_chain_slot();
|
||||
}
|
||||
_ => {
|
||||
info!("Producing block at slot height {}.", slot_height);
|
||||
harness.advance_chain_with_block();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
harness.run_fork_choice();
|
||||
|
||||
info!("Test execution complete!");
|
||||
|
||||
info!("Building chain dump for analysis...");
|
||||
|
||||
ExecutionResult {
|
||||
chain: harness.chain_dump().expect("Chain dump failed."),
|
||||
spec: (*harness.spec).clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks that the `ExecutionResult` is consistent with the specifications in `self.results`.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics with a message if any result does not match exepectations.
|
||||
pub fn assert_result_valid(&self, execution_result: ExecutionResult) {
|
||||
info!("Verifying test results...");
|
||||
let spec = &execution_result.spec;
|
||||
|
||||
if let Some(num_skipped_slots) = self.results.num_skipped_slots {
|
||||
assert_eq!(
|
||||
execution_result.chain.len(),
|
||||
self.config.num_slots as usize - num_skipped_slots,
|
||||
"actual skipped slots != expected."
|
||||
);
|
||||
info!(
|
||||
"OK: Chain length is {} ({} skipped slots).",
|
||||
execution_result.chain.len(),
|
||||
num_skipped_slots
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(ref state_checks) = self.results.state_checks {
|
||||
for checkpoint in &execution_result.chain {
|
||||
let state = &checkpoint.beacon_state;
|
||||
|
||||
for state_check in state_checks {
|
||||
let adjusted_state_slot =
|
||||
state.slot - spec.genesis_epoch.start_slot(spec.epoch_length);
|
||||
|
||||
if state_check.slot == adjusted_state_slot {
|
||||
state_check.assert_valid(state, spec);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds an `AttesterSlashing` for some `validator_indices`.
|
||||
///
|
||||
/// Signs the message using a `BeaconChainHarness`.
|
||||
fn build_double_vote_attester_slashing(
|
||||
harness: &BeaconChainHarness,
|
||||
validator_indices: &[u64],
|
||||
) -> AttesterSlashing {
|
||||
let signer = |validator_index: u64, message: &[u8], epoch: Epoch, domain: u64| {
|
||||
harness
|
||||
.validator_sign(validator_index as usize, message, epoch, domain)
|
||||
.expect("Unable to sign AttesterSlashing")
|
||||
};
|
||||
|
||||
AttesterSlashingBuilder::double_vote(validator_indices, signer, &harness.spec)
|
||||
}
|
||||
|
||||
/// Builds an `ProposerSlashing` for some `validator_index`.
|
||||
///
|
||||
/// Signs the message using a `BeaconChainHarness`.
|
||||
fn build_proposer_slashing(harness: &BeaconChainHarness, validator_index: u64) -> ProposerSlashing {
|
||||
let signer = |validator_index: u64, message: &[u8], epoch: Epoch, domain: u64| {
|
||||
harness
|
||||
.validator_sign(validator_index as usize, message, epoch, domain)
|
||||
.expect("Unable to sign AttesterSlashing")
|
||||
};
|
||||
|
||||
ProposerSlashingBuilder::double_vote(validator_index, signer, &harness.spec)
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
use super::state_check::StateCheck;
|
||||
use super::yaml_helpers::as_usize;
|
||||
use yaml_rust::Yaml;
|
||||
|
||||
/// A series of tests to be carried out upon an `ExecutionResult`, returned from executing a
|
||||
/// `TestCase`.
|
||||
#[derive(Debug)]
|
||||
pub struct Results {
|
||||
pub num_skipped_slots: Option<usize>,
|
||||
pub state_checks: Option<Vec<StateCheck>>,
|
||||
}
|
||||
|
||||
impl Results {
|
||||
/// Load from a YAML document.
|
||||
///
|
||||
/// Expects the `results` section of the YAML document.
|
||||
pub fn from_yaml(yaml: &Yaml) -> Self {
|
||||
Self {
|
||||
num_skipped_slots: as_usize(yaml, "num_skipped_slots"),
|
||||
state_checks: parse_state_checks(yaml),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse the `state_checks` section of the YAML document.
|
||||
fn parse_state_checks(yaml: &Yaml) -> Option<Vec<StateCheck>> {
|
||||
let mut states = vec![];
|
||||
|
||||
for state_yaml in yaml["states"].as_vec()? {
|
||||
states.push(StateCheck::from_yaml(state_yaml));
|
||||
}
|
||||
|
||||
Some(states)
|
||||
}
|
@ -0,0 +1,98 @@
|
||||
use super::yaml_helpers::{as_u64, as_usize, as_vec_u64};
|
||||
use log::info;
|
||||
use types::*;
|
||||
use yaml_rust::Yaml;
|
||||
|
||||
/// Tests to be conducted upon a `BeaconState` object generated during the execution of a
|
||||
/// `TestCase`.
|
||||
#[derive(Debug)]
|
||||
pub struct StateCheck {
|
||||
/// Checked against `beacon_state.slot`.
|
||||
pub slot: Slot,
|
||||
/// Checked against `beacon_state.validator_registry.len()`.
|
||||
pub num_validators: Option<usize>,
|
||||
/// A list of validator indices which have been penalized. Must be in ascending order.
|
||||
pub slashed_validators: Option<Vec<u64>>,
|
||||
/// A list of validator indices which have been exited. Must be in ascending order.
|
||||
pub exited_validators: Option<Vec<u64>>,
|
||||
}
|
||||
|
||||
impl StateCheck {
|
||||
/// Load from a YAML document.
|
||||
///
|
||||
/// Expects the `state_check` section of the YAML document.
|
||||
pub fn from_yaml(yaml: &Yaml) -> Self {
|
||||
Self {
|
||||
slot: Slot::from(as_u64(&yaml, "slot").expect("State must specify slot")),
|
||||
num_validators: as_usize(&yaml, "num_validators"),
|
||||
slashed_validators: as_vec_u64(&yaml, "slashed_validators"),
|
||||
exited_validators: as_vec_u64(&yaml, "exited_validators"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs all checks against a `BeaconState`
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics with an error message if any test fails.
|
||||
pub fn assert_valid(&self, state: &BeaconState, spec: &ChainSpec) {
|
||||
let state_epoch = state.slot.epoch(spec.epoch_length);
|
||||
|
||||
info!("Running state check for slot height {}.", self.slot);
|
||||
|
||||
assert_eq!(
|
||||
self.slot,
|
||||
state.slot - spec.genesis_epoch.start_slot(spec.epoch_length),
|
||||
"State slot is invalid."
|
||||
);
|
||||
|
||||
if let Some(num_validators) = self.num_validators {
|
||||
assert_eq!(
|
||||
state.validator_registry.len(),
|
||||
num_validators,
|
||||
"State validator count != expected."
|
||||
);
|
||||
info!("OK: num_validators = {}.", num_validators);
|
||||
}
|
||||
|
||||
if let Some(ref slashed_validators) = self.slashed_validators {
|
||||
let actually_slashed_validators: Vec<u64> = state
|
||||
.validator_registry
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, validator)| {
|
||||
if validator.is_penalized_at(state_epoch) {
|
||||
Some(i as u64)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(
|
||||
actually_slashed_validators, *slashed_validators,
|
||||
"Slashed validators != expected."
|
||||
);
|
||||
info!("OK: slashed_validators = {:?}.", slashed_validators);
|
||||
}
|
||||
|
||||
if let Some(ref exited_validators) = self.exited_validators {
|
||||
let actually_exited_validators: Vec<u64> = state
|
||||
.validator_registry
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, validator)| {
|
||||
if validator.is_exited_at(state_epoch) {
|
||||
Some(i as u64)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(
|
||||
actually_exited_validators, *exited_validators,
|
||||
"Exited validators != expected."
|
||||
);
|
||||
info!("OK: exited_validators = {:?}.", exited_validators);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
use yaml_rust::Yaml;
|
||||
|
||||
pub fn as_usize(yaml: &Yaml, key: &str) -> Option<usize> {
|
||||
yaml[key].as_i64().and_then(|n| Some(n as usize))
|
||||
}
|
||||
|
||||
pub fn as_u64(yaml: &Yaml, key: &str) -> Option<u64> {
|
||||
yaml[key].as_i64().and_then(|n| Some(n as u64))
|
||||
}
|
||||
|
||||
pub fn as_vec_u64(yaml: &Yaml, key: &str) -> Option<Vec<u64>> {
|
||||
yaml[key].clone().into_vec().and_then(|vec| {
|
||||
Some(
|
||||
vec.iter()
|
||||
.map(|item| item.as_i64().unwrap() as u64)
|
||||
.collect(),
|
||||
)
|
||||
})
|
||||
}
|
@ -1,27 +1,16 @@
|
||||
use attester::Signer as AttesterSigner;
|
||||
use block_proposer::Signer as BlockProposerSigner;
|
||||
use std::sync::RwLock;
|
||||
use types::{Keypair, Signature};
|
||||
|
||||
/// A test-only struct used to perform signing for a proposer or attester.
|
||||
pub struct LocalSigner {
|
||||
keypair: Keypair,
|
||||
should_sign: RwLock<bool>,
|
||||
}
|
||||
|
||||
impl LocalSigner {
|
||||
/// Produce a new TestSigner with signing enabled by default.
|
||||
pub fn new(keypair: Keypair) -> Self {
|
||||
Self {
|
||||
keypair,
|
||||
should_sign: RwLock::new(true),
|
||||
}
|
||||
}
|
||||
|
||||
/// If set to `false`, the service will refuse to sign all messages. Otherwise, all messages
|
||||
/// will be signed.
|
||||
pub fn enable_signing(&self, enabled: bool) {
|
||||
*self.should_sign.write().unwrap() = enabled;
|
||||
Self { keypair }
|
||||
}
|
||||
|
||||
/// Sign some message.
|
||||
|
@ -41,6 +41,4 @@ fn it_can_produce_past_first_epoch_boundary() {
|
||||
let dump = harness.chain_dump().expect("Chain dump failed.");
|
||||
|
||||
assert_eq!(dump.len() as u64, blocks + 1); // + 1 for genesis block.
|
||||
|
||||
harness.dump_to_file("/tmp/chaindump.json".to_string(), &dump);
|
||||
}
|
||||
|
@ -1,19 +1,14 @@
|
||||
use self::verify_slashable_attestation::verify_slashable_attestation;
|
||||
use crate::SlotProcessingError;
|
||||
use hashing::hash;
|
||||
use int_to_bytes::int_to_bytes32;
|
||||
use log::{debug, trace};
|
||||
use ssz::{ssz_encode, TreeHash};
|
||||
use types::{
|
||||
AggregatePublicKey, Attestation, BeaconBlock, BeaconState, BeaconStateError, ChainSpec,
|
||||
Crosslink, Epoch, Exit, Fork, Hash256, PendingAttestation, PublicKey, RelativeEpoch, Signature,
|
||||
};
|
||||
use types::*;
|
||||
|
||||
mod verify_slashable_attestation;
|
||||
|
||||
// TODO: define elsehwere.
|
||||
const DOMAIN_PROPOSAL: u64 = 2;
|
||||
const DOMAIN_EXIT: u64 = 3;
|
||||
const DOMAIN_RANDAO: u64 = 4;
|
||||
const PHASE_0_CUSTODY_BIT: bool = false;
|
||||
const DOMAIN_ATTESTATION: u64 = 1;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum Error {
|
||||
@ -31,10 +26,13 @@ pub enum Error {
|
||||
BadRandaoSignature,
|
||||
MaxProposerSlashingsExceeded,
|
||||
BadProposerSlashing,
|
||||
MaxAttesterSlashingsExceed,
|
||||
MaxAttestationsExceeded,
|
||||
BadAttesterSlashing,
|
||||
InvalidAttestation(AttestationValidationError),
|
||||
NoBlockRoot,
|
||||
MaxDepositsExceeded,
|
||||
BadDeposit,
|
||||
MaxExitsExceeded,
|
||||
BadExit,
|
||||
BadCustodyReseeds,
|
||||
@ -89,7 +87,7 @@ impl BlockProcessable for BeaconState {
|
||||
}
|
||||
|
||||
fn per_block_processing_signature_optional(
|
||||
state: &mut BeaconState,
|
||||
mut state: &mut BeaconState,
|
||||
block: &BeaconBlock,
|
||||
verify_block_signature: bool,
|
||||
spec: &ChainSpec,
|
||||
@ -113,7 +111,7 @@ fn per_block_processing_signature_optional(
|
||||
&block_proposer.pubkey,
|
||||
&block.proposal_root(spec)[..],
|
||||
&block.signature,
|
||||
get_domain(&state.fork, state.current_epoch(spec), DOMAIN_PROPOSAL)
|
||||
get_domain(&state.fork, state.current_epoch(spec), spec.domain_proposal)
|
||||
),
|
||||
Error::BadBlockSignature
|
||||
);
|
||||
@ -127,7 +125,7 @@ fn per_block_processing_signature_optional(
|
||||
&block_proposer.pubkey,
|
||||
&int_to_bytes32(state.current_epoch(spec).as_u64()),
|
||||
&block.randao_reveal,
|
||||
get_domain(&state.fork, state.current_epoch(spec), DOMAIN_RANDAO)
|
||||
get_domain(&state.fork, state.current_epoch(spec), spec.domain_randao)
|
||||
),
|
||||
Error::BadRandaoSignature
|
||||
);
|
||||
@ -188,7 +186,7 @@ fn per_block_processing_signature_optional(
|
||||
.proposal_data_1
|
||||
.slot
|
||||
.epoch(spec.epoch_length),
|
||||
DOMAIN_PROPOSAL
|
||||
spec.domain_proposal
|
||||
)
|
||||
),
|
||||
Error::BadProposerSlashing
|
||||
@ -204,7 +202,7 @@ fn per_block_processing_signature_optional(
|
||||
.proposal_data_2
|
||||
.slot
|
||||
.epoch(spec.epoch_length),
|
||||
DOMAIN_PROPOSAL
|
||||
spec.domain_proposal
|
||||
)
|
||||
),
|
||||
Error::BadProposerSlashing
|
||||
@ -212,6 +210,17 @@ fn per_block_processing_signature_optional(
|
||||
state.penalize_validator(proposer_slashing.proposer_index as usize, spec)?;
|
||||
}
|
||||
|
||||
/*
|
||||
* Attester slashings
|
||||
*/
|
||||
ensure!(
|
||||
block.body.attester_slashings.len() as u64 <= spec.max_attester_slashings,
|
||||
Error::MaxAttesterSlashingsExceed
|
||||
);
|
||||
for attester_slashing in &block.body.attester_slashings {
|
||||
verify_slashable_attestation(&mut state, &attester_slashing, spec)?;
|
||||
}
|
||||
|
||||
/*
|
||||
* Attestations
|
||||
*/
|
||||
@ -242,7 +251,27 @@ fn per_block_processing_signature_optional(
|
||||
Error::MaxDepositsExceeded
|
||||
);
|
||||
|
||||
// TODO: process deposits.
|
||||
// TODO: verify deposit merkle branches.
|
||||
for deposit in &block.body.deposits {
|
||||
debug!(
|
||||
"Processing deposit for pubkey {:?}",
|
||||
deposit.deposit_data.deposit_input.pubkey
|
||||
);
|
||||
state
|
||||
.process_deposit(
|
||||
deposit.deposit_data.deposit_input.pubkey.clone(),
|
||||
deposit.deposit_data.amount,
|
||||
deposit
|
||||
.deposit_data
|
||||
.deposit_input
|
||||
.proof_of_possession
|
||||
.clone(),
|
||||
deposit.deposit_data.deposit_input.withdrawal_credentials,
|
||||
None,
|
||||
spec,
|
||||
)
|
||||
.map_err(|_| Error::BadDeposit)?;
|
||||
}
|
||||
|
||||
/*
|
||||
* Exits
|
||||
@ -276,7 +305,7 @@ fn per_block_processing_signature_optional(
|
||||
&validator.pubkey,
|
||||
&exit_message,
|
||||
&exit.signature,
|
||||
get_domain(&state.fork, exit.epoch, DOMAIN_EXIT)
|
||||
get_domain(&state.fork, exit.epoch, spec.domain_exit)
|
||||
),
|
||||
Error::BadProposerSlashing
|
||||
);
|
||||
@ -370,11 +399,7 @@ fn validate_attestation_signature_optional(
|
||||
);
|
||||
let mut group_public_key = AggregatePublicKey::new();
|
||||
for participant in participants {
|
||||
group_public_key.add(
|
||||
state.validator_registry[participant as usize]
|
||||
.pubkey
|
||||
.as_raw(),
|
||||
)
|
||||
group_public_key.add(&state.validator_registry[participant as usize].pubkey)
|
||||
}
|
||||
ensure!(
|
||||
attestation.verify_signature(
|
||||
@ -383,7 +408,7 @@ fn validate_attestation_signature_optional(
|
||||
get_domain(
|
||||
&state.fork,
|
||||
attestation.data.slot.epoch(spec.epoch_length),
|
||||
DOMAIN_ATTESTATION,
|
||||
spec.domain_attestation,
|
||||
)
|
||||
),
|
||||
AttestationValidationError::BadSignature
|
||||
|
@ -0,0 +1,61 @@
|
||||
use super::Error;
|
||||
use types::*;
|
||||
|
||||
macro_rules! ensure {
|
||||
($condition: expr, $result: expr) => {
|
||||
if !$condition {
|
||||
return Err($result);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns `Ok(())` if some `AttesterSlashing` is valid to be included in some `BeaconState`,
|
||||
/// otherwise returns an `Err`.
|
||||
pub fn verify_slashable_attestation(
|
||||
state: &mut BeaconState,
|
||||
attester_slashing: &AttesterSlashing,
|
||||
spec: &ChainSpec,
|
||||
) -> Result<(), Error> {
|
||||
let slashable_attestation_1 = &attester_slashing.slashable_attestation_1;
|
||||
let slashable_attestation_2 = &attester_slashing.slashable_attestation_2;
|
||||
|
||||
ensure!(
|
||||
slashable_attestation_1.data != slashable_attestation_2.data,
|
||||
Error::BadAttesterSlashing
|
||||
);
|
||||
ensure!(
|
||||
slashable_attestation_1.is_double_vote(slashable_attestation_2, spec)
|
||||
| slashable_attestation_1.is_surround_vote(slashable_attestation_2, spec),
|
||||
Error::BadAttesterSlashing
|
||||
);
|
||||
ensure!(
|
||||
state.verify_slashable_attestation(&slashable_attestation_1, spec),
|
||||
Error::BadAttesterSlashing
|
||||
);
|
||||
ensure!(
|
||||
state.verify_slashable_attestation(&slashable_attestation_2, spec),
|
||||
Error::BadAttesterSlashing
|
||||
);
|
||||
|
||||
let mut slashable_indices = vec![];
|
||||
for i in &slashable_attestation_1.validator_indices {
|
||||
let validator = state
|
||||
.validator_registry
|
||||
.get(*i as usize)
|
||||
.ok_or_else(|| Error::BadAttesterSlashing)?;
|
||||
|
||||
if slashable_attestation_1.validator_indices.contains(&i)
|
||||
& !validator.is_penalized_at(state.current_epoch(spec))
|
||||
{
|
||||
slashable_indices.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
ensure!(!slashable_indices.is_empty(), Error::BadAttesterSlashing);
|
||||
|
||||
for i in slashable_indices {
|
||||
state.penalize_validator(*i as usize, spec)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
@ -4,6 +4,10 @@ use serde_derive::Serialize;
|
||||
use ssz_derive::{Decode, Encode, TreeHash};
|
||||
use test_random_derive::TestRandom;
|
||||
|
||||
mod builder;
|
||||
|
||||
pub use builder::AttesterSlashingBuilder;
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Serialize, Encode, Decode, TreeHash, TestRandom)]
|
||||
pub struct AttesterSlashing {
|
||||
pub slashable_attestation_1: SlashableAttestation,
|
||||
|
96
eth2/types/src/attester_slashing/builder.rs
Normal file
96
eth2/types/src/attester_slashing/builder.rs
Normal file
@ -0,0 +1,96 @@
|
||||
use crate::*;
|
||||
use ssz::TreeHash;
|
||||
|
||||
/// Builds an `AttesterSlashing`.
|
||||
pub struct AttesterSlashingBuilder();
|
||||
|
||||
impl AttesterSlashingBuilder {
|
||||
/// Builds an `AttesterSlashing` that is a double vote.
|
||||
///
|
||||
/// The `signer` function is used to sign the double-vote and accepts:
|
||||
///
|
||||
/// - `validator_index: u64`
|
||||
/// - `message: &[u8]`
|
||||
/// - `epoch: Epoch`
|
||||
/// - `domain: u64`
|
||||
///
|
||||
/// Where domain is a domain "constant" (e.g., `spec.domain_attestation`).
|
||||
pub fn double_vote<F>(
|
||||
validator_indices: &[u64],
|
||||
signer: F,
|
||||
spec: &ChainSpec,
|
||||
) -> AttesterSlashing
|
||||
where
|
||||
F: Fn(u64, &[u8], Epoch, u64) -> Signature,
|
||||
{
|
||||
let double_voted_slot = Slot::new(0);
|
||||
let shard = 0;
|
||||
let justified_epoch = Epoch::new(0);
|
||||
let epoch = Epoch::new(0);
|
||||
let hash_1 = Hash256::from(&[1][..]);
|
||||
let hash_2 = Hash256::from(&[2][..]);
|
||||
|
||||
let mut slashable_attestation_1 = SlashableAttestation {
|
||||
validator_indices: validator_indices.to_vec(),
|
||||
data: AttestationData {
|
||||
slot: double_voted_slot,
|
||||
shard,
|
||||
beacon_block_root: hash_1,
|
||||
epoch_boundary_root: hash_1,
|
||||
shard_block_root: hash_1,
|
||||
latest_crosslink: Crosslink {
|
||||
epoch,
|
||||
shard_block_root: hash_1,
|
||||
},
|
||||
justified_epoch,
|
||||
justified_block_root: hash_1,
|
||||
},
|
||||
custody_bitfield: Bitfield::new(),
|
||||
aggregate_signature: AggregateSignature::new(),
|
||||
};
|
||||
|
||||
let mut slashable_attestation_2 = SlashableAttestation {
|
||||
validator_indices: validator_indices.to_vec(),
|
||||
data: AttestationData {
|
||||
slot: double_voted_slot,
|
||||
shard,
|
||||
beacon_block_root: hash_2,
|
||||
epoch_boundary_root: hash_2,
|
||||
shard_block_root: hash_2,
|
||||
latest_crosslink: Crosslink {
|
||||
epoch,
|
||||
shard_block_root: hash_2,
|
||||
},
|
||||
justified_epoch,
|
||||
justified_block_root: hash_2,
|
||||
},
|
||||
custody_bitfield: Bitfield::new(),
|
||||
aggregate_signature: AggregateSignature::new(),
|
||||
};
|
||||
|
||||
let add_signatures = |attestation: &mut SlashableAttestation| {
|
||||
for (i, validator_index) in validator_indices.iter().enumerate() {
|
||||
let attestation_data_and_custody_bit = AttestationDataAndCustodyBit {
|
||||
data: attestation.data.clone(),
|
||||
custody_bit: attestation.custody_bitfield.get(i).unwrap(),
|
||||
};
|
||||
let message = attestation_data_and_custody_bit.hash_tree_root();
|
||||
let signature = signer(
|
||||
*validator_index,
|
||||
&message[..],
|
||||
epoch,
|
||||
spec.domain_attestation,
|
||||
);
|
||||
attestation.aggregate_signature.add(&signature);
|
||||
}
|
||||
};
|
||||
|
||||
add_signatures(&mut slashable_attestation_1);
|
||||
add_signatures(&mut slashable_attestation_2);
|
||||
|
||||
AttesterSlashing {
|
||||
slashable_attestation_1,
|
||||
slashable_attestation_2,
|
||||
}
|
||||
}
|
||||
}
|
@ -1,14 +1,11 @@
|
||||
use self::epoch_cache::EpochCache;
|
||||
use crate::test_utils::TestRandom;
|
||||
use crate::{
|
||||
validator::StatusFlags, validator_registry::get_active_validator_indices, AttestationData,
|
||||
Bitfield, ChainSpec, Crosslink, Deposit, DepositData, DepositInput, Epoch, Eth1Data,
|
||||
Eth1DataVote, Fork, Hash256, PendingAttestation, PublicKey, Signature, Slot, Validator,
|
||||
};
|
||||
use crate::{validator::StatusFlags, validator_registry::get_active_validator_indices, *};
|
||||
use bls::verify_proof_of_possession;
|
||||
use honey_badger_split::SplitExt;
|
||||
use log::{debug, error, trace};
|
||||
use rand::RngCore;
|
||||
use rayon::prelude::*;
|
||||
use serde_derive::Serialize;
|
||||
use ssz::{hash, Decodable, DecodeError, Encodable, SszStream, TreeHash};
|
||||
use std::collections::HashMap;
|
||||
@ -199,10 +196,10 @@ impl BeaconState {
|
||||
let mut genesis_state =
|
||||
BeaconState::genesis_without_validators(genesis_time, latest_eth1_data, spec)?;
|
||||
|
||||
trace!("Processing genesis deposits...");
|
||||
debug!("Processing genesis deposits...");
|
||||
|
||||
let deposit_data = initial_validator_deposits
|
||||
.iter()
|
||||
.par_iter()
|
||||
.map(|deposit| &deposit.deposit_data)
|
||||
.collect();
|
||||
|
||||
@ -411,6 +408,8 @@ impl BeaconState {
|
||||
return Err(Error::InsufficientValidators);
|
||||
}
|
||||
|
||||
debug!("Shuffling {} validators...", active_validator_indices.len());
|
||||
|
||||
let committees_per_epoch =
|
||||
self.get_epoch_committee_count(active_validator_indices.len(), spec);
|
||||
|
||||
@ -420,8 +419,7 @@ impl BeaconState {
|
||||
committees_per_epoch
|
||||
);
|
||||
|
||||
let active_validator_indices: Vec<usize> =
|
||||
active_validator_indices.iter().cloned().collect();
|
||||
let active_validator_indices: Vec<usize> = active_validator_indices.to_vec();
|
||||
|
||||
let shuffled_active_validator_indices = shuffle_list(
|
||||
active_validator_indices,
|
||||
@ -1000,6 +998,10 @@ impl BeaconState {
|
||||
whistleblower_reward
|
||||
);
|
||||
self.validator_registry[validator_index].penalized_epoch = current_epoch;
|
||||
debug!(
|
||||
"Whistleblower {} penalized validator {}.",
|
||||
whistleblower_index, validator_index
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -1138,6 +1140,114 @@ impl BeaconState {
|
||||
)
|
||||
}
|
||||
|
||||
/// Verify ``bitfield`` against the ``committee_size``.
|
||||
///
|
||||
/// Spec v0.2.0
|
||||
pub fn verify_bitfield(&self, bitfield: &Bitfield, committee_size: usize) -> bool {
|
||||
if bitfield.num_bytes() != ((committee_size + 7) / 8) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for i in committee_size..(bitfield.num_bytes() * 8) {
|
||||
match bitfield.get(i) {
|
||||
Ok(bit) => {
|
||||
if bit {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Err(_) => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// Verify validity of ``slashable_attestation`` fields.
|
||||
///
|
||||
/// Spec v0.2.0
|
||||
pub fn verify_slashable_attestation(
|
||||
&self,
|
||||
slashable_attestation: &SlashableAttestation,
|
||||
spec: &ChainSpec,
|
||||
) -> bool {
|
||||
if slashable_attestation.custody_bitfield.num_set_bits() > 0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
if slashable_attestation.validator_indices.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
for i in 0..(slashable_attestation.validator_indices.len() - 1) {
|
||||
if slashable_attestation.validator_indices[i]
|
||||
>= slashable_attestation.validator_indices[i + 1]
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if !self.verify_bitfield(
|
||||
&slashable_attestation.custody_bitfield,
|
||||
slashable_attestation.validator_indices.len(),
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if slashable_attestation.validator_indices.len()
|
||||
> spec.max_indices_per_slashable_vote as usize
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut aggregate_pubs = vec![AggregatePublicKey::new(); 2];
|
||||
let mut message_exists = vec![false; 2];
|
||||
|
||||
for (i, v) in slashable_attestation.validator_indices.iter().enumerate() {
|
||||
let custody_bit = match slashable_attestation.custody_bitfield.get(i) {
|
||||
Ok(bit) => bit,
|
||||
Err(_) => unreachable!(),
|
||||
};
|
||||
|
||||
message_exists[custody_bit as usize] = true;
|
||||
|
||||
match self.validator_registry.get(*v as usize) {
|
||||
Some(validator) => {
|
||||
aggregate_pubs[custody_bit as usize].add(&validator.pubkey);
|
||||
}
|
||||
None => return false,
|
||||
};
|
||||
}
|
||||
|
||||
let message_0 = AttestationDataAndCustodyBit {
|
||||
data: slashable_attestation.data.clone(),
|
||||
custody_bit: false,
|
||||
}
|
||||
.hash_tree_root();
|
||||
let message_1 = AttestationDataAndCustodyBit {
|
||||
data: slashable_attestation.data.clone(),
|
||||
custody_bit: true,
|
||||
}
|
||||
.hash_tree_root();
|
||||
|
||||
let mut messages = vec![];
|
||||
let mut keys = vec![];
|
||||
|
||||
if message_exists[0] {
|
||||
messages.push(&message_0[..]);
|
||||
keys.push(&aggregate_pubs[0]);
|
||||
}
|
||||
if message_exists[1] {
|
||||
messages.push(&message_1[..]);
|
||||
keys.push(&aggregate_pubs[1]);
|
||||
}
|
||||
|
||||
slashable_attestation.aggregate_signature.verify_multiple(
|
||||
&messages[..],
|
||||
spec.domain_attestation,
|
||||
&keys[..],
|
||||
)
|
||||
}
|
||||
|
||||
/// Return the block root at a recent `slot`.
|
||||
///
|
||||
/// Spec v0.2.0
|
||||
|
@ -6,6 +6,10 @@ use serde_derive::Serialize;
|
||||
use ssz_derive::{Decode, Encode, TreeHash};
|
||||
use test_random_derive::TestRandom;
|
||||
|
||||
mod builder;
|
||||
|
||||
pub use builder::ProposerSlashingBuilder;
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Serialize, Encode, Decode, TreeHash, TestRandom)]
|
||||
pub struct ProposerSlashing {
|
||||
pub proposer_index: u64,
|
||||
|
59
eth2/types/src/proposer_slashing/builder.rs
Normal file
59
eth2/types/src/proposer_slashing/builder.rs
Normal file
@ -0,0 +1,59 @@
|
||||
use crate::*;
|
||||
use ssz::TreeHash;
|
||||
|
||||
/// Builds a `ProposerSlashing`.
|
||||
pub struct ProposerSlashingBuilder();
|
||||
|
||||
impl ProposerSlashingBuilder {
|
||||
/// Builds a `ProposerSlashing` that is a double vote.
|
||||
///
|
||||
/// The `signer` function is used to sign the double-vote and accepts:
|
||||
///
|
||||
/// - `validator_index: u64`
|
||||
/// - `message: &[u8]`
|
||||
/// - `epoch: Epoch`
|
||||
/// - `domain: u64`
|
||||
///
|
||||
/// Where domain is a domain "constant" (e.g., `spec.domain_attestation`).
|
||||
pub fn double_vote<F>(proposer_index: u64, signer: F, spec: &ChainSpec) -> ProposerSlashing
|
||||
where
|
||||
F: Fn(u64, &[u8], Epoch, u64) -> Signature,
|
||||
{
|
||||
let slot = Slot::new(0);
|
||||
let shard = 0;
|
||||
|
||||
let proposal_data_1 = ProposalSignedData {
|
||||
slot,
|
||||
shard,
|
||||
block_root: Hash256::from(&[1][..]),
|
||||
};
|
||||
|
||||
let proposal_data_2 = ProposalSignedData {
|
||||
slot,
|
||||
shard,
|
||||
block_root: Hash256::from(&[2][..]),
|
||||
};
|
||||
|
||||
let proposal_signature_1 = {
|
||||
let message = proposal_data_1.hash_tree_root();
|
||||
let epoch = slot.epoch(spec.epoch_length);
|
||||
let domain = spec.domain_proposal;
|
||||
signer(proposer_index, &message[..], epoch, domain)
|
||||
};
|
||||
|
||||
let proposal_signature_2 = {
|
||||
let message = proposal_data_2.hash_tree_root();
|
||||
let epoch = slot.epoch(spec.epoch_length);
|
||||
let domain = spec.domain_proposal;
|
||||
signer(proposer_index, &message[..], epoch, domain)
|
||||
};
|
||||
|
||||
ProposerSlashing {
|
||||
proposer_index,
|
||||
proposal_data_1,
|
||||
proposal_signature_1,
|
||||
proposal_data_2,
|
||||
proposal_signature_2,
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
use crate::{test_utils::TestRandom, AggregateSignature, AttestationData, Bitfield};
|
||||
use crate::{test_utils::TestRandom, AggregateSignature, AttestationData, Bitfield, ChainSpec};
|
||||
use rand::RngCore;
|
||||
use serde_derive::Serialize;
|
||||
use ssz_derive::{Decode, Encode, TreeHash};
|
||||
@ -12,12 +12,107 @@ pub struct SlashableAttestation {
|
||||
pub aggregate_signature: AggregateSignature,
|
||||
}
|
||||
|
||||
impl SlashableAttestation {
|
||||
/// Check if ``attestation_data_1`` and ``attestation_data_2`` have the same target.
|
||||
///
|
||||
/// Spec v0.3.0
|
||||
pub fn is_double_vote(&self, other: &SlashableAttestation, spec: &ChainSpec) -> bool {
|
||||
self.data.slot.epoch(spec.epoch_length) == other.data.slot.epoch(spec.epoch_length)
|
||||
}
|
||||
|
||||
/// Check if ``attestation_data_1`` surrounds ``attestation_data_2``.
|
||||
///
|
||||
/// Spec v0.3.0
|
||||
pub fn is_surround_vote(&self, other: &SlashableAttestation, spec: &ChainSpec) -> bool {
|
||||
let source_epoch_1 = self.data.justified_epoch;
|
||||
let source_epoch_2 = other.data.justified_epoch;
|
||||
let target_epoch_1 = self.data.slot.epoch(spec.epoch_length);
|
||||
let target_epoch_2 = other.data.slot.epoch(spec.epoch_length);
|
||||
|
||||
(source_epoch_1 < source_epoch_2) && (target_epoch_2 < target_epoch_1)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::chain_spec::ChainSpec;
|
||||
use crate::slot_epoch::{Epoch, Slot};
|
||||
use crate::test_utils::{SeedableRng, TestRandom, XorShiftRng};
|
||||
use ssz::{ssz_encode, Decodable, TreeHash};
|
||||
|
||||
#[test]
|
||||
pub fn test_is_double_vote_true() {
|
||||
let spec = ChainSpec::foundation();
|
||||
let slashable_vote_first = create_slashable_attestation(1, 1, &spec);
|
||||
let slashable_vote_second = create_slashable_attestation(1, 1, &spec);
|
||||
|
||||
assert_eq!(
|
||||
slashable_vote_first.is_double_vote(&slashable_vote_second, &spec),
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_is_double_vote_false() {
|
||||
let spec = ChainSpec::foundation();
|
||||
let slashable_vote_first = create_slashable_attestation(1, 1, &spec);
|
||||
let slashable_vote_second = create_slashable_attestation(2, 1, &spec);
|
||||
|
||||
assert_eq!(
|
||||
slashable_vote_first.is_double_vote(&slashable_vote_second, &spec),
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_is_surround_vote_true() {
|
||||
let spec = ChainSpec::foundation();
|
||||
let slashable_vote_first = create_slashable_attestation(2, 1, &spec);
|
||||
let slashable_vote_second = create_slashable_attestation(1, 2, &spec);
|
||||
|
||||
assert_eq!(
|
||||
slashable_vote_first.is_surround_vote(&slashable_vote_second, &spec),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_is_surround_vote_true_realistic() {
|
||||
let spec = ChainSpec::foundation();
|
||||
let slashable_vote_first = create_slashable_attestation(4, 1, &spec);
|
||||
let slashable_vote_second = create_slashable_attestation(3, 2, &spec);
|
||||
|
||||
assert_eq!(
|
||||
slashable_vote_first.is_surround_vote(&slashable_vote_second, &spec),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_is_surround_vote_false_source_epoch_fails() {
|
||||
let spec = ChainSpec::foundation();
|
||||
let slashable_vote_first = create_slashable_attestation(2, 2, &spec);
|
||||
let slashable_vote_second = create_slashable_attestation(1, 1, &spec);
|
||||
|
||||
assert_eq!(
|
||||
slashable_vote_first.is_surround_vote(&slashable_vote_second, &spec),
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_is_surround_vote_false_target_epoch_fails() {
|
||||
let spec = ChainSpec::foundation();
|
||||
let slashable_vote_first = create_slashable_attestation(1, 1, &spec);
|
||||
let slashable_vote_second = create_slashable_attestation(2, 2, &spec);
|
||||
|
||||
assert_eq!(
|
||||
slashable_vote_first.is_surround_vote(&slashable_vote_second, &spec),
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_ssz_round_trip() {
|
||||
let mut rng = XorShiftRng::from_seed([42; 16]);
|
||||
@ -40,4 +135,17 @@ mod tests {
|
||||
// TODO: Add further tests
|
||||
// https://github.com/sigp/lighthouse/issues/170
|
||||
}
|
||||
|
||||
fn create_slashable_attestation(
|
||||
slot_factor: u64,
|
||||
justified_epoch: u64,
|
||||
spec: &ChainSpec,
|
||||
) -> SlashableAttestation {
|
||||
let mut rng = XorShiftRng::from_seed([42; 16]);
|
||||
let mut slashable_vote = SlashableAttestation::random_for_test(&mut rng);
|
||||
|
||||
slashable_vote.data.slot = Slot::new(slot_factor * spec.epoch_length);
|
||||
slashable_vote.data.justified_epoch = Epoch::new(justified_epoch);
|
||||
slashable_vote
|
||||
}
|
||||
}
|
||||
|
@ -54,9 +54,19 @@ pub struct Validator {
|
||||
}
|
||||
|
||||
impl Validator {
|
||||
/// This predicate indicates if the validator represented by this record is considered "active" at `slot`.
|
||||
pub fn is_active_at(&self, slot: Epoch) -> bool {
|
||||
self.activation_epoch <= slot && slot < self.exit_epoch
|
||||
/// Returns `true` if the validator is considered active at some epoch.
|
||||
pub fn is_active_at(&self, epoch: Epoch) -> bool {
|
||||
self.activation_epoch <= epoch && epoch < self.exit_epoch
|
||||
}
|
||||
|
||||
/// Returns `true` if the validator is considered exited at some epoch.
|
||||
pub fn is_exited_at(&self, epoch: Epoch) -> bool {
|
||||
self.exit_epoch <= epoch
|
||||
}
|
||||
|
||||
/// Returns `true` if the validator is considered penalized at some epoch.
|
||||
pub fn is_penalized_at(&self, epoch: Epoch) -> bool {
|
||||
self.penalized_epoch <= epoch
|
||||
}
|
||||
}
|
||||
|
||||
|
24
eth2/utils/bls/src/aggregate_public_key.rs
Normal file
24
eth2/utils/bls/src/aggregate_public_key.rs
Normal file
@ -0,0 +1,24 @@
|
||||
use super::PublicKey;
|
||||
use bls_aggregates::AggregatePublicKey as RawAggregatePublicKey;
|
||||
|
||||
/// A single BLS signature.
|
||||
///
|
||||
/// This struct is a wrapper upon a base type and provides helper functions (e.g., SSZ
|
||||
/// serialization).
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct AggregatePublicKey(RawAggregatePublicKey);
|
||||
|
||||
impl AggregatePublicKey {
|
||||
pub fn new() -> Self {
|
||||
AggregatePublicKey(RawAggregatePublicKey::new())
|
||||
}
|
||||
|
||||
pub fn add(&mut self, public_key: &PublicKey) {
|
||||
self.0.add(public_key.as_raw())
|
||||
}
|
||||
|
||||
/// Returns the underlying signature.
|
||||
pub fn as_raw(&self) -> &RawAggregatePublicKey {
|
||||
&self.0
|
||||
}
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
use super::{AggregatePublicKey, Signature};
|
||||
use bls_aggregates::AggregateSignature as RawAggregateSignature;
|
||||
use bls_aggregates::{
|
||||
AggregatePublicKey as RawAggregatePublicKey, AggregateSignature as RawAggregateSignature,
|
||||
};
|
||||
use serde::ser::{Serialize, Serializer};
|
||||
use ssz::{
|
||||
decode_ssz_list, hash, ssz_encode, Decodable, DecodeError, Encodable, SszStream, TreeHash,
|
||||
@ -33,7 +35,38 @@ impl AggregateSignature {
|
||||
domain: u64,
|
||||
aggregate_public_key: &AggregatePublicKey,
|
||||
) -> bool {
|
||||
self.0.verify(msg, domain, aggregate_public_key)
|
||||
self.0.verify(msg, domain, aggregate_public_key.as_raw())
|
||||
}
|
||||
|
||||
/// Verify this AggregateSignature against multiple AggregatePublickeys with multiple Messages.
|
||||
///
|
||||
/// All PublicKeys related to a Message should be aggregated into one AggregatePublicKey.
|
||||
/// Each AggregatePublicKey has a 1:1 ratio with a 32 byte Message.
|
||||
pub fn verify_multiple(
|
||||
&self,
|
||||
messages: &[&[u8]],
|
||||
domain: u64,
|
||||
aggregate_public_keys: &[&AggregatePublicKey],
|
||||
) -> bool {
|
||||
// TODO: the API for `RawAggregatePublicKey` shoudn't need to take an owned
|
||||
// `AggregatePublicKey`. There is an issue to fix this, but in the meantime we need to
|
||||
// clone.
|
||||
//
|
||||
// https://github.com/sigp/signature-schemes/issues/10
|
||||
let aggregate_public_keys: Vec<RawAggregatePublicKey> = aggregate_public_keys
|
||||
.iter()
|
||||
.map(|pk| pk.as_raw())
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
// Messages are concatenated into one long message.
|
||||
let mut msg: Vec<u8> = vec![];
|
||||
for message in messages {
|
||||
msg.extend_from_slice(message);
|
||||
}
|
||||
|
||||
self.0
|
||||
.verify_multiple(&msg[..], domain, &aggregate_public_keys[..])
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,20 +1,20 @@
|
||||
extern crate bls_aggregates;
|
||||
extern crate ssz;
|
||||
|
||||
mod aggregate_public_key;
|
||||
mod aggregate_signature;
|
||||
mod keypair;
|
||||
mod public_key;
|
||||
mod secret_key;
|
||||
mod signature;
|
||||
|
||||
pub use crate::aggregate_public_key::AggregatePublicKey;
|
||||
pub use crate::aggregate_signature::AggregateSignature;
|
||||
pub use crate::keypair::Keypair;
|
||||
pub use crate::public_key::PublicKey;
|
||||
pub use crate::secret_key::SecretKey;
|
||||
pub use crate::signature::Signature;
|
||||
|
||||
pub use self::bls_aggregates::AggregatePublicKey;
|
||||
|
||||
pub const BLS_AGG_SIG_BYTE_SIZE: usize = 96;
|
||||
|
||||
use ssz::ssz_encode;
|
||||
|
Loading…
Reference in New Issue
Block a user