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: Solidity ABI encode functions and utils #279

Merged
merged 6 commits into from
Mar 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Scarb.lock
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ dependencies = [
name = "alexandria_encoding"
version = "0.1.0"
dependencies = [
"alexandria_bytes",
"alexandria_math",
"alexandria_numeric",
]
Expand Down
76 changes: 73 additions & 3 deletions src/bytes/src/bytes.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use alexandria_bytes::utils::{
u128_join, read_sub_u128, u128_split, u128_array_slice, keccak_u128s_be, u8_array_to_u256
};
use alexandria_math::sha256::sha256;
use alexandria_math::{U128BitShift, U256BitShift};
use starknet::ContractAddress;

/// Bytes is a dynamic array of u128, where each element contains 16 bytes.
Expand Down Expand Up @@ -42,6 +43,10 @@ trait BytesTrait {
fn new_empty() -> Bytes;
/// Create a Bytes with size bytes 0
fn zero(size: usize) -> Bytes;
/// Create a Bytes from ByteArray ( Array<bytes31> )
fn from_byte_array(bytes: ByteArray) -> Bytes;
/// Create a ByteArray from Bytes
fn to_byte_array(self: Bytes) -> ByteArray;
/// Locate offset in Bytes
fn locate(offset: usize) -> (usize, usize);
/// Get Bytes size
Expand Down Expand Up @@ -76,6 +81,8 @@ trait BytesTrait {
fn read_bytes(self: @Bytes, offset: usize, size: usize) -> (usize, Bytes);
/// Read felt252 from Bytes, which stored as u256
fn read_felt252(self: @Bytes, offset: usize) -> (usize, felt252);
/// Read bytes31 from Bytes
fn read_bytes31(self: @Bytes, offset: usize) -> (usize, bytes31);
/// Read a ContractAddress from Bytes
fn read_address(self: @Bytes, offset: usize) -> (usize, ContractAddress);
/// Write value with size bytes into Bytes, value is packed into u128
Expand All @@ -96,6 +103,8 @@ trait BytesTrait {
fn append_u256(ref self: Bytes, value: u256);
/// Write felt252 into Bytes, which stored as u256
fn append_felt252(ref self: Bytes, value: felt252);
/// Write bytes31 into Bytes
fn append_bytes31(ref self: Bytes, value: bytes31);
/// Write address into Bytes
fn append_address(ref self: Bytes, value: ContractAddress);
/// concat with other Bytes
Expand Down Expand Up @@ -135,6 +144,42 @@ impl BytesImpl of BytesTrait {
Bytes { size, data }
}

fn from_byte_array(mut bytes: ByteArray) -> Bytes {
let mut res = BytesTrait::new_empty();
loop {
match bytes.data.pop_front() {
Option::Some(val) => res.append_bytes31(val),
Option::None => { break; }
}
};
// Last elem
if bytes.pending_word_len != 0 {
let mut val: u256 = bytes.pending_word.into();
// Only append the right-aligned bytes of the last word ( using specified length )
val = U256BitShift::shl(val, 8 * (32 - bytes.pending_word_len.into()));
res.concat(@BytesTrait::new(bytes.pending_word_len, array![val.high, val.low]));
0xLucqs marked this conversation as resolved.
Show resolved Hide resolved
}
res
}

fn to_byte_array(self: Bytes) -> ByteArray {
let mut res: ByteArray = Default::default();
let mut offset = 0;
while offset < self
.size() {
if offset + 31 <= self.size() {
let (new_offset, value) = self.read_bytes31(offset);
res.append_word(value.into(), 31);
offset = new_offset;
} else {
let (new_offset, value) = self.read_u8(offset);
res.append_byte(value);
offset = new_offset;
}
};
res
}

/// Locate offset in Bytes
/// Arguments:
/// - offset: the offset in Bytes
Expand Down Expand Up @@ -358,6 +403,24 @@ impl BytesImpl of BytesTrait {
(new_offset, value.try_into().expect('Couldn\'t convert to felt252'))
}

/// read bytes31 from Bytes
#[inline(always)]
fn read_bytes31(self: @Bytes, offset: usize) -> (usize, bytes31) {
// Read 31 bytes of data ( 16 bytes high + 15 bytes low )
let (new_offset, high) = self.read_u128(0);
let (new_offset, low) = self.read_u128_packed(new_offset, 15);
// low bits shifting to remove the left padded 0 byte on u128 type
let low = U128BitShift::shl(low, 8);
let mut value: u256 = u256 { high, low };
// shift left aligned 31 bytes to the right
// thus the value is stored in the last 31 bytes of u256
value = U256BitShift::shr(value, 8);
// Unwrap is always safe here, because highest byte is always 0
let value: felt252 = value.try_into().expect('Couldn\'t convert to felt252');
0xLucqs marked this conversation as resolved.
Show resolved Hide resolved
let value: bytes31 = value.try_into().expect('Couldn\'t convert to bytes31');
(new_offset, value)
}

/// read Contract Address from Bytes
#[inline(always)]
fn read_address(self: @Bytes, offset: usize) -> (usize, ContractAddress) {
Expand Down Expand Up @@ -450,12 +513,19 @@ impl BytesImpl of BytesTrait {
self.append_u256(value)
}

/// Write bytes31 into Bytes
#[inline(always)]
fn append_bytes31(ref self: Bytes, value: bytes31) {
let mut value: u256 = value.into();
value = U256BitShift::shl(value, 8);
self.concat(@BytesTrait::new(31, array![value.high, value.low]));
}

/// Write address into Bytes
#[inline(always)]
fn append_address(ref self: Bytes, value: ContractAddress) {
let address_felt256: felt252 = value.into();
let address_u256: u256 = address_felt256.into();
self.append_u256(address_u256)
let address: felt252 = value.into();
self.append_felt252(address)
}

/// concat with other Bytes
Expand Down
31 changes: 31 additions & 0 deletions src/bytes/src/tests/test_bytes.cairo
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use alexandria_bytes::utils::{BytesDebug, BytesDisplay};
use alexandria_bytes::{Bytes, BytesTrait};
use starknet::ContractAddress;

Expand Down Expand Up @@ -351,6 +352,20 @@ fn test_bytes_read_u256() {
assert_eq!(value.low, 0x05060708091011121314151601020304, "read_u256_1_value_low");
}

#[test]
#[available_gas(20000000)]
fn test_bytes_read_bytes31() {
let bytes: Bytes = BytesTrait::new(
31, array![0x0102030405060708090a0b0c0d0e0f10, 0x1112131415161718191a1b1c1d1e1f00]
);
let (offset, val) = bytes.read_bytes31(0);
assert_eq!(offset, 31, "Offset after read_bytes31 failed");
assert!(
val == bytes31_const::<0x0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f>(),
"read_bytes31 test failed"
)
}

#[test]
#[available_gas(20000000)]
fn test_bytes_read_u256_array() {
Expand Down Expand Up @@ -682,3 +697,19 @@ fn test_bytes_sha256() {
let res = bytes.sha256();
assert_eq!(res, hash, "bytes_sha256_2");
}

#[test]
fn test_byte_array_conversions() {
let bytes = BytesTrait::new(
52,
array![
0x01020304050607080910111213141516,
0x16151413121110090807060504030201,
0x60196ff92381eb7910f5446c2e0e04e1,
0x3db2194a000000000000000000000000
]
);
let byte_array = bytes.clone().to_byte_array();
let new_bytes = BytesTrait::from_byte_array(byte_array);
assert_eq!(bytes, new_bytes, "byte <-> byte_array conversion failed");
}
51 changes: 51 additions & 0 deletions src/bytes/src/utils.cairo
Original file line number Diff line number Diff line change
@@ -1,7 +1,58 @@
use alexandria_bytes::{Bytes, BytesTrait};
use alexandria_data_structures::array_ext::ArrayTraitExt;
use core::fmt::{Debug, Display, Formatter, Error};
use core::to_byte_array::{AppendFormattedToByteArray, FormatAsByteArray};
use integer::u128_byte_reverse;
use keccak::{u128_to_u64, u128_split as u128_split_to_u64, cairo_keccak};

fn format_byte_hex(byte: u8, ref f: Formatter) -> Result<(), Error> {
let base: NonZero<u8> = 16_u8.try_into().unwrap();
if byte < 0x10 {
// Add leading zero for single digit numbers
let zero: ByteArray = "0";
Display::fmt(@zero, ref f)?;
}
Display::fmt(@byte.format_as_byte_array(base), ref f)
}

impl BytesDebug of Debug<Bytes> {
fn fmt(self: @Bytes, ref f: Formatter) -> Result<(), Error> {
let mut i: usize = 0;
let prefix: ByteArray = "0x";
Display::fmt(@prefix, ref f)?;
let mut res: Result<(), Error> = Result::Ok(());
while i < self
.size() {
let (new_i, value) = self.read_u8(i);
res = format_byte_hex(value, ref f);
if res.is_err() {
break;
}
i = new_i;
};
res
}
}

impl BytesDisplay of Display<Bytes> {
fn fmt(self: @Bytes, ref f: Formatter) -> Result<(), Error> {
let mut i: usize = 0;
let prefix: ByteArray = "0x";
Display::fmt(@prefix, ref f)?;
let mut res: Result<(), Error> = Result::Ok(());
while i < self
.size() {
let (new_i, value) = self.read_u8(i);
res = format_byte_hex(value, ref f);
if res.is_err() {
break;
}
i = new_i;
};
res
}
}

/// Computes the keccak256 of multiple uint128 values.
/// The values are interpreted as big-endian.
/// https://github.com/starkware-libs/cairo/blob/main/corelib/src/keccak.cairo
Expand Down
96 changes: 96 additions & 0 deletions src/encoding/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,99 @@
## [Base64](./src/base64.cairo)

Base64 is a binary-to-text encoding scheme used for transferring binary data safely over media designed for text. It divides input data into 3-byte blocks, each converted into 4 ASCII characters using a specific index table. If input bytes aren't divisible by three, it's padded with '=' characters. The process is reversed for decoding.

## [Solidity ABI](./src/sol_abi.cairo)

**sol_abi** is a wrapper around `alexandria_bytes::Bytes` providing easy to use interfaces to mimic Solidity's `abi` functions.

### Examples

1. **Encode** and **EncodePacked**

Solidity's `abi.encode` calls go from :

```solidity
uint8 v1 = 0x1a;
uint128 v2 = 0x101112131415161718191a1b1c1d1e1f;
bool v3 = true;
address v4 = 0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF;
bytes7 v5 = 0x000a0b0c0d0e0f;

abi.encode(v1, v2, v3, v4, v5);
```

to the Cairo equivalent :

```rust
use alexandria_bytes::{Bytes, BytesTrait};
use alexandria_encoding::sol_abi::{SolBytesTrait, SolAbiEncodeTrait};
use starknet::{ContractAddress, eth_address::U256IntoEthAddress, EthAddress};

fn main() {
let eth_address: EthAddress = 0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF_u256.into();
let mut encoded: Bytes = BytesTrait::new_empty();
encoded = encoded
.encode(0x1a_u8)
.encode(0x101112131415161718191a1b1c1d1e1f_u128)
.encode(true)
.encode(eth_address)
.encode(SolBytesTrait::bytes7(0xa0b0c0d0e0f));
}
```

Which will properly pad individual types according to Solidity's spec. A similar interface is also supported for `abi.encodePacked` with the Cairo `encode_packed`.

2. **Decode**

Solidity's `abi.decode` calls go from :

```solidity
bytes memory encoded = ...
(uint8 o1, uint128 o2, bool o3, address o4, bytes7 o5) = abi.decode(encoded, (uint8, uint128, bool, address, bytes7));
```

to the Cairo equivalent :

```rust
use alexandria_bytes::{Bytes, BytesTrait};
use alexandria_encoding::sol_abi::{SolBytesTrait, SolAbiDecodeTrait};
use starknet::{ContractAddress, eth_address::U256IntoEthAddress, EthAddress};

fn main() {
let encoded: Bytes = ...

let mut offset = 0;
let o1: u8 = encoded.decode(ref offset);
let o2: u128 = encoded.decode(ref offset);
let o3: bool = encoded.decode(ref offset);
let o4: EthAddress = encoded.decode(ref offset);
let o5: Bytes = SolBytesTrait::<Bytes>::bytes7(encoded.decode(ref offset));
}
```

Which decodes bytes formatted like from an `encode` call.

3. **BytesX**

Solidity supports types `bytes1`, `bytes2`, ..., and `bytes32`, which are left-aligned/right-padded byte arrays when encoded.

This module adds the `SolBytesTrait` wrapper for `alexandria_bytes::Bytes`, so declaring and using these is easier in Cairo.

Solidity bytes declarations :

```solidity
bytes3 v1 = 0xabcdef;
bytes32 v2 = 0x101112131415161718191a1b1c1d1e1f0102030405060708090a0b0c0d0e1011;
bytes7 v3 = 0x01020304050607;
```

can be done in Cairo like :

```rust
use alexandria_bytes::{Bytes, BytesTrait};
use alexandria_encoding::sol_abi::SolBytesTrait;

let v1: Bytes = SolBytesTrait::bytes3(0xabcdef_u128);
let v2: Bytes = SolBytesTrait::bytes32(0x101112131415161718191a1b1c1d1e1f0102030405060708090a0b0c0d0e1011_u256);
let v3: Bytes = SolBytesTrait::bytes7(BytesTrait::new(16, array![0x01020304050607000000000000000000]));
```
1 change: 1 addition & 0 deletions src/encoding/Scarb.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ fmt.workspace = true
[dependencies]
alexandria_math = { path = "../math" }
alexandria_numeric = { path = "../numeric" }
alexandria_bytes = { path = "../bytes" }
1 change: 1 addition & 0 deletions src/encoding/src/lib.cairo
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
mod base64;
mod reversible;
mod rlp;
mod sol_abi;

#[cfg(test)]
mod tests;
9 changes: 9 additions & 0 deletions src/encoding/src/sol_abi.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
pub mod decode;
pub mod encode;
pub mod encode_as;
pub mod sol_bytes;

use decode::{SolAbiDecodeTrait};
use encode::{SolAbiEncodeSelectorTrait, SolAbiEncodeTrait};
use encode_as::{SolAbiEncodeAsTrait};
use sol_bytes::{SolBytesTrait};
Loading
Loading