Skip to content

Commit

Permalink
feat: add logs, add log dynamic decoding (#271)
Browse files Browse the repository at this point in the history
* feat: add logs, add log dynamic decoding

* feat: logs bloom function

* merge fixes

* DynSolEvent & ResolveSolEvent (#309)

* refactor: DynSolEvent and ResolveSolEvent

* fix: num_topics

* feature: Log::is_valid

* test: make assertion

* chore: getters on DynSolEvent

* chore: remove dbg

* lint: clippy

* lint: clippy

* nit: indent

---------

Co-authored-by: James Prestwich <[email protected]>
  • Loading branch information
DaniPopes and prestwich authored Oct 3, 2023
1 parent e49fad6 commit fe4a535
Show file tree
Hide file tree
Showing 16 changed files with 719 additions and 71 deletions.
29 changes: 27 additions & 2 deletions crates/dyn-abi/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use alloc::{borrow::Cow, string::String};
use alloy_primitives::B256;
use alloy_sol_type_parser::Error as TypeParserError;
use alloy_sol_types::Error as SolTypesError;
use core::fmt;
Expand Down Expand Up @@ -37,6 +38,21 @@ pub enum Error {
actual: usize,
},

/// Length mismatch during event topic decoding.
TopicLengthMismatch {
/// The expected length.
expected: usize,
/// The actual length.
actual: usize,
},
/// Invalid event signature.
EventSignatureMismatch {
/// The expected signature.
expected: B256,
/// The actual signature.
actual: B256,
},

/// [`hex`] error.
Hex(hex::FromHexError),
/// [`alloy_sol_type_parser`] error.
Expand Down Expand Up @@ -88,11 +104,20 @@ impl fmt::Display for Error {

Self::TypeMismatch { expected, actual } => write!(
f,
"type mismatch, expected type {expected:?}, got value with type {actual:?}",
"type mismatch: expected type {expected:?}, got value with type {actual:?}",
),
&Self::EncodeLengthMismatch { expected, actual } => write!(
f,
"encode length mismatch, expected {expected} types, got {actual}",
"encode length mismatch: expected {expected} types, got {actual}",
),

&Self::TopicLengthMismatch { expected, actual } => write!(
f,
"invalid log topic list length: expected {expected} topics, got {actual}",
),
Self::EventSignatureMismatch { expected, actual } => write!(
f,
"invalid event signature: expected {expected}, got {actual}",
),

Self::Hex(e) => e.fmt(f),
Expand Down
204 changes: 204 additions & 0 deletions crates/dyn-abi/src/event.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
use crate::{DynSolType, DynSolValue, Error, Result};
use alloc::vec::Vec;
use alloy_primitives::{Log, B256};

/// A dynamic ABI event.
///
/// This is a representation of a Solidity event, which can be used to decode
/// logs.
#[derive(Debug, Clone, PartialEq)]
pub struct DynSolEvent {
pub(crate) topic_0: Option<B256>,
pub(crate) indexed: Vec<DynSolType>,
pub(crate) body: DynSolType,
}

impl DynSolEvent {
/// Creates a new event, without length-checking the indexed, or ensuring
/// the body is a tuple. This allows creation of invalid events.
pub fn new_unchecked(
topic_0: Option<B256>,
indexed: Vec<DynSolType>,
body: DynSolType,
) -> Self {
Self {
topic_0,
indexed,
body,
}
}

/// Creates a new event.
///
/// Checks that the indexed length is less than or equal to 4, and that the
/// body is a tuple.
pub fn new(topic_0: Option<B256>, indexed: Vec<DynSolType>, body: DynSolType) -> Option<Self> {
if indexed.len() > 4 || body.as_tuple().is_none() {
return None
}
Some(Self::new_unchecked(topic_0, indexed, body))
}

/// True if anonymous.
pub const fn is_anonymous(&self) -> bool {
self.topic_0.is_none()
}

/// Decode the event from the given log info.
pub fn decode_log_parts<I>(
&self,
topics: I,
data: &[u8],
validate: bool,
) -> Result<DecodedEvent>
where
I: IntoIterator<Item = B256>,
{
let mut topics = topics.into_iter();
let num_topics = self.indexed.len() + !self.is_anonymous() as usize;
if validate {
match topics.size_hint() {
(n, Some(m)) if n == m && n != num_topics => {
return Err(Error::TopicLengthMismatch {
expected: num_topics,
actual: n,
})
}
_ => {}
}
}

// skip event hash if not anonymous
if !self.is_anonymous() {
let t = topics.next();
// Validate only if requested
if validate {
match t {
Some(sig) => {
let expected = self.topic_0.expect("not anonymous");
if sig != expected {
return Err(Error::EventSignatureMismatch {
expected,
actual: sig,
})
}
}
None => {
return Err(Error::TopicLengthMismatch {
expected: num_topics,
actual: 0,
})
}
}
}
}

let indexed = self
.indexed
.iter()
.zip(topics.by_ref().take(self.indexed.len()))
.map(|(ty, topic)| {
let value = ty.decode_event_topic(topic);
Ok(value)
})
.collect::<Result<_>>()?;

let body = self
.body
.abi_decode_sequence(data)?
.into_fixed_seq()
.expect("body is a tuple");

if validate {
let remaining = topics.count();
if remaining > 0 {
return Err(Error::TopicLengthMismatch {
expected: num_topics,
actual: num_topics + remaining,
})
}
}

Ok(DecodedEvent { indexed, body })
}

/// Decode the event from the given log info.
pub fn decode_log(&self, log: &Log, validate: bool) -> Result<DecodedEvent> {
self.decode_log_parts(log.topics().iter().copied(), &log.data, validate)
}

/// Get the selector for this event, if any.
pub const fn topic_0(&self) -> Option<B256> {
self.topic_0
}

/// Get the indexed types.
pub fn indexed(&self) -> &[DynSolType] {
&self.indexed
}

/// Get the un-indexed types.
pub fn body(&self) -> &[DynSolType] {
self.body.as_tuple().expect("body is a tuple")
}
}

/// A decoded dynamic ABI event.
#[derive(Debug, Clone, PartialEq)]
pub struct DecodedEvent {
/// The indexed values, in order.
pub indexed: Vec<DynSolValue>,
/// The un-indexed values, in order.
pub body: Vec<DynSolValue>,
}

#[cfg(test)]
mod test {
use alloy_primitives::{address, b256, bytes, U256};

use super::*;

#[test]
fn it_decodes_a_simple_log() {
let log = Log::new_unchecked(vec![], U256::ZERO.to_be_bytes_vec().into());
let event = DynSolEvent {
topic_0: None,
indexed: vec![],
body: DynSolType::Tuple(vec![DynSolType::Uint(256)]),
};
event.decode_log(&log, true).unwrap();
}

#[test]
fn it_decodes_logs_with_indexed_params() {
let t0 = b256!("cf74b4e62f836eeedcd6f92120ffb5afea90e6fa490d36f8b81075e2a7de0cf7");
let log = Log::new_unchecked(
vec![
t0,
b256!("0000000000000000000000000000000000000000000000000000000000012321"),
],
bytes!(
"
0000000000000000000000000000000000000000000000000000000000012345
0000000000000000000000000000000000000000000000000000000000054321
"
),
);
let event = DynSolEvent {
topic_0: Some(t0),
indexed: vec![DynSolType::Address],
body: DynSolType::Tuple(vec![DynSolType::Tuple(vec![
DynSolType::Address,
DynSolType::Address,
])]),
};

let decoded = event.decode_log(&log, true).unwrap();
assert_eq!(
decoded.indexed,
vec![DynSolValue::Address(address!(
"0000000000000000000000000000000000012321"
))]
);
}
}
29 changes: 15 additions & 14 deletions crates/dyn-abi/src/ext/abi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ pub trait JsonAbiExt: Sealed {
///
/// This function will return an error if the decoded data does not match
/// the expected input types.
fn abi_decode_input(&self, data: &[u8]) -> Result<Vec<DynSolValue>>;
fn abi_decode_input(&self, data: &[u8], validate: bool) -> Result<Vec<DynSolValue>>;
}

/// Provide ABI encoding and decoding for the [`Function`] type.
Expand All @@ -79,7 +79,7 @@ pub trait FunctionExt: JsonAbiExt + Sealed {
/// ABI-decodes the given data according to this functions's output types.
///
/// This method does not check for any prefixes or selectors.
fn abi_decode_output(&self, data: &[u8]) -> Result<Vec<DynSolValue>>;
fn abi_decode_output(&self, data: &[u8], validate: bool) -> Result<Vec<DynSolValue>>;
}

impl JsonAbiExt for Constructor {
Expand All @@ -94,8 +94,8 @@ impl JsonAbiExt for Constructor {
}

#[inline]
fn abi_decode_input(&self, data: &[u8]) -> Result<Vec<DynSolValue>> {
abi_decode(data, &self.inputs)
fn abi_decode_input(&self, data: &[u8], validate: bool) -> Result<Vec<DynSolValue>> {
abi_decode(data, &self.inputs, validate)
}
}

Expand All @@ -111,8 +111,8 @@ impl JsonAbiExt for Error {
}

#[inline]
fn abi_decode_input(&self, data: &[u8]) -> Result<Vec<DynSolValue>> {
abi_decode(data, &self.inputs)
fn abi_decode_input(&self, data: &[u8], validate: bool) -> Result<Vec<DynSolValue>> {
abi_decode(data, &self.inputs, validate)
}
}

Expand All @@ -128,8 +128,8 @@ impl JsonAbiExt for Function {
}

#[inline]
fn abi_decode_input(&self, data: &[u8]) -> Result<Vec<DynSolValue>> {
abi_decode(data, &self.inputs)
fn abi_decode_input(&self, data: &[u8], validate: bool) -> Result<Vec<DynSolValue>> {
abi_decode(data, &self.inputs, validate)
}
}

Expand All @@ -140,8 +140,8 @@ impl FunctionExt for Function {
}

#[inline]
fn abi_decode_output(&self, data: &[u8]) -> Result<Vec<DynSolValue>> {
abi_decode(data, &self.outputs)
fn abi_decode_output(&self, data: &[u8], validate: bool) -> Result<Vec<DynSolValue>> {
abi_decode(data, &self.outputs, validate)
}
}

Expand Down Expand Up @@ -183,9 +183,9 @@ fn abi_encode(values: &[DynSolValue]) -> Vec<u8> {
DynSolValue::encode_seq(values)
}

fn abi_decode(data: &[u8], params: &[Param]) -> Result<Vec<DynSolValue>> {
fn abi_decode(data: &[u8], params: &[Param], validate: bool) -> Result<Vec<DynSolValue>> {
let mut values = Vec::with_capacity(params.len());
let mut decoder = Decoder::new(data, false);
let mut decoder = Decoder::new(data, validate);
for param in params {
let ty = param.resolve()?;
let value = ty.abi_decode_inner(&mut decoder, crate::DynToken::decode_single_populate)?;
Expand Down Expand Up @@ -253,11 +253,12 @@ mod tests {

// decode
let response = U256::from(1u8).to_be_bytes_vec();
let decoded = func.abi_decode_output(&response).unwrap();
let decoded = func.abi_decode_output(&response, true).unwrap();
assert_eq!(decoded, [DynSolValue::Uint(U256::from(1u8), 256)]);

// Fail on wrong response type
let bad_response = Address::repeat_byte(3u8).to_vec();
assert!(func.abi_decode_output(&bad_response).is_err());
assert!(func.abi_decode_output(&bad_response, true).is_err());
assert!(func.abi_decode_output(&bad_response, false).is_err());
}
}
Loading

0 comments on commit fe4a535

Please sign in to comment.