Skip to content

Commit

Permalink
feat: Solidity ABI encode functions and utils (#279)
Browse files Browse the repository at this point in the history
<!--- Please provide a general summary of your changes in the title
above -->
Added various traits/utils for supporting Solidity-like ABI encode
functions. This includes the traits :
- `SolAbiEncodeTrait` : providing equivalents for Solidity `abi.encode`
and `abi.encodePacked`
- `SolAbiEncodeSelectorTrait` : for Solidity `abi.encodeWithSelector`
- `SolAbiDecodeTrait` : for Solidity `abi.decode`
- `SolBytesTrait` : Providing utilities for easy conversion of types
into Solidity-like `bytes1`, `bytes2`, ..., `bytes32` types

Also added some helpers for `alexandria_bytes::Bytes`, such as : 
- `Bytes` <-> Cairo's `ByteArray` & `bytes31` function options
- Debug & Display traits for `Bytes`, to allow printing `Bytes` as a hex
string

## 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):

Issue Number: #274 

## 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. -->
  • Loading branch information
b-j-roberts authored Mar 12, 2024
1 parent 946e6e2 commit c1a604e
Show file tree
Hide file tree
Showing 14 changed files with 1,166 additions and 3 deletions.
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]));
}
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');
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

0 comments on commit c1a604e

Please sign in to comment.