Skip to main content

df_elysium_core/bridge/
claim.rs

1//! Claiming a withdrawal on HyperEVM. The rollup (BoLD) confirms assertions about Elysium's state;
2//! each names an Elysium block, and that block's send count and send root are the outbox's tree up
3//! to it. A withdrawal at a position under the latest confirmed send count is executed on the
4//! outbox with a proof against that root, built by the NodeInterface on Elysium.
5//!
6//! [`claim`] does the whole read: the latest confirmed assertion, the block it names, the proof,
7//! and the call that executes the withdrawal, or why it cannot yet. Anyone may send that call,
8//! from any address with HYPE on the parent chain for its gas; the HYPE goes to the withdrawal's
9//! destination, and a leaf is executed once.
10
11use 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
35/// An assertion made: its state after, whose global state names an Elysium block and send root.
36pub 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/// The latest assertion the rollup has confirmed.
60#[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/// Where an assertion stands.
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
67pub enum AssertionStatus {
68    NoAssertion,
69    Pending,
70    Confirmed,
71    Other(u8),
72}
73
74/// An assertion as the rollup keeps it.
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76pub struct Assertion {
77    pub first_child_block: u64,
78    pub second_child_block: u64,
79    /// The parent block it was created in: where its `AssertionCreated` log is.
80    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/// Parent blocks an assertion must wait, unchallenged, to be confirmed.
108#[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/// Whether the outbox has executed the leaf at `position`.
116#[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/// The Elysium block a confirmed send root is from; zero for a root the outbox does not know.
122#[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/// Executes `withdrawal` on the outbox with `proof` ([`construct_outbox_proof`]).
128#[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/// What a confirmed assertion says of Elysium.
145#[derive(Debug, Clone, Copy, PartialEq, Eq)]
146pub struct Confirmed {
147    pub assertion: B256,
148    /// The Elysium block it confirms up to.
149    pub block_hash: B256,
150    /// The outbox's root at that block.
151    pub send_root: B256,
152}
153
154/// The filter that finds an assertion's `AssertionCreated` log: the rollup's, at the block it was
155/// created in. One block, inside HyperEVM's 50-block window.
156#[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
167/// The Elysium block and send root an `AssertionCreated` log names in its state after.
168pub 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/// Why a claim could not be read.
195#[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/// Where a withdrawal stands on the parent chain.
212#[derive(Debug, Clone, PartialEq, Eq)]
213pub enum Claim {
214    /// Executed already: the HYPE was paid.
215    Spent,
216    /// Not under a confirmed assertion yet: the latest confirms the first `confirmed` leaves.
217    Waiting { confirmed: u64 },
218    /// Ready: send this call on the parent chain.
219    Ready { call: Call, confirmed: Confirmed },
220}
221
222/// Where `withdrawal` stands, read from Elysium through `elysium` and from HyperEVM through
223/// `parent`, and the call that claims it once it can be.
224pub 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
257/// The latest confirmed assertion and what it says, read from the parent chain.
258pub 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;