Skip to content

Commit

Permalink
feat(eth-watch): process governor upgrades (#247)
Browse files Browse the repository at this point in the history
# 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

<!-- Check your PR fulfills the following items. -->
<!-- For draft PRs check the boxes as you complete them. -->

- [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`.
  • Loading branch information
perekopskiy committed Oct 19, 2023
1 parent a135127 commit d250294
Show file tree
Hide file tree
Showing 9 changed files with 622 additions and 45 deletions.
2 changes: 2 additions & 0 deletions core/lib/config/src/configs/contracts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Address>,
}

impl ContractsConfig {
Expand Down Expand Up @@ -93,6 +94,7 @@ mod tests {
fri_recursion_leaf_level_vk_hash: hash(
"0x72167c43a46cf38875b267d67716edc4563861364a3c03ab7aee73498421e828",
),
governance_addr: None,
}
}

Expand Down
20 changes: 18 additions & 2 deletions core/lib/contracts/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand All @@ -50,9 +52,19 @@ fn read_file_to_json_value(path: impl AsRef<Path>) -> serde_json::Value {
.unwrap_or_else(|e| panic!("Failed to parse file {:?}: {}", path, e))
}

pub fn load_contract_if_present<P: AsRef<Path> + std::fmt::Debug>(path: P) -> Option<Contract> {
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<P: AsRef<Path> + 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 {
Expand All @@ -69,6 +81,10 @@ pub fn read_contract_abi(path: impl AsRef<Path>) -> String {
.to_string()
}

pub fn governance_contract() -> Option<Contract> {
load_contract_if_present(GOVERNANCE_CONTRACT_FILE)
}

pub fn zksync_contract() -> Contract {
load_contract(ZKSYNC_CONTRACT_FILE)
}
Expand Down
169 changes: 169 additions & 0 deletions core/lib/types/src/protocol_version.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u8>,
/// 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<Call>,
/// 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.
Expand Down Expand Up @@ -408,6 +434,106 @@ impl TryFrom<Log> for ProtocolUpgrade {
}
}

impl TryFrom<Log> for GovernanceOperation {
type Error = crate::ethabi::Error;

fn try_from(event: Log) -> Result<Self, Self::Error> {
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<Call> for ProtocolUpgrade {
type Error = crate::ethabi::Error;

fn try_from(call: Call) -> Result<Self, Self::Error> {
// 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::<Vec<u8>>()
.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
Expand Down Expand Up @@ -559,3 +685,46 @@ impl From<ProtocolVersionId> 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::<GovernanceOperation>::try_into(incorrect_log).is_err());
}
}
48 changes: 42 additions & 6 deletions core/lib/zksync_core/src/eth_watch/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ pub struct EthHttpQueryClient<E> {
client: E,
topics: Vec<H256>,
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<Address>,
verifier_contract_abi: Contract,
confirmations_for_eth_event: Option<u64>,
}
Expand All @@ -56,13 +59,19 @@ impl<E: EthInterface> EthHttpQueryClient<E> {
pub fn new(
client: E,
zksync_contract_addr: Address,
governance_address: Option<Address>,
confirmations_for_eth_event: Option<u64>,
) -> 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,
}
Expand All @@ -75,7 +84,13 @@ impl<E: EthInterface> EthHttpQueryClient<E> {
topics: Vec<H256>,
) -> Result<Vec<Log>, 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)
Expand All @@ -88,19 +103,40 @@ impl<E: EthInterface> EthHttpQueryClient<E> {
#[async_trait::async_trait]
impl<E: EthInterface + Send + Sync + 'static> EthClient for EthHttpQueryClient<E> {
async fn scheduler_vk_hash(&self, verifier_address: Address) -> Result<H256, Error> {
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(),
None,
verifier_address,
self.verifier_contract_abi.clone(),
)
.await?;
Ok(l1_vk_commitment(vk_token))
.await;

if let Ok(Token::FixedBytes(vk_hash)) = vk_hash {
Ok(H256::from_slice(&vk_hash))
} else {
let vk = self
.client
.call_contract_function(
"get_verification_key",
(),
None,
Default::default(),
None,
verifier_address,
self.verifier_contract_abi.clone(),
)
.await?;
Ok(l1_vk_commitment(vk))
}
}

async fn get_events(
Expand Down

0 comments on commit d250294

Please sign in to comment.