-
Notifications
You must be signed in to change notification settings - Fork 69
feat: initialize lean node architecture #679
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 13 commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
e20ffc5
feat: add skeleton codes for validator service
syjn99 f26095a
chore: add LeanChainService
syjn99 2a6f951
chore: [WIP] remove ream-p2p dependency from ream-chain-lean to resol…
syjn99 cb8449a
feat: add LeanChain that holds necessary consensus information
syjn99 98843f0
feat: implement get_current_slot using network_spec
syjn99 ac429b8
feat: implement actual tick function in LeanChainService and Validato…
syjn99 32b3fcd
chore: make clippy happy
syjn99 8bcff55
chore: add some comments
syjn99 68882f1
chore: rollback debug logs
syjn99 0eed944
fix: calculation of elapsed
syjn99 2ff6a0d
feat: set genesis time if it is in the past & add num_validators for …
syjn99 1468e18
docs: adding comments for design decision and responsibility
syjn99 8033f4d
docs: update responsibility for network service (static peers)
syjn99 186fbab
Merge branch 'master' into feat/init-lean-arch
syjn99 6ea1283
fix: resolve discrepancy between 3sf-mini impl
syjn99 32330ca
fix: small nit on genesis handling in get_fork_choice_head
syjn99 15f9bfc
feat: send vote via channel to LeanChainService
syjn99 9f12a83
feat: reimpl block receive func
syjn99 92a0fd2
chore: import anyhow::anyhow
syjn99 96af999
chore: delete staker.rs
syjn99 ce0bf38
chore: apply Kayden's review (no abbreviation & clear logs
syjn99 fe88be5
chore: apply review from O (improve docs)
syjn99 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,3 @@ | ||
| SECONDS_PER_SLOT: 12 | ||
| GENESIS_TIME: 0 | ||
| NUM_VALIDATORS: 1 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| use alloy_primitives::B256; | ||
| use ream_consensus_lean::{block::Block, state::LeanState}; | ||
| use ream_network_spec::networks::lean_network_spec; | ||
| use ssz_types::VariableList; | ||
| use tree_hash::TreeHash; | ||
|
|
||
| fn genesis_block(state_root: B256) -> Block { | ||
| Block { | ||
| slot: 1, | ||
| parent: B256::ZERO, | ||
| votes: VariableList::empty(), | ||
| state_root, | ||
| } | ||
| } | ||
|
|
||
| fn genesis_state(num_validators: u64) -> LeanState { | ||
| LeanState::new(num_validators) | ||
| } | ||
|
|
||
| /// Setup the genesis block and state for the Lean chain. | ||
| /// | ||
| /// Reference: https://github.com/ethereum/research/blob/d225a6775a9b184b5c1fd6c830cc58a375d9535f/3sf-mini/test_p2p.py#L119-L131 | ||
| pub fn setup_genesis() -> (Block, LeanState) { | ||
| let genesis_state = genesis_state(lean_network_spec().num_validators); | ||
| let genesis_block = genesis_block(genesis_state.tree_hash_root()); | ||
|
|
||
| (genesis_block, genesis_state) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,207 @@ | ||
| use std::collections::HashMap; | ||
|
|
||
| use alloy_primitives::B256; | ||
| use ream_consensus_lean::{ | ||
| QueueItem, block::Block, get_fork_choice_head, get_latest_justified_hash, is_justifiable_slot, | ||
| process_block, state::LeanState, vote::Vote, | ||
| }; | ||
| use ssz_types::VariableList; | ||
| use tree_hash::TreeHash; | ||
|
|
||
| use crate::slot::get_current_slot; | ||
|
|
||
| /// [LeanChain] represents the state that the Lean node should maintain. | ||
| /// | ||
| /// Most of the fields are based on the Python implementation of [`Staker`](https://github.com/ethereum/research/blob/d225a6775a9b184b5c1fd6c830cc58a375d9535f/3sf-mini/p2p.py#L15-L42), | ||
| /// but doesn't include `validator_id` as a node should manage multiple validators. | ||
| #[derive(Clone, Debug)] | ||
| pub struct LeanChain { | ||
| pub chain: HashMap<B256, Block>, | ||
| pub post_states: HashMap<B256, LeanState>, | ||
| pub known_votes: Vec<Vote>, | ||
| pub new_votes: Vec<Vote>, | ||
| pub dependencies: HashMap<B256, Vec<QueueItem>>, | ||
| pub genesis_hash: B256, | ||
| pub num_validators: u64, | ||
| pub safe_target: B256, | ||
| pub head: B256, | ||
| } | ||
|
|
||
| impl LeanChain { | ||
| pub fn new(genesis_block: Block, genesis_state: LeanState) -> LeanChain { | ||
| let genesis_hash = genesis_block.tree_hash_root(); | ||
|
|
||
| LeanChain { | ||
| // Votes that we have received and taken into account | ||
| known_votes: Vec::new(), | ||
| // Votes that we have received but not yet taken into account | ||
| new_votes: Vec::new(), | ||
| // Objects that we will process once we have processed their parents | ||
| dependencies: HashMap::new(), | ||
| // Initialize the chain with the genesis block | ||
| genesis_hash, | ||
| num_validators: genesis_state.config.num_validators, | ||
| // Block that it is safe to use to vote as the target | ||
| // Diverge from Python implementation: Use genesis hash instead of `None` | ||
| safe_target: genesis_hash, | ||
| // Head of the chain | ||
| head: genesis_hash, | ||
| // {block_hash: block} for all blocks that we know about | ||
| chain: HashMap::from([(genesis_hash, genesis_block)]), | ||
| // {block_hash: post_state} for all blocks that we know about | ||
| post_states: HashMap::from([(genesis_hash, genesis_state)]), | ||
| } | ||
| } | ||
|
|
||
| pub fn latest_justified_hash(&self) -> Option<B256> { | ||
| get_latest_justified_hash(&self.post_states) | ||
| } | ||
|
|
||
| pub fn latest_finalized_hash(&self) -> Option<B256> { | ||
| self.post_states | ||
| .get(&self.head) | ||
| .map(|state| state.latest_finalized_hash) | ||
| } | ||
syjn99 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| /// Compute the latest block that the staker is allowed to choose as the target | ||
| pub fn compute_safe_target(&self) -> anyhow::Result<B256> { | ||
| let justified_hash = get_latest_justified_hash(&self.post_states) | ||
| .ok_or_else(|| anyhow::anyhow!("No justified hash found in post states"))?; | ||
|
|
||
| get_fork_choice_head( | ||
| &self.chain, | ||
| &justified_hash, | ||
| &self.new_votes, | ||
| self.num_validators * 2 / 3, | ||
| ) | ||
| } | ||
syjn99 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| /// Process new votes that the staker has received. Vote processing is done | ||
| /// at a particular time, because of safe target and view merge rule | ||
| pub fn accept_new_votes(&mut self) -> anyhow::Result<()> { | ||
| for new_vote in self.new_votes.drain(..) { | ||
| if !self.known_votes.contains(&new_vote) { | ||
| self.known_votes.push(new_vote); | ||
| } | ||
| } | ||
|
|
||
| self.recompute_head()?; | ||
| Ok(()) | ||
| } | ||
syjn99 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| /// Done upon processing new votes or a new block | ||
| fn recompute_head(&mut self) -> anyhow::Result<()> { | ||
| let justified_hash = get_latest_justified_hash(&self.post_states).ok_or_else(|| { | ||
| anyhow::anyhow!("Failed to get latest_justified_hash from post_states") | ||
| })?; | ||
| self.head = get_fork_choice_head(&self.chain, &justified_hash, &self.known_votes, 0)?; | ||
| Ok(()) | ||
| } | ||
syjn99 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| pub fn propose_block(&mut self) -> anyhow::Result<Block> { | ||
| let new_slot = get_current_slot(); | ||
|
|
||
| let head_state = self | ||
| .post_states | ||
| .get(&self.head) | ||
| .ok_or_else(|| anyhow::anyhow!("Post state not found for head: {}", self.head))?; | ||
| let mut new_block = Block { | ||
| slot: new_slot, | ||
| parent: self.head, | ||
| votes: VariableList::empty(), | ||
| // Diverged from Python implementation: Using `B256::ZERO` instead of `None`) | ||
| state_root: B256::ZERO, | ||
| }; | ||
| let mut state: LeanState; | ||
|
|
||
| // Keep attempt to add valid votes from the list of available votes | ||
| loop { | ||
| state = process_block(head_state, &new_block)?; | ||
|
|
||
| let new_votes_to_add = self | ||
| .known_votes | ||
| .clone() | ||
| .into_iter() | ||
| .filter(|vote| vote.source == state.latest_justified_hash) | ||
| .filter(|vote| !new_block.votes.contains(vote)) | ||
| .collect::<Vec<_>>(); | ||
|
|
||
| if new_votes_to_add.is_empty() { | ||
| break; | ||
| } | ||
|
|
||
| for vote in new_votes_to_add { | ||
| new_block | ||
| .votes | ||
| .push(vote) | ||
| .map_err(|err| anyhow::anyhow!("Failed to add vote to new_block: {err:?}"))?; | ||
| } | ||
| } | ||
|
|
||
| new_block.state_root = state.tree_hash_root(); | ||
|
|
||
| let digest = new_block.tree_hash_root(); | ||
|
|
||
| self.chain.insert(digest, new_block.clone()); | ||
| self.post_states.insert(digest, state); | ||
|
|
||
| Ok(new_block) | ||
| } | ||
syjn99 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| pub fn build_vote(&self) -> anyhow::Result<Vote> { | ||
| let state = self | ||
| .post_states | ||
| .get(&self.head) | ||
| .ok_or_else(|| anyhow::anyhow!("Post state not found for head: {}", self.head))?; | ||
syjn99 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| let mut target_block = self | ||
| .chain | ||
| .get(&self.head) | ||
| .ok_or_else(|| anyhow::anyhow!("Block not found in chain for head: {}", self.head))?; | ||
syjn99 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| // If there is no very recent safe target, then vote for the k'th ancestor | ||
| // of the head | ||
| for _ in 0..3 { | ||
| let safe_target_block = self.chain.get(&self.safe_target).ok_or_else(|| { | ||
| anyhow::anyhow!("Block not found for safe target hash: {}", self.safe_target) | ||
syjn99 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| })?; | ||
| if target_block.slot > safe_target_block.slot { | ||
| target_block = self.chain.get(&target_block.parent).ok_or_else(|| { | ||
| anyhow::anyhow!( | ||
syjn99 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| "Block not found for target block's parent hash: {}", | ||
| target_block.parent | ||
| ) | ||
| })?; | ||
| } | ||
| } | ||
|
|
||
| // If the latest finalized slot is very far back, then only some slots are | ||
| // valid to justify, make sure the target is one of those | ||
| while !is_justifiable_slot(&state.latest_finalized_slot, &target_block.slot) { | ||
| target_block = self.chain.get(&target_block.parent).ok_or_else(|| { | ||
| anyhow::anyhow!( | ||
syjn99 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| "Block not found for target block's parent hash: {}", | ||
| target_block.parent | ||
| ) | ||
| })?; | ||
| } | ||
|
|
||
| let head_block = self | ||
| .chain | ||
| .get(&self.head) | ||
| .ok_or_else(|| anyhow::anyhow!("Block not found for head: {}", self.head))?; | ||
syjn99 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| Ok(Vote { | ||
| // Replace with actual validator ID | ||
syjn99 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| validator_id: 0, | ||
| slot: get_current_slot(), | ||
| head: self.head, | ||
| head_slot: head_block.slot, | ||
| target: target_block.tree_hash_root(), | ||
| target_slot: target_block.slot, | ||
| source: state.latest_justified_hash, | ||
| source_slot: state.latest_justified_slot, | ||
| }) | ||
| } | ||
|
|
||
| // TODO: Add necessary methods for receive. | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,5 @@ | ||
| pub mod genesis; | ||
| pub mod lean_chain; | ||
| pub mod service; | ||
| pub mod slot; | ||
| pub mod staker; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.