|
| 1 | +use crate::{ |
| 2 | + decoding::{constants::KECCAK_256_DIGEST_BYTES_SIZE, payload::PayloadData}, |
| 3 | + BlockContext, L2Block, |
| 4 | +}; |
| 5 | + |
| 6 | +use alloy_primitives::{bytes::BufMut, keccak256, B256}; |
| 7 | +use scroll_alloy_consensus::TxL1Message; |
| 8 | + |
| 9 | +/// The deserialized batch data. |
| 10 | +#[derive(Debug, Clone, PartialEq, Eq)] |
| 11 | +pub struct Batch { |
| 12 | + /// The batch version. |
| 13 | + pub version: u8, |
| 14 | + /// The amount of blocks for each chunk of the batch. Only relevant for codec versions v0 -> |
| 15 | + /// v6. |
| 16 | + pub chunks_block_count: Option<Vec<usize>>, |
| 17 | + /// The data for the batch. |
| 18 | + pub data: PayloadData, |
| 19 | +} |
| 20 | + |
| 21 | +impl Batch { |
| 22 | + /// Returns a new instance of a batch. |
| 23 | + pub fn new(version: u8, chunks_block_count: Option<Vec<usize>>, data: PayloadData) -> Self { |
| 24 | + Self { version, chunks_block_count, data } |
| 25 | + } |
| 26 | + |
| 27 | + /// Computes the data hash for the batch, using the provided L1 messages associated with each |
| 28 | + /// block. |
| 29 | + pub fn try_compute_data_hash(&self, l1_messages: &[TxL1Message]) -> Option<B256> { |
| 30 | + // From version 7 and above, the batch doesn't have a data hash. |
| 31 | + if self.version >= 7 { |
| 32 | + return None; |
| 33 | + } |
| 34 | + |
| 35 | + let total_l1_messages: usize = |
| 36 | + self.data.l2_blocks().iter().map(|b| b.context.num_l1_messages as usize).sum(); |
| 37 | + debug_assert_eq!(total_l1_messages, l1_messages.len(), "invalid l1 messages count"); |
| 38 | + |
| 39 | + let chunks_count = self.chunks_block_count.as_ref()?; |
| 40 | + let blocks_buf = &mut (&**self.data.l2_blocks()); |
| 41 | + let l1_messages_buf = &mut (&*l1_messages); |
| 42 | + |
| 43 | + let mut chunk_hashes = |
| 44 | + Vec::with_capacity(chunks_count.len() * KECCAK_256_DIGEST_BYTES_SIZE); |
| 45 | + |
| 46 | + for chunk_count in chunks_count { |
| 47 | + // slice the blocks at chunk_count. |
| 48 | + let blocks = blocks_buf.get(..*chunk_count)?; |
| 49 | + |
| 50 | + // take the correct amount of l1 messages for each block and advance the buffer. |
| 51 | + let l1_messages_per_block = blocks |
| 52 | + .iter() |
| 53 | + .map(|b| { |
| 54 | + let num_l1_messages = b.context.num_l1_messages as usize; |
| 55 | + let block_messages = l1_messages_buf.get(..num_l1_messages).unwrap_or(&[]); |
| 56 | + *l1_messages_buf = l1_messages_buf.get(num_l1_messages..).unwrap_or(&[]); |
| 57 | + block_messages |
| 58 | + }) |
| 59 | + .collect::<Vec<_>>(); |
| 60 | + |
| 61 | + // compute the chunk data hash. |
| 62 | + chunk_hashes |
| 63 | + .append(&mut hash_chunk(self.version, blocks, l1_messages_per_block).to_vec()); |
| 64 | + |
| 65 | + // advance the buffer. |
| 66 | + *blocks_buf = blocks_buf.get(*chunk_count..).unwrap_or(&[]); |
| 67 | + } |
| 68 | + |
| 69 | + Some(keccak256(chunk_hashes)) |
| 70 | + } |
| 71 | +} |
| 72 | + |
| 73 | +/// Compute the hash for the chunk. |
| 74 | +fn hash_chunk( |
| 75 | + version: u8, |
| 76 | + l2_blocks: &[L2Block], |
| 77 | + l1_messages_per_block: Vec<&[TxL1Message]>, |
| 78 | +) -> B256 { |
| 79 | + // reserve the correct capacity. |
| 80 | + let l1_messages_count: usize = |
| 81 | + l1_messages_per_block.iter().map(|messages| messages.len()).sum(); |
| 82 | + let mut capacity = l2_blocks.len() * (BlockContext::BYTES_LENGTH - 2) + |
| 83 | + l1_messages_count * KECCAK_256_DIGEST_BYTES_SIZE; |
| 84 | + if version == 0 { |
| 85 | + capacity += l2_blocks.iter().map(|b| b.transactions.len()).sum::<usize>(); |
| 86 | + } |
| 87 | + let mut buf = Vec::with_capacity(capacity); |
| 88 | + |
| 89 | + for block in l2_blocks { |
| 90 | + let context = block.context.to_be_bytes(); |
| 91 | + // we don't use the last 2 bytes. |
| 92 | + // <https://github.com/scroll-tech/da-codec/blob/main/encoding/codecv0_types.go#L175> |
| 93 | + buf.put_slice(&context[..BlockContext::BYTES_LENGTH - 2]); |
| 94 | + } |
| 95 | + |
| 96 | + for (block, l1_messages) in l2_blocks.iter().zip(l1_messages_per_block) { |
| 97 | + for l1_message in l1_messages { |
| 98 | + buf.put_slice(l1_message.tx_hash().as_slice()) |
| 99 | + } |
| 100 | + |
| 101 | + // for v0, we add the l2 transaction hashes. |
| 102 | + if version == 0 { |
| 103 | + for tx in &block.transactions { |
| 104 | + buf.put_slice(keccak256(&tx.0).as_slice()); |
| 105 | + } |
| 106 | + } |
| 107 | + } |
| 108 | + |
| 109 | + keccak256(buf) |
| 110 | +} |
| 111 | + |
| 112 | +#[cfg(test)] |
| 113 | +mod tests { |
| 114 | + use crate::decoding::{test_utils::read_to_bytes, v0::decode_v0, v1::decode_v1}; |
| 115 | + |
| 116 | + use alloy_primitives::{address, b256, bytes, U256}; |
| 117 | + use scroll_alloy_consensus::TxL1Message; |
| 118 | + |
| 119 | + #[test] |
| 120 | + fn test_should_compute_data_hash_v0() -> eyre::Result<()> { |
| 121 | + // <https://etherscan.io/tx/0x2c7bb77d6086befd9bdcf936479fd246d1065cbd2c6aff55b1d39a67aff965c1> |
| 122 | + let raw_calldata = read_to_bytes("./testdata/calldata_v0.bin")?; |
| 123 | + let batch = decode_v0(&raw_calldata)?; |
| 124 | + |
| 125 | + let hash = batch.try_compute_data_hash(&[]).unwrap(); |
| 126 | + |
| 127 | + assert_eq!(hash, b256!("33e608dbf683c1ee03a34d01de52f67d60a0563b7e713b65a7395bb3b646f71f")); |
| 128 | + |
| 129 | + Ok(()) |
| 130 | + } |
| 131 | + |
| 132 | + #[test] |
| 133 | + fn test_should_compute_data_hash_v0_with_l1_messages() -> eyre::Result<()> { |
| 134 | + // <https://etherscan.io/tx/0xdc0a315b25b46f4c1085e3884c63f8ede61e984e47655f7667e5f14e3df55f82> |
| 135 | + let raw_calldata = read_to_bytes("./testdata/calldata_v0_with_l1_messages.bin")?; |
| 136 | + let batch = decode_v0(&raw_calldata)?; |
| 137 | + |
| 138 | + let hash = batch |
| 139 | + .try_compute_data_hash(&[ |
| 140 | + TxL1Message { |
| 141 | + queue_index: 39, |
| 142 | + gas_limit: 180000, |
| 143 | + to: address!("781e90f1c8Fc4611c9b7497C3B47F99Ef6969CbC"), |
| 144 | + value: U256::ZERO, |
| 145 | + sender: address!("7885BcBd5CeCEf1336b5300fb5186A12DDD8c478"), |
| 146 | + input: bytes!("8ef1332e000000000000000000000000f1af3b23de0a5ca3cab7261cb0061c0d779a5c7b00000000000000000000000033b60d5dd260d453cac3782b0bdc01ce846721420000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002700000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000e48431f5c1000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000006efdbff2a14a7c8e15944d1f4a48f9f95f663a4000000000000000000000000c451b0191351ce308fdfd779d73814c910fc5ecb000000000000000000000000c451b0191351ce308fdfd779d73814c910fc5ecb00000000000000000000000000000000000000000000000000000005d21dba0000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"), |
| 147 | + }, |
| 148 | + TxL1Message { |
| 149 | + queue_index: 40, |
| 150 | + gas_limit: 168000, |
| 151 | + to: address!("781e90f1c8Fc4611c9b7497C3B47F99Ef6969CbC"), |
| 152 | + value: U256::ZERO, |
| 153 | + sender: address!("7885BcBd5CeCEf1336b5300fb5186A12DDD8c478"), |
| 154 | + input: bytes!("8ef1332e0000000000000000000000007f2b8c31f88b6006c382775eea88297ec1e3e9050000000000000000000000006ea73e05adc79974b931123675ea8f78ffdacdf00000000000000000000000000000000000000000000000000011c37937e08000000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000a4232e8748000000000000000000000000b89db2813541287a4dd1fc6801eec30595ecdc6c000000000000000000000000b89db2813541287a4dd1fc6801eec30595ecdc6c0000000000000000000000000000000000000000000000000011c37937e080000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"), |
| 155 | + }, |
| 156 | + TxL1Message { |
| 157 | + queue_index: 41, |
| 158 | + gas_limit: 168000, |
| 159 | + to: address!("781e90f1c8Fc4611c9b7497C3B47F99Ef6969CbC"), |
| 160 | + value: U256::ZERO, |
| 161 | + sender: address!("7885BcBd5CeCEf1336b5300fb5186A12DDD8c478"), |
| 162 | + input: bytes!("8ef1332e0000000000000000000000007f2b8c31f88b6006c382775eea88297ec1e3e9050000000000000000000000006ea73e05adc79974b931123675ea8f78ffdacdf0000000000000000000000000000000000000000000000000002386f26fc10000000000000000000000000000000000000000000000000000000000000000002900000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000a4232e87480000000000000000000000003219c394111d45757ccb68a4fd353b4f7f9660960000000000000000000000003219c394111d45757ccb68a4fd353b4f7f966096000000000000000000000000000000000000000000000000002386f26fc100000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"), |
| 163 | + }, |
| 164 | + ]) |
| 165 | + .unwrap(); |
| 166 | + |
| 167 | + assert_eq!(hash, b256!("55fd647c58461d910b5bfb4539f2177ba575c9c8d578a344558976a4375cc287")); |
| 168 | + |
| 169 | + Ok(()) |
| 170 | + } |
| 171 | + |
| 172 | + #[test] |
| 173 | + fn test_should_compute_data_hash_v1() -> eyre::Result<()> { |
| 174 | + // <https://etherscan.io/tx/0x27d73eef6f0de411f8db966f0def9f28c312a0ae5cfb1ac09ec23f8fa18b005b> |
| 175 | + let raw_calldata = read_to_bytes("./testdata/calldata_v1.bin")?; |
| 176 | + let blob = read_to_bytes("./testdata/blob_v1.bin")?; |
| 177 | + let batch = decode_v1(&raw_calldata, &blob)?; |
| 178 | + |
| 179 | + let hash = batch.try_compute_data_hash(&[]).unwrap(); |
| 180 | + |
| 181 | + assert_eq!(hash, b256!("c20f5914a772663080f8a77955b33814a04f7a19c880536e562a1bcfd5343a37")); |
| 182 | + |
| 183 | + Ok(()) |
| 184 | + } |
| 185 | + |
| 186 | + #[test] |
| 187 | + fn test_should_compute_data_hash_v1_with_l1_messages() -> eyre::Result<()> { |
| 188 | + // <https://etherscan.io/tx/0x30451fc1a7ad4a87f9a2616e972d2489326bafa2a41aba8cfb664aec5f727d94> |
| 189 | + let raw_calldata = read_to_bytes("./testdata/calldata_v1_with_l1_messages.bin")?; |
| 190 | + let raw_blob = read_to_bytes("./testdata/blob_v1_with_l1_messages.bin")?; |
| 191 | + let batch = decode_v1(&raw_calldata, &raw_blob)?; |
| 192 | + |
| 193 | + let l1_messages: Vec<TxL1Message> = |
| 194 | + serde_json::from_str(&std::fs::read_to_string("./testdata/l1_messages_v1.json")?)?; |
| 195 | + |
| 196 | + let hash = batch.try_compute_data_hash(&l1_messages).unwrap(); |
| 197 | + |
| 198 | + assert_eq!(hash, b256!("e20ac534891e7f96c3a945e2aafe0a05c7079959eccd94ad217ee0f3b29ac030")); |
| 199 | + |
| 200 | + Ok(()) |
| 201 | + } |
| 202 | +} |
0 commit comments