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