diff --git a/yarn.lock b/yarn.lock index aad8c79d..25e2807e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -28,6 +28,13 @@ chalk "^2.0.0" js-tokens "^4.0.0" +"@babel/runtime@^7.0.0": + version "7.22.6" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.6.tgz#57d64b9ae3cff1d67eb067ae117dac087f5bd438" + integrity sha512-wDb5pWm4WDdF6LFUde3Jl8WzPA+3ZbxYqkC6xAXuD3irdEHN1k0NfTRrJD8ZD378SJ61miMLCqIOXYhd8x+AJQ== + dependencies: + regenerator-runtime "^0.13.11" + "@cspotcode/source-map-support@^0.8.0": version "0.8.1" resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" @@ -80,6 +87,21 @@ "@ethersproject/properties" "^5.6.0" "@ethersproject/strings" "^5.6.1" +"@ethersproject/abi@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.7.0.tgz#b3f3e045bbbeed1af3947335c247ad625a44e449" + integrity sha512-351ktp42TiRcYB3H1OP8yajPeAQstMW/yCFokj/AthP9bLHzQFPlOrxOcwYEDkUAICmOHljvN4K39OMTMUa9RA== + dependencies: + "@ethersproject/address" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/constants" "^5.7.0" + "@ethersproject/hash" "^5.7.0" + "@ethersproject/keccak256" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/strings" "^5.7.0" + "@ethersproject/abstract-provider@^5.6.1": version "5.6.1" resolved "https://registry.yarnpkg.com/@ethersproject/abstract-provider/-/abstract-provider-5.6.1.tgz#02ddce150785caf0c77fe036a0ebfcee61878c59" @@ -93,6 +115,19 @@ "@ethersproject/transactions" "^5.6.2" "@ethersproject/web" "^5.6.1" +"@ethersproject/abstract-provider@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/abstract-provider/-/abstract-provider-5.7.0.tgz#b0a8550f88b6bf9d51f90e4795d48294630cb9ef" + integrity sha512-R41c9UkchKCpAqStMYUpdunjo3pkEvZC3FAwZn5S5MGbXoMQOHIdHItezTETxAO5bevtMApSyEhn9+CHcDsWBw== + dependencies: + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/networks" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/transactions" "^5.7.0" + "@ethersproject/web" "^5.7.0" + "@ethersproject/abstract-signer@^5.6.2": version "5.6.2" resolved "https://registry.yarnpkg.com/@ethersproject/abstract-signer/-/abstract-signer-5.6.2.tgz#491f07fc2cbd5da258f46ec539664713950b0b33" @@ -104,6 +139,17 @@ "@ethersproject/logger" "^5.6.0" "@ethersproject/properties" "^5.6.0" +"@ethersproject/abstract-signer@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/abstract-signer/-/abstract-signer-5.7.0.tgz#13f4f32117868452191a4649723cb086d2b596b2" + integrity sha512-a16V8bq1/Cz+TGCkE2OPMTOUDLS3grCpdjoJCYNnVBbdYEMSgKrU0+B90s8b6H+ByYTBZN7a3g76jdIJi7UfKQ== + dependencies: + "@ethersproject/abstract-provider" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/address@^5.6.1": version "5.6.1" resolved "https://registry.yarnpkg.com/@ethersproject/address/-/address-5.6.1.tgz#ab57818d9aefee919c5721d28cd31fd95eff413d" @@ -115,6 +161,17 @@ "@ethersproject/logger" "^5.6.0" "@ethersproject/rlp" "^5.6.1" +"@ethersproject/address@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/address/-/address-5.7.0.tgz#19b56c4d74a3b0a46bfdbb6cfcc0a153fc697f37" + integrity sha512-9wYhYt7aghVGo758POM5nqcOMaE168Q6aRLJZwUmiqSrAungkG74gSSeKEIR7ukixesdRZGPgVqme6vmxs1fkA== + dependencies: + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/keccak256" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/rlp" "^5.7.0" + "@ethersproject/base64@^5.6.1": version "5.6.1" resolved "https://registry.yarnpkg.com/@ethersproject/base64/-/base64-5.6.1.tgz#2c40d8a0310c9d1606c2c37ae3092634b41d87cb" @@ -122,6 +179,13 @@ dependencies: "@ethersproject/bytes" "^5.6.1" +"@ethersproject/base64@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/base64/-/base64-5.7.0.tgz#ac4ee92aa36c1628173e221d0d01f53692059e1c" + integrity sha512-Dr8tcHt2mEbsZr/mwTPIQAf3Ai0Bks/7gTw9dSqk1mQvhW3XvRlmDJr/4n+wg1JmCl16NZue17CDh8xb/vZ0sQ== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/bignumber@^5.6.2": version "5.6.2" resolved "https://registry.yarnpkg.com/@ethersproject/bignumber/-/bignumber-5.6.2.tgz#72a0717d6163fab44c47bcc82e0c550ac0315d66" @@ -131,6 +195,15 @@ "@ethersproject/logger" "^5.6.0" bn.js "^5.2.1" +"@ethersproject/bignumber@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/bignumber/-/bignumber-5.7.0.tgz#e2f03837f268ba655ffba03a57853e18a18dc9c2" + integrity sha512-n1CAdIHRWjSucQO3MC1zPSVgV/6dy/fjL9pMrPP9peL+QxEg9wOsVqwD4+818B6LUEtaXzVHQiuivzRoxPxUGw== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + bn.js "^5.2.1" + "@ethersproject/bytes@^5.6.1": version "5.6.1" resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.6.1.tgz#24f916e411f82a8a60412344bf4a813b917eefe7" @@ -138,6 +211,13 @@ dependencies: "@ethersproject/logger" "^5.6.0" +"@ethersproject/bytes@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.7.0.tgz#a00f6ea8d7e7534d6d87f47188af1148d71f155d" + integrity sha512-nsbxwgFXWh9NyYWo+U8atvmMsSdKJprTcICAkvbBffT75qDocbuggBU0SJiVK2MuTrp0q+xvLkTnGMPK1+uA9A== + dependencies: + "@ethersproject/logger" "^5.7.0" + "@ethersproject/constants@^5.6.1": version "5.6.1" resolved "https://registry.yarnpkg.com/@ethersproject/constants/-/constants-5.6.1.tgz#e2e974cac160dd101cf79fdf879d7d18e8cb1370" @@ -145,6 +225,13 @@ dependencies: "@ethersproject/bignumber" "^5.6.2" +"@ethersproject/constants@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/constants/-/constants-5.7.0.tgz#df80a9705a7e08984161f09014ea012d1c75295e" + integrity sha512-DHI+y5dBNvkpYUMiRQyxRBYBefZkJfo70VUkUAsRjcPs47muV9evftfZ0PJVCXYbAiCgght0DtcF9srFQmIgWA== + dependencies: + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/hash@^5.6.1": version "5.6.1" resolved "https://registry.yarnpkg.com/@ethersproject/hash/-/hash-5.6.1.tgz#224572ea4de257f05b4abf8ae58b03a67e99b0f4" @@ -159,6 +246,21 @@ "@ethersproject/properties" "^5.6.0" "@ethersproject/strings" "^5.6.1" +"@ethersproject/hash@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/hash/-/hash-5.7.0.tgz#eb7aca84a588508369562e16e514b539ba5240a7" + integrity sha512-qX5WrQfnah1EFnO5zJv1v46a8HW0+E5xuBBDTwMFZLuVTx0tbU2kkx15NqdjxecrLGatQN9FGQKpb1FKdHCt+g== + dependencies: + "@ethersproject/abstract-signer" "^5.7.0" + "@ethersproject/address" "^5.7.0" + "@ethersproject/base64" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/keccak256" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/strings" "^5.7.0" + "@ethersproject/keccak256@^5.6.1": version "5.6.1" resolved "https://registry.yarnpkg.com/@ethersproject/keccak256/-/keccak256-5.6.1.tgz#b867167c9b50ba1b1a92bccdd4f2d6bd168a91cc" @@ -167,11 +269,24 @@ "@ethersproject/bytes" "^5.6.1" js-sha3 "0.8.0" +"@ethersproject/keccak256@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/keccak256/-/keccak256-5.7.0.tgz#3186350c6e1cd6aba7940384ec7d6d9db01f335a" + integrity sha512-2UcPboeL/iW+pSg6vZ6ydF8tCnv3Iu/8tUmLLzWWGzxWKFFqOBQFLo6uLUv6BDrLgCDfN28RJ/wtByx+jZ4KBg== + dependencies: + "@ethersproject/bytes" "^5.7.0" + js-sha3 "0.8.0" + "@ethersproject/logger@^5.6.0": version "5.6.0" resolved "https://registry.yarnpkg.com/@ethersproject/logger/-/logger-5.6.0.tgz#d7db1bfcc22fd2e4ab574cba0bb6ad779a9a3e7a" integrity sha512-BiBWllUROH9w+P21RzoxJKzqoqpkyM1pRnEKG69bulE9TSQD8SAIvTQqIMZmmCO8pUNkgLP1wndX1gKghSpBmg== +"@ethersproject/logger@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/logger/-/logger-5.7.0.tgz#6ce9ae168e74fecf287be17062b590852c311892" + integrity sha512-0odtFdXu/XHtjQXJYA3u9G0G8btm0ND5Cu8M7i5vhEcE8/HmF4Lbdqanwyv4uQTr2tx6b7fQRmgLrsnpQlmnig== + "@ethersproject/networks@^5.6.3": version "5.6.4" resolved "https://registry.yarnpkg.com/@ethersproject/networks/-/networks-5.6.4.tgz#51296d8fec59e9627554f5a8a9c7791248c8dc07" @@ -179,6 +294,13 @@ dependencies: "@ethersproject/logger" "^5.6.0" +"@ethersproject/networks@^5.7.0": + version "5.7.1" + resolved "https://registry.yarnpkg.com/@ethersproject/networks/-/networks-5.7.1.tgz#118e1a981d757d45ccea6bb58d9fd3d9db14ead6" + integrity sha512-n/MufjFYv3yFcUyfhnXotyDlNdFb7onmkSy8aQERi2PjNcnWQ66xXxa3XlS8nCcA8aJKJjIIMNJTC7tu80GwpQ== + dependencies: + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties@^5.6.0": version "5.6.0" resolved "https://registry.yarnpkg.com/@ethersproject/properties/-/properties-5.6.0.tgz#38904651713bc6bdd5bdd1b0a4287ecda920fa04" @@ -186,6 +308,13 @@ dependencies: "@ethersproject/logger" "^5.6.0" +"@ethersproject/properties@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/properties/-/properties-5.7.0.tgz#a6e12cb0439b878aaf470f1902a176033067ed30" + integrity sha512-J87jy8suntrAkIZtecpxEPxY//szqr1mlBaYlQ0r4RCaiD2hjheqF9s1LVE8vVuJCXisjIP+JgtK/Do54ej4Sw== + dependencies: + "@ethersproject/logger" "^5.7.0" + "@ethersproject/rlp@^5.6.1": version "5.6.1" resolved "https://registry.yarnpkg.com/@ethersproject/rlp/-/rlp-5.6.1.tgz#df8311e6f9f24dcb03d59a2bac457a28a4fe2bd8" @@ -194,6 +323,14 @@ "@ethersproject/bytes" "^5.6.1" "@ethersproject/logger" "^5.6.0" +"@ethersproject/rlp@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/rlp/-/rlp-5.7.0.tgz#de39e4d5918b9d74d46de93af80b7685a9c21304" + integrity sha512-rBxzX2vK8mVF7b0Tol44t5Tb8gomOHkj5guL+HhzQ1yBh/ydjGnpw6at+X6Iw0Kp3OzzzkcKp8N9r0W4kYSs9w== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/signing-key@^5.6.2": version "5.6.2" resolved "https://registry.yarnpkg.com/@ethersproject/signing-key/-/signing-key-5.6.2.tgz#8a51b111e4d62e5a62aee1da1e088d12de0614a3" @@ -206,6 +343,18 @@ elliptic "6.5.4" hash.js "1.1.7" +"@ethersproject/signing-key@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/signing-key/-/signing-key-5.7.0.tgz#06b2df39411b00bc57c7c09b01d1e41cf1b16ab3" + integrity sha512-MZdy2nL3wO0u7gkB4nA/pEf8lu1TlFswPNmy8AiYkfKTdO6eXBJyUdmHO/ehm/htHw9K/qF8ujnTyUAD+Ry54Q== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + bn.js "^5.2.1" + elliptic "6.5.4" + hash.js "1.1.7" + "@ethersproject/strings@^5.6.1": version "5.6.1" resolved "https://registry.yarnpkg.com/@ethersproject/strings/-/strings-5.6.1.tgz#dbc1b7f901db822b5cafd4ebf01ca93c373f8952" @@ -215,6 +364,15 @@ "@ethersproject/constants" "^5.6.1" "@ethersproject/logger" "^5.6.0" +"@ethersproject/strings@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/strings/-/strings-5.7.0.tgz#54c9d2a7c57ae8f1205c88a9d3a56471e14d5ed2" + integrity sha512-/9nu+lj0YswRNSH0NXYqrh8775XNyEdUQAuf3f+SmOrnVewcJ5SBNAjF7lpgehKi4abvNNXyf+HX86czCdJ8Mg== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/constants" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/transactions@^5.6.2": version "5.6.2" resolved "https://registry.yarnpkg.com/@ethersproject/transactions/-/transactions-5.6.2.tgz#793a774c01ced9fe7073985bb95a4b4e57a6370b" @@ -230,6 +388,21 @@ "@ethersproject/rlp" "^5.6.1" "@ethersproject/signing-key" "^5.6.2" +"@ethersproject/transactions@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/transactions/-/transactions-5.7.0.tgz#91318fc24063e057885a6af13fdb703e1f993d3b" + integrity sha512-kmcNicCp1lp8qanMTC3RIikGgoJ80ztTyvtsFvCYpSCfkjhD0jZ2LOrnbcuxuToLIUYYf+4XwD1rP+B/erDIhQ== + dependencies: + "@ethersproject/address" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/constants" "^5.7.0" + "@ethersproject/keccak256" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/rlp" "^5.7.0" + "@ethersproject/signing-key" "^5.7.0" + "@ethersproject/web@^5.6.1": version "5.6.1" resolved "https://registry.yarnpkg.com/@ethersproject/web/-/web-5.6.1.tgz#6e2bd3ebadd033e6fe57d072db2b69ad2c9bdf5d" @@ -241,6 +414,17 @@ "@ethersproject/properties" "^5.6.0" "@ethersproject/strings" "^5.6.1" +"@ethersproject/web@^5.7.0": + version "5.7.1" + resolved "https://registry.yarnpkg.com/@ethersproject/web/-/web-5.7.1.tgz#de1f285b373149bee5928f4eb7bcb87ee5fbb4ae" + integrity sha512-Gueu8lSvyjBWL4cYsWsjh6MtMwM0+H4HvqFPZfB6dV8ctbP9zFAO73VG1cMWae0FLPCtz0peKPpZY8/ugJJX2w== + dependencies: + "@ethersproject/base64" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/strings" "^5.7.0" + "@findeth/abi@^0.7.1": version "0.7.1" resolved "https://registry.yarnpkg.com/@findeth/abi/-/abi-0.7.1.tgz#60d0801cb252e587dc3228f00c00581bb748aebc" @@ -326,11 +510,23 @@ dependencies: "@mycrypto/eth-scan" "3.5.2" +"@noble/curves@1.1.0", "@noble/curves@~1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.1.0.tgz#f13fc667c89184bc04cccb9b11e8e7bae27d8c3d" + integrity sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA== + dependencies: + "@noble/hashes" "1.3.1" + "@noble/hashes@1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.1.2.tgz#e9e035b9b166ca0af657a7848eb2718f0f22f183" integrity sha512-KYRCASVTv6aeUi1tsF8/vpyR7zpfs3FUzy2Jqm+MU+LmUKhQ0y2FpfwqkCcxSg2ua4GALJd8k2R76WxwZGbQpA== +"@noble/hashes@1.3.1", "@noble/hashes@~1.3.0", "@noble/hashes@~1.3.1": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.1.tgz#8831ef002114670c603c458ab8b11328406953a9" + integrity sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA== + "@noble/secp256k1@1.7.1": version "1.7.1" resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.7.1.tgz#b251c70f824ce3ca7f8dc3df08d58f005cc0507c" @@ -357,6 +553,28 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@scure/base@~1.1.0": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.1.tgz#ebb651ee52ff84f420097055f4bf46cfba403938" + integrity sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA== + +"@scure/bip32@1.3.1": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.3.1.tgz#7248aea723667f98160f593d621c47e208ccbb10" + integrity sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A== + dependencies: + "@noble/curves" "~1.1.0" + "@noble/hashes" "~1.3.1" + "@scure/base" "~1.1.0" + +"@scure/bip39@1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.2.1.tgz#5cee8978656b272a917b7871c981e0541ad6ac2a" + integrity sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg== + dependencies: + "@noble/hashes" "~1.3.0" + "@scure/base" "~1.1.0" + "@sindresorhus/is@^0.14.0": version "0.14.0" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" @@ -369,6 +587,11 @@ dependencies: defer-to-connect "^1.0.1" +"@tronweb3/google-protobuf@^3.21.2": + version "3.21.2" + resolved "https://registry.yarnpkg.com/@tronweb3/google-protobuf/-/google-protobuf-3.21.2.tgz#0964cf83ed7826d31c3cb4e4ecf07655681631c9" + integrity sha512-IVcT2GfWX3K6tHUVhs14NP5uzKhQt4KeDya1g9ACxuZsUzsaoGUIGzceK2Ltu7xp1YV94AaHOf4yxLAivlvEkQ== + "@tsconfig/node10@^1.0.7": version "1.0.8" resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.8.tgz#c1e4e80d6f964fbecb3359c43bd48b40f7cadad9" @@ -980,6 +1203,13 @@ axios@^0.21.2: dependencies: follow-redirects "^1.14.0" +axios@^0.26.1: + version "0.26.1" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.26.1.tgz#1ede41c51fcf51bbbd6fd43669caaa4f0495aaa9" + integrity sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA== + dependencies: + follow-redirects "^1.14.8" + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" @@ -1026,6 +1256,11 @@ bignumber.js@^9.0.0: resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.1.tgz#8d7ba124c882bfd8e43260c67475518d0689e4e5" integrity sha512-IdZR9mh6ahOBv/hYGiXyVuyCetmGJhtYkqLBpTStdhEGjegpPlUawydyaF3pbIOFynJTpllEs+NP+CS9jKFLjA== +bignumber.js@^9.0.1: + version "9.1.1" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.1.tgz#c4df7dc496bd849d4c9464344c1aa74228b4dac6" + integrity sha512-pHm4LsMJ6lzgNGVfZHjMoO8sdoRhOzOH4MLmY65Jg70bpxCKu5iOHNJyfF6OyvYw7t8Fpf35RuzUyqnQsj8Vig== + binary-extensions@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" @@ -2231,6 +2466,16 @@ ethereum-cryptography@^0.1.3: secp256k1 "^4.0.1" setimmediate "^1.0.5" +ethereum-cryptography@^2.0.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ethereum-cryptography/-/ethereum-cryptography-2.1.2.tgz#18fa7108622e56481157a5cb7c01c0c6a672eb67" + integrity sha512-Z5Ba0T0ImZ8fqXrJbpHcbpAvIswRte2wGNR/KePnu8GbbvgJ47lMxT/ZZPG6i9Jaht4azPDop4HaM00J0J59ug== + dependencies: + "@noble/curves" "1.1.0" + "@noble/hashes" "1.3.1" + "@scure/bip32" "1.3.1" + "@scure/bip39" "1.2.1" + ethereumjs-abi@^0.6.8: version "0.6.8" resolved "https://registry.yarnpkg.com/ethereumjs-abi/-/ethereumjs-abi-0.6.8.tgz#71bc152db099f70e62f108b7cdfca1b362c6fcae" @@ -2276,6 +2521,19 @@ ethers@^6.5.1: tslib "2.4.0" ws "8.5.0" +ethers@^6.6.0: + version "6.7.0" + resolved "https://registry.yarnpkg.com/ethers/-/ethers-6.7.0.tgz#0f772c31a9450de28aa518b181c8cb269bbe7fd1" + integrity sha512-pxt5hK82RNwcTX2gOZP81t6qVPVspnkpeivwEgQuK9XUvbNtghBnT8GNIb/gPh+WnVSfi8cXC9XlfT8sqc6D6w== + dependencies: + "@adraffy/ens-normalize" "1.9.2" + "@noble/hashes" "1.1.2" + "@noble/secp256k1" "1.7.1" + "@types/node" "18.15.13" + aes-js "4.0.0-beta.5" + tslib "2.4.0" + ws "8.5.0" + ethjs-unit@0.1.6: version "0.1.6" resolved "https://registry.yarnpkg.com/ethjs-unit/-/ethjs-unit-0.1.6.tgz#c665921e476e87bce2a9d588a6fe0405b2c41699" @@ -2297,6 +2555,11 @@ eventemitter3@4.0.4: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.4.tgz#b5463ace635a083d018bdc7c917b4c5f10a85384" integrity sha512-rlaVLnVxtxvoyLsQQFBx53YmXHDxRIzzTLbdfxqi4yocpSjAxXwkU0cScM5JgSKMqEhrZpnvQ2D9gjylR0AimQ== +eventemitter3@^3.1.0: + version "3.1.2" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7" + integrity sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q== + eventemitter3@^4.0.0: version "4.0.7" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" @@ -2519,6 +2782,11 @@ follow-redirects@^1.14.0: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.1.tgz#0ca6a452306c9b276e4d3127483e29575e207ad5" integrity sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA== +follow-redirects@^1.14.8: + version "1.15.2" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" + integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== + foreach@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99" @@ -3040,6 +3308,11 @@ inherits@2.0.3: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= +injectpromise@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/injectpromise/-/injectpromise-1.0.0.tgz#c621f7df2bbfc1164d714f1fb229adec2079da39" + integrity sha512-qNq5wy4qX4uWHcVFOEU+RqZkoVG65FhvGkyDWbuBxILMjK6A1LFf5A1mgXZkD4nRx5FCorD81X/XvPKp/zVfPA== + internal-slot@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c" @@ -4821,6 +5094,11 @@ redis-parser@^3.0.0: dependencies: redis-errors "^1.0.0" +regenerator-runtime@^0.13.11: + version "0.13.11" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" + integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== + remove-array-items@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/remove-array-items/-/remove-array-items-1.1.1.tgz#fd745ff73d0822e561ea910bf1b401fc7843e693" @@ -5031,6 +5309,11 @@ semver@7.3.8: dependencies: lru-cache "^6.0.0" +semver@^5.6.0: + version "5.7.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" + integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== + semver@^7.3.4: version "7.3.5" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" @@ -5557,6 +5840,25 @@ triple-beam@^1.2.0, triple-beam@^1.3.0: resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9" integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw== +tronweb@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/tronweb/-/tronweb-5.3.0.tgz#b40c4aa68f81b70bac4d8de52960b82b61f9ab04" + integrity sha512-i03+3UviQacqdrr3VgXHDL8h/2E24BeULak4w6+yRkJaCuEyxjWOtEn1dq87ulTkHzS/vKK0zIyvW7rSxuISOA== + dependencies: + "@babel/runtime" "^7.0.0" + "@ethersproject/abi" "^5.7.0" + "@tronweb3/google-protobuf" "^3.21.2" + axios "^0.26.1" + bignumber.js "^9.0.1" + ethereum-cryptography "^2.0.0" + ethers "^6.6.0" + eventemitter3 "^3.1.0" + injectpromise "^1.0.0" + lodash "^4.17.21" + querystring-es3 "^0.2.1" + semver "^5.6.0" + validator "^13.7.0" + ts-loader@9.4.2: version "9.4.2" resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.4.2.tgz#80a45eee92dd5170b900b3d00abcfa14949aeb78" @@ -5868,6 +6170,11 @@ validate-npm-package-license@^3.0.1: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" +validator@^13.7.0: + version "13.11.0" + resolved "https://registry.yarnpkg.com/validator/-/validator-13.11.0.tgz#23ab3fd59290c61248364eabf4067f04955fbb1b" + integrity sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ== + varint@^5.0.0: version "5.0.2" resolved "https://registry.yarnpkg.com/varint/-/varint-5.0.2.tgz#5b47f8a947eb668b848e034dcfa87d0ff8a7f7a4" @@ -6555,6 +6862,11 @@ yocto-queue@^0.1.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== +zod@^3.21.4: + version "3.21.4" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.21.4.tgz#10882231d992519f0a10b5dd58a38c9dabbb64db" + integrity sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw== + "zp-memo-parser@link:zp-memo-parser": version "0.0.3" dependencies: diff --git a/zp-relayer/configs/baseConfig.ts b/zp-relayer/configs/baseConfig.ts index a3c0da46..e541a6ef 100644 --- a/zp-relayer/configs/baseConfig.ts +++ b/zp-relayer/configs/baseConfig.ts @@ -1,20 +1,36 @@ -const config = { - poolAddress: process.env.COMMON_POOL_ADDRESS as string, - startBlock: parseInt(process.env.COMMON_START_BLOCK || '0'), - colorizeLogs: process.env.COMMON_COLORIZE_LOGS === 'true', - logLevel: process.env.COMMON_LOG_LEVEL || 'debug', - redisUrl: process.env.COMMON_REDIS_URL as string, - rpcUrls: (process.env.COMMON_RPC_URL as string).split(' ').filter(url => url.length > 0), - requireHTTPS: process.env.COMMON_REQUIRE_RPC_HTTPS === 'true', - rpcSyncCheckInterval: parseInt(process.env.COMMON_RPC_SYNC_STATE_CHECK_INTERVAL || '0'), - rpcRequestTimeout: parseInt(process.env.COMMON_RPC_REQUEST_TIMEOUT || '1000'), - jsonRpcErrorCodes: (process.env.COMMON_JSONRPC_ERROR_CODES || '-32603 -32002 -32005') - .split(' ') - .filter(s => s.length > 0) - .map(s => parseInt(s, 10)), - eventsProcessingBatchSize: parseInt(process.env.COMMON_EVENTS_PROCESSING_BATCH_SIZE || '10000'), - screenerUrl: process.env.COMMON_SCREENER_URL || null, - screenerToken: process.env.COMMON_SCREENER_TOKEN || null, -} +import { z } from 'zod' + +export const zBooleanString = () => z.enum(['true', 'false']).transform(value => value === 'true') +export const zNullishString = () => + z + .string() + .optional() + .transform(x => x ?? null) + +const schema = z.object({ + COMMON_POOL_ADDRESS: z.string(), + COMMON_START_BLOCK: z.coerce.number().default(0), + COMMON_COLORIZE_LOGS: zBooleanString().default('false'), + COMMON_LOG_LEVEL: z.string().default('debug'), + COMMON_REDIS_URL: z.string(), + COMMON_RPC_URL: z.string().transform(us => us.split(' ').filter(url => url.length > 0)), + COMMON_REQUIRE_RPC_HTTPS: zBooleanString().default('false'), + COMMON_RPC_SYNC_STATE_CHECK_INTERVAL: z.coerce.number().default(0), + COMMON_RPC_REQUEST_TIMEOUT: z.coerce.number().default(1000), + COMMON_JSONRPC_ERROR_CODES: z + .string() + .transform(s => + s + .split(' ') + .filter(s => s.length > 0) + .map(s => parseInt(s, 10)) + ) + .default('-32603 -32002 -32005'), + COMMON_EVENTS_PROCESSING_BATCH_SIZE: z.coerce.number().default(10000), + COMMON_SCREENER_URL: zNullishString(), + COMMON_SCREENER_TOKEN: zNullishString(), +}) + +const config = schema.parse(process.env) export default config diff --git a/zp-relayer/configs/guardConfig.ts b/zp-relayer/configs/guardConfig.ts new file mode 100644 index 00000000..833d5038 --- /dev/null +++ b/zp-relayer/configs/guardConfig.ts @@ -0,0 +1,22 @@ +import { z } from 'zod' +import { Network } from '@/services/network/types' + +export const zBooleanString = () => z.enum(['true', 'false']).transform(value => value === 'true') + +const schema = z.object({ + GUARD_PORT: z.coerce.number(), + GUARD_NETWORK: z.nativeEnum(Network), + COMMON_RPC_URL: z.string().transform(us => us.split(' ').filter(url => url.length > 0)), + GUARD_ADDRESS_PRIVATE_KEY: z.string(), + GUARD_TOKEN_ADDRESS: z.string(), + COMMON_REQUIRE_RPC_HTTPS: zBooleanString().default('false'), + COMMON_POOL_ADDRESS: z.string(), + GUARD_TX_VK_PATH: z.string().default('../params/transfer_verification_key.json'), + GUARD_TREE_VK_PATH: z.string().default('../params/tree_verification_key.json'), +}) + +const config = schema.parse(process.env) + +export default { + ...config, +} diff --git a/zp-relayer/configs/loggerConfig.ts b/zp-relayer/configs/loggerConfig.ts new file mode 100644 index 00000000..7fcbd3e3 --- /dev/null +++ b/zp-relayer/configs/loggerConfig.ts @@ -0,0 +1,12 @@ +import { z } from 'zod' + +export const zBooleanString = () => z.enum(['true', 'false']).transform(value => value === 'true') + +const schema = z.object({ + COMMON_COLORIZE_LOGS: zBooleanString().default('false'), + COMMON_LOG_LEVEL: z.string().default('debug'), +}) + +const config = schema.parse(process.env) + +export default config diff --git a/zp-relayer/configs/relayerConfig.ts b/zp-relayer/configs/relayerConfig.ts index e3a56575..41d2326d 100644 --- a/zp-relayer/configs/relayerConfig.ts +++ b/zp-relayer/configs/relayerConfig.ts @@ -1,14 +1,16 @@ import Web3 from 'web3' import { toBN } from 'web3-utils' -import baseConfig from './baseConfig' +import baseConfig, { zBooleanString, zNullishString } from './baseConfig' import { FeeManagerType } from '@/services/fee' import { PriceFeedType } from '@/services/price-feed' -import type { EstimationType, GasPriceKey } from '@/services/gas-price' +import { EstimationType } from '@/services/gas-price' import { ProverType } from '@/prover' import { countryCodes } from '@/utils/countryCodes' import { logger } from '@/services/appLogger' import { PermitType } from '@/utils/permit/types' import { TxType } from 'zp-memo-parser' +import { z } from 'zod' +import { Network } from '@/services/network/types' const relayerAddress = new Web3().eth.accounts.privateKeyToAccount( process.env.RELAYER_ADDRESS_PRIVATE_KEY as string @@ -17,72 +19,146 @@ const relayerAddress = new Web3().eth.accounts.privateKeyToAccount( const defaultHeaderBlacklist = 'accept accept-language accept-encoding connection content-length content-type postman-token referer upgrade-insecure-requests' -const config = { - ...baseConfig, - relayerRef: process.env.RELAYER_REF || null, - relayerSHA: process.env.RELAYER_SHA || null, - port: parseInt(process.env.RELAYER_PORT || '8000'), - relayerAddress, - relayerPrivateKey: process.env.RELAYER_ADDRESS_PRIVATE_KEY as string, - tokenAddress: process.env.RELAYER_TOKEN_ADDRESS as string, - relayerGasLimit: toBN(process.env.RELAYER_GAS_LIMIT as string), - minBaseFee: toBN(process.env.RELAYER_MIN_BASE_FEE || '0'), - relayerFee: process.env.RELAYER_FEE ? toBN(process.env.RELAYER_FEE) : null, - maxNativeAmount: toBN(process.env.RELAYER_MAX_NATIVE_AMOUNT || '0'), - treeUpdateParamsPath: process.env.RELAYER_TREE_UPDATE_PARAMS_PATH || './params/tree_params.bin', - transferParamsPath: process.env.RELAYER_TRANSFER_PARAMS_PATH || './params/transfer_params.bin', - directDepositParamsPath: process.env.RELAYER_DIRECT_DEPOSIT_PARAMS_PATH || './params/delegated_deposit_params.bin', - txVKPath: process.env.RELAYER_TX_VK_PATH || './params/transfer_verification_key.json', - requestLogPath: process.env.RELAYER_REQUEST_LOG_PATH || './zp.log', - stateDirPath: process.env.RELAYER_STATE_DIR_PATH || './POOL_STATE', - gasPriceFallback: process.env.RELAYER_GAS_PRICE_FALLBACK as string, - gasPriceEstimationType: (process.env.RELAYER_GAS_PRICE_ESTIMATION_TYPE as EstimationType) || 'web3', - gasPriceSpeedType: (process.env.RELAYER_GAS_PRICE_SPEED_TYPE as GasPriceKey) || 'fast', - gasPriceFactor: parseInt(process.env.RELAYER_GAS_PRICE_FACTOR || '1'), - gasPriceUpdateInterval: parseInt(process.env.RELAYER_GAS_PRICE_UPDATE_INTERVAL || '5000'), - gasPriceSurplus: parseFloat(process.env.RELAYER_GAS_PRICE_SURPLUS || '0.1'), - minGasPriceBumpFactor: parseFloat(process.env.RELAYER_MIN_GAS_PRICE_BUMP_FACTOR || '0.1'), - maxFeeLimit: process.env.RELAYER_MAX_FEE_PER_GAS_LIMIT ? toBN(process.env.RELAYER_MAX_FEE_PER_GAS_LIMIT) : null, - maxSentQueueSize: parseInt(process.env.RELAYER_MAX_SENT_QUEUE_SIZE || '20'), - relayerTxRedundancy: process.env.RELAYER_TX_REDUNDANCY === 'true', - sentTxDelay: parseInt(process.env.RELAYER_SENT_TX_DELAY || '30000'), - sentTxLogErrorThreshold: parseInt(process.env.RELAYER_SENT_TX_ERROR_THRESHOLD || '3'), - insufficientBalanceCheckTimeout: parseInt(process.env.RELAYER_INSUFFICIENT_BALANCE_CHECK_TIMEOUT || '60000'), - permitDeadlineThresholdInitial: parseInt(process.env.RELAYER_PERMIT_DEADLINE_THRESHOLD_INITIAL || '300'), - requireTraceId: process.env.RELAYER_REQUIRE_TRACE_ID === 'true', - requireLibJsVersion: process.env.RELAYER_REQUIRE_LIBJS_VERSION === 'true', - logIgnoreRoutes: (process.env.RELAYER_LOG_IGNORE_ROUTES || '').split(' ').filter(r => r.length > 0), - logHeaderBlacklist: (process.env.RELAYER_LOG_HEADER_BLACKLIST || defaultHeaderBlacklist) - .split(' ') - .filter(r => r.length > 0), - blockedCountries: (process.env.RELAYER_BLOCKED_COUNTRIES || '').split(' ').filter(c => { - if (c.length === 0) return false +const zBN = () => z.string().transform(toBN) + +const zTreeProver = z.discriminatedUnion('RELAYER_TREE_PROVER_TYPE', [ + z.object({ RELAYER_TREE_PROVER_TYPE: z.literal(ProverType.Local) }), + z.object({ RELAYER_TREE_PROVER_TYPE: z.literal(ProverType.Remote) }), // TODO remote prover url +]) - const exists = countryCodes.has(c) - if (!exists) { - logger.error(`Country code ${c} is not valid, skipping`) - } - return exists +const zDirectDepositProver = z.discriminatedUnion('RELAYER_DD_PROVER_TYPE', [ + z.object({ RELAYER_DD_PROVER_TYPE: z.literal(ProverType.Local) }), + z.object({ RELAYER_DD_PROVER_TYPE: z.literal(ProverType.Remote) }), // TODO remote prover url +]) + +const zPriceFeed = z.discriminatedUnion('RELAYER_PRICE_FEED_TYPE', [ + z.object({ RELAYER_PRICE_FEED_TYPE: z.literal(PriceFeedType.Native) }), + z.object({ + RELAYER_PRICE_FEED_TYPE: z.literal(PriceFeedType.OneInch), + RELAYER_PRICE_FEED_CONTRACT_ADDRESS: z.string(), + RELAYER_PRICE_FEED_BASE_TOKEN_ADDRESS: z.string(), }), - trustProxy: process.env.RELAYER_EXPRESS_TRUST_PROXY === 'true', - treeProverType: (process.env.RELAYER_TREE_PROVER_TYPE || ProverType.Local) as ProverType, - directDepositProverType: (process.env.RELAYER_DD_PROVER_TYPE || ProverType.Local) as ProverType, - feeManagerType: (process.env.RELAYER_FEE_MANAGER_TYPE || FeeManagerType.Dynamic) as FeeManagerType, - feeManagerUpdateInterval: parseInt(process.env.RELAYER_FEE_MANAGER_UPDATE_INTERVAL || '10000'), - feeMarginFactor: toBN(process.env.RELAYER_FEE_MARGIN_FACTOR || '100'), - feeScalingFactor: toBN(process.env.RELAYER_FEE_SCALING_FACTOR || '100'), - priceFeedType: (process.env.RELAYER_PRICE_FEED_TYPE || PriceFeedType.Native) as PriceFeedType, - priceFeedContractAddress: process.env.RELAYER_PRICE_FEED_CONTRACT_ADDRESS || null, - priceFeedBaseTokenAddress: process.env.RELAYER_PRICE_FEED_BASE_TOKEN_ADDRESS || null, - precomputeParams: process.env.RELAYER_PRECOMPUTE_PARAMS === 'true', - permitType: (process.env.RELAYER_PERMIT_TYPE || PermitType.SaltedPermit) as PermitType, - baseTxGas: { - [TxType.DEPOSIT]: toBN(process.env.RELAYER_BASE_TX_GAS_DEPOSIT || '650000'), - [TxType.PERMITTABLE_DEPOSIT]: toBN(process.env.RELAYER_BASE_TX_GAS_PERMITTABLE_DEPOSIT || '650000'), - [TxType.TRANSFER]: toBN(process.env.RELAYER_BASE_TX_GAS_TRANSFER || '650000'), - [TxType.WITHDRAWAL]: toBN(process.env.RELAYER_BASE_TX_GAS_WITHDRAWAL || '650000'), - nativeConvertOverhead: toBN(process.env.RELAYER_BASE_TX_GAS_NATIVE_CONVERT || '200000'), - }, -} +]) + +const zBaseTxGas = z + .object({ + RELAYER_BASE_TX_GAS_DEPOSIT: zBN().default('650000'), + RELAYER_BASE_TX_GAS_PERMITTABLE_DEPOSIT: zBN().default('650000'), + RELAYER_BASE_TX_GAS_TRANSFER: zBN().default('650000'), + RELAYER_BASE_TX_GAS_WITHDRAWAL: zBN().default('650000'), + RELAYER_BASE_TX_GAS_NATIVE_CONVERT: zBN().default('200000'), + }) + .transform(o => ({ + baseTxGas: { + [TxType.DEPOSIT]: o.RELAYER_BASE_TX_GAS_DEPOSIT, + [TxType.PERMITTABLE_DEPOSIT]: o.RELAYER_BASE_TX_GAS_PERMITTABLE_DEPOSIT, + [TxType.TRANSFER]: o.RELAYER_BASE_TX_GAS_TRANSFER, + [TxType.WITHDRAWAL]: o.RELAYER_BASE_TX_GAS_WITHDRAWAL, + RELAYER_BASE_TX_GAS_NATIVE_CONVERT: o.RELAYER_BASE_TX_GAS_NATIVE_CONVERT, + }, + })) + +const zFeeManager = z + .object({ + RELAYER_FEE_MARGIN_FACTOR: zBN().default('100'), + RELAYER_FEE_SCALING_FACTOR: zBN().default('100'), + RELAYER_FEE_MANAGER_UPDATE_INTERVAL: z.coerce.number().default(10000), + }) + .and( + z.discriminatedUnion('RELAYER_FEE_MANAGER_TYPE', [ + z.object({ RELAYER_FEE_MANAGER_TYPE: z.literal(FeeManagerType.Optimism) }), + z.object({ RELAYER_FEE_MANAGER_TYPE: z.literal(FeeManagerType.Dynamic) }), + z.object({ RELAYER_FEE_MANAGER_TYPE: z.literal(FeeManagerType.Static), RELAYER_FEE: zBN() }), + ]) + ) + +const zGasPrice = z.object({ + RELAYER_GAS_PRICE_ESTIMATION_TYPE: z.nativeEnum(EstimationType).default(EstimationType.Web3), + RELAYER_GAS_PRICE_UPDATE_INTERVAL: z.coerce.number().default(5000), + RELAYER_GAS_PRICE_SURPLUS: z.coerce.number().default(0.1), + RELAYER_MIN_GAS_PRICE_BUMP_FACTOR: z.coerce.number().default(0.1), + RELAYER_GAS_PRICE_FACTOR: z.coerce.number().default(1), + RELAYER_GAS_PRICE_SPEED_TYPE: z.string().default('fast'), + RELAYER_GAS_PRICE_FALLBACK: z.string(), + RELAYER_MAX_FEE_PER_GAS_LIMIT: zBN().nullable().default(null), +}) +z.discriminatedUnion('RELAYER_GAS_PRICE_ESTIMATION_TYPE', [ + z.object({ RELAYER_GAS_PRICE_ESTIMATION_TYPE: z.literal(EstimationType.EIP1559) }), + z.object({ RELAYER_GAS_PRICE_ESTIMATION_TYPE: z.literal(EstimationType.Web3) }), + z.object({ + RELAYER_GAS_PRICE_ESTIMATION_TYPE: z.literal(EstimationType.Oracle), + RELAYER_GAS_PRICE_FALLBACK: z.string(), + }), +]) + +const zGuards = z.object({ + RELAYER_GUARDS_CONFIG_PATH: z.string().optional(), + RELAYER_MPC_GUARD_CONTRACT: z.string().optional(), +}) + +const zSchema = z + .object({ + RELAYER_NETWORK: z.nativeEnum(Network), + RELAYER_REF: zNullishString(), + RELAYER_SHA: zNullishString(), + RELAYER_PORT: z.coerce.number().default(8000), + RELAYER_ADDRESS_PRIVATE_KEY: z.string(), + RELAYER_TOKEN_ADDRESS: z.string(), + RELAYER_GAS_LIMIT: zBN(), + RELAYER_MIN_BASE_FEE: zBN().default('0'), + RELAYER_MAX_NATIVE_AMOUNT: zBN().default('0'), + RELAYER_TREE_UPDATE_PARAMS_PATH: z.string().default('./params/tree_params.bin'), + RELAYER_TRANSFER_PARAMS_PATH: z.string().default('./params/transfer_params.bin'), + RELAYER_DIRECT_DEPOSIT_PARAMS_PATH: z.string().default('./params/delegated_deposit_params.bin'), + RELAYER_TX_VK_PATH: z.string().default('./params/transfer_verification_key.json'), + RELAYER_REQUEST_LOG_PATH: z.string().default('./zp.log'), + RELAYER_STATE_DIR_PATH: z.string().default('./POOL_STATE'), + RELAYER_GAS_PRICE_FALLBACK: z.string(), + RELAYER_TX_REDUNDANCY: zBooleanString().default('false'), + RELAYER_SENT_TX_DELAY: z.coerce.number().default(30000), + RELAYER_SENT_TX_ERROR_THRESHOLD: z.coerce.number().default(3), + RELAYER_INSUFFICIENT_BALANCE_CHECK_TIMEOUT: z.coerce.number().default(60000), + RELAYER_PERMIT_DEADLINE_THRESHOLD_INITIAL: z.coerce.number().default(300), + RELAYER_REQUIRE_TRACE_ID: zBooleanString().default('false'), + RELAYER_REQUIRE_LIBJS_VERSION: zBooleanString().default('false'), + RELAYER_EXPRESS_TRUST_PROXY: zBooleanString().default('false'), + RELAYER_PRECOMPUTE_PARAMS: zBooleanString().default('false'), + RELAYER_LOG_IGNORE_ROUTES: z + .string() + .default('') + .transform(rs => rs.split(' ').filter(r => r.length > 0)), + RELAYER_LOG_HEADER_BLACKLIST: z + .string() + .default(defaultHeaderBlacklist) + .transform(hs => hs.split(' ').filter(r => r.length > 0)), + RELAYER_PERMIT_TYPE: z.nativeEnum(PermitType).default(PermitType.SaltedPermit), + RELAYER_BLOCKED_COUNTRIES: z + .string() + .default('') + .transform(cs => + cs.split(' ').filter(c => { + if (c.length === 0) return false + + const exists = countryCodes.has(c) + if (!exists) { + logger.error(`Country code ${c} is not valid, skipping`) + } + return exists + }) + ), + }) + .and(zTreeProver) + .and(zDirectDepositProver) + .and(zPriceFeed) + .and(zBaseTxGas) + .and(zFeeManager) + .and(zGasPrice) + .and(zGuards) -export default config +const config = zSchema.parse(process.env) + +export default { + ...config, + ...baseConfig, + RELAYER_ADDRESS: relayerAddress, +} diff --git a/zp-relayer/guard/guard.ts b/zp-relayer/guard/guard.ts new file mode 100644 index 00000000..da308c73 --- /dev/null +++ b/zp-relayer/guard/guard.ts @@ -0,0 +1,13 @@ +import express from 'express' +import { logger } from '../services/appLogger' +import { createRouter } from './router' +import { init } from './init' +import config from '@/configs/guardConfig' + +const app = express() + +init().then(({ poolContract }) => { + app.use(createRouter({ poolContract })) + const PORT = config.GUARD_PORT + app.listen(PORT, () => logger.info(`Started guard on port ${PORT}`)) +}) diff --git a/zp-relayer/guard/init.ts b/zp-relayer/guard/init.ts new file mode 100644 index 00000000..c1f5b4e1 --- /dev/null +++ b/zp-relayer/guard/init.ts @@ -0,0 +1,30 @@ +// @ts-ignore +import TronWeb from 'tronweb' +import config from '@/configs/guardConfig' +import { Network, NetworkContract } from '@/services/network' +import { EthereumContract } from '@/services/network/evm/EvmContract' +import { TronContract } from '@/services/network/tron/TronContract' +import Web3 from 'web3' +import PoolAbi from '../abi/pool-abi.json' + +function getPoolContract(): NetworkContract { + if (config.GUARD_NETWORK === Network.Tron) { + const tronWeb = new TronWeb(config.COMMON_RPC_URL[0], config.COMMON_RPC_URL[0], config.COMMON_RPC_URL[0]) + + const address = tronWeb.address.fromPrivateKey(config.GUARD_ADDRESS_PRIVATE_KEY.slice(2)) + tronWeb.setAddress(address) + + return new TronContract(tronWeb, PoolAbi, config.COMMON_POOL_ADDRESS) + } else if (config.GUARD_NETWORK === Network.Ethereum) { + const web3 = new Web3(config.COMMON_RPC_URL[0]) + return new EthereumContract(web3, PoolAbi, config.COMMON_POOL_ADDRESS) + } else { + throw new Error('Unsupported network') + } +} + +export async function init() { + const poolContract = getPoolContract() + + return { poolContract } +} diff --git a/zp-relayer/guard/router.ts b/zp-relayer/guard/router.ts new file mode 100644 index 00000000..48905e1d --- /dev/null +++ b/zp-relayer/guard/router.ts @@ -0,0 +1,108 @@ +// @ts-ignore +import TronWeb from 'tronweb' +import cors from 'cors' +import { keccak256, getBytes } from 'ethers' +import { toBN } from 'web3-utils' +import express, { NextFunction, Request, Response } from 'express' +import { checkSignMPCSchema, validateBatch } from '../validation/api/validation' +import { logger } from '../services/appLogger' +import { TxDataMPC, validateTxMPC } from '../validation/tx/validateTx' +import { TxData, buildTxData } from '@/txProcessor' +import { numToHex, packSignature } from '@/utils/helpers' +import { VK } from 'libzkbob-rs-node' +import { getTxProofField, parseDelta } from '@/utils/proofInputs' +import { ENERGY_SIZE, TOKEN_SIZE, TRANSFER_INDEX_SIZE } from '@/utils/constants' +import config from '@/configs/guardConfig' +import type { Network, NetworkContract } from '@/services/network' + +function wrapErr(f: (_req: Request, _res: Response, _next: NextFunction) => Promise | void) { + return async (req: Request, res: Response, next: NextFunction) => { + try { + await f(req, res, next) + } catch (e) { + next(e) + } + } +} + +interface RouterConfig { + poolContract: NetworkContract +} + +export function createRouter({ poolContract }: RouterConfig) { + const router = express.Router() + + router.use(cors()) + router.use(express.urlencoded({ extended: true })) + router.use(express.json()) + router.use(express.text()) + + router.use((err: any, _req: Request, res: Response, next: NextFunction) => { + if (err) { + logger.error('Request error:', err) + return res.sendStatus(500) + } + next() + }) + + router.post( + '/sign', + wrapErr(async (req: Request, res: Response) => { + validateBatch([[checkSignMPCSchema, req.body]]) + const message = req.body as TxDataMPC + + // Validate + const txVK: VK = require(config.GUARD_TX_VK_PATH) + const treeVK: VK = require(config.GUARD_TREE_VK_PATH) + + const poolId = toBN(await poolContract.call('pool_id')) + + try { + await validateTxMPC(message, poolId, treeVK, txVK) + } catch (e) { + logger.error('Validation error', e) + throw new Error('Invalid transaction') + } + + // Sign + const { txProof, treeProof } = message + const nullifier = getTxProofField(txProof, 'nullifier') + const outCommit = getTxProofField(txProof, 'out_commit') + const delta = parseDelta(getTxProofField(txProof, 'delta')) + + const rootAfter = treeProof.inputs[1] + + const txData: TxData = { + txProof: message.txProof.proof, + treeProof: message.treeProof.proof, + nullifier: numToHex(toBN(nullifier)), + outCommit: numToHex(toBN(outCommit)), + rootAfter: numToHex(toBN(rootAfter)), + delta: { + transferIndex: numToHex(delta.transferIndex, TRANSFER_INDEX_SIZE), + energyAmount: numToHex(delta.energyAmount, ENERGY_SIZE), + tokenAmount: numToHex(delta.tokenAmount, TOKEN_SIZE), + }, + txType: message.txType, + memo: message.memo, + depositSignature: message.depositSignature, + } + + const transferRoot = numToHex(toBN(getTxProofField(txProof, 'root'))) + const currentRoot = numToHex(toBN(treeProof.inputs[0])) + logger.debug(`Using transferRoot: ${transferRoot}; Current root: ${currentRoot}; PoolId ${poolId}`) + + let calldata = buildTxData(txData) + calldata += transferRoot + currentRoot + numToHex(poolId) + + logger.debug(`Signing ${calldata}`) + const digest = getBytes(keccak256(calldata)) + const signature = packSignature(await TronWeb.Trx.signMessageV2(digest, config.GUARD_ADDRESS_PRIVATE_KEY)) + + logger.info(`Signed ${signature}}`) + res.json({ signature }) + }) + ) + + return router +} diff --git a/zp-relayer/index.ts b/zp-relayer/index.ts deleted file mode 100644 index bc96453d..00000000 --- a/zp-relayer/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -import express from 'express' -import { createRouter } from './router' -import { logger } from './services/appLogger' -import { createConsoleLoggerMiddleware, createPersistentLoggerMiddleware } from './services/loggerMiddleware' -import config from './configs/relayerConfig' -import { init } from './init' - -init().then(({ feeManager }) => { - const app = express() - - if (config.trustProxy) { - app.set('trust proxy', true) - } - - app.use(createPersistentLoggerMiddleware(config.requestLogPath)) - app.use(createConsoleLoggerMiddleware()) - - app.use(createRouter({ feeManager })) - const PORT = config.port - app.listen(PORT, () => logger.info(`Started relayer on port ${PORT}`)) -}) diff --git a/zp-relayer/init.ts b/zp-relayer/init.ts deleted file mode 100644 index 4513cff1..00000000 --- a/zp-relayer/init.ts +++ /dev/null @@ -1,134 +0,0 @@ -import type Web3 from 'web3' -import { Mutex } from 'async-mutex' -import { Params } from 'libzkbob-rs-node' -import { pool } from './pool' -import { EstimationType, GasPrice } from './services/gas-price' -import { web3 } from './services/web3' -import { web3Redundant } from './services/web3Redundant' -import config from './configs/relayerConfig' -import { createPoolTxWorker } from './workers/poolTxWorker' -import { createSentTxWorker } from './workers/sentTxWorker' -import { createDirectDepositWorker } from './workers/directDepositWorker' -import { redis } from './services/redisClient' -import { validateTx } from './validation/tx/validateTx' -import { TxManager } from './tx/TxManager' -import { Circuit, IProver, LocalProver, ProverType, RemoteProver } from './prover' -import { FeeManagerType, FeeManager, StaticFeeManager, DynamicFeeManager, OptimismFeeManager } from './services/fee' -import type { IPriceFeed } from './services/price-feed/IPriceFeed' -import type { IWorkerBaseConfig } from './workers/workerTypes' -import { NativePriceFeed, OneInchPriceFeed, PriceFeedType } from './services/price-feed' - -function buildProver(circuit: T, type: ProverType, path: string): IProver { - if (type === ProverType.Local) { - const params = Params.fromFile(path, config.precomputeParams) - return new LocalProver(circuit, params) - } else if (type === ProverType.Remote) { - // TODO: test relayer with remote prover - return new RemoteProver('') - } else { - throw new Error('Unsupported prover type') - } -} - -function buildFeeManager( - type: FeeManagerType, - priceFeed: IPriceFeed, - gasPrice: GasPrice, - web3: Web3 -): FeeManager { - const managerConfig = { - priceFeed, - scaleFactor: config.feeScalingFactor, - marginFactor: config.feeMarginFactor, - updateInterval: config.feeManagerUpdateInterval, - } - if (type === FeeManagerType.Static) { - if (config.relayerFee === null) throw new Error('Static relayer fee is not set') - return new StaticFeeManager(managerConfig, config.relayerFee) - } - if (type === FeeManagerType.Dynamic) { - return new DynamicFeeManager(managerConfig, gasPrice) - } else if (type === FeeManagerType.Optimism) { - return new OptimismFeeManager(managerConfig, gasPrice, web3) - } else { - throw new Error('Unsupported fee manager') - } -} - -function buildPriceFeed(type: PriceFeedType, web3: Web3): IPriceFeed { - if (type === PriceFeedType.OneInch) { - if (!config.priceFeedContractAddress) throw new Error('Price feed contract address is not set') - return new OneInchPriceFeed(web3, config.priceFeedContractAddress, { - poolTokenAddress: config.tokenAddress, - customBaseTokenAddress: config.priceFeedBaseTokenAddress, - }) - } else if (type === PriceFeedType.Native) { - return new NativePriceFeed() - } else { - throw new Error('Unsupported price feed') - } -} - -export async function init() { - await pool.init() - - const gasPriceService = new GasPrice( - web3, - { gasPrice: config.gasPriceFallback }, - config.gasPriceUpdateInterval, - config.gasPriceEstimationType, - { - speedType: config.gasPriceSpeedType, - factor: config.gasPriceFactor, - maxFeeLimit: config.maxFeeLimit, - } - ) - await gasPriceService.start() - - const txManager = new TxManager(web3Redundant, config.relayerPrivateKey, gasPriceService) - await txManager.init() - - const mutex = new Mutex() - - const baseConfig: IWorkerBaseConfig = { - redis, - } - - const treeProver = buildProver(Circuit.Tree, config.treeProverType, config.treeUpdateParamsPath as string) - - const directDepositProver = buildProver( - Circuit.DirectDeposit, - config.directDepositProverType, - config.directDepositParamsPath as string - ) - - const priceFeed = buildPriceFeed(config.priceFeedType, web3) - await priceFeed.init() - const feeManager = buildFeeManager(config.feeManagerType, priceFeed, gasPriceService, web3) - await feeManager.start() - - const workerPromises = [ - createPoolTxWorker({ - ...baseConfig, - validateTx, - treeProver, - mutex, - txManager, - feeManager, - }), - createSentTxWorker({ - ...baseConfig, - mutex, - txManager, - }), - createDirectDepositWorker({ - ...baseConfig, - directDepositProver, - }), - ] - - const workers = await Promise.all(workerPromises) - workers.forEach(w => w.run()) - - return { feeManager } -} diff --git a/zp-relayer/package.json b/zp-relayer/package.json index bdd39a2b..6f7f7621 100644 --- a/zp-relayer/package.json +++ b/zp-relayer/package.json @@ -6,8 +6,10 @@ "scripts": { "initialize": "yarn install --frozen-lockfile", "build": "tsc --project ./ && tsc-alias", - "start:dev": "DOTENV_CONFIG_PATH=relayer.env ts-node -r dotenv/config index.ts", - "start:prod": "node index.js", + "start:dev": "DOTENV_CONFIG_PATH=relayer.env ts-node -r dotenv/config relayer/relayer.ts", + "start:mpc:guard:dev": "DOTENV_CONFIG_PATH=guard.env ts-node -r dotenv/config guard/guard.ts", + "start:mpc:guard:prod": "node guard/guard.js", + "start:prod": "node relayer/relayer.js", "start:direct-deposit-watcher:dev": "DOTENV_CONFIG_PATH=watcher.env ts-node -r dotenv/config direct-deposit/watcher.ts", "start:direct-deposit-watcher:prod": "node direct-deposit/watcher.js", "deploy:local": "ts-node test/deploy.ts", @@ -31,8 +33,10 @@ "libzkbob-rs-node": "1.1.0", "promise-retry": "^2.0.1", "semver": "7.3.8", + "tronweb": "^5.3.0", "web3": "1.7.4", "winston": "3.3.3", + "zod": "^3.21.4", "zp-memo-parser": "link:../zp-memo-parser" }, "devDependencies": { diff --git a/zp-relayer/pool.ts b/zp-relayer/pool.ts index 314d17de..0a62e160 100644 --- a/zp-relayer/pool.ts +++ b/zp-relayer/pool.ts @@ -1,23 +1,23 @@ import BN from 'bn.js' -import PoolAbi from './abi/pool-abi.json' -import TokenAbi from './abi/token-abi.json' -import { AbiItem, toBN } from 'web3-utils' -import type { Contract } from 'web3-eth-contract' +import { toBN } from 'web3-utils' import config from './configs/relayerConfig' -import { web3 } from './services/web3' import { logger } from './services/appLogger' import { redis } from './services/redisClient' -import { poolTxQueue, WorkerTxType, WorkerTxTypePriority } from './queue/poolTxQueue' +import { JobState, poolTxQueue, WorkerTxType, WorkerTxTypePriority } from './queue/poolTxQueue' import { getBlockNumber, getEvents, getTransaction } from './utils/web3' import { Helpers, Proof, SnarkProof, VK } from 'libzkbob-rs-node' import { PoolState } from './state/PoolState' import type { TxType } from 'zp-memo-parser' -import { contractCallRetry, numToHex, toTxType, truncateHexPrefix, truncateMemoTxPrefix } from './utils/helpers' +import { buildPrefixedMemo, numToHex, toTxType, truncateHexPrefix, truncateMemoTxPrefix } from './utils/helpers' import { PoolCalldataParser } from './utils/PoolCalldataParser' import { OUTPLUSONE, PERMIT2_CONTRACT } from './utils/constants' import { Permit2Recover, SaltedPermitRecover, TransferWithAuthorizationRecover } from './utils/permit' import { PermitRecover, PermitType } from './utils/permit/types' +import { isEthereum, isTron, NetworkBackend } from './services/network/NetworkBackend' +import { Network } from './services/network/types' +import AbiCoder from 'web3-eth-abi' +import { hexToNumber, hexToNumberString } from 'web3-utils' export interface PoolTx { proof: Proof @@ -74,26 +74,20 @@ export interface LimitsFetch { tier: string } -class Pool { - public PoolInstance: Contract - public TokenInstance: Contract - private txVK: VK +export class Pool { + public txVK: VK public state: PoolState public optimisticState: PoolState public denominator: BN = toBN(1) public poolId: BN = toBN(0) public isInitialized = false - public permitRecover!: PermitRecover + public permitRecover: PermitRecover | null = null - constructor() { - this.PoolInstance = new web3.eth.Contract(PoolAbi as AbiItem[], config.poolAddress) - this.TokenInstance = new web3.eth.Contract(TokenAbi as AbiItem[], config.tokenAddress) + constructor(public network: NetworkBackend) { + this.txVK = require(config.RELAYER_TX_VK_PATH) - const txVK = require(config.txVKPath) - this.txVK = txVK - - this.state = new PoolState('pool', redis, config.stateDirPath) - this.optimisticState = new PoolState('optimistic', redis, config.stateDirPath) + this.state = new PoolState('pool', redis, config.RELAYER_STATE_DIR_PATH) + this.optimisticState = new PoolState('optimistic', redis, config.RELAYER_STATE_DIR_PATH) } loadState(states: { poolState: PoolState; optimisticState: PoolState }) { @@ -101,41 +95,44 @@ class Pool { this.optimisticState = states.optimisticState } - async init() { + async init(sync: boolean = true) { if (this.isInitialized) return - this.denominator = toBN(await this.PoolInstance.methods.denominator().call()) - this.poolId = toBN(await this.PoolInstance.methods.pool_id().call()) - - if (config.permitType === PermitType.SaltedPermit) { - this.permitRecover = new SaltedPermitRecover(web3, config.tokenAddress) - } else if (config.permitType === PermitType.Permit2) { - this.permitRecover = new Permit2Recover(web3, PERMIT2_CONTRACT) - } else if (config.permitType === PermitType.TransferWithAuthorization) { - this.permitRecover = new TransferWithAuthorizationRecover(web3, config.tokenAddress) + this.denominator = toBN(await this.network.pool.call('denominator')) + this.poolId = toBN(await this.network.pool.call('pool_id')) + + if (config.RELAYER_PERMIT_TYPE === PermitType.SaltedPermit) { + this.permitRecover = new SaltedPermitRecover(this.network, config.RELAYER_TOKEN_ADDRESS) + } else if (config.RELAYER_PERMIT_TYPE === PermitType.Permit2) { + this.permitRecover = new Permit2Recover(this.network, PERMIT2_CONTRACT) + } else if (config.RELAYER_PERMIT_TYPE === PermitType.TransferWithAuthorization) { + this.permitRecover = new TransferWithAuthorizationRecover(this.network, config.RELAYER_TOKEN_ADDRESS) + } else if (config.RELAYER_PERMIT_TYPE === PermitType.None) { + this.permitRecover = null } else { throw new Error("Cannot infer pool's permit standard") } - await this.permitRecover.initializeDomain() - - await this.syncState(config.startBlock) + await this.permitRecover?.initializeDomain() + if (sync) { + await this.syncState(config.COMMON_START_BLOCK) + } this.isInitialized = true } - async transact(txs: PoolTx[], traceId?: string) { - const queueTxs = txs.map(({ proof, txType, memo, depositSignature }) => { - return { - amount: '0', - gas: config.relayerGasLimit.toString(), - txProof: proof, - txType, - rawMemo: memo, - depositSignature, - } - }) + async transact(tx: PoolTx, traceId?: string) { + const queueTx = { + amount: '0', + txProof: tx.proof, + txType: tx.txType, + rawMemo: tx.memo, + depositSignature: tx.depositSignature, + txHash: null, + sentJobId: null, + state: JobState.WAITING, + } const job = await poolTxQueue.add( 'tx', - { type: WorkerTxType.Normal, transactions: queueTxs, traceId }, + { type: WorkerTxType.Normal, transaction: queueTx, traceId }, { priority: WorkerTxTypePriority[WorkerTxType.Normal], } @@ -144,8 +141,19 @@ class Pool { return job.id } + async clearOptimisticState() { + logger.info('Rollback optimistic state...') + this.optimisticState.rollbackTo(this.state) + logger.info('Clearing optimistic nullifiers...') + await this.optimisticState.nullifiers.clear() + + const root1 = this.state.getMerkleRoot() + const root2 = this.optimisticState.getMerkleRoot() + logger.info(`Assert roots are equal: ${root1}, ${root2}, ${root1 === root2}`) + } + async getLastBlockToProcess() { - const lastBlockNumber = await getBlockNumber(web3) + const lastBlockNumber = await getBlockNumber(this.network) return lastBlockNumber } @@ -167,87 +175,123 @@ class Pool { } const numTxs = Math.floor((contractIndex - localIndex) / OUTPLUSONE) + if (numTxs < 0) { + // TODO: rollback state + throw new Error('State is corrupted, contract index is less than local index') + } + const missedIndices = Array(numTxs) for (let i = 0; i < numTxs; i++) { missedIndices[i] = localIndex + (i + 1) * OUTPLUSONE } + if (isEthereum(this.network)) { + const lastBlockNumber = (await this.getLastBlockToProcess()) + 1 + let toBlock = startBlock + for (let fromBlock = startBlock; toBlock < lastBlockNumber; fromBlock = toBlock) { + toBlock = Math.min(toBlock + config.COMMON_EVENTS_PROCESSING_BATCH_SIZE, lastBlockNumber) + const events = await getEvents(this.network.pool.instance, 'Message', { + fromBlock, + toBlock: toBlock - 1, + filter: { + index: missedIndices, + }, + }) + + for (let i = 0; i < events.length; i++) { + await this.addTxToState( + events[i].transactionHash, + events[i].returnValues.index, + events[i].returnValues.message + ) + } + } + } else if (isTron(this.network)) { + let fingerprint = null + const MESSAGE_TOPIC = '7d39f8a6bc8929456fba511441be7361aa014ac6f8e21b99990ce9e1c7373536' + do { + const events: any[] = await this.network.tronWeb.getEventResult(this.network.pool.address(), { + sinceTimestamp: 0, + eventName: 'Message', + onlyConfirmed: true, + sort: 'block_timestamp', + size: 200, + fingerprint, + }) + if (events.length === 0) { + break + } + for (let i = 0; i < events.length; i++) { + const txHash = events[i].transaction + const txInfo = await this.network.tronWeb.trx.getTransactionInfo(txHash) + const log = txInfo.log.find((l: any) => l.topics[0] === MESSAGE_TOPIC) + const index = parseInt(log.topics[1], 16) + const message = log.data + await this.addTxToState(events[i].transaction, index, message) + } + fingerprint = events[events.length - 1].fingerprint || null + } while (fingerprint !== null) + } + + const newLocalRoot = this.state.getMerkleRoot() + logger.debug(`LOCAL ROOT AFTER UPDATE ${newLocalRoot}`) + if (newLocalRoot !== contractRoot) { + logger.error('State is corrupted, roots mismatch') + } + } + + async addTxToState(txHash: string, newPoolIndex: number, message: string) { const transactSelector = '0xaf989083' const directDepositSelector = '0x1dc4cb33' - const lastBlockNumber = (await this.getLastBlockToProcess()) + 1 - let toBlock = startBlock - for (let fromBlock = startBlock; toBlock < lastBlockNumber; fromBlock = toBlock) { - toBlock = Math.min(toBlock + config.eventsProcessingBatchSize, lastBlockNumber) - const events = await getEvents(this.PoolInstance, 'Message', { - fromBlock, - toBlock: toBlock - 1, - filter: { - index: missedIndices, - }, - }) - - for (let i = 0; i < events.length; i++) { - const { returnValues, transactionHash } = events[i] - const { input } = await getTransaction(web3, transactionHash) - - const newPoolIndex = Number(returnValues.index) - const prevPoolIndex = newPoolIndex - OUTPLUSONE - const prevCommitIndex = Math.floor(Number(prevPoolIndex) / OUTPLUSONE) - - let outCommit: string - let memo: string - - if (input.startsWith(directDepositSelector)) { - // Direct deposit case - const res = web3.eth.abi.decodeParameters( - [ - 'uint256', // Root after - 'uint256[]', // Indices - 'uint256', // Out commit - 'uint256[8]', // Deposit proof - 'uint256[8]', // Tree proof - ], - input.slice(10) // Cut off selector - ) - outCommit = res[2] - memo = truncateHexPrefix(returnValues.message || '') - } else if (input.startsWith(transactSelector)) { - // Normal tx case - const calldata = Buffer.from(truncateHexPrefix(input), 'hex') + const input = await this.network.getTxCalldata(txHash) - const parser = new PoolCalldataParser(calldata) + const prevPoolIndex = newPoolIndex - OUTPLUSONE + const prevCommitIndex = Math.floor(Number(prevPoolIndex) / OUTPLUSONE) - const outCommitRaw = parser.getField('outCommit') - outCommit = web3.utils.hexToNumberString(outCommitRaw) + let outCommit: string + let memo: string - const txTypeRaw = parser.getField('txType') - const txType = toTxType(txTypeRaw) + if (input.startsWith(directDepositSelector)) { + // Direct deposit case + const res = AbiCoder.decodeParameters( + [ + 'uint256', // Root after + 'uint256[]', // Indices + 'uint256', // Out commit + 'uint256[8]', // Deposit proof + 'uint256[8]', // Tree proof + ], + input.slice(10) // Cut off selector + ) + outCommit = res[2] + memo = truncateHexPrefix(message || '') + } else if (input.startsWith(transactSelector)) { + // Normal tx case + const calldata = Buffer.from(truncateHexPrefix(input), 'hex') - const memoSize = web3.utils.hexToNumber(parser.getField('memoSize')) - const memoRaw = truncateHexPrefix(parser.getField('memo', memoSize)) + const parser = new PoolCalldataParser(calldata) - memo = truncateMemoTxPrefix(memoRaw, txType) + outCommit = hexToNumberString(parser.getField('outCommit')) - // Save nullifier in confirmed state - const nullifier = parser.getField('nullifier') - await this.state.nullifiers.add([web3.utils.hexToNumberString(nullifier)]) - } else { - throw new Error(`Unknown transaction type: ${input}`) - } + const txType = toTxType(parser.getField('txType')) - const commitAndMemo = numToHex(toBN(outCommit)).concat(transactionHash.slice(2)).concat(memo) - for (let state of [this.state, this.optimisticState]) { - state.addCommitment(prevCommitIndex, Helpers.strToNum(outCommit)) - state.addTx(prevPoolIndex, Buffer.from(commitAndMemo, 'hex')) - } - } + const memoSize = hexToNumber(parser.getField('memoSize')) + const memoRaw = truncateHexPrefix(parser.getField('memo', memoSize)) + + memo = truncateMemoTxPrefix(memoRaw, txType) + + // Save nullifier in confirmed state + const nullifier = parser.getField('nullifier') + await this.state.nullifiers.add([hexToNumberString(nullifier)]) + } else { + throw new Error(`Unknown transaction type: ${input}`) } - const newLocalRoot = this.state.getMerkleRoot() - logger.debug(`LOCAL ROOT AFTER UPDATE ${newLocalRoot}`) - if (newLocalRoot !== contractRoot) { - logger.error('State is corrupted, roots mismatch') + const prefixedMemo = buildPrefixedMemo(outCommit, txHash, memo) + for (let state of [this.state, this.optimisticState]) { + state.addCommitment(prevCommitIndex, Helpers.strToNum(outCommit)) + state.addTx(prevPoolIndex, Buffer.from(prefixedMemo, 'hex')) } } @@ -256,7 +300,7 @@ class Pool { } async getContractIndex() { - const poolIndex = await contractCallRetry(this.PoolInstance, 'pool_index') + const poolIndex = await this.network.pool.callRetry('pool_index') return Number(poolIndex) } @@ -265,12 +309,12 @@ class Pool { index = await this.getContractIndex() logger.info('CONTRACT INDEX %d', index) } - const root = await contractCallRetry(this.PoolInstance, 'roots', [index]) + const root = await this.network.pool.callRetry('roots', [index]) return root.toString() } async getLimitsFor(address: string): Promise { - const limits = await contractCallRetry(this.PoolInstance, 'getLimitsFor', [address]) + const limits = await this.network.pool.callRetry('getLimitsFor', [address]) return { tvlCap: toBN(limits.tvlCap), tvl: toBN(limits.tvl), @@ -323,7 +367,3 @@ class Pool { return limitsFetch } } - -export let pool: Pool = new Pool() - -export type { Pool } diff --git a/zp-relayer/queue/poolTxQueue.ts b/zp-relayer/queue/poolTxQueue.ts index 9f64ce82..4fb25d9c 100644 --- a/zp-relayer/queue/poolTxQueue.ts +++ b/zp-relayer/queue/poolTxQueue.ts @@ -4,14 +4,29 @@ import type { Proof } from 'libzkbob-rs-node' import type { TxType } from 'zp-memo-parser' import { redis } from '@/services/redisClient' -export interface TxPayload { - amount: string +export enum JobState { + WAITING = 'waiting', + SENT = 'sent', + COMPLETED = 'completed', + REVERTED = 'reverted', + FAILED = 'failed', +} + +export interface BasePayload { + txHash: string | null + state: JobState +} + +export interface Tx { txProof: Proof + amount: string txType: TxType rawMemo: string depositSignature: string | null } +export interface TxPayload extends BasePayload, Tx {} + interface ZkAddress { diversifier: string pk: string @@ -25,13 +40,15 @@ export interface DirectDeposit { deposit: string } -export interface DirectDepositTxPayload { - deposits: DirectDeposit[] +export interface DirectDepositTx { txProof: Proof + deposits: DirectDeposit[] outCommit: string memo: string } +export interface DirectDepositTxPayload extends BasePayload, DirectDepositTx {} + export enum WorkerTxType { Normal = 'normal', DirectDeposit = 'dd', @@ -48,14 +65,12 @@ export type WorkerTx = T extends WorkerTxType.Normal ? DirectDepositTxPayload : never -export interface BatchTx { +export interface PoolTx { type: T - transactions: M extends true ? WorkerTx[] : WorkerTx + transaction: WorkerTx traceId?: string } -export type PoolTxResult = [string, string] - -export const poolTxQueue = new Queue, PoolTxResult[]>(TX_QUEUE_NAME, { +export const poolTxQueue = new Queue>(TX_QUEUE_NAME, { connection: redis, }) diff --git a/zp-relayer/queue/sentTxQueue.ts b/zp-relayer/queue/sentTxQueue.ts deleted file mode 100644 index f69f6589..00000000 --- a/zp-relayer/queue/sentTxQueue.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Queue } from 'bullmq' -import { SENT_TX_QUEUE_NAME } from '@/utils/constants' -import { redis } from '@/services/redisClient' -import type { TransactionConfig } from 'web3-core' -import type { GasPriceValue } from '@/services/gas-price' -import type { BatchTx, WorkerTxType } from './poolTxQueue' - -export type SendAttempt = [string, GasPriceValue] -export interface SentTxPayload { - poolJobId: string - root: string - outCommit: string - commitIndex: number - truncatedMemo: string - txConfig: TransactionConfig - nullifier?: string - txPayload: BatchTx - prevAttempts: SendAttempt[] -} - -export enum SentTxState { - MINED = 'MINED', - REVERT = 'REVERT', - SKIPPED = 'SKIPPED', -} - -export type SentTxResult = [SentTxState, string, string[]] - -export const sentTxQueue = new Queue(SENT_TX_QUEUE_NAME, { - connection: redis, -}) diff --git a/zp-relayer/queue/submitTxQueue.ts b/zp-relayer/queue/submitTxQueue.ts new file mode 100644 index 00000000..e69de29b diff --git a/zp-relayer/endpoints.ts b/zp-relayer/relayer/endpoints.ts similarity index 55% rename from zp-relayer/endpoints.ts rename to zp-relayer/relayer/endpoints.ts index 5fec68bc..1254c1af 100644 --- a/zp-relayer/endpoints.ts +++ b/zp-relayer/relayer/endpoints.ts @@ -1,8 +1,8 @@ import type { Queue } from 'bullmq' import { Request, Response } from 'express' -import { LimitsFetch, pool, PoolTx } from './pool' -import { poolTxQueue } from './queue/poolTxQueue' -import config from './configs/relayerConfig' +import type { LimitsFetch, Pool, PoolTx } from '../pool' +import { JobState, PoolTx as Tx, WorkerTxType, poolTxQueue } from '../queue/poolTxQueue' +import config from '../configs/relayerConfig' import { validateCountryIP, checkGetLimits, @@ -12,19 +12,37 @@ import { checkSendTransactionsErrors, checkTraceId, validateBatch, -} from './validation/api/validation' -import { sentTxQueue, SentTxState } from './queue/sentTxQueue' -import { HEADER_TRACE_ID } from './utils/constants' -import { getFileHash } from './utils/helpers' -import type { FeeManager } from './services/fee' + ValidationFunction, +} from '../validation/api/validation' +import { HEADER_TRACE_ID } from '../utils/constants' +import type { FeeManager } from '../services/fee' -async function sendTransactions(req: Request, res: Response) { +interface PoolInjection { + pool: Pool +} + +interface FeeManagerInjection { + feeManager: FeeManager +} + +interface HashInjection { + hash: string | null +} + +const checkTraceIdFromConfig: ValidationFunction = (() => { + if (config.RELAYER_REQUIRE_TRACE_ID) { + return checkTraceId + } + return () => null +})() + +async function sendTransactions(req: Request, res: Response, { pool }: PoolInjection) { validateBatch([ - [checkTraceId, req.headers], + [checkTraceIdFromConfig, req.headers], [checkSendTransactionsErrors, req.body], ]) - await validateCountryIP(req.ip) + await validateCountryIP(req.ip, config.RELAYER_BLOCKED_COUNTRIES) const rawTxs = req.body as PoolTx[] const traceId = req.headers[HEADER_TRACE_ID] as string @@ -38,13 +56,16 @@ async function sendTransactions(req: Request, res: Response) { depositSignature, } }) - const jobId = await pool.transact(txs, traceId) + if (txs.length !== 1) { + throw new Error('Batch transactions are not supported') + } + const jobId = await pool.transact(txs[0], traceId) res.json({ jobId }) } -async function merkleRoot(req: Request, res: Response) { +async function merkleRoot(req: Request, res: Response, { pool }: PoolInjection) { validateBatch([ - [checkTraceId, req.headers], + [checkTraceIdFromConfig, req.headers], [checkMerkleRootErrors, req.params], ]) @@ -53,9 +74,9 @@ async function merkleRoot(req: Request, res: Response) { res.json(root) } -async function getTransactionsV2(req: Request, res: Response) { +async function getTransactionsV2(req: Request, res: Response, { pool }: PoolInjection) { validateBatch([ - [checkTraceId, req.headers], + [checkTraceIdFromConfig, req.headers], [checkGetTransactionsV2, req.query], ]) @@ -82,25 +103,17 @@ async function getTransactionsV2(req: Request, res: Response) { res.json(txs) } -async function getJob(req: Request, res: Response) { - enum JobStatus { - WAITING = 'waiting', - FAILED = 'failed', - SENT = 'sent', - REVERTED = 'reverted', - COMPLETED = 'completed', - } - +async function getJob(req: Request, res: Response, { pool }: PoolInjection) { interface GetJobResponse { resolvedJobId: string createdOn: number failedReason: null | string finishedOn: null | number - state: JobStatus + state: JobState txHash: null | string } - validateBatch([[checkTraceId, req.headers]]) + validateBatch([[checkTraceIdFromConfig, req.headers]]) const jobId = req.params.id @@ -108,7 +121,7 @@ async function getJob(req: Request, res: Response) { const INCONSISTENCY_ERR = 'Internal job inconsistency' // Should be used in places where job is expected to exist - const safeGetJob = async (queue: Queue, id: string) => { + const safeGetJob = async (queue: Queue>, id: string) => { const job = await queue.getJob(id) if (!job) { throw new Error(INCONSISTENCY_ERR) @@ -129,7 +142,7 @@ async function getJob(req: Request, res: Response) { createdOn: job.timestamp, failedReason: null, finishedOn: null, - state: JobStatus.WAITING, + state: JobState.WAITING, txHash: null, } @@ -137,45 +150,8 @@ async function getJob(req: Request, res: Response) { // Transaction was included in optimistic state, waiting to be mined // Sanity check - if (job.returnvalue === null) throw new Error(INCONSISTENCY_ERR) - const sentJobId = job.returnvalue[0][1] - - const sentJobState = await sentTxQueue.getJobState(sentJobId) - // Should not happen here, but need to verify to be sure - if (sentJobState === 'unknown') throw new Error('Sent job not found') - - const sentJob = await safeGetJob(sentTxQueue, sentJobId) - if (sentJobState === 'waiting' || sentJobState === 'active' || sentJobState === 'delayed') { - // Transaction is in re-send loop - const txHash = sentJob.data.prevAttempts.at(-1)?.[0] - result.state = JobStatus.SENT - result.txHash = txHash || null - } else if (sentJobState === 'completed') { - // Sanity check - if (sentJob.returnvalue === null) throw new Error(INCONSISTENCY_ERR) - - const [txState, txHash] = sentJob.returnvalue - if (txState === SentTxState.MINED) { - // Transaction mined successfully - result.state = JobStatus.COMPLETED - result.txHash = txHash - result.finishedOn = sentJob.finishedOn || null - } else if (txState === SentTxState.REVERT) { - // Transaction reverted - result.state = JobStatus.REVERTED - result.txHash = txHash - result.finishedOn = sentJob.finishedOn || null - } - } - } else if (poolJobState === 'failed') { - // Either validation or tx sending failed - - // Sanity check - if (!job.finishedOn) throw new Error(INCONSISTENCY_ERR) - - result.state = JobStatus.FAILED - result.failedReason = job.failedReason - result.finishedOn = job.finishedOn || null + // if (job.returnvalue === null) throw new Error(INCONSISTENCY_ERR) + result.state = job.data.transaction.state } // Other states mean that transaction is either waiting in queue // or being processed by worker @@ -192,7 +168,7 @@ async function getJob(req: Request, res: Response) { } } -function relayerInfo(req: Request, res: Response) { +function relayerInfo(req: Request, res: Response, { pool }: PoolInjection) { const deltaIndex = pool.state.getNextIndex() const optimisticDeltaIndex = pool.optimisticState.getNextIndex() const root = pool.state.getMerkleRoot() @@ -206,20 +182,18 @@ function relayerInfo(req: Request, res: Response) { }) } -function getFeeBuilder(feeManager: FeeManager) { - return async (req: Request, res: Response) => { - validateBatch([[checkTraceId, req.headers]]) +async function getFee(req: Request, res: Response, { pool, feeManager }: PoolInjection & FeeManagerInjection) { + validateBatch([[checkTraceIdFromConfig, req.headers]]) - const feeOptions = await feeManager.getFeeOptions() - const fees = feeOptions.denominate(pool.denominator).getObject() + const feeOptions = await feeManager.getFeeOptions() + const fees = feeOptions.denominate(pool.denominator).getObject() - res.json(fees) - } + res.json(fees) } -async function getLimits(req: Request, res: Response) { +async function getLimits(req: Request, res: Response, { pool }: PoolInjection) { validateBatch([ - [checkTraceId, req.headers], + [checkTraceIdFromConfig, req.headers], [checkGetLimits, req.query], ]) @@ -237,16 +211,16 @@ async function getLimits(req: Request, res: Response) { } function getMaxNativeAmount(req: Request, res: Response) { - validateBatch([[checkTraceId, req.headers]]) + validateBatch([[checkTraceIdFromConfig, req.headers]]) res.json({ - maxNativeAmount: config.maxNativeAmount.toString(10), + maxNativeAmount: config.RELAYER_MAX_NATIVE_AMOUNT.toString(10), }) } -function getSiblings(req: Request, res: Response) { +function getSiblings(req: Request, res: Response, { pool }: PoolInjection) { validateBatch([ - [checkTraceId, req.headers], + [checkTraceIdFromConfig, req.headers], [checkGetSiblings, req.query], ]) @@ -261,20 +235,14 @@ function getSiblings(req: Request, res: Response) { res.json(siblings) } -function getParamsHashBuilder(path: string | null) { - let hash: string | null = null - if (path) { - hash = getFileHash(path) - } - return (req: Request, res: Response) => { - res.json({ hash }) - } +function getParamsHash(req: Request, res: Response, { hash }: HashInjection) { + res.json({ hash }) } function relayerVersion(req: Request, res: Response) { res.json({ - ref: config.relayerRef, - commitHash: config.relayerSHA, + ref: config.RELAYER_REF, + commitHash: config.RELAYER_SHA, }) } @@ -282,17 +250,24 @@ function root(req: Request, res: Response) { return res.sendStatus(200) } +export function inject(values: T, f: (req: Request, res: Response, e: T) => void) { + return (req: Request, res: Response) => { + return f(req, res, values) + } +} + export default { sendTransactions, merkleRoot, getTransactionsV2, getJob, relayerInfo, - getFeeBuilder, + getFee, getLimits, getMaxNativeAmount, getSiblings, - getParamsHashBuilder, + getParamsHash, relayerVersion, root, + inject, } diff --git a/zp-relayer/relayer/init.ts b/zp-relayer/relayer/init.ts new file mode 100644 index 00000000..606cce72 --- /dev/null +++ b/zp-relayer/relayer/init.ts @@ -0,0 +1,149 @@ +import { Mutex } from 'async-mutex' +import { Params } from 'libzkbob-rs-node' +import { Pool } from '../pool' +import config from '../configs/relayerConfig' +import { createPoolTxWorker } from '../workers/poolTxWorker' +import { createDirectDepositWorker } from '../workers/directDepositWorker' +import { redis } from '../services/redisClient' +import { validateTx } from '../validation/tx/validateTx' +import { Circuit, IProver, LocalProver, ProverType, RemoteProver } from '../prover' +import { FeeManagerType, FeeManager, StaticFeeManager, DynamicFeeManager, OptimismFeeManager } from '../services/fee' +import type { IPriceFeed } from '../services/price-feed/IPriceFeed' +import type { IWorkerBaseConfig } from '../workers/workerTypes' +import { NativePriceFeed, OneInchPriceFeed, PriceFeedType } from '../services/price-feed' +import { Network, TronBackend, EvmBackend, NetworkBackend, isEthereum } from '../services/network' + +function buildProver(circuit: T, type: ProverType, path: string): IProver { + switch (type) { + case ProverType.Local: { + const params = Params.fromFile(path, config.RELAYER_PRECOMPUTE_PARAMS) + return new LocalProver(circuit, params) + } + case ProverType.Remote: + // TODO: test relayer with remote prover + return new RemoteProver(path) + default: + throw new Error('Unsupported prover type') + } +} + +function buildPriceFeed(network: NetworkBackend): IPriceFeed { + switch (config.RELAYER_PRICE_FEED_TYPE) { + case PriceFeedType.OneInch: + return new OneInchPriceFeed(network, config.RELAYER_PRICE_FEED_CONTRACT_ADDRESS, { + poolTokenAddress: config.RELAYER_TOKEN_ADDRESS, + customBaseTokenAddress: config.RELAYER_PRICE_FEED_BASE_TOKEN_ADDRESS, + }) + case PriceFeedType.Native: + return new NativePriceFeed() + default: + throw new Error('Unsupported price feed') + } +} + +export async function init() { + let networkBackend: NetworkBackend + const baseConfig = { + poolAddress: config.COMMON_POOL_ADDRESS, + tokenAddress: config.RELAYER_TOKEN_ADDRESS, + pk: config.RELAYER_ADDRESS_PRIVATE_KEY, + rpcUrls: config.COMMON_RPC_URL, + requireHTTPS: config.COMMON_REQUIRE_RPC_HTTPS, + } + if (config.RELAYER_NETWORK === Network.Ethereum) { + networkBackend = new EvmBackend({ + ...baseConfig, + rpcRequestTimeout: config.COMMON_RPC_REQUEST_TIMEOUT, + rpcSyncCheckInterval: config.COMMON_RPC_SYNC_STATE_CHECK_INTERVAL, + jsonRpcErrorCodes: config.COMMON_JSONRPC_ERROR_CODES, + relayerTxRedundancy: config.RELAYER_TX_REDUNDANCY, + + gasPriceFallback: config.RELAYER_GAS_PRICE_FALLBACK, + gasPriceUpdateInterval: config.RELAYER_GAS_PRICE_UPDATE_INTERVAL, + gasPriceEstimationType: config.RELAYER_GAS_PRICE_ESTIMATION_TYPE, + gasPriceSpeedType: config.RELAYER_GAS_PRICE_SPEED_TYPE, + gasPriceFactor: config.RELAYER_GAS_PRICE_FACTOR, + gasPriceMaxFeeLimit: config.RELAYER_MAX_FEE_PER_GAS_LIMIT, + gasPriceBumpFactor: config.RELAYER_MIN_GAS_PRICE_BUMP_FACTOR, + gasPriceSurplus: config.RELAYER_GAS_PRICE_SURPLUS, + redis, + }) + } else if (config.RELAYER_NETWORK === Network.Tron) { + networkBackend = new TronBackend({ + ...baseConfig, + }) + } else { + throw new Error('Unsupported network backend') + } + await networkBackend.init() + + const pool = new Pool(networkBackend) + await pool.init() + + const mutex = new Mutex() + + const workerBaseConfig: IWorkerBaseConfig = { + pool, + redis, + } + + const treeProver = buildProver( + Circuit.Tree, + config.RELAYER_TREE_PROVER_TYPE, + config.RELAYER_TREE_UPDATE_PARAMS_PATH as string + ) + + const directDepositProver = buildProver( + Circuit.DirectDeposit, + config.RELAYER_DD_PROVER_TYPE, + config.RELAYER_DIRECT_DEPOSIT_PARAMS_PATH as string + ) + + const priceFeed = buildPriceFeed(networkBackend) + await priceFeed.init() + + let feeManager: FeeManager + const managerConfig = { + priceFeed, + scaleFactor: config.RELAYER_FEE_SCALING_FACTOR, + marginFactor: config.RELAYER_FEE_MARGIN_FACTOR, + updateInterval: config.RELAYER_FEE_MANAGER_UPDATE_INTERVAL, + } + switch (config.RELAYER_FEE_MANAGER_TYPE) { + case FeeManagerType.Static: + feeManager = new StaticFeeManager(managerConfig, config.RELAYER_FEE) + break + case FeeManagerType.Dynamic: { + if (!isEthereum(networkBackend)) throw new Error('Dynamic fee manager is only supported for Ethereum') + feeManager = new DynamicFeeManager(managerConfig, networkBackend.gasPrice) + break + } + case FeeManagerType.Optimism: { + if (!isEthereum(networkBackend)) throw new Error('Dynamic fee manager is only supported for Ethereum') + feeManager = new OptimismFeeManager(managerConfig, networkBackend) + break + } + default: + throw new Error('Unsupported fee manager') + } + await feeManager.start() + + const workerPromises = [ + createPoolTxWorker({ + ...workerBaseConfig, + validateTx, + treeProver, + mutex, + feeManager, + }), + createDirectDepositWorker({ + ...workerBaseConfig, + directDepositProver, + }), + ] + + const workers = await Promise.all(workerPromises) + workers.forEach(w => w.run()) + + return { feeManager, pool } +} diff --git a/zp-relayer/relayer/relayer.ts b/zp-relayer/relayer/relayer.ts new file mode 100644 index 00000000..c0bdc2b7 --- /dev/null +++ b/zp-relayer/relayer/relayer.ts @@ -0,0 +1,21 @@ +import express from 'express' +import { createRouter } from './router' +import { logger } from '../services/appLogger' +import { createConsoleLoggerMiddleware, createPersistentLoggerMiddleware } from '../services/loggerMiddleware' +import config from '../configs/relayerConfig' +import { init } from './init' + +init().then(({ feeManager, pool }) => { + const app = express() + + if (config.RELAYER_EXPRESS_TRUST_PROXY) { + app.set('trust proxy', true) + } + + app.use(createPersistentLoggerMiddleware(config.RELAYER_REQUEST_LOG_PATH)) + app.use(createConsoleLoggerMiddleware()) + + app.use(createRouter({ feeManager, pool })) + const PORT = config.RELAYER_PORT + app.listen(PORT, () => logger.info(`Started relayer on port ${PORT}`)) +}) diff --git a/zp-relayer/router.ts b/zp-relayer/relayer/router.ts similarity index 56% rename from zp-relayer/router.ts rename to zp-relayer/relayer/router.ts index c0205413..21772b31 100644 --- a/zp-relayer/router.ts +++ b/zp-relayer/relayer/router.ts @@ -1,15 +1,18 @@ import express, { NextFunction, Request, Response } from 'express' import cors from 'cors' import semver from 'semver' -import endpoints from './endpoints' -import { logger } from './services/appLogger' -import { ValidationError } from './validation/api/validation' -import config from './configs/relayerConfig' -import { HEADER_LIBJS, HEADER_TRACE_ID, LIBJS_MIN_VERSION } from './utils/constants' -import type { FeeManager } from './services/fee' +import endpoints, { inject } from './endpoints' +import { logger } from '@/services/appLogger' +import { ValidationError } from '@/validation/api/validation' +import config from '@/configs/relayerConfig' +import { HEADER_LIBJS, HEADER_TRACE_ID, LIBJS_MIN_VERSION } from '../utils/constants' +import { getFileHash } from '@/utils/helpers' +import type { FeeManager } from '@/services/fee' +import type { Pool } from '@/pool' interface IRouterConfig { feeManager: FeeManager + pool: Pool } function wrapErr(f: (_req: Request, _res: Response, _next: NextFunction) => Promise | void) { @@ -22,7 +25,7 @@ function wrapErr(f: (_req: Request, _res: Response, _next: NextFunction) => Prom } } -export function createRouter({ feeManager }: IRouterConfig) { +export function createRouter({ feeManager, pool }: IRouterConfig) { const router = express.Router() router.use(cors()) @@ -40,11 +43,11 @@ export function createRouter({ feeManager }: IRouterConfig) { router.use((req: Request, res: Response, next: NextFunction) => { const traceId = req.headers[HEADER_TRACE_ID] - if (config.requireTraceId && traceId) { + if (config.RELAYER_REQUIRE_TRACE_ID && traceId) { logger.info('TraceId', { traceId, path: req.path }) } - if (config.requireLibJsVersion) { + if (config.RELAYER_REQUIRE_LIBJS_VERSION) { const libJsVersion = req.headers[HEADER_LIBJS] as string let isValidVersion = false try { @@ -63,18 +66,27 @@ export function createRouter({ feeManager }: IRouterConfig) { router.get('/', endpoints.root) router.get('/version', endpoints.relayerVersion) - router.post('/sendTransactions', wrapErr(endpoints.sendTransactions)) - router.get('/transactions/v2', wrapErr(endpoints.getTransactionsV2)) - router.get('/merkle/root/:index?', wrapErr(endpoints.merkleRoot)) - router.get('/job/:id', wrapErr(endpoints.getJob)) - router.get('/info', wrapErr(endpoints.relayerInfo)) - router.get('/fee', wrapErr(endpoints.getFeeBuilder(feeManager))) - router.get('/limits', wrapErr(endpoints.getLimits)) + router.post('/sendTransactions', wrapErr(inject({ pool }, endpoints.sendTransactions))) + router.get('/transactions/v2', wrapErr(inject({ pool }, endpoints.getTransactionsV2))) + router.get('/merkle/root/:index?', wrapErr(inject({ pool }, endpoints.merkleRoot))) + router.get('/job/:id', wrapErr(inject({ pool }, endpoints.getJob))) + router.get('/info', wrapErr(inject({ pool }, endpoints.relayerInfo))) + router.get('/fee', wrapErr(endpoints.inject({ pool, feeManager }, endpoints.getFee))) + router.get('/limits', wrapErr(inject({ pool }, endpoints.getLimits))) router.get('/maxNativeAmount', wrapErr(endpoints.getMaxNativeAmount)) - router.get('/siblings', wrapErr(endpoints.getSiblings)) - router.get('/params/hash/tree', wrapErr(endpoints.getParamsHashBuilder(config.treeUpdateParamsPath))) - router.get('/params/hash/tx', wrapErr(endpoints.getParamsHashBuilder(config.transferParamsPath))) - router.get('/params/hash/direct-deposit', wrapErr(endpoints.getParamsHashBuilder(config.directDepositParamsPath))) + router.get('/siblings', wrapErr(inject({ pool }, endpoints.getSiblings))) + router.get( + '/params/hash/tree', + wrapErr(inject({ hash: getFileHash(config.RELAYER_TREE_UPDATE_PARAMS_PATH) }, endpoints.getParamsHash)) + ) + router.get( + '/params/hash/tx', + wrapErr(inject({ hash: getFileHash(config.RELAYER_TRANSFER_PARAMS_PATH) }, endpoints.getParamsHash)) + ) + router.get( + '/params/hash/direct-deposit', + wrapErr(inject({ hash: getFileHash(config.RELAYER_DIRECT_DEPOSIT_PARAMS_PATH) }, endpoints.getParamsHash)) + ) // Error handler middleware router.use((error: any, req: Request, res: Response, next: NextFunction) => { diff --git a/zp-relayer/services/appLogger.ts b/zp-relayer/services/appLogger.ts index 1c8a629c..8def80bc 100644 --- a/zp-relayer/services/appLogger.ts +++ b/zp-relayer/services/appLogger.ts @@ -1,13 +1,13 @@ import { createLogger, format, transports } from 'winston' -import config from '@/configs/baseConfig' +import config from '@/configs/loggerConfig' let logFormat = format.combine(format.timestamp(), format.splat(), format.simple()) -if (config.colorizeLogs) { +if (config.COMMON_COLORIZE_LOGS) { logFormat = format.combine(format.colorize(), logFormat) } export const logger = createLogger({ - level: config.logLevel, + level: config.COMMON_LOG_LEVEL, format: logFormat, transports: [new transports.Console()], }) diff --git a/zp-relayer/services/fee/DynamicFeeManager.ts b/zp-relayer/services/fee/DynamicFeeManager.ts index 6bb1568f..b01863db 100644 --- a/zp-relayer/services/fee/DynamicFeeManager.ts +++ b/zp-relayer/services/fee/DynamicFeeManager.ts @@ -25,6 +25,6 @@ export class DynamicFeeManager extends FeeManager { async _fetchFeeOptions(): Promise { const gasPrice = await this.gasPrice.fetchOnce() const oneByteFee = FeeManager.executionFee(gasPrice, toBN(NZERO_BYTE_GAS)) - return DynamicFeeOptions.fromGasPice(gasPrice, oneByteFee, relayerConfig.minBaseFee) + return DynamicFeeOptions.fromGasPice(gasPrice, oneByteFee, relayerConfig.RELAYER_MIN_BASE_FEE) } } diff --git a/zp-relayer/services/fee/FeeManager.ts b/zp-relayer/services/fee/FeeManager.ts index 8200f937..ba315660 100644 --- a/zp-relayer/services/fee/FeeManager.ts +++ b/zp-relayer/services/fee/FeeManager.ts @@ -103,7 +103,7 @@ export class DynamicFeeOptions extends FeeOptions { [TxType.TRANSFER]: getFee(TxType.TRANSFER), [TxType.WITHDRAWAL]: getFee(TxType.WITHDRAWAL), oneByteFee, - nativeConvertFee: FeeManager.executionFee(gasPrice, config.baseTxGas.nativeConvertOverhead), + nativeConvertFee: FeeManager.executionFee(gasPrice, config.baseTxGas.RELAYER_BASE_TX_GAS_NATIVE_CONVERT), } const minFees: Fees = { [TxType.DEPOSIT]: minFee, diff --git a/zp-relayer/services/fee/OptimismFeeManager.ts b/zp-relayer/services/fee/OptimismFeeManager.ts index f1e8cf4e..d73f3c22 100644 --- a/zp-relayer/services/fee/OptimismFeeManager.ts +++ b/zp-relayer/services/fee/OptimismFeeManager.ts @@ -9,22 +9,26 @@ import { FeeManager, FeeEstimate, IFeeEstimateParams, IFeeManagerConfig, Dynamic import relayerConfig from '@/configs/relayerConfig' import { ZERO_BYTE_GAS, NZERO_BYTE_GAS } from '@/utils/constants' import type { EstimationType, GasPrice } from '../gas-price' +import { NetworkBackend } from '../network/NetworkBackend' +import { Network, NetworkContract } from '../network/types' export class OptimismFeeManager extends FeeManager { - private oracle: Contract + private oracle: NetworkContract private overhead!: BN private decimals!: BN private scalar!: BN + private gasPrice: GasPrice - constructor(config: IFeeManagerConfig, private gasPrice: GasPrice, web3: Web3) { + constructor(config: IFeeManagerConfig, network: NetworkBackend) { super(config) - this.oracle = new web3.eth.Contract(OracleAbi as AbiItem[], OP_GAS_ORACLE_ADDRESS) + this.gasPrice = network.gasPrice + this.oracle = network.contract(OracleAbi, OP_GAS_ORACLE_ADDRESS) } async init() { - this.overhead = await contractCallRetry(this.oracle, 'overhead').then(toBN) - this.decimals = await contractCallRetry(this.oracle, 'decimals').then(toBN) - this.scalar = await contractCallRetry(this.oracle, 'scalar').then(toBN) + this.overhead = await this.oracle.callRetry('overhead').then(toBN) + this.decimals = await this.oracle.callRetry('decimals').then(toBN) + this.scalar = await this.oracle.callRetry('scalar').then(toBN) } private getL1GasUsed(data: string): BN { @@ -63,10 +67,10 @@ export class OptimismFeeManager extends FeeManager { async _fetchFeeOptions(): Promise { const gasPrice = await this.gasPrice.fetchOnce() - const l1BaseFee = await contractCallRetry(this.oracle, 'l1BaseFee').then(toBN) + const l1BaseFee = await this.oracle.callRetry('l1BaseFee').then(toBN) const oneByteFee = l1BaseFee.muln(NZERO_BYTE_GAS) - return DynamicFeeOptions.fromGasPice(gasPrice, oneByteFee, relayerConfig.minBaseFee) + return DynamicFeeOptions.fromGasPice(gasPrice, oneByteFee, relayerConfig.RELAYER_MIN_BASE_FEE) } } diff --git a/zp-relayer/services/gas-price/GasPrice.ts b/zp-relayer/services/gas-price/GasPrice.ts index 39c033c3..3acb7e43 100644 --- a/zp-relayer/services/gas-price/GasPrice.ts +++ b/zp-relayer/services/gas-price/GasPrice.ts @@ -200,6 +200,7 @@ export class GasPrice { [EstimationType.Oracle]: this.fetchGasPriceOracle, [EstimationType.PolygonGSV2]: this.fetchPolygonGasStationV2, [EstimationType.OptimismOracle]: this.fetchOptimismOracle, + [EstimationType.Tron]: this.fetchTron, } return funcs[estimationType] } @@ -256,6 +257,10 @@ export class GasPrice { return { gasPrice } } + private fetchTron: FetchFunc = async () => { + return { gasPrice: '0' } + } + static normalizeGasPrice(rawGasPrice: number, factor = 1) { const gasPrice = rawGasPrice * factor return toWei(gasPrice.toFixed(2).toString(), 'gwei') diff --git a/zp-relayer/services/gas-price/types.ts b/zp-relayer/services/gas-price/types.ts index 09651012..3fe01155 100644 --- a/zp-relayer/services/gas-price/types.ts +++ b/zp-relayer/services/gas-price/types.ts @@ -38,6 +38,7 @@ export enum EstimationType { Web3 = 'web3', PolygonGSV2 = 'polygon-gasstation-v2', OptimismOracle = 'optimism-gas-price-oracle', + Tron = 'tron', } export type EstimationOracleOptions = { speedType: GasPriceKey; factor: number } diff --git a/zp-relayer/services/loggerMiddleware.ts b/zp-relayer/services/loggerMiddleware.ts index 213dd52f..f149e877 100644 --- a/zp-relayer/services/loggerMiddleware.ts +++ b/zp-relayer/services/loggerMiddleware.ts @@ -14,8 +14,8 @@ export function createConsoleLoggerMiddleware() { return expressWinston.logger({ winstonInstance: logger, level: 'debug', - ignoredRoutes: config.logIgnoreRoutes, - headerBlacklist: config.logHeaderBlacklist, + ignoredRoutes: config.RELAYER_LOG_IGNORE_ROUTES, + headerBlacklist: config.RELAYER_LOG_HEADER_BLACKLIST, requestWhitelist: ['headers', 'httpVersion'], }) } diff --git a/zp-relayer/services/network/NetworkBackend.ts b/zp-relayer/services/network/NetworkBackend.ts new file mode 100644 index 00000000..c671495d --- /dev/null +++ b/zp-relayer/services/network/NetworkBackend.ts @@ -0,0 +1,26 @@ +import type { EvmBackend } from './evm/EvmBackend' +import type { TronBackend } from './tron/TronBackend' +import { Network, NetworkContract, TransactionManager } from './types' + +export function isTron(n: NetworkBackend): n is NetworkBackend { + return n.type === Network.Tron +} + +export function isEthereum(n: NetworkBackend): n is NetworkBackend { + return n.type === Network.Ethereum +} + +export interface INetworkBackend { + type: N + pool: NetworkContract + token: NetworkContract + txManager: TransactionManager + + init(): Promise + contract(abi: any[], address: string): NetworkContract + recover(msg: string, sig: string): Promise + getBlockNumber(): Promise + getTxCalldata(hash: string): Promise +} + +export type NetworkBackend = N extends Network.Tron ? TronBackend : EvmBackend diff --git a/zp-relayer/services/network/evm/EvmBackend.ts b/zp-relayer/services/network/evm/EvmBackend.ts new file mode 100644 index 00000000..9d77fa84 --- /dev/null +++ b/zp-relayer/services/network/evm/EvmBackend.ts @@ -0,0 +1,89 @@ +import Web3 from 'web3' +import { AbiItem } from 'web3-utils' +import type { HttpProvider } from 'web3-core' +import { RETRY_CONFIG } from '@/utils/constants' +import { checkHTTPS } from '@/utils/helpers' +import HttpListProvider from '../../providers/HttpListProvider' +import { SafeEthLogsProvider } from '../../providers/SafeEthLogsProvider' +import type { INetworkBackend } from '../NetworkBackend' +import PoolAbi from '../../../abi/pool-abi.json' +import TokenAbi from '../../../abi/token-abi.json' +import { Network, NetworkBackendConfig, TransactionManager } from '../types' +import { EvmTxManager } from './EvmTxManager' +import { EstimationType, GasPrice } from '@/services/gas-price' +import RedundantHttpListProvider from '@/services/providers/RedundantHttpListProvider' +import { EthereumContract } from './EvmContract' + +export class EvmBackend implements INetworkBackend { + type: Network.Ethereum = Network.Ethereum + web3: Web3 + private web3Redundant: Web3 + pool: EthereumContract + token: EthereumContract + txManager: TransactionManager + public gasPrice: GasPrice + + constructor(config: NetworkBackendConfig) { + const providerOptions = { + requestTimeout: config.rpcRequestTimeout, + retry: RETRY_CONFIG, + } + config.rpcUrls.forEach(checkHTTPS(config.requireHTTPS)) + const provider = new HttpListProvider(config.rpcUrls, providerOptions, config.jsonRpcErrorCodes) + provider.startSyncStateChecker(config.rpcSyncCheckInterval) + + this.web3 = new Web3(SafeEthLogsProvider(provider as HttpProvider)) + this.web3Redundant = this.web3 + + if (config.relayerTxRedundancy && config.rpcUrls.length > 1) { + const redundantProvider = new RedundantHttpListProvider(config.rpcUrls, { + ...providerOptions, + name: 'redundant', + }) + this.web3Redundant = new Web3(redundantProvider) + } + + this.gasPrice = new GasPrice( + this.web3, + { gasPrice: config.gasPriceFallback }, + config.gasPriceUpdateInterval, + config.gasPriceEstimationType, + { + speedType: config.gasPriceSpeedType, + factor: config.gasPriceFactor, + maxFeeLimit: config.gasPriceMaxFeeLimit, + } + ) + + this.pool = this.contract(PoolAbi as AbiItem[], config.poolAddress) + this.token = this.contract(TokenAbi as AbiItem[], config.tokenAddress) + this.txManager = new EvmTxManager(this.web3Redundant, config.pk, this.gasPrice, { + gasPriceBumpFactor: config.gasPriceBumpFactor, + gasPriceMaxFeeLimit: config.gasPriceMaxFeeLimit, + gasPriceSurplus: config.gasPriceSurplus, + redis: config.redis, + }) + } + + async init() { + await this.gasPrice.start() + await this.txManager.init() + } + + recover(msg: string, sig: string): Promise { + return Promise.resolve(this.web3.eth.accounts.recover(msg, sig)) + } + + contract(abi: any[], address: string) { + return new EthereumContract(this.web3, abi, address) + } + + public getBlockNumber() { + return this.web3.eth.getBlockNumber() + } + + public async getTxCalldata(hash: string): Promise { + const tx = await this.web3.eth.getTransaction(hash) + return tx.input + } +} diff --git a/zp-relayer/services/network/evm/EvmContract.ts b/zp-relayer/services/network/evm/EvmContract.ts new file mode 100644 index 00000000..e6753e0e --- /dev/null +++ b/zp-relayer/services/network/evm/EvmContract.ts @@ -0,0 +1,30 @@ +import Web3 from 'web3' +import type { Contract } from 'web3-eth-contract' +import { INetworkContract } from '../types' + +export class EthereumContract implements INetworkContract { + instance: Contract + + constructor(web3: Web3, public abi: any[], address: string) { + this.instance = new web3.eth.Contract(abi, address) + } + + address(): string { + return this.instance.options.address + } + + call(method: string, args: any[] = []): Promise { + return this.instance.methods[method](...args).call() + } + + callRetry(method: string, args: any[] = []): Promise { + return this.instance.methods[method](...args).call() + } + + getEvents(event: string) { + return this.instance.getPastEvents(event, { + fromBlock: 0, + toBlock: 'latest', + }) + } +} diff --git a/zp-relayer/services/network/evm/EvmTxManager.ts b/zp-relayer/services/network/evm/EvmTxManager.ts new file mode 100644 index 00000000..f4d0d618 --- /dev/null +++ b/zp-relayer/services/network/evm/EvmTxManager.ts @@ -0,0 +1,205 @@ +import Web3 from 'web3' +import BN from 'bn.js' +import { isSameTransactionError } from '@/utils/web3Errors' +import { + addExtraGasPrice, + chooseGasPriceOptions, + EstimationType, + GasPrice, + GasPriceValue, + getGasPriceValue, +} from '@/services/gas-price' +import { getChainId, getNonce } from '@/utils/web3' +import { Mutex } from 'async-mutex' +import { logger } from '@/services/appLogger' +import { readNonce, updateNonce } from '@/utils/redisFields' +import type { Network, SendTx, TransactionManager, TxDesc } from '@/services/network/types' +import { Logger } from 'winston' +import { sleep } from '@/utils/helpers' +import type { TransactionReceipt, TransactionConfig } from 'web3-core' +import type { Redis } from 'ioredis' + +export interface EvmTxManagerConfig { + redis: Redis + gasPriceBumpFactor: number + gasPriceSurplus: number + gasPriceMaxFeeLimit: BN | null +} + +export class EvmTxManager implements TransactionManager { + txQueue: SendTx[] = [] + private isSending = false + nonce!: number + chainId!: number + mutex: Mutex + logger!: Logger + address: string + + constructor( + private web3: Web3, + private pk: string, + private gasPrice: GasPrice, + private config: EvmTxManagerConfig + ) { + this.mutex = new Mutex() + this.address = new Web3().eth.accounts.privateKeyToAccount(pk).address + } + + async init() { + this.nonce = await readNonce(this.config.redis, this.web3, this.address)(true) + await updateNonce(this.config.redis, this.nonce) + this.chainId = await getChainId(this.web3) + } + + async updateAndBumpGasPrice( + txConfig: TransactionConfig, + newGasPrice: GasPriceValue + ): Promise<[GasPriceValue | null, GasPriceValue]> { + const oldGasPrice = getGasPriceValue(txConfig) + if (oldGasPrice) { + const oldGasPriceWithExtra = addExtraGasPrice(oldGasPrice, this.config.gasPriceBumpFactor, null) + return [oldGasPrice, chooseGasPriceOptions(oldGasPriceWithExtra, newGasPrice)] + } else { + return [null, newGasPrice] + } + } + + async prepareTx( + txDesc: TxDesc, + { isResend = false, shouldUpdateGasPrice = true }: { isResend?: boolean; shouldUpdateGasPrice?: boolean } + ) { + const release = await this.mutex.acquire() + try { + const gasPriceValue = shouldUpdateGasPrice ? await this.gasPrice.fetchOnce() : this.gasPrice.getPrice() + const newGasPriceWithExtra = addExtraGasPrice( + gasPriceValue, + this.config.gasPriceSurplus, + this.config.gasPriceMaxFeeLimit + ) + + let updatedTxConfig: TransactionConfig = {} + let newGasPrice: GasPriceValue + + if (isResend) { + if (typeof txDesc.nonce === 'undefined') { + throw new Error('Nonce should be set for re-send') + } + const [oldGasPrice, updatedGasPrice] = await this.updateAndBumpGasPrice(txDesc, newGasPriceWithExtra) + newGasPrice = updatedGasPrice + logger.info('Updating tx gasPrice: %o -> %o', oldGasPrice, newGasPrice) + } else { + logger.info('Nonce', { nonce: this.nonce }) + newGasPrice = newGasPriceWithExtra + updatedTxConfig.nonce = this.nonce++ + updatedTxConfig.chainId = this.chainId + await updateNonce(this.config.redis, this.nonce) + } + + updatedTxConfig = { + ...updatedTxConfig, + ...txDesc, + ...newGasPrice, + } + const { transactionHash, rawTransaction } = await this.web3.eth.accounts.signTransaction(updatedTxConfig, this.pk) + + return { + txHash: transactionHash as string, + rawTransaction: rawTransaction as string, + gasPrice: newGasPrice, + txConfig: updatedTxConfig, + } + } finally { + release() + } + } + + async confirmTx(txHashes: string[], txNonce: number): Promise { + // Transaction was not mined + const actualNonce = await getNonce(this.web3, this.address) + logger.info('Nonce value from RPC: %d; tx nonce: %d', actualNonce, txNonce) + if (actualNonce <= txNonce) { + return null + } + + let tx = null + // Iterate in reverse order to check the latest hash first + for (let i = txHashes.length - 1; i >= 0; i--) { + const txHash = txHashes[i] + logger.info('Verifying tx', { txHash }) + try { + tx = await this.web3.eth.getTransactionReceipt(txHash) + } catch (e) { + logger.warn('Cannot get tx receipt; RPC response: %s', (e as Error).message, { txHash }) + // Exception should be caught by `withLoop` to re-run job + throw e + } + if (tx && tx.blockNumber) return tx + } + return null + } + + async _sendTx(rawTransaction: string): Promise { + return new Promise((res, rej) => + // prettier-ignore + this.web3.eth.sendSignedTransaction(rawTransaction) + .once('transactionHash', () => res()) + .once('error', e => { + // Consider 'already known' errors as a successful send + if (isSameTransactionError(e)){ + res() + } else { + rej(e) + } + }) + ) + } + + async consumer() { + this.isSending = true + while (this.txQueue.length !== 0) { + const a = this.txQueue.shift() + if (!a) { + return // TODO + } + const { txDesc, onSend, onIncluded, onRevert } = a + + let isResend = false + const sendAttempts: string[] = [] + while (1) { + const { txConfig } = await this.prepareTx(txDesc, { + isResend, + shouldUpdateGasPrice: false, + }) + const signedTx = await this.web3.eth.accounts.signTransaction(txConfig, this.pk) + const txHash = signedTx.transactionHash as string + + sendAttempts.push(txHash) + logger.info('Sending tx', { txHash }) + await this._sendTx(signedTx.rawTransaction as string) + await onSend(txHash) + + await sleep(1000) + + const receipt = await this.confirmTx(sendAttempts, txConfig.nonce as number) + if (receipt === null) { + continue + } + if (receipt.status) { + await onIncluded(txHash) + } else { + await onRevert(txHash) + } + break + } + } + this.isSending = false + } + + async sendTx(sendTx: SendTx) { + logger.info('Adding tx to queue', { txDesc: sendTx.txDesc }) + this.txQueue.push(sendTx) + if (!this.isSending) { + this.consumer() + } + } +} diff --git a/zp-relayer/services/network/index.ts b/zp-relayer/services/network/index.ts new file mode 100644 index 00000000..5ece0b3a --- /dev/null +++ b/zp-relayer/services/network/index.ts @@ -0,0 +1,4 @@ +export * from './types' +export * from './NetworkBackend' +export * from './evm/EvmBackend' +export * from './tron/TronBackend' diff --git a/zp-relayer/services/network/tron/TronBackend.ts b/zp-relayer/services/network/tron/TronBackend.ts new file mode 100644 index 00000000..a326e233 --- /dev/null +++ b/zp-relayer/services/network/tron/TronBackend.ts @@ -0,0 +1,52 @@ +// @ts-ignore +import TronWeb from 'tronweb' +import { hexToBytes } from 'web3-utils' +import type { INetworkBackend } from '../NetworkBackend' +import { Network, NetworkBackendConfig, TransactionManager } from '../types' +import { TronTxManager } from './TronTxManager' +import PoolAbi from '@/abi/pool-abi.json' +import TokenAbi from '@/abi/token-abi.json' +import { TronContract } from './TronContract' + +export class TronBackend implements INetworkBackend { + type: Network.Tron = Network.Tron + tronWeb: any + pool: TronContract + token: TronContract + txManager: TransactionManager + + constructor(config: NetworkBackendConfig) { + this.tronWeb = new TronWeb(config.rpcUrls[0], config.rpcUrls[0], config.rpcUrls[0]) + const pk = config.pk.slice(2) + const callerAddress = this.tronWeb.address.fromPrivateKey(pk) + // Workaround for https://github.com/tronprotocol/tronweb/issues/90 + this.tronWeb.setAddress(callerAddress) + this.txManager = new TronTxManager(this.tronWeb, pk) + + this.pool = new TronContract(this.tronWeb, PoolAbi, config.poolAddress) + this.token = new TronContract(this.tronWeb, TokenAbi, config.tokenAddress) + } + + async init() { + await this.txManager.init() + } + + async recover(msg: string, sig: string): Promise { + const bytes = hexToBytes(msg) + const address = await this.tronWeb.trx.verifyMessageV2(bytes, sig) + return address + } + + contract(abi: any[], address: string) { + return new TronContract(this.tronWeb, abi, address) + } + + getBlockNumber(): Promise { + throw new Error('Method not implemented.') + } + + public async getTxCalldata(hash: string): Promise { + const tx = await this.tronWeb.trx.getTransaction(hash) + return '0x' + tx.raw_data.contract[0].parameter.value.data + } +} diff --git a/zp-relayer/services/network/tron/TronContract.ts b/zp-relayer/services/network/tron/TronContract.ts new file mode 100644 index 00000000..38206444 --- /dev/null +++ b/zp-relayer/services/network/tron/TronContract.ts @@ -0,0 +1,35 @@ +// @ts-ignore +import TronWeb from 'tronweb' +import { INetworkContract } from '../types' + +export class TronContract implements INetworkContract { + instance: any + + constructor(tron: TronWeb, public abi: any[], address: string) { + this.instance = tron.contract(abi, address) + } + + address(): string { + return this.instance.address + } + + call(method: string, args: any[] = []): Promise { + return this.instance[method](...args).call() + } + + callRetry(method: string, args: any[] = []): Promise { + return this.instance[method](...args).call() + } + + async getEvents(eventName: string) { + const res = await this.instance._getEvents({ + eventName, + size: 0, + onlyConfirmed: true, + }) + return res.map((e: any) => ({ + returnValues: e.result, + transactionHash: e.transaction, + })) + } +} diff --git a/zp-relayer/services/network/tron/TronTxManager.ts b/zp-relayer/services/network/tron/TronTxManager.ts new file mode 100644 index 00000000..257b7c14 --- /dev/null +++ b/zp-relayer/services/network/tron/TronTxManager.ts @@ -0,0 +1,84 @@ +import { sleep } from '@/utils/helpers' +import { Network, SendTx, TransactionManager } from '../types' +import { logger } from '@/services/appLogger' + +export class TronTxManager implements TransactionManager { + constructor(private tronWeb: any, private pk: string) {} + + async init() {} + + async confirmTx(txHash: string) { + const info = await this.tronWeb.trx.getTransactionInfo(txHash) + if (typeof info.blockNumber !== 'number') { + return null + } + return info + } + + txQueue: SendTx[] = [] + private isSending = false + + // TODO: is a race condition possible here? + async runConsumer() { + this.isSending = true + while (this.txQueue.length !== 0) { + const tx = this.txQueue.shift() + if (!tx) { + return // TODO + } + const { txDesc, onSend, onIncluded, onRevert } = tx + const options = { + feeLimit: txDesc.feeLimit, + callValue: txDesc.value, + rawParameter: txDesc.data.slice(10), // TODO: cut off selector + } + const txObject = await this.tronWeb.transactionBuilder.triggerSmartContract(txDesc.to, txDesc.func, options, []) + const signedTx = await this.tronWeb.trx.sign(txObject.transaction, this.pk) + + let info = null + const res = await this.tronWeb.trx.sendRawTransaction(signedTx) + const txHash = res.transaction.txID + await onSend(txHash) + + while (1) { + await sleep(5000) + + try { + info = await this.confirmTx(txHash) + } catch (e) { + logger.error('Failed to fetch transaction info, waiting...:', e) + continue + } + + if (info === null) { + logger.info('Tx not included, waiting...') + continue + } else { + break + } + } + + if (info.receipt.result === 'SUCCESS') { + await onIncluded(info.id) + } else { + await onRevert(info.id) + } + } + this.isSending = false + } + + async safeRunConsumer() { + try { + await this.runConsumer() + } catch (e) { + logger.error(e) + } + } + + async sendTx(a: SendTx) { + this.txQueue.push(a) + if (!this.isSending) { + this.safeRunConsumer() + } + } +} diff --git a/zp-relayer/services/network/types.ts b/zp-relayer/services/network/types.ts new file mode 100644 index 00000000..9fc9aac4 --- /dev/null +++ b/zp-relayer/services/network/types.ts @@ -0,0 +1,87 @@ +import BN from 'bn.js' +import type { TransactionConfig } from 'web3-core' +import type { EthereumContract } from './evm/EvmContract' +import type { TronContract } from './tron/TronContract' +import { EstimationType } from '../gas-price' +import type { Redis } from 'ioredis' + +export enum Network { + Tron = 'tron', + Ethereum = 'ethereum', +} + +interface BaseBackendConfig { + poolAddress: string + tokenAddress: string + pk: string + rpcUrls: string[] + requireHTTPS: boolean +} + +interface EvmBackendConfig extends BaseBackendConfig { + rpcRequestTimeout: number + rpcSyncCheckInterval: number + jsonRpcErrorCodes: number[] + relayerTxRedundancy: boolean + + gasPriceFallback: string + gasPriceUpdateInterval: number + gasPriceEstimationType: EstimationType + gasPriceSpeedType: string + gasPriceFactor: number + gasPriceMaxFeeLimit: BN | null + gasPriceBumpFactor: number + gasPriceSurplus: number + + redis: Redis +} + +interface TronBackendConfig extends BaseBackendConfig {} + +export type NetworkBackendConfig = N extends Network.Tron ? TronBackendConfig : EvmBackendConfig + +export type NetworkContract = N extends Network.Tron ? TronContract : EthereumContract + +type BaseTxDesc = Required> +type EvmTxDesc = TransactionConfig +type TronTxDesc = BaseTxDesc + +export type TxDesc = N extends Network.Tron + ? TronTxDesc & { + func: string + feeLimit: number + } + : EvmTxDesc & { + isResend?: boolean + shouldUpdateGasPrice?: boolean + } + +export interface EvmTx extends TransactionConfig {} +export interface TronTx { + tx: {} +} +export type Tx = N extends Network.Tron ? TronTx : EvmTx + +export interface SendTx { + txDesc: TxDesc + onSend: (txHash: string) => Promise + onIncluded: (txHash: string) => Promise + onRevert: (txHash: string) => Promise + onResend?: (txHash: string) => Promise +} + +export interface TransactionManager { + txQueue: SendTx[] + init(): Promise + sendTx(sendTx: SendTx): Promise + // sendTx(tx: Tx): Promise +} + +export interface INetworkContract { + abi: any[] + instance: any + address(): string + call(method: string, args: any[]): Promise + callRetry(method: string, args: any[]): Promise + getEvents(event: string): Promise +} diff --git a/zp-relayer/services/price-feed/OneInchPriceFeed.ts b/zp-relayer/services/price-feed/OneInchPriceFeed.ts index a709c9b9..48c5c710 100644 --- a/zp-relayer/services/price-feed/OneInchPriceFeed.ts +++ b/zp-relayer/services/price-feed/OneInchPriceFeed.ts @@ -6,16 +6,18 @@ import { toBN, toWei, AbiItem } from 'web3-utils' import { ZERO_ADDRESS } from '@/utils/constants' import Erc20Abi from '@/abi/erc20.json' import OracleAbi from '@/abi/one-inch-oracle.json' +import { NetworkBackend } from '../network/NetworkBackend' +import { Network, NetworkContract } from '../network/types' // 1Inch price feed oracle: https://github.com/1inch/spot-price-aggregator export class OneInchPriceFeed implements IPriceFeed { - private contract: Contract + private contract: NetworkContract private baseTokenAddress: string private baseTokenDecimals!: BN private poolTokenAddress: string constructor( - private web3: Web3, + private network: NetworkBackend, contractAddress: string, config: { poolTokenAddress: string @@ -24,7 +26,7 @@ export class OneInchPriceFeed implements IPriceFeed { ) { this.poolTokenAddress = config.poolTokenAddress this.baseTokenAddress = config.customBaseTokenAddress || ZERO_ADDRESS - this.contract = new web3.eth.Contract(OracleAbi as AbiItem[], contractAddress) + this.contract = network.contract(OracleAbi, contractAddress) } async init() { @@ -36,13 +38,13 @@ export class OneInchPriceFeed implements IPriceFeed { } private async getContractDecimals(contractAddress: string): Promise { - const contract = new this.web3.eth.Contract(Erc20Abi as AbiItem[], contractAddress) - const decimals = await contract.methods.decimals().call() + const contract = this.network.contract(Erc20Abi, contractAddress) + const decimals = await contract.call('decimals') return toBN(10).pow(toBN(decimals)) } getRate(): Promise { - return this.contract.methods.getRate(this.baseTokenAddress, this.poolTokenAddress, true).call().then(toBN) + return this.contract.call('getRate', [this.baseTokenAddress, this.poolTokenAddress, true]).then(toBN) } convert(rate: BN, baseTokenAmount: BN): BN { diff --git a/zp-relayer/services/redisClient.ts b/zp-relayer/services/redisClient.ts index 372ee7b1..e66281d4 100644 --- a/zp-relayer/services/redisClient.ts +++ b/zp-relayer/services/redisClient.ts @@ -1,6 +1,6 @@ import Redis from 'ioredis' import config from '@/configs/baseConfig' -export const redis = new Redis(config.redisUrl, { +export const redis = new Redis(config.COMMON_REDIS_URL, { maxRetriesPerRequest: null, }) diff --git a/zp-relayer/services/web3.ts b/zp-relayer/services/web3.ts deleted file mode 100644 index b5da1345..00000000 --- a/zp-relayer/services/web3.ts +++ /dev/null @@ -1,16 +0,0 @@ -import Web3 from 'web3' -import type { HttpProvider } from 'web3-core' -import { RETRY_CONFIG } from '@/utils/constants' -import HttpListProvider from './providers/HttpListProvider' -import { checkHTTPS } from '@/utils/helpers' -import { SafeEthLogsProvider } from './providers/SafeEthLogsProvider' -import config from '@/configs/baseConfig' - -const providerOptions = { - requestTimeout: config.rpcRequestTimeout, - retry: RETRY_CONFIG, -} -config.rpcUrls.forEach(checkHTTPS(config.requireHTTPS)) -const provider = new HttpListProvider(config.rpcUrls, providerOptions, config.jsonRpcErrorCodes) -provider.startSyncStateChecker(config.rpcSyncCheckInterval) -export const web3 = new Web3(SafeEthLogsProvider(provider as HttpProvider)) diff --git a/zp-relayer/services/web3Redundant.ts b/zp-relayer/services/web3Redundant.ts deleted file mode 100644 index e261dd97..00000000 --- a/zp-relayer/services/web3Redundant.ts +++ /dev/null @@ -1,19 +0,0 @@ -import Web3 from 'web3' -import RedundantHttpListProvider from './providers/RedundantHttpListProvider' -import config from '@/configs/relayerConfig' -import { web3 } from './web3' -import { RETRY_CONFIG } from '@/utils/constants' - -export let web3Redundant = web3 - -const providerOptions = { - requestTimeout: config.rpcRequestTimeout, - retry: RETRY_CONFIG, -} -if (config.relayerTxRedundancy && config.rpcUrls.length > 1) { - const redundantProvider = new RedundantHttpListProvider(config.rpcUrls, { - ...providerOptions, - name: 'redundant', - }) - web3Redundant = new Web3(redundantProvider) -} diff --git a/zp-relayer/state/PoolState.ts b/zp-relayer/state/PoolState.ts index 3f73505c..e7e19aa6 100644 --- a/zp-relayer/state/PoolState.ts +++ b/zp-relayer/state/PoolState.ts @@ -4,12 +4,14 @@ import { OUTPLUSONE } from '@/utils/constants' import { MerkleTree, TxStorage, MerkleProof, Constants, Helpers } from 'libzkbob-rs-node' import { NullifierSet } from './nullifierSet' import { JobIdsMapping } from './jobIdsMapping' +import { Mutex } from 'async-mutex' export class PoolState { private tree: MerkleTree private txs: TxStorage public nullifiers: NullifierSet public jobIdsMapping: JobIdsMapping + private mutex: Mutex = new Mutex() constructor(private name: string, redis: Redis, path: string) { this.tree = new MerkleTree(`${path}/${name}Tree.db`) @@ -20,6 +22,16 @@ export class PoolState { this.jobIdsMapping = new JobIdsMapping('job-id-mapping', redis) } + async withLock(f: () => Promise): Promise { + const release = await this.mutex.acquire() + try { + const res = await f() + return res + } finally { + release() + } + } + getVirtualTreeProofInputs(outCommit: string, transferNum?: number) { logger.debug(`Building virtual tree proof...`) diff --git a/zp-relayer/tsconfig.json b/zp-relayer/tsconfig.json index 11986365..81ab8a2b 100644 --- a/zp-relayer/tsconfig.json +++ b/zp-relayer/tsconfig.json @@ -8,5 +8,5 @@ "@/*": ["./*"] } }, - "exclude": ["./test", "./build"] + "exclude": ["./test", "./build", "./direct-deposit"] } diff --git a/zp-relayer/tx/TxManager.ts b/zp-relayer/tx/TxManager.ts deleted file mode 100644 index 31399e27..00000000 --- a/zp-relayer/tx/TxManager.ts +++ /dev/null @@ -1,115 +0,0 @@ -import Web3 from 'web3' -import type { TransactionConfig } from 'web3-core' -import { isSameTransactionError } from '@/utils/web3Errors' -import { - addExtraGasPrice, - chooseGasPriceOptions, - EstimationType, - GasPrice, - GasPriceValue, - getGasPriceValue, -} from '@/services/gas-price' -import { getChainId } from '@/utils/web3' -import config from '@/configs/relayerConfig' -import { Mutex } from 'async-mutex' -import { logger } from '@/services/appLogger' -import { readNonce, updateNonce } from '@/utils/redisFields' - -interface PrepareTxConfig { - isResend?: boolean - shouldUpdateGasPrice?: boolean -} - -export class TxManager { - nonce!: number - chainId!: number - mutex: Mutex - - constructor(private web3: Web3, private privateKey: string, private gasPrice: GasPrice) { - this.mutex = new Mutex() - } - - async init() { - this.nonce = await readNonce(true) - await updateNonce(this.nonce) - this.chainId = await getChainId(this.web3) - } - - async updateAndBumpGasPrice( - txConfig: TransactionConfig, - newGasPrice: GasPriceValue - ): Promise<[GasPriceValue | null, GasPriceValue]> { - const oldGasPrice = getGasPriceValue(txConfig) - if (oldGasPrice) { - const oldGasPriceWithExtra = addExtraGasPrice(oldGasPrice, config.minGasPriceBumpFactor, null) - return [oldGasPrice, chooseGasPriceOptions(oldGasPriceWithExtra, newGasPrice)] - } else { - return [null, newGasPrice] - } - } - - async prepareTx( - txConfig: TransactionConfig, - { isResend = false, shouldUpdateGasPrice = true }: PrepareTxConfig, - tLogger = logger - ) { - const release = await this.mutex.acquire() - try { - const gasPriceValue = shouldUpdateGasPrice ? await this.gasPrice.fetchOnce() : this.gasPrice.getPrice() - const newGasPriceWithExtra = addExtraGasPrice(gasPriceValue, config.gasPriceSurplus, config.maxFeeLimit) - - let updatedTxConfig: TransactionConfig = {} - let newGasPrice: GasPriceValue - - if (isResend) { - if (typeof txConfig.nonce === 'undefined') { - throw new Error('Nonce should be set for re-send') - } - const [oldGasPrice, updatedGasPrice] = await this.updateAndBumpGasPrice(txConfig, newGasPriceWithExtra) - newGasPrice = updatedGasPrice - tLogger.info('Updating tx gasPrice: %o -> %o', oldGasPrice, newGasPrice) - } else { - tLogger.info('Nonce', { nonce: this.nonce }) - newGasPrice = newGasPriceWithExtra - updatedTxConfig.nonce = this.nonce++ - updatedTxConfig.chainId = this.chainId - await updateNonce(this.nonce) - } - - updatedTxConfig = { - ...updatedTxConfig, - ...txConfig, - ...newGasPrice, - } - - const { transactionHash, rawTransaction } = await this.web3.eth.accounts.signTransaction( - updatedTxConfig, - this.privateKey - ) - return { - txHash: transactionHash as string, - rawTransaction: rawTransaction as string, - gasPrice: newGasPrice, - txConfig: updatedTxConfig, - } - } finally { - release() - } - } - - async sendTransaction(rawTransaction: string): Promise { - return new Promise((res, rej) => - // prettier-ignore - this.web3.eth.sendSignedTransaction(rawTransaction) - .once('transactionHash', () => res()) - .once('error', e => { - // Consider 'already known' errors as a successful send - if (isSameTransactionError(e)){ - res() - } else { - rej(e) - } - }) - ) - } -} diff --git a/zp-relayer/txProcessor.ts b/zp-relayer/txProcessor.ts index e6733136..05a2181f 100644 --- a/zp-relayer/txProcessor.ts +++ b/zp-relayer/txProcessor.ts @@ -10,29 +10,31 @@ import { numToHex, flattenProof, truncateHexPrefix, encodeProof, truncateMemoTxP import { Delta, getTxProofField, parseDelta } from './utils/proofInputs' import type { DirectDeposit, WorkerTx, WorkerTxType } from './queue/poolTxQueue' import type { Circuit, IProver } from './prover/IProver' +import { TxDataMPC } from './validation/tx/validateTx' // @ts-ignore // Used only to get `transact` method selector const PoolInstance = new Contract(PoolAbi as AbiItem[]) -interface TxData { +type Stringified = { + [P in keyof T]: string +} +export interface TxData { txProof: SnarkProof treeProof: SnarkProof nullifier: string outCommit: string rootAfter: string - delta: Delta + delta: Stringified> txType: TxType memo: string depositSignature: string | null } -function buildTxData(txData: TxData) { +export function buildTxData(txData: TxData, mpcSignatures: string[] = []) { const selector: string = PoolInstance.methods.transact().encodeABI() - const transferIndex = numToHex(txData.delta.transferIndex, TRANSFER_INDEX_SIZE) - const energyAmount = numToHex(txData.delta.energyAmount, ENERGY_SIZE) - const tokenAmount = numToHex(txData.delta.tokenAmount, TOKEN_SIZE) + const { transferIndex, energyAmount, tokenAmount } = txData.delta logger.debug(`DELTA ${transferIndex} ${energyAmount} ${tokenAmount}`) const txFlatProof = encodeProof(txData.txProof) @@ -61,6 +63,11 @@ function buildTxData(txData: TxData) { data.push(signature) } + if (mpcSignatures.length > 0) { + data.push(numToHex(toBN(mpcSignatures.length), 2)) + data.push(...mpcSignatures) + } + return data.join('') } @@ -95,18 +102,22 @@ export async function getDirectDepositProof(deposits: DirectDeposit[], prover: I export interface ProcessResult { data: string + func: string commitIndex: number outCommit: string rootAfter: string memo: string nullifier?: string + mpc: boolean } export async function buildTx( tx: WorkerTx, treeProver: IProver, - state: PoolState + state: PoolState, + mpcGuards: [string, string][] | null ): Promise { + const func = 'transact()' const { txType, txProof, rawMemo, depositSignature } = tx const nullifier = getTxProofField(txProof, 'nullifier') @@ -116,21 +127,56 @@ export async function buildTx( const { treeProof, commitIndex } = await getTreeProof(state, outCommit, treeProver) const rootAfter = treeProof.inputs[1] - const data = buildTxData({ + + const txData: TxData = { txProof: txProof.proof, treeProof: treeProof.proof, - nullifier: numToHex(toBN(nullifier)), - outCommit: numToHex(toBN(outCommit)), - rootAfter: numToHex(toBN(rootAfter)), - delta, + delta: { + transferIndex: numToHex(delta.transferIndex, TRANSFER_INDEX_SIZE), + energyAmount: numToHex(delta.energyAmount, ENERGY_SIZE), + tokenAmount: numToHex(delta.tokenAmount, TOKEN_SIZE), + }, txType, memo: rawMemo, depositSignature, - }) + nullifier: numToHex(toBN(nullifier)), + outCommit: numToHex(toBN(outCommit)), + rootAfter: numToHex(toBN(rootAfter)), + } + + let mpc = false + const mpcSignatures = [] + if (mpcGuards) { + const txDataMPC: TxDataMPC = { + txProof, + treeProof, + memo: rawMemo, + txType, + depositSignature, + } + logger.debug('Collecting signatures from MPC guards...') + for (const [, guardHttp] of mpcGuards) { + const rawRes = await fetch(guardHttp, { + headers: { + 'Content-type': 'application/json', + }, + method: 'POST', + body: JSON.stringify(txDataMPC), + }) + const res = await rawRes.json() + const signature = truncateHexPrefix(res.signature) + mpcSignatures.push(signature) + } + logger.debug('Collected %d signatures', mpcSignatures.length) + console.log(mpcSignatures) + mpc = true + } + + const data = buildTxData(txData, mpcSignatures) const memo = truncateMemoTxPrefix(rawMemo, txType) - return { data, commitIndex, outCommit, rootAfter, nullifier, memo } + return { data, func, commitIndex, outCommit, rootAfter, nullifier, memo, mpc } } export async function buildDirectDeposits( @@ -138,6 +184,7 @@ export async function buildDirectDeposits( treeProver: IProver, state: PoolState ): Promise { + const func = 'appendDirectDeposits(uint256,uint256[],uint256,uint256[8],uint256[8])' const outCommit = tx.outCommit const { treeProof, commitIndex } = await getTreeProof(state, outCommit, treeProver) @@ -149,5 +196,5 @@ export async function buildDirectDeposits( .appendDirectDeposits(rootAfter, indices, outCommit, flattenProof(tx.txProof.proof), flattenProof(treeProof.proof)) .encodeABI() - return { data, commitIndex, outCommit, rootAfter, memo: tx.memo } + return { data, func, commitIndex, outCommit, rootAfter, memo: tx.memo, mpc: false } } diff --git a/zp-relayer/utils/PoolCalldataParser.ts b/zp-relayer/utils/PoolCalldataParser.ts index 8bb94988..c9acb326 100644 --- a/zp-relayer/utils/PoolCalldataParser.ts +++ b/zp-relayer/utils/PoolCalldataParser.ts @@ -1,22 +1,19 @@ -type Field = 'selector' | 'nullifier' | 'outCommit' | 'txType' | 'memoSize' | 'memo' - -type FieldMapping = { - [key in Field]: { start: number; size: number } +const FIELDS = { + selector: { start: 0, size: 4 }, + nullifier: { start: 4, size: 32 }, + outCommit: { start: 36, size: 32 }, + txType: { start: 640, size: 2 }, + memoSize: { start: 642, size: 2 }, + memo: { start: 644, size: 0 }, } +type Field = keyof typeof FIELDS + export class PoolCalldataParser { - private fields: FieldMapping = { - selector: { start: 0, size: 4 }, - nullifier: { start: 4, size: 32 }, - outCommit: { start: 36, size: 32 }, - txType: { start: 640, size: 2 }, - memoSize: { start: 642, size: 2 }, - memo: { start: 644, size: 0 }, - } constructor(private calldata: Buffer) {} getField(f: Field, defaultSize?: number) { - let { start, size } = this.fields[f] + let { start, size } = FIELDS[f] size = defaultSize || size return '0x' + this.calldata.subarray(start, start + size).toString('hex') } diff --git a/zp-relayer/utils/helpers.ts b/zp-relayer/utils/helpers.ts index 4d2c52b3..cb811297 100644 --- a/zp-relayer/utils/helpers.ts +++ b/zp-relayer/utils/helpers.ts @@ -55,6 +55,25 @@ export function numToHex(num: BN, pad = 64) { return padLeft(hex, pad) } +export function packSignature(signature: string): string { + signature = truncateHexPrefix(signature) + + if (signature.length > 128) { + let v = signature.slice(128, 130) + if (v == '1c') { + return `${signature.slice(0, 64)}${(parseInt(signature[64], 16) | 8).toString(16)}${signature.slice(65, 128)}` + } else if (v != '1b') { + throw 'invalid signature: v should be 27 or 28' + } + + return signature.slice(0, 128) + } else if (signature.length < 128) { + throw 'invalid signature: it should consist at least 64 bytes (128 chars)' + } + + return signature +} + export function unpackSignature(packedSign: string) { if (packedSign.length === 130) { return '0x' + packedSign @@ -101,7 +120,7 @@ export function encodeProof(p: SnarkProof): string { } export function buildPrefixedMemo(outCommit: string, txHash: string, truncatedMemo: string) { - return numToHex(toBN(outCommit)).concat(txHash.slice(2)).concat(truncatedMemo) + return numToHex(toBN(outCommit)).concat(truncateHexPrefix(txHash)).concat(truncatedMemo) } export async function setIntervalAndRun(f: () => Promise | void, interval: number) { @@ -142,7 +161,7 @@ export async function withErrorLog( } } -function sleep(ms: number) { +export function sleep(ms: number) { return new Promise(resolve => setTimeout(resolve, ms)) } @@ -242,7 +261,11 @@ export function contractCallRetry(contract: Contract, method: string, args: any[ ) } -export function getFileHash(path: string) { +export function getFileHash(path: string | null) { + if (!path) { + return null + } + const buffer = fs.readFileSync(path) const hash = crypto.createHash('sha256') hash.update(buffer) @@ -250,7 +273,5 @@ export function getFileHash(path: string) { } export function applyDenominator(n: BN, d: BN) { - return d.testn(255) - ? n.div(d.maskn(255)) - : n.mul(d) + return d.testn(255) ? n.div(d.maskn(255)) : n.mul(d) } diff --git a/zp-relayer/utils/permit/IPermitRecover.ts b/zp-relayer/utils/permit/IPermitRecover.ts index 29d60c28..dc9c79df 100644 --- a/zp-relayer/utils/permit/IPermitRecover.ts +++ b/zp-relayer/utils/permit/IPermitRecover.ts @@ -1,7 +1,6 @@ -import type Web3 from 'web3' -import type { Contract } from 'web3-eth-contract' import { ethers } from 'ethers' -import { contractCallRetry } from '../helpers' +import { Network, NetworkContract } from '@/services/network/types' +import { NetworkBackend } from '@/services/network/NetworkBackend' export class PreconditionError extends Error { name = 'PreconditionError' @@ -14,7 +13,7 @@ export interface CommonMessageParams { owner: string deadline: string spender: string - tokenContract: Contract + tokenContract: NetworkContract amount: string nullifier: string } @@ -25,10 +24,10 @@ export abstract class IPermitRecover> { DOMAIN_SEPARATOR: string | null = null abstract TYPES: { [key: string]: TypedMessage> } - constructor(protected web3: Web3, protected verifyingContract: string) {} + constructor(protected network: NetworkBackend, protected verifyingContract: string) {} async initializeDomain() { - const contract = new this.web3.eth.Contract( + const contract = this.network.contract( [ { inputs: [], @@ -46,7 +45,7 @@ export abstract class IPermitRecover> { ], this.verifyingContract ) - this.DOMAIN_SEPARATOR = await contractCallRetry(contract, 'DOMAIN_SEPARATOR') + this.DOMAIN_SEPARATOR = await contract.callRetry('DOMAIN_SEPARATOR') } abstract precondition(params: CommonMessageParams): Promise diff --git a/zp-relayer/utils/permit/Permit2Recover.ts b/zp-relayer/utils/permit/Permit2Recover.ts index beb1dbda..346782e1 100644 --- a/zp-relayer/utils/permit/Permit2Recover.ts +++ b/zp-relayer/utils/permit/Permit2Recover.ts @@ -1,6 +1,5 @@ import { toBN, AbiItem } from 'web3-utils' import { CommonMessageParams, IPermitRecover, PreconditionError, TypedMessage } from './IPermitRecover' -import { contractCallRetry } from '../helpers' import Permit2Abi from '@/abi/permit2.json' export interface ITokenPermissions { @@ -35,16 +34,16 @@ export class Permit2Recover extends IPermitRecover { async precondition({ nullifier, amount, owner, tokenContract }: CommonMessageParams) { // Make sure user approved tokens for Permit2 contract - const approved = await contractCallRetry(tokenContract, 'allowance', [owner, this.verifyingContract]) + const approved = await tokenContract.callRetry('allowance', [owner, this.verifyingContract]) if (toBN(approved).lt(toBN(amount))) return new PreconditionError('Permit2: Allowance is too low') - const permit2 = new this.web3.eth.Contract(Permit2Abi as AbiItem[], this.verifyingContract) + const permit2 = this.network.contract(Permit2Abi as AbiItem[], this.verifyingContract) const nonce = toBN(nullifier) const wordPos = nonce.shrn(8) const bitPos = nonce.maskn(8) - const pointer = await contractCallRetry(permit2, 'nonceBitmap', [owner, wordPos]) + const pointer = await permit2.callRetry('nonceBitmap', [owner, wordPos]) const isSet = toBN(pointer).testn(bitPos.toNumber()) if (isSet) return new PreconditionError('Permit2: Nonce already used') @@ -58,7 +57,7 @@ export class Permit2Recover extends IPermitRecover { amount, nullifier, }: CommonMessageParams): Promise { - const token = tokenContract.options.address + const token = tokenContract.address() const message: IPermitTransferFrom = { permitted: { diff --git a/zp-relayer/utils/permit/SaltedPermitRecover.ts b/zp-relayer/utils/permit/SaltedPermitRecover.ts index 335c3076..515433ac 100644 --- a/zp-relayer/utils/permit/SaltedPermitRecover.ts +++ b/zp-relayer/utils/permit/SaltedPermitRecover.ts @@ -36,7 +36,7 @@ export class SaltedPermitRecover extends IPermitRecover { amount, nullifier, }: CommonMessageParams): Promise { - const nonce = await contractCallRetry(tokenContract, 'nonces', [owner]) + const nonce = await tokenContract.callRetry('nonces', [owner]) const message: SaltedPermitMessage = { owner, diff --git a/zp-relayer/utils/permit/TransferWithAuthorizationRecover.ts b/zp-relayer/utils/permit/TransferWithAuthorizationRecover.ts index 96730cc5..bafcfdc8 100644 --- a/zp-relayer/utils/permit/TransferWithAuthorizationRecover.ts +++ b/zp-relayer/utils/permit/TransferWithAuthorizationRecover.ts @@ -1,6 +1,5 @@ import { toBN, numberToHex, AbiItem } from 'web3-utils' import { CommonMessageParams, IPermitRecover, PreconditionError, TypedMessage } from './IPermitRecover' -import { contractCallRetry } from '../helpers' import Erc3009Abi from '@/abi/erc3009.json' export interface ITransferWithAuthorization { @@ -27,8 +26,8 @@ export class TransferWithAuthorizationRecover extends IPermitRecover getNonce(web3, config.relayerAddress)) +export const readNonce = (redis: Redis, web3: Web3, address: string) => + readFieldBuilder(redis, RelayerKeys.NONCE, () => getNonce(web3, address)) -function readFieldBuilder(key: RelayerKeys, forceUpdateFunc?: Function) { +function readFieldBuilder(redis: Redis, key: RelayerKeys, forceUpdateFunc?: Function) { return async (forceUpdate?: boolean) => { const update = () => { if (!forceUpdateFunc) throw new Error('Force update function not provided') @@ -34,10 +34,10 @@ function readFieldBuilder(key: RelayerKeys, forceUpdateFunc?: Function) { } } -export function updateField(key: RelayerKeys, val: any) { +export function updateField(redis: Redis, key: RelayerKeys, val: any) { return redis.set(key, val) } -export function updateNonce(nonce: number) { - return updateField(RelayerKeys.NONCE, nonce) +export function updateNonce(redis: Redis, nonce: number) { + return updateField(redis, RelayerKeys.NONCE, nonce) } diff --git a/zp-relayer/utils/web3.ts b/zp-relayer/utils/web3.ts index 6ac45b76..372bed20 100644 --- a/zp-relayer/utils/web3.ts +++ b/zp-relayer/utils/web3.ts @@ -1,6 +1,8 @@ import type Web3 from 'web3' import type { Contract, PastEventOptions } from 'web3-eth-contract' import { logger } from '@/services/appLogger' +import { NetworkBackend } from '@/services/network/NetworkBackend' +import { Network } from '@/services/network/types' export async function getNonce(web3: Web3, address: string) { try { @@ -60,10 +62,10 @@ export async function getChainId(web3: Web3) { } } -export async function getBlockNumber(web3: Web3) { +export async function getBlockNumber(network: NetworkBackend) { try { logger.debug('Getting block number') - const blockNumber = await web3.eth.getBlockNumber() + const blockNumber = await network.getBlockNumber() logger.debug('Block number obtained', { blockNumber }) return blockNumber } catch (e) { diff --git a/zp-relayer/validation/api/validation.ts b/zp-relayer/validation/api/validation.ts index c688a2a9..8899b5a0 100644 --- a/zp-relayer/validation/api/validation.ts +++ b/zp-relayer/validation/api/validation.ts @@ -1,11 +1,13 @@ +// @ts-ignore +import TronWeb from 'tronweb' import Ajv, { JSONSchemaType } from 'ajv' import { isAddress } from 'web3-utils' import { Proof, SnarkProof } from 'libzkbob-rs-node' import { TxType } from 'zp-memo-parser' import type { PoolTx } from '@/pool' import { HEADER_TRACE_ID, ZERO_ADDRESS } from '@/utils/constants' -import config from '@/configs/relayerConfig' import { logger } from '@/services/appLogger' +import { TxDataMPC } from '../tx/validateTx' const ajv = new Ajv({ allErrors: true, coerceTypes: true, useDefaults: true }) @@ -30,7 +32,6 @@ const AjvNullableString: JSONSchemaType = { type: 'string', nullable: tr const AjvNullableAddress: JSONSchemaType = { type: 'string', - pattern: '^0x[a-fA-F0-9]{40}$', default: ZERO_ADDRESS, isAddress: true, } @@ -85,6 +86,21 @@ const AjvSendTransactionSchema: JSONSchemaType = { required: ['proof', 'memo', 'txType'], } +const AjvSignMPCSchema: JSONSchemaType = { + type: 'object', + properties: { + txProof: AjvProofSchema, + treeProof: AjvProofSchema, + txType: { + type: 'string', + enum: [TxType.DEPOSIT, TxType.PERMITTABLE_DEPOSIT, TxType.TRANSFER, TxType.WITHDRAWAL], + }, + memo: AjvString, + depositSignature: AjvNullableString, + }, + required: ['txProof', 'treeProof', 'txType', 'memo'], +} + const AjvSendTransactionsSchema: JSONSchemaType = { type: 'array', items: AjvSendTransactionSchema, @@ -149,7 +165,7 @@ const AjvGetSiblingsSchema: JSONSchemaType<{ const AjvTraceIdSchema: JSONSchemaType<{ [HEADER_TRACE_ID]: string }> = { type: 'object', properties: { [HEADER_TRACE_ID]: AjvNullableString }, - required: config.requireTraceId ? [HEADER_TRACE_ID] : [], + required: [HEADER_TRACE_ID], } function checkErrors(schema: JSONSchemaType) { @@ -165,7 +181,7 @@ function checkErrors(schema: JSONSchemaType) { } } -type ValidationFunction = ReturnType +export type ValidationFunction = ReturnType export class ValidationError extends Error { constructor(public validationErrors: ReturnType) { @@ -173,6 +189,11 @@ export class ValidationError extends Error { } } +export function validateBatchWithTrace(req: any, validationSet: [ValidationFunction, any][]) { + validationSet.push([checkTraceId, req.headers]) + return validateBatch(validationSet) +} + export function validateBatch(validationSet: [ValidationFunction, any][]) { for (const [validate, data] of validationSet) { const errors = validate(data) @@ -183,6 +204,7 @@ export function validateBatch(validationSet: [ValidationFunction, any][]) { export const checkMerkleRootErrors = checkErrors(AjvMerkleRootSchema) export const checkSendTransactionsErrors = checkErrors(AjvSendTransactionsSchema) +export const checkSignMPCSchema = checkErrors(AjvSignMPCSchema) export const checkGetTransactionsV2 = checkErrors(AjvGetTransactionsV2Schema) export const checkGetLimits = checkErrors(AjvGetLimitsSchema) export const checkGetSiblings = checkErrors(AjvGetSiblingsSchema) @@ -196,8 +218,8 @@ async function fetchSafe(url: string) { return r } -export async function validateCountryIP(ip: string) { - if (config.blockedCountries.length === 0) return null +export async function validateCountryIP(ip: string, blockedCountries: string[]) { + if (blockedCountries.length === 0) return null const apis = [ fetchSafe(`https://ipapi.co/${ip}/country`).then(res => res.text()), @@ -216,7 +238,7 @@ export async function validateCountryIP(ip: string) { ]) }) - if (config.blockedCountries.includes(country)) { + if (blockedCountries.includes(country)) { logger.warn('Restricted country', { ip, country }) throw new ValidationError([ { diff --git a/zp-relayer/validation/tx/common.ts b/zp-relayer/validation/tx/common.ts index 956a9751..57f13dfe 100644 --- a/zp-relayer/validation/tx/common.ts +++ b/zp-relayer/validation/tx/common.ts @@ -1,4 +1,3 @@ -import config from '@/configs/baseConfig' import { logger } from '@/services/appLogger' import { HEADER_TRACE_ID } from '@/utils/constants' @@ -27,22 +26,18 @@ export function checkSize(data: string, size: number) { return data.length === size } -export async function checkScreener(address: string, traceId?: string) { - if (config.screenerUrl === null || config.screenerToken === null) { - return null - } - +export async function checkScreener(address: string, screenerUrl: string, screenerToken: string, traceId?: string) { const ACC_VALIDATION_FAILED = 'Internal account validation failed' const headers: Record = { 'Content-type': 'application/json', - 'Authorization': `Bearer ${config.screenerToken}`, + 'Authorization': `Bearer ${screenerToken}`, } if (traceId) headers[HEADER_TRACE_ID] = traceId try { - const rawResponse = await fetch(config.screenerUrl, { + const rawResponse = await fetch(screenerUrl, { method: 'POST', headers, body: JSON.stringify({ address }), diff --git a/zp-relayer/validation/tx/validateDirectDeposit.ts b/zp-relayer/validation/tx/validateDirectDeposit.ts index 399024fe..89c6708a 100644 --- a/zp-relayer/validation/tx/validateDirectDeposit.ts +++ b/zp-relayer/validation/tx/validateDirectDeposit.ts @@ -4,6 +4,7 @@ import type { DirectDeposit } from '@/queue/poolTxQueue' import { Helpers } from 'libzkbob-rs-node' import { contractCallRetry } from '@/utils/helpers' import { checkAssertion, checkScreener, TxValidationError } from './common' +import config from '@/configs/baseConfig' const SNARK_SCALAR_FIELD = toBN('21888242871839275222246405745257275088548364400416034343698204186575808495617') @@ -65,6 +66,11 @@ async function checkDirectDepositConsistency(dd: DirectDeposit, directDepositCon export async function validateDirectDeposit(dd: DirectDeposit, directDepositContract: Contract) { await checkAssertion(() => checkDirectDepositPK(dd.zkAddress.pk)) await checkAssertion(() => checkDirectDepositConsistency(dd, directDepositContract)) - await checkAssertion(() => checkScreener(dd.sender)) - await checkAssertion(() => checkScreener(dd.fallbackUser)) + + const screenerUrl = config.COMMON_SCREENER_URL + const screenerToken = config.COMMON_SCREENER_TOKEN + if (screenerUrl && screenerToken) { + await checkAssertion(() => checkScreener(dd.sender, screenerUrl, screenerToken)) + await checkAssertion(() => checkScreener(dd.fallbackUser, screenerUrl, screenerToken)) + } } diff --git a/zp-relayer/validation/tx/validateTx.ts b/zp-relayer/validation/tx/validateTx.ts index 6a0da120..93c1914d 100644 --- a/zp-relayer/validation/tx/validateTx.ts +++ b/zp-relayer/validation/tx/validateTx.ts @@ -1,14 +1,11 @@ import BN from 'bn.js' -import { toBN } from 'web3-utils' -import type { Contract } from 'web3-eth-contract' +import { toBN, toChecksumAddress, bytesToHex } from 'web3-utils' import { TxType, TxData, getTxData } from 'zp-memo-parser' -import { Proof, SnarkProof } from 'libzkbob-rs-node' +import { Proof, SnarkProof, VK } from 'libzkbob-rs-node' import { logger } from '@/services/appLogger' -import config from '@/configs/relayerConfig' import type { Limits, Pool } from '@/pool' import type { NullifierSet } from '@/state/nullifierSet' -import { web3 } from '@/services/web3' -import { applyDenominator, contractCallRetry, numToHex, truncateMemoTxPrefix, unpackSignature } from '@/utils/helpers' +import { applyDenominator, numToHex, truncateMemoTxPrefix, unpackSignature } from '@/utils/helpers' import { ZERO_ADDRESS, MESSAGE_PREFIX_COMMON_V1, MOCK_CALLDATA } from '@/utils/constants' import { getTxProofField, parseDelta } from '@/utils/proofInputs' import type { TxPayload } from '@/queue/poolTxQueue' @@ -16,18 +13,11 @@ import type { PoolState } from '@/state/PoolState' import { checkAssertion, TxValidationError, checkSize, checkScreener, checkCondition } from './common' import type { PermitRecover } from '@/utils/permit/types' import type { FeeManager } from '@/services/fee' +import type { NetworkBackend } from '@/services/network/NetworkBackend' +import type { Network } from '@/services/network/types' const ZERO = toBN(0) -export async function checkBalance(token: Contract, address: string, minBalance: string) { - const balance = await contractCallRetry(token, 'balanceOf', [address]) - const res = toBN(balance).gte(toBN(minBalance)) - if (!res) { - return new TxValidationError('Not enough balance for deposit') - } - return null -} - export function checkCommitment(treeProof: Proof, txProof: Proof) { return treeProof.inputs[2] === txProof.inputs[2] } @@ -51,12 +41,12 @@ export function checkTransferIndex(contractPoolIndex: BN, transferIndex: BN) { return new TxValidationError(`Incorrect transfer index`) } -export function checkNativeAmount(nativeAmount: BN | null, withdrawalAmount: BN) { +export function checkNativeAmount(nativeAmount: BN | null, withdrawalAmount: BN, maxNativeAmount: BN) { logger.debug(`Native amount: ${nativeAmount}`) if (nativeAmount === null) { return null } - if (nativeAmount.gt(config.maxNativeAmount) || nativeAmount.gt(withdrawalAmount)) { + if (nativeAmount.gt(maxNativeAmount) || nativeAmount.gt(withdrawalAmount)) { return new TxValidationError('Native amount too high') } return null @@ -115,19 +105,23 @@ export function checkLimits(limits: Limits, amount: BN) { return null } -async function checkDepositEnoughBalance(token: Contract, address: string, requiredTokenAmount: BN) { +async function checkDepositEnoughBalance(network: NetworkBackend, address: string, requiredTokenAmount: BN) { if (requiredTokenAmount.lte(toBN(0))) { throw new TxValidationError('Requested balance check for token amount <= 0') } - - return checkBalance(token, address, requiredTokenAmount.toString(10)) + const balance = await network.token.callRetry('balanceOf', [address]) + const res = toBN(balance).gte(requiredTokenAmount) + if (!res) { + return new TxValidationError('Not enough balance for deposit') + } + return null } -async function getRecoveredAddress( +async function getRecoveredAddress( txType: T, proofNullifier: string, txData: TxData, - tokenContract: Contract, + network: NetworkBackend, tokenAmount: BN, depositSignature: string, permitRecover: PermitRecover @@ -140,17 +134,21 @@ async function getRecoveredAddress( let recoveredAddress: string if (txType === TxType.DEPOSIT) { - recoveredAddress = web3.eth.accounts.recover(nullifier, sig) + recoveredAddress = await network.recover(nullifier, sig) } else if (txType === TxType.PERMITTABLE_DEPOSIT) { + if (permitRecover === null) { + throw new TxValidationError('Permittable deposits are not enabled') + } + const { holder, deadline } = txData as TxData - const spender = web3.utils.toChecksumAddress(config.poolAddress as string) - const owner = web3.utils.toChecksumAddress(web3.utils.bytesToHex(Array.from(holder))) + const spender = toChecksumAddress(network.pool.address()) + const owner = toChecksumAddress(bytesToHex(Array.from(holder))) const recoverParams = { owner, deadline, spender, - tokenContract, + tokenContract: network.token, amount: tokenAmount.toString(10), nullifier, } @@ -195,10 +193,27 @@ function checkMemoPrefix(memo: string, txType: TxType) { return new TxValidationError(`Memo prefix is incorrect: ${numItemsSuffix}`) } +export interface OptionalChecks { + treeProof?: { + proof: Proof + vk: VK + } + fee?: { + feeManager: FeeManager + } + screener?: { + screenerUrl: string + screenerToken: string + } +} + export async function validateTx( { txType, rawMemo, txProof, depositSignature }: TxPayload, pool: Pool, - feeManager: FeeManager, + relayerAddress: string, + permitDeadlineThreshold: number, + maxNativeAmount: BN, + optionalChecks: OptionalChecks = {}, traceId?: string ) { await checkAssertion(() => checkMemoPrefix(rawMemo, txType)) @@ -224,6 +239,10 @@ export async function validateTx( await checkAssertion(() => checkNullifier(nullifier, pool.optimisticState.nullifiers)) await checkAssertion(() => checkTransferIndex(toBN(pool.optimisticState.getNextIndex()), delta.transferIndex)) await checkAssertion(() => checkProof(txProof, (p, i) => pool.verifyProof(p, i))) + if (optionalChecks.treeProof) { + const { proof, vk } = optionalChecks.treeProof + await checkAssertion(() => checkProof(proof, (p, i) => Proof.verify(vk, p, i))) + } const tokenAmount = delta.tokenAmount const tokenAmountWithFee = tokenAmount.add(fee) @@ -237,10 +256,10 @@ export async function validateTx( const { nativeAmount, receiver } = txData as TxData const nativeAmountBN = toBN(nativeAmount) - userAddress = web3.utils.bytesToHex(Array.from(receiver)) + userAddress = bytesToHex(Array.from(receiver)) logger.info('Withdraw address: %s', userAddress) await checkAssertion(() => checkNonZeroWithdrawAddress(userAddress)) - await checkAssertion(() => checkNativeAmount(nativeAmountBN, tokenAmountWithFee.neg())) + await checkAssertion(() => checkNativeAmount(nativeAmountBN, tokenAmountWithFee.neg(), maxNativeAmount)) if (!nativeAmountBN.isZero()) { nativeConvert = true @@ -254,27 +273,31 @@ export async function validateTx( txType, nullifier, txData, - pool.TokenInstance, + pool.network, requiredTokenAmount, depositSignature as string, pool.permitRecover ) logger.info('Deposit address: %s', userAddress) - await checkAssertion(() => checkDepositEnoughBalance(pool.TokenInstance, userAddress, requiredTokenAmount)) + // TODO check for approve in case of deposit + await checkAssertion(() => checkDepositEnoughBalance(pool.network, userAddress, requiredTokenAmount)) } else if (txType === TxType.TRANSFER) { - userAddress = config.relayerAddress + userAddress = relayerAddress checkCondition(tokenAmountWithFee.eq(ZERO) && energyAmount.eq(ZERO), 'Incorrect transfer amounts') } else { throw new TxValidationError('Unsupported TxType') } - const requiredFee = await feeManager.estimateFee({ - txType, - nativeConvert, - txData: MOCK_CALLDATA + rawMemo + (depositSignature || ''), - }) - const denominatedFee = requiredFee.denominate(pool.denominator).getEstimate() - await checkAssertion(() => checkFee(fee, denominatedFee)) + if (optionalChecks.fee) { + const { feeManager } = optionalChecks.fee + const requiredFee = await feeManager.estimateFee({ + txType, + nativeConvert, + txData: MOCK_CALLDATA + rawMemo + (depositSignature || ''), + }) + const denominatedFee = requiredFee.denominate(pool.denominator).getEstimate() + await checkAssertion(() => checkFee(fee, denominatedFee)) + } const limits = await pool.getLimitsFor(userAddress) await checkAssertion(() => checkLimits(limits, delta.tokenAmount)) @@ -282,10 +305,62 @@ export async function validateTx( if (txType === TxType.PERMITTABLE_DEPOSIT) { const { deadline } = txData as TxData logger.info('Deadline: %s', deadline) - await checkAssertion(() => checkDeadline(toBN(deadline), config.permitDeadlineThresholdInitial)) + await checkAssertion(() => checkDeadline(toBN(deadline), permitDeadlineThreshold)) } if (txType === TxType.DEPOSIT || txType === TxType.PERMITTABLE_DEPOSIT || txType === TxType.WITHDRAWAL) { - await checkAssertion(() => checkScreener(userAddress, traceId)) + if (optionalChecks.screener) { + const { screenerUrl, screenerToken } = optionalChecks.screener + await checkAssertion(() => checkScreener(userAddress, screenerUrl, screenerToken, traceId)) + } + } +} + +export type TxDataMPC = { + txProof: Proof + treeProof: Proof + memo: string + depositSignature: string | null + txType: TxType +} + +export async function validateTxMPC( + { memo: rawMemo, txType, txProof, depositSignature, treeProof }: TxDataMPC, + poolId: BN, + treeVK: VK, + txVK: VK +) { + await checkAssertion(() => checkMemoPrefix(rawMemo, txType)) + + const buf = Buffer.from(rawMemo, 'hex') + const txData = getTxData(buf, txType) + + const delta = parseDelta(getTxProofField(txProof, 'delta')) + const fee = toBN(txData.fee) + + logger.info( + 'Delta tokens: %s, Energy tokens: %s, Fee: %s', + delta.tokenAmount.toString(10), + delta.energyAmount.toString(10), + fee.toString(10) + ) + + await checkAssertion(() => checkPoolId(delta.poolId, poolId)) + await checkAssertion(() => checkProof(txProof, (p, i) => Proof.verify(txVK, p, i))) + await checkAssertion(() => checkProof(treeProof, (p, i) => Proof.verify(treeVK, p, i))) + + const tokenAmount = delta.tokenAmount + const tokenAmountWithFee = tokenAmount.add(fee) + const energyAmount = delta.energyAmount + + if (txType === TxType.WITHDRAWAL) { + checkCondition(tokenAmountWithFee.lte(ZERO) && energyAmount.lte(ZERO), 'Incorrect withdraw amounts') + } else if (txType === TxType.DEPOSIT || txType === TxType.PERMITTABLE_DEPOSIT) { + checkCondition(tokenAmount.gt(ZERO) && energyAmount.eq(ZERO), 'Incorrect deposit amounts') + checkCondition(depositSignature !== null, 'Deposit signature is required') + } else if (txType === TxType.TRANSFER) { + checkCondition(tokenAmountWithFee.eq(ZERO) && energyAmount.eq(ZERO), 'Incorrect transfer amounts') + } else { + throw new TxValidationError('Unsupported TxType') } } diff --git a/zp-relayer/workers/directDepositWorker.ts b/zp-relayer/workers/directDepositWorker.ts index 942051ec..8ca43dbd 100644 --- a/zp-relayer/workers/directDepositWorker.ts +++ b/zp-relayer/workers/directDepositWorker.ts @@ -2,7 +2,7 @@ import { Job, Worker } from 'bullmq' import { logger } from '@/services/appLogger' import { withErrorLog } from '@/utils/helpers' import { DIRECT_DEPOSIT_QUEUE_NAME } from '@/utils/constants' -import { DirectDeposit, poolTxQueue, WorkerTxType, WorkerTxTypePriority } from '@/queue/poolTxQueue' +import { DirectDeposit, JobState, poolTxQueue, WorkerTxType, WorkerTxTypePriority } from '@/queue/poolTxQueue' import type { IDirectDepositWorkerConfig } from './workerTypes' import { getDirectDepositProof } from '@/txProcessor' @@ -29,14 +29,14 @@ export async function createDirectDepositWorker({ redis, directDepositProver }: '', { type: WorkerTxType.DirectDeposit, - transactions: [ - { - deposits: directDeposits, - txProof: proof, - outCommit, - memo, - }, - ], + transaction: { + deposits: directDeposits, + txProof: proof, + outCommit, + memo, + txHash: null, + state: JobState.WAITING, + }, }, { priority: WorkerTxTypePriority[WorkerTxType.DirectDeposit], diff --git a/zp-relayer/workers/poolTxWorker.ts b/zp-relayer/workers/poolTxWorker.ts index d6be1fb7..679a1871 100644 --- a/zp-relayer/workers/poolTxWorker.ts +++ b/zp-relayer/workers/poolTxWorker.ts @@ -1,20 +1,31 @@ import type { Logger } from 'winston' import { Job, Worker } from 'bullmq' -import { toBN } from 'web3-utils' -import { web3 } from '@/services/web3' import { logger } from '@/services/appLogger' -import { poolTxQueue, BatchTx, PoolTxResult, WorkerTx, WorkerTxType } from '@/queue/poolTxQueue' -import { TX_QUEUE_NAME } from '@/utils/constants' -import { buildPrefixedMemo, waitForFunds, withErrorLog, withMutex } from '@/utils/helpers' -import { pool } from '@/pool' -import { sentTxQueue } from '@/queue/sentTxQueue' +import { poolTxQueue, PoolTx, WorkerTx, WorkerTxType, JobState } from '@/queue/poolTxQueue' +import { OUTPLUSONE, TX_QUEUE_NAME } from '@/utils/constants' +import { buildPrefixedMemo, withErrorLog, withMutex } from '@/utils/helpers' import { buildDirectDeposits, ProcessResult, buildTx } from '@/txProcessor' import config from '@/configs/relayerConfig' -import { getMaxRequiredGasPrice } from '@/services/gas-price' -import { isInsufficientBalanceError } from '@/utils/web3Errors' import { TxValidationError } from '@/validation/tx/common' import type { IPoolWorkerConfig } from './workerTypes' +import { isEthereum, isTron } from '@/services/network' +import { TronTxManager } from '@/services/network/tron/TronTxManager' +import { EvmTxManager } from '@/services/network/evm/EvmTxManager' +import Redis from 'ioredis' +import { OptionalChecks } from '@/validation/tx/validateTx' +const REVERTED_SET = 'reverted' +const RECHECK_ERROR = 'Waiting for next check' + +async function markFailed(redis: Redis, ids: string[]) { + if (ids.length === 0) return + await redis.sadd(REVERTED_SET, ids) +} + +async function checkMarked(redis: Redis, id: string) { + const inSet = await redis.sismember(REVERTED_SET, id) + return Boolean(inSet) +} interface HandlerConfig { type: T tx: WorkerTx @@ -27,10 +38,10 @@ interface HandlerConfig { export async function createPoolTxWorker({ redis, mutex, - txManager, validateTx, treeProver, feeManager, + pool, }: IPoolWorkerConfig) { const workerLogger = logger.child({ worker: 'pool' }) const WORKER_OPTIONS = { @@ -39,46 +50,135 @@ export async function createPoolTxWorker({ concurrency: 1, } - async function handleTx({ - type, - tx, - processResult, - logger, - traceId, - jobId, - }: HandlerConfig): Promise<[string, string]> { - const { data, outCommit, commitIndex, memo, rootAfter, nullifier } = processResult - - const gas = config.relayerGasLimit - const { txHash, rawTransaction, gasPrice, txConfig } = await txManager.prepareTx( - { - data, - gas: gas.toString(), - to: config.poolAddress, - }, - // XXX: Assumed that gasPrice was updated during fee validation - { shouldUpdateGasPrice: false } - ) - logger.info('Sending tx', { txHash }) - try { - await txManager.sendTransaction(rawTransaction) - } catch (e) { - if (isInsufficientBalanceError(e as Error)) { - const minimumBalance = gas.mul(toBN(getMaxRequiredGasPrice(gasPrice))) - logger.error('Insufficient balance, waiting for funds', { minimumBalance: minimumBalance.toString(10) }) - await Promise.all([poolTxQueue.pause(), sentTxQueue.pause()]) - waitForFunds( - web3, - config.relayerAddress, - () => Promise.all([poolTxQueue.resume(), sentTxQueue.resume()]), - minimumBalance, - config.insufficientBalanceCheckTimeout - ) - } - logger.warn('Tx send failed; it will be re-sent later', { txHash, error: (e as Error).message }) + async function onSend(txHash: string, jobId: string, { outCommit, memo, commitIndex }: ProcessResult) { + const job = await poolTxQueue.getJob(jobId) + if (!job) return + + job.data.transaction.txHash = txHash + job.data.transaction.state = JobState.SENT + await job.update(job.data) + + // Overwrite old tx recorded in optimistic state db with new tx hash + const prefixedMemo = buildPrefixedMemo(outCommit, txHash, memo) + pool.optimisticState.addTx(commitIndex * OUTPLUSONE, Buffer.from(prefixedMemo, 'hex')) + } + + async function onRevert(txHash: string, jobId: string) { + // pass here already sent txs' job ids in case of eth + logger.error('Transaction reverted', { txHash }) + + // Means that rollback was done previously, no need to do it now + if (await checkMarked(redis, jobId)) { + logger.info('Job marked as failed, skipping') + // TODO: update job state + return + // return [SentTxState.REVERT, txHash, []] as SentTxResult } + await pool.clearOptimisticState() + + // TODO: also re-process not sent txs for tron + // Send all jobs to re-process + // Validation of these jobs will be done in `poolTxWorker` + const waitingJobIds = [] + const reschedulePromises = [] + const newPoolJobIdMapping: Record = {} + const waitingJobs = await poolTxQueue.getJobs(['delayed', 'waiting']) + for (let wj of waitingJobs) { + // One of the jobs can be undefined, we need to skip it + // https://github.com/taskforcesh/bullmq/blob/master/src/commands/addJob-8.lua#L142-L143 + if (!wj?.id) continue + waitingJobIds.push(wj.id) + + let reschedulePromise: Promise + + reschedulePromise = poolTxQueue.add(txHash, wj.data) + + // To not mess up traceId we add each transaction separately + reschedulePromises.push( + reschedulePromise.then(newJob => { + const newJobId = newJob.id as string + newPoolJobIdMapping[wj.id as string] = newJobId + return newJobId + }) + ) + } + + logger.info('Marking ids %j as failed', waitingJobIds) + await markFailed(redis, waitingJobIds) + logger.info('Rescheduling %d jobs to process...', waitingJobs.length) + // TODO: handle rescheduling + const rescheduledIds = await Promise.all(reschedulePromises) + logger.info('Update pool job id mapping %j ...', newPoolJobIdMapping) + await pool.state.jobIdsMapping.add(newPoolJobIdMapping) + } + + async function onIncluded(txHash: string, { outCommit, commitIndex, nullifier, memo, rootAfter }: ProcessResult) { + // Successful + logger.info('Transaction was successfully mined', { txHash }) + const prefixedMemo = buildPrefixedMemo(outCommit, txHash, memo) + pool.state.updateState(commitIndex, outCommit, prefixedMemo) + // Update tx hash in optimistic state tx db + pool.optimisticState.addTx(commitIndex * OUTPLUSONE, Buffer.from(prefixedMemo, 'hex')) + + // Add nullifier to confirmed state and remove from optimistic one + if (nullifier) { + logger.info('Adding nullifier %s to PS', nullifier) + await pool.state.nullifiers.add([nullifier]) + logger.info('Removing nullifier %s from OS', nullifier) + await pool.optimisticState.nullifiers.remove([nullifier]) + } + + const node1 = pool.state.getCommitment(commitIndex) + const node2 = pool.optimisticState.getCommitment(commitIndex) + logger.info('Assert commitments are equal: %s, %s', node1, node2) + if (node1 !== node2) { + logger.error('Commitments are not equal') + } + + const rootConfirmed = pool.state.getMerkleRoot() + logger.info('Assert roots are equal') + if (rootConfirmed !== rootAfter) { + // TODO: Should be impossible but in such case + // we should recover from some checkpoint + logger.error('Roots are not equal: %s should be %s', rootConfirmed, rootAfter) + } + } + + async function handleTx({ processResult, logger, jobId }: HandlerConfig) { + const { data, func, outCommit, commitIndex, memo, nullifier, mpc } = processResult + const to = mpc ? (config.RELAYER_MPC_GUARD_CONTRACT as string) : config.COMMON_POOL_ADDRESS + + const txManager = pool.network.txManager + if (isTron(pool.network)) { + await (txManager as TronTxManager).sendTx({ + txDesc: { + to, + value: 0, + data, + func, + feeLimit: config.RELAYER_GAS_LIMIT.toNumber(), + }, + onSend: txHash => onSend(txHash, jobId, processResult), + onIncluded: txHash => onIncluded(txHash, processResult), + onRevert: txHash => onRevert(txHash, jobId), + }) + } else if (isEthereum(pool.network)) { + await (txManager as EvmTxManager).sendTx({ + txDesc: { + data, + to, + gas: config.RELAYER_GAS_LIMIT.toString(), + }, + onSend: txHash => onSend(txHash, jobId, processResult), + onIncluded: txHash => onIncluded(txHash, processResult), + onRevert: txHash => onRevert(txHash, jobId), + }) + } + + const emptyTxHash = '0x' + '0'.repeat(64) + const prefixedMemo = buildPrefixedMemo(outCommit, emptyTxHash, memo) pool.optimisticState.updateState(commitIndex, outCommit, prefixedMemo) @@ -86,41 +186,14 @@ export async function createPoolTxWorker({ logger.debug('Adding nullifier %s to OS', nullifier) await pool.optimisticState.nullifiers.add([nullifier]) } - - const sentJob = await sentTxQueue.add( - txHash, - { - poolJobId: jobId, - root: rootAfter, - outCommit, - commitIndex, - truncatedMemo: memo, - nullifier, - txConfig, - txPayload: { transactions: tx, traceId, type }, - prevAttempts: [[txHash, gasPrice]], - }, - { - delay: config.sentTxDelay, - } - ) - logger.info(`Added sentTxWorker job: ${sentJob.id}`) - return [txHash, sentJob.id as string] } - const poolTxWorkerProcessor = async (job: Job, PoolTxResult[]>) => { - const sentTxNum = await sentTxQueue.count() - if (sentTxNum >= config.maxSentQueueSize) { - throw new Error('Optimistic state overflow') - } - - const { transactions: txs, traceId, type } = job.data + const poolTxWorkerProcessor = async (job: Job>) => { + // TODO: handle queue overflow + const { transaction, traceId, type } = job.data const jobLogger = workerLogger.child({ jobId: job.id, traceId }) jobLogger.info('Processing...') - jobLogger.info('Received %s txs', txs.length) - - const txHashes: [string, string][] = [] const baseConfig = { logger: jobLogger, @@ -130,42 +203,57 @@ export async function createPoolTxWorker({ } let handlerConfig: HandlerConfig - for (const payload of txs) { - let processResult: ProcessResult - if (type === WorkerTxType.DirectDeposit) { - const tx = payload as WorkerTx - jobLogger.info('Received direct deposit', { number: txs.length }) - - if (tx.deposits.length === 0) { - logger.warn('Empty direct deposit batch, skipping') - continue - } + let processResult: ProcessResult + if (type === WorkerTxType.DirectDeposit) { + const tx = transaction as WorkerTx + jobLogger.info('Received direct deposit', { number: tx.deposits.length }) - processResult = await buildDirectDeposits(tx, treeProver, pool.optimisticState) - } else if (type === WorkerTxType.Normal) { - const tx = payload as WorkerTx + if (tx.deposits.length === 0) { + logger.warn('Empty direct deposit batch, skipping') + return + } - await validateTx(tx, pool, feeManager, traceId) + processResult = await buildDirectDeposits(tx, treeProver, pool.optimisticState) + } else if (type === WorkerTxType.Normal) { + const tx = transaction as WorkerTx - processResult = await buildTx(tx, treeProver, pool.optimisticState) - } else { - throw new Error(`Unknown tx type: ${type}`) + const optionalChecks: OptionalChecks = { + fee: { + feeManager, + }, } - - handlerConfig = { - ...baseConfig, - tx: payload, - processResult, + if (config.COMMON_SCREENER_URL && config.COMMON_SCREENER_TOKEN) { + optionalChecks.screener = { + screenerUrl: config.COMMON_SCREENER_URL, + screenerToken: config.COMMON_SCREENER_TOKEN, + } } + await validateTx( + tx, + pool, + config.RELAYER_ADDRESS, + config.RELAYER_PERMIT_DEADLINE_THRESHOLD_INITIAL, + config.RELAYER_MAX_NATIVE_AMOUNT, + optionalChecks, + traceId + ) + + const guards = config.RELAYER_GUARDS_CONFIG_PATH ? require(config.RELAYER_GUARDS_CONFIG_PATH) : null + processResult = await buildTx(tx, treeProver, pool.optimisticState, guards) + } else { + throw new Error(`Unknown tx type: ${type}`) + } - const res = await handleTx(handlerConfig) - txHashes.push(res) + handlerConfig = { + ...baseConfig, + tx: transaction, + processResult, } - return txHashes + await handleTx(handlerConfig) } - const poolTxWorker = new Worker, PoolTxResult[]>( + const poolTxWorker = new Worker>( TX_QUEUE_NAME, job => withErrorLog( diff --git a/zp-relayer/workers/sentTxWorker.ts b/zp-relayer/workers/sentTxWorker.ts deleted file mode 100644 index 34ff8734..00000000 --- a/zp-relayer/workers/sentTxWorker.ts +++ /dev/null @@ -1,294 +0,0 @@ -import type Redis from 'ioredis' -import { toBN } from 'web3-utils' -import type { TransactionReceipt, TransactionConfig } from 'web3-core' -import { Job, Worker } from 'bullmq' -import { DIRECT_DEPOSIT_REPROCESS_NAME } from '@/utils/constants' -import config from '@/configs/relayerConfig' -import { pool } from '@/pool' -import { web3 } from '@/services/web3' -import { logger } from '@/services/appLogger' -import { getMaxRequiredGasPrice } from '@/services/gas-price' -import { buildPrefixedMemo, withErrorLog, withLoop, withMutex } from '@/utils/helpers' -import { OUTPLUSONE, SENT_TX_QUEUE_NAME } from '@/utils/constants' -import { isGasPriceError, isInsufficientBalanceError, isNonceError, isSameTransactionError } from '@/utils/web3Errors' -import { SendAttempt, SentTxPayload, sentTxQueue, SentTxResult, SentTxState } from '@/queue/sentTxQueue' -import { DirectDepositTxPayload, poolTxQueue, WorkerTxType } from '@/queue/poolTxQueue' -import { getNonce } from '@/utils/web3' -import type { ISentWorkerConfig } from './workerTypes' -import type { TxManager } from '@/tx/TxManager' - -const REVERTED_SET = 'reverted' -const RECHECK_ERROR = 'Waiting for next check' - -async function markFailed(redis: Redis, ids: string[]) { - if (ids.length === 0) return - await redis.sadd(REVERTED_SET, ids) -} - -async function checkMarked(redis: Redis, id: string) { - const inSet = await redis.sismember(REVERTED_SET, id) - return Boolean(inSet) -} - -async function clearOptimisticState() { - logger.info('Rollback optimistic state...') - pool.optimisticState.rollbackTo(pool.state) - logger.info('Clearing optimistic nullifiers...') - await pool.optimisticState.nullifiers.clear() - - const root1 = pool.state.getMerkleRoot() - const root2 = pool.optimisticState.getMerkleRoot() - logger.info(`Assert roots are equal: ${root1}, ${root2}, ${root1 === root2}`) -} - -async function handleMined( - { transactionHash, blockNumber }: TransactionReceipt, - { outCommit, commitIndex, nullifier, truncatedMemo, root }: SentTxPayload, - jobLogger = logger -): Promise { - // Successful - jobLogger.info('Transaction was successfully mined', { transactionHash, blockNumber }) - - const prefixedMemo = buildPrefixedMemo(outCommit, transactionHash, truncatedMemo) - pool.state.updateState(commitIndex, outCommit, prefixedMemo) - // Update tx hash in optimistic state tx db - pool.optimisticState.addTx(commitIndex * OUTPLUSONE, Buffer.from(prefixedMemo, 'hex')) - - // Add nullifier to confirmed state and remove from optimistic one - if (nullifier) { - jobLogger.info('Adding nullifier %s to PS', nullifier) - await pool.state.nullifiers.add([nullifier]) - jobLogger.info('Removing nullifier %s from OS', nullifier) - await pool.optimisticState.nullifiers.remove([nullifier]) - } - - const node1 = pool.state.getCommitment(commitIndex) - const node2 = pool.optimisticState.getCommitment(commitIndex) - jobLogger.info('Assert commitments are equal: %s, %s', node1, node2) - if (node1 !== node2) { - jobLogger.error('Commitments are not equal') - } - - const rootConfirmed = pool.state.getMerkleRoot() - jobLogger.info('Assert roots are equal') - if (rootConfirmed !== root) { - // TODO: Should be impossible but in such case - // we should recover from some checkpoint - jobLogger.error('Roots are not equal: %s should be %s', rootConfirmed, root) - } - - return [SentTxState.MINED, transactionHash, []] as SentTxResult -} - -async function handleReverted( - { transactionHash: txHash, blockNumber }: TransactionReceipt, - jobId: string, - redis: Redis, - jobLogger = logger -): Promise { - jobLogger.error('Transaction reverted', { txHash, blockNumber }) - - // Means that rollback was done previously, no need to do it now - if (await checkMarked(redis, jobId)) { - jobLogger.info('Job marked as failed, skipping') - return [SentTxState.REVERT, txHash, []] as SentTxResult - } - - await clearOptimisticState() - - // Send all jobs to re-process - // Validation of these jobs will be done in `poolTxWorker` - const waitingJobIds = [] - const reschedulePromises = [] - const newPoolJobIdMapping: Record = {} - const waitingJobs = await sentTxQueue.getJobs(['delayed', 'waiting']) - for (let wj of waitingJobs) { - // One of the jobs can be undefined, so we need to check it - // https://github.com/taskforcesh/bullmq/blob/master/src/commands/addJob-8.lua#L142-L143 - if (!wj?.id) continue - waitingJobIds.push(wj.id) - - const { txPayload } = wj.data - let reschedulePromise: Promise - - reschedulePromise = poolTxQueue.add(txHash, { - type: txPayload.type, - transactions: [txPayload.transactions], - traceId: txPayload.traceId, - }) - - // To not mess up traceId we add each transaction separately - reschedulePromises.push( - reschedulePromise.then(j => { - const newPoolJobId = j.id as string - newPoolJobIdMapping[wj.data.poolJobId] = newPoolJobId - return newPoolJobId - }) - ) - } - jobLogger.info('Marking ids %j as failed', waitingJobIds) - await markFailed(redis, waitingJobIds) - jobLogger.info('Rescheduling %d jobs to process...', waitingJobs.length) - const rescheduledIds = await Promise.all(reschedulePromises) - jobLogger.info('Update pool job id mapping %j ...', newPoolJobIdMapping) - await pool.state.jobIdsMapping.add(newPoolJobIdMapping) - - return [SentTxState.REVERT, txHash, rescheduledIds] as SentTxResult -} - -async function handleResend( - txConfig: TransactionConfig, - txManager: TxManager, - job: Job, - jobLogger = logger -) { - const [lastHash, lastGasPrice] = job.data.prevAttempts.at(-1) as SendAttempt - jobLogger.warn('Tx %s is not mined, resending', lastHash) - - const { - txConfig: newTxConfig, - gasPrice, - txHash, - rawTransaction, - } = await txManager.prepareTx(txConfig, { isResend: true }, jobLogger) - - job.data.prevAttempts.push([txHash, gasPrice]) - jobLogger.info('Re-send tx', { txHash }) - try { - await txManager.sendTransaction(rawTransaction) - } catch (e) { - const err = e as Error - jobLogger.warn('Tx resend failed', { error: err.message, txHash }) - if (isGasPriceError(err) || isSameTransactionError(err)) { - // Tx wasn't sent successfully, but still update last attempt's - // gasPrice to be accounted in the next iteration - await job.update({ - ...job.data, - }) - } else if (isInsufficientBalanceError(err)) { - // We don't want to take into account last gasPrice increase - job.data.prevAttempts.at(-1)![1] = lastGasPrice - - const minimumBalance = toBN(txConfig.gas!).mul(toBN(getMaxRequiredGasPrice(gasPrice))) - jobLogger.error('Insufficient balance, waiting for funds', { minimumBalance: minimumBalance.toString(10) }) - } else if (isNonceError(err)) { - jobLogger.warn('Nonce error', { error: err.message, txHash }) - // Throw suppressed error to be treated as a warning - throw new Error(RECHECK_ERROR) - } - // Error should be caught by `withLoop` to re-run job - throw e - } - - // Overwrite old tx recorded in optimistic state db with new tx hash - const { truncatedMemo, outCommit, commitIndex } = job.data - const prefixedMemo = buildPrefixedMemo(outCommit, txHash, truncatedMemo) - pool.optimisticState.addTx(commitIndex * OUTPLUSONE, Buffer.from(prefixedMemo, 'hex')) - - // Update job - await job.update({ - ...job.data, - txConfig: newTxConfig, - }) - await job.updateProgress({ txHash, gasPrice }) -} - -export async function createSentTxWorker({ redis, mutex, txManager }: ISentWorkerConfig) { - const workerLogger = logger.child({ worker: 'sent-tx' }) - const WORKER_OPTIONS = { - autorun: false, - connection: redis, - concurrency: 1, - } - - async function checkMined( - prevAttempts: SendAttempt[], - txNonce: number - ): Promise<[TransactionReceipt | null, boolean]> { - // Transaction was not mined - const actualNonce = await getNonce(web3, config.relayerAddress) - logger.info('Nonce value from RPC: %d; tx nonce: %d', actualNonce, txNonce) - if (actualNonce <= txNonce) { - return [null, false] - } - - let tx = null - // Iterate in reverse order to check the latest hash first - for (let i = prevAttempts.length - 1; i >= 0; i--) { - const txHash = prevAttempts[i][0] - logger.info('Verifying tx', { txHash }) - try { - tx = await web3.eth.getTransactionReceipt(txHash) - } catch (e) { - logger.warn('Cannot get tx receipt; RPC response: %s', (e as Error).message, { txHash }) - // Exception should be caught by `withLoop` to re-run job - throw e - } - if (tx && tx.blockNumber) return [tx, false] - } - - // Transaction was not mined, but nonce was increased - return [null, true] - } - - const sentTxWorkerProcessor = async (job: Job, resendNum: number = 1) => { - const jobLogger = workerLogger.child({ jobId: job.id, traceId: job.data.txPayload.traceId, resendNum }) - - jobLogger.info('Verifying job %s', job.data.poolJobId) - const { prevAttempts, txConfig, txPayload } = job.data - - // Any thrown web3 error will re-trigger re-send loop iteration - const [tx, shouldReprocess] = await checkMined(prevAttempts, txConfig.nonce as number) - - if (shouldReprocess) { - // TODO: handle this case later - jobLogger.warn('Ambiguity detected: nonce increased but no respond that transaction was mined') - // Error should be caught by `withLoop` to re-run job - throw new Error(RECHECK_ERROR) - } - - if (!tx) { - // Resend with updated gas price - if (resendNum > config.sentTxLogErrorThreshold) { - jobLogger.error('Too many unsuccessful re-sends') - } - - await handleResend(txConfig, txManager, job, jobLogger) - - // Tx re-send successful - // Throw error to re-run job after delay and - // check if tx was mined - throw new Error(RECHECK_ERROR) - } - - if (tx.status) { - return await handleMined(tx, job.data, jobLogger) - } else { - if (txPayload.type === WorkerTxType.DirectDeposit) { - const deposits = (txPayload.transactions as DirectDepositTxPayload).deposits - jobLogger.info('Adding reverted direct deposit to reprocess list', { count: deposits.length }) - await redis.lpush(DIRECT_DEPOSIT_REPROCESS_NAME, ...deposits.map(d => JSON.stringify(d))) - } - return await handleReverted(tx, job.id as string, redis, jobLogger) - } - } - - const sentTxWorker = new Worker( - SENT_TX_QUEUE_NAME, - job => - withErrorLog( - withLoop( - withMutex(mutex, (i: number) => sentTxWorkerProcessor(job, i)), - config.sentTxDelay, - [RECHECK_ERROR] - ) - ), - WORKER_OPTIONS - ) - - sentTxWorker.on('error', e => { - workerLogger.info('SENT_WORKER ERR: %o', e) - }) - - return sentTxWorker -} diff --git a/zp-relayer/workers/workerTypes.ts b/zp-relayer/workers/workerTypes.ts index 3bdb9b98..de1acf46 100644 --- a/zp-relayer/workers/workerTypes.ts +++ b/zp-relayer/workers/workerTypes.ts @@ -1,26 +1,24 @@ import type { Redis } from 'ioredis' import type { Mutex } from 'async-mutex' -import type { TxManager } from '@/tx/TxManager' import type { Pool } from '@/pool' -import type { TxPayload } from '@/queue/poolTxQueue' import type { Circuit, IProver } from '@/prover' import type { FeeManager } from '@/services/fee' +import type { validateTx } from '@/validation/tx/validateTx' export interface IWorkerBaseConfig { redis: Redis + pool: Pool } export interface IPoolWorkerConfig extends IWorkerBaseConfig { - validateTx: (tx: TxPayload, pool: Pool, feeManager: FeeManager, traceId?: string) => Promise + validateTx: typeof validateTx treeProver: IProver mutex: Mutex - txManager: TxManager feeManager: FeeManager } export interface ISentWorkerConfig extends IWorkerBaseConfig { mutex: Mutex - txManager: TxManager } export interface IDirectDepositWorkerConfig extends IWorkerBaseConfig {