From e2ce7c24e3435683d3fe729a80f93c14df6e2f01 Mon Sep 17 00:00:00 2001 From: Stoyan Kirov Date: Mon, 29 Apr 2024 19:20:26 +0300 Subject: [PATCH] feat: rust implementation for some of the Cairo types (#18) --- Cargo.lock | 117 ++++++ ampd/Cargo.toml | 2 + ampd/src/lib.rs | 9 +- ampd/src/starknet/mod.rs | 1 + ampd/src/starknet/types/array_span.rs | 272 +++++++++++++ ampd/src/starknet/types/byte_array.rs | 544 ++++++++++++++++++++++++++ ampd/src/starknet/types/mod.rs | 2 + 7 files changed, 945 insertions(+), 2 deletions(-) create mode 100644 ampd/src/starknet/mod.rs create mode 100644 ampd/src/starknet/types/array_span.rs create mode 100644 ampd/src/starknet/types/byte_array.rs create mode 100644 ampd/src/starknet/types/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 86f4f019f..89ead5cf1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -269,6 +269,8 @@ dependencies = [ "serde_with 3.3.0", "service-registry", "sha3 0.10.8", + "starknet-core", + "starknet-providers", "sui-json-rpc-types", "sui-types", "tendermint 0.33.0", @@ -983,6 +985,18 @@ dependencies = [ "thiserror", ] +[[package]] +name = "bigdecimal" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6773ddc0eafc0e509fb60e48dff7f450f8e674a0686ae8605e8d9901bd5eefa" +dependencies = [ + "num-bigint 0.4.4", + "num-integer", + "num-traits", + "serde", +] + [[package]] name = "bincode" version = "1.3.3" @@ -7469,6 +7483,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_json_pythonic" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62212da9872ca2a0cad0093191ee33753eddff9266cbbc1b4a602d13a3a768db" +dependencies = [ + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_path_to_error" version = "0.1.14" @@ -7852,6 +7877,98 @@ dependencies = [ "der 0.7.8", ] +[[package]] +name = "starknet-core" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ed286d637e34fb8ae1cd2f9615120ec8ff38d1cffd311ed7fdd497cdd2bd01f" +dependencies = [ + "base64 0.21.4", + "flate2", + "hex", + "serde", + "serde_json", + "serde_json_pythonic", + "serde_with 2.3.3", + "sha3 0.10.8", + "starknet-crypto", + "starknet-ff", +] + +[[package]] +name = "starknet-crypto" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e2c30c01e8eb0fc913c4ee3cf676389fffc1d1182bfe5bb9670e4e72e968064" +dependencies = [ + "crypto-bigint", + "hex", + "hmac", + "num-bigint 0.4.4", + "num-integer", + "num-traits", + "rfc6979", + "sha2 0.10.7", + "starknet-crypto-codegen", + "starknet-curve", + "starknet-ff", + "zeroize", +] + +[[package]] +name = "starknet-crypto-codegen" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbc159a1934c7be9761c237333a57febe060ace2bc9e3b337a59a37af206d19f" +dependencies = [ + "starknet-curve", + "starknet-ff", + "syn 2.0.37", +] + +[[package]] +name = "starknet-curve" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c383518bb312751e4be80f53e8644034aa99a0afb29d7ac41b89a997db875b" +dependencies = [ + "starknet-ff", +] + +[[package]] +name = "starknet-ff" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7abf1b44ec5b18d87c1ae5f54590ca9d0699ef4dd5b2ffa66fc97f24613ec585" +dependencies = [ + "ark-ff", + "bigdecimal", + "crypto-bigint", + "getrandom", + "hex", + "serde", +] + +[[package]] +name = "starknet-providers" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6abf40ffcbe3b887b4d5cfc8ab73170c816b4aa78d1d4ad59abd3fb3b0f53cd" +dependencies = [ + "async-trait", + "auto_impl", + "ethereum-types", + "flate2", + "log", + "reqwest", + "serde", + "serde_json", + "serde_with 2.3.3", + "starknet-core", + "thiserror", + "url", +] + [[package]] name = "static_assertions" version = "1.1.0" diff --git a/ampd/Cargo.toml b/ampd/Cargo.toml index a2dce5e2f..02e27a040 100644 --- a/ampd/Cargo.toml +++ b/ampd/Cargo.toml @@ -43,6 +43,8 @@ serde_json = { workspace = true } serde_with = "3.2.0" service-registry = { workspace = true } sha3 = { workspace = true } +starknet-core = "0.10.0" +starknet-providers = "0.10.0" sui-json-rpc-types = { git = "https://github.com/mystenlabs/sui", tag = "mainnet-v1.14.2" } sui-types = { git = "https://github.com/mystenlabs/sui", features = ["test-utils"], tag = "mainnet-v1.14.2" } # Need to switch to our own fork of tendermint and tendermint-rpc due to event attribute value being nullable. diff --git a/ampd/src/lib.rs b/ampd/src/lib.rs index 4f8ffc0c8..5ed308d16 100644 --- a/ampd/src/lib.rs +++ b/ampd/src/lib.rs @@ -6,16 +6,18 @@ use cosmrs::proto::cosmos::{ auth::v1beta1::query_client::QueryClient, tx::v1beta1::service_client::ServiceClient, }; use error_stack::{report, FutureExt, Result, ResultExt}; +use event_processor::EventHandler; +use events::Event; use evm::finalizer::{pick, Finalization}; use evm::json_rpc::EthereumClient; use router_api::ChainName; use thiserror::Error; +use tofnd::grpc::{MultisigClient, SharableEcdsaClient}; use tokio::signal::unix::{signal, SignalKind}; use tokio::sync::oneshot; use tokio_stream::Stream; use tokio_util::sync::CancellationToken; use tracing::info; - use asyncutil::task::{CancellableTask, TaskError, TaskGroup}; use broadcaster::Broadcaster; use event_processor::EventHandler; @@ -26,6 +28,7 @@ use state::StateUpdater; use tofnd::grpc::{Multisig, MultisigClient}; use types::TMAddress; +use crate::asyncutil::task::{CancellableTask, TaskError, TaskGroup}; use crate::config::Config; use crate::state::State; @@ -43,6 +46,7 @@ mod handlers; mod health_check; mod json_rpc; mod queue; +pub mod starknet; pub mod state; mod sui; mod tm_client; @@ -429,7 +433,8 @@ where broadcaster.run().change_context(Error::Broadcaster) })) .add_task(CancellableTask::create(|_| async move { - // assert: the state updater only stops when all handlers that are updating their states have stopped + // assert: the state updater only stops when all handlers that are updating + // their states have stopped state_tx .send(state_updater.run().await) .map_err(|_| report!(Error::ReturnState)) diff --git a/ampd/src/starknet/mod.rs b/ampd/src/starknet/mod.rs new file mode 100644 index 000000000..cd408564e --- /dev/null +++ b/ampd/src/starknet/mod.rs @@ -0,0 +1 @@ +pub mod types; diff --git a/ampd/src/starknet/types/array_span.rs b/ampd/src/starknet/types/array_span.rs new file mode 100644 index 000000000..2aaf27520 --- /dev/null +++ b/ampd/src/starknet/types/array_span.rs @@ -0,0 +1,272 @@ +use starknet_core::types::{FieldElement, ValueOutOfRangeError}; +use thiserror::Error; + +/// Represents Cairo's Array and Span types. +/// Implements `TryFrom>`, which is the way to create it. +/// +/// ## Example usage with the string "hello" +/// +/// ```rust +/// use ampd::starknet::types::array_span::ArraySpan; +/// use std::str::FromStr; +/// use starknet_core::types::FieldElement; +/// +/// let data = vec![ +/// FieldElement::from_str( +/// "0x0000000000000000000000000000000000000000000000000000000000000005", +/// ) +/// .unwrap(), +/// FieldElement::from_str( +/// "0x0000000000000000000000000000000000000000000000000000000000000068", +/// ) +/// .unwrap(), +/// FieldElement::from_str( +/// "0x0000000000000000000000000000000000000000000000000000000000000065", +/// ) +/// .unwrap(), +/// FieldElement::from_str( +/// "0x000000000000000000000000000000000000000000000000000000000000006c", +/// ) +/// .unwrap(), +/// FieldElement::from_str( +/// "0x000000000000000000000000000000000000000000000000000000000000006c", +/// ) +/// .unwrap(), +/// FieldElement::from_str( +/// "0x000000000000000000000000000000000000000000000000000000000000006f", +/// ) +/// .unwrap(), +/// ]; +/// +/// let array_span = ArraySpan::try_from(data).unwrap(); +/// assert_eq!(array_span.bytes, vec![104, 101, 108, 108, 111]); +/// assert_eq!(String::from_utf8(array_span.bytes).unwrap(), "hello"); +/// ``` +/// +/// For more info: +/// https://docs.starknet.io/documentation/architecture_and_concepts/Smart_Contracts/serialization_of_Cairo_types/#serialization_of_byte_arrays +#[derive(Debug)] +pub struct ArraySpan { + pub bytes: Vec, +} + +#[derive(Error, Debug)] +pub enum ArraySpanError { + #[error("Invalid array/span length")] + InvalidLength, + #[error("Failed to parse felt - {0}")] + ParsingFelt(#[from] ValueOutOfRangeError), +} + +impl TryFrom> for ArraySpan { + type Error = ArraySpanError; + + fn try_from(data: Vec) -> Result { + // First element is always the array length, which is a felt (so u8 is enough) + let arr_length = u8::try_from(data[0])?; + + // -1 because we have to offset the first element (the length itself) + let arr_length_usize = usize::from(arr_length); + if arr_length_usize != data.len().wrapping_sub(1) { + return Err(ArraySpanError::InvalidLength); + } + + let bytes: Result, ArraySpanError> = data + .get(1..) + .ok_or(ArraySpanError::InvalidLength)? + .iter() + .copied() + .map(|e| e.try_into().map_err(ArraySpanError::ParsingFelt)) + .collect(); + + Ok(ArraySpan { bytes: bytes? }) + } +} + +#[cfg(test)] +mod array_span_tests { + use std::str::FromStr; + + use starknet_core::types::FieldElement; + + use crate::starknet::types::array_span::ArraySpan; + + #[test] + fn try_from_valid_zeros() { + // the string "hello", but FieldElement is bigger than u8::max + let data = vec![FieldElement::from_str( + "0x0000000000000000000000000000000000000000000000000000000000000000", + ) + .unwrap()]; + + let array_span = ArraySpan::try_from(data).unwrap(); + assert_eq!(array_span.bytes, Vec::::new()); + } + + #[test] + fn try_from_failed_to_parse_element_to_u8() { + // the string "hello", but FieldElement is bigger than u8::max + let data = vec![ + FieldElement::from_str( + "0x0000000000000000000000000000000000000000000000000000000000000005", + ) + .unwrap(), + FieldElement::from_str( + "0x00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", + ) + .unwrap(), + FieldElement::from_str( + "0x0000000000000000000000000000000000000000000000000000000000000065", + ) + .unwrap(), + FieldElement::from_str( + "0x000000000000000000000000000000000000000000000000000000000000006c", + ) + .unwrap(), + FieldElement::from_str( + "0x000000000000000000000000000000000000000000000000000000000000006c", + ) + .unwrap(), + FieldElement::from_str( + "0x000000000000000000000000000000000000000000000000000000000000006f", + ) + .unwrap(), + ]; + + let array_span = ArraySpan::try_from(data); + assert!(array_span.is_err()); + } + + #[test] + fn try_from_failed_to_parse_elements_length_to_u32() { + // the string "hello", but element counte bigger than u32::max + let data = vec![ + FieldElement::from_str( + "0x00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", + ) + .unwrap(), + FieldElement::from_str( + "0x0000000000000000000000000000000000000000000000000000000000000068", + ) + .unwrap(), + FieldElement::from_str( + "0x0000000000000000000000000000000000000000000000000000000000000065", + ) + .unwrap(), + FieldElement::from_str( + "0x000000000000000000000000000000000000000000000000000000000000006c", + ) + .unwrap(), + FieldElement::from_str( + "0x000000000000000000000000000000000000000000000000000000000000006c", + ) + .unwrap(), + FieldElement::from_str( + "0x000000000000000000000000000000000000000000000000000000000000006f", + ) + .unwrap(), + ]; + + let array_span = ArraySpan::try_from(data); + assert!(array_span.is_err()); + } + + #[test] + fn try_from_invalid_number_of_elements() { + // the string "hello", but with only 4 bytes + let data = vec![ + FieldElement::from_str( + "0x0000000000000000000000000000000000000000000000000000000000000005", + ) + .unwrap(), + FieldElement::from_str( + "0x0000000000000000000000000000000000000000000000000000000000000068", + ) + .unwrap(), + FieldElement::from_str( + "0x0000000000000000000000000000000000000000000000000000000000000065", + ) + .unwrap(), + FieldElement::from_str( + "0x000000000000000000000000000000000000000000000000000000000000006c", + ) + .unwrap(), + FieldElement::from_str( + "0x000000000000000000000000000000000000000000000000000000000000006c", + ) + .unwrap(), + ]; + + let array_span = ArraySpan::try_from(data); + assert!(array_span.is_err()); + } + + #[test] + fn try_from_invalid_declared_length() { + // the string "hello", with correct number of bytes, but only 4 declared, + // instead of 5 + let data = vec![ + FieldElement::from_str( + "0x0000000000000000000000000000000000000000000000000000000000000004", + ) + .unwrap(), + FieldElement::from_str( + "0x0000000000000000000000000000000000000000000000000000000000000068", + ) + .unwrap(), + FieldElement::from_str( + "0x0000000000000000000000000000000000000000000000000000000000000065", + ) + .unwrap(), + FieldElement::from_str( + "0x000000000000000000000000000000000000000000000000000000000000006c", + ) + .unwrap(), + FieldElement::from_str( + "0x000000000000000000000000000000000000000000000000000000000000006c", + ) + .unwrap(), + FieldElement::from_str( + "0x000000000000000000000000000000000000000000000000000000000000006f", + ) + .unwrap(), + ]; + + let array_span = ArraySpan::try_from(data); + assert!(array_span.is_err()); + } + + #[test] + fn try_from_valid() { + // the string "hello" + let data = vec![ + FieldElement::from_str( + "0x0000000000000000000000000000000000000000000000000000000000000005", + ) + .unwrap(), + FieldElement::from_str( + "0x0000000000000000000000000000000000000000000000000000000000000068", + ) + .unwrap(), + FieldElement::from_str( + "0x0000000000000000000000000000000000000000000000000000000000000065", + ) + .unwrap(), + FieldElement::from_str( + "0x000000000000000000000000000000000000000000000000000000000000006c", + ) + .unwrap(), + FieldElement::from_str( + "0x000000000000000000000000000000000000000000000000000000000000006c", + ) + .unwrap(), + FieldElement::from_str( + "0x000000000000000000000000000000000000000000000000000000000000006f", + ) + .unwrap(), + ]; + + let array_span = ArraySpan::try_from(data).unwrap(); + assert_eq!(array_span.bytes, vec![104, 101, 108, 108, 111]); + } +} diff --git a/ampd/src/starknet/types/byte_array.rs b/ampd/src/starknet/types/byte_array.rs new file mode 100644 index 000000000..aa3467ad0 --- /dev/null +++ b/ampd/src/starknet/types/byte_array.rs @@ -0,0 +1,544 @@ +use itertools::FoldWhile::{Continue, Done}; +use itertools::Itertools; +use starknet_core::types::{FieldElement, ValueOutOfRangeError}; +use starknet_core::utils::parse_cairo_short_string; +use thiserror::Error; + +/// Represents Cairo's ByteArray type. +/// Implements `TryFrom>`, which is the way to create it. +/// +/// ## Example usage with the string "hello" +/// +/// ```rust +/// use ampd::starknet::types::byte_array::ByteArray; +/// use std::str::FromStr; +/// use starknet_core::types::FieldElement; +/// +/// let data = vec![ +/// FieldElement::from_str( +/// "0x0000000000000000000000000000000000000000000000000000000000000000", +/// ) +/// .unwrap(), +/// FieldElement::from_str( +/// "0x00000000000000000000000000000000000000000000000000000068656c6c6f", +/// ) +/// .unwrap(), +/// FieldElement::from_str( +/// "0x0000000000000000000000000000000000000000000000000000000000000005", +/// ) +/// .unwrap(), +/// ]; +/// +/// let byte_array = ByteArray::try_from(data); +/// assert!(byte_array.is_ok()); +/// ``` +/// +/// For more info: +/// https://docs.starknet.io/documentation/architecture_and_concepts/Smart_Contracts/serialization_of_Cairo_types/#serialization_of_byte_arrays +#[derive(Debug, Default)] +pub struct ByteArray { + /// The data byte array. Contains 31-byte chunks of the byte array. + data: Vec, + /// The bytes that remain after filling the data array with full 31-byte + /// chunks + pending_word: FieldElement, + /// The byte count of the pending_word + pending_word_length: u8, // can't be more than 30 bytes +} + +#[derive(Error, Debug)] +pub enum ByteArrayError { + #[error("Failed to fetch element from byte array at index")] + OutOfBound, + #[error("Invalid byte array - {0}")] + InvalidByteArray(String), + #[error("Failed to convert felt - {0}")] + ParsingFelt(#[from] ValueOutOfRangeError), + #[error("Failed to convert the byte array into a string")] + ToString, +} + +impl TryFrom> for ByteArray { + type Error = ByteArrayError; + + fn try_from(data: Vec) -> Result { + // pending word is always the next to last element + let pending_word_index = data.len().wrapping_sub(2); + let last_element_index = data.len().wrapping_sub(1); + + let mut byte_array = ByteArray { + ..Default::default() + }; + + if data.len() < 3 { + return Err(ByteArrayError::InvalidByteArray( + "vec should have minimum 3 elements".to_owned(), + )); + } + + // word count is always the first element, which is a felt (so u8 is enough) + let word_count = u8::try_from(data[0])?; + + // vec element count should be whatever the word count is + an offset of 3 + // the 3 stands for the minimum 3 elements: + // - word count + // - pending_word + // - pendint_word_length + let word_count_usize = usize::from(word_count.wrapping_add(3)); + if word_count_usize != data.len() { + return Err(ByteArrayError::InvalidByteArray( + "pre-defined count doesn't match actual 31byte element count".to_owned(), + )); + } + + // pending word byte count is always the last element + let pending_word_length_felt = data + .get(last_element_index) + .ok_or(ByteArrayError::OutOfBound)?; + let pending_word_length = u8::try_from(*pending_word_length_felt)?; + byte_array.pending_word_length = pending_word_length; + + let pending_word = data + .get(pending_word_index) + .ok_or(ByteArrayError::OutOfBound)?; + byte_array.pending_word = *pending_word; + + // count bytes, excluding leading zeros + let non_zero_pw_length = pending_word + .to_bytes_be() + .iter() + .fold_while(32, |acc: u8, n| { + if *n == 0 { + Continue(acc.saturating_sub(1)) + } else { + Done(acc) + } + }) + .into_inner(); + + if pending_word_length != non_zero_pw_length { + return Err(ByteArrayError::InvalidByteArray( + "pending_word length doesn't match it's defined length".to_owned(), + )); + } + + if word_count > 0 { + let byte_array_data = data + .get(1..pending_word_index) + .ok_or(ByteArrayError::OutOfBound)? + .to_vec(); + + byte_array.data = byte_array_data; + } + + Ok(byte_array) + } +} + +impl ByteArray { + /// Takes the ByteArray struct and tries to parse it as a single string + /// + /// ## Example usage with the string "hello" + /// + /// ```rust + /// use ampd::starknet::types::byte_array::ByteArray; + /// use std::str::FromStr; + /// use starknet_core::types::FieldElement; + /// + /// let data = vec![ + /// FieldElement::from_str( + /// "0x0000000000000000000000000000000000000000000000000000000000000000", + /// ) + /// .unwrap(), + /// FieldElement::from_str( + /// "0x00000000000000000000000000000000000000000000000000000068656c6c6f", + /// ) + /// .unwrap(), + /// FieldElement::from_str( + /// "0x0000000000000000000000000000000000000000000000000000000000000005", + /// ) + /// .unwrap(), + /// ]; + /// + /// let byte_array = ByteArray::try_from(data).unwrap(); + /// assert_eq!("hello", byte_array.try_to_string().unwrap()); + /// ``` + /// + /// Additional documentation you can find here: + /// https://docs.starknet.io/documentation/architecture_and_concepts/Smart_Contracts/serialization_of_Cairo_types/#serialization_of_byte_arrays + pub fn try_to_string(&self) -> Result { + match self + .data + .iter() + .chain(std::iter::once(&self.pending_word)) + .map(parse_cairo_short_string) + .collect::>() + { + Ok(s) => Ok(s), + Err(_) => Err(ByteArrayError::ToString), + } + } +} + +#[cfg(test)] +mod byte_array_tests { + use std::str::FromStr; + + use starknet_core::types::FieldElement; + + use crate::starknet::types::byte_array::ByteArray; + + #[test] + fn byte_array_parse_fail_wrong_pending_word_length() { + // Example for a small string (fits in a single felt) taken from here: + // https://docs.starknet.io/documentation/architecture_and_concepts/Smart_Contracts/serialization_of_Cairo_types/#serialization_of_byte_arrays + // + // So this is the string "hello" + let data = vec![ + FieldElement::from_str( + "0x0000000000000000000000000000000000000000000000000000000000000000", + ) + .unwrap(), + FieldElement::from_str( + "0x0000000000000000000000000000000000000000000000000000068656c6c6f", + ) + .unwrap(), + FieldElement::from_str( + // Should be of length 5 bytes, but we put 6 bytes, in order to fail + // the parsing + "0x0000000000000000000000000000000000000000000000000000000000000020", + ) + .unwrap(), + ]; + + let byte_array = ByteArray::try_from(data); + assert!(byte_array.is_err()); + } + + #[test] + fn byte_array_to_string_error() { + // Example for a small string (fits in a single felt) taken from here: + // https://docs.starknet.io/documentation/architecture_and_concepts/Smart_Contracts/serialization_of_Cairo_types/#serialization_of_byte_arrays + // + // So this is the string "hello" + let data = vec![ + FieldElement::from_str( + "0x0000000000000000000000000000000000000000000000000000000000000000", + ) + .unwrap(), + // Note the 01 in the beginning. This is what causes the parse + // function to error. + FieldElement::from_str( + "0x01000000000000000000000000000000000000000000000000000068656c6c6f", + ) + .unwrap(), + FieldElement::from_str( + // 32(0x20) bytes long pending_word + "0x0000000000000000000000000000000000000000000000000000000000000020", + ) + .unwrap(), + ]; + + let byte_array = ByteArray::try_from(data).unwrap(); + assert!(byte_array.try_to_string().is_err()); + } + + #[test] + fn byte_array_single_pending_word_only_to_string_valid() { + // Example for a small string (fits in a single felt) taken from here: + // https://docs.starknet.io/documentation/architecture_and_concepts/Smart_Contracts/serialization_of_Cairo_types/#serialization_of_byte_arrays + // + // So this is the string "hello" + let data = vec![ + FieldElement::from_str( + "0x0000000000000000000000000000000000000000000000000000000000000000", + ) + .unwrap(), + FieldElement::from_str( + "0x00000000000000000000000000000000000000000000000000000068656c6c6f", + ) + .unwrap(), + FieldElement::from_str( + "0x0000000000000000000000000000000000000000000000000000000000000005", + ) + .unwrap(), + ]; + + let byte_array = ByteArray::try_from(data).unwrap(); + assert_eq!("hello", byte_array.try_to_string().unwrap()); + } + + #[test] + fn byte_array_to_long_string_valid() { + // Example for a long string (doesn't fit in a single felt) taken from here: + // https://docs.starknet.io/documentation/architecture_and_concepts/Smart_Contracts/serialization_of_Cairo_types/#serialization_of_byte_arrays + // + // So this is the string "Long long string, a lot more than 31 characters that + // wouldn't even fit in two felts, so we'll have at least two felts and a + // pending word." + let data = vec![ + FieldElement::from_str( + "0x0000000000000000000000000000000000000000000000000000000000000004", + ) + .unwrap(), + FieldElement::from_str( + "0x00004c6f6e67206c6f6e6720737472696e672c2061206c6f74206d6f72652074", + ) + .unwrap(), + FieldElement::from_str( + "0x000068616e2033312063686172616374657273207468617420776f756c646e27", + ) + .unwrap(), + FieldElement::from_str( + "0x000074206576656e2066697420696e2074776f2066656c74732c20736f207765", + ) + .unwrap(), + FieldElement::from_str( + "0x0000276c6c2068617665206174206c656173742074776f2066656c747320616e", + ) + .unwrap(), + FieldElement::from_str( + "0x0000000000000000000000000000006420612070656e64696e6720776f72642e", + ) + .unwrap(), + FieldElement::from_str( + "0x0000000000000000000000000000000000000000000000000000000000000011", + ) + .unwrap(), + ]; + + let byte_array = ByteArray::try_from(data).unwrap(); + assert_eq!("Long long string, a lot more than 31 characters that wouldn't even fit in two felts, so we'll have at least two felts and a pending word.", byte_array.try_to_string().unwrap()); + } + + #[test] + fn try_from_vec_count_less_then_3() { + let data = vec![FieldElement::from_str( + "0x0000000000000000000000000000000000000000000000000000000000000005", + ) + .unwrap()]; + + let byte_array_err = ByteArray::try_from(data); + assert!(byte_array_err.is_err()); + } + + #[test] + fn try_from_non_u32_word_count() { + let data = vec![ + // should be 0, because the message is short + // enough to fit in a single FieldElement + FieldElement::from_str( + "0x00000000000000000000000000000000000000000000000000000068656c6c6f", + ) + .unwrap(), + FieldElement::from_str( + "0x00000000000000000000000000000000000000000000000000000068656c6c6f", + ) + .unwrap(), + FieldElement::from_str( + "0x0000000000000000000000000000000000000000000000000000000000000005", + ) + .unwrap(), + ]; + + let byte_array_err = ByteArray::try_from(data); + assert!(byte_array_err.is_err()); + } + #[test] + fn try_from_invalid_byte_array_element_count() { + let data = vec![ + // should be 0, because the message is short + // enough to fit in a single FieldElement + FieldElement::from_str( + "0x0000000000000000000000000000000000000000000000000000000000000005", + ) + .unwrap(), + FieldElement::from_str( + "0x00000000000000000000000000000000000000000000000000000068656c6c6f", + ) + .unwrap(), + FieldElement::from_str( + "0x0000000000000000000000000000000000000000000000000000000000000005", + ) + .unwrap(), + ]; + + let byte_array_err = ByteArray::try_from(data); + assert!(byte_array_err.is_err()); + } + + #[test] + fn try_from_non_u8_pending_word_length() { + // Example for a small string (fits in a single felt) taken from here: + // https://docs.starknet.io/documentation/architecture_and_concepts/Smart_Contracts/serialization_of_Cairo_types/#serialization_of_byte_arrays + // + // So this is the string "hello" + let data = vec![ + FieldElement::from_str( + "0x0000000000000000000000000000000000000000000000000000000000000000", + ) + .unwrap(), + FieldElement::from_str( + "0x00000000000000000000000000000000000000000000000000000068656c6c6f", + ) + .unwrap(), + FieldElement::from_str( + "0x00000000000000000000000000000000000000000000000000000068656c6c6f", + ) + .unwrap(), + ]; + + let byte_array = ByteArray::try_from(data); + assert!(byte_array.is_err()); + } + + #[test] + fn try_from_valid_only_pending_word() { + // Example for a small string (fits in a single felt) taken from here: + // https://docs.starknet.io/documentation/architecture_and_concepts/Smart_Contracts/serialization_of_Cairo_types/#serialization_of_byte_arrays + // + // So this is the string "hello" + let data = vec![ + FieldElement::from_str( + "0x0000000000000000000000000000000000000000000000000000000000000000", + ) + .unwrap(), + FieldElement::from_str( + "0x00000000000000000000000000000000000000000000000000000068656c6c6f", + ) + .unwrap(), + FieldElement::from_str( + "0x0000000000000000000000000000000000000000000000000000000000000005", + ) + .unwrap(), + ]; + + let byte_array = ByteArray::try_from(data).unwrap(); + + assert_eq!(byte_array.data, vec![]); + assert_eq!( + byte_array.pending_word, + FieldElement::from_str( + "0x00000000000000000000000000000000000000000000000000000068656c6c6f", + ) + .unwrap() + ); + assert_eq!(byte_array.pending_word_length, 5); + } + + #[test] + fn try_from_valid_one_big_string_split_in_multiple_data_elements() { + // Example for a long string (doesn't fit in a single felt) taken from here: + // https://docs.starknet.io/documentation/architecture_and_concepts/Smart_Contracts/serialization_of_Cairo_types/#serialization_of_byte_arrays + // + // So this is the string "Long long string, a lot more than 31 characters that + // wouldn't even fit in two felts, so we'll have at least two felts and a + // pending word." + let data = vec![ + FieldElement::from_str( + "0x0000000000000000000000000000000000000000000000000000000000000004", + ) + .unwrap(), + FieldElement::from_str( + "0x00004c6f6e67206c6f6e6720737472696e672c2061206c6f74206d6f72652074", + ) + .unwrap(), + FieldElement::from_str( + "0x000068616e2033312063686172616374657273207468617420776f756c646e27", + ) + .unwrap(), + FieldElement::from_str( + "0x000074206576656e2066697420696e2074776f2066656c74732c20736f207765", + ) + .unwrap(), + FieldElement::from_str( + "0x0000276c6c2068617665206174206c656173742074776f2066656c747320616e", + ) + .unwrap(), + FieldElement::from_str( + "0x0000000000000000000000000000006420612070656e64696e6720776f72642e", + ) + .unwrap(), + FieldElement::from_str( + "0x0000000000000000000000000000000000000000000000000000000000000011", + ) + .unwrap(), + ]; + + let byte_array = ByteArray::try_from(data).unwrap(); + + assert_eq!( + byte_array.data, + vec![ + FieldElement::from_str( + "0x00004c6f6e67206c6f6e6720737472696e672c2061206c6f74206d6f72652074", + ) + .unwrap(), + FieldElement::from_str( + "0x000068616e2033312063686172616374657273207468617420776f756c646e27", + ) + .unwrap(), + FieldElement::from_str( + "0x000074206576656e2066697420696e2074776f2066656c74732c20736f207765", + ) + .unwrap(), + FieldElement::from_str( + "0x0000276c6c2068617665206174206c656173742074776f2066656c747320616e", + ) + .unwrap() + ] + ); + assert_eq!( + byte_array.pending_word, + FieldElement::from_str( + "0x0000000000000000000000000000006420612070656e64696e6720776f72642e", + ) + .unwrap() + ); + assert_eq!(byte_array.pending_word_length, 17); + } + + #[test] + fn try_from_valid_one_very_big_string() { + // Example for a long string (doesn't fit in a single felt) taken from here: + // https://docs.starknet.io/documentation/architecture_and_concepts/Smart_Contracts/serialization_of_Cairo_types/#serialization_of_byte_arrays + // + // So this is the string "Long string, more than 31 characters." + let data = vec![ + FieldElement::from_str( + "0x0000000000000000000000000000000000000000000000000000000000000001", + ) + .unwrap(), + FieldElement::from_str( + "0x004c6f6e6720737472696e672c206d6f7265207468616e203331206368617261", + ) + .unwrap(), + FieldElement::from_str( + "0x000000000000000000000000000000000000000000000000000063746572732e", + ) + .unwrap(), + FieldElement::from_str( + "0x0000000000000000000000000000000000000000000000000000000000000006", + ) + .unwrap(), + ]; + + let byte_array = ByteArray::try_from(data).unwrap(); + + assert_eq!( + byte_array.data, + vec![FieldElement::from_str( + "0x004c6f6e6720737472696e672c206d6f7265207468616e203331206368617261", + ) + .unwrap()] + ); + assert_eq!( + byte_array.pending_word, + FieldElement::from_str( + "0x000000000000000000000000000000000000000000000000000063746572732e", + ) + .unwrap() + ); + assert_eq!(byte_array.pending_word_length, 6); + } +} diff --git a/ampd/src/starknet/types/mod.rs b/ampd/src/starknet/types/mod.rs new file mode 100644 index 000000000..c74f260f7 --- /dev/null +++ b/ampd/src/starknet/types/mod.rs @@ -0,0 +1,2 @@ +pub mod array_span; +pub mod byte_array;