Skip to content

Conversation

@arkpar
Copy link
Member

@arkpar arkpar commented Feb 20, 2019

For some applications, such as substrate it is desirable to have each node to be unique in the trie. So that the same node can't be inserted into two separate branches of the same trie. This simplifies implementation of node storage quite a lot, since it removes the need for reference counting.

In ethereum the uniqueness is already guaranteed by the fact that the keys are hashes and each value ends up in a leaf node with a long random partial key. In Substrate this is not the case, as the keys are plain.

This PR introduces an additional parameter for HashDB functions that takes encoded partial node key. This allows for separating colliding nodes on the database levels, and for more efficient database implementation. E.g. The nodes that are close to each other in the trie may be grouped on disk.
Trie root calculation is not affected.

@cheme
Copy link
Contributor

cheme commented Feb 21, 2019

Looks interesting, out of curiosity, why is this needed ? (TrieStream of trie-root probably also need an updated).
Edit: no need to reply, did not see the referenced issue :)

@arkpar
Copy link
Member Author

arkpar commented Feb 21, 2019

Updated the description.

@xlc
Copy link

xlc commented Mar 10, 2019

@cheme Anything prevents this from been merged? I really want paritytech/substrate#1733 get fixed.

@cheme
Copy link
Contributor

cheme commented Mar 10, 2019

@xlc, the code in itself seems functionally fine and could be merge, but it is currently unclear if this will be the actual fix for paritytech/substrate#1733, it would also require trie_root changes for full compatibility (probably requiring modifying 'TrieStream' trait).
Using this alternate scheme changes all tries content (every hash ref in every node so also every root) : as I see it, for substrate it means either implementing a way to run two version of trie depending on block (quite difficult), or rebooting the chain.
I believe @arkpar is working on an alternate fix to avoid such breaking change, I do not have the details (for my part I can only imagine using key as immediate backing db key for storage, which would require a db migration (could improve perf a bit for fewer operation on update of value but I did not think to much about this)).
So I totally agree with you that 1733 is a very high priority issue, but using time on alternate possible solution could really be worth it.

@xlc
Copy link

xlc commented Mar 10, 2019

@cheme Thanks for the detailed explanation. Good to know this is not getting stale.

@arkpar arkpar removed the inprogress label Mar 20, 2019
@arkpar arkpar changed the title Allow node hashing with key Node key prefixes in the database. Mar 20, 2019
@arkpar arkpar changed the title Node key prefixes in the database. Node key prefixes in the database Mar 20, 2019
Copy link
Contributor

@cheme cheme left a comment

Choose a reason for hiding this comment

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

This looks great, I did not run tests yet (I could probably fuzz it a bit later but I need to update my trie_root alternative algo from #11), still I put a few first comments.
I am also starting to wonder : why not have a trie unique id and calculate full_key from concat of unique_id + prefix only : this would allow direct access to a value without going through all the trie. (the unique_id thing may not even be needed in case of a column containing only the trie).
Ok, I am stupid, this does not keep the history, it is only possible if managing history or using one unique trie id per block. Still allowing custom full_key scheme would be interesting (at least for parity-eth compatibility).
Last observation, this PR may reduce the possibilities of trie_root crate (some form of Stream could be use to build a trie by including a db, this should now requires some modification of Stream trait if I am correct).



/// Make database key from hash and prefix.
pub fn full_key<H: KeyHasher>(key: &H::Out, prefix: &[u8]) -> Key {
Copy link
Contributor

Choose a reason for hiding this comment

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

Here I would certainly see something like :
pub fn full_key<H: KeyHasher>(key: &H::Out, prefix: &[u8], key_dest: &mut [u8]) {
and reuse a full key buffer in MemoryDB
Would also require a function returning size

Copy link
Member Author

Choose a reason for hiding this comment

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

Reusing the buffer would only make sense in read-only methods, and that would make MemoryDB not thread safe for reading, or require additional synchronization.

pub fn full_key<H: KeyHasher>(key: &H::Out, prefix: &[u8]) -> Key {
let mut full_key = Vec::with_capacity(key.as_ref().len() + prefix.len());
full_key.extend_from_slice(prefix);
full_key.extend_from_slice(key.as_ref());
Copy link
Contributor

Choose a reason for hiding this comment

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

This is not mixing key with prefix anymore, but assume variable length for full_key: the parity_common kvdb crate would probably benefit from being able to choose the key type (currently ElasticArray32 : in our case ElasticArray64 or an intermediatory value would fit better).
Adding an issue to not forget that may be an idea.

Copy link
Member Author

Choose a reason for hiding this comment

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

Profiling shows that allocations here and calculating partial keys are insignificant compared to IO and other code. So I left it for later.

/// Look up a given hash into the bytes that hash to it, returning None if the
/// hash is not known.
fn get(&self, key: &H::Out) -> Option<T>;
fn get(&self, key: &H::Out, prefix: &[u8]) -> Option<T>;
Copy link
Contributor

Choose a reason for hiding this comment

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

PlainDB could probably use that too (fwiu plaindb is for varlen key), @sorpaas would know better than I.

// this loop iterates through non-inline nodes.
for depth in 0.. {
let node_data = match self.db.get(&hash) {
let node_data = match self.db.get(&hash, &key.encoded_leftmost(key_nibbles, false)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Small slowdown for parity ethereum, I do not really see a way of avoiding it.

@cheme
Copy link
Contributor

cheme commented Mar 24, 2019

I did start updating another trie pr on friday using this pr changes: things seems to work fine :) (after solving few indexing issue of my own, I could fuzz my triebuilder against this prefixed implementation). Manipulating this new api makes me wonder about two points:

fn key(&self, hash: &H::Out, prefix: &[u8]) -> Self::Key; 

I could make keyspace indexing in the keyfunction implementation.
This implies that triedb and triedbmut will build with a third parameter (keyfunction struct as inner field), but it won't really add verbosity (we already need to set keyfunction type when building trie and triemut).
This could also be use to add additional contextual info in the key other trie usage.

But honestly, this is still doable by overloading the db if we consider this not being the role of the keyfunction trait.

  • I am not really comfortable with using nibble trie encoding for db key prefix (it does not allow db sorted iteration over a common prefixed key in the db due to the header infos).
    Ideally the way we encode prefix in db could be defined into memorydb: into KeyFunction implementation. It would requires to pass Key plus nibble index (or an equivalent to nibbleslice) to the key function instead of the encoded prefix.
    It can also avoid running the nibble encoding in the parity eth case (shouldn't be the reason to do it this way).

@arkpar
Copy link
Member Author

arkpar commented Mar 27, 2019

Regarding paritytech/substrate#2035, this could be done purely in the HashDb implementation. KeyFunction is implementation detail of MemoryDb. I don't think it should be part of the TrieDb or TrieDbMut. The trie itself is not concerned with duplicate nodes or reference counting or storage optimizations. It just provide enough information for the backend to resolve all these issues.

I am not really comfortable with using nibble trie encoding for db key prefix (it does not allow db sorted iteration over a common prefixed key in the db due to the header infos).

Not sure I understand this. Trie iteration is surely still possible. Node backend iteration or seek depends on HashDb implementation and out of the scope of this PR. E.g. with substrate key function iteration is still possible, but seeking by node hash is not.

@cheme
Copy link
Contributor

cheme commented Mar 27, 2019

About paritytech/substrate#2035 , the HashDB way of doing things is fine (still I found it more elegant with KeyFunction (I am currently realizing that I need to handle the empty node value of hash db case : with keyfunction it is native). But it is really a matter of design and does not have to make it to this PR.
About iteration, it is mostly speculating about db (rocksdb or other with ordered tree iteration) optimization : having a common prefix looks better for me but as for the previous point it does not have to make it to this pr.

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.

5 participants