From e7b19516c0e0673644aa515e113c177bbf28cdc3 Mon Sep 17 00:00:00 2001 From: Kolby Moroz Liebl <31669092+KolbyML@users.noreply.github.com> Date: Tue, 29 Oct 2024 15:51:39 -0600 Subject: [PATCH] feat: implement new trie_walker which can iterate full+partial tries --- portal-bridge/src/bridge/state.rs | 19 +- trin-execution/src/lib.rs | 2 +- trin-execution/src/trie_walker.rs | 199 ----------- trin-execution/src/walkers/memory_db.rs | 47 +++ trin-execution/src/walkers/mod.rs | 2 + trin-execution/src/walkers/trie_walker.rs | 377 +++++++++++++++++++++ trin-execution/tests/content_generation.rs | 19 +- 7 files changed, 448 insertions(+), 217 deletions(-) delete mode 100644 trin-execution/src/trie_walker.rs create mode 100644 trin-execution/src/walkers/memory_db.rs create mode 100644 trin-execution/src/walkers/mod.rs create mode 100644 trin-execution/src/walkers/trie_walker.rs diff --git a/portal-bridge/src/bridge/state.rs b/portal-bridge/src/bridge/state.rs index 3fd5dae8c..fcf025187 100644 --- a/portal-bridge/src/bridge/state.rs +++ b/portal-bridge/src/bridge/state.rs @@ -25,9 +25,9 @@ use trin_execution::{ create_contract_content_value, create_storage_content_key, create_storage_content_value, }, execution::TrinExecution, - trie_walker::TrieWalker, types::{block_to_trace::BlockToTrace, trie_proof::TrieProof}, utils::full_nibble_path_to_address_hash, + walkers::{memory_db::ReadOnlyMemoryDB, trie_walker::TrieWalker}, }; use trin_metrics::bridge::BridgeMetricsReporter; use trin_utils::dir::create_temp_dir; @@ -159,13 +159,14 @@ impl StateBridge { .header .hash(); - let walk_diff = TrieWalker::new(root_with_trie_diff.root, root_with_trie_diff.trie_diff); + let walk_diff = TrieWalker::new_partial_trie( + root_with_trie_diff.root, + ReadOnlyMemoryDB::new(root_with_trie_diff.trie_diff), + )?; // gossip block's new state transitions let mut content_idx = 0; - for node in walk_diff.nodes.keys() { - let account_proof = walk_diff.get_proof(*node); - + for account_proof in walk_diff { // gossip the account self.gossip_account(&account_proof, block_hash, content_idx) .await?; @@ -213,10 +214,12 @@ impl StateBridge { // gossip contract storage let storage_changed_nodes = trin_execution.database.get_storage_trie_diff(address_hash); - let storage_walk_diff = TrieWalker::new(account.storage_root, storage_changed_nodes); + let storage_walk_diff = TrieWalker::new_partial_trie( + account.storage_root, + ReadOnlyMemoryDB::new(storage_changed_nodes), + )?; - for storage_node in storage_walk_diff.nodes.keys() { - let storage_proof = storage_walk_diff.get_proof(*storage_node); + for storage_proof in storage_walk_diff { self.gossip_storage( &account_proof, &storage_proof, diff --git a/trin-execution/src/lib.rs b/trin-execution/src/lib.rs index 4809cb3aa..cce74cefa 100644 --- a/trin-execution/src/lib.rs +++ b/trin-execution/src/lib.rs @@ -8,6 +8,6 @@ pub mod execution; pub mod metrics; pub mod storage; pub mod subcommands; -pub mod trie_walker; pub mod types; pub mod utils; +pub mod walkers; diff --git a/trin-execution/src/trie_walker.rs b/trin-execution/src/trie_walker.rs deleted file mode 100644 index 4c05359e0..000000000 --- a/trin-execution/src/trie_walker.rs +++ /dev/null @@ -1,199 +0,0 @@ -use std::collections::VecDeque; - -use alloy::{consensus::EMPTY_ROOT_HASH, primitives::B256}; -use eth_trie::{decode_node, node::Node}; -use hashbrown::HashMap as BrownHashMap; -use serde::{Deserialize, Serialize}; - -use super::types::trie_proof::TrieProof; - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] -pub struct TrieWalkerNode { - /// The encoded version of the trie node. - pub encoded_node: Vec, - /// The hash of the parent node. It is `None` only for root of the trie. - pub parent_hash: Option, - /// Path from parent node to this node. - pub path_nibbles: Vec, -} - -impl TrieWalkerNode { - pub fn new(encoded_node: Vec, parent_hash: Option, path_nibbles: Vec) -> Self { - Self { - encoded_node, - parent_hash, - path_nibbles, - } - } -} - -/// This struct takes in a root hash and a hashmap of changed nodes, then you can call an iterator -/// which will return every proof to gossip -pub struct TrieWalker { - pub nodes: BrownHashMap, -} - -impl TrieWalker { - pub fn new(root_hash: B256, nodes: BrownHashMap>) -> Self { - // if the storage root is empty then there is no storage to gossip - if root_hash == EMPTY_ROOT_HASH { - return Self { - nodes: BrownHashMap::new(), - }; - } - - if nodes.is_empty() { - return Self { - nodes: BrownHashMap::new(), - }; - } - - let processed_nodes = Self::process_trie(root_hash, &nodes) - .expect("This shouldn't fail as we only pass valid tries"); - Self { - nodes: processed_nodes, - } - } - - fn process_trie( - root_hash: B256, - nodes: &BrownHashMap>, - ) -> anyhow::Result> { - let mut trie_walker_nodes: BrownHashMap = BrownHashMap::new(); - let mut stack = vec![root_hash]; - - trie_walker_nodes.insert( - root_hash, - TrieWalkerNode::new( - nodes - .get(&root_hash) - .expect("Failed to get encoded node for root node. This should never happen.") - .clone(), - None, - vec![], - ), - ); - while let Some(node_key) = stack.pop() { - let encoded_node = nodes - .get(&node_key) - .expect("The stack should only contain nodes that are in the changed nodes"); - - let decoded_node = decode_node(&mut encoded_node.as_slice()) - .expect("Should should only be passing valid encoded nodes"); - - match decoded_node { - Node::Extension(extension) => { - let extension = extension.read().expect("Reading an extension should work"); - // We look for hash nodes in order to connect them to the root. If this node is - // not a hash node, then neither is any of its children. - // We know this because any node that has a hash node as it's descendant would - // also become hash node, because its encoding would be longer than 32 bytes. - if let Node::Hash(hash_node) = &extension.node { - // Only process provided nodes (they belong to the partial trie that we care - // about) - if let Some(encoded_node) = nodes.get(&hash_node.hash) { - stack.push(hash_node.hash); - trie_walker_nodes.insert( - hash_node.hash, - TrieWalkerNode::new( - encoded_node.clone(), - Some(node_key), - extension.prefix.get_data().to_vec(), - ), - ); - } - } - } - Node::Branch(branch) => { - let branch = branch.read().expect("Reading a branch should work"); - for (i, child) in branch.children.iter().enumerate() { - // We look for hash nodes in order to connect them to the root. If this node - // is not a hash node, then neither is any of its children. - // We know this because any node that has a hash node as it's descendant - // would also become hash node, because its encoding would be longer than 32 - // bytes. - if let Node::Hash(hash_node) = child { - //Only process provided nodes (they belong to the partial trie that we - // care about) - if let Some(encoded_node) = nodes.get(&hash_node.hash) { - stack.push(hash_node.hash); - trie_walker_nodes.insert( - hash_node.hash, - TrieWalkerNode::new( - encoded_node.clone(), - Some(node_key), - vec![i as u8], - ), - ); - } - } - } - } - _ => {} - } - } - - Ok(trie_walker_nodes) - } - - pub fn get_proof(&self, node_hash: B256) -> TrieProof { - let mut path_parts = VecDeque::new(); - let mut proof = VecDeque::new(); - let mut next_node: Option = Some(node_hash); - while let Some(current_node) = next_node { - let Some(node) = self.nodes.get(¤t_node) else { - panic!("Node not found in trie walker nodes. This should never happen."); - }; - path_parts.push_front(node.path_nibbles.clone()); - proof.push_front(node.encoded_node.clone().into()); - next_node = node.parent_hash; - } - - TrieProof { - path: Vec::from(path_parts).concat(), - proof: Vec::from(proof), - } - } -} - -#[cfg(test)] -mod tests { - use std::str::FromStr; - - use alloy::primitives::{keccak256, Address, Bytes}; - use eth_trie::{RootWithTrieDiff, Trie}; - use trin_utils::dir::create_temp_test_dir; - - use crate::{config::StateConfig, execution::TrinExecution, trie_walker::TrieWalker}; - - #[tokio::test] - #[ignore = "This test downloads data from a remote server"] - async fn test_trie_walker_builds_valid_proof() { - let temp_directory = create_temp_test_dir().unwrap(); - let mut trin_execution = TrinExecution::new(temp_directory.path(), StateConfig::default()) - .await - .unwrap(); - let RootWithTrieDiff { trie_diff, .. } = trin_execution.process_next_block().await.unwrap(); - let root_hash = trin_execution.get_root().unwrap(); - let walk_diff = TrieWalker::new(root_hash, trie_diff); - - let address = Address::from_str("0x001d14804b399c6ef80e64576f657660804fec0b").unwrap(); - let valid_proof = trin_execution - .database - .trie - .lock() - .get_proof(keccak256(address).as_slice()) - .unwrap() - .into_iter() - .map(Bytes::from) - .collect::>(); - let last_node = valid_proof.last().expect("Missing proof!"); - - let account_proof = walk_diff.get_proof(keccak256(last_node)); - - assert_eq!(account_proof.path, [5, 9, 2, 13]); - assert_eq!(account_proof.proof, valid_proof); - - temp_directory.close().unwrap(); - } -} diff --git a/trin-execution/src/walkers/memory_db.rs b/trin-execution/src/walkers/memory_db.rs new file mode 100644 index 000000000..c409f87bc --- /dev/null +++ b/trin-execution/src/walkers/memory_db.rs @@ -0,0 +1,47 @@ +use std::sync::Arc; + +use anyhow::anyhow; +use eth_trie::DB; +use hashbrown::HashMap; +use parking_lot::Mutex; +use revm_primitives::B256; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum ReadOnlyMemoryDBError { + #[error("read only memory db error {0}")] + ANYHOW(#[from] anyhow::Error), +} + +#[derive(Debug)] +pub struct ReadOnlyMemoryDB { + storage: Arc>>>, +} + +impl ReadOnlyMemoryDB { + pub fn new(storage: HashMap>) -> Self { + ReadOnlyMemoryDB { + storage: Arc::new(Mutex::new(storage)), + } + } +} + +impl DB for ReadOnlyMemoryDB { + type Error = ReadOnlyMemoryDBError; + + fn get(&self, key: &[u8]) -> Result>, Self::Error> { + Ok(self.storage.lock().get(key).cloned()) + } + + fn insert(&self, _key: &[u8], _value: Vec) -> Result<(), Self::Error> { + Err(anyhow!("Cannot insert into read only memory db").into()) + } + + fn remove(&self, _key: &[u8]) -> Result<(), Self::Error> { + Err(anyhow!("Cannot remove from read only memory db").into()) + } + + fn flush(&self) -> Result<(), Self::Error> { + Err(anyhow!("Cannot flush read only memory db").into()) + } +} diff --git a/trin-execution/src/walkers/mod.rs b/trin-execution/src/walkers/mod.rs new file mode 100644 index 000000000..9b13e995f --- /dev/null +++ b/trin-execution/src/walkers/mod.rs @@ -0,0 +1,2 @@ +pub mod memory_db; +pub mod trie_walker; diff --git a/trin-execution/src/walkers/trie_walker.rs b/trin-execution/src/walkers/trie_walker.rs new file mode 100644 index 000000000..3f4ef14c5 --- /dev/null +++ b/trin-execution/src/walkers/trie_walker.rs @@ -0,0 +1,377 @@ +use std::sync::Arc; + +use alloy::consensus::EMPTY_ROOT_HASH; +use eth_trie::{decode_node, node::Node, DB}; +use revm_primitives::{Bytes, B256}; + +use crate::types::trie_proof::TrieProof; + +/// This is used for walking the whole state trie and partial tries (forward state diffs) +/// use cases are +/// 1. gossiping the whole state trie +/// 2. gossiping the forward state diffs +/// 3. getting stats about the state trie +pub struct TrieWalker { + is_partial_trie: bool, + trie: Arc, + stack: Vec, +} + +impl TrieWalker { + pub fn new(root_hash: B256, trie: Arc) -> anyhow::Result { + let root_value = trie + .get(root_hash.as_slice()) + .expect("Failed to read node from") + .expect("Root node not found") + .into(); + let root_proof = TrieProof { + path: vec![], + proof: vec![root_value], + }; + + Ok(Self { + is_partial_trie: false, + trie, + stack: vec![root_proof], + }) + } + + pub fn new_partial_trie(root_hash: B256, trie: TrieDB) -> anyhow::Result { + // if the storage root is empty then there is no storage to gossip + if root_hash == EMPTY_ROOT_HASH { + return Ok(Self { + is_partial_trie: true, + trie: Arc::new(trie), + stack: vec![], + }); + } + + let root_value = match trie + .get(root_hash.as_slice()) + .expect("Failed to read node from") + { + Some(root_value) => root_value.into(), + None => { + // The trie db is empty so we can't walk it return an empty iterator + return Ok(Self { + is_partial_trie: true, + trie: Arc::new(trie), + stack: vec![], + }); + } + }; + + let root_proof = TrieProof { + path: vec![], + proof: vec![root_value], + }; + + Ok(Self { + is_partial_trie: true, + trie: Arc::new(trie), + stack: vec![root_proof], + }) + } + + fn process_node( + &mut self, + node: Node, + partial_proof: Vec, + path: Vec, + ) -> anyhow::Result<()> { + // We only need to process hash nodes, because if the node isn't a hash node the leaf is + // already embedded in the proof + if let Node::Hash(hash) = node { + let value_result = self + .trie + .get(hash.hash.as_slice()) + .expect("Failed to read node from the database"); + + let value = match value_result { + Some(value) => value, + None => { + // If we are walking a partial trie, some nodes won't be available in the + // database + if self.is_partial_trie { + return Ok(()); + } + return Err(anyhow::anyhow!("Node not found in the database")); + } + }; + let decoded_node = decode_node(&mut value.as_ref())?; + match decoded_node { + Node::Leaf(_) | Node::Extension(_) | Node::Branch(_) => { + self.stack.push(TrieProof { + path, + proof: [partial_proof, vec![value.into()]].concat(), + }); + } + // can't be a hash node because we just decoded it + Node::Empty | Node::Hash(_) => (), + } + } + Ok(()) + } +} + +impl Iterator for TrieWalker { + type Item = TrieProof; + + fn next(&mut self) -> Option { + let next_proof = match self.stack.pop() { + Some(next_proof) => next_proof, + None => return None, + }; + + let TrieProof { path, proof } = &next_proof; + let last_node = proof.last().expect("Proof is empty"); + let decoded_last_node = + decode_node(&mut last_node.as_ref()).expect("Failed to decode node"); + + match decoded_last_node { + Node::Extension(extension) => { + let extension = extension.read().expect("Extension node must be readable"); + self.process_node( + extension.node.clone(), + proof.clone(), + [ + path.as_slice(), + extension.prefix.get_data().to_vec().as_slice(), + ] + .concat(), + ) + .expect("Failed to process node"); + } + Node::Branch(branch) => { + let branch = branch.read().expect("Branch node must be readable"); + + // We don't need to check the branches value as it is already encoded in the proof + + // We want to iterate over the children in reverse order so that we can push them to + // the stack in order + for (i, child) in branch.children.iter().enumerate().rev() { + self.process_node( + child.clone(), + proof.clone(), + [path.as_slice(), &[i as u8]].concat(), + ) + .expect("Failed to process node"); + } + } + // If the node is a leaf node, we don't need to go deeper + _ => {} + } + + Some(next_proof) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + config::StateConfig, + execution::TrinExecution, + storage::{trie_db::TrieRocksDB, utils::setup_rocksdb}, + utils::full_nibble_path_to_address_hash, + walkers::memory_db::ReadOnlyMemoryDB, + }; + use eth_trie::{EthTrie, RootWithTrieDiff, Trie}; + use parking_lot::Mutex; + use revm_primitives::{keccak256, Address, B256, U256}; + use std::{str::FromStr, sync::Arc}; + use tracing_test::traced_test; + use trin_utils::dir::create_temp_test_dir; + + #[tokio::test] + #[traced_test] + async fn test_state_walker() { + let temp_directory = create_temp_test_dir().unwrap(); + let db = Arc::new(setup_rocksdb(temp_directory.path()).unwrap()); + let trie = Arc::new(Mutex::new(EthTrie::new(Arc::new(TrieRocksDB::new( + false, + db.clone(), + ))))); + { + let mut trie = trie.lock(); + trie.insert( + B256::from(U256::from(1)).as_slice(), + B256::from(U256::from(1)).as_slice(), + ) + .unwrap(); + trie.insert( + B256::from(U256::from(2)).as_slice(), + B256::from(U256::from(2)).as_slice(), + ) + .unwrap(); + trie.insert( + B256::from(U256::from(3)).as_slice(), + B256::from(U256::from(3)).as_slice(), + ) + .unwrap(); + trie.insert( + B256::from(U256::from(4)).as_slice(), + B256::from(U256::from(4)).as_slice(), + ) + .unwrap(); + trie.insert( + B256::from(U256::from(5)).as_slice(), + B256::from(U256::from(5)).as_slice(), + ) + .unwrap(); + trie.insert( + B256::from(U256::from(6)).as_slice(), + B256::from(U256::from(6)).as_slice(), + ) + .unwrap(); + trie.insert( + B256::from(U256::from(7)).as_slice(), + B256::from(U256::from(7)).as_slice(), + ) + .unwrap(); + trie.insert( + B256::from(U256::from(8)).as_slice(), + B256::from(U256::from(8)).as_slice(), + ) + .unwrap(); + trie.insert( + B256::from(U256::from(9)).as_slice(), + B256::from(U256::from(9)).as_slice(), + ) + .unwrap(); + trie.insert( + B256::from(U256::from(10)).as_slice(), + B256::from(U256::from(10)).as_slice(), + ) + .unwrap(); + trie.insert( + B256::from(U256::from(11)).as_slice(), + B256::from(U256::from(11)).as_slice(), + ) + .unwrap(); + trie.insert( + B256::from(U256::from(12)).as_slice(), + B256::from(U256::from(12)).as_slice(), + ) + .unwrap(); + trie.insert( + B256::from(U256::from(13)).as_slice(), + B256::from(U256::from(13)).as_slice(), + ) + .unwrap(); + trie.insert( + B256::from(U256::from(14)).as_slice(), + B256::from(U256::from(14)).as_slice(), + ) + .unwrap(); + trie.insert( + B256::from(U256::from(15)).as_slice(), + B256::from(U256::from(15)).as_slice(), + ) + .unwrap(); + trie.insert( + B256::from(U256::from(16)).as_slice(), + B256::from(U256::from(16)).as_slice(), + ) + .unwrap(); + trie.insert( + B256::from(U256::from(17)).as_slice(), + B256::from(U256::from(17)).as_slice(), + ) + .unwrap(); + trie.insert( + B256::from(U256::from(18)).as_slice(), + B256::from(U256::from(18)).as_slice(), + ) + .unwrap(); + trie.root_hash().unwrap(); + } + + let root_hash = trie.lock().root_hash().unwrap(); + let walker = TrieWalker::new(root_hash, trie.lock().db.clone()).unwrap(); + let mut count = 0; + let mut leaf_count = 0; + for proof in walker { + count += 1; + + let Some(encoded_last_node) = proof.proof.last() else { + panic!("Account proof is empty"); + }; + + let Node::Leaf(leaf) = + decode_node(&mut encoded_last_node.as_ref()).expect("Failed to decode node") + else { + continue; + }; + leaf_count += 1; + + // reconstruct the address hash from the path so that we can fetch the + // address from the database + let mut partial_key_path = leaf.key.get_data().to_vec(); + partial_key_path.pop(); + let full_key_path = [&proof.path.clone(), partial_key_path.as_slice()].concat(); + let key = full_nibble_path_to_address_hash(&full_key_path); + let valid_proof = trie + .lock() + .get_proof(key.as_slice()) + .expect("Proof not found"); + assert_eq!(valid_proof, proof.proof); + } + assert_eq!(leaf_count, 18); + assert_eq!(count, 22); + } + + #[tokio::test] + #[ignore = "This test downloads data from a remote server"] + async fn test_trie_walker_builds_valid_proof() { + let temp_directory = create_temp_test_dir().unwrap(); + let mut trin_execution = TrinExecution::new(temp_directory.path(), StateConfig::default()) + .await + .unwrap(); + let RootWithTrieDiff { trie_diff, .. } = trin_execution.process_next_block().await.unwrap(); + let root_hash = trin_execution.get_root().unwrap(); + let walk_diff = + TrieWalker::new_partial_trie(root_hash, ReadOnlyMemoryDB::new(trie_diff)).unwrap(); + + let address = Address::from_str("0x001d14804b399c6ef80e64576f657660804fec0b").unwrap(); + let address_hash = keccak256(address); + let valid_proof = trin_execution + .database + .trie + .lock() + .get_proof(address_hash.as_slice()) + .unwrap() + .into_iter() + .map(Bytes::from) + .collect::>(); + + let mut trie_iter = walk_diff.into_iter(); + let account_proof = loop { + let proof = trie_iter.next().expect("Proof not found"); + let Some(encoded_last_node) = proof.proof.last() else { + panic!("Account proof is empty"); + }; + + let Node::Leaf(leaf) = + decode_node(&mut encoded_last_node.as_ref()).expect("Failed to decode node") + else { + continue; + }; + + // reconstruct the address hash from the path so that we can fetch the + // address from the database + let mut partial_key_path = leaf.key.get_data().to_vec(); + partial_key_path.pop(); + let full_key_path = [&proof.path.clone(), partial_key_path.as_slice()].concat(); + let key = full_nibble_path_to_address_hash(&full_key_path); + if key == address_hash { + break proof; + } + }; + + assert_eq!(account_proof.path, [5, 9, 2, 13]); + assert_eq!(account_proof.proof, valid_proof); + + temp_directory.close().unwrap(); + } +} diff --git a/trin-execution/tests/content_generation.rs b/trin-execution/tests/content_generation.rs index ccee2eba5..d9d31d339 100644 --- a/trin-execution/tests/content_generation.rs +++ b/trin-execution/tests/content_generation.rs @@ -18,9 +18,9 @@ use trin_execution::{ create_contract_content_value, create_storage_content_key, create_storage_content_value, }, execution::TrinExecution, - trie_walker::TrieWalker, types::block_to_trace::BlockToTrace, utils::full_nibble_path_to_address_hash, + walkers::{memory_db::ReadOnlyMemoryDB, trie_walker::TrieWalker}, }; use trin_utils::dir::create_temp_test_dir; @@ -83,7 +83,7 @@ impl Stats { /// Following command should be used for running: /// /// ``` -/// BLOCKS=1000000 cargo test -p trin-execution --test content_generation -- --include-ignored --nocapture +/// BLOCKS=1000000 cargo test --release -p trin-execution --test content_generation -- --include-ignored --nocapture /// ``` #[tokio::test] #[traced_test] @@ -126,10 +126,10 @@ async fn test_we_can_generate_content_key_values_up_to_x() -> Result<()> { "State root doesn't match" ); - let walk_diff = TrieWalker::new(root_hash, changed_nodes); - for node in walk_diff.nodes.keys() { + let walk_diff = + TrieWalker::new_partial_trie(root_hash, ReadOnlyMemoryDB::new(changed_nodes))?; + for account_proof in walk_diff { let block_hash = block.header.hash(); - let account_proof = walk_diff.get_proof(*node); // check account content key/value let content_key = @@ -173,10 +173,11 @@ async fn test_we_can_generate_content_key_values_up_to_x() -> Result<()> { // check contract storage content key/value let storage_changed_nodes = trin_execution.database.get_storage_trie_diff(address_hash); - let storage_walk_diff = TrieWalker::new(account.storage_root, storage_changed_nodes); - for storage_node in storage_walk_diff.nodes.keys() { - let storage_proof = storage_walk_diff.get_proof(*storage_node); - + let storage_walk_diff = TrieWalker::new_partial_trie( + account.storage_root, + ReadOnlyMemoryDB::new(storage_changed_nodes), + )?; + for storage_proof in storage_walk_diff { let content_key = create_storage_content_key(&storage_proof, address_hash) .expect("Content key should be present"); let content_value =