diff --git a/etherlink/fa-bridge-watchtower/config.ml b/etherlink/fa-bridge-watchtower/config.ml index ada92a7291f45cd38bcdbd779e97dc9095c4cbe2..dffc5b907358ac480968b6156cd356de08908eee 100644 --- a/etherlink/fa-bridge-watchtower/config.ml +++ b/etherlink/fa-bridge-watchtower/config.ml @@ -28,6 +28,7 @@ type t = { secret_key : secret_key option; (** Optional secret key for signing *) whitelist : whitelist_item list option; (** Optional list of whitelisted addresses *) + monitor_all_deposits : bool; (** Whether to store all deposits in the DB. *) } let ctxt = Efunc_core.Eth.Crypto.context () @@ -73,6 +74,7 @@ let default = rpc = default_rpc_config; secret_key = default_secret_key; whitelist = default_whitelist; + monitor_all_deposits = false; } let rpc_encoding = @@ -109,14 +111,22 @@ let encoding = rpc; secret_key; whitelist; + monitor_all_deposits; } -> - (evm_node_endpoint, gas_limit, max_fee_per_gas, rpc, secret_key, whitelist)) + ( evm_node_endpoint, + gas_limit, + max_fee_per_gas, + rpc, + secret_key, + whitelist, + monitor_all_deposits )) (fun ( evm_node_endpoint, gas_limit, max_fee_per_gas, rpc, secret_key, - whitelist ) -> + whitelist, + monitor_all_deposits ) -> { evm_node_endpoint; gas_limit; @@ -124,8 +134,9 @@ let encoding = rpc; secret_key; whitelist; + monitor_all_deposits; }) - (obj6 + (obj7 (dft "evm_node_endpoint" ~description:"URL of the EVM node" @@ -157,7 +168,12 @@ let encoding = (opt "whitelist" ~description:"Optional list of whitelisted items" - (list whitelist_item_encoding))) + (list whitelist_item_encoding)) + (dft + "monitor_all_deposits" + ~description:"Monitor and store all deposits in the DB." + bool + false)) (** [load_file ~data_dir] attempts to load the configuration file from the specified data directory. Returns [None] if the file doesn't exist or [Some @@ -172,10 +188,10 @@ let load_file ~data_dir = let config = Data_encoding.Json.destruct encoding json in return_some config -(** [patch_config ~secret_key ~evm_node_endpoint config] updates the - configuration with the provided secret key and EVM node endpoint if they are - present. Returns the updated configuration. *) -let patch_config ~secret_key ~evm_node_endpoint config = +(** [patch_config ~secret_key ~evm_node_endpoint ~monitor_all_deposits config] + updates the configuration with the provided secret key and EVM node endpoint + if they are present. Returns the updated configuration. *) +let patch_config ~secret_key ~evm_node_endpoint ~monitor_all_deposits config = let config = Option.fold secret_key ~none:config ~some:(fun secret_key -> {config with secret_key = Some secret_key}) @@ -184,4 +200,7 @@ let patch_config ~secret_key ~evm_node_endpoint config = Option.fold evm_node_endpoint ~none:config ~some:(fun evm_node_endpoint -> {config with evm_node_endpoint}) in + let config = + if monitor_all_deposits then {config with monitor_all_deposits} else config + in config diff --git a/etherlink/fa-bridge-watchtower/db.ml b/etherlink/fa-bridge-watchtower/db.ml index 066c2ad4a78ef0245d97a8d57f991d64cdb5d84e..a29138bd55692f8f191001a009ee7be7f99296c4 100644 --- a/etherlink/fa-bridge-watchtower/db.ml +++ b/etherlink/fa-bridge-watchtower/db.ml @@ -10,7 +10,7 @@ open Caqti_request.Infix open Caqti_type.Std (** Current version for migrations. *) -let version = 0 +let version = 1 module Contract = Tezos_raw_protocol_alpha.Alpha_context.Contract @@ -304,7 +304,7 @@ module Deposits = struct VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) |sql} - let unclaimed = + let unclaimed_ignore_whitelist = (unit ->* deposit) @@ {sql| SELECT @@ -314,7 +314,22 @@ module Deposits = struct ORDER BY nonce DESC |sql} - let unclaimed_full = + let unclaimed = + (unit ->* deposit) + @@ {sql| + SELECT + d.nonce, d.proxy, d.ticket_hash, d.receiver, d.amount + FROM deposits d + JOIN whitelist w + ON ( + (d.proxy = w.proxy or w.proxy IS NULL) + AND + (d.ticket_hash = w.ticket_hash or w.ticket_hash IS NULL)) + where d.exec_transactionHash IS NULL + ORDER BY nonce DESC + |sql} + + let unclaimed_full_ignore_whitelist = (unit ->* t2 deposit log_info) @@ {sql| SELECT @@ -326,7 +341,24 @@ module Deposits = struct ORDER BY nonce DESC |sql} - let list = + let unclaimed_full = + (unit ->* t2 deposit log_info) + @@ {sql| + SELECT + nonce, d.proxy, d.ticket_hash, receiver, amount, + log_transactionHash, log_transactionIndex, log_logIndex, log_blockHash, + log_blockNumber, log_removed + FROM deposits d + JOIN whitelist w + ON ( + (d.proxy = w.proxy or w.proxy IS NULL) + AND + (d.ticket_hash = w.ticket_hash or w.ticket_hash IS NULL)) + where exec_transactionHash IS NULL + ORDER BY nonce DESC + |sql} + + let list_ignore_whitelist = (t2 int int ->* deposit_log) @@ {sql| SELECT @@ -339,7 +371,25 @@ module Deposits = struct ORDER BY nonce DESC LIMIT ? OFFSET ? |sql} - let list_by_receiver = + let list = + (t2 int int ->* deposit_log) + @@ {sql| + SELECT + nonce, d.proxy, d.ticket_hash, receiver, amount, + log_transactionHash, log_transactionIndex, log_logIndex, log_blockHash, + log_blockNumber, log_removed, + exec_transactionHash, exec_transactionIndex, exec_blockHash, + exec_blockNumber + FROM deposits d + JOIN whitelist w + ON ( + (d.proxy = w.proxy or w.proxy IS NULL) + AND + (d.ticket_hash = w.ticket_hash or w.ticket_hash IS NULL)) + ORDER BY nonce DESC LIMIT ? OFFSET ? + |sql} + + let list_by_receiver_ignore_whitelist = (t3 address int int ->* deposit_log) @@ {sql| SELECT @@ -353,6 +403,25 @@ module Deposits = struct ORDER BY nonce DESC LIMIT ? OFFSET ? |sql} + let list_by_receiver = + (t3 address int int ->* deposit_log) + @@ {sql| + SELECT + nonce, d.proxy, d.ticket_hash, receiver, amount, + log_transactionHash, log_transactionIndex, log_logIndex, log_blockHash, + log_blockNumber, log_removed, + exec_transactionHash, exec_transactionIndex, exec_blockHash, + exec_blockNumber + FROM deposits d + JOIN whitelist w + ON ( + (d.proxy = w.proxy or w.proxy IS NULL) + AND + (d.ticket_hash = w.ticket_hash or w.ticket_hash IS NULL)) + WHERE receiver = ? + ORDER BY nonce DESC LIMIT ? OFFSET ? + |sql} + let set_claimed = (t2 execution_info level ->. unit) @@ {sql| @@ -393,10 +462,6 @@ module Deposits = struct with_connection db conn @@ fun conn -> Sqlite.Db.rev_collect_list conn Q.unclaimed_full () - let set_claimed ?conn db nonce execution_info = - with_connection db conn @@ fun conn -> - Sqlite.Db.exec conn Q.set_claimed (execution_info, nonce) - let list ?conn db ~limit ~offset = with_connection db conn @@ fun conn -> Sqlite.Db.collect_list conn Q.list (limit, offset) @@ -405,6 +470,29 @@ module Deposits = struct with_connection db conn @@ fun conn -> Sqlite.Db.collect_list conn Q.list_by_receiver (addr, limit, offset) + let get_unclaimed_ignore_whitelist ?conn db = + with_connection db conn @@ fun conn -> + Sqlite.Db.rev_collect_list conn Q.unclaimed_ignore_whitelist () + + let get_unclaimed_full_ignore_whitelist ?conn db = + with_connection db conn @@ fun conn -> + Sqlite.Db.rev_collect_list conn Q.unclaimed_full_ignore_whitelist () + + let list_ignore_whitelist ?conn db ~limit ~offset = + with_connection db conn @@ fun conn -> + Sqlite.Db.collect_list conn Q.list_ignore_whitelist (limit, offset) + + let list_by_receiver_ignore_whitelist ?conn db addr ~limit ~offset = + with_connection db conn @@ fun conn -> + Sqlite.Db.collect_list + conn + Q.list_by_receiver_ignore_whitelist + (addr, limit, offset) + + let set_claimed ?conn db nonce execution_info = + with_connection db conn @@ fun conn -> + Sqlite.Db.exec conn Q.set_claimed (execution_info, nonce) + let delete_before ?conn db level = with_connection db conn @@ fun conn -> Sqlite.Db.exec conn Q.delete_before level @@ -442,3 +530,44 @@ module Pointers = struct with_connection db conn @@ fun conn -> Sqlite.Db.find conn Q.get () end end + +module Whitelist = struct + module Q = struct + open Types + + let insert = + (t2 (option address) (option hash) ->. unit) + @@ {sql| + INSERT INTO whitelist + (proxy, ticket_hash) + VALUES (?, ?) + |sql} + + let clear = (unit ->. unit) @@ {sql| DELETE FROM whitelist |sql} + end + + let insert ?conn db pth = + with_connection db conn @@ fun conn -> Sqlite.Db.exec conn Q.insert pth + + let clear ?conn db = + with_connection db conn @@ fun conn -> Sqlite.Db.exec conn Q.clear () + + let register db (whitelist : Config.whitelist_item list option) = + let open Lwt_result_syntax in + with_transaction db @@ fun conn -> + let* () = clear ~conn db in + match whitelist with + | None -> + (* Matches every deposit log *) + insert ~conn db (None, None) + | Some whitelist -> + List.iter_es + (fun {Config.proxy; ticket_hashes} -> + match ticket_hashes with + | None -> insert ~conn db (proxy, None) + | Some ticket_hashes -> + List.iter_es + (fun ticket_hash -> insert ~conn db (proxy, Some ticket_hash)) + ticket_hashes) + whitelist +end diff --git a/etherlink/fa-bridge-watchtower/etherlink_monitor.ml b/etherlink/fa-bridge-watchtower/etherlink_monitor.ml index 8cc82aaf6f4e7f19a28ffd9e887cdd4f5eb05858..950432d6e8413da5818bf86f81e2963173e22357 100644 --- a/etherlink/fa-bridge-watchtower/etherlink_monitor.ml +++ b/etherlink/fa-bridge-watchtower/etherlink_monitor.ml @@ -873,7 +873,8 @@ let start db ~config ~notify_ws_change ~first_block = sk = secret_key; chain_id; gas_limit = Z.of_int64 config.gas_limit; - whitelist = config.whitelist; + whitelist = + (if config.monitor_all_deposits then None else config.whitelist); nonce = Ethereum_types.Qty.zero; } in diff --git a/etherlink/fa-bridge-watchtower/main.ml b/etherlink/fa-bridge-watchtower/main.ml index 4a7c3a4a6d030d5b051a5413a950962123926406..7b9922218ba8752687ebcce3401c28743907d427 100644 --- a/etherlink/fa-bridge-watchtower/main.ml +++ b/etherlink/fa-bridge-watchtower/main.ml @@ -82,6 +82,12 @@ module Arg = struct @@ Tezos_clic.parameter (fun _ x -> Lwt_result.return (Ethereum_types.Qty (Z.of_string x))) + let monitor_all_deposits = + Tezos_clic.switch + ~long:"monitor-all-deposits" + ~doc:"Monitor and store all deposits in the DB." + () + let level_param ~desc = Tezos_clic.param ~name:"level" ~desc @@ Tezos_clic.parameter (fun _ x -> @@ -147,21 +153,34 @@ let run_command = om_command ~group:run_group ~desc:"Start FA bridge watchtower" - (args3 Arg.evm_node_endpoint Arg.secret_key Arg.first_block) + (args4 + Arg.evm_node_endpoint + Arg.secret_key + Arg.first_block + Arg.monitor_all_deposits) (prefixes ["run"] @@ stop) - (fun {data_dir; verbosity} (evm_node_endpoint, secret_key, first_block) _ -> + (fun {data_dir; verbosity} + (evm_node_endpoint, secret_key, first_block, monitor_all_deposits) + _ -> let open Lwt_result_syntax in let* loaded_config = Config.load_file ~data_dir in let config = Option.value ~default:Config.default loaded_config in let*! () = log_config ~verbosity ~data_dir in let* db = Db.init ~data_dir `Read_write in - let config = Config.patch_config config ~secret_key ~evm_node_endpoint in + let config = + Config.patch_config + config + ~secret_key + ~evm_node_endpoint + ~monitor_all_deposits + in let* () = match config.secret_key with | Some _ -> return_unit | None -> failwith "secret key not provided neither in config, cli nor env" in + let* () = Db.Whitelist.register db config.whitelist in let*! notify_ws_change = match config.rpc with | None -> Lwt.return ignore diff --git a/etherlink/fa-bridge-watchtower/migrations/001_whitelist.sql b/etherlink/fa-bridge-watchtower/migrations/001_whitelist.sql new file mode 100644 index 0000000000000000000000000000000000000000..9ab7b9fc5e3a691ba0a6ea5cb1cc574264228677 --- /dev/null +++ b/etherlink/fa-bridge-watchtower/migrations/001_whitelist.sql @@ -0,0 +1,17 @@ +----------------------------------------------------------------- +-- SPDX-License-Identifier: MIT -- +-- Copyright (c) 2025 Functori -- +-- Copyright (c) 2025 Nomadic Labs -- +----------------------------------------------------------------- + +CREATE INDEX idx_proxy ON deposits(proxy); +CREATE INDEX idx_ticket_hash ON deposits(ticket_hash); +CREATE INDEX idx_receiver ON deposits(receiver); + +CREATE TABLE whitelist ( + proxy BLOB, + ticket_hash BLOB +); + +CREATE INDEX wid_proxy ON whitelist(proxy); +CREATE INDEX wid_ticket_hash ON whitelist(ticket_hash); diff --git a/etherlink/fa-bridge-watchtower/rpc_server.ml b/etherlink/fa-bridge-watchtower/rpc_server.ml index c6c6794b8f4d36eaf2a54e4331df44744be1a9de..bc6e614ef9646813bc25c346a41327c2cfc254e7 100644 --- a/etherlink/fa-bridge-watchtower/rpc_server.ml +++ b/etherlink/fa-bridge-watchtower/rpc_server.ml @@ -354,9 +354,31 @@ let () = "/unclaimed" Data_encoding.(list (merge_objs Encodings.deposit Encodings.log_info)) ~description:"List unclaimed deposits" - @@ fun _ _ ctx -> + ~query: + (* TODO: query parameters are reported by hand for now *) + [ + { + parameter = + { + name = "ignore_whitelist"; + description = Some "List deposits independently of whitelist"; + schema = Tezos_openapi.Openapi.Schema.boolean (); + }; + required = false; + }; + ] + @@ fun req _ ctx -> let open Lwt_result_syntax in - let* deposits = Db.Deposits.get_unclaimed_full ctx.db in + let ignore_whitelist = + Dream.query req "ignore_whitelist" + |> Option.map bool_of_string + |> Option.value ~default:false + in + let* deposits = + if ignore_whitelist then + Db.Deposits.get_unclaimed_full_ignore_whitelist ctx.db + else Db.Deposits.get_unclaimed_full ctx.db + in List.map_es (fun (deposit, log) -> let+ deposit = deposit_with_token_info ctx deposit in @@ -414,11 +436,23 @@ let () = let receiver = Dream.query req "receiver" |> Option.map Ethereum_types.Address.of_string in + let ignore_whitelist = + Dream.query req "ignore_whitelist" + |> Option.map bool_of_string + |> Option.value ~default:false + in let* deposits = - match receiver with - | None -> Db.Deposits.list ctx.db ~limit ~offset - | Some receiver -> + match (receiver, ignore_whitelist) with + | None, false -> Db.Deposits.list ctx.db ~limit ~offset + | Some receiver, false -> Db.Deposits.list_by_receiver ctx.db receiver ~limit ~offset + | None, true -> Db.Deposits.list_ignore_whitelist ctx.db ~limit ~offset + | Some receiver, true -> + Db.Deposits.list_by_receiver_ignore_whitelist + ctx.db + receiver + ~limit + ~offset in List.map_es (fun Db.{deposit; log_info; claimed} ->