From 1221632723162207008efee4f1980359f4ef89ea Mon Sep 17 00:00:00 2001 From: hal3e Date: Mon, 29 Jan 2024 23:14:03 +0100 Subject: [PATCH] feat!: add support for experimental encoding in logs (#1259) closes: https://github.com/FuelLabs/fuels-rs/issues/1247, https://github.com/FuelLabs/fuels-rs/issues/1151, https://github.com/FuelLabs/fuels-rs/issues/901, https://github.com/FuelLabs/fuels-rs/issues/898 This PR adds support for sway's experimental encoding in logs. The new encoder is enabled with the `experimental` rustflag. To run the tests, first build the forc projects with: `forc build --path packages/fuels --experimental-new-encoding` then run the tests with: `RUSTFLAGS='--cfg experimental' cargo test --test logs` What was done: - added `ExperimentalBoundedDecoder` - updated how `RawSlice` was encoded and decoded - added tests for the new encoding - add new CI step that runs tests with the new `experimental` flag BREAKING CHANGE: - change `RawSlice` encoding and decoding --- .github/workflows/ci.yml | 34 +- packages/fuels-core/src/codec/abi_decoder.rs | 19 + .../src/codec/abi_decoder/bounded_decoder.rs | 23 +- .../experimental_bounded_decoder.rs | 398 ++++++++++++++++++ packages/fuels-core/src/codec/abi_encoder.rs | 35 +- packages/fuels-core/src/codec/logs.rs | 26 +- packages/fuels-core/src/types.rs | 2 +- .../fuels-core/src/types/core/raw_slice.rs | 12 +- packages/fuels-core/src/types/param_types.rs | 16 +- packages/fuels/Forc.toml | 6 +- .../needs_custom_decoder/src/main.sw | 1 - packages/fuels/tests/logs.rs | 134 +++++- .../tests/logs/contract_logs/src/main.sw | 41 +- .../tests/logs/contract_logs_abi/src/main.sw | 4 + .../logs/script_experimental_logs/Forc.toml | 5 + .../logs/script_experimental_logs/src/main.sw | 46 ++ .../fuels/tests/logs/script_logs/src/main.sw | 2 + .../script_needs_custom_decoder/src/main.sw | 1 - .../types/contracts/raw_slice/src/main.sw | 8 +- .../predicate_raw_slice/src/main.sw | 2 +- packages/fuels/tests/types_contracts.rs | 4 +- 21 files changed, 738 insertions(+), 81 deletions(-) create mode 100644 packages/fuels-core/src/codec/abi_decoder/experimental_bounded_decoder.rs create mode 100644 packages/fuels/tests/logs/script_experimental_logs/Forc.toml create mode 100644 packages/fuels/tests/logs/script_experimental_logs/src/main.sw diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 918d05e433..c37546303a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -80,7 +80,7 @@ jobs: # TODO: To be removed once https://github.com/FuelLabs/fuels-rs/issues/881 is unblocked. - name: Build Sway test projects w type paths - run: forc build --terse --json-abi-with-callpaths + run: forc build --terse --error-on-warnings --json-abi-with-callpaths working-directory: packages/fuels - uses: actions/upload-artifact@v2 @@ -97,6 +97,25 @@ jobs: !packages/fuels/tests/**/Forc.lock !packages/fuels/tests/.gitignore + # TODO: To be removed once experimental encoding is the default + - name: Build Sway test projects w experimental logs + run: forc build --terse --error-on-warnings --json-abi-with-callpaths --experimental-new-encoding + working-directory: packages/fuels + + - uses: actions/upload-artifact@v2 + with: + retention-days: 2 + name: sway-examples-w-experimental-logs + # cache only the sway build artifacts, skip all src files + path: | + packages/fuels/tests + !packages/fuels/tests/*.rs + !packages/fuels/tests/**/*.rs + !packages/fuels/tests/**/*.sw + !packages/fuels/tests/**/Forc.toml + !packages/fuels/tests/**/Forc.lock + !packages/fuels/tests/.gitignore + get-workspace-members: runs-on: ubuntu-latest outputs: @@ -182,6 +201,11 @@ jobs: args: - command: check_doc_unresolved_links args: + # TODO: To be removed once experimental encoding is the default + - command: test_experimental_logs + args: + download_sway_artifacts: sway-examples-w-experimental-logs + install_fuel_core: true steps: - name: Checkout repository uses: actions/checkout@v3 @@ -218,8 +242,9 @@ jobs: name: ${{ matrix.download_sway_artifacts }} path: packages/fuels/tests/ + # TODO: `test_experimental_logs` to be removed once experimental encoding is the default. - name: Install nextest - if: ${{ matrix.cargo_command == 'nextest' }} + if: ${{ matrix.cargo_command == 'nextest' || matrix.command == 'test_experimental_logs' }} uses: taiki-e/install-action@nextest - name: Install cargo-machete @@ -257,6 +282,11 @@ jobs: run: | ! cargo doc --document-private-items |& grep -A 6 "warning: unresolved link to" + # TODO: To be removed once experimental encoding is the default. + - name: Test experimental logs + if: ${{ matrix.command == 'test_experimental_logs' }} + run: RUSTFLAGS='--cfg experimental' cargo nextest run --test logs + publish: needs: - cargo-verifications diff --git a/packages/fuels-core/src/codec/abi_decoder.rs b/packages/fuels-core/src/codec/abi_decoder.rs index 6b7434af87..2d7bcbd4ab 100644 --- a/packages/fuels-core/src/codec/abi_decoder.rs +++ b/packages/fuels-core/src/codec/abi_decoder.rs @@ -1,10 +1,15 @@ mod bounded_decoder; +#[cfg(experimental)] +mod experimental_bounded_decoder; use crate::{ codec::abi_decoder::bounded_decoder::BoundedDecoder, types::{errors::Result, param_types::ParamType, Token}, }; +#[cfg(experimental)] +use crate::codec::abi_decoder::experimental_bounded_decoder::ExperimentalBoundedDecoder; + #[derive(Debug, Clone, Copy)] pub struct DecoderConfig { /// Entering a struct, array, tuple, enum or vector increases the depth. Decoding will fail if @@ -77,6 +82,20 @@ impl ABIDecoder { pub fn decode_multiple(&self, param_types: &[ParamType], bytes: &[u8]) -> Result> { BoundedDecoder::new(self.config).decode_multiple(param_types, bytes) } + + #[cfg(experimental)] + pub fn experimental_decode(&self, param_type: &ParamType, bytes: &[u8]) -> Result { + ExperimentalBoundedDecoder::new(self.config).decode(param_type, bytes) + } + + #[cfg(experimental)] + pub fn experimental_decode_multiple( + &self, + param_types: &[ParamType], + bytes: &[u8], + ) -> Result> { + ExperimentalBoundedDecoder::new(self.config).decode_multiple(param_types, bytes) + } } #[cfg(test)] 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 3ad13d0cc9..d7556c2ff6 100644 --- a/packages/fuels-core/src/codec/abi_decoder/bounded_decoder.rs +++ b/packages/fuels-core/src/codec/abi_decoder/bounded_decoder.rs @@ -4,7 +4,6 @@ use crate::{ checked_round_up_to_word_alignment, codec::DecoderConfig, constants::WORD_SIZE, - traits::Tokenizable, types::{ enum_variants::EnumVariants, errors::{error, Result}, @@ -36,7 +35,7 @@ impl BoundedDecoder { } } - pub fn decode(&mut self, param_type: &ParamType, bytes: &[u8]) -> Result { + pub(crate) fn decode(&mut self, param_type: &ParamType, bytes: &[u8]) -> Result { param_type.validate_is_decodable(self.config.max_depth)?; match param_type { // Unit, U8 and Bool are returned as u64 from receipt "Return" @@ -94,7 +93,7 @@ impl BoundedDecoder { ParamType::U256 => Self::decode_u256(bytes), ParamType::Bool => Self::decode_bool(bytes), ParamType::B256 => Self::decode_b256(bytes), - ParamType::RawSlice => self.decode_raw_slice(bytes), + ParamType::RawSlice => Self::decode_raw_slice(bytes), ParamType::StringSlice => Self::decode_string_slice(bytes), ParamType::StringArray(len) => Self::decode_string_array(bytes, *len), ParamType::Array(ref t, length) => { @@ -215,22 +214,10 @@ impl BoundedDecoder { }) } - fn decode_raw_slice(&mut self, bytes: &[u8]) -> Result { - let raw_slice_element = ParamType::U64; - let num_of_elements = - ParamType::calculate_num_of_elements(&raw_slice_element, bytes.len())?; - let param_type = ParamType::U64; - let (tokens, bytes_read) = - self.decode_params(std::iter::repeat(¶m_type).take(num_of_elements), bytes)?; - let elements = tokens - .into_iter() - .map(u64::from_token) - .collect::>>() - .map_err(|e| error!(InvalidData, "{e}"))?; - + fn decode_raw_slice(bytes: &[u8]) -> Result { Ok(Decoded { - token: Token::RawSlice(elements), - bytes_read, + token: Token::RawSlice(bytes.to_vec()), + bytes_read: bytes.len(), }) } diff --git a/packages/fuels-core/src/codec/abi_decoder/experimental_bounded_decoder.rs b/packages/fuels-core/src/codec/abi_decoder/experimental_bounded_decoder.rs new file mode 100644 index 0000000000..e8728bf08b --- /dev/null +++ b/packages/fuels-core/src/codec/abi_decoder/experimental_bounded_decoder.rs @@ -0,0 +1,398 @@ +use std::{iter::repeat, str}; + +use crate::{ + codec::DecoderConfig, + constants::WORD_SIZE, + types::{ + enum_variants::EnumVariants, + errors::{error, Result}, + param_types::ParamType, + StaticStringToken, Token, U256, + }, +}; + +/// Is used to decode bytes into `Token`s from which types implementing `Tokenizable` can be +/// instantiated. Implements decoding limits to control resource usage. +pub(crate) struct ExperimentalBoundedDecoder { + depth_tracker: CounterWithLimit, + token_tracker: CounterWithLimit, +} + +const U8_BYTES_SIZE: usize = 1; +const U16_BYTES_SIZE: usize = 2; +const U32_BYTES_SIZE: usize = 4; +const U64_BYTES_SIZE: usize = WORD_SIZE; +const U128_BYTES_SIZE: usize = 2 * WORD_SIZE; +const U256_BYTES_SIZE: usize = 4 * WORD_SIZE; +const B256_BYTES_SIZE: usize = 4 * WORD_SIZE; +const LENGTH_BYTES_SIZE: usize = WORD_SIZE; +const DISCRIMINANT_BYTES_SIZE: usize = WORD_SIZE; + +impl ExperimentalBoundedDecoder { + pub(crate) fn new(config: DecoderConfig) -> Self { + let depth_tracker = CounterWithLimit::new(config.max_depth, "Depth"); + let token_tracker = CounterWithLimit::new(config.max_tokens, "Token"); + Self { + depth_tracker, + token_tracker, + } + } + + pub(crate) fn decode(&mut self, param_type: &ParamType, bytes: &[u8]) -> Result { + self.decode_param(param_type, bytes).map(|x| x.token) + } + + pub(crate) fn decode_multiple( + &mut self, + param_types: &[ParamType], + bytes: &[u8], + ) -> Result> { + let (tokens, _) = self.decode_params(param_types, bytes)?; + + Ok(tokens) + } + + fn run_w_depth_tracking( + &mut self, + decoder: impl FnOnce(&mut Self) -> Result, + ) -> Result { + self.depth_tracker.increase()?; + let res = decoder(self); + self.depth_tracker.decrease(); + + res + } + + fn decode_param(&mut self, param_type: &ParamType, bytes: &[u8]) -> Result { + self.token_tracker.increase()?; + match param_type { + ParamType::Unit => Self::decode_unit(), + ParamType::Bool => Self::decode_bool(bytes), + ParamType::U8 => Self::decode_u8(bytes), + ParamType::U16 => Self::decode_u16(bytes), + ParamType::U32 => Self::decode_u32(bytes), + ParamType::U64 => Self::decode_u64(bytes), + ParamType::U128 => Self::decode_u128(bytes), + ParamType::U256 => Self::decode_u256(bytes), + ParamType::B256 => Self::decode_b256(bytes), + ParamType::Bytes => Self::decode_bytes(bytes), + ParamType::String => Self::decode_std_string(bytes), + ParamType::RawSlice => Self::decode_raw_slice(bytes), + ParamType::StringArray(length) => Self::decode_string_array(bytes, *length), + ParamType::StringSlice => Self::decode_string_slice(bytes), + ParamType::Tuple(param_types) => { + self.run_w_depth_tracking(|ctx| ctx.decode_tuple(param_types, bytes)) + } + ParamType::Array(param_type, length) => { + self.run_w_depth_tracking(|ctx| ctx.decode_array(param_type, bytes, *length)) + } + ParamType::Vector(param_type) => { + self.run_w_depth_tracking(|ctx| ctx.decode_vector(param_type, bytes)) + } + + ParamType::Struct { fields, .. } => { + self.run_w_depth_tracking(|ctx| ctx.decode_struct(fields, bytes)) + } + ParamType::Enum { variants, .. } => { + self.run_w_depth_tracking(|ctx| ctx.decode_enum(bytes, variants)) + } + } + } + + fn decode_unit() -> Result { + Ok(Decoded { + token: Token::Unit, + bytes_read: 0, + }) + } + + fn decode_bool(bytes: &[u8]) -> Result { + let value = peek_u8(bytes)? != 0u8; + + Ok(Decoded { + token: Token::Bool(value), + bytes_read: U8_BYTES_SIZE, + }) + } + + fn decode_u8(bytes: &[u8]) -> Result { + Ok(Decoded { + token: Token::U8(peek_u8(bytes)?), + bytes_read: U8_BYTES_SIZE, + }) + } + + fn decode_u16(bytes: &[u8]) -> Result { + Ok(Decoded { + token: Token::U16(peek_u16(bytes)?), + bytes_read: U16_BYTES_SIZE, + }) + } + + fn decode_u32(bytes: &[u8]) -> Result { + Ok(Decoded { + token: Token::U32(peek_u32(bytes)?), + bytes_read: U32_BYTES_SIZE, + }) + } + + fn decode_u64(bytes: &[u8]) -> Result { + Ok(Decoded { + token: Token::U64(peek_u64(bytes)?), + bytes_read: U64_BYTES_SIZE, + }) + } + + fn decode_u128(bytes: &[u8]) -> Result { + Ok(Decoded { + token: Token::U128(peek_u128(bytes)?), + bytes_read: U128_BYTES_SIZE, + }) + } + + fn decode_u256(bytes: &[u8]) -> Result { + Ok(Decoded { + token: Token::U256(peek_u256(bytes)?), + bytes_read: U256_BYTES_SIZE, + }) + } + + fn decode_b256(bytes: &[u8]) -> Result { + Ok(Decoded { + token: Token::B256(*peek_fixed::(bytes)?), + bytes_read: B256_BYTES_SIZE, + }) + } + + fn decode_bytes(bytes: &[u8]) -> Result { + let length = peek_length(bytes)?; + let bytes = peek(skip(bytes, LENGTH_BYTES_SIZE)?, length)?; + + Ok(Decoded { + token: Token::Bytes(bytes.to_vec()), + bytes_read: LENGTH_BYTES_SIZE + bytes.len(), + }) + } + + fn decode_std_string(bytes: &[u8]) -> Result { + let length = peek_length(bytes)?; + let bytes = peek(skip(bytes, LENGTH_BYTES_SIZE)?, length)?; + + Ok(Decoded { + token: Token::String(str::from_utf8(bytes)?.to_string()), + bytes_read: LENGTH_BYTES_SIZE + bytes.len(), + }) + } + + fn decode_raw_slice(bytes: &[u8]) -> Result { + let length = peek_length(bytes)?; + let bytes = peek(skip(bytes, LENGTH_BYTES_SIZE)?, length)?; + + Ok(Decoded { + token: Token::RawSlice(bytes.to_vec()), + bytes_read: LENGTH_BYTES_SIZE + bytes.len(), + }) + } + + fn decode_string_array(bytes: &[u8], length: usize) -> Result { + let bytes = peek(bytes, length)?; + let decoded = str::from_utf8(bytes)?.to_string(); + + Ok(Decoded { + token: Token::StringArray(StaticStringToken::new(decoded, Some(length))), + bytes_read: length, + }) + } + + fn decode_string_slice(bytes: &[u8]) -> Result { + let length = peek_length(bytes)?; + let bytes = peek(skip(bytes, LENGTH_BYTES_SIZE)?, length)?; + let decoded = str::from_utf8(bytes)?.to_string(); + + Ok(Decoded { + token: Token::StringSlice(StaticStringToken::new(decoded, None)), + bytes_read: bytes.len(), + }) + } + + fn decode_tuple(&mut self, param_types: &[ParamType], bytes: &[u8]) -> Result { + let (tokens, bytes_read) = self.decode_params(param_types, bytes)?; + + Ok(Decoded { + token: Token::Tuple(tokens), + bytes_read, + }) + } + + fn decode_array( + &mut self, + param_type: &ParamType, + bytes: &[u8], + length: usize, + ) -> Result { + let (tokens, bytes_read) = self.decode_params(repeat(param_type).take(length), bytes)?; + + Ok(Decoded { + token: Token::Array(tokens), + bytes_read, + }) + } + + fn decode_vector(&mut self, param_type: &ParamType, bytes: &[u8]) -> Result { + let length = peek_length(bytes)?; + let bytes = skip(bytes, LENGTH_BYTES_SIZE)?; + let (tokens, bytes_read) = self.decode_params(repeat(param_type).take(length), bytes)?; + + Ok(Decoded { + token: Token::Vector(tokens), + bytes_read: LENGTH_BYTES_SIZE + bytes_read, + }) + } + + fn decode_struct(&mut self, param_types: &[ParamType], bytes: &[u8]) -> Result { + let (tokens, bytes_read) = self.decode_params(param_types, bytes)?; + + Ok(Decoded { + token: Token::Struct(tokens), + bytes_read, + }) + } + + fn decode_enum(&mut self, bytes: &[u8], variants: &EnumVariants) -> Result { + let discriminant = peek_discriminant(bytes)?; + let variant_bytes = skip(bytes, DISCRIMINANT_BYTES_SIZE)?; + let selected_variant = variants.param_type_of_variant(discriminant)?; + + let decoded = self.decode_param(selected_variant, variant_bytes)?; + + Ok(Decoded { + token: Token::Enum(Box::new((discriminant, decoded.token, variants.clone()))), + bytes_read: DISCRIMINANT_BYTES_SIZE + decoded.bytes_read, + }) + } + + fn decode_params<'a>( + &mut self, + param_types: impl IntoIterator, + bytes: &[u8], + ) -> Result<(Vec, usize)> { + let mut tokens = vec![]; + let mut bytes_read = 0; + + for param_type in param_types { + let decoded = self.decode_param(param_type, skip(bytes, bytes_read)?)?; + tokens.push(decoded.token); + bytes_read += decoded.bytes_read; + } + + Ok((tokens, bytes_read)) + } +} + +#[derive(Debug, Clone)] +struct Decoded { + token: Token, + bytes_read: usize, +} + +struct CounterWithLimit { + count: usize, + max: usize, + name: String, +} + +impl CounterWithLimit { + fn new(max: usize, name: impl Into) -> Self { + Self { + count: 0, + max, + name: name.into(), + } + } + + fn increase(&mut self) -> Result<()> { + self.count += 1; + if self.count > self.max { + return Err(error!( + InvalidType, + "{} limit ({}) reached while decoding. Try increasing it.", self.name, self.max + )); + } + + Ok(()) + } + + fn decrease(&mut self) { + if self.count > 0 { + self.count -= 1; + } + } +} + +fn peek_u8(bytes: &[u8]) -> Result { + let slice = peek_fixed::(bytes)?; + Ok(u8::from_be_bytes(*slice)) +} + +fn peek_u16(bytes: &[u8]) -> Result { + let slice = peek_fixed::(bytes)?; + Ok(u16::from_be_bytes(*slice)) +} + +fn peek_u32(bytes: &[u8]) -> Result { + let slice = peek_fixed::(bytes)?; + Ok(u32::from_be_bytes(*slice)) +} + +fn peek_u64(bytes: &[u8]) -> Result { + let slice = peek_fixed::(bytes)?; + Ok(u64::from_be_bytes(*slice)) +} + +fn peek_u128(bytes: &[u8]) -> Result { + let slice = peek_fixed::(bytes)?; + Ok(u128::from_be_bytes(*slice)) +} + +fn peek_u256(bytes: &[u8]) -> Result { + let slice = peek_fixed::(bytes)?; + Ok(U256::from(*slice)) +} + +fn peek_length(bytes: &[u8]) -> Result { + let slice = peek_fixed::(bytes)?; + + u64::from_be_bytes(*slice) + .try_into() + .map_err(|_| error!(InvalidData, "could not convert `u64` to `usize`")) +} + +fn peek_discriminant(bytes: &[u8]) -> Result { + let slice = peek_fixed::(bytes)?; + Ok(u64::from_be_bytes(*slice)) +} + +fn peek(data: &[u8], len: usize) -> Result<&[u8]> { + (len <= data.len()).then_some(&data[..len]).ok_or(error!( + InvalidData, + "tried to read `{len}` bytes but only had `{}` remaining!", + data.len() + )) +} + +fn peek_fixed(data: &[u8]) -> Result<&[u8; LEN]> { + let slice_w_correct_length = peek(data, LEN)?; + Ok(slice_w_correct_length + .try_into() + .expect("peek(data, len) must return a slice of length `len` or error out")) +} + +fn skip(slice: &[u8], num_bytes: usize) -> Result<&[u8]> { + (num_bytes <= slice.len()) + .then_some(&slice[num_bytes..]) + .ok_or(error!( + InvalidData, + "tried to consume `{num_bytes}` bytes but only had `{}` remaining!", + slice.len() + )) +} diff --git a/packages/fuels-core/src/codec/abi_encoder.rs b/packages/fuels-core/src/codec/abi_encoder.rs index 3585a52499..b20e2795cc 100644 --- a/packages/fuels-core/src/codec/abi_encoder.rs +++ b/packages/fuels-core/src/codec/abi_encoder.rs @@ -2,7 +2,6 @@ use fuel_types::bytes::padded_len_usize; use crate::{ checked_round_up_to_word_alignment, - constants::WORD_SIZE, types::{ errors::Result, pad_u16, pad_u32, @@ -83,7 +82,7 @@ impl ABIEncoder { Token::Enum(arg_enum) => Self::encode_enum(arg_enum)?, Token::Tuple(arg_tuple) => Self::encode_tuple(arg_tuple)?, Token::Unit => vec![Self::encode_unit()], - Token::RawSlice(data) => Self::encode_raw_slice(data)?, + Token::RawSlice(data) => Self::encode_raw_slice(data.to_vec())?, Token::Bytes(data) => Self::encode_bytes(data.to_vec())?, // `String` in Sway has the same memory layout as the bytes type Token::String(string) => Self::encode_bytes(string.clone().into_bytes())?, @@ -190,16 +189,17 @@ impl ABIEncoder { ]) } - fn encode_raw_slice(data: &[u64]) -> Result> { - let encoded_data = data - .iter() - .map(|&word| Self::encode_u64(word)) - .collect::>(); + fn encode_raw_slice(mut data: Vec) -> Result> { + let len = data.len(); - let num_bytes = data.len() * WORD_SIZE; - let len = Self::encode_u64(num_bytes as u64); + zeropad_to_word_alignment(&mut data); - Ok(vec![Data::Dynamic(encoded_data), len]) + let encoded_data = vec![Data::Inline(data)]; + + Ok(vec![ + Data::Dynamic(encoded_data), + Self::encode_u64(len as u64), + ]) } fn encode_string_slice(arg_string: &StaticStringToken) -> Result> { @@ -248,6 +248,7 @@ mod tests { use super::*; use crate::{ codec::first_four_bytes_of_sha256_hash, + constants::WORD_SIZE, types::{enum_variants::EnumVariants, param_types::ParamType}, }; @@ -1202,16 +1203,12 @@ mod tests { let encoded_bytes = ABIEncoder::encode(&[token])?.resolve(offset); // assert - let ptr = vec![0, 0, 0, 0, 0, 0, 0, 56]; - let len = vec![0, 0, 0, 0, 0, 0, 0, 24]; - let data = [ - [0, 0, 0, 0, 0, 0, 0, 1], - [0, 0, 0, 0, 0, 0, 0, 2], - [0, 0, 0, 0, 0, 0, 0, 3], - ] - .concat(); + let ptr = [0, 0, 0, 0, 0, 0, 0, 56].to_vec(); + let len = [0, 0, 0, 0, 0, 0, 0, 3].to_vec(); + let data = [1, 2, 3].to_vec(); + let padding = [0, 0, 0, 0, 0].to_vec(); - let expected_encoded_bytes = [ptr, len, data].concat(); + let expected_encoded_bytes = [ptr, len, data, padding].concat(); assert_eq!(expected_encoded_bytes, encoded_bytes); diff --git a/packages/fuels-core/src/codec/logs.rs b/packages/fuels-core/src/codec/logs.rs index 1daae05ed5..920c164c79 100644 --- a/packages/fuels-core/src/codec/logs.rs +++ b/packages/fuels-core/src/codec/logs.rs @@ -10,12 +10,12 @@ use fuel_tx::{ContractId, Receipt}; use crate::{ codec::{ABIDecoder, DecoderConfig}, traits::{Parameterize, Tokenizable}, - types::{ - errors::{error, Error, Result}, - param_types::ParamType, - }, + types::errors::{error, Error, Result}, }; +#[cfg(not(experimental))] +use crate::types::param_types::ParamType; + #[derive(Clone)] pub struct LogFormatter { formatter: fn(DecoderConfig, &[u8]) -> Result, @@ -34,11 +34,19 @@ impl LogFormatter { decoder_config: DecoderConfig, bytes: &[u8], ) -> Result { - Self::can_decode_log_with_type::()?; - let token = ABIDecoder::new(decoder_config).decode(&T::param_type(), bytes)?; + #[cfg(not(experimental))] + let token = { + Self::can_decode_log_with_type::()?; + ABIDecoder::new(decoder_config).decode(&T::param_type(), bytes)? + }; + + #[cfg(experimental)] + let token = ABIDecoder::new(decoder_config).experimental_decode(&T::param_type(), bytes)?; + Ok(format!("{:?}", T::from_token(token)?)) } + #[cfg(not(experimental))] fn can_decode_log_with_type() -> Result<()> { match T::param_type() { // String slices can not be decoded from logs as they are encoded as ptr, len @@ -188,8 +196,14 @@ impl LogDecoder { .extract_log_id_and_data() .filter_map(|(log_id, bytes)| { target_ids.contains(&log_id).then(|| { + #[cfg(experimental)] + let token = ABIDecoder::new(self.decoder_config) + .experimental_decode(&T::param_type(), &bytes)?; + + #[cfg(not(experimental))] let token = ABIDecoder::new(self.decoder_config).decode(&T::param_type(), &bytes)?; + T::from_token(token) }) }) diff --git a/packages/fuels-core/src/types.rs b/packages/fuels-core/src/types.rs index a20d7cdcc1..60e7f57a4e 100644 --- a/packages/fuels-core/src/types.rs +++ b/packages/fuels-core/src/types.rs @@ -91,7 +91,7 @@ pub enum Token { Struct(Vec), Enum(Box), Tuple(Vec), - RawSlice(Vec), + RawSlice(Vec), Bytes(Vec), String(String), } diff --git a/packages/fuels-core/src/types/core/raw_slice.rs b/packages/fuels-core/src/types/core/raw_slice.rs index d4e0b0cd62..0eb37a8fe7 100644 --- a/packages/fuels-core/src/types/core/raw_slice.rs +++ b/packages/fuels-core/src/types/core/raw_slice.rs @@ -1,21 +1,21 @@ #[derive(Debug, PartialEq, Clone, Eq)] // `RawSlice` is a mapping of the contract type "untyped raw slice" -- currently the only way of // returning dynamically sized data from a script. -pub struct RawSlice(pub Vec); +pub struct RawSlice(pub Vec); -impl From for Vec { - fn from(raw_slice: RawSlice) -> Vec { +impl From for Vec { + fn from(raw_slice: RawSlice) -> Vec { raw_slice.0 } } -impl PartialEq> for RawSlice { - fn eq(&self, other: &Vec) -> bool { +impl PartialEq> for RawSlice { + fn eq(&self, other: &Vec) -> bool { self.0 == *other } } -impl PartialEq for Vec { +impl PartialEq for Vec { fn eq(&self, other: &RawSlice) -> bool { *self == other.0 } diff --git a/packages/fuels-core/src/types/param_types.rs b/packages/fuels-core/src/types/param_types.rs index e23b13203e..c2bff0e2f8 100644 --- a/packages/fuels-core/src/types/param_types.rs +++ b/packages/fuels-core/src/types/param_types.rs @@ -16,19 +16,23 @@ use crate::{ #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub enum ParamType { + Unit, + Bool, U8, U16, U32, U64, U128, U256, - Bool, B256, - Unit, + Bytes, + String, + RawSlice, + StringArray(usize), + StringSlice, + Tuple(Vec), Array(Box, usize), Vector(Box), - StringSlice, - StringArray(usize), Struct { fields: Vec, generics: Vec, @@ -37,10 +41,6 @@ pub enum ParamType { variants: EnumVariants, generics: Vec, }, - Tuple(Vec), - RawSlice, - Bytes, - String, } pub enum ReturnLocation { diff --git a/packages/fuels/Forc.toml b/packages/fuels/Forc.toml index 10518eb26f..ff25dc0b76 100644 --- a/packages/fuels/Forc.toml +++ b/packages/fuels/Forc.toml @@ -30,14 +30,15 @@ members = [ 'tests/logs/contract_logs', 'tests/logs/contract_logs_abi', 'tests/logs/contract_with_contract_logs', + 'tests/logs/script_experimental_logs', 'tests/logs/script_logs', 'tests/logs/script_needs_custom_decoder_logging', 'tests/logs/script_with_contract_logs', 'tests/predicates/basic_predicate', - 'tests/predicates/swap', 'tests/predicates/predicate_configurables', 'tests/predicates/predicate_witnesses', 'tests/predicates/signatures', + 'tests/predicates/swap', 'tests/scripts/arguments', 'tests/scripts/basic_script', 'tests/scripts/require_from_contract', @@ -103,3 +104,6 @@ members = [ 'tests/types/scripts/script_u256', 'tests/types/scripts/script_vectors', ] + +[patch.'https://github.com/fuellabs/sway'] +std = { git = "https://github.com/fuellabs/sway", branch = "hal3e/abiencoder-impls" } diff --git a/packages/fuels/tests/contracts/needs_custom_decoder/src/main.sw b/packages/fuels/tests/contracts/needs_custom_decoder/src/main.sw index 7ec32330b2..fdec65b4f4 100644 --- a/packages/fuels/tests/contracts/needs_custom_decoder/src/main.sw +++ b/packages/fuels/tests/contracts/needs_custom_decoder/src/main.sw @@ -1,7 +1,6 @@ contract; impl AbiEncode for [u8; 1000] { - #[allow(dead_code)] fn abi_encode(self, ref mut buffer: Buffer) { let mut i = 0; while i < 1000 { diff --git a/packages/fuels/tests/logs.rs b/packages/fuels/tests/logs.rs index a2ea577cef..fe8fe74a9f 100644 --- a/packages/fuels/tests/logs.rs +++ b/packages/fuels/tests/logs.rs @@ -99,6 +99,7 @@ async fn test_parse_logs_custom_types() -> Result<()> { let log_test_struct = response.decode_logs_with_type::()?; let log_test_enum = response.decode_logs_with_type::()?; + let log_tuple = response.decode_logs_with_type::<(TestStruct, TestEnum)>()?; let expected_bits256 = Bits256([ 239, 134, 175, 169, 105, 108, 240, 220, 99, 133, 226, 196, 7, 166, 225, 89, 161, 16, 60, @@ -111,8 +112,9 @@ async fn test_parse_logs_custom_types() -> Result<()> { }; let expected_enum = TestEnum::VariantTwo; - assert_eq!(log_test_struct, vec![expected_struct]); - assert_eq!(log_test_enum, vec![expected_enum]); + assert_eq!(log_test_struct, vec![expected_struct.clone()]); + assert_eq!(log_test_enum, vec![expected_enum.clone()]); + assert_eq!(log_tuple, vec![(expected_struct, expected_enum)]); Ok(()) } @@ -218,7 +220,7 @@ async fn test_decode_logs() -> Result<()> { format!("{expected_generic_struct:?}"), ]; - assert_eq!(logs.filter_succeeded(), expected_logs); + assert_eq!(expected_logs, logs.filter_succeeded()); Ok(()) } @@ -638,6 +640,7 @@ async fn test_script_decode_logs() -> Result<()> { field_3: 64, }; let expected_enum = TestEnum::VariantTwo; + let expected_tuple = (expected_struct.clone(), expected_enum.clone()); let expected_generic_struct = StructWithGeneric { field_1: expected_struct.clone(), field_2: 64, @@ -663,6 +666,7 @@ async fn test_script_decode_logs() -> Result<()> { format!("{:?}", [1, 2, 3]), format!("{expected_struct:?}"), format!("{expected_enum:?}"), + format!("{expected_tuple:?}"), format!("{expected_generic_struct:?}"), format!("{expected_generic_enum:?}"), format!("{expected_nested_struct:?}"), @@ -1417,8 +1421,7 @@ async fn can_configure_decoder_for_script_log_decoding() -> Result<()> { ..Default::default() }) .call() - .await - .unwrap(); + .await?; response .decode_logs_with_type::<[u8; 1000]>() @@ -1436,10 +1439,9 @@ async fn can_configure_decoder_for_script_log_decoding() -> Result<()> { ..Default::default() }) .call() - .await - .unwrap(); + .await?; - let logs = response.decode_logs_with_type::<[u8; 1000]>().unwrap(); + let logs = response.decode_logs_with_type::<[u8; 1000]>()?; assert_eq!(logs, vec![[0u8; 1000]]); let logs = response.decode_logs(); @@ -1452,7 +1454,8 @@ async fn can_configure_decoder_for_script_log_decoding() -> Result<()> { // String slices can not be decoded from logs as they are encoded as ptr, len // TODO: Once https://github.com/FuelLabs/sway/issues/5110 is resolved we can remove this #[tokio::test] -async fn test_string_slice_log() -> Result<()> { +#[cfg(not(experimental))] +async fn string_slice_log() -> Result<()> { setup_program_test!( Wallets("wallet"), Abigen(Contract( @@ -1482,3 +1485,116 @@ async fn test_string_slice_log() -> Result<()> { Ok(()) } + +#[tokio::test] +#[cfg(experimental)] +async fn contract_experimental_log() -> Result<()> { + use fuels_core::types::AsciiString; + + setup_program_test!( + Wallets("wallet"), + Abigen(Contract( + name = "MyContract", + project = "packages/fuels/tests/logs/contract_logs" + ),), + Deploy( + contract = "MyContract", + name = "contract_instance", + wallet = "wallet" + ) + ); + let contract_methods = contract_instance.methods(); + + { + let response = contract_methods.produce_string_slice_log().call().await?; + let logs = response.decode_logs_with_type::()?; + + assert_eq!("fuel".to_string(), logs.first().unwrap().to_string()); + } + { + let response = contract_methods.produce_string_log().call().await?; + let logs = response.decode_logs_with_type::()?; + + assert_eq!(vec!["fuel".to_string()], logs); + } + { + let response = contract_methods.produce_bytes_log().call().await?; + let logs = response.decode_logs_with_type::()?; + + assert_eq!(vec![Bytes("fuel".as_bytes().to_vec())], logs); + } + { + let response = contract_methods.produce_raw_slice_log().call().await?; + let logs = response.decode_logs_with_type::()?; + + assert_eq!(vec![RawSlice("fuel".as_bytes().to_vec())], logs); + } + { + let v = [1u16, 2, 3].to_vec(); + let some_enum = EnumWithGeneric::VariantOne(v); + let other_enum = EnumWithGeneric::VariantTwo; + let v1 = vec![some_enum.clone(), other_enum, some_enum]; + let expected_vec = vec![vec![v1.clone(), v1]]; + + let response = contract_methods.produce_vec_log().call().await?; + let logs = response.decode_logs_with_type::>>>>>()?; + + assert_eq!(vec![expected_vec], logs); + } + + Ok(()) +} + +#[tokio::test] +#[cfg(experimental)] +async fn script_experimental_log() -> Result<()> { + use fuels_core::types::AsciiString; + + setup_program_test!( + Wallets("wallet"), + Abigen(Script( + name = "LogScript", + project = "packages/fuels/tests/logs/script_experimental_logs" + )), + LoadScript( + name = "script_instance", + script = "LogScript", + wallet = "wallet" + ) + ); + let response = script_instance.main().call().await?; + + { + let logs = response.decode_logs_with_type::()?; + + assert_eq!("fuel".to_string(), logs.first().unwrap().to_string()); + } + { + let logs = response.decode_logs_with_type::()?; + + assert_eq!(vec!["fuel".to_string()], logs); + } + { + let logs = response.decode_logs_with_type::()?; + + assert_eq!(vec![Bytes("fuel".as_bytes().to_vec())], logs); + } + { + let logs = response.decode_logs_with_type::()?; + + assert_eq!(vec![RawSlice("fuel".as_bytes().to_vec())], logs); + } + { + let v = [1u16, 2, 3].to_vec(); + let some_enum = EnumWithGeneric::VariantOne(v); + let other_enum = EnumWithGeneric::VariantTwo; + let v1 = vec![some_enum.clone(), other_enum, some_enum]; + let expected_vec = vec![vec![v1.clone(), v1]]; + + let logs = response.decode_logs_with_type::>>>>>()?; + + assert_eq!(vec![expected_vec], logs); + } + + Ok(()) +} diff --git a/packages/fuels/tests/logs/contract_logs/src/main.sw b/packages/fuels/tests/logs/contract_logs/src/main.sw index b78058ae4d..1ddb6c37b9 100644 --- a/packages/fuels/tests/logs/contract_logs/src/main.sw +++ b/packages/fuels/tests/logs/contract_logs/src/main.sw @@ -1,6 +1,6 @@ contract; -use std::logging::log; +use std::{logging::log, string::String}; use contract_logs::ContractLogs; #[allow(dead_code)] @@ -74,6 +74,7 @@ impl ContractLogs for Contract { log(test_struct); log(test_enum); + log((test_struct, test_enum)); } fn produce_logs_generic_types() { @@ -139,6 +140,42 @@ impl ContractLogs for Contract { } fn produce_string_slice_log() { - log("string_slice"); + log("fuel"); + } + + fn produce_string_log() { + log(String::from_ascii_str("fuel")); + } + + fn produce_bytes_log() { + log(String::from_ascii_str("fuel").as_bytes()); + } + + fn produce_raw_slice_log() { + log(String::from_ascii_str("fuel").as_raw_slice()); + } + + fn produce_vec_log() { + let mut v = Vec::new(); + v.push(1u16); + v.push(2u16); + v.push(3u16); + + let some_enum = EnumWithGeneric::VariantOne(v); + let other_enum = EnumWithGeneric::VariantTwo; + + let mut v1 = Vec::new(); + v1.push(some_enum); + v1.push(other_enum); + v1.push(some_enum); + + let mut v2 = Vec::new(); + v2.push(v1); + v2.push(v1); + + let mut v3 = Vec::new(); + v3.push(v2); + + log(v3); } } diff --git a/packages/fuels/tests/logs/contract_logs_abi/src/main.sw b/packages/fuels/tests/logs/contract_logs_abi/src/main.sw index 831826d0dc..074a16c5cb 100644 --- a/packages/fuels/tests/logs/contract_logs_abi/src/main.sw +++ b/packages/fuels/tests/logs/contract_logs_abi/src/main.sw @@ -10,4 +10,8 @@ abi ContractLogs { fn produce_multiple_logs(); fn produce_bad_logs(); fn produce_string_slice_log(); + fn produce_string_log(); + fn produce_bytes_log(); + fn produce_raw_slice_log(); + fn produce_vec_log(); } diff --git a/packages/fuels/tests/logs/script_experimental_logs/Forc.toml b/packages/fuels/tests/logs/script_experimental_logs/Forc.toml new file mode 100644 index 0000000000..3cffec9495 --- /dev/null +++ b/packages/fuels/tests/logs/script_experimental_logs/Forc.toml @@ -0,0 +1,5 @@ +[project] +authors = ["Fuel Labs "] +entry = "main.sw" +license = "Apache-2.0" +name = "script_experimental_logs" diff --git a/packages/fuels/tests/logs/script_experimental_logs/src/main.sw b/packages/fuels/tests/logs/script_experimental_logs/src/main.sw new file mode 100644 index 0000000000..27ac44fd3e --- /dev/null +++ b/packages/fuels/tests/logs/script_experimental_logs/src/main.sw @@ -0,0 +1,46 @@ +script; + +use std::{logging::log, string::String}; + +#[allow(dead_code)] +enum EnumWithGeneric { + VariantOne: D, + VariantTwo: (), +} + +fn main() { + // String slice + log("fuel"); + + // String + log(String::from_ascii_str("fuel")); + + // Bytes + log(String::from_ascii_str("fuel").as_bytes()); + + // RawSlice + log(String::from_ascii_str("fuel").as_raw_slice()); + + // Vector + let mut v = Vec::new(); + v.push(1u16); + v.push(2u16); + v.push(3u16); + + let some_enum = EnumWithGeneric::VariantOne(v); + let other_enum = EnumWithGeneric::VariantTwo; + + let mut v1 = Vec::new(); + v1.push(some_enum); + v1.push(other_enum); + v1.push(some_enum); + + let mut v2 = Vec::new(); + v2.push(v1); + v2.push(v1); + + let mut v3 = Vec::new(); + v3.push(v2); + + log(v3); +} diff --git a/packages/fuels/tests/logs/script_logs/src/main.sw b/packages/fuels/tests/logs/script_logs/src/main.sw index cd44620ec9..5f5cec3a62 100644 --- a/packages/fuels/tests/logs/script_logs/src/main.sw +++ b/packages/fuels/tests/logs/script_logs/src/main.sw @@ -44,6 +44,7 @@ fn main() { let u: b256 = 0xef86afa9696cf0dc6385e2c407a6e159a1103cefb7e2ae0636fb33d3cb2a9e4a; let e: str[4] = __to_str_array("Fuel"); let l: [u8; 3] = [1u8, 2u8, 3u8]; + let test_struct = TestStruct { field_1: true, field_2: u, @@ -75,6 +76,7 @@ fn main() { log(l); log(test_struct); log(test_enum); + log((test_struct, test_enum)); log(test_generic_struct); log(test_generic_enum); log(test_struct_nested); diff --git a/packages/fuels/tests/scripts/script_needs_custom_decoder/src/main.sw b/packages/fuels/tests/scripts/script_needs_custom_decoder/src/main.sw index a8ccaa9b17..665cb5726a 100644 --- a/packages/fuels/tests/scripts/script_needs_custom_decoder/src/main.sw +++ b/packages/fuels/tests/scripts/script_needs_custom_decoder/src/main.sw @@ -13,6 +13,5 @@ impl AbiEncode for [u8; 1000] { fn main() -> [u8; 1000] { let arr: [u8; 1000] = [0; 1000]; - log(arr); arr } diff --git a/packages/fuels/tests/types/contracts/raw_slice/src/main.sw b/packages/fuels/tests/types/contracts/raw_slice/src/main.sw index 04084fef65..2d2d87f1eb 100644 --- a/packages/fuels/tests/types/contracts/raw_slice/src/main.sw +++ b/packages/fuels/tests/types/contracts/raw_slice/src/main.sw @@ -12,13 +12,13 @@ struct Wrapper { } abi RawSliceContract { - fn return_raw_slice(length: u64) -> raw_slice; + fn return_raw_slice(length: u8) -> raw_slice; fn accept_raw_slice(slice: raw_slice); fn accept_nested_raw_slice(wrapper: Wrapper>); } fn validate_raw_slice(input: raw_slice) { - let vec: Vec = Vec::from(input); + let vec: Vec = Vec::from(input); require(vec.len() == 3, "raw slice len is not 3"); require( vec @@ -47,9 +47,9 @@ fn validate_vec(vec: Vec) { } impl RawSliceContract for Contract { - fn return_raw_slice(length: u64) -> raw_slice { + fn return_raw_slice(length: u8) -> raw_slice { let mut vec = Vec::new(); - let mut counter = 0; + let mut counter = 0u8; while counter < length { vec.push(counter); counter = counter + 1; diff --git a/packages/fuels/tests/types/predicates/predicate_raw_slice/src/main.sw b/packages/fuels/tests/types/predicates/predicate_raw_slice/src/main.sw index 19ba59de2c..a63409b911 100644 --- a/packages/fuels/tests/types/predicates/predicate_raw_slice/src/main.sw +++ b/packages/fuels/tests/types/predicates/predicate_raw_slice/src/main.sw @@ -12,7 +12,7 @@ struct Wrapper { } fn valid_raw_slice(slice: raw_slice) -> bool { - let vec: Vec = Vec::from(slice); + let vec: Vec = Vec::from(slice); vec.len() == 3 && vec.get(0).unwrap() == 40 && vec.get(1).unwrap() == 41 && vec.get(2).unwrap() == 42 } diff --git a/packages/fuels/tests/types_contracts.rs b/packages/fuels/tests/types_contracts.rs index 1043d9d595..ad3470d9a6 100644 --- a/packages/fuels/tests/types_contracts.rs +++ b/packages/fuels/tests/types_contracts.rs @@ -1973,9 +1973,9 @@ async fn test_contract_raw_slice() -> Result<()> { let contract_methods = contract_instance.methods(); { - for length in 0..=10 { + for length in 0u8..=10 { let response = contract_methods.return_raw_slice(length).call().await?; - assert_eq!(response.value, (0..length).collect::>()); + assert_eq!(response.value, (0u8..length).collect::>()); } } {