Skip to content

Commit 1de9e5f

Browse files
authored
feat: Implement RLP Encoding and Decoding (#213)
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. -->
1 parent 8426dbb commit 1de9e5f

File tree

12 files changed

+3663
-0
lines changed

12 files changed

+3663
-0
lines changed

Scarb.lock

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ name = "alexandria_encoding"
2828
version = "0.1.0"
2929
dependencies = [
3030
"alexandria_math",
31+
"alexandria_numeric",
3132
]
3233

3334
[[package]]
@@ -48,6 +49,9 @@ version = "0.1.0"
4849
[[package]]
4950
name = "alexandria_numeric"
5051
version = "0.1.0"
52+
dependencies = [
53+
"alexandria_math",
54+
]
5155

5256
[[package]]
5357
name = "alexandria_searching"

src/data_structures/src/array_ext.cairo

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ trait ArrayTraitExt<T> {
44
fn reverse(self: @Array<T>) -> Array<T>;
55
fn contains<+PartialEq<T>>(self: @Array<T>, item: T) -> bool;
66
fn concat(self: @Array<T>, a: @Array<T>) -> Array<T>;
7+
fn concat_span<+Drop<T>>(ref self: Array<T>, arr2: Span<T>);
78
fn index_of<+PartialEq<T>>(self: @Array<T>, item: T) -> Option<usize>;
89
fn occurrences_of<+PartialEq<T>>(self: @Array<T>, item: T) -> usize;
910
fn min<+PartialEq<T>, +PartialOrd<T>>(self: @Array<T>) -> Option<T>;
@@ -85,6 +86,15 @@ impl ArrayImpl<T, +Copy<T>, +Drop<T>> of ArrayTraitExt<T> {
8586
ret
8687
}
8788

89+
fn concat_span<+Destruct<T>>(ref self: Array<T>, mut arr2: Span<T>) {
90+
loop {
91+
match arr2.pop_front() {
92+
Option::Some(elem) => self.append(*elem),
93+
Option::None => { break; }
94+
};
95+
}
96+
}
97+
8898
fn index_of<+PartialEq<T>>(self: @Array<T>, item: T) -> Option<usize> {
8999
self.span().index_of(item)
90100
}

src/encoding/Scarb.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ fmt.workspace = true
99

1010
[dependencies]
1111
alexandria_math = { path = "../math" }
12+
alexandria_numeric = { path = "../numeric" }

src/encoding/src/lib.cairo

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
mod base64;
22
mod reversible;
3+
mod rlp;
34

45
#[cfg(test)]
56
mod tests;

src/encoding/src/rlp.cairo

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
use alexandria_data_structures::array_ext::ArrayTraitExt;
2+
use alexandria_numeric::integers::UIntBytes;
3+
4+
// Possible RLP errors
5+
#[derive(Drop, Copy, PartialEq)]
6+
enum RLPError {
7+
EmptyInput,
8+
InputTooShort,
9+
PayloadTooLong
10+
}
11+
12+
// Possible RLP types
13+
#[derive(Drop, PartialEq)]
14+
enum RLPType {
15+
String,
16+
List
17+
}
18+
19+
#[derive(Drop, Copy, PartialEq)]
20+
enum RLPItem {
21+
String: Span<u8>,
22+
List: Span<RLPItem>
23+
}
24+
25+
#[generate_trait]
26+
impl RLPImpl of RLPTrait {
27+
/// Returns RLPType from the leading byte with
28+
/// its offset in the array as well as its size.
29+
///
30+
/// # Arguments
31+
/// * `input` - Array of byte to decode
32+
/// # Returns
33+
/// * `(RLPType, offset, size)` - A tuple containing the RLPType
34+
/// the offset and the size of the RLPItem to decode
35+
/// # Errors
36+
/// * empty input - if the input is empty
37+
/// * input too short - if the input is too short for a given
38+
fn decode_type(input: Span<u8>) -> Result<(RLPType, u32, u32), RLPError> {
39+
let input_len = input.len();
40+
if input_len == 0 {
41+
return Result::Err(RLPError::EmptyInput);
42+
}
43+
44+
let prefix_byte = *input[0];
45+
46+
if prefix_byte < 0x80 { // Char
47+
return Result::Ok((RLPType::String, 0, 1));
48+
} else if prefix_byte < 0xb8 { // Short String
49+
return Result::Ok((RLPType::String, 1, prefix_byte.into() - 0x80));
50+
} else if prefix_byte < 0xc0 { // Long String
51+
let len_bytes_count: u32 = (prefix_byte - 0xb7).into();
52+
if input_len <= len_bytes_count {
53+
return Result::Err(RLPError::InputTooShort);
54+
}
55+
let string_len_bytes = input.slice(1, len_bytes_count);
56+
let string_len: u32 = UIntBytes::from_bytes(string_len_bytes)
57+
.ok_or(RLPError::PayloadTooLong)?;
58+
59+
return Result::Ok((RLPType::String, 1 + len_bytes_count, string_len));
60+
} else if prefix_byte < 0xf8 { // Short List
61+
return Result::Ok((RLPType::List, 1, prefix_byte.into() - 0xc0));
62+
} else { // Long List
63+
let len_bytes_count = prefix_byte.into() - 0xf7;
64+
if input.len() <= len_bytes_count {
65+
return Result::Err(RLPError::InputTooShort);
66+
}
67+
let list_len_bytes = input.slice(1, len_bytes_count);
68+
let list_len: u32 = UIntBytes::from_bytes(list_len_bytes)
69+
.ok_or(RLPError::PayloadTooLong)?;
70+
return Result::Ok((RLPType::List, 1 + len_bytes_count, list_len));
71+
}
72+
}
73+
74+
/// Recursively encodes multiple a list of RLPItems
75+
/// # Arguments
76+
/// * `input` - Span of RLPItem to encode
77+
/// # Returns
78+
/// * `Span<u8> - RLP encoded items as a span of bytes
79+
/// # Errors
80+
/// * empty input - if the input is empty
81+
fn encode(mut input: Span<RLPItem>) -> Result<Span<u8>, RLPError> {
82+
if input.len() == 0 {
83+
return Result::Err(RLPError::EmptyInput);
84+
}
85+
86+
let mut output: Array<u8> = Default::default();
87+
// Safe to unwrap because input length is not 0
88+
let item = input.pop_front().unwrap();
89+
90+
match item {
91+
RLPItem::String(string) => { output.concat_span(RLPTrait::encode_string(*string)?); },
92+
RLPItem::List(list) => {
93+
if (*list).len() == 0 {
94+
output.append(0xc0);
95+
} else {
96+
let payload = RLPTrait::encode(*list)?;
97+
let payload_len = payload.len();
98+
if payload_len > 55 {
99+
let len_in_bytes = payload_len.to_bytes();
100+
// The payload length being a u32, the length in bytes
101+
// will maximum be equal to 4, making the unwrap safe
102+
output.append(0xf7 + len_in_bytes.len().try_into().unwrap());
103+
output.concat_span(len_in_bytes);
104+
} else {
105+
// Safe to unwrap because payload_len<55
106+
output.append(0xc0 + payload_len.try_into().unwrap());
107+
}
108+
output.concat_span(payload);
109+
}
110+
}
111+
}
112+
113+
if input.len() > 0 {
114+
output.concat_span(RLPTrait::encode(input)?);
115+
}
116+
117+
Result::Ok(output.span())
118+
}
119+
120+
/// RLP encodes a Array of bytes representing a RLP String.
121+
/// # Arguments
122+
/// * `input` - Array of bytes representing a RLP String to encode
123+
/// # Returns
124+
/// * `Span<u8> - RLP encoded items as a span of bytes
125+
fn encode_string(input: Span<u8>) -> Result<Span<u8>, RLPError> {
126+
let len = input.len();
127+
if len == 0 {
128+
return Result::Ok(array![0x80].span());
129+
} else if len == 1 && *input[0] < 0x80 {
130+
return Result::Ok(input);
131+
} else if len < 56 {
132+
let mut encoding: Array<u8> = Default::default();
133+
// Safe to unwrap because len<56
134+
encoding.append(0x80 + len.try_into().unwrap());
135+
encoding.concat_span(input);
136+
return Result::Ok(encoding.span());
137+
} else {
138+
let mut encoding: Array<u8> = Default::default();
139+
let len_as_bytes = len.to_bytes();
140+
let len_bytes_count = len_as_bytes.len();
141+
// The payload length being a u32, the length in bytes
142+
// will maximum be equal to 4, making the unwrap safe
143+
let prefix = 0xb7 + len_bytes_count.try_into().unwrap();
144+
encoding.append(prefix);
145+
encoding.concat_span(len_as_bytes);
146+
encoding.concat_span(input);
147+
return Result::Ok(encoding.span());
148+
}
149+
}
150+
151+
/// Recursively decodes a rlp encoded byte array
152+
/// as described in https://ethereum.org/en/developers/docs/data-structures-and-encoding/rlp/
153+
///
154+
/// # Arguments
155+
/// * `input` - Array of bytes to decode
156+
/// # Returns
157+
/// * `Span<RLPItem>` - Span of RLPItem
158+
/// # Errors
159+
/// * input too short - if the input is too short for a given
160+
fn decode(input: Span<u8>) -> Result<Span<RLPItem>, RLPError> {
161+
let mut output: Array<RLPItem> = Default::default();
162+
let input_len = input.len();
163+
164+
let (rlp_type, offset, len) = RLPTrait::decode_type(input)?;
165+
166+
if input_len < offset + len {
167+
return Result::Err(RLPError::InputTooShort);
168+
}
169+
170+
match rlp_type {
171+
RLPType::String => {
172+
if (len == 0) {
173+
output.append(RLPItem::String(array![].span()));
174+
} else {
175+
output.append(RLPItem::String(input.slice(offset, len)));
176+
}
177+
},
178+
RLPType::List => {
179+
if len > 0 {
180+
let res = RLPTrait::decode(input.slice(offset, len))?;
181+
output.append(RLPItem::List(res));
182+
} else {
183+
output.append(RLPItem::List(array![].span()));
184+
}
185+
}
186+
};
187+
188+
let total_item_len = len + offset;
189+
if total_item_len < input_len {
190+
output
191+
.concat_span(
192+
RLPTrait::decode(input.slice(total_item_len, input_len - total_item_len))?
193+
);
194+
}
195+
196+
Result::Ok(output.span())
197+
}
198+
}

src/encoding/src/tests.cairo

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
mod base64_test;
22
mod reversible_test;
3+
mod rlp_test;

0 commit comments

Comments
 (0)