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

[Ethereum]: Add support for abi.encodePacked() | Resolves #3679 #3862

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions rust/tw_evm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ tw_memory = { path = "../tw_memory" }
tw_misc = { path = "../tw_misc" }
tw_number = { path = "../tw_number" }
tw_proto = { path = "../tw_proto" }
hex = "0.4.3"
theghostmac marked this conversation as resolved.
Show resolved Hide resolved

[dev-dependencies]
tw_coin_entry = { path = "../tw_coin_entry", features = ["test-utils"] }
182 changes: 181 additions & 1 deletion rust/tw_evm/src/abi/encode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use crate::abi::token::Token;
use tw_hash::H256;
use tw_memory::Data;
use crate::abi::uint::UintBits;

pub fn encode_tokens(tokens: &[Token]) -> Data {
let mediates = tokens.iter().map(mediate_token).collect::<Vec<_>>();
Expand Down Expand Up @@ -202,8 +203,67 @@ fn fixed_bytes_append(result: &mut Vec<H256>, bytes: &[u8]) {
}
}

pub fn encode_packed_tokens(tokens: &[Token]) -> Data {
let mut encoded = Vec::new();

for token in tokens {
match token {
Token::Address(address) => {
encoded.extend_from_slice(address.as_slice());
},
Token::Bytes(bytes) => {
encoded.extend_from_slice(bytes);
},
Token::String(s) => {
encoded.extend_from_slice(s.as_bytes());
},
Token::FixedBytes(bytes) => {
encoded.extend_from_slice(bytes);
},
Token::Int { int, .. } => {
let buf = int.to_big_endian();
let trimmed_buf = trim_leading_zeros(buf.as_slice());
encoded.extend_from_slice(trimmed_buf);
},
Token::Uint { uint, .. } => {
let buf = uint.to_big_endian();
let trimmed_buf = trim_leading_zeros(buf.as_slice());
encoded.extend_from_slice(trimmed_buf);
},
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understood the specification correctly, integers are still padded according to the bits parameter.
So for example, uint16 (0x12) should be 0x0012, but this code will produce 0x12 instead

Token::Bool(b) => {
encoded.push(*b as u8);
},
Token::Array { arr, .. } => {
for element in arr.iter() {
let element_data = encode_packed_tokens(&[element.clone()]);
encoded.extend_from_slice(&element_data);
}
},
Token::FixedArray { arr, .. } => {
for element in arr.iter() {
let element_data = encode_packed_tokens(&[element.clone()]);
encoded.extend_from_slice(&element_data);
}
},
Token::Tuple { params } => {
for element in params.iter() {
let element_data = encode_packed_tokens(&[element.value.clone()]);
encoded.extend_from_slice(&element_data);
}
},
}
}
encoded
}

fn trim_leading_zeros(buf: &[u8]) -> &[u8] {
let first_non_zero = buf.iter().position(|&x| x != 0).unwrap_or(buf.len() - 1);
&buf[first_non_zero..]
}

#[cfg(test)]
mod tests {
use hex::FromHex;
use super::*;
use crate::abi::non_empty_array::{NonEmptyArray, NonEmptyBytes};
use crate::abi::param::Param;
Expand Down Expand Up @@ -1083,4 +1143,124 @@ mod tests {
.unwrap();
assert_eq!(encoded, expected);
}
}

#[test]
fn encode_packed_address() {
let address = Token::Address("0x1111111111111111111111111111111111111111".into());
let encoded = encode_packed_tokens(&[address]);
let expected = "1111111111111111111111111111111111111111".decode_hex().unwrap();
assert_eq!(encoded, expected);
}

#[test]
fn encode_packed_bytes() {
let bytes = Token::Bytes(vec![0x12, 0x34]);
let encoded = encode_packed_tokens(&[bytes]);
let expected = "1234".decode_hex().unwrap();
assert_eq!(encoded, expected);
}

#[test]
fn encode_packed_string() {
let s = Token::String("Hello, world!".to_owned());
let encoded = encode_packed_tokens(&[s]);
let expected = "48656c6c6f2c20776f726c6421".decode_hex().unwrap();
assert_eq!(encoded, expected);
}

#[test]
fn encode_packed_int() {
let int = Token::Int {
int: I256::from(-1),
bits: UintBits::new(256).unwrap(),
};
let encoded = encode_packed_tokens(&[int]);
let expected = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff".decode_hex().unwrap();
assert_eq!(encoded, expected);
}

#[test]
fn encode_packed_bool() {
let b = Token::Bool(true);
let encoded = encode_packed_tokens(&[b]);
let expected = "01".decode_hex().unwrap();
assert_eq!(encoded, expected);

let b = Token::Bool(false);
let encoded = encode_packed_tokens(&[b]);
let expected = "00".decode_hex().unwrap();
assert_eq!(encoded, expected);
}

#[test]
fn encode_packed_uint() {
let uint = Token::Uint {
uint: U256::from(3_u64),
bits: UintBits::new(256).unwrap(),
};
let encoded = encode_packed_tokens(&[uint]);
let expected = "03".decode_hex().unwrap();
theghostmac marked this conversation as resolved.
Show resolved Hide resolved
assert_eq!(encoded, expected);
}

#[test]
fn encode_packed_fixed_bytes() {
let bytes = Token::FixedBytes(NonEmptyBytes::new(vec![0x12, 0x34]).unwrap());
let encoded = encode_packed_tokens(&[bytes]);
let expected = "1234".decode_hex().unwrap();
assert_eq!(encoded, expected);
}

#[test]
fn encode_packed_mixed() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done. does this look good enough?:

 #[test]
    fn encode_packed_custom_case() {
        // bytes32 stateRoots
        let state_roots = NonEmptyBytes::from_slice(
            "3a53dc4890241dbe03e486e785761577d1c369548f6b09aa38017828dcdf5c27"
                .decode_hex()
                .unwrap()
                .as_slice(),
        ).unwrap();

        // uint256[2] calldata signatures
        let signatures = Token::Array {
            arr: vec![
                Token::Uint {
                    uint: U256::from_str(
                        "3402053321874964899321528271743396700217057178612185975187363512030360053932"
                    ).unwrap(),
                    bits: UintBits::new(256).unwrap(),
                },
                Token::Uint {
                    uint: U256::from_str(
                        "1235124644010117237054094970590473241953434069965207718920579820322861537001"
                    ).unwrap(),
                    bits: UintBits::new(256).unwrap(),
                },
            ],
            kind: ParamType::Uint {
                bits: UintBits::new(256).unwrap(),
            },
        };

        // uint256 feeReceivers
        let fee_receivers = Token::Uint {
            uint: U256::zero(),
            bits: UintBits::new(256).unwrap(),
        };

        // bytes calldata txss
        let txss = Token::Bytes(
            "000000000000000100010000"
                .decode_hex()
                .unwrap(),
        );

        let tokens = vec![Token::FixedBytes(state_roots), signatures, fee_receivers, txss];
        let encoded = encode_packed_tokens(&tokens);

        let expected = "3a53dc4890241dbe03e486e785761577d1c369548f6b09aa38017828dcdf5c2707857e73108d077c5b7ef89540d6493f70d940f1763a9d34c9d98418a39d28ac02bb0e4743a7d0586711ee3dd6311256579ab7abcd53c9c76f040bfde4d6d6e90000000000000000000000000000000000000000000000000000000000000000000000000000000100010000"
            .decode_hex()
            .unwrap();

        assert_eq!(encoded, expected);
    }
    ```

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code can be cleaned up a bit if you move strings to a separate variable. For example,

let uint1 = "3402053321874964899321528271743396700217057178612185975187363512030360053932";

Token::Uint {
    uint: U256::from_str(uint1).unwrap(),
    bits: UintBits::new(256).unwrap(),
},

Anyway, does this test work?

let tokens = vec![
Token::Int {
int: I256::from(-1),
bits: UintBits::new(256).unwrap(),
},
Token::FixedBytes(NonEmptyBytes::new(vec![0x42]).unwrap()),
Token::Uint {
uint: U256::from(3_u64),
bits: UintBits::new(256).unwrap(),
},
Token::String("Hello, world!".to_owned()),
];
let encoded = encode_packed_tokens(&tokens);
// The expected value must match the packed representation of the tokens.
// The integer -1 as 32 bytes of f, followed by 0x42, followed by the integer 3,
// followed by the ASCII representation of "Hello, world!"
let expected = concat!(
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", // -1
"42", // 0x42
"03", // 3
"48656c6c6f2c20776f726c6421" // "Hello, world!" in ASCII
).decode_hex().unwrap();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test should be fixed accordingly

assert_eq!(encoded, expected);
}

#[test]
fn encode_packed_array() {
let array = Token::Array {
arr: vec![
Token::Uint {
uint: U256::from(1_u64),
bits: UintBits::new(256).unwrap(),
},
Token::Uint {
uint: U256::from(2_u64),
bits: UintBits::new(256).unwrap(),
},
Token::Uint {
uint: U256::from(3_u64),
bits: UintBits::new(256).unwrap(),
},
],
kind: ParamType::Uint { bits: UintBits::new(256).unwrap() },
};
let encoded = encode_packed_tokens(&[array]);

let expected = Vec::from_hex("010203").unwrap();

assert_eq!(encoded, expected);
}
}
6 changes: 6 additions & 0 deletions rust/tw_evm/src/address.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ pub struct Address {
bytes: H160,
}

impl AsRef<[u8]> for Address {
fn as_ref(&self) -> &[u8] {
self.bytes.as_slice()
}
}

/// cbindgen:ignore
impl Address {
pub const LEN: usize = 20;
Expand Down
2 changes: 1 addition & 1 deletion rust/tw_number/src/i256.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ lazy_static! {

#[derive(Clone, PartialEq)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
pub struct I256(BaseU256);
pub struct I256(pub BaseU256);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding getter/setter functions instead


// cbindgen:ignore
impl I256 {
Expand Down
2 changes: 1 addition & 1 deletion rust/tw_number/src/u256.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use tw_memory::Data;

#[derive(Copy, Clone, Debug, Default, PartialEq)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
pub struct U256(pub(crate) primitive_types::U256);
pub struct U256(pub primitive_types::U256);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add necessary getter/setter functions instead

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yo


impl From<primitive_types::U256> for U256 {
#[inline]
Expand Down
Loading