Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implement RLP Encoding and Decoding #213

Merged
merged 15 commits into from
Dec 1, 2023
4 changes: 4 additions & 0 deletions Scarb.lock
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ name = "alexandria_encoding"
version = "0.1.0"
dependencies = [
"alexandria_math",
"alexandria_numeric",
]

[[package]]
Expand All @@ -48,6 +49,9 @@ version = "0.1.0"
[[package]]
name = "alexandria_numeric"
version = "0.1.0"
dependencies = [
"alexandria_math",
]

[[package]]
name = "alexandria_searching"
Expand Down
10 changes: 10 additions & 0 deletions src/data_structures/src/array_ext.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ trait ArrayTraitExt<T> {
fn reverse(self: @Array<T>) -> Array<T>;
fn contains<+PartialEq<T>>(self: @Array<T>, item: T) -> bool;
fn concat(self: @Array<T>, a: @Array<T>) -> Array<T>;
fn concat_span<+Drop<T>>(ref self: Array<T>, arr2: Span<T>);
fn index_of<+PartialEq<T>>(self: @Array<T>, item: T) -> Option<usize>;
fn occurrences_of<+PartialEq<T>>(self: @Array<T>, item: T) -> usize;
fn min<+PartialEq<T>, +PartialOrd<T>>(self: @Array<T>) -> Option<T>;
Expand Down Expand Up @@ -85,6 +86,15 @@ impl ArrayImpl<T, +Copy<T>, +Drop<T>> of ArrayTraitExt<T> {
ret
}

fn concat_span<+Destruct<T>>(ref self: Array<T>, mut arr2: Span<T>) {
loop {
match arr2.pop_front() {
Option::Some(elem) => self.append(*elem),
Option::None => { break; }
};
}
}

fn index_of<+PartialEq<T>>(self: @Array<T>, item: T) -> Option<usize> {
self.span().index_of(item)
}
Expand Down
1 change: 1 addition & 0 deletions src/encoding/Scarb.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ fmt.workspace = true

[dependencies]
alexandria_math = { path = "../math" }
alexandria_numeric = { path = "../numeric" }
1 change: 1 addition & 0 deletions src/encoding/src/lib.cairo
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
mod base64;
mod reversible;
mod rlp;

#[cfg(test)]
mod tests;
198 changes: 198 additions & 0 deletions src/encoding/src/rlp.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
use alexandria_data_structures::array_ext::ArrayTraitExt;
use alexandria_numeric::integers::UIntBytes;

// Possible RLP errors
#[derive(Drop, Copy, PartialEq)]
enum RLPError {
EmptyInput,
InputTooShort,
PayloadTooLong
}

// Possible RLP types
#[derive(Drop, PartialEq)]
enum RLPType {
String,
List
}

#[derive(Drop, Copy, PartialEq)]
enum RLPItem {
String: Span<u8>,
List: Span<RLPItem>
}

#[generate_trait]
impl RLPImpl of RLPTrait {
/// Returns RLPType from the leading byte with
/// its offset in the array as well as its size.
///
/// # Arguments
/// * `input` - Array of byte to decode
/// # Returns
/// * `(RLPType, offset, size)` - A tuple containing the RLPType
/// the offset and the size of the RLPItem to decode
/// # Errors
/// * empty input - if the input is empty
/// * input too short - if the input is too short for a given
fn decode_type(input: Span<u8>) -> Result<(RLPType, u32, u32), RLPError> {
let input_len = input.len();
if input_len == 0 {
return Result::Err(RLPError::EmptyInput);
}

let prefix_byte = *input[0];

if prefix_byte < 0x80 { // Char
return Result::Ok((RLPType::String, 0, 1));
} else if prefix_byte < 0xb8 { // Short String
return Result::Ok((RLPType::String, 1, prefix_byte.into() - 0x80));
} else if prefix_byte < 0xc0 { // Long String
let len_bytes_count: u32 = (prefix_byte - 0xb7).into();
if input_len <= len_bytes_count {
return Result::Err(RLPError::InputTooShort);
}
let string_len_bytes = input.slice(1, len_bytes_count);
let string_len: u32 = UIntBytes::from_bytes(string_len_bytes)
.ok_or(RLPError::PayloadTooLong)?;

return Result::Ok((RLPType::String, 1 + len_bytes_count, string_len));
} else if prefix_byte < 0xf8 { // Short List
return Result::Ok((RLPType::List, 1, prefix_byte.into() - 0xc0));
} else { // Long List
let len_bytes_count = prefix_byte.into() - 0xf7;
if input.len() <= len_bytes_count {
return Result::Err(RLPError::InputTooShort);
}
let list_len_bytes = input.slice(1, len_bytes_count);
let list_len: u32 = UIntBytes::from_bytes(list_len_bytes)
.ok_or(RLPError::PayloadTooLong)?;
return Result::Ok((RLPType::List, 1 + len_bytes_count, list_len));
}
}

/// Recursively encodes multiple a list of RLPItems
/// # Arguments
/// * `input` - Span of RLPItem to encode
/// # Returns
/// * `Span<u8> - RLP encoded items as a span of bytes
/// # Errors
/// * empty input - if the input is empty
fn encode(mut input: Span<RLPItem>) -> Result<Span<u8>, RLPError> {
if input.len() == 0 {
return Result::Err(RLPError::EmptyInput);
}

let mut output: Array<u8> = Default::default();
// Safe to unwrap because input length is not 0
let item = input.pop_front().unwrap();
Quentash marked this conversation as resolved.
Show resolved Hide resolved

match item {
RLPItem::String(string) => { output.concat_span(RLPTrait::encode_string(*string)?); },
RLPItem::List(list) => {
if (*list).len() == 0 {
output.append(0xc0);
} else {
let payload = RLPTrait::encode(*list)?;
let payload_len = payload.len();
if payload_len > 55 {
let len_in_bytes = payload_len.to_bytes();
// The payload length being a u32, the length in bytes
// will maximum be equal to 4, making the unwrap safe
output.append(0xf7 + len_in_bytes.len().try_into().unwrap());
Quentash marked this conversation as resolved.
Show resolved Hide resolved
output.concat_span(len_in_bytes);
} else {
// Safe to unwrap because payload_len<55
output.append(0xc0 + payload_len.try_into().unwrap());
Quentash marked this conversation as resolved.
Show resolved Hide resolved
}
output.concat_span(payload);
}
}
}

if input.len() > 0 {
output.concat_span(RLPTrait::encode(input)?);
}

Result::Ok(output.span())
}

/// RLP encodes a Array of bytes representing a RLP String.
/// # Arguments
/// * `input` - Array of bytes representing a RLP String to encode
/// # Returns
/// * `Span<u8> - RLP encoded items as a span of bytes
fn encode_string(input: Span<u8>) -> Result<Span<u8>, RLPError> {
let len = input.len();
if len == 0 {
return Result::Ok(array![0x80].span());
} else if len == 1 && *input[0] < 0x80 {
return Result::Ok(input);
} else if len < 56 {
let mut encoding: Array<u8> = Default::default();
// Safe to unwrap because len<56
encoding.append(0x80 + len.try_into().unwrap());
Quentash marked this conversation as resolved.
Show resolved Hide resolved
encoding.concat_span(input);
return Result::Ok(encoding.span());
} else {
let mut encoding: Array<u8> = Default::default();
let len_as_bytes = len.to_bytes();
let len_bytes_count = len_as_bytes.len();
// The payload length being a u32, the length in bytes
// will maximum be equal to 4, making the unwrap safe
let prefix = 0xb7 + len_bytes_count.try_into().unwrap();
Quentash marked this conversation as resolved.
Show resolved Hide resolved
encoding.append(prefix);
encoding.concat_span(len_as_bytes);
encoding.concat_span(input);
return Result::Ok(encoding.span());
}
}

/// Recursively decodes a rlp encoded byte array
/// as described in https://ethereum.org/en/developers/docs/data-structures-and-encoding/rlp/
///
/// # Arguments
/// * `input` - Array of bytes to decode
/// # Returns
/// * `Span<RLPItem>` - Span of RLPItem
/// # Errors
/// * input too short - if the input is too short for a given
fn decode(input: Span<u8>) -> Result<Span<RLPItem>, RLPError> {
let mut output: Array<RLPItem> = Default::default();
let input_len = input.len();

let (rlp_type, offset, len) = RLPTrait::decode_type(input)?;

if input_len < offset + len {
return Result::Err(RLPError::InputTooShort);
}

match rlp_type {
RLPType::String => {
if (len == 0) {
output.append(RLPItem::String(array![].span()));
} else {
output.append(RLPItem::String(input.slice(offset, len)));
}
},
RLPType::List => {
if len > 0 {
let res = RLPTrait::decode(input.slice(offset, len))?;
output.append(RLPItem::List(res));
} else {
output.append(RLPItem::List(array![].span()));
}
}
};

let total_item_len = len + offset;
if total_item_len < input_len {
output
.concat_span(
RLPTrait::decode(input.slice(total_item_len, input_len - total_item_len))?
);
}

Result::Ok(output.span())
}
}
1 change: 1 addition & 0 deletions src/encoding/src/tests.cairo
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
mod base64_test;
mod reversible_test;
mod rlp_test;
Loading