diff --git a/packages/fuels-core/src/codec/abi_decoder.rs b/packages/fuels-core/src/codec/abi_decoder.rs index ff12f098fe..a8a2f9baf4 100644 --- a/packages/fuels-core/src/codec/abi_decoder.rs +++ b/packages/fuels-core/src/codec/abi_decoder.rs @@ -521,6 +521,73 @@ mod tests { Ok(()) } + #[test] + fn decoding_enum_with_more_than_one_heap_type_variant_fails() -> Result<()> { + let mut param_types = vec![ + ParamType::U64, + ParamType::Bool, + ParamType::Vector(Box::from(ParamType::U64)), + ]; + // empty data + let data = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]; + let variants = EnumVariants::new(param_types.clone())?; + let enum_param_type = ParamType::Enum { + variants, + generics: vec![], + }; + // it works if there is only one heap type + let _ = ABIDecoder::default().decode(&enum_param_type, &data)?; + + param_types.append(&mut vec![ParamType::Bytes]); + let variants = EnumVariants::new(param_types)?; + let enum_param_type = ParamType::Enum { + variants, + generics: vec![], + }; + // fails if there is more than one variant using heap type in the enum + let error = ABIDecoder::default() + .decode(&enum_param_type, &data) + .expect_err("Should fail"); + let expected_error = + "Invalid type: Enums currently support only one heap-type variant. Found: 2" + .to_string(); + assert_eq!(error.to_string(), expected_error); + + Ok(()) + } + + #[test] + fn enums_w_too_deeply_nested_heap_types_not_allowed() { + let param_types = vec![ + ParamType::U8, + ParamType::Struct { + fields: vec![ParamType::RawSlice], + generics: vec![], + }, + ]; + let variants = EnumVariants::new(param_types).unwrap(); + let enum_param_type = ParamType::Enum { + variants, + generics: vec![], + }; + + let err = ABIDecoder::default() + .decode(&enum_param_type, &[]) + .expect_err("should have failed"); + + let Error::InvalidType(msg) = err else { + panic!("Unexpected err: {err}"); + }; + + assert_eq!( + msg, + "Enums currently support only one level deep heap types." + ); + } + #[test] fn max_depth_surpassed() { const MAX_DEPTH: usize = 2; diff --git a/packages/fuels-core/src/codec/abi_decoder/bounded_decoder.rs b/packages/fuels-core/src/codec/abi_decoder/bounded_decoder.rs index 4c5066c24a..b7b434093e 100644 --- a/packages/fuels-core/src/codec/abi_decoder/bounded_decoder.rs +++ b/packages/fuels-core/src/codec/abi_decoder/bounded_decoder.rs @@ -36,7 +36,7 @@ impl BoundedDecoder { } pub(crate) fn decode(&mut self, param_type: &ParamType, bytes: &[u8]) -> Result { - Self::is_type_decodable(param_type)?; + param_type.validate_is_decodable()?; Ok(self.decode_param(param_type, bytes)?.token) } @@ -46,24 +46,13 @@ impl BoundedDecoder { bytes: &[u8], ) -> Result> { for param_type in param_types { - Self::is_type_decodable(param_type)?; + param_type.validate_is_decodable()?; } let (tokens, _) = self.decode_params(param_types, bytes)?; Ok(tokens) } - fn is_type_decodable(param_type: &ParamType) -> Result<()> { - if param_type.contains_nested_heap_types() { - Err(error!( - InvalidType, - "Type {param_type:?} contains nested heap types (`Vec` or `Bytes`), this is not supported." - )) - } else { - Ok(()) - } - } - fn run_w_depth_tracking( &mut self, decoder: impl FnOnce(&mut Self) -> Result, @@ -312,8 +301,12 @@ impl BoundedDecoder { let discriminant = peek_u32(bytes)? as u8; let selected_variant = variants.param_type_of_variant(discriminant)?; - - let words_to_skip = enum_width - selected_variant.compute_encoding_width(); + let skip_extra = variants + .heap_type_variant() + .is_some_and(|(heap_discriminant, _)| heap_discriminant == discriminant) + .then_some(3); + let words_to_skip = + enum_width - selected_variant.compute_encoding_width() + skip_extra.unwrap_or_default(); let enum_content_bytes = skip(bytes, words_to_skip * WORD_SIZE)?; let result = self.decode_token_in_enum(enum_content_bytes, variants, selected_variant)?; diff --git a/packages/fuels-core/src/types/enum_variants.rs b/packages/fuels-core/src/types/enum_variants.rs index ef532bd876..a709c11d15 100644 --- a/packages/fuels-core/src/types/enum_variants.rs +++ b/packages/fuels-core/src/types/enum_variants.rs @@ -13,11 +13,10 @@ pub struct EnumVariants { impl EnumVariants { pub fn new(param_types: Vec) -> Result { - if !param_types.is_empty() { - Ok(EnumVariants { param_types }) - } else { - Err(error!(InvalidData, "Enum variants can not be empty!")) + if param_types.is_empty() { + return Err(error!(InvalidData, "Enum variants can not be empty!")); } + Ok(EnumVariants { param_types }) } pub fn param_types(&self) -> &[ParamType] { @@ -34,6 +33,13 @@ impl EnumVariants { }) } + pub fn heap_type_variant(&self) -> Option<(u8, &ParamType)> { + self.param_types() + .iter() + .enumerate() + .find_map(|(d, p)| p.is_extra_receipt_needed(false).then_some((d as u8, p))) + } + pub fn only_units_inside(&self) -> bool { self.param_types .iter() @@ -64,3 +70,32 @@ impl EnumVariants { (biggest_variant_width - variant_width) * WORD_SIZE } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_get_heap_type_variant_discriminant() -> Result<()> { + let param_types = vec![ + ParamType::U64, + ParamType::Bool, + ParamType::Vector(Box::from(ParamType::U64)), + ]; + let variants = EnumVariants::new(param_types)?; + assert_eq!(variants.heap_type_variant().unwrap().0, 2); + + let param_types = vec![ + ParamType::Vector(Box::from(ParamType::U64)), + ParamType::U64, + ParamType::Bool, + ]; + let variants = EnumVariants::new(param_types)?; + assert_eq!(variants.heap_type_variant().unwrap().0, 0); + + let param_types = vec![ParamType::U64, ParamType::Bool]; + let variants = EnumVariants::new(param_types)?; + assert!(variants.heap_type_variant().is_none()); + Ok(()) + } +} diff --git a/packages/fuels-core/src/types/param_types.rs b/packages/fuels-core/src/types/param_types.rs index c292dd1e82..7818e19449 100644 --- a/packages/fuels-core/src/types/param_types.rs +++ b/packages/fuels-core/src/types/param_types.rs @@ -1,10 +1,10 @@ +use itertools::chain; use std::{collections::HashMap, iter::zip}; use fuel_abi_types::{ abi::program::{TypeApplication, TypeDeclaration}, utils::{extract_array_len, extract_generic_name, extract_str_len, has_tuple_format}, }; -use itertools::chain; use crate::{ constants::WORD_SIZE, @@ -84,57 +84,87 @@ impl ParamType { Ok(available_bytes / memory_size) } - pub fn contains_nested_heap_types(&self) -> bool { - match &self { - ParamType::Vector(param_type) => param_type.uses_heap_types(), - ParamType::Bytes => false, - // Here, we return false because even though the `Token::String` type has an underlying - // `Bytes` type nested, it is an exception that will be generalized as part of - // https://github.com/FuelLabs/fuels-rs/discussions/944 - ParamType::String => false, - _ => self.uses_heap_types(), - } - } - - fn uses_heap_types(&self) -> bool { - match &self { - ParamType::Vector(..) | ParamType::Bytes | ParamType::String => true, - ParamType::Array(param_type, ..) => param_type.uses_heap_types(), - ParamType::Tuple(param_types, ..) => Self::any_nested_heap_types(param_types), - ParamType::Enum { - generics, variants, .. - } => { - let variants_types = variants.param_types(); - Self::any_nested_heap_types(chain!(generics, variants_types)) + pub fn children_need_extra_receipts(&self) -> bool { + match self { + ParamType::Array(inner, _) | ParamType::Vector(inner) => { + inner.is_extra_receipt_needed(false) } - ParamType::Struct { - fields, generics, .. - } => Self::any_nested_heap_types(chain!(fields, generics)), + ParamType::Struct { fields, .. } => fields + .iter() + .any(|param_type| param_type.is_extra_receipt_needed(false)), + ParamType::Enum { variants, .. } => variants + .param_types() + .iter() + .any(|param_type| param_type.is_extra_receipt_needed(false)), + ParamType::Tuple(inner_types) => inner_types + .iter() + .any(|param_type| param_type.is_extra_receipt_needed(false)), _ => false, } } - fn any_nested_heap_types<'a>(param_types: impl IntoIterator) -> bool { - param_types - .into_iter() - .any(|param_type| param_type.uses_heap_types()) + pub fn validate_is_decodable(&self) -> Result<()> { + match self { + ParamType::Enum { variants, .. } => { + let all_param_types = variants.param_types(); + let grandchildren_need_receipts = all_param_types + .iter() + .any(|child| child.children_need_extra_receipts()); + if grandchildren_need_receipts { + return Err(error!( + InvalidType, + "Enums currently support only one level deep heap types." + )); + } + + let num_of_children_needing_receipts = all_param_types + .iter() + .filter(|param_type| param_type.is_extra_receipt_needed(false)) + .count(); + if num_of_children_needing_receipts > 1 { + Err(error!( + InvalidType, + "Enums currently support only one heap-type variant. Found: \ + {num_of_children_needing_receipts}" + )) + } else { + Ok(()) + } + } + _ if self.children_need_extra_receipts() => Err(error!( + InvalidType, + "Nested heap types are currently not supported except in Enums." + )), + _ => Ok(()), + } } - pub fn is_vm_heap_type(&self) -> bool { - matches!( - self, - ParamType::Vector(..) | ParamType::Bytes | ParamType::String - ) + pub fn is_extra_receipt_needed(&self, top_level_type: bool) -> bool { + match self { + ParamType::Vector(_) | ParamType::Bytes | ParamType::String => true, + ParamType::Array(inner, _) => inner.is_extra_receipt_needed(false), + ParamType::Struct { fields, generics } => { + chain!(fields, generics).any(|param_type| param_type.is_extra_receipt_needed(false)) + } + ParamType::Enum { variants, generics } => chain!(variants.param_types(), generics) + .any(|param_type| param_type.is_extra_receipt_needed(false)), + ParamType::Tuple(elements) => elements + .iter() + .any(|param_type| param_type.is_extra_receipt_needed(false)), + ParamType::RawSlice => !top_level_type, + _ => false, + } } /// Compute the inner memory size of a containing heap type (`Bytes` or `Vec`s). - pub fn heap_inner_element_size(&self) -> Option { + pub fn heap_inner_element_size(&self, top_level_type: bool) -> Option { match &self { ParamType::Vector(inner_param_type) => { Some(inner_param_type.compute_encoding_width() * WORD_SIZE) } // `Bytes` type is byte-packed in the VM, so it's the size of an u8 ParamType::Bytes | ParamType::String => Some(std::mem::size_of::()), + ParamType::RawSlice if !top_level_type => Some(ParamType::U64.compute_encoding_width()), _ => None, } } @@ -1301,125 +1331,219 @@ mod tests { } #[test] - fn contains_nested_heap_types_false_on_simple_types() -> Result<()> { - // Simple types cannot have nested heap types - assert!(!ParamType::Unit.contains_nested_heap_types()); - assert!(!ParamType::U8.contains_nested_heap_types()); - assert!(!ParamType::U16.contains_nested_heap_types()); - assert!(!ParamType::U32.contains_nested_heap_types()); - assert!(!ParamType::U64.contains_nested_heap_types()); - assert!(!ParamType::Bool.contains_nested_heap_types()); - assert!(!ParamType::B256.contains_nested_heap_types()); - assert!(!ParamType::StringArray(10).contains_nested_heap_types()); - assert!(!ParamType::RawSlice.contains_nested_heap_types()); - assert!(!ParamType::Bytes.contains_nested_heap_types()); - assert!(!ParamType::String.contains_nested_heap_types()); + fn validate_is_decodable_simple_types() -> Result<()> { + assert!(ParamType::U8.validate_is_decodable().is_ok()); + assert!(ParamType::U16.validate_is_decodable().is_ok()); + assert!(ParamType::U32.validate_is_decodable().is_ok()); + assert!(ParamType::U64.validate_is_decodable().is_ok()); + assert!(ParamType::U128.validate_is_decodable().is_ok()); + assert!(ParamType::U256.validate_is_decodable().is_ok()); + assert!(ParamType::Bool.validate_is_decodable().is_ok()); + assert!(ParamType::B256.validate_is_decodable().is_ok()); + assert!(ParamType::Unit.validate_is_decodable().is_ok()); + assert!(ParamType::StringSlice.validate_is_decodable().is_ok()); + assert!(ParamType::StringArray(10).validate_is_decodable().is_ok()); + assert!(ParamType::RawSlice.validate_is_decodable().is_ok()); + assert!(ParamType::Bytes.validate_is_decodable().is_ok()); + assert!(ParamType::String.validate_is_decodable().is_ok()); Ok(()) } #[test] - fn test_complex_types_for_nested_heap_types_containing_vectors() -> Result<()> { - let base_vector = ParamType::Vector(Box::from(ParamType::U8)); - let param_types_no_nested_vec = vec![ParamType::U64, ParamType::U32]; - let param_types_nested_vec = vec![ParamType::Unit, ParamType::Bool, base_vector.clone()]; - - let is_nested = |param_type: ParamType| assert!(param_type.contains_nested_heap_types()); - let not_nested = |param_type: ParamType| assert!(!param_type.contains_nested_heap_types()); - - not_nested(base_vector.clone()); - is_nested(ParamType::Vector(Box::from(base_vector.clone()))); + fn validate_is_decodable_complex_types_containing_bytes() -> Result<()> { + let param_types_containing_bytes = vec![ParamType::Bytes, ParamType::U64, ParamType::Bool]; + let param_types_no_bytes = vec![ParamType::U64, ParamType::U32]; + let nested_heap_type_error_message = "Invalid type: Nested heap types are currently not \ + supported except in Enums." + .to_string(); + let cannot_be_decoded = |p: ParamType| { + assert_eq!( + p.validate_is_decodable() + .expect_err(&format!("Should not be decodable: {:?}", p)) + .to_string(), + nested_heap_type_error_message + ) + }; + let can_be_decoded = |p: ParamType| p.validate_is_decodable().is_ok(); - not_nested(ParamType::Array(Box::from(ParamType::U8), 10)); - is_nested(ParamType::Array(Box::from(base_vector), 10)); + can_be_decoded(ParamType::Array(Box::new(ParamType::U64), 10usize)); + cannot_be_decoded(ParamType::Array(Box::new(ParamType::Bytes), 10usize)); - not_nested(ParamType::Tuple(param_types_no_nested_vec.clone())); - is_nested(ParamType::Tuple(param_types_nested_vec.clone())); + can_be_decoded(ParamType::Vector(Box::new(ParamType::U64))); + cannot_be_decoded(ParamType::Vector(Box::new(ParamType::Bytes))); - not_nested(ParamType::Struct { - generics: param_types_no_nested_vec.clone(), - fields: param_types_no_nested_vec.clone(), - }); - is_nested(ParamType::Struct { - generics: param_types_nested_vec.clone(), - fields: param_types_no_nested_vec.clone(), + can_be_decoded(ParamType::Struct { + generics: param_types_no_bytes.clone(), + fields: param_types_no_bytes.clone(), }); - is_nested(ParamType::Struct { - generics: param_types_no_nested_vec.clone(), - fields: param_types_nested_vec.clone(), + cannot_be_decoded(ParamType::Struct { + fields: param_types_containing_bytes.clone(), + generics: param_types_no_bytes.clone(), }); - not_nested(ParamType::Enum { - variants: EnumVariants::new(param_types_no_nested_vec.clone())?, - generics: param_types_no_nested_vec.clone(), + can_be_decoded(ParamType::Tuple(param_types_no_bytes.clone())); + cannot_be_decoded(ParamType::Tuple(param_types_containing_bytes.clone())); + + Ok(()) + } + + #[test] + fn validate_is_decodable_enum_containing_bytes() -> Result<()> { + let can_be_decoded = |p: ParamType| p.validate_is_decodable().is_ok(); + let param_types_containing_bytes = vec![ParamType::Bytes, ParamType::U64, ParamType::Bool]; + let param_types_no_bytes = vec![ParamType::U64, ParamType::U32]; + let variants_no_bytes_type = EnumVariants::new(param_types_no_bytes.clone())?; + let variants_one_bytes_type = EnumVariants::new(param_types_containing_bytes.clone())?; + let variants_two_bytes_type = EnumVariants::new(vec![ParamType::Bytes, ParamType::Bytes])?; + can_be_decoded(ParamType::Enum { + variants: variants_no_bytes_type.clone(), + generics: param_types_no_bytes.clone(), }); - is_nested(ParamType::Enum { - variants: EnumVariants::new(param_types_nested_vec.clone())?, - generics: param_types_no_nested_vec.clone(), + can_be_decoded(ParamType::Enum { + variants: variants_one_bytes_type.clone(), + generics: param_types_no_bytes.clone(), }); - is_nested(ParamType::Enum { - variants: EnumVariants::new(param_types_no_nested_vec)?, - generics: param_types_nested_vec, + let expected = "Invalid type: Enums currently support only one heap-type variant. Found: 2" + .to_string(); + assert_eq!( + ParamType::Enum { + variants: variants_two_bytes_type.clone(), + generics: param_types_no_bytes.clone(), + } + .validate_is_decodable() + .expect_err("Should not be decodable") + .to_string(), + expected + ); + can_be_decoded(ParamType::Enum { + variants: variants_no_bytes_type, + generics: param_types_containing_bytes.clone(), + }); + can_be_decoded(ParamType::Enum { + variants: variants_one_bytes_type, + generics: param_types_containing_bytes.clone(), }); + let expected = "Invalid type: Enums currently support only one heap-type variant. Found: 2" + .to_string(); + assert_eq!( + ParamType::Enum { + variants: variants_two_bytes_type.clone(), + generics: param_types_containing_bytes.clone(), + } + .validate_is_decodable() + .expect_err("Should not be decodable") + .to_string(), + expected + ); + Ok(()) } #[test] - fn test_complex_types_for_nested_heap_types_containing_bytes() -> Result<()> { - let base_bytes = ParamType::Bytes; - let param_types_no_nested_bytes = vec![ParamType::U64, ParamType::U32]; - let param_types_nested_bytes = vec![ParamType::Unit, ParamType::Bool, base_bytes.clone()]; - - let is_nested = |param_type: ParamType| assert!(param_type.contains_nested_heap_types()); - let not_nested = |param_type: ParamType| assert!(!param_type.contains_nested_heap_types()); - - not_nested(base_bytes.clone()); - is_nested(ParamType::Vector(Box::from(base_bytes.clone()))); - - not_nested(ParamType::Array(Box::from(ParamType::U8), 10)); - is_nested(ParamType::Array(Box::from(base_bytes), 10)); - - not_nested(ParamType::Tuple(param_types_no_nested_bytes.clone())); - is_nested(ParamType::Tuple(param_types_nested_bytes.clone())); - - let not_nested_struct = ParamType::Struct { - generics: param_types_no_nested_bytes.clone(), - fields: param_types_no_nested_bytes.clone(), - }; - not_nested(not_nested_struct); - - let nested_struct = ParamType::Struct { - generics: param_types_nested_bytes.clone(), - fields: param_types_no_nested_bytes.clone(), + fn validate_is_decodable_complex_types_containing_vector() -> Result<()> { + let param_types_containing_vector = vec![ + ParamType::Vector(Box::new(ParamType::U32)), + ParamType::U64, + ParamType::Bool, + ]; + let param_types_no_vector = vec![ParamType::U64, ParamType::U32]; + let nested_heap_type_error_message = "Invalid type: Nested heap types are currently not \ + supported except in Enums." + .to_string(); + let cannot_be_decoded = |p: ParamType| { + assert_eq!( + p.validate_is_decodable() + .expect_err(&format!("Should not be decodable: {:?}", p)) + .to_string(), + nested_heap_type_error_message + ) }; - is_nested(nested_struct); + let can_be_decoded = |p: ParamType| p.validate_is_decodable().is_ok(); - let nested_struct = ParamType::Struct { - generics: param_types_no_nested_bytes.clone(), - fields: param_types_nested_bytes.clone(), - }; - is_nested(nested_struct); + can_be_decoded(ParamType::Array(Box::new(ParamType::U64), 10usize)); + cannot_be_decoded(ParamType::Array( + Box::new(ParamType::Vector(Box::new(ParamType::U8))), + 10usize, + )); - let not_nested_enum = ParamType::Enum { - variants: EnumVariants::new(param_types_no_nested_bytes.clone())?, - generics: param_types_no_nested_bytes.clone(), - }; - not_nested(not_nested_enum); + can_be_decoded(ParamType::Vector(Box::new(ParamType::U64))); + cannot_be_decoded(ParamType::Vector(Box::new(ParamType::Vector(Box::new( + ParamType::U8, + ))))); - let nested_enum = ParamType::Enum { - variants: EnumVariants::new(param_types_nested_bytes.clone())?, - generics: param_types_no_nested_bytes.clone(), - }; - is_nested(nested_enum); + can_be_decoded(ParamType::Struct { + fields: param_types_no_vector.clone(), + generics: param_types_no_vector.clone(), + }); + cannot_be_decoded(ParamType::Struct { + generics: param_types_no_vector.clone(), + fields: param_types_containing_vector.clone(), + }); - let nested_enum = ParamType::Enum { - variants: EnumVariants::new(param_types_no_nested_bytes)?, - generics: param_types_nested_bytes, - }; - is_nested(nested_enum); + can_be_decoded(ParamType::Tuple(param_types_no_vector.clone())); + cannot_be_decoded(ParamType::Tuple(param_types_containing_vector.clone())); Ok(()) } + #[test] + fn validate_is_decodable_enum_containing_vector() -> Result<()> { + let can_be_decoded = |p: ParamType| p.validate_is_decodable().is_ok(); + let param_types_containing_vector = vec![ + ParamType::Vector(Box::new(ParamType::Bool)), + ParamType::U64, + ParamType::Bool, + ]; + let param_types_no_vector = vec![ParamType::U64, ParamType::U32]; + let variants_no_vector_type = EnumVariants::new(param_types_no_vector.clone())?; + let variants_one_vector_type = EnumVariants::new(param_types_containing_vector.clone())?; + let variants_two_vector_type = EnumVariants::new(vec![ + ParamType::Vector(Box::new(ParamType::U8)), + ParamType::Vector(Box::new(ParamType::U16)), + ])?; + can_be_decoded(ParamType::Enum { + variants: variants_no_vector_type.clone(), + generics: param_types_no_vector.clone(), + }); + can_be_decoded(ParamType::Enum { + variants: variants_one_vector_type.clone(), + generics: param_types_no_vector.clone(), + }); + let expected = "Invalid type: Enums currently support only one heap-type variant. Found: 2" + .to_string(); + assert_eq!( + ParamType::Enum { + variants: variants_two_vector_type.clone(), + generics: param_types_no_vector.clone(), + } + .validate_is_decodable() + .expect_err("Should not be decodable") + .to_string(), + expected + ); + can_be_decoded(ParamType::Enum { + variants: variants_no_vector_type, + generics: param_types_containing_vector.clone(), + }); + can_be_decoded(ParamType::Enum { + variants: variants_one_vector_type, + generics: param_types_containing_vector.clone(), + }); + let expected = "Invalid type: Enums currently support only one heap-type variant. Found: 2" + .to_string(); + assert_eq!( + ParamType::Enum { + variants: variants_two_vector_type.clone(), + generics: param_types_containing_vector.clone(), + } + .validate_is_decodable() + .expect_err("Should not be decodable") + .to_string(), + expected + ); + + Ok(()) + } #[test] fn try_vector_is_type_path_backward_compatible() { // TODO: To be removed once https://github.com/FuelLabs/fuels-rs/issues/881 is unblocked. diff --git a/packages/fuels-programs/src/call_utils.rs b/packages/fuels-programs/src/call_utils.rs index d8fdf9647d..49ce6be2da 100644 --- a/packages/fuels-programs/src/call_utils.rs +++ b/packages/fuels-programs/src/call_utils.rs @@ -158,13 +158,7 @@ fn compute_calls_instructions_len(calls: &[ContractCall]) -> usize { call_opcode_params.gas_forwarded_offset = Some(0); } - let param_type = if c.output_param.is_vm_heap_type() { - ParamType::Vector(Box::from(ParamType::U64)) - } else { - ParamType::U64 - }; - - get_single_call_instructions(&call_opcode_params, ¶m_type).len() + get_single_call_instructions(&call_opcode_params, &c.output_param).len() }) .sum() } @@ -354,32 +348,69 @@ pub(crate) fn get_single_call_instructions( None => instructions.push(op::call(0x10, 0x11, 0x12, RegId::CGAS)), }; - // The instructions are different if you want to return data that was on the heap - if let Some(inner_type_byte_size) = output_param_type.heap_inner_element_size() { - instructions.extend([ - // The RET register contains the pointer address of the `CALL` return (a stack - // address). - // The RETL register contains the length of the `CALL` return (=24 because the - // Vec/Bytes/String struct takes 3 WORDs). - // We don't actually need it unless the Vec/Bytes/String struct encoding changes in the - // compiler. - // Load the word located at the address contained in RET, it's a word that - // translates to a heap address. 0x15 is a free register. - op::lw(0x15, RegId::RET, 0), - // We know a Vec/Bytes/String struct has its third WORD contain the length of the - // underlying vector, so use a 2 offset to store the length in 0x16, which is a free - // register. - op::lw(0x16, RegId::RET, 2), - // The in-memory size of the type is (in-memory size of the inner type) * length - op::muli(0x16, 0x16, inner_type_byte_size as u16), - op::retd(0x15, 0x16), - ]); - } + instructions.extend(extract_heap_data(output_param_type)); #[allow(clippy::iter_cloned_collect)] instructions.into_iter().collect::>() } +fn extract_heap_data(param_type: &ParamType) -> Vec { + match param_type { + ParamType::Enum { variants, .. } => { + let Some((discriminant, heap_type)) = variants.heap_type_variant() else { + return vec![]; + }; + + let ptr_offset = (param_type.compute_encoding_width() - 3) as u16; + + [ + vec![ + // All the registers 0x15-0x18 are free + // Load the selected discriminant to a free register + op::movi(0x17, discriminant as u32), + // the first word of the CALL return is the enum discriminant. It is safe to load + // because the offset is 0. + op::lw(0x18, RegId::RET, 0), + // If the discriminant is not the one from the heap type, then jump ahead and + // return an empty receipt. Otherwise return heap data with the right length. + // Jump by (last argument + 1) instructions according to specs + op::jnef(0x17, 0x18, RegId::ZERO, 3), + ], + // ================= EXECUTED IF THE DISCRIMINANT POINTS TO A HEAP TYPE + extract_data_receipt(ptr_offset, false, heap_type), + // ================= EXECUTED IF THE DISCRIMINANT DOESN'T POINT TO A HEAP TYPE + vec![op::retd(0x15, RegId::ZERO)], + ] + .concat() + } + _ => extract_data_receipt(0, true, param_type), + } +} + +fn extract_data_receipt( + ptr_offset: u16, + top_level_type: bool, + param_type: &ParamType, +) -> Vec { + let Some(inner_type_byte_size) = param_type.heap_inner_element_size(top_level_type) else { + return vec![]; + }; + + let len_offset = match (top_level_type, param_type) { + // A nested RawSlice shows up as ptr,len + (false, ParamType::RawSlice) => 1, + // Every other heap type (currently) shows up as ptr,cap,len + _ => 2, + }; + + vec![ + op::lw(0x15, RegId::RET, ptr_offset), + op::lw(0x16, RegId::RET, ptr_offset + len_offset), + op::muli(0x16, 0x16, inner_type_byte_size as u16), + op::retd(0x15, 0x16), + ] +} + /// Returns the assets and contracts that will be consumed ([`Input`]s) /// and created ([`Output`]s) by the transaction pub(crate) fn get_transaction_inputs_outputs( diff --git a/packages/fuels-programs/src/contract.rs b/packages/fuels-programs/src/contract.rs index 8e1a3e4627..d42a7a5b34 100644 --- a/packages/fuels-programs/src/contract.rs +++ b/packages/fuels-programs/src/contract.rs @@ -826,7 +826,7 @@ impl MultiContractCallHandler { let number_of_heap_type_calls = self .contract_calls .iter() - .filter(|cc| cc.output_param.is_vm_heap_type()) + .filter(|cc| cc.output_param.is_extra_receipt_needed(true)) .count(); match number_of_heap_type_calls { @@ -837,7 +837,7 @@ impl MultiContractCallHandler { .last() .expect("is not empty") .output_param - .is_vm_heap_type() + .is_extra_receipt_needed(true) { Ok(()) } else { diff --git a/packages/fuels-programs/src/receipt_parser.rs b/packages/fuels-programs/src/receipt_parser.rs index 71414b8e88..7f7393764b 100644 --- a/packages/fuels-programs/src/receipt_parser.rs +++ b/packages/fuels-programs/src/receipt_parser.rs @@ -43,6 +43,8 @@ impl ReceiptParser { // During a script execution, the script's contract id is the **null** contract id .unwrap_or_else(ContractId::zeroed); + output_param.validate_is_decodable()?; + let data = self .extract_raw_data(output_param, &contract_id) .ok_or_else(|| Self::missing_receipts_error(output_param))?; @@ -62,8 +64,14 @@ impl ReceiptParser { output_param: &ParamType, contract_id: &ContractId, ) -> Option> { + let extra_receipts_needed = output_param.is_extra_receipt_needed(true); match output_param.get_return_location() { - ReturnLocation::ReturnData if output_param.is_vm_heap_type() => { + ReturnLocation::ReturnData + if extra_receipts_needed && matches!(output_param, ParamType::Enum { .. }) => + { + self.extract_enum_heap_type_data(contract_id) + } + ReturnLocation::ReturnData if extra_receipts_needed => { self.extract_return_data_heap(contract_id) } ReturnLocation::ReturnData => self.extract_return_data(contract_id), @@ -71,6 +79,24 @@ impl ReceiptParser { } } + fn extract_enum_heap_type_data(&mut self, contract_id: &ContractId) -> Option> { + for (index, (current_receipt, next_receipt)) in + self.receipts.iter().tuple_windows().enumerate() + { + if let (Some(first_data), Some(second_data)) = + Self::extract_heap_data_from_receipts(current_receipt, next_receipt, contract_id) + { + let mut first_data = first_data.clone(); + let mut second_data = second_data.clone(); + self.receipts.drain(index..=index + 1); + first_data.append(&mut second_data); + + return Some(first_data); + } + } + None + } + fn extract_return_data(&mut self, contract_id: &ContractId) -> Option> { for (index, receipt) in self.receipts.iter_mut().enumerate() { if let Receipt::ReturnData { @@ -114,8 +140,10 @@ impl ReceiptParser { for (index, (current_receipt, next_receipt)) in self.receipts.iter().tuple_windows().enumerate() { - if let Some(data) = Self::extract_vec_data(current_receipt, next_receipt, contract_id) { - let data = data.clone(); + if let (_stack_data, Some(heap_data)) = + Self::extract_heap_data_from_receipts(current_receipt, next_receipt, contract_id) + { + let data = heap_data.clone(); self.receipts.drain(index..=index + 1); return Some(data); } @@ -123,11 +151,11 @@ impl ReceiptParser { None } - fn extract_vec_data<'a>( - current_receipt: &Receipt, + fn extract_heap_data_from_receipts<'a>( + current_receipt: &'a Receipt, next_receipt: &'a Receipt, contract_id: &ContractId, - ) -> Option<&'a Vec> { + ) -> (Option<&'a Vec>, Option<&'a Vec>) { match (current_receipt, next_receipt) { ( Receipt::ReturnData { @@ -142,11 +170,13 @@ impl ReceiptParser { }, ) if *first_id == *contract_id && first_data.is_some() + // The second ReturnData receipt was added by a script instruction, its contract id + // is null && *second_id == ContractId::zeroed() => { - vec_data.as_ref() + (first_data.as_ref(), vec_data.as_ref()) } - _ => None, + _ => (None, None), } } } diff --git a/packages/fuels/Forc.toml b/packages/fuels/Forc.toml index 947f9f8938..7dcdc7c6c5 100644 --- a/packages/fuels/Forc.toml +++ b/packages/fuels/Forc.toml @@ -58,6 +58,7 @@ members = [ 'tests/types/contracts/enum_inside_struct', 'tests/types/contracts/evm_address', 'tests/types/contracts/generics', + 'tests/types/contracts/heap_type_in_enums', 'tests/types/contracts/identity', 'tests/types/contracts/native_types', 'tests/types/contracts/nested_structs', diff --git a/packages/fuels/tests/types/contracts/heap_type_in_enums/Forc.toml b/packages/fuels/tests/types/contracts/heap_type_in_enums/Forc.toml new file mode 100644 index 0000000000..cb9028fd51 --- /dev/null +++ b/packages/fuels/tests/types/contracts/heap_type_in_enums/Forc.toml @@ -0,0 +1,7 @@ +[project] +authors = ["Fuel Labs "] +entry = "main.sw" +license = "Apache-2.0" +name = "heap_type_in_enums" + +[dependencies] diff --git a/packages/fuels/tests/types/contracts/heap_type_in_enums/src/main.sw b/packages/fuels/tests/types/contracts/heap_type_in_enums/src/main.sw new file mode 100644 index 0000000000..92f32c8d80 --- /dev/null +++ b/packages/fuels/tests/types/contracts/heap_type_in_enums/src/main.sw @@ -0,0 +1,110 @@ +contract; + +use std::bytes::Bytes; +use std::string::String; + +pub enum TestError { + Something: [u8; 5], + Else: u64, +} + +pub struct Bimbam { + something: Bytes, +} + +abi MyContract { + fn returns_bytes_result(return_ok: bool) -> Result; + fn returns_vec_result(return_ok: bool) -> Result, TestError>; + fn returns_string_result(return_ok: bool) -> Result; + fn returns_bytes_option(return_some: bool) -> Option; + fn returns_vec_option(return_some: bool) -> Option>; + fn returns_string_option(return_some: bool) -> Option; + fn would_raise_a_memory_overflow() -> Result; + fn returns_a_heap_type_too_deep() -> Result; +} + +impl MyContract for Contract { + fn returns_bytes_result(return_ok: bool) -> Result { + return if return_ok { + let mut b = Bytes::new(); + b.push(1u8); + b.push(1u8); + b.push(1u8); + b.push(1u8); + Result::Ok(b) + } else { + Result::Err(TestError::Something([255u8, 255u8, 255u8, 255u8, 255u8])) + } + } + + fn returns_vec_result(return_ok: bool) -> Result, TestError> { + return if return_ok { + let mut v = Vec::new(); + v.push(2); + v.push(2); + v.push(2); + v.push(2); + v.push(2); + Result::Ok(v) + } else { + Result::Err(TestError::Else(7777)) + } + } + + fn returns_string_result(return_ok: bool) -> Result { + return if return_ok { + let s = String::from_ascii_str("Hello World"); + Result::Ok(s) + } else { + Result::Err(TestError::Else(3333)) + } + } + + fn returns_bytes_option(return_some: bool) -> Option { + return if return_some { + let mut b = Bytes::new(); + b.push(1u8); + b.push(1u8); + b.push(1u8); + b.push(1u8); + Option::Some(b) + } else { + Option::None + } + } + + fn returns_vec_option(return_some: bool) -> Option> { + return if return_some { + let mut v = Vec::new(); + v.push(2); + v.push(2); + v.push(2); + v.push(2); + v.push(2); + Option::Some(v) + } else { + None + } + } + + fn returns_string_option(return_some: bool) -> Option { + return if return_some { + let s = String::from_ascii_str("Hello World"); + Option::Some(s) + } else { + None + } + } + + fn would_raise_a_memory_overflow() -> Result { + Result::Err(0x1111111111111111111111111111111111111111111111111111111111111111) + } + + fn returns_a_heap_type_too_deep() -> Result { + let mut b = Bytes::new(); + b.push(2u8); + b.push(2u8); + b.push(2u8); + Result::Ok(Bimbam { something: b }) + } +} diff --git a/packages/fuels/tests/types_contracts.rs b/packages/fuels/tests/types_contracts.rs index 9ed4d5fd02..a50ea9c913 100644 --- a/packages/fuels/tests/types_contracts.rs +++ b/packages/fuels/tests/types_contracts.rs @@ -1995,3 +1995,75 @@ async fn test_contract_std_lib_string() -> Result<()> { Ok(()) } + +#[tokio::test] +async fn test_heap_type_in_enums() -> Result<()> { + let wallet = launch_provider_and_get_wallet().await; + setup_program_test!( + Abigen(Contract( + name = "HeapTypeInEnum", + project = "packages/fuels/tests/types/contracts/heap_type_in_enums" + )), + Deploy( + name = "contract_instance", + contract = "HeapTypeInEnum", + wallet = "wallet" + ), + ); + let contract_methods = contract_instance.methods(); + + let resp = contract_methods.returns_bytes_result(true).call().await?; + let expected = Ok(Bytes(vec![1, 1, 1, 1])); + assert_eq!(resp.value, expected); + let resp = contract_methods.returns_bytes_result(false).call().await?; + let expected = Err(TestError::Something([255u8, 255u8, 255u8, 255u8, 255u8])); + assert_eq!(resp.value, expected); + + let resp = contract_methods.returns_vec_result(true).call().await?; + let expected = Ok(vec![2, 2, 2, 2, 2]); + assert_eq!(resp.value, expected); + let resp = contract_methods.returns_vec_result(false).call().await?; + let expected = Err(TestError::Else(7777)); + assert_eq!(resp.value, expected); + + let resp = contract_methods.returns_string_result(true).call().await?; + let expected = Ok("Hello World".to_string()); + assert_eq!(resp.value, expected); + let resp = contract_methods.returns_string_result(false).call().await?; + let expected = Err(TestError::Else(3333)); + assert_eq!(resp.value, expected); + + let resp = contract_methods.returns_bytes_option(true).call().await?; + let expected = Some(Bytes(vec![1, 1, 1, 1])); + assert_eq!(resp.value, expected); + let resp = contract_methods.returns_bytes_option(false).call().await?; + assert!(resp.value.is_none()); + + let resp = contract_methods.returns_vec_option(true).call().await?; + let expected = Some(vec![2, 2, 2, 2, 2]); + assert_eq!(resp.value, expected); + let resp = contract_methods.returns_vec_option(false).call().await?; + assert!(resp.value.is_none()); + + let resp = contract_methods.returns_string_option(true).call().await?; + let expected = Some("Hello World".to_string()); + assert_eq!(resp.value, expected); + let resp = contract_methods.returns_string_option(false).call().await?; + assert!(resp.value.is_none()); + + // If the LW(RET) instruction was not executed only conditionally, then the FuelVM would OOM. + let _ = contract_methods + .would_raise_a_memory_overflow() + .call() + .await?; + + let resp = contract_methods + .returns_a_heap_type_too_deep() + .call() + .await + .expect_err("Should fail because it has a deeply nested heap type"); + let expected = + "Invalid type: Enums currently support only one level deep heap types.".to_string(); + assert_eq!(resp.to_string(), expected); + Ok(()) +}