Skip to content

Commit 24368d4

Browse files
authored
chore: bump hana (#512)
* chore: bump hana * feat: choose younger l1 head * feat: even younger l1 head * refactor: refac l1 head selection (#518) * refactor: refac l1 head selection * chore: make cargo fmt happy * refac: make safe_db_fallback required * refac: make l1_head_hash required * refac: remove _safe_db_fallback * chore: clean up * docs: reset troubleshooting.md * chore: use tag for hana * chore: fix typo * feat: add batcher address check * refac * refac: make extract_celestia_height not panic * chore: add . * chore: update celestia elf
1 parent d8769ca commit 24368d4

File tree

17 files changed

+310
-209
lines changed

17 files changed

+310
-209
lines changed

Cargo.lock

Lines changed: 8 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,10 @@ kona-registry = { git = "https://github.com/op-rs/kona", tag = "kona-client/v1.0
8080
kona-genesis = { git = "https://github.com/op-rs/kona", tag = "kona-client/v1.0.1", default-features = false }
8181

8282
# hana
83-
hana-blobstream = { git = "https://github.com/celestiaorg/hana", rev = "a6077b794d1c00fc2af129fb28cbb0151f8c81b9" }
84-
hana-celestia = { git = "https://github.com/celestiaorg/hana", rev = "a6077b794d1c00fc2af129fb28cbb0151f8c81b9" }
85-
hana-host = { git = "https://github.com/celestiaorg/hana", rev = "a6077b794d1c00fc2af129fb28cbb0151f8c81b9" }
86-
hana-oracle = { git = "https://github.com/celestiaorg/hana", rev = "a6077b794d1c00fc2af129fb28cbb0151f8c81b9" }
83+
hana-blobstream = { git = "https://github.com/celestiaorg/hana", rev = "904f086fd7335986e87a0f0432b1f07fe997f690" }
84+
hana-celestia = { git = "https://github.com/celestiaorg/hana", rev = "904f086fd7335986e87a0f0432b1f07fe997f690" }
85+
hana-host = { git = "https://github.com/celestiaorg/hana", rev = "904f086fd7335986e87a0f0432b1f07fe997f690" }
86+
hana-oracle = { git = "https://github.com/celestiaorg/hana", rev = "904f086fd7335986e87a0f0432b1f07fe997f690" }
8787

8888
# op-succinct
8989
op-succinct-prove = { path = "scripts/prove" }

book/troubleshooting.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ Failed to validate L2 block #<block_number> with output root <hash>
2828
```
2929

3030
**Cause:**
31-
This error occurs when the L1 head block selected is too close to the batch posting block, causing the derivation process to fail. The L2 node may have an inconsistent view of the safe head state, requiring additional L1 blocks to properly derive and validate the L2 blocks.
31+
This error occurs when the L1 head block selected for ETH DA is too close to the batch posting block, causing the derivation process to fail. The L2 node may have an inconsistent view of the safe head state, requiring additional L1 blocks to properly derive and validate the L2 blocks.
3232

3333
**Solution:**
3434
1. Increase the L1 head offset buffer in the derivation process. The code currently adds a buffer of 20 blocks after the batch posting block, but you can increase this to for example 100 blocks:

elf/celestia-range-elf-embedded

15.7 KB
Binary file not shown.

fault-proof/src/proposer.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ where
123123
l2_block_number.to::<u64>() - self.config.proposal_interval_in_blocks,
124124
l2_block_number.to::<u64>(),
125125
Some(l1_head_hash.into()),
126-
Some(self.config.safe_db_fallback),
126+
self.config.safe_db_fallback,
127127
)
128128
.await
129129
.context("Failed to get host CLI args")?;

scripts/prove/bin/multi.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,7 @@ async fn main() -> Result<()> {
2828
get_validated_block_range(&data_fetcher, args.start, args.end, DEFAULT_RANGE).await?;
2929

3030
let host = initialize_host(Arc::new(data_fetcher.clone()));
31-
let host_args =
32-
host.fetch(l2_start_block, l2_end_block, None, Some(args.safe_db_fallback)).await?;
31+
let host_args = host.fetch(l2_start_block, l2_end_block, None, args.safe_db_fallback).await?;
3332

3433
debug!("Host args: {:?}", host_args);
3534

scripts/prove/tests/cycle_count_diff.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ async fn test_cycle_count_diff() -> Result<()> {
125125
}
126126
};
127127

128-
let host_args = host.fetch(l2_start_block, l2_end_block, None, Some(false)).await?;
128+
let host_args = host.fetch(l2_start_block, l2_end_block, None, false).await?;
129129

130130
let witness_data = host.run(&host_args).await?;
131131
let sp1_stdin = host.witness_generator().get_sp1_stdin(witness_data)?;

scripts/prove/tests/multi.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ async fn execute_batch() -> Result<()> {
2626

2727
let host = initialize_host(Arc::new(data_fetcher.clone()));
2828

29-
let host_args = host.fetch(l2_start_block, l2_end_block, None, Some(false)).await?;
29+
let host_args = host.fetch(l2_start_block, l2_end_block, None, false).await?;
3030

3131
let witness_data = host.run(&host_args).await?;
3232

scripts/utils/bin/cost_estimator.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ async fn main() -> Result<()> {
217217

218218
let host_args = futures::stream::iter(split_ranges.iter())
219219
.map(|range| async {
220-
host.fetch(range.start, range.end, None, Some(args.safe_db_fallback))
220+
host.fetch(range.start, range.end, None, args.safe_db_fallback)
221221
.await
222222
.expect("Failed to get host CLI args")
223223
})

scripts/utils/bin/gen_sp1_test_artifacts.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ async fn main() -> Result<()> {
3838
let host = Arc::new(initialize_host(Arc::new(data_fetcher)));
3939
let host_args = futures::stream::iter(split_ranges.iter())
4040
.map(|range| async {
41-
host.fetch(range.start, range.end, None, Some(args.safe_db_fallback))
41+
host.fetch(range.start, range.end, None, args.safe_db_fallback)
4242
.await
4343
.expect("Failed to get host CLI args")
4444
})
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
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

Comments
 (0)