diff --git a/crates/core/src/types/block.rs b/crates/core/src/types/block.rs index f040c7e5901..6f4b2b06a55 100644 --- a/crates/core/src/types/block.rs +++ b/crates/core/src/types/block.rs @@ -3,7 +3,11 @@ use super::{ GAS_LIMIT_MINIMUM, }; use crate::{ - rlp::{encode::RLPEncode, structs::Encoder}, + rlp::{ + decode::RLPDecode, + encode::RLPEncode, + structs::{Decoder, Encoder}, + }, types::Receipt, Address, H256, U256, }; @@ -78,6 +82,59 @@ impl RLPEncode for BlockHeader { } } +impl RLPDecode for BlockHeader { + fn decode_unfinished(rlp: &[u8]) -> Result<(Self, &[u8]), crate::rlp::error::RLPDecodeError> { + let decoder = Decoder::new(rlp)?; + let (parent_hash, decoder) = decoder.decode_field("parent_hash")?; + let (ommers_hash, decoder) = decoder.decode_field("ommers_hash")?; + let (coinbase, decoder) = decoder.decode_field("coinbase")?; + let (state_root, decoder) = decoder.decode_field("state_root")?; + let (transactions_root, decoder) = decoder.decode_field("transactions_root")?; + let (receipt_root, decoder) = decoder.decode_field("receipt_root")?; + let (logs_bloom, decoder) = decoder.decode_field("logs_bloom")?; + let (difficulty, decoder) = decoder.decode_field("difficulty")?; + let (number, decoder) = decoder.decode_field("number")?; + let (gas_limit, decoder) = decoder.decode_field("gas_limit")?; + let (gas_used, decoder) = decoder.decode_field("gas_used")?; + let (timestamp, decoder) = decoder.decode_field("timestamp")?; + let (extra_data, decoder) = decoder.decode_field("extra_data")?; + let (prev_randao, decoder) = decoder.decode_field("prev_randao")?; + let (nonce, decoder) = decoder.decode_field("nonce")?; + let nonce = u64::from_be_bytes(nonce); + let (base_fee_per_gas, decoder) = decoder.decode_field("base_fee_per_gas")?; + let (withdrawals_root, decoder) = decoder.decode_field("withdrawals_root")?; + let (blob_gas_used, decoder) = decoder.decode_field("blob_gas_used")?; + let (excess_blob_gas, decoder) = decoder.decode_field("excess_blob_gas")?; + let (parent_beacon_block_root, decoder) = + decoder.decode_field("parent_beacon_block_root")?; + Ok(( + BlockHeader { + parent_hash, + ommers_hash, + coinbase, + state_root, + transactions_root, + receipt_root, + logs_bloom, + difficulty, + number, + gas_limit, + gas_used, + timestamp, + extra_data, + prev_randao, + nonce, + base_fee_per_gas, + withdrawals_root, + blob_gas_used, + excess_blob_gas, + parent_beacon_block_root, + }, + decoder.finish()?, + )) + } +} + // The body of a block on the chain #[derive(Clone, Debug, PartialEq, Eq)] pub struct BlockBody { @@ -109,7 +166,7 @@ impl BlockBody { // Value: tx_type || RLP(tx) if tx_type != 0 // RLP(tx) else let mut v = Vec::new(); - tx.encode_with_type(&mut v); + tx.encode(&mut v); (k, v) }) @@ -171,6 +228,23 @@ impl RLPEncode for BlockBody { } } +impl RLPDecode for BlockBody { + fn decode_unfinished(rlp: &[u8]) -> Result<(Self, &[u8]), crate::rlp::error::RLPDecodeError> { + let decoder = Decoder::new(rlp)?; + let (transactions, decoder) = decoder.decode_field("transactions")?; + let (ommers, decoder) = decoder.decode_field("ommers")?; + let (withdrawals, decoder) = decoder.decode_field("withdrawals")?; + Ok(( + BlockBody { + transactions, + ommers, + withdrawals, + }, + decoder.finish()?, + )) + } +} + impl BlockHeader { pub fn compute_block_hash(&self) -> H256 { let mut buf = vec![]; @@ -201,6 +275,25 @@ impl RLPEncode for Withdrawal { } } +impl RLPDecode for Withdrawal { + fn decode_unfinished(rlp: &[u8]) -> Result<(Self, &[u8]), crate::rlp::error::RLPDecodeError> { + let decoder = Decoder::new(rlp)?; + let (index, decoder) = decoder.decode_field("index")?; + let (validator_index, decoder) = decoder.decode_field("validator_index")?; + let (address, decoder) = decoder.decode_field("address")?; + let (amount, decoder) = decoder.decode_field("amount")?; + Ok(( + Withdrawal { + index, + validator_index, + address, + amount, + }, + decoder.finish()?, + )) + } +} + // Checks that the gas_limit fits the gas bounds set by its parent block fn check_gas_limit(gas_limit: u64, parent_gas_limit: u64) -> bool { let max_adjustment_delta = parent_gas_limit / GAS_LIMIT_ADJUSTMENT_FACTOR; diff --git a/crates/core/src/types/engine/payload.rs b/crates/core/src/types/engine/payload.rs index 3123035dfc1..3addd4dda0f 100644 --- a/crates/core/src/types/engine/payload.rs +++ b/crates/core/src/types/engine/payload.rs @@ -7,8 +7,7 @@ use crate::rlp::decode::RLPDecode; use crate::{rlp::error::RLPDecodeError, serde_utils}; use crate::types::{ - compute_withdrawals_root, BlockBody, BlockHeader, EIP1559Transaction, EIP2930Transaction, - EIP4844Transaction, LegacyTransaction, Transaction, Withdrawal, DEFAULT_OMMERS_HASH, + compute_withdrawals_root, BlockBody, BlockHeader, Transaction, Withdrawal, DEFAULT_OMMERS_HASH, }; #[allow(unused)] @@ -64,35 +63,7 @@ impl EncodedTransaction { /// A) `TransactionType || Transaction` (Where Transaction type is an 8-bit number between 0 and 0x7f, and Transaction is an rlp encoded transaction of type TransactionType) /// B) `LegacyTransaction` (An rlp encoded LegacyTransaction) fn decode(&self) -> Result { - // Look at the first byte to check if it corresponds to a TransactionType - match self.0.first() { - // First byte is a valid TransactionType - Some(tx_type) if *tx_type < 0x7f => { - // Decode tx based on type - let tx_bytes = &self.0.as_ref()[1..]; - match *tx_type { - // Legacy - 0x0 => LegacyTransaction::decode(tx_bytes).map(Transaction::LegacyTransaction), // TODO: check if this is a real case scenario - // EIP2930 - 0x1 => { - EIP2930Transaction::decode(tx_bytes).map(Transaction::EIP2930Transaction) - } - // EIP1559 - 0x2 => { - EIP1559Transaction::decode(tx_bytes).map(Transaction::EIP1559Transaction) - } - // EIP4844 - 0x3 => { - EIP4844Transaction::decode(tx_bytes).map(Transaction::EIP4844Transaction) - } - ty => Err(RLPDecodeError::Custom(format!( - "Invalid transaction type: {ty}" - ))), - } - } - // LegacyTransaction - _ => LegacyTransaction::decode(self.0.as_ref()).map(Transaction::LegacyTransaction), - } + Transaction::decode(self.0.as_ref()) } } diff --git a/crates/core/src/types/transaction.rs b/crates/core/src/types/transaction.rs index 9549629774e..f6202373d1a 100644 --- a/crates/core/src/types/transaction.rs +++ b/crates/core/src/types/transaction.rs @@ -92,18 +92,6 @@ pub enum TxType { } impl Transaction { - pub fn encode_with_type(&self, buf: &mut dyn bytes::BufMut) { - // tx_type || RLP(tx) if tx_type != 0 - // RLP(tx) else - match self { - // Legacy transactions don't have a prefix - Transaction::LegacyTransaction(_) => {} - _ => buf.put_u8(self.tx_type() as u8), - } - - self.encode(buf); - } - pub fn tx_type(&self) -> TxType { match self { Transaction::LegacyTransaction(_) => TxType::Legacy, @@ -115,7 +103,16 @@ impl Transaction { } impl RLPEncode for Transaction { + /// Based on [EIP-2718] + /// Transactions can be encoded in the following formats: + /// A) `TransactionType || Transaction` (Where Transaction type is an 8-bit number between 0 and 0x7f, and Transaction is an rlp encoded transaction of type TransactionType) + /// B) `LegacyTransaction` (An rlp encoded LegacyTransaction) fn encode(&self, buf: &mut dyn bytes::BufMut) { + match self { + // Legacy transactions don't have a prefix + Transaction::LegacyTransaction(_) => {} + _ => buf.put_u8(self.tx_type() as u8), + } match self { Transaction::LegacyTransaction(t) => t.encode(buf), Transaction::EIP2930Transaction(t) => t.encode(buf), @@ -125,6 +122,43 @@ impl RLPEncode for Transaction { } } +impl RLPDecode for Transaction { + /// Based on [EIP-2718] + /// Transactions can be encoded in the following formats: + /// A) `TransactionType || Transaction` (Where Transaction type is an 8-bit number between 0 and 0x7f, and Transaction is an rlp encoded transaction of type TransactionType) + /// B) `LegacyTransaction` (An rlp encoded LegacyTransaction) + fn decode_unfinished(rlp: &[u8]) -> Result<(Self, &[u8]), RLPDecodeError> { + // Look at the first byte to check if it corresponds to a TransactionType + match rlp.first() { + // First byte is a valid TransactionType + Some(tx_type) if *tx_type < 0x7f => { + // Decode tx based on type + let tx_bytes = &rlp[1..]; + match *tx_type { + // Legacy + 0x0 => LegacyTransaction::decode_unfinished(tx_bytes) + .map(|(tx, rem)| (Transaction::LegacyTransaction(tx), rem)), // TODO: check if this is a real case scenario + // EIP2930 + 0x1 => EIP2930Transaction::decode_unfinished(tx_bytes) + .map(|(tx, rem)| (Transaction::EIP2930Transaction(tx), rem)), + // EIP1559 + 0x2 => EIP1559Transaction::decode_unfinished(tx_bytes) + .map(|(tx, rem)| (Transaction::EIP1559Transaction(tx), rem)), + // EIP4844 + 0x3 => EIP4844Transaction::decode_unfinished(tx_bytes) + .map(|(tx, rem)| (Transaction::EIP4844Transaction(tx), rem)), + ty => Err(RLPDecodeError::Custom(format!( + "Invalid transaction type: {ty}" + ))), + } + } + // LegacyTransaction + _ => LegacyTransaction::decode_unfinished(rlp) + .map(|(tx, rem)| (Transaction::LegacyTransaction(tx), rem)), + } + } +} + /// The transaction's kind: call or create. #[derive(Clone, Debug, PartialEq, Eq)] pub enum TxKind { diff --git a/crates/storage/src/in_memory.rs b/crates/storage/src/in_memory.rs index db6b6794faa..9668d0851d4 100644 --- a/crates/storage/src/in_memory.rs +++ b/crates/storage/src/in_memory.rs @@ -1,21 +1,20 @@ use super::{Key, StoreEngine, Value}; use crate::error::StoreError; -use ethereum_rust_core::types::AccountInfo; +use ethereum_rust_core::types::{AccountInfo, BlockBody, BlockHeader, BlockNumber}; use ethereum_types::Address; use std::{collections::HashMap, fmt::Debug}; #[derive(Default)] pub struct Store { account_infos: HashMap, + bodies: HashMap, + headers: HashMap, values: HashMap, } impl Store { pub fn new() -> Result { - Ok(Self { - account_infos: HashMap::new(), - values: HashMap::new(), - }) + Ok(Self::default()) } } @@ -41,6 +40,32 @@ impl StoreEngine for Store { fn get_value(&self, key: Key) -> Result>, StoreError> { Ok(self.values.get(&key).cloned()) } + + fn get_block_header(&self, block_number: u64) -> Result, StoreError> { + Ok(self.headers.get(&block_number).cloned()) + } + + fn get_block_body(&self, block_number: u64) -> Result, StoreError> { + Ok(self.bodies.get(&block_number).cloned()) + } + + fn add_block_header( + &mut self, + block_number: BlockNumber, + block_header: BlockHeader, + ) -> Result<(), StoreError> { + self.headers.insert(block_number, block_header); + Ok(()) + } + + fn add_block_body( + &mut self, + block_number: BlockNumber, + block_body: BlockBody, + ) -> Result<(), StoreError> { + self.bodies.insert(block_number, block_body); + Ok(()) + } } impl Debug for Store { diff --git a/crates/storage/src/lib.rs b/crates/storage/src/lib.rs index 2c8f24c9ef4..fe0486836f9 100644 --- a/crates/storage/src/lib.rs +++ b/crates/storage/src/lib.rs @@ -13,7 +13,7 @@ use self::libmdbx::Store as LibmdbxStore; use self::rocksdb::Store as RocksDbStore; #[cfg(feature = "sled")] use self::sled::Store as SledStore; -use ethereum_rust_core::types::AccountInfo; +use ethereum_rust_core::types::{AccountInfo, BlockBody, BlockHeader, BlockNumber}; use ethereum_types::Address; use std::fmt::Debug; use std::sync::{Arc, Mutex}; @@ -44,6 +44,29 @@ pub trait StoreEngine: Debug + Send { /// Obtain account info fn get_account_info(&self, address: Address) -> Result, StoreError>; + /// Add block header + fn add_block_header( + &mut self, + block_number: BlockNumber, + block_header: BlockHeader, + ) -> Result<(), StoreError>; + + /// Obtain block header + fn get_block_header( + &self, + block_number: BlockNumber, + ) -> Result, StoreError>; + + /// Add block body + fn add_block_body( + &mut self, + block_number: BlockNumber, + block_body: BlockBody, + ) -> Result<(), StoreError>; + + /// Obtain block body + fn get_block_body(&self, block_number: BlockNumber) -> Result, StoreError>; + /// Set an arbitrary value (used for eventual persistent values: eg. current_block_height) fn set_value(&mut self, key: Key, value: Value) -> Result<(), StoreError>; @@ -110,15 +133,64 @@ impl Store { .unwrap() .get_account_info(address) } + + pub fn add_block_header( + &self, + block_number: BlockNumber, + block_header: BlockHeader, + ) -> Result<(), StoreError> { + self.engine + .clone() + .lock() + .unwrap() + .add_block_header(block_number, block_header) + } + + pub fn get_block_header( + &self, + block_number: BlockNumber, + ) -> Result, StoreError> { + self.engine + .clone() + .lock() + .unwrap() + .get_block_header(block_number) + } + + pub fn add_block_body( + &self, + block_number: BlockNumber, + block_body: BlockBody, + ) -> Result<(), StoreError> { + self.engine + .clone() + .lock() + .unwrap() + .add_block_body(block_number, block_body) + } + + pub fn get_block_body( + &self, + block_number: BlockNumber, + ) -> Result, StoreError> { + self.engine + .clone() + .lock() + .unwrap() + .get_block_body(block_number) + } } #[cfg(test)] mod tests { - use std::{env, fs}; + use std::{env, fs, str::FromStr}; use bytes::Bytes; - use ethereum_rust_core::types; - use ethereum_types::U256; + use ethereum_rust_core::{ + rlp::decode::RLPDecode, + types::{self, Bloom, Transaction}, + }; + use ethereum_types::{H256, U256}; use super::*; @@ -127,6 +199,7 @@ mod tests { fn test_in_memory_store() { let store = Store::new("test", EngineType::InMemory).unwrap(); test_store_account(store.clone()); + test_store_block(store.clone()); } #[cfg(feature = "libmdbx")] @@ -136,6 +209,7 @@ mod tests { remove_test_dbs("test.mdbx"); let store = Store::new("test.mdbx", EngineType::Libmdbx).unwrap(); test_store_account(store.clone()); + test_store_block(store.clone()); remove_test_dbs("test.mdbx"); } @@ -147,6 +221,7 @@ mod tests { remove_test_dbs("test.sled"); let store = Store::new("test.sled", EngineType::Sled).unwrap(); test_store_account(store.clone()); + // test_store_block(store.clone()); Unimplemented remove_test_dbs("test.sled"); } @@ -158,6 +233,7 @@ mod tests { remove_test_dbs("test.rocksdb"); let store = Store::new("test.rocksdb", EngineType::Sled).unwrap(); test_store_account(store.clone()); + // test_store_block(store.clone()); Unimplemented remove_test_dbs("test.rocksdb"); } @@ -202,4 +278,72 @@ mod tests { } } } + + fn test_store_block(store: Store) { + let (block_header, block_body) = create_block_for_testing(); + let block_number = 6; + + store + .add_block_header(block_number, block_header.clone()) + .unwrap(); + store + .add_block_body(block_number, block_body.clone()) + .unwrap(); + + let stored_header = store.get_block_header(block_number).unwrap().unwrap(); + let stored_body = store.get_block_body(block_number).unwrap().unwrap(); + + assert_eq!(stored_header, block_header); + assert_eq!(stored_body, block_body); + } + + fn create_block_for_testing() -> (BlockHeader, BlockBody) { + let block_header = BlockHeader { + parent_hash: H256::from_str( + "0x1ac1bf1eef97dc6b03daba5af3b89881b7ae4bc1600dc434f450a9ec34d44999", + ) + .unwrap(), + ommers_hash: H256::from_str( + "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + ) + .unwrap(), + coinbase: Address::from_str("0x2adc25665018aa1fe0e6bc666dac8fc2697ff9ba").unwrap(), + state_root: H256::from_str( + "0x9de6f95cb4ff4ef22a73705d6ba38c4b927c7bca9887ef5d24a734bb863218d9", + ) + .unwrap(), + transactions_root: H256::from_str( + "0x578602b2b7e3a3291c3eefca3a08bc13c0d194f9845a39b6f3bcf843d9fed79d", + ) + .unwrap(), + receipt_root: H256::from_str( + "0x035d56bac3f47246c5eed0e6642ca40dc262f9144b582f058bc23ded72aa72fa", + ) + .unwrap(), + logs_bloom: Bloom::from([0; 256]), + difficulty: U256::zero(), + number: 1, + gas_limit: 0x016345785d8a0000, + gas_used: 0xa8de, + timestamp: 0x03e8, + extra_data: Bytes::new(), + prev_randao: H256::zero(), + nonce: 0x0000000000000000, + base_fee_per_gas: 0x07, + withdrawals_root: H256::from_str( + "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + ) + .unwrap(), + blob_gas_used: 0x00, + excess_blob_gas: 0x00, + parent_beacon_block_root: H256::zero(), + }; + let block_body = BlockBody { + transactions: vec![Transaction::decode(&hex::decode("02f86c8330182480114e82f618946177843db3138ae69679a54b95cf345ed759450d870aa87bee53800080c080a0151ccc02146b9b11adf516e6787b59acae3e76544fdcd75e77e67c6b598ce65da064c5dd5aae2fbb535830ebbdad0234975cd7ece3562013b63ea18cc0df6c97d4").unwrap()).unwrap(), + Transaction::decode(&hex::decode("f86d80843baa0c4082f618946177843db3138ae69679a54b95cf345ed759450d870aa87bee538000808360306ba0151ccc02146b9b11adf516e6787b59acae3e76544fdcd75e77e67c6b598ce65da064c5dd5aae2fbb535830ebbdad0234975cd7ece3562013b63ea18cc0df6c97d4").unwrap()).unwrap()], + ommers: Default::default(), + withdrawals: Default::default(), + }; + (block_header, block_body) + } } diff --git a/crates/storage/src/libmdbx.rs b/crates/storage/src/libmdbx.rs index 23a5fcb2602..66daa6b61cb 100644 --- a/crates/storage/src/libmdbx.rs +++ b/crates/storage/src/libmdbx.rs @@ -5,7 +5,7 @@ use crate::rlp::{ AccountStorageValueRLP, AddressRLP, BlockBodyRLP, BlockHeaderRLP, ReceiptRLP, }; use anyhow::Result; -use ethereum_rust_core::types::AccountInfo; +use ethereum_rust_core::types::{AccountInfo, BlockBody, BlockHeader}; use ethereum_rust_core::types::{BlockNumber, Index}; use ethereum_types::Address; use libmdbx::{ @@ -57,6 +57,68 @@ impl StoreEngine for Store { } } + fn add_block_header( + &mut self, + block_number: BlockNumber, + block_header: BlockHeader, + ) -> std::result::Result<(), StoreError> { + // Write block header to mdbx + { + let txn = self.db.begin_readwrite().unwrap(); + match txn.upsert::(block_number, block_header.into()) { + Ok(_) => txn.commit().unwrap(), + Err(err) => return Err(StoreError::LibmdbxError(err)), + } + } + Ok(()) + } + + fn get_block_header( + &self, + block_number: BlockNumber, + ) -> std::result::Result, StoreError> { + // Read block header from mdbx + let read_value = { + let txn = self.db.begin_read().unwrap(); + txn.get::(block_number) + }; + match read_value { + Ok(value) => Ok(value.map(|a| a.to())), + Err(err) => Err(StoreError::LibmdbxError(err)), + } + } + + fn add_block_body( + &mut self, + block_number: BlockNumber, + block_body: BlockBody, + ) -> std::result::Result<(), StoreError> { + // Write block body to mdbx + { + let txn = self.db.begin_readwrite().unwrap(); + match txn.upsert::(block_number, block_body.into()) { + Ok(_) => txn.commit().unwrap(), + Err(err) => return Err(StoreError::LibmdbxError(err)), + } + } + Ok(()) + } + + fn get_block_body( + &self, + block_number: BlockNumber, + ) -> std::result::Result, StoreError> { + // Read block body from mdbx + let read_value = { + let txn = self.db.begin_read().unwrap(); + txn.get::(block_number) + }; + match read_value { + Ok(value) => Ok(value.map(|a| a.to())), + Err(err) => Err(StoreError::LibmdbxError(err)), + } + } + fn set_value(&mut self, _key: Key, _value: Value) -> Result<(), StoreError> { todo!() } diff --git a/crates/storage/src/rocksdb.rs b/crates/storage/src/rocksdb.rs index 25ea1b6afc9..1bc8d771ca3 100644 --- a/crates/storage/src/rocksdb.rs +++ b/crates/storage/src/rocksdb.rs @@ -1,7 +1,7 @@ use super::{Key, StoreEngine, Value}; use crate::error::StoreError; use crate::rlp::{AccountInfoRLP, AddressRLP}; -use ethereum_rust_core::types::AccountInfo; +use ethereum_rust_core::types::{AccountInfo, BlockBody, BlockHeader, BlockNumber}; use ethereum_types::Address; use libmdbx::orm::{Decodable, Encodable}; use std::fmt::Debug; @@ -106,6 +106,33 @@ impl StoreEngine for Store { }) } + fn add_block_header( + &mut self, + _block_number: BlockNumber, + _block_header: BlockHeader, + ) -> Result<(), StoreError> { + todo!() + } + + fn get_block_header( + &self, + _block_number: BlockNumber, + ) -> Result, StoreError> { + todo!() + } + + fn add_block_body( + &mut self, + _block_number: BlockNumber, + _block_body: BlockBody, + ) -> Result<(), StoreError> { + todo!() + } + + fn get_block_body(&self, _block_number: BlockNumber) -> Result, StoreError> { + todo!() + } + fn set_value(&mut self, key: Key, value: Value) -> Result<(), StoreError> { let (reply_sender, reply_receiver) = sync_channel(0); self.command_sender.send(StoreCommand::Put( diff --git a/crates/storage/src/sled.rs b/crates/storage/src/sled.rs index a606b4b52b2..5a3b78f2e66 100644 --- a/crates/storage/src/sled.rs +++ b/crates/storage/src/sled.rs @@ -1,7 +1,7 @@ use super::{Key, StoreEngine, Value}; use crate::error::StoreError; use crate::rlp::{AccountInfoRLP, AddressRLP}; -use ethereum_rust_core::types::AccountInfo; +use ethereum_rust_core::types::{AccountInfo, BlockBody, BlockHeader, BlockNumber}; use ethereum_types::Address; use libmdbx::orm::{Decodable, Encodable}; use sled::Db; @@ -46,6 +46,33 @@ impl StoreEngine for Store { }) } + fn add_block_header( + &mut self, + _block_number: BlockNumber, + _block_header: BlockHeader, + ) -> Result<(), StoreError> { + todo!() + } + + fn get_block_header( + &self, + _block_number: BlockNumber, + ) -> Result, StoreError> { + todo!() + } + + fn add_block_body( + &mut self, + _block_number: BlockNumber, + _block_body: BlockBody, + ) -> Result<(), StoreError> { + todo!() + } + + fn get_block_body(&self, _block_number: BlockNumber) -> Result, StoreError> { + todo!() + } + fn set_value(&mut self, key: Key, value: Value) -> Result<(), StoreError> { let _ = self.values.insert(key, value); Ok(())