From d2502941081fb53387881631c2150803e9f559cc Mon Sep 17 00:00:00 2001 From: perekopskiy <53865202+perekopskiy@users.noreply.github.com> Date: Thu, 19 Oct 2023 12:29:12 +0300 Subject: [PATCH] feat(eth-watch): process governor upgrades (#247) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # What ❔ With boojum upgrades will go through governor. PR adds functionality to process such upgrades while leaving old processor as well. ## Why ❔ Prepare for boojum upgrade ## Checklist - [x] PR title corresponds to the body of PR (we generate changelog entries from PRs). - [x] Tests for the changes have been added / updated. - [x] Documentation comments have been added / updated. - [x] Code has been formatted via `zk fmt` and `zk lint`. --- core/lib/config/src/configs/contracts.rs | 2 + core/lib/contracts/src/lib.rs | 20 +- core/lib/types/src/protocol_version.rs | 169 +++++++++++ core/lib/zksync_core/src/eth_watch/client.rs | 48 +++- .../event_processors/governance_upgrades.rs | 119 ++++++++ .../src/eth_watch/event_processors/mod.rs | 1 + core/lib/zksync_core/src/eth_watch/mod.rs | 34 ++- core/lib/zksync_core/src/eth_watch/tests.rs | 266 +++++++++++++++--- core/lib/zksync_core/src/lib.rs | 8 +- 9 files changed, 622 insertions(+), 45 deletions(-) create mode 100644 core/lib/zksync_core/src/eth_watch/event_processors/governance_upgrades.rs diff --git a/core/lib/config/src/configs/contracts.rs b/core/lib/config/src/configs/contracts.rs index ddc26f7bf35..4c3230f7c26 100644 --- a/core/lib/config/src/configs/contracts.rs +++ b/core/lib/config/src/configs/contracts.rs @@ -34,6 +34,7 @@ pub struct ContractsConfig { pub fri_recursion_scheduler_level_vk_hash: H256, pub fri_recursion_node_level_vk_hash: H256, pub fri_recursion_leaf_level_vk_hash: H256, + pub governance_addr: Option
, } impl ContractsConfig { @@ -93,6 +94,7 @@ mod tests { fri_recursion_leaf_level_vk_hash: hash( "0x72167c43a46cf38875b267d67716edc4563861364a3c03ab7aee73498421e828", ), + governance_addr: None, } } diff --git a/core/lib/contracts/src/lib.rs b/core/lib/contracts/src/lib.rs index 1fb12da5531..cdc5b8b0e6a 100644 --- a/core/lib/contracts/src/lib.rs +++ b/core/lib/contracts/src/lib.rs @@ -24,6 +24,8 @@ pub enum ContractLanguage { Yul, } +const GOVERNANCE_CONTRACT_FILE: &str = + "contracts/ethereum/artifacts/cache/solpp-generated-contracts/governance/IGovernance.sol/IGovernance.json"; const ZKSYNC_CONTRACT_FILE: &str = "contracts/ethereum/artifacts/cache/solpp-generated-contracts/zksync/interfaces/IZkSync.sol/IZkSync.json"; const MULTICALL3_CONTRACT_FILE: &str = @@ -50,9 +52,19 @@ fn read_file_to_json_value(path: impl AsRef) -> serde_json::Value { .unwrap_or_else(|e| panic!("Failed to parse file {:?}: {}", path, e)) } +pub fn load_contract_if_present + std::fmt::Debug>(path: P) -> Option { + let zksync_home = std::env::var("ZKSYNC_HOME").unwrap_or_else(|_| ".".into()); + let path = Path::new(&zksync_home).join(path); + path.exists().then(|| { + serde_json::from_value(read_file_to_json_value(&path)["abi"].take()) + .unwrap_or_else(|e| panic!("Failed to parse contract abi from file {:?}: {}", path, e)) + }) +} + pub fn load_contract + std::fmt::Debug>(path: P) -> Contract { - serde_json::from_value(read_file_to_json_value(&path)["abi"].take()) - .unwrap_or_else(|e| panic!("Failed to parse contract abi from file {:?}: {}", path, e)) + load_contract_if_present(&path).unwrap_or_else(|| { + panic!("Failed to load contract from {:?}", path); + }) } pub fn load_sys_contract(contract_name: &str) -> Contract { @@ -69,6 +81,10 @@ pub fn read_contract_abi(path: impl AsRef) -> String { .to_string() } +pub fn governance_contract() -> Option { + load_contract_if_present(GOVERNANCE_CONTRACT_FILE) +} + pub fn zksync_contract() -> Contract { load_contract(ZKSYNC_CONTRACT_FILE) } diff --git a/core/lib/types/src/protocol_version.rs b/core/lib/types/src/protocol_version.rs index afc4868785a..8faa998636c 100644 --- a/core/lib/types/src/protocol_version.rs +++ b/core/lib/types/src/protocol_version.rs @@ -156,6 +156,32 @@ pub struct L1VerifierConfig { pub recursion_scheduler_level_vk_hash: H256, } +/// Represents a call that was made during governance operation. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Call { + /// The address to which the call will be made. + pub target: Address, + /// The amount of Ether (in wei) to be sent along with the call. + pub value: U256, + /// The calldata to be executed on the `target` address. + pub data: Vec, + /// Hash of the corresponding Ethereum transaction. + pub eth_hash: H256, + /// Block in which Ethereum transaction was included. + pub eth_block: u64, +} + +/// Defines the structure of an operation that Governance contract executed. +#[derive(Debug, Clone, Default)] +pub struct GovernanceOperation { + /// An array of `Call` structs, each representing a call to be made during the operation. + pub calls: Vec, + /// The hash of the predecessor operation, that should be executed before this operation. + pub predecessor: H256, + /// The value used for creating unique operation hashes. + pub salt: H256, +} + /// Protocol upgrade proposal from L1. /// Most of the fields are optional meaning if value is none /// then this field is not changed within an upgrade. @@ -408,6 +434,106 @@ impl TryFrom for ProtocolUpgrade { } } +impl TryFrom for GovernanceOperation { + type Error = crate::ethabi::Error; + + fn try_from(event: Log) -> Result { + let call_param_type = ParamType::Tuple(vec![ + ParamType::Address, + ParamType::Uint(256), + ParamType::Bytes, + ]); + + let operation_param_type = ParamType::Tuple(vec![ + ParamType::Array(Box::new(call_param_type)), + ParamType::FixedBytes(32), + ParamType::FixedBytes(32), + ]); + // Decode data. + let mut decoded = decode(&[ParamType::Uint(256), operation_param_type], &event.data.0)?; + // Extract `GovernanceOperation` data. + let mut decoded_governance_operation = decoded.remove(1).into_tuple().unwrap(); + + let eth_hash = event + .transaction_hash + .expect("Event transaction hash is missing"); + let eth_block = event + .block_number + .expect("Event block number is missing") + .as_u64(); + + let calls = decoded_governance_operation.remove(0).into_array().unwrap(); + let predecessor = H256::from_slice( + &decoded_governance_operation + .remove(0) + .into_fixed_bytes() + .unwrap(), + ); + let salt = H256::from_slice( + &decoded_governance_operation + .remove(0) + .into_fixed_bytes() + .unwrap(), + ); + + let calls = calls + .into_iter() + .map(|call| { + let mut decoded_governance_operation = call.into_tuple().unwrap(); + + Call { + target: decoded_governance_operation + .remove(0) + .into_address() + .unwrap(), + value: decoded_governance_operation.remove(0).into_uint().unwrap(), + data: decoded_governance_operation.remove(0).into_bytes().unwrap(), + eth_hash, + eth_block, + } + }) + .collect(); + + Ok(Self { + calls, + predecessor, + salt, + }) + } +} + +impl TryFrom for ProtocolUpgrade { + type Error = crate::ethabi::Error; + + fn try_from(call: Call) -> Result { + // Reuses `ProtocolUpgrade::try_from`. + // `ProtocolUpgrade::try_from` only uses 3 log fields: `data`, `block_number`, `transaction_hash`. Others can be filled with dummy values. + // We build data as `call.data` without first 4 bytes which are for selector + // and append it with `bytes32(0)` for compatibility with old event data. + let data = call + .data + .into_iter() + .skip(4) + .chain(encode(&[Token::FixedBytes(H256::zero().0.to_vec())])) + .collect::>() + .into(); + let log = Log { + address: Default::default(), + topics: Default::default(), + data, + block_hash: Default::default(), + block_number: Some(call.eth_block.into()), + transaction_hash: Some(call.eth_hash), + transaction_index: Default::default(), + log_index: Default::default(), + transaction_log_index: Default::default(), + log_type: Default::default(), + removed: Default::default(), + }; + ProtocolUpgrade::try_from(log) + } +} + #[derive(Debug, Clone, Default)] pub struct ProtocolVersion { /// Protocol version ID @@ -559,3 +685,46 @@ impl From for VmVersion { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn governance_operation_from_log() { + let call_token = Token::Tuple(vec![ + Token::Address(Address::random()), + Token::Uint(U256::zero()), + Token::Bytes(vec![1, 2, 3]), + ]); + let operation_token = Token::Tuple(vec![ + Token::Array(vec![call_token]), + Token::FixedBytes(H256::random().0.to_vec()), + Token::FixedBytes(H256::random().0.to_vec()), + ]); + let event_data = encode(&[Token::Uint(U256::zero()), operation_token]); + + let correct_log = Log { + address: Default::default(), + topics: Default::default(), + data: event_data.into(), + block_hash: Default::default(), + block_number: Some(1u64.into()), + transaction_hash: Some(H256::random()), + transaction_index: Default::default(), + log_index: Default::default(), + transaction_log_index: Default::default(), + log_type: Default::default(), + removed: Default::default(), + }; + let decoded_op: GovernanceOperation = correct_log.clone().try_into().unwrap(); + assert_eq!(decoded_op.calls.len(), 1); + + let mut incorrect_log = correct_log; + incorrect_log + .data + .0 + .truncate(incorrect_log.data.0.len() - 32); + assert!(TryInto::::try_into(incorrect_log).is_err()); + } +} diff --git a/core/lib/zksync_core/src/eth_watch/client.rs b/core/lib/zksync_core/src/eth_watch/client.rs index f8154f14dbc..af38ac79ae7 100644 --- a/core/lib/zksync_core/src/eth_watch/client.rs +++ b/core/lib/zksync_core/src/eth_watch/client.rs @@ -48,6 +48,9 @@ pub struct EthHttpQueryClient { client: E, topics: Vec, zksync_contract_addr: Address, + /// Address of the `Governance` contract. It's optional because it is present only for post-boojum chains. + /// If address is some then client will listen to events coming from it. + governance_address: Option
, verifier_contract_abi: Contract, confirmations_for_eth_event: Option, } @@ -56,13 +59,19 @@ impl EthHttpQueryClient { pub fn new( client: E, zksync_contract_addr: Address, + governance_address: Option
, confirmations_for_eth_event: Option, ) -> Self { - tracing::debug!("New eth client, contract addr: {:x}", zksync_contract_addr); + tracing::debug!( + "New eth client, zkSync addr: {:x}, governance addr: {:?}", + zksync_contract_addr, + governance_address + ); Self { client, topics: Vec::new(), zksync_contract_addr, + governance_address, verifier_contract_abi: verifier_contract(), confirmations_for_eth_event, } @@ -75,7 +84,13 @@ impl EthHttpQueryClient { topics: Vec, ) -> Result, Error> { let filter = FilterBuilder::default() - .address(vec![self.zksync_contract_addr]) + .address( + [Some(self.zksync_contract_addr), self.governance_address] + .iter() + .flatten() + .copied() + .collect(), + ) .from_block(from) .to_block(to) .topics(Some(topics), None, None, None) @@ -88,10 +103,14 @@ impl EthHttpQueryClient { #[async_trait::async_trait] impl EthClient for EthHttpQueryClient { async fn scheduler_vk_hash(&self, verifier_address: Address) -> Result { - let vk_token: Token = self + // This is here for backward compatibility with the old verifier: + // Legacy verifier returns the full verification key; + // New verifier returns the hash of the verification key. + + let vk_hash = self .client .call_contract_function( - "get_verification_key", + "verificationKeyHash", (), None, Default::default(), @@ -99,8 +118,25 @@ impl EthClient for EthHttpQueryClient Self { + Self { + diamond_proxy_address, + last_seen_version_id, + upgrade_proposal_signature: governance_contract + .event("TransparentOperationScheduled") + .expect("TransparentOperationScheduled event is missing in abi") + .signature(), + } + } +} + +#[async_trait::async_trait] +impl EventProcessor for GovernanceUpgradesEventProcessor { + async fn process_events( + &mut self, + storage: &mut StorageProcessor<'_>, + client: &W, + events: Vec, + ) -> Result<(), Error> { + let mut upgrades = Vec::new(); + for event in events + .into_iter() + .filter(|event| event.topics[0] == self.upgrade_proposal_signature) + { + let governance_operation = GovernanceOperation::try_from(event) + .map_err(|err| Error::LogParse(format!("{:?}", err)))?; + // Some calls can target other contracts than Diamond proxy, skip them. + for call in governance_operation + .calls + .into_iter() + .filter(|call| call.target == self.diamond_proxy_address) + { + let upgrade = ProtocolUpgrade::try_from(call) + .map_err(|err| Error::LogParse(format!("{:?}", err)))?; + // Scheduler VK is not present in proposal event. It is hardcoded in verifier contract. + let scheduler_vk_hash = if let Some(address) = upgrade.verifier_address { + Some(client.scheduler_vk_hash(address).await?) + } else { + None + }; + upgrades.push((upgrade, scheduler_vk_hash)); + } + } + + if upgrades.is_empty() { + return Ok(()); + } + + let ids_str: Vec<_> = upgrades + .iter() + .map(|(u, _)| format!("{}", u.id as u16)) + .collect(); + tracing::debug!("Received upgrades with ids: {}", ids_str.join(", ")); + + let new_upgrades: Vec<_> = upgrades + .into_iter() + .skip_while(|(v, _)| v.id as u16 <= self.last_seen_version_id as u16) + .collect(); + if new_upgrades.is_empty() { + return Ok(()); + } + + let last_id = new_upgrades.last().unwrap().0.id; + let stage_start = Instant::now(); + for (upgrade, scheduler_vk_hash) in new_upgrades { + let previous_version = storage + .protocol_versions_dal() + .load_previous_version(upgrade.id) + .await + .unwrap_or_else(|| { + panic!( + "Expected some version preceding {:?} be present in DB", + upgrade.id + ) + }); + let new_version = previous_version.apply_upgrade(upgrade, scheduler_vk_hash); + storage + .protocol_versions_dal() + .save_protocol_version_with_tx(new_version) + .await; + } + metrics::histogram!("eth_watcher.poll_eth_node", stage_start.elapsed(), "stage" => "persist_upgrades"); + + self.last_seen_version_id = last_id; + + Ok(()) + } + + fn relevant_topic(&self) -> H256 { + self.upgrade_proposal_signature + } +} diff --git a/core/lib/zksync_core/src/eth_watch/event_processors/mod.rs b/core/lib/zksync_core/src/eth_watch/event_processors/mod.rs index 70e1db9a3f1..84ea1eeb04c 100644 --- a/core/lib/zksync_core/src/eth_watch/event_processors/mod.rs +++ b/core/lib/zksync_core/src/eth_watch/event_processors/mod.rs @@ -2,6 +2,7 @@ use crate::eth_watch::client::{Error, EthClient}; use zksync_dal::StorageProcessor; use zksync_types::{web3::types::Log, H256}; +pub mod governance_upgrades; pub mod priority_ops; pub mod upgrades; diff --git a/core/lib/zksync_core/src/eth_watch/mod.rs b/core/lib/zksync_core/src/eth_watch/mod.rs index d6494ae040d..26faf89a300 100644 --- a/core/lib/zksync_core/src/eth_watch/mod.rs +++ b/core/lib/zksync_core/src/eth_watch/mod.rs @@ -14,7 +14,8 @@ use zksync_dal::{ConnectionPool, StorageProcessor}; use zksync_eth_client::EthInterface; use zksync_system_constants::PRIORITY_EXPIRATION; use zksync_types::{ - web3::types::BlockNumber as Web3BlockNumber, Address, PriorityOpId, ProtocolVersionId, + ethabi::Contract, web3::types::BlockNumber as Web3BlockNumber, Address, PriorityOpId, + ProtocolVersionId, }; mod client; @@ -26,6 +27,7 @@ mod tests; use self::{ client::{Error, EthClient, EthHttpQueryClient, RETRY_LIMIT}, event_processors::{ + governance_upgrades::GovernanceUpgradesEventProcessor, priority_ops::PriorityOpsEventProcessor, upgrades::UpgradesEventProcessor, EventProcessor, }, metrics::{PollStage, METRICS}, @@ -48,7 +50,13 @@ pub struct EthWatch { } impl EthWatch { - pub async fn new(mut client: W, pool: &ConnectionPool, poll_interval: Duration) -> Self { + pub async fn new( + diamond_proxy_address: Address, + governance_contract: Option, + mut client: W, + pool: &ConnectionPool, + poll_interval: Duration, + ) -> Self { let mut storage = pool.access_storage_tagged("eth_watch").await.unwrap(); let state = Self::initialize_state(&client, &mut storage).await; @@ -58,11 +66,20 @@ impl EthWatch { let priority_ops_processor = PriorityOpsEventProcessor::new(state.next_expected_priority_id); let upgrades_processor = UpgradesEventProcessor::new(state.last_seen_version_id); - let event_processors: Vec>> = vec![ + let mut event_processors: Vec>> = vec![ Box::new(priority_ops_processor), Box::new(upgrades_processor), ]; + if let Some(governance_contract) = governance_contract { + let governance_upgrades_processor = GovernanceUpgradesEventProcessor::new( + diamond_proxy_address, + state.last_seen_version_id, + &governance_contract, + ); + event_processors.push(Box::new(governance_upgrades_processor)) + } + let topics = event_processors .iter() .map(|p| p.relevant_topic()) @@ -174,16 +191,25 @@ pub async fn start_eth_watch( pool: ConnectionPool, eth_gateway: E, diamond_proxy_addr: Address, + governance: Option<(Contract, Address)>, stop_receiver: watch::Receiver, ) -> anyhow::Result>> { let eth_watch = ETHWatchConfig::from_env().context("ETHWatchConfig::from_env()")?; let eth_client = EthHttpQueryClient::new( eth_gateway, diamond_proxy_addr, + governance.as_ref().map(|(_, address)| *address), eth_watch.confirmations_for_eth_event, ); - let mut eth_watch = EthWatch::new(eth_client, &pool, eth_watch.poll_interval()).await; + let mut eth_watch = EthWatch::new( + diamond_proxy_addr, + governance.map(|(contract, _)| contract), + eth_client, + &pool, + eth_watch.poll_interval(), + ) + .await; Ok(tokio::spawn(async move { eth_watch.run(pool, stop_receiver).await diff --git a/core/lib/zksync_core/src/eth_watch/tests.rs b/core/lib/zksync_core/src/eth_watch/tests.rs index d9448efbfab..01fb83b98c0 100644 --- a/core/lib/zksync_core/src/eth_watch/tests.rs +++ b/core/lib/zksync_core/src/eth_watch/tests.rs @@ -10,7 +10,7 @@ use zksync_dal::{ConnectionPool, StorageProcessor}; use zksync_types::protocol_version::{ProtocolUpgradeTx, ProtocolUpgradeTxCommonData}; use zksync_types::web3::types::{Address, BlockNumber}; use zksync_types::{ - ethabi::{encode, Hash, Token}, + ethabi::{encode, Contract, Hash, Token}, l1::{L1Tx, OpProcessingType, PriorityQueueType}, web3::types::Log, Execute, L1TxCommonData, PriorityOpId, ProtocolUpgrade, ProtocolVersion, ProtocolVersionId, @@ -22,7 +22,8 @@ use crate::eth_watch::{client::EthClient, EthWatch}; struct FakeEthClientData { transactions: HashMap>, - upgrades: HashMap>, + diamond_upgrades: HashMap>, + governance_upgrades: HashMap>, last_finalized_block_number: u64, } @@ -30,7 +31,8 @@ impl FakeEthClientData { fn new() -> Self { Self { transactions: Default::default(), - upgrades: Default::default(), + diamond_upgrades: Default::default(), + governance_upgrades: Default::default(), last_finalized_block_number: 0, } } @@ -45,12 +47,21 @@ impl FakeEthClientData { } } - fn add_upgrades(&mut self, upgrades: &[(ProtocolUpgrade, u64)]) { + fn add_diamond_upgrades(&mut self, upgrades: &[(ProtocolUpgrade, u64)]) { for (upgrade, eth_block) in upgrades { - self.upgrades + self.diamond_upgrades .entry(*eth_block) .or_default() - .push(upgrade_into_log(upgrade.clone(), *eth_block)); + .push(upgrade_into_diamond_proxy_log(upgrade.clone(), *eth_block)); + } + } + + fn add_governance_upgrades(&mut self, upgrades: &[(ProtocolUpgrade, u64)]) { + for (upgrade, eth_block) in upgrades { + self.governance_upgrades + .entry(*eth_block) + .or_default() + .push(upgrade_into_governor_log(upgrade.clone(), *eth_block)); } } @@ -75,8 +86,12 @@ impl FakeEthClient { self.inner.write().await.add_transactions(transactions); } - async fn add_upgrades(&mut self, upgrades: &[(ProtocolUpgrade, u64)]) { - self.inner.write().await.add_upgrades(upgrades); + async fn add_diamond_upgrades(&mut self, upgrades: &[(ProtocolUpgrade, u64)]) { + self.inner.write().await.add_diamond_upgrades(upgrades); + } + + async fn add_governance_upgrades(&mut self, upgrades: &[(ProtocolUpgrade, u64)]) { + self.inner.write().await.add_governance_upgrades(upgrades); } async fn set_last_finalized_block_number(&mut self, number: u64) { @@ -113,7 +128,10 @@ impl EthClient for FakeEthClient { if let Some(ops) = self.inner.read().await.transactions.get(&number) { logs.extend_from_slice(ops); } - if let Some(ops) = self.inner.read().await.upgrades.get(&number) { + if let Some(ops) = self.inner.read().await.diamond_upgrades.get(&number) { + logs.extend_from_slice(ops); + } + if let Some(ops) = self.inner.read().await.governance_upgrades.get(&number) { logs.extend_from_slice(ops); } } @@ -190,6 +208,8 @@ async fn test_normal_operation_l1_txs(connection_pool: ConnectionPool) { let mut client = FakeEthClient::new(); let mut watcher = EthWatch::new( + Address::default(), + None, client.clone(), &connection_pool, std::time::Duration::from_nanos(1), @@ -235,6 +255,8 @@ async fn test_normal_operation_upgrades(connection_pool: ConnectionPool) { let mut client = FakeEthClient::new(); let mut watcher = EthWatch::new( + Address::default(), + None, client.clone(), &connection_pool, std::time::Duration::from_nanos(1), @@ -243,7 +265,7 @@ async fn test_normal_operation_upgrades(connection_pool: ConnectionPool) { let mut storage = connection_pool.access_test_storage().await; client - .add_upgrades(&[ + .add_diamond_upgrades(&[ ( ProtocolUpgrade { id: ProtocolVersionId::latest(), @@ -293,6 +315,8 @@ async fn test_gap_in_upgrades(connection_pool: ConnectionPool) { let mut client = FakeEthClient::new(); let mut watcher = EthWatch::new( + Address::default(), + None, client.clone(), &connection_pool, std::time::Duration::from_nanos(1), @@ -301,7 +325,7 @@ async fn test_gap_in_upgrades(connection_pool: ConnectionPool) { let mut storage = connection_pool.access_test_storage().await; client - .add_upgrades(&[( + .add_diamond_upgrades(&[( ProtocolUpgrade { id: ProtocolVersionId::next(), tx: None, @@ -323,6 +347,66 @@ async fn test_gap_in_upgrades(connection_pool: ConnectionPool) { assert_eq!(db_ids[1], next_version); } +#[db_test] +async fn test_normal_operation_governance_upgrades(connection_pool: ConnectionPool) { + setup_db(&connection_pool).await; + + let mut client = FakeEthClient::new(); + let mut watcher = EthWatch::new( + Address::default(), + Some(governance_contract()), + client.clone(), + &connection_pool, + std::time::Duration::from_nanos(1), + ) + .await; + + let mut storage = connection_pool.access_test_storage().await; + client + .add_governance_upgrades(&[ + ( + ProtocolUpgrade { + id: ProtocolVersionId::latest(), + tx: None, + ..Default::default() + }, + 10, + ), + ( + ProtocolUpgrade { + id: ProtocolVersionId::next(), + tx: Some(build_upgrade_tx(ProtocolVersionId::next(), 18)), + ..Default::default() + }, + 18, + ), + ]) + .await; + client.set_last_finalized_block_number(15).await; + // second upgrade will not be processed, as it has less than 5 confirmations + watcher.loop_iteration(&mut storage).await.unwrap(); + + let db_ids = storage.protocol_versions_dal().all_version_ids().await; + // there should be genesis version and just added version + assert_eq!(db_ids.len(), 2); + assert_eq!(db_ids[1], ProtocolVersionId::latest()); + + client.set_last_finalized_block_number(20).await; + // now the second upgrade will be processed + watcher.loop_iteration(&mut storage).await.unwrap(); + let db_ids = storage.protocol_versions_dal().all_version_ids().await; + assert_eq!(db_ids.len(), 3); + assert_eq!(db_ids[2], ProtocolVersionId::next()); + + // check that tx was saved with the last upgrade + let tx = storage + .protocol_versions_dal() + .get_protocol_upgrade_tx(ProtocolVersionId::next()) + .await + .unwrap(); + assert_eq!(tx.common_data.upgrade_id, ProtocolVersionId::next()); +} + #[db_test] #[should_panic] async fn test_gap_in_single_batch(connection_pool: ConnectionPool) { @@ -330,6 +414,8 @@ async fn test_gap_in_single_batch(connection_pool: ConnectionPool) { let mut client = FakeEthClient::new(); let mut watcher = EthWatch::new( + Address::default(), + None, client.clone(), &connection_pool, std::time::Duration::from_nanos(1), @@ -357,6 +443,8 @@ async fn test_gap_between_batches(connection_pool: ConnectionPool) { let mut client = FakeEthClient::new(); let mut watcher = EthWatch::new( + Address::default(), + None, client.clone(), &connection_pool, std::time::Duration::from_nanos(1), @@ -389,6 +477,8 @@ async fn test_overlapping_batches(connection_pool: ConnectionPool) { let mut client = FakeEthClient::new(); let mut watcher = EthWatch::new( + Address::default(), + None, client.clone(), &connection_pool, std::time::Duration::from_nanos(1), @@ -490,7 +580,72 @@ fn tx_into_log(tx: L1Tx) -> Log { } } -fn upgrade_into_log(upgrade: ProtocolUpgrade, eth_block: u64) -> Log { +fn upgrade_into_diamond_proxy_log(upgrade: ProtocolUpgrade, eth_block: u64) -> Log { + let diamond_cut = upgrade_into_diamond_cut(upgrade); + let data = encode(&[diamond_cut, Token::FixedBytes(vec![0u8; 32])]); + Log { + address: Address::repeat_byte(0x1), + topics: vec![zksync_contract() + .event("ProposeTransparentUpgrade") + .expect("ProposeTransparentUpgrade event is missing in abi") + .signature()], + data: data.into(), + block_hash: Some(H256::repeat_byte(0x11)), + block_number: Some(eth_block.into()), + transaction_hash: Some(H256::random()), + transaction_index: Some(0u64.into()), + log_index: Some(0u64.into()), + transaction_log_index: Some(0u64.into()), + log_type: None, + removed: None, + } +} + +fn upgrade_into_governor_log(upgrade: ProtocolUpgrade, eth_block: u64) -> Log { + let diamond_cut = upgrade_into_diamond_cut(upgrade); + let execute_upgrade_selector = zksync_contract() + .function("executeUpgrade") + .unwrap() + .short_signature(); + let diamond_upgrade_calldata = execute_upgrade_selector + .iter() + .copied() + .chain(encode(&[diamond_cut])) + .collect(); + let governance_call = Token::Tuple(vec![ + Token::Address(Default::default()), + Token::Uint(U256::default()), + Token::Bytes(diamond_upgrade_calldata), + ]); + let governance_operation = Token::Tuple(vec![ + Token::Array(vec![governance_call]), + Token::FixedBytes(vec![0u8; 32]), + Token::FixedBytes(vec![0u8; 32]), + ]); + let final_data = encode(&[Token::FixedBytes(vec![0u8; 32]), governance_operation]); + + Log { + address: Address::repeat_byte(0x1), + topics: vec![ + governance_contract() + .event("TransparentOperationScheduled") + .expect("TransparentOperationScheduled event is missing in abi") + .signature(), + Default::default(), + ], + data: final_data.into(), + block_hash: Some(H256::repeat_byte(0x11)), + block_number: Some(eth_block.into()), + transaction_hash: Some(H256::random()), + transaction_index: Some(0u64.into()), + log_index: Some(0u64.into()), + transaction_log_index: Some(0u64.into()), + log_type: None, + removed: None, + } +} + +fn upgrade_into_diamond_cut(upgrade: ProtocolUpgrade) -> Token { let tx_data_token = if let Some(tx) = upgrade.tx { Token::Tuple(vec![ Token::Uint(0xfe.into()), @@ -592,7 +747,7 @@ fn upgrade_into_log(upgrade: ProtocolUpgrade, eth_block: u64) -> Log { Token::Address(Default::default()), ]); - let final_token = Token::Tuple(vec![ + Token::Tuple(vec![ Token::Array(vec![]), Token::Address(Default::default()), Token::Bytes( @@ -601,25 +756,7 @@ fn upgrade_into_log(upgrade: ProtocolUpgrade, eth_block: u64) -> Log { .chain(encode(&[upgrade_token])) .collect(), ), - ]); - - let data = encode(&[final_token, Token::FixedBytes(vec![0u8; 32])]); - Log { - address: Address::repeat_byte(0x1), - topics: vec![zksync_contract() - .event("ProposeTransparentUpgrade") - .expect("ProposeTransparentUpgrade event is missing in abi") - .signature()], - data: data.into(), - block_hash: Some(H256::repeat_byte(0x11)), - block_number: Some(eth_block.into()), - transaction_hash: Some(H256::random()), - transaction_index: Some(0u64.into()), - log_index: Some(0u64.into()), - transaction_log_index: Some(0u64.into()), - log_type: None, - removed: None, - } + ]) } async fn setup_db(connection_pool: &ConnectionPool) { @@ -633,3 +770,68 @@ async fn setup_db(connection_pool: &ConnectionPool) { }) .await; } + +fn governance_contract() -> Contract { + let json = r#"[ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "_id", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "delay", + "type": "uint256" + }, + { + "components": [ + { + "components": [ + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "internalType": "struct IGovernance.Call[]", + "name": "calls", + "type": "tuple[]" + }, + { + "internalType": "bytes32", + "name": "predecessor", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "salt", + "type": "bytes32" + } + ], + "indexed": false, + "internalType": "struct IGovernance.Operation", + "name": "_operation", + "type": "tuple" + } + ], + "name": "TransparentOperationScheduled", + "type": "event" + } + ]"#; + serde_json::from_str(json).unwrap() +} diff --git a/core/lib/zksync_core/src/lib.rs b/core/lib/zksync_core/src/lib.rs index fd5714d0436..319a9aae1dd 100644 --- a/core/lib/zksync_core/src/lib.rs +++ b/core/lib/zksync_core/src/lib.rs @@ -28,7 +28,7 @@ use zksync_config::{ ApiConfig, ContractsConfig, DBConfig, ETHClientConfig, ETHSenderConfig, FetcherConfig, ProverConfigs, }; -use zksync_contracts::BaseSystemContracts; +use zksync_contracts::{governance_contract, BaseSystemContracts}; use zksync_dal::{ connection::DbVariant, healthcheck::ConnectionPoolHealthCheck, ConnectionPool, StorageProcessor, }; @@ -500,11 +500,17 @@ pub async fn initialize_components( .build() .await .context("failed to build eth_watch_pool")?; + let governance = contracts_config.governance_addr.map(|addr| { + let contract = governance_contract() + .expect("Governance contract must be present if governance_addr is set in config"); + (contract, addr) + }); task_futures.push( start_eth_watch( eth_watch_pool, query_client.clone(), main_zksync_contract_address, + governance, stop_receiver.clone(), ) .await