Skip to content
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

feat: implement new trie_walker which can iterate full+partial tries #1567

Merged

Conversation

KolbyML
Copy link
Member

@KolbyML KolbyML commented Oct 29, 2024

What was wrong?

The old TrieWalker implementation I did only worked for trie-diffs as it did everything in memory. This is bad because we want to be able to gossip the whole state trie onto the network.

My new trie_walker implementation will also allow us to reuse code for horizontal scaling (i.e. walking the trie with ranged scopes). So either way having 1 trie implementation which is more flexible should be easier to maintain, keeping both is redundent.

How was it fixed?

Implement a trie-walker which builds iterative proofs sequentially, instead of building a map of back traces from every node like the old trie-walker did.

Using this new trie-walker I was able to iterate whole the whole state at block 21 million while only using 1.4GB of ram which is really good as the whole state is almost 400GB

I did some testing and both implementations generate the same amount of content I tested it from 0-100k

Using new trie walker
Oct 29 16:33:25.467  INFO test_we_can_generate_content_key_values_up_to_x: content_generation: Finished all 100000 blocks: Stats { content_count: 742202, gossip_size: 1135779983, storage_size: 304058469, account_trie_count: 728452, contract_storage_trie_count: 13476, contract_bytecode_count: 274 }
Using old trie walker
Oct 29 16:34:14.857  INFO test_we_can_generate_content_key_values_up_to_x: content_generation: Finished all 100000 blocks: Stats { content_count: 742202, gossip_size: 1135779983, storage_size: 304058469, account_trie_count: 728452, contract_storage_trie_count: 13476, contract_bytecode_count: 274 }

@KolbyML KolbyML self-assigned this Oct 29, 2024
@KolbyML KolbyML force-pushed the replace-old-trie-walker-implementation branch from e7b1951 to e3759ca Compare October 29, 2024 22:58
Copy link
Collaborator

@morph-dev morph-dev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No major concerns, but several non-trivial things.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of partially implementing eth_trie::DB trait, how about we define out own trait. Something like this:

trait TrieWalkerDb {
    fn get(&self, key: &[u8]) -> anyhow::Result<Option<Bytes>>;
}

Then we can implement it for whatever structure we want

impl TrieWalkerDb for HashMap<B256, Bytes> {
    ...
}

impl TrieWalkerDb for AccountDB {
    ...
}

impl TrieWalkerDb for TrieRocksDB {
    ...
}

And then we would have something like this in the other file:

pub struct TrieWalker<DB: TrieWalkerDb> {
    is_partial_trie: bool,
    db: Arc<DB>,
    stack: Vec<TrieProof>,
}

What do you think?

If you decide to go this path, ignore the rest of the comments in this file.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can even add some custom logic if needed. Not sure how useful it is, but we can do something like this:

trait TrieWalkerDb {
    fn get(&self, key: &[u8]) -> anyhow::Result<Option<Bytes>>;

    fn get_node(&self, key: &[u8]) -> anyhow::Result<Option<Node>> {
        self.get(key)?
            .map(|bytes| decode_node(&mut bytes.as_ref()).map_err(|err| anyhow!(err)))
            .transpose()
    }
}

trin-execution/src/walkers/memory_db.rs Outdated Show resolved Hide resolved
trin-execution/src/walkers/memory_db.rs Outdated Show resolved Hide resolved

use crate::types::trie_proof::TrieProof;

/// This is used for walking the whole state trie and partial tries (forward state diffs)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Usually, first API documentation line is separate by blank line from the rest of the documentation.

Also, I think other lines should start with capital letter.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I think I did what you asked

/// 3. getting stats about the state trie
pub struct TrieWalker<TrieDB: DB> {
is_partial_trie: bool,
trie: Arc<TrieDB>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need Arc<..>?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes becuase trie.lock().db.clone() gives us an Arc

Node::Branch(branch) => {
let branch = branch.read().expect("Branch node must be readable");

// We don't need to check the branches value as it is already encoded in the proof
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I think this comment is not needed. We are iterating Trie nodes, not Trie values.

)))));
{
let mut trie = trie.lock();
trie.insert(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we do this in a loop?

async fn test_state_walker() {
let temp_directory = create_temp_test_dir().unwrap();
let db = Arc::new(setup_rocksdb(temp_directory.path()).unwrap());
let trie = Arc::new(Mutex::new(EthTrie::new(Arc::new(TrieRocksDB::new(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this to be Arc<Mutex<...>>?

}

let root_hash = trie.lock().root_hash().unwrap();
let walker = TrieWalker::new(root_hash, trie.lock().db.clone()).unwrap();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't trie.lock().db.clone() just db?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is an Arc of the traited object

Copy link
Member Author

@KolbyML KolbyML Oct 31, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So it is Arc<DB>

};
leaf_count += 1;

// reconstruct the address hash from the path so that we can fetch the
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused by this comment. What are we fetching from the database?

@morph-dev
Copy link
Collaborator

Also, what do you think about moving this to ethportal-api?
For example here: https://github.com/ethereum/trin/tree/master/ethportal-api/src/types/state_trie

@KolbyML
Copy link
Member Author

KolbyML commented Oct 31, 2024

Also, what do you think about moving this to ethportal-api? For example here: https://github.com/ethereum/trin/tree/master/ethportal-api/src/types/state_trie

I don't see value into moving this into ethportal-api as the only usecases are

  • bridging content
  • generating stats for bridging content

So I don't think it makes sense to store it in ethportal-api.

I also plan to extend this implementation with logic specific to horizontal scaling; I think moving my trie-walker implementation to ethportal-api would open it to needing to handle additional complexity, which for our use cases isn't needed. My trie-walker implementation serves a unique usecase for us. I think a different implementation makes sense for ethportal-api which takes advantage of it's unique constraints.

@KolbyML
Copy link
Member Author

KolbyML commented Oct 31, 2024

@morph-dev ready for another review


impl TrieWalkerDb for HashMap<B256, Vec<u8>> {
fn get(&self, key: &[u8]) -> anyhow::Result<Option<Bytes>> {
Ok(self.get(key).cloned().map(|vec| vec.into()))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can probably be:

Ok(self.get(key).map(Bytes::copy_from_slice))

@@ -7,7 +7,7 @@ use rocksdb::DB as RocksDB;
pub struct TrieRocksDB {
// If "light" is true, the data is deleted from the database at the time of submission.
light: bool,
storage: Arc<RocksDB>,
pub storage: Arc<RocksDB>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need this to be public. You just need to use eth_trie::DB in other place.


impl TrieWalkerDb for TrieRocksDB {
fn get(&self, key: &[u8]) -> anyhow::Result<Option<Bytes>> {
self.storage
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you add use eth_trie::DB, you should be able to remove pub from storage field and do something like this:

self.get(key)
    .map(|result| result.map(Bytes::from))
    .map_err(...)

use alloy::primitives::{Bytes, B256};
use anyhow::{anyhow, Ok};
use db::TrieWalkerDb;
use eth_trie::{decode_node, node::Node};

use crate::types::trie_proof::TrieProof;

/// This is used for walking the whole state trie and partial tries (forward state diffs)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I was thinking more like this:

/// Iterates over trie nodes from the whole or partial state trie
///
/// Use cases are:
/// 1. Gossiping the whole state trie
/// 2. Gossiping the forward state diffs (partial state trie)
/// 3. Getting stats about the state trie

.into();
impl<DB: TrieWalkerDb> TrieWalker<DB> {
pub fn new(root_hash: B256, trie: Arc<DB>) -> anyhow::Result<Self> {
let root_value = match trie.get(root_hash.as_slice())? {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: maybe rename root_value to root_node_trie?


self.stack.push(TrieProof {
path,
proof: [partial_proof, vec![value]].concat(),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there is no reason to use concat(), as I believe that it would iterate and clone partial_proof (it should be cheap, but still not needed). We can:

let mut proof = partial_proof
proof.push(value);
self.stack.push(TrieProof { path, proof });

or if you prefer:

partial_proof.push(value);
self.stack.push(TrieProof {
    path,
    proof: partial_proof,
});

}
Ok(())
}
}

impl<TrieDB: DB> Iterator for TrieWalker<TrieDB> {
/// Panics if the trie is corrupted
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment should probably be on TrieWalker, as I think this will never be shown in IDE or elsewhere.

@@ -170,126 +161,37 @@ impl<TrieDB: DB> Iterator for TrieWalker<TrieDB> {
#[cfg(test)]
mod tests {
use super::*;

use eth_trie::{EthTrie, RootWithTrieDiff, Trie};
use revm_primitives::{keccak256, Address, B256, U256};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: alloy::primitives

};
leaf_count += 1;

// reconstruct the address hash from the path so that we can fetch the
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this comment is still not clear to me. what are we fetching from the database?
aren't we fetching proof directly from the trie?

continue;
};

// reconstruct the address hash from the path so that we can fetch the
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also here

@morph-dev
Copy link
Collaborator

I don't see value into moving this into ethportal-api as the only usecases are

  • bridging content
  • generating stats for bridging content

So I don't think it makes sense to store it in ethportal-api.

Wouldn't it make sense to be in portal-bridge? Especially if you plan to add horizontal scaling.

I thought that ethportal-api might be okay and maybe glados can use it at some point.
But it's probably not the exact use case, and API might not work very well. So we can definitely wait until we get there and reevaluate.

@KolbyML
Copy link
Member Author

KolbyML commented Oct 31, 2024

I don't see value into moving this into ethportal-api as the only usecases are

  • bridging content
  • generating stats for bridging content

So I don't think it makes sense to store it in ethportal-api.

Wouldn't it make sense to be in portal-bridge? Especially if you plan to add horizontal scaling.

I plan to use it in Trin Execution and portal-bridge will depends on Trin Execution so that would create a recursive dependency. Soo I would say no as the horizontal scaling part of the walker isn't related to the bridge it is more of just scoping the walked path.

I thought that ethportal-api might be okay and maybe glados can use it at some point. But it's probably not the exact use case, and API might not work very well. So we can definitely wait until we get there and reevaluate.

Sure, I think a different design would work better for Glados's use case

@KolbyML KolbyML merged commit bcb921d into ethereum:master Oct 31, 2024
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants