mirror of
https://github.com/sigp/lighthouse.git
synced 2026-03-15 19:02:42 +00:00
## Issue Addressed NA ## Proposed Changes Implements the "union" type from the SSZ spec for `ssz`, `ssz_derive`, `tree_hash` and `tree_hash_derive` so it may be derived for `enums`: https://github.com/ethereum/consensus-specs/blob/v1.1.0-beta.3/ssz/simple-serialize.md#union The union type is required for the merge, since the `Transaction` type is defined as a single-variant union `Union[OpaqueTransaction]`. ### Crate Updates This PR will (hopefully) cause CI to publish new versions for the following crates: - `eth2_ssz_derive`: `0.2.1` -> `0.3.0` - `eth2_ssz`: `0.3.0` -> `0.4.0` - `eth2_ssz_types`: `0.2.0` -> `0.2.1` - `tree_hash`: `0.3.0` -> `0.4.0` - `tree_hash_derive`: `0.3.0` -> `0.4.0` These these crates depend on each other, I've had to add a workspace-level `[patch]` for these crates. A follow-up PR will need to remove this patch, ones the new versions are published. ### Union Behaviors We already had SSZ `Encode` and `TreeHash` derive for enums, however it just did a "transparent" pass-through of the inner value. Since the "union" decoding from the spec is in conflict with the transparent method, I've required that all `enum` have exactly one of the following enum-level attributes: #### SSZ - `#[ssz(enum_behaviour = "union")]` - matches the spec used for the merge - `#[ssz(enum_behaviour = "transparent")]` - maintains existing functionality - not supported for `Decode` (never was) #### TreeHash - `#[tree_hash(enum_behaviour = "union")]` - matches the spec used for the merge - `#[tree_hash(enum_behaviour = "transparent")]` - maintains existing functionality This means that we can maintain the existing transparent behaviour, but all existing users will get a compile-time error until they explicitly opt-in to being transparent. ### Legacy Option Encoding Before this PR, we already had a union-esque encoding for `Option<T>`. However, this was with the *old* SSZ spec where the union selector was 4 bytes. During merge specification, the spec was changed to use 1 byte for the selector. Whilst the 4-byte `Option` encoding was never used in the spec, we used it in our database. Writing a migrate script for all occurrences of `Option` in the database would be painful, especially since it's used in the `CommitteeCache`. To avoid the migrate script, I added a serde-esque `#[ssz(with = "module")]` field-level attribute to `ssz_derive` so that we can opt into the 4-byte encoding on a field-by-field basis. The `ssz::legacy::four_byte_impl!` macro allows a one-liner to define the module required for the `#[ssz(with = "module")]` for some `Option<T> where T: Encode + Decode`. Notably, **I have removed `Encode` and `Decode` impls for `Option`**. I've done this to force a break on downstream users. Like I mentioned, `Option` isn't used in the spec so I don't think it'll be *that* annoying. I think it's nicer than quietly having two different union implementations or quietly breaking the existing `Option` impl. ### Crate Publish Ordering I've modified the order in which CI publishes crates to ensure that we don't publish a crate without ensuring we already published a crate that it depends upon. ## TODO - [ ] Queue a follow-up `[patch]`-removing PR.
206 lines
6.0 KiB
Rust
206 lines
6.0 KiB
Rust
pub mod impls;
|
|
mod merkle_hasher;
|
|
mod merkleize_padded;
|
|
mod merkleize_standard;
|
|
|
|
pub use merkle_hasher::{Error, MerkleHasher};
|
|
pub use merkleize_padded::merkleize_padded;
|
|
pub use merkleize_standard::merkleize_standard;
|
|
|
|
use eth2_hashing::{hash_fixed, ZERO_HASHES, ZERO_HASHES_MAX_INDEX};
|
|
|
|
pub const BYTES_PER_CHUNK: usize = 32;
|
|
pub const HASHSIZE: usize = 32;
|
|
pub const MERKLE_HASH_CHUNK: usize = 2 * BYTES_PER_CHUNK;
|
|
pub const MAX_UNION_SELECTOR: u8 = 127;
|
|
|
|
pub type Hash256 = ethereum_types::H256;
|
|
|
|
/// Convenience method for `MerkleHasher` which also provides some fast-paths for small trees.
|
|
///
|
|
/// `minimum_leaf_count` will only be used if it is greater than or equal to the minimum number of leaves that can be created from `bytes`.
|
|
pub fn merkle_root(bytes: &[u8], minimum_leaf_count: usize) -> Hash256 {
|
|
let leaves = std::cmp::max(
|
|
(bytes.len() + (HASHSIZE - 1)) / HASHSIZE,
|
|
minimum_leaf_count,
|
|
);
|
|
|
|
if leaves == 0 {
|
|
// If there are no bytes then the hash is always zero.
|
|
Hash256::zero()
|
|
} else if leaves == 1 {
|
|
// If there is only one leaf, the hash is always those leaf bytes padded out to 32-bytes.
|
|
let mut hash = [0; HASHSIZE];
|
|
hash[0..bytes.len()].copy_from_slice(bytes);
|
|
Hash256::from_slice(&hash)
|
|
} else if leaves == 2 {
|
|
// If there are only two leaves (this is common with BLS pubkeys), we can avoid some
|
|
// overhead with `MerkleHasher` and just do a simple 3-node tree here.
|
|
let mut leaves = [0; HASHSIZE * 2];
|
|
leaves[0..bytes.len()].copy_from_slice(bytes);
|
|
|
|
Hash256::from_slice(&hash_fixed(&leaves))
|
|
} else {
|
|
// If there are 3 or more leaves, use `MerkleHasher`.
|
|
let mut hasher = MerkleHasher::with_leaves(leaves);
|
|
hasher
|
|
.write(bytes)
|
|
.expect("the number of leaves is adequate for the number of bytes");
|
|
hasher
|
|
.finish()
|
|
.expect("the number of leaves is adequate for the number of bytes")
|
|
}
|
|
}
|
|
|
|
/// Returns the node created by hashing `root` and `length`.
|
|
///
|
|
/// Used in `TreeHash` for inserting the length of a list above it's root.
|
|
pub fn mix_in_length(root: &Hash256, length: usize) -> Hash256 {
|
|
let usize_len = std::mem::size_of::<usize>();
|
|
|
|
let mut length_bytes = [0; BYTES_PER_CHUNK];
|
|
length_bytes[0..usize_len].copy_from_slice(&length.to_le_bytes());
|
|
|
|
Hash256::from_slice(ð2_hashing::hash32_concat(root.as_bytes(), &length_bytes)[..])
|
|
}
|
|
|
|
/// Returns `Some(root)` created by hashing `root` and `selector`, if `selector <=
|
|
/// MAX_UNION_SELECTOR`. Otherwise, returns `None`.
|
|
///
|
|
/// Used in `TreeHash` for the "union" type.
|
|
///
|
|
/// ## Specification
|
|
///
|
|
/// ```ignore,text
|
|
/// mix_in_selector: Given a Merkle root root and a type selector selector ("uint256" little-endian
|
|
/// serialization) return hash(root + selector).
|
|
/// ```
|
|
///
|
|
/// https://github.com/ethereum/consensus-specs/blob/v1.1.0-beta.3/ssz/simple-serialize.md#union
|
|
pub fn mix_in_selector(root: &Hash256, selector: u8) -> Option<Hash256> {
|
|
if selector > MAX_UNION_SELECTOR {
|
|
return None;
|
|
}
|
|
|
|
let mut chunk = [0; BYTES_PER_CHUNK];
|
|
chunk[0] = selector;
|
|
|
|
let root = eth2_hashing::hash32_concat(root.as_bytes(), &chunk);
|
|
Some(Hash256::from_slice(&root))
|
|
}
|
|
|
|
/// Returns a cached padding node for a given height.
|
|
fn get_zero_hash(height: usize) -> &'static [u8] {
|
|
if height <= ZERO_HASHES_MAX_INDEX {
|
|
&ZERO_HASHES[height]
|
|
} else {
|
|
panic!("Tree exceeds MAX_TREE_DEPTH of {}", ZERO_HASHES_MAX_INDEX)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Clone)]
|
|
pub enum TreeHashType {
|
|
Basic,
|
|
Vector,
|
|
List,
|
|
Container,
|
|
}
|
|
|
|
pub trait TreeHash {
|
|
fn tree_hash_type() -> TreeHashType;
|
|
|
|
fn tree_hash_packed_encoding(&self) -> Vec<u8>;
|
|
|
|
fn tree_hash_packing_factor() -> usize;
|
|
|
|
fn tree_hash_root(&self) -> Hash256;
|
|
}
|
|
|
|
/// Punch through references.
|
|
impl<'a, T> TreeHash for &'a T
|
|
where
|
|
T: TreeHash,
|
|
{
|
|
fn tree_hash_type() -> TreeHashType {
|
|
T::tree_hash_type()
|
|
}
|
|
|
|
fn tree_hash_packed_encoding(&self) -> Vec<u8> {
|
|
T::tree_hash_packed_encoding(*self)
|
|
}
|
|
|
|
fn tree_hash_packing_factor() -> usize {
|
|
T::tree_hash_packing_factor()
|
|
}
|
|
|
|
fn tree_hash_root(&self) -> Hash256 {
|
|
T::tree_hash_root(*self)
|
|
}
|
|
}
|
|
|
|
#[macro_export]
|
|
macro_rules! tree_hash_ssz_encoding_as_vector {
|
|
($type: ident) => {
|
|
impl tree_hash::TreeHash for $type {
|
|
fn tree_hash_type() -> tree_hash::TreeHashType {
|
|
tree_hash::TreeHashType::Vector
|
|
}
|
|
|
|
fn tree_hash_packed_encoding(&self) -> Vec<u8> {
|
|
unreachable!("Vector should never be packed.")
|
|
}
|
|
|
|
fn tree_hash_packing_factor() -> usize {
|
|
unreachable!("Vector should never be packed.")
|
|
}
|
|
|
|
fn tree_hash_root(&self) -> Vec<u8> {
|
|
tree_hash::merkle_root(&ssz::ssz_encode(self))
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
#[macro_export]
|
|
macro_rules! tree_hash_ssz_encoding_as_list {
|
|
($type: ident) => {
|
|
impl tree_hash::TreeHash for $type {
|
|
fn tree_hash_type() -> tree_hash::TreeHashType {
|
|
tree_hash::TreeHashType::List
|
|
}
|
|
|
|
fn tree_hash_packed_encoding(&self) -> Vec<u8> {
|
|
unreachable!("List should never be packed.")
|
|
}
|
|
|
|
fn tree_hash_packing_factor() -> usize {
|
|
unreachable!("List should never be packed.")
|
|
}
|
|
|
|
fn tree_hash_root(&self) -> Vec<u8> {
|
|
ssz::ssz_encode(self).tree_hash_root()
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn mix_length() {
|
|
let hash = {
|
|
let mut preimage = vec![42; BYTES_PER_CHUNK];
|
|
preimage.append(&mut vec![42]);
|
|
preimage.append(&mut vec![0; BYTES_PER_CHUNK - 1]);
|
|
eth2_hashing::hash(&preimage)
|
|
};
|
|
|
|
assert_eq!(
|
|
mix_in_length(&Hash256::from_slice(&[42; BYTES_PER_CHUNK]), 42).as_bytes(),
|
|
&hash[..]
|
|
);
|
|
}
|
|
}
|