diff --git a/Cargo.lock b/Cargo.lock index d0e3622e77e..98e0a17a6e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1829,9 +1829,10 @@ dependencies = [ [[package]] name = "eth2_ssz_derive" -version = "0.3.0" +version = "0.3.1" dependencies = [ "darling", + "eth2_ssz", "proc-macro2", "quote", "syn", diff --git a/beacon_node/beacon_chain/Cargo.toml b/beacon_node/beacon_chain/Cargo.toml index dd185ac7571..5b85833048b 100644 --- a/beacon_node/beacon_chain/Cargo.toml +++ b/beacon_node/beacon_chain/Cargo.toml @@ -33,7 +33,7 @@ slot_clock = { path = "../../common/slot_clock" } eth2_hashing = "0.3.0" eth2_ssz = "0.4.1" eth2_ssz_types = "0.2.2" -eth2_ssz_derive = "0.3.0" +eth2_ssz_derive = "0.3.1" state_processing = { path = "../../consensus/state_processing" } tree_hash = "0.4.1" types = { path = "../../consensus/types" } diff --git a/beacon_node/eth1/Cargo.toml b/beacon_node/eth1/Cargo.toml index 930301256c6..979ef931791 100644 --- a/beacon_node/eth1/Cargo.toml +++ b/beacon_node/eth1/Cargo.toml @@ -21,7 +21,7 @@ hex = "0.4.2" types = { path = "../../consensus/types"} merkle_proof = { path = "../../consensus/merkle_proof"} eth2_ssz = "0.4.1" -eth2_ssz_derive = "0.3.0" +eth2_ssz_derive = "0.3.1" tree_hash = "0.4.1" parking_lot = "0.12.0" slog = "2.5.2" diff --git a/beacon_node/lighthouse_network/Cargo.toml b/beacon_node/lighthouse_network/Cargo.toml index e5af0a74999..ead029b4e6d 100644 --- a/beacon_node/lighthouse_network/Cargo.toml +++ b/beacon_node/lighthouse_network/Cargo.toml @@ -12,7 +12,7 @@ eth2_ssz_types = "0.2.2" serde = { version = "1.0.116", features = ["derive"] } serde_derive = "1.0.116" eth2_ssz = "0.4.1" -eth2_ssz_derive = "0.3.0" +eth2_ssz_derive = "0.3.1" slog = { version = "2.5.2", features = ["max_level_trace"] } lighthouse_version = { path = "../../common/lighthouse_version" } tokio = { version = "1.14.0", features = ["time", "macros"] } diff --git a/beacon_node/operation_pool/Cargo.toml b/beacon_node/operation_pool/Cargo.toml index 1d67ecdccc2..8483233589f 100644 --- a/beacon_node/operation_pool/Cargo.toml +++ b/beacon_node/operation_pool/Cargo.toml @@ -13,7 +13,7 @@ parking_lot = "0.12.0" types = { path = "../../consensus/types" } state_processing = { path = "../../consensus/state_processing" } eth2_ssz = "0.4.1" -eth2_ssz_derive = "0.3.0" +eth2_ssz_derive = "0.3.1" rayon = "1.5.0" serde = "1.0.116" serde_derive = "1.0.116" diff --git a/beacon_node/store/Cargo.toml b/beacon_node/store/Cargo.toml index 20ae37b3b14..09d960535e4 100644 --- a/beacon_node/store/Cargo.toml +++ b/beacon_node/store/Cargo.toml @@ -14,7 +14,7 @@ leveldb = { version = "0.8.6", default-features = false } parking_lot = "0.12.0" itertools = "0.10.0" eth2_ssz = "0.4.1" -eth2_ssz_derive = "0.3.0" +eth2_ssz_derive = "0.3.1" types = { path = "../../consensus/types" } state_processing = { path = "../../consensus/state_processing" } slog = "2.5.2" diff --git a/common/eth2/Cargo.toml b/common/eth2/Cargo.toml index 294f8ec8a3d..eca086d838f 100644 --- a/common/eth2/Cargo.toml +++ b/common/eth2/Cargo.toml @@ -21,7 +21,7 @@ bytes = "1.0.1" account_utils = { path = "../../common/account_utils" } sensitive_url = { path = "../../common/sensitive_url" } eth2_ssz = "0.4.1" -eth2_ssz_derive = "0.3.0" +eth2_ssz_derive = "0.3.1" futures-util = "0.3.8" futures = "0.3.8" store = { path = "../../beacon_node/store", optional = true } diff --git a/consensus/cached_tree_hash/Cargo.toml b/consensus/cached_tree_hash/Cargo.toml index f9433e4a496..c362af83cd9 100644 --- a/consensus/cached_tree_hash/Cargo.toml +++ b/consensus/cached_tree_hash/Cargo.toml @@ -8,7 +8,7 @@ edition = "2021" ethereum-types = "0.12.1" eth2_ssz_types = "0.2.2" eth2_hashing = "0.3.0" -eth2_ssz_derive = "0.3.0" +eth2_ssz_derive = "0.3.1" eth2_ssz = "0.4.1" tree_hash = "0.4.1" smallvec = "1.6.1" diff --git a/consensus/fork_choice/Cargo.toml b/consensus/fork_choice/Cargo.toml index 52a738351ef..f0381e5ad99 100644 --- a/consensus/fork_choice/Cargo.toml +++ b/consensus/fork_choice/Cargo.toml @@ -11,7 +11,7 @@ types = { path = "../types" } state_processing = { path = "../state_processing" } proto_array = { path = "../proto_array" } eth2_ssz = "0.4.1" -eth2_ssz_derive = "0.3.0" +eth2_ssz_derive = "0.3.1" slog = { version = "2.5.2", features = ["max_level_trace", "release_max_level_trace"] } [dev-dependencies] diff --git a/consensus/proto_array/Cargo.toml b/consensus/proto_array/Cargo.toml index ad79ecc1e6b..1c7b19bf1da 100644 --- a/consensus/proto_array/Cargo.toml +++ b/consensus/proto_array/Cargo.toml @@ -11,7 +11,7 @@ path = "src/bin.rs" [dependencies] types = { path = "../types" } eth2_ssz = "0.4.1" -eth2_ssz_derive = "0.3.0" +eth2_ssz_derive = "0.3.1" serde = "1.0.116" serde_derive = "1.0.116" serde_yaml = "0.8.13" diff --git a/consensus/ssz/Cargo.toml b/consensus/ssz/Cargo.toml index a153c2efc14..e521853c218 100644 --- a/consensus/ssz/Cargo.toml +++ b/consensus/ssz/Cargo.toml @@ -10,7 +10,7 @@ license = "Apache-2.0" name = "ssz" [dev-dependencies] -eth2_ssz_derive = "0.3.0" +eth2_ssz_derive = "0.3.1" [dependencies] ethereum-types = "0.12.1" diff --git a/consensus/ssz/tests/tests.rs b/consensus/ssz/tests/tests.rs index e41fc15dd4e..355cf35b582 100644 --- a/consensus/ssz/tests/tests.rs +++ b/consensus/ssz/tests/tests.rs @@ -370,145 +370,3 @@ mod round_trip { round_trip(data); } } - -mod derive_macro { - use ssz::{Decode, Encode}; - use ssz_derive::{Decode, Encode}; - use std::fmt::Debug; - - fn assert_encode(item: &T, bytes: &[u8]) { - assert_eq!(item.as_ssz_bytes(), bytes); - } - - fn assert_encode_decode(item: &T, bytes: &[u8]) { - assert_encode(item, bytes); - assert_eq!(T::from_ssz_bytes(bytes).unwrap(), *item); - } - - #[derive(PartialEq, Debug, Encode, Decode)] - #[ssz(enum_behaviour = "union")] - enum TwoFixedUnion { - U8(u8), - U16(u16), - } - - #[derive(PartialEq, Debug, Encode, Decode)] - struct TwoFixedUnionStruct { - a: TwoFixedUnion, - } - - #[test] - fn two_fixed_union() { - let eight = TwoFixedUnion::U8(1); - let sixteen = TwoFixedUnion::U16(1); - - assert_encode_decode(&eight, &[0, 1]); - assert_encode_decode(&sixteen, &[1, 1, 0]); - - assert_encode_decode(&TwoFixedUnionStruct { a: eight }, &[4, 0, 0, 0, 0, 1]); - assert_encode_decode(&TwoFixedUnionStruct { a: sixteen }, &[4, 0, 0, 0, 1, 1, 0]); - } - - #[derive(PartialEq, Debug, Encode, Decode)] - struct VariableA { - a: u8, - b: Vec, - } - - #[derive(PartialEq, Debug, Encode, Decode)] - struct VariableB { - a: Vec, - b: u8, - } - - #[derive(PartialEq, Debug, Encode)] - #[ssz(enum_behaviour = "transparent")] - enum TwoVariableTrans { - A(VariableA), - B(VariableB), - } - - #[derive(PartialEq, Debug, Encode)] - struct TwoVariableTransStruct { - a: TwoVariableTrans, - } - - #[derive(PartialEq, Debug, Encode, Decode)] - #[ssz(enum_behaviour = "union")] - enum TwoVariableUnion { - A(VariableA), - B(VariableB), - } - - #[derive(PartialEq, Debug, Encode, Decode)] - struct TwoVariableUnionStruct { - a: TwoVariableUnion, - } - - #[test] - fn two_variable_trans() { - let trans_a = TwoVariableTrans::A(VariableA { - a: 1, - b: vec![2, 3], - }); - let trans_b = TwoVariableTrans::B(VariableB { - a: vec![1, 2], - b: 3, - }); - - assert_encode(&trans_a, &[1, 5, 0, 0, 0, 2, 3]); - assert_encode(&trans_b, &[5, 0, 0, 0, 3, 1, 2]); - - assert_encode( - &TwoVariableTransStruct { a: trans_a }, - &[4, 0, 0, 0, 1, 5, 0, 0, 0, 2, 3], - ); - assert_encode( - &TwoVariableTransStruct { a: trans_b }, - &[4, 0, 0, 0, 5, 0, 0, 0, 3, 1, 2], - ); - } - - #[test] - fn two_variable_union() { - let union_a = TwoVariableUnion::A(VariableA { - a: 1, - b: vec![2, 3], - }); - let union_b = TwoVariableUnion::B(VariableB { - a: vec![1, 2], - b: 3, - }); - - assert_encode_decode(&union_a, &[0, 1, 5, 0, 0, 0, 2, 3]); - assert_encode_decode(&union_b, &[1, 5, 0, 0, 0, 3, 1, 2]); - - assert_encode_decode( - &TwoVariableUnionStruct { a: union_a }, - &[4, 0, 0, 0, 0, 1, 5, 0, 0, 0, 2, 3], - ); - assert_encode_decode( - &TwoVariableUnionStruct { a: union_b }, - &[4, 0, 0, 0, 1, 5, 0, 0, 0, 3, 1, 2], - ); - } - - #[derive(PartialEq, Debug, Encode, Decode)] - #[ssz(enum_behaviour = "union")] - enum TwoVecUnion { - A(Vec), - B(Vec), - } - - #[test] - fn two_vec_union() { - assert_encode_decode(&TwoVecUnion::A(vec![]), &[0]); - assert_encode_decode(&TwoVecUnion::B(vec![]), &[1]); - - assert_encode_decode(&TwoVecUnion::A(vec![0]), &[0, 0]); - assert_encode_decode(&TwoVecUnion::B(vec![0]), &[1, 0]); - - assert_encode_decode(&TwoVecUnion::A(vec![0, 1]), &[0, 0, 1]); - assert_encode_decode(&TwoVecUnion::B(vec![0, 1]), &[1, 0, 1]); - } -} diff --git a/consensus/ssz_derive/Cargo.toml b/consensus/ssz_derive/Cargo.toml index cac617d3917..d3b2865a61d 100644 --- a/consensus/ssz_derive/Cargo.toml +++ b/consensus/ssz_derive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "eth2_ssz_derive" -version = "0.3.0" +version = "0.3.1" authors = ["Paul Hauner "] edition = "2021" description = "Procedural derive macros to accompany the eth2_ssz crate." @@ -15,3 +15,6 @@ syn = "1.0.42" proc-macro2 = "1.0.23" quote = "1.0.7" darling = "0.13.0" + +[dev-dependencies] +eth2_ssz = "0.4.1" diff --git a/consensus/ssz_derive/src/lib.rs b/consensus/ssz_derive/src/lib.rs index a5a5a0dddf2..9f8270e6dd5 100644 --- a/consensus/ssz_derive/src/lib.rs +++ b/consensus/ssz_derive/src/lib.rs @@ -1,23 +1,170 @@ #![recursion_limit = "256"] //! Provides procedural derive macros for the `Encode` and `Decode` traits of the `eth2_ssz` crate. //! -//! Supports field attributes, see each derive macro for more information. +//! ## Attributes +//! +//! The following struct/enum attributes are available: +//! +//! - `#[ssz(enum_behaviour = "union")]`: encodes and decodes an `enum` with a one-byte variant selector. +//! - `#[ssz(enum_behaviour = "transparent")]`: allows encoding an `enum` by serializing only the +//! value whilst ignoring outermost the `enum`. +//! - `#[ssz(struct_behaviour = "container")]`: encodes and decodes the `struct` as an SSZ +//! "container". +//! - `#[ssz(struct_behaviour = "transparent")]`: encodes and decodes a `struct` with exactly one +//! non-skipped field as if the outermost `struct` does not exist. +//! +//! The following field attributes are available: +//! +//! - `#[ssz(with = "module")]`: uses the methods in `module` to implement `ssz::Encode` and +//! `ssz::Decode`. This is useful when it's not possible to create an `impl` for that type +//! (e.g. the type is defined in another crate). +//! - `#[ssz(skip_serializing)]`: this field will not be included in the serialized SSZ vector. +//! - `#[ssz(skip_deserializing)]`: this field will not be expected in the serialized +//! SSZ vector and it will be initialized from a `Default` implementation. +//! +//! ## Examples +//! +//! ### Structs +//! +//! ```rust +//! use ssz::{Encode, Decode}; +//! use ssz_derive::{Encode, Decode}; +//! +//! /// Represented as an SSZ "list" wrapped in an SSZ "container". +//! #[derive(Debug, PartialEq, Encode, Decode)] +//! #[ssz(struct_behaviour = "container")] // "container" is the default behaviour +//! struct TypicalStruct { +//! foo: Vec +//! } +//! +//! assert_eq!( +//! TypicalStruct { foo: vec![42] }.as_ssz_bytes(), +//! vec![4, 0, 0, 0, 42] +//! ); +//! +//! assert_eq!( +//! TypicalStruct::from_ssz_bytes(&[4, 0, 0, 0, 42]).unwrap(), +//! TypicalStruct { foo: vec![42] }, +//! ); +//! +//! /// Represented as an SSZ "list" *without* an SSZ "container". +//! #[derive(Encode, Decode)] +//! #[ssz(struct_behaviour = "transparent")] +//! struct WrapperStruct { +//! foo: Vec +//! } +//! +//! assert_eq!( +//! WrapperStruct { foo: vec![42] }.as_ssz_bytes(), +//! vec![42] +//! ); +//! +//! /// Represented as an SSZ "list" *without* an SSZ "container". The `bar` byte is ignored. +//! #[derive(Debug, PartialEq, Encode, Decode)] +//! #[ssz(struct_behaviour = "transparent")] +//! struct WrapperStructSkippedField { +//! foo: Vec, +//! #[ssz(skip_serializing, skip_deserializing)] +//! bar: u8, +//! } +//! +//! assert_eq!( +//! WrapperStructSkippedField { foo: vec![42], bar: 99 }.as_ssz_bytes(), +//! vec![42] +//! ); +//! assert_eq!( +//! WrapperStructSkippedField::from_ssz_bytes(&[42]).unwrap(), +//! WrapperStructSkippedField { foo: vec![42], bar: 0 } +//! ); +//! +//! /// Represented as an SSZ "list" *without* an SSZ "container". +//! #[derive(Encode, Decode)] +//! #[ssz(struct_behaviour = "transparent")] +//! struct NewType(Vec); +//! +//! assert_eq!( +//! NewType(vec![42]).as_ssz_bytes(), +//! vec![42] +//! ); +//! +//! /// Represented as an SSZ "list" *without* an SSZ "container". The `bar` byte is ignored. +//! #[derive(Debug, PartialEq, Encode, Decode)] +//! #[ssz(struct_behaviour = "transparent")] +//! struct NewTypeSkippedField(Vec, #[ssz(skip_serializing, skip_deserializing)] u8); +//! +//! assert_eq!( +//! NewTypeSkippedField(vec![42], 99).as_ssz_bytes(), +//! vec![42] +//! ); +//! assert_eq!( +//! NewTypeSkippedField::from_ssz_bytes(&[42]).unwrap(), +//! NewTypeSkippedField(vec![42], 0) +//! ); +//! ``` +//! +//! ### Enums +//! +//! ```rust +//! use ssz::{Encode, Decode}; +//! use ssz_derive::{Encode, Decode}; +//! +//! /// Represented as an SSZ "union". +//! #[derive(Debug, PartialEq, Encode, Decode)] +//! #[ssz(enum_behaviour = "union")] +//! enum UnionEnum { +//! Foo(u8), +//! Bar(Vec), +//! } +//! +//! assert_eq!( +//! UnionEnum::Foo(42).as_ssz_bytes(), +//! vec![0, 42] +//! ); +//! assert_eq!( +//! UnionEnum::from_ssz_bytes(&[1, 42, 42]).unwrap(), +//! UnionEnum::Bar(vec![42, 42]), +//! ); +//! +//! /// Represented as only the value in the enum variant. +//! #[derive(Debug, PartialEq, Encode)] +//! #[ssz(enum_behaviour = "transparent")] +//! enum TransparentEnum { +//! Foo(u8), +//! Bar(Vec), +//! } +//! +//! assert_eq!( +//! TransparentEnum::Foo(42).as_ssz_bytes(), +//! vec![42] +//! ); +//! assert_eq!( +//! TransparentEnum::Bar(vec![42, 42]).as_ssz_bytes(), +//! vec![42, 42] +//! ); +//! ``` use darling::{FromDeriveInput, FromMeta}; use proc_macro::TokenStream; use quote::quote; use std::convert::TryInto; -use syn::{parse_macro_input, DataEnum, DataStruct, DeriveInput, Ident}; +use syn::{parse_macro_input, DataEnum, DataStruct, DeriveInput, Ident, Index}; /// The highest possible union selector value (higher values are reserved for backwards compatible /// extensions). const MAX_UNION_SELECTOR: u8 = 127; +const ENUM_TRANSPARENT: &str = "transparent"; +const ENUM_UNION: &str = "union"; +const NO_ENUM_BEHAVIOUR_ERROR: &str = "enums require an \"enum_behaviour\" attribute with \ + a \"transparent\" or \"union\" value, e.g., #[ssz(enum_behaviour = \"transparent\")]"; + #[derive(Debug, FromDeriveInput)] #[darling(attributes(ssz))] struct StructOpts { #[darling(default)] enum_behaviour: Option, + #[darling(default)] + struct_behaviour: Option, } /// Field-level configuration. @@ -31,40 +178,87 @@ struct FieldOpts { skip_deserializing: bool, } -const ENUM_TRANSPARENT: &str = "transparent"; -const ENUM_UNION: &str = "union"; -const ENUM_VARIANTS: &[&str] = &[ENUM_TRANSPARENT, ENUM_UNION]; -const NO_ENUM_BEHAVIOUR_ERROR: &str = "enums require an \"enum_behaviour\" attribute, \ - e.g., #[ssz(enum_behaviour = \"transparent\")]"; +enum Procedure<'a> { + Struct { + data: &'a syn::DataStruct, + behaviour: StructBehaviour, + }, + Enum { + data: &'a syn::DataEnum, + behaviour: EnumBehaviour, + }, +} -enum EnumBehaviour { +enum StructBehaviour { + Container, Transparent, +} + +enum EnumBehaviour { Union, + Transparent, } -impl EnumBehaviour { - pub fn new(s: Option) -> Option { - s.map(|s| match s.as_ref() { - ENUM_TRANSPARENT => EnumBehaviour::Transparent, - ENUM_UNION => EnumBehaviour::Union, - other => panic!( - "{} is an invalid enum_behaviour, use either {:?}", - other, ENUM_VARIANTS - ), - }) +impl<'a> Procedure<'a> { + fn read(item: &'a DeriveInput) -> Self { + let opts = StructOpts::from_derive_input(item).unwrap(); + + match &item.data { + syn::Data::Struct(data) => { + if opts.enum_behaviour.is_some() { + panic!("cannot use \"enum_behaviour\" for a struct"); + } + + match opts.struct_behaviour.as_deref() { + Some("container") | None => Procedure::Struct { + data, + behaviour: StructBehaviour::Container, + }, + Some("transparent") => Procedure::Struct { + data, + behaviour: StructBehaviour::Transparent, + }, + Some(other) => panic!( + "{} is not a valid struct behaviour, use \"container\" or \"transparent\"", + other + ), + } + } + syn::Data::Enum(data) => { + if opts.struct_behaviour.is_some() { + panic!("cannot use \"struct_behaviour\" for an enum"); + } + + match opts.enum_behaviour.as_deref() { + Some("union") => Procedure::Enum { + data, + behaviour: EnumBehaviour::Union, + }, + Some("transparent") => Procedure::Enum { + data, + behaviour: EnumBehaviour::Transparent, + }, + Some(other) => panic!( + "{} is not a valid enum behaviour, use \"container\" or \"transparent\"", + other + ), + None => panic!("{}", NO_ENUM_BEHAVIOUR_ERROR), + } + } + _ => panic!("ssz_derive only supports structs and enums"), + } } } -fn parse_ssz_fields(struct_data: &syn::DataStruct) -> Vec<(&syn::Type, &syn::Ident, FieldOpts)> { +fn parse_ssz_fields( + struct_data: &syn::DataStruct, +) -> Vec<(&syn::Type, Option<&syn::Ident>, FieldOpts)> { struct_data .fields .iter() .map(|field| { let ty = &field.ty; - let ident = match &field.ident { - Some(ref ident) => ident, - _ => panic!("ssz_derive only supports named struct fields."), - }; + let ident = field.ident.as_ref(); let field_opts_candidates = field .attrs @@ -93,21 +287,17 @@ fn parse_ssz_fields(struct_data: &syn::DataStruct) -> Vec<(&syn::Type, &syn::Ide #[proc_macro_derive(Encode, attributes(ssz))] pub fn ssz_encode_derive(input: TokenStream) -> TokenStream { let item = parse_macro_input!(input as DeriveInput); - let opts = StructOpts::from_derive_input(&item).unwrap(); - let enum_opt = EnumBehaviour::new(opts.enum_behaviour); + let procedure = Procedure::read(&item); - match &item.data { - syn::Data::Struct(s) => { - if enum_opt.is_some() { - panic!("enum_behaviour is invalid for structs"); - } - ssz_encode_derive_struct(&item, s) - } - syn::Data::Enum(s) => match enum_opt.expect(NO_ENUM_BEHAVIOUR_ERROR) { - EnumBehaviour::Transparent => ssz_encode_derive_enum_transparent(&item, s), - EnumBehaviour::Union => ssz_encode_derive_enum_union(&item, s), + match procedure { + Procedure::Struct { data, behaviour } => match behaviour { + StructBehaviour::Transparent => ssz_encode_derive_struct_transparent(&item, data), + StructBehaviour::Container => ssz_encode_derive_struct(&item, data), + }, + Procedure::Enum { data, behaviour } => match behaviour { + EnumBehaviour::Transparent => ssz_encode_derive_enum_transparent(&item, data), + EnumBehaviour::Union => ssz_encode_derive_enum_union(&item, data), }, - _ => panic!("ssz_derive only supports structs and enums"), } } @@ -132,6 +322,13 @@ fn ssz_encode_derive_struct(derive_input: &DeriveInput, struct_data: &DataStruct continue; } + let ident = match ident { + Some(ref ident) => ident, + _ => panic!( + "#[ssz(struct_behaviour = \"container\")] only supports named struct fields." + ), + }; + if let Some(module) = field_opts.with { let module = quote! { #module::encode }; field_is_ssz_fixed_len.push(quote! { #module::is_ssz_fixed_len() }); @@ -219,6 +416,86 @@ fn ssz_encode_derive_struct(derive_input: &DeriveInput, struct_data: &DataStruct output.into() } +/// Derive `ssz::Encode` "transparently" for a struct which has exactly one non-skipped field. +/// +/// The single field is encoded directly, making the outermost `struct` transparent. +/// +/// ## Field attributes +/// +/// - `#[ssz(skip_serializing)]`: the field will not be serialized. +fn ssz_encode_derive_struct_transparent( + derive_input: &DeriveInput, + struct_data: &DataStruct, +) -> TokenStream { + let name = &derive_input.ident; + let (impl_generics, ty_generics, where_clause) = &derive_input.generics.split_for_impl(); + let ssz_fields = parse_ssz_fields(struct_data); + let num_fields = ssz_fields + .iter() + .filter(|(_, _, field_opts)| !field_opts.skip_deserializing) + .count(); + + if num_fields != 1 { + panic!( + "A \"transparent\" struct must have exactly one non-skipped field ({} fields found)", + num_fields + ); + } + + let (index, (ty, ident, _field_opts)) = ssz_fields + .iter() + .enumerate() + .find(|(_, (_, _, field_opts))| !field_opts.skip_deserializing) + .expect("\"transparent\" struct must have at least one non-skipped field"); + + // Remove the `_usize` suffix from the value to avoid a compiler warning. + let index = Index::from(index); + + let output = if let Some(field_name) = ident { + quote! { + impl #impl_generics ssz::Encode for #name #ty_generics #where_clause { + fn is_ssz_fixed_len() -> bool { + <#ty as ssz::Encode>::is_ssz_fixed_len() + } + + fn ssz_fixed_len() -> usize { + <#ty as ssz::Encode>::ssz_fixed_len() + } + + fn ssz_bytes_len(&self) -> usize { + self.#field_name.ssz_bytes_len() + } + + fn ssz_append(&self, buf: &mut Vec) { + self.#field_name.ssz_append(buf) + } + } + } + } else { + quote! { + impl #impl_generics ssz::Encode for #name #ty_generics #where_clause { + fn is_ssz_fixed_len() -> bool { + <#ty as ssz::Encode>::is_ssz_fixed_len() + } + + fn ssz_fixed_len() -> usize { + <#ty as ssz::Encode>::ssz_fixed_len() + } + + fn ssz_bytes_len(&self) -> usize { + self.#index.ssz_bytes_len() + } + + fn ssz_append(&self, buf: &mut Vec) { + self.#index.ssz_append(buf) + } + } + } + }; + + output.into() +} + /// Derive `ssz::Encode` for an enum in the "transparent" method. /// /// The "transparent" method is distinct from the "union" method specified in the SSZ specification. @@ -367,24 +644,20 @@ fn ssz_encode_derive_enum_union(derive_input: &DeriveInput, enum_data: &DataEnum #[proc_macro_derive(Decode, attributes(ssz))] pub fn ssz_decode_derive(input: TokenStream) -> TokenStream { let item = parse_macro_input!(input as DeriveInput); - let opts = StructOpts::from_derive_input(&item).unwrap(); - let enum_opt = EnumBehaviour::new(opts.enum_behaviour); + let procedure = Procedure::read(&item); - match &item.data { - syn::Data::Struct(s) => { - if enum_opt.is_some() { - panic!("enum_behaviour is invalid for structs"); - } - ssz_decode_derive_struct(&item, s) - } - syn::Data::Enum(s) => match enum_opt.expect(NO_ENUM_BEHAVIOUR_ERROR) { + match procedure { + Procedure::Struct { data, behaviour } => match behaviour { + StructBehaviour::Transparent => ssz_decode_derive_struct_transparent(&item, data), + StructBehaviour::Container => ssz_decode_derive_struct(&item, data), + }, + Procedure::Enum { data, behaviour } => match behaviour { + EnumBehaviour::Union => ssz_decode_derive_enum_union(&item, data), EnumBehaviour::Transparent => panic!( "Decode cannot be derived for enum_behaviour \"{}\", only \"{}\" is valid.", ENUM_TRANSPARENT, ENUM_UNION ), - EnumBehaviour::Union => ssz_decode_derive_enum_union(&item, s), }, - _ => panic!("ssz_derive only supports structs and enums"), } } @@ -409,6 +682,13 @@ fn ssz_decode_derive_struct(item: &DeriveInput, struct_data: &DataStruct) -> Tok let mut fixed_lens = vec![]; for (ty, ident, field_opts) in parse_ssz_fields(struct_data) { + let ident = match ident { + Some(ref ident) => ident, + _ => panic!( + "#[ssz(struct_behaviour = \"container\")] only supports named struct fields." + ), + }; + field_names.push(quote! { #ident }); @@ -545,6 +825,90 @@ fn ssz_decode_derive_struct(item: &DeriveInput, struct_data: &DataStruct) -> Tok output.into() } +/// Implements `ssz::Decode` "transparently" for a `struct` with exactly one non-skipped field. +/// +/// The bytes will be decoded as if they are the inner field, without the outermost struct. The +/// outermost struct will then be applied artificially. +/// +/// ## Field attributes +/// +/// - `#[ssz(skip_deserializing)]`: during de-serialization the field will be instantiated from a +/// `Default` implementation. The decoder will assume that the field was not serialized at all +/// (e.g., if it has been serialized, an error will be raised instead of `Default` overriding it). +fn ssz_decode_derive_struct_transparent( + item: &DeriveInput, + struct_data: &DataStruct, +) -> TokenStream { + let name = &item.ident; + let (impl_generics, ty_generics, where_clause) = &item.generics.split_for_impl(); + let ssz_fields = parse_ssz_fields(struct_data); + let num_fields = ssz_fields + .iter() + .filter(|(_, _, field_opts)| !field_opts.skip_deserializing) + .count(); + + if num_fields != 1 { + panic!( + "A \"transparent\" struct must have exactly one non-skipped field ({} fields found)", + num_fields + ); + } + + let mut fields = vec![]; + let mut wrapped_type = None; + + for (i, (ty, ident, field_opts)) in ssz_fields.into_iter().enumerate() { + if let Some(name) = ident { + if field_opts.skip_deserializing { + fields.push(quote! { + #name: <_>::default(), + }); + } else { + fields.push(quote! { + #name: <_>::from_ssz_bytes(bytes)?, + }); + wrapped_type = Some(ty); + } + } else { + let index = syn::Index::from(i); + if field_opts.skip_deserializing { + fields.push(quote! { + #index:<_>::default(), + }); + } else { + fields.push(quote! { + #index:<_>::from_ssz_bytes(bytes)?, + }); + wrapped_type = Some(ty); + } + } + } + + let ty = wrapped_type.unwrap(); + + let output = quote! { + impl #impl_generics ssz::Decode for #name #ty_generics #where_clause { + fn is_ssz_fixed_len() -> bool { + <#ty as ssz::Decode>::is_ssz_fixed_len() + } + + fn ssz_fixed_len() -> usize { + <#ty as ssz::Decode>::ssz_fixed_len() + } + + fn from_ssz_bytes(bytes: &[u8]) -> std::result::Result { + Ok(Self { + #( + #fields + )* + + }) + } + } + }; + output.into() +} + /// Derive `ssz::Decode` for an `enum` following the "union" SSZ spec. fn ssz_decode_derive_enum_union(derive_input: &DeriveInput, enum_data: &DataEnum) -> TokenStream { let name = &derive_input.ident; diff --git a/consensus/ssz_derive/tests/tests.rs b/consensus/ssz_derive/tests/tests.rs new file mode 100644 index 00000000000..040d2a34761 --- /dev/null +++ b/consensus/ssz_derive/tests/tests.rs @@ -0,0 +1,236 @@ +use ssz::{Decode, Encode}; +use ssz_derive::{Decode, Encode}; +use std::fmt::Debug; +use std::marker::PhantomData; + +fn assert_encode(item: &T, bytes: &[u8]) { + assert_eq!(item.as_ssz_bytes(), bytes); +} + +fn assert_encode_decode(item: &T, bytes: &[u8]) { + assert_encode(item, bytes); + assert_eq!(T::from_ssz_bytes(bytes).unwrap(), *item); +} + +#[derive(PartialEq, Debug, Encode, Decode)] +#[ssz(enum_behaviour = "union")] +enum TwoFixedUnion { + U8(u8), + U16(u16), +} + +#[derive(PartialEq, Debug, Encode, Decode)] +struct TwoFixedUnionStruct { + a: TwoFixedUnion, +} + +#[test] +fn two_fixed_union() { + let eight = TwoFixedUnion::U8(1); + let sixteen = TwoFixedUnion::U16(1); + + assert_encode_decode(&eight, &[0, 1]); + assert_encode_decode(&sixteen, &[1, 1, 0]); + + assert_encode_decode(&TwoFixedUnionStruct { a: eight }, &[4, 0, 0, 0, 0, 1]); + assert_encode_decode(&TwoFixedUnionStruct { a: sixteen }, &[4, 0, 0, 0, 1, 1, 0]); +} + +#[derive(PartialEq, Debug, Encode, Decode)] +struct VariableA { + a: u8, + b: Vec, +} + +#[derive(PartialEq, Debug, Encode, Decode)] +struct VariableB { + a: Vec, + b: u8, +} + +#[derive(PartialEq, Debug, Encode)] +#[ssz(enum_behaviour = "transparent")] +enum TwoVariableTrans { + A(VariableA), + B(VariableB), +} + +#[derive(PartialEq, Debug, Encode)] +struct TwoVariableTransStruct { + a: TwoVariableTrans, +} + +#[derive(PartialEq, Debug, Encode, Decode)] +#[ssz(enum_behaviour = "union")] +enum TwoVariableUnion { + A(VariableA), + B(VariableB), +} + +#[derive(PartialEq, Debug, Encode, Decode)] +struct TwoVariableUnionStruct { + a: TwoVariableUnion, +} + +#[test] +fn two_variable_trans() { + let trans_a = TwoVariableTrans::A(VariableA { + a: 1, + b: vec![2, 3], + }); + let trans_b = TwoVariableTrans::B(VariableB { + a: vec![1, 2], + b: 3, + }); + + assert_encode(&trans_a, &[1, 5, 0, 0, 0, 2, 3]); + assert_encode(&trans_b, &[5, 0, 0, 0, 3, 1, 2]); + + assert_encode( + &TwoVariableTransStruct { a: trans_a }, + &[4, 0, 0, 0, 1, 5, 0, 0, 0, 2, 3], + ); + assert_encode( + &TwoVariableTransStruct { a: trans_b }, + &[4, 0, 0, 0, 5, 0, 0, 0, 3, 1, 2], + ); +} + +#[test] +fn two_variable_union() { + let union_a = TwoVariableUnion::A(VariableA { + a: 1, + b: vec![2, 3], + }); + let union_b = TwoVariableUnion::B(VariableB { + a: vec![1, 2], + b: 3, + }); + + assert_encode_decode(&union_a, &[0, 1, 5, 0, 0, 0, 2, 3]); + assert_encode_decode(&union_b, &[1, 5, 0, 0, 0, 3, 1, 2]); + + assert_encode_decode( + &TwoVariableUnionStruct { a: union_a }, + &[4, 0, 0, 0, 0, 1, 5, 0, 0, 0, 2, 3], + ); + assert_encode_decode( + &TwoVariableUnionStruct { a: union_b }, + &[4, 0, 0, 0, 1, 5, 0, 0, 0, 3, 1, 2], + ); +} + +#[derive(PartialEq, Debug, Encode, Decode)] +#[ssz(enum_behaviour = "union")] +enum TwoVecUnion { + A(Vec), + B(Vec), +} + +#[test] +fn two_vec_union() { + assert_encode_decode(&TwoVecUnion::A(vec![]), &[0]); + assert_encode_decode(&TwoVecUnion::B(vec![]), &[1]); + + assert_encode_decode(&TwoVecUnion::A(vec![0]), &[0, 0]); + assert_encode_decode(&TwoVecUnion::B(vec![0]), &[1, 0]); + + assert_encode_decode(&TwoVecUnion::A(vec![0, 1]), &[0, 0, 1]); + assert_encode_decode(&TwoVecUnion::B(vec![0, 1]), &[1, 0, 1]); +} + +#[derive(PartialEq, Debug, Encode, Decode)] +#[ssz(struct_behaviour = "transparent")] +struct TransparentStruct { + inner: Vec, +} + +impl TransparentStruct { + fn new(inner: u8) -> Self { + Self { inner: vec![inner] } + } +} + +#[test] +fn transparent_struct() { + assert_encode_decode(&TransparentStruct::new(42), &vec![42_u8].as_ssz_bytes()); +} + +#[derive(PartialEq, Debug, Encode, Decode)] +#[ssz(struct_behaviour = "transparent")] +struct TransparentStructSkippedField { + inner: Vec, + #[ssz(skip_serializing, skip_deserializing)] + skipped: PhantomData, +} + +impl TransparentStructSkippedField { + fn new(inner: u8) -> Self { + Self { + inner: vec![inner], + skipped: PhantomData, + } + } +} + +#[test] +fn transparent_struct_skipped_field() { + assert_encode_decode( + &TransparentStructSkippedField::new(42), + &vec![42_u8].as_ssz_bytes(), + ); +} + +#[derive(PartialEq, Debug, Encode, Decode)] +#[ssz(struct_behaviour = "transparent")] +struct TransparentStructNewType(Vec); + +#[test] +fn transparent_struct_newtype() { + assert_encode_decode( + &TransparentStructNewType(vec![42_u8]), + &vec![42_u8].as_ssz_bytes(), + ); +} + +#[derive(PartialEq, Debug, Encode, Decode)] +#[ssz(struct_behaviour = "transparent")] +struct TransparentStructNewTypeSkippedField( + Vec, + #[ssz(skip_serializing, skip_deserializing)] PhantomData, +); + +impl TransparentStructNewTypeSkippedField { + fn new(inner: Vec) -> Self { + Self(inner, PhantomData) + } +} + +#[test] +fn transparent_struct_newtype_skipped_field() { + assert_encode_decode( + &TransparentStructNewTypeSkippedField::new(vec![42_u8]), + &vec![42_u8].as_ssz_bytes(), + ); +} + +#[derive(PartialEq, Debug, Encode, Decode)] +#[ssz(struct_behaviour = "transparent")] +struct TransparentStructNewTypeSkippedFieldReverse( + #[ssz(skip_serializing, skip_deserializing)] PhantomData, + Vec, +); + +impl TransparentStructNewTypeSkippedFieldReverse { + fn new(inner: Vec) -> Self { + Self(PhantomData, inner) + } +} + +#[test] +fn transparent_struct_newtype_skipped_field_reverse() { + assert_encode_decode( + &TransparentStructNewTypeSkippedFieldReverse::new(vec![42_u8]), + &vec![42_u8].as_ssz_bytes(), + ); +} diff --git a/consensus/state_processing/Cargo.toml b/consensus/state_processing/Cargo.toml index 46ac2bae577..ccb41830be8 100644 --- a/consensus/state_processing/Cargo.toml +++ b/consensus/state_processing/Cargo.toml @@ -14,7 +14,7 @@ bls = { path = "../../crypto/bls" } integer-sqrt = "0.1.5" itertools = "0.10.0" eth2_ssz = "0.4.1" -eth2_ssz_derive = "0.3.0" +eth2_ssz_derive = "0.3.1" eth2_ssz_types = "0.2.2" merkle_proof = { path = "../merkle_proof" } safe_arith = { path = "../safe_arith" } diff --git a/consensus/tree_hash/Cargo.toml b/consensus/tree_hash/Cargo.toml index ab080eac065..1f004724fcb 100644 --- a/consensus/tree_hash/Cargo.toml +++ b/consensus/tree_hash/Cargo.toml @@ -12,7 +12,7 @@ tree_hash_derive = "0.4.0" types = { path = "../types" } beacon_chain = { path = "../../beacon_node/beacon_chain" } eth2_ssz = "0.4.1" -eth2_ssz_derive = "0.3.0" +eth2_ssz_derive = "0.3.1" [dependencies] ethereum-types = "0.12.1" diff --git a/consensus/types/Cargo.toml b/consensus/types/Cargo.toml index 68fdbf7990d..bcb7daa7054 100644 --- a/consensus/types/Cargo.toml +++ b/consensus/types/Cargo.toml @@ -25,7 +25,7 @@ serde = {version = "1.0.116" , features = ["rc"] } serde_derive = "1.0.116" slog = "2.5.2" eth2_ssz = "0.4.1" -eth2_ssz_derive = "0.3.0" +eth2_ssz_derive = "0.3.1" eth2_ssz_types = "0.2.2" swap_or_not_shuffle = { path = "../swap_or_not_shuffle" } test_random_derive = { path = "../../common/test_random_derive" } diff --git a/slasher/Cargo.toml b/slasher/Cargo.toml index 0f24fe9f04f..c5ce8793ad4 100644 --- a/slasher/Cargo.toml +++ b/slasher/Cargo.toml @@ -13,7 +13,7 @@ lmdb = ["lmdb-rkv", "lmdb-rkv-sys"] bincode = "1.3.1" byteorder = "1.3.4" eth2_ssz = "0.4.1" -eth2_ssz_derive = "0.3.0" +eth2_ssz_derive = "0.3.1" flate2 = { version = "1.0.14", features = ["zlib"], default-features = false } lazy_static = "1.4.0" lighthouse_metrics = { path = "../common/lighthouse_metrics" } diff --git a/testing/ef_tests/Cargo.toml b/testing/ef_tests/Cargo.toml index e04d671396e..294bbe69b68 100644 --- a/testing/ef_tests/Cargo.toml +++ b/testing/ef_tests/Cargo.toml @@ -23,7 +23,7 @@ serde_derive = "1.0.116" serde_repr = "0.1.6" serde_yaml = "0.8.13" eth2_ssz = "0.4.1" -eth2_ssz_derive = "0.3.0" +eth2_ssz_derive = "0.3.1" tree_hash = "0.4.1" tree_hash_derive = "0.4.0" cached_tree_hash = { path = "../../consensus/cached_tree_hash" }