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

Use debug traces to extract the input for settlement::Observer #3245

Merged
merged 7 commits into from
Jan 27, 2025
Merged
Changes from 1 commit
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
Prev Previous commit
Next Next commit
use debug_traceTransaction instead
sunce86 committed Jan 20, 2025

Unverified

This user has not yet uploaded their public signing key.
commit 4f3da4fa0d5c793c1a334722672849554206e4a9
14 changes: 9 additions & 5 deletions crates/autopilot/src/domain/eth/mod.rs
Original file line number Diff line number Diff line change
@@ -312,11 +312,15 @@ pub struct TradeEvent {
pub order_uid: domain::OrderUid,
}

/// A trace of a Call type of action.
#[derive(Debug, Clone, Default)]
pub struct TraceCall {
pub to: Address,
/// Call frames of a transaction.
#[derive(Clone, Debug, Default)]
pub struct CallFrame {
/// The address of the contract that was called.
pub to: Option<Address>,
/// Calldata input.
pub input: Calldata,
/// Recorded child calls.
pub calls: Vec<CallFrame>,
}

/// Any type of on-chain transaction.
@@ -335,5 +339,5 @@ pub struct Transaction {
/// The effective gas price of the transaction.
pub gas_price: EffectiveGasPrice,
/// Traces of all Calls contained in the transaction.
pub trace_calls: Vec<TraceCall>,
pub trace_calls: CallFrame,
}
35 changes: 20 additions & 15 deletions crates/autopilot/src/domain/settlement/mod.rs
Original file line number Diff line number Diff line change
@@ -434,10 +434,11 @@ mod tests {
)));
let transaction = super::transaction::Transaction::try_new(
&domain::eth::Transaction {
trace_calls: vec![domain::eth::TraceCall {
to: settlement_contract,
trace_calls: domain::eth::CallFrame {
to: Some(settlement_contract),
input: calldata.into(),
}],
..Default::default()
},
..Default::default()
},
&domain_separator,
@@ -540,10 +541,11 @@ mod tests {
)));
let transaction = super::transaction::Transaction::try_new(
&domain::eth::Transaction {
trace_calls: vec![domain::eth::TraceCall {
to: settlement_contract,
trace_calls: domain::eth::CallFrame {
to: Some(settlement_contract),
input: calldata.into(),
}],
..Default::default()
},
..Default::default()
},
&domain_separator,
@@ -683,10 +685,11 @@ mod tests {
)));
let transaction = super::transaction::Transaction::try_new(
&domain::eth::Transaction {
trace_calls: vec![domain::eth::TraceCall {
to: settlement_contract,
trace_calls: domain::eth::CallFrame {
to: Some(settlement_contract),
input: calldata.into(),
}],
..Default::default()
},
..Default::default()
},
&domain_separator,
@@ -858,10 +861,11 @@ mod tests {
)));
let transaction = super::transaction::Transaction::try_new(
&domain::eth::Transaction {
trace_calls: vec![domain::eth::TraceCall {
to: settlement_contract,
trace_calls: domain::eth::CallFrame {
to: Some(settlement_contract),
input: calldata.into(),
}],
..Default::default()
},
..Default::default()
},
&domain_separator,
@@ -1039,10 +1043,11 @@ mod tests {
)));
let transaction = super::transaction::Transaction::try_new(
&domain::eth::Transaction {
trace_calls: vec![domain::eth::TraceCall {
to: settlement_contract,
trace_calls: domain::eth::CallFrame {
to: Some(settlement_contract),
input: calldata.into(),
}],
..Default::default()
},
..Default::default()
},
&domain_separator,
29 changes: 22 additions & 7 deletions crates/autopilot/src/domain/settlement/transaction/mod.rs
Original file line number Diff line number Diff line change
@@ -35,11 +35,7 @@ impl Transaction {
settlement_contract: eth::Address,
) -> Result<Self, Error> {
// find trace call to settlement contract
let calldata = transaction
.trace_calls
.iter()
.find(|trace| is_settlement_trace(trace, settlement_contract))
.map(|trace| trace.input.clone())
let calldata = find_settlement_trace(&transaction.trace_calls, settlement_contract).map(|trace| trace.input.clone())
// all transactions emitting settlement events should have a /settle call,
// otherwise it's an execution client bug
.ok_or(Error::MissingCalldata)?;
@@ -132,12 +128,31 @@ impl Transaction {
}
}

fn is_settlement_trace(trace: &eth::TraceCall, settlement_contract: eth::Address) -> bool {
fn find_settlement_trace(
call_frame: &eth::CallFrame,
settlement_contract: eth::Address,
) -> Option<&eth::CallFrame> {
// Use a stack to keep track of frames to process
let mut stack = vec![call_frame];

while let Some(call_frame) = stack.pop() {
if is_settlement_trace(call_frame, settlement_contract) {
return Some(call_frame);
}
// Add all nested calls to the stack
stack.extend(&call_frame.calls);
}

None
}

fn is_settlement_trace(trace: &eth::CallFrame, settlement_contract: eth::Address) -> bool {
static SETTLE_FUNCTION_SELECTOR: LazyLock<[u8; 4]> = LazyLock::new(|| {
let abi = &contracts::GPv2Settlement::raw_contract().interface.abi;
abi.function("settle").unwrap().selector()
});
trace.to == settlement_contract && trace.input.0.starts_with(&*SETTLE_FUNCTION_SELECTOR)
trace.to.unwrap_or_default() == settlement_contract
&& trace.input.0.starts_with(&*SETTLE_FUNCTION_SELECTOR)
}

/// Trade containing onchain observable data specific to a settlement
48 changes: 36 additions & 12 deletions crates/autopilot/src/infra/blockchain/mod.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
use {
self::contracts::Contracts,
crate::{boundary, domain::eth},
::serde::Deserialize,
chain::Chain,
ethcontract::dyns::DynWeb3,
ethrpc::block_stream::CurrentBlockWatcher,
primitive_types::U256,
std::time::Duration,
thiserror::Error,
url::Url,
web3::{types::Bytes, Transport},
};

pub mod contracts;
@@ -106,7 +108,15 @@ impl Ethereum {
let (transaction, receipt, traces) = tokio::try_join!(
self.web3.eth().transaction(hash.0.into()),
self.web3.eth().transaction_receipt(hash.0),
self.web3.trace().transaction(hash.0),
{
let hash = web3::helpers::serialize(&hash.0);
let tracing_options = serde_json::json!({ "tracer": "callTracer" });
web3::helpers::CallFuture::new(
self.web3
.transport()
.execute("debug_traceTransaction", vec![hash, tracing_options]),
)
}
)?;
let transaction = transaction.ok_or(Error::TransactionNotFound)?;
let receipt = receipt.ok_or(Error::TransactionNotFound)?;
@@ -130,7 +140,7 @@ impl Ethereum {
fn into_domain(
transaction: web3::types::Transaction,
receipt: web3::types::TransactionReceipt,
traces: Vec<web3::types::Trace>,
trace_calls: CallFrame,
timestamp: U256,
) -> anyhow::Result<eth::Transaction> {
Ok(eth::Transaction {
@@ -153,19 +163,33 @@ fn into_domain(
.ok_or(anyhow::anyhow!("missing effective_gas_price"))?
.into(),
timestamp: timestamp.as_u32(),
trace_calls: traces
.into_iter()
.filter_map(|trace| match trace.action {
web3::types::Action::Call(call) => Some(eth::TraceCall {
to: call.to.into(),
input: call.input.0.into(),
}),
_ => None,
})
.collect(),
trace_calls: trace_calls.into(),
})
}

/// Taken from alloy::rpc::types::trace::geth::CallFrame
#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
pub struct CallFrame {
Copy link
Contributor

Choose a reason for hiding this comment

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

This exact type exists twice. Once in the infra module and once in the domain module. I suspect we only need one of them.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't want to have deserialization specifics in the domain.

Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, I see. I wonder if this and the debug_trace logic might not be better placed in the ethrpc crate.
We might as well make this easily reusable with an extension trait. It's a bit awkward that the web3 crate does not support the debug module out of the box but AFAICS we could add a new namespace for debug which can have the trace_transaction functionality.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note that what I have right now in the code is not actually reusable in general. It's actually the pure minimum of code needed to satisfy the autopilot's needs. The reasons:
1.CallFrame only contains subset of data that is needed for functionality used in autopilot.
2. I use only one type of calltracer here.

In general, to fully implement debug_traceTransaction function so it could be reused with all of it's different input's and outputs would require a lot more code which would be a copy paste from alloy crate.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved the current impl code into ethrpc

/// The address of the contract that was called.
#[serde(default)]
pub to: Option<primitive_types::H160>,
/// Calldata input.
pub input: Bytes,
/// Recorded child calls.
#[serde(default)]
pub calls: Vec<CallFrame>,
}

impl From<CallFrame> for eth::CallFrame {
fn from(frame: CallFrame) -> Self {
eth::CallFrame {
to: frame.to.map(Into::into),
input: frame.input.0.into(),
calls: frame.calls.into_iter().map(Into::into).collect(),
}
}
}

#[derive(Debug, Error)]
pub enum Error {
#[error("web3 error: {0:?}")]