Skip to content

Commit

Permalink
Refactoring dyn-abi to performance parity with ethabi (#144)
Browse files Browse the repository at this point in the history
* fix: dyn token lifetime

* feat: tokenize for dyn sol value

* fix: apples to apples in bench

* feat: move encoding over to the value type

* refactor: remove tokenize from type

* refactor: cow and delete todo fn

* refactor: template unused in tokenization

* feat: move encoding to the value type

* refactor: remove encoding from token

* fix: apples to oranges in encode single

* fix: remove dbg

* opt: encode_sequence takes token slice

* fix: remove broken sol_type_name

* feat: type matches, and derive type and name where possible
  • Loading branch information
prestwich authored Jun 26, 2023
1 parent 26e8aad commit 8c6a5bb
Show file tree
Hide file tree
Showing 6 changed files with 410 additions and 334 deletions.
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()));
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 @@ -30,7 +30,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 @@ -70,14 +70,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

0 comments on commit 8c6a5bb

Please sign in to comment.