From a80dcd732d2ac322bbc81d1472542fa2f424695b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tuomas=20M=C3=A4kinen?= Date: Thu, 28 Sep 2023 15:52:55 +0300 Subject: [PATCH 1/3] Encapsulate field parsing --- src/lib.rs | 261 +++------------------------------------------------ src/state.rs | 252 ++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 264 insertions(+), 249 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 5c4beaf..2c9c360 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,6 @@ #![feature(array_chunks)] -use std::collections::HashMap; use std::fs; -use thiserror::Error; mod state; use crate::state::CommitBlockInfoV1; @@ -19,21 +17,6 @@ pub const ZK_SYNC_ADDR: &str = "0x32400084C286CF3E17e7B677ea9583e60a000324"; pub const GENESIS_BLOCK: u64 = 16_627_460; pub const BLOCK_STEP: u64 = 128; -#[derive(Error, Debug)] -pub enum ParseError { - #[error("invalid Calldata: {0}")] - InvalidCalldata(String), - - #[error("invalid StoredBlockInfo: {0}")] - InvalidStoredBlockInfo(String), - - #[error("invalid CommitBlockInfo: {0}")] - InvalidCommitBlockInfo(String), - - #[error("invalid compressed bytecode: {0}")] - InvalidCompressedByteCode(String), -} - pub fn create_initial_state() { let _input = fs::read_to_string(INITAL_STATE_PATH).unwrap(); todo!(); @@ -52,10 +35,10 @@ pub async fn init_eth_adapter(http_url: &str) -> (Provider, Contract) { fn parse_calldata(commit_blocks_fn: &Function, calldata: &[u8]) -> Result> { let mut parsed_input = commit_blocks_fn .decode_input(&calldata[4..]) - .map_err(|e| ParseError::InvalidCalldata(e.to_string()))?; + .map_err(|e| state::ParseError::InvalidCalldata(e.to_string()))?; if parsed_input.len() != 2 { - return Err(ParseError::InvalidCalldata(format!( + return Err(state::ParseError::InvalidCalldata(format!( "invalid number of parameters (got {}, expected 2) for commitBlocks function", parsed_input.len() )) @@ -64,24 +47,26 @@ fn parse_calldata(commit_blocks_fn: &Function, calldata: &[u8]) -> Result Result> let mut res = vec![]; let abi::Token::Array(data) = data else { - return Err(ParseError::InvalidCommitBlockInfo( + return Err(state::ParseError::InvalidCommitBlockInfo( "cannot convert newBlocksData to array".to_string(), ) .into()); }; for data in data.iter() { - let abi::Token::Tuple(block_elems) = data else { - return Err(ParseError::InvalidCommitBlockInfo("struct elements".to_string()).into()); - }; - let abi::Token::Uint(new_l2_block_number) = block_elems[0].clone() else { - return Err(ParseError::InvalidCommitBlockInfo("blockNumber".to_string()).into()); - }; - - /* TODO(tuommaki): Fix the check below. - if new_l2_block_number <= latest_l2_block_number { - println!("skipping before we even get started"); - continue; + match CommitBlockInfoV1::try_from(data) { + Ok(blk) => res.push(blk), + Err(e) => println!("failed to parse commit block info: {}", e), } - */ - - let abi::Token::Uint(timestamp) = block_elems[1].clone() else { - return Err(ParseError::InvalidCommitBlockInfo("timestamp".to_string()).into()); - }; - - let abi::Token::Uint(new_enumeration_index) = block_elems[2].clone() else { - return Err(ParseError::InvalidCommitBlockInfo( - "indexRepeatedStorageChanges".to_string(), - ) - .into()); - }; - let new_enumeration_index = new_enumeration_index.0[0]; - - let abi::Token::FixedBytes(state_root) = block_elems[3].clone() else { - return Err(ParseError::InvalidCommitBlockInfo("newStateRoot".to_string()).into()); - }; - - let abi::Token::Uint(number_of_l1_txs) = block_elems[4].clone() else { - return Err(ParseError::InvalidCommitBlockInfo("numberOfLayer1Txs".to_string()).into()); - }; - - let abi::Token::FixedBytes(l2_logs_tree_root) = block_elems[5].clone() else { - return Err(ParseError::InvalidCommitBlockInfo("l2LogsTreeRoot".to_string()).into()); - }; - - let abi::Token::FixedBytes(priority_operations_hash) = block_elems[6].clone() else { - return Err( - ParseError::InvalidCommitBlockInfo("priorityOperationsHash".to_string()).into(), - ); - }; - - let abi::Token::Bytes(initial_changes_calldata) = block_elems[7].clone() else { - return Err( - ParseError::InvalidCommitBlockInfo("initialStorageChanges".to_string()).into(), - ); - }; - - if initial_changes_calldata.len() % 64 != 4 { - return Err( - ParseError::InvalidCommitBlockInfo("initialStorageChanges".to_string()).into(), - ); - } - let abi::Token::Bytes(repeated_changes_calldata) = block_elems[8].clone() else { - return Err( - ParseError::InvalidCommitBlockInfo("repeatedStorageChanges".to_string()).into(), - ); - }; - - let abi::Token::Bytes(l2_logs) = block_elems[9].clone() else { - return Err(ParseError::InvalidCommitBlockInfo("l2Logs".to_string()).into()); - }; - - // TODO(tuommaki): Are these useful at all? - /* - let abi::Token::Bytes(_l2_arbitrary_length_msgs) = block_elems[10].clone() else { - return Err(ParseError::InvalidCommitBlockInfo( - "l2ArbitraryLengthMessages".to_string(), - ) - .into()); - }; - */ - - // TODO(tuommaki): Parse factory deps - let abi::Token::Array(factory_deps) = block_elems[11].clone() else { - return Err(ParseError::InvalidCommitBlockInfo("factoryDeps".to_string()).into()); - }; - - let mut smartcontracts = vec![]; - for bytecode in factory_deps.into_iter() { - let abi::Token::Bytes(bytecode) = bytecode else { - return Err(ParseError::InvalidCommitBlockInfo("factoryDeps".to_string()).into()); - }; - - match decompress_bytecode(bytecode) { - Ok(bytecode) => smartcontracts.push(bytecode), - Err(e) => println!("failed to decompress bytecode: {}", e), - }; - } - - assert_eq!(repeated_changes_calldata.len() % 40, 4); - - println!( - "Have {} new keys", - (initial_changes_calldata.len() - 4) / 64 - ); - println!( - "Have {} repeated keys", - (repeated_changes_calldata.len() - 4) / 40 - ); - - let mut blk = CommitBlockInfoV1 { - block_number: new_l2_block_number.as_u64(), - timestamp: timestamp.as_u64(), - index_repeated_storage_changes: new_enumeration_index, - new_state_root: state_root, - number_of_l1_txs, - l2_logs_tree_root, - priority_operations_hash, - initial_storage_changes: HashMap::default(), - repeated_storage_changes: HashMap::default(), - l2_logs: l2_logs.to_vec(), - factory_deps: smartcontracts, - }; - - for initial_calldata in initial_changes_calldata[4..].chunks(64) { - let mut t = initial_calldata.array_chunks::<32>(); - let key = *t.next().ok_or_else(|| { - ParseError::InvalidCommitBlockInfo("initialStorageChanges".to_string()) - })?; - let value = *t.next().ok_or_else(|| { - ParseError::InvalidCommitBlockInfo("initialStorageChanges".to_string()) - })?; - - if t.next().is_some() { - return Err(ParseError::InvalidCommitBlockInfo( - "initialStorageChanges".to_string(), - ) - .into()); - } - - let _ = blk.initial_storage_changes.insert(key, value); - } - - for repeated_calldata in repeated_changes_calldata[4..].chunks(40) { - let index = u64::from_be_bytes([ - repeated_calldata[0], - repeated_calldata[1], - repeated_calldata[2], - repeated_calldata[3], - repeated_calldata[4], - repeated_calldata[5], - repeated_calldata[6], - repeated_calldata[7], - ]); - let mut t = repeated_calldata[8..].array_chunks::<32>(); - let value = *t.next().ok_or_else(|| { - ParseError::InvalidCommitBlockInfo("repeatedStorageChanges".to_string()) - })?; - - if t.next().is_some() { - return Err(ParseError::InvalidCommitBlockInfo( - "repeatedStorageChanges".to_string(), - ) - .into()); - } - - blk.repeated_storage_changes.insert(index, value); - } - - // -------------------------------------------------------------- - // TODO(tuommaki): Handle rest of the fields: - // - factoryDeps -> compressed bytecode - // -------------------------------------------------------------- - - res.push(blk); } Ok(res) } -fn decompress_bytecode(data: Vec) -> Result> { - /* - let num_entries = u32::from_be_bytes([data[3], data[2], data[1], data[0]]); - - */ - let mut offset = 0; - - let dict_len = u16::from_be_bytes([data[offset + 1], data[offset]]); - - offset += 2; - - let end = 2 + dict_len as usize; - let dict = data[offset..end].to_vec(); - offset += end; - let encoded_data = data[offset..].to_vec(); - - // Each dictionary element should be 8 bytes. Verify alignment. - if dict.len() % 8 != 0 { - return Err(ParseError::InvalidCompressedByteCode(format!( - "invalid dict length: {}", - dict.len() - )) - .into()); - } - - let dict: Vec<&[u8]> = dict.chunks(8).collect(); - - // Verify that dictionary size is below maximum. - if dict.len() > (1 << 16) - /* 2^16 */ - { - return Err(ParseError::InvalidCompressedByteCode(format!( - "too many elements in dictionary: {}", - dict.len() - )) - .into()); - } - - let mut bytecode = vec![]; - for idx in encoded_data.chunks(2) { - let idx = u16::from_be_bytes([idx[0], idx[1]]) as usize; - if dict.len() <= idx { - return Err(ParseError::InvalidCompressedByteCode(format!( - "encoded data index ({}) exceeds dictionary size ({})", - idx, - dict.len() - )) - .into()); - } - - bytecode.append(&mut dict[idx].to_vec()); - } - - Ok(bytecode) -} - #[cfg(test)] mod tests { use ethers::{ diff --git a/src/state.rs b/src/state.rs index d60bbea..7a9f47a 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,6 +1,23 @@ -use ethers::types::U256; +use ethers::{abi, types::U256}; +use eyre::Result; use std::collections::HashMap; use std::vec::Vec; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ParseError { + #[error("invalid Calldata: {0}")] + InvalidCalldata(String), + + #[error("invalid StoredBlockInfo: {0}")] + InvalidStoredBlockInfo(String), + + #[error("invalid CommitBlockInfo: {0}")] + InvalidCommitBlockInfo(String), + + #[error("invalid compressed bytecode: {0}")] + InvalidCompressedByteCode(String), +} /// Data needed to commit new block pub struct CommitBlockInfoV1 { @@ -28,6 +45,239 @@ pub struct CommitBlockInfoV1 { pub factory_deps: Vec>, } +impl TryFrom<&abi::Token> for CommitBlockInfoV1 { + type Error = ParseError; + + /// Try to parse Ethereum ABI token into CommitBlockInfoV1. + /// + /// * `token` - ABI token of `CommitBlockInfo` type on Ethereum. + fn try_from(token: &abi::Token) -> Result { + let abi::Token::Tuple(block_elems) = token else { + return Err(ParseError::InvalidCommitBlockInfo( + "struct elements".to_string(), + )); + }; + let abi::Token::Uint(new_l2_block_number) = block_elems[0].clone() else { + return Err(ParseError::InvalidCommitBlockInfo( + "blockNumber".to_string(), + )); + }; + + /* TODO(tuommaki): Fix the check below. + if new_l2_block_number <= latest_l2_block_number { + println!("skipping before we even get started"); + continue; + } + */ + + let abi::Token::Uint(timestamp) = block_elems[1].clone() else { + return Err(ParseError::InvalidCommitBlockInfo("timestamp".to_string()).into()); + }; + + let abi::Token::Uint(new_enumeration_index) = block_elems[2].clone() else { + return Err(ParseError::InvalidCommitBlockInfo( + "indexRepeatedStorageChanges".to_string(), + ) + .into()); + }; + let new_enumeration_index = new_enumeration_index.0[0]; + + let abi::Token::FixedBytes(state_root) = block_elems[3].clone() else { + return Err(ParseError::InvalidCommitBlockInfo("newStateRoot".to_string()).into()); + }; + + let abi::Token::Uint(number_of_l1_txs) = block_elems[4].clone() else { + return Err(ParseError::InvalidCommitBlockInfo("numberOfLayer1Txs".to_string()).into()); + }; + + let abi::Token::FixedBytes(l2_logs_tree_root) = block_elems[5].clone() else { + return Err(ParseError::InvalidCommitBlockInfo("l2LogsTreeRoot".to_string()).into()); + }; + + let abi::Token::FixedBytes(priority_operations_hash) = block_elems[6].clone() else { + return Err( + ParseError::InvalidCommitBlockInfo("priorityOperationsHash".to_string()).into(), + ); + }; + + let abi::Token::Bytes(initial_changes_calldata) = block_elems[7].clone() else { + return Err( + ParseError::InvalidCommitBlockInfo("initialStorageChanges".to_string()).into(), + ); + }; + + if initial_changes_calldata.len() % 64 != 4 { + return Err( + ParseError::InvalidCommitBlockInfo("initialStorageChanges".to_string()).into(), + ); + } + let abi::Token::Bytes(repeated_changes_calldata) = block_elems[8].clone() else { + return Err( + ParseError::InvalidCommitBlockInfo("repeatedStorageChanges".to_string()).into(), + ); + }; + + let abi::Token::Bytes(l2_logs) = block_elems[9].clone() else { + return Err(ParseError::InvalidCommitBlockInfo("l2Logs".to_string()).into()); + }; + + // TODO(tuommaki): Are these useful at all? + /* + let abi::Token::Bytes(_l2_arbitrary_length_msgs) = block_elems[10].clone() else { + return Err(ParseError::InvalidCommitBlockInfo( + "l2ArbitraryLengthMessages".to_string(), + ) + .into()); + }; + */ + + // TODO(tuommaki): Parse factory deps + let abi::Token::Array(factory_deps) = block_elems[11].clone() else { + return Err(ParseError::InvalidCommitBlockInfo("factoryDeps".to_string()).into()); + }; + + let mut smartcontracts = vec![]; + for bytecode in factory_deps.into_iter() { + let abi::Token::Bytes(bytecode) = bytecode else { + return Err(ParseError::InvalidCommitBlockInfo("factoryDeps".to_string()).into()); + }; + + match decompress_bytecode(bytecode) { + Ok(bytecode) => smartcontracts.push(bytecode), + Err(e) => println!("failed to decompress bytecode: {}", e), + }; + } + + assert_eq!(repeated_changes_calldata.len() % 40, 4); + + println!( + "Have {} new keys", + (initial_changes_calldata.len() - 4) / 64 + ); + println!( + "Have {} repeated keys", + (repeated_changes_calldata.len() - 4) / 40 + ); + + let mut blk = CommitBlockInfoV1 { + block_number: new_l2_block_number.as_u64(), + timestamp: timestamp.as_u64(), + index_repeated_storage_changes: new_enumeration_index, + new_state_root: state_root, + number_of_l1_txs, + l2_logs_tree_root, + priority_operations_hash, + initial_storage_changes: HashMap::default(), + repeated_storage_changes: HashMap::default(), + l2_logs: l2_logs.to_vec(), + factory_deps: smartcontracts, + }; + + for initial_calldata in initial_changes_calldata[4..].chunks(64) { + let mut t = initial_calldata.array_chunks::<32>(); + let key = *t.next().ok_or_else(|| { + ParseError::InvalidCommitBlockInfo("initialStorageChanges".to_string()) + })?; + let value = *t.next().ok_or_else(|| { + ParseError::InvalidCommitBlockInfo("initialStorageChanges".to_string()) + })?; + + if t.next().is_some() { + return Err(ParseError::InvalidCommitBlockInfo( + "initialStorageChanges".to_string(), + ) + .into()); + } + + let _ = blk.initial_storage_changes.insert(key, value); + } + + for repeated_calldata in repeated_changes_calldata[4..].chunks(40) { + let index = u64::from_be_bytes([ + repeated_calldata[0], + repeated_calldata[1], + repeated_calldata[2], + repeated_calldata[3], + repeated_calldata[4], + repeated_calldata[5], + repeated_calldata[6], + repeated_calldata[7], + ]); + let mut t = repeated_calldata[8..].array_chunks::<32>(); + let value = *t.next().ok_or_else(|| { + ParseError::InvalidCommitBlockInfo("repeatedStorageChanges".to_string()) + })?; + + if t.next().is_some() { + return Err(ParseError::InvalidCommitBlockInfo( + "repeatedStorageChanges".to_string(), + ) + .into()); + } + + blk.repeated_storage_changes.insert(index, value); + } + + Ok(blk) + } +} + +pub(self) fn decompress_bytecode(data: Vec) -> Result> { + /* + let num_entries = u32::from_be_bytes([data[3], data[2], data[1], data[0]]); + + */ + let mut offset = 0; + + let dict_len = u16::from_be_bytes([data[offset + 1], data[offset]]); + + offset += 2; + + let end = 2 + dict_len as usize; + let dict = data[offset..end].to_vec(); + offset += end; + let encoded_data = data[offset..].to_vec(); + + // Each dictionary element should be 8 bytes. Verify alignment. + if dict.len() % 8 != 0 { + return Err(ParseError::InvalidCompressedByteCode(format!( + "invalid dict length: {}", + dict.len() + )) + .into()); + } + + let dict: Vec<&[u8]> = dict.chunks(8).collect(); + + // Verify that dictionary size is below maximum. + if dict.len() > (1 << 16) + /* 2^16 */ + { + return Err(ParseError::InvalidCompressedByteCode(format!( + "too many elements in dictionary: {}", + dict.len() + )) + .into()); + } + + let mut bytecode = vec![]; + for idx in encoded_data.chunks(2) { + let idx = u16::from_be_bytes([idx[0], idx[1]]) as usize; + if dict.len() <= idx { + return Err(ParseError::InvalidCompressedByteCode(format!( + "encoded data index ({}) exceeds dictionary size ({})", + idx, + dict.len() + )) + .into()); + } + + bytecode.append(&mut dict[idx].to_vec()); + } + + Ok(bytecode) +} + pub enum L2ToL1Pubdata { L2ToL1Log, L2ToL2Message, From eb22dd8ec5fae74756ff0aa1e9d40f1400f490af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tuomas=20M=C3=A4kinen?= <1947505+tuommaki@users.noreply.github.com> Date: Fri, 29 Sep 2023 08:28:18 +0300 Subject: [PATCH 2/3] Expose `decompress_bytecode` to outside Co-authored-by: Jonathan <94441036+zeapoz@users.noreply.github.com> --- src/state.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/state.rs b/src/state.rs index 7a9f47a..0186fa4 100644 --- a/src/state.rs +++ b/src/state.rs @@ -222,7 +222,7 @@ impl TryFrom<&abi::Token> for CommitBlockInfoV1 { } } -pub(self) fn decompress_bytecode(data: Vec) -> Result> { +pub fn decompress_bytecode(data: Vec) -> Result> { /* let num_entries = u32::from_be_bytes([data[3], data[2], data[1], data[0]]); From e496e6b4a91dcf679d8e72e637dfbbc00064f186 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tuomas=20M=C3=A4kinen?= Date: Fri, 29 Sep 2023 08:31:15 +0300 Subject: [PATCH 3/3] Drop unnecessary `.into()` --- src/state.rs | 59 +++++++++++++++++++++++++++++----------------------- 1 file changed, 33 insertions(+), 26 deletions(-) diff --git a/src/state.rs b/src/state.rs index 0186fa4..17685f0 100644 --- a/src/state.rs +++ b/src/state.rs @@ -71,54 +71,59 @@ impl TryFrom<&abi::Token> for CommitBlockInfoV1 { */ let abi::Token::Uint(timestamp) = block_elems[1].clone() else { - return Err(ParseError::InvalidCommitBlockInfo("timestamp".to_string()).into()); + return Err(ParseError::InvalidCommitBlockInfo("timestamp".to_string())); }; let abi::Token::Uint(new_enumeration_index) = block_elems[2].clone() else { return Err(ParseError::InvalidCommitBlockInfo( "indexRepeatedStorageChanges".to_string(), - ) - .into()); + )); }; let new_enumeration_index = new_enumeration_index.0[0]; let abi::Token::FixedBytes(state_root) = block_elems[3].clone() else { - return Err(ParseError::InvalidCommitBlockInfo("newStateRoot".to_string()).into()); + return Err(ParseError::InvalidCommitBlockInfo( + "newStateRoot".to_string(), + )); }; let abi::Token::Uint(number_of_l1_txs) = block_elems[4].clone() else { - return Err(ParseError::InvalidCommitBlockInfo("numberOfLayer1Txs".to_string()).into()); + return Err(ParseError::InvalidCommitBlockInfo( + "numberOfLayer1Txs".to_string(), + )); }; let abi::Token::FixedBytes(l2_logs_tree_root) = block_elems[5].clone() else { - return Err(ParseError::InvalidCommitBlockInfo("l2LogsTreeRoot".to_string()).into()); + return Err(ParseError::InvalidCommitBlockInfo( + "l2LogsTreeRoot".to_string(), + )); }; let abi::Token::FixedBytes(priority_operations_hash) = block_elems[6].clone() else { - return Err( - ParseError::InvalidCommitBlockInfo("priorityOperationsHash".to_string()).into(), - ); + return Err(ParseError::InvalidCommitBlockInfo( + "priorityOperationsHash".to_string(), + )); }; let abi::Token::Bytes(initial_changes_calldata) = block_elems[7].clone() else { - return Err( - ParseError::InvalidCommitBlockInfo("initialStorageChanges".to_string()).into(), - ); + return Err(ParseError::InvalidCommitBlockInfo( + "initialStorageChanges".to_string(), + )); }; if initial_changes_calldata.len() % 64 != 4 { - return Err( - ParseError::InvalidCommitBlockInfo("initialStorageChanges".to_string()).into(), - ); + return Err(ParseError::InvalidCommitBlockInfo( + "initialStorageChanges".to_string(), + )); } let abi::Token::Bytes(repeated_changes_calldata) = block_elems[8].clone() else { - return Err( - ParseError::InvalidCommitBlockInfo("repeatedStorageChanges".to_string()).into(), - ); + return Err(ParseError::InvalidCommitBlockInfo( + "repeatedStorageChanges".to_string(), + )); }; let abi::Token::Bytes(l2_logs) = block_elems[9].clone() else { - return Err(ParseError::InvalidCommitBlockInfo("l2Logs".to_string()).into()); + return Err(ParseError::InvalidCommitBlockInfo("l2Logs".to_string())); }; // TODO(tuommaki): Are these useful at all? @@ -127,19 +132,23 @@ impl TryFrom<&abi::Token> for CommitBlockInfoV1 { return Err(ParseError::InvalidCommitBlockInfo( "l2ArbitraryLengthMessages".to_string(), ) - .into()); + ); }; */ // TODO(tuommaki): Parse factory deps let abi::Token::Array(factory_deps) = block_elems[11].clone() else { - return Err(ParseError::InvalidCommitBlockInfo("factoryDeps".to_string()).into()); + return Err(ParseError::InvalidCommitBlockInfo( + "factoryDeps".to_string(), + )); }; let mut smartcontracts = vec![]; for bytecode in factory_deps.into_iter() { let abi::Token::Bytes(bytecode) = bytecode else { - return Err(ParseError::InvalidCommitBlockInfo("factoryDeps".to_string()).into()); + return Err(ParseError::InvalidCommitBlockInfo( + "factoryDeps".to_string(), + )); }; match decompress_bytecode(bytecode) { @@ -185,8 +194,7 @@ impl TryFrom<&abi::Token> for CommitBlockInfoV1 { if t.next().is_some() { return Err(ParseError::InvalidCommitBlockInfo( "initialStorageChanges".to_string(), - ) - .into()); + )); } let _ = blk.initial_storage_changes.insert(key, value); @@ -211,8 +219,7 @@ impl TryFrom<&abi::Token> for CommitBlockInfoV1 { if t.next().is_some() { return Err(ParseError::InvalidCommitBlockInfo( "repeatedStorageChanges".to_string(), - ) - .into()); + )); } blk.repeated_storage_changes.insert(index, value);