|
| 1 | +use alloy_consensus::Transaction; |
| 2 | +use alloy_eips::{BlockId, BlockNumberOrTag}; |
| 3 | +use alloy_primitives::{Address, B256}; |
| 4 | +use alloy_provider::Provider; |
| 5 | +use alloy_rpc_types::eth::Transaction as EthTransaction; |
| 6 | +use anyhow::{anyhow, Result}; |
| 7 | +use hana_blobstream::blobstream::{blobstream_address, SP1Blobstream}; |
| 8 | +use kona_rpc::SafeHeadResponse; |
| 9 | +use op_succinct_host_utils::fetcher::{OPSuccinctDataFetcher, RPCMode}; |
| 10 | + |
| 11 | +/// Extract the Celestia height from batcher transaction based on the version byte. |
| 12 | +/// |
| 13 | +/// Returns: |
| 14 | +/// - Some(height) if the transaction is a valid Celestia batcher transaction. |
| 15 | +/// - None if the transaction is an ETH DA transaction (EIP4844 transaction or non-EIP4844 |
| 16 | +/// transaction with version byte 0x00). |
| 17 | +/// - Err if the version byte is invalid, the commitment type is incorrect, or the da layer byte is |
| 18 | +/// incorrect for non-EIP4844 transactions. |
| 19 | +pub fn extract_celestia_height(tx: &EthTransaction) -> Result<Option<u64>> { |
| 20 | + // Skip calldata parsing for EIP4844 transactions since there is no calldata. |
| 21 | + if tx.inner.is_eip4844() { |
| 22 | + Ok(None) |
| 23 | + } else { |
| 24 | + let calldata = tx.input(); |
| 25 | + |
| 26 | + // Check minimum calldata length for version byte. |
| 27 | + if calldata.is_empty() { |
| 28 | + return Err(anyhow!("Calldata is empty, cannot extract version byte")); |
| 29 | + } |
| 30 | + |
| 31 | + // Check version byte to determine if it is ETH DA or Alt DA. |
| 32 | + // https://specs.optimism.io/protocol/derivation.html#batcher-transaction-format. |
| 33 | + match calldata[0] { |
| 34 | + 0x00 => Ok(None), // ETH DA transaction. |
| 35 | + 0x01 => { |
| 36 | + // Check minimum length for Celestia DA transaction: |
| 37 | + // [0] = version byte (0x01) |
| 38 | + // [1] = commitment type (0x01 for altda commitment) |
| 39 | + // [2] = da layer byte (0x0c for Celestia) |
| 40 | + // [3..11] = 8-byte Celestia height (little-endian) |
| 41 | + // [11..] = commitment data |
| 42 | + if calldata.len() < 11 { |
| 43 | + return Err(anyhow!( |
| 44 | + "Celestia batcher transaction too short: {} bytes, need at least 11", |
| 45 | + calldata.len() |
| 46 | + )); |
| 47 | + } |
| 48 | + |
| 49 | + // Check that the commitment type is altda (0x01). |
| 50 | + if calldata[1] != 0x01 { |
| 51 | + return Err(anyhow!( |
| 52 | + "Invalid commitment type for Celestia batcher transaction: expected 0x01, got 0x{:02x}", |
| 53 | + calldata[1] |
| 54 | + )); |
| 55 | + } |
| 56 | + |
| 57 | + // Check that the DA layer byte prefix is correct. |
| 58 | + // https://github.com/ethereum-optimism/specs/discussions/135. |
| 59 | + if calldata[2] != 0x0c { |
| 60 | + return Err(anyhow!("Invalid prefix for Celestia batcher transaction")); |
| 61 | + } |
| 62 | + |
| 63 | + // The encoding of the commitment is the Celestia block height followed |
| 64 | + // by the Celestia commitment. |
| 65 | + let height_bytes = &calldata[3..11]; |
| 66 | + let celestia_height = u64::from_le_bytes( |
| 67 | + height_bytes |
| 68 | + .try_into() |
| 69 | + .map_err(|_| anyhow!("Failed to convert height bytes to u64"))?, |
| 70 | + ); |
| 71 | + |
| 72 | + Ok(Some(celestia_height)) |
| 73 | + } |
| 74 | + _ => { |
| 75 | + Err(anyhow!("Invalid version byte for batcher transaction: 0x{:02x}", calldata[0])) |
| 76 | + } |
| 77 | + } |
| 78 | + } |
| 79 | +} |
| 80 | + |
| 81 | +/// Get the latest Celestia block height that has been committed to Ethereum via Blobstream. |
| 82 | +pub async fn get_latest_blobstream_celestia_block(fetcher: &OPSuccinctDataFetcher) -> Result<u64> { |
| 83 | + let blobstream_contract = SP1Blobstream::new( |
| 84 | + blobstream_address(fetcher.rollup_config.as_ref().unwrap().l1_chain_id) |
| 85 | + .expect("Failed to fetch blobstream contract address"), |
| 86 | + fetcher.l1_provider.clone(), |
| 87 | + ); |
| 88 | + |
| 89 | + let latest_celestia_block = blobstream_contract.latestBlock().call().await?; |
| 90 | + Ok(latest_celestia_block) |
| 91 | +} |
| 92 | + |
| 93 | +fn is_valid_batch_transaction( |
| 94 | + tx: &EthTransaction, |
| 95 | + batch_inbox_address: Address, |
| 96 | + batcher_address: Address, |
| 97 | +) -> Result<bool> { |
| 98 | + Ok(tx.to().is_some_and(|addr| addr == batch_inbox_address) && |
| 99 | + tx.inner.recover_signer().is_ok_and(|signer| signer == batcher_address)) |
| 100 | +} |
| 101 | + |
| 102 | +#[derive(Debug, Clone)] |
| 103 | +pub struct CelestiaL1SafeHead { |
| 104 | + pub l1_block_number: u64, |
| 105 | + pub l2_safe_head_number: u64, |
| 106 | +} |
| 107 | + |
| 108 | +impl CelestiaL1SafeHead { |
| 109 | + /// Get the L1 block hash for this safe head. |
| 110 | + pub async fn get_l1_hash(&self, fetcher: &OPSuccinctDataFetcher) -> Result<B256> { |
| 111 | + Ok(fetcher.get_l1_header(self.l1_block_number.into()).await?.hash_slow()) |
| 112 | + } |
| 113 | +} |
| 114 | + |
| 115 | +/// Find the latest safe L1 block with Celestia batches committed via Blobstream. |
| 116 | +/// Uses binary search to efficiently locate the highest L1 block containing batch transactions |
| 117 | +/// with Celestia heights that have been committed to Ethereum through Blobstream. |
| 118 | +pub async fn get_celestia_safe_head_info( |
| 119 | + fetcher: &OPSuccinctDataFetcher, |
| 120 | + l2_reference_block: u64, |
| 121 | +) -> Result<Option<CelestiaL1SafeHead>> { |
| 122 | + let rollup_config = |
| 123 | + fetcher.rollup_config.as_ref().ok_or_else(|| anyhow!("Rollup config not found"))?; |
| 124 | + |
| 125 | + let batch_inbox_address = rollup_config.batch_inbox_address; |
| 126 | + let batcher_address = rollup_config |
| 127 | + .genesis |
| 128 | + .system_config |
| 129 | + .as_ref() |
| 130 | + .ok_or_else(|| anyhow!("System config not found in genesis"))? |
| 131 | + .batcher_address; |
| 132 | + |
| 133 | + // Get the latest Celestia block committed via Blobstream. |
| 134 | + let latest_committed_celestia_block = get_latest_blobstream_celestia_block(fetcher).await?; |
| 135 | + |
| 136 | + // Get the L1 block range to search. |
| 137 | + let mut low = fetcher.get_safe_l1_block_for_l2_block(l2_reference_block).await?.1; |
| 138 | + let mut high = fetcher.get_l1_header(BlockId::finalized()).await?.number; |
| 139 | + let mut result = None; |
| 140 | + |
| 141 | + while low <= high { |
| 142 | + let current_l1_block = low + (high - low) / 2; |
| 143 | + let l1_block_hex = format!("0x{current_l1_block:x}"); |
| 144 | + |
| 145 | + let safe_head_response: SafeHeadResponse = fetcher |
| 146 | + .fetch_rpc_data_with_mode( |
| 147 | + RPCMode::L2Node, |
| 148 | + "optimism_safeHeadAtL1Block", |
| 149 | + vec![l1_block_hex.into()], |
| 150 | + ) |
| 151 | + .await?; |
| 152 | + |
| 153 | + let block = fetcher |
| 154 | + .l1_provider |
| 155 | + .get_block_by_number(BlockNumberOrTag::Number(safe_head_response.l1_block.number)) |
| 156 | + .full() |
| 157 | + .await? |
| 158 | + .ok_or_else(|| anyhow!("Block {} not found", safe_head_response.l1_block.number))?; |
| 159 | + |
| 160 | + let mut found_valid_batch = false; |
| 161 | + for tx in block.transactions.txns() { |
| 162 | + if is_valid_batch_transaction(tx, batch_inbox_address, batcher_address)? { |
| 163 | + match extract_celestia_height(tx)? { |
| 164 | + None => { |
| 165 | + // ETH DA transaction - always valid. |
| 166 | + found_valid_batch = true; |
| 167 | + result = Some(CelestiaL1SafeHead { |
| 168 | + l1_block_number: current_l1_block, |
| 169 | + l2_safe_head_number: safe_head_response.safe_head.number, |
| 170 | + }); |
| 171 | + break; |
| 172 | + } |
| 173 | + Some(celestia_height) => { |
| 174 | + if celestia_height <= latest_committed_celestia_block { |
| 175 | + found_valid_batch = true; |
| 176 | + result = Some(CelestiaL1SafeHead { |
| 177 | + l1_block_number: current_l1_block, |
| 178 | + l2_safe_head_number: safe_head_response.safe_head.number, |
| 179 | + }); |
| 180 | + break; |
| 181 | + } |
| 182 | + } |
| 183 | + } |
| 184 | + } |
| 185 | + } |
| 186 | + |
| 187 | + if found_valid_batch { |
| 188 | + low = current_l1_block + 1; |
| 189 | + } else { |
| 190 | + high = current_l1_block - 1; |
| 191 | + } |
| 192 | + } |
| 193 | + |
| 194 | + Ok(result) |
| 195 | +} |
0 commit comments