kona_protocol/
utils.rs

1//! Utility methods used by protocol types.
2
3use alloc::vec::Vec;
4use alloy_consensus::{Transaction, TxType, Typed2718};
5use alloy_primitives::B256;
6use alloy_rlp::{Buf, Header};
7use kona_genesis::{RollupConfig, SystemConfig};
8use op_alloy_consensus::OpBlock;
9
10use crate::{
11    L1BlockInfoBedrock, L1BlockInfoEcotone, L1BlockInfoInterop, L1BlockInfoIsthmus, L1BlockInfoTx,
12    OpBlockConversionError, SpanBatchError, SpanDecodingError,
13};
14
15/// Returns if the given `value` is a deposit transaction.
16pub fn starts_with_2718_deposit<B>(value: &B) -> bool
17where
18    B: AsRef<[u8]>,
19{
20    value.as_ref().first() == Some(&0x7E)
21}
22
23/// Returns if the given `value` is a [`TxType::Eip7702`] transaction.
24pub fn starts_with_7702_tx<B>(value: &B) -> bool
25where
26    B: AsRef<[u8]>,
27{
28    value.as_ref().first() == Some(&(TxType::Eip7702 as u8))
29}
30
31/// Converts the [OpBlock] to a partial [SystemConfig].
32pub fn to_system_config(
33    block: &OpBlock,
34    rollup_config: &RollupConfig,
35) -> Result<SystemConfig, OpBlockConversionError> {
36    if block.header.number == rollup_config.genesis.l2.number {
37        if block.header.hash_slow() != rollup_config.genesis.l2.hash {
38            return Err(OpBlockConversionError::InvalidGenesisHash(
39                rollup_config.genesis.l2.hash,
40                block.header.hash_slow(),
41            ));
42        }
43        return rollup_config
44            .genesis
45            .system_config
46            .ok_or(OpBlockConversionError::MissingSystemConfigGenesis);
47    }
48
49    if block.body.transactions.is_empty() {
50        return Err(OpBlockConversionError::EmptyTransactions(block.header.hash_slow()));
51    }
52    let Some(tx) = block.body.transactions[0].as_deposit() else {
53        return Err(OpBlockConversionError::InvalidTxType(block.body.transactions[0].ty()));
54    };
55
56    let l1_info = L1BlockInfoTx::decode_calldata(tx.input().as_ref())?;
57    let l1_fee_scalar = match l1_info {
58        L1BlockInfoTx::Bedrock(L1BlockInfoBedrock { l1_fee_scalar, .. }) => l1_fee_scalar,
59        L1BlockInfoTx::Ecotone(L1BlockInfoEcotone {
60            base_fee_scalar,
61            blob_base_fee_scalar,
62            ..
63        }) |
64        L1BlockInfoTx::Interop(L1BlockInfoInterop {
65            base_fee_scalar,
66            blob_base_fee_scalar,
67            ..
68        }) |
69        L1BlockInfoTx::Isthmus(L1BlockInfoIsthmus {
70            base_fee_scalar,
71            blob_base_fee_scalar,
72            ..
73        }) => {
74            // Translate Ecotone values back into encoded scalar if needed.
75            // We do not know if it was derived from a v0 or v1 scalar,
76            // but v1 is fine, a 0 blob base fee has the same effect.
77            let mut buf = B256::ZERO;
78            buf[0] = 0x01;
79            buf[24..28].copy_from_slice(blob_base_fee_scalar.to_be_bytes().as_ref());
80            buf[28..32].copy_from_slice(base_fee_scalar.to_be_bytes().as_ref());
81            buf.into()
82        }
83    };
84
85    let mut cfg = SystemConfig {
86        batcher_address: l1_info.batcher_address(),
87        overhead: l1_info.l1_fee_overhead(),
88        scalar: l1_fee_scalar,
89        gas_limit: block.header.gas_limit,
90        ..Default::default()
91    };
92
93    // After holocene's activation, the EIP-1559 parameters are stored in the block header's nonce.
94    if rollup_config.is_holocene_active(block.header.timestamp) {
95        let eip1559_params = &block.header.extra_data;
96
97        if eip1559_params.len() != 9 {
98            return Err(OpBlockConversionError::Eip1559DecodeError);
99        }
100        if eip1559_params[0] != 0 {
101            return Err(OpBlockConversionError::Eip1559DecodeError);
102        }
103
104        cfg.eip1559_denominator = Some(u32::from_be_bytes(
105            eip1559_params[1..5]
106                .try_into()
107                .map_err(|_| OpBlockConversionError::Eip1559DecodeError)?,
108        ));
109        cfg.eip1559_elasticity = Some(u32::from_be_bytes(
110            eip1559_params[5..9]
111                .try_into()
112                .map_err(|_| OpBlockConversionError::Eip1559DecodeError)?,
113        ));
114    }
115
116    if rollup_config.is_isthmus_active(block.header.timestamp) {
117        cfg.operator_fee_scalar = Some(l1_info.operator_fee_scalar());
118        cfg.operator_fee_constant = Some(l1_info.operator_fee_constant());
119    }
120
121    Ok(cfg)
122}
123
124/// Reads transaction data from a reader.
125pub fn read_tx_data(r: &mut &[u8]) -> Result<(Vec<u8>, TxType), SpanBatchError> {
126    let mut tx_data = Vec::new();
127    let first_byte =
128        *r.first().ok_or(SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionData))?;
129    let mut tx_type = 0;
130    if first_byte <= 0x7F {
131        // EIP-2718: Non-legacy tx, so write tx type
132        tx_type = first_byte;
133        tx_data.push(tx_type);
134        r.advance(1);
135    }
136
137    // Read the RLP header with a different reader pointer. This prevents the initial pointer from
138    // being advanced in the case that what we read is invalid.
139    let rlp_header = Header::decode(&mut (**r).as_ref())
140        .map_err(|_| SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionData))?;
141
142    let tx_payload = if rlp_header.list {
143        // Grab the raw RLP for the transaction data from `r`. It was unaffected since we copied it.
144        let payload_length_with_header = rlp_header.payload_length + rlp_header.length();
145        let payload = r[0..payload_length_with_header].to_vec();
146        r.advance(payload_length_with_header);
147        Ok(payload)
148    } else {
149        Err(SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionData))
150    }?;
151    tx_data.extend_from_slice(&tx_payload);
152
153    Ok((
154        tx_data,
155        tx_type
156            .try_into()
157            .map_err(|_| SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionType))?,
158    ))
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164    use crate::test_utils::{RAW_BEDROCK_INFO_TX, RAW_ECOTONE_INFO_TX, RAW_ISTHMUS_INFO_TX};
165    use alloc::vec;
166    use alloy_eips::eip1898::BlockNumHash;
167    use alloy_primitives::{U256, address, bytes, uint};
168    use kona_genesis::{ChainGenesis, HardForkConfig};
169
170    #[test]
171    fn test_to_system_config_invalid_genesis_hash() {
172        let block = OpBlock::default();
173        let rollup_config = RollupConfig::default();
174        let err = to_system_config(&block, &rollup_config).unwrap_err();
175        assert_eq!(
176            err,
177            OpBlockConversionError::InvalidGenesisHash(
178                rollup_config.genesis.l2.hash,
179                block.header.hash_slow(),
180            )
181        );
182    }
183
184    #[test]
185    fn test_to_system_config_missing_system_config_genesis() {
186        let block = OpBlock::default();
187        let block_hash = block.header.hash_slow();
188        let rollup_config = RollupConfig {
189            genesis: ChainGenesis {
190                l2: BlockNumHash { hash: block_hash, ..Default::default() },
191                ..Default::default()
192            },
193            ..Default::default()
194        };
195        let err = to_system_config(&block, &rollup_config).unwrap_err();
196        assert_eq!(err, OpBlockConversionError::MissingSystemConfigGenesis);
197    }
198
199    #[test]
200    fn test_to_system_config_from_genesis() {
201        let block = OpBlock::default();
202        let block_hash = block.header.hash_slow();
203        let rollup_config = RollupConfig {
204            genesis: ChainGenesis {
205                l2: BlockNumHash { hash: block_hash, ..Default::default() },
206                system_config: Some(SystemConfig::default()),
207                ..Default::default()
208            },
209            ..Default::default()
210        };
211        let config = to_system_config(&block, &rollup_config).unwrap();
212        assert_eq!(config, SystemConfig::default());
213    }
214
215    #[test]
216    fn test_to_system_config_empty_txs() {
217        let block = OpBlock {
218            header: alloy_consensus::Header { number: 1, ..Default::default() },
219            ..Default::default()
220        };
221        let block_hash = block.header.hash_slow();
222        let rollup_config = RollupConfig {
223            genesis: ChainGenesis {
224                l2: BlockNumHash { hash: block_hash, ..Default::default() },
225                ..Default::default()
226            },
227            ..Default::default()
228        };
229        let err = to_system_config(&block, &rollup_config).unwrap_err();
230        assert_eq!(err, OpBlockConversionError::EmptyTransactions(block_hash));
231    }
232
233    #[test]
234    fn test_to_system_config_non_deposit() {
235        let block = OpBlock {
236            header: alloy_consensus::Header { number: 1, ..Default::default() },
237            body: alloy_consensus::BlockBody {
238                transactions: vec![op_alloy_consensus::OpTxEnvelope::Legacy(
239                    alloy_consensus::Signed::new_unchecked(
240                        alloy_consensus::TxLegacy {
241                            chain_id: Some(1),
242                            nonce: 1,
243                            gas_price: 1,
244                            gas_limit: 1,
245                            to: alloy_primitives::TxKind::Create,
246                            value: U256::ZERO,
247                            input: alloy_primitives::Bytes::new(),
248                        },
249                        alloy_primitives::PrimitiveSignature::new(U256::ZERO, U256::ZERO, false),
250                        Default::default(),
251                    ),
252                )],
253                ..Default::default()
254            },
255        };
256        let block_hash = block.header.hash_slow();
257        let rollup_config = RollupConfig {
258            genesis: ChainGenesis {
259                l2: BlockNumHash { hash: block_hash, ..Default::default() },
260                ..Default::default()
261            },
262            ..Default::default()
263        };
264        let err = to_system_config(&block, &rollup_config).unwrap_err();
265        assert_eq!(err, OpBlockConversionError::InvalidTxType(0));
266    }
267
268    #[test]
269    fn test_constructs_bedrock_system_config() {
270        let block = OpBlock {
271            header: alloy_consensus::Header { number: 1, ..Default::default() },
272            body: alloy_consensus::BlockBody {
273                transactions: vec![op_alloy_consensus::OpTxEnvelope::Deposit(
274                    alloy_primitives::Sealed::new(op_alloy_consensus::TxDeposit {
275                        input: alloy_primitives::Bytes::from(&RAW_BEDROCK_INFO_TX),
276                        ..Default::default()
277                    }),
278                )],
279                ..Default::default()
280            },
281        };
282        let block_hash = block.header.hash_slow();
283        let rollup_config = RollupConfig {
284            genesis: ChainGenesis {
285                l2: BlockNumHash { hash: block_hash, ..Default::default() },
286                ..Default::default()
287            },
288            ..Default::default()
289        };
290        let config = to_system_config(&block, &rollup_config).unwrap();
291        let expected = SystemConfig {
292            batcher_address: address!("6887246668a3b87f54deb3b94ba47a6f63f32985"),
293            overhead: uint!(188_U256),
294            scalar: uint!(684000_U256),
295            gas_limit: 0,
296            base_fee_scalar: None,
297            blob_base_fee_scalar: None,
298            eip1559_denominator: None,
299            eip1559_elasticity: None,
300            operator_fee_scalar: None,
301            operator_fee_constant: None,
302        };
303        assert_eq!(config, expected);
304    }
305
306    #[test]
307    fn test_constructs_ecotone_system_config() {
308        let block = OpBlock {
309            header: alloy_consensus::Header {
310                number: 1,
311                // Holocene EIP1559 parameters stored in the extra data.
312                extra_data: bytes!("000000beef0000babe"),
313                ..Default::default()
314            },
315            body: alloy_consensus::BlockBody {
316                transactions: vec![op_alloy_consensus::OpTxEnvelope::Deposit(
317                    alloy_primitives::Sealed::new(op_alloy_consensus::TxDeposit {
318                        input: alloy_primitives::Bytes::from(&RAW_ECOTONE_INFO_TX),
319                        ..Default::default()
320                    }),
321                )],
322                ..Default::default()
323            },
324        };
325        let block_hash = block.header.hash_slow();
326        let rollup_config = RollupConfig {
327            genesis: ChainGenesis {
328                l2: BlockNumHash { hash: block_hash, ..Default::default() },
329                ..Default::default()
330            },
331            hardforks: HardForkConfig { holocene_time: Some(0), ..Default::default() },
332            ..Default::default()
333        };
334        assert!(rollup_config.is_holocene_active(block.header.timestamp));
335        let config = to_system_config(&block, &rollup_config).unwrap();
336        let expected = SystemConfig {
337            batcher_address: address!("6887246668a3b87f54deb3b94ba47a6f63f32985"),
338            overhead: U256::ZERO,
339            scalar: uint!(
340                452312848583266388373324160190187140051835877600158453279134670530344387928_U256
341            ),
342            gas_limit: 0,
343            base_fee_scalar: None,
344            blob_base_fee_scalar: None,
345            eip1559_denominator: Some(0xbeef),
346            eip1559_elasticity: Some(0xbabe),
347            operator_fee_scalar: None,
348            operator_fee_constant: None,
349        };
350        assert_eq!(config, expected);
351    }
352
353    #[test]
354    fn test_constructs_isthmus_system_config() {
355        let block = OpBlock {
356            header: alloy_consensus::Header {
357                number: 1,
358                // Holocene EIP1559 parameters stored in the extra data.
359                extra_data: bytes!("000000beef0000babe"),
360                ..Default::default()
361            },
362            body: alloy_consensus::BlockBody {
363                transactions: vec![op_alloy_consensus::OpTxEnvelope::Deposit(
364                    alloy_primitives::Sealed::new(op_alloy_consensus::TxDeposit {
365                        input: alloy_primitives::Bytes::from(&RAW_ISTHMUS_INFO_TX),
366                        ..Default::default()
367                    }),
368                )],
369                ..Default::default()
370            },
371        };
372        let block_hash = block.header.hash_slow();
373        let rollup_config = RollupConfig {
374            genesis: ChainGenesis {
375                l2: BlockNumHash { hash: block_hash, ..Default::default() },
376                ..Default::default()
377            },
378            hardforks: HardForkConfig {
379                holocene_time: Some(0),
380                isthmus_time: Some(0),
381                ..Default::default()
382            },
383            ..Default::default()
384        };
385        assert!(rollup_config.is_holocene_active(block.header.timestamp));
386        let config = to_system_config(&block, &rollup_config).unwrap();
387        let expected = SystemConfig {
388            batcher_address: address!("6887246668a3b87f54deb3b94ba47a6f63f32985"),
389            overhead: U256::ZERO,
390            scalar: uint!(
391                452312848583266388373324160190187140051835877600158453279134670530344387928_U256
392            ),
393            gas_limit: 0,
394            base_fee_scalar: None,
395            blob_base_fee_scalar: None,
396            eip1559_denominator: Some(0xbeef),
397            eip1559_elasticity: Some(0xbabe),
398            operator_fee_scalar: Some(0xabcd),
399            operator_fee_constant: Some(0xdcba),
400        };
401        assert_eq!(config, expected);
402    }
403}