From a4e91dd1fe20639314f9441185da6cb8ec0d598f Mon Sep 17 00:00:00 2001 From: Fedor Sakharov Date: Tue, 16 Jan 2024 19:09:28 +0100 Subject: [PATCH] fix(finalizer): introduce an optional threshold to eth withdrawals (#348) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # What ❔ ## Why ❔ ## Checklist - [ ] PR title corresponds to the body of PR (we generate changelog entries from PRs). - [ ] Tests for the changes have been added / updated. - [ ] Documentation comments have been added / updated. - [ ] Code has been formatted via `cargo fmt`. --- README.md | 1 + bin/withdrawal-finalizer/src/config.rs | 3 ++ bin/withdrawal-finalizer/src/main.rs | 7 +++++ finalizer/src/lib.rs | 13 +++++++-- ...9d0503e6498288f4f40911dd1b2decb4a327.json} | 7 +++-- ...a785f5128591f617d214d5929152d3469800.json} | 7 +++-- ...f1622e1f15489530ccf7fd8a93088f4b6bf7.json} | 7 +++-- storage/src/lib.rs | 29 ++++++++++++++++++- 8 files changed, 62 insertions(+), 12 deletions(-) rename storage/.sqlx/{query-c16ff516b3b4c276e35d802175f6b8c3a33826b9d8c3425153b3c254198d7af8.json => query-70c7bb78bf525cf29c0d5bd1c3a59d0503e6498288f4f40911dd1b2decb4a327.json} (84%) rename storage/.sqlx/{query-1429bfeafe86ef0ce1449e07709bcce578fe6c917592a00654294c881dcc609a.json => query-e3bccab1832ae37d7a5a274474f9a785f5128591f617d214d5929152d3469800.json} (84%) rename storage/.sqlx/{query-0296c80dfb2f21e40580333a071f07e0c0b25eb548cc83f9386c810847a3aa65.json => query-f041b2f10b01842b3315f7d0354bf1622e1f15489530ccf7fd8a93088f4b6bf7.json} (84%) diff --git a/README.md b/README.md index 250467d2..3b250281 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ Deployment is done by deploying a dockerized image of the service. | `CUSTOM_TOKEN_DEPLOYER_ADDRESSES` | (Optional) Normally ERC20 tokens are deployed by the bridge contract. However, in custom cases it may be necessary to override that behavior with a custom set of addresses that have deployed tokens | | `CUSTOM_TOKEN_ADDRESSES` | (Optional) Adds a predefined list of tokens to finalize. May be useful in case of custom bridge setups when the regular technique of finding token deployments does not work. | | `ENABLE_WITHDRAWAL_METERING` | (Optional, default: `"true"`) By default Finalizer collects metrics about withdrawn token volumens. Users may optionally switch off this metering. | +| `ETH_FINALIZATION_THRESHOLD`| (Optional, default: "0") Finalizer will only finalize ETH withdrawals that are greater or equal to this value | The configuration structure describing the service config can be found in [`config.rs`](https://github.com/matter-labs/zksync-withdrawal-finalizer/blob/main/bin/withdrawal-finalizer/src/config.rs) diff --git a/bin/withdrawal-finalizer/src/config.rs b/bin/withdrawal-finalizer/src/config.rs index 01e64bfb..5b7912c0 100644 --- a/bin/withdrawal-finalizer/src/config.rs +++ b/bin/withdrawal-finalizer/src/config.rs @@ -83,6 +83,9 @@ pub struct Config { #[envconfig(from = "CUSTOM_TOKEN_ADDRESS_MAPPINGS")] pub custom_token_address_mappings: Option, + + #[envconfig(from = "ETH_FINALIZATION_THRESHOLD")] + pub eth_finalization_threshold: Option, } #[derive(Deserialize, Serialize, Debug, Eq, PartialEq)] diff --git a/bin/withdrawal-finalizer/src/main.rs b/bin/withdrawal-finalizer/src/main.rs index facc0081..b544c9e4 100644 --- a/bin/withdrawal-finalizer/src/main.rs +++ b/bin/withdrawal-finalizer/src/main.rs @@ -283,6 +283,12 @@ async fn main() -> Result<()> { config.batch_finalization_gas_limit, ); + let eth_finalization_threshold = match config.eth_finalization_threshold { + Some(eth_finalization_threshold) => { + Some(ethers::utils::parse_ether(eth_finalization_threshold)?) + } + None => None, + }; let finalizer = finalizer::Finalizer::new( pgpool, one_withdrawal_gas_limit, @@ -294,6 +300,7 @@ async fn main() -> Result<()> { finalizer_account_address, config.tokens_to_finalize.unwrap_or_default(), meter_withdrawals, + eth_finalization_threshold, ); let finalizer_handle = tokio::spawn(finalizer.run(client_l2)); diff --git a/finalizer/src/lib.rs b/finalizer/src/lib.rs index edff5576..f339cf97 100644 --- a/finalizer/src/lib.rs +++ b/finalizer/src/lib.rs @@ -103,6 +103,7 @@ pub struct Finalizer { account_address: Address, withdrawals_meterer: Option, token_list: TokenList, + eth_threshold: Option, } const NO_NEW_WITHDRAWALS_BACKOFF: Duration = Duration::from_secs(5); @@ -132,6 +133,7 @@ where account_address: Address, token_list: TokenList, meter_withdrawals: bool, + eth_threshold: Option, ) -> Self { let withdrawals_meterer = meter_withdrawals.then_some(WithdrawalsMeter::new( pgpool.clone(), @@ -157,6 +159,7 @@ where account_address, withdrawals_meterer, token_list, + eth_threshold, } } @@ -351,14 +354,19 @@ where let try_finalize_these = match &self.token_list { TokenList::All => { - storage::withdrawals_to_finalize(&self.pgpool, self.query_db_pagination_limit) - .await? + storage::withdrawals_to_finalize( + &self.pgpool, + self.query_db_pagination_limit, + self.eth_threshold, + ) + .await? } TokenList::WhiteList(w) => { storage::withdrawals_to_finalize_with_whitelist( &self.pgpool, self.query_db_pagination_limit, w, + self.eth_threshold, ) .await? } @@ -367,6 +375,7 @@ where &self.pgpool, self.query_db_pagination_limit, b, + self.eth_threshold, ) .await? } diff --git a/storage/.sqlx/query-c16ff516b3b4c276e35d802175f6b8c3a33826b9d8c3425153b3c254198d7af8.json b/storage/.sqlx/query-70c7bb78bf525cf29c0d5bd1c3a59d0503e6498288f4f40911dd1b2decb4a327.json similarity index 84% rename from storage/.sqlx/query-c16ff516b3b4c276e35d802175f6b8c3a33826b9d8c3425153b3c254198d7af8.json rename to storage/.sqlx/query-70c7bb78bf525cf29c0d5bd1c3a59d0503e6498288f4f40911dd1b2decb4a327.json index 8587e132..e1f56a3e 100644 --- a/storage/.sqlx/query-c16ff516b3b4c276e35d802175f6b8c3a33826b9d8c3425153b3c254198d7af8.json +++ b/storage/.sqlx/query-70c7bb78bf525cf29c0d5bd1c3a59d0503e6498288f4f40911dd1b2decb4a327.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n w.tx_hash,\n w.event_index_in_tx,\n withdrawal_id,\n finalization_data.l2_block_number,\n l1_batch_number,\n l2_message_index,\n l2_tx_number_in_block,\n message,\n sender,\n proof\n FROM\n finalization_data\n JOIN withdrawals w ON finalization_data.withdrawal_id = w.id\n WHERE\n finalization_tx IS NULL\n AND failed_finalization_attempts < 3\n AND finalization_data.l2_block_number <= COALESCE(\n (\n SELECT\n MAX(l2_block_number)\n FROM\n l2_blocks\n WHERE\n execute_l1_block_number IS NOT NULL\n ),\n 1\n )\n AND w.token IN (SELECT * FROM UNNEST (\n $2 :: BYTEA []\n ))\n ORDER BY\n finalization_data.l2_block_number\n LIMIT\n $1\n ", + "query": "\n SELECT\n w.tx_hash,\n w.event_index_in_tx,\n withdrawal_id,\n finalization_data.l2_block_number,\n l1_batch_number,\n l2_message_index,\n l2_tx_number_in_block,\n message,\n sender,\n proof\n FROM\n finalization_data\n JOIN withdrawals w ON finalization_data.withdrawal_id = w.id\n WHERE\n finalization_tx IS NULL\n AND failed_finalization_attempts < 3\n AND finalization_data.l2_block_number <= COALESCE(\n (\n SELECT\n MAX(l2_block_number)\n FROM\n l2_blocks\n WHERE\n execute_l1_block_number IS NOT NULL\n ),\n 1\n )\n AND w.token IN (SELECT * FROM UNNEST (\n $2 :: BYTEA []\n ))\n AND (\n CASE WHEN token = decode('000000000000000000000000000000000000800A', 'hex') THEN amount >= $3\n ELSE TRUE\n END\n )\n ORDER BY\n finalization_data.l2_block_number\n LIMIT\n $1\n ", "describe": { "columns": [ { @@ -57,7 +57,8 @@ "parameters": { "Left": [ "Int8", - "ByteaArray" + "ByteaArray", + "Numeric" ] }, "nullable": [ @@ -73,5 +74,5 @@ false ] }, - "hash": "c16ff516b3b4c276e35d802175f6b8c3a33826b9d8c3425153b3c254198d7af8" + "hash": "70c7bb78bf525cf29c0d5bd1c3a59d0503e6498288f4f40911dd1b2decb4a327" } diff --git a/storage/.sqlx/query-1429bfeafe86ef0ce1449e07709bcce578fe6c917592a00654294c881dcc609a.json b/storage/.sqlx/query-e3bccab1832ae37d7a5a274474f9a785f5128591f617d214d5929152d3469800.json similarity index 84% rename from storage/.sqlx/query-1429bfeafe86ef0ce1449e07709bcce578fe6c917592a00654294c881dcc609a.json rename to storage/.sqlx/query-e3bccab1832ae37d7a5a274474f9a785f5128591f617d214d5929152d3469800.json index a5835dd3..adb68a81 100644 --- a/storage/.sqlx/query-1429bfeafe86ef0ce1449e07709bcce578fe6c917592a00654294c881dcc609a.json +++ b/storage/.sqlx/query-e3bccab1832ae37d7a5a274474f9a785f5128591f617d214d5929152d3469800.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n w.tx_hash,\n w.event_index_in_tx,\n withdrawal_id,\n finalization_data.l2_block_number,\n l1_batch_number,\n l2_message_index,\n l2_tx_number_in_block,\n message,\n sender,\n proof\n FROM\n finalization_data\n JOIN withdrawals w ON finalization_data.withdrawal_id = w.id\n WHERE\n finalization_tx IS NULL\n AND failed_finalization_attempts < 3\n AND finalization_data.l2_block_number <= COALESCE(\n (\n SELECT\n MAX(l2_block_number)\n FROM\n l2_blocks\n WHERE\n execute_l1_block_number IS NOT NULL\n ),\n 1\n )\n AND (\n last_finalization_attempt IS NULL\n OR\n last_finalization_attempt < NOW() - INTERVAL '1 minutes'\n )\n ORDER BY\n finalization_data.l2_block_number\n LIMIT\n $1\n ", + "query": "\n SELECT\n w.tx_hash,\n w.event_index_in_tx,\n withdrawal_id,\n finalization_data.l2_block_number,\n l1_batch_number,\n l2_message_index,\n l2_tx_number_in_block,\n message,\n sender,\n proof\n FROM\n finalization_data\n JOIN withdrawals w ON finalization_data.withdrawal_id = w.id\n WHERE\n finalization_tx IS NULL\n AND failed_finalization_attempts < 3\n AND finalization_data.l2_block_number <= COALESCE(\n (\n SELECT\n MAX(l2_block_number)\n FROM\n l2_blocks\n WHERE\n execute_l1_block_number IS NOT NULL\n ),\n 1\n )\n AND (\n last_finalization_attempt IS NULL\n OR\n last_finalization_attempt < NOW() - INTERVAL '1 minutes'\n )\n AND (\n CASE WHEN token = decode('000000000000000000000000000000000000800A', 'hex') THEN amount >= $2\n ELSE TRUE\n END\n )\n ORDER BY\n finalization_data.l2_block_number\n LIMIT\n $1\n ", "describe": { "columns": [ { @@ -56,7 +56,8 @@ ], "parameters": { "Left": [ - "Int8" + "Int8", + "Numeric" ] }, "nullable": [ @@ -72,5 +73,5 @@ false ] }, - "hash": "1429bfeafe86ef0ce1449e07709bcce578fe6c917592a00654294c881dcc609a" + "hash": "e3bccab1832ae37d7a5a274474f9a785f5128591f617d214d5929152d3469800" } diff --git a/storage/.sqlx/query-0296c80dfb2f21e40580333a071f07e0c0b25eb548cc83f9386c810847a3aa65.json b/storage/.sqlx/query-f041b2f10b01842b3315f7d0354bf1622e1f15489530ccf7fd8a93088f4b6bf7.json similarity index 84% rename from storage/.sqlx/query-0296c80dfb2f21e40580333a071f07e0c0b25eb548cc83f9386c810847a3aa65.json rename to storage/.sqlx/query-f041b2f10b01842b3315f7d0354bf1622e1f15489530ccf7fd8a93088f4b6bf7.json index bf3dce85..3e2e10b4 100644 --- a/storage/.sqlx/query-0296c80dfb2f21e40580333a071f07e0c0b25eb548cc83f9386c810847a3aa65.json +++ b/storage/.sqlx/query-f041b2f10b01842b3315f7d0354bf1622e1f15489530ccf7fd8a93088f4b6bf7.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n w.tx_hash,\n w.event_index_in_tx,\n withdrawal_id,\n finalization_data.l2_block_number,\n l1_batch_number,\n l2_message_index,\n l2_tx_number_in_block,\n message,\n sender,\n proof\n FROM\n finalization_data\n JOIN withdrawals w ON finalization_data.withdrawal_id = w.id\n WHERE\n finalization_tx IS NULL\n AND failed_finalization_attempts < 3\n AND finalization_data.l2_block_number <= COALESCE(\n (\n SELECT\n MAX(l2_block_number)\n FROM\n l2_blocks\n WHERE\n execute_l1_block_number IS NOT NULL\n ),\n 1\n )\n AND w.token NOT IN (SELECT * FROM UNNEST (\n $2 :: BYTEA []\n ))\n ORDER BY\n finalization_data.l2_block_number\n LIMIT\n $1\n ", + "query": "\n SELECT\n w.tx_hash,\n w.event_index_in_tx,\n withdrawal_id,\n finalization_data.l2_block_number,\n l1_batch_number,\n l2_message_index,\n l2_tx_number_in_block,\n message,\n sender,\n proof\n FROM\n finalization_data\n JOIN withdrawals w ON finalization_data.withdrawal_id = w.id\n WHERE\n finalization_tx IS NULL\n AND failed_finalization_attempts < 3\n AND finalization_data.l2_block_number <= COALESCE(\n (\n SELECT\n MAX(l2_block_number)\n FROM\n l2_blocks\n WHERE\n execute_l1_block_number IS NOT NULL\n ),\n 1\n )\n AND w.token NOT IN (SELECT * FROM UNNEST (\n $2 :: BYTEA []\n ))\n AND (\n CASE WHEN token = decode('000000000000000000000000000000000000800A', 'hex') THEN amount >= $3\n ELSE TRUE\n END\n )\n ORDER BY\n finalization_data.l2_block_number\n LIMIT\n $1\n ", "describe": { "columns": [ { @@ -57,7 +57,8 @@ "parameters": { "Left": [ "Int8", - "ByteaArray" + "ByteaArray", + "Numeric" ] }, "nullable": [ @@ -73,5 +74,5 @@ false ] }, - "hash": "0296c80dfb2f21e40580333a071f07e0c0b25eb548cc83f9386c810847a3aa65" + "hash": "f041b2f10b01842b3315f7d0354bf1622e1f15489530ccf7fd8a93088f4b6bf7" } diff --git a/storage/src/lib.rs b/storage/src/lib.rs index 03109032..afc7f56e 100644 --- a/storage/src/lib.rs +++ b/storage/src/lib.rs @@ -5,7 +5,7 @@ //! Finalizer watcher.storage.operations. -use ethers::types::{Address, H160, H256}; +use ethers::types::{Address, H160, H256, U256}; use sqlx::{PgConnection, PgPool}; use chain_events::L2TokenInitEvent; @@ -746,8 +746,11 @@ pub async fn withdrawals_to_finalize_with_blacklist( pool: &PgPool, limit_by: u64, token_blacklist: &[Address], + eth_threshold: Option, ) -> Result> { let blacklist: Vec<_> = token_blacklist.iter().map(|a| a.0.to_vec()).collect(); + // if no threshold, query _all_ ethereum withdrawals since all of them are >= 0. + let eth_threshold = eth_threshold.unwrap_or(U256::zero()); let data = sqlx::query!( " @@ -782,6 +785,11 @@ pub async fn withdrawals_to_finalize_with_blacklist( AND w.token NOT IN (SELECT * FROM UNNEST ( $2 :: BYTEA [] )) + AND ( + CASE WHEN token = decode('000000000000000000000000000000000000800A', 'hex') THEN amount >= $3 + ELSE TRUE + END + ) ORDER BY finalization_data.l2_block_number LIMIT @@ -789,6 +797,7 @@ pub async fn withdrawals_to_finalize_with_blacklist( ", limit_by as i64, &blacklist, + u256_to_big_decimal(eth_threshold), ) .fetch_all(pool) .await? @@ -816,8 +825,11 @@ pub async fn withdrawals_to_finalize_with_whitelist( pool: &PgPool, limit_by: u64, token_whitelist: &[Address], + eth_threshold: Option, ) -> Result> { let whitelist: Vec<_> = token_whitelist.iter().map(|a| a.0.to_vec()).collect(); + // if no threshold, query _all_ ethereum withdrawals since all of them are >= 0. + let eth_threshold = eth_threshold.unwrap_or(U256::zero()); let data = sqlx::query!( " @@ -852,6 +864,11 @@ pub async fn withdrawals_to_finalize_with_whitelist( AND w.token IN (SELECT * FROM UNNEST ( $2 :: BYTEA [] )) + AND ( + CASE WHEN token = decode('000000000000000000000000000000000000800A', 'hex') THEN amount >= $3 + ELSE TRUE + END + ) ORDER BY finalization_data.l2_block_number LIMIT @@ -859,6 +876,7 @@ pub async fn withdrawals_to_finalize_with_whitelist( ", limit_by as i64, &whitelist, + u256_to_big_decimal(eth_threshold), ) .fetch_all(pool) .await? @@ -885,8 +903,11 @@ pub async fn withdrawals_to_finalize_with_whitelist( pub async fn withdrawals_to_finalize( pool: &PgPool, limit_by: u64, + eth_threshold: Option, ) -> Result> { let latency = STORAGE_METRICS.call[&"withdrawals_to_finalize"].start(); + // if no threshold, query _all_ ethereum withdrawals since all of them are >= 0. + let eth_threshold = eth_threshold.unwrap_or(U256::zero()); let data = sqlx::query!( " @@ -923,12 +944,18 @@ pub async fn withdrawals_to_finalize( OR last_finalization_attempt < NOW() - INTERVAL '1 minutes' ) + AND ( + CASE WHEN token = decode('000000000000000000000000000000000000800A', 'hex') THEN amount >= $2 + ELSE TRUE + END + ) ORDER BY finalization_data.l2_block_number LIMIT $1 ", limit_by as i64, + u256_to_big_decimal(eth_threshold), ) .fetch_all(pool) .await?