1use df_hyperliquid_core::client::Http;
12use df_hyperliquid_core::client::evm::abi::{AbiError, Token};
13
14use crate::bridge::Contracts;
15use crate::bridge::withdraw::Withdrawal;
16use crate::client::read::{Fields, one, one_word};
17use crate::client::rpc::{BlockTag, LogFilter, Rpc, RpcError};
18use crate::client::{Arg, BadLog, Call, Client, Event, Log, Method, Read, ReadError};
19use crate::nitro::node::construct_outbox_proof;
20use crate::{B256, U256};
21
22pub const LATEST_CONFIRMED: Method = Method::new("latestConfirmed()", "bytes32");
23pub const GET_ASSERTION: Method = Method::new(
24 "getAssertion(bytes32)",
25 "(uint64 firstChildBlock, uint64 secondChildBlock, uint64 createdAtBlock, bool isFirstChild, uint8 status, \
26 bytes32 configHash)",
27);
28pub const CONFIRM_PERIOD_BLOCKS: Method = Method::new("confirmPeriodBlocks()", "uint64");
29
30pub const EXECUTE_TRANSACTION: Method =
31 Method::new("executeTransaction(bytes32[],uint256,address,address,uint256,uint256,uint256,uint256,bytes)", "");
32pub const IS_SPENT: Method = Method::new("isSpent(uint256)", "bool");
33pub const ROOTS: Method = Method::new("roots(bytes32)", "bytes32");
34
35pub const ASSERTION_CREATED: Event = Event::new(
37 "AssertionCreated(bytes32,bytes32,((bytes32,bytes32,(bytes32,uint256,address,uint64,uint64)),\
38 ((bytes32[2],uint64[2]),uint8,bytes32),((bytes32[2],uint64[2]),uint8,bytes32)),bytes32,uint256,bytes32,\
39 uint256,address,uint64)",
40 "bytes32 indexed assertionHash, bytes32 indexed parentAssertionHash, \
41 ((bytes32 prevPrevAssertionHash, bytes32 sequencerBatchAcc, (bytes32 wasmModuleRoot, uint256 requiredStake, \
42 address challengeManager, uint64 confirmPeriodBlocks, uint64 nextInboxPosition) configData) beforeStateData, \
43 ((bytes32[2] bytes32Vals, uint64[2] u64Vals) globalState, uint8 machineStatus, bytes32 endHistoryRoot) \
44 beforeState, ((bytes32[2] bytes32Vals, uint64[2] u64Vals) globalState, uint8 machineStatus, \
45 bytes32 endHistoryRoot) afterState) assertion, bytes32 afterInboxBatchAcc, uint256 inboxMaxCount, \
46 bytes32 wasmModuleRoot, uint256 requiredStake, address challengeManager, uint64 confirmPeriodBlocks",
47);
48pub const ASSERTION_CONFIRMED: Event = Event::new(
49 "AssertionConfirmed(bytes32,bytes32,bytes32)",
50 "bytes32 indexed assertionHash, bytes32 blockHash, bytes32 sendRoot",
51);
52pub const OUT_BOX_TRANSACTION_EXECUTED: Event = Event::new(
53 "OutBoxTransactionExecuted(address,address,uint256,uint256)",
54 "address indexed to, address indexed l2Sender, uint256 indexed zero, uint256 transactionIndex",
55);
56pub const SEND_ROOT_UPDATED: Event =
57 Event::new("SendRootUpdated(bytes32,bytes32)", "bytes32 indexed outputRoot, bytes32 indexed l2BlockHash");
58
59#[must_use]
61pub fn latest_confirmed(contracts: &Contracts) -> Read<B256> {
62 Read::new(contracts.rollup, &LATEST_CONFIRMED, &[], |answer| one_word(&LATEST_CONFIRMED, answer))
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
67pub enum AssertionStatus {
68 NoAssertion,
69 Pending,
70 Confirmed,
71 Other(u8),
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76pub struct Assertion {
77 pub first_child_block: u64,
78 pub second_child_block: u64,
79 pub created_at_block: u64,
81 pub is_first_child: bool,
82 pub status: AssertionStatus,
83 pub config_hash: B256,
84}
85
86#[must_use]
87pub fn assertion(contracts: &Contracts, hash: B256) -> Read<Assertion> {
88 Read::new(contracts.rollup, &GET_ASSERTION, &[Arg::Word(hash)], |answer| {
89 let tokens = GET_ASSERTION.answer(answer)?;
90 let mut fields = Fields::tuple("getAssertion", &tokens)?;
91 Ok(Assertion {
92 first_child_block: fields.u64()?,
93 second_child_block: fields.u64()?,
94 created_at_block: fields.u64()?,
95 is_first_child: fields.bool()?,
96 status: match fields.u8()? {
97 0 => AssertionStatus::NoAssertion,
98 1 => AssertionStatus::Pending,
99 2 => AssertionStatus::Confirmed,
100 other => AssertionStatus::Other(other),
101 },
102 config_hash: fields.word()?,
103 })
104 })
105}
106
107#[must_use]
109pub fn confirm_period_blocks(contracts: &Contracts) -> Read<u64> {
110 Read::new(contracts.rollup, &CONFIRM_PERIOD_BLOCKS, &[], |answer| {
111 one(&CONFIRM_PERIOD_BLOCKS, answer, Token::as_u64)
112 })
113}
114
115#[must_use]
117pub fn is_spent(contracts: &Contracts, position: u64) -> Read<bool> {
118 Read::new(contracts.outbox, &IS_SPENT, &[Arg::u64(position)], |answer| one(&IS_SPENT, answer, Token::as_bool))
119}
120
121#[must_use]
123pub fn root(contracts: &Contracts, send_root: B256) -> Read<B256> {
124 Read::new(contracts.outbox, &ROOTS, &[Arg::Word(send_root)], |answer| one_word(&ROOTS, answer))
125}
126
127#[must_use]
129pub fn execute(contracts: &Contracts, withdrawal: &Withdrawal, proof: &[B256]) -> Call {
130 let data = EXECUTE_TRANSACTION.calldata(&[
131 Arg::Words(proof),
132 Arg::u64(withdrawal.position),
133 Arg::Address(withdrawal.caller),
134 Arg::Address(withdrawal.destination),
135 Arg::u64(withdrawal.arb_block),
136 Arg::u64(withdrawal.eth_block),
137 Arg::u64(withdrawal.timestamp),
138 Arg::Uint(withdrawal.value),
139 Arg::Bytes(&withdrawal.data),
140 ]);
141 Call { to: Some(contracts.outbox), value: U256::ZERO, data }
142}
143
144#[derive(Debug, Clone, Copy, PartialEq, Eq)]
146pub struct Confirmed {
147 pub assertion: B256,
148 pub block_hash: B256,
150 pub send_root: B256,
152}
153
154#[must_use]
157pub fn assertion_created_filter(contracts: &Contracts, hash: B256, created_at_block: u64) -> LogFilter {
158 LogFilter {
159 from_block: Some(BlockTag::Number(created_at_block)),
160 to_block: Some(BlockTag::Number(created_at_block)),
161 block_hash: None,
162 address: vec![contracts.rollup],
163 topics: vec![Some(vec![ASSERTION_CREATED.topic()]), Some(vec![hash])],
164 }
165}
166
167pub fn read_assertion_created(log: &Log) -> Result<Confirmed, BadLog> {
169 let tokens = ASSERTION_CREATED.decode(log)?;
170 let misfit =
171 || BadLog::Abi { event: "AssertionCreated", error: AbiError::Type("not the declared state".to_owned()) };
172 let word = |token: &Token| token.as_bytes().and_then(|bytes| <[u8; 32]>::try_from(bytes).ok()).map(B256);
173 let assertion = tokens.first().and_then(word).ok_or_else(misfit)?;
174 let words = tokens
175 .get(2)
176 .and_then(Token::as_items)
177 .and_then(|inputs| inputs.get(2))
178 .and_then(Token::as_items)
179 .and_then(|after| after.first())
180 .and_then(Token::as_items)
181 .and_then(|global| global.first())
182 .and_then(Token::as_items)
183 .ok_or_else(misfit)?;
184 match words {
185 [block_hash, send_root] => Ok(Confirmed {
186 assertion,
187 block_hash: word(block_hash).ok_or_else(misfit)?,
188 send_root: word(send_root).ok_or_else(misfit)?,
189 }),
190 _ => Err(misfit()),
191 }
192}
193
194#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
196pub enum ClaimError {
197 #[error(transparent)]
198 Read(#[from] ReadError),
199 #[error(transparent)]
200 Rpc(#[from] RpcError),
201 #[error(transparent)]
202 Log(#[from] BadLog),
203 #[error("no AssertionCreated log for {assertion} at parent block {block}")]
204 NoAssertionLog { assertion: B256, block: u64 },
205 #[error("Elysium has no block {0}, which a confirmed assertion names")]
206 UnknownBlock(B256),
207 #[error("the {what} disagree: {left} and {right}")]
208 Mismatch { what: &'static str, left: B256, right: B256 },
209}
210
211#[derive(Debug, Clone, PartialEq, Eq)]
213pub enum Claim {
214 Spent,
216 Waiting { confirmed: u64 },
218 Ready { call: Call, confirmed: Confirmed },
220}
221
222pub async fn claim<H: Http, S, P: Http>(
225 elysium: &Client<H, S>,
226 parent: &Rpc<P>,
227 withdrawal: &Withdrawal,
228) -> Result<Claim, ClaimError> {
229 let contracts = elysium.network().contracts();
230 if is_spent(&contracts, withdrawal.position).on(parent, BlockTag::Latest).await? {
231 return Ok(Claim::Spent);
232 }
233 let confirmed = latest(&contracts, parent).await?;
234 let block = elysium.block_by_hash(confirmed.block_hash, false).await?;
235 let block = block.ok_or(ClaimError::UnknownBlock(confirmed.block_hash))?;
236 if block.send_root != confirmed.send_root {
237 return Err(ClaimError::Mismatch {
238 what: "assertion's and block's send roots",
239 left: confirmed.send_root,
240 right: block.send_root,
241 });
242 }
243 if withdrawal.position >= block.send_count {
244 return Ok(Claim::Waiting { confirmed: block.send_count });
245 }
246 let proof = elysium.read(&construct_outbox_proof(block.send_count, withdrawal.position)).await?;
247 if proof.root != confirmed.send_root {
248 return Err(ClaimError::Mismatch {
249 what: "proof's and assertion's roots",
250 left: proof.root,
251 right: confirmed.send_root,
252 });
253 }
254 Ok(Claim::Ready { call: execute(&contracts, withdrawal, &proof.proof), confirmed })
255}
256
257pub async fn latest<P: Http>(contracts: &Contracts, parent: &Rpc<P>) -> Result<Confirmed, ClaimError> {
259 let hash = latest_confirmed(contracts).on(parent, BlockTag::Latest).await?;
260 let created = assertion(contracts, hash).on(parent, BlockTag::Latest).await?.created_at_block;
261 let logs = parent.logs(&assertion_created_filter(contracts, hash, created)).await?;
262 let log = logs.iter().find(|log| ASSERTION_CREATED.matches(log) && log.topics.get(1) == Some(&hash));
263 Ok(read_assertion_created(log.ok_or(ClaimError::NoAssertionLog { assertion: hash, block: created })?)?)
264}
265
266#[cfg(test)]
267#[path = "tests/claim.rs"]
268mod tests;