Skip to content

Commit

Permalink
feat: Implement RLP Encoding and Decoding (#213)
Browse files Browse the repository at this point in the history
Implementation of RLP encoding and decoding based on
https://ethereum.org/en/developers/docs/data-structures-and-encoding/rlp/
doc.

<!--- Please provide a general summary of your changes in the title
above -->

## Pull Request type

<!-- Please try to limit your pull request to one type; submit multiple
pull requests if needed. -->

Please check the type of change your PR introduces:

- [ ] Bugfix
- [X] Feature
- [ ] Code style update (formatting, renaming)
- [ ] Refactoring (no functional changes, no API changes)
- [ ] Build-related changes
- [ ] Documentation content changes
- [ ] Other (please describe):

## What is the current behavior?

<!-- Please describe the current behavior that you are modifying, or
link to a relevant issue. -->

Issue Number: #212 

## What is the new behavior?

<!-- Please describe the behavior or changes that are being added by
this PR. -->

-
-
-

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!-- If this does introduce a breaking change, please describe the
impact and migration path for existing applications below. -->

## Other information

<!-- Any other information that is important to this PR, such as
screenshots of how the component looks before and after the change. -->
  • Loading branch information
Quentash authored Dec 1, 2023
1 parent 8426dbb commit 1de9e5f
Show file tree
Hide file tree
Showing 12 changed files with 3,663 additions and 0 deletions.
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();

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());
output.concat_span(len_in_bytes);
} else {
// Safe to unwrap because payload_len<55
output.append(0xc0 + payload_len.try_into().unwrap());
}
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());
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();
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

0 comments on commit 1de9e5f

Please sign in to comment.