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

Refactoring dyn-abi to performance parity with ethabi #144

Merged
merged 14 commits into from
Jun 26, 2023
Merged
15 changes: 6 additions & 9 deletions crates/dyn-abi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ 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();
let encoded = my_values.clone().encode_single();

// decode
let decoded = my_type.decode_single(&encoded).unwrap();
Expand All @@ -51,18 +51,15 @@ equivalent to an enum over types used as [`crate::SolType::RustType`]. The
the types implementing the [`alloy_sol_types::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 [`std::str::FromStr`] impl is used to parse a
solidity type string into a [`DynSolType`] object.
the Rust type system, the dynamic encoder/decoder encodes it as a concrete
instance of [`DynSolType`].

- Tokenizing: `DynSolType` + `DynSolValue` = `DynToken`
- Detokenizing: `DynSolType` + `DynToken` = `DynSolValue`
- 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.
short when dealing with arrays, tuples and structs. We also provide fallible
casts into the contents of each variant.

## `DynToken::decode_populate`

Expand Down
16 changes: 2 additions & 14 deletions crates/dyn-abi/benches/abi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,27 +54,15 @@ fn dyn_abi_encode(c: &mut Criterion) {
g.bench_function("single", |b| {
let input = encode_single_input();
b.iter(|| {
let ty = DynSolType::String;
let value = DynSolValue::String(input.clone());
ty.encode_single(black_box(value))
black_box(value).encode_single()
});
});

g.bench_function("struct", |b| {
let tys = vec![
DynSolType::Address,
DynSolType::Address,
DynSolType::Uint(24),
DynSolType::Address,
DynSolType::Uint(256),
DynSolType::Uint(256),
DynSolType::Uint(256),
DynSolType::Uint(160),
];
let ty = DynSolType::Tuple(tys);
let input = encode_struct_sol_values();
let input = DynSolValue::Tuple(input.to_vec());
b.iter(|| ty.encode_single(black_box(&input).clone()));
Copy link
Member Author

Choose a reason for hiding this comment

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

this was doing an extra alloc in the encode_single

b.iter(|| black_box(&input).encode());
});

g.finish();
Expand Down
8 changes: 3 additions & 5 deletions crates/dyn-abi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ extern crate alloc;

mod no_std_prelude {
pub(crate) use alloc::{
borrow::{Borrow, ToOwned},
borrow::{Borrow, Cow, ToOwned},
boxed::Box,
string::{String, ToString},
vec::Vec,
Expand Down Expand Up @@ -69,14 +69,12 @@ mod tests {
let my_values = DynSolValue::Array(vec![uints]);

// tokenize and detokenize
let tokens = my_type.tokenize(my_values.clone()).unwrap();
let tokens = my_values.tokenize();
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();
let encoded = my_values.clone().encode_single();

// decode
let mut decoder = Decoder::new(&encoded, true);
Expand Down
186 changes: 57 additions & 129 deletions crates/dyn-abi/src/token.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::{no_std_prelude::*, Decoder, Encoder, Error, Result, Word};
use crate::{no_std_prelude::*, Decoder, DynSolValue, Error, Result, Word};
use alloc::borrow::Cow;
use alloy_sol_types::token::{PackedSeqToken, TokenType, WordToken};

Expand All @@ -17,43 +17,61 @@ pub enum DynToken<'a> {
DynSeq {
/// The contents of the dynamic sequence.
contents: Cow<'a, [DynToken<'a>]>,
/// The type of the dynamic sequence.
template: Box<DynToken<'a>>,
/// The type template of the dynamic sequence.
/// This is used only when decoding. It indicates what the token type
/// of the sequence is. During tokenization of data, the type of the
/// contents is known, so this is not needed.
template: Option<Box<DynToken<'a>>>,
},
/// A packed sequence (string or bytes).
PackedSeq(Vec<u8>),
PackedSeq(&'a [u8]),
}

impl<'a> PartialEq<DynToken<'a>> for DynToken<'a> {
fn eq(&self, other: &Self) -> bool {
impl<T: Into<Word>> From<T> for DynToken<'_> {
fn from(value: T) -> Self {
Self::Word(value.into())
}
}

impl<'a> PartialEq<DynToken<'a>> for DynToken<'_> {
fn eq(&self, other: &DynToken<'_>) -> bool {
match (self, other) {
(Self::Word(l0), Self::Word(r0)) => l0 == r0,
(Self::FixedSeq(l0, l1), Self::FixedSeq(r0, r1)) => l0 == r0 && l1 == r1,
(Self::Word(l0), DynToken::Word(r0)) => l0 == r0,
(Self::FixedSeq(l0, l1), DynToken::FixedSeq(r0, r1)) => l0 == r0 && l1 == r1,
(
Self::DynSeq {
contents: l_contents,
..
},
Self::DynSeq {
DynToken::DynSeq {
contents: r_contents,
..
},
) => l_contents == r_contents,
(Self::PackedSeq(l0), Self::PackedSeq(r0)) => l0 == r0,
(Self::PackedSeq(l0), DynToken::PackedSeq(r0)) => l0 == r0,
_ => false,
}
}
}

impl Eq for DynToken<'_> {}

impl From<Word> for DynToken<'_> {
fn from(value: Word) -> Self {
Self::Word(value)
impl<'a> DynToken<'a> {
/// Instantiate a DynToken from a fixed sequence of values.
pub fn from_fixed_seq(seq: &'a [DynSolValue]) -> Self {
let tokens = seq.iter().map(|v| v.tokenize()).collect::<Vec<_>>();
Self::FixedSeq(Cow::Owned(tokens), seq.len())
}

/// Instantiate a DynToken from a dynamic sequence of values.
pub fn from_dyn_seq(seq: &'a [DynSolValue]) -> Self {
let tokens = seq.iter().map(|v| v.tokenize()).collect::<Vec<_>>();
Self::DynSeq {
contents: Cow::Owned(tokens),
template: None,
}
}
}

impl<'a> DynToken<'a> {
/// Attempt to cast to a word.
#[inline]
pub const fn as_word(&self) -> Option<Word> {
Expand Down Expand Up @@ -81,9 +99,18 @@ impl<'a> DynToken<'a> {
}
}

/// Fallible cast into a sequence, dynamic or fixed-size
pub fn as_token_seq(&self) -> Option<&[DynToken<'a>]> {
match self {
Self::FixedSeq(tokens, _) => Some(tokens),
Self::DynSeq { contents, .. } => Some(contents),
_ => None,
}
}

/// Fallible cast into a packed sequence.
#[inline]
pub fn as_packed_seq(&self) -> Option<&[u8]> {
pub const fn as_packed_seq(&self) -> Option<&[u8]> {
match self {
Self::PackedSeq(bytes) => Some(bytes),
_ => None,
Expand All @@ -102,7 +129,8 @@ impl<'a> DynToken<'a> {
}

/// Decodes from a decoder, populating the structure with the decoded data.
pub fn decode_populate(&mut self, dec: &mut Decoder<'_>) -> Result<()> {
#[inline]
pub fn decode_populate(&mut self, dec: &mut Decoder<'a>) -> Result<()> {
let dynamic = self.is_dynamic();
match self {
Self::Word(w) => *w = WordToken::decode_from(dec)?.0,
Expand All @@ -119,122 +147,27 @@ impl<'a> DynToken<'a> {
Self::DynSeq { contents, template } => {
let mut child = dec.take_indirection()?;
let size = child.take_u32()? as usize;
let mut new_tokens = Vec::with_capacity(size);
let mut new_tokens: Vec<DynToken<'a>> = Vec::with_capacity(size);
for _ in 0..size {
let mut t = (**template).clone();
let mut t = if let Some(item) = new_tokens.first() {
item.clone()
} else {
*(template.take().unwrap())
};
t.decode_populate(&mut child)?;
new_tokens.push(t);
}
*contents = new_tokens.into();
}
Self::PackedSeq(buf) => *buf = PackedSeqToken::decode_from(dec)?.into_vec(),
}
Ok(())
}

/// Returns the number of words this type uses in the head of the ABI blob.
#[inline]
pub fn head_words(&self) -> usize {
match self {
Self::Word(_) => 1,
Self::FixedSeq(tokens, _) => {
if self.is_dynamic() {
1
} else {
tokens.iter().map(Self::head_words).sum()
}
}
Self::DynSeq { .. } => 1,
Self::PackedSeq(_) => 1,
}
}

/// Returns the number of words this type uses in the tail of the ABI blob.
#[inline]
pub fn tail_words(&self) -> usize {
match self {
Self::Word(_) => 0,
Self::FixedSeq(_, size) => self.is_dynamic() as usize * *size,
Self::DynSeq { contents, .. } => {
1 + contents.iter().map(Self::tail_words).sum::<usize>()
}
Self::PackedSeq(buf) => 1 + (buf.len() + 31) / 32,
}
}

/// Append this data to the head of an in-progress blob via the encoder.
pub fn head_append(&self, enc: &mut Encoder) {
match self {
Self::Word(word) => enc.append_word(*word),
Self::FixedSeq(tokens, _) => {
if self.is_dynamic() {
enc.append_indirection();
} else {
tokens.iter().for_each(|inner| inner.head_append(enc))
}
}
Self::DynSeq { .. } => enc.append_indirection(),
Self::PackedSeq(_) => enc.append_indirection(),
}
}

/// Append this data to the tail of an in-progress blob via the encoder.
#[inline]
pub fn tail_append(&self, enc: &mut Encoder) {
match self {
Self::Word(_) => {}
Self::FixedSeq(_, _) => {
if self.is_dynamic() {
self.encode_sequence(enc).expect("known to be sequence");
}
}
Self::DynSeq { contents, .. } => {
enc.append_seq_len(contents);
self.encode_sequence(enc).expect("known to be sequence");
}
Self::PackedSeq(buf) => enc.append_packed_seq(buf),
}
}

/// Encode this data, if it is a sequence. Error otherwise.
pub(crate) fn encode_sequence(&self, enc: &mut Encoder) -> Result<()> {
match self {
Self::FixedSeq(tokens, _) => {
let head_words = tokens.iter().map(Self::head_words).sum::<usize>();
enc.push_offset(head_words as u32);
for t in tokens.iter() {
t.head_append(enc);
enc.bump_offset(t.tail_words() as u32);
}
for t in tokens.iter() {
t.tail_append(enc);
}
enc.pop_offset();
}
Self::DynSeq { contents, .. } => {
let head_words = contents.iter().map(Self::head_words).sum::<usize>();
enc.push_offset(head_words as u32);
for t in contents.iter() {
t.head_append(enc);
enc.bump_offset(t.tail_words() as u32);
}
for t in contents.iter() {
t.tail_append(enc);
}
enc.pop_offset();
}
_ => {
return Err(Error::custom(
"Called encode_sequence on non-sequence token",
))
}
Self::PackedSeq(buf) => *buf = PackedSeqToken::decode_from(dec)?.0,
}
Ok(())
}

/// Decode a sequence from the decoder, populating the data by consuming
/// decoder words.
pub(crate) fn decode_sequence_populate(&mut self, dec: &mut Decoder<'_>) -> Result<()> {
#[inline]
pub(crate) fn decode_sequence_populate(&mut self, dec: &mut Decoder<'a>) -> Result<()> {
match self {
Self::FixedSeq(buf, _) => {
for item in buf.to_mut().iter_mut() {
Expand All @@ -249,16 +182,11 @@ impl<'a> DynToken<'a> {
}
}

/// Encode a single item of this type, as a sequence of length 1.
#[inline]
pub(crate) fn encode_single(&'a self, enc: &mut Encoder) -> Result<()> {
Self::FixedSeq(core::slice::from_ref(self).into(), 1).encode_sequence(enc)
}

/// Decode a single item of this type, as a sequence of length 1.
#[inline]
pub(crate) fn decode_single_populate(&mut self, dec: &mut Decoder<'_>) -> Result<()> {
// This is what `Self::FixedSeq(vec![self.clone()], 1).decode_populate()`
pub(crate) fn decode_single_populate(&mut self, dec: &mut Decoder<'a>) -> Result<()> {
// This is what
// `Self::FixedSeq(vec![self.clone()], 1).decode_populate()`
// would do, so we skip the allocation.
self.decode_populate(dec)
}
Expand Down
Loading