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" }
18 changes: 18 additions & 0 deletions src/encoding/src/errors.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// LENGTH
const RLP_EMPTY_INPUT: felt252 = 'KKT: EmptyInput';
const RLP_INPUT_TOO_SHORT: felt252 = 'KKT: InputTooShort';
Quentash marked this conversation as resolved.
Show resolved Hide resolved

#[derive(Drop, Copy, PartialEq)]
enum RLPError {
EmptyInput: felt252,
InputTooShort: felt252,
Quentash marked this conversation as resolved.
Show resolved Hide resolved
}

impl RLPErrorIntoU256 of Into<RLPError, u256> {
fn into(self: RLPError) -> u256 {
match self {
RLPError::EmptyInput(error_message) => error_message.into(),
RLPError::InputTooShort(error_message) => error_message.into(),
}
}
}
Quentash marked this conversation as resolved.
Show resolved Hide resolved
2 changes: 2 additions & 0 deletions src/encoding/src/lib.cairo
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
mod base64;
mod errors;
mod reversible;
mod rlp;

#[cfg(test)]
mod tests;
184 changes: 184 additions & 0 deletions src/encoding/src/rlp.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
use alexandria_data_structures::array_ext::ArrayTraitExt;
use alexandria_encoding::errors::{RLPError, RLP_EMPTY_INPUT, RLP_INPUT_TOO_SHORT};
use alexandria_numeric::integers::U32Trait;

// 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(RLP_EMPTY_INPUT));
}

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(RLP_INPUT_TOO_SHORT));
}
let string_len_bytes = input.slice(1, len_bytes_count);
let string_len: u32 = U32Trait::from_bytes(string_len_bytes).unwrap();

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(RLP_INPUT_TOO_SHORT));
}

let list_len_bytes = input.slice(1, len_bytes_count);
let list_len: u32 = U32Trait::from_bytes(list_len_bytes).unwrap();
return Result::Ok((RLPType::List, 1 + len_bytes_count, list_len));
}
}

/// Recursively encodes multiple 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(RLP_EMPTY_INPUT));
}

let mut output: Array<u8> = Default::default();
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();
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 {
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();
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();
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(RLP_INPUT_TOO_SHORT));
}

match rlp_type {
RLPType::String => {
// checking for default value `0`
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