Merge branch 'master' into improved-message-validation

This commit is contained in:
Grant Wuerker
2019-11-21 15:17:43 +09:00
273 changed files with 12629 additions and 5428 deletions

View File

@@ -4,7 +4,7 @@ default:
image: 'sigp/lighthouse:latest'
cache:
paths:
- tests/ef_tests/*-v0.8.3.tar.gz
- tests/ef_tests/*-v0.9.1.tar.gz
stages:
- test
@@ -47,7 +47,7 @@ test-ef-fake-crypto:
GIT_SUBMODULE_STRATEGY: normal
script:
- make make-ef-tests
- cargo test --manifest-path tests/ef_tests/Cargo.toml --release --features ef_tests fake_crypto
- cargo test --manifest-path tests/ef_tests/Cargo.toml --release --features ef_tests,fake_crypto
documentation:
stage: document

View File

@@ -21,6 +21,7 @@ members = [
"eth2/utils/ssz_derive",
"eth2/utils/ssz_types",
"eth2/utils/swap_or_not_shuffle",
"eth2/utils/cached_tree_hash",
"eth2/utils/tree_hash",
"eth2/utils/tree_hash_derive",
"eth2/utils/test_random_derive",
@@ -32,13 +33,18 @@ members = [
"beacon_node/eth2-libp2p",
"beacon_node/rpc",
"beacon_node/version",
"beacon_node/eth1",
"beacon_node/beacon_chain",
"beacon_node/websocket_server",
"tests/ef_tests",
"tests/eth1_test_rig",
"tests/node_test_rig",
"lcli",
"protos",
"validator_client",
"account_manager",
"lighthouse",
"lighthouse/environment"
]
[patch]
@@ -48,3 +54,4 @@ tree_hash_derive = { path = "eth2/utils/tree_hash_derive" }
eth2_ssz = { path = "eth2/utils/ssz" }
eth2_ssz_derive = { path = "eth2/utils/ssz_derive" }
eth2_ssz_types = { path = "eth2/utils/ssz_types" }
eth2_hashing = { path = "eth2/utils/eth2_hashing" }

View File

@@ -19,6 +19,8 @@ RUN git clone https://github.com/google/protobuf.git && \
cd .. && \
rm -r protobuf
RUN apt-get install -y nodejs npm
RUN npm install -g ganache-cli --unsafe-perm
RUN mkdir -p /cache/cargocache && chmod -R ugo+rwX /cache/cargocache

View File

@@ -11,10 +11,18 @@ release:
# Runs the full workspace tests, without downloading any additional test
# vectors.
test:
cargo test --all --all-features --release
cargo test --all --all-features --release --exclude ef_tests
# only run the ef-test vectors
run-ef-tests:
cargo test --release --manifest-path=$(EF_TESTS)/Cargo.toml --features "ef_tests"
test-ef: make-ef-tests run-ef-tests
# Runs the entire test suite, downloading test vectors if required.
test-full: make-ef-tests test
test-full: test test-ef
# Runs the makefile in the `ef_tests` repo.
#

View File

@@ -15,7 +15,7 @@ An open-source Ethereum 2.0 client, written in Rust and maintained by Sigma Prim
[Swagger Badge]: https://img.shields.io/badge/Open%20API-0.2.0-success
[Swagger Link]: https://app.swaggerhub.com/apis-docs/spble/lighthouse_rest_api/0.2.0
![terminalize](https://i.postimg.cc/Y0BQ0z3R/terminalize.gif)
![terminalize](https://i.postimg.cc/kG11dpCW/lighthouse-cli-png.gif)
## Overview
@@ -47,7 +47,7 @@ Current development overview:
- ~~**April 2019**: Inital single-client testnets.~~
- ~~**September 2019**: Inter-operability with other Ethereum 2.0 clients.~~
- **Early-October 2019**: `lighthouse-0.0.1` release: All major phase 0
- **Q4 2019**: `lighthouse-0.0.1` release: All major phase 0
features implemented.
- **Q4 2019**: Public, multi-client testnet with user-facing functionality.
- **Q4 2019**: Third-party security review.

View File

@@ -6,10 +6,10 @@ edition = "2018"
[dependencies]
bls = { path = "../eth2/utils/bls" }
clap = "2.32.0"
slog = "^2.2.3"
slog-term = "^2.4.0"
slog-async = "^2.3.0"
clap = "2.33.0"
slog = "2.5.2"
slog-term = "2.4.2"
slog-async = "2.3.0"
validator_client = { path = "../validator_client" }
types = { path = "../eth2/types" }
dirs = "2.0.1"
dirs = "2.0.2"

View File

@@ -4,6 +4,13 @@ version = "0.1.0"
authors = ["Paul Hauner <paul@paulhauner.com>", "Age Manning <Age@AgeManning.com"]
edition = "2018"
[lib]
name = "beacon_node"
path = "src/lib.rs"
[dev-dependencies]
node_test_rig = { path = "../tests/node_test_rig" }
[dependencies]
eth2_config = { path = "../eth2/utils/eth2_config" }
lighthouse_bootstrap = { path = "../eth2/utils/lighthouse_bootstrap" }
@@ -12,16 +19,18 @@ types = { path = "../eth2/types" }
store = { path = "./store" }
client = { path = "client" }
version = { path = "version" }
clap = "2.32.0"
rand = "0.7"
slog = { version = "^2.2.3" , features = ["max_level_trace", "release_max_level_trace"] }
slog-term = "^2.4.0"
slog-async = "^2.3.0"
ctrlc = { version = "3.1.1", features = ["termination"] }
tokio = "0.1.15"
tokio-timer = "0.2.10"
futures = "0.1.25"
exit-future = "0.1.3"
env_logger = "0.6.1"
dirs = "2.0.1"
clap = "2.33.0"
rand = "0.7.2"
slog = { version = "2.5.2", features = ["max_level_trace", "release_max_level_trace"] }
slog-term = "2.4.2"
slog-async = "2.3.0"
ctrlc = { version = "3.1.3", features = ["termination"] }
tokio = "0.1.22"
tokio-timer = "0.2.11"
exit-future = "0.1.4"
env_logger = "0.7.1"
dirs = "2.0.2"
logging = { path = "../eth2/utils/logging" }
futures = "0.1.29"
environment = { path = "../lighthouse/environment" }
genesis = { path = "genesis" }

View File

@@ -12,28 +12,35 @@ write_ssz_files = [] # Writes debugging .ssz files to /tmp during block process
eth2_config = { path = "../../eth2/utils/eth2_config" }
merkle_proof = { path = "../../eth2/utils/merkle_proof" }
store = { path = "../store" }
parking_lot = "0.7"
lazy_static = "1.3.0"
parking_lot = "0.9.0"
lazy_static = "1.4.0"
lighthouse_metrics = { path = "../../eth2/utils/lighthouse_metrics" }
lighthouse_bootstrap = { path = "../../eth2/utils/lighthouse_bootstrap" }
log = "0.4"
log = "0.4.8"
operation_pool = { path = "../../eth2/operation_pool" }
rayon = "1.0"
serde = "1.0"
serde_derive = "1.0"
serde_yaml = "0.8"
serde_json = "^1.0"
slog = { version = "^2.2.3" , features = ["max_level_trace"] }
sloggers = { version = "^0.3" }
rayon = "1.2.0"
serde = "1.0.102"
serde_derive = "1.0.102"
serde_yaml = "0.8.11"
serde_json = "1.0.41"
slog = { version = "2.5.2", features = ["max_level_trace"] }
sloggers = "0.3.4"
slot_clock = { path = "../../eth2/utils/slot_clock" }
eth2_hashing = { path = "../../eth2/utils/eth2_hashing" }
eth2_ssz = "0.1"
eth2_ssz_derive = "0.1"
eth2_hashing = "0.1.0"
eth2_ssz = "0.1.2"
eth2_ssz_derive = "0.1.0"
state_processing = { path = "../../eth2/state_processing" }
tree_hash = "0.1"
tree_hash = "0.1.0"
types = { path = "../../eth2/types" }
lmd_ghost = { path = "../../eth2/lmd_ghost" }
eth1 = { path = "../eth1" }
websocket_server = { path = "../websocket_server" }
futures = "0.1.25"
exit-future = "0.1.3"
genesis = { path = "../genesis" }
integer-sqrt = "0.1"
rand = "0.7.2"
[dev-dependencies]
rand = "0.5.5"
lazy_static = "1.3.0"
lazy_static = "1.4.0"
environment = { path = "../../lighthouse/environment" }

View File

@@ -16,7 +16,7 @@ use ssz::Encode;
use state_processing::per_block_processing::{
errors::{
AttestationValidationError, AttesterSlashingValidationError, DepositValidationError,
ExitValidationError, ProposerSlashingValidationError, TransferValidationError,
ExitValidationError, ProposerSlashingValidationError,
},
verify_attestation_for_state, VerifySignatures,
};
@@ -26,7 +26,6 @@ use state_processing::{
use std::fs;
use std::io::prelude::*;
use std::sync::Arc;
use std::time::Duration;
use store::iter::{BlockRootsIterator, StateRootsIterator};
use store::{Error as DBError, Store};
use tree_hash::TreeHash;
@@ -44,6 +43,8 @@ pub const GRAFFITI: &str = "sigp/lighthouse-0.0.0-prerelease";
/// Only useful for testing.
const WRITE_BLOCK_PROCESSING_SSZ: bool = cfg!(feature = "write_ssz_files");
const BLOCK_SKIPPING_LOGGING_THRESHOLD: u64 = 3;
#[derive(Debug, PartialEq)]
pub enum BlockProcessingOutcome {
/// Block was valid and imported into the block graph.
@@ -111,9 +112,9 @@ pub struct BeaconChain<T: BeaconChainTypes> {
/// inclusion in a block.
pub op_pool: OperationPool<T::EthSpec>,
/// Provides information from the Ethereum 1 (PoW) chain.
pub eth1_chain: Eth1Chain<T>,
pub eth1_chain: Option<Eth1Chain<T::Eth1Chain, T::EthSpec>>,
/// Stores a "snapshot" of the chain at the time the head-of-the-chain block was received.
canonical_head: RwLock<CheckPoint<T::EthSpec>>,
pub(crate) canonical_head: RwLock<CheckPoint<T::EthSpec>>,
/// The root of the genesis block.
pub genesis_block_root: Hash256,
/// A state-machine that is updated with information from the network and chooses a canonical
@@ -122,119 +123,12 @@ pub struct BeaconChain<T: BeaconChainTypes> {
/// A handler for events generated by the beacon chain.
pub event_handler: T::EventHandler,
/// Logging to CLI, etc.
log: Logger,
pub(crate) log: Logger,
}
type BeaconInfo<T> = (BeaconBlock<T>, BeaconState<T>);
type BeaconBlockAndState<T> = (BeaconBlock<T>, BeaconState<T>);
impl<T: BeaconChainTypes> BeaconChain<T> {
/// Instantiate a new Beacon Chain, from genesis.
pub fn from_genesis(
store: Arc<T::Store>,
eth1_backend: T::Eth1Chain,
event_handler: T::EventHandler,
mut genesis_state: BeaconState<T::EthSpec>,
mut genesis_block: BeaconBlock<T::EthSpec>,
spec: ChainSpec,
log: Logger,
) -> Result<Self, Error> {
genesis_state.build_all_caches(&spec)?;
let genesis_state_root = genesis_state.canonical_root();
store.put(&genesis_state_root, &genesis_state)?;
genesis_block.state_root = genesis_state_root;
let genesis_block_root = genesis_block.block_header().canonical_root();
store.put(&genesis_block_root, &genesis_block)?;
// Also store the genesis block under the `ZERO_HASH` key.
let genesis_block_root = genesis_block.canonical_root();
store.put(&Hash256::zero(), &genesis_block)?;
let canonical_head = RwLock::new(CheckPoint::new(
genesis_block.clone(),
genesis_block_root,
genesis_state.clone(),
genesis_state_root,
));
// Slot clock
let slot_clock = T::SlotClock::new(
spec.genesis_slot,
Duration::from_secs(genesis_state.genesis_time),
Duration::from_millis(spec.milliseconds_per_slot),
);
info!(log, "Beacon chain initialized from genesis";
"validator_count" => genesis_state.validators.len(),
"state_root" => format!("{}", genesis_state_root),
"block_root" => format!("{}", genesis_block_root),
);
Ok(Self {
spec,
slot_clock,
op_pool: OperationPool::new(),
eth1_chain: Eth1Chain::new(eth1_backend),
canonical_head,
genesis_block_root,
fork_choice: ForkChoice::new(store.clone(), &genesis_block, genesis_block_root),
event_handler,
store,
log,
})
}
/// Attempt to load an existing instance from the given `store`.
pub fn from_store(
store: Arc<T::Store>,
eth1_backend: T::Eth1Chain,
event_handler: T::EventHandler,
spec: ChainSpec,
log: Logger,
) -> Result<Option<BeaconChain<T>>, Error> {
let key = Hash256::from_slice(&BEACON_CHAIN_DB_KEY.as_bytes());
let p: PersistedBeaconChain<T> = match store.get(&key) {
Err(e) => return Err(e.into()),
Ok(None) => return Ok(None),
Ok(Some(p)) => p,
};
let state = &p.canonical_head.beacon_state;
let slot_clock = T::SlotClock::new(
spec.genesis_slot,
Duration::from_secs(state.genesis_time),
Duration::from_millis(spec.milliseconds_per_slot),
);
let last_finalized_root = p.canonical_head.beacon_state.finalized_checkpoint.root;
let last_finalized_block = &p.canonical_head.beacon_block;
let op_pool = p.op_pool.into_operation_pool(state, &spec);
info!(log, "Beacon chain initialized from store";
"head_root" => format!("{}", p.canonical_head.beacon_block_root),
"head_epoch" => format!("{}", p.canonical_head.beacon_block.slot.epoch(T::EthSpec::slots_per_epoch())),
"finalized_root" => format!("{}", last_finalized_root),
"finalized_epoch" => format!("{}", last_finalized_block.slot.epoch(T::EthSpec::slots_per_epoch())),
);
Ok(Some(BeaconChain {
spec,
slot_clock,
fork_choice: ForkChoice::new(store.clone(), last_finalized_block, last_finalized_root),
op_pool,
event_handler,
eth1_chain: Eth1Chain::new(eth1_backend),
canonical_head: RwLock::new(p.canonical_head),
genesis_block_root: p.genesis_block_root,
store,
log,
}))
}
/// Attempt to save this instance to `self.store`.
pub fn persist(&self) -> Result<(), Error> {
let timer = metrics::start_timer(&metrics::PERSIST_CHAIN);
@@ -324,6 +218,44 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
ReverseBlockRootIterator::new((head.beacon_block_root, head.beacon_block.slot), iter)
}
/// Traverse backwards from `block_root` to find the block roots of its ancestors.
///
/// ## Notes
///
/// `slot` always decreases by `1`.
/// - Skipped slots contain the root of the closest prior
/// non-skipped slot (identical to the way they are stored in `state.block_roots`) .
/// - Iterator returns `(Hash256, Slot)`.
/// - The provided `block_root` is included as the first item in the iterator.
pub fn rev_iter_block_roots_from(
&self,
block_root: Hash256,
) -> Result<ReverseBlockRootIterator<T::EthSpec, T::Store>, Error> {
let block = self
.get_block(&block_root)?
.ok_or_else(|| Error::MissingBeaconBlock(block_root))?;
let state = self
.get_state(&block.state_root)?
.ok_or_else(|| Error::MissingBeaconState(block.state_root))?;
let iter = BlockRootsIterator::owned(self.store.clone(), state);
Ok(ReverseBlockRootIterator::new(
(block_root, block.slot),
iter,
))
}
/// Traverse backwards from `block_root` to find the root of the ancestor block at `slot`.
pub fn get_ancestor_block_root(
&self,
block_root: Hash256,
slot: Slot,
) -> Result<Option<Hash256>, Error> {
Ok(self
.rev_iter_block_roots_from(block_root)?
.find(|(_, ancestor_slot)| *ancestor_slot == slot)
.map(|(ancestor_block_root, _)| ancestor_block_root))
}
/// Iterates across all `(state_root, slot)` pairs from the head of the chain (inclusive) to
/// the earliest reachable ancestor (may or may not be genesis).
///
@@ -354,6 +286,18 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
Ok(self.store.get(block_root)?)
}
/// Returns the state at the given root, if any.
///
/// ## Errors
///
/// May return a database error.
pub fn get_state(
&self,
state_root: &Hash256,
) -> Result<Option<BeaconState<T::EthSpec>>, Error> {
Ok(self.store.get(state_root)?)
}
/// Returns a `Checkpoint` representing the head block and state. Contains the "best block";
/// the head of the canonical `BeaconChain`.
///
@@ -374,6 +318,14 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
if slot == head_state.slot {
Ok(head_state)
} else if slot > head_state.slot {
if slot > head_state.slot + BLOCK_SKIPPING_LOGGING_THRESHOLD {
warn!(
self.log,
"Skipping more than {} blocks", BLOCK_SKIPPING_LOGGING_THRESHOLD;
"head_slot" => head_state.slot,
"request_slot" => slot
)
}
let head_state_slot = head_state.slot;
let mut state = head_state;
while state.slot < slot {
@@ -483,15 +435,15 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
}
state
.get_beacon_proposer_index(slot, RelativeEpoch::Current, &self.spec)
.get_beacon_proposer_index(slot, &self.spec)
.map_err(Into::into)
}
/// Returns the attestation slot and shard for a given validator index.
/// Returns the attestation slot and committee index for a given validator index.
///
/// Information is read from the current state, so only information from the present and prior
/// epoch is available.
pub fn validator_attestation_slot_and_shard(
pub fn validator_attestation_slot_and_index(
&self,
validator_index: usize,
epoch: Epoch,
@@ -518,25 +470,25 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
if let Some(attestation_duty) =
state.get_attestation_duties(validator_index, RelativeEpoch::Current)?
{
Ok(Some((attestation_duty.slot, attestation_duty.shard)))
Ok(Some((attestation_duty.slot, attestation_duty.index)))
} else {
Ok(None)
}
}
/// Produce an `AttestationData` that is valid for the given `slot` `shard`.
/// Produce an `AttestationData` that is valid for the given `slot`, `index`.
///
/// Always attests to the canonical chain.
pub fn produce_attestation_data(
&self,
shard: u64,
slot: Slot,
index: CommitteeIndex,
) -> Result<AttestationData, Error> {
let state = self.state_at_slot(slot)?;
let head = self.head();
self.produce_attestation_data_for_block(
shard,
index,
head.beacon_block_root,
head.beacon_block.slot,
&state,
@@ -549,7 +501,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
/// function should be used as it attests to the canonical chain.
pub fn produce_attestation_data_for_block(
&self,
shard: u64,
index: CommitteeIndex,
head_block_root: Hash256,
head_block_slot: Slot,
state: &BeaconState<T::EthSpec>,
@@ -590,18 +542,6 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
root: target_root,
};
let parent_crosslink = state.get_current_crosslink(shard)?;
let crosslink = Crosslink {
shard,
parent_root: Hash256::from_slice(&parent_crosslink.tree_hash_root()),
start_epoch: parent_crosslink.end_epoch,
end_epoch: std::cmp::min(
target.epoch,
parent_crosslink.end_epoch + self.spec.max_epochs_per_crosslink,
),
data_root: Hash256::zero(),
};
// Collect some metrics.
metrics::inc_counter(&metrics::ATTESTATION_PRODUCTION_SUCCESSES);
metrics::stop_timer(timer);
@@ -610,15 +550,16 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
self.log,
"Produced beacon attestation data";
"beacon_block_root" => format!("{}", head_block_root),
"shard" => shard,
"slot" => state.slot
"slot" => state.slot,
"index" => index
);
Ok(AttestationData {
slot: state.slot,
index,
beacon_block_root: head_block_root,
source: state.current_justified_checkpoint.clone(),
target,
crosslink,
})
}
@@ -647,7 +588,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
self.log,
"Beacon attestation imported";
"target_epoch" => attestation.data.target.epoch,
"shard" => attestation.data.crosslink.shard,
"index" => attestation.data.index,
);
let _ = self
.event_handler
@@ -766,16 +707,14 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
state.build_committee_cache(RelativeEpoch::Current, &self.spec)?;
let attestation_slot = state.get_attestation_data_slot(&attestation.data)?;
// Reject any attestation where the `state` loaded from `data.beacon_block_root`
// has a higher slot than the attestation.
//
// Permitting this would allow for attesters to vote on _future_ slots.
if state.slot > attestation_slot {
if state.slot > attestation.data.slot {
Ok(AttestationProcessingOutcome::AttestsToFutureState {
state: state.slot,
attestation: attestation_slot,
attestation: attestation.data.slot,
})
} else {
self.process_attestation_for_state_and_block(
@@ -874,6 +813,12 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
Ok(AttestationProcessingOutcome::Invalid(e))
} else {
// If the attestation is from the current or previous epoch, supply it to the fork
// choice. This is FMD GHOST.
let current_epoch = self.epoch()?;
if attestation.data.target.epoch == current_epoch
|| attestation.data.target.epoch == current_epoch - 1
{
// Provide the attestation to fork choice, updating the validator latest messages but
// _without_ finding and updating the head.
if let Err(e) = self
@@ -889,6 +834,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
);
return Err(e.into());
}
}
// Provide the valid attestation to op pool, which may choose to retain the
// attestation for inclusion in a future block.
@@ -927,22 +873,6 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
}
}
/// Accept some transfer and queue it for inclusion in an appropriate block.
pub fn process_transfer(&self, transfer: Transfer) -> Result<(), TransferValidationError> {
match self.wall_clock_state() {
Ok(state) => self.op_pool.insert_transfer(transfer, &state, &self.spec),
Err(e) => {
error!(
&self.log,
"Unable to process transfer";
"error" => format!("{:?}", e),
"reason" => "no state"
);
Ok(())
}
}
}
/// Accept some proposer slashing and queue it for inclusion in an appropriate block.
pub fn process_proposer_slashing(
&self,
@@ -1003,7 +933,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
self.log,
"Beacon block imported";
"block_root" => format!("{:?}", block_root),
"block_slot" => format!("{:?}", block_root),
"block_slot" => format!("{:?}", block.slot.as_u64()),
);
let _ = self.event_handler.register(EventKind::BeaconBlockImported {
block_root: *block_root,
@@ -1213,7 +1143,10 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
metrics::start_timer(&metrics::BLOCK_PROCESSING_FORK_CHOICE_REGISTER);
// Register the new block with the fork choice service.
if let Err(e) = self.fork_choice.process_block(&state, &block, block_root) {
if let Err(e) = self
.fork_choice
.process_block(self, &state, &block, block_root)
{
error!(
self.log,
"Add block to fork choice failed";
@@ -1260,7 +1193,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
&self,
randao_reveal: Signature,
slot: Slot,
) -> Result<BeaconInfo<T::EthSpec>, BlockProductionError> {
) -> Result<BeaconBlockAndState<T::EthSpec>, BlockProductionError> {
let state = self
.state_at_slot(slot - 1)
.map_err(|_| BlockProductionError::UnableToProduceAtSlot(slot))?;
@@ -1281,10 +1214,15 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
mut state: BeaconState<T::EthSpec>,
produce_at_slot: Slot,
randao_reveal: Signature,
) -> Result<BeaconInfo<T::EthSpec>, BlockProductionError> {
) -> Result<BeaconBlockAndState<T::EthSpec>, BlockProductionError> {
metrics::inc_counter(&metrics::BLOCK_PRODUCTION_REQUESTS);
let timer = metrics::start_timer(&metrics::BLOCK_PRODUCTION_TIMES);
let eth1_chain = self
.eth1_chain
.as_ref()
.ok_or_else(|| BlockProductionError::NoEth1ChainConnection)?;
// If required, transition the new state to the present slot.
while state.slot < produce_at_slot {
per_slot_processing(&mut state, &self.spec)?;
@@ -1309,19 +1247,20 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
let mut block = BeaconBlock {
slot: state.slot,
parent_root,
state_root: Hash256::zero(), // Updated after the state is calculated.
signature: Signature::empty_signature(), // To be completed by a validator.
state_root: Hash256::zero(),
// The block is not signed here, that is the task of a validator client.
signature: Signature::empty_signature(),
body: BeaconBlockBody {
randao_reveal,
// TODO: replace with real data.
eth1_data: self.eth1_chain.eth1_data_for_block_production(&state)?,
eth1_data: eth1_chain.eth1_data_for_block_production(&state, &self.spec)?,
graffiti,
proposer_slashings: proposer_slashings.into(),
attester_slashings: attester_slashings.into(),
attestations: self.op_pool.get_attestations(&state, &self.spec).into(),
deposits: self.eth1_chain.deposits_for_block_inclusion(&state)?.into(),
deposits: eth1_chain
.deposits_for_block_inclusion(&state, &self.spec)?
.into(),
voluntary_exits: self.op_pool.get_voluntary_exits(&state, &self.spec).into(),
transfers: self.op_pool.get_transfers(&state, &self.spec).into(),
},
};

View File

@@ -210,22 +210,19 @@ fn interop_genesis_state<T: EthSpec>(
.collect::<Vec<_>>();
let mut proofs = vec![];
for i in 1..=deposit_root_leaves.len() {
// Note: this implementation is not so efficient.
//
// If `MerkleTree` had a push method, we could just build one tree and sample it instead of
// rebuilding the tree for each deposit.
let tree = MerkleTree::create(
&deposit_root_leaves[0..i],
spec.deposit_contract_tree_depth as usize,
);
let depth = spec.deposit_contract_tree_depth as usize;
let mut tree = MerkleTree::create(&[], depth);
for (i, deposit_leaf) in deposit_root_leaves.iter().enumerate() {
if let Err(_) = tree.push_leaf(*deposit_leaf, depth) {
return Err(String::from("Failed to push leaf"));
}
let (_, mut proof) = tree.generate_proof(i - 1, spec.deposit_contract_tree_depth as usize);
proof.push(Hash256::from_slice(&int_to_bytes32(i)));
let (_, mut proof) = tree.generate_proof(i, depth);
proof.push(Hash256::from_slice(&int_to_bytes32(i + 1)));
assert_eq!(
proof.len(),
spec.deposit_contract_tree_depth as usize + 1,
depth + 1,
"Deposit proof should be correct len"
);

View File

@@ -0,0 +1,627 @@
use crate::eth1_chain::CachingEth1Backend;
use crate::events::NullEventHandler;
use crate::persisted_beacon_chain::{PersistedBeaconChain, BEACON_CHAIN_DB_KEY};
use crate::{
BeaconChain, BeaconChainTypes, CheckPoint, Eth1Chain, Eth1ChainBackend, EventHandler,
ForkChoice,
};
use eth1::Config as Eth1Config;
use lmd_ghost::{LmdGhost, ThreadSafeReducedTree};
use operation_pool::OperationPool;
use parking_lot::RwLock;
use slog::{info, Logger};
use slot_clock::{SlotClock, TestingSlotClock};
use std::marker::PhantomData;
use std::sync::Arc;
use std::time::Duration;
use store::Store;
use types::{BeaconBlock, BeaconState, ChainSpec, EthSpec, Hash256, Slot};
/// An empty struct used to "witness" all the `BeaconChainTypes` traits. It has no user-facing
/// functionality and only exists to satisfy the type system.
pub struct Witness<TStore, TSlotClock, TLmdGhost, TEth1Backend, TEthSpec, TEventHandler>(
PhantomData<(
TStore,
TSlotClock,
TLmdGhost,
TEth1Backend,
TEthSpec,
TEventHandler,
)>,
);
impl<TStore, TSlotClock, TLmdGhost, TEth1Backend, TEthSpec, TEventHandler> BeaconChainTypes
for Witness<TStore, TSlotClock, TLmdGhost, TEth1Backend, TEthSpec, TEventHandler>
where
TStore: Store + 'static,
TSlotClock: SlotClock + 'static,
TLmdGhost: LmdGhost<TStore, TEthSpec> + 'static,
TEth1Backend: Eth1ChainBackend<TEthSpec> + 'static,
TEthSpec: EthSpec + 'static,
TEventHandler: EventHandler<TEthSpec> + 'static,
{
type Store = TStore;
type SlotClock = TSlotClock;
type LmdGhost = TLmdGhost;
type Eth1Chain = TEth1Backend;
type EthSpec = TEthSpec;
type EventHandler = TEventHandler;
}
/// Builds a `BeaconChain` by either creating anew from genesis, or, resuming from an existing chain
/// persisted to `store`.
///
/// Types may be elided and the compiler will infer them if all necessary builder methods have been
/// called. If type inference errors are being raised, it is likely that not all required methods
/// have been called.
///
/// See the tests for an example of a complete working example.
pub struct BeaconChainBuilder<T: BeaconChainTypes> {
store: Option<Arc<T::Store>>,
/// The finalized checkpoint to anchor the chain. May be genesis or a higher
/// checkpoint.
pub finalized_checkpoint: Option<CheckPoint<T::EthSpec>>,
genesis_block_root: Option<Hash256>,
op_pool: Option<OperationPool<T::EthSpec>>,
fork_choice: Option<ForkChoice<T>>,
eth1_chain: Option<Eth1Chain<T::Eth1Chain, T::EthSpec>>,
event_handler: Option<T::EventHandler>,
slot_clock: Option<T::SlotClock>,
spec: ChainSpec,
log: Option<Logger>,
}
impl<TStore, TSlotClock, TLmdGhost, TEth1Backend, TEthSpec, TEventHandler>
BeaconChainBuilder<
Witness<TStore, TSlotClock, TLmdGhost, TEth1Backend, TEthSpec, TEventHandler>,
>
where
TStore: Store + 'static,
TSlotClock: SlotClock + 'static,
TLmdGhost: LmdGhost<TStore, TEthSpec> + 'static,
TEth1Backend: Eth1ChainBackend<TEthSpec> + 'static,
TEthSpec: EthSpec + 'static,
TEventHandler: EventHandler<TEthSpec> + 'static,
{
/// Returns a new builder.
///
/// The `_eth_spec_instance` parameter is only supplied to make concrete the `TEthSpec` trait.
/// This should generally be either the `MinimalEthSpec` or `MainnetEthSpec` types.
pub fn new(_eth_spec_instance: TEthSpec) -> Self {
Self {
store: None,
finalized_checkpoint: None,
genesis_block_root: None,
op_pool: None,
fork_choice: None,
eth1_chain: None,
event_handler: None,
slot_clock: None,
spec: TEthSpec::default_spec(),
log: None,
}
}
/// Override the default spec (as defined by `TEthSpec`).
///
/// This method should generally be called immediately after `Self::new` to ensure components
/// are started with a consistent spec.
pub fn custom_spec(mut self, spec: ChainSpec) -> Self {
self.spec = spec;
self
}
/// Sets the store (database).
///
/// Should generally be called early in the build chain.
pub fn store(mut self, store: Arc<TStore>) -> Self {
self.store = Some(store);
self
}
/// Sets the logger.
///
/// Should generally be called early in the build chain.
pub fn logger(mut self, logger: Logger) -> Self {
self.log = Some(logger);
self
}
/// Attempt to load an existing chain from the builder's `Store`.
///
/// May initialize several components; including the op_pool and finalized checkpoints.
pub fn resume_from_db(mut self) -> Result<Self, String> {
let log = self
.log
.as_ref()
.ok_or_else(|| "resume_from_db requires a log".to_string())?;
info!(
log,
"Starting beacon chain";
"method" => "resume"
);
let store = self
.store
.clone()
.ok_or_else(|| "load_from_store requires a store.".to_string())?;
let key = Hash256::from_slice(&BEACON_CHAIN_DB_KEY.as_bytes());
let p: PersistedBeaconChain<
Witness<TStore, TSlotClock, TLmdGhost, TEth1Backend, TEthSpec, TEventHandler>,
> = match store.get(&key) {
Err(e) => {
return Err(format!(
"DB error when reading persisted beacon chain: {:?}",
e
))
}
Ok(None) => return Err("No persisted beacon chain found in store".into()),
Ok(Some(p)) => p,
};
self.op_pool = Some(
p.op_pool
.into_operation_pool(&p.canonical_head.beacon_state, &self.spec),
);
self.finalized_checkpoint = Some(p.canonical_head);
self.genesis_block_root = Some(p.genesis_block_root);
Ok(self)
}
/// Starts a new chain from a genesis state.
pub fn genesis_state(
mut self,
mut beacon_state: BeaconState<TEthSpec>,
) -> Result<Self, String> {
let store = self
.store
.clone()
.ok_or_else(|| "genesis_state requires a store")?;
let mut beacon_block = genesis_block(&beacon_state, &self.spec);
beacon_state
.build_all_caches(&self.spec)
.map_err(|e| format!("Failed to build genesis state caches: {:?}", e))?;
let beacon_state_root = beacon_state.canonical_root();
beacon_block.state_root = beacon_state_root;
let beacon_block_root = beacon_block.canonical_root();
self.genesis_block_root = Some(beacon_block_root);
store
.put(&beacon_state_root, &beacon_state)
.map_err(|e| format!("Failed to store genesis state: {:?}", e))?;
store
.put(&beacon_block_root, &beacon_block)
.map_err(|e| format!("Failed to store genesis block: {:?}", e))?;
// Store the genesis block under the `ZERO_HASH` key.
store.put(&Hash256::zero(), &beacon_block).map_err(|e| {
format!(
"Failed to store genesis block under 0x00..00 alias: {:?}",
e
)
})?;
self.finalized_checkpoint = Some(CheckPoint {
beacon_block_root,
beacon_block,
beacon_state_root,
beacon_state,
});
Ok(self.empty_op_pool())
}
/// Sets the `BeaconChain` fork choice backend.
///
/// Requires the store and state to have been specified earlier in the build chain.
pub fn fork_choice_backend(mut self, backend: TLmdGhost) -> Result<Self, String> {
let store = self
.store
.clone()
.ok_or_else(|| "reduced_tree_fork_choice requires a store")?;
let genesis_block_root = self
.genesis_block_root
.ok_or_else(|| "fork_choice_backend requires a genesis_block_root")?;
self.fork_choice = Some(ForkChoice::new(
store,
backend,
genesis_block_root,
self.spec.genesis_slot,
));
Ok(self)
}
/// Sets the `BeaconChain` eth1 backend.
pub fn eth1_backend(mut self, backend: Option<TEth1Backend>) -> Self {
self.eth1_chain = backend.map(Eth1Chain::new);
self
}
/// Sets the `BeaconChain` event handler backend.
///
/// For example, provide `WebSocketSender` as a `handler`.
pub fn event_handler(mut self, handler: TEventHandler) -> Self {
self.event_handler = Some(handler);
self
}
/// Sets the `BeaconChain` slot clock.
///
/// For example, provide `SystemTimeSlotClock` as a `clock`.
pub fn slot_clock(mut self, clock: TSlotClock) -> Self {
self.slot_clock = Some(clock);
self
}
/// Creates a new, empty operation pool.
fn empty_op_pool(mut self) -> Self {
self.op_pool = Some(OperationPool::new());
self
}
/// Consumes `self`, returning a `BeaconChain` if all required parameters have been supplied.
///
/// An error will be returned at runtime if all required parameters have not been configured.
///
/// Will also raise ambiguous type errors at compile time if some parameters have not been
/// configured.
#[allow(clippy::type_complexity)] // I think there's nothing to be gained here from a type alias.
pub fn build(
self,
) -> Result<
BeaconChain<Witness<TStore, TSlotClock, TLmdGhost, TEth1Backend, TEthSpec, TEventHandler>>,
String,
> {
let mut canonical_head = self
.finalized_checkpoint
.ok_or_else(|| "Cannot build without a state".to_string())?;
canonical_head
.beacon_state
.build_all_caches(&self.spec)
.map_err(|e| format!("Failed to build state caches: {:?}", e))?;
let log = self
.log
.ok_or_else(|| "Cannot build without a logger".to_string())?;
if canonical_head.beacon_block.state_root != canonical_head.beacon_state_root {
return Err("beacon_block.state_root != beacon_state".to_string());
}
let beacon_chain = BeaconChain {
spec: self.spec,
store: self
.store
.ok_or_else(|| "Cannot build without store".to_string())?,
slot_clock: self
.slot_clock
.ok_or_else(|| "Cannot build without slot clock".to_string())?,
op_pool: self
.op_pool
.ok_or_else(|| "Cannot build without op pool".to_string())?,
eth1_chain: self.eth1_chain,
canonical_head: RwLock::new(canonical_head),
genesis_block_root: self
.genesis_block_root
.ok_or_else(|| "Cannot build without a genesis block root".to_string())?,
fork_choice: self
.fork_choice
.ok_or_else(|| "Cannot build without a fork choice".to_string())?,
event_handler: self
.event_handler
.ok_or_else(|| "Cannot build without an event handler".to_string())?,
log: log.clone(),
};
info!(
log,
"Beacon chain initialized";
"head_state" => format!("{}", beacon_chain.head().beacon_state_root),
"head_block" => format!("{}", beacon_chain.head().beacon_block_root),
"head_slot" => format!("{}", beacon_chain.head().beacon_block.slot),
);
Ok(beacon_chain)
}
}
impl<TStore, TSlotClock, TEth1Backend, TEthSpec, TEventHandler>
BeaconChainBuilder<
Witness<
TStore,
TSlotClock,
ThreadSafeReducedTree<TStore, TEthSpec>,
TEth1Backend,
TEthSpec,
TEventHandler,
>,
>
where
TStore: Store + 'static,
TSlotClock: SlotClock + 'static,
TEth1Backend: Eth1ChainBackend<TEthSpec> + 'static,
TEthSpec: EthSpec + 'static,
TEventHandler: EventHandler<TEthSpec> + 'static,
{
/// Initializes a new, empty (no recorded votes or blocks) fork choice, using the
/// `ThreadSafeReducedTree` backend.
///
/// Requires the store and state to be initialized.
pub fn empty_reduced_tree_fork_choice(self) -> Result<Self, String> {
let store = self
.store
.clone()
.ok_or_else(|| "reduced_tree_fork_choice requires a store")?;
let finalized_checkpoint = &self
.finalized_checkpoint
.as_ref()
.expect("should have finalized checkpoint");
let backend = ThreadSafeReducedTree::new(
store.clone(),
&finalized_checkpoint.beacon_block,
finalized_checkpoint.beacon_block_root,
);
self.fork_choice_backend(backend)
}
}
impl<TStore, TSlotClock, TLmdGhost, TEthSpec, TEventHandler>
BeaconChainBuilder<
Witness<
TStore,
TSlotClock,
TLmdGhost,
CachingEth1Backend<TEthSpec, TStore>,
TEthSpec,
TEventHandler,
>,
>
where
TStore: Store + 'static,
TSlotClock: SlotClock + 'static,
TLmdGhost: LmdGhost<TStore, TEthSpec> + 'static,
TEthSpec: EthSpec + 'static,
TEventHandler: EventHandler<TEthSpec> + 'static,
{
/// Sets the `BeaconChain` eth1 back-end to `CachingEth1Backend`.
pub fn caching_eth1_backend(self, backend: CachingEth1Backend<TEthSpec, TStore>) -> Self {
self.eth1_backend(Some(backend))
}
/// Do not use any eth1 backend. The client will not be able to produce beacon blocks.
pub fn no_eth1_backend(self) -> Self {
self.eth1_backend(None)
}
/// Sets the `BeaconChain` eth1 back-end to produce predictably junk data when producing blocks.
pub fn dummy_eth1_backend(mut self) -> Result<Self, String> {
let log = self
.log
.as_ref()
.ok_or_else(|| "dummy_eth1_backend requires a log".to_string())?;
let store = self
.store
.clone()
.ok_or_else(|| "dummy_eth1_backend requires a store.".to_string())?;
let backend = CachingEth1Backend::new(Eth1Config::default(), log.clone(), store);
let mut eth1_chain = Eth1Chain::new(backend);
eth1_chain.use_dummy_backend = true;
self.eth1_chain = Some(eth1_chain);
Ok(self)
}
}
impl<TStore, TLmdGhost, TEth1Backend, TEthSpec, TEventHandler>
BeaconChainBuilder<
Witness<TStore, TestingSlotClock, TLmdGhost, TEth1Backend, TEthSpec, TEventHandler>,
>
where
TStore: Store + 'static,
TLmdGhost: LmdGhost<TStore, TEthSpec> + 'static,
TEth1Backend: Eth1ChainBackend<TEthSpec> + 'static,
TEthSpec: EthSpec + 'static,
TEventHandler: EventHandler<TEthSpec> + 'static,
{
/// Sets the `BeaconChain` slot clock to `TestingSlotClock`.
///
/// Requires the state to be initialized.
pub fn testing_slot_clock(self, slot_duration: Duration) -> Result<Self, String> {
let genesis_time = self
.finalized_checkpoint
.as_ref()
.ok_or_else(|| "testing_slot_clock requires an initialized state")?
.beacon_state
.genesis_time;
let slot_clock = TestingSlotClock::new(
Slot::new(0),
Duration::from_secs(genesis_time),
slot_duration,
);
Ok(self.slot_clock(slot_clock))
}
}
impl<TStore, TSlotClock, TLmdGhost, TEth1Backend, TEthSpec>
BeaconChainBuilder<
Witness<TStore, TSlotClock, TLmdGhost, TEth1Backend, TEthSpec, NullEventHandler<TEthSpec>>,
>
where
TStore: Store + 'static,
TSlotClock: SlotClock + 'static,
TLmdGhost: LmdGhost<TStore, TEthSpec> + 'static,
TEth1Backend: Eth1ChainBackend<TEthSpec> + 'static,
TEthSpec: EthSpec + 'static,
{
/// Sets the `BeaconChain` event handler to `NullEventHandler`.
pub fn null_event_handler(self) -> Self {
let handler = NullEventHandler::default();
self.event_handler(handler)
}
}
fn genesis_block<T: EthSpec>(genesis_state: &BeaconState<T>, spec: &ChainSpec) -> BeaconBlock<T> {
let mut genesis_block = BeaconBlock::empty(&spec);
genesis_block.state_root = genesis_state.canonical_root();
genesis_block
}
#[cfg(test)]
mod test {
use super::*;
use eth2_hashing::hash;
use genesis::{generate_deterministic_keypairs, interop_genesis_state};
use sloggers::{null::NullLoggerBuilder, Build};
use ssz::Encode;
use std::time::Duration;
use store::MemoryStore;
use types::{EthSpec, MinimalEthSpec, Slot};
type TestEthSpec = MinimalEthSpec;
fn get_logger() -> Logger {
let builder = NullLoggerBuilder;
builder.build().expect("should build logger")
}
#[test]
fn recent_genesis() {
let validator_count = 8;
let genesis_time = 13371337;
let log = get_logger();
let store = Arc::new(MemoryStore::open());
let spec = MinimalEthSpec::default_spec();
let genesis_state = interop_genesis_state(
&generate_deterministic_keypairs(validator_count),
genesis_time,
&spec,
)
.expect("should create interop genesis state");
let chain = BeaconChainBuilder::new(MinimalEthSpec)
.logger(log.clone())
.store(store.clone())
.genesis_state(genesis_state)
.expect("should build state using recent genesis")
.dummy_eth1_backend()
.expect("should build the dummy eth1 backend")
.null_event_handler()
.testing_slot_clock(Duration::from_secs(1))
.expect("should configure testing slot clock")
.empty_reduced_tree_fork_choice()
.expect("should add fork choice to builder")
.build()
.expect("should build");
let head = chain.head();
let state = head.beacon_state;
let block = head.beacon_block;
assert_eq!(state.slot, Slot::new(0), "should start from genesis");
assert_eq!(
state.genesis_time, 13371337,
"should have the correct genesis time"
);
assert_eq!(
block.state_root,
state.canonical_root(),
"block should have correct state root"
);
assert_eq!(
chain
.store
.get::<BeaconBlock<_>>(&Hash256::zero())
.expect("should read db")
.expect("should find genesis block"),
block,
"should store genesis block under zero hash alias"
);
assert_eq!(
state.validators.len(),
validator_count,
"should have correct validator count"
);
assert_eq!(
chain.genesis_block_root,
block.canonical_root(),
"should have correct genesis block root"
);
}
#[test]
fn interop_state() {
let validator_count = 16;
let genesis_time = 42;
let spec = &TestEthSpec::default_spec();
let keypairs = generate_deterministic_keypairs(validator_count);
let state = interop_genesis_state::<TestEthSpec>(&keypairs, genesis_time, spec)
.expect("should build state");
assert_eq!(
state.eth1_data.block_hash,
Hash256::from_slice(&[0x42; 32]),
"eth1 block hash should be co-ordinated junk"
);
assert_eq!(
state.genesis_time, genesis_time,
"genesis time should be as specified"
);
for b in &state.balances {
assert_eq!(
*b, spec.max_effective_balance,
"validator balances should be max effective balance"
);
}
for v in &state.validators {
let creds = v.withdrawal_credentials.as_bytes();
assert_eq!(
creds[0], spec.bls_withdrawal_prefix_byte,
"first byte of withdrawal creds should be bls prefix"
);
assert_eq!(
&creds[1..],
&hash(&v.pubkey.as_ssz_bytes())[1..],
"rest of withdrawal creds should be pubkey hash"
)
}
assert_eq!(
state.balances.len(),
validator_count,
"validator balances len should be correct"
);
assert_eq!(
state.validators.len(),
validator_count,
"validator count should be correct"
);
}
}

View File

@@ -18,7 +18,6 @@ macro_rules! easy_from_to {
#[derive(Debug, PartialEq)]
pub enum BeaconChainError {
InsufficientValidators,
BadRecentBlockRoots,
UnableToReadSlot,
RevertedFinalizedEpoch {
previous_epoch: Epoch,
@@ -55,6 +54,9 @@ pub enum BlockProductionError {
BlockProcessingError(BlockProcessingError),
Eth1ChainError(Eth1ChainError),
BeaconStateError(BeaconStateError),
/// The `BeaconChain` was explicitly configured _without_ a connection to eth1, therefore it
/// cannot produce blocks.
NoEth1ChainConnection,
}
easy_from_to!(BlockProcessingError, BlockProductionError);

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
use serde_derive::{Deserialize, Serialize};
use std::marker::PhantomData;
use types::{Attestation, BeaconBlock, Epoch, EthSpec, Hash256};
pub use websocket_server::WebSocketSender;
pub trait EventHandler<T: EthSpec>: Sized + Send + Sync {
fn register(&self, kind: EventKind<T>) -> Result<(), String>;
@@ -8,6 +9,15 @@ pub trait EventHandler<T: EthSpec>: Sized + Send + Sync {
pub struct NullEventHandler<T: EthSpec>(PhantomData<T>);
impl<T: EthSpec> EventHandler<T> for WebSocketSender<T> {
fn register(&self, kind: EventKind<T>) -> Result<(), String> {
self.send_string(
serde_json::to_string(&kind)
.map_err(|e| format!("Unable to serialize event: {:?}", e))?,
)
}
}
impl<T: EthSpec> EventHandler<T> for NullEventHandler<T> {
fn register(&self, _kind: EventKind<T>) -> Result<(), String> {
Ok(())

View File

@@ -1,10 +1,11 @@
use crate::{metrics, BeaconChain, BeaconChainTypes};
use crate::{errors::BeaconChainError, metrics, BeaconChain, BeaconChainTypes};
use lmd_ghost::LmdGhost;
use state_processing::common::get_attesting_indices;
use parking_lot::RwLock;
use state_processing::{common::get_attesting_indices, per_slot_processing};
use std::sync::Arc;
use store::{Error as StoreError, Store};
use types::{
Attestation, BeaconBlock, BeaconState, BeaconStateError, Epoch, EthSpec, Hash256, Slot,
Attestation, BeaconBlock, BeaconState, BeaconStateError, Checkpoint, EthSpec, Hash256, Slot,
};
type Result<T> = std::result::Result<T, Error>;
@@ -16,6 +17,7 @@ pub enum Error {
BackendError(String),
BeaconStateError(BeaconStateError),
StoreError(StoreError),
BeaconChainError(Box<BeaconChainError>),
}
pub struct ForkChoice<T: BeaconChainTypes> {
@@ -26,6 +28,10 @@ pub struct ForkChoice<T: BeaconChainTypes> {
/// Does not necessarily need to be the _actual_ genesis, it suffices to be the finalized root
/// whenever the struct was instantiated.
genesis_block_root: Hash256,
/// The fork choice rule's current view of the justified checkpoint.
justified_checkpoint: RwLock<Checkpoint>,
/// The best justified checkpoint we've seen, which may be ahead of `justified_checkpoint`.
best_justified_checkpoint: RwLock<Checkpoint>,
}
impl<T: BeaconChainTypes> ForkChoice<T> {
@@ -35,41 +41,89 @@ impl<T: BeaconChainTypes> ForkChoice<T> {
/// block.
pub fn new(
store: Arc<T::Store>,
genesis_block: &BeaconBlock<T::EthSpec>,
backend: T::LmdGhost,
genesis_block_root: Hash256,
genesis_slot: Slot,
) -> Self {
let justified_checkpoint = Checkpoint {
epoch: genesis_slot.epoch(T::EthSpec::slots_per_epoch()),
root: genesis_block_root,
};
Self {
store: store.clone(),
backend: T::LmdGhost::new(store, genesis_block, genesis_block_root),
backend,
genesis_block_root,
justified_checkpoint: RwLock::new(justified_checkpoint.clone()),
best_justified_checkpoint: RwLock::new(justified_checkpoint),
}
}
/// Determine whether the fork choice's view of the justified checkpoint should be updated.
///
/// To prevent the bouncing attack, an update is allowed only in these conditions:
///
/// * We're in the first SAFE_SLOTS_TO_UPDATE_JUSTIFIED slots of the epoch, or
/// * The new justified checkpoint is a descendant of the current justified checkpoint
fn should_update_justified_checkpoint(
&self,
chain: &BeaconChain<T>,
new_justified_checkpoint: &Checkpoint,
) -> Result<bool> {
if Self::compute_slots_since_epoch_start(chain.slot()?)
< chain.spec.safe_slots_to_update_justified
{
return Ok(true);
}
let justified_checkpoint = self.justified_checkpoint.read().clone();
let current_justified_block = chain
.get_block(&justified_checkpoint.root)?
.ok_or_else(|| Error::MissingBlock(justified_checkpoint.root))?;
let new_justified_block = chain
.get_block(&new_justified_checkpoint.root)?
.ok_or_else(|| Error::MissingBlock(new_justified_checkpoint.root))?;
let slots_per_epoch = T::EthSpec::slots_per_epoch();
Ok(
new_justified_block.slot > justified_checkpoint.epoch.start_slot(slots_per_epoch)
&& chain.get_ancestor_block_root(
new_justified_checkpoint.root,
current_justified_block.slot,
)? == Some(justified_checkpoint.root),
)
}
/// Calculate how far `slot` lies from the start of its epoch.
fn compute_slots_since_epoch_start(slot: Slot) -> u64 {
let slots_per_epoch = T::EthSpec::slots_per_epoch();
(slot - slot.epoch(slots_per_epoch).start_slot(slots_per_epoch)).as_u64()
}
/// Run the fork choice rule to determine the head.
pub fn find_head(&self, chain: &BeaconChain<T>) -> Result<Hash256> {
let timer = metrics::start_timer(&metrics::FORK_CHOICE_FIND_HEAD_TIMES);
let start_slot = |epoch: Epoch| epoch.start_slot(T::EthSpec::slots_per_epoch());
// From the specification:
//
// Let justified_head be the descendant of finalized_head with the highest epoch that has
// been justified for at least 1 epoch ... If no such descendant exists,
// set justified_head to finalized_head.
let (start_state, start_block_root, start_block_slot) = {
let state = &chain.head().beacon_state;
// Check if we should update our view of the justified checkpoint.
// Doing this check here should be quasi-equivalent to the update in the `on_tick`
// function of the spec, so long as `find_head` is called at least once during the first
// SAFE_SLOTS_TO_UPDATE_JUSTIFIED slots.
let best_justified_checkpoint = self.best_justified_checkpoint.read();
if self.should_update_justified_checkpoint(chain, &best_justified_checkpoint)? {
*self.justified_checkpoint.write() = best_justified_checkpoint.clone();
}
let (block_root, block_slot) =
if state.current_epoch() + 1 > state.current_justified_checkpoint.epoch {
(
state.current_justified_checkpoint.root,
start_slot(state.current_justified_checkpoint.epoch),
)
} else {
(
state.finalized_checkpoint.root,
start_slot(state.finalized_checkpoint.epoch),
)
};
let current_justified_checkpoint = self.justified_checkpoint.read().clone();
let (block_root, block_justified_slot) = (
current_justified_checkpoint.root,
current_justified_checkpoint
.epoch
.start_slot(T::EthSpec::slots_per_epoch()),
);
let block = chain
.store
@@ -83,12 +137,17 @@ impl<T: BeaconChainTypes> ForkChoice<T> {
block_root
};
let state = chain
.store
.get::<BeaconState<T::EthSpec>>(&block.state_root)?
let mut state = chain
.get_state(&block.state_root)?
.ok_or_else(|| Error::MissingState(block.state_root))?;
(state, block_root, block_slot)
// Fast-forward the state to the start slot of the epoch where it was justified.
for _ in block.slot.as_u64()..block_justified_slot.as_u64() {
per_slot_processing(&mut state, &chain.spec)
.map_err(|e| BeaconChainError::SlotProcessingError(e))?
}
(state, block_root, block_justified_slot)
};
// A function that returns the weight for some validator index.
@@ -111,10 +170,11 @@ impl<T: BeaconChainTypes> ForkChoice<T> {
/// Process all attestations in the given `block`.
///
/// Assumes the block (and therefore it's attestations) are valid. It is a logic error to
/// Assumes the block (and therefore its attestations) are valid. It is a logic error to
/// provide an invalid block.
pub fn process_block(
&self,
chain: &BeaconChain<T>,
state: &BeaconState<T::EthSpec>,
block: &BeaconBlock<T::EthSpec>,
block_root: Hash256,
@@ -137,6 +197,16 @@ impl<T: BeaconChainTypes> ForkChoice<T> {
}
}
// Check if we should update our view of the justified checkpoint
if state.current_justified_checkpoint.epoch > self.justified_checkpoint.read().epoch {
*self.best_justified_checkpoint.write() = state.current_justified_checkpoint.clone();
if self
.should_update_justified_checkpoint(chain, &state.current_justified_checkpoint)?
{
*self.justified_checkpoint.write() = state.current_justified_checkpoint.clone();
}
}
// This does not apply a vote to the block, it just makes fork choice aware of the block so
// it can still be identified as the head even if it doesn't have any votes.
//
@@ -228,6 +298,12 @@ impl From<BeaconStateError> for Error {
}
}
impl From<BeaconChainError> for Error {
fn from(e: BeaconChainError) -> Error {
Error::BeaconChainError(Box::new(e))
}
}
impl From<StoreError> for Error {
fn from(e: StoreError) -> Error {
Error::StoreError(e)

View File

@@ -3,10 +3,10 @@
extern crate lazy_static;
mod beacon_chain;
mod beacon_chain_builder;
pub mod builder;
mod checkpoint;
mod errors;
mod eth1_chain;
pub mod eth1_chain;
pub mod events;
mod fork_choice;
mod iter;
@@ -19,15 +19,16 @@ pub use self::beacon_chain::{
};
pub use self::checkpoint::CheckPoint;
pub use self::errors::{BeaconChainError, BlockProductionError};
pub use beacon_chain_builder::BeaconChainBuilder;
pub use eth1_chain::{Eth1ChainBackend, InteropEth1ChainBackend};
pub use eth1_chain::{Eth1Chain, Eth1ChainBackend};
pub use events::EventHandler;
pub use fork_choice::ForkChoice;
pub use lmd_ghost;
pub use metrics::scrape_for_metrics;
pub use parking_lot;
pub use slot_clock;
pub use state_processing::per_block_processing::errors::{
AttestationValidationError, AttesterSlashingValidationError, DepositValidationError,
ExitValidationError, ProposerSlashingValidationError, TransferValidationError,
ExitValidationError, ProposerSlashingValidationError,
};
pub use store;
pub use types;

View File

@@ -172,8 +172,6 @@ lazy_static! {
try_create_int_gauge("beacon_head_state_finalized_root", "Finalized root at the head of the chain");
pub static ref HEAD_STATE_FINALIZED_EPOCH: Result<IntGauge> =
try_create_int_gauge("beacon_head_state_finalized_epoch", "Finalized epoch at the head of the chain");
pub static ref HEAD_STATE_SHARDS: Result<IntGauge> =
try_create_int_gauge("beacon_head_state_shard_total", "Count of shards in the beacon chain");
pub static ref HEAD_STATE_TOTAL_VALIDATORS: Result<IntGauge> =
try_create_int_gauge("beacon_head_state_total_validators_total", "Count of validators at the head of the chain");
pub static ref HEAD_STATE_ACTIVE_VALIDATORS: Result<IntGauge> =
@@ -226,7 +224,6 @@ fn scrape_head_state<T: BeaconChainTypes>(state: &BeaconState<T::EthSpec>, state
&HEAD_STATE_FINALIZED_EPOCH,
state.finalized_checkpoint.epoch,
);
set_gauge_by_usize(&HEAD_STATE_SHARDS, state.previous_crosslinks.len());
set_gauge_by_usize(&HEAD_STATE_TOTAL_VALIDATORS, state.validators.len());
set_gauge_by_u64(&HEAD_STATE_VALIDATOR_BALANCES, state.balances.iter().sum());
set_gauge_by_usize(

View File

@@ -1,28 +1,38 @@
use crate::{
events::NullEventHandler, AttestationProcessingOutcome, BeaconChain, BeaconChainBuilder,
BeaconChainTypes, BlockProcessingOutcome, InteropEth1ChainBackend,
builder::{BeaconChainBuilder, Witness},
eth1_chain::CachingEth1Backend,
events::NullEventHandler,
AttestationProcessingOutcome, BeaconChain, BeaconChainTypes, BlockProcessingOutcome,
};
use lmd_ghost::LmdGhost;
use genesis::interop_genesis_state;
use lmd_ghost::ThreadSafeReducedTree;
use rayon::prelude::*;
use sloggers::{terminal::TerminalLoggerBuilder, types::Severity, Build};
use slot_clock::TestingSlotClock;
use state_processing::per_slot_processing;
use std::marker::PhantomData;
use std::sync::Arc;
use std::time::Duration;
use store::MemoryStore;
use tree_hash::{SignedRoot, TreeHash};
use types::{
AggregateSignature, Attestation, AttestationDataAndCustodyBit, BeaconBlock, BeaconState,
BitList, ChainSpec, Domain, EthSpec, Hash256, Keypair, RelativeEpoch, SecretKey, Signature,
Slot,
AggregateSignature, Attestation, BeaconBlock, BeaconState, BitList, ChainSpec, Domain, EthSpec,
Hash256, Keypair, SecretKey, Signature, Slot,
};
pub use crate::persisted_beacon_chain::{PersistedBeaconChain, BEACON_CHAIN_DB_KEY};
pub use types::test_utils::generate_deterministic_keypairs;
pub use crate::persisted_beacon_chain::{PersistedBeaconChain, BEACON_CHAIN_DB_KEY};
pub const HARNESS_GENESIS_TIME: u64 = 1_567_552_690; // 4th September 2019
pub type HarnessType<E> = Witness<
MemoryStore,
TestingSlotClock,
ThreadSafeReducedTree<MemoryStore, E>,
CachingEth1Backend<E, MemoryStore>,
E,
NullEventHandler<E>,
>;
/// Indicates how the `BeaconChainHarness` should produce blocks.
#[derive(Clone, Copy, Debug)]
pub enum BlockStrategy {
@@ -48,50 +58,19 @@ pub enum AttestationStrategy {
SomeValidators(Vec<usize>),
}
/// Used to make the `BeaconChainHarness` generic over some types.
pub struct CommonTypes<L, E>
where
L: LmdGhost<MemoryStore, E>,
E: EthSpec,
{
_phantom_l: PhantomData<L>,
_phantom_e: PhantomData<E>,
}
impl<L, E> BeaconChainTypes for CommonTypes<L, E>
where
L: LmdGhost<MemoryStore, E> + 'static,
E: EthSpec,
{
type Store = MemoryStore;
type SlotClock = TestingSlotClock;
type LmdGhost = L;
type Eth1Chain = InteropEth1ChainBackend<E>;
type EthSpec = E;
type EventHandler = NullEventHandler<E>;
}
/// A testing harness which can instantiate a `BeaconChain` and populate it with blocks and
/// attestations.
///
/// Used for testing.
pub struct BeaconChainHarness<L, E>
where
L: LmdGhost<MemoryStore, E> + 'static,
E: EthSpec,
{
pub chain: BeaconChain<CommonTypes<L, E>>,
pub struct BeaconChainHarness<T: BeaconChainTypes> {
pub chain: BeaconChain<T>,
pub keypairs: Vec<Keypair>,
pub spec: ChainSpec,
}
impl<L, E> BeaconChainHarness<L, E>
where
L: LmdGhost<MemoryStore, E>,
E: EthSpec,
{
impl<E: EthSpec> BeaconChainHarness<HarnessType<E>> {
/// Instantiate a new harness with `validator_count` initial validators.
pub fn new(keypairs: Vec<Keypair>) -> Self {
pub fn new(eth_spec_instance: E, keypairs: Vec<Keypair>) -> Self {
let spec = E::default_spec();
let log = TerminalLoggerBuilder::new()
@@ -99,22 +78,29 @@ where
.build()
.expect("logger should build");
let store = Arc::new(MemoryStore::open());
let chain =
BeaconChainBuilder::quick_start(HARNESS_GENESIS_TIME, &keypairs, spec.clone(), log)
.unwrap_or_else(|e| panic!("Failed to create beacon chain builder: {}", e))
.build(
store.clone(),
InteropEth1ChainBackend::default(),
NullEventHandler::default(),
let chain = BeaconChainBuilder::new(eth_spec_instance)
.logger(log.clone())
.custom_spec(spec.clone())
.store(Arc::new(MemoryStore::open()))
.genesis_state(
interop_genesis_state::<E>(&keypairs, HARNESS_GENESIS_TIME, &spec)
.expect("should generate interop state"),
)
.unwrap_or_else(|e| panic!("Failed to build beacon chain: {}", e));
.expect("should build state using recent genesis")
.dummy_eth1_backend()
.expect("should build dummy backend")
.null_event_handler()
.testing_slot_clock(Duration::from_secs(1))
.expect("should configure testing slot clock")
.empty_reduced_tree_fork_choice()
.expect("should add fork choice to builder")
.build()
.expect("should build");
Self {
spec: chain.spec.clone(),
chain,
keypairs,
spec,
}
}
@@ -216,7 +202,7 @@ where
.block_proposer(slot)
.expect("should get block proposer from chain"),
_ => state
.get_beacon_proposer_index(slot, RelativeEpoch::Current, &self.spec)
.get_beacon_proposer_index(slot, &self.spec)
.expect("should get block proposer from state"),
};
@@ -293,13 +279,13 @@ where
let mut attestations = vec![];
state
.get_crosslink_committees_at_slot(state.slot)
.get_beacon_committees_at_slot(state.slot)
.expect("should get committees")
.iter()
.for_each(|cc| {
let committee_size = cc.committee.len();
.for_each(|bc| {
let committee_size = bc.committee.len();
let mut local_attestations: Vec<Attestation<E>> = cc
let mut local_attestations: Vec<Attestation<E>> = bc
.committee
.par_iter()
.enumerate()
@@ -310,7 +296,7 @@ where
let data = self
.chain
.produce_attestation_data_for_block(
cc.shard,
bc.index,
head_block_root,
head_block_slot,
state,
@@ -322,18 +308,15 @@ where
aggregation_bits
.set(i, true)
.expect("should be able to set aggregation bits");
let custody_bits = BitList::with_capacity(committee_size)
.expect("should make custody bits");
let signature = {
let message = AttestationDataAndCustodyBit {
data: data.clone(),
custody_bit: false,
}
.tree_hash_root();
let message = data.tree_hash_root();
let domain =
spec.get_domain(data.target.epoch, Domain::Attestation, fork);
let domain = spec.get_domain(
data.target.epoch,
Domain::BeaconAttester,
fork,
);
let mut agg_sig = AggregateSignature::new();
agg_sig.add(&Signature::new(
@@ -348,7 +331,6 @@ where
let attestation = Attestation {
aggregation_bits,
data,
custody_bits,
signature,
};

View File

@@ -6,14 +6,13 @@ extern crate lazy_static;
use beacon_chain::AttestationProcessingOutcome;
use beacon_chain::{
test_utils::{
AttestationStrategy, BeaconChainHarness, BlockStrategy, CommonTypes, PersistedBeaconChain,
AttestationStrategy, BeaconChainHarness, BlockStrategy, HarnessType, PersistedBeaconChain,
BEACON_CHAIN_DB_KEY,
},
BlockProcessingOutcome,
};
use lmd_ghost::ThreadSafeReducedTree;
use rand::Rng;
use store::{MemoryStore, Store};
use store::Store;
use types::test_utils::{SeedableRng, TestRandom, XorShiftRng};
use types::{Deposit, EthSpec, Hash256, Keypair, MinimalEthSpec, RelativeEpoch, Slot};
@@ -25,10 +24,8 @@ lazy_static! {
static ref KEYPAIRS: Vec<Keypair> = types::test_utils::generate_deterministic_keypairs(VALIDATOR_COUNT);
}
type TestForkChoice = ThreadSafeReducedTree<MemoryStore, MinimalEthSpec>;
fn get_harness(validator_count: usize) -> BeaconChainHarness<TestForkChoice, MinimalEthSpec> {
let harness = BeaconChainHarness::new(KEYPAIRS[0..validator_count].to_vec());
fn get_harness(validator_count: usize) -> BeaconChainHarness<HarnessType<MinimalEthSpec>> {
let harness = BeaconChainHarness::new(MinimalEthSpec, KEYPAIRS[0..validator_count].to_vec());
harness.advance_slot();
@@ -322,7 +319,7 @@ fn roundtrip_operation_pool() {
harness.chain.persist().unwrap();
let key = Hash256::from_slice(&BEACON_CHAIN_DB_KEY.as_bytes());
let p: PersistedBeaconChain<CommonTypes<TestForkChoice, MinimalEthSpec>> =
let p: PersistedBeaconChain<HarnessType<MinimalEthSpec>> =
harness.chain.store.get(&key).unwrap().unwrap();
let restored_op_pool = p

View File

@@ -4,6 +4,10 @@ version = "0.1.0"
authors = ["Age Manning <Age@AgeManning.com>"]
edition = "2018"
[dev-dependencies]
sloggers = "0.3.4"
toml = "^0.5"
[dependencies]
beacon_chain = { path = "../beacon_chain" }
store = { path = "../store" }
@@ -12,22 +16,27 @@ eth2-libp2p = { path = "../eth2-libp2p" }
rpc = { path = "../rpc" }
rest_api = { path = "../rest_api" }
websocket_server = { path = "../websocket_server" }
prometheus = "^0.6"
prometheus = "0.7.0"
types = { path = "../../eth2/types" }
tree_hash = "0.1"
tree_hash = "0.1.0"
eth2_config = { path = "../../eth2/utils/eth2_config" }
slot_clock = { path = "../../eth2/utils/slot_clock" }
serde = "1.0.93"
serde_derive = "1.0"
error-chain = "0.12.0"
serde_yaml = "0.8"
slog = { version = "^2.2.3" , features = ["max_level_trace"] }
slog-async = "^2.3.0"
slog-json = "^2.3"
tokio = "0.1.15"
clap = "2.32.0"
dirs = "1.0.3"
exit-future = "0.1.3"
futures = "0.1.25"
reqwest = "0.9"
url = "1.2"
serde = "1.0.102"
serde_derive = "1.0.102"
error-chain = "0.12.1"
serde_yaml = "0.8.11"
slog = { version = "2.5.2", features = ["max_level_trace"] }
slog-async = "2.3.0"
slog-json = "2.3.0"
tokio = "0.1.22"
clap = "2.33.0"
dirs = "2.0.2"
exit-future = "0.1.4"
futures = "0.1.29"
reqwest = "0.9.22"
url = "2.1.0"
lmd_ghost = { path = "../../eth2/lmd_ghost" }
eth1 = { path = "../eth1" }
genesis = { path = "../genesis" }
environment = { path = "../../lighthouse/environment" }
lighthouse_bootstrap = { path = "../../eth2/utils/lighthouse_bootstrap" }

View File

@@ -0,0 +1,715 @@
use crate::config::{ClientGenesis, Config as ClientConfig};
use crate::Client;
use beacon_chain::{
builder::{BeaconChainBuilder, Witness},
eth1_chain::CachingEth1Backend,
lmd_ghost::ThreadSafeReducedTree,
slot_clock::{SlotClock, SystemTimeSlotClock},
store::{DiskStore, MemoryStore, Store},
BeaconChain, BeaconChainTypes, Eth1ChainBackend, EventHandler,
};
use environment::RuntimeContext;
use eth1::{Config as Eth1Config, Service as Eth1Service};
use eth2_config::Eth2Config;
use exit_future::Signal;
use futures::{future, Future, IntoFuture, Stream};
use genesis::{
generate_deterministic_keypairs, interop_genesis_state, state_from_ssz_file, Eth1GenesisService,
};
use lighthouse_bootstrap::Bootstrapper;
use lmd_ghost::LmdGhost;
use network::{NetworkConfig, NetworkMessage, Service as NetworkService};
use rpc::Config as RpcConfig;
use slog::{debug, error, info, warn};
use std::net::SocketAddr;
use std::path::Path;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::mpsc::UnboundedSender;
use tokio::timer::Interval;
use types::{ChainSpec, EthSpec};
use websocket_server::{Config as WebSocketConfig, WebSocketSender};
/// The interval between notifier events.
pub const NOTIFIER_INTERVAL_SECONDS: u64 = 15;
/// Create a warning log whenever the peer count is at or below this value.
pub const WARN_PEER_COUNT: usize = 1;
/// Interval between polling the eth1 node for genesis information.
pub const ETH1_GENESIS_UPDATE_INTERVAL_MILLIS: u64 = 500;
/// Builds a `Client` instance.
///
/// ## Notes
///
/// The builder may start some services (e.g.., libp2p, http server) immediately after they are
/// initialized, _before_ the `self.build(..)` method has been called.
///
/// Types may be elided and the compiler will infer them once all required methods have been
/// called.
///
/// If type inference errors are raised, ensure all necessary components have been initialized. For
/// example, the compiler will be unable to infer `T::Store` unless `self.disk_store(..)` or
/// `self.memory_store(..)` has been called.
pub struct ClientBuilder<T: BeaconChainTypes> {
slot_clock: Option<T::SlotClock>,
store: Option<Arc<T::Store>>,
runtime_context: Option<RuntimeContext<T::EthSpec>>,
chain_spec: Option<ChainSpec>,
beacon_chain_builder: Option<BeaconChainBuilder<T>>,
beacon_chain: Option<Arc<BeaconChain<T>>>,
eth1_service: Option<Eth1Service>,
exit_signals: Vec<Signal>,
event_handler: Option<T::EventHandler>,
libp2p_network: Option<Arc<NetworkService<T>>>,
libp2p_network_send: Option<UnboundedSender<NetworkMessage>>,
http_listen_addr: Option<SocketAddr>,
websocket_listen_addr: Option<SocketAddr>,
eth_spec_instance: T::EthSpec,
}
impl<TStore, TSlotClock, TLmdGhost, TEth1Backend, TEthSpec, TEventHandler>
ClientBuilder<Witness<TStore, TSlotClock, TLmdGhost, TEth1Backend, TEthSpec, TEventHandler>>
where
TStore: Store + 'static,
TSlotClock: SlotClock + Clone + 'static,
TLmdGhost: LmdGhost<TStore, TEthSpec> + 'static,
TEth1Backend: Eth1ChainBackend<TEthSpec> + 'static,
TEthSpec: EthSpec + 'static,
TEventHandler: EventHandler<TEthSpec> + 'static,
{
/// Instantiates a new, empty builder.
///
/// The `eth_spec_instance` parameter is used to concretize `TEthSpec`.
pub fn new(eth_spec_instance: TEthSpec) -> Self {
Self {
slot_clock: None,
store: None,
runtime_context: None,
chain_spec: None,
beacon_chain_builder: None,
beacon_chain: None,
eth1_service: None,
exit_signals: vec![],
event_handler: None,
libp2p_network: None,
libp2p_network_send: None,
http_listen_addr: None,
websocket_listen_addr: None,
eth_spec_instance,
}
}
/// Specifies the runtime context (tokio executor, logger, etc) for client services.
pub fn runtime_context(mut self, context: RuntimeContext<TEthSpec>) -> Self {
self.runtime_context = Some(context);
self
}
/// Specifies the `ChainSpec`.
pub fn chain_spec(mut self, spec: ChainSpec) -> Self {
self.chain_spec = Some(spec);
self
}
/// Initializes the `BeaconChainBuilder`. The `build_beacon_chain` method will need to be
/// called later in order to actually instantiate the `BeaconChain`.
pub fn beacon_chain_builder(
mut self,
client_genesis: ClientGenesis,
config: Eth1Config,
) -> impl Future<Item = Self, Error = String> {
let store = self.store.clone();
let chain_spec = self.chain_spec.clone();
let runtime_context = self.runtime_context.clone();
let eth_spec_instance = self.eth_spec_instance.clone();
future::ok(())
.and_then(move |()| {
let store = store
.ok_or_else(|| "beacon_chain_start_method requires a store".to_string())?;
let context = runtime_context
.ok_or_else(|| "beacon_chain_start_method requires a log".to_string())?
.service_context("beacon");
let spec = chain_spec
.ok_or_else(|| "beacon_chain_start_method requires a chain spec".to_string())?;
let builder = BeaconChainBuilder::new(eth_spec_instance)
.logger(context.log.clone())
.store(store.clone())
.custom_spec(spec.clone());
Ok((builder, spec, context))
})
.and_then(move |(builder, spec, context)| {
let genesis_state_future: Box<dyn Future<Item = _, Error = _> + Send> =
match client_genesis {
ClientGenesis::Interop {
validator_count,
genesis_time,
} => {
let keypairs = generate_deterministic_keypairs(validator_count);
let result = interop_genesis_state(&keypairs, genesis_time, &spec);
let future = result
.and_then(move |genesis_state| builder.genesis_state(genesis_state))
.into_future()
.map(|v| (v, None));
Box::new(future)
}
ClientGenesis::SszFile { path } => {
let result = state_from_ssz_file(path);
let future = result
.and_then(move |genesis_state| builder.genesis_state(genesis_state))
.into_future()
.map(|v| (v, None));
Box::new(future)
}
ClientGenesis::DepositContract => {
let genesis_service = Eth1GenesisService::new(
// Some of the configuration options for `Eth1Config` are
// hard-coded when listening for genesis from the deposit contract.
//
// The idea is that the `Eth1Config` supplied to this function
// (`config`) is intended for block production duties (i.e.,
// listening for deposit events and voting on eth1 data) and that
// we can make listening for genesis more efficient if we modify
// some params.
Eth1Config {
// Truncating the block cache makes searching for genesis more
// complicated.
block_cache_truncation: None,
// Scan large ranges of blocks when awaiting genesis.
blocks_per_log_query: 1_000,
// Only perform a single log request each time the eth1 node is
// polled.
//
// For small testnets this makes finding genesis much faster,
// as it usually happens within 1,000 blocks.
max_log_requests_per_update: Some(1),
// Only perform a single block request each time the eth1 node
// is polled.
//
// For small testnets, this is much faster as they do not have
// a `MIN_GENESIS_SECONDS`, so after `MIN_GENESIS_VALIDATOR_COUNT`
// has been reached only a single block needs to be read.
max_blocks_per_update: Some(1),
..config
},
context.log.clone(),
);
let future = genesis_service
.wait_for_genesis_state(
Duration::from_millis(ETH1_GENESIS_UPDATE_INTERVAL_MILLIS),
context.eth2_config().spec.clone(),
)
.and_then(move |genesis_state| builder.genesis_state(genesis_state))
.map(|v| (v, Some(genesis_service.into_core_service())));
Box::new(future)
}
ClientGenesis::RemoteNode { server, .. } => {
let future = Bootstrapper::connect(server.to_string(), &context.log)
.map_err(|e| {
format!("Failed to initialize bootstrap client: {}", e)
})
.into_future()
.and_then(|bootstrapper| {
let (genesis_state, _genesis_block) =
bootstrapper.genesis().map_err(|e| {
format!("Failed to bootstrap genesis state: {}", e)
})?;
builder.genesis_state(genesis_state)
})
.map(|v| (v, None));
Box::new(future)
}
ClientGenesis::Resume => {
let future = builder.resume_from_db().into_future().map(|v| (v, None));
Box::new(future)
}
};
genesis_state_future
})
.map(move |(beacon_chain_builder, eth1_service_option)| {
self.eth1_service = eth1_service_option;
self.beacon_chain_builder = Some(beacon_chain_builder);
self
})
}
/// Immediately starts the libp2p networking stack.
pub fn libp2p_network(mut self, config: &NetworkConfig) -> Result<Self, String> {
let beacon_chain = self
.beacon_chain
.clone()
.ok_or_else(|| "libp2p_network requires a beacon chain")?;
let context = self
.runtime_context
.as_ref()
.ok_or_else(|| "libp2p_network requires a runtime_context")?
.service_context("network");
let (network, network_send) =
NetworkService::new(beacon_chain, config, &context.executor, context.log)
.map_err(|e| format!("Failed to start libp2p network: {:?}", e))?;
self.libp2p_network = Some(network);
self.libp2p_network_send = Some(network_send);
Ok(self)
}
/// Immediately starts the gRPC server (gRPC is soon to be deprecated).
pub fn grpc_server(mut self, config: &RpcConfig) -> Result<Self, String> {
let beacon_chain = self
.beacon_chain
.clone()
.ok_or_else(|| "grpc_server requires a beacon chain")?;
let context = self
.runtime_context
.as_ref()
.ok_or_else(|| "grpc_server requires a runtime_context")?
.service_context("grpc");
let network_send = self
.libp2p_network_send
.clone()
.ok_or_else(|| "grpc_server requires a libp2p network")?;
let exit_signal = rpc::start_server(
config,
&context.executor,
network_send,
beacon_chain,
context.log,
);
self.exit_signals.push(exit_signal);
Ok(self)
}
/// Immediately starts the beacon node REST API http server.
pub fn http_server(
mut self,
client_config: &ClientConfig,
eth2_config: &Eth2Config,
) -> Result<Self, String> {
let beacon_chain = self
.beacon_chain
.clone()
.ok_or_else(|| "grpc_server requires a beacon chain")?;
let context = self
.runtime_context
.as_ref()
.ok_or_else(|| "http_server requires a runtime_context")?
.service_context("http");
let network = self
.libp2p_network
.clone()
.ok_or_else(|| "grpc_server requires a libp2p network")?;
let network_send = self
.libp2p_network_send
.clone()
.ok_or_else(|| "grpc_server requires a libp2p network sender")?;
let network_info = rest_api::NetworkInfo {
network_service: network.clone(),
network_chan: network_send.clone(),
};
let (exit_signal, listening_addr) = rest_api::start_server(
&client_config.rest_api,
&context.executor,
beacon_chain.clone(),
network_info,
client_config.db_path().expect("unable to read datadir"),
eth2_config.clone(),
context.log,
)
.map_err(|e| format!("Failed to start HTTP API: {:?}", e))?;
self.exit_signals.push(exit_signal);
self.http_listen_addr = Some(listening_addr);
Ok(self)
}
/// Immediately starts the service that periodically logs about the libp2p peer count.
pub fn peer_count_notifier(mut self) -> Result<Self, String> {
let context = self
.runtime_context
.as_ref()
.ok_or_else(|| "peer_count_notifier requires a runtime_context")?
.service_context("peer_notifier");
let log = context.log.clone();
let log_2 = context.log.clone();
let network = self
.libp2p_network
.clone()
.ok_or_else(|| "peer_notifier requires a libp2p network")?;
let (exit_signal, exit) = exit_future::signal();
self.exit_signals.push(exit_signal);
let interval_future = Interval::new(
Instant::now(),
Duration::from_secs(NOTIFIER_INTERVAL_SECONDS),
)
.map_err(move |e| error!(log_2, "Notifier timer failed"; "error" => format!("{:?}", e)))
.for_each(move |_| {
// NOTE: Panics if libp2p is poisoned.
let connected_peer_count = network.libp2p_service().lock().swarm.connected_peers();
debug!(log, "Connected peer status"; "peer_count" => connected_peer_count);
if connected_peer_count <= WARN_PEER_COUNT {
warn!(log, "Low peer count"; "peer_count" => connected_peer_count);
}
Ok(())
});
context
.executor
.spawn(exit.until(interval_future).map(|_| ()));
Ok(self)
}
/// Immediately starts the service that periodically logs information each slot.
pub fn slot_notifier(mut self) -> Result<Self, String> {
let context = self
.runtime_context
.as_ref()
.ok_or_else(|| "slot_notifier requires a runtime_context")?
.service_context("slot_notifier");
let log = context.log.clone();
let log_2 = log.clone();
let beacon_chain = self
.beacon_chain
.clone()
.ok_or_else(|| "slot_notifier requires a libp2p network")?;
let spec = self
.chain_spec
.clone()
.ok_or_else(|| "slot_notifier requires a chain spec".to_string())?;
let slot_duration = Duration::from_millis(spec.milliseconds_per_slot);
let duration_to_next_slot = beacon_chain
.slot_clock
.duration_to_next_slot()
.ok_or_else(|| "slot_notifier unable to determine time to next slot")?;
let (exit_signal, exit) = exit_future::signal();
self.exit_signals.push(exit_signal);
let interval_future = Interval::new(Instant::now() + duration_to_next_slot, slot_duration)
.map_err(move |e| error!(log_2, "Slot timer failed"; "error" => format!("{:?}", e)))
.for_each(move |_| {
let best_slot = beacon_chain.head().beacon_block.slot;
let latest_block_root = beacon_chain.head().beacon_block_root;
if let Ok(current_slot) = beacon_chain.slot() {
info!(
log,
"Slot start";
"skip_slots" => current_slot.saturating_sub(best_slot),
"best_block_root" => format!("{}", latest_block_root),
"best_block_slot" => best_slot,
"slot" => current_slot,
)
} else {
error!(
log,
"Beacon chain running whilst slot clock is unavailable."
);
};
Ok(())
});
context
.executor
.spawn(exit.until(interval_future).map(|_| ()));
Ok(self)
}
/// Consumers the builder, returning a `Client` if all necessary components have been
/// specified.
///
/// If type inference errors are being raised, see the comment on the definition of `Self`.
pub fn build(
self,
) -> Client<Witness<TStore, TSlotClock, TLmdGhost, TEth1Backend, TEthSpec, TEventHandler>> {
Client {
beacon_chain: self.beacon_chain,
libp2p_network: self.libp2p_network,
http_listen_addr: self.http_listen_addr,
websocket_listen_addr: self.websocket_listen_addr,
_exit_signals: self.exit_signals,
}
}
}
impl<TStore, TSlotClock, TEth1Backend, TEthSpec, TEventHandler>
ClientBuilder<
Witness<
TStore,
TSlotClock,
ThreadSafeReducedTree<TStore, TEthSpec>,
TEth1Backend,
TEthSpec,
TEventHandler,
>,
>
where
TStore: Store + 'static,
TSlotClock: SlotClock + Clone + 'static,
TEth1Backend: Eth1ChainBackend<TEthSpec> + 'static,
TEthSpec: EthSpec + 'static,
TEventHandler: EventHandler<TEthSpec> + 'static,
{
/// Consumes the internal `BeaconChainBuilder`, attaching the resulting `BeaconChain` to self.
pub fn build_beacon_chain(mut self) -> Result<Self, String> {
let chain = self
.beacon_chain_builder
.ok_or_else(|| "beacon_chain requires a beacon_chain_builder")?
.event_handler(
self.event_handler
.ok_or_else(|| "beacon_chain requires an event handler")?,
)
.slot_clock(
self.slot_clock
.clone()
.ok_or_else(|| "beacon_chain requires a slot clock")?,
)
.empty_reduced_tree_fork_choice()
.map_err(|e| format!("Failed to init fork choice: {}", e))?
.build()
.map_err(|e| format!("Failed to build beacon chain: {}", e))?;
self.beacon_chain = Some(Arc::new(chain));
self.beacon_chain_builder = None;
self.event_handler = None;
Ok(self)
}
}
impl<TStore, TSlotClock, TLmdGhost, TEth1Backend, TEthSpec>
ClientBuilder<
Witness<TStore, TSlotClock, TLmdGhost, TEth1Backend, TEthSpec, WebSocketSender<TEthSpec>>,
>
where
TStore: Store + 'static,
TSlotClock: SlotClock + 'static,
TLmdGhost: LmdGhost<TStore, TEthSpec> + 'static,
TEth1Backend: Eth1ChainBackend<TEthSpec> + 'static,
TEthSpec: EthSpec + 'static,
{
/// Specifies that the `BeaconChain` should publish events using the WebSocket server.
pub fn websocket_event_handler(mut self, config: WebSocketConfig) -> Result<Self, String> {
let context = self
.runtime_context
.as_ref()
.ok_or_else(|| "websocket_event_handler requires a runtime_context")?
.service_context("ws");
let (sender, exit_signal, listening_addr): (
WebSocketSender<TEthSpec>,
Option<_>,
Option<_>,
) = if config.enabled {
let (sender, exit, listening_addr) =
websocket_server::start_server(&config, &context.executor, &context.log)?;
(sender, Some(exit), Some(listening_addr))
} else {
(WebSocketSender::dummy(), None, None)
};
if let Some(signal) = exit_signal {
self.exit_signals.push(signal);
}
self.event_handler = Some(sender);
self.websocket_listen_addr = listening_addr;
Ok(self)
}
}
impl<TSlotClock, TLmdGhost, TEth1Backend, TEthSpec, TEventHandler>
ClientBuilder<Witness<DiskStore, TSlotClock, TLmdGhost, TEth1Backend, TEthSpec, TEventHandler>>
where
TSlotClock: SlotClock + 'static,
TLmdGhost: LmdGhost<DiskStore, TEthSpec> + 'static,
TEth1Backend: Eth1ChainBackend<TEthSpec> + 'static,
TEthSpec: EthSpec + 'static,
TEventHandler: EventHandler<TEthSpec> + 'static,
{
/// Specifies that the `Client` should use a `DiskStore` database.
pub fn disk_store(mut self, path: &Path) -> Result<Self, String> {
let store = DiskStore::open(path)
.map_err(|e| format!("Unable to open database: {:?}", e).to_string())?;
self.store = Some(Arc::new(store));
Ok(self)
}
}
impl<TSlotClock, TLmdGhost, TEth1Backend, TEthSpec, TEventHandler>
ClientBuilder<
Witness<MemoryStore, TSlotClock, TLmdGhost, TEth1Backend, TEthSpec, TEventHandler>,
>
where
TSlotClock: SlotClock + 'static,
TLmdGhost: LmdGhost<MemoryStore, TEthSpec> + 'static,
TEth1Backend: Eth1ChainBackend<TEthSpec> + 'static,
TEthSpec: EthSpec + 'static,
TEventHandler: EventHandler<TEthSpec> + 'static,
{
/// Specifies that the `Client` should use a `MemoryStore` database.
pub fn memory_store(mut self) -> Self {
let store = MemoryStore::open();
self.store = Some(Arc::new(store));
self
}
}
impl<TStore, TSlotClock, TLmdGhost, TEthSpec, TEventHandler>
ClientBuilder<
Witness<
TStore,
TSlotClock,
TLmdGhost,
CachingEth1Backend<TEthSpec, TStore>,
TEthSpec,
TEventHandler,
>,
>
where
TStore: Store + 'static,
TSlotClock: SlotClock + 'static,
TLmdGhost: LmdGhost<TStore, TEthSpec> + 'static,
TEthSpec: EthSpec + 'static,
TEventHandler: EventHandler<TEthSpec> + 'static,
{
/// Specifies that the `BeaconChain` should cache eth1 blocks/logs from a remote eth1 node
/// (e.g., Parity/Geth) and refer to that cache when collecting deposits or eth1 votes during
/// block production.
pub fn caching_eth1_backend(mut self, config: Eth1Config) -> Result<Self, String> {
let context = self
.runtime_context
.as_ref()
.ok_or_else(|| "caching_eth1_backend requires a runtime_context")?
.service_context("eth1_rpc");
let beacon_chain_builder = self
.beacon_chain_builder
.ok_or_else(|| "caching_eth1_backend requires a beacon_chain_builder")?;
let store = self
.store
.clone()
.ok_or_else(|| "caching_eth1_backend requires a store".to_string())?;
let backend = if let Some(eth1_service_from_genesis) = self.eth1_service {
eth1_service_from_genesis.update_config(config.clone())?;
CachingEth1Backend::from_service(eth1_service_from_genesis, store)
} else {
CachingEth1Backend::new(config, context.log, store)
};
self.eth1_service = None;
let exit = {
let (tx, rx) = exit_future::signal();
self.exit_signals.push(tx);
rx
};
// Starts the service that connects to an eth1 node and periodically updates caches.
context.executor.spawn(backend.start(exit));
self.beacon_chain_builder = Some(beacon_chain_builder.eth1_backend(Some(backend)));
Ok(self)
}
/// Do not use any eth1 backend. The client will not be able to produce beacon blocks.
pub fn no_eth1_backend(mut self) -> Result<Self, String> {
let beacon_chain_builder = self
.beacon_chain_builder
.ok_or_else(|| "caching_eth1_backend requires a beacon_chain_builder")?;
self.beacon_chain_builder = Some(beacon_chain_builder.no_eth1_backend());
Ok(self)
}
/// Use an eth1 backend that can produce blocks but is not connected to an Eth1 node.
///
/// This backend will never produce deposits so it's impossible to add validators after
/// genesis. The `Eth1Data` votes will be deterministic junk data.
///
/// ## Notes
///
/// The client is given the `CachingEth1Backend` type, but the http backend is never started and the
/// caches are never used.
pub fn dummy_eth1_backend(mut self) -> Result<Self, String> {
let beacon_chain_builder = self
.beacon_chain_builder
.ok_or_else(|| "caching_eth1_backend requires a beacon_chain_builder")?;
self.beacon_chain_builder = Some(beacon_chain_builder.dummy_eth1_backend()?);
Ok(self)
}
}
impl<TStore, TLmdGhost, TEth1Backend, TEthSpec, TEventHandler>
ClientBuilder<
Witness<TStore, SystemTimeSlotClock, TLmdGhost, TEth1Backend, TEthSpec, TEventHandler>,
>
where
TStore: Store + 'static,
TLmdGhost: LmdGhost<TStore, TEthSpec> + 'static,
TEth1Backend: Eth1ChainBackend<TEthSpec> + 'static,
TEthSpec: EthSpec + 'static,
TEventHandler: EventHandler<TEthSpec> + 'static,
{
/// Specifies that the slot clock should read the time from the computers system clock.
pub fn system_time_slot_clock(mut self) -> Result<Self, String> {
let beacon_chain_builder = self
.beacon_chain_builder
.as_ref()
.ok_or_else(|| "system_time_slot_clock requires a beacon_chain_builder")?;
let genesis_time = beacon_chain_builder
.finalized_checkpoint
.as_ref()
.ok_or_else(|| "system_time_slot_clock requires an initialized beacon state")?
.beacon_state
.genesis_time;
let spec = self
.chain_spec
.clone()
.ok_or_else(|| "system_time_slot_clock requires a chain spec".to_string())?;
let slot_clock = SystemTimeSlotClock::new(
spec.genesis_slot,
Duration::from_secs(genesis_time),
Duration::from_millis(spec.milliseconds_per_slot),
);
self.slot_clock = Some(slot_clock);
Ok(self)
}
}

View File

@@ -9,6 +9,32 @@ use std::sync::Mutex;
/// The number initial validators when starting the `Minimal`.
const TESTNET_SPEC_CONSTANTS: &str = "minimal";
/// Defines how the client should initialize the `BeaconChain` and other components.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ClientGenesis {
/// Reads the genesis state and other persisted data from the `Store`.
Resume,
/// Creates a genesis state as per the 2019 Canada interop specifications.
Interop {
validator_count: usize,
genesis_time: u64,
},
/// Connects to an eth1 node and waits until it can create the genesis state from the deposit
/// contract.
DepositContract,
/// Loads the genesis state from a SSZ-encoded `BeaconState` file.
SszFile { path: PathBuf },
/// Connects to another Lighthouse instance and reads the genesis state and other data via the
/// HTTP API.
RemoteNode { server: String, port: Option<u16> },
}
impl Default for ClientGenesis {
fn default() -> Self {
Self::DepositContract
}
}
/// The core configuration of a Lighthouse beacon node.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
@@ -17,74 +43,20 @@ pub struct Config {
db_name: String,
pub log_file: PathBuf,
pub spec_constants: String,
/// Defines how we should initialize a BeaconChain instances.
/// If true, the node will use co-ordinated junk for eth1 values.
///
/// This field is not serialized, there for it will not be written to (or loaded from) config
/// files. It can only be configured via the CLI.
/// This is the method used for the 2019 client interop in Canada.
pub dummy_eth1_backend: bool,
pub sync_eth1_chain: bool,
#[serde(skip)]
pub beacon_chain_start_method: BeaconChainStartMethod,
pub eth1_backend_method: Eth1BackendMethod,
/// The `genesis` field is not serialized or deserialized by `serde` to ensure it is defined
/// via the CLI at runtime, instead of from a configuration file saved to disk.
pub genesis: ClientGenesis,
pub network: network::NetworkConfig,
pub rpc: rpc::RPCConfig,
pub rest_api: rest_api::ApiConfig,
pub rpc: rpc::Config,
pub rest_api: rest_api::Config,
pub websocket_server: websocket_server::Config,
}
/// Defines how the client should initialize a BeaconChain.
///
/// In general, there are two methods:
/// - resuming a new chain, or
/// - initializing a new one.
#[derive(Debug, Clone)]
pub enum BeaconChainStartMethod {
/// Resume from an existing BeaconChain, loaded from the existing local database.
Resume,
/// Resume from an existing BeaconChain, loaded from the existing local database.
Mainnet,
/// Create a new beacon chain that can connect to mainnet.
///
/// Set the genesis time to be the start of the previous 30-minute window.
RecentGenesis {
validator_count: usize,
minutes: u64,
},
/// Create a new beacon chain with `genesis_time` and `validator_count` validators, all with well-known
/// secret keys.
Generated {
validator_count: usize,
genesis_time: u64,
},
/// Create a new beacon chain by loading a YAML-encoded genesis state from a file.
Yaml { file: PathBuf },
/// Create a new beacon chain by loading a SSZ-encoded genesis state from a file.
Ssz { file: PathBuf },
/// Create a new beacon chain by loading a JSON-encoded genesis state from a file.
Json { file: PathBuf },
/// Create a new beacon chain by using a HTTP server (running our REST-API) to load genesis and
/// finalized states and blocks.
HttpBootstrap { server: String, port: Option<u16> },
}
impl Default for BeaconChainStartMethod {
fn default() -> Self {
BeaconChainStartMethod::Resume
}
}
/// Defines which Eth1 backend the client should use.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum Eth1BackendMethod {
/// Use the mocked eth1 backend used in interop testing
Interop,
/// Use a web3 connection to a running Eth1 node.
Web3 { server: String },
}
impl Default for Eth1BackendMethod {
fn default() -> Self {
Eth1BackendMethod::Interop
}
pub eth1: eth1::Config,
}
impl Default for Config {
@@ -94,13 +66,15 @@ impl Default for Config {
log_file: PathBuf::from(""),
db_type: "disk".to_string(),
db_name: "chain_db".to_string(),
genesis: <_>::default(),
network: NetworkConfig::new(),
rpc: <_>::default(),
rest_api: <_>::default(),
websocket_server: <_>::default(),
spec_constants: TESTNET_SPEC_CONSTANTS.into(),
beacon_chain_start_method: <_>::default(),
eth1_backend_method: <_>::default(),
dummy_eth1_backend: false,
sync_eth1_chain: false,
eth1: <_>::default(),
}
}
}
@@ -183,3 +157,16 @@ impl Config {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use toml;
#[test]
fn serde() {
let config = Config::default();
let serialized = toml::to_string(&config).expect("should serde encode default config");
toml::from_str::<Config>(&serialized).expect("should serde decode default config");
}
}

View File

@@ -2,327 +2,58 @@ extern crate slog;
mod config;
pub mod builder;
pub mod error;
pub mod notifier;
use beacon_chain::{
lmd_ghost::ThreadSafeReducedTree, slot_clock::SystemTimeSlotClock, store::Store,
test_utils::generate_deterministic_keypairs, BeaconChain, BeaconChainBuilder,
};
use beacon_chain::BeaconChain;
use exit_future::Signal;
use futures::{future::Future, Stream};
use network::Service as NetworkService;
use rest_api::NetworkInfo;
use slog::{crit, debug, error, info, o};
use slot_clock::SlotClock;
use std::marker::PhantomData;
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use tokio::runtime::TaskExecutor;
use tokio::timer::Interval;
use types::EthSpec;
use websocket_server::WebSocketSender;
pub use beacon_chain::{BeaconChainTypes, Eth1ChainBackend, InteropEth1ChainBackend};
pub use config::{BeaconChainStartMethod, Config as ClientConfig, Eth1BackendMethod};
pub use beacon_chain::{BeaconChainTypes, Eth1ChainBackend};
pub use builder::ClientBuilder;
pub use config::{ClientGenesis, Config as ClientConfig};
pub use eth2_config::Eth2Config;
#[derive(Clone)]
pub struct RuntimeBeaconChainTypes<S: Store, E: EthSpec> {
_phantom_s: PhantomData<S>,
_phantom_e: PhantomData<E>,
/// The core "beacon node" client.
///
/// Holds references to running services, cleanly shutting them down when dropped.
pub struct Client<T: BeaconChainTypes> {
beacon_chain: Option<Arc<BeaconChain<T>>>,
libp2p_network: Option<Arc<NetworkService<T>>>,
http_listen_addr: Option<SocketAddr>,
websocket_listen_addr: Option<SocketAddr>,
/// Exit signals will "fire" when dropped, causing each service to exit gracefully.
_exit_signals: Vec<Signal>,
}
impl<S, E> BeaconChainTypes for RuntimeBeaconChainTypes<S, E>
where
S: Store + 'static,
E: EthSpec,
{
type Store = S;
type SlotClock = SystemTimeSlotClock;
type LmdGhost = ThreadSafeReducedTree<S, E>;
type Eth1Chain = InteropEth1ChainBackend<E>;
type EthSpec = E;
type EventHandler = WebSocketSender<E>;
}
/// Main beacon node client service. This provides the connection and initialisation of the clients
/// sub-services in multiple threads.
pub struct Client<S, E>
where
S: Store + Clone + 'static,
E: EthSpec,
{
/// Configuration for the lighthouse client.
_client_config: ClientConfig,
/// The beacon chain for the running client.
beacon_chain: Arc<BeaconChain<RuntimeBeaconChainTypes<S, E>>>,
/// Reference to the network service.
pub network: Arc<NetworkService<RuntimeBeaconChainTypes<S, E>>>,
/// Signal to terminate the RPC server.
pub rpc_exit_signal: Option<Signal>,
/// Signal to terminate the slot timer.
pub slot_timer_exit_signal: Option<Signal>,
/// Signal to terminate the API
pub api_exit_signal: Option<Signal>,
/// Signal to terminate the websocket server
pub websocket_exit_signal: Option<Signal>,
/// The clients logger.
log: slog::Logger,
}
impl<S, E> Client<S, E>
where
S: Store + Clone + 'static,
E: EthSpec,
{
/// Generate an instance of the client. Spawn and link all internal sub-processes.
pub fn new(
client_config: ClientConfig,
eth2_config: Eth2Config,
store: S,
log: slog::Logger,
executor: &TaskExecutor,
) -> error::Result<Self> {
let store = Arc::new(store);
let milliseconds_per_slot = eth2_config.spec.milliseconds_per_slot;
let spec = &eth2_config.spec.clone();
let beacon_chain_builder = match &client_config.beacon_chain_start_method {
BeaconChainStartMethod::Resume => {
info!(
log,
"Starting beacon chain";
"method" => "resume"
);
BeaconChainBuilder::from_store(spec.clone(), log.clone())
}
BeaconChainStartMethod::Mainnet => {
crit!(log, "No mainnet beacon chain startup specification.");
return Err("Mainnet launch is not yet announced.".into());
}
BeaconChainStartMethod::RecentGenesis {
validator_count,
minutes,
} => {
info!(
log,
"Starting beacon chain";
"validator_count" => validator_count,
"minutes" => minutes,
"method" => "recent"
);
BeaconChainBuilder::recent_genesis(
&generate_deterministic_keypairs(*validator_count),
*minutes,
spec.clone(),
log.clone(),
)?
}
BeaconChainStartMethod::Generated {
validator_count,
genesis_time,
} => {
info!(
log,
"Starting beacon chain";
"validator_count" => validator_count,
"genesis_time" => genesis_time,
"method" => "quick"
);
BeaconChainBuilder::quick_start(
*genesis_time,
&generate_deterministic_keypairs(*validator_count),
spec.clone(),
log.clone(),
)?
}
BeaconChainStartMethod::Yaml { file } => {
info!(
log,
"Starting beacon chain";
"file" => format!("{:?}", file),
"method" => "yaml"
);
BeaconChainBuilder::yaml_state(file, spec.clone(), log.clone())?
}
BeaconChainStartMethod::Ssz { file } => {
info!(
log,
"Starting beacon chain";
"file" => format!("{:?}", file),
"method" => "ssz"
);
BeaconChainBuilder::ssz_state(file, spec.clone(), log.clone())?
}
BeaconChainStartMethod::Json { file } => {
info!(
log,
"Starting beacon chain";
"file" => format!("{:?}", file),
"method" => "json"
);
BeaconChainBuilder::json_state(file, spec.clone(), log.clone())?
}
BeaconChainStartMethod::HttpBootstrap { server, port } => {
info!(
log,
"Starting beacon chain";
"port" => port,
"server" => server,
"method" => "bootstrap"
);
BeaconChainBuilder::http_bootstrap(server, spec.clone(), log.clone())?
}
};
let eth1_backend =
InteropEth1ChainBackend::new(String::new()).map_err(|e| format!("{:?}", e))?;
// Start the websocket server.
let (websocket_sender, websocket_exit_signal): (WebSocketSender<E>, Option<_>) =
if client_config.websocket_server.enabled {
let (sender, exit) = websocket_server::start_server(
&client_config.websocket_server,
executor,
&log,
)?;
(sender, Some(exit))
} else {
(WebSocketSender::dummy(), None)
};
let beacon_chain: Arc<BeaconChain<RuntimeBeaconChainTypes<S, E>>> = Arc::new(
beacon_chain_builder
.build(store, eth1_backend, websocket_sender)
.map_err(error::Error::from)?,
);
let since_epoch = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|e| format!("Unable to read system time: {}", e))?;
let since_genesis = Duration::from_secs(beacon_chain.head().beacon_state.genesis_time);
if since_genesis > since_epoch {
info!(
log,
"Starting node prior to genesis";
"now" => since_epoch.as_secs(),
"genesis_seconds" => since_genesis.as_secs(),
);
impl<T: BeaconChainTypes> Client<T> {
/// Returns an `Arc` reference to the client's `BeaconChain`, if it was started.
pub fn beacon_chain(&self) -> Option<Arc<BeaconChain<T>>> {
self.beacon_chain.clone()
}
let network_config = &client_config.network;
let (network, network_send) =
NetworkService::new(beacon_chain.clone(), network_config, executor, log.clone())?;
// spawn the RPC server
let rpc_exit_signal = if client_config.rpc.enabled {
Some(rpc::start_server(
&client_config.rpc,
executor,
network_send.clone(),
beacon_chain.clone(),
&log,
))
} else {
None
};
// Start the `rest_api` service
let api_exit_signal = if client_config.rest_api.enabled {
let network_info = NetworkInfo {
network_service: network.clone(),
network_chan: network_send.clone(),
};
match rest_api::start_server(
&client_config.rest_api,
executor,
beacon_chain.clone(),
network_info,
client_config.db_path().expect("unable to read datadir"),
eth2_config.clone(),
&log,
) {
Ok(s) => Some(s),
Err(e) => {
error!(log, "API service failed to start."; "error" => format!("{:?}",e));
None
}
}
} else {
None
};
let (slot_timer_exit_signal, exit) = exit_future::signal();
if let Some(duration_to_next_slot) = beacon_chain.slot_clock.duration_to_next_slot() {
// set up the validator work interval - start at next slot and proceed every slot
let interval = {
// Set the interval to start at the next slot, and every slot after
let slot_duration = Duration::from_millis(milliseconds_per_slot);
//TODO: Handle checked add correctly
Interval::new(Instant::now() + duration_to_next_slot, slot_duration)
};
let chain = beacon_chain.clone();
let log = log.new(o!("Service" => "SlotTimer"));
executor.spawn(
exit.until(
interval
.for_each(move |_| {
log_new_slot(&chain, &log);
Ok(())
})
.map_err(|_| ()),
)
.map(|_| ()),
);
/// Returns the address of the client's HTTP API server, if it was started.
pub fn http_listen_addr(&self) -> Option<SocketAddr> {
self.http_listen_addr
}
Ok(Client {
_client_config: client_config,
beacon_chain,
rpc_exit_signal,
slot_timer_exit_signal: Some(slot_timer_exit_signal),
api_exit_signal,
websocket_exit_signal,
log,
network,
})
/// Returns the address of the client's WebSocket API server, if it was started.
pub fn websocket_listen_addr(&self) -> Option<SocketAddr> {
self.websocket_listen_addr
}
/// Returns the port of the client's libp2p stack, if it was started.
pub fn libp2p_listen_port(&self) -> Option<u16> {
self.libp2p_network.as_ref().map(|n| n.listen_port())
}
}
impl<S: Store + Clone, E: EthSpec> Drop for Client<S, E> {
impl<T: BeaconChainTypes> Drop for Client<T> {
fn drop(&mut self) {
// Save the beacon chain to it's store before dropping.
let _result = self.beacon_chain.persist();
if let Some(beacon_chain) = &self.beacon_chain {
let _result = beacon_chain.persist();
}
}
}
fn log_new_slot<T: BeaconChainTypes>(chain: &Arc<BeaconChain<T>>, log: &slog::Logger) {
let best_slot = chain.head().beacon_block.slot;
let latest_block_root = chain.head().beacon_block_root;
if let Ok(current_slot) = chain.slot() {
info!(
log,
"Slot start";
"best_slot" => best_slot,
"slot" => current_slot,
);
debug!(
log,
"Slot info";
"skip_slots" => current_slot.saturating_sub(best_slot),
"best_block_root" => format!("{}", latest_block_root),
"slot" => current_slot,
);
} else {
error!(
log,
"Beacon chain running whilst slot clock is unavailable."
);
};
}

View File

@@ -1,58 +0,0 @@
use crate::Client;
use exit_future::Exit;
use futures::{Future, Stream};
use slog::{debug, o, warn};
use std::time::{Duration, Instant};
use store::Store;
use tokio::runtime::TaskExecutor;
use tokio::timer::Interval;
use types::EthSpec;
/// The interval between heartbeat events.
pub const HEARTBEAT_INTERVAL_SECONDS: u64 = 15;
/// Create a warning log whenever the peer count is at or below this value.
pub const WARN_PEER_COUNT: usize = 1;
/// Spawns a thread that can be used to run code periodically, on `HEARTBEAT_INTERVAL_SECONDS`
/// durations.
///
/// Presently unused, but remains for future use.
pub fn run<S, E>(client: &Client<S, E>, executor: TaskExecutor, exit: Exit)
where
S: Store + Clone + 'static,
E: EthSpec,
{
// notification heartbeat
let interval = Interval::new(
Instant::now(),
Duration::from_secs(HEARTBEAT_INTERVAL_SECONDS),
);
let log = client.log.new(o!("Service" => "Notifier"));
let libp2p = client.network.libp2p_service();
let heartbeat = move |_| {
// Number of libp2p (not discv5) peers connected.
//
// Panics if libp2p is poisoned.
let connected_peer_count = libp2p.lock().swarm.connected_peers();
debug!(log, "Connected peer status"; "peer_count" => connected_peer_count);
if connected_peer_count <= WARN_PEER_COUNT {
warn!(log, "Low peer count"; "peer_count" => connected_peer_count);
}
Ok(())
};
// map error and spawn
let err_log = client.log.clone();
let heartbeat_interval = interval
.map_err(move |e| debug!(err_log, "Timer error {}", e))
.for_each(heartbeat);
executor.spawn(exit.until(heartbeat_interval).map(|_| ()));
}

View File

@@ -0,0 +1,29 @@
[package]
name = "eth1"
version = "0.1.0"
authors = ["Paul Hauner <paul@paulhauner.com>"]
edition = "2018"
[dev-dependencies]
eth1_test_rig = { path = "../../tests/eth1_test_rig" }
environment = { path = "../../lighthouse/environment" }
toml = "^0.5"
web3 = "0.8.0"
[dependencies]
reqwest = "0.9"
futures = "0.1.25"
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
hex = "0.4"
types = { path = "../../eth2/types"}
merkle_proof = { path = "../../eth2/utils/merkle_proof"}
eth2_ssz = { path = "../../eth2/utils/ssz"}
tree_hash = { path = "../../eth2/utils/tree_hash"}
eth2_hashing = { path = "../../eth2/utils/eth2_hashing"}
parking_lot = "0.7"
slog = "^2.2.3"
tokio = "0.1.17"
state_processing = { path = "../../eth2/state_processing" }
exit-future = "0.1.4"
libflate = "0.1"

View File

@@ -0,0 +1,271 @@
use std::ops::RangeInclusive;
use types::{Eth1Data, Hash256};
#[derive(Debug, PartialEq, Clone)]
pub enum Error {
/// The timestamp of each block equal to or later than the block prior to it.
InconsistentTimestamp { parent: u64, child: u64 },
/// Some `Eth1Block` was provided with the same block number but different data. The source
/// of eth1 data is inconsistent.
Conflicting(u64),
/// The given block was not one block number higher than the higest known block number.
NonConsecutive { given: u64, expected: u64 },
/// Some invariant was violated, there is a likely bug in the code.
Internal(String),
}
/// A block of the eth1 chain.
///
/// Contains all information required to add a `BlockCache` entry.
#[derive(Debug, PartialEq, Clone, Eq, Hash)]
pub struct Eth1Block {
pub hash: Hash256,
pub timestamp: u64,
pub number: u64,
pub deposit_root: Option<Hash256>,
pub deposit_count: Option<u64>,
}
impl Eth1Block {
pub fn eth1_data(self) -> Option<Eth1Data> {
Some(Eth1Data {
deposit_root: self.deposit_root?,
deposit_count: self.deposit_count?,
block_hash: self.hash,
})
}
}
/// Stores block and deposit contract information and provides queries based upon the block
/// timestamp.
#[derive(Debug, PartialEq, Clone, Default)]
pub struct BlockCache {
blocks: Vec<Eth1Block>,
}
impl BlockCache {
/// Returns the number of blocks stored in `self`.
pub fn len(&self) -> usize {
self.blocks.len()
}
/// True if the cache does not store any blocks.
pub fn is_empty(&self) -> bool {
self.blocks.is_empty()
}
/// Returns the highest block number stored.
pub fn highest_block_number(&self) -> Option<u64> {
self.blocks.last().map(|block| block.number)
}
/// Returns an iterator over all blocks.
///
/// Blocks a guaranteed to be returned with;
///
/// - Monotonically increasing block numbers.
/// - Non-uniformly increasing block timestamps.
pub fn iter(&self) -> impl DoubleEndedIterator<Item = &Eth1Block> + Clone {
self.blocks.iter()
}
/// Shortens the cache, keeping the latest (by block number) `len` blocks while dropping the
/// rest.
///
/// If `len` is greater than the vector's current length, this has no effect.
pub fn truncate(&mut self, len: usize) {
if len < self.blocks.len() {
self.blocks = self.blocks.split_off(self.blocks.len() - len);
}
}
/// Returns the range of block numbers stored in the block cache. All blocks in this range can
/// be accessed.
fn available_block_numbers(&self) -> Option<RangeInclusive<u64>> {
Some(self.blocks.first()?.number..=self.blocks.last()?.number)
}
/// Returns a block with the corresponding number, if any.
pub fn block_by_number(&self, block_number: u64) -> Option<&Eth1Block> {
self.blocks.get(
self.blocks
.as_slice()
.binary_search_by(|block| block.number.cmp(&block_number))
.ok()?,
)
}
/// Insert an `Eth1Snapshot` into `self`, allowing future queries.
///
/// Allows inserting either:
///
/// - The root block (i.e., any block if there are no existing blocks), or,
/// - An immediate child of the most recent (highest block number) block.
///
/// ## Errors
///
/// - If the cache is not empty and `item.block.block_number - 1` is not already in `self`.
/// - If `item.block.block_number` is in `self`, but is not identical to the supplied
/// `Eth1Snapshot`.
/// - If `item.block.timestamp` is prior to the parent.
pub fn insert_root_or_child(&mut self, block: Eth1Block) -> Result<(), Error> {
let expected_block_number = self
.highest_block_number()
.map(|n| n + 1)
.unwrap_or_else(|| block.number);
// If there are already some cached blocks, check to see if the new block number is one of
// them.
//
// If the block is already known, check to see the given block is identical to it. If not,
// raise an inconsistency error. This is mostly likely caused by some fork on the eth1
// chain.
if let Some(local) = self.available_block_numbers() {
if local.contains(&block.number) {
let known_block = self.block_by_number(block.number).ok_or_else(|| {
Error::Internal("An expected block was not present".to_string())
})?;
if known_block == &block {
return Ok(());
} else {
return Err(Error::Conflicting(block.number));
};
}
}
// Only permit blocks when it's either:
//
// - The first block inserted.
// - Exactly one block number higher than the highest known block number.
if block.number != expected_block_number {
return Err(Error::NonConsecutive {
given: block.number,
expected: expected_block_number,
});
}
// If the block is not the first block inserted, ensure that its timestamp is not higher
// than its parents.
if let Some(previous_block) = self.blocks.last() {
if previous_block.timestamp > block.timestamp {
return Err(Error::InconsistentTimestamp {
parent: previous_block.timestamp,
child: block.timestamp,
});
}
}
self.blocks.push(block);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn get_block(i: u64, interval_secs: u64) -> Eth1Block {
Eth1Block {
hash: Hash256::from_low_u64_be(i),
timestamp: i * interval_secs,
number: i,
deposit_root: Some(Hash256::from_low_u64_be(i << 32)),
deposit_count: Some(i),
}
}
fn get_blocks(n: usize, interval_secs: u64) -> Vec<Eth1Block> {
(0..n as u64)
.into_iter()
.map(|i| get_block(i, interval_secs))
.collect()
}
fn insert(cache: &mut BlockCache, s: Eth1Block) -> Result<(), Error> {
cache.insert_root_or_child(s)
}
#[test]
fn truncate() {
let n = 16;
let blocks = get_blocks(n, 10);
let mut cache = BlockCache::default();
for block in blocks {
insert(&mut cache, block.clone()).expect("should add consecutive blocks");
}
for len in vec![0, 1, 2, 3, 4, 8, 15, 16] {
let mut cache = cache.clone();
cache.truncate(len);
assert_eq!(
cache.blocks.len(),
len,
"should truncate to length: {}",
len
);
}
let mut cache_2 = cache.clone();
cache_2.truncate(17);
assert_eq!(
cache_2.blocks.len(),
n,
"truncate to larger than n should be a no-op"
);
}
#[test]
fn inserts() {
let n = 16;
let blocks = get_blocks(n, 10);
let mut cache = BlockCache::default();
for block in blocks {
insert(&mut cache, block.clone()).expect("should add consecutive blocks");
}
// No error for re-adding a block identical to one that exists.
assert!(insert(&mut cache, get_block(n as u64 - 1, 10)).is_ok());
// Error for re-adding a block that is different to the one that exists.
assert!(insert(&mut cache, get_block(n as u64 - 1, 11)).is_err());
// Error for adding non-consecutive blocks.
assert!(insert(&mut cache, get_block(n as u64 + 1, 10)).is_err());
assert!(insert(&mut cache, get_block(n as u64 + 2, 10)).is_err());
// Error for adding timestamp prior to previous.
assert!(insert(&mut cache, get_block(n as u64, 1)).is_err());
// Double check to make sure previous test was only affected by timestamp.
assert!(insert(&mut cache, get_block(n as u64, 10)).is_ok());
}
#[test]
fn duplicate_timestamp() {
let mut blocks = get_blocks(7, 10);
blocks[0].timestamp = 0;
blocks[1].timestamp = 10;
blocks[2].timestamp = 10;
blocks[3].timestamp = 20;
blocks[4].timestamp = 30;
blocks[5].timestamp = 40;
blocks[6].timestamp = 40;
let mut cache = BlockCache::default();
for block in &blocks {
insert(&mut cache, block.clone())
.expect("should add consecutive blocks with duplicate timestamps");
}
assert_eq!(cache.blocks, blocks, "should have added all blocks");
}
}

View File

@@ -0,0 +1,371 @@
use crate::DepositLog;
use eth2_hashing::hash;
use std::ops::Range;
use tree_hash::TreeHash;
use types::{Deposit, Hash256};
#[derive(Debug, PartialEq, Clone)]
pub enum Error {
/// A deposit log was added when a prior deposit was not already in the cache.
///
/// Logs have to be added with monotonically-increasing block numbers.
NonConsecutive { log_index: u64, expected: usize },
/// The eth1 event log data was unable to be parsed.
LogParseError(String),
/// There are insufficient deposits in the cache to fulfil the request.
InsufficientDeposits {
known_deposits: usize,
requested: u64,
},
/// A log with the given index is already present in the cache and it does not match the one
/// provided.
DuplicateDistinctLog(u64),
/// The deposit count must always be large enough to account for the requested deposit range.
///
/// E.g., you cannot request deposit 10 when the deposit count is 9.
DepositCountInvalid { deposit_count: u64, range_end: u64 },
/// An unexpected condition was encountered.
InternalError(String),
}
/// Emulates the eth1 deposit contract merkle tree.
pub struct DepositDataTree {
tree: merkle_proof::MerkleTree,
mix_in_length: usize,
depth: usize,
}
impl DepositDataTree {
/// Create a new Merkle tree from a list of leaves (`DepositData::tree_hash_root`) and a fixed depth.
pub fn create(leaves: &[Hash256], mix_in_length: usize, depth: usize) -> Self {
Self {
tree: merkle_proof::MerkleTree::create(leaves, depth),
mix_in_length,
depth,
}
}
/// Returns 32 bytes representing the "mix in length" for the merkle root of this tree.
fn length_bytes(&self) -> Vec<u8> {
int_to_bytes32(self.mix_in_length)
}
/// Retrieve the root hash of this Merkle tree with the length mixed in.
pub fn root(&self) -> Hash256 {
let mut preimage = [0; 64];
preimage[0..32].copy_from_slice(&self.tree.hash()[..]);
preimage[32..64].copy_from_slice(&self.length_bytes());
Hash256::from_slice(&hash(&preimage))
}
/// Return the leaf at `index` and a Merkle proof of its inclusion.
///
/// The Merkle proof is in "bottom-up" order, starting with a leaf node
/// and moving up the tree. Its length will be exactly equal to `depth + 1`.
pub fn generate_proof(&self, index: usize) -> (Hash256, Vec<Hash256>) {
let (root, mut proof) = self.tree.generate_proof(index, self.depth);
proof.push(Hash256::from_slice(&self.length_bytes()));
(root, proof)
}
}
/// Mirrors the merkle tree of deposits in the eth1 deposit contract.
///
/// Provides `Deposit` objects with merkle proofs included.
#[derive(Default)]
pub struct DepositCache {
logs: Vec<DepositLog>,
roots: Vec<Hash256>,
}
impl DepositCache {
/// Returns the number of deposits available in the cache.
pub fn len(&self) -> usize {
self.logs.len()
}
/// True if the cache does not store any blocks.
pub fn is_empty(&self) -> bool {
self.logs.is_empty()
}
/// Returns the block number for the most recent deposit in the cache.
pub fn latest_block_number(&self) -> Option<u64> {
self.logs.last().map(|log| log.block_number)
}
/// Returns an iterator over all the logs in `self`.
pub fn iter(&self) -> impl Iterator<Item = &DepositLog> {
self.logs.iter()
}
/// Returns the i'th deposit log.
pub fn get(&self, i: usize) -> Option<&DepositLog> {
self.logs.get(i)
}
/// Adds `log` to self.
///
/// This function enforces that `logs` are imported one-by-one with no gaps between
/// `log.index`, starting at `log.index == 0`.
///
/// ## Errors
///
/// - If a log with index `log.index - 1` is not already present in `self` (ignored when empty).
/// - If a log with `log.index` is already known, but the given `log` is distinct to it.
pub fn insert_log(&mut self, log: DepositLog) -> Result<(), Error> {
if log.index == self.logs.len() as u64 {
self.roots
.push(Hash256::from_slice(&log.deposit_data.tree_hash_root()));
self.logs.push(log);
Ok(())
} else if log.index < self.logs.len() as u64 {
if self.logs[log.index as usize] == log {
Ok(())
} else {
Err(Error::DuplicateDistinctLog(log.index))
}
} else {
Err(Error::NonConsecutive {
log_index: log.index,
expected: self.logs.len(),
})
}
}
/// Returns a list of `Deposit` objects, within the given deposit index `range`.
///
/// The `deposit_count` is used to generate the proofs for the `Deposits`. For example, if we
/// have 100 proofs, but the eth2 chain only acknowledges 50 of them, we must produce our
/// proofs with respect to a tree size of 50.
///
///
/// ## Errors
///
/// - If `deposit_count` is larger than `range.end`.
/// - There are not sufficient deposits in the tree to generate the proof.
pub fn get_deposits(
&self,
range: Range<u64>,
deposit_count: u64,
tree_depth: usize,
) -> Result<(Hash256, Vec<Deposit>), Error> {
if deposit_count < range.end {
// It's invalid to ask for more deposits than should exist.
Err(Error::DepositCountInvalid {
deposit_count,
range_end: range.end,
})
} else if range.end > self.logs.len() as u64 {
// The range of requested deposits exceeds the deposits stored locally.
Err(Error::InsufficientDeposits {
requested: range.end,
known_deposits: self.logs.len(),
})
} else if deposit_count > self.roots.len() as u64 {
// There are not `deposit_count` known deposit roots, so we can't build the merkle tree
// to prove into.
Err(Error::InsufficientDeposits {
requested: deposit_count,
known_deposits: self.logs.len(),
})
} else {
let roots = self
.roots
.get(0..deposit_count as usize)
.ok_or_else(|| Error::InternalError("Unable to get known root".into()))?;
// Note: there is likely a more optimal solution than recreating the `DepositDataTree`
// each time this function is called.
//
// Perhaps a base merkle tree could be maintained that contains all deposits up to the
// last finalized eth1 deposit count. Then, that tree could be cloned and extended for
// each of these calls.
let tree = DepositDataTree::create(roots, deposit_count as usize, tree_depth);
let deposits = self
.logs
.get(range.start as usize..range.end as usize)
.ok_or_else(|| Error::InternalError("Unable to get known log".into()))?
.iter()
.map(|deposit_log| {
let (_leaf, proof) = tree.generate_proof(deposit_log.index as usize);
Deposit {
proof: proof.into(),
data: deposit_log.deposit_data.clone(),
}
})
.collect();
Ok((tree.root(), deposits))
}
}
}
/// Returns `int` as little-endian bytes with a length of 32.
fn int_to_bytes32(int: usize) -> Vec<u8> {
let mut vec = int.to_le_bytes().to_vec();
vec.resize(32, 0);
vec
}
#[cfg(test)]
pub mod tests {
use super::*;
use crate::deposit_log::tests::EXAMPLE_LOG;
use crate::http::Log;
pub const TREE_DEPTH: usize = 32;
fn example_log() -> DepositLog {
let log = Log {
block_number: 42,
data: EXAMPLE_LOG.to_vec(),
};
DepositLog::from_log(&log).expect("should decode log")
}
#[test]
fn insert_log_valid() {
let mut tree = DepositCache::default();
for i in 0..16 {
let mut log = example_log();
log.index = i;
tree.insert_log(log).expect("should add consecutive logs")
}
}
#[test]
fn insert_log_invalid() {
let mut tree = DepositCache::default();
for i in 0..4 {
let mut log = example_log();
log.index = i;
tree.insert_log(log).expect("should add consecutive logs")
}
// Add duplicate, when given is the same as the one known.
let mut log = example_log();
log.index = 3;
assert!(tree.insert_log(log).is_ok());
// Add duplicate, when given is different to the one known.
let mut log = example_log();
log.index = 3;
log.block_number = 99;
assert!(tree.insert_log(log).is_err());
// Skip inserting a log.
let mut log = example_log();
log.index = 5;
assert!(tree.insert_log(log).is_err());
}
#[test]
fn get_deposit_valid() {
let n = 1_024;
let mut tree = DepositCache::default();
for i in 0..n {
let mut log = example_log();
log.index = i;
log.block_number = i;
log.deposit_data.withdrawal_credentials = Hash256::from_low_u64_be(i);
tree.insert_log(log).expect("should add consecutive logs")
}
// Get 0 deposits, with max deposit count.
let (_, deposits) = tree
.get_deposits(0..0, n, TREE_DEPTH)
.expect("should get the full tree");
assert_eq!(deposits.len(), 0, "should return no deposits");
// Get 0 deposits, with 0 deposit count.
let (_, deposits) = tree
.get_deposits(0..0, 0, TREE_DEPTH)
.expect("should get the full tree");
assert_eq!(deposits.len(), 0, "should return no deposits");
// Get 0 deposits, with 0 deposit count, tree depth 0.
let (_, deposits) = tree
.get_deposits(0..0, 0, 0)
.expect("should get the full tree");
assert_eq!(deposits.len(), 0, "should return no deposits");
// Get all deposits, with max deposit count.
let (full_root, deposits) = tree
.get_deposits(0..n, n, TREE_DEPTH)
.expect("should get the full tree");
assert_eq!(deposits.len(), n as usize, "should return all deposits");
// Get 4 deposits, with max deposit count.
let (root, deposits) = tree
.get_deposits(0..4, n, TREE_DEPTH)
.expect("should get the four from the full tree");
assert_eq!(
deposits.len(),
4 as usize,
"should get 4 deposits from full tree"
);
assert_eq!(
root, full_root,
"should still return full root when getting deposit subset"
);
// Get half of the deposits, with half deposit count.
let (half_root, deposits) = tree
.get_deposits(0..n / 2, n / 2, TREE_DEPTH)
.expect("should get the half tree");
assert_eq!(
deposits.len(),
n as usize / 2,
"should return half deposits"
);
// Get 4 deposits, with half deposit count.
let (root, deposits) = tree
.get_deposits(0..4, n / 2, TREE_DEPTH)
.expect("should get the half tree");
assert_eq!(
deposits.len(),
4 as usize,
"should get 4 deposits from half tree"
);
assert_eq!(
root, half_root,
"should still return half root when getting deposit subset"
);
assert_ne!(
full_root, half_root,
"should get different root when pinning deposit count"
);
}
#[test]
fn get_deposit_invalid() {
let n = 16;
let mut tree = DepositCache::default();
for i in 0..n {
let mut log = example_log();
log.index = i;
log.block_number = i;
log.deposit_data.withdrawal_credentials = Hash256::from_low_u64_be(i);
tree.insert_log(log).expect("should add consecutive logs")
}
// Range too high.
assert!(tree.get_deposits(0..n + 1, n, TREE_DEPTH).is_err());
// Count too high.
assert!(tree.get_deposits(0..n, n + 1, TREE_DEPTH).is_err());
// Range higher than count.
assert!(tree.get_deposits(0..4, 2, TREE_DEPTH).is_err());
}
}

View File

@@ -0,0 +1,107 @@
use super::http::Log;
use ssz::Decode;
use types::{DepositData, Hash256, PublicKeyBytes, SignatureBytes};
/// The following constants define the layout of bytes in the deposit contract `DepositEvent`. The
/// event bytes are formatted according to the Ethereum ABI.
const PUBKEY_START: usize = 192;
const PUBKEY_LEN: usize = 48;
const CREDS_START: usize = PUBKEY_START + 64 + 32;
const CREDS_LEN: usize = 32;
const AMOUNT_START: usize = CREDS_START + 32 + 32;
const AMOUNT_LEN: usize = 8;
const SIG_START: usize = AMOUNT_START + 32 + 32;
const SIG_LEN: usize = 96;
const INDEX_START: usize = SIG_START + 96 + 32;
const INDEX_LEN: usize = 8;
/// A fully parsed eth1 deposit contract log.
#[derive(Debug, PartialEq, Clone)]
pub struct DepositLog {
pub deposit_data: DepositData,
/// The block number of the log that included this `DepositData`.
pub block_number: u64,
/// The index included with the deposit log.
pub index: u64,
}
impl DepositLog {
/// Attempts to parse a raw `Log` from the deposit contract into a `DepositLog`.
pub fn from_log(log: &Log) -> Result<Self, String> {
let bytes = &log.data;
let pubkey = bytes
.get(PUBKEY_START..PUBKEY_START + PUBKEY_LEN)
.ok_or_else(|| "Insufficient bytes for pubkey".to_string())?;
let withdrawal_credentials = bytes
.get(CREDS_START..CREDS_START + CREDS_LEN)
.ok_or_else(|| "Insufficient bytes for withdrawal credential".to_string())?;
let amount = bytes
.get(AMOUNT_START..AMOUNT_START + AMOUNT_LEN)
.ok_or_else(|| "Insufficient bytes for amount".to_string())?;
let signature = bytes
.get(SIG_START..SIG_START + SIG_LEN)
.ok_or_else(|| "Insufficient bytes for signature".to_string())?;
let index = bytes
.get(INDEX_START..INDEX_START + INDEX_LEN)
.ok_or_else(|| "Insufficient bytes for index".to_string())?;
let deposit_data = DepositData {
pubkey: PublicKeyBytes::from_ssz_bytes(pubkey)
.map_err(|e| format!("Invalid pubkey ssz: {:?}", e))?,
withdrawal_credentials: Hash256::from_ssz_bytes(withdrawal_credentials)
.map_err(|e| format!("Invalid withdrawal_credentials ssz: {:?}", e))?,
amount: u64::from_ssz_bytes(amount)
.map_err(|e| format!("Invalid amount ssz: {:?}", e))?,
signature: SignatureBytes::from_ssz_bytes(signature)
.map_err(|e| format!("Invalid signature ssz: {:?}", e))?,
};
Ok(DepositLog {
deposit_data,
block_number: log.block_number,
index: u64::from_ssz_bytes(index).map_err(|e| format!("Invalid index ssz: {:?}", e))?,
})
}
}
#[cfg(test)]
pub mod tests {
use super::*;
use crate::http::Log;
/// The data from a deposit event, using the v0.8.3 version of the deposit contract.
pub const EXAMPLE_LOG: &[u8] = &[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 1, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 1, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 48, 167, 108, 6, 69, 88, 17, 3, 51, 6, 4, 158, 232, 82,
248, 218, 2, 71, 219, 55, 102, 86, 125, 136, 203, 36, 77, 64, 213, 43, 52, 175, 154, 239,
50, 142, 52, 201, 77, 54, 239, 0, 229, 22, 46, 139, 120, 62, 240, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 64, 89, 115, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 96, 140, 74, 175, 158, 209, 20, 206,
30, 63, 215, 238, 113, 60, 132, 216, 211, 100, 186, 202, 71, 34, 200, 160, 225, 212, 213,
119, 88, 51, 80, 101, 74, 2, 45, 78, 153, 12, 192, 44, 51, 77, 40, 10, 72, 246, 34, 193,
187, 22, 95, 4, 211, 245, 224, 13, 162, 21, 163, 54, 225, 22, 124, 3, 56, 14, 81, 122, 189,
149, 250, 251, 159, 22, 77, 94, 157, 197, 196, 253, 110, 201, 88, 193, 246, 136, 226, 221,
18, 113, 232, 105, 100, 114, 103, 237, 189, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
];
#[test]
fn can_parse_example_log() {
let log = Log {
block_number: 42,
data: EXAMPLE_LOG.to_vec(),
};
DepositLog::from_log(&log).expect("should decode log");
}
}

View File

@@ -0,0 +1,405 @@
//! Provides a very minimal set of functions for interfacing with the eth2 deposit contract via an
//! eth1 HTTP JSON-RPC endpoint.
//!
//! All remote functions return a future (i.e., are async).
//!
//! Does not use a web3 library, instead it uses `reqwest` (`hyper`) to call the remote endpoint
//! and `serde` to decode the response.
//!
//! ## Note
//!
//! There is no ABI parsing here, all function signatures and topics are hard-coded as constants.
use futures::{Future, Stream};
use libflate::gzip::Decoder;
use reqwest::{header::CONTENT_TYPE, r#async::ClientBuilder, StatusCode};
use serde_json::{json, Value};
use std::io::prelude::*;
use std::ops::Range;
use std::time::Duration;
use types::Hash256;
/// `keccak("DepositEvent(bytes,bytes,bytes,bytes,bytes)")`
pub const DEPOSIT_EVENT_TOPIC: &str =
"0x649bbc62d0e31342afea4e5cd82d4049e7e1ee912fc0889aa790803be39038c5";
/// `keccak("get_deposit_root()")[0..4]`
pub const DEPOSIT_ROOT_FN_SIGNATURE: &str = "0x863a311b";
/// `keccak("get_deposit_count()")[0..4]`
pub const DEPOSIT_COUNT_FN_SIGNATURE: &str = "0x621fd130";
/// Number of bytes in deposit contract deposit root response.
pub const DEPOSIT_COUNT_RESPONSE_BYTES: usize = 96;
/// Number of bytes in deposit contract deposit root (value only).
pub const DEPOSIT_ROOT_BYTES: usize = 32;
#[derive(Debug, PartialEq, Clone)]
pub struct Block {
pub hash: Hash256,
pub timestamp: u64,
pub number: u64,
}
/// Returns the current block number.
///
/// Uses HTTP JSON RPC at `endpoint`. E.g., `http://localhost:8545`.
pub fn get_block_number(
endpoint: &str,
timeout: Duration,
) -> impl Future<Item = u64, Error = String> {
send_rpc_request(endpoint, "eth_blockNumber", json!([]), timeout)
.and_then(|response_body| {
hex_to_u64_be(
response_result(&response_body)?
.ok_or_else(|| "No result field was returned for block number".to_string())?
.as_str()
.ok_or_else(|| "Data was not string")?,
)
})
.map_err(|e| format!("Failed to get block number: {}", e))
}
/// Gets a block hash by block number.
///
/// Uses HTTP JSON RPC at `endpoint`. E.g., `http://localhost:8545`.
pub fn get_block(
endpoint: &str,
block_number: u64,
timeout: Duration,
) -> impl Future<Item = Block, Error = String> {
let params = json!([
format!("0x{:x}", block_number),
false // do not return full tx objects.
]);
send_rpc_request(endpoint, "eth_getBlockByNumber", params, timeout)
.and_then(|response_body| {
let hash = hex_to_bytes(
response_result(&response_body)?
.ok_or_else(|| "No result field was returned for block".to_string())?
.get("hash")
.ok_or_else(|| "No hash for block")?
.as_str()
.ok_or_else(|| "Block hash was not string")?,
)?;
let hash = if hash.len() == 32 {
Ok(Hash256::from_slice(&hash))
} else {
Err(format!("Block has was not 32 bytes: {:?}", hash))
}?;
let timestamp = hex_to_u64_be(
response_result(&response_body)?
.ok_or_else(|| "No result field was returned for timestamp".to_string())?
.get("timestamp")
.ok_or_else(|| "No timestamp for block")?
.as_str()
.ok_or_else(|| "Block timestamp was not string")?,
)?;
let number = hex_to_u64_be(
response_result(&response_body)?
.ok_or_else(|| "No result field was returned for number".to_string())?
.get("number")
.ok_or_else(|| "No number for block")?
.as_str()
.ok_or_else(|| "Block number was not string")?,
)?;
if number <= usize::max_value() as u64 {
Ok(Block {
hash,
timestamp,
number,
})
} else {
Err(format!("Block number {} is larger than a usize", number))
}
})
.map_err(|e| format!("Failed to get block number: {}", e))
}
/// Returns the value of the `get_deposit_count()` call at the given `address` for the given
/// `block_number`.
///
/// Assumes that the `address` has the same ABI as the eth2 deposit contract.
///
/// Uses HTTP JSON RPC at `endpoint`. E.g., `http://localhost:8545`.
pub fn get_deposit_count(
endpoint: &str,
address: &str,
block_number: u64,
timeout: Duration,
) -> impl Future<Item = Option<u64>, Error = String> {
call(
endpoint,
address,
DEPOSIT_COUNT_FN_SIGNATURE,
block_number,
timeout,
)
.and_then(|result| result.ok_or_else(|| "No response to deposit count".to_string()))
.and_then(|bytes| {
if bytes.is_empty() {
Ok(None)
} else if bytes.len() == DEPOSIT_COUNT_RESPONSE_BYTES {
let mut array = [0; 8];
array.copy_from_slice(&bytes[32 + 32..32 + 32 + 8]);
Ok(Some(u64::from_le_bytes(array)))
} else {
Err(format!(
"Deposit count response was not {} bytes: {:?}",
DEPOSIT_COUNT_RESPONSE_BYTES, bytes
))
}
})
}
/// Returns the value of the `get_hash_tree_root()` call at the given `block_number`.
///
/// Assumes that the `address` has the same ABI as the eth2 deposit contract.
///
/// Uses HTTP JSON RPC at `endpoint`. E.g., `http://localhost:8545`.
pub fn get_deposit_root(
endpoint: &str,
address: &str,
block_number: u64,
timeout: Duration,
) -> impl Future<Item = Option<Hash256>, Error = String> {
call(
endpoint,
address,
DEPOSIT_ROOT_FN_SIGNATURE,
block_number,
timeout,
)
.and_then(|result| result.ok_or_else(|| "No response to deposit root".to_string()))
.and_then(|bytes| {
if bytes.is_empty() {
Ok(None)
} else if bytes.len() == DEPOSIT_ROOT_BYTES {
Ok(Some(Hash256::from_slice(&bytes)))
} else {
Err(format!(
"Deposit root response was not {} bytes: {:?}",
DEPOSIT_ROOT_BYTES, bytes
))
}
})
}
/// Performs a instant, no-transaction call to the contract `address` with the given `0x`-prefixed
/// `hex_data`.
///
/// Returns bytes, if any.
///
/// Uses HTTP JSON RPC at `endpoint`. E.g., `http://localhost:8545`.
fn call(
endpoint: &str,
address: &str,
hex_data: &str,
block_number: u64,
timeout: Duration,
) -> impl Future<Item = Option<Vec<u8>>, Error = String> {
let params = json! ([
{
"to": address,
"data": hex_data,
},
format!("0x{:x}", block_number)
]);
send_rpc_request(endpoint, "eth_call", params, timeout).and_then(|response_body| {
match response_result(&response_body)? {
None => Ok(None),
Some(result) => {
let hex = result
.as_str()
.map(|s| s.to_string())
.ok_or_else(|| "'result' value was not a string".to_string())?;
Ok(Some(hex_to_bytes(&hex)?))
}
}
})
}
/// A reduced set of fields from an Eth1 contract log.
#[derive(Debug, PartialEq, Clone)]
pub struct Log {
pub(crate) block_number: u64,
pub(crate) data: Vec<u8>,
}
/// Returns logs for the `DEPOSIT_EVENT_TOPIC`, for the given `address` in the given
/// `block_height_range`.
///
/// It's not clear from the Ethereum JSON-RPC docs if this range is inclusive or not.
///
/// Uses HTTP JSON RPC at `endpoint`. E.g., `http://localhost:8545`.
pub fn get_deposit_logs_in_range(
endpoint: &str,
address: &str,
block_height_range: Range<u64>,
timeout: Duration,
) -> impl Future<Item = Vec<Log>, Error = String> {
let params = json! ([{
"address": address,
"topics": [DEPOSIT_EVENT_TOPIC],
"fromBlock": format!("0x{:x}", block_height_range.start),
"toBlock": format!("0x{:x}", block_height_range.end),
}]);
send_rpc_request(endpoint, "eth_getLogs", params, timeout)
.and_then(|response_body| {
response_result(&response_body)?
.ok_or_else(|| "No result field was returned for deposit logs".to_string())?
.as_array()
.cloned()
.ok_or_else(|| "'result' value was not an array".to_string())?
.into_iter()
.map(|value| {
let block_number = value
.get("blockNumber")
.ok_or_else(|| "No block number field in log")?
.as_str()
.ok_or_else(|| "Block number was not string")?;
let data = value
.get("data")
.ok_or_else(|| "No block number field in log")?
.as_str()
.ok_or_else(|| "Data was not string")?;
Ok(Log {
block_number: hex_to_u64_be(&block_number)?,
data: hex_to_bytes(data)?,
})
})
.collect::<Result<Vec<Log>, String>>()
})
.map_err(|e| format!("Failed to get logs in range: {}", e))
}
/// Sends an RPC request to `endpoint`, using a POST with the given `body`.
///
/// Tries to receive the response and parse the body as a `String`.
pub fn send_rpc_request(
endpoint: &str,
method: &str,
params: Value,
timeout: Duration,
) -> impl Future<Item = String, Error = String> {
let body = json! ({
"jsonrpc": "2.0",
"method": method,
"params": params,
"id": 1
})
.to_string();
// Note: it is not ideal to create a new client for each request.
//
// A better solution would be to create some struct that contains a built client and pass it
// around (similar to the `web3` crate's `Transport` structs).
ClientBuilder::new()
.timeout(timeout)
.build()
.expect("The builder should always build a client")
.post(endpoint)
.header(CONTENT_TYPE, "application/json")
.body(body)
.send()
.map_err(|e| format!("Request failed: {:?}", e))
.and_then(|response| {
if response.status() != StatusCode::OK {
Err(format!(
"Response HTTP status was not 200 OK: {}.",
response.status()
))
} else {
Ok(response)
}
})
.and_then(|response| {
response
.headers()
.get(CONTENT_TYPE)
.ok_or_else(|| "No content-type header in response".to_string())
.and_then(|encoding| {
encoding
.to_str()
.map(|s| s.to_string())
.map_err(|e| format!("Failed to parse content-type header: {}", e))
})
.map(|encoding| (response, encoding))
})
.and_then(|(response, encoding)| {
response
.into_body()
.concat2()
.map(|chunk| chunk.iter().cloned().collect::<Vec<u8>>())
.map_err(|e| format!("Failed to receive body: {:?}", e))
.and_then(move |bytes| match encoding.as_str() {
"application/json" => Ok(bytes),
"application/json; charset=utf-8" => Ok(bytes),
// Note: gzip is not presently working because we always seem to get an empty
// response from the server.
//
// I expect this is some simple-to-solve issue for someone who is familiar with
// the eth1 JSON RPC.
//
// Some public-facing web3 servers use gzip to compress their traffic, it would
// be good to support this.
"application/x-gzip" => {
let mut decoder = Decoder::new(&bytes[..])
.map_err(|e| format!("Failed to create gzip decoder: {}", e))?;
let mut decompressed = vec![];
decoder
.read_to_end(&mut decompressed)
.map_err(|e| format!("Failed to decompress gzip data: {}", e))?;
Ok(decompressed)
}
other => Err(format!("Unsupported encoding: {}", other)),
})
.map(|bytes| String::from_utf8_lossy(&bytes).into_owned())
.map_err(|e| format!("Failed to receive body: {:?}", e))
})
}
/// Accepts an entire HTTP body (as a string) and returns the `result` field, as a serde `Value`.
fn response_result(response: &str) -> Result<Option<Value>, String> {
Ok(serde_json::from_str::<Value>(&response)
.map_err(|e| format!("Failed to parse response: {:?}", e))?
.get("result")
.cloned()
.map(Some)
.unwrap_or_else(|| None))
}
/// Parses a `0x`-prefixed, **big-endian** hex string as a u64.
///
/// Note: the JSON-RPC encodes integers as big-endian. The deposit contract uses little-endian.
/// Therefore, this function is only useful for numbers encoded by the JSON RPC.
///
/// E.g., `0x01 == 1`
fn hex_to_u64_be(hex: &str) -> Result<u64, String> {
u64::from_str_radix(strip_prefix(hex)?, 16)
.map_err(|e| format!("Failed to parse hex as u64: {:?}", e))
}
/// Parses a `0x`-prefixed, big-endian hex string as bytes.
///
/// E.g., `0x0102 == vec![1, 2]`
fn hex_to_bytes(hex: &str) -> Result<Vec<u8>, String> {
hex::decode(strip_prefix(hex)?).map_err(|e| format!("Failed to parse hex as bytes: {:?}", e))
}
/// Removes the `0x` prefix from some bytes. Returns an error if the prefix is not present.
fn strip_prefix(hex: &str) -> Result<&str, String> {
if hex.starts_with("0x") {
Ok(&hex[2..])
} else {
Err("Hex string did not start with `0x`".to_string())
}
}

View File

@@ -0,0 +1,27 @@
use crate::Config;
use crate::{block_cache::BlockCache, deposit_cache::DepositCache};
use parking_lot::RwLock;
#[derive(Default)]
pub struct DepositUpdater {
pub cache: DepositCache,
pub last_processed_block: Option<u64>,
}
#[derive(Default)]
pub struct Inner {
pub block_cache: RwLock<BlockCache>,
pub deposit_cache: RwLock<DepositUpdater>,
pub config: RwLock<Config>,
}
impl Inner {
/// Prunes the block cache to `self.target_block_cache_len`.
///
/// Is a no-op if `self.target_block_cache_len` is `None`.
pub fn prune_blocks(&self) {
if let Some(block_cache_truncation) = self.config.read().block_cache_truncation {
self.block_cache.write().truncate(block_cache_truncation);
}
}
}

View File

@@ -0,0 +1,11 @@
mod block_cache;
mod deposit_cache;
mod deposit_log;
pub mod http;
mod inner;
mod service;
pub use block_cache::{BlockCache, Eth1Block};
pub use deposit_cache::DepositCache;
pub use deposit_log::DepositLog;
pub use service::{BlockCacheUpdateOutcome, Config, DepositCacheUpdateOutcome, Error, Service};

View File

@@ -0,0 +1,643 @@
use crate::{
block_cache::{BlockCache, Error as BlockCacheError, Eth1Block},
deposit_cache::Error as DepositCacheError,
http::{
get_block, get_block_number, get_deposit_count, get_deposit_logs_in_range, get_deposit_root,
},
inner::{DepositUpdater, Inner},
DepositLog,
};
use exit_future::Exit;
use futures::{
future::{loop_fn, Loop},
stream, Future, Stream,
};
use parking_lot::{RwLock, RwLockReadGuard};
use serde::{Deserialize, Serialize};
use slog::{debug, error, trace, Logger};
use std::ops::{Range, RangeInclusive};
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::timer::Delay;
const STANDARD_TIMEOUT_MILLIS: u64 = 15_000;
/// Timeout when doing a eth_blockNumber call.
const BLOCK_NUMBER_TIMEOUT_MILLIS: u64 = STANDARD_TIMEOUT_MILLIS;
/// Timeout when doing an eth_getBlockByNumber call.
const GET_BLOCK_TIMEOUT_MILLIS: u64 = STANDARD_TIMEOUT_MILLIS;
/// Timeout when doing an eth_call to read the deposit contract root.
const GET_DEPOSIT_ROOT_TIMEOUT_MILLIS: u64 = STANDARD_TIMEOUT_MILLIS;
/// Timeout when doing an eth_call to read the deposit contract deposit count.
const GET_DEPOSIT_COUNT_TIMEOUT_MILLIS: u64 = STANDARD_TIMEOUT_MILLIS;
/// Timeout when doing an eth_getLogs to read the deposit contract logs.
const GET_DEPOSIT_LOG_TIMEOUT_MILLIS: u64 = STANDARD_TIMEOUT_MILLIS;
#[derive(Debug, PartialEq, Clone)]
pub enum Error {
/// The remote node is less synced that we expect, it is not useful until has done more
/// syncing.
RemoteNotSynced {
next_required_block: u64,
remote_highest_block: u64,
follow_distance: u64,
},
/// Failed to download a block from the eth1 node.
BlockDownloadFailed(String),
/// Failed to get the current block number from the eth1 node.
GetBlockNumberFailed(String),
/// Failed to read the deposit contract root from the eth1 node.
GetDepositRootFailed(String),
/// Failed to read the deposit contract deposit count from the eth1 node.
GetDepositCountFailed(String),
/// Failed to read the deposit contract root from the eth1 node.
GetDepositLogsFailed(String),
/// There was an inconsistency when adding a block to the cache.
FailedToInsertEth1Block(BlockCacheError),
/// There was an inconsistency when adding a deposit to the cache.
FailedToInsertDeposit(DepositCacheError),
/// A log downloaded from the eth1 contract was not well formed.
FailedToParseDepositLog {
block_range: Range<u64>,
error: String,
},
/// There was an unexpected internal error.
Internal(String),
}
/// The success message for an Eth1Data cache update.
#[derive(Debug, PartialEq, Clone)]
pub enum BlockCacheUpdateOutcome {
/// The cache was sucessfully updated.
Success {
blocks_imported: usize,
head_block_number: Option<u64>,
},
}
/// The success message for an Eth1 deposit cache update.
#[derive(Debug, PartialEq, Clone)]
pub enum DepositCacheUpdateOutcome {
/// The cache was sucessfully updated.
Success { logs_imported: usize },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
/// An Eth1 node (e.g., Geth) running a HTTP JSON-RPC endpoint.
pub endpoint: String,
/// The address the `BlockCache` and `DepositCache` should assume is the canonical deposit contract.
pub deposit_contract_address: String,
/// Defines the first block that the `DepositCache` will start searching for deposit logs.
///
/// Setting too high can result in missed logs. Setting too low will result in unnecessary
/// calls to the Eth1 node's HTTP JSON RPC.
pub deposit_contract_deploy_block: u64,
/// Defines the lowest block number that should be downloaded and added to the `BlockCache`.
pub lowest_cached_block_number: u64,
/// Defines how far behind the Eth1 node's head we should follow.
///
/// Note: this should be less than or equal to the specification's `ETH1_FOLLOW_DISTANCE`.
pub follow_distance: u64,
/// Defines the number of blocks that should be retained each time the `BlockCache` calls truncate on
/// itself.
pub block_cache_truncation: Option<usize>,
/// The interval between updates when using the `auto_update` function.
pub auto_update_interval_millis: u64,
/// The span of blocks we should query for logs, per request.
pub blocks_per_log_query: usize,
/// The maximum number of log requests per update.
pub max_log_requests_per_update: Option<usize>,
/// The maximum number of log requests per update.
pub max_blocks_per_update: Option<usize>,
}
impl Default for Config {
fn default() -> Self {
Self {
endpoint: "http://localhost:8545".into(),
deposit_contract_address: "0x0000000000000000000000000000000000000000".into(),
deposit_contract_deploy_block: 0,
lowest_cached_block_number: 0,
follow_distance: 128,
block_cache_truncation: Some(4_096),
auto_update_interval_millis: 500,
blocks_per_log_query: 1_000,
max_log_requests_per_update: None,
max_blocks_per_update: None,
}
}
}
/// Provides a set of Eth1 caches and async functions to update them.
///
/// Stores the following caches:
///
/// - Deposit cache: stores all deposit logs from the deposit contract.
/// - Block cache: stores some number of eth1 blocks.
#[derive(Clone)]
pub struct Service {
inner: Arc<Inner>,
pub log: Logger,
}
impl Service {
/// Creates a new service. Does not attempt to connect to the eth1 node.
pub fn new(config: Config, log: Logger) -> Self {
Self {
inner: Arc::new(Inner {
config: RwLock::new(config),
..Inner::default()
}),
log,
}
}
/// Provides access to the block cache.
pub fn blocks(&self) -> &RwLock<BlockCache> {
&self.inner.block_cache
}
/// Provides access to the deposit cache.
pub fn deposits(&self) -> &RwLock<DepositUpdater> {
&self.inner.deposit_cache
}
/// Returns the number of currently cached blocks.
pub fn block_cache_len(&self) -> usize {
self.blocks().read().len()
}
/// Returns the number deposits available in the deposit cache.
pub fn deposit_cache_len(&self) -> usize {
self.deposits().read().cache.len()
}
/// Read the service's configuration.
pub fn config(&self) -> RwLockReadGuard<Config> {
self.inner.config.read()
}
/// Updates the configuration in `self to be `new_config`.
///
/// Will truncate the block cache if the new configure specifies truncation.
pub fn update_config(&self, new_config: Config) -> Result<(), String> {
let mut old_config = self.inner.config.write();
if new_config.deposit_contract_deploy_block != old_config.deposit_contract_deploy_block {
// This may be possible, I just haven't looked into the details to ensure it's safe.
Err("Updating deposit_contract_deploy_block is not supported".to_string())
} else {
*old_config = new_config;
// Prevents a locking condition when calling prune_blocks.
drop(old_config);
self.inner.prune_blocks();
Ok(())
}
}
/// Set the lowest block that the block cache will store.
///
/// Note: this block may not always be present if truncating is enabled.
pub fn set_lowest_cached_block(&self, block_number: u64) {
self.inner.config.write().lowest_cached_block_number = block_number;
}
/// Update the deposit and block cache, returning an error if either fail.
///
/// ## Returns
///
/// - Ok(_) if the update was successful (the cache may or may not have been modified).
/// - Err(_) if there is an error.
///
/// Emits logs for debugging and errors.
pub fn update(
&self,
) -> impl Future<Item = (DepositCacheUpdateOutcome, BlockCacheUpdateOutcome), Error = String>
{
let log_a = self.log.clone();
let log_b = self.log.clone();
let deposit_future = self
.update_deposit_cache()
.map_err(|e| format!("Failed to update eth1 cache: {:?}", e))
.then(move |result| {
match &result {
Ok(DepositCacheUpdateOutcome::Success { logs_imported }) => trace!(
log_a,
"Updated eth1 deposit cache";
"logs_imported" => logs_imported,
),
Err(e) => error!(
log_a,
"Failed to update eth1 deposit cache";
"error" => e
),
};
result
});
let block_future = self
.update_block_cache()
.map_err(|e| format!("Failed to update eth1 cache: {:?}", e))
.then(move |result| {
match &result {
Ok(BlockCacheUpdateOutcome::Success {
blocks_imported,
head_block_number,
}) => trace!(
log_b,
"Updated eth1 block cache";
"blocks_imported" => blocks_imported,
"head_block" => head_block_number,
),
Err(e) => error!(
log_b,
"Failed to update eth1 block cache";
"error" => e
),
};
result
});
deposit_future.join(block_future)
}
/// A looping future that updates the cache, then waits `config.auto_update_interval` before
/// updating it again.
///
/// ## Returns
///
/// - Ok(_) if the update was successful (the cache may or may not have been modified).
/// - Err(_) if there is an error.
///
/// Emits logs for debugging and errors.
pub fn auto_update(&self, exit: Exit) -> impl Future<Item = (), Error = ()> {
let service = self.clone();
let log = self.log.clone();
let update_interval = Duration::from_millis(self.config().auto_update_interval_millis);
loop_fn((), move |()| {
let exit = exit.clone();
let service = service.clone();
let log_a = log.clone();
let log_b = log.clone();
service
.update()
.then(move |update_result| {
match update_result {
Err(e) => error!(
log_a,
"Failed to update eth1 genesis cache";
"retry_millis" => update_interval.as_millis(),
"error" => e,
),
Ok((deposit, block)) => debug!(
log_a,
"Updated eth1 genesis cache";
"retry_millis" => update_interval.as_millis(),
"blocks" => format!("{:?}", block),
"deposits" => format!("{:?}", deposit),
),
};
// Do not break the loop if there is an update failure.
Ok(())
})
.and_then(move |_| Delay::new(Instant::now() + update_interval))
.then(move |timer_result| {
if let Err(e) = timer_result {
error!(
log_b,
"Failed to trigger eth1 cache update delay";
"error" => format!("{:?}", e),
);
}
// Do not break the loop if there is an timer failure.
Ok(())
})
.map(move |_| {
if exit.is_live() {
Loop::Continue(())
} else {
Loop::Break(())
}
})
})
}
/// Contacts the remote eth1 node and attempts to import deposit logs up to the configured
/// follow-distance block.
///
/// Will process no more than `BLOCKS_PER_LOG_QUERY * MAX_LOG_REQUESTS_PER_UPDATE` blocks in a
/// single update.
///
/// ## Resolves with
///
/// - Ok(_) if the update was successful (the cache may or may not have been modified).
/// - Err(_) if there is an error.
///
/// Emits logs for debugging and errors.
pub fn update_deposit_cache(
&self,
) -> impl Future<Item = DepositCacheUpdateOutcome, Error = Error> {
let service_1 = self.clone();
let service_2 = self.clone();
let blocks_per_log_query = self.config().blocks_per_log_query;
let max_log_requests_per_update = self
.config()
.max_log_requests_per_update
.unwrap_or_else(usize::max_value);
let next_required_block = self
.deposits()
.read()
.last_processed_block
.map(|n| n + 1)
.unwrap_or_else(|| self.config().deposit_contract_deploy_block);
get_new_block_numbers(
&self.config().endpoint,
next_required_block,
self.config().follow_distance,
)
.map(move |range| {
range
.map(|range| {
range
.collect::<Vec<u64>>()
.chunks(blocks_per_log_query)
.take(max_log_requests_per_update)
.map(|vec| {
let first = vec.first().cloned().unwrap_or_else(|| 0);
let last = vec.last().map(|n| n + 1).unwrap_or_else(|| 0);
(first..last)
})
.collect::<Vec<Range<u64>>>()
})
.unwrap_or_else(|| vec![])
})
.and_then(move |block_number_chunks| {
stream::unfold(
block_number_chunks.into_iter(),
move |mut chunks| match chunks.next() {
Some(chunk) => {
let chunk_1 = chunk.clone();
Some(
get_deposit_logs_in_range(
&service_1.config().endpoint,
&service_1.config().deposit_contract_address,
chunk,
Duration::from_millis(GET_DEPOSIT_LOG_TIMEOUT_MILLIS),
)
.map_err(Error::GetDepositLogsFailed)
.map(|logs| (chunk_1, logs))
.map(|logs| (logs, chunks)),
)
}
None => None,
},
)
.fold(0, move |mut sum, (block_range, log_chunk)| {
let mut cache = service_2.deposits().write();
log_chunk
.into_iter()
.map(|raw_log| {
DepositLog::from_log(&raw_log).map_err(|error| {
Error::FailedToParseDepositLog {
block_range: block_range.clone(),
error,
}
})
})
// Return early if any of the logs cannot be parsed.
//
// This costs an additional `collect`, however it enforces that no logs are
// imported if any one of them cannot be parsed.
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.map(|deposit_log| {
cache
.cache
.insert_log(deposit_log)
.map_err(Error::FailedToInsertDeposit)?;
sum += 1;
Ok(())
})
// Returns if a deposit is unable to be added to the cache.
//
// If this error occurs, the cache will no longer be guaranteed to hold either
// none or all of the logs for each block (i.e., they may exist _some_ logs for
// a block, but not _all_ logs for that block). This scenario can cause the
// node to choose an invalid genesis state or propose an invalid block.
.collect::<Result<_, _>>()?;
cache.last_processed_block = Some(block_range.end.saturating_sub(1));
Ok(sum)
})
.map(|logs_imported| DepositCacheUpdateOutcome::Success { logs_imported })
})
}
/// Contacts the remote eth1 node and attempts to import all blocks up to the configured
/// follow-distance block.
///
/// If configured, prunes the block cache after importing new blocks.
///
/// ## Resolves with
///
/// - Ok(_) if the update was successful (the cache may or may not have been modified).
/// - Err(_) if there is an error.
///
/// Emits logs for debugging and errors.
pub fn update_block_cache(&self) -> impl Future<Item = BlockCacheUpdateOutcome, Error = Error> {
let cache_1 = self.inner.clone();
let cache_2 = self.inner.clone();
let cache_3 = self.inner.clone();
let cache_4 = self.inner.clone();
let cache_5 = self.inner.clone();
let block_cache_truncation = self.config().block_cache_truncation;
let max_blocks_per_update = self
.config()
.max_blocks_per_update
.unwrap_or_else(usize::max_value);
let next_required_block = cache_1
.block_cache
.read()
.highest_block_number()
.map(|n| n + 1)
.unwrap_or_else(|| self.config().lowest_cached_block_number);
get_new_block_numbers(
&self.config().endpoint,
next_required_block,
self.config().follow_distance,
)
// Map the range of required blocks into a Vec.
//
// If the required range is larger than the size of the cache, drop the exiting cache
// because it's exipred and just download enough blocks to fill the cache.
.and_then(move |range| {
range
.map(|range| {
if range.start() > range.end() {
// Note: this check is not strictly necessary, however it remains to safe
// guard against any regression which may cause an underflow in a following
// subtraction operation.
Err(Error::Internal("Range was not increasing".into()))
} else {
let range_size = range.end() - range.start();
let max_size = block_cache_truncation
.map(|n| n as u64)
.unwrap_or_else(u64::max_value);
if range_size > max_size {
// If the range of required blocks is larger than `max_size`, drop all
// existing blocks and download `max_size` count of blocks.
let first_block = range.end() - max_size;
(*cache_5.block_cache.write()) = BlockCache::default();
Ok((first_block..=*range.end()).collect::<Vec<u64>>())
} else {
Ok(range.collect::<Vec<u64>>())
}
}
})
.unwrap_or_else(|| Ok(vec![]))
})
// Download the range of blocks and sequentially import them into the cache.
.and_then(move |required_block_numbers| {
let required_block_numbers = required_block_numbers
.into_iter()
.take(max_blocks_per_update);
// Produce a stream from the list of required block numbers and return a future that
// consumes the it.
stream::unfold(
required_block_numbers,
move |mut block_numbers| match block_numbers.next() {
Some(block_number) => Some(
download_eth1_block(cache_2.clone(), block_number)
.map(|v| (v, block_numbers)),
),
None => None,
},
)
.fold(0, move |sum, eth1_block| {
cache_3
.block_cache
.write()
.insert_root_or_child(eth1_block)
.map_err(Error::FailedToInsertEth1Block)?;
Ok(sum + 1)
})
})
.and_then(move |blocks_imported| {
// Prune the block cache, preventing it from growing too large.
cache_4.prune_blocks();
Ok(BlockCacheUpdateOutcome::Success {
blocks_imported,
head_block_number: cache_4.clone().block_cache.read().highest_block_number(),
})
})
}
}
/// Determine the range of blocks that need to be downloaded, given the remotes best block and
/// the locally stored best block.
fn get_new_block_numbers<'a>(
endpoint: &str,
next_required_block: u64,
follow_distance: u64,
) -> impl Future<Item = Option<RangeInclusive<u64>>, Error = Error> + 'a {
get_block_number(endpoint, Duration::from_millis(BLOCK_NUMBER_TIMEOUT_MILLIS))
.map_err(Error::GetBlockNumberFailed)
.and_then(move |remote_highest_block| {
let remote_follow_block = remote_highest_block.saturating_sub(follow_distance);
if next_required_block <= remote_follow_block {
Ok(Some(next_required_block..=remote_follow_block))
} else if next_required_block > remote_highest_block + 1 {
// If this is the case, the node must have gone "backwards" in terms of it's sync
// (i.e., it's head block is lower than it was before).
//
// We assume that the `follow_distance` should be sufficient to ensure this never
// happens, otherwise it is an error.
Err(Error::RemoteNotSynced {
next_required_block,
remote_highest_block,
follow_distance,
})
} else {
// Return an empty range.
Ok(None)
}
})
}
/// Downloads the `(block, deposit_root, deposit_count)` tuple from an eth1 node for the given
/// `block_number`.
///
/// Performs three async calls to an Eth1 HTTP JSON RPC endpoint.
fn download_eth1_block<'a>(
cache: Arc<Inner>,
block_number: u64,
) -> impl Future<Item = Eth1Block, Error = Error> + 'a {
// Performs a `get_blockByNumber` call to an eth1 node.
get_block(
&cache.config.read().endpoint,
block_number,
Duration::from_millis(GET_BLOCK_TIMEOUT_MILLIS),
)
.map_err(Error::BlockDownloadFailed)
.join3(
// Perform 2x `eth_call` via an eth1 node to read the deposit contract root and count.
get_deposit_root(
&cache.config.read().endpoint,
&cache.config.read().deposit_contract_address,
block_number,
Duration::from_millis(GET_DEPOSIT_ROOT_TIMEOUT_MILLIS),
)
.map_err(Error::GetDepositRootFailed),
get_deposit_count(
&cache.config.read().endpoint,
&cache.config.read().deposit_contract_address,
block_number,
Duration::from_millis(GET_DEPOSIT_COUNT_TIMEOUT_MILLIS),
)
.map_err(Error::GetDepositCountFailed),
)
.map(|(http_block, deposit_root, deposit_count)| Eth1Block {
hash: http_block.hash,
number: http_block.number,
timestamp: http_block.timestamp,
deposit_root,
deposit_count,
})
}
#[cfg(test)]
mod tests {
use super::*;
use toml;
#[test]
fn serde_serialize() {
let serialized =
toml::to_string(&Config::default()).expect("Should serde encode default config");
toml::from_str::<Config>(&serialized).expect("Should serde decode default config");
}
}

View File

@@ -0,0 +1,713 @@
#![cfg(test)]
use environment::{Environment, EnvironmentBuilder};
use eth1::http::{get_deposit_count, get_deposit_logs_in_range, get_deposit_root, Block, Log};
use eth1::{Config, Service};
use eth1::{DepositCache, DepositLog};
use eth1_test_rig::GanacheEth1Instance;
use exit_future;
use futures::Future;
use merkle_proof::verify_merkle_proof;
use std::ops::Range;
use std::time::Duration;
use tokio::runtime::Runtime;
use tree_hash::TreeHash;
use types::{DepositData, EthSpec, Hash256, Keypair, MainnetEthSpec, MinimalEthSpec, Signature};
use web3::{transports::Http, Web3};
const DEPOSIT_CONTRACT_TREE_DEPTH: usize = 32;
pub fn new_env() -> Environment<MinimalEthSpec> {
EnvironmentBuilder::minimal()
// Use a single thread, so that when all tests are run in parallel they don't have so many
// threads.
.single_thread_tokio_runtime()
.expect("should start tokio runtime")
.null_logger()
.expect("should start null logger")
.build()
.expect("should build env")
}
fn timeout() -> Duration {
Duration::from_secs(1)
}
fn random_deposit_data() -> DepositData {
let keypair = Keypair::random();
let mut deposit = DepositData {
pubkey: keypair.pk.into(),
withdrawal_credentials: Hash256::zero(),
amount: 32_000_000_000,
signature: Signature::empty_signature().into(),
};
deposit.signature = deposit.create_signature(&keypair.sk, &MainnetEthSpec::default_spec());
deposit
}
/// Blocking operation to get the deposit logs from the `deposit_contract`.
fn blocking_deposit_logs(
runtime: &mut Runtime,
eth1: &GanacheEth1Instance,
range: Range<u64>,
) -> Vec<Log> {
runtime
.block_on(get_deposit_logs_in_range(
&eth1.endpoint(),
&eth1.deposit_contract.address(),
range,
timeout(),
))
.expect("should get logs")
}
/// Blocking operation to get the deposit root from the `deposit_contract`.
fn blocking_deposit_root(
runtime: &mut Runtime,
eth1: &GanacheEth1Instance,
block_number: u64,
) -> Option<Hash256> {
runtime
.block_on(get_deposit_root(
&eth1.endpoint(),
&eth1.deposit_contract.address(),
block_number,
timeout(),
))
.expect("should get deposit root")
}
/// Blocking operation to get the deposit count from the `deposit_contract`.
fn blocking_deposit_count(
runtime: &mut Runtime,
eth1: &GanacheEth1Instance,
block_number: u64,
) -> Option<u64> {
runtime
.block_on(get_deposit_count(
&eth1.endpoint(),
&eth1.deposit_contract.address(),
block_number,
timeout(),
))
.expect("should get deposit count")
}
fn get_block_number(runtime: &mut Runtime, web3: &Web3<Http>) -> u64 {
runtime
.block_on(web3.eth().block_number().map(|v| v.as_u64()))
.expect("should get block number")
}
mod auto_update {
use super::*;
#[test]
fn can_auto_update() {
let mut env = new_env();
let log = env.core_context().log;
let runtime = env.runtime();
let eth1 = runtime
.block_on(GanacheEth1Instance::new())
.expect("should start eth1 environment");
let deposit_contract = &eth1.deposit_contract;
let web3 = eth1.web3();
let now = get_block_number(runtime, &web3);
let service = Service::new(
Config {
endpoint: eth1.endpoint(),
deposit_contract_address: deposit_contract.address(),
deposit_contract_deploy_block: now,
lowest_cached_block_number: now,
follow_distance: 0,
block_cache_truncation: None,
..Config::default()
},
log,
);
// NOTE: this test is sensitive to the response speed of the external web3 server. If
// you're experiencing failures, try increasing the update_interval.
let update_interval = Duration::from_millis(2_000);
assert_eq!(
service.block_cache_len(),
0,
"should have imported no blocks"
);
assert_eq!(
service.deposit_cache_len(),
0,
"should have imported no deposits"
);
let (_exit, signal) = exit_future::signal();
runtime.executor().spawn(service.auto_update(signal));
let n = 4;
for _ in 0..n {
deposit_contract
.deposit(runtime, random_deposit_data())
.expect("should do first deposits");
}
std::thread::sleep(update_interval * 5);
assert!(
service.deposit_cache_len() >= n,
"should have imported n deposits"
);
for _ in 0..n {
deposit_contract
.deposit(runtime, random_deposit_data())
.expect("should do second deposits");
}
std::thread::sleep(update_interval * 4);
assert!(
service.block_cache_len() >= n * 2,
"should have imported all blocks"
);
assert!(
service.deposit_cache_len() >= n * 2,
"should have imported all deposits, not {}",
service.deposit_cache_len()
);
}
}
mod eth1_cache {
use super::*;
#[test]
fn simple_scenario() {
let mut env = new_env();
let log = env.core_context().log;
let runtime = env.runtime();
for follow_distance in 0..2 {
let eth1 = runtime
.block_on(GanacheEth1Instance::new())
.expect("should start eth1 environment");
let deposit_contract = &eth1.deposit_contract;
let web3 = eth1.web3();
let initial_block_number = get_block_number(runtime, &web3);
let service = Service::new(
Config {
endpoint: eth1.endpoint(),
deposit_contract_address: deposit_contract.address(),
lowest_cached_block_number: initial_block_number,
follow_distance,
..Config::default()
},
log.clone(),
);
// Create some blocks and then consume them, performing the test `rounds` times.
for round in 0..2 {
let blocks = 4;
let initial = if round == 0 {
initial_block_number
} else {
service
.blocks()
.read()
.highest_block_number()
.map(|n| n + follow_distance)
.expect("should have a latest block after the first round")
};
for _ in 0..blocks {
runtime
.block_on(eth1.ganache.evm_mine())
.expect("should mine block");
}
runtime
.block_on(service.update_block_cache())
.expect("should update cache");
runtime
.block_on(service.update_block_cache())
.expect("should update cache when nothing has changed");
assert_eq!(
service
.blocks()
.read()
.highest_block_number()
.map(|n| n + follow_distance),
Some(initial + blocks),
"should update {} blocks in round {} (follow {})",
blocks,
round,
follow_distance,
);
}
}
}
/// Tests the case where we attempt to download more blocks than will fit in the cache.
#[test]
fn big_skip() {
let mut env = new_env();
let log = env.core_context().log;
let runtime = env.runtime();
let eth1 = runtime
.block_on(GanacheEth1Instance::new())
.expect("should start eth1 environment");
let deposit_contract = &eth1.deposit_contract;
let web3 = eth1.web3();
let cache_len = 4;
let service = Service::new(
Config {
endpoint: eth1.endpoint(),
deposit_contract_address: deposit_contract.address(),
lowest_cached_block_number: get_block_number(runtime, &web3),
follow_distance: 0,
block_cache_truncation: Some(cache_len),
..Config::default()
},
log,
);
let blocks = cache_len * 2;
for _ in 0..blocks {
runtime
.block_on(eth1.ganache.evm_mine())
.expect("should mine block")
}
runtime
.block_on(service.update_block_cache())
.expect("should update cache");
assert_eq!(
service.block_cache_len(),
cache_len,
"should not grow cache beyond target"
);
}
/// Tests to ensure that the cache gets pruned when doing multiple downloads smaller than the
/// cache size.
#[test]
fn pruning() {
let mut env = new_env();
let log = env.core_context().log;
let runtime = env.runtime();
let eth1 = runtime
.block_on(GanacheEth1Instance::new())
.expect("should start eth1 environment");
let deposit_contract = &eth1.deposit_contract;
let web3 = eth1.web3();
let cache_len = 4;
let service = Service::new(
Config {
endpoint: eth1.endpoint(),
deposit_contract_address: deposit_contract.address(),
lowest_cached_block_number: get_block_number(runtime, &web3),
follow_distance: 0,
block_cache_truncation: Some(cache_len),
..Config::default()
},
log,
);
for _ in 0..4 {
for _ in 0..cache_len / 2 {
runtime
.block_on(eth1.ganache.evm_mine())
.expect("should mine block")
}
runtime
.block_on(service.update_block_cache())
.expect("should update cache");
}
assert_eq!(
service.block_cache_len(),
cache_len,
"should not grow cache beyond target"
);
}
#[test]
fn double_update() {
let mut env = new_env();
let log = env.core_context().log;
let runtime = env.runtime();
let n = 16;
let eth1 = runtime
.block_on(GanacheEth1Instance::new())
.expect("should start eth1 environment");
let deposit_contract = &eth1.deposit_contract;
let web3 = eth1.web3();
let service = Service::new(
Config {
endpoint: eth1.endpoint(),
deposit_contract_address: deposit_contract.address(),
lowest_cached_block_number: get_block_number(runtime, &web3),
follow_distance: 0,
..Config::default()
},
log,
);
for _ in 0..n {
runtime
.block_on(eth1.ganache.evm_mine())
.expect("should mine block")
}
runtime
.block_on(
service
.update_block_cache()
.join(service.update_block_cache()),
)
.expect("should perform two simultaneous updates");
assert!(service.block_cache_len() >= n, "should grow the cache");
}
}
mod deposit_tree {
use super::*;
#[test]
fn updating() {
let mut env = new_env();
let log = env.core_context().log;
let runtime = env.runtime();
let n = 4;
let eth1 = runtime
.block_on(GanacheEth1Instance::new())
.expect("should start eth1 environment");
let deposit_contract = &eth1.deposit_contract;
let web3 = eth1.web3();
let start_block = get_block_number(runtime, &web3);
let service = Service::new(
Config {
endpoint: eth1.endpoint(),
deposit_contract_address: deposit_contract.address(),
deposit_contract_deploy_block: start_block,
follow_distance: 0,
..Config::default()
},
log,
);
for round in 0..3 {
let deposits: Vec<_> = (0..n).into_iter().map(|_| random_deposit_data()).collect();
for deposit in &deposits {
deposit_contract
.deposit(runtime, deposit.clone())
.expect("should perform a deposit");
}
runtime
.block_on(service.update_deposit_cache())
.expect("should perform update");
runtime
.block_on(service.update_deposit_cache())
.expect("should perform update when nothing has changed");
let first = n * round;
let last = n * (round + 1);
let (_root, local_deposits) = service
.deposits()
.read()
.cache
.get_deposits(first..last, last, 32)
.expect(&format!("should get deposits in round {}", round));
assert_eq!(
local_deposits.len(),
n as usize,
"should get the right number of deposits in round {}",
round
);
assert_eq!(
local_deposits
.iter()
.map(|d| d.data.clone())
.collect::<Vec<_>>(),
deposits.to_vec(),
"obtained deposits should match those submitted in round {}",
round
);
}
}
#[test]
fn double_update() {
let mut env = new_env();
let log = env.core_context().log;
let runtime = env.runtime();
let n = 8;
let eth1 = runtime
.block_on(GanacheEth1Instance::new())
.expect("should start eth1 environment");
let deposit_contract = &eth1.deposit_contract;
let web3 = eth1.web3();
let start_block = get_block_number(runtime, &web3);
let service = Service::new(
Config {
endpoint: eth1.endpoint(),
deposit_contract_address: deposit_contract.address(),
deposit_contract_deploy_block: start_block,
lowest_cached_block_number: start_block,
follow_distance: 0,
..Config::default()
},
log,
);
let deposits: Vec<_> = (0..n).into_iter().map(|_| random_deposit_data()).collect();
for deposit in &deposits {
deposit_contract
.deposit(runtime, deposit.clone())
.expect("should perform a deposit");
}
runtime
.block_on(
service
.update_deposit_cache()
.join(service.update_deposit_cache()),
)
.expect("should perform two updates concurrently");
assert_eq!(service.deposit_cache_len(), n);
}
#[test]
fn cache_consistency() {
let mut env = new_env();
let runtime = env.runtime();
let n = 8;
let deposits: Vec<_> = (0..n).into_iter().map(|_| random_deposit_data()).collect();
let eth1 = runtime
.block_on(GanacheEth1Instance::new())
.expect("should start eth1 environment");
let deposit_contract = &eth1.deposit_contract;
let web3 = eth1.web3();
let mut deposit_roots = vec![];
let mut deposit_counts = vec![];
// Perform deposits to the smart contract, recording it's state along the way.
for deposit in &deposits {
deposit_contract
.deposit(runtime, deposit.clone())
.expect("should perform a deposit");
let block_number = get_block_number(runtime, &web3);
deposit_roots.push(
blocking_deposit_root(runtime, &eth1, block_number)
.expect("should get root if contract exists"),
);
deposit_counts.push(
blocking_deposit_count(runtime, &eth1, block_number)
.expect("should get count if contract exists"),
);
}
let mut tree = DepositCache::default();
// Pull all the deposit logs from the contract.
let block_number = get_block_number(runtime, &web3);
let logs: Vec<_> = blocking_deposit_logs(runtime, &eth1, 0..block_number)
.iter()
.map(|raw| DepositLog::from_log(raw).expect("should parse deposit log"))
.inspect(|log| {
tree.insert_log(log.clone())
.expect("should add consecutive logs")
})
.collect();
// Check the logs for invariants.
for i in 0..logs.len() {
let log = &logs[i];
assert_eq!(
log.deposit_data, deposits[i],
"log {} should have correct deposit data",
i
);
assert_eq!(log.index, i as u64, "log {} should have correct index", i);
}
// For each deposit test some more invariants
for i in 0..n {
// Ensure the deposit count from the smart contract was as expected.
assert_eq!(
deposit_counts[i],
i as u64 + 1,
"deposit count should be accurate"
);
// Ensure that the root from the deposit tree matches what the contract reported.
let (root, deposits) = tree
.get_deposits(0..i as u64, deposit_counts[i], DEPOSIT_CONTRACT_TREE_DEPTH)
.expect("should get deposits");
assert_eq!(
root, deposit_roots[i],
"tree deposit root {} should match the contract",
i
);
// Ensure that the deposits all prove into the root from the smart contract.
let deposit_root = deposit_roots[i];
for (j, deposit) in deposits.iter().enumerate() {
assert!(
verify_merkle_proof(
Hash256::from_slice(&deposit.data.tree_hash_root()),
&deposit.proof,
DEPOSIT_CONTRACT_TREE_DEPTH + 1,
j,
deposit_root
),
"deposit merkle proof should prove into deposit contract root"
)
}
}
}
}
/// Tests for the base HTTP requests and response handlers.
mod http {
use super::*;
fn get_block(runtime: &mut Runtime, eth1: &GanacheEth1Instance, block_number: u64) -> Block {
runtime
.block_on(eth1::http::get_block(
&eth1.endpoint(),
block_number,
timeout(),
))
.expect("should get block number")
}
#[test]
fn incrementing_deposits() {
let mut env = new_env();
let runtime = env.runtime();
let eth1 = runtime
.block_on(GanacheEth1Instance::new())
.expect("should start eth1 environment");
let deposit_contract = &eth1.deposit_contract;
let web3 = eth1.web3();
let block_number = get_block_number(runtime, &web3);
let logs = blocking_deposit_logs(runtime, &eth1, 0..block_number);
assert_eq!(logs.len(), 0);
let mut old_root = blocking_deposit_root(runtime, &eth1, block_number);
let mut old_block = get_block(runtime, &eth1, block_number);
let mut old_block_number = block_number;
assert_eq!(
blocking_deposit_count(runtime, &eth1, block_number),
Some(0),
"should have deposit count zero"
);
for i in 1..=8 {
runtime
.block_on(eth1.ganache.increase_time(1))
.expect("should be able to increase time on ganache");
deposit_contract
.deposit(runtime, random_deposit_data())
.expect("should perform a deposit");
// Check the logs.
let block_number = get_block_number(runtime, &web3);
let logs = blocking_deposit_logs(runtime, &eth1, 0..block_number);
assert_eq!(logs.len(), i, "the number of logs should be as expected");
// Check the deposit count.
assert_eq!(
blocking_deposit_count(runtime, &eth1, block_number),
Some(i as u64),
"should have a correct deposit count"
);
// Check the deposit root.
let new_root = blocking_deposit_root(runtime, &eth1, block_number);
assert_ne!(
new_root, old_root,
"deposit root should change with each deposit"
);
old_root = new_root;
// Check the block hash.
let new_block = get_block(runtime, &eth1, block_number);
assert_ne!(
new_block.hash, old_block.hash,
"block hash should change with each deposit"
);
// Check to ensure the timestamp is increasing
assert!(
old_block.timestamp <= new_block.timestamp,
"block timestamp should increase"
);
old_block = new_block.clone();
// Check the block number.
assert!(
block_number > old_block_number,
"block number should increase"
);
old_block_number = block_number;
// Check to ensure the block root is changing
assert_ne!(
new_root,
Some(new_block.hash),
"the deposit root should be different to the block hash"
);
}
}
}

View File

@@ -5,28 +5,28 @@ authors = ["Age Manning <Age@AgeManning.com>"]
edition = "2018"
[dependencies]
clap = "2.32.0"
clap = "2.33.0"
hex = "0.3"
#SigP repository
libp2p = { git = "https://github.com/SigP/rust-libp2p", rev = "8ac9c744197faaadc0e2b64fed7470ac4e2a41ca" }
enr = { git = "https://github.com/SigP/rust-libp2p/", rev = "8ac9c744197faaadc0e2b64fed7470ac4e2a41ca", features = ["serde"] }
libp2p = { git = "https://github.com/SigP/rust-libp2p", rev = "0d4583e110b3ab9406ecd512655bba1a9906d470" }
enr = { git = "https://github.com/SigP/rust-libp2p/", rev = "0d4583e110b3ab9406ecd512655bba1a9906d470", features = ["serde"] }
types = { path = "../../eth2/types" }
serde = "1.0"
serde_derive = "1.0"
eth2_ssz = "0.1"
eth2_ssz_derive = "0.1"
slog = { version = "^2.4.1" , features = ["max_level_trace"] }
serde = "1.0.102"
serde_derive = "1.0.102"
eth2_ssz = "0.1.2"
eth2_ssz_derive = "0.1.0"
slog = { version = "2.5.2", features = ["max_level_trace"] }
version = { path = "../version" }
tokio = "0.1.16"
futures = "0.1.25"
error-chain = "0.12.0"
tokio-timer = "0.2.10"
dirs = "2.0.1"
tokio = "0.1.22"
futures = "0.1.29"
error-chain = "0.12.1"
tokio-timer = "0.2.11"
dirs = "2.0.2"
tokio-io = "0.1.12"
smallvec = "0.6.10"
smallvec = "0.6.11"
fnv = "1.0.6"
unsigned-varint = "0.2.2"
unsigned-varint = "0.2.3"
bytes = "0.4.12"
tokio-io-timeout = "0.3.1"
lazy_static = "1.3.0"
lazy_static = "1.4.0"
lighthouse_metrics = { path = "../../eth2/utils/lighthouse_metrics" }

View File

@@ -69,7 +69,7 @@ impl<TSubstream: AsyncRead + AsyncWrite> Behaviour<TSubstream> {
);
Ok(Behaviour {
eth2_rpc: RPC::new(log),
eth2_rpc: RPC::new(log.clone()),
gossipsub: Gossipsub::new(local_peer_id.clone(), net_conf.gs_config.clone()),
discovery: Discovery::new(local_key, net_conf, log)?,
ping: Ping::new(ping_config),
@@ -153,8 +153,10 @@ impl<TSubstream: AsyncRead + AsyncWrite> NetworkBehaviourEventProcess<IdentifyEv
{
fn inject_event(&mut self, event: IdentifyEvent) {
match event {
IdentifyEvent::Identified {
peer_id, mut info, ..
IdentifyEvent::Received {
peer_id,
mut info,
observed_addr,
} => {
if info.listen_addrs.len() > MAX_IDENTIFY_ADDRESSES {
debug!(
@@ -167,11 +169,12 @@ impl<TSubstream: AsyncRead + AsyncWrite> NetworkBehaviourEventProcess<IdentifyEv
"Protocol Version" => info.protocol_version,
"Agent Version" => info.agent_version,
"Listening Addresses" => format!("{:?}", info.listen_addrs),
"Observed Address" => format!("{:?}", observed_addr),
"Protocols" => format!("{:?}", info.protocols)
);
}
IdentifyEvent::Sent { .. } => {}
IdentifyEvent::Error { .. } => {}
IdentifyEvent::SendBack { .. } => {}
}
}
}
@@ -219,6 +222,11 @@ impl<TSubstream: AsyncRead + AsyncWrite> Behaviour<TSubstream> {
pub fn connected_peers(&self) -> usize {
self.discovery.connected_peers()
}
/// Informs the discovery behaviour if a new IP/Port is set at the application layer
pub fn update_local_enr_socket(&mut self, socket: std::net::SocketAddr, is_tcp: bool) {
self.discovery.update_local_enr(socket, is_tcp);
}
}
/// The types of events than can be obtained from polling the behaviour.

View File

@@ -82,7 +82,7 @@ impl Default for Config {
gs_config: GossipsubConfigBuilder::new()
.max_transmit_size(1_048_576)
.heartbeat_interval(Duration::from_secs(20)) // TODO: Reduce for mainnet
.propagate_messages(false) // require validation before propagation
.manual_propagation(true) // require validation before propagation
.build(),
boot_nodes: vec![],
libp2p_nodes: vec![],

View File

@@ -103,6 +103,24 @@ impl<TSubstream> Discovery<TSubstream> {
})
}
/// Allows the application layer to update the `ip` and `port` of the local ENR. The second
/// parameter defines whether the port is a TPC port. If false, this is interpreted as a UDP
/// port.
pub fn update_local_enr(&mut self, socket: std::net::SocketAddr, is_tcp: bool) {
// discv5 checks to see if an update is necessary before performing it, so we do not
// need to check here
if self.discovery.update_local_enr_socket(socket, is_tcp) {
let enr = self.discovery.local_enr();
info!(
self.log,
"ENR Updated";
"enr" => enr.to_base64(),
"seq" => enr.seq(),
"address" => format!("{:?}", socket));
}
}
/// Return the nodes local ENR.
pub fn local_enr(&self) -> &Enr {
self.discovery.local_enr()
}
@@ -286,7 +304,7 @@ fn load_enr(
// Build the local ENR.
// Note: Discovery should update the ENR record's IP to the external IP as seen by the
// majority of our peers.
let mut local_enr = EnrBuilder::new()
let mut local_enr = EnrBuilder::new("v4")
.ip(config.discovery_address)
.tcp(config.libp2p_port)
.udp(config.discovery_port)
@@ -302,7 +320,7 @@ fn load_enr(
match Enr::from_str(&enr_string) {
Ok(enr) => {
if enr.node_id() == local_enr.node_id() {
if enr.ip() == config.discovery_address.into()
if enr.ip().map(Into::into) == Some(config.discovery_address)
&& enr.tcp() == Some(config.libp2p_port)
&& enr.udp() == Some(config.discovery_port)
{

View File

@@ -69,8 +69,8 @@ pub struct RPC<TSubstream> {
}
impl<TSubstream> RPC<TSubstream> {
pub fn new(log: &slog::Logger) -> Self {
let log = log.new(o!("Service" => "Libp2p-RPC"));
pub fn new(log: slog::Logger) -> Self {
let log = log.new(o!("service" => "libp2p_rpc"));
RPC {
events: Vec::new(),
marker: PhantomData,

View File

@@ -8,12 +8,8 @@ use crate::{Topic, TopicHash};
use futures::prelude::*;
use futures::Stream;
use libp2p::core::{
identity::Keypair,
multiaddr::Multiaddr,
muxing::StreamMuxerBox,
nodes::Substream,
identity::Keypair, multiaddr::Multiaddr, muxing::StreamMuxerBox, nodes::Substream,
transport::boxed::Boxed,
upgrade::{InboundUpgradeExt, OutboundUpgradeExt},
};
use libp2p::{core, secio, PeerId, Swarm, Transport};
use slog::{crit, debug, info, trace, warn};
@@ -34,6 +30,8 @@ pub struct Service {
pub swarm: Swarm<Libp2pStream, Libp2pBehaviour>,
/// This node's PeerId.
pub local_peer_id: PeerId,
/// Indicates if the listening address have been verified and compared to the expected ENR.
pub verified_listen_address: bool,
/// The libp2p logger handle.
pub log: slog::Logger,
}
@@ -155,6 +153,7 @@ impl Service {
Ok(Service {
local_peer_id,
swarm,
verified_listen_address: false,
log,
})
}
@@ -193,7 +192,18 @@ impl Stream for Service {
}
},
Ok(Async::Ready(None)) => unreachable!("Swarm stream shouldn't end"),
Ok(Async::NotReady) => break,
Ok(Async::NotReady) => {
// check to see if the address is different to the config. If so, update our ENR
if !self.verified_listen_address {
let multiaddr = Swarm::listeners(&self.swarm).next();
if let Some(multiaddr) = multiaddr {
if let Some(socket_addr) = multiaddr_to_socket_addr(multiaddr) {
self.swarm.update_local_enr_socket(socket_addr, true);
}
}
}
break;
}
_ => break,
}
}
@@ -201,12 +211,36 @@ impl Stream for Service {
}
}
/// Converts a multiaddr to a `SocketAddr` if the multiaddr has the TCP/IP form. Libp2p currently
/// only supports TCP, so the UDP case is currently ignored.
fn multiaddr_to_socket_addr(multiaddr: &Multiaddr) -> Option<std::net::SocketAddr> {
let protocols = multiaddr.iter().collect::<Vec<_>>();
// assume the IP protocol
match protocols[0] {
Protocol::Ip4(address) => {
if let Protocol::Tcp(port) = protocols[1] {
Some(std::net::SocketAddr::new(address.into(), port))
} else {
None
}
}
Protocol::Ip6(address) => {
if let Protocol::Tcp(port) = protocols[1] {
Some(std::net::SocketAddr::new(address.into(), port))
} else {
None
}
}
_ => None,
}
}
/// The implementation supports TCP/IP, WebSockets over TCP/IP, secio as the encryption layer, and
/// mplex or yamux as the multiplexing layer.
fn build_transport(local_private_key: Keypair) -> Boxed<(PeerId, StreamMuxerBox), Error> {
// TODO: The Wire protocol currently doesn't specify encryption and this will need to be customised
// in the future.
let transport = libp2p::tcp::TcpConfig::new();
let transport = libp2p::tcp::TcpConfig::new().nodelay(true);
let transport = libp2p::dns::DnsConfig::new(transport);
#[cfg(feature = "libp2p-websocket")]
let transport = {
@@ -214,22 +248,15 @@ fn build_transport(local_private_key: Keypair) -> Boxed<(PeerId, StreamMuxerBox)
transport.or_transport(websocket::WsConfig::new(trans_clone))
};
transport
.with_upgrade(secio::SecioConfig::new(local_private_key))
.and_then(move |out, endpoint| {
let peer_id = out.remote_key.into_peer_id();
let peer_id2 = peer_id.clone();
let upgrade = core::upgrade::SelectUpgrade::new(
.upgrade(core::upgrade::Version::V1)
.authenticate(secio::SecioConfig::new(local_private_key))
.multiplex(core::upgrade::SelectUpgrade::new(
libp2p::yamux::Config::default(),
libp2p::mplex::MplexConfig::new(),
)
// TODO: use a single `.map` instead of two maps
.map_inbound(move |muxer| (peer_id, muxer))
.map_outbound(move |muxer| (peer_id2, muxer));
core::upgrade::apply(out.stream, upgrade, endpoint)
.map(|(id, muxer)| (id, core::muxing::StreamMuxerBox::new(muxer)))
})
.with_timeout(Duration::from_secs(20))
))
.map(|(peer, muxer), _| (peer, core::muxing::StreamMuxerBox::new(muxer)))
.timeout(Duration::from_secs(20))
.timeout(Duration::from_secs(20))
.map_err(|err| Error::new(ErrorKind::Other, err))
.boxed()
}

View File

@@ -0,0 +1,28 @@
[package]
name = "genesis"
version = "0.1.0"
authors = ["Paul Hauner <paul@paulhauner.com>"]
edition = "2018"
[dev-dependencies]
eth1_test_rig = { path = "../../tests/eth1_test_rig" }
futures = "0.1.25"
[dependencies]
futures = "0.1.25"
types = { path = "../../eth2/types"}
environment = { path = "../../lighthouse/environment"}
eth1 = { path = "../eth1"}
rayon = "1.0"
state_processing = { path = "../../eth2/state_processing" }
merkle_proof = { path = "../../eth2/utils/merkle_proof" }
eth2_ssz = "0.1"
eth2_hashing = { path = "../../eth2/utils/eth2_hashing" }
tree_hash = "0.1"
tokio = "0.1.17"
parking_lot = "0.7"
slog = "^2.2.3"
exit-future = "0.1.4"
serde = "1.0"
serde_derive = "1.0"
int_to_bytes = { path = "../../eth2/utils/int_to_bytes" }

View File

@@ -0,0 +1,44 @@
use int_to_bytes::int_to_bytes32;
use merkle_proof::MerkleTree;
use rayon::prelude::*;
use tree_hash::TreeHash;
use types::{ChainSpec, Deposit, DepositData, Hash256};
/// Accepts the genesis block validator `DepositData` list and produces a list of `Deposit`, with
/// proofs.
pub fn genesis_deposits(
deposit_data: Vec<DepositData>,
spec: &ChainSpec,
) -> Result<Vec<Deposit>, String> {
let deposit_root_leaves = deposit_data
.par_iter()
.map(|data| Hash256::from_slice(&data.tree_hash_root()))
.collect::<Vec<_>>();
let mut proofs = vec![];
let depth = spec.deposit_contract_tree_depth as usize;
let mut tree = MerkleTree::create(&[], depth);
for (i, deposit_leaf) in deposit_root_leaves.iter().enumerate() {
if let Err(_) = tree.push_leaf(*deposit_leaf, depth) {
return Err(String::from("Failed to push leaf"));
}
let (_, mut proof) = tree.generate_proof(i, depth);
proof.push(Hash256::from_slice(&int_to_bytes32((i + 1) as u64)));
assert_eq!(
proof.len(),
depth + 1,
"Deposit proof should be correct len"
);
proofs.push(proof);
}
Ok(deposit_data
.into_iter()
.zip(proofs.into_iter())
.map(|(data, proof)| (data, proof.into()))
.map(|(data, proof)| Deposit { proof, data })
.collect())
}

View File

@@ -0,0 +1,379 @@
pub use crate::{common::genesis_deposits, interop::interop_genesis_state};
pub use eth1::Config as Eth1Config;
use eth1::{DepositLog, Eth1Block, Service};
use futures::{
future,
future::{loop_fn, Loop},
Future,
};
use parking_lot::Mutex;
use slog::{debug, error, info, Logger};
use state_processing::{
initialize_beacon_state_from_eth1, is_valid_genesis_state,
per_block_processing::process_deposit, process_activations,
};
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::timer::Delay;
use types::{BeaconState, ChainSpec, Deposit, Eth1Data, EthSpec, Hash256};
/// Provides a service that connects to some Eth1 HTTP JSON-RPC endpoint and maintains a cache of eth1
/// blocks and deposits, listening for the eth1 block that triggers eth2 genesis and returning the
/// genesis `BeaconState`.
///
/// Is a wrapper around the `Service` struct of the `eth1` crate.
#[derive(Clone)]
pub struct Eth1GenesisService {
/// The underlying service. Access to this object is only required for testing and diagnosis.
pub core: Service,
/// The highest block number we've processed and determined it does not trigger genesis.
highest_processed_block: Arc<Mutex<Option<u64>>>,
/// Enabled when the genesis service should start downloading blocks.
///
/// It is disabled until there are enough deposit logs to start syncing.
sync_blocks: Arc<Mutex<bool>>,
}
impl Eth1GenesisService {
/// Creates a new service. Does not attempt to connect to the Eth1 node.
pub fn new(config: Eth1Config, log: Logger) -> Self {
Self {
core: Service::new(config, log),
highest_processed_block: Arc::new(Mutex::new(None)),
sync_blocks: Arc::new(Mutex::new(false)),
}
}
fn first_viable_eth1_block(&self, min_genesis_active_validator_count: usize) -> Option<u64> {
if self.core.deposit_cache_len() < min_genesis_active_validator_count {
None
} else {
self.core
.deposits()
.read()
.cache
.get(min_genesis_active_validator_count.saturating_sub(1))
.map(|log| log.block_number)
}
}
/// Returns a future that will keep updating the cache and resolve once it has discovered the
/// first Eth1 block that triggers an Eth2 genesis.
///
/// ## Returns
///
/// - `Ok(state)` once the canonical eth2 genesis state has been discovered.
/// - `Err(e)` if there is some internal error during updates.
pub fn wait_for_genesis_state<E: EthSpec>(
&self,
update_interval: Duration,
spec: ChainSpec,
) -> impl Future<Item = BeaconState<E>, Error = String> {
let service = self.clone();
loop_fn::<(ChainSpec, Option<BeaconState<E>>), _, _, _>(
(spec, None),
move |(spec, state)| {
let service_1 = service.clone();
let service_2 = service.clone();
let service_3 = service.clone();
let service_4 = service.clone();
let log = service.core.log.clone();
let min_genesis_active_validator_count = spec.min_genesis_active_validator_count;
Delay::new(Instant::now() + update_interval)
.map_err(|e| format!("Delay between genesis deposit checks failed: {:?}", e))
.and_then(move |()| {
service_1
.core
.update_deposit_cache()
.map_err(|e| format!("{:?}", e))
})
.then(move |update_result| {
if let Err(e) = update_result {
error!(
log,
"Failed to update eth1 deposit cache";
"error" => e
)
}
// Do not exit the loop if there is an error whilst updating.
Ok(())
})
// Only enable the `sync_blocks` flag if there are enough deposits to feasibly
// trigger genesis.
//
// Note: genesis is triggered by the _active_ validator count, not just the
// deposit count, so it's possible that block downloads are started too early.
// This is just wasteful, not erroneous.
.and_then(move |()| {
let mut sync_blocks = service_2.sync_blocks.lock();
if !(*sync_blocks) {
if let Some(viable_eth1_block) = service_2.first_viable_eth1_block(
min_genesis_active_validator_count as usize,
) {
info!(
service_2.core.log,
"Minimum genesis deposit count met";
"deposit_count" => min_genesis_active_validator_count,
"block_number" => viable_eth1_block,
);
service_2.core.set_lowest_cached_block(viable_eth1_block);
*sync_blocks = true
}
}
Ok(*sync_blocks)
})
.and_then(move |should_update_block_cache| {
let maybe_update_future: Box<dyn Future<Item = _, Error = _> + Send> =
if should_update_block_cache {
Box::new(service_3.core.update_block_cache().then(
move |update_result| {
if let Err(e) = update_result {
error!(
service_3.core.log,
"Failed to update eth1 block cache";
"error" => format!("{:?}", e)
);
}
// Do not exit the loop if there is an error whilst updating.
Ok(())
},
))
} else {
Box::new(future::ok(()))
};
maybe_update_future
})
.and_then(move |()| {
if let Some(genesis_state) = service_4
.scan_new_blocks::<E>(&spec)
.map_err(|e| format!("Failed to scan for new blocks: {}", e))?
{
Ok(Loop::Break((spec, genesis_state)))
} else {
debug!(
service_4.core.log,
"No eth1 genesis block found";
"cached_blocks" => service_4.core.block_cache_len(),
"cached_deposits" => service_4.core.deposit_cache_len(),
"cache_head" => service_4.highest_known_block(),
);
Ok(Loop::Continue((spec, state)))
}
})
},
)
.map(|(_spec, state)| state)
}
/// Processes any new blocks that have appeared since this function was last run.
///
/// A `highest_processed_block` value is stored in `self`. This function will find any blocks
/// in it's caches that have a higher block number than `highest_processed_block` and check to
/// see if they would trigger an Eth2 genesis.
///
/// Blocks are always tested in increasing order, starting with the lowest unknown block
/// number in the cache.
///
/// ## Returns
///
/// - `Ok(Some(eth1_block))` if a previously-unprocessed block would trigger Eth2 genesis.
/// - `Ok(None)` if none of the new blocks would trigger genesis, or there were no new blocks.
/// - `Err(_)` if there was some internal error.
fn scan_new_blocks<E: EthSpec>(
&self,
spec: &ChainSpec,
) -> Result<Option<BeaconState<E>>, String> {
let genesis_trigger_eth1_block = self
.core
.blocks()
.read()
.iter()
// It's only worth scanning blocks that have timestamps _after_ genesis time. It's
// impossible for any other block to trigger genesis.
.filter(|block| block.timestamp >= spec.min_genesis_time)
// The block cache might be more recently updated than deposit cache. Restrict any
// block numbers that are not known by all caches.
.filter(|block| {
self.highest_known_block()
.map(|n| block.number <= n)
.unwrap_or_else(|| false)
})
.find(|block| {
let mut highest_processed_block = self.highest_processed_block.lock();
let next_new_block_number =
highest_processed_block.map(|n| n + 1).unwrap_or_else(|| 0);
if block.number < next_new_block_number {
return false;
}
self.is_valid_genesis_eth1_block::<E>(block, &spec)
.and_then(|val| {
*highest_processed_block = Some(block.number);
Ok(val)
})
.unwrap_or_else(|_| {
error!(
self.core.log,
"Failed to detect if eth1 block triggers genesis";
"eth1_block_number" => block.number,
"eth1_block_hash" => format!("{}", block.hash),
);
false
})
})
.cloned();
if let Some(eth1_block) = genesis_trigger_eth1_block {
debug!(
self.core.log,
"All genesis conditions met";
"eth1_block_height" => eth1_block.number,
);
let genesis_state = self
.genesis_from_eth1_block(eth1_block.clone(), &spec)
.map_err(|e| format!("Failed to generate valid genesis state : {}", e))?;
info!(
self.core.log,
"Deposit contract genesis complete";
"eth1_block_height" => eth1_block.number,
"validator_count" => genesis_state.validators.len(),
);
Ok(Some(genesis_state))
} else {
Ok(None)
}
}
/// Produces an eth2 genesis `BeaconState` from the given `eth1_block`.
///
/// ## Returns
///
/// - Ok(genesis_state) if all went well.
/// - Err(e) if the given `eth1_block` was not a viable block to trigger genesis or there was
/// an internal error.
fn genesis_from_eth1_block<E: EthSpec>(
&self,
eth1_block: Eth1Block,
spec: &ChainSpec,
) -> Result<BeaconState<E>, String> {
let deposit_logs = self
.core
.deposits()
.read()
.cache
.iter()
.take_while(|log| log.block_number <= eth1_block.number)
.map(|log| log.deposit_data.clone())
.collect::<Vec<_>>();
let genesis_state = initialize_beacon_state_from_eth1(
eth1_block.hash,
eth1_block.timestamp,
genesis_deposits(deposit_logs, &spec)?,
&spec,
)
.map_err(|e| format!("Unable to initialize genesis state: {:?}", e))?;
if is_valid_genesis_state(&genesis_state, &spec) {
Ok(genesis_state)
} else {
Err("Generated state was not valid.".to_string())
}
}
/// A cheap (compared to using `initialize_beacon_state_from_eth1) method for determining if some
/// `target_block` will trigger genesis.
fn is_valid_genesis_eth1_block<E: EthSpec>(
&self,
target_block: &Eth1Block,
spec: &ChainSpec,
) -> Result<bool, String> {
if target_block.timestamp < spec.min_genesis_time {
Ok(false)
} else {
let mut local_state: BeaconState<E> = BeaconState::new(
0,
Eth1Data {
block_hash: Hash256::zero(),
deposit_root: Hash256::zero(),
deposit_count: 0,
},
&spec,
);
local_state.genesis_time = target_block.timestamp;
self.deposit_logs_at_block(target_block.number)
.iter()
// TODO: add the signature field back.
//.filter(|deposit_log| deposit_log.signature_is_valid)
.map(|deposit_log| Deposit {
proof: vec![Hash256::zero(); spec.deposit_contract_tree_depth as usize].into(),
data: deposit_log.deposit_data.clone(),
})
.try_for_each(|deposit| {
// No need to verify proofs in order to test if some block will trigger genesis.
const PROOF_VERIFICATION: bool = false;
// Note: presently all the signatures are verified each time this function is
// run.
//
// It would be more efficient to pre-verify signatures, filter out the invalid
// ones and disable verification for `process_deposit`.
//
// This is only more efficient in scenarios where `min_genesis_time` occurs
// _before_ `min_validator_count` is met. We're unlikely to see this scenario
// in testnets (`min_genesis_time` is usually `0`) and I'm not certain it will
// happen for the real, production deposit contract.
process_deposit(&mut local_state, &deposit, spec, PROOF_VERIFICATION)
.map_err(|e| format!("Error whilst processing deposit: {:?}", e))
})?;
process_activations(&mut local_state, spec);
Ok(is_valid_genesis_state(&local_state, spec))
}
}
/// Returns the `block_number` of the highest (by block number) block in the cache.
///
/// Takes the lower block number of the deposit and block caches to ensure this number is safe.
fn highest_known_block(&self) -> Option<u64> {
let block_cache = self.core.blocks().read().highest_block_number()?;
let deposit_cache = self.core.deposits().read().last_processed_block?;
Some(std::cmp::min(block_cache, deposit_cache))
}
/// Returns all deposit logs included in `block_number` and all prior blocks.
fn deposit_logs_at_block(&self, block_number: u64) -> Vec<DepositLog> {
self.core
.deposits()
.read()
.cache
.iter()
.take_while(|log| log.block_number <= block_number)
.cloned()
.collect()
}
/// Returns the `Service` contained in `self`.
pub fn into_core_service(self) -> Service {
self.core
}
}

View File

@@ -0,0 +1,142 @@
use crate::common::genesis_deposits;
use eth2_hashing::hash;
use rayon::prelude::*;
use ssz::Encode;
use state_processing::initialize_beacon_state_from_eth1;
use std::time::SystemTime;
use tree_hash::SignedRoot;
use types::{
BeaconState, ChainSpec, DepositData, Domain, EthSpec, Fork, Hash256, Keypair, PublicKey,
Signature,
};
/// Builds a genesis state as defined by the Eth2 interop procedure (see below).
///
/// Reference:
/// https://github.com/ethereum/eth2.0-pm/tree/6e41fcf383ebeb5125938850d8e9b4e9888389b4/interop/mocked_start
pub fn interop_genesis_state<T: EthSpec>(
keypairs: &[Keypair],
genesis_time: u64,
spec: &ChainSpec,
) -> Result<BeaconState<T>, String> {
let eth1_block_hash = Hash256::from_slice(&[0x42; 32]);
let eth1_timestamp = 2_u64.pow(40);
let amount = spec.max_effective_balance;
let withdrawal_credentials = |pubkey: &PublicKey| {
let mut credentials = hash(&pubkey.as_ssz_bytes());
credentials[0] = spec.bls_withdrawal_prefix_byte;
Hash256::from_slice(&credentials)
};
let datas = keypairs
.into_par_iter()
.map(|keypair| {
let mut data = DepositData {
withdrawal_credentials: withdrawal_credentials(&keypair.pk),
pubkey: keypair.pk.clone().into(),
amount,
signature: Signature::empty_signature().into(),
};
let domain = spec.get_domain(
spec.genesis_slot.epoch(T::slots_per_epoch()),
Domain::Deposit,
&Fork::default(),
);
data.signature = Signature::new(&data.signed_root()[..], domain, &keypair.sk).into();
data
})
.collect::<Vec<_>>();
let mut state = initialize_beacon_state_from_eth1(
eth1_block_hash,
eth1_timestamp,
genesis_deposits(datas, spec)?,
spec,
)
.map_err(|e| format!("Unable to initialize genesis state: {:?}", e))?;
state.genesis_time = genesis_time;
// Invalid all the caches after all the manual state surgery.
state.drop_all_caches();
Ok(state)
}
/// Returns the system time, mod 30 minutes.
///
/// Used for easily creating testnets.
pub fn recent_genesis_time(minutes: u64) -> u64 {
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs();
let secs_after_last_period = now.checked_rem(minutes * 60).unwrap_or(0);
now - secs_after_last_period
}
#[cfg(test)]
mod test {
use super::*;
use types::{test_utils::generate_deterministic_keypairs, EthSpec, MinimalEthSpec};
type TestEthSpec = MinimalEthSpec;
#[test]
fn interop_state() {
let validator_count = 16;
let genesis_time = 42;
let spec = &TestEthSpec::default_spec();
let keypairs = generate_deterministic_keypairs(validator_count);
let state = interop_genesis_state::<TestEthSpec>(&keypairs, genesis_time, spec)
.expect("should build state");
assert_eq!(
state.eth1_data.block_hash,
Hash256::from_slice(&[0x42; 32]),
"eth1 block hash should be co-ordinated junk"
);
assert_eq!(
state.genesis_time, genesis_time,
"genesis time should be as specified"
);
for b in &state.balances {
assert_eq!(
*b, spec.max_effective_balance,
"validator balances should be max effective balance"
);
}
for v in &state.validators {
let creds = v.withdrawal_credentials.as_bytes();
assert_eq!(
creds[0], spec.bls_withdrawal_prefix_byte,
"first byte of withdrawal creds should be bls prefix"
);
assert_eq!(
&creds[1..],
&hash(&v.pubkey.as_ssz_bytes())[1..],
"rest of withdrawal creds should be pubkey hash"
)
}
assert_eq!(
state.balances.len(),
validator_count,
"validator balances len should be correct"
);
assert_eq!(
state.validators.len(),
validator_count,
"validator count should be correct"
);
}
}

View File

@@ -0,0 +1,31 @@
mod common;
mod eth1_genesis_service;
mod interop;
pub use eth1::Config as Eth1Config;
pub use eth1_genesis_service::Eth1GenesisService;
pub use interop::{interop_genesis_state, recent_genesis_time};
pub use types::test_utils::generate_deterministic_keypairs;
use ssz::Decode;
use std::fs::File;
use std::io::prelude::*;
use std::path::PathBuf;
use types::{BeaconState, EthSpec};
/// Load a `BeaconState` from the given `path`. The file should contain raw SSZ bytes (i.e., no
/// ASCII encoding or schema).
pub fn state_from_ssz_file<E: EthSpec>(path: PathBuf) -> Result<BeaconState<E>, String> {
File::open(path.clone())
.map_err(move |e| format!("Unable to open SSZ genesis state file {:?}: {:?}", path, e))
.and_then(|mut file| {
let mut bytes = vec![];
file.read_to_end(&mut bytes)
.map_err(|e| format!("Failed to read SSZ file: {:?}", e))?;
Ok(bytes)
})
.and_then(|bytes| {
BeaconState::from_ssz_bytes(&bytes)
.map_err(|e| format!("Unable to parse SSZ genesis state file: {:?}", e))
})
}

View File

@@ -0,0 +1,105 @@
//! NOTE: These tests will not pass unless ganache-cli is running on `ENDPOINT` (see below).
//!
//! You can start a suitable instance using the `ganache_test_node.sh` script in the `scripts`
//! dir in the root of the `lighthouse` repo.
#![cfg(test)]
use environment::{Environment, EnvironmentBuilder};
use eth1_test_rig::{DelayThenDeposit, GanacheEth1Instance};
use futures::Future;
use genesis::{Eth1Config, Eth1GenesisService};
use state_processing::is_valid_genesis_state;
use std::time::Duration;
use types::{test_utils::generate_deterministic_keypair, Hash256, MinimalEthSpec};
pub fn new_env() -> Environment<MinimalEthSpec> {
EnvironmentBuilder::minimal()
.single_thread_tokio_runtime()
.expect("should start tokio runtime")
.null_logger()
.expect("should start null logger")
.build()
.expect("should build env")
}
#[test]
fn basic() {
let mut env = new_env();
let log = env.core_context().log;
let mut spec = env.eth2_config().spec.clone();
let runtime = env.runtime();
let eth1 = runtime
.block_on(GanacheEth1Instance::new())
.expect("should start eth1 environment");
let deposit_contract = &eth1.deposit_contract;
let web3 = eth1.web3();
let now = runtime
.block_on(web3.eth().block_number().map(|v| v.as_u64()))
.expect("should get block number");
let service = Eth1GenesisService::new(
Eth1Config {
endpoint: eth1.endpoint(),
deposit_contract_address: deposit_contract.address(),
deposit_contract_deploy_block: now,
lowest_cached_block_number: now,
follow_distance: 0,
block_cache_truncation: None,
..Eth1Config::default()
},
log,
);
// NOTE: this test is sensitive to the response speed of the external web3 server. If
// you're experiencing failures, try increasing the update_interval.
let update_interval = Duration::from_millis(500);
spec.min_genesis_time = 0;
spec.min_genesis_active_validator_count = 8;
let deposits = (0..spec.min_genesis_active_validator_count + 2)
.into_iter()
.map(|i| {
deposit_contract.deposit_helper::<MinimalEthSpec>(
generate_deterministic_keypair(i as usize),
Hash256::from_low_u64_le(i),
32_000_000_000,
)
})
.map(|deposit| DelayThenDeposit {
delay: Duration::from_secs(0),
deposit,
})
.collect::<Vec<_>>();
let deposit_future = deposit_contract.deposit_multiple(deposits.clone());
let wait_future =
service.wait_for_genesis_state::<MinimalEthSpec>(update_interval, spec.clone());
let state = runtime
.block_on(deposit_future.join(wait_future))
.map(|(_, state)| state)
.expect("should finish waiting for genesis");
// Note: using ganache these deposits are 1-per-block, therefore we know there should only be
// the minimum number of validators.
assert_eq!(
state.validators.len(),
spec.min_genesis_active_validator_count as usize,
"should have expected validator count"
);
assert!(state.genesis_time > 0, "should have some genesis time");
assert!(
is_valid_genesis_state(&state, &spec),
"should be valid genesis state"
);
assert!(
is_valid_genesis_state(&state, &spec),
"should be valid genesis state"
);
}

View File

@@ -5,21 +5,21 @@ authors = ["Age Manning <Age@AgeManning.com>"]
edition = "2018"
[dev-dependencies]
sloggers = "0.3.2"
sloggers = "0.3.4"
[dependencies]
beacon_chain = { path = "../beacon_chain" }
store = { path = "../store" }
eth2-libp2p = { path = "../eth2-libp2p" }
types = { path = "../../eth2/types" }
slog = { version = "^2.2.3" , features = ["max_level_trace"] }
slog = { version = "2.5.2", features = ["max_level_trace"] }
hex = "0.3"
eth2_ssz = "0.1"
tree_hash = "0.1"
futures = "0.1.25"
error-chain = "0.12.0"
tokio = "0.1.16"
eth2_ssz = "0.1.2"
tree_hash = "0.1.0"
futures = "0.1.29"
error-chain = "0.12.1"
tokio = "0.1.22"
parking_lot = "0.9.0"
smallvec = "0.6.10"
smallvec = "0.6.11"
state_processing = { path = "../../eth2/state_processing" }
bls = { path = "../../eth2/utils/bls" }

View File

@@ -51,7 +51,7 @@ impl<T: BeaconChainTypes + 'static> MessageHandler<T> {
executor: &tokio::runtime::TaskExecutor,
log: slog::Logger,
) -> error::Result<mpsc::UnboundedSender<HandlerMessage>> {
let message_handler_log = log.new(o!("Service"=> "Message Handler"));
let message_handler_log = log.new(o!("service"=> "msg_handler"));
trace!(message_handler_log, "Service starting");
let (handler_send, handler_recv) = mpsc::unbounded_channel();
@@ -146,9 +146,15 @@ impl<T: BeaconChainTypes + 'static> MessageHandler<T> {
) {
// an error could have occurred.
match error_response {
RPCErrorResponse::InvalidRequest(error) => warn!(self.log, "Peer indicated invalid request";"peer_id" => format!("{:?}", peer_id), "error" => error.as_string()),
RPCErrorResponse::ServerError(error) => warn!(self.log, "Peer internal server error";"peer_id" => format!("{:?}", peer_id), "error" => error.as_string()),
RPCErrorResponse::Unknown(error) => warn!(self.log, "Unknown peer error";"peer" => format!("{:?}", peer_id), "error" => error.as_string()),
RPCErrorResponse::InvalidRequest(error) => {
warn!(self.log, "Peer indicated invalid request";"peer_id" => format!("{:?}", peer_id), "error" => error.as_string())
}
RPCErrorResponse::ServerError(error) => {
warn!(self.log, "Peer internal server error";"peer_id" => format!("{:?}", peer_id), "error" => error.as_string())
}
RPCErrorResponse::Unknown(error) => {
warn!(self.log, "Unknown peer error";"peer" => format!("{:?}", peer_id), "error" => error.as_string())
}
RPCErrorResponse::Success(response) => {
match response {
RPCResponse::Hello(hello_message) => {

View File

@@ -10,7 +10,7 @@ use eth2_libp2p::{PubsubMessage, RPCEvent};
use futures::prelude::*;
use futures::Stream;
use parking_lot::Mutex;
use slog::{debug, info, o, trace};
use slog::{debug, info, trace};
use std::sync::Arc;
use tokio::runtime::TaskExecutor;
use tokio::sync::{mpsc, oneshot};
@@ -29,15 +29,18 @@ impl<T: BeaconChainTypes + 'static> Service<T> {
beacon_chain: Arc<BeaconChain<T>>,
config: &NetworkConfig,
executor: &TaskExecutor,
log: slog::Logger,
network_log: slog::Logger,
) -> error::Result<(Arc<Self>, mpsc::UnboundedSender<NetworkMessage>)> {
// build the network channel
let (network_send, network_recv) = mpsc::unbounded_channel::<NetworkMessage>();
// launch message handler thread
let message_handler_send =
MessageHandler::spawn(beacon_chain, network_send.clone(), executor, log.clone())?;
let message_handler_send = MessageHandler::spawn(
beacon_chain,
network_send.clone(),
executor,
network_log.clone(),
)?;
let network_log = log.new(o!("Service" => "Network"));
// launch libp2p service
let libp2p_service = Arc::new(Mutex::new(LibP2PService::new(
config.clone(),

View File

@@ -74,7 +74,7 @@ impl<T: BeaconChainTypes> MessageProcessor<T> {
network_send: mpsc::UnboundedSender<NetworkMessage>,
log: &slog::Logger,
) -> Self {
let sync_logger = log.new(o!("Service"=> "Sync"));
let sync_logger = log.new(o!("service"=> "sync"));
let sync_network_context = NetworkContext::new(network_send.clone(), sync_logger.clone());
// spawn the sync thread

View File

@@ -12,28 +12,28 @@ network = { path = "../network" }
eth2-libp2p = { path = "../eth2-libp2p" }
store = { path = "../store" }
version = { path = "../version" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "^1.0"
serde_yaml = "0.8"
slog = "^2.2.3"
slog-term = "^2.4.0"
slog-async = "^2.3.0"
eth2_ssz = { path = "../../eth2/utils/ssz" }
eth2_ssz_derive = { path = "../../eth2/utils/ssz_derive" }
serde = { version = "1.0.102", features = ["derive"] }
serde_json = "1.0.41"
serde_yaml = "0.8.11"
slog = "2.5.2"
slog-term = "2.4.2"
slog-async = "2.3.0"
eth2_ssz = "0.1.2"
eth2_ssz_derive = "0.1.0"
state_processing = { path = "../../eth2/state_processing" }
types = { path = "../../eth2/types" }
clap = "2.32.0"
http = "^0.1.17"
prometheus = { version = "^0.6", features = ["process"] }
hyper = "0.12.34"
exit-future = "0.1.3"
tokio = "0.1.17"
url = "2.0"
lazy_static = "1.3.0"
clap = "2.33.0"
http = "0.1.19"
prometheus = { version = "0.7.0", features = ["process"] }
hyper = "0.12.35"
exit-future = "0.1.4"
tokio = "0.1.22"
url = "2.1.0"
lazy_static = "1.4.0"
eth2_config = { path = "../../eth2/utils/eth2_config" }
lighthouse_metrics = { path = "../../eth2/utils/lighthouse_metrics" }
slot_clock = { path = "../../eth2/utils/slot_clock" }
hex = "0.3.2"
parking_lot = "0.9"
futures = "0.1.25"
hex = "0.3"
parking_lot = "0.9.0"
futures = "0.1.29"

View File

@@ -26,7 +26,8 @@ use hyper::rt::Future;
use hyper::service::Service;
use hyper::{Body, Method, Request, Response, Server};
use parking_lot::RwLock;
use slog::{info, o, warn};
use slog::{info, warn};
use std::net::SocketAddr;
use std::ops::Deref;
use std::path::PathBuf;
use std::sync::Arc;
@@ -35,7 +36,7 @@ use tokio::sync::mpsc;
use url_query::UrlQuery;
pub use beacon::{BlockResponse, HeadResponse, StateResponse};
pub use config::Config as ApiConfig;
pub use config::Config;
type BoxFut = Box<dyn Future<Item = Response<Body>, Error = ApiError> + Send>;
@@ -196,16 +197,14 @@ impl<T: BeaconChainTypes> Service for ApiService<T> {
}
pub fn start_server<T: BeaconChainTypes>(
config: &ApiConfig,
config: &Config,
executor: &TaskExecutor,
beacon_chain: Arc<BeaconChain<T>>,
network_info: NetworkInfo<T>,
db_path: PathBuf,
eth2_config: Eth2Config,
log: &slog::Logger,
) -> Result<exit_future::Signal, hyper::Error> {
let log = log.new(o!("Service" => "Api"));
log: slog::Logger,
) -> Result<(exit_future::Signal, SocketAddr), hyper::Error> {
// build a channel to kill the HTTP server
let (exit_signal, exit) = exit_future::signal();
@@ -237,8 +236,11 @@ pub fn start_server<T: BeaconChainTypes>(
};
let log_clone = log.clone();
let server = Server::bind(&bind_addr)
.serve(service)
let server = Server::bind(&bind_addr).serve(service);
let actual_listen_addr = server.local_addr();
let server_future = server
.with_graceful_shutdown(server_exit)
.map_err(move |e| {
warn!(
@@ -250,13 +252,13 @@ pub fn start_server<T: BeaconChainTypes>(
info!(
log,
"REST API started";
"address" => format!("{}", config.listen_address),
"port" => config.port,
"address" => format!("{}", actual_listen_addr.ip()),
"port" => actual_listen_addr.port(),
);
executor.spawn(server);
executor.spawn(server_future);
Ok(exit_signal)
Ok((exit_signal, actual_listen_addr))
}
#[derive(Clone)]

View File

@@ -17,7 +17,7 @@ use std::sync::Arc;
use tokio;
use tokio::sync::mpsc;
use types::beacon_state::EthSpec;
use types::{Attestation, BeaconBlock, BitList, Epoch, RelativeEpoch, Shard, Slot};
use types::{Attestation, BeaconBlock, BitList, CommitteeIndex, Epoch, RelativeEpoch, Slot};
#[derive(Debug, Serialize, Deserialize)]
pub struct ValidatorDuty {
@@ -25,8 +25,8 @@ pub struct ValidatorDuty {
pub validator_pubkey: String,
/// The slot at which the validator must attest.
pub attestation_slot: Option<Slot>,
/// The shard in which the validator must attest.
pub attestation_shard: Option<Shard>,
/// The index of the committee within `slot` of which the validator is a member.
pub attestation_committee_index: Option<CommitteeIndex>,
/// The slot in which a validator must propose a block, or `null` if block production is not required.
pub block_proposal_slot: Option<Slot>,
}
@@ -36,7 +36,7 @@ impl ValidatorDuty {
ValidatorDuty {
validator_pubkey: "".to_string(),
attestation_slot: None,
attestation_shard: None,
attestation_committee_index: None,
block_proposal_slot: None,
}
}
@@ -90,7 +90,7 @@ pub fn get_validator_duties<T: BeaconChainTypes + 'static>(req: Request<Body>) -
.slot_iter(T::EthSpec::slots_per_epoch())
.map(|slot| {
head_state
.get_beacon_proposer_index(slot, relative_epoch, &beacon_chain.spec)
.get_beacon_proposer_index(slot, &beacon_chain.spec)
.map_err(|e| {
ApiError::ServerError(format!(
"Unable to get proposer index for validator: {:?}",
@@ -125,7 +125,7 @@ pub fn get_validator_duties<T: BeaconChainTypes + 'static>(req: Request<Body>) -
match head_state.get_attestation_duties(val_index, relative_epoch) {
Ok(Some(d)) => {
duty.attestation_slot = Some(d.slot);
duty.attestation_shard = Some(d.shard);
duty.attestation_committee_index = Some(d.index);
}
Ok(None) => {}
Err(e) => {
@@ -311,7 +311,7 @@ pub fn get_new_attestation<T: BeaconChainTypes + 'static>(req: Request<Body>) ->
let mut aggregation_bits = BitList::with_capacity(val_duty.committee_len)
.expect("An empty BitList should always be created, or we have bigger problems.");
aggregation_bits
.set(val_duty.committee_index, poc_bit)
.set(val_duty.committee_position, poc_bit)
.map_err(|e| {
ApiError::ServerError(format!(
"Unable to set aggregation bits for the attestation: {:?}",
@@ -334,21 +334,19 @@ pub fn get_new_attestation<T: BeaconChainTypes + 'static>(req: Request<Body>) ->
return Err(ApiError::BadRequest(format!("Attestation data can only be requested for the current slot ({:?}), not your requested slot ({:?})", current_slot, requested_slot)));
}
let shard = query
.first_of(&["shard"])
let index = query
.first_of(&["index"])
.map(|(_key, value)| value)?
.parse::<u64>()
.map_err(|e| ApiError::BadRequest(format!("Shard is not a valid u64 value: {:?}", e)))?;
.map_err(|e| ApiError::BadRequest(format!("Index is not a valid u64 value: {:?}", e)))?;
let attestation_data = beacon_chain
.produce_attestation_data(shard, current_slot.into())
.produce_attestation_data(current_slot.into(), index)
.map_err(|e| ApiError::ServerError(format!("Could not produce an attestation: {:?}", e)))?;
let attestation: Attestation<T::EthSpec> = Attestation {
aggregation_bits,
data: attestation_data,
custody_bits: BitList::with_capacity(val_duty.committee_len)
.expect("Should be able to create an empty BitList for the custody bits."),
signature: AggregateSignature::new(),
};

View File

@@ -11,13 +11,13 @@ network = { path = "../network" }
eth2-libp2p = { path = "../eth2-libp2p" }
version = { path = "../version" }
types = { path = "../../eth2/types" }
eth2_ssz = "0.1"
eth2_ssz = "0.1.2"
protos = { path = "../../protos" }
grpcio = { version = "0.4", default-features = false, features = ["protobuf-codec"] }
clap = "2.32.0"
futures = "0.1.23"
serde = "1.0"
serde_derive = "1.0"
slog = { version = "^2.2.3" , features = ["max_level_trace"] }
tokio = "0.1.17"
grpcio = { version = "0.4.6", default-features = false, features = ["protobuf-codec"] }
clap = "2.33.0"
futures = "0.1.29"
serde = "1.0.102"
serde_derive = "1.0.102"
slog = { version = "2.5.2", features = ["max_level_trace"] }
tokio = "0.1.22"
exit-future = "0.1.4"

View File

@@ -16,13 +16,23 @@ use std::sync::Arc;
use tokio::sync::mpsc;
use types::{Attestation, Slot};
#[derive(Clone)]
pub struct AttestationServiceInstance<T: BeaconChainTypes> {
pub chain: Arc<BeaconChain<T>>,
pub network_chan: mpsc::UnboundedSender<NetworkMessage>,
pub log: slog::Logger,
}
// NOTE: Deriving Clone puts bogus bounds on T, so we implement it manually.
impl<T: BeaconChainTypes> Clone for AttestationServiceInstance<T> {
fn clone(&self) -> Self {
Self {
chain: self.chain.clone(),
network_chan: self.network_chan.clone(),
log: self.log.clone(),
}
}
}
impl<T: BeaconChainTypes> AttestationService for AttestationServiceInstance<T> {
/// Produce the `AttestationData` for signing by a validator.
fn produce_attestation_data(
@@ -38,11 +48,12 @@ impl<T: BeaconChainTypes> AttestationService for AttestationServiceInstance<T> {
);
// Then get the AttestationData from the beacon chain
// NOTE(v0.9): shard is incorrectly named, all this should be deleted
let shard = req.get_shard();
let slot_requested = req.get_slot();
let attestation_data = match self
.chain
.produce_attestation_data(shard, Slot::from(slot_requested))
.produce_attestation_data(Slot::from(slot_requested), shard)
{
Ok(v) => v,
Err(e) => {
@@ -105,7 +116,7 @@ impl<T: BeaconChainTypes> AttestationService for AttestationServiceInstance<T> {
self.log,
"Valid attestation from RPC";
"target_epoch" => attestation.data.target.epoch,
"shard" => attestation.data.crosslink.shard,
"index" => attestation.data.index,
);
// valid attestation, propagate to the network

View File

@@ -16,13 +16,23 @@ use std::sync::Arc;
use tokio::sync::mpsc;
use types::{BeaconBlock, Signature, Slot};
#[derive(Clone)]
pub struct BeaconBlockServiceInstance<T: BeaconChainTypes> {
pub chain: Arc<BeaconChain<T>>,
pub network_chan: mpsc::UnboundedSender<NetworkMessage>,
pub log: Logger,
}
// NOTE: Deriving Clone puts bogus bounds on T, so we implement it manually.
impl<T: BeaconChainTypes> Clone for BeaconBlockServiceInstance<T> {
fn clone(&self) -> Self {
Self {
chain: self.chain.clone(),
network_chan: self.network_chan.clone(),
log: self.log.clone(),
}
}
}
impl<T: BeaconChainTypes> BeaconBlockService for BeaconBlockServiceInstance<T> {
/// Produce a `BeaconBlock` for signing by a validator.
fn produce_beacon_block(

View File

@@ -6,12 +6,21 @@ use protos::services_grpc::BeaconNodeService;
use slog::{trace, warn};
use std::sync::Arc;
#[derive(Clone)]
pub struct BeaconNodeServiceInstance<T: BeaconChainTypes> {
pub chain: Arc<BeaconChain<T>>,
pub log: slog::Logger,
}
// NOTE: Deriving Clone puts bogus bounds on T, so we implement it manually.
impl<T: BeaconChainTypes> Clone for BeaconNodeServiceInstance<T> {
fn clone(&self) -> Self {
Self {
chain: self.chain.clone(),
log: self.log.clone(),
}
}
}
impl<T: BeaconChainTypes> BeaconNodeService for BeaconNodeServiceInstance<T> {
/// Provides basic node information.
fn info(&mut self, ctx: RpcContext, _req: Empty, sink: UnarySink<NodeInfoResponse>) {

View File

@@ -9,7 +9,7 @@ use self::beacon_block::BeaconBlockServiceInstance;
use self::beacon_node::BeaconNodeServiceInstance;
use self::validator::ValidatorServiceInstance;
use beacon_chain::{BeaconChain, BeaconChainTypes};
pub use config::Config as RPCConfig;
pub use config::Config;
use futures::Future;
use grpcio::{Environment, ServerBuilder};
use network::NetworkMessage;
@@ -17,19 +17,18 @@ use protos::services_grpc::{
create_attestation_service, create_beacon_block_service, create_beacon_node_service,
create_validator_service,
};
use slog::{info, o, warn};
use slog::{info, warn};
use std::sync::Arc;
use tokio::runtime::TaskExecutor;
use tokio::sync::mpsc;
pub fn start_server<T: BeaconChainTypes + Clone + 'static>(
config: &RPCConfig,
pub fn start_server<T: BeaconChainTypes>(
config: &Config,
executor: &TaskExecutor,
network_chan: mpsc::UnboundedSender<NetworkMessage>,
beacon_chain: Arc<BeaconChain<T>>,
log: &slog::Logger,
log: slog::Logger,
) -> exit_future::Signal {
let log = log.new(o!("Service"=>"RPC"));
let env = Arc::new(Environment::new(1));
// build a channel to kill the rpc server

View File

@@ -9,12 +9,21 @@ use ssz::Decode;
use std::sync::Arc;
use types::{Epoch, EthSpec, RelativeEpoch};
#[derive(Clone)]
pub struct ValidatorServiceInstance<T: BeaconChainTypes> {
pub chain: Arc<BeaconChain<T>>,
pub log: slog::Logger,
}
// NOTE: Deriving Clone puts bogus bounds on T, so we implement it manually.
impl<T: BeaconChainTypes> Clone for ValidatorServiceInstance<T> {
fn clone(&self) -> Self {
Self {
chain: self.chain.clone(),
log: self.log.clone(),
}
}
}
impl<T: BeaconChainTypes> ValidatorService for ValidatorServiceInstance<T> {
/// For a list of validator public keys, this function returns the slot at which each
/// validator must propose a block, attest to a shard, their shard committee and the shard they
@@ -57,9 +66,7 @@ impl<T: BeaconChainTypes> ValidatorService for ValidatorServiceInstance<T> {
let validator_proposers: Result<Vec<usize>, _> = epoch
.slot_iter(T::EthSpec::slots_per_epoch())
.map(|slot| {
state.get_beacon_proposer_index(slot, RelativeEpoch::Current, &self.chain.spec)
})
.map(|slot| state.get_beacon_proposer_index(slot, &self.chain.spec))
.collect();
let validator_proposers = match validator_proposers {
Ok(v) => v,
@@ -161,9 +168,9 @@ impl<T: BeaconChainTypes> ValidatorService for ValidatorServiceInstance<T> {
duty.set_none(false)
}
duty.set_committee_index(attestation_duties.committee_index as u64);
duty.set_committee_index(attestation_duties.committee_position as u64);
duty.set_attestation_slot(attestation_duties.slot.as_u64());
duty.set_attestation_shard(attestation_duties.shard);
duty.set_attestation_shard(attestation_duties.index);
duty.set_committee_len(attestation_duties.committee_len as u64);
active_validator.set_duty(duty);

View File

@@ -1,22 +1,9 @@
mod config;
mod run;
use clap::{App, Arg, SubCommand};
use config::get_configs;
use env_logger::{Builder, Env};
use slog::{crit, o, warn, Drain, Level};
pub const DEFAULT_DATA_DIR: &str = ".lighthouse";
pub const CLIENT_CONFIG_FILENAME: &str = "beacon-node.toml";
pub const ETH2_CONFIG_FILENAME: &str = "eth2-spec.toml";
pub const TESTNET_CONFIG_FILENAME: &str = "testnet.toml";
fn main() {
// debugging output for libp2p and external crates
Builder::from_env(Env::default()).init();
let matches = App::new("Lighthouse")
.version(version::version().as_str())
pub fn cli_app<'a, 'b>() -> App<'a, 'b> {
App::new("Beacon Node")
.visible_aliases(&["b", "bn", "beacon", "beacon_node"])
.version(crate_version!())
.author("Sigma Prime <contact@sigmaprime.io>")
.about("Eth 2.0 Client")
/*
@@ -30,13 +17,6 @@ fn main() {
.takes_value(true)
.global(true)
)
.arg(
Arg::with_name("logfile")
.long("logfile")
.value_name("FILE")
.help("File path where output will be written.")
.takes_value(true),
)
.arg(
Arg::with_name("network-dir")
.long("network-dir")
@@ -197,35 +177,44 @@ fn main() {
* Eth1 Integration
*/
.arg(
Arg::with_name("eth1-server")
.long("eth1-server")
.value_name("SERVER")
Arg::with_name("dummy-eth1")
.long("dummy-eth1")
.help("If present, uses an eth1 backend that generates static dummy data.\
Identical to the method used at the 2019 Canada interop.")
)
.arg(
Arg::with_name("eth1-endpoint")
.long("eth1-endpoint")
.value_name("HTTP-ENDPOINT")
.help("Specifies the server for a web3 connection to the Eth1 chain.")
.takes_value(true)
.default_value("http://localhost:8545")
)
/*
* Database parameters.
*/
.arg(
Arg::with_name("db")
.long("db")
.value_name("DB")
.help("Type of database to use.")
Arg::with_name("eth1-follow")
.long("eth1-follow")
.value_name("BLOCK_COUNT")
.help("Specifies how many blocks we should cache behind the eth1 head. A larger number means a smaller cache.")
.takes_value(true)
.possible_values(&["disk", "memory"])
.default_value("disk"),
// TODO: set this higher once we're not using testnets all the time.
.default_value("0")
)
/*
* Logging.
*/
.arg(
Arg::with_name("debug-level")
.long("debug-level")
.value_name("LEVEL")
.help("The title of the spec constants for chain config.")
Arg::with_name("deposit-contract")
.long("deposit-contract")
.short("e")
.value_name("DEPOSIT-CONTRACT")
.help("Specifies the deposit contract address on the Eth1 chain.")
.takes_value(true)
.possible_values(&["info", "debug", "trace", "warn", "error", "crit"])
.default_value("trace"),
)
.arg(
Arg::with_name("deposit-contract-deploy")
.long("deposit-contract-deploy")
.value_name("BLOCK_NUMBER")
.help("Specifies the block number that the deposit contract was deployed at.")
.takes_value(true)
// TODO: set this higher once we're not using testnets all the time.
.default_value("0")
)
/*
* The "testnet" sub-command.
@@ -234,17 +223,6 @@ fn main() {
*/
.subcommand(SubCommand::with_name("testnet")
.about("Create a new Lighthouse datadir using a testnet strategy.")
.arg(
Arg::with_name("spec")
.short("s")
.long("spec")
.value_name("TITLE")
.help("Specifies the default eth2 spec type. Only effective when creating a new datadir.")
.takes_value(true)
.required(true)
.possible_values(&["mainnet", "minimal", "interop"])
.default_value("minimal")
)
.arg(
Arg::with_name("eth2-config")
.long("eth2-config")
@@ -347,68 +325,25 @@ fn main() {
* Start a new node, using a genesis state loaded from a YAML file
*/
.subcommand(SubCommand::with_name("file")
.about("Creates a new datadir where the genesis state is read from YAML. May fail to parse \
.about("Creates a new datadir where the genesis state is read from file. May fail to parse \
a file that was generated to a different spec than that specified by --spec.")
.arg(Arg::with_name("format")
.value_name("FORMAT")
.required(true)
.possible_values(&["yaml", "ssz", "json"])
.possible_values(&["ssz"])
.help("The encoding of the state in the file."))
.arg(Arg::with_name("file")
.value_name("YAML_FILE")
.value_name("FILE")
.required(true)
.help("A YAML file from which to read the state"))
.help("A file from which to read the state"))
)
/*
* `prysm`
*
* Connect to the Prysmatic Labs testnet.
*/
.subcommand(SubCommand::with_name("prysm")
.about("Connect to the Prysmatic Labs testnet on Goerli.")
)
)
.get_matches();
// build the initial logger
let decorator = slog_term::TermDecorator::new().build();
let decorator = logging::AlignedTermDecorator::new(decorator, logging::MAX_MESSAGE_WIDTH);
let drain = slog_term::FullFormat::new(decorator).build().fuse();
let drain = slog_async::Async::new(drain).build();
let drain = match matches.value_of("debug-level") {
Some("info") => drain.filter_level(Level::Info),
Some("debug") => drain.filter_level(Level::Debug),
Some("trace") => drain.filter_level(Level::Trace),
Some("warn") => drain.filter_level(Level::Warning),
Some("error") => drain.filter_level(Level::Error),
Some("crit") => drain.filter_level(Level::Critical),
_ => unreachable!("guarded by clap"),
};
let log = slog::Logger::root(drain.fuse(), o!());
if std::mem::size_of::<usize>() != 8 {
crit!(
log,
"Lighthouse only supports 64bit CPUs";
"detected" => format!("{}bit", std::mem::size_of::<usize>() * 8)
);
}
warn!(
log,
"Ethereum 2.0 is pre-release. This software is experimental."
);
let log_clone = log.clone();
// Load the process-wide configuration.
//
// May load this from disk or create a new configuration, depending on the CLI flags supplied.
let (client_config, eth2_config, log) = match get_configs(&matches, log) {
Ok(configs) => configs,
Err(e) => {
crit!(log_clone, "Failed to load configuration. Exiting"; "error" => e);
return;
}
};
// Start the node using a `tokio` executor.
match run::run_beacon_node(client_config, eth2_config, &log) {
Ok(_) => {}
Err(e) => crit!(log, "Beacon node failed to start"; "reason" => format!("{:}", e)),
}
}

View File

@@ -1,12 +1,14 @@
use clap::ArgMatches;
use client::{BeaconChainStartMethod, ClientConfig, Eth1BackendMethod, Eth2Config};
use client::{ClientConfig, ClientGenesis, Eth2Config};
use eth2_config::{read_from_file, write_to_file};
use genesis::recent_genesis_time;
use lighthouse_bootstrap::Bootstrapper;
use rand::{distributions::Alphanumeric, Rng};
use slog::{crit, info, warn, Logger};
use std::fs;
use std::net::Ipv4Addr;
use std::path::{Path, PathBuf};
use types::{Address, Epoch, Fork};
pub const DEFAULT_DATA_DIR: &str = ".lighthouse";
pub const CLIENT_CONFIG_FILENAME: &str = "beacon-node.toml";
@@ -27,12 +29,33 @@ pub fn get_configs(cli_args: &ArgMatches, core_log: Logger) -> Result<Config> {
let mut builder = ConfigBuilder::new(cli_args, core_log)?;
if let Some(server) = cli_args.value_of("eth1-server") {
builder.set_eth1_backend_method(Eth1BackendMethod::Web3 {
server: server.into(),
})
} else {
builder.set_eth1_backend_method(Eth1BackendMethod::Interop)
if cli_args.is_present("dummy-eth1") {
builder.client_config.dummy_eth1_backend = true;
}
if let Some(val) = cli_args.value_of("eth1-endpoint") {
builder.set_eth1_endpoint(val)
}
if let Some(val) = cli_args.value_of("deposit-contract") {
builder.set_deposit_contract(
val.parse::<Address>()
.map_err(|e| format!("Unable to parse deposit-contract address: {:?}", e))?,
)
}
if let Some(val) = cli_args.value_of("deposit-contract-deploy") {
builder.set_deposit_contract_deploy_block(
val.parse::<u64>()
.map_err(|e| format!("Unable to parse deposit-contract-deploy: {:?}", e))?,
)
}
if let Some(val) = cli_args.value_of("eth1-follow") {
builder.set_eth1_follow(
val.parse::<u64>()
.map_err(|e| format!("Unable to parse follow distance: {:?}", e))?,
)
}
match cli_args.subcommand() {
@@ -49,7 +72,7 @@ pub fn get_configs(cli_args: &ArgMatches, core_log: Logger) -> Result<Config> {
// If no primary subcommand was given, start the beacon chain from an existing
// database.
builder.set_beacon_chain_start_method(BeaconChainStartMethod::Resume);
builder.set_genesis(ClientGenesis::Resume);
// Whilst there is no large testnet or mainnet force the user to specify how they want
// to start a new chain (e.g., from a genesis YAML file, another node, etc).
@@ -142,7 +165,7 @@ fn process_testnet_subcommand(
builder.import_bootstrap_enr_address(server)?;
builder.import_bootstrap_eth2_config(server)?;
builder.set_beacon_chain_start_method(BeaconChainStartMethod::HttpBootstrap {
builder.set_genesis(ClientGenesis::RemoteNode {
server: server.to_string(),
port,
})
@@ -160,9 +183,11 @@ fn process_testnet_subcommand(
.parse::<u64>()
.map_err(|e| format!("Unable to parse minutes: {:?}", e))?;
builder.set_beacon_chain_start_method(BeaconChainStartMethod::RecentGenesis {
builder.client_config.dummy_eth1_backend = true;
builder.set_genesis(ClientGenesis::Interop {
validator_count,
minutes,
genesis_time: recent_genesis_time(minutes),
})
}
("quick", Some(cli_args)) => {
@@ -178,13 +203,15 @@ fn process_testnet_subcommand(
.parse::<u64>()
.map_err(|e| format!("Unable to parse genesis time: {:?}", e))?;
builder.set_beacon_chain_start_method(BeaconChainStartMethod::Generated {
builder.client_config.dummy_eth1_backend = true;
builder.set_genesis(ClientGenesis::Interop {
validator_count,
genesis_time,
})
}
("file", Some(cli_args)) => {
let file = cli_args
let path = cli_args
.value_of("file")
.ok_or_else(|| "No filename specified")?
.parse::<PathBuf>()
@@ -195,13 +222,34 @@ fn process_testnet_subcommand(
.ok_or_else(|| "No file format specified")?;
let start_method = match format {
"yaml" => BeaconChainStartMethod::Yaml { file },
"ssz" => BeaconChainStartMethod::Ssz { file },
"json" => BeaconChainStartMethod::Json { file },
"ssz" => ClientGenesis::SszFile { path },
other => return Err(format!("Unknown genesis file format: {}", other)),
};
builder.set_beacon_chain_start_method(start_method)
builder.set_genesis(start_method)
}
("prysm", Some(_)) => {
let mut spec = &mut builder.eth2_config.spec;
let mut client_config = &mut builder.client_config;
spec.min_deposit_amount = 100;
spec.max_effective_balance = 3_200_000_000;
spec.ejection_balance = 1_600_000_000;
spec.effective_balance_increment = 100_000_000;
spec.min_genesis_time = 0;
spec.genesis_fork = Fork {
previous_version: [0; 4],
current_version: [0, 0, 0, 2],
epoch: Epoch::new(0),
};
client_config.eth1.deposit_contract_address =
"0x802dF6aAaCe28B2EEb1656bb18dF430dDC42cc2e".to_string();
client_config.eth1.deposit_contract_deploy_block = 1487270;
client_config.eth1.follow_distance = 16;
client_config.dummy_eth1_backend = false;
builder.set_genesis(ClientGenesis::DepositContract)
}
(cmd, Some(_)) => {
return Err(format!(
@@ -220,8 +268,8 @@ fn process_testnet_subcommand(
/// Allows for building a set of configurations based upon `clap` arguments.
struct ConfigBuilder {
log: Logger,
eth2_config: Eth2Config,
client_config: ClientConfig,
pub eth2_config: Eth2Config,
pub client_config: ClientConfig,
}
impl ConfigBuilder {
@@ -294,14 +342,24 @@ impl ConfigBuilder {
Ok(())
}
/// Sets the method for starting the beacon chain.
pub fn set_beacon_chain_start_method(&mut self, method: BeaconChainStartMethod) {
self.client_config.beacon_chain_start_method = method;
pub fn set_eth1_endpoint(&mut self, endpoint: &str) {
self.client_config.eth1.endpoint = endpoint.to_string();
}
/// Sets the method for starting the beacon chain.
pub fn set_eth1_backend_method(&mut self, method: Eth1BackendMethod) {
self.client_config.eth1_backend_method = method;
pub fn set_deposit_contract(&mut self, deposit_contract: Address) {
self.client_config.eth1.deposit_contract_address = format!("{:?}", deposit_contract);
}
pub fn set_deposit_contract_deploy_block(&mut self, eth1_block_number: u64) {
self.client_config.eth1.deposit_contract_deploy_block = eth1_block_number;
}
pub fn set_eth1_follow(&mut self, distance: u64) {
self.client_config.eth1.follow_distance = distance;
}
pub fn set_genesis(&mut self, method: ClientGenesis) {
self.client_config.genesis = method;
}
/// Import the libp2p address for `server` into the list of libp2p nodes to connect with.
@@ -540,7 +598,6 @@ impl ConfigBuilder {
/// The supplied `cli_args` should be the base-level `clap` cli_args (i.e., not a subcommand
/// cli_args).
pub fn build(mut self, cli_args: &ArgMatches) -> Result<Config> {
self.eth2_config.apply_cli_args(cli_args)?;
self.client_config.apply_cli_args(cli_args, &mut self.log)?;
if let Some(bump) = cli_args.value_of("port-bump") {

153
beacon_node/src/lib.rs Normal file
View File

@@ -0,0 +1,153 @@
#[macro_use]
extern crate clap;
mod cli;
mod config;
pub use beacon_chain;
pub use cli::cli_app;
pub use client::{Client, ClientBuilder, ClientConfig, ClientGenesis};
pub use eth2_config::Eth2Config;
use beacon_chain::{
builder::Witness, eth1_chain::CachingEth1Backend, events::WebSocketSender,
lmd_ghost::ThreadSafeReducedTree, slot_clock::SystemTimeSlotClock,
};
use clap::ArgMatches;
use config::get_configs;
use environment::RuntimeContext;
use futures::{Future, IntoFuture};
use slog::{info, warn};
use std::ops::{Deref, DerefMut};
use store::DiskStore;
use types::EthSpec;
/// A type-alias to the tighten the definition of a production-intended `Client`.
pub type ProductionClient<E> = Client<
Witness<
DiskStore,
SystemTimeSlotClock,
ThreadSafeReducedTree<DiskStore, E>,
CachingEth1Backend<E, DiskStore>,
E,
WebSocketSender<E>,
>,
>;
/// The beacon node `Client` that will be used in production.
///
/// Generic over some `EthSpec`.
///
/// ## Notes:
///
/// Despite being titled `Production...`, this code is not ready for production. The name
/// demonstrates an intention, not a promise.
pub struct ProductionBeaconNode<E: EthSpec>(ProductionClient<E>);
impl<E: EthSpec> ProductionBeaconNode<E> {
/// Starts a new beacon node `Client` in the given `environment`.
///
/// Identical to `start_from_client_config`, however the `client_config` is generated from the
/// given `matches` and potentially configuration files on the local filesystem or other
/// configurations hosted remotely.
pub fn new_from_cli<'a, 'b>(
mut context: RuntimeContext<E>,
matches: &ArgMatches<'b>,
) -> impl Future<Item = Self, Error = String> + 'a {
let log = context.log.clone();
// TODO: the eth2 config in the env is being completely ignored.
//
// See https://github.com/sigp/lighthouse/issues/602
get_configs(&matches, log).into_future().and_then(
move |(client_config, eth2_config, _log)| {
context.eth2_config = eth2_config;
Self::new(context, client_config)
},
)
}
/// Starts a new beacon node `Client` in the given `environment`.
///
/// Client behaviour is defined by the given `client_config`.
pub fn new(
context: RuntimeContext<E>,
client_config: ClientConfig,
) -> impl Future<Item = Self, Error = String> {
let http_eth2_config = context.eth2_config().clone();
let spec = context.eth2_config().spec.clone();
let genesis_eth1_config = client_config.eth1.clone();
let client_genesis = client_config.genesis.clone();
let log = context.log.clone();
client_config
.db_path()
.ok_or_else(|| "Unable to access database path".to_string())
.into_future()
.and_then(move |db_path| {
Ok(ClientBuilder::new(context.eth_spec_instance.clone())
.runtime_context(context)
.disk_store(&db_path)?
.chain_spec(spec))
})
.and_then(move |builder| {
builder.beacon_chain_builder(client_genesis, genesis_eth1_config)
})
.and_then(move |builder| {
let builder = if client_config.sync_eth1_chain && !client_config.dummy_eth1_backend
{
info!(
log,
"Block production enabled";
"endpoint" => &client_config.eth1.endpoint,
"method" => "json rpc via http"
);
builder.caching_eth1_backend(client_config.eth1.clone())?
} else if client_config.dummy_eth1_backend {
warn!(
log,
"Block production impaired";
"reason" => "dummy eth1 backend is enabled"
);
builder.dummy_eth1_backend()?
} else {
info!(
log,
"Block production disabled";
"reason" => "no eth1 backend configured"
);
builder.no_eth1_backend()?
};
let builder = builder
.system_time_slot_clock()?
.websocket_event_handler(client_config.websocket_server.clone())?
.build_beacon_chain()?
.libp2p_network(&client_config.network)?
.http_server(&client_config, &http_eth2_config)?
.grpc_server(&client_config.rpc)?
.peer_count_notifier()?
.slot_notifier()?;
Ok(Self(builder.build()))
})
}
pub fn into_inner(self) -> ProductionClient<E> {
self.0
}
}
impl<E: EthSpec> Deref for ProductionBeaconNode<E> {
type Target = ProductionClient<E>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<E: EthSpec> DerefMut for ProductionBeaconNode<E> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}

View File

@@ -1,138 +0,0 @@
use client::{error, notifier, Client, ClientConfig, Eth1BackendMethod, Eth2Config};
use futures::sync::oneshot;
use futures::Future;
use slog::{error, info};
use std::cell::RefCell;
use std::path::Path;
use std::path::PathBuf;
use store::Store;
use store::{DiskStore, MemoryStore};
use tokio::runtime::Builder;
use tokio::runtime::Runtime;
use tokio::runtime::TaskExecutor;
use tokio_timer::clock::Clock;
use types::{EthSpec, InteropEthSpec, MainnetEthSpec, MinimalEthSpec};
/// Reads the configuration and initializes a `BeaconChain` with the required types and parameters.
///
/// Spawns an executor which performs syncing, networking, block production, etc.
///
/// Blocks the current thread, returning after the `BeaconChain` has exited or a `Ctrl+C`
/// signal.
pub fn run_beacon_node(
client_config: ClientConfig,
eth2_config: Eth2Config,
log: &slog::Logger,
) -> error::Result<()> {
let runtime = Builder::new()
.name_prefix("main-")
.clock(Clock::system())
.build()
.map_err(|e| format!("{:?}", e))?;
let executor = runtime.executor();
let db_path: PathBuf = client_config
.db_path()
.ok_or_else::<error::Error, _>(|| "Unable to access database path".into())?;
let db_type = &client_config.db_type;
let spec_constants = eth2_config.spec_constants.clone();
let other_client_config = client_config.clone();
info!(
log,
"Starting beacon node";
"p2p_listen_address" => format!("{}", &other_client_config.network.listen_address),
"db_type" => &other_client_config.db_type,
"spec_constants" => &spec_constants,
);
macro_rules! run_client {
($store: ty, $eth_spec: ty) => {
run::<$store, $eth_spec>(&db_path, client_config, eth2_config, executor, runtime, log)
};
}
if let Eth1BackendMethod::Web3 { .. } = client_config.eth1_backend_method {
return Err("Starting from web3 backend is not supported for interop.".into());
}
match (db_type.as_str(), spec_constants.as_str()) {
("disk", "minimal") => run_client!(DiskStore, MinimalEthSpec),
("disk", "mainnet") => run_client!(DiskStore, MainnetEthSpec),
("disk", "interop") => run_client!(DiskStore, InteropEthSpec),
("memory", "minimal") => run_client!(MemoryStore, MinimalEthSpec),
("memory", "mainnet") => run_client!(MemoryStore, MainnetEthSpec),
("memory", "interop") => run_client!(MemoryStore, InteropEthSpec),
(db_type, spec) => {
error!(log, "Unknown runtime configuration"; "spec_constants" => spec, "db_type" => db_type);
Err("Unknown specification and/or db_type.".into())
}
}
}
/// Performs the type-generic parts of launching a `BeaconChain`.
fn run<S, E>(
db_path: &Path,
client_config: ClientConfig,
eth2_config: Eth2Config,
executor: TaskExecutor,
mut runtime: Runtime,
log: &slog::Logger,
) -> error::Result<()>
where
S: Store + Clone + 'static + OpenDatabase,
E: EthSpec,
{
let store = S::open_database(&db_path)?;
let client: Client<S, E> =
Client::new(client_config, eth2_config, store, log.clone(), &executor)?;
// run service until ctrl-c
let (ctrlc_send, ctrlc_oneshot) = oneshot::channel();
let ctrlc_send_c = RefCell::new(Some(ctrlc_send));
ctrlc::set_handler(move || {
if let Some(ctrlc_send) = ctrlc_send_c.try_borrow_mut().unwrap().take() {
ctrlc_send.send(()).expect("Error sending ctrl-c message");
}
})
.map_err(|e| format!("Could not set ctrlc handler: {:?}", e))?;
let (exit_signal, exit) = exit_future::signal();
notifier::run(&client, executor, exit);
runtime
.block_on(ctrlc_oneshot)
.map_err(|e| format!("Ctrlc oneshot failed: {:?}", e))?;
// perform global shutdown operations.
info!(log, "Shutting down..");
exit_signal.fire();
// shutdown the client
// client.exit_signal.fire();
drop(client);
runtime.shutdown_on_idle().wait().unwrap();
Ok(())
}
/// A convenience trait, providing a method to open a database.
///
/// Panics if unable to open the database.
pub trait OpenDatabase: Sized {
fn open_database(path: &Path) -> error::Result<Self>;
}
impl OpenDatabase for MemoryStore {
fn open_database(_path: &Path) -> error::Result<Self> {
Ok(MemoryStore::open())
}
}
impl OpenDatabase for DiskStore {
fn open_database(path: &Path) -> error::Result<Self> {
DiskStore::open(path).map_err(|e| format!("Unable to open database: {:?}", e).into())
}
}

View File

@@ -5,15 +5,15 @@ authors = ["Paul Hauner <paul@paulhauner.com>"]
edition = "2018"
[dev-dependencies]
tempfile = "3"
tempfile = "3.1.0"
[dependencies]
db-key = "0.0.5"
leveldb = "0.8.4"
parking_lot = "0.7"
eth2_ssz = "0.1"
eth2_ssz_derive = "0.1"
tree_hash = "0.1"
parking_lot = "0.9.0"
eth2_ssz = "0.1.2"
eth2_ssz_derive = "0.1.0"
tree_hash = "0.1.0"
types = { path = "../../eth2/types" }
lazy_static = "1.3.0"
lazy_static = "1.4.0"
lighthouse_metrics = { path = "../../eth2/utils/lighthouse_metrics" }

View File

@@ -2,13 +2,15 @@ use crate::*;
use ssz::{Decode, DecodeError, Encode};
use ssz_derive::{Decode, Encode};
use std::convert::TryInto;
use types::beacon_state::{CommitteeCache, CACHED_EPOCHS};
use types::beacon_state::{BeaconTreeHashCache, CommitteeCache, CACHED_EPOCHS};
/// A container for storing `BeaconState` components.
// TODO: would be more space efficient with the caches stored separately and referenced by hash
#[derive(Encode, Decode)]
struct StorageContainer {
state_bytes: Vec<u8>,
committee_caches_bytes: Vec<Vec<u8>>,
tree_hash_cache_bytes: Vec<u8>,
}
impl StorageContainer {
@@ -20,9 +22,12 @@ impl StorageContainer {
committee_caches_bytes.push(cache.as_ssz_bytes());
}
let tree_hash_cache_bytes = state.tree_hash_cache.as_ssz_bytes();
Self {
state_bytes: state.as_ssz_bytes(),
committee_caches_bytes,
tree_hash_cache_bytes,
}
}
}
@@ -43,6 +48,8 @@ impl<T: EthSpec> TryInto<BeaconState<T>> for StorageContainer {
state.committee_caches[i] = CommitteeCache::from_ssz_bytes(bytes)?;
}
state.tree_hash_cache = BeaconTreeHashCache::from_ssz_bytes(&self.tree_hash_cache_bytes)?;
Ok(state)
}
}

40
beacon_node/tests/test.rs Normal file
View File

@@ -0,0 +1,40 @@
#![cfg(test)]
use node_test_rig::{environment::EnvironmentBuilder, LocalBeaconNode};
use types::{MinimalEthSpec, Slot};
fn env_builder() -> EnvironmentBuilder<MinimalEthSpec> {
EnvironmentBuilder::minimal()
}
#[test]
fn http_server_genesis_state() {
let mut env = env_builder()
.null_logger()
.expect("should build env logger")
.multi_threaded_tokio_runtime()
.expect("should start tokio runtime")
.build()
.expect("environment should build");
let node = LocalBeaconNode::production(env.core_context());
let remote_node = node.remote_node().expect("should produce remote node");
let (api_state, _root) = env
.runtime()
.block_on(remote_node.http.beacon().state_at_slot(Slot::new(0)))
.expect("should fetch state from http api");
let mut db_state = node
.client
.beacon_chain()
.expect("client should have beacon chain")
.state_at_slot(Slot::new(0))
.expect("should find state");
db_state.drop_all_caches();
assert_eq!(
api_state, db_state,
"genesis state from api should match that from the DB"
);
}

View File

@@ -7,14 +7,13 @@ edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
beacon_chain = { path = "../beacon_chain" }
clap = "2.32.0"
exit-future = "0.1.3"
futures = "0.1.25"
serde = "1.0"
serde_derive = "1.0"
serde_json = "^1.0"
slog = "^2.2.3"
tokio = "0.1.16"
clap = "2.33.0"
exit-future = "0.1.4"
futures = "0.1.29"
serde = "1.0.102"
serde_derive = "1.0.102"
serde_json = "1.0.41"
slog = "2.5.2"
tokio = "0.1.22"
types = { path = "../../eth2/types" }
ws = "0.9"
ws = "0.9.1"

View File

@@ -1,7 +1,7 @@
use beacon_chain::events::{EventHandler, EventKind};
use futures::Future;
use slog::{debug, error, info, warn, Logger};
use std::marker::PhantomData;
use std::net::SocketAddr;
use std::thread;
use tokio::runtime::TaskExecutor;
use types::EthSpec;
@@ -36,31 +36,30 @@ impl<T: EthSpec> WebSocketSender<T> {
}
}
impl<T: EthSpec> EventHandler<T> for WebSocketSender<T> {
fn register(&self, kind: EventKind<T>) -> Result<(), String> {
self.send_string(
serde_json::to_string(&kind)
.map_err(|e| format!("Unable to serialize event: {:?}", e))?,
)
}
}
pub fn start_server<T: EthSpec>(
config: &Config,
executor: &TaskExecutor,
log: &Logger,
) -> Result<(WebSocketSender<T>, exit_future::Signal), String> {
) -> Result<(WebSocketSender<T>, exit_future::Signal, SocketAddr), String> {
let server_string = format!("{}:{}", config.listen_address, config.port);
info!(
log,
"Websocket server starting";
"listen_address" => &server_string
);
// Create a server that simply ignores any incoming messages.
let server = WebSocket::new(|_| |_| Ok(()))
.map_err(|e| format!("Failed to initialize websocket server: {:?}", e))?;
.map_err(|e| format!("Failed to initialize websocket server: {:?}", e))?
.bind(server_string.clone())
.map_err(|e| {
format!(
"Failed to bind websocket server to {}: {:?}",
server_string, e
)
})?;
let actual_listen_addr = server.local_addr().map_err(|e| {
format!(
"Failed to read listening addr from websocket server: {:?}",
e
)
})?;
let broadcaster = server.broadcaster();
@@ -91,7 +90,7 @@ pub fn start_server<T: EthSpec>(
};
let log_inner = log.clone();
let _handle = thread::spawn(move || match server.listen(server_string) {
let _handle = thread::spawn(move || match server.run() {
Ok(_) => {
debug!(
log_inner,
@@ -107,11 +106,19 @@ pub fn start_server<T: EthSpec>(
}
});
info!(
log,
"WebSocket server started";
"address" => format!("{}", actual_listen_addr.ip()),
"port" => actual_listen_addr.port(),
);
Ok((
WebSocketSender {
sender: Some(broadcaster),
_phantom: PhantomData,
},
exit_signal,
actual_listen_addr,
))
}

View File

@@ -1,24 +1,32 @@
# Command-Line Interface (CLI)
Lighthouse a collection of CLI applications. The two primary binaries are:
The `lighthouse` binary provides all necessary Ethereum 2.0 functionality. It
has two primary sub-commands:
- `beacon_node`: the largest and most fundamental component which connects to
- `$ lighthouse beacon_node`: the largest and most fundamental component which connects to
the p2p network, processes messages and tracks the head of the beacon
chain.
- `validator_client`: a lightweight but important component which loads a validators private
- `$ lighthouse validator_client`: a lightweight but important component which loads a validators private
key and signs messages using a `beacon_node` as a source-of-truth.
There are also some ancillary binaries:
There are also some ancillary binaries like `lcli` and `account_manager`, but
these are primarily for testing.
- `account_manager`: generates cryptographic keys.
- `lcli`: a general-purpose utility for troubleshooting Lighthouse state
transitions (developer tool).
> **Note:** documentation sometimes uses `$ lighthouse bn` and `$ lighthouse
> vc` instead of the long-form `beacon_node` and `validator_client`. These
> commands are valid on the CLI too.
## Installation
Presently, we recommend building Lighthouse using the `$ cargo build --release
--all` command and executing binaries from the
`<lighthouse-repository>/target/release` directory.
Typical users may install `lighthouse` to `CARGO_HOME` with `cargo install
--path lighthouse` from the root of the repository. See ["Configuring the
`PATH` environment variable"](https://www.rust-lang.org/tools/install) for more
information.
For develeopers, we recommend building Lighthouse using the `$ cargo build --release
--bin lighthouse` command and executing binaries from the
`<lighthouse-repository>/target/release` directory. This is more ergonomic when
modifying and rebuilding regularly.
## Documentation
@@ -27,36 +35,29 @@ documentation.
```bash
$ ./beacon_node --help
$ lighthouse beacon_node --help
```
```bash
$ ./validator_client --help
```
```bash
$ ./account_manager --help
```
```bash
$ ./lcli --help
$ lighthouse validator_client --help
```
## Beacon Node
The `beacon_node` CLI has two primary tasks:
The `$ lighthouse beacon_node` (or `$ lighthouse bn`) command has two primary
tasks:
- **Resuming** an existing database with `$ ./beacon_node`.
- **Creating** a new testnet database using `$ ./beacon_node testnet`.
- **Resuming** an existing database with `$ lighthouse bn`.
- **Creating** a new testnet database using `$ lighthouse bn testnet`.
## Creating a new database
Use the `$./beacon_node testnet` command (see [testnets](./testnets.md) for more
information).
Use the `$ lighthouse bn testnet` command (see [testnets](./testnets.md) for
more information).
## Resuming from an existing database
Once a database has been created, it can be resumed by running `$ ./beacon_node`.
Once a database has been created, it can be resumed by running `$ lighthouse bn`.
Presently, this command will fail if no existing database is found. You must
use the `$ ./beacon_node testnet` command to create a new database.
Presently, you are not allowed to call `$ lighthouse bn` unless you have first
created a database using `$ lighthouse bn testnet`.

View File

@@ -19,6 +19,18 @@
> `target/release` directory.
> - First-time compilation may take several minutes.
### Installing to `PATH`
Use `cargo install --path lighthouse` from the root of the repository to
install the compiled binary to `CARGO_HOME` or `$HOME/.cargo`. If this
directory is on your `PATH`, you can run `$ lighthouse ..` from anywhere.
See ["Configuring the `PATH` environment
variable" (rust-lang.org)](https://www.rust-lang.org/tools/install) for more information.
> If you _don't_ install `lighthouse` to the path, you'll need to run the
> binaries directly from the `target` directory or using `cargo run ...`.
### Windows
Perl may also be required to build Lighthouse. You can install [Strawberry

View File

@@ -3,9 +3,9 @@
With a functional [development environment](./setup.md), starting a local multi-node
testnet is easy:
1. Start the first node: `$ ./beacon_node testnet -f recent 8`
1. Start a validator client: `$ ./validator_client testnet -b insecure 0 8`
1. Start more nodes with `$ ./beacon_node -b 10 testnet -f bootstrap
1. Start the first node: `$ lighthouse bn testnet -f recent 8`
1. Start a validator client: `$ lighthouse bn testnet -b insecure 0 8`
1. Start more nodes with `$ lighthouse bn -b 10 testnet -f bootstrap
http://localhost:5052`
- Increment the `-b` value by `10` for each additional node.
@@ -16,10 +16,10 @@ First, setup a Lighthouse development environment and navigate to the
## Starting a beacon node
Start a new node (creating a fresh database and configuration in `~/.lighthouse`), using:
Start a new node (creating a fresh database and configuration in `$HOME/.lighthouse`), using:
```bash
$ ./beacon_node testnet -f recent 8
$ lighthouse bn testnet -f recent 8
```
> Notes:
@@ -27,7 +27,7 @@ $ ./beacon_node testnet -f recent 8
> - The `-f` flag ignores any existing database or configuration, backing them
> up before re-initializing.
> - `8` is number of validators with deposits in the genesis state.
> - See `$ ./beacon_node testnet recent --help` for more configuration options,
> - See `$ lighthouse bn testnet recent --help` for more configuration options,
> including `minimal`/`mainnet` specification.
## Starting a validator client
@@ -35,7 +35,7 @@ $ ./beacon_node testnet -f recent 8
In a new terminal window, start the validator client with:
```bash
$ ./validator_client testnet -b insecure 0 8
$ lighthouse vc testnet -b insecure 0 8
```
> Notes:
@@ -58,7 +58,7 @@ In a new terminal window, run:
```bash
$ ./beacon_node -b 10 testnet -r bootstrap
$ lighthouse bn -b 10 testnet -r bootstrap
```
> Notes:
@@ -70,4 +70,4 @@ $ ./beacon_node -b 10 testnet -r bootstrap
> (avoids data directory collisions between nodes).
> - The default bootstrap HTTP address is `http://localhost:5052`. The new node
> will download configuration via HTTP before starting sync via libp2p.
> - See `$ ./beacon_node testnet bootstrap --help` for more configuration.
> - See `$ lighthouse bn testnet bootstrap --help` for more configuration.

View File

@@ -1,16 +1,16 @@
# Testnets
The Lighthouse CLI has a `testnet` sub-command to allow creating or connecting
to Eth2 beacon chain testnets.
The `beacon_node` and `validator` commands have a `testnet` sub-command to
allow creating or connecting to Eth2 beacon chain testnets.
For detailed documentation, use the `--help` flag on the CLI:
```bash
$ ./beacon_node testnet --help
$ lighthouse bn testnet --help
```
```bash
$ ./validator_client testnet --help
$ lighthouse vc testnet --help
```
## Examples
@@ -25,7 +25,7 @@ commands are based in the `target/release` directory (this is the build dir for
To start a brand-new beacon node (with no history) use:
```bash
$ ./beacon_node testnet -f quick 8 <GENESIS_TIME>
$ lighthouse bn testnet -f quick 8 <GENESIS_TIME>
```
Where `GENESIS_TIME` is in [unix time](https://duckduckgo.com/?q=unix+time&t=ffab&ia=answer).
@@ -38,7 +38,7 @@ method in the `ethereum/eth2.0-pm` repository.
> - The `-f` flag ignores any existing database or configuration, backing them
> up before re-initializing.
> - `8` is the validator count and `1567222226` is the genesis time.
> - See `$ ./beacon_node testnet quick --help` for more configuration options.
> - See `$ lighthouse bn testnet quick --help` for more configuration options.
### Start a beacon node given a genesis state file
@@ -52,14 +52,14 @@ There are three supported formats:
Start a new node using `/tmp/genesis.ssz` as the genesis state:
```bash
$ ./beacon_node testnet --spec minimal -f file ssz /tmp/genesis.ssz
$ lighthouse bn testnet --spec minimal -f file ssz /tmp/genesis.ssz
```
> Notes:
>
> - The `-f` flag ignores any existing database or configuration, backing them
> up before re-initializing.
> - See `$ ./beacon_node testnet file --help` for more configuration options.
> - See `$ lighthouse bn testnet file --help` for more configuration options.
> - The `--spec` flag is required to allow SSZ parsing of fixed-length lists.
> Here the `minimal` eth2 specification is chosen, allowing for lower
> validator counts. See
@@ -71,7 +71,7 @@ $ ./beacon_node testnet --spec minimal -f file ssz /tmp/genesis.ssz
To start a brand-new validator client (with no history) use:
```bash
$ ./validator_client testnet -b insecure 0 8
$ lighthouse vc testnet -b insecure 0 8
```
> Notes:
@@ -113,7 +113,7 @@ the `--libp2p-addresses` command.
#### Example:
```bash
$ ./beacon_node --libp2p-addresses /ip4/192.168.0.1/tcp/9000
$ lighthouse bn --libp2p-addresses /ip4/192.168.0.1/tcp/9000
```
### Specify a boot node by ENR (Ethereum Name Record)
@@ -124,7 +124,7 @@ the `--boot-nodes` command.
#### Example:
```bash
$ ./beacon_node --boot-nodes -IW4QB2Hi8TPuEzQ41Cdf1r2AUU1FFVFDBJdJyOkWk2qXpZfFZQy2YnJIyoT_5fnbtrXUouoskmydZl4pIg90clIkYUDgmlwhH8AAAGDdGNwgiMog3VkcIIjKIlzZWNwMjU2azGhAjg0-DsTkQynhJCRnLLttBK1RS78lmUkLa-wgzAi-Ob5
$ lighthouse bn --boot-nodes -IW4QB2Hi8TPuEzQ41Cdf1r2AUU1FFVFDBJdJyOkWk2qXpZfFZQy2YnJIyoT_5fnbtrXUouoskmydZl4pIg90clIkYUDgmlwhH8AAAGDdGNwgiMog3VkcIIjKIlzZWNwMjU2azGhAjg0-DsTkQynhJCRnLLttBK1RS78lmUkLa-wgzAi-Ob5
```
### Avoid port clashes when starting nodes
@@ -138,7 +138,7 @@ ports by some `n`.
Increase all ports by `10` (using multiples of `10` is recommended).
```bash
$ ./beacon_node -b 10
$ lighthouse bn -b 10
```
### Start a testnet with a custom slot time
@@ -151,7 +151,7 @@ Lighthouse can run at quite low slot times when there are few validators (e.g.,
The `-t` (`--slot-time`) flag specifies the milliseconds per slot.
```bash
$ ./beacon_node testnet -t 500 recent 8
$ lighthouse bn testnet -t 500 recent 8
```
> Note: `bootstrap` loads the slot time via HTTP and therefore conflicts with

View File

@@ -5,17 +5,18 @@ authors = ["Age Manning <Age@AgeManning.com>", "Paul Hauner <paul@sigmaprime.io>
edition = "2018"
[dependencies]
parking_lot = "0.7"
parking_lot = "0.9.0"
store = { path = "../../beacon_node/store" }
types = { path = "../types" }
itertools = "0.8.1"
[dev-dependencies]
criterion = "0.2"
hex = "0.3.2"
yaml-rust = "0.4.2"
criterion = "0.3.0"
hex = "0.3"
yaml-rust = "0.4.3"
bls = { path = "../utils/bls" }
slot_clock = { path = "../utils/slot_clock" }
beacon_chain = { path = "../../beacon_node/beacon_chain" }
env_logger = "0.6.0"
lazy_static = "1.3.0"
rand = "0.7"
env_logger = "0.7.1"
lazy_static = "1.4.0"
rand = "0.7.2"

View File

@@ -49,7 +49,7 @@ pub trait LmdGhost<S: Store, E: EthSpec>: Send + Sync {
/// Runs an integrity verification function on fork choice algorithm.
///
/// Returns `Ok(())` if the underlying fork choice has maintained it's integrity,
/// Returns `Ok(())` if the underlying fork choice has maintained its integrity,
/// `Err(description)` otherwise.
fn verify_integrity(&self) -> Result<()>;
}

View File

@@ -4,6 +4,7 @@
//!
//! This implementation is incomplete and has known bugs. Do not use in production.
use super::{LmdGhost, Result as SuperResult};
use itertools::Itertools;
use parking_lot::RwLock;
use std::collections::HashMap;
use std::fmt;
@@ -20,6 +21,7 @@ pub enum Error {
MissingBlock(Hash256),
MissingState(Hash256),
MissingChild(Hash256),
MissingSuccessor(Hash256, Hash256),
NotInTree(Hash256),
NoCommonAncestor((Hash256, Hash256)),
StoreError(StoreError),
@@ -177,8 +179,8 @@ where
if current_hash != subtree_hash {
let children = self.get_node(current_hash)?.children.clone();
for child_hash in children {
self.retain_subtree(child_hash, subtree_hash)?;
for child in children {
self.retain_subtree(child.hash, subtree_hash)?;
}
self.nodes.remove(&current_hash);
@@ -239,7 +241,7 @@ where
let _root_weight = self.update_weight(start_block_root, weight_fn)?;
let start_node = self.get_node(start_block_root)?;
let head_node = self.find_head_from(start_node)?;
let head_node = self.find_head_from(start_node, start_block_slot)?;
Ok(head_node.block_hash)
}
@@ -251,31 +253,32 @@ where
}
}
fn find_head_from<'a>(&'a self, start_node: &'a Node) -> Result<&'a Node> {
if start_node.does_not_have_children() {
Ok(start_node)
} else {
// Corresponds to the loop in `get_head` in the spec.
fn find_head_from<'a>(
&'a self,
start_node: &'a Node,
justified_slot: Slot,
) -> Result<&'a Node> {
let children = start_node
.children
.iter()
.map(|hash| self.get_node(*hash))
// This check is primarily for the first iteration, where we must ensure that
// we only consider votes that were made after the last justified checkpoint.
.filter(|c| c.successor_slot > justified_slot)
.map(|c| self.get_node(c.hash))
.collect::<Result<Vec<&Node>>>()?;
// TODO: check if `max_by` is `O(n^2)`.
if children.is_empty() {
Ok(start_node)
} else {
let best_child = children
.iter()
.max_by(|a, b| {
if a.weight != b.weight {
a.weight.cmp(&b.weight)
} else {
a.block_hash.cmp(&b.block_hash)
}
})
.max_by_key(|child| (child.weight, child.block_hash))
// There can only be no maximum if there are no children. This code path is guarded
// against that condition.
.expect("There must be a maximally weighted node.");
self.find_head_from(best_child)
self.find_head_from(best_child, justified_slot)
}
}
@@ -288,8 +291,8 @@ where
let mut weight = 0;
for &child in &node.children {
weight += self.update_weight(child, weight_fn)?;
for child in &node.children {
weight += self.update_weight(child.hash, weight_fn)?;
}
for &voter in &node.voters {
@@ -323,13 +326,13 @@ where
//
// Load the child of the node and set it's parent to be the parent of this
// node (viz., graft the node's child to the node's parent)
let child = self.get_mut_node(node.children[0])?;
let child = self.get_mut_node(node.children[0].hash)?;
child.parent_hash = node.parent_hash;
// Graft the parent of this node to it's child.
if let Some(parent_hash) = node.parent_hash {
let parent = self.get_mut_node(parent_hash)?;
parent.replace_child(node.block_hash, node.children[0])?;
parent.replace_child_hash(node.block_hash, node.children[0].hash)?;
}
self.nodes.remove(&vote.hash);
@@ -376,15 +379,16 @@ where
let node = node.clone();
if let Some(parent_hash) = node.parent_hash {
if (node.children.len() == 1) && !node.has_votes() {
let child_hash = node.children[0];
if node.children.len() == 1 && !node.has_votes() {
let child = &node.children[0];
// Graft the single descendant `node` to the `parent` of node.
self.get_mut_node(child_hash)?.parent_hash = Some(parent_hash);
self.get_mut_node(child.hash)?.parent_hash = Some(parent_hash);
// Detach `node` from `parent`, replacing it with `child`.
// Preserve the parent's direct descendant slot.
self.get_mut_node(parent_hash)?
.replace_child(hash, child_hash)?;
.replace_child_hash(hash, child.hash)?;
true
} else {
@@ -442,6 +446,40 @@ where
Ok(())
}
/// Find the direct successor block of `ancestor` if `descendant` is a descendant.
fn find_ancestor_successor_opt(
&self,
ancestor: Hash256,
descendant: Hash256,
) -> Result<Option<Hash256>> {
Ok(std::iter::once(descendant)
.chain(
self.iter_ancestors(descendant)?
.take_while(|(_, slot)| *slot >= self.root_slot())
.map(|(block_hash, _)| block_hash),
)
.tuple_windows()
.find_map(|(successor, block_hash)| {
if block_hash == ancestor {
Some(successor)
} else {
None
}
}))
}
/// Same as `find_ancestor_successor_opt` but will return an error instead of an option.
fn find_ancestor_successor(&self, ancestor: Hash256, descendant: Hash256) -> Result<Hash256> {
self.find_ancestor_successor_opt(ancestor, descendant)?
.ok_or_else(|| Error::MissingSuccessor(ancestor, descendant))
}
/// Look up the successor of the given `ancestor`, returning the slot of that block.
fn find_ancestor_successor_slot(&self, ancestor: Hash256, descendant: Hash256) -> Result<Slot> {
let successor_hash = self.find_ancestor_successor(ancestor, descendant)?;
Ok(self.get_block(successor_hash)?.slot)
}
/// Add `node` to the reduced tree, returning an error if `node` is not rooted in the tree.
fn add_node(&mut self, mut node: Node) -> Result<()> {
// Find the highest (by slot) ancestor of the given node in the reduced tree.
@@ -460,7 +498,9 @@ where
// `node` to it.
// 3. Graft `node` to an existing node.
if !prev_in_tree.children.is_empty() {
for &child_hash in &prev_in_tree.children {
for child_link in &prev_in_tree.children {
let child_hash = child_link.hash;
// 1. Graft the new node between two existing nodes.
//
// If `node` is a descendant of `prev_in_tree` but an ancestor of a child connected to
@@ -468,19 +508,20 @@ where
//
// This means that `node` can be grafted between `prev_in_tree` and the child that is a
// descendant of both `node` and `prev_in_tree`.
if self
.iter_ancestors(child_hash)?
.take_while(|(_, slot)| *slot >= self.root_slot())
.any(|(ancestor, _slot)| ancestor == node.block_hash)
if let Some(successor) =
self.find_ancestor_successor_opt(node.block_hash, child_hash)?
{
let child = self.get_mut_node(child_hash)?;
// Graft `child` to `node`.
child.parent_hash = Some(node.block_hash);
// Graft `node` to `child`.
node.children.push(child_hash);
node.children.push(ChildLink {
hash: child_hash,
successor_slot: self.get_block(successor)?.slot,
});
// Detach `child` from `prev_in_tree`, replacing it with `node`.
prev_in_tree.replace_child(child_hash, node.block_hash)?;
prev_in_tree.replace_child_hash(child_hash, node.block_hash)?;
// Graft `node` to `prev_in_tree`.
node.parent_hash = Some(prev_in_tree.block_hash);
@@ -495,7 +536,8 @@ where
// any of the children of `prev_in_tree`, we know that `node` is on a different fork to
// all of the children of `prev_in_tree`.
if node.parent_hash.is_none() {
for &child_hash in &prev_in_tree.children {
for child_link in &prev_in_tree.children {
let child_hash = child_link.hash;
// Find the highest (by slot) common ancestor between `node` and `child`.
//
// The common ancestor is the last block before `node` and `child` forked.
@@ -506,24 +548,37 @@ where
// must add this new block into the tree (because it is a decision node
// between two forks).
if ancestor_hash != prev_in_tree.block_hash {
let child = self.get_mut_node(child_hash)?;
// Create a new `common_ancestor` node which represents the `ancestor_hash`
// block, has `prev_in_tree` as the parent and has both `node` and `child`
// as children.
let common_ancestor = Node {
block_hash: ancestor_hash,
parent_hash: Some(prev_in_tree.block_hash),
children: vec![node.block_hash, child_hash],
children: vec![
ChildLink {
hash: node.block_hash,
successor_slot: self.find_ancestor_successor_slot(
ancestor_hash,
node.block_hash,
)?,
},
ChildLink {
hash: child_hash,
successor_slot: self
.find_ancestor_successor_slot(ancestor_hash, child_hash)?,
},
],
..Node::default()
};
let child = self.get_mut_node(child_hash)?;
// Graft `child` and `node` to `common_ancestor`.
child.parent_hash = Some(common_ancestor.block_hash);
node.parent_hash = Some(common_ancestor.block_hash);
// Detach `child` from `prev_in_tree`, replacing it with `common_ancestor`.
prev_in_tree.replace_child(child_hash, common_ancestor.block_hash)?;
prev_in_tree.replace_child_hash(child_hash, common_ancestor.block_hash)?;
// Store the new `common_ancestor` node.
self.nodes
@@ -540,7 +595,11 @@ where
//
// Graft `node` to `prev_in_tree` and `prev_in_tree` to `node`
node.parent_hash = Some(prev_in_tree.block_hash);
prev_in_tree.children.push(node.block_hash);
prev_in_tree.children.push(ChildLink {
hash: node.block_hash,
successor_slot: self
.find_ancestor_successor_slot(prev_in_tree.block_hash, node.block_hash)?,
});
}
// Update `prev_in_tree`. A mutable reference was not maintained to satisfy the borrow
@@ -655,7 +714,17 @@ where
node.children
.iter()
.map(|child| verify_node_exists(*child, "child_must_exist".to_string()))
.map(|child| {
verify_node_exists(child.hash, "child_must_exist".to_string())?;
if self.find_ancestor_successor_slot(node.block_hash, child.hash)?
== child.successor_slot
{
Ok(())
} else {
Err("successor slot on child link is incorrect".to_string())
}
})
.collect::<std::result::Result<(), String>>()?;
verify_node_exists(node.block_hash, "block hash must exist".to_string())?;
@@ -698,25 +767,35 @@ where
#[derive(Default, Clone, Debug)]
pub struct Node {
/// Hash of the parent node in the reduced tree (not necessarily parent block).
pub parent_hash: Option<Hash256>,
pub children: Vec<Hash256>,
pub children: Vec<ChildLink>,
pub weight: u64,
pub block_hash: Hash256,
pub voters: Vec<usize>,
}
impl Node {
pub fn does_not_have_children(&self) -> bool {
self.children.is_empty()
}
#[derive(Default, Clone, Debug)]
pub struct ChildLink {
/// Hash of the child block (may not be a direct descendant).
pub hash: Hash256,
/// Slot of the block which is a direct descendant on the chain leading to `hash`.
///
/// Node <--- Successor <--- ... <--- Child
pub successor_slot: Slot,
}
pub fn replace_child(&mut self, old: Hash256, new: Hash256) -> Result<()> {
impl Node {
/// Replace a child with a new child, whilst preserving the successor slot.
///
/// The new child should have the same ancestor successor block as the old one.
pub fn replace_child_hash(&mut self, old: Hash256, new: Hash256) -> Result<()> {
let i = self
.children
.iter()
.position(|&c| c == old)
.position(|c| c.hash == old)
.ok_or_else(|| Error::MissingChild(old))?;
self.children[i] = new;
self.children[i].hash = new;
Ok(())
}
@@ -725,7 +804,7 @@ impl Node {
let i = self
.children
.iter()
.position(|&c| c == child)
.position(|c| c.hash == child)
.ok_or_else(|| Error::MissingChild(child))?;
self.children.remove(i);

View File

@@ -5,7 +5,7 @@ extern crate lazy_static;
use beacon_chain::test_utils::{
generate_deterministic_keypairs, AttestationStrategy,
BeaconChainHarness as BaseBeaconChainHarness, BlockStrategy,
BeaconChainHarness as BaseBeaconChainHarness, BlockStrategy, HarnessType,
};
use lmd_ghost::{LmdGhost, ThreadSafeReducedTree as BaseThreadSafeReducedTree};
use rand::{prelude::*, rngs::StdRng};
@@ -21,7 +21,7 @@ pub const VALIDATOR_COUNT: usize = 3 * 8;
type TestEthSpec = MinimalEthSpec;
type ThreadSafeReducedTree = BaseThreadSafeReducedTree<MemoryStore, TestEthSpec>;
type BeaconChainHarness = BaseBeaconChainHarness<ThreadSafeReducedTree, TestEthSpec>;
type BeaconChainHarness = BaseBeaconChainHarness<HarnessType<TestEthSpec>>;
type RootAndSlot = (Hash256, Slot);
lazy_static! {
@@ -45,14 +45,19 @@ struct ForkedHarness {
pub genesis_block: BeaconBlock<TestEthSpec>,
pub honest_head: RootAndSlot,
pub faulty_head: RootAndSlot,
/// Honest roots in reverse order (slot high to low)
pub honest_roots: Vec<RootAndSlot>,
/// Faulty roots in reverse order (slot high to low)
pub faulty_roots: Vec<RootAndSlot>,
}
impl ForkedHarness {
/// A new standard instance of with constant parameters.
pub fn new() -> Self {
let harness = BeaconChainHarness::new(generate_deterministic_keypairs(VALIDATOR_COUNT));
let harness = BeaconChainHarness::new(
MinimalEthSpec,
generate_deterministic_keypairs(VALIDATOR_COUNT),
);
// Move past the zero slot.
harness.advance_slot();
@@ -222,7 +227,7 @@ fn single_voter_persistent_instance_reverse_order() {
"New tree should have integrity"
);
for (root, slot) in harness.honest_roots.iter().rev() {
for (root, slot) in &harness.honest_roots {
lmd.process_attestation(0, *root, *slot)
.expect("fork choice should accept attestations to honest roots in reverse");
@@ -234,11 +239,15 @@ fn single_voter_persistent_instance_reverse_order() {
}
// The honest head should be selected.
let (head_root, head_slot) = harness.honest_roots.first().unwrap();
let (finalized_root, _) = harness.honest_roots.last().unwrap();
let (head_root, _) = harness.honest_roots.first().unwrap();
let (finalized_root, finalized_slot) = harness.honest_roots.last().unwrap();
assert_eq!(
lmd.find_head(*head_slot, *finalized_root, ForkedHarness::weight_function),
lmd.find_head(
*finalized_slot,
*finalized_root,
ForkedHarness::weight_function
),
Ok(*head_root),
"Honest head should be selected"
);
@@ -250,7 +259,7 @@ fn single_voter_persistent_instance_reverse_order() {
fn single_voter_many_instance_honest_blocks_voting_forwards() {
let harness = &FORKED_HARNESS;
for (root, slot) in &harness.honest_roots {
for (root, slot) in harness.honest_roots.iter().rev() {
let lmd = harness.new_fork_choice();
lmd.process_attestation(0, *root, *slot)
.expect("fork choice should accept attestations to honest roots");
@@ -269,7 +278,7 @@ fn single_voter_many_instance_honest_blocks_voting_in_reverse() {
let harness = &FORKED_HARNESS;
// Same as above, but in reverse order (votes on the highest honest block first).
for (root, slot) in harness.honest_roots.iter().rev() {
for (root, slot) in &harness.honest_roots {
let lmd = harness.new_fork_choice();
lmd.process_attestation(0, *root, *slot)
.expect("fork choice should accept attestations to honest roots in reverse");
@@ -288,7 +297,7 @@ fn single_voter_many_instance_honest_blocks_voting_in_reverse() {
fn single_voter_many_instance_faulty_blocks_voting_forwards() {
let harness = &FORKED_HARNESS;
for (root, slot) in &harness.faulty_roots {
for (root, slot) in harness.faulty_roots.iter().rev() {
let lmd = harness.new_fork_choice();
lmd.process_attestation(0, *root, *slot)
.expect("fork choice should accept attestations to faulty roots");
@@ -306,7 +315,7 @@ fn single_voter_many_instance_faulty_blocks_voting_forwards() {
fn single_voter_many_instance_faulty_blocks_voting_in_reverse() {
let harness = &FORKED_HARNESS;
for (root, slot) in harness.faulty_roots.iter().rev() {
for (root, slot) in &harness.faulty_roots {
let lmd = harness.new_fork_choice();
lmd.process_attestation(0, *root, *slot)
.expect("fork choice should accept attestations to faulty roots in reverse");
@@ -319,6 +328,44 @@ fn single_voter_many_instance_faulty_blocks_voting_in_reverse() {
}
}
/// Ensure that votes with slots before the justified slot are not counted.
#[test]
fn discard_votes_before_justified_slot() {
let harness = &FORKED_HARNESS;
let lmd = harness.new_fork_choice();
let (genesis_root, genesis_slot) = *harness.honest_roots.last().unwrap();
// Add attestations from all validators for all honest blocks.
for (root, slot) in harness.honest_roots.iter().rev() {
for i in 0..VALIDATOR_COUNT {
lmd.process_attestation(i, *root, *slot)
.expect("should accept attestations in increasing order");
}
// Head starting from 0 checkpoint (genesis) should be current root
assert_eq!(
lmd.find_head(genesis_slot, genesis_root, ForkedHarness::weight_function),
Ok(*root),
"Honest head should be selected"
);
// Head from one slot after genesis should still be genesis, because the successor
// block of the genesis block has slot `genesis_slot + 1` which isn't greater than
// the slot we're starting from. This is a very artifical test, but one that's easy to
// describe.
assert_eq!(
lmd.find_head(
genesis_slot + 1,
genesis_root,
ForkedHarness::weight_function
),
Ok(genesis_root)
);
}
}
/// Ensures that the finalized root can be set to all values in `roots`.
fn test_update_finalized_root(roots: &[(Hash256, Slot)]) {
let harness = &FORKED_HARNESS;

View File

@@ -6,12 +6,11 @@ edition = "2018"
[dependencies]
int_to_bytes = { path = "../utils/int_to_bytes" }
itertools = "0.8"
parking_lot = "0.7"
parking_lot = "0.9.0"
types = { path = "../types" }
state_processing = { path = "../state_processing" }
eth2_ssz = "0.1"
eth2_ssz_derive = { path = "../utils/ssz_derive" }
eth2_ssz = "0.1.2"
eth2_ssz_derive = "0.1.0"
[dev-dependencies]
rand = "0.5.5"
rand = "0.7.2"

View File

@@ -35,16 +35,14 @@ impl<'a, T: EthSpec> MaxCover for AttMaxCover<'a, T> {
/// Sneaky: we keep all the attestations together in one bucket, even though
/// their aggregation bitfields refer to different committees. In order to avoid
/// confusing committees when updating covering sets, we update only those attestations
/// whose shard and epoch match the attestation being included in the solution, by the logic
/// that a shard and epoch uniquely identify a committee.
/// whose slot and index match the attestation being included in the solution, by the logic
/// that a slot and index uniquely identify a committee.
fn update_covering_set(
&mut self,
best_att: &Attestation<T>,
covered_validators: &BitList<T::MaxValidatorsPerCommittee>,
) {
if self.att.data.crosslink.shard == best_att.data.crosslink.shard
&& self.att.data.target.epoch == best_att.data.target.epoch
{
if self.att.data.slot == best_att.data.slot && self.att.data.index == best_att.data.index {
self.fresh_validators.difference_inplace(covered_validators);
}
}
@@ -80,11 +78,12 @@ pub fn earliest_attestation_validators<T: EthSpec>(
state_attestations
.iter()
// In a single epoch, an attester should only be attesting for one shard.
// In a single epoch, an attester should only be attesting for one slot and index.
// TODO: we avoid including slashable attestations in the state here,
// but maybe we should do something else with them (like construct slashings).
.filter(|existing_attestation| {
existing_attestation.data.crosslink.shard == attestation.data.crosslink.shard
existing_attestation.data.slot == attestation.data.slot
&& existing_attestation.data.index == attestation.data.index
})
.for_each(|existing_attestation| {
// Remove the validators who have signed the existing attestation (they are not new)

View File

@@ -29,7 +29,7 @@ impl AttestationId {
state: &BeaconState<T>,
spec: &ChainSpec,
) -> Vec<u8> {
int_to_bytes8(spec.get_domain(epoch, Domain::Attestation, &state.fork))
int_to_bytes8(spec.get_domain(epoch, Domain::BeaconAttester, &state.fork))
}
pub fn domain_bytes_match(&self, domain_bytes: &[u8]) -> bool {

View File

@@ -7,24 +7,22 @@ pub use persistence::PersistedOperationPool;
use attestation::{earliest_attestation_validators, AttMaxCover};
use attestation_id::AttestationId;
use itertools::Itertools;
use max_cover::maximum_cover;
use parking_lot::RwLock;
use state_processing::per_block_processing::errors::{
AttestationValidationError, AttesterSlashingValidationError, DepositValidationError,
ExitValidationError, ProposerSlashingValidationError, TransferValidationError,
ExitValidationError, ProposerSlashingValidationError,
};
use state_processing::per_block_processing::{
get_slashable_indices_modular, verify_attestation_for_block_inclusion,
verify_attester_slashing, verify_exit, verify_exit_time_independent_only,
verify_proposer_slashing, verify_transfer, verify_transfer_time_independent_only,
VerifySignatures,
verify_proposer_slashing, VerifySignatures,
};
use std::collections::{btree_map::Entry, hash_map, BTreeMap, HashMap, HashSet};
use std::marker::PhantomData;
use types::{
typenum::Unsigned, Attestation, AttesterSlashing, BeaconState, ChainSpec, Deposit, EthSpec,
ProposerSlashing, Transfer, Validator, VoluntaryExit,
ProposerSlashing, Validator, VoluntaryExit,
};
#[derive(Default, Debug)]
@@ -43,8 +41,6 @@ pub struct OperationPool<T: EthSpec + Default> {
proposer_slashings: RwLock<HashMap<u64, ProposerSlashing>>,
/// Map from exiting validator to their exit data.
voluntary_exits: RwLock<HashMap<u64, VoluntaryExit>>,
/// Set of transfers.
transfers: RwLock<HashSet<Transfer>>,
_phantom: PhantomData<T>,
}
@@ -375,44 +371,6 @@ impl<T: EthSpec> OperationPool<T> {
);
}
/// Insert a transfer into the pool, checking it for validity in the process.
pub fn insert_transfer(
&self,
transfer: Transfer,
state: &BeaconState<T>,
spec: &ChainSpec,
) -> Result<(), TransferValidationError> {
// The signature of the transfer isn't hashed, but because we check
// it before we insert into the HashSet, we can't end up with duplicate
// transactions.
verify_transfer_time_independent_only(state, &transfer, VerifySignatures::True, spec)?;
self.transfers.write().insert(transfer);
Ok(())
}
/// Get a list of transfers for inclusion in a block.
// TODO: improve the economic optimality of this function by accounting for
// dependencies between transfers in the same block e.g. A pays B, B pays C
pub fn get_transfers(&self, state: &BeaconState<T>, spec: &ChainSpec) -> Vec<Transfer> {
self.transfers
.read()
.iter()
.filter(|transfer| {
verify_transfer(state, transfer, VerifySignatures::False, spec).is_ok()
})
.sorted_by_key(|transfer| std::cmp::Reverse(transfer.fee))
.take(T::MaxTransfers::to_usize())
.cloned()
.collect()
}
/// Prune the set of transfers by removing all those whose slot has already passed.
pub fn prune_transfers(&self, finalized_state: &BeaconState<T>) {
self.transfers
.write()
.retain(|transfer| transfer.slot > finalized_state.slot)
}
/// Prune all types of transactions given the latest finalized state.
pub fn prune_all(&self, finalized_state: &BeaconState<T>, spec: &ChainSpec) {
self.prune_attestations(finalized_state);
@@ -420,7 +378,6 @@ impl<T: EthSpec> OperationPool<T> {
self.prune_proposer_slashings(finalized_state);
self.prune_attester_slashings(finalized_state, spec);
self.prune_voluntary_exits(finalized_state);
self.prune_transfers(finalized_state);
}
}
@@ -467,7 +424,6 @@ impl<T: EthSpec + Default> PartialEq for OperationPool<T> {
&& *self.attester_slashings.read() == *other.attester_slashings.read()
&& *self.proposer_slashings.read() == *other.proposer_slashings.read()
&& *self.voluntary_exits.read() == *other.voluntary_exits.read()
&& *self.transfers.read() == *other.transfers.read()
}
}
@@ -598,7 +554,7 @@ mod tests {
let mut state = BeaconState::random_for_test(rng);
state.fork = Fork::genesis(MainnetEthSpec::genesis_epoch());
state.fork = Fork::default();
(spec, state)
}
@@ -611,7 +567,7 @@ mod tests {
/// Signed by all validators in `committee[signing_range]` and `committee[extra_signer]`.
fn signed_attestation<R: std::slice::SliceIndex<[usize], Output = [usize]>, E: EthSpec>(
committee: &[usize],
shard: u64,
index: u64,
keypairs: &[Keypair],
signing_range: R,
slot: Slot,
@@ -619,18 +575,31 @@ mod tests {
spec: &ChainSpec,
extra_signer: Option<usize>,
) -> Attestation<E> {
let mut builder = TestingAttestationBuilder::new(state, committee, slot, shard, spec);
let mut builder = TestingAttestationBuilder::new(
AttestationTestTask::Valid,
state,
committee,
slot,
index,
spec,
);
let signers = &committee[signing_range];
let committee_keys = signers.iter().map(|&i| &keypairs[i].sk).collect::<Vec<_>>();
builder.sign(signers, &committee_keys, &state.fork, spec, false);
builder.sign(
AttestationTestTask::Valid,
signers,
&committee_keys,
&state.fork,
spec,
);
extra_signer.map(|c_idx| {
let validator_index = committee[c_idx];
builder.sign(
AttestationTestTask::Valid,
&[validator_index],
&[&keypairs[validator_index].sk],
&state.fork,
spec,
false,
)
});
builder.build()
@@ -662,16 +631,16 @@ mod tests {
attestation_test_state::<MainnetEthSpec>(1);
let slot = state.slot - 1;
let committees = state
.get_crosslink_committees_at_slot(slot)
.get_beacon_committees_at_slot(slot)
.unwrap()
.into_iter()
.map(CrosslinkCommittee::into_owned)
.map(BeaconCommittee::into_owned)
.collect::<Vec<_>>();
for cc in committees {
for bc in committees {
let att1 = signed_attestation(
&cc.committee,
cc.shard,
&bc.committee,
bc.index,
keypairs,
..2,
slot,
@@ -680,8 +649,8 @@ mod tests {
None,
);
let att2 = signed_attestation(
&cc.committee,
cc.shard,
&bc.committee,
bc.index,
keypairs,
..,
slot,
@@ -705,7 +674,7 @@ mod tests {
.unwrap();
assert_eq!(
cc.committee.len() - 2,
bc.committee.len() - 2,
earliest_attestation_validators(&att2, state).num_set_bits()
);
}
@@ -721,10 +690,10 @@ mod tests {
let slot = state.slot - 1;
let committees = state
.get_crosslink_committees_at_slot(slot)
.get_beacon_committees_at_slot(slot)
.unwrap()
.into_iter()
.map(CrosslinkCommittee::into_owned)
.map(BeaconCommittee::into_owned)
.collect::<Vec<_>>();
assert_eq!(
@@ -733,12 +702,12 @@ mod tests {
"we expect just one committee with this many validators"
);
for cc in &committees {
for bc in &committees {
let step_size = 2;
for i in (0..cc.committee.len()).step_by(step_size) {
for i in (0..bc.committee.len()).step_by(step_size) {
let att = signed_attestation(
&cc.committee,
cc.shard,
&bc.committee,
bc.index,
keypairs,
i..i + step_size,
slot,
@@ -790,16 +759,16 @@ mod tests {
let slot = state.slot - 1;
let committees = state
.get_crosslink_committees_at_slot(slot)
.get_beacon_committees_at_slot(slot)
.unwrap()
.into_iter()
.map(CrosslinkCommittee::into_owned)
.map(BeaconCommittee::into_owned)
.collect::<Vec<_>>();
for cc in &committees {
for bc in &committees {
let att = signed_attestation(
&cc.committee,
cc.shard,
&bc.committee,
bc.index,
keypairs,
..,
slot,
@@ -827,20 +796,20 @@ mod tests {
let slot = state.slot - 1;
let committees = state
.get_crosslink_committees_at_slot(slot)
.get_beacon_committees_at_slot(slot)
.unwrap()
.into_iter()
.map(CrosslinkCommittee::into_owned)
.map(BeaconCommittee::into_owned)
.collect::<Vec<_>>();
let step_size = 2;
for cc in &committees {
for bc in &committees {
// Create attestations that overlap on `step_size` validators, like:
// {0,1,2,3}, {2,3,4,5}, {4,5,6,7}, ...
for i in (0..cc.committee.len() - step_size).step_by(step_size) {
for i in (0..bc.committee.len() - step_size).step_by(step_size) {
let att = signed_attestation(
&cc.committee,
cc.shard,
&bc.committee,
bc.index,
keypairs,
i..i + 2 * step_size,
slot,
@@ -875,20 +844,20 @@ mod tests {
let slot = state.slot - 1;
let committees = state
.get_crosslink_committees_at_slot(slot)
.get_beacon_committees_at_slot(slot)
.unwrap()
.into_iter()
.map(CrosslinkCommittee::into_owned)
.map(BeaconCommittee::into_owned)
.collect::<Vec<_>>();
let max_attestations = <MainnetEthSpec as EthSpec>::MaxAttestations::to_usize();
let target_committee_size = spec.target_committee_size as usize;
let insert_attestations = |cc: &OwnedCrosslinkCommittee, step_size| {
let insert_attestations = |bc: &OwnedBeaconCommittee, step_size| {
for i in (0..target_committee_size).step_by(step_size) {
let att = signed_attestation(
&cc.committee,
cc.shard,
&bc.committee,
bc.index,
keypairs,
i..i + step_size,
slot,

View File

@@ -21,8 +21,6 @@ pub struct PersistedOperationPool<T: EthSpec> {
proposer_slashings: Vec<ProposerSlashing>,
/// Voluntary exits.
voluntary_exits: Vec<VoluntaryExit>,
/// Transfers.
transfers: Vec<Transfer>,
}
impl<T: EthSpec> PersistedOperationPool<T> {
@@ -63,15 +61,12 @@ impl<T: EthSpec> PersistedOperationPool<T> {
.map(|(_, exit)| exit.clone())
.collect();
let transfers = operation_pool.transfers.read().iter().cloned().collect();
Self {
attestations,
deposits,
attester_slashings,
proposer_slashings,
voluntary_exits,
transfers,
}
}
@@ -102,7 +97,6 @@ impl<T: EthSpec> PersistedOperationPool<T> {
.map(|exit| (exit.validator_index, exit))
.collect(),
);
let transfers = RwLock::new(self.transfers.into_iter().collect());
OperationPool {
attestations,
@@ -110,7 +104,6 @@ impl<T: EthSpec> PersistedOperationPool<T> {
attester_slashings,
proposer_slashings,
voluntary_exits,
transfers,
_phantom: Default::default(),
}
}

View File

@@ -9,13 +9,13 @@ name = "benches"
harness = false
[dev-dependencies]
criterion = "0.2"
env_logger = "0.6.0"
serde = "1.0"
serde_derive = "1.0"
lazy_static = "1.4"
serde_yaml = "0.8"
eth2_ssz = { path = "../utils/ssz" }
criterion = "0.3.0"
env_logger = "0.7.1"
serde = "1.0.102"
serde_derive = "1.0.102"
lazy_static = "1.4.0"
serde_yaml = "0.8.11"
eth2_ssz = "0.1.2"
beacon_chain = { path = "../../beacon_node/beacon_chain" }
store = { path = "../../beacon_node/store" }
lmd_ghost = { path = "../lmd_ghost" }
@@ -23,15 +23,15 @@ lmd_ghost = { path = "../lmd_ghost" }
[dependencies]
bls = { path = "../utils/bls" }
integer-sqrt = "0.1"
itertools = "0.8"
integer-sqrt = "0.1.2"
itertools = "0.8.1"
eth2_ssz_types = { path = "../utils/ssz_types" }
merkle_proof = { path = "../utils/merkle_proof" }
log = "0.4"
tree_hash = "0.1"
log = "0.4.8"
tree_hash = "0.1.0"
tree_hash_derive = "0.2"
types = { path = "../types" }
rayon = "1.0"
rayon = "1.2.0"
[features]
fake_crypto = ["bls/fake_crypto"]

View File

@@ -3,19 +3,13 @@ use types::*;
/// Returns validator indices which participated in the attestation, sorted by increasing index.
///
/// Spec v0.8.1
/// Spec v0.9.1
pub fn get_attesting_indices<T: EthSpec>(
state: &BeaconState<T>,
attestation_data: &AttestationData,
bitlist: &BitList<T::MaxValidatorsPerCommittee>,
) -> Result<BTreeSet<usize>, BeaconStateError> {
let target_relative_epoch =
RelativeEpoch::from_epoch(state.current_epoch(), attestation_data.target.epoch)?;
let committee = state.get_crosslink_committee_for_shard(
attestation_data.crosslink.shard,
target_relative_epoch,
)?;
let committee = state.get_beacon_committee(attestation_data.slot, attestation_data.index)?;
if bitlist.len() != committee.committee.len() {
return Err(BeaconStateError::InvalidBitfield);

View File

@@ -1,41 +0,0 @@
use tree_hash::TreeHash;
use types::*;
/// Return the compact committee root at `relative_epoch`.
///
/// Spec v0.8.3
pub fn get_compact_committees_root<T: EthSpec>(
state: &BeaconState<T>,
relative_epoch: RelativeEpoch,
spec: &ChainSpec,
) -> Result<Hash256, BeaconStateError> {
let mut committees =
FixedVector::<_, T::ShardCount>::from_elem(CompactCommittee::<T>::default());
let start_shard = state.get_epoch_start_shard(relative_epoch)?;
for committee_number in 0..state.get_committee_count(relative_epoch)? {
let shard = (start_shard + committee_number) % T::ShardCount::to_u64();
for &index in state
.get_crosslink_committee_for_shard(shard, relative_epoch)?
.committee
{
let validator = state
.validators
.get(index)
.ok_or(BeaconStateError::UnknownValidator)?;
committees[shard as usize]
.pubkeys
.push(validator.pubkey.clone())?;
let compact_balance = validator.effective_balance / spec.effective_balance_increment;
// `index` (top 6 bytes) + `slashed` (16th bit) + `compact_balance` (bottom 15 bits)
let compact_validator: u64 =
((index as u64) << 16) + (u64::from(validator.slashed) << 15) + compact_balance;
committees[shard as usize]
.compact_validators
.push(compact_validator)?;
}
}
Ok(Hash256::from_slice(&committees.tree_hash_root()))
}

View File

@@ -6,119 +6,19 @@ type Result<T> = std::result::Result<T, BlockOperationError<Invalid>>;
/// Convert `attestation` to (almost) indexed-verifiable form.
///
/// Spec v0.8.0
/// Spec v0.9.1
pub fn get_indexed_attestation<T: EthSpec>(
state: &BeaconState<T>,
attestation: &Attestation<T>,
) -> Result<IndexedAttestation<T>> {
// Note: we rely on both calls to `get_attesting_indices` to check the bitfield lengths
// against the committee length
let attesting_indices =
get_attesting_indices(state, &attestation.data, &attestation.aggregation_bits)?;
let custody_bit_1_indices =
get_attesting_indices(state, &attestation.data, &attestation.custody_bits)?;
verify!(
custody_bit_1_indices.is_subset(&attesting_indices),
Invalid::CustodyBitfieldNotSubset
);
let custody_bit_0_indices = &attesting_indices - &custody_bit_1_indices;
Ok(IndexedAttestation {
custody_bit_0_indices: VariableList::new(
custody_bit_0_indices
.into_iter()
.map(|x| x as u64)
.collect(),
)?,
custody_bit_1_indices: VariableList::new(
custody_bit_1_indices
.into_iter()
.map(|x| x as u64)
.collect(),
attesting_indices: VariableList::new(
attesting_indices.into_iter().map(|x| x as u64).collect(),
)?,
data: attestation.data.clone(),
signature: attestation.signature.clone(),
})
}
#[cfg(test)]
mod test {
use super::*;
use itertools::{Either, Itertools};
use types::test_utils::*;
#[test]
fn custody_bitfield_indexing() {
let validator_count = 128;
let spec = MinimalEthSpec::default_spec();
let state_builder =
TestingBeaconStateBuilder::<MinimalEthSpec>::from_default_keypairs_file_if_exists(
validator_count,
&spec,
);
let (mut state, keypairs) = state_builder.build();
state.build_all_caches(&spec).unwrap();
state.slot += 1;
let shard = 0;
let cc = state
.get_crosslink_committee_for_shard(shard, RelativeEpoch::Current)
.unwrap();
// Make a third of the validators sign with custody bit 0, a third with custody bit 1
// and a third not sign at all.
assert!(
cc.committee.len() >= 4,
"need at least 4 validators per committee for this test to work"
);
let (mut bit_0_indices, mut bit_1_indices): (Vec<_>, Vec<_>) = cc
.committee
.iter()
.enumerate()
.filter(|(i, _)| i % 3 != 0)
.partition_map(|(i, index)| {
if i % 3 == 1 {
Either::Left(*index)
} else {
Either::Right(*index)
}
});
assert!(!bit_0_indices.is_empty());
assert!(!bit_1_indices.is_empty());
let bit_0_keys = bit_0_indices
.iter()
.map(|validator_index| &keypairs[*validator_index].sk)
.collect::<Vec<_>>();
let bit_1_keys = bit_1_indices
.iter()
.map(|validator_index| &keypairs[*validator_index].sk)
.collect::<Vec<_>>();
let mut attestation_builder =
TestingAttestationBuilder::new(&state, &cc.committee, cc.slot, shard, &spec);
attestation_builder
.sign(&bit_0_indices, &bit_0_keys, &state.fork, &spec, false)
.sign(&bit_1_indices, &bit_1_keys, &state.fork, &spec, true);
let attestation = attestation_builder.build();
let indexed_attestation = get_indexed_attestation(&state, &attestation).unwrap();
bit_0_indices.sort();
bit_1_indices.sort();
assert!(indexed_attestation
.custody_bit_0_indices
.iter()
.copied()
.eq(bit_0_indices.iter().map(|idx| *idx as u64)));
assert!(indexed_attestation
.custody_bit_1_indices
.iter()
.copied()
.eq(bit_1_indices.iter().map(|idx| *idx as u64)));
}
}

View File

@@ -3,7 +3,7 @@ use types::{BeaconStateError as Error, *};
/// Initiate the exit of the validator of the given `index`.
///
/// Spec v0.8.1
/// Spec v0.9.1
pub fn initiate_validator_exit<T: EthSpec>(
state: &mut BeaconState<T>,
index: usize,

View File

@@ -1,11 +1,9 @@
mod get_attesting_indices;
mod get_compact_committees_root;
mod get_indexed_attestation;
mod initiate_validator_exit;
mod slash_validator;
pub use get_attesting_indices::get_attesting_indices;
pub use get_compact_committees_root::get_compact_committees_root;
pub use get_indexed_attestation::get_indexed_attestation;
pub use initiate_validator_exit::initiate_validator_exit;
pub use slash_validator::slash_validator;

View File

@@ -4,7 +4,7 @@ use types::{BeaconStateError as Error, *};
/// Slash the validator with index ``index``.
///
/// Spec v0.8.0
/// Spec v0.9.1
pub fn slash_validator<T: EthSpec>(
state: &mut BeaconState<T>,
slashed_index: usize,
@@ -35,8 +35,7 @@ pub fn slash_validator<T: EthSpec>(
);
// Apply proposer and whistleblower rewards
let proposer_index =
state.get_beacon_proposer_index(state.slot, RelativeEpoch::Current, spec)?;
let proposer_index = state.get_beacon_proposer_index(state.slot, spec)?;
let whistleblower_index = opt_whistleblower_index.unwrap_or(proposer_index);
let whistleblower_reward = validator_effective_balance / spec.whistleblower_reward_quotient;
let proposer_reward = whistleblower_reward / spec.proposer_reward_quotient;

View File

@@ -1,12 +1,11 @@
use super::per_block_processing::{errors::BlockProcessingError, process_deposit};
use crate::common::get_compact_committees_root;
use tree_hash::TreeHash;
use types::typenum::U4294967296;
use types::*;
/// Initialize a `BeaconState` from genesis data.
///
/// Spec v0.8.0
/// Spec v0.9.1
// TODO: this is quite inefficient and we probably want to rethink how we do this
pub fn initialize_beacon_state_from_eth1<T: EthSpec>(
eth1_block_hash: Hash256,
@@ -24,6 +23,9 @@ pub fn initialize_beacon_state_from_eth1<T: EthSpec>(
};
let mut state = BeaconState::new(genesis_time, eth1_data, spec);
// Seed RANDAO with Eth1 entropy
state.fill_randao_mixes_with(eth1_block_hash);
// Process deposits
let leaves: Vec<_> = deposits
.iter()
@@ -35,7 +37,27 @@ pub fn initialize_beacon_state_from_eth1<T: EthSpec>(
process_deposit(&mut state, &deposit, spec, true)?;
}
// Process activations
process_activations(&mut state, spec);
// Now that we have our validators, initialize the caches (including the committees)
state.build_all_caches(spec)?;
Ok(state)
}
/// Determine whether a candidate genesis state is suitable for starting the chain.
///
/// Spec v0.9.1
pub fn is_valid_genesis_state<T: EthSpec>(state: &BeaconState<T>, spec: &ChainSpec) -> bool {
state.genesis_time >= spec.min_genesis_time
&& state.get_active_validator_indices(T::genesis_epoch()).len() as u64
>= spec.min_genesis_active_validator_count
}
/// Activate genesis validators, if their balance is acceptable.
///
/// Spec v0.8.0
pub fn process_activations<T: EthSpec>(state: &mut BeaconState<T>, spec: &ChainSpec) {
for (index, validator) in state.validators.iter_mut().enumerate() {
let balance = state.balances[index];
validator.effective_balance = std::cmp::min(
@@ -47,27 +69,4 @@ pub fn initialize_beacon_state_from_eth1<T: EthSpec>(
validator.activation_epoch = T::genesis_epoch();
}
}
// Now that we have our validators, initialize the caches (including the committees)
state.build_all_caches(spec)?;
// Populate active_index_roots and compact_committees_roots
let indices_list = VariableList::<usize, T::ValidatorRegistryLimit>::from(
state.get_active_validator_indices(T::genesis_epoch()),
);
let active_index_root = Hash256::from_slice(&indices_list.tree_hash_root());
let committee_root = get_compact_committees_root(&state, RelativeEpoch::Current, spec)?;
state.fill_active_index_roots_with(active_index_root);
state.fill_compact_committees_roots_with(committee_root);
Ok(state)
}
/// Determine whether a candidate genesis state is suitable for starting the chain.
///
/// Spec v0.8.1
pub fn is_valid_genesis_state<T: EthSpec>(state: &BeaconState<T>, spec: &ChainSpec) -> bool {
state.genesis_time >= spec.min_genesis_time
&& state.get_active_validator_indices(T::genesis_epoch()).len() as u64
>= spec.min_genesis_active_validator_count
}

View File

@@ -8,7 +8,7 @@ pub mod per_epoch_processing;
pub mod per_slot_processing;
pub mod test_utils;
pub use genesis::{initialize_beacon_state_from_eth1, is_valid_genesis_state};
pub use genesis::{initialize_beacon_state_from_eth1, is_valid_genesis_state, process_activations};
pub use per_block_processing::{
errors::BlockProcessingError, per_block_processing, BlockSignatureStrategy, VerifySignatures,
};

View File

@@ -2,9 +2,7 @@ use crate::common::{initiate_validator_exit, slash_validator};
use errors::{BlockOperationError, BlockProcessingError, HeaderInvalid, IntoWithIndex};
use rayon::prelude::*;
use signature_sets::{block_proposal_signature_set, randao_signature_set};
use std::collections::HashSet;
use std::convert::TryInto;
use std::iter::FromIterator;
use tree_hash::SignedRoot;
use types::*;
@@ -21,9 +19,6 @@ pub use verify_deposit::{
get_existing_validator_index, verify_deposit_merkle_proof, verify_deposit_signature,
};
pub use verify_exit::{verify_exit, verify_exit_time_independent_only};
pub use verify_transfer::{
execute_transfer, verify_transfer, verify_transfer_time_independent_only,
};
pub mod block_processing_builder;
mod block_signature_verifier;
@@ -36,7 +31,6 @@ mod verify_attester_slashing;
mod verify_deposit;
mod verify_exit;
mod verify_proposer_slashing;
mod verify_transfer;
/// The strategy to be used when validating the block's signatures.
#[derive(PartialEq, Clone, Copy)]
@@ -74,7 +68,7 @@ impl VerifySignatures {
/// signature. If it is `None` the signed root is calculated here. This parameter only exists to
/// avoid re-calculating the root when it is already known.
///
/// Spec v0.8.0
/// Spec v0.9.1
pub fn per_block_processing<T: EthSpec>(
mut state: &mut BeaconState<T>,
block: &BeaconBlock<T>,
@@ -128,14 +122,13 @@ pub fn per_block_processing<T: EthSpec>(
verify_signatures,
spec,
)?;
process_transfers(&mut state, &block.body.transfers, verify_signatures, spec)?;
Ok(())
}
/// Processes the block header.
///
/// Spec v0.8.0
/// Spec v0.9.1
pub fn process_block_header<T: EthSpec>(
state: &mut BeaconState<T>,
block: &BeaconBlock<T>,
@@ -158,7 +151,7 @@ pub fn process_block_header<T: EthSpec>(
state.latest_block_header = block.temporary_block_header();
// Verify proposer is not slashed
let proposer_idx = state.get_beacon_proposer_index(block.slot, RelativeEpoch::Current, spec)?;
let proposer_idx = state.get_beacon_proposer_index(block.slot, spec)?;
let proposer = &state.validators[proposer_idx];
verify!(
!proposer.slashed,
@@ -174,7 +167,7 @@ pub fn process_block_header<T: EthSpec>(
/// Verifies the signature of a block.
///
/// Spec v0.8.0
/// Spec v0.9.1
pub fn verify_block_signature<T: EthSpec>(
state: &BeaconState<T>,
block: &BeaconBlock<T>,
@@ -192,7 +185,7 @@ pub fn verify_block_signature<T: EthSpec>(
/// Verifies the `randao_reveal` against the block's proposer pubkey and updates
/// `state.latest_randao_mixes`.
///
/// Spec v0.8.0
/// Spec v0.9.1
pub fn process_randao<T: EthSpec>(
state: &mut BeaconState<T>,
block: &BeaconBlock<T>,
@@ -215,7 +208,7 @@ pub fn process_randao<T: EthSpec>(
/// Update the `state.eth1_data_votes` based upon the `eth1_data` provided.
///
/// Spec v0.8.0
/// Spec v0.9.1
pub fn process_eth1_data<T: EthSpec>(
state: &mut BeaconState<T>,
eth1_data: &Eth1Data,
@@ -240,7 +233,7 @@ pub fn process_eth1_data<T: EthSpec>(
/// Returns `Ok(())` if the validation and state updates completed successfully, otherwise returns
/// an `Err` describing the invalid object or cause of failure.
///
/// Spec v0.8.0
/// Spec v0.9.1
pub fn process_proposer_slashings<T: EthSpec>(
state: &mut BeaconState<T>,
proposer_slashings: &[ProposerSlashing],
@@ -269,7 +262,7 @@ pub fn process_proposer_slashings<T: EthSpec>(
/// Returns `Ok(())` if the validation and state updates completed successfully, otherwise returns
/// an `Err` describing the invalid object or cause of failure.
///
/// Spec v0.8.0
/// Spec v0.9.1
pub fn process_attester_slashings<T: EthSpec>(
state: &mut BeaconState<T>,
attester_slashings: &[AttesterSlashing<T>],
@@ -323,7 +316,7 @@ pub fn process_attester_slashings<T: EthSpec>(
/// Returns `Ok(())` if the validation and state updates completed successfully, otherwise returns
/// an `Err` describing the invalid object or cause of failure.
///
/// Spec v0.8.0
/// Spec v0.9.1
pub fn process_attestations<T: EthSpec>(
state: &mut BeaconState<T>,
attestations: &[Attestation<T>],
@@ -343,14 +336,12 @@ pub fn process_attestations<T: EthSpec>(
})?;
// Update the state in series.
let proposer_index =
state.get_beacon_proposer_index(state.slot, RelativeEpoch::Current, spec)? as u64;
let proposer_index = state.get_beacon_proposer_index(state.slot, spec)? as u64;
for attestation in attestations {
let attestation_slot = state.get_attestation_data_slot(&attestation.data)?;
let pending_attestation = PendingAttestation {
aggregation_bits: attestation.aggregation_bits.clone(),
data: attestation.data.clone(),
inclusion_delay: (state.slot - attestation_slot).as_u64(),
inclusion_delay: (state.slot - attestation.data.slot).as_u64(),
proposer_index,
};
@@ -371,7 +362,7 @@ pub fn process_attestations<T: EthSpec>(
/// Returns `Ok(())` if the validation and state updates completed successfully, otherwise returns
/// an `Err` describing the invalid object or cause of failure.
///
/// Spec v0.8.0
/// Spec v0.9.1
pub fn process_deposits<T: EthSpec>(
state: &mut BeaconState<T>,
deposits: &[Deposit],
@@ -408,7 +399,7 @@ pub fn process_deposits<T: EthSpec>(
/// Process a single deposit, optionally verifying its merkle proof.
///
/// Spec v0.8.1
/// Spec v0.9.1
pub fn process_deposit<T: EthSpec>(
state: &mut BeaconState<T>,
deposit: &Deposit,
@@ -444,7 +435,7 @@ pub fn process_deposit<T: EthSpec>(
} else {
// The signature should be checked for new validators. Return early for a bad
// signature.
if verify_deposit_signature(state, deposit, spec).is_err() {
if verify_deposit_signature(&deposit.data, spec).is_err() {
return Ok(());
}
@@ -474,7 +465,7 @@ pub fn process_deposit<T: EthSpec>(
/// Returns `Ok(())` if the validation and state updates completed successfully, otherwise returns
/// an `Err` describing the invalid object or cause of failure.
///
/// Spec v0.8.0
/// Spec v0.9.1
pub fn process_exits<T: EthSpec>(
state: &mut BeaconState<T>,
voluntary_exits: &[VoluntaryExit],
@@ -496,39 +487,3 @@ pub fn process_exits<T: EthSpec>(
Ok(())
}
/// Validates each `Transfer` and updates the state, short-circuiting on an invalid object.
///
/// Returns `Ok(())` if the validation and state updates completed successfully, otherwise returns
/// an `Err` describing the invalid object or cause of failure.
///
/// Spec v0.8.0
pub fn process_transfers<T: EthSpec>(
state: &mut BeaconState<T>,
transfers: &[Transfer],
verify_signatures: VerifySignatures,
spec: &ChainSpec,
) -> Result<(), BlockProcessingError> {
let expected_transfers = HashSet::<_>::from_iter(transfers).len();
// Verify that there are no duplicate transfers
block_verify!(
transfers.len() == expected_transfers,
BlockProcessingError::DuplicateTransfers {
duplicates: transfers.len().saturating_sub(expected_transfers)
}
);
transfers
.par_iter()
.enumerate()
.try_for_each(|(i, transfer)| {
verify_transfer(&state, transfer, verify_signatures, spec)
.map_err(|e| e.into_with_index(i))
})?;
for (i, transfer) in transfers.iter().enumerate() {
execute_transfer(state, transfer, spec).map_err(|e| e.into_with_index(i))?;
}
Ok(())
}

View File

@@ -1,5 +1,9 @@
use std::convert::TryInto;
use tree_hash::SignedRoot;
use types::test_utils::{TestingBeaconBlockBuilder, TestingBeaconStateBuilder};
use types::test_utils::{
AttestationTestTask, AttesterSlashingTestTask, DepositTestTask, ExitTestTask,
ProposerSlashingTestTask, TestingBeaconBlockBuilder, TestingBeaconStateBuilder,
};
use types::*;
pub struct BlockProcessingBuilder<T: EthSpec> {
@@ -30,6 +34,242 @@ impl<T: EthSpec> BlockProcessingBuilder<T> {
self.state_builder.build_caches(&spec).unwrap();
}
pub fn build_with_n_deposits(
mut self,
num_deposits: u64,
test_task: DepositTestTask,
randao_sk: Option<SecretKey>,
previous_block_root: Option<Hash256>,
spec: &ChainSpec,
) -> (BeaconBlock<T>, BeaconState<T>) {
let (mut state, keypairs) = self.state_builder.build();
let builder = &mut self.block_builder;
builder.set_slot(state.slot);
match previous_block_root {
Some(root) => builder.set_parent_root(root),
None => builder.set_parent_root(Hash256::from_slice(
&state.latest_block_header.signed_root(),
)),
}
let proposer_index = state.get_beacon_proposer_index(state.slot, spec).unwrap();
let keypair = &keypairs[proposer_index];
match randao_sk {
Some(sk) => builder.set_randao_reveal(&sk, &state.fork, spec),
None => builder.set_randao_reveal(&keypair.sk, &state.fork, spec),
}
self.block_builder.insert_deposits(
spec.max_effective_balance,
test_task,
1,
num_deposits,
&mut state,
spec,
);
let block = self.block_builder.build(&keypair.sk, &state.fork, spec);
(block, state)
}
pub fn build_with_n_exits(
mut self,
num_exits: usize,
test_task: ExitTestTask,
randao_sk: Option<SecretKey>,
previous_block_root: Option<Hash256>,
spec: &ChainSpec,
) -> (BeaconBlock<T>, BeaconState<T>) {
let (mut state, keypairs) = self.state_builder.build();
let builder = &mut self.block_builder;
builder.set_slot(state.slot);
match previous_block_root {
Some(root) => builder.set_parent_root(root),
None => builder.set_parent_root(Hash256::from_slice(
&state.latest_block_header.signed_root(),
)),
}
let proposer_index = state.get_beacon_proposer_index(state.slot, spec).unwrap();
let keypair = &keypairs[proposer_index];
match randao_sk {
Some(sk) => builder.set_randao_reveal(&sk, &state.fork, spec),
None => builder.set_randao_reveal(&keypair.sk, &state.fork, spec),
}
match test_task {
ExitTestTask::AlreadyInitiated => {
for _ in 0..2 {
self.block_builder.insert_exit(
test_task,
&mut state,
(0 as usize).try_into().unwrap(),
&keypairs[0].sk,
spec,
)
}
}
_ => {
for (i, keypair) in keypairs.iter().take(num_exits).enumerate() {
self.block_builder.insert_exit(
test_task,
&mut state,
(i as usize).try_into().unwrap(),
&keypair.sk,
spec,
);
}
}
}
let block = self.block_builder.build(&keypair.sk, &state.fork, spec);
(block, state)
}
pub fn build_with_n_attestations(
mut self,
test_task: AttestationTestTask,
num_attestations: u64,
randao_sk: Option<SecretKey>,
previous_block_root: Option<Hash256>,
spec: &ChainSpec,
) -> (BeaconBlock<T>, BeaconState<T>) {
let (state, keypairs) = self.state_builder.build();
let builder = &mut self.block_builder;
builder.set_slot(state.slot);
match previous_block_root {
Some(root) => builder.set_parent_root(root),
None => builder.set_parent_root(Hash256::from_slice(
&state.latest_block_header.signed_root(),
)),
}
let proposer_index = state.get_beacon_proposer_index(state.slot, spec).unwrap();
let keypair = &keypairs[proposer_index];
match randao_sk {
Some(sk) => builder.set_randao_reveal(&sk, &state.fork, spec),
None => builder.set_randao_reveal(&keypair.sk, &state.fork, spec),
}
let all_secret_keys: Vec<&SecretKey> = keypairs.iter().map(|keypair| &keypair.sk).collect();
self.block_builder
.insert_attestations(
test_task,
&state,
&all_secret_keys,
num_attestations as usize,
spec,
)
.unwrap();
let block = self.block_builder.build(&keypair.sk, &state.fork, spec);
(block, state)
}
pub fn build_with_attester_slashing(
mut self,
test_task: AttesterSlashingTestTask,
num_attester_slashings: u64,
randao_sk: Option<SecretKey>,
previous_block_root: Option<Hash256>,
spec: &ChainSpec,
) -> (BeaconBlock<T>, BeaconState<T>) {
let (state, keypairs) = self.state_builder.build();
let builder = &mut self.block_builder;
builder.set_slot(state.slot);
match previous_block_root {
Some(root) => builder.set_parent_root(root),
None => builder.set_parent_root(Hash256::from_slice(
&state.latest_block_header.signed_root(),
)),
}
let proposer_index = state.get_beacon_proposer_index(state.slot, spec).unwrap();
let keypair = &keypairs[proposer_index];
match randao_sk {
Some(sk) => builder.set_randao_reveal(&sk, &state.fork, spec),
None => builder.set_randao_reveal(&keypair.sk, &state.fork, spec),
}
let mut validator_indices = vec![];
let mut secret_keys = vec![];
for i in 0..num_attester_slashings {
validator_indices.push(i);
secret_keys.push(&keypairs[i as usize].sk);
}
for _ in 0..num_attester_slashings {
self.block_builder.insert_attester_slashing(
test_task,
&validator_indices,
&secret_keys,
&state.fork,
spec,
);
}
let block = self.block_builder.build(&keypair.sk, &state.fork, spec);
(block, state)
}
pub fn build_with_proposer_slashing(
mut self,
test_task: ProposerSlashingTestTask,
num_proposer_slashings: u64,
randao_sk: Option<SecretKey>,
previous_block_root: Option<Hash256>,
spec: &ChainSpec,
) -> (BeaconBlock<T>, BeaconState<T>) {
let (state, keypairs) = self.state_builder.build();
let builder = &mut self.block_builder;
builder.set_slot(state.slot);
match previous_block_root {
Some(root) => builder.set_parent_root(root),
None => builder.set_parent_root(Hash256::from_slice(
&state.latest_block_header.signed_root(),
)),
}
let proposer_index = state.get_beacon_proposer_index(state.slot, spec).unwrap();
let keypair = &keypairs[proposer_index];
match randao_sk {
Some(sk) => builder.set_randao_reveal(&sk, &state.fork, spec),
None => builder.set_randao_reveal(&keypair.sk, &state.fork, spec),
}
for i in 0..num_proposer_slashings {
let validator_indices = i;
let secret_keys = &keypairs[i as usize].sk;
self.block_builder.insert_proposer_slashing(
test_task,
validator_indices,
&secret_keys,
&state.fork,
spec,
);
}
let block = self.block_builder.build(&keypair.sk, &state.fork, spec);
(block, state)
}
pub fn build(
mut self,
randao_sk: Option<SecretKey>,
@@ -48,9 +288,7 @@ impl<T: EthSpec> BlockProcessingBuilder<T> {
)),
}
let proposer_index = state
.get_beacon_proposer_index(state.slot, RelativeEpoch::Current, spec)
.unwrap();
let proposer_index = state.get_beacon_proposer_index(state.slot, spec).unwrap();
let keypair = &keypairs[proposer_index];
match randao_sk {

View File

@@ -86,7 +86,6 @@ impl<'a, T: EthSpec> BlockSignatureVerifier<'a, T> {
* Deposits are not included because they can legally have invalid signatures.
*/
verifier.include_exits()?;
verifier.include_transfers()?;
verifier.verify()
}
@@ -209,19 +208,4 @@ impl<'a, T: EthSpec> BlockSignatureVerifier<'a, T> {
Ok(())
}
/// Includes all signatures in `self.block.body.transfers` for verification.
fn include_transfers(&mut self) -> Result<()> {
let mut sets = self
.block
.body
.transfers
.iter()
.map(|transfer| transfer_signature_set(&self.state, transfer, &self.spec))
.collect::<SignatureSetResult<_>>()?;
self.sets.append(&mut sets);
Ok(())
}
}

View File

@@ -16,9 +16,6 @@ pub enum BlockProcessingError {
expected: usize,
found: usize,
},
DuplicateTransfers {
duplicates: usize,
},
HeaderInvalid {
reason: HeaderInvalid,
},
@@ -46,10 +43,6 @@ pub enum BlockProcessingError {
index: usize,
reason: ExitInvalid,
},
TransferInvalid {
index: usize,
reason: TransferInvalid,
},
BeaconStateError(BeaconStateError),
SignatureSetError(SignatureSetError),
SszTypesError(ssz_types::Error),
@@ -119,8 +112,7 @@ impl_into_block_processing_error_with_index!(
IndexedAttestationInvalid,
AttestationInvalid,
DepositInvalid,
ExitInvalid,
TransferInvalid
ExitInvalid
);
pub type HeaderValidationError = BlockOperationError<HeaderInvalid>;
@@ -129,7 +121,6 @@ pub type ProposerSlashingValidationError = BlockOperationError<ProposerSlashingI
pub type AttestationValidationError = BlockOperationError<AttestationInvalid>;
pub type DepositValidationError = BlockOperationError<DepositInvalid>;
pub type ExitValidationError = BlockOperationError<ExitInvalid>;
pub type TransferValidationError = BlockOperationError<TransferInvalid>;
#[derive(Debug, PartialEq)]
pub enum BlockOperationError<T> {
@@ -174,10 +165,10 @@ pub enum HeaderInvalid {
pub enum ProposerSlashingInvalid {
/// The proposer index is not a known validator.
ProposerUnknown(u64),
/// The two proposal have different epochs.
/// The two proposal have different slots.
///
/// (proposal_1_slot, proposal_2_slot)
ProposalEpochMismatch(Slot, Slot),
ProposalSlotMismatch(Slot, Slot),
/// The proposals are identical and therefore not slashable.
ProposalsIdentical,
/// The specified proposer cannot be slashed because they are already slashed, or not active.
@@ -190,8 +181,6 @@ pub enum ProposerSlashingInvalid {
#[derive(Debug, PartialEq)]
pub enum AttesterSlashingInvalid {
/// The attestation data is identical, an attestation cannot conflict with itself.
AttestationDataIdentical,
/// The attestations were not in conflict.
NotSlashable,
/// The first `IndexedAttestation` was invalid.
@@ -209,8 +198,8 @@ pub enum AttesterSlashingInvalid {
/// Describes why an object is invalid.
#[derive(Debug, PartialEq)]
pub enum AttestationInvalid {
/// Shard exceeds SHARD_COUNT.
BadShard,
/// Commmittee index exceeds number of committees in that slot.
BadCommitteeIndex,
/// Attestation included before the inclusion delay.
IncludedTooEarly {
state: Slot,
@@ -231,38 +220,18 @@ pub enum AttestationInvalid {
attestation: Checkpoint,
is_current: bool,
},
/// Attestation crosslink root does not match the state crosslink root for the attestations
/// slot.
BadParentCrosslinkHash,
/// Attestation crosslink start epoch does not match the end epoch of the state crosslink.
BadParentCrosslinkStartEpoch,
/// Attestation crosslink end epoch does not match the expected value.
BadParentCrosslinkEndEpoch,
/// The custody bitfield has some bits set `true`. This is not allowed in phase 0.
CustodyBitfieldHasSetBits,
/// There are no set bits on the attestation -- an attestation must be signed by at least one
/// validator.
AggregationBitfieldIsEmpty,
/// The custody bitfield length is not the smallest possible size to represent the committee.
BadCustodyBitfieldLength {
committee_len: usize,
bitfield_len: usize,
},
/// The aggregation bitfield length is not the smallest possible size to represent the committee.
BadAggregationBitfieldLength {
committee_len: usize,
bitfield_len: usize,
},
/// The bits set in the custody bitfield are not a subset of those set in the aggregation bits.
CustodyBitfieldNotSubset,
/// There was no known committee in this `epoch` for the given shard and slot.
NoCommitteeForShard { shard: u64, slot: Slot },
/// The validator index was unknown.
UnknownValidator(u64),
/// The attestation signature verification failed.
BadSignature,
/// The shard block root was not set to zero. This is a phase 0 requirement.
ShardBlockRootNotZero,
/// The indexed attestation created from this attestation was found to be invalid.
BadIndexedAttestation(IndexedAttestationInvalid),
}
@@ -284,14 +253,6 @@ impl From<BlockOperationError<IndexedAttestationInvalid>>
#[derive(Debug, PartialEq)]
pub enum IndexedAttestationInvalid {
/// The custody bit 0 validators intersect with the bit 1 validators.
CustodyBitValidatorsIntersect,
/// The custody bitfield has some bits set `true`. This is not allowed in phase 0.
CustodyBitfieldHasSetBits,
/// The custody bitfield violated a type-level bound.
CustodyBitfieldBoundsError(ssz_types::Error),
/// No validator indices were specified.
NoValidatorIndices,
/// The number of indices exceeds the global maximum.
///
/// (max_indices, indices_given)
@@ -311,8 +272,6 @@ pub enum IndexedAttestationInvalid {
#[derive(Debug, PartialEq)]
pub enum DepositInvalid {
/// The deposit index does not match the state index.
BadIndex { state: u64, deposit: u64 },
/// The signature (proof-of-possession) does not match the given pubkey.
BadSignature,
/// The signature or pubkey does not represent a valid BLS point.
@@ -331,7 +290,7 @@ pub enum ExitInvalid {
/// The specified validator has a non-maximum exit epoch.
AlreadyExited(u64),
/// The specified validator has already initiated exit.
AlreadyInitiatedExited(u64),
AlreadyInitiatedExit(u64),
/// The exit is for a future epoch.
FutureEpoch { state: Epoch, exit: Epoch },
/// The validator has not been active for long enough.
@@ -345,56 +304,3 @@ pub enum ExitInvalid {
/// been invalid or an internal error occurred.
SignatureSetError(SignatureSetError),
}
#[derive(Debug, PartialEq)]
pub enum TransferInvalid {
/// The validator indicated by `transfer.from` is unknown.
FromValidatorUnknown(u64),
/// The validator indicated by `transfer.to` is unknown.
ToValidatorUnknown(u64),
/// The balance of `transfer.from` is insufficient.
///
/// (required, available)
FromBalanceInsufficient(u64, u64),
/// Adding `transfer.fee` to `transfer.amount` causes an overflow.
///
/// (transfer_fee, transfer_amount)
FeeOverflow(u64, u64),
/// This transfer would result in the `transfer.from` account to have `0 < balance <
/// min_deposit_amount`
///
/// (resulting_amount, min_deposit_amount)
SenderDust(u64, u64),
/// This transfer would result in the `transfer.to` account to have `0 < balance <
/// min_deposit_amount`
///
/// (resulting_amount, min_deposit_amount)
RecipientDust(u64, u64),
/// The state slot does not match `transfer.slot`.
///
/// (state_slot, transfer_slot)
StateSlotMismatch(Slot, Slot),
/// The `transfer.slot` is in the past relative to the state slot.
///
///
/// (state_slot, transfer_slot)
TransferSlotInPast(Slot, Slot),
/// The `transfer.from` validator has been activated and is not withdrawable.
///
/// (from_validator)
FromValidatorIneligibleForTransfer(u64),
/// The validators withdrawal credentials do not match `transfer.pubkey`.
///
/// (state_credentials, transfer_pubkey_credentials)
WithdrawalCredentialsMismatch(Hash256, Hash256),
/// The deposit was not signed by `deposit.pubkey`.
BadSignature,
/// Overflow when adding to `transfer.to` balance.
///
/// (to_balance, transfer_amount)
ToBalanceOverflow(u64, u64),
/// Overflow when adding to beacon proposer balance.
///
/// (proposer_balance, transfer_fee)
ProposerBalanceOverflow(u64, u64),
}

View File

@@ -1,8 +1,6 @@
use super::errors::{BlockOperationError, IndexedAttestationInvalid as Invalid};
use super::signature_sets::indexed_attestation_signature_set;
use crate::VerifySignatures;
use std::collections::HashSet;
use std::iter::FromIterator;
use types::*;
type Result<T> = std::result::Result<T, BlockOperationError<Invalid>>;
@@ -13,38 +11,26 @@ fn error(reason: Invalid) -> BlockOperationError<Invalid> {
/// Verify an `IndexedAttestation`.
///
/// Spec v0.8.0
/// Spec v0.9.1
pub fn is_valid_indexed_attestation<T: EthSpec>(
state: &BeaconState<T>,
indexed_attestation: &IndexedAttestation<T>,
verify_signatures: VerifySignatures,
spec: &ChainSpec,
) -> Result<()> {
let bit_0_indices = &indexed_attestation.custody_bit_0_indices;
let bit_1_indices = &indexed_attestation.custody_bit_1_indices;
// Verify no index has custody bit equal to 1 [to be removed in phase 1]
verify!(bit_1_indices.is_empty(), Invalid::CustodyBitfieldHasSetBits);
let indices = &indexed_attestation.attesting_indices;
// Verify max number of indices
let total_indices = bit_0_indices.len() + bit_1_indices.len();
verify!(
total_indices <= T::MaxValidatorsPerCommittee::to_usize(),
Invalid::MaxIndicesExceed(T::MaxValidatorsPerCommittee::to_usize(), total_indices)
indices.len() <= T::MaxValidatorsPerCommittee::to_usize(),
Invalid::MaxIndicesExceed(T::MaxValidatorsPerCommittee::to_usize(), indices.len())
);
// Verify index sets are disjoint
let custody_bit_intersection: HashSet<&u64> =
&HashSet::from_iter(bit_0_indices.iter()) & &HashSet::from_iter(bit_1_indices.iter());
verify!(
custody_bit_intersection.is_empty(),
Invalid::CustodyBitValidatorsIntersect
);
// Check that both vectors of indices are sorted
// Check that indices are sorted
let check_sorted = |list: &[u64]| -> Result<()> {
list.windows(2).enumerate().try_for_each(|(i, pair)| {
if pair[0] >= pair[1] {
// The spec allows duplicates, so use strict comparison (>).
if pair[0] > pair[1] {
Err(error(Invalid::BadValidatorIndicesOrdering(i)))
} else {
Ok(())
@@ -52,8 +38,7 @@ pub fn is_valid_indexed_attestation<T: EthSpec>(
})?;
Ok(())
};
check_sorted(&bit_0_indices)?;
check_sorted(&bit_1_indices)?;
check_sorted(indices)?;
if verify_signatures.is_true() {
verify!(

View File

@@ -2,14 +2,13 @@
//! validated individually, or alongside in others in a potentially cheaper bulk operation.
//!
//! This module exposes one function to extract each type of `SignatureSet` from a `BeaconBlock`.
use bls::SignatureSet;
use bls::{SignatureSet, SignedMessage};
use std::convert::TryInto;
use tree_hash::{SignedRoot, TreeHash};
use types::{
AggregateSignature, AttestationDataAndCustodyBit, AttesterSlashing, BeaconBlock,
BeaconBlockHeader, BeaconState, BeaconStateError, ChainSpec, Deposit, Domain, EthSpec, Fork,
Hash256, IndexedAttestation, ProposerSlashing, PublicKey, RelativeEpoch, Signature, Transfer,
VoluntaryExit,
AggregateSignature, AttesterSlashing, BeaconBlock, BeaconBlockHeader, BeaconState,
BeaconStateError, ChainSpec, DepositData, Domain, EthSpec, Hash256, IndexedAttestation,
ProposerSlashing, PublicKey, Signature, VoluntaryExit,
};
pub type Result<T> = std::result::Result<T, Error>;
@@ -42,8 +41,7 @@ pub fn block_proposal_signature_set<'a, T: EthSpec>(
block_signed_root: Option<Hash256>,
spec: &'a ChainSpec,
) -> Result<SignatureSet<'a>> {
let proposer_index =
state.get_beacon_proposer_index(block.slot, RelativeEpoch::Current, spec)?;
let proposer_index = state.get_beacon_proposer_index(block.slot, spec)?;
let block_proposer = &state
.validators
.get(proposer_index)
@@ -75,8 +73,7 @@ pub fn randao_signature_set<'a, T: EthSpec>(
block: &'a BeaconBlock<T>,
spec: &'a ChainSpec,
) -> Result<SignatureSet<'a>> {
let block_proposer = &state.validators
[state.get_beacon_proposer_index(block.slot, RelativeEpoch::Current, spec)?];
let block_proposer = &state.validators[state.get_beacon_proposer_index(block.slot, spec)?];
let domain = spec.get_domain(
block.slot.epoch(T::slots_per_epoch()),
@@ -141,31 +138,20 @@ pub fn indexed_attestation_signature_set<'a, 'b, T: EthSpec>(
indexed_attestation: &'b IndexedAttestation<T>,
spec: &'a ChainSpec,
) -> Result<SignatureSet<'a>> {
let message_0 = AttestationDataAndCustodyBit {
data: indexed_attestation.data.clone(),
custody_bit: false,
}
.tree_hash_root();
let message_1 = AttestationDataAndCustodyBit {
data: indexed_attestation.data.clone(),
custody_bit: true,
}
.tree_hash_root();
let message = indexed_attestation.data.tree_hash_root();
let signed_message = SignedMessage::new(
get_pubkeys(state, &indexed_attestation.attesting_indices)?,
message,
);
let domain = spec.get_domain(
indexed_attestation.data.target.epoch,
Domain::Attestation,
Domain::BeaconAttester,
&state.fork,
);
Ok(SignatureSet::dual(
signature,
message_0,
get_pubkeys(state, &indexed_attestation.custody_bit_0_indices)?,
message_1,
get_pubkeys(state, &indexed_attestation.custody_bit_1_indices)?,
domain,
))
Ok(SignatureSet::new(signature, vec![signed_message], domain))
}
/// Returns the signature set for the given `attester_slashing` and corresponding `pubkeys`.
@@ -194,18 +180,17 @@ pub fn attester_slashing_signature_sets<'a, T: EthSpec>(
///
/// This method is separate to `deposit_signature_set` to satisfy lifetime requirements.
pub fn deposit_pubkey_signature_message(
deposit: &Deposit,
deposit_data: &DepositData,
) -> Option<(PublicKey, Signature, Vec<u8>)> {
let pubkey = (&deposit.data.pubkey).try_into().ok()?;
let signature = (&deposit.data.signature).try_into().ok()?;
let message = deposit.data.signed_root();
let pubkey = (&deposit_data.pubkey).try_into().ok()?;
let signature = (&deposit_data.signature).try_into().ok()?;
let message = deposit_data.signed_root();
Some((pubkey, signature, message))
}
/// Returns the signature set for some set of deposit signatures, made with
/// `deposit_pubkey_signature_message`.
pub fn deposit_signature_set<'a, T: EthSpec>(
state: &'a BeaconState<T>,
pub fn deposit_signature_set<'a>(
pubkey_signature_message: &'a (PublicKey, Signature, Vec<u8>),
spec: &'a ChainSpec,
) -> SignatureSet<'a> {
@@ -213,9 +198,12 @@ pub fn deposit_signature_set<'a, T: EthSpec>(
// Note: Deposits are valid across forks, thus the deposit domain is computed
// with the fork zeroed.
let domain = spec.get_domain(state.current_epoch(), Domain::Deposit, &Fork::default());
SignatureSet::single(signature, pubkey, message.clone(), domain)
SignatureSet::single(
signature,
pubkey,
message.clone(),
spec.get_deposit_domain(),
)
}
/// Returns a signature set that is valid if the `VoluntaryExit` was signed by the indicated
@@ -242,28 +230,6 @@ pub fn exit_signature_set<'a, T: EthSpec>(
))
}
/// Returns a signature set that is valid if the `Transfer` was signed by `transfer.pubkey`.
pub fn transfer_signature_set<'a, T: EthSpec>(
state: &'a BeaconState<T>,
transfer: &'a Transfer,
spec: &'a ChainSpec,
) -> Result<SignatureSet<'a>> {
let domain = spec.get_domain(
transfer.slot.epoch(T::slots_per_epoch()),
Domain::Transfer,
&state.fork,
);
let message = transfer.signed_root();
Ok(SignatureSet::single(
&transfer.signature,
&transfer.pubkey,
message,
domain,
))
}
/// Maps validator indices to public keys.
fn get_pubkeys<'a, 'b, T, I>(
state: &'a BeaconState<T>,

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,6 @@ use super::errors::{AttestationInvalid as Invalid, BlockOperationError};
use super::VerifySignatures;
use crate::common::get_indexed_attestation;
use crate::per_block_processing::is_valid_indexed_attestation;
use tree_hash::TreeHash;
use types::*;
type Result<T> = std::result::Result<T, BlockOperationError<Invalid>>;
@@ -16,7 +15,7 @@ fn error(reason: Invalid) -> BlockOperationError<Invalid> {
///
/// Optionally verifies the aggregate signature, depending on `verify_signatures`.
///
/// Spec v0.8.0
/// Spec v0.9.1
pub fn verify_attestation_for_block_inclusion<T: EthSpec>(
state: &BeaconState<T>,
attestation: &Attestation<T>,
@@ -25,22 +24,19 @@ pub fn verify_attestation_for_block_inclusion<T: EthSpec>(
) -> Result<()> {
let data = &attestation.data;
// Check attestation slot.
let attestation_slot = state.get_attestation_data_slot(&data)?;
verify!(
attestation_slot + spec.min_attestation_inclusion_delay <= state.slot,
data.slot + spec.min_attestation_inclusion_delay <= state.slot,
Invalid::IncludedTooEarly {
state: state.slot,
delay: spec.min_attestation_inclusion_delay,
attestation: attestation_slot
attestation: data.slot,
}
);
verify!(
state.slot <= attestation_slot + T::slots_per_epoch(),
state.slot <= data.slot + T::slots_per_epoch(),
Invalid::IncludedTooLate {
state: state.slot,
attestation: attestation_slot
attestation: data.slot,
}
);
@@ -53,7 +49,7 @@ pub fn verify_attestation_for_block_inclusion<T: EthSpec>(
/// Returns a descriptive `Err` if the attestation is malformed or does not accurately reflect the
/// prior blocks in `state`.
///
/// Spec v0.8.0
/// Spec v0.9.1
pub fn verify_attestation_for_state<T: EthSpec>(
state: &BeaconState<T>,
attestation: &Attestation<T>,
@@ -61,36 +57,14 @@ pub fn verify_attestation_for_state<T: EthSpec>(
spec: &ChainSpec,
) -> Result<()> {
let data = &attestation.data;
verify!(
data.crosslink.shard < T::ShardCount::to_u64(),
Invalid::BadShard
);
// Verify the Casper FFG vote and crosslink data.
let parent_crosslink = verify_casper_ffg_vote(attestation, state)?;
verify!(
data.crosslink.parent_root == Hash256::from_slice(&parent_crosslink.tree_hash_root()),
Invalid::BadParentCrosslinkHash
);
verify!(
data.crosslink.start_epoch == parent_crosslink.end_epoch,
Invalid::BadParentCrosslinkStartEpoch
);
verify!(
data.crosslink.end_epoch
== std::cmp::min(
data.target.epoch,
parent_crosslink.end_epoch + spec.max_epochs_per_crosslink
),
Invalid::BadParentCrosslinkEndEpoch
data.index < state.get_committee_count_at_slot(data.slot)?,
Invalid::BadCommitteeIndex
);
// Crosslink data root is zero (to be removed in phase 1).
verify!(
attestation.data.crosslink.data_root == Hash256::zero(),
Invalid::ShardBlockRootNotZero
);
// Verify the Casper FFG vote.
verify_casper_ffg_vote(attestation, state)?;
// Check signature and bitfields
let indexed_attestation = get_indexed_attestation(state, attestation)?;
@@ -101,13 +75,11 @@ pub fn verify_attestation_for_state<T: EthSpec>(
/// Check target epoch and source checkpoint.
///
/// Return the parent crosslink for further checks.
///
/// Spec v0.8.0
fn verify_casper_ffg_vote<'a, T: EthSpec>(
/// Spec v0.9.1
fn verify_casper_ffg_vote<T: EthSpec>(
attestation: &Attestation<T>,
state: &'a BeaconState<T>,
) -> Result<&'a Crosslink> {
state: &BeaconState<T>,
) -> Result<()> {
let data = &attestation.data;
if data.target.epoch == state.current_epoch() {
verify!(
@@ -118,7 +90,7 @@ fn verify_casper_ffg_vote<'a, T: EthSpec>(
is_current: true,
}
);
Ok(state.get_current_crosslink(data.crosslink.shard)?)
Ok(())
} else if data.target.epoch == state.previous_epoch() {
verify!(
data.source == state.previous_justified_checkpoint,
@@ -128,7 +100,7 @@ fn verify_casper_ffg_vote<'a, T: EthSpec>(
is_current: false,
}
);
Ok(state.get_previous_crosslink(data.crosslink.shard)?)
Ok(())
} else {
Err(error(Invalid::BadTargetEpoch))
}

View File

@@ -15,7 +15,7 @@ fn error(reason: Invalid) -> BlockOperationError<Invalid> {
///
/// Returns `Ok(())` if the `AttesterSlashing` is valid, otherwise indicates the reason for invalidity.
///
/// Spec v0.8.1
/// Spec v0.9.1
pub fn verify_attester_slashing<T: EthSpec>(
state: &BeaconState<T>,
attester_slashing: &AttesterSlashing<T>,
@@ -47,7 +47,7 @@ pub fn verify_attester_slashing<T: EthSpec>(
///
/// Returns Ok(indices) if `indices.len() > 0`.
///
/// Spec v0.8.1
/// Spec v0.9.1
pub fn get_slashable_indices<T: EthSpec>(
state: &BeaconState<T>,
attester_slashing: &AttesterSlashing<T>,
@@ -71,15 +71,13 @@ where
let attestation_2 = &attester_slashing.attestation_2;
let attesting_indices_1 = attestation_1
.custody_bit_0_indices
.attesting_indices
.iter()
.chain(&attestation_1.custody_bit_1_indices)
.cloned()
.collect::<BTreeSet<_>>();
let attesting_indices_2 = attestation_2
.custody_bit_0_indices
.attesting_indices
.iter()
.chain(&attestation_2.custody_bit_1_indices)
.cloned()
.collect::<BTreeSet<_>>();

View File

@@ -14,17 +14,13 @@ fn error(reason: DepositInvalid) -> BlockOperationError<DepositInvalid> {
/// Verify `Deposit.pubkey` signed `Deposit.signature`.
///
/// Spec v0.8.0
pub fn verify_deposit_signature<T: EthSpec>(
state: &BeaconState<T>,
deposit: &Deposit,
spec: &ChainSpec,
) -> Result<()> {
let deposit_signature_message = deposit_pubkey_signature_message(deposit)
/// Spec v0.9.1
pub fn verify_deposit_signature(deposit_data: &DepositData, spec: &ChainSpec) -> Result<()> {
let deposit_signature_message = deposit_pubkey_signature_message(&deposit_data)
.ok_or_else(|| error(DepositInvalid::BadBlsBytes))?;
verify!(
deposit_signature_set(state, &deposit_signature_message, spec).is_valid(),
deposit_signature_set(&deposit_signature_message, spec).is_valid(),
DepositInvalid::BadSignature
);
@@ -50,7 +46,7 @@ pub fn get_existing_validator_index<T: EthSpec>(
/// The deposit index is provided as a parameter so we can check proofs
/// before they're due to be processed, and in parallel.
///
/// Spec v0.8.0
/// Spec v0.9.1
pub fn verify_deposit_merkle_proof<T: EthSpec>(
state: &BeaconState<T>,
deposit: &Deposit,

View File

@@ -13,7 +13,7 @@ fn error(reason: ExitInvalid) -> BlockOperationError<ExitInvalid> {
///
/// Returns `Ok(())` if the `Exit` is valid, otherwise indicates the reason for invalidity.
///
/// Spec v0.8.0
/// Spec v0.9.1
pub fn verify_exit<T: EthSpec>(
state: &BeaconState<T>,
exit: &VoluntaryExit,
@@ -25,7 +25,7 @@ pub fn verify_exit<T: EthSpec>(
/// Like `verify_exit` but doesn't run checks which may become true in future states.
///
/// Spec v0.8.0
/// Spec v0.9.1
pub fn verify_exit_time_independent_only<T: EthSpec>(
state: &BeaconState<T>,
exit: &VoluntaryExit,
@@ -37,7 +37,7 @@ pub fn verify_exit_time_independent_only<T: EthSpec>(
/// Parametric version of `verify_exit` that skips some checks if `time_independent_only` is true.
///
/// Spec v0.8.0
/// Spec v0.9.1
fn verify_exit_parametric<T: EthSpec>(
state: &BeaconState<T>,
exit: &VoluntaryExit,

Some files were not shown because too many files have changed in this diff Show More