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

Prestwich/dyn enc #2

Merged
merged 9 commits into from
Apr 7, 2023
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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ foundry.
- Set up branch protection
- fix uint CI
- maybe: integrate uint CI with local CI?
- fix wasm in uint
3 changes: 2 additions & 1 deletion abi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,6 @@ thiserror = { version = "1.0.39", optional = true }
hex-literal = "0.3.4"

[features]
default = ["std"]
default = ["std", "dynamic"]
std = ["hex/std", "thiserror"]
dynamic = ["std"]
40 changes: 36 additions & 4 deletions abi/src/decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,10 @@ pub(crate) fn check_fixed_bytes(word: Word, len: usize) -> bool {

pub(crate) fn as_u32(word: Word, type_check: bool) -> AbiResult<u32> {
if type_check && !check_zeroes(&word.as_slice()[..28]) {
return Err(Error::TypeCheckFail {
data: hex::encode(word),
expected_type: "Solidity pointer (uint32)".to_owned(),
});
return Err(Error::type_check_fail(
hex::encode(word),
"Solidity pointer (uint32)",
));
}

let result = ((word[28] as u32) << 24)
Expand Down Expand Up @@ -125,6 +125,10 @@ impl core::fmt::Debug for Decoder<'_> {
}

impl<'a> Decoder<'a> {
/// Instantiate a new decoder from a byte slice and a validation flag. If
/// Validation is set to true, the decoder will check that the bytes
/// conform to expected type limitations, and that the decoded values can be
/// re-encoded to an identical bytestring.
pub fn new(buf: &'a [u8], validate: bool) -> Self {
Self {
buf,
Expand All @@ -133,6 +137,9 @@ impl<'a> Decoder<'a> {
}
}

/// Create a child decoder, starting at `offset` bytes from the current
/// decoder's offset. The child decoder shares the buffer and validation
/// flag
fn child(&self, offset: usize) -> Result<Decoder<'a>, Error> {
if offset > self.buf.len() {
return Err(Error::Overrun);
Expand All @@ -144,62 +151,80 @@ impl<'a> Decoder<'a> {
})
}

/// Get a child decoder at the current offset
pub fn raw_child(&self) -> Decoder<'a> {
self.child(self.offset).unwrap()
}

/// Advance the offset by `len` bytes
fn increase_offset(&mut self, len: usize) {
self.offset += len;
}

/// Peek a range from the buffer
pub fn peek(&self, range: Range<usize>) -> Result<&'a [u8], Error> {
(self.buf.len() >= range.end)
.then(|| &self.buf[range])
.ok_or(Error::Overrun)
}

/// Peek a slice of size `len` from the buffer at a specific offset, without
/// advancing the offset
pub fn peek_len_at(&self, offset: usize, len: usize) -> Result<&'a [u8], Error> {
self.peek(offset..offset + len)
}

/// Peek a slice of size `len` from the buffer without advancing the offset.
pub fn peek_len(&self, len: usize) -> Result<&'a [u8], Error> {
self.peek_len_at(self.offset, len)
}

/// Peek a word from the buffer at a specific offset, without advancing the
/// offset
pub fn peek_word_at(&self, offset: usize) -> Result<Word, Error> {
Ok(Word::from_slice(
self.peek_len_at(offset, Word::len_bytes())?,
))
}

/// Peek the next word from the buffer without advancing the offset.
pub fn peek_word(&self) -> Result<Word, Error> {
self.peek_word_at(self.offset)
}

/// Peek a u32 from the buffer at a specific offset, without advancing the
/// offset.
pub fn peek_u32_at(&self, offset: usize) -> AbiResult<u32> {
as_u32(self.peek_word_at(offset)?, true)
}

/// Peek the next word as a u32
pub fn peek_u32(&self) -> AbiResult<u32> {
as_u32(self.peek_word()?, true)
}

/// Take a word from the buffer, advancing the offset.
pub fn take_word(&mut self) -> Result<Word, Error> {
let contents = self.peek_word()?;
self.increase_offset(Word::len_bytes());
Ok(contents)
}

/// Return a child decoder by consuming a word, interpreting it as a
/// pointer, and following it.
pub fn take_indirection(&mut self) -> Result<Decoder<'a>, Error> {
let ptr = self.take_u32()? as usize;
self.child(ptr)
}

/// Take a u32 from the buffer by consuming a word.
pub fn take_u32(&mut self) -> AbiResult<u32> {
let word = self.take_word()?;
as_u32(word, true)
}

/// Takes a slice of bytes of the given length by consuming up to the next
/// word boundary
pub fn take_slice(&mut self, len: usize) -> Result<&[u8], Error> {
if self.validate {
let padded_len = round_up_nearest_multiple(len, 32);
Expand All @@ -217,22 +242,28 @@ impl<'a> Decoder<'a> {
Ok(res)
}

/// True if this decoder is validating type correctness
pub fn validate(&self) -> bool {
self.validate
}

/// Takes the offset from the child decoder and sets it as the current
/// offset.
pub fn take_offset(&mut self, child: Decoder<'a>) {
self.set_offset(child.offset + (self.buf.len() - child.buf.len()))
}

/// Sets the current offset in the buffer.
pub fn set_offset(&mut self, offset: usize) {
self.offset = offset;
}

/// Returns the current offset in the buffer.
pub fn offset(&self) -> usize {
self.offset
}

/// Decodes a single token from the underlying buffer.
pub fn decode<T>(&mut self, data: &[u8]) -> AbiResult<T>
where
T: TokenType,
Expand All @@ -246,6 +277,7 @@ impl<'a> Decoder<'a> {
Ok(token)
}

/// Decodes a sequence of tokens from the underlying buffer.
pub fn decode_sequence<T>(&mut self, data: &[u8]) -> AbiResult<T>
where
T: TokenType + TokenSeq,
Expand Down
111 changes: 111 additions & 0 deletions abi/src/dyn_abi/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
//! Dynamic Solidity Type Encoder
//!
//! This module provides a runtime encoder/decoder for solidity types. It is
//! intended to be used when the solidity type is not known at compile time.
//! This is particularly useful for EIP-712 signing interfaces.
//!
//! We **strongly** recommend using the static encoder/decoder when possible.
//! The dyanmic encoder/decoder is significantly more expensive, especially for
//! complex types. It is also significantly more error prone, as the mapping
//! between solidity types and rust types is not enforced by the compiler.
//!
//! ## Example
//!
//! ```
//! # use ethers_abi_enc::{Decoder, Encoder, DynSolType, DynSolValue};
//! // parse a type from a string
//! let my_type: DynSolType = "uint8[2][]".parse().unwrap();
//!
//! // set values
//! let uints = DynSolValue::FixedArray(vec![0u8.into(), 1u8.into()]);
//! let my_values = DynSolValue::Array(vec![uints]);
//!
//! // encode
//! let encoded = my_type.encode_single(my_values.clone()).unwrap();
//!
//! // decode
//! let decoded = my_type.decode_single(&encoded).unwrap();
//!
//! assert_eq!(decoded, my_values);
//! ```
//!
//! ## How it works
//!
//! The dynamic encodr/decoder is implemented as a set of enums that represent
//! solidity types, solidity values (in rust representation form), and ABI
//! tokens. Unlike the static encoder, each of these must be instantiated at
//! runtime. The [`DynSolType`] enum represents a solidity type, and is
//! equivalent to an enum over types implementing the [`crate::SolType`] trait.
//! The [`DynSolValue`] enum represents a solidity value, and describes the
//! rust shapes of possible solidity values. It is similar to, but not
//! equivalent to an enum over types used as [`crate::SolType::RustType`]. The
//! [`DynToken`] enum represents an ABI token, and is equivalent to an enum over
//! the types implementing the [`crate::TokenType`] trait.
//!
//! Where the static encoding system encodes the expected type information into
//! the rust type system, the dynamic encoder/decoder encodes it as a concrete
//! instance of [`DynSolType`]. This type is used to tokenize and detokenize
//! [`DynSolValue`] instances. The [`parse`] function is used to parse a
//! solidity type string into a [`DynSolType`] object.
//!
//! Tokenizing - `DynSolType + `DynSolValue` = `DynToken`
//! Detokenizing - `DynSolType` + `DynToken` = `DynSolValue`
//!
//! Users must manually handle the conversions between [`DynSolValue`] and their
//! own rust types. We provide several `From` implementations, but they fall
//! short when dealing with arrays and tuples. We also provide fallible casts
//! into the contents of each variant.
//!
//! ## `DynToken::decode_populate`
//!
//! Because the shape of the data is known only at runtime, we cannot
//! compile-time allocate the memory needed to hold decoded data. Instead, we
//! pre-allocate a [`DynToken`] with the same shape as the expected type, and
//! empty values. We then populate the empty values with the decoded data.
//!
//! This is a significant behavior departure from the static decoder. We do not
//! recommend using the [`DynToken`] type directly. Instead, we recommend using
//! the encoding and decoding methods on [`DynSolType`].
mod sol_type;
pub use sol_type::DynSolType;

mod sol_value;
pub use sol_value::DynSolValue;

mod token;
pub use token::DynToken;

mod parser;
pub use parser::ParserError;

#[cfg(test)]
mod test {
use crate::{decoder::Decoder, encoder::Encoder, DynSolType, DynSolValue};

#[test]
fn simple_e2e() {
// parse a type from a string
let my_type: DynSolType = "uint8[2][]".parse().unwrap();

// set values
let uints = DynSolValue::FixedArray(vec![64u8.into(), 128u8.into()]);
let my_values = DynSolValue::Array(vec![uints]);

// tokenize and detokenize
let tokens = my_type.tokenize(my_values.clone()).unwrap();
let detokenized = my_type.detokenize(tokens.clone()).unwrap();
assert_eq!(detokenized, my_values);

// encode
let mut encoder = Encoder::default();
tokens.encode_single(&mut encoder).unwrap();
let encoded = encoder.into_bytes();

// decode
let mut decoder = Decoder::new(&encoded, true);
let mut decoded = my_type.empty_dyn_token();
decoded.decode_single_populate(&mut decoder).unwrap();

assert_eq!(decoded, tokens);
}
}
Loading