Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(eth-watch): process governor upgrades #247

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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> {
perekopskiy marked this conversation as resolved.
Show resolved Hide resolved
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> {
perekopskiy marked this conversation as resolved.
Show resolved Hide resolved
// 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(),
perekopskiy marked this conversation as resolved.
Show resolved Hide resolved
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>,
perekopskiy marked this conversation as resolved.
Show resolved Hide resolved
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
perekopskiy marked this conversation as resolved.
Show resolved Hide resolved
.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