From dd63047402bea42edae97ed02eaa16587a5a8051 Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Sat, 30 Jan 2021 06:31:33 +0200 Subject: [PATCH 01/90] ForcedExit contract --- contracts/contracts/ForcedExit.sol | 57 ++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 contracts/contracts/ForcedExit.sol diff --git a/contracts/contracts/ForcedExit.sol b/contracts/contracts/ForcedExit.sol new file mode 100644 index 0000000000..12c13089e3 --- /dev/null +++ b/contracts/contracts/ForcedExit.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 + +pragma solidity ^0.7.0; + +pragma experimental ABIEncoderV2; + +import "./Utils.sol"; +import "./Ownable.sol"; + +contract SyncForcedExit is Ownable { + // This is the role of the zkSync server + // that will be able to withdraw the funds + address public deputy; + + bool public enabled = true; + + constructor() Ownable(msg.sender) {} + + event FundsReceived( + uint256 _amount + ); + + function setDeputy(address _newDeputy) external { + requireMaster(msg.sender); + + deputy = _newDeputy; + } + + function requireMasterOrDeputy(address _address) internal view { + require(_address == deputy || _address == getMaster(), "only by deputy or master"); + } + + function withdrawFunds(address payable _to) external { + requireMasterOrDeputy(msg.sender); + + (bool success, ) = _to.call{value: address(this).balance}(""); + require(success, "d"); // ETH withdraw failed + } + + function disable() external { + requireMaster(msg.sender); + + enabled = false; + } + + function enable() external { + requireMaster(msg.sender); + + enabled = true; + } + + receive() external payable { + require(enabled, "Contract is disabled"); + + emit FundsReceived(msg.value); + } +} From 2cd71880eb3acec8924ba7c43052e755ec9d8676 Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Sat, 30 Jan 2021 07:59:59 +0200 Subject: [PATCH 02/90] More security and deployment script --- contracts/contracts/ForcedExit.sol | 43 ++++++++------- .../contracts/dev-contracts/SelfDesctruct.sol | 13 +++++ contracts/scripts/deploy-testkit.ts | 2 +- contracts/scripts/deploy.ts | 4 ++ contracts/src.ts/deploy.ts | 54 ++++++++++++++----- .../test/unit_tests/specific_tokens_test.ts | 2 +- contracts/test/unit_tests/zksync_test.ts | 4 +- 7 files changed, 87 insertions(+), 35 deletions(-) create mode 100644 contracts/contracts/dev-contracts/SelfDesctruct.sol diff --git a/contracts/contracts/ForcedExit.sol b/contracts/contracts/ForcedExit.sol index 12c13089e3..a51e475879 100644 --- a/contracts/contracts/ForcedExit.sol +++ b/contracts/contracts/ForcedExit.sol @@ -6,35 +6,27 @@ pragma experimental ABIEncoderV2; import "./Utils.sol"; import "./Ownable.sol"; +import "./ReentrancyGuard.sol"; -contract SyncForcedExit is Ownable { +contract ForcedExit is Ownable, ReentrancyGuard { // This is the role of the zkSync server // that will be able to withdraw the funds - address public deputy; + address payable public receiver; bool public enabled = true; - constructor() Ownable(msg.sender) {} + constructor() Ownable(msg.sender) { + initializeReentrancyGuard(); + } event FundsReceived( uint256 _amount ); - function setDeputy(address _newDeputy) external { + function setReceiver(address payable _newReceiver) external { requireMaster(msg.sender); - deputy = _newDeputy; - } - - function requireMasterOrDeputy(address _address) internal view { - require(_address == deputy || _address == getMaster(), "only by deputy or master"); - } - - function withdrawFunds(address payable _to) external { - requireMasterOrDeputy(msg.sender); - - (bool success, ) = _to.call{value: address(this).balance}(""); - require(success, "d"); // ETH withdraw failed + receiver = _newReceiver; } function disable() external { @@ -49,9 +41,24 @@ contract SyncForcedExit is Ownable { enabled = true; } - receive() external payable { - require(enabled, "Contract is disabled"); + // Withdraw funds that failed to reach zkSync due to out-of-gas + function withdrawPendingFunds(address payable _to, uint128 amount) external nonReentrant { + requireMaster(msg.sender); + + uint256 balance = address(this).balance; + + require(amount <= balance, "The balance is lower than the amount"); + (bool success, ) = _to.call{value: amount}(""); + require(success, "d"); // ETH withdraw failed + } + + receive() external payable nonReentrant { + require(enabled, "Contract is disabled"); + emit FundsReceived(msg.value); + + (bool success, ) = receiver.call{value: msg.value}(""); + require(success, "d"); // ETH withdraw failed } } diff --git a/contracts/contracts/dev-contracts/SelfDesctruct.sol b/contracts/contracts/dev-contracts/SelfDesctruct.sol new file mode 100644 index 0000000000..92618607ad --- /dev/null +++ b/contracts/contracts/dev-contracts/SelfDesctruct.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 + +pragma solidity ^0.7.0; + +pragma experimental ABIEncoderV2; + +contract SelfDesctruct { + + function kill(address payable to) external { + selfdestruct(to); + } + +} diff --git a/contracts/scripts/deploy-testkit.ts b/contracts/scripts/deploy-testkit.ts index d454f7f883..eebc724750 100644 --- a/contracts/scripts/deploy-testkit.ts +++ b/contracts/scripts/deploy-testkit.ts @@ -1,5 +1,5 @@ import { ethers, Wallet } from 'ethers'; -import { Deployer, readContractCode, readTestContracts, readProductionContracts } from '../src.ts/deploy'; +import { Deployer, readContractCode, readProductionContracts } from '../src.ts/deploy'; import { deployContract } from 'ethereum-waffle'; import { ArgumentParser } from 'argparse'; import * as fs from 'fs'; diff --git a/contracts/scripts/deploy.ts b/contracts/scripts/deploy.ts index 0996a0d4b6..4635bf6076 100644 --- a/contracts/scripts/deploy.ts +++ b/contracts/scripts/deploy.ts @@ -65,6 +65,10 @@ async function main() { if (args.contract === 'Proxies' || args.contract == null) { await deployer.deployProxiesAndGatekeeper({ gasPrice, nonce: args.nonce }); } + + if (args.contract === 'ForcedExit' || args.contract == null) { + await deployer.deployForcedExit({ gasPrice, nonce: args.nonce }); + } } main() diff --git a/contracts/src.ts/deploy.ts b/contracts/src.ts/deploy.ts index 14eb9f4c61..31f1bf4641 100644 --- a/contracts/src.ts/deploy.ts +++ b/contracts/src.ts/deploy.ts @@ -16,7 +16,9 @@ import { Verifier, VerifierFactory, ZkSync, - ZkSyncFactory + ZkSyncFactory, + ForcedExit, + ForcedExitFactory } from '../typechain'; export interface Contracts { @@ -25,6 +27,7 @@ export interface Contracts { verifier; proxy; upgradeGatekeeper; + forcedExit; } export interface DeployedAddresses { @@ -36,6 +39,7 @@ export interface DeployedAddresses { ZkSync: string; ZkSyncTarget: string; DeployFactory: string; + ForcedExit: string; } export interface DeployerConfig { @@ -58,17 +62,8 @@ export function readProductionContracts(): Contracts { zkSync: readContractCode('ZkSync'), verifier: readContractCode('Verifier'), proxy: readContractCode('Proxy'), - upgradeGatekeeper: readContractCode('UpgradeGatekeeper') - }; -} - -export function readTestContracts(): Contracts { - return { - governance: readContractCode('GovernanceTest'), - zkSync: readContractCode('ZkSyncTest'), - verifier: readContractCode('VerifierTest'), - proxy: readContractCode('Proxy'), - upgradeGatekeeper: readContractCode('UpgradeGatekeeperTest') + upgradeGatekeeper: readContractCode('UpgradeGatekeeper'), + forcedExit: readContractCode('ForcedExit') }; } @@ -81,7 +76,8 @@ export function deployedAddressesFromEnv(): DeployedAddresses { Verifier: process.env.CONTRACTS_VERIFIER_ADDR, VerifierTarget: process.env.CONTRACTS_VERIFIER_TARGET_ADDR, ZkSync: process.env.CONTRACTS_CONTRACT_ADDR, - ZkSyncTarget: process.env.CONTRACTS_CONTRACT_TARGET_ADDR + ZkSyncTarget: process.env.CONTRACTS_CONTRACT_TARGET_ADDR, + ForcedExit: process.env.CONTRACTS_FORCED_EXIT_ADDR }; } @@ -215,6 +211,28 @@ export class Deployer { } } + public async deployForcedExit(ethTxOptions?: ethers.providers.TransactionRequest) { + if (this.verbose) { + console.log('Deploying ForcedExit helper contract'); + } + const forcedExitContract = await deployContract(this.deployWallet, this.contracts.forcedExit, [], { + gasLimit: 6000000, + ...ethTxOptions + }); + const zksRec = await forcedExitContract.deployTransaction.wait(); + const zksGasUsed = zksRec.gasUsed; + const gasPrice = forcedExitContract.deployTransaction.gasPrice; + if (this.verbose) { + console.log(`CONTRACTS_CONTRACT_TARGET_ADDR=${forcedExitContract.address}`); + console.log( + `zkSync target deployed, gasUsed: ${zksGasUsed.toString()}, eth spent: ${formatEther( + zksGasUsed.mul(gasPrice) + )}` + ); + } + this.addresses.ForcedExit = forcedExitContract.address; + } + public async publishSourcesToTesseracts() { console.log('Publishing ABI for UpgradeGatekeeper'); await publishAbiToTesseracts(this.addresses.UpgradeGatekeeper, this.contracts.upgradeGatekeeper); @@ -224,6 +242,8 @@ export class Deployer { await publishAbiToTesseracts(this.addresses.Verifier, this.contracts.verifier); console.log('Publishing ABI for Governance (proxy)'); await publishAbiToTesseracts(this.addresses.Governance, this.contracts.governance); + console.log('Publishing ABI for ForcedExit'); + await publishAbiToTesseracts(this.addresses.ForcedExit, this.contracts.forcedExit); } public async publishSourcesToEtherscan() { @@ -271,6 +291,9 @@ export class Deployer { ['address'] ) ); + + console.log('Publishing sourcecode for ForcedExit', this.addresses.ForcedExit); + await publishSourceCodeToEtherscan(this.addresses.ForcedExit, 'ForcedExit', ''); } public async deployAll(ethTxOptions?: ethers.providers.TransactionRequest) { @@ -278,6 +301,7 @@ export class Deployer { await this.deployGovernanceTarget(ethTxOptions); await this.deployVerifierTarget(ethTxOptions); await this.deployProxiesAndGatekeeper(ethTxOptions); + await this.deployForcedExit(ethTxOptions); } public governanceContract(signerOrProvider: Signer | providers.Provider): Governance { @@ -295,4 +319,8 @@ export class Deployer { public upgradeGatekeeperContract(signerOrProvider: Signer | providers.Provider): UpgradeGatekeeper { return UpgradeGatekeeperFactory.connect(this.addresses.UpgradeGatekeeper, signerOrProvider); } + + public forcedExitContract(signerOrProvider: Signer | providers.Provider): ForcedExit { + return ForcedExitFactory.connect(this.addresses.ForcedExit, signerOrProvider); + } } diff --git a/contracts/test/unit_tests/specific_tokens_test.ts b/contracts/test/unit_tests/specific_tokens_test.ts index 91a63d7a57..60445b1041 100644 --- a/contracts/test/unit_tests/specific_tokens_test.ts +++ b/contracts/test/unit_tests/specific_tokens_test.ts @@ -2,7 +2,7 @@ import { Contract, ethers, constants, BigNumber } from 'ethers'; import { parseEther } from 'ethers/lib/utils'; import { ETHProxy } from 'zksync'; import { Address, TokenAddress } from 'zksync/build/types'; -import { Deployer, readContractCode, readProductionContracts, readTestContracts } from '../../src.ts/deploy'; +import { Deployer, readContractCode, readProductionContracts } from '../../src.ts/deploy'; import { ZkSyncWithdrawalUnitTestFactory } from '../../typechain'; const hardhat = require('hardhat'); diff --git a/contracts/test/unit_tests/zksync_test.ts b/contracts/test/unit_tests/zksync_test.ts index 1605f09f29..8b025df4a1 100644 --- a/contracts/test/unit_tests/zksync_test.ts +++ b/contracts/test/unit_tests/zksync_test.ts @@ -1,8 +1,8 @@ -import { Contract, ethers, constants, BigNumber, BigNumberish } from 'ethers'; +import { Contract, ethers, constants, BigNumber } from 'ethers'; import { keccak256, parseEther } from 'ethers/lib/utils'; import { ETHProxy } from 'zksync'; import { Address, TokenAddress } from 'zksync/build/types'; -import { Deployer, readContractCode, readProductionContracts, readTestContracts } from '../../src.ts/deploy'; +import { Deployer, readContractCode, readProductionContracts } from '../../src.ts/deploy'; const hardhat = require('hardhat'); const { simpleEncode } = require('ethereumjs-abi'); From da26f7ab8363f5dfa2136f659c49b73372e76a77 Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Sat, 30 Jan 2021 08:12:58 +0200 Subject: [PATCH 03/90] Saving env var with ForcedExit contract address --- contracts/src.ts/deploy.ts | 6 +++--- etc/env/base/contracts.toml | 1 + infrastructure/zk/src/contract.ts | 1 + 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/contracts/src.ts/deploy.ts b/contracts/src.ts/deploy.ts index 31f1bf4641..23bbac5bee 100644 --- a/contracts/src.ts/deploy.ts +++ b/contracts/src.ts/deploy.ts @@ -213,7 +213,7 @@ export class Deployer { public async deployForcedExit(ethTxOptions?: ethers.providers.TransactionRequest) { if (this.verbose) { - console.log('Deploying ForcedExit helper contract'); + console.log('Deploying ForcedExit contract'); } const forcedExitContract = await deployContract(this.deployWallet, this.contracts.forcedExit, [], { gasLimit: 6000000, @@ -223,9 +223,9 @@ export class Deployer { const zksGasUsed = zksRec.gasUsed; const gasPrice = forcedExitContract.deployTransaction.gasPrice; if (this.verbose) { - console.log(`CONTRACTS_CONTRACT_TARGET_ADDR=${forcedExitContract.address}`); + console.log(`CONTRACTS_FORCED_EXIT_ADDR=${forcedExitContract.address}`); console.log( - `zkSync target deployed, gasUsed: ${zksGasUsed.toString()}, eth spent: ${formatEther( + `ForcedExit contract deployed, gasUsed: ${zksGasUsed.toString()}, eth spent: ${formatEther( zksGasUsed.mul(gasPrice) )}` ); diff --git a/etc/env/base/contracts.toml b/etc/env/base/contracts.toml index c61ac18ef6..ea68bd5162 100644 --- a/etc/env/base/contracts.toml +++ b/etc/env/base/contracts.toml @@ -9,6 +9,7 @@ CONTRACT_TARGET_ADDR="0x5E6D086F5eC079ADFF4FB3774CDf3e8D6a34F7E9" CONTRACT_ADDR="0x70a0F165d6f8054d0d0CF8dFd4DD2005f0AF6B55" GOVERNANCE_ADDR="0x5E6D086F5eC079ADFF4FB3774CDf3e8D6a34F7E9" VERIFIER_ADDR="0xDAbb67b676F5b01FcC8997Cc8439846D0d8078ca" +FORCED_EXIT_ADDR="0x9c7AeE886D6FcFc14e37784f143a6dAccEf50Db7" DEPLOY_FACTORY_ADDR="0xFC073319977e314F251EAE6ae6bE76B0B3BAeeCF" GENESIS_TX_HASH="0xb99ebfea46cbe05a21cd80fe5597d97b204befc52a16303f579c607dc1ac2e2e" GENESIS_ROOT="0x2d5ab622df708ab44944bb02377be85b6f27812e9ae520734873b7a193898ba4" diff --git a/infrastructure/zk/src/contract.ts b/infrastructure/zk/src/contract.ts index b1f0a4f986..00204f46aa 100644 --- a/infrastructure/zk/src/contract.ts +++ b/infrastructure/zk/src/contract.ts @@ -44,6 +44,7 @@ export async function deploy() { 'CONTRACTS_VERIFIER_ADDR', 'CONTRACTS_UPGRADE_GATEKEEPER_ADDR', 'CONTRACTS_DEPLOY_FACTORY_ADDR', + 'CONTRACTS_FORCED_EXIT_ADDR', 'CONTRACTS_GENESIS_TX_HASH' ]; let updatedContracts = ''; From 03523681a174f714c700fea4a0022317940a3a48 Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Sat, 30 Jan 2021 10:39:43 +0200 Subject: [PATCH 04/90] ForcedExit contract tests --- contracts/contracts/ForcedExit.sol | 9 +- .../contracts/dev-contracts/SelfDesctruct.sol | 13 --- .../contracts/dev-contracts/SelfDestruct.sol | 15 +++ contracts/test/unit_tests/forced_exit_test.ts | 104 ++++++++++++++++++ 4 files changed, 124 insertions(+), 17 deletions(-) delete mode 100644 contracts/contracts/dev-contracts/SelfDesctruct.sol create mode 100644 contracts/contracts/dev-contracts/SelfDestruct.sol create mode 100644 contracts/test/unit_tests/forced_exit_test.ts diff --git a/contracts/contracts/ForcedExit.sol b/contracts/contracts/ForcedExit.sol index a51e475879..f752773284 100644 --- a/contracts/contracts/ForcedExit.sol +++ b/contracts/contracts/ForcedExit.sol @@ -15,7 +15,7 @@ contract ForcedExit is Ownable, ReentrancyGuard { bool public enabled = true; - constructor() Ownable(msg.sender) { + constructor(address _master) Ownable(_master) { initializeReentrancyGuard(); } @@ -55,10 +55,11 @@ contract ForcedExit is Ownable, ReentrancyGuard { receive() external payable nonReentrant { require(enabled, "Contract is disabled"); - - emit FundsReceived(msg.value); - + require(receiver != address(0), "Receiver must be non-zero"); + (bool success, ) = receiver.call{value: msg.value}(""); require(success, "d"); // ETH withdraw failed + + emit FundsReceived(msg.value); } } diff --git a/contracts/contracts/dev-contracts/SelfDesctruct.sol b/contracts/contracts/dev-contracts/SelfDesctruct.sol deleted file mode 100644 index 92618607ad..0000000000 --- a/contracts/contracts/dev-contracts/SelfDesctruct.sol +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-License-Identifier: MIT OR Apache-2.0 - -pragma solidity ^0.7.0; - -pragma experimental ABIEncoderV2; - -contract SelfDesctruct { - - function kill(address payable to) external { - selfdestruct(to); - } - -} diff --git a/contracts/contracts/dev-contracts/SelfDestruct.sol b/contracts/contracts/dev-contracts/SelfDestruct.sol new file mode 100644 index 0000000000..198c6e0754 --- /dev/null +++ b/contracts/contracts/dev-contracts/SelfDestruct.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 + +pragma solidity ^0.7.0; + +pragma experimental ABIEncoderV2; + +contract SelfDestruct { + + function destroy(address payable to) external { + selfdestruct(to); + } + + // Need this to send some funds to the contract + receive() external payable {} +} diff --git a/contracts/test/unit_tests/forced_exit_test.ts b/contracts/test/unit_tests/forced_exit_test.ts new file mode 100644 index 0000000000..40b56e53e3 --- /dev/null +++ b/contracts/test/unit_tests/forced_exit_test.ts @@ -0,0 +1,104 @@ +const { expect } = require('chai'); +const { getCallRevertReason } = require('./common'); +const hardhat = require('hardhat'); + +import { Signer, Contract, ContractTransaction, utils, BigNumber } from 'ethers'; + +const TX_AMOUNT = utils.parseEther('1.0'); + +describe('ForcedExit unit tests', function () { + this.timeout(50000); + + let forcedExitContract: Contract; + let wallet1: Signer; + let wallet2: Signer; + let wallet3: Signer; + + before(async () => { + [wallet1, wallet2, wallet3] = await hardhat.ethers.getSigners(); + + const forcedExitContractFactory = await hardhat.ethers.getContractFactory('ForcedExit'); + forcedExitContract = await forcedExitContractFactory.deploy(wallet1.getAddress()); + forcedExitContract.connect(wallet1); + }); + + it('Check redirecting funds to receiver', async () => { + const setReceiverHandle = await forcedExitContract.setReceiver(wallet3.getAddress()); + await setReceiverHandle.wait(); + + const receiverBalanceBefore = await wallet3.getBalance(); + + const txHandle = await wallet2.sendTransaction({ + to: forcedExitContract.address, + value: TX_AMOUNT + }); + const txReceipt = await txHandle.wait(); + + expect(txReceipt.logs.length == 1, 'No events were emitted').to.be.true; + const receivedFundsAmount: BigNumber = forcedExitContract + .interface + .parseLog(txReceipt.logs[0]) + .args[0]; + + expect(receivedFundsAmount.eq(TX_AMOUNT), 'Didn\'t emit the amount of sent data').to.be.true; + + const receiverBalanceAfter = await wallet3.getBalance(); + const diff = receiverBalanceAfter.sub(receiverBalanceBefore); + + expect(diff.eq(TX_AMOUNT), 'Funds were not redirected to the receiver').to.be.true; + }); + + it('Check receiving pending funds', async () => { + const selfDestructContractFactory = await hardhat.ethers.getContractFactory('SelfDestruct'); + let selfDestructContract: Contract = await selfDestructContractFactory.deploy(); + + const txHandle = await wallet2.sendTransaction({ + to: selfDestructContract.address, + value: TX_AMOUNT + }); + await txHandle.wait(); + selfDestructContract.connect(wallet2); + + const destructHandle: ContractTransaction = await selfDestructContract.destroy(forcedExitContract.address); + await destructHandle.wait(); + const masterBalanceBefore = await wallet1.getBalance(); + + const withdrawHandle: ContractTransaction = await forcedExitContract.withdrawPendingFunds(wallet1.getAddress(), TX_AMOUNT); + const withdrawReceipt = await withdrawHandle.wait(); + const masterBalanceAfter = await wallet1.getBalance(); + + const diff = masterBalanceAfter.sub(masterBalanceBefore); + const expectedDiff = TX_AMOUNT.sub(withdrawReceipt.gasUsed.mul(withdrawHandle.gasPrice)); + expect(diff.eq(expectedDiff), 'Pending funds have not arrived to the account').to.be.true; + }); + + + it('Check redirection', async () => { + const disableHandle = await forcedExitContract.disable(); + await disableHandle.wait(); + + let failed1 = false; + try { + const txHandle = await wallet2.sendTransaction({ + to: forcedExitContract.address, + value: TX_AMOUNT + }); + await txHandle.wait(); + } catch { + failed1 = true; + } + + expect(failed1, "Transfer to the disabled contract does not fail").to.be.true; + + const enableHandle = await forcedExitContract.enable(); + await enableHandle.wait(); + + const txHandle = await wallet2.sendTransaction({ + to: forcedExitContract.address, + value: TX_AMOUNT + }); + const txReceipt = await txHandle.wait(); + + expect(txReceipt.blockNumber, 'A transfer to the enabled account have failed').to.exist; + }); +}); From 838bb1966c1935e2baadccd6cbd7ace6ac5e53ef Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Mon, 1 Feb 2021 17:08:05 +0200 Subject: [PATCH 05/90] Storage for ForcedExitRequests --- .../down.sql | 1 + .../up.sql | 7 ++ core/lib/storage/sqlx-data.json | 16 +++++ core/lib/storage/src/lib.rs | 1 + .../src/misc/forced_exit_requests_schema.rs | 71 +++++++++++++++++++ core/lib/storage/src/misc/mod.rs | 2 + core/lib/storage/src/misc/records.rs | 48 +++++++++++++ .../storage/src/tests/forced_exit_requests.rs | 31 ++++++++ core/lib/storage/src/tests/mod.rs | 1 + core/lib/types/src/lib.rs | 1 + core/lib/types/src/misc.rs | 19 +++++ 11 files changed, 198 insertions(+) create mode 100644 core/lib/storage/migrations/2021-01-31-160151_forced_exit_requests/down.sql create mode 100644 core/lib/storage/migrations/2021-01-31-160151_forced_exit_requests/up.sql create mode 100644 core/lib/storage/src/misc/forced_exit_requests_schema.rs create mode 100644 core/lib/storage/src/misc/mod.rs create mode 100644 core/lib/storage/src/misc/records.rs create mode 100644 core/lib/storage/src/tests/forced_exit_requests.rs create mode 100644 core/lib/types/src/misc.rs diff --git a/core/lib/storage/migrations/2021-01-31-160151_forced_exit_requests/down.sql b/core/lib/storage/migrations/2021-01-31-160151_forced_exit_requests/down.sql new file mode 100644 index 0000000000..d033a6f858 --- /dev/null +++ b/core/lib/storage/migrations/2021-01-31-160151_forced_exit_requests/down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS forced_exit_requests; diff --git a/core/lib/storage/migrations/2021-01-31-160151_forced_exit_requests/up.sql b/core/lib/storage/migrations/2021-01-31-160151_forced_exit_requests/up.sql new file mode 100644 index 0000000000..b95d200231 --- /dev/null +++ b/core/lib/storage/migrations/2021-01-31-160151_forced_exit_requests/up.sql @@ -0,0 +1,7 @@ +CREATE TABLE forced_exit_requests ( + id BIGINT NOT NULL PRIMARY KEY, + account_id BIGINT NOT NULL, + token_id INTEGER NOT NULL, + price_in_wei NUMERIC NOT NULL, + valid_until TIMESTAMP with time zone NOT NULL +); diff --git a/core/lib/storage/sqlx-data.json b/core/lib/storage/sqlx-data.json index d41aaa17de..859bd2a897 100644 --- a/core/lib/storage/sqlx-data.json +++ b/core/lib/storage/sqlx-data.json @@ -291,6 +291,22 @@ ] } }, + "10b5aa910eabf8c1537da0f0cecadbe33da406ad39703b741d0746add351b9d4": { + "query": "\n INSERT INTO forced_exit_requests ( id, token_id, account_id, price_in_wei, valid_until )\n VALUES ( $1, $2, $3, $4, $5 )\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Int4", + "Int8", + "Numeric", + "Timestamptz" + ] + }, + "nullable": [] + } + }, "15faacf14edd991dedc35011ef12eefc5a04771a6b3f24a4c655f9259c9ea572": { "query": "SELECT * FROM account_balance_updates WHERE block_number > $1 AND block_number <= $2 ", "describe": { diff --git a/core/lib/storage/src/lib.rs b/core/lib/storage/src/lib.rs index a6a644096b..171c156fd9 100644 --- a/core/lib/storage/src/lib.rs +++ b/core/lib/storage/src/lib.rs @@ -91,6 +91,7 @@ pub mod connection; pub mod data_restore; pub mod diff; pub mod ethereum; +pub mod misc; pub mod prover; pub mod test_data; pub mod tokens; diff --git a/core/lib/storage/src/misc/forced_exit_requests_schema.rs b/core/lib/storage/src/misc/forced_exit_requests_schema.rs new file mode 100644 index 0000000000..b73137d112 --- /dev/null +++ b/core/lib/storage/src/misc/forced_exit_requests_schema.rs @@ -0,0 +1,71 @@ +// Built-in deps +use num::BigInt; +use sqlx::types::BigDecimal; +use std::time::Instant; +// External imports +// Workspace imports +use zksync_types::{Token, TokenId, TokenLike, TokenPrice}; +use zksync_utils::ratio_to_big_decimal; +// Local imports +use crate::{QueryResult, StorageProcessor}; +use zksync_types::misc::{ForcedExitRequest, ForcedExitRequestId}; +use zksync_types::tokens::TokenMarketVolume; + +use super::records::DbForcedExitRequest; + +/// Precision of the USD price per token +pub(crate) const STORED_USD_PRICE_PRECISION: usize = 6; + +/// ForcedExitRequests schema handles the `forced_exit_requests` table, providing methods to +#[derive(Debug)] +pub struct ForcedExitRequestsSchema<'a, 'c>(pub &'a mut StorageProcessor<'c>); + +impl<'a, 'c> ForcedExitRequestsSchema<'a, 'c> { + pub async fn store_request(&mut self, request: &ForcedExitRequest) -> QueryResult<()> { + let start = Instant::now(); + let price_in_wei = BigDecimal::from(BigInt::from(request.price_in_wei.clone())); + sqlx::query!( + r#" + INSERT INTO forced_exit_requests ( id, token_id, account_id, price_in_wei, valid_until ) + VALUES ( $1, $2, $3, $4, $5 ) + "#, + request.id, + i32::from(request.token_id), + i64::from(request.account_id), + price_in_wei, + request.valid_until + ) + .execute(self.0.conn()) + .await?; + + metrics::histogram!("sql.forced_exit_requests.store_request", start.elapsed()); + Ok(()) + } + + pub async fn get_request_by_id( + &mut self, + id: ForcedExitRequestId, + ) -> QueryResult { + let start = Instant::now(); + // Unfortunately there were some bugs with + // sqlx macros, so just have to resort to the old way + let request: DbForcedExitRequest = sqlx::query_as( + r#" + SELECT * FROM forced_exit_requests + WHERE id = $1 + LIMIT 1 + "#, + ) + .bind(id) + .fetch_one(self.0.conn()) + .await?; + + let request: ForcedExitRequest = request.into(); + metrics::histogram!( + "sql.forced_exit_requests.get_request_by_id", + start.elapsed() + ); + + Ok(request) + } +} diff --git a/core/lib/storage/src/misc/mod.rs b/core/lib/storage/src/misc/mod.rs new file mode 100644 index 0000000000..5e30d8e9f1 --- /dev/null +++ b/core/lib/storage/src/misc/mod.rs @@ -0,0 +1,2 @@ +pub mod forced_exit_requests_schema; +pub mod records; diff --git a/core/lib/storage/src/misc/records.rs b/core/lib/storage/src/misc/records.rs new file mode 100644 index 0000000000..406816aa55 --- /dev/null +++ b/core/lib/storage/src/misc/records.rs @@ -0,0 +1,48 @@ +use chrono::{DateTime, Utc}; +use num::{bigint::ToBigInt, BigInt, BigUint}; +use sqlx::{types::BigDecimal, FromRow}; +use zksync_basic_types::{AccountId, TokenId}; +use zksync_types::misc::{ForcedExitRequest, ForcedExitRequestId}; + +#[derive(Debug, Clone, FromRow)] +pub struct DbForcedExitRequest { + pub id: i64, + pub account_id: u32, + pub token_id: i32, + pub price_in_wei: BigDecimal, + pub valid_until: DateTime, +} + +impl From for DbForcedExitRequest { + fn from(request: ForcedExitRequest) -> Self { + let price_in_wei = BigDecimal::from(BigInt::from(request.price_in_wei.clone())); + Self { + id: request.id, + account_id: u32::from(request.account_id), + token_id: request.token_id as i32, + price_in_wei, + valid_until: request.valid_until, + } + } +} + +impl Into for DbForcedExitRequest { + fn into(self) -> ForcedExitRequest { + let price_in_wei = self + .price_in_wei + .to_bigint() + .map(|int| int.to_biguint()) + .flatten() + // The fact that the request was found, but could not be convert into the ForcedExitRequest + // means that invalid data is stored in the DB + .expect("Invalid forced exit request has been stored"); + + ForcedExitRequest { + id: self.id, + account_id: self.account_id, + token_id: self.token_id as u16, + price_in_wei, + valid_until: self.valid_until, + } + } +} diff --git a/core/lib/storage/src/tests/forced_exit_requests.rs b/core/lib/storage/src/tests/forced_exit_requests.rs new file mode 100644 index 0000000000..09b4f922e3 --- /dev/null +++ b/core/lib/storage/src/tests/forced_exit_requests.rs @@ -0,0 +1,31 @@ +use crate::misc::forced_exit_requests_schema::ForcedExitRequestsSchema; +use crate::tests::db_test; +use crate::QueryResult; +use crate::{chain::operations::OperationsSchema, ethereum::EthereumSchema, StorageProcessor}; +use chrono::{DateTime, Utc}; +use num::{BigInt, BigUint, FromPrimitive}; +use zksync_types::misc::ForcedExitRequest; + +#[db_test] +async fn store_forced_exit_request(mut storage: StorageProcessor<'_>) -> QueryResult<()> { + let now = Utc::now(); + + let request = ForcedExitRequest { + id: 1, + account_id: 12, + token_id: 0, + price_in_wei: BigUint::from_i32(121212).unwrap(), + valid_until: DateTime::from(now), + }; + + ForcedExitRequestsSchema(&mut storage).store_request(&request); + + let fe = ForcedExitRequestsSchema(&mut storage) + .get_request_by_id(1) + .await + .expect("Failed to get forced exit by id"); + + assert_eq!(request, fe); + + Ok(()) +} diff --git a/core/lib/storage/src/tests/mod.rs b/core/lib/storage/src/tests/mod.rs index 7492f8c1cf..3fbb7bd25a 100644 --- a/core/lib/storage/src/tests/mod.rs +++ b/core/lib/storage/src/tests/mod.rs @@ -32,6 +32,7 @@ pub(crate) mod chain; mod config; mod data_restore; mod ethereum; +mod forced_exit_requests; mod prover; mod tokens; diff --git a/core/lib/types/src/lib.rs b/core/lib/types/src/lib.rs index 6833a3fc45..0a1365bb21 100644 --- a/core/lib/types/src/lib.rs +++ b/core/lib/types/src/lib.rs @@ -44,6 +44,7 @@ pub mod ethereum; pub mod gas_counter; pub mod helpers; pub mod mempool; +pub mod misc; pub mod network; pub mod operations; pub mod priority_ops; diff --git a/core/lib/types/src/misc.rs b/core/lib/types/src/misc.rs new file mode 100644 index 0000000000..cc0e5062ef --- /dev/null +++ b/core/lib/types/src/misc.rs @@ -0,0 +1,19 @@ +use chrono::{DateTime, Utc}; +use num::BigUint; +use zksync_basic_types::{AccountId, TokenId}; +use zksync_utils::BigUintSerdeAsRadix10Str; + +use serde::{Deserialize, Serialize}; + +pub type ForcedExitRequestId = i64; + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)] +#[serde(rename_all = "camelCase")] +pub struct ForcedExitRequest { + pub id: ForcedExitRequestId, + pub account_id: AccountId, + pub token_id: TokenId, + #[serde(with = "BigUintSerdeAsRadix10Str")] + pub price_in_wei: BigUint, + pub valid_until: DateTime, +} From ed87b6c9acad6a3a1736ff9b45aeb2eeb9b225c5 Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Tue, 2 Feb 2021 05:22:46 +0200 Subject: [PATCH 06/90] Some tests for ForcedExit requests. --- core/lib/storage/sqlx-data.json | 94 +++++++++++++++---- .../src/misc/forced_exit_requests_schema.rs | 29 ++++-- core/lib/storage/src/misc/records.rs | 11 +-- .../storage/src/tests/forced_exit_requests.rs | 58 +++++++++++- 4 files changed, 155 insertions(+), 37 deletions(-) diff --git a/core/lib/storage/sqlx-data.json b/core/lib/storage/sqlx-data.json index e8b7db9252..d2866fb8b8 100644 --- a/core/lib/storage/sqlx-data.json +++ b/core/lib/storage/sqlx-data.json @@ -303,22 +303,6 @@ ] } }, - "10b5aa910eabf8c1537da0f0cecadbe33da406ad39703b741d0746add351b9d4": { - "query": "\n INSERT INTO forced_exit_requests ( id, token_id, account_id, price_in_wei, valid_until )\n VALUES ( $1, $2, $3, $4, $5 )\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8", - "Int4", - "Int8", - "Numeric", - "Timestamptz" - ] - }, - "nullable": [] - } - }, "15faacf14edd991dedc35011ef12eefc5a04771a6b3f24a4c655f9259c9ea572": { "query": "SELECT * FROM account_balance_updates WHERE block_number > $1 AND block_number <= $2 ", "describe": { @@ -610,6 +594,24 @@ ] } }, + "1dfd4fafff703f719789690f88ba036e25bde60a69580e644ab24f2799249c99": { + "query": "SELECT MAX(id) FROM forced_exit_requests", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "max", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + null + ] + } + }, "222e3946401772e3f6e0d9ce9909e8e7ac2dc830c5ecfcd522f56b3bf70fd679": { "query": "INSERT INTO data_restore_storage_state_update (storage_state) VALUES ($1)", "describe": { @@ -1417,6 +1419,22 @@ ] } }, + "53ab59757e47450b31e9fb2ad006943246acdb14ac03d79518bcc5e19bb66cc8": { + "query": "\n INSERT INTO forced_exit_requests ( id, account_id, token_id, price_in_wei, valid_until )\n VALUES ( $1, $2, $3, $4, $5 )\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Int8", + "Int4", + "Numeric", + "Timestamptz" + ] + }, + "nullable": [] + } + }, "58b251c3fbdf9be9b62f669f8cdc2d98940026c831e02a53337474d36a5224f0": { "query": "UPDATE aggregate_operations\n SET confirmed = $1\n WHERE from_block >= $2 AND to_block <= $3 AND action_type = $4", "describe": { @@ -2028,6 +2046,50 @@ "nullable": [] } }, + "7dfa76c3e12c301dc3d7fbf820ecf0be45e0b1c5f01ce13f7cdc1a82880804c1": { + "query": "\n SELECT * FROM forced_exit_requests\n WHERE id = $1\n LIMIT 1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "account_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "token_id", + "type_info": "Int4" + }, + { + "ordinal": 3, + "name": "price_in_wei", + "type_info": "Numeric" + }, + { + "ordinal": 4, + "name": "valid_until", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + false + ] + } + }, "7ff98a4fddc441ea83f72a4a75a7caf53b9661c37f26a90984a349bfa5aeab70": { "query": "INSERT INTO eth_aggregated_ops_binding (op_id, eth_op_id) VALUES ($1, $2)", "describe": { diff --git a/core/lib/storage/src/misc/forced_exit_requests_schema.rs b/core/lib/storage/src/misc/forced_exit_requests_schema.rs index b73137d112..dfe92808c6 100644 --- a/core/lib/storage/src/misc/forced_exit_requests_schema.rs +++ b/core/lib/storage/src/misc/forced_exit_requests_schema.rs @@ -4,18 +4,12 @@ use sqlx::types::BigDecimal; use std::time::Instant; // External imports // Workspace imports -use zksync_types::{Token, TokenId, TokenLike, TokenPrice}; -use zksync_utils::ratio_to_big_decimal; // Local imports use crate::{QueryResult, StorageProcessor}; use zksync_types::misc::{ForcedExitRequest, ForcedExitRequestId}; -use zksync_types::tokens::TokenMarketVolume; use super::records::DbForcedExitRequest; -/// Precision of the USD price per token -pub(crate) const STORED_USD_PRICE_PRECISION: usize = 6; - /// ForcedExitRequests schema handles the `forced_exit_requests` table, providing methods to #[derive(Debug)] pub struct ForcedExitRequestsSchema<'a, 'c>(pub &'a mut StorageProcessor<'c>); @@ -26,12 +20,12 @@ impl<'a, 'c> ForcedExitRequestsSchema<'a, 'c> { let price_in_wei = BigDecimal::from(BigInt::from(request.price_in_wei.clone())); sqlx::query!( r#" - INSERT INTO forced_exit_requests ( id, token_id, account_id, price_in_wei, valid_until ) + INSERT INTO forced_exit_requests ( id, account_id, token_id, price_in_wei, valid_until ) VALUES ( $1, $2, $3, $4, $5 ) "#, request.id, - i32::from(request.token_id), i64::from(request.account_id), + i32::from(request.token_id), price_in_wei, request.valid_until ) @@ -49,14 +43,15 @@ impl<'a, 'c> ForcedExitRequestsSchema<'a, 'c> { let start = Instant::now(); // Unfortunately there were some bugs with // sqlx macros, so just have to resort to the old way - let request: DbForcedExitRequest = sqlx::query_as( + let request: DbForcedExitRequest = sqlx::query_as!( + DbForcedExitRequest, r#" SELECT * FROM forced_exit_requests WHERE id = $1 LIMIT 1 "#, + id ) - .bind(id) .fetch_one(self.0.conn()) .await?; @@ -68,4 +63,18 @@ impl<'a, 'c> ForcedExitRequestsSchema<'a, 'c> { Ok(request) } + + pub async fn get_max_used_id(&mut self) -> QueryResult { + let start = Instant::now(); + + let max_value: i64 = sqlx::query!(r#"SELECT MAX(id) FROM forced_exit_requests"#) + .fetch_one(self.0.conn()) + .await? + .max + .unwrap_or(0); + + metrics::histogram!("sql.forced_exit_requests.get_max_used_id", start.elapsed()); + + Ok(max_value) + } } diff --git a/core/lib/storage/src/misc/records.rs b/core/lib/storage/src/misc/records.rs index 406816aa55..8265abb047 100644 --- a/core/lib/storage/src/misc/records.rs +++ b/core/lib/storage/src/misc/records.rs @@ -1,13 +1,12 @@ use chrono::{DateTime, Utc}; -use num::{bigint::ToBigInt, BigInt, BigUint}; +use num::{bigint::ToBigInt, BigInt, ToPrimitive}; use sqlx::{types::BigDecimal, FromRow}; -use zksync_basic_types::{AccountId, TokenId}; -use zksync_types::misc::{ForcedExitRequest, ForcedExitRequestId}; +use zksync_types::misc::ForcedExitRequest; #[derive(Debug, Clone, FromRow)] pub struct DbForcedExitRequest { pub id: i64, - pub account_id: u32, + pub account_id: i64, pub token_id: i32, pub price_in_wei: BigDecimal, pub valid_until: DateTime, @@ -18,7 +17,7 @@ impl From for DbForcedExitRequest { let price_in_wei = BigDecimal::from(BigInt::from(request.price_in_wei.clone())); Self { id: request.id, - account_id: u32::from(request.account_id), + account_id: request.account_id as i64, token_id: request.token_id as i32, price_in_wei, valid_until: request.valid_until, @@ -39,7 +38,7 @@ impl Into for DbForcedExitRequest { ForcedExitRequest { id: self.id, - account_id: self.account_id, + account_id: self.account_id.to_u32().expect("Account Id is negative"), token_id: self.token_id as u16, price_in_wei, valid_until: self.valid_until, diff --git a/core/lib/storage/src/tests/forced_exit_requests.rs b/core/lib/storage/src/tests/forced_exit_requests.rs index 09b4f922e3..c04a757aae 100644 --- a/core/lib/storage/src/tests/forced_exit_requests.rs +++ b/core/lib/storage/src/tests/forced_exit_requests.rs @@ -1,14 +1,16 @@ +use std::str::FromStr; + use crate::misc::forced_exit_requests_schema::ForcedExitRequestsSchema; use crate::tests::db_test; use crate::QueryResult; -use crate::{chain::operations::OperationsSchema, ethereum::EthereumSchema, StorageProcessor}; -use chrono::{DateTime, Utc}; -use num::{BigInt, BigUint, FromPrimitive}; +use crate::StorageProcessor; +use chrono::{DateTime, Timelike, Utc}; +use num::{BigUint, FromPrimitive}; use zksync_types::misc::ForcedExitRequest; #[db_test] async fn store_forced_exit_request(mut storage: StorageProcessor<'_>) -> QueryResult<()> { - let now = Utc::now(); + let now = Utc::now().with_nanosecond(0).unwrap(); let request = ForcedExitRequest { id: 1, @@ -18,7 +20,9 @@ async fn store_forced_exit_request(mut storage: StorageProcessor<'_>) -> QueryRe valid_until: DateTime::from(now), }; - ForcedExitRequestsSchema(&mut storage).store_request(&request); + ForcedExitRequestsSchema(&mut storage) + .store_request(&request) + .await?; let fe = ForcedExitRequestsSchema(&mut storage) .get_request_by_id(1) @@ -29,3 +33,47 @@ async fn store_forced_exit_request(mut storage: StorageProcessor<'_>) -> QueryRe Ok(()) } + +#[db_test] +async fn get_max_forced_exit_used_id(mut storage: StorageProcessor<'_>) -> QueryResult<()> { + let now = Utc::now().with_nanosecond(0).unwrap(); + + let requests = [ + ForcedExitRequest { + id: 1, + account_id: 1, + token_id: 1, + price_in_wei: BigUint::from_i32(212).unwrap(), + valid_until: DateTime::from(now), + }, + ForcedExitRequest { + id: 2, + account_id: 12, + token_id: 0, + price_in_wei: BigUint::from_i32(1).unwrap(), + valid_until: DateTime::from(now), + }, + ForcedExitRequest { + id: 7, + account_id: 3, + token_id: 20, + price_in_wei: BigUint::from_str("1000000000000000").unwrap(), + valid_until: DateTime::from(now), + }, + ]; + + for req in requests.iter() { + ForcedExitRequestsSchema(&mut storage) + .store_request(&req) + .await?; + } + + let max_id = ForcedExitRequestsSchema(&mut storage) + .get_max_used_id() + .await + .expect("Failed to get forced exit by id"); + + assert_eq!(max_id, 7); + + Ok(()) +} From 7cec0d6c81b078677d353fa535a180ca30284b0a Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Tue, 2 Feb 2021 12:13:24 +0200 Subject: [PATCH 07/90] [WIP]: api for forced_exit_requesys --- .../zksync_api/src/api_server/rest/v01/api_decl.rs | 4 ++++ .../zksync_api/src/api_server/rest/v01/api_impl.rs | 12 ++++++++++++ .../zksync_api/src/api_server/rest/v01/types.rs | 4 ++++ .../lib/config/src/configs/forced_exit_requests.rs | 14 ++++++++++++++ core/lib/config/src/configs/mod.rs | 4 +++- core/lib/config/src/lib.rs | 4 +++- etc/env/base/forced_exit_requests.toml | 3 +++ 7 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 core/lib/config/src/configs/forced_exit_requests.rs create mode 100644 etc/env/base/forced_exit_requests.toml diff --git a/core/bin/zksync_api/src/api_server/rest/v01/api_decl.rs b/core/bin/zksync_api/src/api_server/rest/v01/api_decl.rs index bff74f0b13..e1102647d5 100644 --- a/core/bin/zksync_api/src/api_server/rest/v01/api_decl.rs +++ b/core/bin/zksync_api/src/api_server/rest/v01/api_decl.rs @@ -97,6 +97,10 @@ impl ApiV01 { "/withdrawal_processing_time", web::get().to(Self::withdrawal_processing_time), ) + .route( + "/forced_exit/enabled", + web::get().to(Self::is_forced_exit_enabled), + ) } pub(crate) async fn access_storage(&self) -> ActixResult> { diff --git a/core/bin/zksync_api/src/api_server/rest/v01/api_impl.rs b/core/bin/zksync_api/src/api_server/rest/v01/api_impl.rs index 981f7082ea..8171246fa1 100644 --- a/core/bin/zksync_api/src/api_server/rest/v01/api_impl.rs +++ b/core/bin/zksync_api/src/api_server/rest/v01/api_impl.rs @@ -492,4 +492,16 @@ impl ApiV01 { metrics::histogram!("api.v01.withdrawal_processing_time", start.elapsed()); ok_json!(processing_time) } + + pub async fn is_forced_exit_enabled(self_: web::Data) -> ActixResult { + let start = Instant::now(); + + let is_enabled = self_.config.forced_exit_requests.enabled; + let response = IsForcedExitEnabledResponse { + enabled: is_enabled, + }; + + metrics::histogram!("api.v01.is_forced_exit_enabled", start.elapsed()); + ok_json!(response) + } } diff --git a/core/bin/zksync_api/src/api_server/rest/v01/types.rs b/core/bin/zksync_api/src/api_server/rest/v01/types.rs index 40be453db4..689fcb6e5a 100644 --- a/core/bin/zksync_api/src/api_server/rest/v01/types.rs +++ b/core/bin/zksync_api/src/api_server/rest/v01/types.rs @@ -39,3 +39,7 @@ pub struct HandleBlocksQuery { pub struct BlockExplorerSearchQuery { pub query: String, } +#[derive(Serialize, Deserialize)] +pub struct IsForcedExitEnabledResponse { + pub enabled: bool, +} diff --git a/core/lib/config/src/configs/forced_exit_requests.rs b/core/lib/config/src/configs/forced_exit_requests.rs new file mode 100644 index 0000000000..247969c039 --- /dev/null +++ b/core/lib/config/src/configs/forced_exit_requests.rs @@ -0,0 +1,14 @@ +use crate::envy_load; +/// External uses +use serde::Deserialize; + +#[derive(Debug, Deserialize, Clone, PartialEq)] +pub struct ForcedExitRequestsConfig { + pub enabled: bool, +} + +impl ForcedExitRequestsConfig { + pub fn from_env() -> Self { + envy_load!("forced_exit_requests", "FORCED_EXIT_REQUESTS") + } +} diff --git a/core/lib/config/src/configs/mod.rs b/core/lib/config/src/configs/mod.rs index e45e112a98..bace986365 100644 --- a/core/lib/config/src/configs/mod.rs +++ b/core/lib/config/src/configs/mod.rs @@ -2,7 +2,8 @@ pub use self::{ api::ApiConfig, chain::ChainConfig, contracts::ContractsConfig, db::DBConfig, eth_client::ETHClientConfig, eth_sender::ETHSenderConfig, eth_watch::ETHWatchConfig, - misc::MiscConfig, prover::ProverConfig, ticker::TickerConfig, + forced_exit_requests::ForcedExitRequestsConfig, misc::MiscConfig, prover::ProverConfig, + ticker::TickerConfig, }; pub mod api; @@ -12,6 +13,7 @@ pub mod db; pub mod eth_client; pub mod eth_sender; pub mod eth_watch; +pub mod forced_exit_requests; pub mod misc; pub mod prover; pub mod ticker; diff --git a/core/lib/config/src/lib.rs b/core/lib/config/src/lib.rs index 429906396a..c3b0c8f2d7 100644 --- a/core/lib/config/src/lib.rs +++ b/core/lib/config/src/lib.rs @@ -2,7 +2,7 @@ use serde::Deserialize; pub use crate::configs::{ ApiConfig, ChainConfig, ContractsConfig, DBConfig, ETHClientConfig, ETHSenderConfig, - ETHWatchConfig, MiscConfig, ProverConfig, TickerConfig, + ETHWatchConfig, ForcedExitRequestsConfig, MiscConfig, ProverConfig, TickerConfig, }; pub mod configs; @@ -19,6 +19,7 @@ pub struct ZkSyncConfig { pub eth_watch: ETHWatchConfig, pub prover: ProverConfig, pub ticker: TickerConfig, + pub forced_exit_requests: ForcedExitRequestsConfig, } impl ZkSyncConfig { @@ -33,6 +34,7 @@ impl ZkSyncConfig { eth_watch: ETHWatchConfig::from_env(), prover: ProverConfig::from_env(), ticker: TickerConfig::from_env(), + forced_exit_requests: ForcedExitRequestsConfig::from_env(), } } } diff --git a/etc/env/base/forced_exit_requests.toml b/etc/env/base/forced_exit_requests.toml new file mode 100644 index 0000000000..c11d7985b6 --- /dev/null +++ b/etc/env/base/forced_exit_requests.toml @@ -0,0 +1,3 @@ +# Options for L1-based ForcedExit utility +[forced_exit_requests] +enabled=true From ba5381b541cd5540923580ca0da58dc43f409aea Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Wed, 3 Feb 2021 15:33:04 +0200 Subject: [PATCH 08/90] [WIP] api for forced_exit_requests is ready --- .../src/api_server/forced_exit_checker.rs | 52 ++++++ core/bin/zksync_api/src/api_server/mod.rs | 1 + .../zksync_api/src/api_server/rest/helpers.rs | 1 + .../bin/zksync_api/src/api_server/rest/mod.rs | 7 +- .../src/api_server/rest/v01/api_decl.rs | 19 +- .../src/api_server/rest/v01/api_impl.rs | 103 ++++++++++- .../src/api_server/rest/v01/types.rs | 13 +- .../zksync_api/src/api_server/rest/v1/mod.rs | 6 +- .../zksync_api/src/api_server/tx_sender.rs | 164 ++++++++---------- .../src/configs/forced_exit_requests.rs | 2 +- .../up.sql | 6 +- core/lib/storage/sqlx-data.json | 71 ++++---- core/lib/storage/src/lib.rs | 7 + .../src/misc/forced_exit_requests_schema.rs | 36 ++-- core/lib/storage/src/misc/records.rs | 19 +- .../storage/src/tests/forced_exit_requests.rs | 78 ++++----- core/lib/storage/src/tokens/mod.rs | 3 +- core/lib/storage/src/tokens/records.rs | 2 +- core/lib/storage/src/{tokens => }/utils.rs | 0 core/lib/types/src/misc.rs | 15 +- 20 files changed, 404 insertions(+), 201 deletions(-) create mode 100644 core/bin/zksync_api/src/api_server/forced_exit_checker.rs rename core/lib/storage/src/{tokens => }/utils.rs (100%) diff --git a/core/bin/zksync_api/src/api_server/forced_exit_checker.rs b/core/bin/zksync_api/src/api_server/forced_exit_checker.rs new file mode 100644 index 0000000000..2440c3f2ed --- /dev/null +++ b/core/bin/zksync_api/src/api_server/forced_exit_checker.rs @@ -0,0 +1,52 @@ +use crate::api_server::tx_sender::SubmitError; +use zksync_config::ZkSyncConfig; +use zksync_storage::StorageProcessor; +use zksync_types::Address; + +use crate::internal_error; + +use chrono::Utc; +#[derive(Clone)] +pub struct ForcedExitChecker { + /// Mimimum age of the account for `ForcedExit` operations to be allowed. + pub forced_exit_minimum_account_age: chrono::Duration, +} + +impl ForcedExitChecker { + pub fn new(config: &ZkSyncConfig) -> Self { + let forced_exit_minimum_account_age = chrono::Duration::seconds( + config.api.common.forced_exit_minimum_account_age_secs as i64, + ); + + Self { + forced_exit_minimum_account_age, + } + } + + pub async fn check_forced_exit<'a>( + &self, + storage: &mut StorageProcessor<'a>, + target_account_address: Address, + ) -> Result<(), SubmitError> { + let account_age = storage + .chain() + .operations_ext_schema() + .account_created_on(&target_account_address) + .await + .map_err(|err| internal_error!(err, target_account_address))?; + + match account_age { + Some(age) if Utc::now() - age < self.forced_exit_minimum_account_age => { + let msg = format!( + "Target account exists less than required minimum amount ({} hours)", + self.forced_exit_minimum_account_age.num_hours() + ); + + Err(SubmitError::InvalidParams(msg)) + } + None => Err(SubmitError::invalid_params("Target account does not exist")), + + Some(..) => Ok(()), + } + } +} diff --git a/core/bin/zksync_api/src/api_server/mod.rs b/core/bin/zksync_api/src/api_server/mod.rs index b1432f67b5..41a6f950a1 100644 --- a/core/bin/zksync_api/src/api_server/mod.rs +++ b/core/bin/zksync_api/src/api_server/mod.rs @@ -18,6 +18,7 @@ use crate::signature_checker; mod admin_server; mod event_notify; +mod forced_exit_checker; mod helpers; mod loggers; mod rest; diff --git a/core/bin/zksync_api/src/api_server/rest/helpers.rs b/core/bin/zksync_api/src/api_server/rest/helpers.rs index 6aa1411442..4c3a14a8d8 100644 --- a/core/bin/zksync_api/src/api_server/rest/helpers.rs +++ b/core/bin/zksync_api/src/api_server/rest/helpers.rs @@ -2,6 +2,7 @@ use crate::core_api_client::EthBlockId; use actix_web::{HttpResponse, Result as ActixResult}; +use bigdecimal::BigDecimal; use std::collections::HashMap; use zksync_storage::chain::{ block::records::BlockDetails, diff --git a/core/bin/zksync_api/src/api_server/rest/mod.rs b/core/bin/zksync_api/src/api_server/rest/mod.rs index d8d511fa76..6c953770b1 100644 --- a/core/bin/zksync_api/src/api_server/rest/mod.rs +++ b/core/bin/zksync_api/src/api_server/rest/mod.rs @@ -75,7 +75,12 @@ pub(super) fn start_server_thread_detached( let _panic_sentinel = ThreadPanicNotify(panic_notify.clone()); actix_rt::System::new("api-server").block_on(async move { - let api_v01 = ApiV01::new(connection_pool, contract_address, config.clone()); + let api_v01 = ApiV01::new( + connection_pool, + contract_address, + config.clone(), + fee_ticker.clone(), + ); api_v01.spawn_network_status_updater(panic_notify); start_server(api_v01, fee_ticker, sign_verifier, listen_addr).await; diff --git a/core/bin/zksync_api/src/api_server/rest/v01/api_decl.rs b/core/bin/zksync_api/src/api_server/rest/v01/api_decl.rs index e1102647d5..bad5bf1095 100644 --- a/core/bin/zksync_api/src/api_server/rest/v01/api_decl.rs +++ b/core/bin/zksync_api/src/api_server/rest/v01/api_decl.rs @@ -1,11 +1,15 @@ //! Declaration of the API structure. use crate::{ + api_server::forced_exit_checker::ForcedExitChecker, api_server::rest::{ helpers::*, v01::{caches::Caches, network_status::SharedNetworkStatus}, + TxSender, }, core_api_client::{CoreApiClient, EthBlockId}, + fee_ticker::TickerRequest, + signature_checker::VerifyTxSignatureRequest, }; use actix_web::{web, HttpResponse, Result as ActixResult}; use futures::channel::mpsc; @@ -24,7 +28,7 @@ use zksync_types::{block::ExecutedOperations, PriorityOp, H160, H256}; /// /// Once a new API is designed, it will be created as `ApiV1` structure, so that /// each API version is encapsulated inside one type. -#[derive(Debug, Clone)] +#[derive(Clone)] pub struct ApiV01 { pub(crate) caches: Caches, pub(crate) connection_pool: ConnectionPool, @@ -32,6 +36,8 @@ pub struct ApiV01 { pub(crate) network_status: SharedNetworkStatus, pub(crate) contract_address: String, pub(crate) config: ZkSyncConfig, + pub(crate) forced_exit_checker: ForcedExitChecker, + pub(crate) ticker_request_sender: mpsc::Sender, } impl ApiV01 { @@ -39,14 +45,18 @@ impl ApiV01 { connection_pool: ConnectionPool, contract_address: H160, config: ZkSyncConfig, + ticker_request_sender: mpsc::Sender, ) -> Self { let api_client = CoreApiClient::new(config.api.private.url.clone()); + Self { caches: Caches::new(config.api.common.caches_size), connection_pool, api_client, network_status: SharedNetworkStatus::default(), contract_address: format!("{:?}", contract_address), + forced_exit_checker: ForcedExitChecker::new(&config), + ticker_request_sender, config, } } @@ -101,6 +111,13 @@ impl ApiV01 { "/forced_exit/enabled", web::get().to(Self::is_forced_exit_enabled), ) + .route( + "/forced_exit/register", + // All the other routes in this file are the `get` routes + // Unfortunately to preserve consistency would mean to the JSON-RPC + // api which is even greater evil + web::post().to(Self::register_forced_exit_request), + ) } pub(crate) async fn access_storage(&self) -> ActixResult> { diff --git a/core/bin/zksync_api/src/api_server/rest/v01/api_impl.rs b/core/bin/zksync_api/src/api_server/rest/v01/api_impl.rs index 8171246fa1..b8c0d6e4fe 100644 --- a/core/bin/zksync_api/src/api_server/rest/v01/api_impl.rs +++ b/core/bin/zksync_api/src/api_server/rest/v01/api_impl.rs @@ -9,12 +9,27 @@ use crate::api_server::{ rest::{ helpers::{deposit_op_to_tx_by_hash, parse_tx_id, priority_op_to_tx_history}, v01::{api_decl::ApiV01, types::*}, + v1::{ApiError, Error, JsonResult}, }, + tx_sender::ticker_request, }; use actix_web::{web, HttpResponse, Result as ActixResult}; +use std::ops::{Add, Mul}; + +use bigdecimal::BigDecimal; +use futures::{SinkExt, TryFutureExt}; +use num::{bigint::ToBigInt, BigUint}; +use web::Json; +use zksync_config::test_config::unit_vectors::ForcedExit; + +use chrono::{DateTime, Duration, Utc}; +use std::str::FromStr; use std::time::Instant; use zksync_storage::chain::operations_ext::SearchDirection; -use zksync_types::{Address, BlockNumber}; +use zksync_types::{ + misc::{ForcedExitRequest, SaveForcedExitRequestQuery}, + Address, BlockNumber, TokenLike, TxFeeTypes, +}; /// Helper macro which wraps the serializable object into `Ok(HttpResponse::Ok().json(...))`. macro_rules! ok_json { @@ -504,4 +519,90 @@ impl ApiV01 { metrics::histogram!("api.v01.is_forced_exit_enabled", start.elapsed()); ok_json!(response) } + + pub async fn register_forced_exit_request( + self_: web::Data, + params: web::Json, + ) -> JsonResult { + let start = Instant::now(); + + let time = Utc::now(); + + if !self_.config.forced_exit_requests.enabled { + return Err(ApiError::bad_request( + "ForcedExit requests feature is disabled!", + )); + } + + let mut storage = self_.access_storage().await.map_err(|err| { + vlog::warn!("Internal Server Error: '{}';", err); + return ApiError::internal(""); + })?; + + self_ + .forced_exit_checker + .check_forced_exit(&mut storage, params.target) + .await + .map_err(ApiError::from)?; + + let price = ticker_request( + self_.ticker_request_sender.clone(), + TxFeeTypes::Withdraw, + TokenLike::Id(0), + ) + .await + .map_err(ApiError::from)?; + let price = BigDecimal::from(price.total_fee.to_bigint().unwrap()); + + let scaling_coefficient = BigDecimal::from_str("1.6").unwrap(); + let scaled_price = price * scaling_coefficient; + + let user_price = params.price_in_wei.to_bigint().unwrap(); + let user_price = BigDecimal::from(user_price); + let user_scaling_coefficient = BigDecimal::from_str("1.05").unwrap(); + let user_scaled_fee = user_scaling_coefficient * user_price; + + if user_scaled_fee < scaled_price { + return Err(ApiError::bad_request("Not enough fee")); + } + + if params.tokens.len() > 10 { + return Err(ApiError::bad_request( + "Maximum number of tokens per FE request exceeded", + )); + } + + let mut tokens_schema = storage.tokens_schema(); + + for token_id in params.tokens.iter() { + // The result is going nowhere. + // This is simply to make sure that the tokens + // that were supplied do indeed exist + tokens_schema + .get_token(TokenLike::Id(*token_id)) + .await + .map_err(|e| { + return ApiError::bad_request("One of the tokens does no exist"); + })?; + } + + let mut fe_schema = storage.forced_exit_requests_schema(); + + let valid_until = Utc::now().add(Duration::weeks(1)); + + let saved_fe_request = fe_schema + .store_request(SaveForcedExitRequestQuery { + target: params.target, + tokens: params.tokens.clone(), + price_in_wei: params.price_in_wei.clone(), + valid_until, + }) + .await + .map_err(|e| { + return ApiError::internal(""); + })?; + + metrics::histogram!("api.v01.register_forced_exit_request", start.elapsed()); + Ok(Json(saved_fe_request)) + } } diff --git a/core/bin/zksync_api/src/api_server/rest/v01/types.rs b/core/bin/zksync_api/src/api_server/rest/v01/types.rs index 689fcb6e5a..c103ae8d79 100644 --- a/core/bin/zksync_api/src/api_server/rest/v01/types.rs +++ b/core/bin/zksync_api/src/api_server/rest/v01/types.rs @@ -1,7 +1,10 @@ //! Requests and responses used by the REST API. +use num::BigUint; use serde::{Deserialize, Serialize}; -use zksync_types::{Account, AccountId}; +use zksync_types::TokenId; +use zksync_types::{Account, AccountId, Address}; +use zksync_utils::BigUintSerdeAsRadix10Str; #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] @@ -43,3 +46,11 @@ pub struct BlockExplorerSearchQuery { pub struct IsForcedExitEnabledResponse { pub enabled: bool, } + +#[derive(Deserialize)] +pub struct ForcedExitRegisterRequest { + pub target: Address, + pub tokens: Vec, + #[serde(with = "BigUintSerdeAsRadix10Str")] + pub price_in_wei: BigUint, +} diff --git a/core/bin/zksync_api/src/api_server/rest/v1/mod.rs b/core/bin/zksync_api/src/api_server/rest/v1/mod.rs index c0679f1921..64932359b8 100644 --- a/core/bin/zksync_api/src/api_server/rest/v1/mod.rs +++ b/core/bin/zksync_api/src/api_server/rest/v1/mod.rs @@ -6,7 +6,7 @@ use actix_web::{ Scope, }; -use Error as ApiError; +pub use Error as ApiError; // Workspace uses pub use zksync_api_client::rest::v1::{ Client, ClientError, Pagination, PaginationQuery, MAX_LIMIT, @@ -22,7 +22,7 @@ pub use self::error::{Error, ErrorBody}; pub(crate) mod accounts; mod blocks; mod config; -mod error; +pub mod error; mod operations; mod search; #[cfg(test)] @@ -30,7 +30,7 @@ mod test_utils; mod tokens; mod transactions; -type JsonResult = std::result::Result, Error>; +pub type JsonResult = std::result::Result, Error>; pub(crate) fn api_scope(tx_sender: TxSender, zk_config: &ZkSyncConfig) -> Scope { web::scope("/api/v1") diff --git a/core/bin/zksync_api/src/api_server/tx_sender.rs b/core/bin/zksync_api/src/api_server/tx_sender.rs index 799b026ad3..a6fc9bf403 100644 --- a/core/bin/zksync_api/src/api_server/tx_sender.rs +++ b/core/bin/zksync_api/src/api_server/tx_sender.rs @@ -25,6 +25,7 @@ use zksync_types::{ // Local uses use crate::api_server::rpc_server::types::TxWithSignature; use crate::{ + api_server::forced_exit_checker::ForcedExitChecker, core_api_client::CoreApiClient, fee_ticker::{Fee, TickerRequest, TokenPriceRequestType}, signature_checker::{TxVariant, VerifiedTx, VerifyTxSignatureRequest}, @@ -40,8 +41,8 @@ pub struct TxSender { pub pool: ConnectionPool, pub tokens: TokenDBCache, - /// Mimimum age of the account for `ForcedExit` operations to be allowed. - pub forced_exit_minimum_account_age: chrono::Duration, + + pub forced_exit_checker: ForcedExitChecker, pub enforce_pubkey_change_fee: bool, // Limit the number of both transactions and Ethereum signatures per batch. pub max_number_of_transactions_per_batch: usize, @@ -72,23 +73,24 @@ pub enum SubmitError { } impl SubmitError { - fn internal(inner: impl Into) -> Self { + pub fn internal(inner: impl Into) -> Self { Self::Internal(inner.into()) } - fn other(msg: impl Display) -> Self { + pub fn other(msg: impl Display) -> Self { Self::Other(msg.to_string()) } - fn communication_core_server(msg: impl Display) -> Self { + pub fn communication_core_server(msg: impl Display) -> Self { Self::CommunicationCoreServer(msg.to_string()) } - fn invalid_params(msg: impl Display) -> Self { + pub fn invalid_params(msg: impl Display) -> Self { Self::InvalidParams(msg.to_string()) } } +#[macro_export] macro_rules! internal_error { ($err:tt, $input:tt) => {{ vlog::warn!("Internal Server error: {}, input: {:?}", $err, $input); @@ -125,10 +127,6 @@ impl TxSender { ticker_request_sender: mpsc::Sender, config: &ZkSyncConfig, ) -> Self { - let forced_exit_minimum_account_age = chrono::Duration::seconds( - config.api.common.forced_exit_minimum_account_age_secs as i64, - ); - let max_number_of_transactions_per_batch = config.api.common.max_number_of_transactions_per_batch as usize; let max_number_of_authors_per_batch = @@ -140,9 +138,8 @@ impl TxSender { sign_verify_requests: sign_verify_request_sender, ticker_requests: ticker_request_sender, tokens: TokenDBCache::new(), - + forced_exit_checker: ForcedExitChecker::new(config), enforce_pubkey_change_fee: config.api.common.enforce_pubkey_change_fee, - forced_exit_minimum_account_age, max_number_of_transactions_per_batch, max_number_of_authors_per_batch, } @@ -229,14 +226,14 @@ impl TxSender { || self.enforce_pubkey_change_fee; let fee_allowed = - Self::token_allowed_for_fees(ticker_request_sender.clone(), token.clone()).await?; + token_allowed_for_fees(ticker_request_sender.clone(), token.clone()).await?; if !fee_allowed { return Err(SubmitError::InappropriateFeeToken); } let required_fee = - Self::ticker_request(ticker_request_sender, tx_type, token.clone()).await?; + ticker_request(ticker_request_sender, tx_type, token.clone()).await?; // Converting `BitUint` to `BigInt` is safe. let required_fee: BigDecimal = required_fee.total_fee.to_bigint().unwrap().into(); let provided_fee: BigDecimal = provided_fee.to_bigint().unwrap().into(); @@ -314,8 +311,7 @@ impl TxSender { if let Some((tx_type, token, provided_fee)) = tx_fee_info { let fee_allowed = - Self::token_allowed_for_fees(self.ticker_requests.clone(), token.clone()) - .await?; + token_allowed_for_fees(self.ticker_requests.clone(), token.clone()).await?; // In batches, transactions with non-popular token are allowed to be included, but should not // used to pay fees. Fees must be covered by some more common token. @@ -332,13 +328,10 @@ impl TxSender { TokenLike::Id(0) }; - let required_fee = Self::ticker_request( - self.ticker_requests.clone(), - tx_type, - check_token.clone(), - ) - .await?; - let token_price_in_usd = Self::ticker_price_request( + let required_fee = + ticker_request(self.ticker_requests.clone(), tx_type, check_token.clone()) + .await?; + let token_price_in_usd = ticker_price_request( self.ticker_requests.clone(), check_token.clone(), TokenPriceRequestType::USDForOneWei, @@ -467,28 +460,9 @@ impl TxSender { .await .map_err(SubmitError::internal)?; - let target_account_address = forced_exit.target; - - let account_age = storage - .chain() - .operations_ext_schema() - .account_created_on(&target_account_address) + self.forced_exit_checker + .check_forced_exit(&mut storage, forced_exit.target) .await - .map_err(|err| internal_error!(err, forced_exit))?; - - match account_age { - Some(age) if Utc::now() - age < self.forced_exit_minimum_account_age => { - let msg = format!( - "Target account exists less than required minimum amount ({} hours)", - self.forced_exit_minimum_account_age.num_hours() - ); - - Err(SubmitError::InvalidParams(msg)) - } - None => Err(SubmitError::invalid_params("Target account does not exist")), - - Some(..) => Ok(()), - } } /// Resolves the token from the database. @@ -506,61 +480,61 @@ impl TxSender { // TODO Make error more clean .ok_or_else(|| SubmitError::other("Token not found in the DB")) } +} - async fn ticker_request( - mut ticker_request_sender: mpsc::Sender, - tx_type: TxFeeTypes, - token: TokenLike, - ) -> Result { - let req = oneshot::channel(); - ticker_request_sender - .send(TickerRequest::GetTxFee { - tx_type, - token: token.clone(), - response: req.0, - }) - .await - .map_err(SubmitError::internal)?; +pub async fn ticker_request( + mut ticker_request_sender: mpsc::Sender, + tx_type: TxFeeTypes, + token: TokenLike, +) -> Result { + let req = oneshot::channel(); + ticker_request_sender + .send(TickerRequest::GetTxFee { + tx_type, + token: token.clone(), + response: req.0, + }) + .await + .map_err(SubmitError::internal)?; - let resp = req.1.await.map_err(SubmitError::internal)?; - resp.map_err(|err| internal_error!(err)) - } + let resp = req.1.await.map_err(SubmitError::internal)?; + resp.map_err(|err| internal_error!(err)) +} - async fn token_allowed_for_fees( - mut ticker_request_sender: mpsc::Sender, - token: TokenLike, - ) -> Result { - let (sender, receiver) = oneshot::channel(); - ticker_request_sender - .send(TickerRequest::IsTokenAllowed { - token: token.clone(), - response: sender, - }) - .await - .expect("ticker receiver dropped"); - receiver - .await - .expect("ticker answer sender dropped") - .map_err(SubmitError::internal) - } +pub async fn token_allowed_for_fees( + mut ticker_request_sender: mpsc::Sender, + token: TokenLike, +) -> Result { + let (sender, receiver) = oneshot::channel(); + ticker_request_sender + .send(TickerRequest::IsTokenAllowed { + token: token.clone(), + response: sender, + }) + .await + .expect("ticker receiver dropped"); + receiver + .await + .expect("ticker answer sender dropped") + .map_err(SubmitError::internal) +} - async fn ticker_price_request( - mut ticker_request_sender: mpsc::Sender, - token: TokenLike, - req_type: TokenPriceRequestType, - ) -> Result { - let req = oneshot::channel(); - ticker_request_sender - .send(TickerRequest::GetTokenPrice { - token: token.clone(), - response: req.0, - req_type, - }) - .await - .map_err(SubmitError::internal)?; - let resp = req.1.await.map_err(SubmitError::internal)?; - resp.map_err(|err| internal_error!(err)) - } +pub async fn ticker_price_request( + mut ticker_request_sender: mpsc::Sender, + token: TokenLike, + req_type: TokenPriceRequestType, +) -> Result { + let req = oneshot::channel(); + ticker_request_sender + .send(TickerRequest::GetTokenPrice { + token: token.clone(), + response: req.0, + req_type, + }) + .await + .map_err(SubmitError::internal)?; + let resp = req.1.await.map_err(SubmitError::internal)?; + resp.map_err(|err| internal_error!(err)) } async fn send_verify_request_and_recv( diff --git a/core/lib/config/src/configs/forced_exit_requests.rs b/core/lib/config/src/configs/forced_exit_requests.rs index 247969c039..79704155c3 100644 --- a/core/lib/config/src/configs/forced_exit_requests.rs +++ b/core/lib/config/src/configs/forced_exit_requests.rs @@ -9,6 +9,6 @@ pub struct ForcedExitRequestsConfig { impl ForcedExitRequestsConfig { pub fn from_env() -> Self { - envy_load!("forced_exit_requests", "FORCED_EXIT_REQUESTS") + envy_load!("forced_exit_requests", "FORCED_EXIT_REQUESTS_") } } diff --git a/core/lib/storage/migrations/2021-01-31-160151_forced_exit_requests/up.sql b/core/lib/storage/migrations/2021-01-31-160151_forced_exit_requests/up.sql index b95d200231..0430dd27df 100644 --- a/core/lib/storage/migrations/2021-01-31-160151_forced_exit_requests/up.sql +++ b/core/lib/storage/migrations/2021-01-31-160151_forced_exit_requests/up.sql @@ -1,7 +1,7 @@ CREATE TABLE forced_exit_requests ( - id BIGINT NOT NULL PRIMARY KEY, - account_id BIGINT NOT NULL, - token_id INTEGER NOT NULL, + id BIGSERIAL PRIMARY KEY, + target TEXT NOT NULL, + tokens INTEGER ARRAY NOT NULL, price_in_wei NUMERIC NOT NULL, valid_until TIMESTAMP with time zone NOT NULL ); diff --git a/core/lib/storage/sqlx-data.json b/core/lib/storage/sqlx-data.json index 9d1b44c73c..c4fbf04781 100644 --- a/core/lib/storage/sqlx-data.json +++ b/core/lib/storage/sqlx-data.json @@ -895,13 +895,13 @@ }, { "ordinal": 8, - "name": "commitment", - "type_info": "Bytea" + "name": "timestamp", + "type_info": "Int8" }, { "ordinal": 9, - "name": "timestamp", - "type_info": "Int8" + "name": "commitment", + "type_info": "Bytea" } ], "parameters": { @@ -918,8 +918,8 @@ false, false, false, - false, - true + true, + false ] } }, @@ -1419,22 +1419,6 @@ ] } }, - "53ab59757e47450b31e9fb2ad006943246acdb14ac03d79518bcc5e19bb66cc8": { - "query": "\n INSERT INTO forced_exit_requests ( id, account_id, token_id, price_in_wei, valid_until )\n VALUES ( $1, $2, $3, $4, $5 )\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8", - "Int8", - "Int4", - "Numeric", - "Timestamptz" - ] - }, - "nullable": [] - } - }, "58b251c3fbdf9be9b62f669f8cdc2d98940026c831e02a53337474d36a5224f0": { "query": "UPDATE aggregate_operations\n SET confirmed = $1\n WHERE from_block >= $2 AND to_block <= $3 AND action_type = $4", "describe": { @@ -2119,13 +2103,13 @@ }, { "ordinal": 1, - "name": "account_id", - "type_info": "Int8" + "name": "target", + "type_info": "Text" }, { "ordinal": 2, - "name": "token_id", - "type_info": "Int4" + "name": "tokens", + "type_info": "Int4Array" }, { "ordinal": 3, @@ -2422,13 +2406,13 @@ }, { "ordinal": 4, - "name": "previous_root_hash", - "type_info": "Bytea" + "name": "timestamp", + "type_info": "Int8" }, { "ordinal": 5, - "name": "timestamp", - "type_info": "Int8" + "name": "previous_root_hash", + "type_info": "Bytea" } ], "parameters": { @@ -2439,8 +2423,8 @@ false, false, false, - false, - true + true, + false ] } }, @@ -3602,6 +3586,29 @@ ] } }, + "df8ac3519c49215ebef5ac11c8fed5af12b097eb0c65f9a4ce078bb6e8342885": { + "query": "\n INSERT INTO forced_exit_requests ( target, tokens, price_in_wei, valid_until )\n VALUES ( $1, $2, $3, $4 )\n RETURNING id\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Text", + "Int4Array", + "Numeric", + "Timestamptz" + ] + }, + "nullable": [ + false + ] + } + }, "e3ee3cb9cbe8d05a635e71daea301cf6b2310f89f3d9f8fdabc28e7ebf8d3521": { "query": "\n INSERT INTO eth_account_types VALUES ( $1, $2 )\n ON CONFLICT (account_id) DO UPDATE SET account_type = $2\n ", "describe": { diff --git a/core/lib/storage/src/lib.rs b/core/lib/storage/src/lib.rs index 171c156fd9..aa71f1863d 100644 --- a/core/lib/storage/src/lib.rs +++ b/core/lib/storage/src/lib.rs @@ -72,6 +72,8 @@ // `sqlx` macros result in these warning being triggered. #![allow(clippy::toplevel_ref_arg, clippy::suspicious_else_formatting)] +use forced_exit_requests_schema::ForcedExitRequestsSchema; +use misc::forced_exit_requests_schema; // Built-in deps // use std::env; // External imports @@ -95,6 +97,7 @@ pub mod misc; pub mod prover; pub mod test_data; pub mod tokens; +mod utils; pub use crate::connection::ConnectionPool; pub type QueryResult = Result; @@ -196,6 +199,10 @@ impl<'a> StorageProcessor<'a> { tokens::TokensSchema(self) } + pub fn forced_exit_requests_schema(&mut self) -> ForcedExitRequestsSchema<'_, 'a> { + ForcedExitRequestsSchema(self) + } + fn conn(&mut self) -> &mut PgConnection { match &mut self.conn { ConnectionHolder::Pooled(conn) => conn, diff --git a/core/lib/storage/src/misc/forced_exit_requests_schema.rs b/core/lib/storage/src/misc/forced_exit_requests_schema.rs index dfe92808c6..af57a39d6a 100644 --- a/core/lib/storage/src/misc/forced_exit_requests_schema.rs +++ b/core/lib/storage/src/misc/forced_exit_requests_schema.rs @@ -6,34 +6,48 @@ use std::time::Instant; // Workspace imports // Local imports use crate::{QueryResult, StorageProcessor}; -use zksync_types::misc::{ForcedExitRequest, ForcedExitRequestId}; +use zksync_types::misc::{ForcedExitRequest, ForcedExitRequestId, SaveForcedExitRequestQuery}; use super::records::DbForcedExitRequest; +use crate::utils::address_to_stored_string; /// ForcedExitRequests schema handles the `forced_exit_requests` table, providing methods to #[derive(Debug)] pub struct ForcedExitRequestsSchema<'a, 'c>(pub &'a mut StorageProcessor<'c>); impl<'a, 'c> ForcedExitRequestsSchema<'a, 'c> { - pub async fn store_request(&mut self, request: &ForcedExitRequest) -> QueryResult<()> { + pub async fn store_request( + &mut self, + request: SaveForcedExitRequestQuery, + ) -> QueryResult { let start = Instant::now(); let price_in_wei = BigDecimal::from(BigInt::from(request.price_in_wei.clone())); - sqlx::query!( + + let target_str = address_to_stored_string(&request.target); + let tokens: Vec = request.tokens.iter().map(|t| *t as i32).collect(); + let id: i64 = sqlx::query!( r#" - INSERT INTO forced_exit_requests ( id, account_id, token_id, price_in_wei, valid_until ) - VALUES ( $1, $2, $3, $4, $5 ) + INSERT INTO forced_exit_requests ( target, tokens, price_in_wei, valid_until ) + VALUES ( $1, $2, $3, $4 ) + RETURNING id "#, - request.id, - i64::from(request.account_id), - i32::from(request.token_id), + target_str, + &tokens, price_in_wei, request.valid_until ) - .execute(self.0.conn()) - .await?; + .fetch_one(self.0.conn()) + .await? + .id; metrics::histogram!("sql.forced_exit_requests.store_request", start.elapsed()); - Ok(()) + Ok(ForcedExitRequest { + id, + target: request.target, + tokens: request.tokens.clone(), + price_in_wei: request.price_in_wei.clone(), + valid_until: request.valid_until, + }) } pub async fn get_request_by_id( diff --git a/core/lib/storage/src/misc/records.rs b/core/lib/storage/src/misc/records.rs index 8265abb047..c50f2e2690 100644 --- a/core/lib/storage/src/misc/records.rs +++ b/core/lib/storage/src/misc/records.rs @@ -1,13 +1,15 @@ +use crate::utils::{address_to_stored_string, stored_str_address_to_address}; use chrono::{DateTime, Utc}; use num::{bigint::ToBigInt, BigInt, ToPrimitive}; use sqlx::{types::BigDecimal, FromRow}; +use zksync_basic_types::Address; use zksync_types::misc::ForcedExitRequest; -#[derive(Debug, Clone, FromRow)] +#[derive(Debug, Clone, sqlx::Type)] pub struct DbForcedExitRequest { pub id: i64, - pub account_id: i64, - pub token_id: i32, + pub target: String, + pub tokens: Vec, pub price_in_wei: BigDecimal, pub valid_until: DateTime, } @@ -15,10 +17,11 @@ pub struct DbForcedExitRequest { impl From for DbForcedExitRequest { fn from(request: ForcedExitRequest) -> Self { let price_in_wei = BigDecimal::from(BigInt::from(request.price_in_wei.clone())); + let tokens: Vec = request.tokens.iter().map(|t| *t as i32).collect(); Self { id: request.id, - account_id: request.account_id as i64, - token_id: request.token_id as i32, + target: address_to_stored_string(&request.target), + tokens: tokens, price_in_wei, valid_until: request.valid_until, } @@ -36,10 +39,12 @@ impl Into for DbForcedExitRequest { // means that invalid data is stored in the DB .expect("Invalid forced exit request has been stored"); + let tokens: Vec = self.tokens.iter().map(|t| *t as u16).collect(); + ForcedExitRequest { id: self.id, - account_id: self.account_id.to_u32().expect("Account Id is negative"), - token_id: self.token_id as u16, + target: stored_str_address_to_address(&self.target), + tokens, price_in_wei, valid_until: self.valid_until, } diff --git a/core/lib/storage/src/tests/forced_exit_requests.rs b/core/lib/storage/src/tests/forced_exit_requests.rs index c04a757aae..e52daed3f1 100644 --- a/core/lib/storage/src/tests/forced_exit_requests.rs +++ b/core/lib/storage/src/tests/forced_exit_requests.rs @@ -15,7 +15,7 @@ async fn store_forced_exit_request(mut storage: StorageProcessor<'_>) -> QueryRe let request = ForcedExitRequest { id: 1, account_id: 12, - token_id: 0, + tokens: vec![0], price_in_wei: BigUint::from_i32(121212).unwrap(), valid_until: DateTime::from(now), }; @@ -34,46 +34,46 @@ async fn store_forced_exit_request(mut storage: StorageProcessor<'_>) -> QueryRe Ok(()) } -#[db_test] -async fn get_max_forced_exit_used_id(mut storage: StorageProcessor<'_>) -> QueryResult<()> { - let now = Utc::now().with_nanosecond(0).unwrap(); +// #[db_test] +// async fn get_max_forced_exit_used_id(mut storage: StorageProcessor<'_>) -> QueryResult<()> { +// let now = Utc::now().with_nanosecond(0).unwrap(); - let requests = [ - ForcedExitRequest { - id: 1, - account_id: 1, - token_id: 1, - price_in_wei: BigUint::from_i32(212).unwrap(), - valid_until: DateTime::from(now), - }, - ForcedExitRequest { - id: 2, - account_id: 12, - token_id: 0, - price_in_wei: BigUint::from_i32(1).unwrap(), - valid_until: DateTime::from(now), - }, - ForcedExitRequest { - id: 7, - account_id: 3, - token_id: 20, - price_in_wei: BigUint::from_str("1000000000000000").unwrap(), - valid_until: DateTime::from(now), - }, - ]; +// let requests = [ +// ForcedExitRequest { +// id: 1, +// account_id: 1, +// tokens: vec!(1), +// price_in_wei: BigUint::from_i32(212).unwrap(), +// valid_until: DateTime::from(now), +// }, +// ForcedExitRequest { +// id: 2, +// account_id: 12, +// tokens: vec!(0), +// price_in_wei: BigUint::from_i32(1).unwrap(), +// valid_until: DateTime::from(now), +// }, +// ForcedExitRequest { +// id: 7, +// account_id: 3, +// tokens: vec!(20), +// price_in_wei: BigUint::from_str("1000000000000000").unwrap(), +// valid_until: DateTime::from(now), +// }, +// ]; - for req in requests.iter() { - ForcedExitRequestsSchema(&mut storage) - .store_request(&req) - .await?; - } +// for req in requests.iter() { +// ForcedExitRequestsSchema(&mut storage) +// .store_request(&req) +// .await?; +// } - let max_id = ForcedExitRequestsSchema(&mut storage) - .get_max_used_id() - .await - .expect("Failed to get forced exit by id"); +// let max_id = ForcedExitRequestsSchema(&mut storage) +// .get_max_used_id() +// .await +// .expect("Failed to get forced exit by id"); - assert_eq!(max_id, 7); +// assert_eq!(max_id, 7); - Ok(()) -} +// Ok(()) +// } diff --git a/core/lib/storage/src/tokens/mod.rs b/core/lib/storage/src/tokens/mod.rs index 32c11f8dc8..2f968cdeb4 100644 --- a/core/lib/storage/src/tokens/mod.rs +++ b/core/lib/storage/src/tokens/mod.rs @@ -7,12 +7,11 @@ use zksync_types::{Token, TokenId, TokenLike, TokenPrice}; use zksync_utils::ratio_to_big_decimal; // Local imports use self::records::{DBMarketVolume, DbTickerPrice, DbToken}; -use crate::tokens::utils::address_to_stored_string; +use crate::utils::address_to_stored_string; use crate::{QueryResult, StorageProcessor}; use zksync_types::tokens::TokenMarketVolume; pub mod records; -mod utils; /// Precision of the USD price per token pub(crate) const STORED_USD_PRICE_PRECISION: usize = 6; diff --git a/core/lib/storage/src/tokens/records.rs b/core/lib/storage/src/tokens/records.rs index be567fa96e..1c1580cc9f 100644 --- a/core/lib/storage/src/tokens/records.rs +++ b/core/lib/storage/src/tokens/records.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use sqlx::{types::BigDecimal, FromRow}; // Workspace imports // Local imports -use crate::tokens::utils::{address_to_stored_string, stored_str_address_to_address}; +use crate::utils::{address_to_stored_string, stored_str_address_to_address}; use chrono::{DateTime, Utc}; use zksync_types::tokens::{TokenMarketVolume, TokenPrice}; use zksync_types::{Token, TokenId}; diff --git a/core/lib/storage/src/tokens/utils.rs b/core/lib/storage/src/utils.rs similarity index 100% rename from core/lib/storage/src/tokens/utils.rs rename to core/lib/storage/src/utils.rs diff --git a/core/lib/types/src/misc.rs b/core/lib/types/src/misc.rs index cc0e5062ef..20d9bc5401 100644 --- a/core/lib/types/src/misc.rs +++ b/core/lib/types/src/misc.rs @@ -1,6 +1,6 @@ use chrono::{DateTime, Utc}; use num::BigUint; -use zksync_basic_types::{AccountId, TokenId}; +use zksync_basic_types::{Address, TokenId}; use zksync_utils::BigUintSerdeAsRadix10Str; use serde::{Deserialize, Serialize}; @@ -11,8 +11,17 @@ pub type ForcedExitRequestId = i64; #[serde(rename_all = "camelCase")] pub struct ForcedExitRequest { pub id: ForcedExitRequestId, - pub account_id: AccountId, - pub token_id: TokenId, + pub target: Address, + pub tokens: Vec, + #[serde(with = "BigUintSerdeAsRadix10Str")] + pub price_in_wei: BigUint, + pub valid_until: DateTime, +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)] +pub struct SaveForcedExitRequestQuery { + pub target: Address, + pub tokens: Vec, #[serde(with = "BigUintSerdeAsRadix10Str")] pub price_in_wei: BigUint, pub valid_until: DateTime, From f8493cd95f85a5ceb12b74d2f471b84bf1baa03a Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Wed, 3 Feb 2021 17:22:02 +0200 Subject: [PATCH 09/90] Move ForcedExitRequests api to v1 --- contracts/src.ts/deploy.ts | 2 +- .../src/api_server/rest/v01/api_decl.rs | 11 - .../src/api_server/rest/v01/api_impl.rs | 98 ---- .../src/api_server/rest/v01/types.rs | 15 +- .../rest/v1/forced_exit_requests.rs | 426 ++++++++++++++++++ .../zksync_api/src/api_server/rest/v1/mod.rs | 6 + infrastructure/zk/src/config.ts | 3 +- 7 files changed, 436 insertions(+), 125 deletions(-) create mode 100644 core/bin/zksync_api/src/api_server/rest/v1/forced_exit_requests.rs diff --git a/contracts/src.ts/deploy.ts b/contracts/src.ts/deploy.ts index 23bbac5bee..836cc2a2fc 100644 --- a/contracts/src.ts/deploy.ts +++ b/contracts/src.ts/deploy.ts @@ -215,7 +215,7 @@ export class Deployer { if (this.verbose) { console.log('Deploying ForcedExit contract'); } - const forcedExitContract = await deployContract(this.deployWallet, this.contracts.forcedExit, [], { + const forcedExitContract = await deployContract(this.deployWallet, this.contracts.forcedExit, [this.deployWallet.address], { gasLimit: 6000000, ...ethTxOptions }); diff --git a/core/bin/zksync_api/src/api_server/rest/v01/api_decl.rs b/core/bin/zksync_api/src/api_server/rest/v01/api_decl.rs index bad5bf1095..577b45a06c 100644 --- a/core/bin/zksync_api/src/api_server/rest/v01/api_decl.rs +++ b/core/bin/zksync_api/src/api_server/rest/v01/api_decl.rs @@ -107,17 +107,6 @@ impl ApiV01 { "/withdrawal_processing_time", web::get().to(Self::withdrawal_processing_time), ) - .route( - "/forced_exit/enabled", - web::get().to(Self::is_forced_exit_enabled), - ) - .route( - "/forced_exit/register", - // All the other routes in this file are the `get` routes - // Unfortunately to preserve consistency would mean to the JSON-RPC - // api which is even greater evil - web::post().to(Self::register_forced_exit_request), - ) } pub(crate) async fn access_storage(&self) -> ActixResult> { diff --git a/core/bin/zksync_api/src/api_server/rest/v01/api_impl.rs b/core/bin/zksync_api/src/api_server/rest/v01/api_impl.rs index b8c0d6e4fe..c51c951669 100644 --- a/core/bin/zksync_api/src/api_server/rest/v01/api_impl.rs +++ b/core/bin/zksync_api/src/api_server/rest/v01/api_impl.rs @@ -507,102 +507,4 @@ impl ApiV01 { metrics::histogram!("api.v01.withdrawal_processing_time", start.elapsed()); ok_json!(processing_time) } - - pub async fn is_forced_exit_enabled(self_: web::Data) -> ActixResult { - let start = Instant::now(); - - let is_enabled = self_.config.forced_exit_requests.enabled; - let response = IsForcedExitEnabledResponse { - enabled: is_enabled, - }; - - metrics::histogram!("api.v01.is_forced_exit_enabled", start.elapsed()); - ok_json!(response) - } - - pub async fn register_forced_exit_request( - self_: web::Data, - params: web::Json, - ) -> JsonResult { - let start = Instant::now(); - - let time = Utc::now(); - - if !self_.config.forced_exit_requests.enabled { - return Err(ApiError::bad_request( - "ForcedExit requests feature is disabled!", - )); - } - - let mut storage = self_.access_storage().await.map_err(|err| { - vlog::warn!("Internal Server Error: '{}';", err); - return ApiError::internal(""); - })?; - - self_ - .forced_exit_checker - .check_forced_exit(&mut storage, params.target) - .await - .map_err(ApiError::from)?; - - let price = ticker_request( - self_.ticker_request_sender.clone(), - TxFeeTypes::Withdraw, - TokenLike::Id(0), - ) - .await - .map_err(ApiError::from)?; - let price = BigDecimal::from(price.total_fee.to_bigint().unwrap()); - - let scaling_coefficient = BigDecimal::from_str("1.6").unwrap(); - let scaled_price = price * scaling_coefficient; - - let user_price = params.price_in_wei.to_bigint().unwrap(); - let user_price = BigDecimal::from(user_price); - let user_scaling_coefficient = BigDecimal::from_str("1.05").unwrap(); - let user_scaled_fee = user_scaling_coefficient * user_price; - - if user_scaled_fee < scaled_price { - return Err(ApiError::bad_request("Not enough fee")); - } - - if params.tokens.len() > 10 { - return Err(ApiError::bad_request( - "Maximum number of tokens per FE request exceeded", - )); - } - - let mut tokens_schema = storage.tokens_schema(); - - for token_id in params.tokens.iter() { - // The result is going nowhere. - // This is simply to make sure that the tokens - // that were supplied do indeed exist - tokens_schema - .get_token(TokenLike::Id(*token_id)) - .await - .map_err(|e| { - return ApiError::bad_request("One of the tokens does no exist"); - })?; - } - - let mut fe_schema = storage.forced_exit_requests_schema(); - - let valid_until = Utc::now().add(Duration::weeks(1)); - - let saved_fe_request = fe_schema - .store_request(SaveForcedExitRequestQuery { - target: params.target, - tokens: params.tokens.clone(), - price_in_wei: params.price_in_wei.clone(), - valid_until, - }) - .await - .map_err(|e| { - return ApiError::internal(""); - })?; - - metrics::histogram!("api.v01.register_forced_exit_request", start.elapsed()); - Ok(Json(saved_fe_request)) - } } diff --git a/core/bin/zksync_api/src/api_server/rest/v01/types.rs b/core/bin/zksync_api/src/api_server/rest/v01/types.rs index c103ae8d79..66358812e3 100644 --- a/core/bin/zksync_api/src/api_server/rest/v01/types.rs +++ b/core/bin/zksync_api/src/api_server/rest/v01/types.rs @@ -3,8 +3,7 @@ use num::BigUint; use serde::{Deserialize, Serialize}; use zksync_types::TokenId; -use zksync_types::{Account, AccountId, Address}; -use zksync_utils::BigUintSerdeAsRadix10Str; +use zksync_types::{Account, AccountId}; #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] @@ -42,15 +41,3 @@ pub struct HandleBlocksQuery { pub struct BlockExplorerSearchQuery { pub query: String, } -#[derive(Serialize, Deserialize)] -pub struct IsForcedExitEnabledResponse { - pub enabled: bool, -} - -#[derive(Deserialize)] -pub struct ForcedExitRegisterRequest { - pub target: Address, - pub tokens: Vec, - #[serde(with = "BigUintSerdeAsRadix10Str")] - pub price_in_wei: BigUint, -} diff --git a/core/bin/zksync_api/src/api_server/rest/v1/forced_exit_requests.rs b/core/bin/zksync_api/src/api_server/rest/v1/forced_exit_requests.rs new file mode 100644 index 0000000000..c064c64410 --- /dev/null +++ b/core/bin/zksync_api/src/api_server/rest/v1/forced_exit_requests.rs @@ -0,0 +1,426 @@ +//! Transactions part of API implementation. + +// Built-in uses + +// External uses +use actix_web::{ + web::{self, Json}, + Scope, +}; + +use crate::{ + api_server::{ + forced_exit_checker::ForcedExitChecker, + helpers::try_parse_hash, + rest::{ + helpers::{deposit_op_to_tx_by_hash, parse_tx_id, priority_op_to_tx_history}, + v01::{api_decl::ApiV01, types::*}, + }, + tx_sender::ticker_request, + }, + fee_ticker::{Fee, TickerRequest}, +}; +use actix_web::{HttpResponse, Result as ActixResult}; +use std::ops::{Add, Mul}; + +use bigdecimal::BigDecimal; +use futures::{channel::mpsc, SinkExt, TryFutureExt}; +use num::{ + bigint::{ToBigInt, ToBigUint}, + BigUint, +}; + +use zksync_config::{test_config::unit_vectors::ForcedExit, ZkSyncConfig}; + +use chrono::{DateTime, Duration, Utc}; +use std::str::FromStr; +use std::time::Instant; +use zksync_storage::{chain::operations_ext::SearchDirection, ConnectionPool}; +use zksync_types::{ + misc::{ForcedExitRequest, SaveForcedExitRequestQuery}, + Address, BlockNumber, TokenId, TokenLike, TxFeeTypes, +}; + +// Workspace uses +pub use zksync_api_client::rest::v1::{ + FastProcessingQuery, IncomingTx, IncomingTxBatch, Receipt, TxData, +}; +use zksync_storage::{ + chain::operations_ext::records::TxReceiptResponse, QueryResult, StorageProcessor, +}; +use zksync_types::{tx::TxHash, SignedZkSyncTx}; + +// Local uses +use super::{Client, ClientError, Error as ApiError, JsonResult, Pagination, PaginationQuery}; +use crate::api_server::rpc_server::types::TxWithSignature; +use crate::api_server::tx_sender::{SubmitError, TxSender}; + +use serde::{Deserialize, Serialize}; +use zksync_utils::BigUintSerdeAsRadix10Str; + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ForcedExitRequestFee { + #[serde(with = "BigUintSerdeAsRadix10Str")] + pub request_fee: BigUint, +} + +#[derive(Serialize, Deserialize)] +pub struct IsForcedExitEnabledResponse { + pub enabled: bool, +} + +#[derive(Deserialize)] +pub struct ForcedExitRegisterRequest { + pub target: Address, + pub tokens: Vec, + #[serde(with = "BigUintSerdeAsRadix10Str")] + pub price_in_wei: BigUint, +} + +/// Shared data between `api/v1/transactions` endpoints. +#[derive(Clone)] +pub struct ApiForcedExitRequestsData { + pub(crate) connection_pool: ConnectionPool, + pub(crate) config: ZkSyncConfig, + pub(crate) forced_exit_checker: ForcedExitChecker, + pub(crate) ticker_request_sender: mpsc::Sender, +} + +impl ApiForcedExitRequestsData { + fn new( + connection_pool: ConnectionPool, + config: ZkSyncConfig, + ticker_request_sender: mpsc::Sender, + ) -> Self { + let forced_exit_checker = ForcedExitChecker::new(&config); + Self { + connection_pool, + config, + forced_exit_checker, + ticker_request_sender, + } + } + + async fn tx_receipt( + storage: &mut StorageProcessor<'_>, + tx_hash: TxHash, + ) -> QueryResult> { + storage + .chain() + .operations_ext_schema() + .tx_receipt(tx_hash.as_ref()) + .await + } +} + +// Server implementation + +async fn is_enabled( + data: web::Data, +) -> JsonResult { + let start = Instant::now(); + + let is_enabled = data.config.forced_exit_requests.enabled; + let response = IsForcedExitEnabledResponse { + enabled: is_enabled, + }; + + metrics::histogram!("api.v01.is_forced_exit_enabled", start.elapsed()); + Ok(Json(response)) +} + +async fn get_forced_exit_request_fee( + ticker_request_sender: mpsc::Sender, +) -> Result { + let price = ticker_request( + ticker_request_sender.clone(), + TxFeeTypes::Withdraw, + TokenLike::Id(0), + ) + .await?; + let price = BigDecimal::from(price.total_fee.to_bigint().unwrap()); + + let scaling_coefficient = BigDecimal::from_str("1.6").unwrap(); + let scaled_price = price * scaling_coefficient; + let scaled_price = scaled_price.round(0).to_bigint().unwrap(); + + Ok(scaled_price.to_biguint().unwrap()) +} + +async fn get_fee(data: web::Data) -> JsonResult { + let request_fee = get_forced_exit_request_fee(data.ticker_request_sender.clone()) + .await + .map_err(ApiError::from)?; + + Ok(Json(ForcedExitRequestFee { request_fee })) +} + +pub async fn submit_request( + data: web::Data, + params: web::Json, +) -> JsonResult { + let start = Instant::now(); + + if !data.config.forced_exit_requests.enabled { + return Err(ApiError::bad_request( + "ForcedExit requests feature is disabled!", + )); + } + + let mut storage = data.connection_pool.access_storage().await.map_err(|err| { + vlog::warn!("Internal Server Error: '{}';", err); + return ApiError::internal(""); + })?; + + data.forced_exit_checker + .check_forced_exit(&mut storage, params.target) + .await + .map_err(ApiError::from)?; + + let price = get_forced_exit_request_fee(data.ticker_request_sender.clone()) + .await + .map_err(ApiError::from)?; + let price = BigDecimal::from(price.to_bigint().unwrap()); + + let user_fee = params.price_in_wei.to_bigint().unwrap(); + let user_fee = BigDecimal::from(user_fee); + let user_scaling_coefficient = BigDecimal::from_str("1.05").unwrap(); + let user_scaled_fee = user_scaling_coefficient * user_fee; + + if user_scaled_fee < price { + return Err(ApiError::bad_request("Not enough fee")); + } + + if params.tokens.len() > 10 { + return Err(ApiError::bad_request( + "Maximum number of tokens per FE request exceeded", + )); + } + + let mut tokens_schema = storage.tokens_schema(); + + for token_id in params.tokens.iter() { + // The result is going nowhere. + // This is simply to make sure that the tokens + // that were supplied do indeed exist + tokens_schema + .get_token(TokenLike::Id(*token_id)) + .await + .map_err(|e| { + return ApiError::bad_request("One of the tokens does no exist"); + })?; + } + + let mut fe_schema = storage.forced_exit_requests_schema(); + + let valid_until = Utc::now().add(Duration::weeks(1)); + + let saved_fe_request = fe_schema + .store_request(SaveForcedExitRequestQuery { + target: params.target, + tokens: params.tokens.clone(), + price_in_wei: params.price_in_wei.clone(), + valid_until, + }) + .await + .map_err(|e| { + return ApiError::internal(""); + })?; + + metrics::histogram!("api.v01.register_forced_exit_request", start.elapsed()); + Ok(Json(saved_fe_request)) +} + +pub fn api_scope( + connection_pool: ConnectionPool, + config: &ZkSyncConfig, + ticker_request_sender: mpsc::Sender, +) -> Scope { + let data = + ApiForcedExitRequestsData::new(connection_pool, config.clone(), ticker_request_sender); + + web::scope("forced_exit") + .data(data) + .route("enabled", web::get().to(is_enabled)) + .route("submit", web::post().to(submit_request)) + .route("fee", web::get().to(get_fee)) +} + +//#[cfg(test)] +// mod tests { +// use actix_web::App; +// use bigdecimal::BigDecimal; +// use futures::{channel::mpsc, StreamExt}; +// use num::BigUint; + +// use zksync_api_client::rest::v1::Client; +// use zksync_storage::ConnectionPool; +// use zksync_test_account::ZkSyncAccount; +// use zksync_types::{ +// tokens::TokenLike, +// tx::{PackedEthSignature, TxEthSignature}, +// ZkSyncTx, +// }; + +// use crate::{ +// // api_server::helpers::try_parse_tx_hash, +// core_api_client::CoreApiClient, +// fee_ticker::{Fee, OutputFeeType::Withdraw, TickerRequest}, +// signature_checker::{VerifiedTx, VerifyTxSignatureRequest}, +// }; + +// use super::super::test_utils::{TestServerConfig, TestTransactions}; +// use super::*; + +// fn submit_txs_loopback() -> (CoreApiClient, actix_web::test::TestServer) { +// async fn send_tx(_tx: Json) -> Json> { +// Json(Ok(())) +// } + +// async fn send_txs_batch( +// _txs: Json<(Vec, Vec)>, +// ) -> Json> { +// Json(Ok(())) +// } + +// let server = actix_web::test::start(move || { +// App::new() +// .route("new_tx", web::post().to(send_tx)) +// .route("new_txs_batch", web::post().to(send_txs_batch)) +// }); + +// let url = server.url("").trim_end_matches('/').to_owned(); + +// (CoreApiClient::new(url), server) +// } + +// fn dummy_fee_ticker() -> mpsc::Sender { +// let (sender, mut receiver) = mpsc::channel(10); + +// actix_rt::spawn(async move { +// while let Some(item) = receiver.next().await { +// match item { +// TickerRequest::GetTxFee { response, .. } => { +// let fee = Ok(Fee::new( +// Withdraw, +// BigUint::from(1_u64).into(), +// BigUint::from(1_u64).into(), +// 1_u64.into(), +// 1_u64.into(), +// )); + +// response.send(fee).expect("Unable to send response"); +// } +// TickerRequest::GetTokenPrice { response, .. } => { +// let price = Ok(BigDecimal::from(1_u64)); + +// response.send(price).expect("Unable to send response"); +// } +// TickerRequest::IsTokenAllowed { token, response } => { +// // For test purposes, PHNX token is not allowed. +// let is_phnx = match token { +// TokenLike::Id(id) => id == 1, +// TokenLike::Symbol(sym) => sym == "PHNX", +// TokenLike::Address(_) => unreachable!(), +// }; +// response.send(Ok(!is_phnx)).unwrap_or_default(); +// } +// } +// } +// }); + +// sender +// } + +// fn dummy_sign_verifier() -> mpsc::Sender { +// let (sender, mut receiver) = mpsc::channel::(10); + +// actix_rt::spawn(async move { +// while let Some(item) = receiver.next().await { +// let verified = VerifiedTx::unverified(item.tx); +// item.response +// .send(Ok(verified)) +// .expect("Unable to send response"); +// } +// }); + +// sender +// } + +// struct TestServer { +// core_server: actix_web::test::TestServer, +// api_server: actix_web::test::TestServer, +// #[allow(dead_code)] +// pool: ConnectionPool, +// } + +// impl TestServer { +// async fn new() -> anyhow::Result<(Client, Self)> { +// let (core_client, core_server) = submit_txs_loopback(); + +// let cfg = TestServerConfig::default(); +// let pool = cfg.pool.clone(); +// cfg.fill_database().await?; + +// let sign_verifier = dummy_sign_verifier(); +// let fee_ticker = dummy_fee_ticker(); + +// let (api_client, api_server) = cfg.start_server(move |cfg| { +// api_scope(TxSender::with_client( +// core_client.clone(), +// cfg.pool.clone(), +// sign_verifier.clone(), +// fee_ticker.clone(), +// &cfg.config, +// )) +// }); + +// Ok(( +// api_client, +// Self { +// core_server, +// api_server, +// pool, +// }, +// )) +// } + +// async fn stop(self) { +// self.api_server.stop().await; +// self.core_server.stop().await; +// } +// } + +// #[actix_rt::test] +// #[cfg_attr( +// not(feature = "api_test"), +// ignore = "Use `zk test rust-api` command to perform this test" +// )] +// async fn test_submit_txs_loopback() -> anyhow::Result<()> { +// let (core_client, core_server) = submit_txs_loopback(); + +// let signed_tx = SignedZkSyncTx { +// tx: TestServerConfig::gen_zk_txs(0).txs[0].0.clone(), +// eth_sign_data: None, +// }; + +// core_client.send_tx(signed_tx.clone()).await??; +// core_client +// .send_txs_batch(vec![signed_tx], vec![]) +// .await??; + +// core_server.stop().await; +// Ok(()) +// } + +// #[actix_rt::test] +// #[cfg_attr( +// not(feature = "api_test"), +// ignore = "Use `zk test rust-api` command to perform this test" +// )] +// async fn test_transactions_scope() -> anyhow::Result<()> { +// todo!(); + +// } +// } diff --git a/core/bin/zksync_api/src/api_server/rest/v1/mod.rs b/core/bin/zksync_api/src/api_server/rest/v1/mod.rs index 64932359b8..ea20cb8d05 100644 --- a/core/bin/zksync_api/src/api_server/rest/v1/mod.rs +++ b/core/bin/zksync_api/src/api_server/rest/v1/mod.rs @@ -23,6 +23,7 @@ pub(crate) mod accounts; mod blocks; mod config; pub mod error; +mod forced_exit_requests; mod operations; mod search; #[cfg(test)] @@ -48,6 +49,11 @@ pub(crate) fn api_scope(tx_sender: TxSender, zk_config: &ZkSyncConfig) -> Scope .service(tokens::api_scope( tx_sender.pool.clone(), tx_sender.tokens, + tx_sender.ticker_requests.clone(), + )) + .service(forced_exit_requests::api_scope( + tx_sender.pool.clone(), + zk_config, tx_sender.ticker_requests, )) } diff --git a/infrastructure/zk/src/config.ts b/infrastructure/zk/src/config.ts index 649981bf21..52ce7d91bc 100644 --- a/infrastructure/zk/src/config.ts +++ b/infrastructure/zk/src/config.ts @@ -17,7 +17,8 @@ const CONFIG_FILES = [ 'misc.toml', 'prover.toml', 'rust.toml', - 'private.toml' + 'private.toml', + 'forced_exit_requests.toml' ]; async function getEnvironment(): Promise { From 9f34b6337c8922e848de6c7b9788cfc33aef00ca Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Wed, 3 Feb 2021 20:28:38 +0200 Subject: [PATCH 10/90] Forced exit params as env vars --- .../rest/v1/forced_exit_requests.rs | 47 ++++++++++++------- .../src/configs/forced_exit_requests.rs | 2 + etc/env/base/forced_exit_requests.toml | 9 ++++ 3 files changed, 41 insertions(+), 17 deletions(-) diff --git a/core/bin/zksync_api/src/api_server/rest/v1/forced_exit_requests.rs b/core/bin/zksync_api/src/api_server/rest/v1/forced_exit_requests.rs index c064c64410..a4b73c0839 100644 --- a/core/bin/zksync_api/src/api_server/rest/v1/forced_exit_requests.rs +++ b/core/bin/zksync_api/src/api_server/rest/v1/forced_exit_requests.rs @@ -23,7 +23,7 @@ use crate::{ use actix_web::{HttpResponse, Result as ActixResult}; use std::ops::{Add, Mul}; -use bigdecimal::BigDecimal; +use bigdecimal::{BigDecimal, FromPrimitive}; use futures::{channel::mpsc, SinkExt, TryFutureExt}; use num::{ bigint::{ToBigInt, ToBigUint}, @@ -82,23 +82,32 @@ pub struct ForcedExitRegisterRequest { #[derive(Clone)] pub struct ApiForcedExitRequestsData { pub(crate) connection_pool: ConnectionPool, - pub(crate) config: ZkSyncConfig, pub(crate) forced_exit_checker: ForcedExitChecker, pub(crate) ticker_request_sender: mpsc::Sender, + + pub(crate) is_enabled: bool, + pub(crate) price_scaling_factor: BigDecimal, + pub(crate) max_tokens: u8, } impl ApiForcedExitRequestsData { fn new( connection_pool: ConnectionPool, - config: ZkSyncConfig, + config: &ZkSyncConfig, ticker_request_sender: mpsc::Sender, ) -> Self { let forced_exit_checker = ForcedExitChecker::new(&config); Self { connection_pool, - config, forced_exit_checker, ticker_request_sender, + + is_enabled: config.forced_exit_requests.enabled, + price_scaling_factor: BigDecimal::from_f64( + config.forced_exit_requests.price_scaling_factor, + ) + .unwrap(), + max_tokens: config.forced_exit_requests.max_tokens, } } @@ -121,9 +130,8 @@ async fn is_enabled( ) -> JsonResult { let start = Instant::now(); - let is_enabled = data.config.forced_exit_requests.enabled; let response = IsForcedExitEnabledResponse { - enabled: is_enabled, + enabled: data.is_enabled, }; metrics::histogram!("api.v01.is_forced_exit_enabled", start.elapsed()); @@ -132,6 +140,7 @@ async fn is_enabled( async fn get_forced_exit_request_fee( ticker_request_sender: mpsc::Sender, + price_scaling_factor: BigDecimal, ) -> Result { let price = ticker_request( ticker_request_sender.clone(), @@ -141,17 +150,19 @@ async fn get_forced_exit_request_fee( .await?; let price = BigDecimal::from(price.total_fee.to_bigint().unwrap()); - let scaling_coefficient = BigDecimal::from_str("1.6").unwrap(); - let scaled_price = price * scaling_coefficient; + let scaled_price = price * price_scaling_factor; let scaled_price = scaled_price.round(0).to_bigint().unwrap(); Ok(scaled_price.to_biguint().unwrap()) } async fn get_fee(data: web::Data) -> JsonResult { - let request_fee = get_forced_exit_request_fee(data.ticker_request_sender.clone()) - .await - .map_err(ApiError::from)?; + let request_fee = get_forced_exit_request_fee( + data.ticker_request_sender.clone(), + data.price_scaling_factor.clone(), + ) + .await + .map_err(ApiError::from)?; Ok(Json(ForcedExitRequestFee { request_fee })) } @@ -162,7 +173,7 @@ pub async fn submit_request( ) -> JsonResult { let start = Instant::now(); - if !data.config.forced_exit_requests.enabled { + if !data.is_enabled { return Err(ApiError::bad_request( "ForcedExit requests feature is disabled!", )); @@ -178,9 +189,12 @@ pub async fn submit_request( .await .map_err(ApiError::from)?; - let price = get_forced_exit_request_fee(data.ticker_request_sender.clone()) - .await - .map_err(ApiError::from)?; + let price = get_forced_exit_request_fee( + data.ticker_request_sender.clone(), + data.price_scaling_factor.clone(), + ) + .await + .map_err(ApiError::from)?; let price = BigDecimal::from(price.to_bigint().unwrap()); let user_fee = params.price_in_wei.to_bigint().unwrap(); @@ -237,8 +251,7 @@ pub fn api_scope( config: &ZkSyncConfig, ticker_request_sender: mpsc::Sender, ) -> Scope { - let data = - ApiForcedExitRequestsData::new(connection_pool, config.clone(), ticker_request_sender); + let data = ApiForcedExitRequestsData::new(connection_pool, config, ticker_request_sender); web::scope("forced_exit") .data(data) diff --git a/core/lib/config/src/configs/forced_exit_requests.rs b/core/lib/config/src/configs/forced_exit_requests.rs index 79704155c3..7317a57e45 100644 --- a/core/lib/config/src/configs/forced_exit_requests.rs +++ b/core/lib/config/src/configs/forced_exit_requests.rs @@ -5,6 +5,8 @@ use serde::Deserialize; #[derive(Debug, Deserialize, Clone, PartialEq)] pub struct ForcedExitRequestsConfig { pub enabled: bool, + pub price_scaling_factor: f64, + pub max_tokens: u8, } impl ForcedExitRequestsConfig { diff --git a/etc/env/base/forced_exit_requests.toml b/etc/env/base/forced_exit_requests.toml index c11d7985b6..cd3966a63e 100644 --- a/etc/env/base/forced_exit_requests.toml +++ b/etc/env/base/forced_exit_requests.toml @@ -1,3 +1,12 @@ # Options for L1-based ForcedExit utility [forced_exit_requests] +# Whether the feature is enabled. Used to be able to quickly stop serving FE requests +# in times of attacks or upgrages enabled=true + +# The price that will be demanded from a user is equal to +# price_for_withdrawal * price_scaling +price_scaling_factor=1.6 + +# Maximum number of tokens in a request +max_tokens=10 From a2cf7cb145caeeea1535910df4bab30dc013ea62 Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Thu, 4 Feb 2021 04:15:03 +0200 Subject: [PATCH 11/90] Api tests for forced_exit_requests (without submit) --- Cargo.lock | 1 + .../rest/v1/forced_exit_requests.rs | 481 ++++++++++-------- .../src/api_server/rest/v1/test_utils.rs | 2 +- core/lib/api_client/Cargo.toml | 1 + .../src/rest/v1/forced_exit_requests.rs | 62 +++ core/lib/api_client/src/rest/v1/mod.rs | 4 + 6 files changed, 338 insertions(+), 213 deletions(-) create mode 100644 core/lib/api_client/src/rest/v1/forced_exit_requests.rs diff --git a/Cargo.lock b/Cargo.lock index 3bbfeb048c..f664249ea0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5681,6 +5681,7 @@ dependencies = [ "bigdecimal", "chrono", "hex", + "num", "reqwest", "serde", "serde_json", diff --git a/core/bin/zksync_api/src/api_server/rest/v1/forced_exit_requests.rs b/core/bin/zksync_api/src/api_server/rest/v1/forced_exit_requests.rs index a4b73c0839..4cc3992de3 100644 --- a/core/bin/zksync_api/src/api_server/rest/v1/forced_exit_requests.rs +++ b/core/bin/zksync_api/src/api_server/rest/v1/forced_exit_requests.rs @@ -56,28 +56,11 @@ use crate::api_server::rpc_server::types::TxWithSignature; use crate::api_server::tx_sender::{SubmitError, TxSender}; use serde::{Deserialize, Serialize}; +pub use zksync_api_client::rest::v1::{ + ForcedExitRegisterRequest, ForcedExitRequestFee, IsForcedExitEnabledResponse, +}; use zksync_utils::BigUintSerdeAsRadix10Str; -#[derive(Debug, Serialize, Deserialize, Clone)] -#[serde(rename_all = "camelCase")] -pub struct ForcedExitRequestFee { - #[serde(with = "BigUintSerdeAsRadix10Str")] - pub request_fee: BigUint, -} - -#[derive(Serialize, Deserialize)] -pub struct IsForcedExitEnabledResponse { - pub enabled: bool, -} - -#[derive(Deserialize)] -pub struct ForcedExitRegisterRequest { - pub target: Address, - pub tokens: Vec, - #[serde(with = "BigUintSerdeAsRadix10Str")] - pub price_in_wei: BigUint, -} - /// Shared data between `api/v1/transactions` endpoints. #[derive(Clone)] pub struct ApiForcedExitRequestsData { @@ -110,17 +93,6 @@ impl ApiForcedExitRequestsData { max_tokens: config.forced_exit_requests.max_tokens, } } - - async fn tx_receipt( - storage: &mut StorageProcessor<'_>, - tx_hash: TxHash, - ) -> QueryResult> { - storage - .chain() - .operations_ext_schema() - .tx_receipt(tx_hash.as_ref()) - .await - } } // Server implementation @@ -253,187 +225,272 @@ pub fn api_scope( ) -> Scope { let data = ApiForcedExitRequestsData::new(connection_pool, config, ticker_request_sender); - web::scope("forced_exit") + // `enabled` endpoint should always be there + let scope = web::scope("forced_exit") .data(data) - .route("enabled", web::get().to(is_enabled)) - .route("submit", web::post().to(submit_request)) - .route("fee", web::get().to(get_fee)) + .route("enabled", web::get().to(is_enabled)); + + if config.forced_exit_requests.enabled { + scope + .route("submit", web::post().to(submit_request)) + .route("fee", web::get().to(get_fee)) + } else { + scope + } } -//#[cfg(test)] -// mod tests { -// use actix_web::App; -// use bigdecimal::BigDecimal; -// use futures::{channel::mpsc, StreamExt}; -// use num::BigUint; - -// use zksync_api_client::rest::v1::Client; -// use zksync_storage::ConnectionPool; -// use zksync_test_account::ZkSyncAccount; -// use zksync_types::{ -// tokens::TokenLike, -// tx::{PackedEthSignature, TxEthSignature}, -// ZkSyncTx, -// }; - -// use crate::{ -// // api_server::helpers::try_parse_tx_hash, -// core_api_client::CoreApiClient, -// fee_ticker::{Fee, OutputFeeType::Withdraw, TickerRequest}, -// signature_checker::{VerifiedTx, VerifyTxSignatureRequest}, -// }; - -// use super::super::test_utils::{TestServerConfig, TestTransactions}; -// use super::*; - -// fn submit_txs_loopback() -> (CoreApiClient, actix_web::test::TestServer) { -// async fn send_tx(_tx: Json) -> Json> { -// Json(Ok(())) -// } - -// async fn send_txs_batch( -// _txs: Json<(Vec, Vec)>, -// ) -> Json> { -// Json(Ok(())) -// } - -// let server = actix_web::test::start(move || { -// App::new() -// .route("new_tx", web::post().to(send_tx)) -// .route("new_txs_batch", web::post().to(send_txs_batch)) -// }); - -// let url = server.url("").trim_end_matches('/').to_owned(); - -// (CoreApiClient::new(url), server) -// } - -// fn dummy_fee_ticker() -> mpsc::Sender { -// let (sender, mut receiver) = mpsc::channel(10); - -// actix_rt::spawn(async move { -// while let Some(item) = receiver.next().await { -// match item { -// TickerRequest::GetTxFee { response, .. } => { -// let fee = Ok(Fee::new( -// Withdraw, -// BigUint::from(1_u64).into(), -// BigUint::from(1_u64).into(), -// 1_u64.into(), -// 1_u64.into(), -// )); - -// response.send(fee).expect("Unable to send response"); -// } -// TickerRequest::GetTokenPrice { response, .. } => { -// let price = Ok(BigDecimal::from(1_u64)); - -// response.send(price).expect("Unable to send response"); -// } -// TickerRequest::IsTokenAllowed { token, response } => { -// // For test purposes, PHNX token is not allowed. -// let is_phnx = match token { -// TokenLike::Id(id) => id == 1, -// TokenLike::Symbol(sym) => sym == "PHNX", -// TokenLike::Address(_) => unreachable!(), -// }; -// response.send(Ok(!is_phnx)).unwrap_or_default(); -// } -// } -// } -// }); - -// sender -// } - -// fn dummy_sign_verifier() -> mpsc::Sender { -// let (sender, mut receiver) = mpsc::channel::(10); - -// actix_rt::spawn(async move { -// while let Some(item) = receiver.next().await { -// let verified = VerifiedTx::unverified(item.tx); -// item.response -// .send(Ok(verified)) -// .expect("Unable to send response"); -// } -// }); - -// sender -// } - -// struct TestServer { -// core_server: actix_web::test::TestServer, -// api_server: actix_web::test::TestServer, -// #[allow(dead_code)] -// pool: ConnectionPool, -// } - -// impl TestServer { -// async fn new() -> anyhow::Result<(Client, Self)> { -// let (core_client, core_server) = submit_txs_loopback(); - -// let cfg = TestServerConfig::default(); -// let pool = cfg.pool.clone(); -// cfg.fill_database().await?; - -// let sign_verifier = dummy_sign_verifier(); -// let fee_ticker = dummy_fee_ticker(); - -// let (api_client, api_server) = cfg.start_server(move |cfg| { -// api_scope(TxSender::with_client( -// core_client.clone(), -// cfg.pool.clone(), -// sign_verifier.clone(), -// fee_ticker.clone(), -// &cfg.config, -// )) -// }); - -// Ok(( -// api_client, -// Self { -// core_server, -// api_server, -// pool, -// }, -// )) -// } - -// async fn stop(self) { -// self.api_server.stop().await; -// self.core_server.stop().await; -// } -// } - -// #[actix_rt::test] -// #[cfg_attr( -// not(feature = "api_test"), -// ignore = "Use `zk test rust-api` command to perform this test" -// )] -// async fn test_submit_txs_loopback() -> anyhow::Result<()> { -// let (core_client, core_server) = submit_txs_loopback(); - -// let signed_tx = SignedZkSyncTx { -// tx: TestServerConfig::gen_zk_txs(0).txs[0].0.clone(), -// eth_sign_data: None, -// }; - -// core_client.send_tx(signed_tx.clone()).await??; -// core_client -// .send_txs_batch(vec![signed_tx], vec![]) -// .await??; - -// core_server.stop().await; -// Ok(()) -// } - -// #[actix_rt::test] -// #[cfg_attr( -// not(feature = "api_test"), -// ignore = "Use `zk test rust-api` command to perform this test" -// )] -// async fn test_transactions_scope() -> anyhow::Result<()> { -// todo!(); - -// } -// } +#[cfg(test)] +mod tests { + use actix_web::App; + use bigdecimal::BigDecimal; + use futures::{channel::mpsc, StreamExt}; + use num::BigUint; + + use crate::api_server::tx_sender::ticker_price_request; + use zksync_api_client::rest::v1::Client; + use zksync_config::{test_config::TestConfig, ForcedExitRequestsConfig}; + use zksync_storage::ConnectionPool; + use zksync_test_account::ZkSyncAccount; + use zksync_types::TxFeeTypes; + use zksync_types::{ + tokens::TokenLike, + tx::{PackedEthSignature, TxEthSignature}, + ZkSyncTx, + }; + + use crate::{ + // api_server::helpers::try_parse_tx_hash, + core_api_client::CoreApiClient, + fee_ticker::{Fee, OutputFeeType::Withdraw, TickerRequest}, + signature_checker::{VerifiedTx, VerifyTxSignatureRequest}, + }; + + use super::super::test_utils::{TestServerConfig, TestTransactions}; + use super::*; + + fn submit_txs_loopback() -> (CoreApiClient, actix_web::test::TestServer) { + async fn send_tx(_tx: Json) -> Json> { + Json(Ok(())) + } + + async fn send_txs_batch( + _txs: Json<(Vec, Vec)>, + ) -> Json> { + Json(Ok(())) + } + + let server = actix_web::test::start(move || { + App::new() + .route("new_tx", web::post().to(send_tx)) + .route("new_txs_batch", web::post().to(send_txs_batch)) + }); + + let url = server.url("").trim_end_matches('/').to_owned(); + + (CoreApiClient::new(url), server) + } + + fn dummy_fee_ticker(zkp_fee: Option, gas_fee: Option) -> mpsc::Sender { + let (sender, mut receiver) = mpsc::channel(10); + + let zkp_fee = zkp_fee.unwrap_or(1_u64); + let gas_fee = gas_fee.unwrap_or(1_u64); + + actix_rt::spawn(async move { + while let Some(item) = receiver.next().await { + match item { + TickerRequest::GetTxFee { response, .. } => { + let fee = Ok(Fee::new( + Withdraw, + BigUint::from(zkp_fee).into(), + BigUint::from(gas_fee).into(), + 1_u64.into(), + 1_u64.into(), + )); + + response.send(fee).expect("Unable to send response"); + } + TickerRequest::GetTokenPrice { response, .. } => { + let price = Ok(BigDecimal::from(1_u64)); + + response.send(price).expect("Unable to send response"); + } + TickerRequest::IsTokenAllowed { token, response } => { + // For test purposes, PHNX token is not allowed. + let is_phnx = match token { + TokenLike::Id(id) => id == 1, + TokenLike::Symbol(sym) => sym == "PHNX", + TokenLike::Address(_) => unreachable!(), + }; + response.send(Ok(!is_phnx)).unwrap_or_default(); + } + } + } + }); + + sender + } + + struct TestServer { + api_server: actix_web::test::TestServer, + #[allow(dead_code)] + pool: ConnectionPool, + fee_ticker: mpsc::Sender, + } + + impl TestServer { + async fn new() -> anyhow::Result<(Client, Self)> { + let cfg = TestServerConfig::default(); + + Self::new_with_config(cfg).await + } + + async fn new_with_config(cfg: TestServerConfig) -> anyhow::Result<(Client, Self)> { + let pool = cfg.pool.clone(); + + let fee_ticker = dummy_fee_ticker(None, None); + + let fee_ticker2 = fee_ticker.clone(); + let (api_client, api_server) = cfg.start_server(move |cfg| { + api_scope(cfg.pool.clone(), &cfg.config, fee_ticker2.clone()) + }); + + Ok(( + api_client, + Self { + api_server, + pool, + fee_ticker, + }, + )) + } + + async fn new_with_fee_ticker( + cfg: TestServerConfig, + gas_fee: Option, + zkp_fee: Option, + ) -> anyhow::Result<(Client, Self)> { + let pool = cfg.pool.clone(); + + let fee_ticker = dummy_fee_ticker(gas_fee, zkp_fee); + + let fee_ticker2 = fee_ticker.clone(); + let (api_client, api_server) = cfg.start_server(move |cfg| { + api_scope(cfg.pool.clone(), &cfg.config, fee_ticker2.clone()) + }); + + Ok(( + api_client, + Self { + api_server, + pool, + fee_ticker, + }, + )) + } + + async fn stop(self) { + self.api_server.stop().await; + } + } + + fn test_config_from_forced_exit_requests( + forced_exit_requests: ForcedExitRequestsConfig, + ) -> TestServerConfig { + let config_from_env = ZkSyncConfig::from_env(); + let config = ZkSyncConfig { + forced_exit_requests, + ..config_from_env + }; + + TestServerConfig { + config, + pool: ConnectionPool::new(Some(1)), + } + } + + #[actix_rt::test] + #[cfg_attr( + not(feature = "api_test"), + ignore = "Use `zk test rust-api` command to perform this test" + )] + async fn test_disabled_forced_exit_requests() -> anyhow::Result<()> { + let forced_exit_requests = ForcedExitRequestsConfig::from_env(); + let test_config = test_config_from_forced_exit_requests(ForcedExitRequestsConfig { + enabled: false, + ..forced_exit_requests + }); + + let (client, server) = TestServer::new_with_config(test_config).await?; + let enabled = client.is_forced_exit_enabled().await?.enabled; + + assert_eq!(enabled, false); + + let should_be_disabled_msg = "Forced-exit related requests don't fail when it's disabled"; + + client + .get_forced_exit_request_fee() + .await + .expect_err(should_be_disabled_msg); + + let register_request = ForcedExitRegisterRequest { + target: Address::from_str("c0f97CC918C9d6fA4E9fc6be61a6a06589D199b2").unwrap(), + tokens: vec![0], + price_in_wei: BigUint::from_str("1212").unwrap(), + }; + + client + .submit_forced_exit_request(register_request) + .await + .expect_err(should_be_disabled_msg); + + server.stop().await; + Ok(()) + } + + #[actix_rt::test] + #[cfg_attr( + not(feature = "api_test"), + ignore = "Use `zk test rust-api` command to perform this test" + )] + async fn test_forced_exit_requests_get_fee() -> anyhow::Result<()> { + let forced_exit_requests = ForcedExitRequestsConfig::from_env(); + let test_config = test_config_from_forced_exit_requests(ForcedExitRequestsConfig { + price_scaling_factor: 1.5, + ..forced_exit_requests + }); + + let (client, server) = + TestServer::new_with_fee_ticker(test_config, Some(10000), Some(10000)).await?; + + let enabled = client.is_forced_exit_enabled().await?.enabled; + assert_eq!(enabled, true); + + let fee = client.get_forced_exit_request_fee().await?.request_fee; + // 30000 = (10000 + 10000) * 1.5 + assert_eq!(fee, BigUint::from_u32(30000).unwrap()); + + server.stop().await; + Ok(()) + } + + // #[actix_rt::test] + // #[cfg_attr( + // not(feature = "api_test"), + // ignore = "Use `zk test rust-api` command to perform this test" + // )] + // async fn test_forced_exit_requests_submit() -> anyhow::Result<()> { + // let (client, server) = TestServer::new().await?; + + // let enabled = client.is_forced_exit_enabled().await?.enabled; + // assert_eq!(enabled, true); + + // let fee = client.get_forced_exit_request_fee().await?.request_fee; + + // let fe_request = ForcedExitRegisterRequest { + // target: "" + // }; + + // Ok(()) + // } +} diff --git a/core/bin/zksync_api/src/api_server/rest/v1/test_utils.rs b/core/bin/zksync_api/src/api_server/rest/v1/test_utils.rs index 8eb447c758..dd572f94e0 100644 --- a/core/bin/zksync_api/src/api_server/rest/v1/test_utils.rs +++ b/core/bin/zksync_api/src/api_server/rest/v1/test_utils.rs @@ -239,7 +239,7 @@ impl TestServerConfig { } pub async fn fill_database(&self) -> anyhow::Result<()> { - todo!(); + // todo!(); // static INITED: Lazy> = Lazy::new(|| Mutex::new(false)); diff --git a/core/lib/api_client/Cargo.toml b/core/lib/api_client/Cargo.toml index e7db1dde4e..91c12157b2 100644 --- a/core/lib/api_client/Cargo.toml +++ b/core/lib/api_client/Cargo.toml @@ -21,3 +21,4 @@ reqwest = { version = "0.10", features = ["blocking", "json"] } thiserror = "1.0" bigdecimal = { version = "0.2.0", features = ["serde"]} hex = "0.4" +num = { version = "0.3.1", features = ["serde"] } diff --git a/core/lib/api_client/src/rest/v1/forced_exit_requests.rs b/core/lib/api_client/src/rest/v1/forced_exit_requests.rs new file mode 100644 index 0000000000..2c68937567 --- /dev/null +++ b/core/lib/api_client/src/rest/v1/forced_exit_requests.rs @@ -0,0 +1,62 @@ +//! Blocks part of API implementation. + +// Built-in uses + +// External uses +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +// Workspace uses +use zksync_crypto::{serialization::FrSerde, Fr}; +use zksync_types::{tx::TxHash, Address, BlockNumber, TokenId}; +use zksync_utils::BigUintSerdeAsRadix10Str; + +use num::BigUint; + +// Local uses +use super::{ + client::{self, Client}, + Pagination, +}; + +// Data transfer objects. + +#[derive(Serialize, Deserialize)] +pub struct IsForcedExitEnabledResponse { + pub enabled: bool, +} +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ForcedExitRequestFee { + #[serde(with = "BigUintSerdeAsRadix10Str")] + pub request_fee: BigUint, +} + +#[derive(Deserialize, Serialize)] +pub struct ForcedExitRegisterRequest { + pub target: Address, + pub tokens: Vec, + #[serde(with = "BigUintSerdeAsRadix10Str")] + pub price_in_wei: BigUint, +} + +impl Client { + pub async fn is_forced_exit_enabled(&self) -> client::Result { + self.get("forced_exit/enabled").send().await + } + + pub async fn get_forced_exit_request_fee(&self) -> client::Result { + self.get("forced_exit/fee").send().await + } + + pub async fn submit_forced_exit_request( + &self, + regiter_request: ForcedExitRegisterRequest, + ) -> client::Result { + self.post("forced_exit/submit") + .body(®iter_request) + .send() + .await + } +} diff --git a/core/lib/api_client/src/rest/v1/mod.rs b/core/lib/api_client/src/rest/v1/mod.rs index 286e41367e..687b9450f0 100644 --- a/core/lib/api_client/src/rest/v1/mod.rs +++ b/core/lib/api_client/src/rest/v1/mod.rs @@ -12,6 +12,9 @@ pub use self::{ client::{Client, ClientError}, config::Contracts, error::ErrorBody, + forced_exit_requests::{ + ForcedExitRegisterRequest, ForcedExitRequestFee, IsForcedExitEnabledResponse, + }, operations::{PriorityOpData, PriorityOpQuery, PriorityOpQueryError, PriorityOpReceipt}, search::BlockSearchQuery, tokens::{TokenPriceKind, TokenPriceQuery}, @@ -24,6 +27,7 @@ mod blocks; mod client; mod config; mod error; +mod forced_exit_requests; mod operations; mod search; mod tokens; From 9ddcf53629119dcfbbe7d47861ad6461bc43b281 Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Thu, 4 Feb 2021 04:52:34 +0200 Subject: [PATCH 12/90] Small refacoring, remove unused vars --- .../src/api_server/rest/v01/api_decl.rs | 2 - .../src/api_server/rest/v01/api_impl.rs | 16 +-- .../src/api_server/rest/v01/types.rs | 2 - .../rest/v1/forced_exit_requests.rs | 109 +++++------------- .../zksync_api/src/api_server/tx_sender.rs | 1 - core/lib/storage/src/misc/records.rs | 5 +- 6 files changed, 31 insertions(+), 104 deletions(-) diff --git a/core/bin/zksync_api/src/api_server/rest/v01/api_decl.rs b/core/bin/zksync_api/src/api_server/rest/v01/api_decl.rs index 577b45a06c..1b33647b0a 100644 --- a/core/bin/zksync_api/src/api_server/rest/v01/api_decl.rs +++ b/core/bin/zksync_api/src/api_server/rest/v01/api_decl.rs @@ -5,11 +5,9 @@ use crate::{ api_server::rest::{ helpers::*, v01::{caches::Caches, network_status::SharedNetworkStatus}, - TxSender, }, core_api_client::{CoreApiClient, EthBlockId}, fee_ticker::TickerRequest, - signature_checker::VerifyTxSignatureRequest, }; use actix_web::{web, HttpResponse, Result as ActixResult}; use futures::channel::mpsc; diff --git a/core/bin/zksync_api/src/api_server/rest/v01/api_impl.rs b/core/bin/zksync_api/src/api_server/rest/v01/api_impl.rs index c51c951669..be7756aa09 100644 --- a/core/bin/zksync_api/src/api_server/rest/v01/api_impl.rs +++ b/core/bin/zksync_api/src/api_server/rest/v01/api_impl.rs @@ -9,27 +9,13 @@ use crate::api_server::{ rest::{ helpers::{deposit_op_to_tx_by_hash, parse_tx_id, priority_op_to_tx_history}, v01::{api_decl::ApiV01, types::*}, - v1::{ApiError, Error, JsonResult}, }, - tx_sender::ticker_request, }; use actix_web::{web, HttpResponse, Result as ActixResult}; -use std::ops::{Add, Mul}; -use bigdecimal::BigDecimal; -use futures::{SinkExt, TryFutureExt}; -use num::{bigint::ToBigInt, BigUint}; -use web::Json; -use zksync_config::test_config::unit_vectors::ForcedExit; - -use chrono::{DateTime, Duration, Utc}; -use std::str::FromStr; use std::time::Instant; use zksync_storage::chain::operations_ext::SearchDirection; -use zksync_types::{ - misc::{ForcedExitRequest, SaveForcedExitRequestQuery}, - Address, BlockNumber, TokenLike, TxFeeTypes, -}; +use zksync_types::{Address, BlockNumber}; /// Helper macro which wraps the serializable object into `Ok(HttpResponse::Ok().json(...))`. macro_rules! ok_json { diff --git a/core/bin/zksync_api/src/api_server/rest/v01/types.rs b/core/bin/zksync_api/src/api_server/rest/v01/types.rs index 66358812e3..40be453db4 100644 --- a/core/bin/zksync_api/src/api_server/rest/v01/types.rs +++ b/core/bin/zksync_api/src/api_server/rest/v01/types.rs @@ -1,8 +1,6 @@ //! Requests and responses used by the REST API. -use num::BigUint; use serde::{Deserialize, Serialize}; -use zksync_types::TokenId; use zksync_types::{Account, AccountId}; #[derive(Debug, Serialize)] diff --git a/core/bin/zksync_api/src/api_server/rest/v1/forced_exit_requests.rs b/core/bin/zksync_api/src/api_server/rest/v1/forced_exit_requests.rs index 4cc3992de3..2031c0947f 100644 --- a/core/bin/zksync_api/src/api_server/rest/v1/forced_exit_requests.rs +++ b/core/bin/zksync_api/src/api_server/rest/v1/forced_exit_requests.rs @@ -8,58 +8,36 @@ use actix_web::{ Scope, }; -use crate::{ - api_server::{ - forced_exit_checker::ForcedExitChecker, - helpers::try_parse_hash, - rest::{ - helpers::{deposit_op_to_tx_by_hash, parse_tx_id, priority_op_to_tx_history}, - v01::{api_decl::ApiV01, types::*}, - }, - tx_sender::ticker_request, - }, - fee_ticker::{Fee, TickerRequest}, -}; -use actix_web::{HttpResponse, Result as ActixResult}; -use std::ops::{Add, Mul}; - use bigdecimal::{BigDecimal, FromPrimitive}; -use futures::{channel::mpsc, SinkExt, TryFutureExt}; -use num::{ - bigint::{ToBigInt, ToBigUint}, - BigUint, -}; - -use zksync_config::{test_config::unit_vectors::ForcedExit, ZkSyncConfig}; - -use chrono::{DateTime, Duration, Utc}; +use chrono::{Duration, Utc}; +use futures::channel::mpsc; +use num::{bigint::ToBigInt, BigUint}; +use std::ops::Add; use std::str::FromStr; use std::time::Instant; -use zksync_storage::{chain::operations_ext::SearchDirection, ConnectionPool}; -use zksync_types::{ - misc::{ForcedExitRequest, SaveForcedExitRequestQuery}, - Address, BlockNumber, TokenId, TokenLike, TxFeeTypes, -}; // Workspace uses pub use zksync_api_client::rest::v1::{ - FastProcessingQuery, IncomingTx, IncomingTxBatch, Receipt, TxData, + FastProcessingQuery, ForcedExitRegisterRequest, ForcedExitRequestFee, IncomingTx, + IncomingTxBatch, IsForcedExitEnabledResponse, Receipt, TxData, }; -use zksync_storage::{ - chain::operations_ext::records::TxReceiptResponse, QueryResult, StorageProcessor, +use zksync_config::ZkSyncConfig; +use zksync_storage::ConnectionPool; +use zksync_types::{ + misc::{ForcedExitRequest, SaveForcedExitRequestQuery}, + TokenLike, TxFeeTypes, }; -use zksync_types::{tx::TxHash, SignedZkSyncTx}; // Local uses -use super::{Client, ClientError, Error as ApiError, JsonResult, Pagination, PaginationQuery}; -use crate::api_server::rpc_server::types::TxWithSignature; -use crate::api_server::tx_sender::{SubmitError, TxSender}; +use super::{Error as ApiError, JsonResult}; -use serde::{Deserialize, Serialize}; -pub use zksync_api_client::rest::v1::{ - ForcedExitRegisterRequest, ForcedExitRequestFee, IsForcedExitEnabledResponse, +use crate::{ + api_server::{ + forced_exit_checker::ForcedExitChecker, + tx_sender::{ticker_request, SubmitError}, + }, + fee_ticker::TickerRequest, }; -use zksync_utils::BigUintSerdeAsRadix10Str; /// Shared data between `api/v1/transactions` endpoints. #[derive(Clone)] @@ -193,7 +171,7 @@ pub async fn submit_request( tokens_schema .get_token(TokenLike::Id(*token_id)) .await - .map_err(|e| { + .map_err(|_| { return ApiError::bad_request("One of the tokens does no exist"); })?; } @@ -210,7 +188,7 @@ pub async fn submit_request( valid_until, }) .await - .map_err(|e| { + .map_err(|_| { return ApiError::internal(""); })?; @@ -241,55 +219,21 @@ pub fn api_scope( #[cfg(test)] mod tests { - use actix_web::App; use bigdecimal::BigDecimal; use futures::{channel::mpsc, StreamExt}; use num::BigUint; - use crate::api_server::tx_sender::ticker_price_request; use zksync_api_client::rest::v1::Client; - use zksync_config::{test_config::TestConfig, ForcedExitRequestsConfig}; + use zksync_config::ForcedExitRequestsConfig; use zksync_storage::ConnectionPool; - use zksync_test_account::ZkSyncAccount; - use zksync_types::TxFeeTypes; - use zksync_types::{ - tokens::TokenLike, - tx::{PackedEthSignature, TxEthSignature}, - ZkSyncTx, - }; + use zksync_types::tokens::TokenLike; + use zksync_types::Address; - use crate::{ - // api_server::helpers::try_parse_tx_hash, - core_api_client::CoreApiClient, - fee_ticker::{Fee, OutputFeeType::Withdraw, TickerRequest}, - signature_checker::{VerifiedTx, VerifyTxSignatureRequest}, - }; + use crate::fee_ticker::{Fee, OutputFeeType::Withdraw, TickerRequest}; - use super::super::test_utils::{TestServerConfig, TestTransactions}; + use super::super::test_utils::TestServerConfig; use super::*; - fn submit_txs_loopback() -> (CoreApiClient, actix_web::test::TestServer) { - async fn send_tx(_tx: Json) -> Json> { - Json(Ok(())) - } - - async fn send_txs_batch( - _txs: Json<(Vec, Vec)>, - ) -> Json> { - Json(Ok(())) - } - - let server = actix_web::test::start(move || { - App::new() - .route("new_tx", web::post().to(send_tx)) - .route("new_txs_batch", web::post().to(send_txs_batch)) - }); - - let url = server.url("").trim_end_matches('/').to_owned(); - - (CoreApiClient::new(url), server) - } - fn dummy_fee_ticker(zkp_fee: Option, gas_fee: Option) -> mpsc::Sender { let (sender, mut receiver) = mpsc::channel(10); @@ -335,10 +279,13 @@ mod tests { api_server: actix_web::test::TestServer, #[allow(dead_code)] pool: ConnectionPool, + #[allow(dead_code)] fee_ticker: mpsc::Sender, } impl TestServer { + // It should be used in the test for submitting requests + #[allow(dead_code)] async fn new() -> anyhow::Result<(Client, Self)> { let cfg = TestServerConfig::default(); diff --git a/core/bin/zksync_api/src/api_server/tx_sender.rs b/core/bin/zksync_api/src/api_server/tx_sender.rs index a6fc9bf403..1ffcc8c090 100644 --- a/core/bin/zksync_api/src/api_server/tx_sender.rs +++ b/core/bin/zksync_api/src/api_server/tx_sender.rs @@ -5,7 +5,6 @@ use std::{fmt::Display, str::FromStr}; // External uses use bigdecimal::BigDecimal; -use chrono::Utc; use futures::{ channel::{mpsc, oneshot}, prelude::*, diff --git a/core/lib/storage/src/misc/records.rs b/core/lib/storage/src/misc/records.rs index c50f2e2690..39fbb5b0e3 100644 --- a/core/lib/storage/src/misc/records.rs +++ b/core/lib/storage/src/misc/records.rs @@ -1,8 +1,7 @@ use crate::utils::{address_to_stored_string, stored_str_address_to_address}; use chrono::{DateTime, Utc}; -use num::{bigint::ToBigInt, BigInt, ToPrimitive}; -use sqlx::{types::BigDecimal, FromRow}; -use zksync_basic_types::Address; +use num::{bigint::ToBigInt, BigInt}; +use sqlx::types::BigDecimal; use zksync_types::misc::ForcedExitRequest; #[derive(Debug, Clone, sqlx::Type)] From 907df567c2a9c4263889704155cf4affa8ee7bf3 Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Thu, 4 Feb 2021 05:06:34 +0200 Subject: [PATCH 13/90] Refactored some names of the functions --- .../rest/v1/forced_exit_requests.rs | 22 ++++++++++--------- .../src/rest/v1/forced_exit_requests.rs | 20 +++++++---------- 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/core/bin/zksync_api/src/api_server/rest/v1/forced_exit_requests.rs b/core/bin/zksync_api/src/api_server/rest/v1/forced_exit_requests.rs index 2031c0947f..3c7147478d 100644 --- a/core/bin/zksync_api/src/api_server/rest/v1/forced_exit_requests.rs +++ b/core/bin/zksync_api/src/api_server/rest/v1/forced_exit_requests.rs @@ -84,11 +84,11 @@ async fn is_enabled( enabled: data.is_enabled, }; - metrics::histogram!("api.v01.is_forced_exit_enabled", start.elapsed()); + metrics::histogram!("api.v01.are_forced_exit_requests_enabled", start.elapsed()); Ok(Json(response)) } -async fn get_forced_exit_request_fee( +async fn get_fee_for_one_forced_exit( ticker_request_sender: mpsc::Sender, price_scaling_factor: BigDecimal, ) -> Result { @@ -106,8 +106,10 @@ async fn get_forced_exit_request_fee( Ok(scaled_price.to_biguint().unwrap()) } -async fn get_fee(data: web::Data) -> JsonResult { - let request_fee = get_forced_exit_request_fee( +async fn get_forced_exit_request_fee( + data: web::Data, +) -> JsonResult { + let request_fee = get_fee_for_one_forced_exit( data.ticker_request_sender.clone(), data.price_scaling_factor.clone(), ) @@ -139,7 +141,7 @@ pub async fn submit_request( .await .map_err(ApiError::from)?; - let price = get_forced_exit_request_fee( + let price = get_fee_for_one_forced_exit( data.ticker_request_sender.clone(), data.price_scaling_factor.clone(), ) @@ -204,14 +206,14 @@ pub fn api_scope( let data = ApiForcedExitRequestsData::new(connection_pool, config, ticker_request_sender); // `enabled` endpoint should always be there - let scope = web::scope("forced_exit") + let scope = web::scope("forced_exit_requests") .data(data) .route("enabled", web::get().to(is_enabled)); if config.forced_exit_requests.enabled { scope .route("submit", web::post().to(submit_request)) - .route("fee", web::get().to(get_fee)) + .route("fee", web::get().to(get_forced_exit_request_fee)) } else { scope } @@ -369,7 +371,7 @@ mod tests { }); let (client, server) = TestServer::new_with_config(test_config).await?; - let enabled = client.is_forced_exit_enabled().await?.enabled; + let enabled = client.are_forced_exit_requests_enabled().await?.enabled; assert_eq!(enabled, false); @@ -410,7 +412,7 @@ mod tests { let (client, server) = TestServer::new_with_fee_ticker(test_config, Some(10000), Some(10000)).await?; - let enabled = client.is_forced_exit_enabled().await?.enabled; + let enabled = client.are_forced_exit_requests_enabled().await?.enabled; assert_eq!(enabled, true); let fee = client.get_forced_exit_request_fee().await?.request_fee; @@ -429,7 +431,7 @@ mod tests { // async fn test_forced_exit_requests_submit() -> anyhow::Result<()> { // let (client, server) = TestServer::new().await?; - // let enabled = client.is_forced_exit_enabled().await?.enabled; + // let enabled = client.are_forced_exit_requests_enabled().await?.enabled; // assert_eq!(enabled, true); // let fee = client.get_forced_exit_request_fee().await?.request_fee; diff --git a/core/lib/api_client/src/rest/v1/forced_exit_requests.rs b/core/lib/api_client/src/rest/v1/forced_exit_requests.rs index 2c68937567..357276c716 100644 --- a/core/lib/api_client/src/rest/v1/forced_exit_requests.rs +++ b/core/lib/api_client/src/rest/v1/forced_exit_requests.rs @@ -3,22 +3,16 @@ // Built-in uses // External uses -use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; -use serde_json::Value; // Workspace uses -use zksync_crypto::{serialization::FrSerde, Fr}; -use zksync_types::{tx::TxHash, Address, BlockNumber, TokenId}; +use zksync_types::{tx::TxHash, Address, TokenId}; use zksync_utils::BigUintSerdeAsRadix10Str; use num::BigUint; // Local uses -use super::{ - client::{self, Client}, - Pagination, -}; +use super::client::{self, Client}; // Data transfer objects. @@ -42,19 +36,21 @@ pub struct ForcedExitRegisterRequest { } impl Client { - pub async fn is_forced_exit_enabled(&self) -> client::Result { - self.get("forced_exit/enabled").send().await + pub async fn are_forced_exit_requests_enabled( + &self, + ) -> client::Result { + self.get("forced_exit_requests/enabled").send().await } pub async fn get_forced_exit_request_fee(&self) -> client::Result { - self.get("forced_exit/fee").send().await + self.get("forced_exit_requests/fee").send().await } pub async fn submit_forced_exit_request( &self, regiter_request: ForcedExitRegisterRequest, ) -> client::Result { - self.post("forced_exit/submit") + self.post("forced_exit_requests/submit") .body(®iter_request) .send() .await From f88c77cc52fd1467ff99142e5b3659d674be031a Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Thu, 4 Feb 2021 05:27:29 +0200 Subject: [PATCH 14/90] Remove get_max_id and restore storage tests for ForcedExitRequests --- core/lib/storage/sqlx-data.json | 18 ------------ .../src/misc/forced_exit_requests_schema.rs | 14 --------- .../storage/src/tests/forced_exit_requests.rs | 29 ++++++++++++------- 3 files changed, 19 insertions(+), 42 deletions(-) diff --git a/core/lib/storage/sqlx-data.json b/core/lib/storage/sqlx-data.json index c4fbf04781..bf17720010 100644 --- a/core/lib/storage/sqlx-data.json +++ b/core/lib/storage/sqlx-data.json @@ -594,24 +594,6 @@ ] } }, - "1dfd4fafff703f719789690f88ba036e25bde60a69580e644ab24f2799249c99": { - "query": "SELECT MAX(id) FROM forced_exit_requests", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "max", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - null - ] - } - }, "222e3946401772e3f6e0d9ce9909e8e7ac2dc830c5ecfcd522f56b3bf70fd679": { "query": "INSERT INTO data_restore_storage_state_update (storage_state) VALUES ($1)", "describe": { diff --git a/core/lib/storage/src/misc/forced_exit_requests_schema.rs b/core/lib/storage/src/misc/forced_exit_requests_schema.rs index af57a39d6a..1c2c81ca75 100644 --- a/core/lib/storage/src/misc/forced_exit_requests_schema.rs +++ b/core/lib/storage/src/misc/forced_exit_requests_schema.rs @@ -77,18 +77,4 @@ impl<'a, 'c> ForcedExitRequestsSchema<'a, 'c> { Ok(request) } - - pub async fn get_max_used_id(&mut self) -> QueryResult { - let start = Instant::now(); - - let max_value: i64 = sqlx::query!(r#"SELECT MAX(id) FROM forced_exit_requests"#) - .fetch_one(self.0.conn()) - .await? - .max - .unwrap_or(0); - - metrics::histogram!("sql.forced_exit_requests.get_max_used_id", start.elapsed()); - - Ok(max_value) - } } diff --git a/core/lib/storage/src/tests/forced_exit_requests.rs b/core/lib/storage/src/tests/forced_exit_requests.rs index e52daed3f1..a0e3fa7108 100644 --- a/core/lib/storage/src/tests/forced_exit_requests.rs +++ b/core/lib/storage/src/tests/forced_exit_requests.rs @@ -6,31 +6,40 @@ use crate::QueryResult; use crate::StorageProcessor; use chrono::{DateTime, Timelike, Utc}; use num::{BigUint, FromPrimitive}; -use zksync_types::misc::ForcedExitRequest; +use zksync_basic_types::Address; +use zksync_types::misc::{ForcedExitRequest, SaveForcedExitRequestQuery}; #[db_test] async fn store_forced_exit_request(mut storage: StorageProcessor<'_>) -> QueryResult<()> { let now = Utc::now().with_nanosecond(0).unwrap(); - let request = ForcedExitRequest { - id: 1, - account_id: 12, + let request = SaveForcedExitRequestQuery { + target: Address::from_str("c0f97CC918C9d6fA4E9fc6be61a6a06589D199b2").unwrap(), tokens: vec![0], price_in_wei: BigUint::from_i32(121212).unwrap(), valid_until: DateTime::from(now), }; - ForcedExitRequestsSchema(&mut storage) - .store_request(&request) + let fe_request = ForcedExitRequestsSchema(&mut storage) + .store_request(request) .await?; - let fe = ForcedExitRequestsSchema(&mut storage) - .get_request_by_id(1) + assert_eq!(fe_request.id, 1); + + let expected_response = ForcedExitRequest { + id: 1, + target: Address::from_str("c0f97CC918C9d6fA4E9fc6be61a6a06589D199b2").unwrap(), + tokens: vec![0], + price_in_wei: BigUint::from_i32(121212).unwrap(), + valid_until: DateTime::from(now), + }; + + let response = ForcedExitRequestsSchema(&mut storage) + .get_request_by_id(fe_request.id) .await .expect("Failed to get forced exit by id"); - assert_eq!(request, fe); - + assert_eq!(expected_response, response); Ok(()) } From 578841eb6b9c72fd85420baf219b5f3b556d6a2c Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Thu, 4 Feb 2021 05:37:43 +0200 Subject: [PATCH 15/90] Take into account the number of the tokens to be withdrawn --- .../src/api_server/rest/v1/forced_exit_requests.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/core/bin/zksync_api/src/api_server/rest/v1/forced_exit_requests.rs b/core/bin/zksync_api/src/api_server/rest/v1/forced_exit_requests.rs index 3c7147478d..d601c898ae 100644 --- a/core/bin/zksync_api/src/api_server/rest/v1/forced_exit_requests.rs +++ b/core/bin/zksync_api/src/api_server/rest/v1/forced_exit_requests.rs @@ -141,20 +141,21 @@ pub async fn submit_request( .await .map_err(ApiError::from)?; - let price = get_fee_for_one_forced_exit( + let price_of_one_exit = get_fee_for_one_forced_exit( data.ticker_request_sender.clone(), data.price_scaling_factor.clone(), ) .await .map_err(ApiError::from)?; - let price = BigDecimal::from(price.to_bigint().unwrap()); + let price_of_one_exit = BigDecimal::from(price_of_one_exit.to_bigint().unwrap()); + let price_of_request = price_of_one_exit * BigDecimal::from_usize(params.tokens.len()).unwrap(); let user_fee = params.price_in_wei.to_bigint().unwrap(); let user_fee = BigDecimal::from(user_fee); let user_scaling_coefficient = BigDecimal::from_str("1.05").unwrap(); let user_scaled_fee = user_scaling_coefficient * user_fee; - if user_scaled_fee < price { + if user_scaled_fee < price_of_request { return Err(ApiError::bad_request("Not enough fee")); } From a28ae8894898170b2f30fd76d8c02d3525926dc7 Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Thu, 4 Feb 2021 11:49:17 +0200 Subject: [PATCH 16/90] fulfilled_at field for forced_exit_requests --- .../up.sql | 5 +++-- core/lib/storage/sqlx-data.json | 12 +++++++++--- .../mod.rs} | 12 ++++++++++-- .../{misc => forced_exit_requests}/records.rs | 19 +++++++++++++++---- .../storage/src/forced_exit_requests/utils.rs | 6 ++++++ core/lib/storage/src/lib.rs | 6 +++--- core/lib/storage/src/misc/mod.rs | 2 -- .../storage/src/tests/forced_exit_requests.rs | 3 ++- core/lib/types/src/misc.rs | 1 + 9 files changed, 49 insertions(+), 17 deletions(-) rename core/lib/storage/src/{misc/forced_exit_requests_schema.rs => forced_exit_requests/mod.rs} (93%) rename core/lib/storage/src/{misc => forced_exit_requests}/records.rs (75%) create mode 100644 core/lib/storage/src/forced_exit_requests/utils.rs delete mode 100644 core/lib/storage/src/misc/mod.rs diff --git a/core/lib/storage/migrations/2021-01-31-160151_forced_exit_requests/up.sql b/core/lib/storage/migrations/2021-01-31-160151_forced_exit_requests/up.sql index 0430dd27df..1f71696b1c 100644 --- a/core/lib/storage/migrations/2021-01-31-160151_forced_exit_requests/up.sql +++ b/core/lib/storage/migrations/2021-01-31-160151_forced_exit_requests/up.sql @@ -1,7 +1,8 @@ CREATE TABLE forced_exit_requests ( id BIGSERIAL PRIMARY KEY, target TEXT NOT NULL, - tokens INTEGER ARRAY NOT NULL, + tokens TEXT NOT NULL, price_in_wei NUMERIC NOT NULL, - valid_until TIMESTAMP with time zone NOT NULL + valid_until TIMESTAMP with time zone NOT NULL, + fulfilled_at TIMESTAMP with time zone ); diff --git a/core/lib/storage/sqlx-data.json b/core/lib/storage/sqlx-data.json index bf17720010..79181633b9 100644 --- a/core/lib/storage/sqlx-data.json +++ b/core/lib/storage/sqlx-data.json @@ -2091,7 +2091,7 @@ { "ordinal": 2, "name": "tokens", - "type_info": "Int4Array" + "type_info": "Text" }, { "ordinal": 3, @@ -2102,6 +2102,11 @@ "ordinal": 4, "name": "valid_until", "type_info": "Timestamptz" + }, + { + "ordinal": 5, + "name": "fulfilled_at", + "type_info": "Timestamptz" } ], "parameters": { @@ -2114,7 +2119,8 @@ false, false, false, - false + false, + true ] } }, @@ -3581,7 +3587,7 @@ "parameters": { "Left": [ "Text", - "Int4Array", + "Text", "Numeric", "Timestamptz" ] diff --git a/core/lib/storage/src/misc/forced_exit_requests_schema.rs b/core/lib/storage/src/forced_exit_requests/mod.rs similarity index 93% rename from core/lib/storage/src/misc/forced_exit_requests_schema.rs rename to core/lib/storage/src/forced_exit_requests/mod.rs index 1c2c81ca75..5e55922a0e 100644 --- a/core/lib/storage/src/misc/forced_exit_requests_schema.rs +++ b/core/lib/storage/src/forced_exit_requests/mod.rs @@ -8,7 +8,12 @@ use std::time::Instant; use crate::{QueryResult, StorageProcessor}; use zksync_types::misc::{ForcedExitRequest, ForcedExitRequestId, SaveForcedExitRequestQuery}; -use super::records::DbForcedExitRequest; +pub mod records; + +mod utils; + +use records::DbForcedExitRequest; + use crate::utils::address_to_stored_string; /// ForcedExitRequests schema handles the `forced_exit_requests` table, providing methods to @@ -24,7 +29,9 @@ impl<'a, 'c> ForcedExitRequestsSchema<'a, 'c> { let price_in_wei = BigDecimal::from(BigInt::from(request.price_in_wei.clone())); let target_str = address_to_stored_string(&request.target); - let tokens: Vec = request.tokens.iter().map(|t| *t as i32).collect(); + + let tokens = utils::tokens_vec_to_str(request.tokens.clone()); + let id: i64 = sqlx::query!( r#" INSERT INTO forced_exit_requests ( target, tokens, price_in_wei, valid_until ) @@ -47,6 +54,7 @@ impl<'a, 'c> ForcedExitRequestsSchema<'a, 'c> { tokens: request.tokens.clone(), price_in_wei: request.price_in_wei.clone(), valid_until: request.valid_until, + fulfilled_at: None, }) } diff --git a/core/lib/storage/src/misc/records.rs b/core/lib/storage/src/forced_exit_requests/records.rs similarity index 75% rename from core/lib/storage/src/misc/records.rs rename to core/lib/storage/src/forced_exit_requests/records.rs index 39fbb5b0e3..0247053a53 100644 --- a/core/lib/storage/src/misc/records.rs +++ b/core/lib/storage/src/forced_exit_requests/records.rs @@ -2,27 +2,33 @@ use crate::utils::{address_to_stored_string, stored_str_address_to_address}; use chrono::{DateTime, Utc}; use num::{bigint::ToBigInt, BigInt}; use sqlx::types::BigDecimal; +use zksync_basic_types::TokenId; use zksync_types::misc::ForcedExitRequest; -#[derive(Debug, Clone, sqlx::Type)] +use super::utils; + +#[derive(Debug, Clone)] pub struct DbForcedExitRequest { pub id: i64, pub target: String, - pub tokens: Vec, + pub tokens: String, pub price_in_wei: BigDecimal, pub valid_until: DateTime, + pub fulfilled_at: Option>, } impl From for DbForcedExitRequest { fn from(request: ForcedExitRequest) -> Self { let price_in_wei = BigDecimal::from(BigInt::from(request.price_in_wei.clone())); - let tokens: Vec = request.tokens.iter().map(|t| *t as i32).collect(); + + let tokens = utils::tokens_vec_to_str(request.tokens.clone()); Self { id: request.id, target: address_to_stored_string(&request.target), tokens: tokens, price_in_wei, valid_until: request.valid_until, + fulfilled_at: request.fulfilled_at, } } } @@ -38,7 +44,11 @@ impl Into for DbForcedExitRequest { // means that invalid data is stored in the DB .expect("Invalid forced exit request has been stored"); - let tokens: Vec = self.tokens.iter().map(|t| *t as u16).collect(); + let tokens: Vec = self + .tokens + .split(",") + .map(|num_str| num_str.parse().unwrap()) + .collect(); ForcedExitRequest { id: self.id, @@ -46,6 +56,7 @@ impl Into for DbForcedExitRequest { tokens, price_in_wei, valid_until: self.valid_until, + fulfilled_at: self.fulfilled_at, } } } diff --git a/core/lib/storage/src/forced_exit_requests/utils.rs b/core/lib/storage/src/forced_exit_requests/utils.rs new file mode 100644 index 0000000000..1fafc1b862 --- /dev/null +++ b/core/lib/storage/src/forced_exit_requests/utils.rs @@ -0,0 +1,6 @@ +use zksync_basic_types::TokenId; + +pub fn tokens_vec_to_str(token_ids: Vec) -> String { + let token_strings: Vec = token_ids.iter().map(|t| (*t).to_string()).collect(); + token_strings.join(",") +} diff --git a/core/lib/storage/src/lib.rs b/core/lib/storage/src/lib.rs index aa71f1863d..42df67f24f 100644 --- a/core/lib/storage/src/lib.rs +++ b/core/lib/storage/src/lib.rs @@ -72,8 +72,6 @@ // `sqlx` macros result in these warning being triggered. #![allow(clippy::toplevel_ref_arg, clippy::suspicious_else_formatting)] -use forced_exit_requests_schema::ForcedExitRequestsSchema; -use misc::forced_exit_requests_schema; // Built-in deps // use std::env; // External imports @@ -93,12 +91,14 @@ pub mod connection; pub mod data_restore; pub mod diff; pub mod ethereum; -pub mod misc; +pub mod forced_exit_requests; pub mod prover; pub mod test_data; pub mod tokens; mod utils; +use forced_exit_requests::ForcedExitRequestsSchema; + pub use crate::connection::ConnectionPool; pub type QueryResult = Result; diff --git a/core/lib/storage/src/misc/mod.rs b/core/lib/storage/src/misc/mod.rs deleted file mode 100644 index 5e30d8e9f1..0000000000 --- a/core/lib/storage/src/misc/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod forced_exit_requests_schema; -pub mod records; diff --git a/core/lib/storage/src/tests/forced_exit_requests.rs b/core/lib/storage/src/tests/forced_exit_requests.rs index a0e3fa7108..3f7b1daacc 100644 --- a/core/lib/storage/src/tests/forced_exit_requests.rs +++ b/core/lib/storage/src/tests/forced_exit_requests.rs @@ -1,6 +1,6 @@ use std::str::FromStr; -use crate::misc::forced_exit_requests_schema::ForcedExitRequestsSchema; +use crate::forced_exit_requests::ForcedExitRequestsSchema; use crate::tests::db_test; use crate::QueryResult; use crate::StorageProcessor; @@ -32,6 +32,7 @@ async fn store_forced_exit_request(mut storage: StorageProcessor<'_>) -> QueryRe tokens: vec![0], price_in_wei: BigUint::from_i32(121212).unwrap(), valid_until: DateTime::from(now), + fulfilled_at: None, }; let response = ForcedExitRequestsSchema(&mut storage) diff --git a/core/lib/types/src/misc.rs b/core/lib/types/src/misc.rs index 20d9bc5401..f012fe9e25 100644 --- a/core/lib/types/src/misc.rs +++ b/core/lib/types/src/misc.rs @@ -16,6 +16,7 @@ pub struct ForcedExitRequest { #[serde(with = "BigUintSerdeAsRadix10Str")] pub price_in_wei: BigUint, pub valid_until: DateTime, + pub fulfilled_at: Option>, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)] From 30e4d0952c8a4e45b2743a54977fab5f2787101f Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Thu, 4 Feb 2021 13:57:58 +0200 Subject: [PATCH 17/90] Tx interval params to env and minor refactoring --- .../rest/v1/forced_exit_requests.rs | 9 ++--- .../src/configs/forced_exit_requests.rs | 35 +++++++++++++++++-- .../storage/src/forced_exit_requests/mod.rs | 4 ++- .../src/forced_exit_requests/records.rs | 2 +- .../storage/src/tests/forced_exit_requests.rs | 2 +- .../src/{misc.rs => forced_exit_requests.rs} | 0 core/lib/types/src/lib.rs | 2 +- etc/env/base/forced_exit_requests.toml | 10 ++++-- sdk/zksync-crypto/src/lib.rs | 2 +- 9 files changed, 53 insertions(+), 13 deletions(-) rename core/lib/types/src/{misc.rs => forced_exit_requests.rs} (100%) diff --git a/core/bin/zksync_api/src/api_server/rest/v1/forced_exit_requests.rs b/core/bin/zksync_api/src/api_server/rest/v1/forced_exit_requests.rs index d601c898ae..d29f82158a 100644 --- a/core/bin/zksync_api/src/api_server/rest/v1/forced_exit_requests.rs +++ b/core/bin/zksync_api/src/api_server/rest/v1/forced_exit_requests.rs @@ -24,7 +24,7 @@ pub use zksync_api_client::rest::v1::{ use zksync_config::ZkSyncConfig; use zksync_storage::ConnectionPool; use zksync_types::{ - misc::{ForcedExitRequest, SaveForcedExitRequestQuery}, + forced_exit_requests::{ForcedExitRequest, SaveForcedExitRequestQuery}, TokenLike, TxFeeTypes, }; @@ -48,7 +48,8 @@ pub struct ApiForcedExitRequestsData { pub(crate) is_enabled: bool, pub(crate) price_scaling_factor: BigDecimal, - pub(crate) max_tokens: u8, + pub(crate) max_tokens_per_request: u8, + pub(crate) max_tx_interval_millisecs: u64, } impl ApiForcedExitRequestsData { @@ -68,7 +69,7 @@ impl ApiForcedExitRequestsData { config.forced_exit_requests.price_scaling_factor, ) .unwrap(), - max_tokens: config.forced_exit_requests.max_tokens, + max_tokens_per_request: config.forced_exit_requests.max_tokens_per_request, } } } @@ -181,7 +182,7 @@ pub async fn submit_request( let mut fe_schema = storage.forced_exit_requests_schema(); - let valid_until = Utc::now().add(Duration::weeks(1)); + let valid_until = Utc::now().add(Duration::from_millis(self.max_tx_interval_millisecs)); let saved_fe_request = fe_schema .store_request(SaveForcedExitRequestQuery { diff --git a/core/lib/config/src/configs/forced_exit_requests.rs b/core/lib/config/src/configs/forced_exit_requests.rs index 7317a57e45..a4769b8bdb 100644 --- a/core/lib/config/src/configs/forced_exit_requests.rs +++ b/core/lib/config/src/configs/forced_exit_requests.rs @@ -2,15 +2,46 @@ use crate::envy_load; /// External uses use serde::Deserialize; +// There are two types of configs: +// The original one (with tx_interval_scaling_factor) +// And the public one (with max_tx_interval) + +// It's easier for humans to think in factors +// But the rest of the codebase does not +// really care about the factor, it only needs the max_tx_interval + +#[derive(Debug, Deserialize, Clone, PartialEq)] +struct ForcedExitRequestsInternalConfig { + pub enabled: bool, + pub price_scaling_factor: f64, + pub max_tokens_per_request: u8, + pub recomended_tx_interval: u64, + pub tx_interval_scaling_factor: f64, +} + #[derive(Debug, Deserialize, Clone, PartialEq)] pub struct ForcedExitRequestsConfig { pub enabled: bool, pub price_scaling_factor: f64, - pub max_tokens: u8, + pub max_tokens_per_request: u8, + pub recomended_tx_interval: u64, + pub max_tx_interval: u64, } impl ForcedExitRequestsConfig { pub fn from_env() -> Self { - envy_load!("forced_exit_requests", "FORCED_EXIT_REQUESTS_") + let config: ForcedExitRequestsInternalConfig = + envy_load!("forced_exit_requests", "FORCED_EXIT_REQUESTS_"); + + let max_tx_interval: f64 = + (config.recomended_tx_interval as f64) * config.tx_interval_scaling_factor; + + ForcedExitRequestsConfig { + enabled: config.enabled, + price_scaling_factor: config.price_scaling_factor, + max_tokens_per_request: config.max_tokens_per_request, + recomended_tx_interval: config.recomended_tx_interval, + max_tx_interval: max_tx_interval.round() as u64, + } } } diff --git a/core/lib/storage/src/forced_exit_requests/mod.rs b/core/lib/storage/src/forced_exit_requests/mod.rs index 5e55922a0e..1720aef671 100644 --- a/core/lib/storage/src/forced_exit_requests/mod.rs +++ b/core/lib/storage/src/forced_exit_requests/mod.rs @@ -6,7 +6,9 @@ use std::time::Instant; // Workspace imports // Local imports use crate::{QueryResult, StorageProcessor}; -use zksync_types::misc::{ForcedExitRequest, ForcedExitRequestId, SaveForcedExitRequestQuery}; +use zksync_types::forced_exit_requests::{ + ForcedExitRequest, ForcedExitRequestId, SaveForcedExitRequestQuery, +}; pub mod records; diff --git a/core/lib/storage/src/forced_exit_requests/records.rs b/core/lib/storage/src/forced_exit_requests/records.rs index 0247053a53..1e07e1c5f6 100644 --- a/core/lib/storage/src/forced_exit_requests/records.rs +++ b/core/lib/storage/src/forced_exit_requests/records.rs @@ -3,7 +3,7 @@ use chrono::{DateTime, Utc}; use num::{bigint::ToBigInt, BigInt}; use sqlx::types::BigDecimal; use zksync_basic_types::TokenId; -use zksync_types::misc::ForcedExitRequest; +use zksync_types::forced_exit_requests::ForcedExitRequest; use super::utils; diff --git a/core/lib/storage/src/tests/forced_exit_requests.rs b/core/lib/storage/src/tests/forced_exit_requests.rs index 3f7b1daacc..00b13b7558 100644 --- a/core/lib/storage/src/tests/forced_exit_requests.rs +++ b/core/lib/storage/src/tests/forced_exit_requests.rs @@ -7,7 +7,7 @@ use crate::StorageProcessor; use chrono::{DateTime, Timelike, Utc}; use num::{BigUint, FromPrimitive}; use zksync_basic_types::Address; -use zksync_types::misc::{ForcedExitRequest, SaveForcedExitRequestQuery}; +use zksync_types::forced_exit_requests::{ForcedExitRequest, SaveForcedExitRequestQuery}; #[db_test] async fn store_forced_exit_request(mut storage: StorageProcessor<'_>) -> QueryResult<()> { diff --git a/core/lib/types/src/misc.rs b/core/lib/types/src/forced_exit_requests.rs similarity index 100% rename from core/lib/types/src/misc.rs rename to core/lib/types/src/forced_exit_requests.rs diff --git a/core/lib/types/src/lib.rs b/core/lib/types/src/lib.rs index 0a1365bb21..68f8681dab 100644 --- a/core/lib/types/src/lib.rs +++ b/core/lib/types/src/lib.rs @@ -41,10 +41,10 @@ pub mod aggregated_operations; pub mod block; pub mod config; pub mod ethereum; +pub mod forced_exit_requests; pub mod gas_counter; pub mod helpers; pub mod mempool; -pub mod misc; pub mod network; pub mod operations; pub mod priority_ops; diff --git a/etc/env/base/forced_exit_requests.toml b/etc/env/base/forced_exit_requests.toml index cd3966a63e..5ef8fc5f17 100644 --- a/etc/env/base/forced_exit_requests.toml +++ b/etc/env/base/forced_exit_requests.toml @@ -8,5 +8,11 @@ enabled=true # price_for_withdrawal * price_scaling price_scaling_factor=1.6 -# Maximum number of tokens in a request -max_tokens=10 +max_tokens_per_request=10 + +# Recommended interval to send the transaction in milliseconds +recomended_tx_interval=300 + +# How many times the maximum acceptable interval will be longer +# than the recommended interval +tx_interval_scaling_factor=1.5 diff --git a/sdk/zksync-crypto/src/lib.rs b/sdk/zksync-crypto/src/lib.rs index 75a9032c48..bea80328ee 100644 --- a/sdk/zksync-crypto/src/lib.rs +++ b/sdk/zksync-crypto/src/lib.rs @@ -27,7 +27,7 @@ use franklin_crypto::{ jubjub::JubjubEngine, }; -use crate::utils::{self, set_panic_hook}; +use crate::utils::{set_panic_hook}; use sha2::{Digest, Sha256}; // When the `wee_alloc` feature is enabled, use `wee_alloc` as the global From d090928db0f603ef6cfdd1c84eaff0a8b2e68f03 Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Thu, 4 Feb 2021 19:58:47 +0200 Subject: [PATCH 18/90] Move forced_exit_requests to their own scope --- .../rest/forced_exit_requests/mod.rs | 59 ++ .../rest/forced_exit_requests/v01.rs | 453 +++++++++ .../bin/zksync_api/src/api_server/rest/mod.rs | 8 + .../rest/v1/forced_exit_requests.rs | 894 +++++++++--------- .../zksync_api/src/api_server/rest/v1/mod.rs | 12 +- .../src/api_server/rest/v1/test_utils.rs | 15 +- .../mod.rs} | 40 +- core/lib/api_client/src/rest/mod.rs | 1 + core/lib/api_client/src/rest/v1/client.rs | 26 +- core/lib/api_client/src/rest/v1/mod.rs | 6 +- .../src/configs/forced_exit_requests.rs | 8 +- 11 files changed, 1035 insertions(+), 487 deletions(-) create mode 100644 core/bin/zksync_api/src/api_server/rest/forced_exit_requests/mod.rs create mode 100644 core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs rename core/lib/api_client/src/rest/{v1/forced_exit_requests.rs => forced_exit_requests/mod.rs} (50%) diff --git a/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/mod.rs b/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/mod.rs new file mode 100644 index 0000000000..ec3fd958fe --- /dev/null +++ b/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/mod.rs @@ -0,0 +1,59 @@ +//! First stable API implementation. + +// External uses +use actix_web::{ + web::{self, Json}, + Scope, +}; + +// Workspace uses +pub use zksync_api_client::rest::v1::{ + Client, ClientError, Pagination, PaginationQuery, MAX_LIMIT, +}; +use zksync_config::ZkSyncConfig; +use zksync_storage::ConnectionPool; + +// Local uses +use crate::api_server::tx_sender::TxSender; + +use bigdecimal::{BigDecimal, FromPrimitive}; +use chrono::{Duration, Utc}; +use futures::channel::mpsc; +use num::{bigint::ToBigInt, BigUint}; +use std::ops::Add; +use std::str::FromStr; +use std::time::Instant; + +// Workspace uses +pub use zksync_api_client::rest::v1::{ + FastProcessingQuery, IncomingTx, IncomingTxBatch, Receipt, TxData, +}; +use zksync_types::{ + forced_exit_requests::{ForcedExitRequest, SaveForcedExitRequestQuery}, + TokenLike, TxFeeTypes, +}; + +// Local uses +use crate::api_server::rest::v1::{Error as ApiError, JsonResult}; + +use crate::{ + api_server::{ + forced_exit_checker::ForcedExitChecker, + tx_sender::{ticker_request, SubmitError}, + }, + fee_ticker::TickerRequest, +}; + +mod v01; + +pub(crate) fn api_scope( + connection_pool: ConnectionPool, + config: &ZkSyncConfig, + ticker_request_sender: mpsc::Sender, +) -> Scope { + web::scope("/api/forced_exit_requests").service(v01::api_scope( + connection_pool, + config, + ticker_request_sender, + )) +} diff --git a/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs b/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs new file mode 100644 index 0000000000..8969736dca --- /dev/null +++ b/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs @@ -0,0 +1,453 @@ +//! Transactions part of API implementation. + +// Built-in uses + +// External uses +use actix_web::{ + web::{self, Json}, + Scope, +}; + +use bigdecimal::{BigDecimal, FromPrimitive}; +use chrono::{Duration, Utc}; +use futures::channel::mpsc; +use num::{bigint::ToBigInt, BigUint}; +use std::ops::Add; +use std::str::FromStr; +use std::time::Instant; +use zksync_api_client::rest::forced_exit_requests::ConfigInfo; + +// Workspace uses +pub use zksync_api_client::rest::forced_exit_requests::{ + ForcedExitRegisterRequest, ForcedExitRequestStatus, +}; +pub use zksync_api_client::rest::v1::{ + FastProcessingQuery, IncomingTx, IncomingTxBatch, Receipt, TxData, +}; + +use zksync_config::ZkSyncConfig; +use zksync_storage::ConnectionPool; +use zksync_types::{ + forced_exit_requests::{ForcedExitRequest, SaveForcedExitRequestQuery}, + TokenLike, TxFeeTypes, +}; + +// Local uses +use crate::api_server::rest::v1::{Error as ApiError, JsonResult}; + +use crate::{ + api_server::{ + forced_exit_checker::ForcedExitChecker, + tx_sender::{ticker_request, SubmitError}, + }, + fee_ticker::TickerRequest, +}; + +/// Shared data between `api/v1/transactions` endpoints. +#[derive(Clone)] +pub struct ApiForcedExitRequestsData { + pub(crate) connection_pool: ConnectionPool, + pub(crate) forced_exit_checker: ForcedExitChecker, + pub(crate) ticker_request_sender: mpsc::Sender, + + pub(crate) is_enabled: bool, + pub(crate) price_scaling_factor: BigDecimal, + pub(crate) max_tokens_per_request: u8, + pub(crate) recomended_tx_interval_millisecs: i64, + pub(crate) max_tx_interval_millisecs: i64, +} + +impl ApiForcedExitRequestsData { + fn new( + connection_pool: ConnectionPool, + config: &ZkSyncConfig, + ticker_request_sender: mpsc::Sender, + ) -> Self { + let forced_exit_checker = ForcedExitChecker::new(&config); + Self { + connection_pool, + forced_exit_checker, + ticker_request_sender, + + is_enabled: config.forced_exit_requests.enabled, + price_scaling_factor: BigDecimal::from_f64( + config.forced_exit_requests.price_scaling_factor, + ) + .unwrap(), + max_tokens_per_request: config.forced_exit_requests.max_tokens_per_request, + recomended_tx_interval_millisecs: config.forced_exit_requests.recomended_tx_interval, + max_tx_interval_millisecs: config.forced_exit_requests.max_tx_interval, + } + } +} + +// Server implementation + +async fn get_status( + data: web::Data, +) -> JsonResult { + let start = Instant::now(); + + let response = if data.is_enabled { + ForcedExitRequestStatus::Enabled(ConfigInfo { + request_fee: get_fee_for_one_forced_exit( + data.ticker_request_sender.clone(), + data.price_scaling_factor.clone(), + ) + .await?, + max_tokens_per_request: data.max_tokens_per_request, + recomended_tx_interval_millis: data.recomended_tx_interval_millisecs, + }) + } else { + ForcedExitRequestStatus::Disabled + }; + + metrics::histogram!("api.forced_exit_requests.v01.status", start.elapsed()); + Ok(Json(response)) +} + +async fn get_fee_for_one_forced_exit( + ticker_request_sender: mpsc::Sender, + price_scaling_factor: BigDecimal, +) -> Result { + let price = ticker_request( + ticker_request_sender.clone(), + TxFeeTypes::Withdraw, + TokenLike::Id(0), + ) + .await?; + let price = BigDecimal::from(price.total_fee.to_bigint().unwrap()); + + let scaled_price = price * price_scaling_factor; + let scaled_price = scaled_price.round(0).to_bigint().unwrap(); + + Ok(scaled_price.to_biguint().unwrap()) +} + +pub async fn submit_request( + data: web::Data, + params: web::Json, +) -> JsonResult { + let start = Instant::now(); + + if !data.is_enabled { + return Err(ApiError::bad_request( + "ForcedExit requests feature is disabled!", + )); + } + + let mut storage = data.connection_pool.access_storage().await.map_err(|err| { + vlog::warn!("Internal Server Error: '{}';", err); + return ApiError::internal(""); + })?; + + data.forced_exit_checker + .check_forced_exit(&mut storage, params.target) + .await + .map_err(ApiError::from)?; + + let price_of_one_exit = get_fee_for_one_forced_exit( + data.ticker_request_sender.clone(), + data.price_scaling_factor.clone(), + ) + .await + .map_err(ApiError::from)?; + let price_of_one_exit = BigDecimal::from(price_of_one_exit.to_bigint().unwrap()); + let price_of_request = price_of_one_exit * BigDecimal::from_usize(params.tokens.len()).unwrap(); + + let user_fee = params.price_in_wei.to_bigint().unwrap(); + let user_fee = BigDecimal::from(user_fee); + let user_scaling_coefficient = BigDecimal::from_str("1.05").unwrap(); + let user_scaled_fee = user_scaling_coefficient * user_fee; + + if user_scaled_fee < price_of_request { + return Err(ApiError::bad_request("Not enough fee")); + } + + if params.tokens.len() > data.max_tokens_per_request as usize { + return Err(ApiError::bad_request( + "Maximum number of tokens per FE request exceeded", + )); + } + + let mut tokens_schema = storage.tokens_schema(); + + for token_id in params.tokens.iter() { + // The result is going nowhere. + // This is simply to make sure that the tokens + // that were supplied do indeed exist + tokens_schema + .get_token(TokenLike::Id(*token_id)) + .await + .map_err(|_| { + return ApiError::bad_request("One of the tokens does no exist"); + })?; + } + + let mut fe_schema = storage.forced_exit_requests_schema(); + + let valid_until = Utc::now().add(Duration::milliseconds(data.max_tx_interval_millisecs)); + + let saved_fe_request = fe_schema + .store_request(SaveForcedExitRequestQuery { + target: params.target, + tokens: params.tokens.clone(), + price_in_wei: params.price_in_wei.clone(), + valid_until, + }) + .await + .map_err(|_| { + return ApiError::internal(""); + })?; + + metrics::histogram!("api.v01.register_forced_exit_request", start.elapsed()); + Ok(Json(saved_fe_request)) +} + +pub fn api_scope( + connection_pool: ConnectionPool, + config: &ZkSyncConfig, + ticker_request_sender: mpsc::Sender, +) -> Scope { + let data = ApiForcedExitRequestsData::new(connection_pool, config, ticker_request_sender); + + // `enabled` endpoint should always be there + let scope = web::scope("v0.1") + .data(data) + .route("status", web::get().to(get_status)); + + if config.forced_exit_requests.enabled { + scope.route("submit", web::post().to(submit_request)) + } else { + scope + } +} + +#[cfg(test)] +mod tests { + use bigdecimal::BigDecimal; + use futures::{channel::mpsc, StreamExt}; + use num::BigUint; + + use zksync_api_client::rest::v1::Client; + use zksync_config::ForcedExitRequestsConfig; + use zksync_storage::ConnectionPool; + use zksync_types::tokens::TokenLike; + use zksync_types::Address; + + use crate::fee_ticker::{Fee, OutputFeeType::Withdraw, TickerRequest}; + + use super::*; + use crate::api_server::v1::test_utils::TestServerConfig; + + fn dummy_fee_ticker(zkp_fee: Option, gas_fee: Option) -> mpsc::Sender { + let (sender, mut receiver) = mpsc::channel(10); + + let zkp_fee = zkp_fee.unwrap_or(1_u64); + let gas_fee = gas_fee.unwrap_or(1_u64); + + actix_rt::spawn(async move { + while let Some(item) = receiver.next().await { + match item { + TickerRequest::GetTxFee { response, .. } => { + let fee = Ok(Fee::new( + Withdraw, + BigUint::from(zkp_fee).into(), + BigUint::from(gas_fee).into(), + 1_u64.into(), + 1_u64.into(), + )); + + response.send(fee).expect("Unable to send response"); + } + TickerRequest::GetTokenPrice { response, .. } => { + let price = Ok(BigDecimal::from(1_u64)); + + response.send(price).expect("Unable to send response"); + } + TickerRequest::IsTokenAllowed { token, response } => { + // For test purposes, PHNX token is not allowed. + let is_phnx = match token { + TokenLike::Id(id) => id == 1, + TokenLike::Symbol(sym) => sym == "PHNX", + TokenLike::Address(_) => unreachable!(), + }; + response.send(Ok(!is_phnx)).unwrap_or_default(); + } + } + } + }); + + sender + } + + struct TestServer { + api_server: actix_web::test::TestServer, + #[allow(dead_code)] + pool: ConnectionPool, + #[allow(dead_code)] + fee_ticker: mpsc::Sender, + } + + impl TestServer { + // It should be used in the test for submitting requests + #[allow(dead_code)] + async fn new() -> anyhow::Result<(Client, Self)> { + let cfg = TestServerConfig::default(); + + Self::new_with_config(cfg).await + } + + async fn new_with_config(cfg: TestServerConfig) -> anyhow::Result<(Client, Self)> { + let pool = cfg.pool.clone(); + + let fee_ticker = dummy_fee_ticker(None, None); + + let fee_ticker2 = fee_ticker.clone(); + let (api_client, api_server) = cfg + .start_server_with_scope(String::from("api/forced_exit_requests"), move |cfg| { + api_scope(cfg.pool.clone(), &cfg.config, fee_ticker2.clone()) + }); + + Ok(( + api_client, + Self { + api_server, + pool, + fee_ticker, + }, + )) + } + + async fn new_with_fee_ticker( + cfg: TestServerConfig, + gas_fee: Option, + zkp_fee: Option, + ) -> anyhow::Result<(Client, Self)> { + let pool = cfg.pool.clone(); + + let fee_ticker = dummy_fee_ticker(gas_fee, zkp_fee); + + let fee_ticker2 = fee_ticker.clone(); + let (api_client, api_server) = cfg + .start_server_with_scope(String::from("/api/forced_exit_requests"), move |cfg| { + api_scope(cfg.pool.clone(), &cfg.config, fee_ticker2.clone()) + }); + + Ok(( + api_client, + Self { + api_server, + pool, + fee_ticker, + }, + )) + } + + async fn stop(self) { + self.api_server.stop().await; + } + } + + fn get_test_config_from_forced_exit_requests( + forced_exit_requests: ForcedExitRequestsConfig, + ) -> TestServerConfig { + let config_from_env = ZkSyncConfig::from_env(); + let config = ZkSyncConfig { + forced_exit_requests, + ..config_from_env + }; + + TestServerConfig { + config, + pool: ConnectionPool::new(Some(1)), + } + } + + #[actix_rt::test] + #[cfg_attr( + not(feature = "api_test"), + ignore = "Use `zk test rust-api` command to perform this test" + )] + async fn test_disabled_forced_exit_requests() -> anyhow::Result<()> { + let forced_exit_requests = ForcedExitRequestsConfig::from_env(); + let test_config = get_test_config_from_forced_exit_requests(ForcedExitRequestsConfig { + enabled: false, + ..forced_exit_requests + }); + + let (client, server) = TestServer::new_with_config(test_config).await?; + + let status = client.get_forced_exit_requests_status().await?; + //panic!("LAZHA"); + //println!("{}", status) + + assert_eq!(status, ForcedExitRequestStatus::Disabled); + + let should_be_disabled_msg = "Forced-exit related requests don't fail when it's disabled"; + let register_request = ForcedExitRegisterRequest { + target: Address::from_str("c0f97CC918C9d6fA4E9fc6be61a6a06589D199b2").unwrap(), + tokens: vec![0], + price_in_wei: BigUint::from_str("1212").unwrap(), + }; + + // client + // .submit_forced_exit_request(register_request) + // .await + // .expect_err(should_be_disabled_msg); + + server.stop().await; + Ok(()) + } + + #[actix_rt::test] + #[cfg_attr( + not(feature = "api_test"), + ignore = "Use `zk test rust-api` command to perform this test" + )] + async fn test_forced_exit_requests_get_fee() -> anyhow::Result<()> { + let forced_exit_requests = ForcedExitRequestsConfig::from_env(); + let test_config = get_test_config_from_forced_exit_requests(ForcedExitRequestsConfig { + price_scaling_factor: 1.5, + ..forced_exit_requests + }); + + let (client, server) = + TestServer::new_with_fee_ticker(test_config, Some(10000), Some(10000)).await?; + + let status = client.get_forced_exit_requests_status().await?; + + match status { + ForcedExitRequestStatus::Enabled(config_info) => { + // 10000 * 1.5 = 15000 + assert_eq!(config_info.request_fee, BigUint::from_u32(30000).unwrap()); + } + ForcedExitRequestStatus::Disabled => { + panic!("ForcedExitRequests feature is not disabled"); + } + } + + server.stop().await; + Ok(()) + } + + // #[actix_rt::test] + // #[cfg_attr( + // not(feature = "api_test"), + // ignore = "Use `zk test rust-api` command to perform this test" + // )] + // async fn test_forced_exit_requests_submit() -> anyhow::Result<()> { + // let (client, server) = TestServer::new().await?; + + // let enabled = client.are_forced_exit_requests_enabled().await?.enabled; + // assert_eq!(enabled, true); + + // let fee = client.get_forced_exit_request_fee().await?.request_fee; + + // let fe_request = ForcedExitRegisterRequest { + // target: "" + // }; + + // Ok(()) + // } +} diff --git a/core/bin/zksync_api/src/api_server/rest/mod.rs b/core/bin/zksync_api/src/api_server/rest/mod.rs index 6c953770b1..d21d953921 100644 --- a/core/bin/zksync_api/src/api_server/rest/mod.rs +++ b/core/bin/zksync_api/src/api_server/rest/mod.rs @@ -13,6 +13,7 @@ use crate::{fee_ticker::TickerRequest, signature_checker::VerifyTxSignatureReque use super::tx_sender::TxSender; use zksync_config::ZkSyncConfig; +mod forced_exit_requests; mod helpers; mod v01; pub mod v1; @@ -38,11 +39,18 @@ async fn start_server( v1::api_scope(tx_sender, &api_v01.config) }; + let forced_exit_requests_api_scope = forced_exit_requests::api_scope( + api_v01.connection_pool.clone(), + &api_v01.config, + fee_ticker.clone(), + ); + App::new() .wrap(middleware::Logger::new(&logger_format)) .wrap(Cors::new().send_wildcard().max_age(3600).finish()) .service(api_v01.into_scope()) .service(api_v1_scope) + .service(forced_exit_requests_api_scope) // Endpoint needed for js isReachable .route( "/favicon.ico", diff --git a/core/bin/zksync_api/src/api_server/rest/v1/forced_exit_requests.rs b/core/bin/zksync_api/src/api_server/rest/v1/forced_exit_requests.rs index d29f82158a..caa9e5718a 100644 --- a/core/bin/zksync_api/src/api_server/rest/v1/forced_exit_requests.rs +++ b/core/bin/zksync_api/src/api_server/rest/v1/forced_exit_requests.rs @@ -1,447 +1,447 @@ -//! Transactions part of API implementation. - -// Built-in uses - -// External uses -use actix_web::{ - web::{self, Json}, - Scope, -}; - -use bigdecimal::{BigDecimal, FromPrimitive}; -use chrono::{Duration, Utc}; -use futures::channel::mpsc; -use num::{bigint::ToBigInt, BigUint}; -use std::ops::Add; -use std::str::FromStr; -use std::time::Instant; - -// Workspace uses -pub use zksync_api_client::rest::v1::{ - FastProcessingQuery, ForcedExitRegisterRequest, ForcedExitRequestFee, IncomingTx, - IncomingTxBatch, IsForcedExitEnabledResponse, Receipt, TxData, -}; -use zksync_config::ZkSyncConfig; -use zksync_storage::ConnectionPool; -use zksync_types::{ - forced_exit_requests::{ForcedExitRequest, SaveForcedExitRequestQuery}, - TokenLike, TxFeeTypes, -}; - -// Local uses -use super::{Error as ApiError, JsonResult}; - -use crate::{ - api_server::{ - forced_exit_checker::ForcedExitChecker, - tx_sender::{ticker_request, SubmitError}, - }, - fee_ticker::TickerRequest, -}; - -/// Shared data between `api/v1/transactions` endpoints. -#[derive(Clone)] -pub struct ApiForcedExitRequestsData { - pub(crate) connection_pool: ConnectionPool, - pub(crate) forced_exit_checker: ForcedExitChecker, - pub(crate) ticker_request_sender: mpsc::Sender, - - pub(crate) is_enabled: bool, - pub(crate) price_scaling_factor: BigDecimal, - pub(crate) max_tokens_per_request: u8, - pub(crate) max_tx_interval_millisecs: u64, -} - -impl ApiForcedExitRequestsData { - fn new( - connection_pool: ConnectionPool, - config: &ZkSyncConfig, - ticker_request_sender: mpsc::Sender, - ) -> Self { - let forced_exit_checker = ForcedExitChecker::new(&config); - Self { - connection_pool, - forced_exit_checker, - ticker_request_sender, - - is_enabled: config.forced_exit_requests.enabled, - price_scaling_factor: BigDecimal::from_f64( - config.forced_exit_requests.price_scaling_factor, - ) - .unwrap(), - max_tokens_per_request: config.forced_exit_requests.max_tokens_per_request, - } - } -} - -// Server implementation - -async fn is_enabled( - data: web::Data, -) -> JsonResult { - let start = Instant::now(); - - let response = IsForcedExitEnabledResponse { - enabled: data.is_enabled, - }; - - metrics::histogram!("api.v01.are_forced_exit_requests_enabled", start.elapsed()); - Ok(Json(response)) -} - -async fn get_fee_for_one_forced_exit( - ticker_request_sender: mpsc::Sender, - price_scaling_factor: BigDecimal, -) -> Result { - let price = ticker_request( - ticker_request_sender.clone(), - TxFeeTypes::Withdraw, - TokenLike::Id(0), - ) - .await?; - let price = BigDecimal::from(price.total_fee.to_bigint().unwrap()); - - let scaled_price = price * price_scaling_factor; - let scaled_price = scaled_price.round(0).to_bigint().unwrap(); - - Ok(scaled_price.to_biguint().unwrap()) -} - -async fn get_forced_exit_request_fee( - data: web::Data, -) -> JsonResult { - let request_fee = get_fee_for_one_forced_exit( - data.ticker_request_sender.clone(), - data.price_scaling_factor.clone(), - ) - .await - .map_err(ApiError::from)?; - - Ok(Json(ForcedExitRequestFee { request_fee })) -} - -pub async fn submit_request( - data: web::Data, - params: web::Json, -) -> JsonResult { - let start = Instant::now(); - - if !data.is_enabled { - return Err(ApiError::bad_request( - "ForcedExit requests feature is disabled!", - )); - } - - let mut storage = data.connection_pool.access_storage().await.map_err(|err| { - vlog::warn!("Internal Server Error: '{}';", err); - return ApiError::internal(""); - })?; - - data.forced_exit_checker - .check_forced_exit(&mut storage, params.target) - .await - .map_err(ApiError::from)?; - - let price_of_one_exit = get_fee_for_one_forced_exit( - data.ticker_request_sender.clone(), - data.price_scaling_factor.clone(), - ) - .await - .map_err(ApiError::from)?; - let price_of_one_exit = BigDecimal::from(price_of_one_exit.to_bigint().unwrap()); - let price_of_request = price_of_one_exit * BigDecimal::from_usize(params.tokens.len()).unwrap(); - - let user_fee = params.price_in_wei.to_bigint().unwrap(); - let user_fee = BigDecimal::from(user_fee); - let user_scaling_coefficient = BigDecimal::from_str("1.05").unwrap(); - let user_scaled_fee = user_scaling_coefficient * user_fee; - - if user_scaled_fee < price_of_request { - return Err(ApiError::bad_request("Not enough fee")); - } - - if params.tokens.len() > 10 { - return Err(ApiError::bad_request( - "Maximum number of tokens per FE request exceeded", - )); - } - - let mut tokens_schema = storage.tokens_schema(); - - for token_id in params.tokens.iter() { - // The result is going nowhere. - // This is simply to make sure that the tokens - // that were supplied do indeed exist - tokens_schema - .get_token(TokenLike::Id(*token_id)) - .await - .map_err(|_| { - return ApiError::bad_request("One of the tokens does no exist"); - })?; - } - - let mut fe_schema = storage.forced_exit_requests_schema(); - - let valid_until = Utc::now().add(Duration::from_millis(self.max_tx_interval_millisecs)); - - let saved_fe_request = fe_schema - .store_request(SaveForcedExitRequestQuery { - target: params.target, - tokens: params.tokens.clone(), - price_in_wei: params.price_in_wei.clone(), - valid_until, - }) - .await - .map_err(|_| { - return ApiError::internal(""); - })?; - - metrics::histogram!("api.v01.register_forced_exit_request", start.elapsed()); - Ok(Json(saved_fe_request)) -} - -pub fn api_scope( - connection_pool: ConnectionPool, - config: &ZkSyncConfig, - ticker_request_sender: mpsc::Sender, -) -> Scope { - let data = ApiForcedExitRequestsData::new(connection_pool, config, ticker_request_sender); - - // `enabled` endpoint should always be there - let scope = web::scope("forced_exit_requests") - .data(data) - .route("enabled", web::get().to(is_enabled)); - - if config.forced_exit_requests.enabled { - scope - .route("submit", web::post().to(submit_request)) - .route("fee", web::get().to(get_forced_exit_request_fee)) - } else { - scope - } -} - -#[cfg(test)] -mod tests { - use bigdecimal::BigDecimal; - use futures::{channel::mpsc, StreamExt}; - use num::BigUint; - - use zksync_api_client::rest::v1::Client; - use zksync_config::ForcedExitRequestsConfig; - use zksync_storage::ConnectionPool; - use zksync_types::tokens::TokenLike; - use zksync_types::Address; - - use crate::fee_ticker::{Fee, OutputFeeType::Withdraw, TickerRequest}; - - use super::super::test_utils::TestServerConfig; - use super::*; - - fn dummy_fee_ticker(zkp_fee: Option, gas_fee: Option) -> mpsc::Sender { - let (sender, mut receiver) = mpsc::channel(10); - - let zkp_fee = zkp_fee.unwrap_or(1_u64); - let gas_fee = gas_fee.unwrap_or(1_u64); - - actix_rt::spawn(async move { - while let Some(item) = receiver.next().await { - match item { - TickerRequest::GetTxFee { response, .. } => { - let fee = Ok(Fee::new( - Withdraw, - BigUint::from(zkp_fee).into(), - BigUint::from(gas_fee).into(), - 1_u64.into(), - 1_u64.into(), - )); - - response.send(fee).expect("Unable to send response"); - } - TickerRequest::GetTokenPrice { response, .. } => { - let price = Ok(BigDecimal::from(1_u64)); - - response.send(price).expect("Unable to send response"); - } - TickerRequest::IsTokenAllowed { token, response } => { - // For test purposes, PHNX token is not allowed. - let is_phnx = match token { - TokenLike::Id(id) => id == 1, - TokenLike::Symbol(sym) => sym == "PHNX", - TokenLike::Address(_) => unreachable!(), - }; - response.send(Ok(!is_phnx)).unwrap_or_default(); - } - } - } - }); - - sender - } - - struct TestServer { - api_server: actix_web::test::TestServer, - #[allow(dead_code)] - pool: ConnectionPool, - #[allow(dead_code)] - fee_ticker: mpsc::Sender, - } - - impl TestServer { - // It should be used in the test for submitting requests - #[allow(dead_code)] - async fn new() -> anyhow::Result<(Client, Self)> { - let cfg = TestServerConfig::default(); - - Self::new_with_config(cfg).await - } - - async fn new_with_config(cfg: TestServerConfig) -> anyhow::Result<(Client, Self)> { - let pool = cfg.pool.clone(); - - let fee_ticker = dummy_fee_ticker(None, None); - - let fee_ticker2 = fee_ticker.clone(); - let (api_client, api_server) = cfg.start_server(move |cfg| { - api_scope(cfg.pool.clone(), &cfg.config, fee_ticker2.clone()) - }); - - Ok(( - api_client, - Self { - api_server, - pool, - fee_ticker, - }, - )) - } - - async fn new_with_fee_ticker( - cfg: TestServerConfig, - gas_fee: Option, - zkp_fee: Option, - ) -> anyhow::Result<(Client, Self)> { - let pool = cfg.pool.clone(); - - let fee_ticker = dummy_fee_ticker(gas_fee, zkp_fee); - - let fee_ticker2 = fee_ticker.clone(); - let (api_client, api_server) = cfg.start_server(move |cfg| { - api_scope(cfg.pool.clone(), &cfg.config, fee_ticker2.clone()) - }); - - Ok(( - api_client, - Self { - api_server, - pool, - fee_ticker, - }, - )) - } - - async fn stop(self) { - self.api_server.stop().await; - } - } - - fn test_config_from_forced_exit_requests( - forced_exit_requests: ForcedExitRequestsConfig, - ) -> TestServerConfig { - let config_from_env = ZkSyncConfig::from_env(); - let config = ZkSyncConfig { - forced_exit_requests, - ..config_from_env - }; - - TestServerConfig { - config, - pool: ConnectionPool::new(Some(1)), - } - } - - #[actix_rt::test] - #[cfg_attr( - not(feature = "api_test"), - ignore = "Use `zk test rust-api` command to perform this test" - )] - async fn test_disabled_forced_exit_requests() -> anyhow::Result<()> { - let forced_exit_requests = ForcedExitRequestsConfig::from_env(); - let test_config = test_config_from_forced_exit_requests(ForcedExitRequestsConfig { - enabled: false, - ..forced_exit_requests - }); - - let (client, server) = TestServer::new_with_config(test_config).await?; - let enabled = client.are_forced_exit_requests_enabled().await?.enabled; - - assert_eq!(enabled, false); - - let should_be_disabled_msg = "Forced-exit related requests don't fail when it's disabled"; - - client - .get_forced_exit_request_fee() - .await - .expect_err(should_be_disabled_msg); - - let register_request = ForcedExitRegisterRequest { - target: Address::from_str("c0f97CC918C9d6fA4E9fc6be61a6a06589D199b2").unwrap(), - tokens: vec![0], - price_in_wei: BigUint::from_str("1212").unwrap(), - }; - - client - .submit_forced_exit_request(register_request) - .await - .expect_err(should_be_disabled_msg); - - server.stop().await; - Ok(()) - } - - #[actix_rt::test] - #[cfg_attr( - not(feature = "api_test"), - ignore = "Use `zk test rust-api` command to perform this test" - )] - async fn test_forced_exit_requests_get_fee() -> anyhow::Result<()> { - let forced_exit_requests = ForcedExitRequestsConfig::from_env(); - let test_config = test_config_from_forced_exit_requests(ForcedExitRequestsConfig { - price_scaling_factor: 1.5, - ..forced_exit_requests - }); - - let (client, server) = - TestServer::new_with_fee_ticker(test_config, Some(10000), Some(10000)).await?; - - let enabled = client.are_forced_exit_requests_enabled().await?.enabled; - assert_eq!(enabled, true); - - let fee = client.get_forced_exit_request_fee().await?.request_fee; - // 30000 = (10000 + 10000) * 1.5 - assert_eq!(fee, BigUint::from_u32(30000).unwrap()); - - server.stop().await; - Ok(()) - } - - // #[actix_rt::test] - // #[cfg_attr( - // not(feature = "api_test"), - // ignore = "Use `zk test rust-api` command to perform this test" - // )] - // async fn test_forced_exit_requests_submit() -> anyhow::Result<()> { - // let (client, server) = TestServer::new().await?; - - // let enabled = client.are_forced_exit_requests_enabled().await?.enabled; - // assert_eq!(enabled, true); - - // let fee = client.get_forced_exit_request_fee().await?.request_fee; - - // let fe_request = ForcedExitRegisterRequest { - // target: "" - // }; - - // Ok(()) - // } -} +// //! Transactions part of API implementation. + +// // Built-in uses + +// // External uses +// use actix_web::{ +// web::{self, Json}, +// Scope, +// }; + +// use bigdecimal::{BigDecimal, FromPrimitive}; +// use chrono::{Duration, Utc}; +// use futures::channel::mpsc; +// use num::{bigint::ToBigInt, BigUint}; +// use std::ops::Add; +// use std::str::FromStr; +// use std::time::Instant; + +// // Workspace uses +// pub use zksync_api_client::rest::v1::{ +// FastProcessingQuery, ForcedExitRegisterRequest, IncomingTx, +// IncomingTxBatch, Receipt, TxData, +// }; +// use zksync_config::ZkSyncConfig; +// use zksync_storage::ConnectionPool; +// use zksync_types::{ +// forced_exit_requests::{ForcedExitRequest, SaveForcedExitRequestQuery}, +// TokenLike, TxFeeTypes, +// }; + +// // Local uses +// use super::{Error as ApiError, JsonResult}; + +// use crate::{ +// api_server::{ +// forced_exit_checker::ForcedExitChecker, +// tx_sender::{ticker_request, SubmitError}, +// }, +// fee_ticker::TickerRequest, +// }; + +// /// Shared data between `api/v1/transactions` endpoints. +// #[derive(Clone)] +// pub struct ApiForcedExitRequestsData { +// pub(crate) connection_pool: ConnectionPool, +// pub(crate) forced_exit_checker: ForcedExitChecker, +// pub(crate) ticker_request_sender: mpsc::Sender, + +// pub(crate) is_enabled: bool, +// pub(crate) price_scaling_factor: BigDecimal, +// pub(crate) max_tokens_per_request: u8, +// pub(crate) max_tx_interval_millisecs: u64, +// } + +// impl ApiForcedExitRequestsData { +// fn new( +// connection_pool: ConnectionPool, +// config: &ZkSyncConfig, +// ticker_request_sender: mpsc::Sender, +// ) -> Self { +// let forced_exit_checker = ForcedExitChecker::new(&config); +// Self { +// connection_pool, +// forced_exit_checker, +// ticker_request_sender, + +// is_enabled: config.forced_exit_requests.enabled, +// price_scaling_factor: BigDecimal::from_f64( +// config.forced_exit_requests.price_scaling_factor, +// ) +// .unwrap(), +// max_tokens_per_request: config.forced_exit_requests.max_tokens_per_request, +// } +// } +// } + +// // Server implementation + +// async fn is_enabled( +// data: web::Data, +// ) -> JsonResult { +// let start = Instant::now(); + +// let response = IsForcedExitEnabledResponse { +// enabled: data.is_enabled, +// }; + +// metrics::histogram!("api.v01.are_forced_exit_requests_enabled", start.elapsed()); +// Ok(Json(response)) +// } + +// async fn get_fee_for_one_forced_exit( +// ticker_request_sender: mpsc::Sender, +// price_scaling_factor: BigDecimal, +// ) -> Result { +// let price = ticker_request( +// ticker_request_sender.clone(), +// TxFeeTypes::Withdraw, +// TokenLike::Id(0), +// ) +// .await?; +// let price = BigDecimal::from(price.total_fee.to_bigint().unwrap()); + +// let scaled_price = price * price_scaling_factor; +// let scaled_price = scaled_price.round(0).to_bigint().unwrap(); + +// Ok(scaled_price.to_biguint().unwrap()) +// } + +// async fn get_forced_exit_request_fee( +// data: web::Data, +// ) -> JsonResult { +// let request_fee = get_fee_for_one_forced_exit( +// data.ticker_request_sender.clone(), +// data.price_scaling_factor.clone(), +// ) +// .await +// .map_err(ApiError::from)?; + +// Ok(Json(ForcedExitRequestFee { request_fee })) +// } + +// pub async fn submit_request( +// data: web::Data, +// params: web::Json, +// ) -> JsonResult { +// let start = Instant::now(); + +// if !data.is_enabled { +// return Err(ApiError::bad_request( +// "ForcedExit requests feature is disabled!", +// )); +// } + +// let mut storage = data.connection_pool.access_storage().await.map_err(|err| { +// vlog::warn!("Internal Server Error: '{}';", err); +// return ApiError::internal(""); +// })?; + +// data.forced_exit_checker +// .check_forced_exit(&mut storage, params.target) +// .await +// .map_err(ApiError::from)?; + +// let price_of_one_exit = get_fee_for_one_forced_exit( +// data.ticker_request_sender.clone(), +// data.price_scaling_factor.clone(), +// ) +// .await +// .map_err(ApiError::from)?; +// let price_of_one_exit = BigDecimal::from(price_of_one_exit.to_bigint().unwrap()); +// let price_of_request = price_of_one_exit * BigDecimal::from_usize(params.tokens.len()).unwrap(); + +// let user_fee = params.price_in_wei.to_bigint().unwrap(); +// let user_fee = BigDecimal::from(user_fee); +// let user_scaling_coefficient = BigDecimal::from_str("1.05").unwrap(); +// let user_scaled_fee = user_scaling_coefficient * user_fee; + +// if user_scaled_fee < price_of_request { +// return Err(ApiError::bad_request("Not enough fee")); +// } + +// if params.tokens.len() > 10 { +// return Err(ApiError::bad_request( +// "Maximum number of tokens per FE request exceeded", +// )); +// } + +// let mut tokens_schema = storage.tokens_schema(); + +// for token_id in params.tokens.iter() { +// // The result is going nowhere. +// // This is simply to make sure that the tokens +// // that were supplied do indeed exist +// tokens_schema +// .get_token(TokenLike::Id(*token_id)) +// .await +// .map_err(|_| { +// return ApiError::bad_request("One of the tokens does no exist"); +// })?; +// } + +// let mut fe_schema = storage.forced_exit_requests_schema(); + +// let valid_until = Utc::now().add(Duration::from_millis(self.max_tx_interval_millisecs)); + +// let saved_fe_request = fe_schema +// .store_request(SaveForcedExitRequestQuery { +// target: params.target, +// tokens: params.tokens.clone(), +// price_in_wei: params.price_in_wei.clone(), +// valid_until, +// }) +// .await +// .map_err(|_| { +// return ApiError::internal(""); +// })?; + +// metrics::histogram!("api.v01.register_forced_exit_request", start.elapsed()); +// Ok(Json(saved_fe_request)) +// } + +// pub fn api_scope( +// connection_pool: ConnectionPool, +// config: &ZkSyncConfig, +// ticker_request_sender: mpsc::Sender, +// ) -> Scope { +// let data = ApiForcedExitRequestsData::new(connection_pool, config, ticker_request_sender); + +// // `enabled` endpoint should always be there +// let scope = web::scope("forced_exit_requests") +// .data(data) +// .route("enabled", web::get().to(is_enabled)); + +// if config.forced_exit_requests.enabled { +// scope +// .route("submit", web::post().to(submit_request)) +// .route("fee", web::get().to(get_forced_exit_request_fee)) +// } else { +// scope +// } +// } + +// #[cfg(test)] +// mod tests { +// use bigdecimal::BigDecimal; +// use futures::{channel::mpsc, StreamExt}; +// use num::BigUint; + +// use zksync_api_client::rest::v1::Client; +// use zksync_config::ForcedExitRequestsConfig; +// use zksync_storage::ConnectionPool; +// use zksync_types::tokens::TokenLike; +// use zksync_types::Address; + +// use crate::fee_ticker::{Fee, OutputFeeType::Withdraw, TickerRequest}; + +// use super::super::test_utils::TestServerConfig; +// use super::*; + +// fn dummy_fee_ticker(zkp_fee: Option, gas_fee: Option) -> mpsc::Sender { +// let (sender, mut receiver) = mpsc::channel(10); + +// let zkp_fee = zkp_fee.unwrap_or(1_u64); +// let gas_fee = gas_fee.unwrap_or(1_u64); + +// actix_rt::spawn(async move { +// while let Some(item) = receiver.next().await { +// match item { +// TickerRequest::GetTxFee { response, .. } => { +// let fee = Ok(Fee::new( +// Withdraw, +// BigUint::from(zkp_fee).into(), +// BigUint::from(gas_fee).into(), +// 1_u64.into(), +// 1_u64.into(), +// )); + +// response.send(fee).expect("Unable to send response"); +// } +// TickerRequest::GetTokenPrice { response, .. } => { +// let price = Ok(BigDecimal::from(1_u64)); + +// response.send(price).expect("Unable to send response"); +// } +// TickerRequest::IsTokenAllowed { token, response } => { +// // For test purposes, PHNX token is not allowed. +// let is_phnx = match token { +// TokenLike::Id(id) => id == 1, +// TokenLike::Symbol(sym) => sym == "PHNX", +// TokenLike::Address(_) => unreachable!(), +// }; +// response.send(Ok(!is_phnx)).unwrap_or_default(); +// } +// } +// } +// }); + +// sender +// } + +// struct TestServer { +// api_server: actix_web::test::TestServer, +// #[allow(dead_code)] +// pool: ConnectionPool, +// #[allow(dead_code)] +// fee_ticker: mpsc::Sender, +// } + +// impl TestServer { +// // It should be used in the test for submitting requests +// #[allow(dead_code)] +// async fn new() -> anyhow::Result<(Client, Self)> { +// let cfg = TestServerConfig::default(); + +// Self::new_with_config(cfg).await +// } + +// async fn new_with_config(cfg: TestServerConfig) -> anyhow::Result<(Client, Self)> { +// let pool = cfg.pool.clone(); + +// let fee_ticker = dummy_fee_ticker(None, None); + +// let fee_ticker2 = fee_ticker.clone(); +// let (api_client, api_server) = cfg.start_server(move |cfg| { +// api_scope(cfg.pool.clone(), &cfg.config, fee_ticker2.clone()) +// }); + +// Ok(( +// api_client, +// Self { +// api_server, +// pool, +// fee_ticker, +// }, +// )) +// } + +// async fn new_with_fee_ticker( +// cfg: TestServerConfig, +// gas_fee: Option, +// zkp_fee: Option, +// ) -> anyhow::Result<(Client, Self)> { +// let pool = cfg.pool.clone(); + +// let fee_ticker = dummy_fee_ticker(gas_fee, zkp_fee); + +// let fee_ticker2 = fee_ticker.clone(); +// let (api_client, api_server) = cfg.start_server(move |cfg| { +// api_scope(cfg.pool.clone(), &cfg.config, fee_ticker2.clone()) +// }); + +// Ok(( +// api_client, +// Self { +// api_server, +// pool, +// fee_ticker, +// }, +// )) +// } + +// async fn stop(self) { +// self.api_server.stop().await; +// } +// } + +// fn get_test_config_from_forced_exit_requests( +// forced_exit_requests: ForcedExitRequestsConfig, +// ) -> TestServerConfig { +// let config_from_env = ZkSyncConfig::from_env(); +// let config = ZkSyncConfig { +// forced_exit_requests, +// ..config_from_env +// }; + +// TestServerConfig { +// config, +// pool: ConnectionPool::new(Some(1)), +// } +// } + +// #[actix_rt::test] +// #[cfg_attr( +// not(feature = "api_test"), +// ignore = "Use `zk test rust-api` command to perform this test" +// )] +// async fn test_disabled_forced_exit_requests() -> anyhow::Result<()> { +// let forced_exit_requests = ForcedExitRequestsConfig::from_env(); +// let test_config = get_test_config_from_forced_exit_requests(ForcedExitRequestsConfig { +// enabled: false, +// ..forced_exit_requests +// }); + +// let (client, server) = TestServer::new_with_config(test_config).await?; +// let enabled = client.are_forced_exit_requests_enabled().await?.enabled; + +// assert_eq!(enabled, false); + +// let should_be_disabled_msg = "Forced-exit related requests don't fail when it's disabled"; + +// client +// .get_forced_exit_request_fee() +// .await +// .expect_err(should_be_disabled_msg); + +// let register_request = ForcedExitRegisterRequest { +// target: Address::from_str("c0f97CC918C9d6fA4E9fc6be61a6a06589D199b2").unwrap(), +// tokens: vec![0], +// price_in_wei: BigUint::from_str("1212").unwrap(), +// }; + +// client +// .submit_forced_exit_request(register_request) +// .await +// .expect_err(should_be_disabled_msg); + +// server.stop().await; +// Ok(()) +// } + +// #[actix_rt::test] +// #[cfg_attr( +// not(feature = "api_test"), +// ignore = "Use `zk test rust-api` command to perform this test" +// )] +// async fn test_forced_exit_requests_get_fee() -> anyhow::Result<()> { +// let forced_exit_requests = ForcedExitRequestsConfig::from_env(); +// let test_config = get_test_config_from_forced_exit_requests(ForcedExitRequestsConfig { +// price_scaling_factor: 1.5, +// ..forced_exit_requests +// }); + +// let (client, server) = +// TestServer::new_with_fee_ticker(test_config, Some(10000), Some(10000)).await?; + +// let enabled = client.are_forced_exit_requests_enabled().await?.enabled; +// assert_eq!(enabled, true); + +// let fee = client.get_forced_exit_request_fee().await?.request_fee; +// // 30000 = (10000 + 10000) * 1.5 +// assert_eq!(fee, BigUint::from_u32(30000).unwrap()); + +// server.stop().await; +// Ok(()) +// } + +// // #[actix_rt::test] +// // #[cfg_attr( +// // not(feature = "api_test"), +// // ignore = "Use `zk test rust-api` command to perform this test" +// // )] +// // async fn test_forced_exit_requests_submit() -> anyhow::Result<()> { +// // let (client, server) = TestServer::new().await?; + +// // let enabled = client.are_forced_exit_requests_enabled().await?.enabled; +// // assert_eq!(enabled, true); + +// // let fee = client.get_forced_exit_request_fee().await?.request_fee; + +// // let fe_request = ForcedExitRegisterRequest { +// // target: "" +// // }; + +// // Ok(()) +// // } +// } diff --git a/core/bin/zksync_api/src/api_server/rest/v1/mod.rs b/core/bin/zksync_api/src/api_server/rest/v1/mod.rs index ea20cb8d05..fb29f52e7a 100644 --- a/core/bin/zksync_api/src/api_server/rest/v1/mod.rs +++ b/core/bin/zksync_api/src/api_server/rest/v1/mod.rs @@ -27,7 +27,7 @@ mod forced_exit_requests; mod operations; mod search; #[cfg(test)] -mod test_utils; +pub mod test_utils; mod tokens; mod transactions; @@ -51,9 +51,9 @@ pub(crate) fn api_scope(tx_sender: TxSender, zk_config: &ZkSyncConfig) -> Scope tx_sender.tokens, tx_sender.ticker_requests.clone(), )) - .service(forced_exit_requests::api_scope( - tx_sender.pool.clone(), - zk_config, - tx_sender.ticker_requests, - )) + // .service(forced_exit_requests::api_scope( + // tx_sender.pool.clone(), + // zk_config, + // tx_sender.ticker_requests, + // )) } diff --git a/core/bin/zksync_api/src/api_server/rest/v1/test_utils.rs b/core/bin/zksync_api/src/api_server/rest/v1/test_utils.rs index dd572f94e0..8b1909c3b4 100644 --- a/core/bin/zksync_api/src/api_server/rest/v1/test_utils.rs +++ b/core/bin/zksync_api/src/api_server/rest/v1/test_utils.rs @@ -66,13 +66,17 @@ pub struct TestTransactions { } impl TestServerConfig { - pub fn start_server(&self, scope_factory: F) -> (Client, actix_web::test::TestServer) + pub fn start_server_with_scope( + &self, + scope: String, + scope_factory: F, + ) -> (Client, actix_web::test::TestServer) where F: Fn(&TestServerConfig) -> Scope + Clone + Send + 'static, { let this = self.clone(); let server = actix_web::test::start(move || { - App::new().service(web::scope("/api/v1").service(scope_factory(&this))) + App::new().service(web::scope(scope.as_ref()).service(scope_factory(&this))) }); let url = server.url("").trim_end_matches('/').to_owned(); @@ -81,6 +85,13 @@ impl TestServerConfig { (client, server) } + pub fn start_server(&self, scope_factory: F) -> (Client, actix_web::test::TestServer) + where + F: Fn(&TestServerConfig) -> Scope + Clone + Send + 'static, + { + self.start_server_with_scope(String::from("/api/v1"), scope_factory) + } + /// Creates several transactions and the corresponding executed operations. pub fn gen_zk_txs(fee: u64) -> TestTransactions { Self::gen_zk_txs_for_account(0xdead, ZkSyncAccount::rand().address, fee) diff --git a/core/lib/api_client/src/rest/v1/forced_exit_requests.rs b/core/lib/api_client/src/rest/forced_exit_requests/mod.rs similarity index 50% rename from core/lib/api_client/src/rest/v1/forced_exit_requests.rs rename to core/lib/api_client/src/rest/forced_exit_requests/mod.rs index 357276c716..970f379adb 100644 --- a/core/lib/api_client/src/rest/v1/forced_exit_requests.rs +++ b/core/lib/api_client/src/rest/forced_exit_requests/mod.rs @@ -12,19 +12,23 @@ use zksync_utils::BigUintSerdeAsRadix10Str; use num::BigUint; // Local uses -use super::client::{self, Client}; +use crate::rest::v1::Client; +use crate::rest::v1::ClientResult; // Data transfer objects. - -#[derive(Serialize, Deserialize)] -pub struct IsForcedExitEnabledResponse { - pub enabled: bool, -} -#[derive(Debug, Serialize, Deserialize, Clone)] -#[serde(rename_all = "camelCase")] -pub struct ForcedExitRequestFee { +#[derive(Serialize, Deserialize, PartialEq, Debug)] +pub struct ConfigInfo { #[serde(with = "BigUintSerdeAsRadix10Str")] pub request_fee: BigUint, + pub max_tokens_per_request: u8, + pub recomended_tx_interval_millis: i64, +} + +#[derive(Serialize, Deserialize, PartialEq, Debug)] +#[serde(tag = "status", rename_all = "camelCase")] +pub enum ForcedExitRequestStatus { + Enabled(ConfigInfo), + Disabled, } #[derive(Deserialize, Serialize)] @@ -35,22 +39,20 @@ pub struct ForcedExitRegisterRequest { pub price_in_wei: BigUint, } -impl Client { - pub async fn are_forced_exit_requests_enabled( - &self, - ) -> client::Result { - self.get("forced_exit_requests/enabled").send().await - } +const FORCED_EXIT_REQUESTS_SCOPE: &'static str = "/api/forced_exit_requests/v0.1/"; - pub async fn get_forced_exit_request_fee(&self) -> client::Result { - self.get("forced_exit_requests/fee").send().await +impl Client { + pub async fn get_forced_exit_requests_status(&self) -> ClientResult { + self.get_with_scope(FORCED_EXIT_REQUESTS_SCOPE, "status") + .send() + .await } pub async fn submit_forced_exit_request( &self, regiter_request: ForcedExitRegisterRequest, - ) -> client::Result { - self.post("forced_exit_requests/submit") + ) -> ClientResult { + self.post_with_scope(FORCED_EXIT_REQUESTS_SCOPE, "submit") .body(®iter_request) .send() .await diff --git a/core/lib/api_client/src/rest/mod.rs b/core/lib/api_client/src/rest/mod.rs index a3a6d96c3f..cf50baa86b 100644 --- a/core/lib/api_client/src/rest/mod.rs +++ b/core/lib/api_client/src/rest/mod.rs @@ -1 +1,2 @@ +pub mod forced_exit_requests; pub mod v1; diff --git a/core/lib/api_client/src/rest/v1/client.rs b/core/lib/api_client/src/rest/v1/client.rs index e6c30bbb37..1d0e8f7728 100644 --- a/core/lib/api_client/src/rest/v1/client.rs +++ b/core/lib/api_client/src/rest/v1/client.rs @@ -52,6 +52,8 @@ pub struct Client { url: String, } +const API_V1_SCOPE: &'static str = "/api/v1/"; + impl Client { /// Creates a new REST API client with the specified Url. pub fn new(url: String) -> Self { @@ -61,13 +63,21 @@ impl Client { } } - fn endpoint(&self, method: &str) -> String { - [&self.url, "/api/v1/", method].concat() + fn endpoint(&self, scope: &str, method: &str) -> String { + [&self.url, scope, method].concat() } /// Constructs GET request for the specified method. pub(crate) fn get(&self, method: impl AsRef) -> ClientRequestBuilder { - let url = self.endpoint(method.as_ref()); + self.get_with_scope(API_V1_SCOPE, method) + } + + pub(crate) fn get_with_scope( + &self, + scope: impl AsRef, + method: impl AsRef, + ) -> ClientRequestBuilder { + let url = self.endpoint(scope.as_ref(), method.as_ref()); ClientRequestBuilder { inner: self.inner.get(&url), url, @@ -76,7 +86,15 @@ impl Client { /// Constructs POST request for the specified method. pub(crate) fn post(&self, method: impl AsRef) -> ClientRequestBuilder { - let url = self.endpoint(method.as_ref()); + self.post_with_scope(API_V1_SCOPE, method) + } + + pub(crate) fn post_with_scope( + &self, + scope: impl AsRef, + method: impl AsRef, + ) -> ClientRequestBuilder { + let url = self.endpoint(scope.as_ref(), method.as_ref()); ClientRequestBuilder { inner: self.inner.post(&url), url, diff --git a/core/lib/api_client/src/rest/v1/mod.rs b/core/lib/api_client/src/rest/v1/mod.rs index 687b9450f0..56c4156b4f 100644 --- a/core/lib/api_client/src/rest/v1/mod.rs +++ b/core/lib/api_client/src/rest/v1/mod.rs @@ -9,12 +9,9 @@ use zksync_types::BlockNumber; // Public uses pub use self::{ blocks::{BlockInfo, TransactionInfo}, - client::{Client, ClientError}, + client::{Client, ClientError, Result as ClientResult}, config::Contracts, error::ErrorBody, - forced_exit_requests::{ - ForcedExitRegisterRequest, ForcedExitRequestFee, IsForcedExitEnabledResponse, - }, operations::{PriorityOpData, PriorityOpQuery, PriorityOpQueryError, PriorityOpReceipt}, search::BlockSearchQuery, tokens::{TokenPriceKind, TokenPriceQuery}, @@ -27,7 +24,6 @@ mod blocks; mod client; mod config; mod error; -mod forced_exit_requests; mod operations; mod search; mod tokens; diff --git a/core/lib/config/src/configs/forced_exit_requests.rs b/core/lib/config/src/configs/forced_exit_requests.rs index a4769b8bdb..d70337d624 100644 --- a/core/lib/config/src/configs/forced_exit_requests.rs +++ b/core/lib/config/src/configs/forced_exit_requests.rs @@ -15,7 +15,7 @@ struct ForcedExitRequestsInternalConfig { pub enabled: bool, pub price_scaling_factor: f64, pub max_tokens_per_request: u8, - pub recomended_tx_interval: u64, + pub recomended_tx_interval: i64, pub tx_interval_scaling_factor: f64, } @@ -24,8 +24,8 @@ pub struct ForcedExitRequestsConfig { pub enabled: bool, pub price_scaling_factor: f64, pub max_tokens_per_request: u8, - pub recomended_tx_interval: u64, - pub max_tx_interval: u64, + pub recomended_tx_interval: i64, + pub max_tx_interval: i64, } impl ForcedExitRequestsConfig { @@ -41,7 +41,7 @@ impl ForcedExitRequestsConfig { price_scaling_factor: config.price_scaling_factor, max_tokens_per_request: config.max_tokens_per_request, recomended_tx_interval: config.recomended_tx_interval, - max_tx_interval: max_tx_interval.round() as u64, + max_tx_interval: max_tx_interval.round() as i64, } } } From 9d51dcc6f553c179f1f0b7b64aab16e22a7039b4 Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Fri, 5 Feb 2021 05:53:21 +0200 Subject: [PATCH 19/90] Test on checking the number of tokens in a request --- .../rest/forced_exit_requests/v01.rs | 44 ++++++++++++++++--- infrastructure/zk/src/test/test.ts | 2 +- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs b/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs index 8969736dca..26b9cff6e2 100644 --- a/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs +++ b/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs @@ -379,8 +379,6 @@ mod tests { let (client, server) = TestServer::new_with_config(test_config).await?; let status = client.get_forced_exit_requests_status().await?; - //panic!("LAZHA"); - //println!("{}", status) assert_eq!(status, ForcedExitRequestStatus::Disabled); @@ -391,10 +389,10 @@ mod tests { price_in_wei: BigUint::from_str("1212").unwrap(), }; - // client - // .submit_forced_exit_request(register_request) - // .await - // .expect_err(should_be_disabled_msg); + client + .submit_forced_exit_request(register_request) + .await + .expect_err(should_be_disabled_msg); server.stop().await; Ok(()) @@ -431,6 +429,40 @@ mod tests { Ok(()) } + #[actix_rt::test] + #[cfg_attr( + not(feature = "api_test"), + ignore = "Use `zk test rust-api` command to perform this test" + )] + async fn test_forced_exit_requests_wrongs_tokens_number() -> anyhow::Result<()> { + let forced_exit_requests = ForcedExitRequestsConfig::from_env(); + let test_config = get_test_config_from_forced_exit_requests(ForcedExitRequestsConfig { + max_tokens_per_request: 5, + ..forced_exit_requests + }); + + let (client, server) = + TestServer::new_with_fee_ticker(test_config, Some(10000), Some(10000)).await?; + + let status = client.get_forced_exit_requests_status().await?; + + assert_ne!(status, ForcedExitRequestStatus::Disabled); + + let register_request = ForcedExitRegisterRequest { + target: Address::from_str("c0f97CC918C9d6fA4E9fc6be61a6a06589D199b2").unwrap(), + tokens: vec![0, 1, 2, 3, 4, 5, 6, 7], + price_in_wei: BigUint::from_str("1212").unwrap(), + }; + + client + .submit_forced_exit_request(register_request) + .await + .expect_err("Api does not take the limit on the number of tokens into account"); + + server.stop().await; + Ok(()) + } + // #[actix_rt::test] // #[cfg_attr( // not(feature = "api_test"), diff --git a/infrastructure/zk/src/test/test.ts b/infrastructure/zk/src/test/test.ts index 11c9a4d085..382d78192e 100644 --- a/infrastructure/zk/src/test/test.ts +++ b/infrastructure/zk/src/test/test.ts @@ -34,7 +34,7 @@ export async function rustApi(reset: boolean, ...args: string[]) { await runOnTestDb( reset, 'core/bin/zksync_api', - `cargo test --release -p zksync_api -- --ignored --nocapture api_server + `cargo test -p zksync_api -- --ignored --nocapture api_server ${args.join(' ')}` ); } From bfb12fe11cafe1cff712a449fdc5c911d5708891 Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Sat, 6 Feb 2021 15:14:22 +0200 Subject: [PATCH 20/90] [WIP]: zksync_forced_exit_requests eth_watcher --- Cargo.lock | 23 ++ Cargo.toml | 1 + contracts/contracts/ForcedExit.sol | 8 +- core/bin/server/Cargo.toml | 1 + core/bin/server/src/main.rs | 9 +- core/bin/zksync_core/src/eth_watch/client.rs | 63 +++-- core/bin/zksync_core/src/eth_watch/mod.rs | 4 +- .../zksync_forced_exit_requests/Cargo.toml | 34 +++ .../src/eth_watch.rs | 219 ++++++++++++++++++ .../src/forced_exit_sender.rs | 38 +++ .../zksync_forced_exit_requests/src/lib.rs | 83 +++++++ core/lib/config/src/configs/contracts.rs | 3 + core/lib/contracts/src/lib.rs | 11 + core/lib/types/src/forced_exit_requests.rs | 43 ++++ 14 files changed, 513 insertions(+), 27 deletions(-) create mode 100644 core/bin/zksync_forced_exit_requests/Cargo.toml create mode 100644 core/bin/zksync_forced_exit_requests/src/eth_watch.rs create mode 100644 core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs create mode 100644 core/bin/zksync_forced_exit_requests/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index f664249ea0..581308781b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5888,6 +5888,28 @@ dependencies = [ "zksync_types", ] +[[package]] +name = "zksync_forced_exit_requests" +version = "1.0.0" +dependencies = [ + "actix-web", + "anyhow", + "async-trait", + "env_logger", + "ethabi", + "log", + "metrics", + "num", + "tokio 0.2.22", + "web3", + "zksync_api", + "zksync_config", + "zksync_contracts", + "zksync_core", + "zksync_storage", + "zksync_types", +] + [[package]] name = "zksync_prometheus_exporter" version = "1.0.0" @@ -5979,6 +6001,7 @@ dependencies = [ "zksync_core", "zksync_crypto", "zksync_eth_sender", + "zksync_forced_exit_requests", "zksync_prometheus_exporter", "zksync_prover", "zksync_storage", diff --git a/Cargo.toml b/Cargo.toml index dafb7d8d45..42ecdd89a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ members = [ "core/bin/zksync_core", "core/bin/zksync_eth_sender", "core/bin/zksync_witness_generator", + "core/bin/zksync_forced_exit_requests", # Libraries "core/lib/circuit", diff --git a/contracts/contracts/ForcedExit.sol b/contracts/contracts/ForcedExit.sol index f752773284..a9a33e4bcf 100644 --- a/contracts/contracts/ForcedExit.sol +++ b/contracts/contracts/ForcedExit.sol @@ -17,6 +17,9 @@ contract ForcedExit is Ownable, ReentrancyGuard { constructor(address _master) Ownable(_master) { initializeReentrancyGuard(); + + // The master is the default receiver + receiver = payable(_master); } event FundsReceived( @@ -53,7 +56,10 @@ contract ForcedExit is Ownable, ReentrancyGuard { require(success, "d"); // ETH withdraw failed } - receive() external payable nonReentrant { + // We ave to use fallback instead of `receive` since the ethabi + // library can't decode the receive function: + // https://github.com/rust-ethereum/ethabi/issues/185 + fallback() external payable nonReentrant { require(enabled, "Contract is disabled"); require(receiver != address(0), "Receiver must be non-zero"); diff --git a/core/bin/server/Cargo.toml b/core/bin/server/Cargo.toml index 8906dc280f..bfc5388636 100644 --- a/core/bin/server/Cargo.toml +++ b/core/bin/server/Cargo.toml @@ -15,6 +15,7 @@ zksync_api = { path = "../zksync_api", version = "1.0" } zksync_core = { path = "../zksync_core", version = "1.0" } zksync_witness_generator = { path = "../zksync_witness_generator", version = "1.0" } zksync_eth_sender = { path = "../zksync_eth_sender", version = "1.0" } +zksync_forced_exit_requests = { path = "../zksync_forced_exit_requests", version = "1.0" } zksync_prometheus_exporter = { path = "../../lib/prometheus_exporter", version = "1.0" } zksync_config = { path = "../../lib/config", version = "1.0" } diff --git a/core/bin/server/src/main.rs b/core/bin/server/src/main.rs index b221ce8dc0..7a32ed9bc0 100644 --- a/core/bin/server/src/main.rs +++ b/core/bin/server/src/main.rs @@ -4,6 +4,7 @@ use structopt::StructOpt; use zksync_api::run_api; use zksync_core::{genesis_init, run_core, wait_for_tasks}; use zksync_eth_sender::run_eth_sender; +use zksync_forced_exit_requests::run_forced_exit_requests_actors; use zksync_prometheus_exporter::run_prometheus_exporter; use zksync_witness_generator::run_prover_server; @@ -78,9 +79,12 @@ async fn main() -> anyhow::Result<()> { // Run prover server & witness generator. log::info!("Starting the Prover server actors"); - let database = zksync_witness_generator::database::Database::new(connection_pool); + let database = zksync_witness_generator::database::Database::new(connection_pool.clone()); run_prover_server(database, stop_signal_sender, ZkSyncConfig::from_env()); + log::info!("Starting the ForcedExitRequests actors"); + let forced_exit_requests_task_handle = run_forced_exit_requests_actors(connection_pool, config); + tokio::select! { _ = async { wait_for_tasks(core_task_handles).await } => { // We don't need to do anything here, since Core actors will panic upon future resolving. @@ -97,6 +101,9 @@ async fn main() -> anyhow::Result<()> { _ = async { counter_task_handle.unwrap().await } => { panic!("Operation counting actor is not supposed to finish its execution") }, + _ = async { forced_exit_requests_task_handle.await } => { + panic!("ForcedExitRequests actor is not supposed to finish its execution") + }, _ = async { stop_signal_receiver.next().await } => { log::warn!("Stop signal received, shutting down"); } diff --git a/core/bin/zksync_core/src/eth_watch/client.rs b/core/bin/zksync_core/src/eth_watch/client.rs index 89646c0de4..5220c4ad3d 100644 --- a/core/bin/zksync_core/src/eth_watch/client.rs +++ b/core/bin/zksync_core/src/eth_watch/client.rs @@ -69,28 +69,7 @@ impl EthHttpClient { T: TryFrom, T::Error: Debug, { - let filter = FilterBuilder::default() - .address(vec![self.zksync_contract.address()]) - .from_block(from) - .to_block(to) - .topics(Some(topics), None, None, None) - .build(); - - self.web3 - .eth() - .logs(filter) - .await? - .into_iter() - .filter_map(|event| { - if let Ok(event) = T::try_from(event) { - Some(Ok(event)) - } else { - None - } - // TODO: remove after update - // .map_err(|e| format_err!("Failed to parse event log from ETH: {:?}", e)) - }) - .collect() + get_contract_events(&self.web3, self.zksync_contract.address(), from, to, topics).await } } @@ -111,7 +90,7 @@ impl EthClient for EthHttpClient { } async fn block_number(&self) -> anyhow::Result { - Ok(self.web3.eth().block_number().await?.as_u64()) + get_web3_block_number(&self.web3).await } async fn get_auth_fact(&self, address: Address, nonce: u32) -> anyhow::Result> { @@ -141,3 +120,41 @@ impl EthClient for EthHttpClient { .map(|res: U256| res.as_u64()) } } + +pub async fn get_contract_events( + web3: &Web3, + contract_address: Address, + from: BlockNumber, + to: BlockNumber, + topics: Vec, +) -> anyhow::Result> +where + T: TryFrom, + T::Error: Debug, +{ + let filter = FilterBuilder::default() + .address(vec![contract_address]) + .from_block(from) + .to_block(to) + .topics(Some(topics), None, None, None) + .build(); + + web3.eth() + .logs(filter) + .await? + .into_iter() + .filter_map(|event| { + if let Ok(event) = T::try_from(event) { + Some(Ok(event)) + } else { + None + } + // TODO: remove after update + // .map_err(|e| format_err!("Failed to parse event log from ETH: {:?}", e)) + }) + .collect() +} + +pub async fn get_web3_block_number(web3: &Web3) -> anyhow::Result { + Ok(web3.eth().block_number().await?.as_u64()) +} diff --git a/core/bin/zksync_core/src/eth_watch/mod.rs b/core/bin/zksync_core/src/eth_watch/mod.rs index b80293d005..129bba01f0 100644 --- a/core/bin/zksync_core/src/eth_watch/mod.rs +++ b/core/bin/zksync_core/src/eth_watch/mod.rs @@ -31,7 +31,7 @@ use self::{ received_ops::{sift_outdated_ops, ReceivedPriorityOp}, }; -pub use client::EthHttpClient; +pub use client::{get_contract_events, get_web3_block_number, EthHttpClient}; use zksync_config::ZkSyncConfig; mod client; @@ -53,7 +53,7 @@ const RATE_LIMIT_DELAY: Duration = Duration::from_secs(30); /// watcher goes into "backoff" mode in which polling is disabled for a /// certain amount of time. #[derive(Debug)] -enum WatcherMode { +pub enum WatcherMode { /// ETHWatcher operates normally. Working, /// Polling is currently disabled. diff --git a/core/bin/zksync_forced_exit_requests/Cargo.toml b/core/bin/zksync_forced_exit_requests/Cargo.toml new file mode 100644 index 0000000000..da8cbdaf91 --- /dev/null +++ b/core/bin/zksync_forced_exit_requests/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "zksync_forced_exit_requests" +version = "1.0.0" +edition = "2018" +authors = ["The Matter Labs Team "] +homepage = "https://zksync.io/" +repository = "https://github.com/matter-labs/zksync" +license = "Apache-2.0" +keywords = ["blockchain", "zksync"] +categories = ["cryptography"] +publish = false # We don't want to publish our binaries. + +[dependencies] +zksync_types = { path = "../../lib/types", version = "1.0" } +zksync_storage = { path = "../../lib/storage", version = "1.0" } + +zksync_config = { path = "../../lib/config", version = "1.0" } +zksync_contracts = { path = "../../lib/contracts", version = "1.0" } + +zksync_core = { path = "../zksync_core", version = "1.0" } +zksync_api = { path = "../zksync_api", version = "1.0" } +actix-web = "3.0.0" +ethabi = "12.0.0" +web3 = "0.13.0" +log = "0.4" +env_logger = "0.6" +metrics = "0.13.0-alpha.8" + +tokio = { version = "0.2", features = ["full"] } +anyhow = "1.0" +async-trait = "0.1" + +[dev-dependencies] +num = { version = "0.3.1", features = ["serde"] } diff --git a/core/bin/zksync_forced_exit_requests/src/eth_watch.rs b/core/bin/zksync_forced_exit_requests/src/eth_watch.rs new file mode 100644 index 0000000000..ed37a6810c --- /dev/null +++ b/core/bin/zksync_forced_exit_requests/src/eth_watch.rs @@ -0,0 +1,219 @@ +use actix_web::client; +use anyhow::format_err; +use ethabi::{Contract as ContractAbi, Hash}; +use std::fmt::Debug; +use std::{ + convert::TryFrom, + time::{Duration, Instant}, +}; +use tokio::task::JoinHandle; +use tokio::time; +use web3::{ + contract::{Contract, Options}, + transports::Http, + types::{BlockNumber, FilterBuilder, Log}, + Web3, +}; +use zksync_config::ZkSyncConfig; +use zksync_storage::ConnectionPool; + +use zksync_contracts::forced_exit_contract; +use zksync_types::{block::Block, Address, Nonce, PriorityOp, H160, U256}; + +use zksync_api::core_api_client::CoreApiClient; +use zksync_core::eth_watch::{get_contract_events, get_web3_block_number, WatcherMode}; +use zksync_types::forced_exit_requests::FundsReceivedEvent; + +/// As `infura` may limit the requests, upon error we need to wait for a while +/// before repeating the request. +const RATE_LIMIT_DELAY: Duration = Duration::from_secs(30); + +struct ContractTopics { + pub funds_received: Hash, +} + +impl ContractTopics { + fn new(contract: ðabi::Contract) -> Self { + Self { + funds_received: contract + .event("FundsReceived") + .expect("forced_exit contract abi error") + .signature(), + } + } +} +pub struct EthClient { + web3: Web3, + forced_exit_contract: Contract, + topics: ContractTopics, +} + +impl EthClient { + pub fn new(web3: Web3, zksync_contract_addr: H160) -> Self { + let forced_exit_contract = + Contract::new(web3.eth(), zksync_contract_addr, forced_exit_contract()); + + let topics = ContractTopics::new(forced_exit_contract.abi()); + Self { + forced_exit_contract, + web3, + topics, + } + } + + async fn get_events(&self, from: u64, to: u64, topics: Vec) -> anyhow::Result> + where + T: TryFrom, + T::Error: Debug, + { + let from = BlockNumber::from(from); + let to = BlockNumber::from(to); + get_contract_events( + &self.web3, + self.forced_exit_contract.address(), + from, + to, + topics, + ) + .await + } + + async fn get_funds_received_events( + &self, + from: u64, + to: u64, + ) -> anyhow::Result> { + let start = Instant::now(); + let result = self + .get_events(from, to, vec![self.topics.funds_received]) + .await; + + metrics::histogram!( + "forced_exit_requests.get_funds_received_events", + start.elapsed() + ); + result + } + + async fn get_block_number(&self) -> anyhow::Result { + get_web3_block_number(&self.web3).await + } +} + +struct ForcedExitContractWatcher { + core_api_client: CoreApiClient, + connection_pool: ConnectionPool, + config: ZkSyncConfig, + eth_client: EthClient, + last_viewed_block: u64, + + mode: WatcherMode, +} + +impl ForcedExitContractWatcher { + // TODO try to move it to eth client + fn is_backoff_requested(&self, error: &anyhow::Error) -> bool { + error.to_string().contains("429 Too Many Requests") + } + + fn enter_backoff_mode(&mut self) { + let backoff_until = Instant::now() + RATE_LIMIT_DELAY; + self.mode = WatcherMode::Backoff(backoff_until); + // This is needed to track how much time is spent in backoff mode + // and trigger grafana alerts + metrics::histogram!("eth_watcher.enter_backoff_mode", RATE_LIMIT_DELAY); + } + + fn polling_allowed(&mut self) -> bool { + match self.mode { + WatcherMode::Working => true, + WatcherMode::Backoff(delay_until) => { + if Instant::now() >= delay_until { + log::info!("Exiting the backoff mode"); + self.mode = WatcherMode::Working; + true + } else { + // We have to wait more until backoff is disabled. + false + } + } + } + } + + fn handle_infura_error(&mut self, error: anyhow::Error) { + if self.is_backoff_requested(&error) { + log::warn!( + "Rate limit was reached, as reported by Ethereum node. \ + Entering the backoff mode" + ); + self.enter_backoff_mode(); + } else { + // Some unexpected kind of error, we won't shutdown the node because of it, + // but rather expect node administrators to handle the situation. + log::error!("Failed to process new blocks {}", error); + } + } + + pub async fn poll(&mut self) { + let current_block = self.eth_client.get_block_number().await; + + if !self.polling_allowed() { + // Polling is currently disabled, skip it. + return; + } + + if let Err(error) = current_block { + self.handle_infura_error(error); + return; + } + let block = current_block.unwrap(); + if self.last_viewed_block >= block { + return; + } + + let events = self + .eth_client + .get_funds_received_events(self.last_viewed_block + 1, block) + .await; + + if let Err(error) = events { + self.handle_infura_error(error); + return; + } + let events = events.unwrap(); + + for e in events { + dbg!("An event has come for us: {}", e.amount); + } + + self.last_viewed_block = block; + } +} + +pub fn run_forced_exit_contract_watcher( + core_api_client: CoreApiClient, + connection_pool: ConnectionPool, + config: ZkSyncConfig, +) -> JoinHandle<()> { + let transport = web3::transports::Http::new(&config.eth_client.web3_url).unwrap(); + let web3 = web3::Web3::new(transport); + let eth_client = EthClient::new(web3, config.contracts.forced_exit_addr); + + let mut contract_watcher = ForcedExitContractWatcher { + core_api_client, + connection_pool, + config, + eth_client, + last_viewed_block: 0, + mode: WatcherMode::Working, + }; + + tokio::spawn(async move { + let mut timer = time::interval(Duration::from_secs(1)); + + loop { + timer.tick().await; + contract_watcher.poll().await; + } + }) +} diff --git a/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs b/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs new file mode 100644 index 0000000000..b28e7e5ea4 --- /dev/null +++ b/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs @@ -0,0 +1,38 @@ +use std::{convert::TryFrom, time::Instant}; + +use anyhow::format_err; +use ethabi::{Contract as ContractAbi, Hash}; +use std::fmt::Debug; +use tokio::task::JoinHandle; +use web3::{ + contract::{Contract, Options}, + transports::Http, + types::{BlockNumber, FilterBuilder, Log}, + Web3, +}; +use zksync_config::ZkSyncConfig; +use zksync_storage::ConnectionPool; + +use zksync_contracts::zksync_contract; +use zksync_types::{Address, Nonce, PriorityOp, H160, U256}; + +use zksync_api::core_api_client::CoreApiClient; +use zksync_core::eth_watch::get_contract_events; +use zksync_types::forced_exit_requests::FundsReceivedEvent; + +pub struct ForcedExitSender { + core_api_client: CoreApiClient, + connection_pool: ConnectionPool, + // requests: Receiver +} + +impl ForcedExitSender { + pub fn new(core_api_client: CoreApiClient, connection_pool: ConnectionPool) -> Self { + Self { + core_api_client, + connection_pool, + } + } + + pub async fn run(mut self) {} +} diff --git a/core/bin/zksync_forced_exit_requests/src/lib.rs b/core/bin/zksync_forced_exit_requests/src/lib.rs new file mode 100644 index 0000000000..3cf9583b36 --- /dev/null +++ b/core/bin/zksync_forced_exit_requests/src/lib.rs @@ -0,0 +1,83 @@ +use std::{convert::TryFrom, time::Instant}; + +use anyhow::format_err; +use ethabi::{Contract as ContractAbi, Hash}; +use std::fmt::Debug; +use tokio::task::JoinHandle; +use web3::{ + contract::{Contract, Options}, + transports::Http, + types::{BlockNumber, FilterBuilder, Log}, + Web3, +}; +use zksync_config::ZkSyncConfig; +use zksync_storage::ConnectionPool; + +use zksync_contracts::zksync_contract; +use zksync_types::{Address, Nonce, PriorityOp, H160, U256}; + +use zksync_api::core_api_client::CoreApiClient; +use zksync_core::eth_watch::get_contract_events; +use zksync_types::forced_exit_requests::FundsReceivedEvent; + +pub mod eth_watch; +pub mod forced_exit_sender; + +// #[must_use] +// pub fn start_eth_watch( +// config_options: &ZkSyncConfig, +// connection_pool: ConnectionPool, +// ) -> JoinHandle<()> { +// let transport = web3::transports::Http::new(&config_options.eth_client.web3_url).unwrap(); +// let web3 = web3::Web3::new(transport); +// let eth_client = EthHttpClient::new(web3, config_options.contracts.contract_addr); + +// let eth_watch = EthWatch::new( +// eth_client, +// config_options.eth_watch.confirmations_for_eth_event, +// ); + +// tokio::spawn(eth_watch.run(eth_req_receiver)); + +// let poll_interval = config_options.eth_watch.poll_interval(); +// tokio::spawn(async move { +// let mut timer = time::interval(poll_interval); + +// loop { +// timer.tick().await; +// eth_req_sender +// .clone() +// .send(EthWatchRequest::PollETHNode) +// .await +// .expect("ETH watch receiver dropped"); +// } +// }) +// } + +#[must_use] +pub fn run_forced_exit_requests_actors( + pool: ConnectionPool, + config: ZkSyncConfig, +) -> JoinHandle<()> { + let core_api_client = CoreApiClient::new(config.api.private.url.clone()); + let eth_watch_handle = + eth_watch::run_forced_exit_contract_watcher(core_api_client, pool, config); + + eth_watch_handle +} + +/* + +Polling like eth_watch + +If sees a funds_received -> extracts id + +Get_by_id => gets by id + +If sum is enough => set_fullfilled_and_send_tx + + +FE requests consist of 2 (or 3 if needed actors) + + +**/ diff --git a/core/lib/config/src/configs/contracts.rs b/core/lib/config/src/configs/contracts.rs index bd418b3322..beaab32ff5 100644 --- a/core/lib/config/src/configs/contracts.rs +++ b/core/lib/config/src/configs/contracts.rs @@ -16,6 +16,7 @@ pub struct ContractsConfig { pub governance_addr: Address, pub verifier_addr: Address, pub deploy_factory_addr: Address, + pub forced_exit_addr: Address, pub genesis_tx_hash: H256, } @@ -40,6 +41,7 @@ mod tests { governance_addr: addr("5E6D086F5eC079ADFF4FB3774CDf3e8D6a34F7E9"), verifier_addr: addr("DAbb67b676F5b01FcC8997Cc8439846D0d8078ca"), deploy_factory_addr: addr("FC073319977e314F251EAE6ae6bE76B0B3BAeeCF"), + forced_exit_addr: addr("9c7AeE886D6FcFc14e37784f143a6dAccEf50Db7"), genesis_tx_hash: hash( "b99ebfea46cbe05a21cd80fe5597d97b204befc52a16303f579c607dc1ac2e2e", ), @@ -57,6 +59,7 @@ CONTRACTS_CONTRACT_ADDR="0x70a0F165d6f8054d0d0CF8dFd4DD2005f0AF6B55" CONTRACTS_GOVERNANCE_ADDR="0x5E6D086F5eC079ADFF4FB3774CDf3e8D6a34F7E9" CONTRACTS_VERIFIER_ADDR="0xDAbb67b676F5b01FcC8997Cc8439846D0d8078ca" CONTRACTS_DEPLOY_FACTORY_ADDR="0xFC073319977e314F251EAE6ae6bE76B0B3BAeeCF" +CONTRACTS_FORCED_EXIT_ADDR="0x9c7AeE886D6FcFc14e37784f143a6dAccEf50Db7" CONTRACTS_GENESIS_TX_HASH="0xb99ebfea46cbe05a21cd80fe5597d97b204befc52a16303f579c607dc1ac2e2e" "#; set_env(config); diff --git a/core/lib/contracts/src/lib.rs b/core/lib/contracts/src/lib.rs index ae83a41d12..4a937b2ac0 100644 --- a/core/lib/contracts/src/lib.rs +++ b/core/lib/contracts/src/lib.rs @@ -17,6 +17,8 @@ const IEIP1271_CONTRACT_FILE: &str = "contracts/artifacts/cache/solpp-generated-contracts/dev-contracts/IEIP1271.sol/IEIP1271.json"; const UPGRADE_GATEKEEPER_CONTRACT_FILE: &str = "contracts/artifacts/cache/solpp-generated-contracts/UpgradeGatekeeper.sol/UpgradeGatekeeper.json"; +const FORCED_EXIT_CONTRACT_FILE: &str = + "contracts/artifacts/cache/solpp-generated-contracts/ForcedExit.sol/ForcedExit.json"; fn read_file_to_json_value(path: &str) -> io::Result { let zksync_home = std::env::var("ZKSYNC_HOME").unwrap_or_else(|_| ".".into()); @@ -105,3 +107,12 @@ pub fn upgrade_gatekeeper() -> Contract { .to_string(); Contract::load(abi_string.as_bytes()).expect("gatekeeper contract abi") } + +pub fn forced_exit_contract() -> Contract { + let abi_string = read_file_to_json_value(FORCED_EXIT_CONTRACT_FILE) + .expect("couldn't read FORCED_EXIT_CONTRACT_FILE") + .get("abi") + .expect("couldn't get abi from FORCED_EXIT_CONTRACT_FILE") + .to_string(); + Contract::load(abi_string.as_bytes()).expect("forced_exit contract abi") +} diff --git a/core/lib/types/src/forced_exit_requests.rs b/core/lib/types/src/forced_exit_requests.rs index f012fe9e25..677a8c02a9 100644 --- a/core/lib/types/src/forced_exit_requests.rs +++ b/core/lib/types/src/forced_exit_requests.rs @@ -7,6 +7,22 @@ use serde::{Deserialize, Serialize}; pub type ForcedExitRequestId = i64; +use anyhow::{bail, ensure, format_err}; +use ethabi::{decode, ParamType}; +use std::convert::{TryFrom, TryInto}; +use zksync_basic_types::{Log, H256, U256}; +use zksync_crypto::params::{ + ACCOUNT_ID_BIT_WIDTH, BALANCE_BIT_WIDTH, ETH_ADDRESS_BIT_WIDTH, FR_ADDRESS_LEN, + TOKEN_BIT_WIDTH, TX_TYPE_BIT_WIDTH, +}; + +use super::{ + operations::{DepositOp, FullExitOp}, + utils::h256_as_vec, + AccountId, SerialId, +}; +use zksync_crypto::primitives::FromBytes; + #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)] #[serde(rename_all = "camelCase")] pub struct ForcedExitRequest { @@ -27,3 +43,30 @@ pub struct SaveForcedExitRequestQuery { pub price_in_wei: BigUint, pub valid_until: DateTime, } + +pub struct FundsReceivedEvent { + pub amount: u64, +} + +impl TryFrom for FundsReceivedEvent { + type Error = anyhow::Error; + + fn try_from(event: Log) -> Result { + let mut dec_ev = decode( + &[ + ParamType::Uint(256), // amount + ], + &event.data.0, + ) + .map_err(|e| format_err!("Event data decode: {:?}", e))?; + + Ok(FundsReceivedEvent { + amount: dec_ev + .remove(0) + .to_uint() + .as_ref() + .map(U256::as_u64) + .unwrap(), + }) + } +} From da2f306e27bdfe4fdde7edd91fc04003db931d30 Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Sun, 7 Feb 2021 14:13:30 +0200 Subject: [PATCH 21/90] [WIP]: Dummy forced exit sender --- Cargo.lock | 1 + .../zksync_forced_exit_requests/Cargo.toml | 2 +- .../src/eth_watch.rs | 36 ++++-- .../src/forced_exit_sender.rs | 116 ++++++++++++++++-- .../zksync_forced_exit_requests/src/lib.rs | 32 +---- .../src/configs/forced_exit_requests.rs | 3 + core/lib/storage/sqlx-data.json | 13 ++ .../storage/src/forced_exit_requests/mod.rs | 27 +++- etc/env/base/forced_exit_requests.toml | 3 + 9 files changed, 182 insertions(+), 51 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 581308781b..7e46b629cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5895,6 +5895,7 @@ dependencies = [ "actix-web", "anyhow", "async-trait", + "chrono", "env_logger", "ethabi", "log", diff --git a/core/bin/zksync_forced_exit_requests/Cargo.toml b/core/bin/zksync_forced_exit_requests/Cargo.toml index da8cbdaf91..bb706c8740 100644 --- a/core/bin/zksync_forced_exit_requests/Cargo.toml +++ b/core/bin/zksync_forced_exit_requests/Cargo.toml @@ -25,10 +25,10 @@ web3 = "0.13.0" log = "0.4" env_logger = "0.6" metrics = "0.13.0-alpha.8" +chrono = { version = "0.4", features = ["serde", "rustc-serialize"] } tokio = { version = "0.2", features = ["full"] } anyhow = "1.0" async-trait = "0.1" -[dev-dependencies] num = { version = "0.3.1", features = ["serde"] } diff --git a/core/bin/zksync_forced_exit_requests/src/eth_watch.rs b/core/bin/zksync_forced_exit_requests/src/eth_watch.rs index ed37a6810c..a5a6dfa744 100644 --- a/core/bin/zksync_forced_exit_requests/src/eth_watch.rs +++ b/core/bin/zksync_forced_exit_requests/src/eth_watch.rs @@ -28,6 +28,8 @@ use zksync_types::forced_exit_requests::FundsReceivedEvent; /// before repeating the request. const RATE_LIMIT_DELAY: Duration = Duration::from_secs(30); +use super::ForcedExitSender; + struct ContractTopics { pub funds_received: Hash, } @@ -106,6 +108,7 @@ struct ForcedExitContractWatcher { config: ZkSyncConfig, eth_client: EthClient, last_viewed_block: u64, + forced_exit_sender: ForcedExitSender, mode: WatcherMode, } @@ -183,7 +186,9 @@ impl ForcedExitContractWatcher { let events = events.unwrap(); for e in events { - dbg!("An event has come for us: {}", e.amount); + self.forced_exit_sender + .process_request(e.amount as i64) + .await; } self.last_viewed_block = block; @@ -199,16 +204,27 @@ pub fn run_forced_exit_contract_watcher( let web3 = web3::Web3::new(transport); let eth_client = EthClient::new(web3, config.contracts.forced_exit_addr); - let mut contract_watcher = ForcedExitContractWatcher { - core_api_client, - connection_pool, - config, - eth_client, - last_viewed_block: 0, - mode: WatcherMode::Working, - }; - tokio::spawn(async move { + // It is ok to unwrap here, since if fe_sender is not created, then + // the watcher is meaningless + let forced_exit_sender = ForcedExitSender::new( + core_api_client.clone(), + connection_pool.clone(), + config.clone(), + ) + .await + .unwrap(); + + let mut contract_watcher = ForcedExitContractWatcher { + core_api_client, + connection_pool, + config, + eth_client, + last_viewed_block: 0, + forced_exit_sender, + mode: WatcherMode::Working, + }; + let mut timer = time::interval(Duration::from_secs(1)); loop { diff --git a/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs b/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs index b28e7e5ea4..d111d2c45a 100644 --- a/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs +++ b/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs @@ -2,6 +2,7 @@ use std::{convert::TryFrom, time::Instant}; use anyhow::format_err; use ethabi::{Contract as ContractAbi, Hash}; +use num::{BigUint, ToPrimitive}; use std::fmt::Debug; use tokio::task::JoinHandle; use web3::{ @@ -11,28 +12,127 @@ use web3::{ Web3, }; use zksync_config::ZkSyncConfig; -use zksync_storage::ConnectionPool; +use zksync_storage::{ConnectionPool, StorageProcessor}; use zksync_contracts::zksync_contract; -use zksync_types::{Address, Nonce, PriorityOp, H160, U256}; +use zksync_types::{ + forced_exit_requests::ForcedExitRequest, tx::TimeRange, AccountId, Address, Nonce, PriorityOp, + ZkSyncTx, H160, U256, +}; -use zksync_api::core_api_client::CoreApiClient; +use chrono::Utc; +use zksync_api::{core_api_client::CoreApiClient, fee_ticker}; use zksync_core::eth_watch::get_contract_events; use zksync_types::forced_exit_requests::FundsReceivedEvent; +use zksync_types::ForcedExit; +use zksync_types::SignedZkSyncTx; + +use crate::eth_watch; pub struct ForcedExitSender { core_api_client: CoreApiClient, connection_pool: ConnectionPool, - // requests: Receiver + config: ZkSyncConfig, + operator_account_id: AccountId, +} +async fn get_operator_account_id( + connection_pool: ConnectionPool, + config: &ZkSyncConfig, +) -> anyhow::Result { + let mut storage = connection_pool.access_storage().await?; + let mut accounts_schema = storage.chain().account_schema(); + + let account_id = accounts_schema + .account_id_by_address(config.eth_sender.sender.operator_commit_eth_addr) + .await?; + + account_id.ok_or(anyhow::Error::msg("1")) } impl ForcedExitSender { - pub fn new(core_api_client: CoreApiClient, connection_pool: ConnectionPool) -> Self { - Self { + pub async fn new( + core_api_client: CoreApiClient, + connection_pool: ConnectionPool, + config: ZkSyncConfig, + ) -> anyhow::Result { + let operator_account_id = get_operator_account_id(connection_pool.clone(), &config).await?; + + Ok(Self { core_api_client, connection_pool, - } + operator_account_id, + config, + }) + } + + pub fn extract_id_from_amount(&self, amount: i64) -> i64 { + let id_space_size: i64 = + (10 as i64).pow(self.config.forced_exit_requests.digits_in_id as u32); + + amount % id_space_size } - pub async fn run(mut self) {} + // pub async fn construct_forced_exit<'a>( + // &self, + // storage: StorageProcessor<'a>, + // fe_request: ForcedExitRequest + // ) -> anyhow::Result { + + // let account_schema = storage.chain().account_schema(); + + // let operator_state = account_schema.last_committed_state_for_account(self.operator_account_id).await?.expect("The operator account has no committed state"); + // let operator_nonce = operator_state.nonce; + + // // TODO: allow batches + // let tx = ForcedExit::new_signed( + // self.operator_account_id, + // fe_request.target, + // fe_request.tokens[0], + // BigUint::from(0), + // operator_nonce, + // TimeRange::default(), + // self.config.eth_sender.sender.operator_private_key.clone(), + // ).expect("Failed to create signed transaction from ForcedExit"); + + // Ok(SignedZkSyncTx { + // tx: ZkSyncTx::ForcedExit(Box::new(tx)), + // eth_sign_data: None + // }) + // } + + pub async fn process_request(&self, amount: i64) { + let id = self.extract_id_from_amount(amount); + + let mut storage = self + .connection_pool + .access_storage() + .await + .expect("forced_exit_porcess_request"); + + let mut fe_schema = storage.forced_exit_requests_schema(); + + let fe_request = fe_schema.get_request_by_id(id).await; + // The error means that such on id does not exists + // TOOD: Actually handle differently when id does not exist or an actual error + if let Err(_) = fe_request { + return; + } + + let fe_request = fe_request.unwrap(); + + // TODO: take aging into account + if fe_request.id == id && fe_request.price_in_wei.to_i64().unwrap() == amount { + fe_schema + .fulfill_request(id, Utc::now()) + .await + // TODO: Handle such cases gracefully, and not panic + .expect("An error occured, while fu;lfilling the request"); + + log::info!("FE request with id {} was fulfilled", id); + + // let tx = self.construct_forced_exit(storage, fe_request).await.expect("Failed to construct forced exit transaction"); + // TODO: Handle such cases gracefully, and not panic + // self.core_api_client.send_tx(tx).await.expect("An erro occureed, while submitting tx"); + } + } } diff --git a/core/bin/zksync_forced_exit_requests/src/lib.rs b/core/bin/zksync_forced_exit_requests/src/lib.rs index 3cf9583b36..5bc3418246 100644 --- a/core/bin/zksync_forced_exit_requests/src/lib.rs +++ b/core/bin/zksync_forced_exit_requests/src/lib.rs @@ -23,36 +23,7 @@ use zksync_types::forced_exit_requests::FundsReceivedEvent; pub mod eth_watch; pub mod forced_exit_sender; -// #[must_use] -// pub fn start_eth_watch( -// config_options: &ZkSyncConfig, -// connection_pool: ConnectionPool, -// ) -> JoinHandle<()> { -// let transport = web3::transports::Http::new(&config_options.eth_client.web3_url).unwrap(); -// let web3 = web3::Web3::new(transport); -// let eth_client = EthHttpClient::new(web3, config_options.contracts.contract_addr); - -// let eth_watch = EthWatch::new( -// eth_client, -// config_options.eth_watch.confirmations_for_eth_event, -// ); - -// tokio::spawn(eth_watch.run(eth_req_receiver)); - -// let poll_interval = config_options.eth_watch.poll_interval(); -// tokio::spawn(async move { -// let mut timer = time::interval(poll_interval); - -// loop { -// timer.tick().await; -// eth_req_sender -// .clone() -// .send(EthWatchRequest::PollETHNode) -// .await -// .expect("ETH watch receiver dropped"); -// } -// }) -// } +use forced_exit_sender::ForcedExitSender; #[must_use] pub fn run_forced_exit_requests_actors( @@ -60,6 +31,7 @@ pub fn run_forced_exit_requests_actors( config: ZkSyncConfig, ) -> JoinHandle<()> { let core_api_client = CoreApiClient::new(config.api.private.url.clone()); + let eth_watch_handle = eth_watch::run_forced_exit_contract_watcher(core_api_client, pool, config); diff --git a/core/lib/config/src/configs/forced_exit_requests.rs b/core/lib/config/src/configs/forced_exit_requests.rs index d70337d624..3f68070de2 100644 --- a/core/lib/config/src/configs/forced_exit_requests.rs +++ b/core/lib/config/src/configs/forced_exit_requests.rs @@ -17,6 +17,7 @@ struct ForcedExitRequestsInternalConfig { pub max_tokens_per_request: u8, pub recomended_tx_interval: i64, pub tx_interval_scaling_factor: f64, + pub digits_in_id: u8, } #[derive(Debug, Deserialize, Clone, PartialEq)] @@ -26,6 +27,7 @@ pub struct ForcedExitRequestsConfig { pub max_tokens_per_request: u8, pub recomended_tx_interval: i64, pub max_tx_interval: i64, + pub digits_in_id: u8, } impl ForcedExitRequestsConfig { @@ -42,6 +44,7 @@ impl ForcedExitRequestsConfig { max_tokens_per_request: config.max_tokens_per_request, recomended_tx_interval: config.recomended_tx_interval, max_tx_interval: max_tx_interval.round() as i64, + digits_in_id: config.digits_in_id, } } } diff --git a/core/lib/storage/sqlx-data.json b/core/lib/storage/sqlx-data.json index 79181633b9..6661e51e96 100644 --- a/core/lib/storage/sqlx-data.json +++ b/core/lib/storage/sqlx-data.json @@ -594,6 +594,19 @@ ] } }, + "1e491f4afb54c10a9e4f2ea467bd7f219e7a32bdf741691cb6f350d50caae417": { + "query": "\n UPDATE forced_exit_requests\n SET fulfilled_at = $1\n WHERE id = $2\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Timestamptz", + "Int8" + ] + }, + "nullable": [] + } + }, "222e3946401772e3f6e0d9ce9909e8e7ac2dc830c5ecfcd522f56b3bf70fd679": { "query": "INSERT INTO data_restore_storage_state_update (storage_state) VALUES ($1)", "describe": { diff --git a/core/lib/storage/src/forced_exit_requests/mod.rs b/core/lib/storage/src/forced_exit_requests/mod.rs index 1720aef671..6bfd31d9ab 100644 --- a/core/lib/storage/src/forced_exit_requests/mod.rs +++ b/core/lib/storage/src/forced_exit_requests/mod.rs @@ -1,3 +1,4 @@ +use chrono::{DateTime, Utc}; // Built-in deps use num::BigInt; use sqlx::types::BigDecimal; @@ -65,8 +66,6 @@ impl<'a, 'c> ForcedExitRequestsSchema<'a, 'c> { id: ForcedExitRequestId, ) -> QueryResult { let start = Instant::now(); - // Unfortunately there were some bugs with - // sqlx macros, so just have to resort to the old way let request: DbForcedExitRequest = sqlx::query_as!( DbForcedExitRequest, r#" @@ -87,4 +86,28 @@ impl<'a, 'c> ForcedExitRequestsSchema<'a, 'c> { Ok(request) } + + pub async fn fulfill_request( + &mut self, + id: ForcedExitRequestId, + fulfilled_at: DateTime, + ) -> QueryResult<()> { + let start = Instant::now(); + + sqlx::query!( + r#" + UPDATE forced_exit_requests + SET fulfilled_at = $1 + WHERE id = $2 + "#, + fulfilled_at, + id + ) + .execute(self.0.conn()) + .await?; + + metrics::histogram!("sql.forced_exit_requests.fulfill_request", start.elapsed()); + + Ok(()) + } } diff --git a/etc/env/base/forced_exit_requests.toml b/etc/env/base/forced_exit_requests.toml index 5ef8fc5f17..e7324578a2 100644 --- a/etc/env/base/forced_exit_requests.toml +++ b/etc/env/base/forced_exit_requests.toml @@ -16,3 +16,6 @@ recomended_tx_interval=300 # How many times the maximum acceptable interval will be longer # than the recommended interval tx_interval_scaling_factor=1.5 + +# Number of digits in id +digits_in_id=13 From e72600aca673bbede7a3ee6a4a8265e47a438cf5 Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Sun, 7 Feb 2021 14:35:20 +0200 Subject: [PATCH 22/90] [WIP]: Take the fee for a request from env --- .../rest/forced_exit_requests/v01.rs | 49 ++++--------------- .../src/configs/forced_exit_requests.rs | 6 +-- etc/env/base/forced_exit_requests.toml | 7 ++- 3 files changed, 16 insertions(+), 46 deletions(-) diff --git a/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs b/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs index 26b9cff6e2..d7a0ad9744 100644 --- a/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs +++ b/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs @@ -51,10 +51,10 @@ pub struct ApiForcedExitRequestsData { pub(crate) ticker_request_sender: mpsc::Sender, pub(crate) is_enabled: bool, - pub(crate) price_scaling_factor: BigDecimal, pub(crate) max_tokens_per_request: u8, pub(crate) recomended_tx_interval_millisecs: i64, pub(crate) max_tx_interval_millisecs: i64, + pub(crate) price_per_token: i64, } impl ApiForcedExitRequestsData { @@ -70,10 +70,7 @@ impl ApiForcedExitRequestsData { ticker_request_sender, is_enabled: config.forced_exit_requests.enabled, - price_scaling_factor: BigDecimal::from_f64( - config.forced_exit_requests.price_scaling_factor, - ) - .unwrap(), + price_per_token: config.forced_exit_requests.price_per_token, max_tokens_per_request: config.forced_exit_requests.max_tokens_per_request, recomended_tx_interval_millisecs: config.forced_exit_requests.recomended_tx_interval, max_tx_interval_millisecs: config.forced_exit_requests.max_tx_interval, @@ -90,11 +87,7 @@ async fn get_status( let response = if data.is_enabled { ForcedExitRequestStatus::Enabled(ConfigInfo { - request_fee: get_fee_for_one_forced_exit( - data.ticker_request_sender.clone(), - data.price_scaling_factor.clone(), - ) - .await?, + request_fee: BigUint::from(data.price_per_token as u64), max_tokens_per_request: data.max_tokens_per_request, recomended_tx_interval_millis: data.recomended_tx_interval_millisecs, }) @@ -106,24 +99,6 @@ async fn get_status( Ok(Json(response)) } -async fn get_fee_for_one_forced_exit( - ticker_request_sender: mpsc::Sender, - price_scaling_factor: BigDecimal, -) -> Result { - let price = ticker_request( - ticker_request_sender.clone(), - TxFeeTypes::Withdraw, - TokenLike::Id(0), - ) - .await?; - let price = BigDecimal::from(price.total_fee.to_bigint().unwrap()); - - let scaled_price = price * price_scaling_factor; - let scaled_price = scaled_price.round(0).to_bigint().unwrap(); - - Ok(scaled_price.to_biguint().unwrap()) -} - pub async fn submit_request( data: web::Data, params: web::Json, @@ -146,12 +121,7 @@ pub async fn submit_request( .await .map_err(ApiError::from)?; - let price_of_one_exit = get_fee_for_one_forced_exit( - data.ticker_request_sender.clone(), - data.price_scaling_factor.clone(), - ) - .await - .map_err(ApiError::from)?; + let price_of_one_exit = BigDecimal::from(data.price_per_token); let price_of_one_exit = BigDecimal::from(price_of_one_exit.to_bigint().unwrap()); let price_of_request = price_of_one_exit * BigDecimal::from_usize(params.tokens.len()).unwrap(); @@ -406,19 +376,20 @@ mod tests { async fn test_forced_exit_requests_get_fee() -> anyhow::Result<()> { let forced_exit_requests = ForcedExitRequestsConfig::from_env(); let test_config = get_test_config_from_forced_exit_requests(ForcedExitRequestsConfig { - price_scaling_factor: 1.5, + price_per_token: 1000000000, ..forced_exit_requests }); - let (client, server) = - TestServer::new_with_fee_ticker(test_config, Some(10000), Some(10000)).await?; + let (client, server) = TestServer::new_with_config(test_config).await?; let status = client.get_forced_exit_requests_status().await?; match status { ForcedExitRequestStatus::Enabled(config_info) => { - // 10000 * 1.5 = 15000 - assert_eq!(config_info.request_fee, BigUint::from_u32(30000).unwrap()); + assert_eq!( + config_info.request_fee, + BigUint::from_u32(1000000000).unwrap() + ); } ForcedExitRequestStatus::Disabled => { panic!("ForcedExitRequests feature is not disabled"); diff --git a/core/lib/config/src/configs/forced_exit_requests.rs b/core/lib/config/src/configs/forced_exit_requests.rs index 3f68070de2..1874607c04 100644 --- a/core/lib/config/src/configs/forced_exit_requests.rs +++ b/core/lib/config/src/configs/forced_exit_requests.rs @@ -13,20 +13,20 @@ use serde::Deserialize; #[derive(Debug, Deserialize, Clone, PartialEq)] struct ForcedExitRequestsInternalConfig { pub enabled: bool, - pub price_scaling_factor: f64, pub max_tokens_per_request: u8, pub recomended_tx_interval: i64, pub tx_interval_scaling_factor: f64, + pub price_per_token: i64, pub digits_in_id: u8, } #[derive(Debug, Deserialize, Clone, PartialEq)] pub struct ForcedExitRequestsConfig { pub enabled: bool, - pub price_scaling_factor: f64, pub max_tokens_per_request: u8, pub recomended_tx_interval: i64, pub max_tx_interval: i64, + pub price_per_token: i64, pub digits_in_id: u8, } @@ -40,11 +40,11 @@ impl ForcedExitRequestsConfig { ForcedExitRequestsConfig { enabled: config.enabled, - price_scaling_factor: config.price_scaling_factor, max_tokens_per_request: config.max_tokens_per_request, recomended_tx_interval: config.recomended_tx_interval, max_tx_interval: max_tx_interval.round() as i64, digits_in_id: config.digits_in_id, + price_per_token: config.price_per_token, } } } diff --git a/etc/env/base/forced_exit_requests.toml b/etc/env/base/forced_exit_requests.toml index e7324578a2..f2e0752a26 100644 --- a/etc/env/base/forced_exit_requests.toml +++ b/etc/env/base/forced_exit_requests.toml @@ -4,10 +4,6 @@ # in times of attacks or upgrages enabled=true -# The price that will be demanded from a user is equal to -# price_for_withdrawal * price_scaling -price_scaling_factor=1.6 - max_tokens_per_request=10 # Recommended interval to send the transaction in milliseconds @@ -19,3 +15,6 @@ tx_interval_scaling_factor=1.5 # Number of digits in id digits_in_id=13 + +# Price per exit in wei (currently it's 0.03 ETH) +price_per_token=30000000000000000 From 3888cae3f86020774e44001ff0bfcb7b20b604d4 Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Mon, 8 Feb 2021 14:06:02 +0200 Subject: [PATCH 23/90] [WIP]: eth_watcher --- Cargo.lock | 3 + core/bin/server/src/main.rs | 46 +-- .../rest/forced_exit_requests/v01.rs | 49 ++- .../src/api_server/rest/v1/error.rs | 4 + .../zksync_forced_exit_requests/Cargo.toml | 5 + .../src/database.rs | 0 .../src/eth_watch.rs | 137 +++++++- .../src/forced_exit_sender.rs | 310 ++++++++++++++---- .../zksync_forced_exit_requests/src/lib.rs | 5 - .../zksync_forced_exit_requests/src/utils.rs | 3 + .../src/rest/forced_exit_requests/mod.rs | 1 + .../src/configs/forced_exit_requests.rs | 10 + .../up.sql | 3 +- core/lib/storage/sqlx-data.json | 142 ++++++-- .../storage/src/forced_exit_requests/mod.rs | 54 ++- .../src/forced_exit_requests/records.rs | 3 + core/lib/types/src/forced_exit_requests.rs | 18 +- etc/env/base/forced_exit_requests.toml | 6 + etc/env/base/private.toml | 4 + etc/env/base/rust.toml | 3 +- 20 files changed, 639 insertions(+), 167 deletions(-) create mode 100644 core/bin/zksync_forced_exit_requests/src/database.rs create mode 100644 core/bin/zksync_forced_exit_requests/src/utils.rs diff --git a/Cargo.lock b/Cargo.lock index 7e46b629cf..c46028c86d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5898,6 +5898,8 @@ dependencies = [ "chrono", "env_logger", "ethabi", + "franklin-crypto", + "hex", "log", "metrics", "num", @@ -5907,6 +5909,7 @@ dependencies = [ "zksync_config", "zksync_contracts", "zksync_core", + "zksync_crypto", "zksync_storage", "zksync_types", ] diff --git a/core/bin/server/src/main.rs b/core/bin/server/src/main.rs index 7a32ed9bc0..4c0ee9d28d 100644 --- a/core/bin/server/src/main.rs +++ b/core/bin/server/src/main.rs @@ -83,31 +83,31 @@ async fn main() -> anyhow::Result<()> { run_prover_server(database, stop_signal_sender, ZkSyncConfig::from_env()); log::info!("Starting the ForcedExitRequests actors"); - let forced_exit_requests_task_handle = run_forced_exit_requests_actors(connection_pool, config); + //let forced_exit_requests_task_handle = run_forced_exit_requests_actors(connection_pool, config); tokio::select! { - _ = async { wait_for_tasks(core_task_handles).await } => { - // We don't need to do anything here, since Core actors will panic upon future resolving. - }, - _ = async { api_task_handle.await } => { - panic!("API server actors aren't supposed to finish their execution") - }, - _ = async { eth_sender_task_handle.await } => { - panic!("Ethereum Sender actors aren't supposed to finish their execution") - }, - _ = async { prometheus_task_handle.await } => { - panic!("Prometheus exporter actors aren't supposed to finish their execution") - }, - _ = async { counter_task_handle.unwrap().await } => { - panic!("Operation counting actor is not supposed to finish its execution") - }, - _ = async { forced_exit_requests_task_handle.await } => { - panic!("ForcedExitRequests actor is not supposed to finish its execution") - }, - _ = async { stop_signal_receiver.next().await } => { - log::warn!("Stop signal received, shutting down"); - } - }; + _ = async { wait_for_tasks(core_task_handles).await } => { + // We don't need to do anything here, since Core actors will panic upon future resolving. + }, + _ = async { api_task_handle.await } => { + panic!("API server actors aren't supposed to finish their execution") + }, + _ = async { eth_sender_task_handle.await } => { + panic!("Ethereum Sender actors aren't supposed to finish their execution") + }, + _ = async { prometheus_task_handle.await } => { + panic!("Prometheus exporter actors aren't supposed to finish their execution") + }, + _ = async { counter_task_handle.unwrap().await } => { + panic!("Operation counting actor is not supposed to finish its execution") + }, + // _ = async { forced_exit_requests_task_handle.await } => { + // panic!("ForcedExitRequests actor is not supposed to finish its execution") + // }, + _ = async { stop_signal_receiver.next().await } => { + log::warn!("Stop signal received, shutting down"); + } + }; Ok(()) } diff --git a/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs b/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs index d7a0ad9744..59efdd85f5 100644 --- a/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs +++ b/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs @@ -28,7 +28,7 @@ pub use zksync_api_client::rest::v1::{ use zksync_config::ZkSyncConfig; use zksync_storage::ConnectionPool; use zksync_types::{ - forced_exit_requests::{ForcedExitRequest, SaveForcedExitRequestQuery}, + forced_exit_requests::{ForcedExitRequest, ForcedExitRequestId, SaveForcedExitRequestQuery}, TokenLike, TxFeeTypes, }; @@ -105,14 +105,8 @@ pub async fn submit_request( ) -> JsonResult { let start = Instant::now(); - if !data.is_enabled { - return Err(ApiError::bad_request( - "ForcedExit requests feature is disabled!", - )); - } - let mut storage = data.connection_pool.access_storage().await.map_err(|err| { - vlog::warn!("Internal Server Error: '{}';", err); + log::warn!("Internal Server Error: '{}';", err); return ApiError::internal(""); })?; @@ -122,7 +116,6 @@ pub async fn submit_request( .map_err(ApiError::from)?; let price_of_one_exit = BigDecimal::from(data.price_per_token); - let price_of_one_exit = BigDecimal::from(price_of_one_exit.to_bigint().unwrap()); let price_of_request = price_of_one_exit * BigDecimal::from_usize(params.tokens.len()).unwrap(); let user_fee = params.price_in_wei.to_bigint().unwrap(); @@ -170,10 +163,42 @@ pub async fn submit_request( return ApiError::internal(""); })?; - metrics::histogram!("api.v01.register_forced_exit_request", start.elapsed()); + metrics::histogram!( + "api.forced_exit_requests.v01.submit_request", + start.elapsed() + ); Ok(Json(saved_fe_request)) } +pub async fn get_request_by_id( + data: web::Data, + web::Path(request_id): web::Path, +) -> JsonResult { + let start = Instant::now(); + + let mut storage = data.connection_pool.access_storage().await.map_err(|err| { + vlog::warn!("Internal Server Error: '{}';", err); + return ApiError::internal(""); + })?; + + let mut fe_requests_schema = storage.forced_exit_requests_schema(); + + metrics::histogram!( + "api.forced_exit_requests.v01.get_request_by_id", + start.elapsed() + ); + + let fe_request_from_db = fe_requests_schema + .get_request_by_id(request_id) + .await + .map_err(ApiError::internal)?; + + match fe_request_from_db { + Some(fe_request) => Ok(Json(fe_request)), + None => Err(ApiError::not_found("Request with such id does not exist")), + } +} + pub fn api_scope( connection_pool: ConnectionPool, config: &ZkSyncConfig, @@ -187,7 +212,9 @@ pub fn api_scope( .route("status", web::get().to(get_status)); if config.forced_exit_requests.enabled { - scope.route("submit", web::post().to(submit_request)) + scope + .route("/submit", web::post().to(submit_request)) + .route("/requests/{id}", web::get().to(get_request_by_id)) } else { scope } diff --git a/core/bin/zksync_api/src/api_server/rest/v1/error.rs b/core/bin/zksync_api/src/api_server/rest/v1/error.rs index 83fa29c9f9..d79529f44b 100644 --- a/core/bin/zksync_api/src/api_server/rest/v1/error.rs +++ b/core/bin/zksync_api/src/api_server/rest/v1/error.rs @@ -35,6 +35,10 @@ impl Error { Self::with_code(StatusCode::NOT_IMPLEMENTED, title) } + pub fn not_found(title: impl Display) -> Self { + Self::with_code(StatusCode::NOT_FOUND, title) + } + fn with_code(http_code: StatusCode, title: impl Display) -> Self { Self { http_code, diff --git a/core/bin/zksync_forced_exit_requests/Cargo.toml b/core/bin/zksync_forced_exit_requests/Cargo.toml index bb706c8740..6fdc8e0a4a 100644 --- a/core/bin/zksync_forced_exit_requests/Cargo.toml +++ b/core/bin/zksync_forced_exit_requests/Cargo.toml @@ -17,12 +17,17 @@ zksync_storage = { path = "../../lib/storage", version = "1.0" } zksync_config = { path = "../../lib/config", version = "1.0" } zksync_contracts = { path = "../../lib/contracts", version = "1.0" } +zksync_crypto = { path = "../../lib/crypto", version = "1.0" } + +franklin_crypto = { package = "franklin-crypto", version = "0.0.5", git = "https://github.com/matter-labs/franklin-crypto.git", branch="beta", features = ["multicore", "plonk"]} + zksync_core = { path = "../zksync_core", version = "1.0" } zksync_api = { path = "../zksync_api", version = "1.0" } actix-web = "3.0.0" ethabi = "12.0.0" web3 = "0.13.0" log = "0.4" +hex = "0.4" env_logger = "0.6" metrics = "0.13.0-alpha.8" chrono = { version = "0.4", features = ["serde", "rustc-serialize"] } diff --git a/core/bin/zksync_forced_exit_requests/src/database.rs b/core/bin/zksync_forced_exit_requests/src/database.rs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/core/bin/zksync_forced_exit_requests/src/eth_watch.rs b/core/bin/zksync_forced_exit_requests/src/eth_watch.rs index a5a6dfa744..50ee24e09d 100644 --- a/core/bin/zksync_forced_exit_requests/src/eth_watch.rs +++ b/core/bin/zksync_forced_exit_requests/src/eth_watch.rs @@ -1,11 +1,12 @@ use actix_web::client; use anyhow::format_err; +use chrono::{DateTime, Utc}; use ethabi::{Contract as ContractAbi, Hash}; -use std::fmt::Debug; use std::{ convert::TryFrom, time::{Duration, Instant}, }; +use std::{convert::TryInto, fmt::Debug}; use tokio::task::JoinHandle; use tokio::time; use web3::{ @@ -97,7 +98,7 @@ impl EthClient { result } - async fn get_block_number(&self) -> anyhow::Result { + async fn block_number(&self) -> anyhow::Result { get_web3_block_number(&self.web3).await } } @@ -113,7 +114,72 @@ struct ForcedExitContractWatcher { mode: WatcherMode, } +fn dummy_get_min() -> i64 { + 1 +} + +// Usually blocks are created much slower (at rate 1 block per 10-20s), +// but the block time falls through time, so just to double-check +const MILLIS_PER_BLOCK: i64 = 7000; + +// Returns number of blocks that should have been created during the time +fn time_range_to_block_diff(from: DateTime, to: DateTime) -> u64 { + let millis_from = from.timestamp_millis(); + let millis_to = to.timestamp_millis(); + + // It does not really matter to wether cail or floor the division + return ((millis_to - millis_from) / MILLIS_PER_BLOCK) + .try_into() + .unwrap(); +} + +// clean the db from txs being older than ... +fn clean() {} + impl ForcedExitContractWatcher { + async fn restore_state_from_eth(&mut self, block: u64) -> anyhow::Result<()> { + //let last_block = self.eth_client.get_block_number().await.expect("Failed to restore "); + + let mut storage = self.connection_pool.access_storage().await?; + let mut fe_schema = storage.forced_exit_requests_schema(); + + let oldest_request = fe_schema.get_oldest_unfulfilled_request().await?; + + let wait_confirmations: u64 = self + .config + .forced_exit_requests + .wait_confirmations + .try_into() + .unwrap(); + + // No oldest requests means that there are no requests that were possibly ignored + let oldest_request = match oldest_request { + Some(r) => r, + None => { + self.last_viewed_block = block - wait_confirmations; + return Ok(()); + } + }; + + let block_diff = time_range_to_block_diff(oldest_request.created_at, Utc::now()); + let max_possible_viewed_block = block - wait_confirmations; + + self.last_viewed_block = std::cmp::min(block - block_diff, max_possible_viewed_block); + /* + blocks = time_diff_to_blocks = + + last_processed_block = block - blocks + + + comes a tx => check that it's valid + comes a tx => check that the id hasn't already been added to the fulfilled db + if everything is finve => add the tx + + once the block is processed, remove everything too old and unfulfilled and move on + */ + Ok(()) + } + // TODO try to move it to eth client fn is_backoff_requested(&self, error: &anyhow::Error) -> bool { error.to_string().contains("429 Too Many Requests") @@ -158,25 +224,36 @@ impl ForcedExitContractWatcher { } pub async fn poll(&mut self) { - let current_block = self.eth_client.get_block_number().await; - if !self.polling_allowed() { // Polling is currently disabled, skip it. return; } - if let Err(error) = current_block { + let last_block = self.eth_client.block_number().await; + + if let Err(error) = last_block { self.handle_infura_error(error); return; } - let block = current_block.unwrap(); - if self.last_viewed_block >= block { + + let wait_confirmations: u64 = self + .config + .forced_exit_requests + .wait_confirmations + .try_into() + .unwrap(); + + let last_block = last_block.unwrap(); + + let last_confirmed_block = last_block - wait_confirmations; + + if last_confirmed_block <= self.last_viewed_block { return; } let events = self .eth_client - .get_funds_received_events(self.last_viewed_block + 1, block) + .get_funds_received_events(self.last_viewed_block + 1, last_confirmed_block) .await; if let Err(error) = events { @@ -191,7 +268,42 @@ impl ForcedExitContractWatcher { .await; } - self.last_viewed_block = block; + self.last_viewed_block = last_block; + } + + pub async fn run(mut self) { + // As infura may be not responsive, we want to retry the query until we've actually got the + // block number. + // Normally, however, this loop is not expected to last more than one iteration. + let block = loop { + let block = self.eth_client.block_number().await; + + match block { + Ok(block) => { + break block; + } + Err(error) => { + log::warn!( + "Unable to fetch last block number: '{}'. Retrying again in {} seconds", + error, + RATE_LIMIT_DELAY.as_secs() + ); + + time::delay_for(RATE_LIMIT_DELAY).await; + } + } + }; + + self.restore_state_from_eth(block) + .await + .expect("Failed to restore state for ForcedExit eth_watcher"); + + let mut timer = time::interval(Duration::from_secs(1)); + + loop { + timer.tick().await; + self.poll().await; + } } } @@ -225,11 +337,6 @@ pub fn run_forced_exit_contract_watcher( mode: WatcherMode::Working, }; - let mut timer = time::interval(Duration::from_secs(1)); - - loop { - timer.tick().await; - contract_watcher.poll().await; - } + contract_watcher.run().await; }) } diff --git a/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs b/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs index d111d2c45a..cd15574460 100644 --- a/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs +++ b/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs @@ -2,7 +2,8 @@ use std::{convert::TryFrom, time::Instant}; use anyhow::format_err; use ethabi::{Contract as ContractAbi, Hash}; -use num::{BigUint, ToPrimitive}; +use fee_ticker::validator::watcher; +use num::{BigUint, FromPrimitive, ToPrimitive}; use std::fmt::Debug; use tokio::task::JoinHandle; use web3::{ @@ -12,21 +13,37 @@ use web3::{ Web3, }; use zksync_config::ZkSyncConfig; -use zksync_storage::{ConnectionPool, StorageProcessor}; +use zksync_storage::{ + chain::{account::AccountSchema, operations_ext::records::TxReceiptResponse}, + ConnectionPool, StorageProcessor, +}; use zksync_contracts::zksync_contract; use zksync_types::{ - forced_exit_requests::ForcedExitRequest, tx::TimeRange, AccountId, Address, Nonce, PriorityOp, - ZkSyncTx, H160, U256, + forced_exit_requests::ForcedExitRequest, tx::TimeRange, tx::TxHash, AccountId, Address, Nonce, + PriorityOp, TokenId, ZkSyncTx, H160, U256, }; use chrono::Utc; -use zksync_api::{core_api_client::CoreApiClient, fee_ticker}; +use zksync_api::{api_server::rpc_server, core_api_client::CoreApiClient, fee_ticker}; use zksync_core::eth_watch::get_contract_events; use zksync_types::forced_exit_requests::FundsReceivedEvent; use zksync_types::ForcedExit; use zksync_types::SignedZkSyncTx; +use franklin_crypto::{ + alt_babyjubjub::fs::FsRepr, + bellman::{pairing::bn256, PrimeFieldRepr}, +}; +use zksync_crypto::franklin_crypto::{eddsa::PrivateKey, jubjub::JubjubEngine}; + +pub type Engine = bn256::Bn256; + +pub type Fr = bn256::Fr; +pub type Fs = ::Fs; + +use zksync_crypto::ff::PrimeField; + use crate::eth_watch; pub struct ForcedExitSender { @@ -34,6 +51,7 @@ pub struct ForcedExitSender { connection_pool: ConnectionPool, config: ZkSyncConfig, operator_account_id: AccountId, + sender_private_key: PrivateKey, } async fn get_operator_account_id( connection_pool: ConnectionPool, @@ -49,19 +67,43 @@ async fn get_operator_account_id( account_id.ok_or(anyhow::Error::msg("1")) } +// A dummy tmp function +fn send_to_mempool(account_id: AccountId, token: TokenId) { + let msg = format!( + "The following tx was sent to mempool {} {}", + account_id, token + ); + dbg!(msg); +} + +fn read_signing_key(private_key: &[u8]) -> anyhow::Result> { + let mut fs_repr = FsRepr::default(); + fs_repr.read_be(private_key)?; + Ok(PrivateKey::( + Fs::from_repr(fs_repr).expect("couldn't read private key from repr"), + )) +} + impl ForcedExitSender { pub async fn new( core_api_client: CoreApiClient, connection_pool: ConnectionPool, config: ZkSyncConfig, ) -> anyhow::Result { - let operator_account_id = get_operator_account_id(connection_pool.clone(), &config).await?; + let operator_account_id = config.forced_exit_requests.sender_account_id; + + let sender_private_key = + hex::decode(config.clone().forced_exit_requests.sender_private_key) + .expect("Decoding private key failed"); + let sender_private_key = + read_signing_key(&sender_private_key).expect("Reading private key failed"); Ok(Self { core_api_client, connection_pool, operator_account_id, config, + sender_private_key, }) } @@ -72,67 +114,221 @@ impl ForcedExitSender { amount % id_space_size } - // pub async fn construct_forced_exit<'a>( - // &self, - // storage: StorageProcessor<'a>, - // fe_request: ForcedExitRequest - // ) -> anyhow::Result { - - // let account_schema = storage.chain().account_schema(); - - // let operator_state = account_schema.last_committed_state_for_account(self.operator_account_id).await?.expect("The operator account has no committed state"); - // let operator_nonce = operator_state.nonce; - - // // TODO: allow batches - // let tx = ForcedExit::new_signed( - // self.operator_account_id, - // fe_request.target, - // fe_request.tokens[0], - // BigUint::from(0), - // operator_nonce, - // TimeRange::default(), - // self.config.eth_sender.sender.operator_private_key.clone(), - // ).expect("Failed to create signed transaction from ForcedExit"); - - // Ok(SignedZkSyncTx { - // tx: ZkSyncTx::ForcedExit(Box::new(tx)), - // eth_sign_data: None - // }) - // } + pub async fn construct_forced_exit<'a>( + &self, + storage: &mut StorageProcessor<'a>, + fe_request: ForcedExitRequest, + ) -> anyhow::Result { + let mut account_schema = storage.chain().account_schema(); - pub async fn process_request(&self, amount: i64) { - let id = self.extract_id_from_amount(amount); + let operator_state = account_schema + .last_committed_state_for_account(self.operator_account_id) + .await? + .expect("The operator account has no committed state"); + let operator_nonce = operator_state.nonce; - let mut storage = self - .connection_pool - .access_storage() - .await - .expect("forced_exit_porcess_request"); + // TODO: allow batches + let tx = ForcedExit::new_signed( + self.operator_account_id, + fe_request.target, + fe_request.tokens[0], + BigUint::from(0u32), + operator_nonce, + TimeRange::default(), + &self.sender_private_key, + ) + .expect("Failed to create signed transaction from ForcedExit"); - let mut fe_schema = storage.forced_exit_requests_schema(); + Ok(SignedZkSyncTx { + tx: ZkSyncTx::ForcedExit(Box::new(tx)), + eth_sign_data: None, + }) + } - let fe_request = fe_schema.get_request_by_id(id).await; - // The error means that such on id does not exists - // TOOD: Actually handle differently when id does not exist or an actual error - if let Err(_) = fe_request { - return; + // TODO: take the block timestamp into account instead of + // the now + pub fn expired(&self, request: &ForcedExitRequest) -> bool { + let now_millis = Utc::now().timestamp_millis(); + let created_at_millis = request.created_at.timestamp_millis(); + + return now_millis.saturating_sub(created_at_millis) + <= self.config.forced_exit_requests.max_tx_interval; + } + + // Returns the id the request if it should be fulfilled, + // error otherwise + pub fn verify_request( + &self, + amount: i64, + request: Option, + ) -> anyhow::Result { + let request = match request { + Some(r) => r, + None => { + return Err(anyhow::Error::msg("The request was not found")); + } + }; + + if self.expired(&request) { + return Err(anyhow::Error::msg("The request was not found")); } - let fe_request = fe_request.unwrap(); + if request.price_in_wei != BigUint::from_i64(amount).unwrap() { + return Err(anyhow::Error::msg("The request was not found")); + } - // TODO: take aging into account - if fe_request.id == id && fe_request.price_in_wei.to_i64().unwrap() == amount { - fe_schema - .fulfill_request(id, Utc::now()) - .await - // TODO: Handle such cases gracefully, and not panic - .expect("An error occured, while fu;lfilling the request"); + return Ok(request); + } + + pub async fn get_request_by_id<'a>( + &self, + storage: &mut StorageProcessor<'a>, + id: i64, + ) -> anyhow::Result> { + let mut fe_schema = storage.forced_exit_requests_schema(); + + let request = fe_schema.get_request_by_id(id).await?; + // { + // Ok(r) => r, + // Err(e) => { + // log::warn!("ForcedExitRequests: Fail to get request by id: {}", e); + // return; + // } + // }; + + Ok(request) + } - log::info!("FE request with id {} was fulfilled", id); + pub async fn fulfill_request<'a>( + &self, + storage: &mut StorageProcessor<'a>, + id: i64, + ) -> anyhow::Result<()> { + let mut fe_schema = storage.forced_exit_requests_schema(); - // let tx = self.construct_forced_exit(storage, fe_request).await.expect("Failed to construct forced exit transaction"); + fe_schema + .fulfill_request(id, Utc::now()) + .await // TODO: Handle such cases gracefully, and not panic - // self.core_api_client.send_tx(tx).await.expect("An erro occureed, while submitting tx"); + .expect("An error occured, while fu;lfilling the request"); + + log::info!("FE request with id {} was fulfilled", id); + + Ok(()) + } + + pub async fn get_receipt<'a>( + &self, + storage: &mut StorageProcessor<'a>, + tx_hash: TxHash, + ) -> anyhow::Result> { + storage + .chain() + .operations_ext_schema() + .tx_receipt(tx_hash.as_ref()) + .await + } + + pub async fn wait_until_comitted<'a>( + &self, + storage: &mut StorageProcessor<'a>, + tx_hash: TxHash, + ) -> anyhow::Result<()> { + let poll_interval: i32 = 200; + + // If there is no receipt for 20 seconds, we consider the comitment failed + let timeout: i32 = 60000; + let mut time_passed: i32 = 0; + + loop { + if time_passed >= timeout { + panic!("Comitting tx failed!"); + } + + let receipt = self.get_receipt(storage, tx_hash).await?; + + if let Some(tx_receipt) = receipt { + if tx_receipt.success { + return Ok(()); + } else { + panic!("FE Transaction failed") + } + } } } + + pub async fn process_request(&self, amount: i64) { + let id = self.extract_id_from_amount(amount); + + let mut storage = match self.connection_pool.access_storage().await { + Ok(storage) => storage, + Err(error) => { + log::warn!("Failed to acquire db connection for processing forced_exit_request, reason: {}", error); + return; + } + }; + + let fe_request = self + .get_request_by_id(&mut storage, id) + .await + .expect("Failed to get request by id"); + + let fe_request = match self.verify_request(amount, fe_request) { + Ok(r) => r, + Err(_) => { + // The request was not valid, that's fine + return; + } + }; + + let fe_tx = self + .construct_forced_exit(&mut storage, fe_request) + .await + .expect("Failed to construct ForcedExit"); + let tx_hash = fe_tx.hash(); + + self.core_api_client + .send_tx(fe_tx) + .await + .expect("Failed to send transaction to mempool") + .unwrap(); + + self.wait_until_comitted(&mut storage, tx_hash) + .await + .expect("Comittment waiting failed"); + + self.fulfill_request(&mut storage, id) + .await + .expect("Error while fulfulling the request"); + + // let db_transaction = match fe_schema.0.start_transaction().await { + // Ok(transaction ) => transaction, + // Err(error) => { + // log::warn!("Failed to start db transaction for processing forced_exit_requests, reason {}", error); + // return; + // } + // }; + + // send_to_mempool(); + // await until its committed + + // db_transaction + // .fe_schema() + // .fulfill_request() + + // let fe_request = fe_schema.get_request_by_id(id).await; + // // The error means that such on id does not exists + // // TOOD: Actually handle differently when id does not exist or an actual error + // if let Err(_) = fe_request { + // return; + // } + + // let fe_request = fe_request.unwrap().unwrap(); + + // TODO: take aging into account + + // let tx = self.construct_forced_exit(storage, fe_request).await.expect("Failed to construct forced exit transaction"); + // TODO: Handle such cases gracefully, and not panic + // self.core_api_client.send_tx(tx).await.expect("An erro occureed, while submitting tx"); + } } diff --git a/core/bin/zksync_forced_exit_requests/src/lib.rs b/core/bin/zksync_forced_exit_requests/src/lib.rs index 5bc3418246..a78e83959f 100644 --- a/core/bin/zksync_forced_exit_requests/src/lib.rs +++ b/core/bin/zksync_forced_exit_requests/src/lib.rs @@ -13,12 +13,7 @@ use web3::{ use zksync_config::ZkSyncConfig; use zksync_storage::ConnectionPool; -use zksync_contracts::zksync_contract; -use zksync_types::{Address, Nonce, PriorityOp, H160, U256}; - use zksync_api::core_api_client::CoreApiClient; -use zksync_core::eth_watch::get_contract_events; -use zksync_types::forced_exit_requests::FundsReceivedEvent; pub mod eth_watch; pub mod forced_exit_sender; diff --git a/core/bin/zksync_forced_exit_requests/src/utils.rs b/core/bin/zksync_forced_exit_requests/src/utils.rs new file mode 100644 index 0000000000..b28b04f643 --- /dev/null +++ b/core/bin/zksync_forced_exit_requests/src/utils.rs @@ -0,0 +1,3 @@ + + + diff --git a/core/lib/api_client/src/rest/forced_exit_requests/mod.rs b/core/lib/api_client/src/rest/forced_exit_requests/mod.rs index 970f379adb..47348d20f3 100644 --- a/core/lib/api_client/src/rest/forced_exit_requests/mod.rs +++ b/core/lib/api_client/src/rest/forced_exit_requests/mod.rs @@ -35,6 +35,7 @@ pub enum ForcedExitRequestStatus { pub struct ForcedExitRegisterRequest { pub target: Address, pub tokens: Vec, + // We still gotta specify that, since the price might change #[serde(with = "BigUintSerdeAsRadix10Str")] pub price_in_wei: BigUint, } diff --git a/core/lib/config/src/configs/forced_exit_requests.rs b/core/lib/config/src/configs/forced_exit_requests.rs index 1874607c04..00d5c21159 100644 --- a/core/lib/config/src/configs/forced_exit_requests.rs +++ b/core/lib/config/src/configs/forced_exit_requests.rs @@ -1,6 +1,7 @@ use crate::envy_load; /// External uses use serde::Deserialize; +use zksync_types::AccountId; // There are two types of configs: // The original one (with tx_interval_scaling_factor) @@ -18,6 +19,9 @@ struct ForcedExitRequestsInternalConfig { pub tx_interval_scaling_factor: f64, pub price_per_token: i64, pub digits_in_id: u8, + pub wait_confirmations: i64, + pub sender_private_key: String, + pub sender_account_id: AccountId, } #[derive(Debug, Deserialize, Clone, PartialEq)] @@ -28,6 +32,9 @@ pub struct ForcedExitRequestsConfig { pub max_tx_interval: i64, pub price_per_token: i64, pub digits_in_id: u8, + pub wait_confirmations: i64, + pub sender_private_key: String, + pub sender_account_id: AccountId, } impl ForcedExitRequestsConfig { @@ -45,6 +52,9 @@ impl ForcedExitRequestsConfig { max_tx_interval: max_tx_interval.round() as i64, digits_in_id: config.digits_in_id, price_per_token: config.price_per_token, + wait_confirmations: config.wait_confirmations, + sender_private_key: config.sender_private_key, + sender_account_id: config.sender_account_id, } } } diff --git a/core/lib/storage/migrations/2021-01-31-160151_forced_exit_requests/up.sql b/core/lib/storage/migrations/2021-01-31-160151_forced_exit_requests/up.sql index 1f71696b1c..54d0a1c0c9 100644 --- a/core/lib/storage/migrations/2021-01-31-160151_forced_exit_requests/up.sql +++ b/core/lib/storage/migrations/2021-01-31-160151_forced_exit_requests/up.sql @@ -4,5 +4,6 @@ CREATE TABLE forced_exit_requests ( tokens TEXT NOT NULL, price_in_wei NUMERIC NOT NULL, valid_until TIMESTAMP with time zone NOT NULL, + created_at TIMESTAMP with time zone NOT NULL DEFAULT NOW(), fulfilled_at TIMESTAMP with time zone -); +) diff --git a/core/lib/storage/sqlx-data.json b/core/lib/storage/sqlx-data.json index 6661e51e96..de69ef5171 100644 --- a/core/lib/storage/sqlx-data.json +++ b/core/lib/storage/sqlx-data.json @@ -2118,6 +2118,11 @@ }, { "ordinal": 5, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 6, "name": "fulfilled_at", "type_info": "Timestamptz" } @@ -2133,6 +2138,7 @@ false, false, false, + false, true ] } @@ -2243,6 +2249,60 @@ "nullable": [] } }, + "8a9cf4dca8a6f276a1366a232f0981f0ccaa9e1837a75dc72a85f37f646ed179": { + "query": "\n SELECT * FROM forced_exit_requests\n WHERE fulfilled_at IS NULL AND created_at = (\n SELECT MIN(created_at) FROM forced_exit_requests\n )\n LIMIT 1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "target", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "tokens", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "price_in_wei", + "type_info": "Numeric" + }, + { + "ordinal": 4, + "name": "valid_until", + "type_info": "Timestamptz" + }, + { + "ordinal": 5, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 6, + "name": "fulfilled_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + true + ] + } + }, "8aa384bd2d145e1b7a8a6e18b560af991da3ef0d41ee5cae8f0c0573287acf04": { "query": "\n SELECT * FROM balances\n WHERE account_id = $1\n ", "describe": { @@ -2858,6 +2918,65 @@ ] } }, + "be9256cd3f1bf963746b135b29678dc82a050dde0eca60258dd991b2d7d7d1eb": { + "query": "\n INSERT INTO forced_exit_requests ( target, tokens, price_in_wei, valid_until )\n VALUES ( $1, $2, $3, $4 )\n RETURNING *\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "target", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "tokens", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "price_in_wei", + "type_info": "Numeric" + }, + { + "ordinal": 4, + "name": "valid_until", + "type_info": "Timestamptz" + }, + { + "ordinal": 5, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 6, + "name": "fulfilled_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Text", + "Text", + "Numeric", + "Timestamptz" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + true + ] + } + }, "bf002ea8011c653cebce62d2c49f4a5e7415e45fb7db5f7f68ae86c43b60b393": { "query": "SELECT * FROM eth_parameters WHERE id = true", "describe": { @@ -3587,29 +3706,6 @@ ] } }, - "df8ac3519c49215ebef5ac11c8fed5af12b097eb0c65f9a4ce078bb6e8342885": { - "query": "\n INSERT INTO forced_exit_requests ( target, tokens, price_in_wei, valid_until )\n VALUES ( $1, $2, $3, $4 )\n RETURNING id\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Text", - "Text", - "Numeric", - "Timestamptz" - ] - }, - "nullable": [ - false - ] - } - }, "e3ee3cb9cbe8d05a635e71daea301cf6b2310f89f3d9f8fdabc28e7ebf8d3521": { "query": "\n INSERT INTO eth_account_types VALUES ( $1, $2 )\n ON CONFLICT (account_id) DO UPDATE SET account_type = $2\n ", "describe": { diff --git a/core/lib/storage/src/forced_exit_requests/mod.rs b/core/lib/storage/src/forced_exit_requests/mod.rs index 6bfd31d9ab..142531ed7d 100644 --- a/core/lib/storage/src/forced_exit_requests/mod.rs +++ b/core/lib/storage/src/forced_exit_requests/mod.rs @@ -35,11 +35,12 @@ impl<'a, 'c> ForcedExitRequestsSchema<'a, 'c> { let tokens = utils::tokens_vec_to_str(request.tokens.clone()); - let id: i64 = sqlx::query!( + let stored_request: DbForcedExitRequest = sqlx::query_as!( + DbForcedExitRequest, r#" INSERT INTO forced_exit_requests ( target, tokens, price_in_wei, valid_until ) VALUES ( $1, $2, $3, $4 ) - RETURNING id + RETURNING * "#, target_str, &tokens, @@ -47,26 +48,18 @@ impl<'a, 'c> ForcedExitRequestsSchema<'a, 'c> { request.valid_until ) .fetch_one(self.0.conn()) - .await? - .id; + .await?; metrics::histogram!("sql.forced_exit_requests.store_request", start.elapsed()); - Ok(ForcedExitRequest { - id, - target: request.target, - tokens: request.tokens.clone(), - price_in_wei: request.price_in_wei.clone(), - valid_until: request.valid_until, - fulfilled_at: None, - }) + Ok(stored_request.into()) } pub async fn get_request_by_id( &mut self, id: ForcedExitRequestId, - ) -> QueryResult { + ) -> QueryResult> { let start = Instant::now(); - let request: DbForcedExitRequest = sqlx::query_as!( + let request: Option = sqlx::query_as!( DbForcedExitRequest, r#" SELECT * FROM forced_exit_requests @@ -75,10 +68,10 @@ impl<'a, 'c> ForcedExitRequestsSchema<'a, 'c> { "#, id ) - .fetch_one(self.0.conn()) - .await?; + .fetch_optional(self.0.conn()) + .await? + .map(|r| r.into()); - let request: ForcedExitRequest = request.into(); metrics::histogram!( "sql.forced_exit_requests.get_request_by_id", start.elapsed() @@ -110,4 +103,31 @@ impl<'a, 'c> ForcedExitRequestsSchema<'a, 'c> { Ok(()) } + + pub async fn get_oldest_unfulfilled_request( + &mut self, + ) -> QueryResult> { + let start = Instant::now(); + + let request: Option = sqlx::query_as!( + DbForcedExitRequest, + r#" + SELECT * FROM forced_exit_requests + WHERE fulfilled_at IS NULL AND created_at = ( + SELECT MIN(created_at) FROM forced_exit_requests + ) + LIMIT 1 + "# + ) + .fetch_optional(self.0.conn()) + .await? + .map(|r| r.into()); + + metrics::histogram!( + "sql.forced_exit_requests.get_min_unfulfilled_request", + start.elapsed() + ); + + Ok(request) + } } diff --git a/core/lib/storage/src/forced_exit_requests/records.rs b/core/lib/storage/src/forced_exit_requests/records.rs index 1e07e1c5f6..0298d08323 100644 --- a/core/lib/storage/src/forced_exit_requests/records.rs +++ b/core/lib/storage/src/forced_exit_requests/records.rs @@ -14,6 +14,7 @@ pub struct DbForcedExitRequest { pub tokens: String, pub price_in_wei: BigDecimal, pub valid_until: DateTime, + pub created_at: DateTime, pub fulfilled_at: Option>, } @@ -28,6 +29,7 @@ impl From for DbForcedExitRequest { tokens: tokens, price_in_wei, valid_until: request.valid_until, + created_at: request.created_at, fulfilled_at: request.fulfilled_at, } } @@ -55,6 +57,7 @@ impl Into for DbForcedExitRequest { target: stored_str_address_to_address(&self.target), tokens, price_in_wei, + created_at: self.created_at, valid_until: self.valid_until, fulfilled_at: self.fulfilled_at, } diff --git a/core/lib/types/src/forced_exit_requests.rs b/core/lib/types/src/forced_exit_requests.rs index 677a8c02a9..4b13320926 100644 --- a/core/lib/types/src/forced_exit_requests.rs +++ b/core/lib/types/src/forced_exit_requests.rs @@ -7,21 +7,10 @@ use serde::{Deserialize, Serialize}; pub type ForcedExitRequestId = i64; -use anyhow::{bail, ensure, format_err}; +use anyhow::format_err; use ethabi::{decode, ParamType}; -use std::convert::{TryFrom, TryInto}; -use zksync_basic_types::{Log, H256, U256}; -use zksync_crypto::params::{ - ACCOUNT_ID_BIT_WIDTH, BALANCE_BIT_WIDTH, ETH_ADDRESS_BIT_WIDTH, FR_ADDRESS_LEN, - TOKEN_BIT_WIDTH, TX_TYPE_BIT_WIDTH, -}; - -use super::{ - operations::{DepositOp, FullExitOp}, - utils::h256_as_vec, - AccountId, SerialId, -}; -use zksync_crypto::primitives::FromBytes; +use std::convert::TryFrom; +use zksync_basic_types::{Log, U256}; #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)] #[serde(rename_all = "camelCase")] @@ -32,6 +21,7 @@ pub struct ForcedExitRequest { #[serde(with = "BigUintSerdeAsRadix10Str")] pub price_in_wei: BigUint, pub valid_until: DateTime, + pub created_at: DateTime, pub fulfilled_at: Option>, } diff --git a/etc/env/base/forced_exit_requests.toml b/etc/env/base/forced_exit_requests.toml index f2e0752a26..ea1a9ddbee 100644 --- a/etc/env/base/forced_exit_requests.toml +++ b/etc/env/base/forced_exit_requests.toml @@ -18,3 +18,9 @@ digits_in_id=13 # Price per exit in wei (currently it's 0.03 ETH) price_per_token=30000000000000000 + +# Wait confirmations +wait_confirmations=1 + +# The account Id of the ForcedExit sender +sender_account_id=1 diff --git a/etc/env/base/private.toml b/etc/env/base/private.toml index a9552df82d..2aba30515f 100644 --- a/etc/env/base/private.toml +++ b/etc/env/base/private.toml @@ -25,3 +25,7 @@ secret_auth="sample" [misc] # Private key for the fee seller account fee_account_private_key="0x27593fea79697e947890ecbecce7901b0008345e5d7259710d0dd5e500d040be" + +[forced_exit_requests] +# L2 private key of the account that sends ForcedExits (unprefixed hex) +sender_private_key="0092788f3890ed50dcab7f72fb574a0a9d30b1bc778ba076c609c311a8555352" diff --git a/etc/env/base/rust.toml b/etc/env/base/rust.toml index 72825d13f4..3a4040d4e4 100644 --- a/etc/env/base/rust.toml +++ b/etc/env/base/rust.toml @@ -22,7 +22,8 @@ exodus_test=info,\ loadtest=info,\ kube=debug,\ dev_ticker=info,\ -block_sizes_test=info\ +block_sizes_test=info,\ +zksync_forced_exit_requests=info\ """ # `RUST_BACKTRACE` variable From 1b29337e1aeacf20c6cc3cf4536410ad7aa724d5 Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Wed, 10 Feb 2021 06:18:17 +0200 Subject: [PATCH 24/90] [WIP]: eth_watcher --- core/bin/server/src/main.rs | 10 +- .../rest/forced_exit_requests/mod.rs | 5 +- .../rest/forced_exit_requests/v01.rs | 2 +- .../zksync_api/src/api_server/tx_sender.rs | 145 +++++++++--------- core/bin/zksync_core/src/eth_watch/client.rs | 6 +- .../src/eth_watch.rs | 2 +- core/lib/storage/sqlx-data.json | 13 ++ 7 files changed, 98 insertions(+), 85 deletions(-) diff --git a/core/bin/server/src/main.rs b/core/bin/server/src/main.rs index c2d1bd5a1d..ac1c59175c 100644 --- a/core/bin/server/src/main.rs +++ b/core/bin/server/src/main.rs @@ -78,11 +78,11 @@ async fn main() -> anyhow::Result<()> { // Run prover server & witness generator. vlog::info!("Starting the Prover server actors"); - let database = zksync_witness_generator::database::Database::new(connection_pool); + let database = zksync_witness_generator::database::Database::new(connection_pool.clone()); run_prover_server(database, stop_signal_sender, ZkSyncConfig::from_env()); vlog::info!("Starting the ForcedExitRequests actors"); - //let forced_exit_requests_task_handle = run_forced_exit_requests_actors(connection_pool, config); + let forced_exit_requests_task_handle = run_forced_exit_requests_actors(connection_pool, config); tokio::select! { _ = async { wait_for_tasks(core_task_handles).await } => { @@ -100,9 +100,9 @@ async fn main() -> anyhow::Result<()> { _ = async { counter_task_handle.unwrap().await } => { panic!("Operation counting actor is not supposed to finish its execution") }, - // _ = async { forced_exit_requests_task_handle.await } => { - // panic!("ForcedExitRequests actor is not supposed to finish its execution") - // }, + _ = async { forced_exit_requests_task_handle.await } => { + panic!("ForcedExitRequests actor is not supposed to finish its execution") + }, _ = async { stop_signal_receiver.next().await } => { vlog::warn!("Stop signal received, shutting down"); } diff --git a/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/mod.rs b/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/mod.rs index ec3fd958fe..807cec45c6 100644 --- a/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/mod.rs +++ b/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/mod.rs @@ -37,10 +37,7 @@ use zksync_types::{ use crate::api_server::rest::v1::{Error as ApiError, JsonResult}; use crate::{ - api_server::{ - forced_exit_checker::ForcedExitChecker, - tx_sender::{ticker_request, SubmitError}, - }, + api_server::{forced_exit_checker::ForcedExitChecker, tx_sender::SubmitError}, fee_ticker::TickerRequest, }; diff --git a/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs b/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs index 9edde5a103..bfd4c3cdd0 100644 --- a/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs +++ b/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs @@ -38,7 +38,7 @@ use crate::api_server::rest::v1::{Error as ApiError, JsonResult}; use crate::{ api_server::{ forced_exit_checker::ForcedExitChecker, - tx_sender::{ticker_request, SubmitError}, + tx_sender::{SubmitError, TxSender}, }, fee_ticker::TickerRequest, }; diff --git a/core/bin/zksync_api/src/api_server/tx_sender.rs b/core/bin/zksync_api/src/api_server/tx_sender.rs index 4c8cd1ea28..a9aaace996 100644 --- a/core/bin/zksync_api/src/api_server/tx_sender.rs +++ b/core/bin/zksync_api/src/api_server/tx_sender.rs @@ -227,7 +227,7 @@ impl TxSender { || self.enforce_pubkey_change_fee; let fee_allowed = - token_allowed_for_fees(ticker_request_sender.clone(), token.clone()).await?; + Self::token_allowed_for_fees(ticker_request_sender.clone(), token.clone()).await?; if !fee_allowed { return Err(SubmitError::InappropriateFeeToken); @@ -322,7 +322,8 @@ impl TxSender { continue; } let fee_allowed = - token_allowed_for_fees(self.ticker_requests.clone(), token.clone()).await?; + Self::token_allowed_for_fees(self.ticker_requests.clone(), token.clone()) + .await?; transaction_types.push((tx_type, address)); @@ -549,81 +550,81 @@ impl TxSender { // TODO Make error more clean .ok_or_else(|| SubmitError::other("Token not found in the DB")) } -} -async fn ticker_batch_fee_request( - mut ticker_request_sender: mpsc::Sender, - transactions: Vec<(TxFeeTypes, Address)>, - token: TokenLike, -) -> Result { - let req = oneshot::channel(); - ticker_request_sender - .send(TickerRequest::GetBatchTxFee { - transactions, - token: token.clone(), - response: req.0, - }) - .await - .map_err(SubmitError::internal)?; - let resp = req.1.await.map_err(SubmitError::internal)?; - resp.map_err(|err| internal_error!(err)) -} + async fn ticker_batch_fee_request( + mut ticker_request_sender: mpsc::Sender, + transactions: Vec<(TxFeeTypes, Address)>, + token: TokenLike, + ) -> Result { + let req = oneshot::channel(); + ticker_request_sender + .send(TickerRequest::GetBatchTxFee { + transactions, + token: token.clone(), + response: req.0, + }) + .await + .map_err(SubmitError::internal)?; + let resp = req.1.await.map_err(SubmitError::internal)?; + resp.map_err(|err| internal_error!(err)) + } -async fn ticker_request( - mut ticker_request_sender: mpsc::Sender, - tx_type: TxFeeTypes, - address: Address, - token: TokenLike, -) -> Result { - let req = oneshot::channel(); - ticker_request_sender - .send(TickerRequest::GetTxFee { - tx_type, - address, - token: token.clone(), - response: req.0, - }) - .await - .map_err(SubmitError::internal)?; + async fn ticker_request( + mut ticker_request_sender: mpsc::Sender, + tx_type: TxFeeTypes, + address: Address, + token: TokenLike, + ) -> Result { + let req = oneshot::channel(); + ticker_request_sender + .send(TickerRequest::GetTxFee { + tx_type, + address, + token: token.clone(), + response: req.0, + }) + .await + .map_err(SubmitError::internal)?; - let resp = req.1.await.map_err(SubmitError::internal)?; - resp.map_err(|err| internal_error!(err)) -} + let resp = req.1.await.map_err(SubmitError::internal)?; + resp.map_err(|err| internal_error!(err)) + } -pub async fn token_allowed_for_fees( - mut ticker_request_sender: mpsc::Sender, - token: TokenLike, -) -> Result { - let (sender, receiver) = oneshot::channel(); - ticker_request_sender - .send(TickerRequest::IsTokenAllowed { - token: token.clone(), - response: sender, - }) - .await - .expect("ticker receiver dropped"); - receiver - .await - .expect("ticker answer sender dropped") - .map_err(SubmitError::internal) -} + pub async fn token_allowed_for_fees( + mut ticker_request_sender: mpsc::Sender, + token: TokenLike, + ) -> Result { + let (sender, receiver) = oneshot::channel(); + ticker_request_sender + .send(TickerRequest::IsTokenAllowed { + token: token.clone(), + response: sender, + }) + .await + .expect("ticker receiver dropped"); + receiver + .await + .expect("ticker answer sender dropped") + .map_err(SubmitError::internal) + } -pub async fn ticker_price_request( - mut ticker_request_sender: mpsc::Sender, - token: TokenLike, - req_type: TokenPriceRequestType, -) -> Result { - let req = oneshot::channel(); - ticker_request_sender - .send(TickerRequest::GetTokenPrice { - token: token.clone(), - response: req.0, - req_type, - }) - .await - .map_err(SubmitError::internal)?; - let resp = req.1.await.map_err(SubmitError::internal)?; - resp.map_err(|err| internal_error!(err)) + pub async fn ticker_price_request( + mut ticker_request_sender: mpsc::Sender, + token: TokenLike, + req_type: TokenPriceRequestType, + ) -> Result { + let req = oneshot::channel(); + ticker_request_sender + .send(TickerRequest::GetTokenPrice { + token: token.clone(), + response: req.0, + req_type, + }) + .await + .map_err(SubmitError::internal)?; + let resp = req.1.await.map_err(SubmitError::internal)?; + resp.map_err(|err| internal_error!(err)) + } } async fn send_verify_request_and_recv( diff --git a/core/bin/zksync_core/src/eth_watch/client.rs b/core/bin/zksync_core/src/eth_watch/client.rs index 5b08516baf..7cda80f633 100644 --- a/core/bin/zksync_core/src/eth_watch/client.rs +++ b/core/bin/zksync_core/src/eth_watch/client.rs @@ -5,7 +5,9 @@ use ethabi::Hash; use std::fmt::Debug; use web3::{ contract::Options, + transports::http, types::{BlockNumber, FilterBuilder, Log}, + Web3, }; use zksync_contracts::zksync_contract; @@ -143,7 +145,7 @@ impl EthClient for EthHttpClient { } pub async fn get_contract_events( - web3: &Web3, + web3: &Web3, contract_address: Address, from: BlockNumber, to: BlockNumber, @@ -176,6 +178,6 @@ where .collect() } -pub async fn get_web3_block_number(web3: &Web3) -> anyhow::Result { +pub async fn get_web3_block_number(web3: &Web3) -> anyhow::Result { Ok(web3.eth().block_number().await?.as_u64()) } diff --git a/core/bin/zksync_forced_exit_requests/src/eth_watch.rs b/core/bin/zksync_forced_exit_requests/src/eth_watch.rs index 50ee24e09d..5b2d8d8c50 100644 --- a/core/bin/zksync_forced_exit_requests/src/eth_watch.rs +++ b/core/bin/zksync_forced_exit_requests/src/eth_watch.rs @@ -312,7 +312,7 @@ pub fn run_forced_exit_contract_watcher( connection_pool: ConnectionPool, config: ZkSyncConfig, ) -> JoinHandle<()> { - let transport = web3::transports::Http::new(&config.eth_client.web3_url).unwrap(); + let transport = web3::transports::Http::new(&config.eth_client.web3_url[0]).unwrap(); let web3 = web3::Web3::new(transport); let eth_client = EthClient::new(web3, config.contracts.forced_exit_addr); diff --git a/core/lib/storage/sqlx-data.json b/core/lib/storage/sqlx-data.json index 623f89778f..5b92e3b167 100644 --- a/core/lib/storage/sqlx-data.json +++ b/core/lib/storage/sqlx-data.json @@ -647,6 +647,19 @@ "nullable": [] } }, + "1e491f4afb54c10a9e4f2ea467bd7f219e7a32bdf741691cb6f350d50caae417": { + "query": "\n UPDATE forced_exit_requests\n SET fulfilled_at = $1\n WHERE id = $2\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Timestamptz", + "Int8" + ] + }, + "nullable": [] + } + }, "222e3946401772e3f6e0d9ce9909e8e7ac2dc830c5ecfcd522f56b3bf70fd679": { "query": "INSERT INTO data_restore_storage_state_update (storage_state) VALUES ($1)", "describe": { From f71449116e5794bce0d6fa73a7e031fe8a2eae32 Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Thu, 11 Feb 2021 14:58:17 +0200 Subject: [PATCH 25/90] [WIP]: Preparing forced exit sender account --- Cargo.lock | 2 + .../rest/v1/forced_exit_requests.rs | 447 ------------------ .../zksync_api/src/api_server/rest/v1/mod.rs | 1 - .../zksync_forced_exit_requests/Cargo.toml | 3 + .../src/eth_watch.rs | 7 + .../src/forced_exit_sender.rs | 21 +- .../zksync_forced_exit_requests/src/lib.rs | 275 ++++++++++- .../src/configs/forced_exit_requests.rs | 7 +- etc/env/base/forced_exit_requests.toml | 4 +- etc/env/base/private.toml | 2 +- infrastructure/zk/src/server.ts | 34 ++ 11 files changed, 334 insertions(+), 469 deletions(-) delete mode 100644 core/bin/zksync_api/src/api_server/rest/v1/forced_exit_requests.rs diff --git a/Cargo.lock b/Cargo.lock index 1c5f0274de..718fa8e2bc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6019,12 +6019,14 @@ dependencies = [ "metrics", "num", "tokio 0.2.22", + "vlog", "web3", "zksync_api", "zksync_config", "zksync_contracts", "zksync_core", "zksync_crypto", + "zksync_eth_signer", "zksync_storage", "zksync_types", ] diff --git a/core/bin/zksync_api/src/api_server/rest/v1/forced_exit_requests.rs b/core/bin/zksync_api/src/api_server/rest/v1/forced_exit_requests.rs deleted file mode 100644 index caa9e5718a..0000000000 --- a/core/bin/zksync_api/src/api_server/rest/v1/forced_exit_requests.rs +++ /dev/null @@ -1,447 +0,0 @@ -// //! Transactions part of API implementation. - -// // Built-in uses - -// // External uses -// use actix_web::{ -// web::{self, Json}, -// Scope, -// }; - -// use bigdecimal::{BigDecimal, FromPrimitive}; -// use chrono::{Duration, Utc}; -// use futures::channel::mpsc; -// use num::{bigint::ToBigInt, BigUint}; -// use std::ops::Add; -// use std::str::FromStr; -// use std::time::Instant; - -// // Workspace uses -// pub use zksync_api_client::rest::v1::{ -// FastProcessingQuery, ForcedExitRegisterRequest, IncomingTx, -// IncomingTxBatch, Receipt, TxData, -// }; -// use zksync_config::ZkSyncConfig; -// use zksync_storage::ConnectionPool; -// use zksync_types::{ -// forced_exit_requests::{ForcedExitRequest, SaveForcedExitRequestQuery}, -// TokenLike, TxFeeTypes, -// }; - -// // Local uses -// use super::{Error as ApiError, JsonResult}; - -// use crate::{ -// api_server::{ -// forced_exit_checker::ForcedExitChecker, -// tx_sender::{ticker_request, SubmitError}, -// }, -// fee_ticker::TickerRequest, -// }; - -// /// Shared data between `api/v1/transactions` endpoints. -// #[derive(Clone)] -// pub struct ApiForcedExitRequestsData { -// pub(crate) connection_pool: ConnectionPool, -// pub(crate) forced_exit_checker: ForcedExitChecker, -// pub(crate) ticker_request_sender: mpsc::Sender, - -// pub(crate) is_enabled: bool, -// pub(crate) price_scaling_factor: BigDecimal, -// pub(crate) max_tokens_per_request: u8, -// pub(crate) max_tx_interval_millisecs: u64, -// } - -// impl ApiForcedExitRequestsData { -// fn new( -// connection_pool: ConnectionPool, -// config: &ZkSyncConfig, -// ticker_request_sender: mpsc::Sender, -// ) -> Self { -// let forced_exit_checker = ForcedExitChecker::new(&config); -// Self { -// connection_pool, -// forced_exit_checker, -// ticker_request_sender, - -// is_enabled: config.forced_exit_requests.enabled, -// price_scaling_factor: BigDecimal::from_f64( -// config.forced_exit_requests.price_scaling_factor, -// ) -// .unwrap(), -// max_tokens_per_request: config.forced_exit_requests.max_tokens_per_request, -// } -// } -// } - -// // Server implementation - -// async fn is_enabled( -// data: web::Data, -// ) -> JsonResult { -// let start = Instant::now(); - -// let response = IsForcedExitEnabledResponse { -// enabled: data.is_enabled, -// }; - -// metrics::histogram!("api.v01.are_forced_exit_requests_enabled", start.elapsed()); -// Ok(Json(response)) -// } - -// async fn get_fee_for_one_forced_exit( -// ticker_request_sender: mpsc::Sender, -// price_scaling_factor: BigDecimal, -// ) -> Result { -// let price = ticker_request( -// ticker_request_sender.clone(), -// TxFeeTypes::Withdraw, -// TokenLike::Id(0), -// ) -// .await?; -// let price = BigDecimal::from(price.total_fee.to_bigint().unwrap()); - -// let scaled_price = price * price_scaling_factor; -// let scaled_price = scaled_price.round(0).to_bigint().unwrap(); - -// Ok(scaled_price.to_biguint().unwrap()) -// } - -// async fn get_forced_exit_request_fee( -// data: web::Data, -// ) -> JsonResult { -// let request_fee = get_fee_for_one_forced_exit( -// data.ticker_request_sender.clone(), -// data.price_scaling_factor.clone(), -// ) -// .await -// .map_err(ApiError::from)?; - -// Ok(Json(ForcedExitRequestFee { request_fee })) -// } - -// pub async fn submit_request( -// data: web::Data, -// params: web::Json, -// ) -> JsonResult { -// let start = Instant::now(); - -// if !data.is_enabled { -// return Err(ApiError::bad_request( -// "ForcedExit requests feature is disabled!", -// )); -// } - -// let mut storage = data.connection_pool.access_storage().await.map_err(|err| { -// vlog::warn!("Internal Server Error: '{}';", err); -// return ApiError::internal(""); -// })?; - -// data.forced_exit_checker -// .check_forced_exit(&mut storage, params.target) -// .await -// .map_err(ApiError::from)?; - -// let price_of_one_exit = get_fee_for_one_forced_exit( -// data.ticker_request_sender.clone(), -// data.price_scaling_factor.clone(), -// ) -// .await -// .map_err(ApiError::from)?; -// let price_of_one_exit = BigDecimal::from(price_of_one_exit.to_bigint().unwrap()); -// let price_of_request = price_of_one_exit * BigDecimal::from_usize(params.tokens.len()).unwrap(); - -// let user_fee = params.price_in_wei.to_bigint().unwrap(); -// let user_fee = BigDecimal::from(user_fee); -// let user_scaling_coefficient = BigDecimal::from_str("1.05").unwrap(); -// let user_scaled_fee = user_scaling_coefficient * user_fee; - -// if user_scaled_fee < price_of_request { -// return Err(ApiError::bad_request("Not enough fee")); -// } - -// if params.tokens.len() > 10 { -// return Err(ApiError::bad_request( -// "Maximum number of tokens per FE request exceeded", -// )); -// } - -// let mut tokens_schema = storage.tokens_schema(); - -// for token_id in params.tokens.iter() { -// // The result is going nowhere. -// // This is simply to make sure that the tokens -// // that were supplied do indeed exist -// tokens_schema -// .get_token(TokenLike::Id(*token_id)) -// .await -// .map_err(|_| { -// return ApiError::bad_request("One of the tokens does no exist"); -// })?; -// } - -// let mut fe_schema = storage.forced_exit_requests_schema(); - -// let valid_until = Utc::now().add(Duration::from_millis(self.max_tx_interval_millisecs)); - -// let saved_fe_request = fe_schema -// .store_request(SaveForcedExitRequestQuery { -// target: params.target, -// tokens: params.tokens.clone(), -// price_in_wei: params.price_in_wei.clone(), -// valid_until, -// }) -// .await -// .map_err(|_| { -// return ApiError::internal(""); -// })?; - -// metrics::histogram!("api.v01.register_forced_exit_request", start.elapsed()); -// Ok(Json(saved_fe_request)) -// } - -// pub fn api_scope( -// connection_pool: ConnectionPool, -// config: &ZkSyncConfig, -// ticker_request_sender: mpsc::Sender, -// ) -> Scope { -// let data = ApiForcedExitRequestsData::new(connection_pool, config, ticker_request_sender); - -// // `enabled` endpoint should always be there -// let scope = web::scope("forced_exit_requests") -// .data(data) -// .route("enabled", web::get().to(is_enabled)); - -// if config.forced_exit_requests.enabled { -// scope -// .route("submit", web::post().to(submit_request)) -// .route("fee", web::get().to(get_forced_exit_request_fee)) -// } else { -// scope -// } -// } - -// #[cfg(test)] -// mod tests { -// use bigdecimal::BigDecimal; -// use futures::{channel::mpsc, StreamExt}; -// use num::BigUint; - -// use zksync_api_client::rest::v1::Client; -// use zksync_config::ForcedExitRequestsConfig; -// use zksync_storage::ConnectionPool; -// use zksync_types::tokens::TokenLike; -// use zksync_types::Address; - -// use crate::fee_ticker::{Fee, OutputFeeType::Withdraw, TickerRequest}; - -// use super::super::test_utils::TestServerConfig; -// use super::*; - -// fn dummy_fee_ticker(zkp_fee: Option, gas_fee: Option) -> mpsc::Sender { -// let (sender, mut receiver) = mpsc::channel(10); - -// let zkp_fee = zkp_fee.unwrap_or(1_u64); -// let gas_fee = gas_fee.unwrap_or(1_u64); - -// actix_rt::spawn(async move { -// while let Some(item) = receiver.next().await { -// match item { -// TickerRequest::GetTxFee { response, .. } => { -// let fee = Ok(Fee::new( -// Withdraw, -// BigUint::from(zkp_fee).into(), -// BigUint::from(gas_fee).into(), -// 1_u64.into(), -// 1_u64.into(), -// )); - -// response.send(fee).expect("Unable to send response"); -// } -// TickerRequest::GetTokenPrice { response, .. } => { -// let price = Ok(BigDecimal::from(1_u64)); - -// response.send(price).expect("Unable to send response"); -// } -// TickerRequest::IsTokenAllowed { token, response } => { -// // For test purposes, PHNX token is not allowed. -// let is_phnx = match token { -// TokenLike::Id(id) => id == 1, -// TokenLike::Symbol(sym) => sym == "PHNX", -// TokenLike::Address(_) => unreachable!(), -// }; -// response.send(Ok(!is_phnx)).unwrap_or_default(); -// } -// } -// } -// }); - -// sender -// } - -// struct TestServer { -// api_server: actix_web::test::TestServer, -// #[allow(dead_code)] -// pool: ConnectionPool, -// #[allow(dead_code)] -// fee_ticker: mpsc::Sender, -// } - -// impl TestServer { -// // It should be used in the test for submitting requests -// #[allow(dead_code)] -// async fn new() -> anyhow::Result<(Client, Self)> { -// let cfg = TestServerConfig::default(); - -// Self::new_with_config(cfg).await -// } - -// async fn new_with_config(cfg: TestServerConfig) -> anyhow::Result<(Client, Self)> { -// let pool = cfg.pool.clone(); - -// let fee_ticker = dummy_fee_ticker(None, None); - -// let fee_ticker2 = fee_ticker.clone(); -// let (api_client, api_server) = cfg.start_server(move |cfg| { -// api_scope(cfg.pool.clone(), &cfg.config, fee_ticker2.clone()) -// }); - -// Ok(( -// api_client, -// Self { -// api_server, -// pool, -// fee_ticker, -// }, -// )) -// } - -// async fn new_with_fee_ticker( -// cfg: TestServerConfig, -// gas_fee: Option, -// zkp_fee: Option, -// ) -> anyhow::Result<(Client, Self)> { -// let pool = cfg.pool.clone(); - -// let fee_ticker = dummy_fee_ticker(gas_fee, zkp_fee); - -// let fee_ticker2 = fee_ticker.clone(); -// let (api_client, api_server) = cfg.start_server(move |cfg| { -// api_scope(cfg.pool.clone(), &cfg.config, fee_ticker2.clone()) -// }); - -// Ok(( -// api_client, -// Self { -// api_server, -// pool, -// fee_ticker, -// }, -// )) -// } - -// async fn stop(self) { -// self.api_server.stop().await; -// } -// } - -// fn get_test_config_from_forced_exit_requests( -// forced_exit_requests: ForcedExitRequestsConfig, -// ) -> TestServerConfig { -// let config_from_env = ZkSyncConfig::from_env(); -// let config = ZkSyncConfig { -// forced_exit_requests, -// ..config_from_env -// }; - -// TestServerConfig { -// config, -// pool: ConnectionPool::new(Some(1)), -// } -// } - -// #[actix_rt::test] -// #[cfg_attr( -// not(feature = "api_test"), -// ignore = "Use `zk test rust-api` command to perform this test" -// )] -// async fn test_disabled_forced_exit_requests() -> anyhow::Result<()> { -// let forced_exit_requests = ForcedExitRequestsConfig::from_env(); -// let test_config = get_test_config_from_forced_exit_requests(ForcedExitRequestsConfig { -// enabled: false, -// ..forced_exit_requests -// }); - -// let (client, server) = TestServer::new_with_config(test_config).await?; -// let enabled = client.are_forced_exit_requests_enabled().await?.enabled; - -// assert_eq!(enabled, false); - -// let should_be_disabled_msg = "Forced-exit related requests don't fail when it's disabled"; - -// client -// .get_forced_exit_request_fee() -// .await -// .expect_err(should_be_disabled_msg); - -// let register_request = ForcedExitRegisterRequest { -// target: Address::from_str("c0f97CC918C9d6fA4E9fc6be61a6a06589D199b2").unwrap(), -// tokens: vec![0], -// price_in_wei: BigUint::from_str("1212").unwrap(), -// }; - -// client -// .submit_forced_exit_request(register_request) -// .await -// .expect_err(should_be_disabled_msg); - -// server.stop().await; -// Ok(()) -// } - -// #[actix_rt::test] -// #[cfg_attr( -// not(feature = "api_test"), -// ignore = "Use `zk test rust-api` command to perform this test" -// )] -// async fn test_forced_exit_requests_get_fee() -> anyhow::Result<()> { -// let forced_exit_requests = ForcedExitRequestsConfig::from_env(); -// let test_config = get_test_config_from_forced_exit_requests(ForcedExitRequestsConfig { -// price_scaling_factor: 1.5, -// ..forced_exit_requests -// }); - -// let (client, server) = -// TestServer::new_with_fee_ticker(test_config, Some(10000), Some(10000)).await?; - -// let enabled = client.are_forced_exit_requests_enabled().await?.enabled; -// assert_eq!(enabled, true); - -// let fee = client.get_forced_exit_request_fee().await?.request_fee; -// // 30000 = (10000 + 10000) * 1.5 -// assert_eq!(fee, BigUint::from_u32(30000).unwrap()); - -// server.stop().await; -// Ok(()) -// } - -// // #[actix_rt::test] -// // #[cfg_attr( -// // not(feature = "api_test"), -// // ignore = "Use `zk test rust-api` command to perform this test" -// // )] -// // async fn test_forced_exit_requests_submit() -> anyhow::Result<()> { -// // let (client, server) = TestServer::new().await?; - -// // let enabled = client.are_forced_exit_requests_enabled().await?.enabled; -// // assert_eq!(enabled, true); - -// // let fee = client.get_forced_exit_request_fee().await?.request_fee; - -// // let fe_request = ForcedExitRegisterRequest { -// // target: "" -// // }; - -// // Ok(()) -// // } -// } diff --git a/core/bin/zksync_api/src/api_server/rest/v1/mod.rs b/core/bin/zksync_api/src/api_server/rest/v1/mod.rs index fb29f52e7a..f6fed49108 100644 --- a/core/bin/zksync_api/src/api_server/rest/v1/mod.rs +++ b/core/bin/zksync_api/src/api_server/rest/v1/mod.rs @@ -23,7 +23,6 @@ pub(crate) mod accounts; mod blocks; mod config; pub mod error; -mod forced_exit_requests; mod operations; mod search; #[cfg(test)] diff --git a/core/bin/zksync_forced_exit_requests/Cargo.toml b/core/bin/zksync_forced_exit_requests/Cargo.toml index 6fdc8e0a4a..7124a7ac61 100644 --- a/core/bin/zksync_forced_exit_requests/Cargo.toml +++ b/core/bin/zksync_forced_exit_requests/Cargo.toml @@ -18,6 +18,9 @@ zksync_config = { path = "../../lib/config", version = "1.0" } zksync_contracts = { path = "../../lib/contracts", version = "1.0" } zksync_crypto = { path = "../../lib/crypto", version = "1.0" } +zksync_eth_signer = { path = "../../lib/eth_signer", version = "1.0" } + +vlog = { path = "../../lib/vlog", version = "1.0" } franklin_crypto = { package = "franklin-crypto", version = "0.0.5", git = "https://github.com/matter-labs/franklin-crypto.git", branch="beta", features = ["multicore", "plonk"]} diff --git a/core/bin/zksync_forced_exit_requests/src/eth_watch.rs b/core/bin/zksync_forced_exit_requests/src/eth_watch.rs index 5b2d8d8c50..313c27f2af 100644 --- a/core/bin/zksync_forced_exit_requests/src/eth_watch.rs +++ b/core/bin/zksync_forced_exit_requests/src/eth_watch.rs @@ -29,6 +29,8 @@ use zksync_types::forced_exit_requests::FundsReceivedEvent; /// before repeating the request. const RATE_LIMIT_DELAY: Duration = Duration::from_secs(30); +use crate::prepare_forced_exit_sender; + use super::ForcedExitSender; struct ContractTopics { @@ -317,6 +319,11 @@ pub fn run_forced_exit_contract_watcher( let eth_client = EthClient::new(web3, config.contracts.forced_exit_addr); tokio::spawn(async move { + // It is fine to unwrap here, since without it there is not way + prepare_forced_exit_sender(connection_pool.clone(), core_api_client.clone(), &config) + .await + .unwrap(); + // It is ok to unwrap here, since if fe_sender is not created, then // the watcher is meaningless let forced_exit_sender = ForcedExitSender::new( diff --git a/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs b/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs index cd15574460..c168612735 100644 --- a/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs +++ b/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs @@ -3,6 +3,7 @@ use std::{convert::TryFrom, time::Instant}; use anyhow::format_err; use ethabi::{Contract as ContractAbi, Hash}; use fee_ticker::validator::watcher; +use franklin_crypto::bellman::PrimeFieldRepr; use num::{BigUint, FromPrimitive, ToPrimitive}; use std::fmt::Debug; use tokio::task::JoinHandle; @@ -31,16 +32,8 @@ use zksync_types::forced_exit_requests::FundsReceivedEvent; use zksync_types::ForcedExit; use zksync_types::SignedZkSyncTx; -use franklin_crypto::{ - alt_babyjubjub::fs::FsRepr, - bellman::{pairing::bn256, PrimeFieldRepr}, -}; -use zksync_crypto::franklin_crypto::{eddsa::PrivateKey, jubjub::JubjubEngine}; - -pub type Engine = bn256::Bn256; - -pub type Fr = bn256::Fr; -pub type Fs = ::Fs; +use super::PrivateKey; +use super::{Engine, Fs, FsRepr}; use zksync_crypto::ff::PrimeField; @@ -61,7 +54,7 @@ async fn get_operator_account_id( let mut accounts_schema = storage.chain().account_schema(); let account_id = accounts_schema - .account_id_by_address(config.eth_sender.sender.operator_commit_eth_addr) + .account_id_by_address(config.forced_exit_requests.sender_account_address) .await?; account_id.ok_or(anyhow::Error::msg("1")) @@ -90,10 +83,12 @@ impl ForcedExitSender { connection_pool: ConnectionPool, config: ZkSyncConfig, ) -> anyhow::Result { - let operator_account_id = config.forced_exit_requests.sender_account_id; + let operator_account_id = get_operator_account_id(connection_pool.clone(), &config) + .await + .expect("Failed to get the sender id"); let sender_private_key = - hex::decode(config.clone().forced_exit_requests.sender_private_key) + hex::decode(&config.clone().forced_exit_requests.sender_private_key[2..]) .expect("Decoding private key failed"); let sender_private_key = read_signing_key(&sender_private_key).expect("Reading private key failed"); diff --git a/core/bin/zksync_forced_exit_requests/src/lib.rs b/core/bin/zksync_forced_exit_requests/src/lib.rs index a78e83959f..c41be80823 100644 --- a/core/bin/zksync_forced_exit_requests/src/lib.rs +++ b/core/bin/zksync_forced_exit_requests/src/lib.rs @@ -1,24 +1,59 @@ -use std::{convert::TryFrom, time::Instant}; +use std::{convert::TryFrom, str::FromStr, time::Instant}; use anyhow::format_err; +use api::Accounts; use ethabi::{Contract as ContractAbi, Hash}; +use num::BigUint; use std::fmt::Debug; +use std::time::Duration; use tokio::task::JoinHandle; use web3::{ + api, contract::{Contract, Options}, transports::Http, types::{BlockNumber, FilterBuilder, Log}, Web3, }; use zksync_config::ZkSyncConfig; -use zksync_storage::ConnectionPool; +use zksync_storage::{ + chain::operations_ext::records::TxReceiptResponse, ConnectionPool, StorageProcessor, +}; use zksync_api::core_api_client::CoreApiClient; +use zksync_types::{ + tx::{EthSignData, PackedEthSignature, TimeRange, TxEthSignature, TxHash, TxSignature}, + Account, AccountId, Address, PubKeyHash, ZkSyncTx, H256, +}; + pub mod eth_watch; pub mod forced_exit_sender; use forced_exit_sender::ForcedExitSender; +use zksync_types::tx::ChangePubKey; +use zksync_types::SignedZkSyncTx; + +use franklin_crypto::{ + alt_babyjubjub::fs::FsRepr, + bellman::{pairing::bn256, PrimeFieldRepr}, +}; + +use zksync_crypto::franklin_crypto::{eddsa::PrivateKey, jubjub::JubjubEngine}; + +pub type Engine = bn256::Bn256; + +pub type Fr = bn256::Fr; +pub type Fs = ::Fs; +use zksync_crypto::ff::PrimeField; + +use tokio::time; +use zksync_eth_signer::{EthereumSigner, PrivateKeySigner}; + +use zksync_types::Nonce; +use zksync_types::TokenId; + +#[macro_use] +use vlog; #[must_use] pub fn run_forced_exit_requests_actors( @@ -33,6 +68,242 @@ pub fn run_forced_exit_requests_actors( eth_watch_handle } +// pub fn get_sk_from_hex(hex_string: String) -> PrivateKey { + +// } + +// This private key is for testing purposes only and shoud not be used in production +// The address should be 0xe1faB3eFD74A77C23B426c302D96372140FF7d0C +const FORCED_EXIT_SENDER_ETH_PRIVATE_KEY: &str = + "0x0559b9f000b4e4bbb7fe02e1374cef9623c2ab7c3791204b490e1f229191d104"; + +fn read_signing_key(private_key: &[u8]) -> anyhow::Result> { + let mut fs_repr = FsRepr::default(); + fs_repr.read_be(private_key)?; + Ok(PrivateKey::( + Fs::from_repr(fs_repr).expect("couldn't read private key from repr"), + )) +} + +pub async fn check_forced_exit_sender_prepared<'a>( + storage: &mut StorageProcessor<'a>, + sender_sk: &PrivateKey, + sender_address: Address, +) -> anyhow::Result { + let mut accounts_schema = storage.chain().account_schema(); + + let state = accounts_schema + .account_state_by_address(sender_address) + .await? + .committed; + + match state { + Some(account_state) => { + let pk_hash = account_state.1.pub_key_hash; + + let sk_pub_key_hash = PubKeyHash::from_privkey(sender_sk); + + Ok(pk_hash == sk_pub_key_hash) + } + None => Ok(false), + } +} + +pub async fn wait_for_account_id<'a>( + storage: &mut StorageProcessor<'a>, + sender_address: Address, +) -> anyhow::Result { + vlog::info!("Forced exit sender account is not yet prepared. Waiting for account id..."); + + let mut account_schema = storage.chain().account_schema(); + let mut timer = time::interval(Duration::from_secs(1)); + + loop { + let account_id = account_schema.account_id_by_address(sender_address).await?; + + match account_id { + Some(id) => { + vlog::info!("Forced exit sender account has account id = {}", 1); + return Ok(id); + } + None => { + timer.tick().await; + } + } + } +} + +async fn get_receipt<'a>( + storage: &mut StorageProcessor<'a>, + tx_hash: TxHash, +) -> anyhow::Result> { + storage + .chain() + .operations_ext_schema() + .tx_receipt(tx_hash.as_ref()) + .await +} + +pub async fn wait_for_change_pub_key_tx<'a>( + storage: &mut StorageProcessor<'a>, + tx_hash: TxHash, +) -> anyhow::Result<()> { + vlog::info!( + "Forced exit sender account is not yet prepared. Waiting for public key to be set..." + ); + + let mut timer = time::interval(Duration::from_secs(1)); + + loop { + let tx_receipt = get_receipt(storage, tx_hash) + .await + .expect("Faield t oget the traecipt pf ChangePubKey transaction"); + + match tx_receipt { + Some(receipt) => { + if receipt.success { + vlog::info!("Public key of the forced exit sender successfully set"); + return Ok(()); + } else { + let fail_reason = receipt.fail_reason.unwrap_or(String::from("unknown")); + panic!( + "Failed to set public for forced exit sedner. Reason: {}", + fail_reason + ); + } + } + None => { + timer.tick().await; + } + } + } +} + +// Use PackedEthSignature::address_from_private_key +async fn get_verified_eth_sk(sender_address: Address) -> H256 { + let eth_sk = hex::decode(&FORCED_EXIT_SENDER_ETH_PRIVATE_KEY[2..]) + .expect("Failed to parse eth signing key of the forced exit account"); + + let private_key = H256::from_slice(ð_sk); + + let pk_address = PackedEthSignature::address_from_private_key(&private_key).unwrap(); + + dbg!(pk_address.clone()); + dbg!(sender_address.clone()); + + if pk_address != sender_address { + panic!("Private key provided does not correspond to the sender address"); + } + + private_key +} + +pub async fn register_signing_key<'a>( + storage: &mut StorageProcessor<'a>, + sender_id: AccountId, + api_client: CoreApiClient, + sender_address: Address, + sender_sk: &PrivateKey, +) -> anyhow::Result<()> { + let eth_sk = get_verified_eth_sk(sender_address).await; + + // Unfortunately, currently the only way to create a CPK + // transaction from eth_private_key is to cre + let mut cpk_tx = ChangePubKey::new_signed( + sender_id, + sender_address, + PubKeyHash::from_privkey(sender_sk), + TokenId::from_str("0").unwrap(), + BigUint::from(0u8), + Nonce::from_str("0").unwrap(), + TimeRange::default(), + None, + sender_sk, + ) + .expect("Failed to create unsigned cpk transaction"); + + let eth_sign_bytes = cpk_tx + .get_eth_signed_data() + .expect("Failed to get eth signed data"); + + let eth_signature = + PackedEthSignature::sign(ð_sk, ð_sign_bytes).expect("Failed to sign eth message"); + + cpk_tx.eth_signature = Some(PackedEthSignature::from(eth_signature.clone())); + + let tx = ZkSyncTx::ChangePubKey(Box::new(cpk_tx)); + let eth_sign_data = EthSignData { + signature: TxEthSignature::EthereumSignature(eth_signature), + message: eth_sign_bytes, + }; + + let tx_signed = SignedZkSyncTx { + tx, + eth_sign_data: Some(eth_sign_data), + }; + let tx_hash = tx_signed.tx.hash(); + + api_client + .send_tx(tx_signed) + .await + .expect("Failed to send CPK transaction"); + + wait_for_change_pub_key_tx(storage, tx_hash) + .await + .expect("Failed to wait for ChangePubKey tx"); + + Ok(()) +} + +fn verify_pub_key_hash() {} + +pub async fn prepare_forced_exit_sender( + connection_pool: ConnectionPool, + api_client: CoreApiClient, + config: &ZkSyncConfig, +) -> anyhow::Result<()> { + let mut storage = connection_pool + .access_storage() + .await + .expect("forced_exit_requests: Failed to get the connection to storage"); + + let sender_sk = hex::decode(&config.forced_exit_requests.sender_private_key[2..]) + .expect("Failed to decode forced_exit_sender sk"); + let sender_sk = read_signing_key(&sender_sk).expect("Failed to read forced exit sender sk"); + let sender_address = config.forced_exit_requests.sender_account_address; + + let is_sender_prepared = + check_forced_exit_sender_prepared(&mut storage, &sender_sk, sender_address) + .await + .expect("Failed to check if the sender is prepared"); + + if is_sender_prepared { + return Ok(()); + } + + // The sender is not prepared. This should not ever happen in production, but handling + // such step is vital for testing locally. + + // Waiting until the sender has an id (sending funds to the account should be done by an external script) + let id = wait_for_account_id(&mut storage, sender_address) + .await + .expect("Failed to get account id for forced exit sender"); + + register_signing_key(&mut storage, id, api_client, sender_address, &sender_sk).await?; + + Ok(()) +} + +// Inserts the forced exit sender account into db +// should be used only for local setup/testing +// pub fn insert_forced_exit_account(config: &ZkSyncConfig) { +// let pool = ConnectionPool::new(Some(1)); + +// vlog::info!("Inserting forced exit sender into db"); + +// let sender_account = Account:: +// } + /* Polling like eth_watch diff --git a/core/lib/config/src/configs/forced_exit_requests.rs b/core/lib/config/src/configs/forced_exit_requests.rs index 00d5c21159..f7d70864ba 100644 --- a/core/lib/config/src/configs/forced_exit_requests.rs +++ b/core/lib/config/src/configs/forced_exit_requests.rs @@ -2,6 +2,7 @@ use crate::envy_load; /// External uses use serde::Deserialize; use zksync_types::AccountId; +use zksync_types::Address; // There are two types of configs: // The original one (with tx_interval_scaling_factor) @@ -21,7 +22,7 @@ struct ForcedExitRequestsInternalConfig { pub digits_in_id: u8, pub wait_confirmations: i64, pub sender_private_key: String, - pub sender_account_id: AccountId, + pub sender_account_address: Address, } #[derive(Debug, Deserialize, Clone, PartialEq)] @@ -34,7 +35,7 @@ pub struct ForcedExitRequestsConfig { pub digits_in_id: u8, pub wait_confirmations: i64, pub sender_private_key: String, - pub sender_account_id: AccountId, + pub sender_account_address: Address, } impl ForcedExitRequestsConfig { @@ -54,7 +55,7 @@ impl ForcedExitRequestsConfig { price_per_token: config.price_per_token, wait_confirmations: config.wait_confirmations, sender_private_key: config.sender_private_key, - sender_account_id: config.sender_account_id, + sender_account_address: config.sender_account_address, } } } diff --git a/etc/env/base/forced_exit_requests.toml b/etc/env/base/forced_exit_requests.toml index ea1a9ddbee..3bb1cfb456 100644 --- a/etc/env/base/forced_exit_requests.toml +++ b/etc/env/base/forced_exit_requests.toml @@ -22,5 +22,5 @@ price_per_token=30000000000000000 # Wait confirmations wait_confirmations=1 -# The account Id of the ForcedExit sender -sender_account_id=1 +# The account of the ForcedExit sender +sender_account_address="0xe1faB3eFD74A77C23B426c302D96372140FF7d0C" diff --git a/etc/env/base/private.toml b/etc/env/base/private.toml index 2aba30515f..d304b7e35a 100644 --- a/etc/env/base/private.toml +++ b/etc/env/base/private.toml @@ -28,4 +28,4 @@ fee_account_private_key="0x27593fea79697e947890ecbecce7901b0008345e5d7259710d0dd [forced_exit_requests] # L2 private key of the account that sends ForcedExits (unprefixed hex) -sender_private_key="0092788f3890ed50dcab7f72fb574a0a9d30b1bc778ba076c609c311a8555352" +sender_private_key="0x0092788f3890ed50dcab7f72fb574a0a9d30b1bc778ba076c609c311a8555352" diff --git a/infrastructure/zk/src/server.ts b/infrastructure/zk/src/server.ts index f9506aefde..41130bbc39 100644 --- a/infrastructure/zk/src/server.ts +++ b/infrastructure/zk/src/server.ts @@ -4,6 +4,9 @@ import * as env from './env'; import fs from 'fs'; import * as db from './db/db'; +import { ethers } from 'ethers'; +import { utils as syncUtils } from 'zksync'; + export async function server() { let child = utils.background('cargo run --bin zksync_server --release'); @@ -11,6 +14,8 @@ export async function server() { process.on('SIGINT', () => { child.kill('SIGINT'); }); + + await prepareForcedExitRequestAccount(); } export async function genesis() { @@ -34,6 +39,35 @@ export async function genesis() { env.modify_contracts_toml('CONTRACTS_GENESIS_ROOT', genesisRoot); } +// This functions deposits funds onto the forced exit sender account +// This is needed to make sure that it has the account id +async function prepareForcedExitRequestAccount() { + console.log('Depositing to the forced exit sender account'); + const forcedExitAccount = process.env.FORCED_EXIT_REQUESTS_SENDER_ACCOUNT_ADDRESS as string; + + // This is the private key of the first test account + const ethProvider = new ethers.providers.JsonRpcProvider('http://localhost:8545'); + const ethRichWallet = new ethers.Wallet('0x7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110'); + + const mainZkSyncContract = new ethers.Contract( + process.env.CONTRACTS_CONTRACT_ADDR as string, + syncUtils.SYNC_MAIN_CONTRACT_INTERFACE, + ethRichWallet.connect(ethProvider) + ); + const gasPrice = await ethProvider.getGasPrice(); + + const ethTransaction = await mainZkSyncContract.depositETH(forcedExitAccount, { + // The amount to deposit does not really matter + value: ethers.utils.parseEther('1.0'), + gasLimit: ethers.BigNumber.from('200000'), + gasPrice, + }) as ethers.ContractTransaction; + + await ethTransaction.wait(); + + console.log('Deposit to the forced exit sender account has been successfully completed'); +} + export const command = new Command('server') .description('start zksync server') .option('--genesis', 'generate genesis data via server') From 3637d416d021d34e355cca64289207b3aa8e7063 Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Thu, 11 Feb 2021 16:27:45 +0200 Subject: [PATCH 26/90] [WIP]: basic integration testing for forced_exit_requests --- .../src/eth_watch.rs | 13 +-- .../src/rest/forced_exit_requests/mod.rs | 3 +- .../ts-tests/tests/forced-exit-requests.ts | 110 ++++++++++++++++++ core/tests/ts-tests/tests/misc.ts | 7 ++ .../ts-tests/tests/withdrawal-helpers.test.ts | 14 ++- 5 files changed, 135 insertions(+), 12 deletions(-) create mode 100644 core/tests/ts-tests/tests/forced-exit-requests.ts diff --git a/core/bin/zksync_forced_exit_requests/src/eth_watch.rs b/core/bin/zksync_forced_exit_requests/src/eth_watch.rs index 313c27f2af..53d439ed77 100644 --- a/core/bin/zksync_forced_exit_requests/src/eth_watch.rs +++ b/core/bin/zksync_forced_exit_requests/src/eth_watch.rs @@ -116,10 +116,6 @@ struct ForcedExitContractWatcher { mode: WatcherMode, } -fn dummy_get_min() -> i64 { - 1 -} - // Usually blocks are created much slower (at rate 1 block per 10-20s), // but the block time falls through time, so just to double-check const MILLIS_PER_BLOCK: i64 = 7000; @@ -135,9 +131,6 @@ fn time_range_to_block_diff(from: DateTime, to: DateTime) -> u64 { .unwrap(); } -// clean the db from txs being older than ... -fn clean() {} - impl ForcedExitContractWatcher { async fn restore_state_from_eth(&mut self, block: u64) -> anyhow::Result<()> { //let last_block = self.eth_client.get_block_number().await.expect("Failed to restore "); @@ -320,9 +313,9 @@ pub fn run_forced_exit_contract_watcher( tokio::spawn(async move { // It is fine to unwrap here, since without it there is not way - prepare_forced_exit_sender(connection_pool.clone(), core_api_client.clone(), &config) - .await - .unwrap(); + //prepare_forced_exit_sender(connection_pool.clone(), core_api_client.clone(), &config) + // .await + // .unwrap(); // It is ok to unwrap here, since if fe_sender is not created, then // the watcher is meaningless diff --git a/core/lib/api_client/src/rest/forced_exit_requests/mod.rs b/core/lib/api_client/src/rest/forced_exit_requests/mod.rs index 47348d20f3..054754dd0a 100644 --- a/core/lib/api_client/src/rest/forced_exit_requests/mod.rs +++ b/core/lib/api_client/src/rest/forced_exit_requests/mod.rs @@ -35,7 +35,8 @@ pub enum ForcedExitRequestStatus { pub struct ForcedExitRegisterRequest { pub target: Address, pub tokens: Vec, - // We still gotta specify that, since the price might change + // Even though the price is constant, we still need to specify it, + // since the price might change (with config) #[serde(with = "BigUintSerdeAsRadix10Str")] pub price_in_wei: BigUint, } diff --git a/core/tests/ts-tests/tests/forced-exit-requests.ts b/core/tests/ts-tests/tests/forced-exit-requests.ts new file mode 100644 index 0000000000..73e1777f5e --- /dev/null +++ b/core/tests/ts-tests/tests/forced-exit-requests.ts @@ -0,0 +1,110 @@ +import { Tester } from './tester'; +import { expect } from 'chai'; +import { Wallet, types, Provider, utils } from 'zksync'; +import { BigNumber, ethers } from 'ethers'; +import { Address } from 'zksync/build/types'; + +import * as path from 'path'; +import fs from 'fs'; +import { RevertReceiveAccountFactory, RevertTransferERC20Factory } from '../../../../contracts/typechain'; +import { waitForOnchainWithdrawal, loadTestConfig } from './helpers'; + +import fetch from 'node-fetch'; + +import './transfer'; +import { reporters } from 'mocha'; + +const TEST_CONFIG = loadTestConfig(); + +const apiTypesFolder = './api-types'; +const ADDRESS_REGEX = /^0x([0-9a-fA-F]){40}$/; +const DATE_REGEX = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{6})?/; +const testConfigPath = path.join(process.env.ZKSYNC_HOME as string, `etc/test_config/constant`); +const apiTestConfig = JSON.parse(fs.readFileSync(`${testConfigPath}/api.json`, { encoding: 'utf-8' })); +const apiUrl = `${apiTestConfig.rest_api_url}/api/forced_exit_requests/v0.1`; + +type TokenLike = types.TokenLike; + +declare module './tester' { + interface Tester { + testForcedExitRequestOneToken(from: Wallet, to: ethers.Signer, token: TokenLike, value: BigNumber): Promise; + } +} + +interface StatusResponse { + status: "enabled" | "disabled", + request_fee: string, + max_tokens_per_request: number, + recomended_tx_interval_millis: number +} + +async function getStatus() { + const endpoint = `${apiUrl}/status`; + + const response = await fetch(endpoint); + + console.log(response.status); + /// console.log(await response.text()); + + return (await response.json()) as StatusResponse; +} + +async function submitRequest( + address: string, + tokens: number[], + price_in_wei: string +) { + const endpoint = `${apiUrl}/submit`; + + const data = { + target: address, + tokens, + price_in_wei + }; + + const response = await fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + redirect: 'follow', + body: JSON.stringify(data) + }); + + return await response.json(); +} + +Tester.prototype.testForcedExitRequestOneToken = async function ( + from: Wallet, + to: ethers.Signer, + token: TokenLike, + amount: BigNumber +) { + const toAddress = await to.getAddress(); + let toBalanceBefore = await utils.getEthereumBalance( + this.ethProvider, + this.syncProvider, + toAddress, + token + ); + + const transferHandle = await from.syncTransfer({ + to: toAddress, + token, + amount, + }); + + await transferHandle.awaitReceipt(); + + const status = await getStatus(); + + const tokenId = await this.syncProvider.tokenSet.resolveTokenId(token); + + console.log(await submitRequest( + toAddress, + [tokenId], + status.request_fee + )); + // expect(toBalance.eq(expectedToBalance), 'The withdrawal was not recovered').to.be.true; +}; + diff --git a/core/tests/ts-tests/tests/misc.ts b/core/tests/ts-tests/tests/misc.ts index f31d5906ba..11b0675a3c 100644 --- a/core/tests/ts-tests/tests/misc.ts +++ b/core/tests/ts-tests/tests/misc.ts @@ -14,6 +14,7 @@ declare module './tester' { testMultipleBatchSigners(wallets: Wallet[], token: TokenLike, amount: BigNumber): Promise; testMultipleWalletsWrongSignature(from: Wallet, to: Wallet, token: TokenLike, amount: BigNumber): Promise; testBackwardCompatibleEthMessages(from: Wallet, to: Wallet, token: TokenLike, amount: BigNumber): Promise; + testForcedExitRequests(from: Wallet, to: Wallet, token: TokenLike, amount: BigNumber): Promise; } } @@ -236,3 +237,9 @@ Tester.prototype.testBackwardCompatibleEthMessages = async function ( await Promise.all(handles.map((handle) => handle.awaitVerifyReceipt())); this.runningFee = this.runningFee.add(totalFee); }; + +Tester.prototype.testForcedExitRequests = async function ( + +) { + +} diff --git a/core/tests/ts-tests/tests/withdrawal-helpers.test.ts b/core/tests/ts-tests/tests/withdrawal-helpers.test.ts index 0e7cef97be..e24e83a6a2 100644 --- a/core/tests/ts-tests/tests/withdrawal-helpers.test.ts +++ b/core/tests/ts-tests/tests/withdrawal-helpers.test.ts @@ -4,6 +4,7 @@ import { utils } from 'ethers'; import './priority-ops'; import './change-pub-key'; import './withdrawal-helpers'; +import './forced-exit-requests'; import { loadTestConfig } from './helpers'; @@ -15,13 +16,15 @@ const TEST_CONFIG = loadTestConfig(); // The token here should have the ERC20 implementation from RevertTransferERC20.sol const erc20Token = 'wBTC'; -describe('Withdrawal helpers tests', () => { +describe.only('Withdrawal helpers tests', () => { let tester: Tester; let alice: Wallet; + let bob: Wallet; before('create tester and test wallets', async () => { tester = await Tester.init('localhost', 'HTTP'); alice = await tester.fundedWallet('10.0'); + bob = await tester.fundedWallet('10.0'); for (const token of ['ETH', erc20Token]) { await tester.testDeposit(alice, token, DEPOSIT_AMOUNT, true); @@ -60,4 +63,13 @@ describe('Withdrawal helpers tests', () => { [TX_AMOUNT, TX_AMOUNT] ); }); + + it.only('forced_exit_requests should recover multiple tokens', async () => { + await tester.testForcedExitRequestOneToken( + alice, + bob.ethSigner, + 'ETH', + utils.parseEther('1.0') + ) + }) }); From ec5993f86c78e5ea67d507ea4bd0515e770238db Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Thu, 11 Feb 2021 20:34:26 +0200 Subject: [PATCH 27/90] Recovering single token integration test passes --- .../rest/forced_exit_requests/v01.rs | 5 +- .../src/eth_watch.rs | 11 ++- .../src/forced_exit_sender.rs | 33 +------ .../zksync_forced_exit_requests/src/lib.rs | 22 ++++- .../src/rest/forced_exit_requests/mod.rs | 1 + core/lib/types/src/forced_exit_requests.rs | 2 +- .../ts-tests/tests/forced-exit-requests.ts | 94 ++++++++++++++----- .../ts-tests/tests/withdrawal-helpers.test.ts | 2 +- 8 files changed, 104 insertions(+), 66 deletions(-) diff --git a/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs b/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs index bfd4c3cdd0..82ac614a22 100644 --- a/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs +++ b/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs @@ -29,7 +29,7 @@ use zksync_config::ZkSyncConfig; use zksync_storage::ConnectionPool; use zksync_types::{ forced_exit_requests::{ForcedExitRequest, ForcedExitRequestId, SaveForcedExitRequestQuery}, - TokenLike, TxFeeTypes, + Address, TokenLike, TxFeeTypes, }; // Local uses @@ -55,6 +55,7 @@ pub struct ApiForcedExitRequestsData { pub(crate) recomended_tx_interval_millisecs: i64, pub(crate) max_tx_interval_millisecs: i64, pub(crate) price_per_token: i64, + pub(crate) forced_exit_contract_address: Address, } impl ApiForcedExitRequestsData { @@ -74,6 +75,7 @@ impl ApiForcedExitRequestsData { max_tokens_per_request: config.forced_exit_requests.max_tokens_per_request, recomended_tx_interval_millisecs: config.forced_exit_requests.recomended_tx_interval, max_tx_interval_millisecs: config.forced_exit_requests.max_tx_interval, + forced_exit_contract_address: config.contracts.forced_exit_addr, } } } @@ -90,6 +92,7 @@ async fn get_status( request_fee: BigUint::from(data.price_per_token as u64), max_tokens_per_request: data.max_tokens_per_request, recomended_tx_interval_millis: data.recomended_tx_interval_millisecs, + forced_exit_contract_address: data.forced_exit_contract_address, }) } else { ForcedExitRequestStatus::Disabled diff --git a/core/bin/zksync_forced_exit_requests/src/eth_watch.rs b/core/bin/zksync_forced_exit_requests/src/eth_watch.rs index 53d439ed77..5c40c977b2 100644 --- a/core/bin/zksync_forced_exit_requests/src/eth_watch.rs +++ b/core/bin/zksync_forced_exit_requests/src/eth_watch.rs @@ -263,7 +263,7 @@ impl ForcedExitContractWatcher { .await; } - self.last_viewed_block = last_block; + self.last_viewed_block = last_confirmed_block; } pub async fn run(mut self) { @@ -312,10 +312,11 @@ pub fn run_forced_exit_contract_watcher( let eth_client = EthClient::new(web3, config.contracts.forced_exit_addr); tokio::spawn(async move { - // It is fine to unwrap here, since without it there is not way - //prepare_forced_exit_sender(connection_pool.clone(), core_api_client.clone(), &config) - // .await - // .unwrap(); + // It is fine to unwrap here, since without it there is not way we + // can be sure that the forced exit sender will work properly + prepare_forced_exit_sender(connection_pool.clone(), core_api_client.clone(), &config) + .await + .unwrap(); // It is ok to unwrap here, since if fe_sender is not created, then // the watcher is meaningless diff --git a/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs b/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs index c168612735..545fbc7e7f 100644 --- a/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs +++ b/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs @@ -254,6 +254,9 @@ impl ForcedExitSender { pub async fn process_request(&self, amount: i64) { let id = self.extract_id_from_amount(amount); + // After extracting the id we need to delete it from the db + // to make sure that amount is the same as in the db + let amount = amount - id; let mut storage = match self.connection_pool.access_storage().await { Ok(storage) => storage, @@ -295,35 +298,5 @@ impl ForcedExitSender { self.fulfill_request(&mut storage, id) .await .expect("Error while fulfulling the request"); - - // let db_transaction = match fe_schema.0.start_transaction().await { - // Ok(transaction ) => transaction, - // Err(error) => { - // log::warn!("Failed to start db transaction for processing forced_exit_requests, reason {}", error); - // return; - // } - // }; - - // send_to_mempool(); - // await until its committed - - // db_transaction - // .fe_schema() - // .fulfill_request() - - // let fe_request = fe_schema.get_request_by_id(id).await; - // // The error means that such on id does not exists - // // TOOD: Actually handle differently when id does not exist or an actual error - // if let Err(_) = fe_request { - // return; - // } - - // let fe_request = fe_request.unwrap().unwrap(); - - // TODO: take aging into account - - // let tx = self.construct_forced_exit(storage, fe_request).await.expect("Failed to construct forced exit transaction"); - // TODO: Handle such cases gracefully, and not panic - // self.core_api_client.send_tx(tx).await.expect("An erro occureed, while submitting tx"); } } diff --git a/core/bin/zksync_forced_exit_requests/src/lib.rs b/core/bin/zksync_forced_exit_requests/src/lib.rs index c41be80823..d16951b205 100644 --- a/core/bin/zksync_forced_exit_requests/src/lib.rs +++ b/core/bin/zksync_forced_exit_requests/src/lib.rs @@ -207,12 +207,14 @@ pub async fn register_signing_key<'a>( ) -> anyhow::Result<()> { let eth_sk = get_verified_eth_sk(sender_address).await; + let pub_key_hash = PubKeyHash::from_privkey(sender_sk); + // Unfortunately, currently the only way to create a CPK // transaction from eth_private_key is to cre let mut cpk_tx = ChangePubKey::new_signed( sender_id, sender_address, - PubKeyHash::from_privkey(sender_sk), + pub_key_hash.clone(), TokenId::from_str("0").unwrap(), BigUint::from(0u8), Nonce::from_str("0").unwrap(), @@ -229,9 +231,20 @@ pub async fn register_signing_key<'a>( let eth_signature = PackedEthSignature::sign(ð_sk, ð_sign_bytes).expect("Failed to sign eth message"); - cpk_tx.eth_signature = Some(PackedEthSignature::from(eth_signature.clone())); + let cpk_tx_signed = ChangePubKey::new_signed( + sender_id, + sender_address, + pub_key_hash, + TokenId::from_str("0").unwrap(), + BigUint::from(0u8), + Nonce::from_str("0").unwrap(), + TimeRange::default(), + Some(eth_signature.clone()), + sender_sk, + ) + .expect("Failed to created signed CPK transaction"); - let tx = ZkSyncTx::ChangePubKey(Box::new(cpk_tx)); + let tx = ZkSyncTx::ChangePubKey(Box::new(cpk_tx_signed)); let eth_sign_data = EthSignData { signature: TxEthSignature::EthereumSignature(eth_signature), message: eth_sign_bytes, @@ -246,7 +259,8 @@ pub async fn register_signing_key<'a>( api_client .send_tx(tx_signed) .await - .expect("Failed to send CPK transaction"); + .expect("Failed to send CPK transaction") + .expect("Failed to send"); wait_for_change_pub_key_tx(storage, tx_hash) .await diff --git a/core/lib/api_client/src/rest/forced_exit_requests/mod.rs b/core/lib/api_client/src/rest/forced_exit_requests/mod.rs index 054754dd0a..5fe87b9efb 100644 --- a/core/lib/api_client/src/rest/forced_exit_requests/mod.rs +++ b/core/lib/api_client/src/rest/forced_exit_requests/mod.rs @@ -22,6 +22,7 @@ pub struct ConfigInfo { pub request_fee: BigUint, pub max_tokens_per_request: u8, pub recomended_tx_interval_millis: i64, + pub forced_exit_contract_address: Address, } #[derive(Serialize, Deserialize, PartialEq, Debug)] diff --git a/core/lib/types/src/forced_exit_requests.rs b/core/lib/types/src/forced_exit_requests.rs index 4b13320926..095799ebad 100644 --- a/core/lib/types/src/forced_exit_requests.rs +++ b/core/lib/types/src/forced_exit_requests.rs @@ -33,7 +33,7 @@ pub struct SaveForcedExitRequestQuery { pub price_in_wei: BigUint, pub valid_until: DateTime, } - +#[derive(Debug, Clone, Copy)] pub struct FundsReceivedEvent { pub amount: u64, } diff --git a/core/tests/ts-tests/tests/forced-exit-requests.ts b/core/tests/ts-tests/tests/forced-exit-requests.ts index 73e1777f5e..ead4ebd74c 100644 --- a/core/tests/ts-tests/tests/forced-exit-requests.ts +++ b/core/tests/ts-tests/tests/forced-exit-requests.ts @@ -1,24 +1,17 @@ import { Tester } from './tester'; import { expect } from 'chai'; -import { Wallet, types, Provider, utils } from 'zksync'; +import fs from 'fs'; +import fetch from 'node-fetch'; +import { Wallet, types, utils } from 'zksync'; import { BigNumber, ethers } from 'ethers'; -import { Address } from 'zksync/build/types'; - import * as path from 'path'; -import fs from 'fs'; -import { RevertReceiveAccountFactory, RevertTransferERC20Factory } from '../../../../contracts/typechain'; -import { waitForOnchainWithdrawal, loadTestConfig } from './helpers'; -import fetch from 'node-fetch'; +import { loadTestConfig } from './helpers'; +import { Address } from 'zksync/build/types'; +import { sleep } from 'zksync/build/utils'; import './transfer'; -import { reporters } from 'mocha'; -const TEST_CONFIG = loadTestConfig(); - -const apiTypesFolder = './api-types'; -const ADDRESS_REGEX = /^0x([0-9a-fA-F]){40}$/; -const DATE_REGEX = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{6})?/; const testConfigPath = path.join(process.env.ZKSYNC_HOME as string, `etc/test_config/constant`); const apiTestConfig = JSON.parse(fs.readFileSync(`${testConfigPath}/api.json`, { encoding: 'utf-8' })); const apiUrl = `${apiTestConfig.rest_api_url}/api/forced_exit_requests/v0.1`; @@ -35,16 +28,14 @@ interface StatusResponse { status: "enabled" | "disabled", request_fee: string, max_tokens_per_request: number, - recomended_tx_interval_millis: number + recomended_tx_interval_millis: number, + forced_exit_contract_address: Address } async function getStatus() { const endpoint = `${apiUrl}/status`; - - const response = await fetch(endpoint); - console.log(response.status); - /// console.log(await response.text()); + const response = await fetch(endpoint); return (await response.json()) as StatusResponse; } @@ -74,6 +65,22 @@ async function submitRequest( return await response.json(); } +async function getFullOnchainBalance( + tester: Tester, + address: Address, + tokenAddress: Address +) { + const onchainBalance = await utils.getEthereumBalance( + tester.ethProvider, + tester.syncProvider, + address, + tokenAddress + ); + const pendingToBeOnchain = await tester.contract.getPendingBalance(address, tokenAddress); + + return BigNumber.from(onchainBalance).add(BigNumber.from(pendingToBeOnchain)); +} + Tester.prototype.testForcedExitRequestOneToken = async function ( from: Wallet, to: ethers.Signer, @@ -81,6 +88,7 @@ Tester.prototype.testForcedExitRequestOneToken = async function ( amount: BigNumber ) { const toAddress = await to.getAddress(); + const tokenAddress = await this.syncProvider.tokenSet.resolveTokenAddress(token); let toBalanceBefore = await utils.getEthereumBalance( this.ethProvider, this.syncProvider, @@ -93,18 +101,56 @@ Tester.prototype.testForcedExitRequestOneToken = async function ( token, amount, }); - await transferHandle.awaitReceipt(); const status = await getStatus(); - const tokenId = await this.syncProvider.tokenSet.resolveTokenId(token); + expect(status.status).to.eq('enabled', 'Forced exit requests status is disabled'); - console.log(await submitRequest( + const tokenId = await this.syncProvider.tokenSet.resolveTokenId(token); + const request = await submitRequest( toAddress, [tokenId], status.request_fee - )); - // expect(toBalance.eq(expectedToBalance), 'The withdrawal was not recovered').to.be.true; -}; + ); + + const contractAddress = status.forced_exit_contract_address; + + const amountToPay = BigNumber.from(request.priceInWei).add(BigNumber.from(request.id)); + + const gasPrice = await to.provider?.getGasPrice() as BigNumber; + + const txHandle = await to.sendTransaction({ + value: amountToPay, + gasPrice: gasPrice, + to: contractAddress + }); + + const receipt = await txHandle.wait(); + // We have to wait for verification and execution of the + // block with the forced exit, so waiting for a while is fine + let timeout = 45000; + let interval = 500; + + let timePassed = 0; + + let spentOnGas = receipt.gasUsed.mul(gasPrice); + let spentTotal = spentOnGas.add(amountToPay); + + let expectedToBalance = toBalanceBefore.add(amount).sub(spentTotal); + while(timePassed <= timeout) { + let balance = await getFullOnchainBalance(this, toAddress, tokenAddress); + + if(balance.eq(expectedToBalance)) { + break; + } + + await sleep(interval); + timePassed += interval; + } + + let balance = await getFullOnchainBalance(this, toAddress, tokenAddress); + + expect(balance.eq(expectedToBalance), "The ForcedExit has not completed").to.be.true; +}; diff --git a/core/tests/ts-tests/tests/withdrawal-helpers.test.ts b/core/tests/ts-tests/tests/withdrawal-helpers.test.ts index e24e83a6a2..7ad027de17 100644 --- a/core/tests/ts-tests/tests/withdrawal-helpers.test.ts +++ b/core/tests/ts-tests/tests/withdrawal-helpers.test.ts @@ -64,7 +64,7 @@ describe.only('Withdrawal helpers tests', () => { ); }); - it.only('forced_exit_requests should recover multiple tokens', async () => { + it.only('forced_exit_request should recover single token', async () => { await tester.testForcedExitRequestOneToken( alice, bob.ethSigner, From 46b8a1b1a5a938d3389e82d645980350c0367aa0 Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Mon, 15 Feb 2021 14:56:44 +0200 Subject: [PATCH 28/90] Minor API refactoring and restore API unit tests --- .../src/api_server/forced_exit_checker.rs | 28 ++- .../rest/forced_exit_requests/mod.rs | 39 +--- .../rest/forced_exit_requests/v01.rs | 213 +++++++----------- .../bin/zksync_api/src/api_server/rest/mod.rs | 7 +- .../zksync_api/src/api_server/rest/v1/mod.rs | 5 - .../zksync_api/src/api_server/tx_sender.rs | 2 +- .../src/rest/forced_exit_requests/mod.rs | 4 +- 7 files changed, 116 insertions(+), 182 deletions(-) diff --git a/core/bin/zksync_api/src/api_server/forced_exit_checker.rs b/core/bin/zksync_api/src/api_server/forced_exit_checker.rs index 2440c3f2ed..0815d6c5e2 100644 --- a/core/bin/zksync_api/src/api_server/forced_exit_checker.rs +++ b/core/bin/zksync_api/src/api_server/forced_exit_checker.rs @@ -6,6 +6,16 @@ use zksync_types::Address; use crate::internal_error; use chrono::Utc; + +#[async_trait::async_trait] +pub trait ForcedExitAccountAgeChecker { + async fn check_forced_exit<'a>( + &self, + storage: &mut StorageProcessor<'a>, + target_account_address: Address, + ) -> Result<(), SubmitError>; +} + #[derive(Clone)] pub struct ForcedExitChecker { /// Mimimum age of the account for `ForcedExit` operations to be allowed. @@ -22,8 +32,11 @@ impl ForcedExitChecker { forced_exit_minimum_account_age, } } +} - pub async fn check_forced_exit<'a>( +#[async_trait::async_trait] +impl ForcedExitAccountAgeChecker for ForcedExitChecker { + async fn check_forced_exit<'a>( &self, storage: &mut StorageProcessor<'a>, target_account_address: Address, @@ -50,3 +63,16 @@ impl ForcedExitChecker { } } } + +pub struct DummyForcedExitChecker; + +#[async_trait::async_trait] +impl ForcedExitAccountAgeChecker for DummyForcedExitChecker { + async fn check_forced_exit<'a>( + &self, + _storage: &mut StorageProcessor<'a>, + _target_account_address: Address, + ) -> Result<(), SubmitError> { + Ok(()) + } +} diff --git a/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/mod.rs b/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/mod.rs index 807cec45c6..d03aa63c5b 100644 --- a/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/mod.rs +++ b/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/mod.rs @@ -1,10 +1,7 @@ //! First stable API implementation. // External uses -use actix_web::{ - web::{self, Json}, - Scope, -}; +use actix_web::{web, Scope}; // Workspace uses pub use zksync_api_client::rest::v1::{ @@ -13,44 +10,20 @@ pub use zksync_api_client::rest::v1::{ use zksync_config::ZkSyncConfig; use zksync_storage::ConnectionPool; -// Local uses -use crate::api_server::tx_sender::TxSender; - -use bigdecimal::{BigDecimal, FromPrimitive}; -use chrono::{Duration, Utc}; -use futures::channel::mpsc; -use num::{bigint::ToBigInt, BigUint}; -use std::ops::Add; -use std::str::FromStr; -use std::time::Instant; - -// Workspace uses pub use zksync_api_client::rest::v1::{ FastProcessingQuery, IncomingTx, IncomingTxBatch, Receipt, TxData, }; -use zksync_types::{ - forced_exit_requests::{ForcedExitRequest, SaveForcedExitRequestQuery}, - TokenLike, TxFeeTypes, -}; -// Local uses -use crate::api_server::rest::v1::{Error as ApiError, JsonResult}; - -use crate::{ - api_server::{forced_exit_checker::ForcedExitChecker, tx_sender::SubmitError}, - fee_ticker::TickerRequest, -}; +use crate::api_server::forced_exit_checker::ForcedExitChecker; +// Local uses mod v01; -pub(crate) fn api_scope( - connection_pool: ConnectionPool, - config: &ZkSyncConfig, - ticker_request_sender: mpsc::Sender, -) -> Scope { +pub(crate) fn api_scope(connection_pool: ConnectionPool, config: &ZkSyncConfig) -> Scope { + let fe_age_checker = ForcedExitChecker::new(&config); web::scope("/api/forced_exit_requests").service(v01::api_scope( connection_pool, config, - ticker_request_sender, + Box::new(fe_age_checker), )) } diff --git a/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs b/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs index 82ac614a22..f052fe0c11 100644 --- a/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs +++ b/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs @@ -10,7 +10,6 @@ use actix_web::{ use bigdecimal::{BigDecimal, FromPrimitive}; use chrono::{Duration, Utc}; -use futures::channel::mpsc; use num::{bigint::ToBigInt, BigUint}; use std::ops::Add; use std::str::FromStr; @@ -29,26 +28,18 @@ use zksync_config::ZkSyncConfig; use zksync_storage::ConnectionPool; use zksync_types::{ forced_exit_requests::{ForcedExitRequest, ForcedExitRequestId, SaveForcedExitRequestQuery}, - Address, TokenLike, TxFeeTypes, + Address, TokenLike, }; // Local uses use crate::api_server::rest::v1::{Error as ApiError, JsonResult}; -use crate::{ - api_server::{ - forced_exit_checker::ForcedExitChecker, - tx_sender::{SubmitError, TxSender}, - }, - fee_ticker::TickerRequest, -}; +use crate::api_server::forced_exit_checker::ForcedExitAccountAgeChecker; /// Shared data between `api/v1/transactions` endpoints. -#[derive(Clone)] pub struct ApiForcedExitRequestsData { pub(crate) connection_pool: ConnectionPool, - pub(crate) forced_exit_checker: ForcedExitChecker, - pub(crate) ticker_request_sender: mpsc::Sender, + pub(crate) forced_exit_checker: Box, pub(crate) is_enabled: bool, pub(crate) max_tokens_per_request: u8, @@ -62,13 +53,11 @@ impl ApiForcedExitRequestsData { fn new( connection_pool: ConnectionPool, config: &ZkSyncConfig, - ticker_request_sender: mpsc::Sender, + forced_exit_checker: Box, ) -> Self { - let forced_exit_checker = ForcedExitChecker::new(&config); Self { connection_pool, forced_exit_checker, - ticker_request_sender, is_enabled: config.forced_exit_requests.enabled, price_per_token: config.forced_exit_requests.price_per_token, @@ -205,9 +194,9 @@ pub async fn get_request_by_id( pub fn api_scope( connection_pool: ConnectionPool, config: &ZkSyncConfig, - ticker_request_sender: mpsc::Sender, + fe_checker: Box, ) -> Scope { - let data = ApiForcedExitRequestsData::new(connection_pool, config, ticker_request_sender); + let data = ApiForcedExitRequestsData::new(connection_pool, config, fe_checker); // `enabled` endpoint should always be there let scope = web::scope("v0.1") @@ -225,68 +214,23 @@ pub fn api_scope( #[cfg(test)] mod tests { - use bigdecimal::BigDecimal; - use futures::{channel::mpsc, StreamExt}; + use std::ops::Mul; + use num::BigUint; use zksync_api_client::rest::v1::Client; use zksync_config::ForcedExitRequestsConfig; use zksync_storage::ConnectionPool; - use zksync_types::tokens::TokenLike; - use zksync_types::Address; - - use crate::fee_ticker::{Fee, OutputFeeType::Withdraw, TickerRequest}; + use zksync_types::{Address, TokenId}; use super::*; + use crate::api_server::forced_exit_checker::DummyForcedExitChecker; use crate::api_server::v1::test_utils::TestServerConfig; - // fn dummy_fee_ticker(zkp_fee: Option, gas_fee: Option) -> mpsc::Sender { - // let (sender, mut receiver) = mpsc::channel(10); - - // let zkp_fee = zkp_fee.unwrap_or(1_u64); - // let gas_fee = gas_fee.unwrap_or(1_u64); - - // actix_rt::spawn(async move { - // while let Some(item) = receiver.next().await { - // match item { - // TickerRequest::GetTxFee { response, .. } => { - // let fee = Ok(Fee::new( - // Withdraw, - // BigUint::from(zkp_fee).into(), - // BigUint::from(gas_fee).into(), - // 1_u64.into(), - // 1_u64.into(), - // )); - - // response.send(fee).expect("Unable to send response"); - // } - // TickerRequest::GetTokenPrice { response, .. } => { - // let price = Ok(BigDecimal::from(1_u64)); - - // response.send(price).expect("Unable to send response"); - // } - // TickerRequest::IsTokenAllowed { token, response } => { - // // For test purposes, PHNX token is not allowed. - // let is_phnx = match token { - // TokenLike::Id(id) => id == 1, - // TokenLike::Symbol(sym) => sym == "PHNX", - // TokenLike::Address(_) => unreachable!(), - // }; - // response.send(Ok(!is_phnx)).unwrap_or_default(); - // } - // } - // } - // }); - - // sender - // } - struct TestServer { api_server: actix_web::test::TestServer, #[allow(dead_code)] pool: ConnectionPool, - #[allow(dead_code)] - fee_ticker: mpsc::Sender, } impl TestServer { @@ -295,53 +239,22 @@ mod tests { async fn new() -> anyhow::Result<(Client, Self)> { let cfg = TestServerConfig::default(); - Self::new_with_config(cfg).await + Self::from_config(cfg).await } - async fn new_with_config(cfg: TestServerConfig) -> anyhow::Result<(Client, Self)> { + async fn from_config(cfg: TestServerConfig) -> anyhow::Result<(Client, Self)> { let pool = cfg.pool.clone(); - let fee_ticker = dummy_fee_ticker(None, None); - - let fee_ticker2 = fee_ticker.clone(); - let (api_client, api_server) = cfg - .start_server_with_scope(String::from("api/forced_exit_requests"), move |cfg| { - api_scope(cfg.pool.clone(), &cfg.config, fee_ticker2.clone()) + let (api_client, api_server) = + cfg.start_server_with_scope(String::from("api/forced_exit_requests"), move |cfg| { + api_scope( + cfg.pool.clone(), + &cfg.config, + Box::new(DummyForcedExitChecker {}), + ) }); - Ok(( - api_client, - Self { - api_server, - pool, - fee_ticker, - }, - )) - } - - async fn new_with_fee_ticker( - cfg: TestServerConfig, - gas_fee: Option, - zkp_fee: Option, - ) -> anyhow::Result<(Client, Self)> { - let pool = cfg.pool.clone(); - - let fee_ticker = dummy_fee_ticker(gas_fee, zkp_fee); - - let fee_ticker2 = fee_ticker.clone(); - let (api_client, api_server) = cfg - .start_server_with_scope(String::from("/api/forced_exit_requests"), move |cfg| { - api_scope(cfg.pool.clone(), &cfg.config, fee_ticker2.clone()) - }); - - Ok(( - api_client, - Self { - api_server, - pool, - fee_ticker, - }, - )) + Ok((api_client, Self { api_server, pool })) } async fn stop(self) { @@ -376,23 +289,22 @@ mod tests { ..forced_exit_requests }); - let (client, server) = TestServer::new_with_config(test_config).await?; + let (client, server) = TestServer::from_config(test_config).await?; let status = client.get_forced_exit_requests_status().await?; assert_eq!(status, ForcedExitRequestStatus::Disabled); - let should_be_disabled_msg = "Forced-exit related requests don't fail when it's disabled"; let register_request = ForcedExitRegisterRequest { target: Address::from_str("c0f97CC918C9d6fA4E9fc6be61a6a06589D199b2").unwrap(), - tokens: vec![0], + tokens: vec![TokenId(0)], price_in_wei: BigUint::from_str("1212").unwrap(), }; client .submit_forced_exit_request(register_request) .await - .expect_err(should_be_disabled_msg); + .expect_err("Forced-exit related requests don't fail when it's disabled"); server.stop().await; Ok(()) @@ -410,7 +322,7 @@ mod tests { ..forced_exit_requests }); - let (client, server) = TestServer::new_with_config(test_config).await?; + let (client, server) = TestServer::from_config(test_config).await?; let status = client.get_forced_exit_requests_status().await?; @@ -435,24 +347,30 @@ mod tests { not(feature = "api_test"), ignore = "Use `zk test rust-api` command to perform this test" )] - async fn test_forced_exit_requests_wrongs_tokens_number() -> anyhow::Result<()> { - let forced_exit_requests = ForcedExitRequestsConfig::from_env(); + async fn test_forced_exit_requests_wrong_tokens_number() -> anyhow::Result<()> { + let forced_exit_requests_config = ForcedExitRequestsConfig::from_env(); let test_config = get_test_config_from_forced_exit_requests(ForcedExitRequestsConfig { max_tokens_per_request: 5, - ..forced_exit_requests + ..forced_exit_requests_config }); - let (client, server) = - TestServer::new_with_fee_ticker(test_config, Some(10000), Some(10000)).await?; + let (client, server) = TestServer::from_config(test_config).await?; let status = client.get_forced_exit_requests_status().await?; - assert_ne!(status, ForcedExitRequestStatus::Disabled); + let price_per_token = forced_exit_requests_config.price_per_token; + // 6 tokens: + let tokens: Vec = vec![0, 1, 2, 3, 4, 5]; + let tokens: Vec = tokens.iter().map(|t| TokenId(*t)).collect(); + let price_in_wei = BigUint::from_i64(price_per_token) + .unwrap() + .mul(tokens.len()); + let register_request = ForcedExitRegisterRequest { target: Address::from_str("c0f97CC918C9d6fA4E9fc6be61a6a06589D199b2").unwrap(), - tokens: vec![0, 1, 2, 3, 4, 5, 6, 7], - price_in_wei: BigUint::from_str("1212").unwrap(), + tokens, + price_in_wei, }; client @@ -464,23 +382,48 @@ mod tests { Ok(()) } - // #[actix_rt::test] - // #[cfg_attr( - // not(feature = "api_test"), - // ignore = "Use `zk test rust-api` command to perform this test" - // )] - // async fn test_forced_exit_requests_submit() -> anyhow::Result<()> { - // let (client, server) = TestServer::new().await?; + #[actix_rt::test] + #[cfg_attr( + not(feature = "api_test"), + ignore = "Use `zk test rust-api` command to perform this test" + )] + async fn test_forced_exit_requests_submit() -> anyhow::Result<()> { + let price_per_token: i64 = 1000000000000000000; + let max_tokens_per_request = 3; + let config = ForcedExitRequestsConfig { + max_tokens_per_request, + price_per_token, + ..ForcedExitRequestsConfig::from_env() + }; + let server_config = get_test_config_from_forced_exit_requests(config); - // let enabled = client.are_forced_exit_requests_enabled().await?.enabled; - // assert_eq!(enabled, true); + let (client, server) = TestServer::from_config(server_config).await?; + + let status = client.get_forced_exit_requests_status().await?; + assert!(matches!(status, ForcedExitRequestStatus::Enabled(_))); - // let fee = client.get_forced_exit_request_fee().await?.request_fee; + let tokens: Vec = vec![0, 1, 2]; + let tokens: Vec = tokens.iter().map(|t| TokenId(*t)).collect(); - // let fe_request = ForcedExitRegisterRequest { - // target: "" - // }; + let price_in_wei = BigUint::from_i64(price_per_token) + .unwrap() + .mul(tokens.len()); - // Ok(()) - // } + let target = Address::from_str("c0f97CC918C9d6fA4E9fc6be61a6a06589D199b2").unwrap(); + + let fe_request = ForcedExitRegisterRequest { + target, + tokens: tokens.clone(), + price_in_wei: price_in_wei.clone(), + }; + + let submit_result = client.submit_forced_exit_request(fe_request).await?; + + assert_eq!(submit_result.price_in_wei, price_in_wei); + assert_eq!(submit_result.tokens, tokens); + assert_eq!(submit_result.target, target); + + server.stop().await; + Ok(()) + } } diff --git a/core/bin/zksync_api/src/api_server/rest/mod.rs b/core/bin/zksync_api/src/api_server/rest/mod.rs index 18acc941db..e72079fe66 100644 --- a/core/bin/zksync_api/src/api_server/rest/mod.rs +++ b/core/bin/zksync_api/src/api_server/rest/mod.rs @@ -37,11 +37,8 @@ async fn start_server( v1::api_scope(tx_sender, &api_v01.config) }; - let forced_exit_requests_api_scope = forced_exit_requests::api_scope( - api_v01.connection_pool.clone(), - &api_v01.config, - fee_ticker.clone(), - ); + let forced_exit_requests_api_scope = + forced_exit_requests::api_scope(api_v01.connection_pool.clone(), &api_v01.config); App::new() .wrap(Cors::new().send_wildcard().max_age(3600).finish()) diff --git a/core/bin/zksync_api/src/api_server/rest/v1/mod.rs b/core/bin/zksync_api/src/api_server/rest/v1/mod.rs index f6fed49108..f6eccb4ed1 100644 --- a/core/bin/zksync_api/src/api_server/rest/v1/mod.rs +++ b/core/bin/zksync_api/src/api_server/rest/v1/mod.rs @@ -50,9 +50,4 @@ pub(crate) fn api_scope(tx_sender: TxSender, zk_config: &ZkSyncConfig) -> Scope tx_sender.tokens, tx_sender.ticker_requests.clone(), )) - // .service(forced_exit_requests::api_scope( - // tx_sender.pool.clone(), - // zk_config, - // tx_sender.ticker_requests, - // )) } diff --git a/core/bin/zksync_api/src/api_server/tx_sender.rs b/core/bin/zksync_api/src/api_server/tx_sender.rs index a9aaace996..0f4d782b6b 100644 --- a/core/bin/zksync_api/src/api_server/tx_sender.rs +++ b/core/bin/zksync_api/src/api_server/tx_sender.rs @@ -26,7 +26,7 @@ use zksync_types::{ // Local uses use crate::api_server::rpc_server::types::TxWithSignature; use crate::{ - api_server::forced_exit_checker::ForcedExitChecker, + api_server::forced_exit_checker::{ForcedExitAccountAgeChecker, ForcedExitChecker}, core_api_client::CoreApiClient, fee_ticker::{TickerRequest, TokenPriceRequestType}, signature_checker::{TxVariant, VerifiedTx, VerifyTxSignatureRequest}, diff --git a/core/lib/api_client/src/rest/forced_exit_requests/mod.rs b/core/lib/api_client/src/rest/forced_exit_requests/mod.rs index 5fe87b9efb..39e791bbb2 100644 --- a/core/lib/api_client/src/rest/forced_exit_requests/mod.rs +++ b/core/lib/api_client/src/rest/forced_exit_requests/mod.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; // Workspace uses -use zksync_types::{tx::TxHash, Address, TokenId}; +use zksync_types::{forced_exit_requests::ForcedExitRequest, tx::TxHash, Address, TokenId}; use zksync_utils::BigUintSerdeAsRadix10Str; use num::BigUint; @@ -54,7 +54,7 @@ impl Client { pub async fn submit_forced_exit_request( &self, regiter_request: ForcedExitRegisterRequest, - ) -> ClientResult { + ) -> ClientResult { self.post_with_scope(FORCED_EXIT_REQUESTS_SCOPE, "submit") .body(®iter_request) .send() From 3a84a3dceaff3ea98687ec22f5ed59259ac4cf5c Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Tue, 16 Feb 2021 12:17:44 +0200 Subject: [PATCH 29/90] fulfilled_by and storage tests for forced_exit_automation restored --- .../rest/forced_exit_requests/v01.rs | 4 +- .../src/forced_exit_sender.rs | 6 +- .../up.sql | 5 +- core/lib/storage/sqlx-data.json | 318 +++++++++++------- .../storage/src/forced_exit_requests/mod.rs | 68 +++- .../src/forced_exit_requests/records.rs | 14 +- .../storage/src/forced_exit_requests/utils.rs | 37 +- .../storage/src/tests/forced_exit_requests.rs | 140 ++++---- core/lib/types/src/forced_exit_requests.rs | 4 + minilog | 4 + 10 files changed, 398 insertions(+), 202 deletions(-) create mode 100644 minilog diff --git a/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs b/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs index f052fe0c11..677b928928 100644 --- a/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs +++ b/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs @@ -141,13 +141,15 @@ pub async fn submit_request( let mut fe_schema = storage.forced_exit_requests_schema(); - let valid_until = Utc::now().add(Duration::milliseconds(data.max_tx_interval_millisecs)); + let created_at = Utc::now(); + let valid_until = created_at.add(Duration::milliseconds(data.max_tx_interval_millisecs)); let saved_fe_request = fe_schema .store_request(SaveForcedExitRequestQuery { target: params.target, tokens: params.tokens.clone(), price_in_wei: params.price_in_wei.clone(), + created_at, valid_until, }) .await diff --git a/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs b/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs index 545fbc7e7f..b07a2a5e65 100644 --- a/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs +++ b/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs @@ -194,7 +194,7 @@ impl ForcedExitSender { Ok(request) } - pub async fn fulfill_request<'a>( + pub async fn set_fulfilled_at<'a>( &self, storage: &mut StorageProcessor<'a>, id: i64, @@ -202,7 +202,7 @@ impl ForcedExitSender { let mut fe_schema = storage.forced_exit_requests_schema(); fe_schema - .fulfill_request(id, Utc::now()) + .set_fulfilled_at(id, Utc::now()) .await // TODO: Handle such cases gracefully, and not panic .expect("An error occured, while fu;lfilling the request"); @@ -295,7 +295,7 @@ impl ForcedExitSender { .await .expect("Comittment waiting failed"); - self.fulfill_request(&mut storage, id) + self.set_fulfilled_at(&mut storage, id) .await .expect("Error while fulfulling the request"); } diff --git a/core/lib/storage/migrations/2021-01-31-160151_forced_exit_requests/up.sql b/core/lib/storage/migrations/2021-01-31-160151_forced_exit_requests/up.sql index 54d0a1c0c9..7bcd1082a0 100644 --- a/core/lib/storage/migrations/2021-01-31-160151_forced_exit_requests/up.sql +++ b/core/lib/storage/migrations/2021-01-31-160151_forced_exit_requests/up.sql @@ -1,9 +1,10 @@ CREATE TABLE forced_exit_requests ( id BIGSERIAL PRIMARY KEY, target TEXT NOT NULL, - tokens TEXT NOT NULL, + tokens TEXT NOT NULL, -- comma-separated list of TokenIds price_in_wei NUMERIC NOT NULL, valid_until TIMESTAMP with time zone NOT NULL, - created_at TIMESTAMP with time zone NOT NULL DEFAULT NOW(), + created_at TIMESTAMP with time zone NOT NULL, + fulfilled_by TEXT, -- comma-separated list of the hashes of ForcedExit transactions fulfilled_at TIMESTAMP with time zone ) diff --git a/core/lib/storage/sqlx-data.json b/core/lib/storage/sqlx-data.json index 89261f4b43..a261fac471 100644 --- a/core/lib/storage/sqlx-data.json +++ b/core/lib/storage/sqlx-data.json @@ -406,6 +406,66 @@ ] } }, + "0e43c955bab97c4e3c2d8566c1c32c8448e27f658db0dea9540679b903dcdfd7": { + "query": "\n SELECT * FROM forced_exit_requests\n WHERE fulfilled_at IS NULL AND fulfilled_by IS NOT NULL\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "target", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "tokens", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "price_in_wei", + "type_info": "Numeric" + }, + { + "ordinal": 4, + "name": "valid_until", + "type_info": "Timestamptz" + }, + { + "ordinal": 5, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 6, + "name": "fulfilled_by", + "type_info": "Text" + }, + { + "ordinal": 7, + "name": "fulfilled_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + true, + true + ] + } + }, "0fbc25e0f2aab2b56acf7e09d75690a78f7c2df7cec0644a8e45461ee9aab75b": { "query": "SELECT * FROM data_restore_rollup_ops\n ORDER BY id ASC", "describe": { @@ -1558,6 +1618,66 @@ ] } }, + "502e94a5b03c686539721f133998c66fa53f50a620167666d2e1b6084d3832b9": { + "query": "\n SELECT * FROM forced_exit_requests\n WHERE fulfilled_at IS NULL AND created_at = (\n SELECT MIN(created_at) FROM forced_exit_requests\n WHERE fulfilled_at IS NULL\n )\n LIMIT 1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "target", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "tokens", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "price_in_wei", + "type_info": "Numeric" + }, + { + "ordinal": 4, + "name": "valid_until", + "type_info": "Timestamptz" + }, + { + "ordinal": 5, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 6, + "name": "fulfilled_by", + "type_info": "Text" + }, + { + "ordinal": 7, + "name": "fulfilled_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + true, + true + ] + } + }, "50a7a224aeba0065b57858fc989c3a09d45f833b68fbc9909a73817f782dd3c3": { "query": "\n WITH aggr_exec AS (\n SELECT \n aggregate_operations.confirmed, \n execute_aggregated_blocks_binding.block_number \n FROM aggregate_operations\n INNER JOIN execute_aggregated_blocks_binding ON aggregate_operations.id = execute_aggregated_blocks_binding.op_id\n WHERE aggregate_operations.confirmed = true \n ),\n transactions AS (\n SELECT\n *\n FROM (\n SELECT\n concat_ws(',', block_number, block_index) AS tx_id,\n tx,\n 'sync-tx:' || encode(tx_hash, 'hex') AS hash,\n null as pq_id,\n null as eth_block,\n success,\n fail_reason,\n block_number,\n created_at\n FROM\n executed_transactions\n WHERE\n from_account = $1\n or\n to_account = $1\n or\n primary_account_address = $1\n union all\n select\n concat_ws(',', block_number, block_index) as tx_id,\n operation as tx,\n '0x' || encode(eth_hash, 'hex') as hash,\n priority_op_serialid as pq_id,\n eth_block,\n true as success,\n null as fail_reason,\n block_number,\n created_at\n from \n executed_priority_operations\n where \n from_account = $1\n or\n to_account = $1) t\n order by\n block_number desc, created_at desc\n offset \n $2\n limit \n $3\n )\n select\n tx_id as \"tx_id!\",\n hash as \"hash?\",\n eth_block as \"eth_block?\",\n pq_id as \"pq_id?\",\n tx as \"tx!\",\n success as \"success?\",\n fail_reason as \"fail_reason?\",\n true as \"commited!\",\n coalesce(verified.confirmed, false) as \"verified!\",\n created_at as \"created_at!\"\n from transactions\n LEFT JOIN aggr_exec verified ON transactions.block_number = verified.block_number\n order by transactions.block_number desc, created_at desc\n ", "describe": { @@ -2099,6 +2219,19 @@ ] } }, + "7bc4a6d9e909dce159213d0826726c10c7ec4008db2a4f05cbe613aa849e8a40": { + "query": "\n UPDATE forced_exit_requests\n SET fulfilled_by = $1\n WHERE id = $2\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Int8" + ] + }, + "nullable": [] + } + }, "7c51337430beeb0ed6e1f244da727797194ab44b5049b15cd2bcba4fc4642fb9": { "query": "SELECT * FROM server_config", "describe": { @@ -2199,6 +2332,11 @@ }, { "ordinal": 6, + "name": "fulfilled_by", + "type_info": "Text" + }, + { + "ordinal": 7, "name": "fulfilled_at", "type_info": "Timestamptz" } @@ -2215,6 +2353,7 @@ false, false, false, + true, true ] } @@ -2337,60 +2476,6 @@ "nullable": [] } }, - "8a9cf4dca8a6f276a1366a232f0981f0ccaa9e1837a75dc72a85f37f646ed179": { - "query": "\n SELECT * FROM forced_exit_requests\n WHERE fulfilled_at IS NULL AND created_at = (\n SELECT MIN(created_at) FROM forced_exit_requests\n )\n LIMIT 1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "target", - "type_info": "Text" - }, - { - "ordinal": 2, - "name": "tokens", - "type_info": "Text" - }, - { - "ordinal": 3, - "name": "price_in_wei", - "type_info": "Numeric" - }, - { - "ordinal": 4, - "name": "valid_until", - "type_info": "Timestamptz" - }, - { - "ordinal": 5, - "name": "created_at", - "type_info": "Timestamptz" - }, - { - "ordinal": 6, - "name": "fulfilled_at", - "type_info": "Timestamptz" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - false, - false, - false, - false, - false, - false, - true - ] - } - }, "8aa384bd2d145e1b7a8a6e18b560af991da3ef0d41ee5cae8f0c0573287acf04": { "query": "\n SELECT * FROM balances\n WHERE account_id = $1\n ", "describe": { @@ -3137,65 +3222,6 @@ ] } }, - "be9256cd3f1bf963746b135b29678dc82a050dde0eca60258dd991b2d7d7d1eb": { - "query": "\n INSERT INTO forced_exit_requests ( target, tokens, price_in_wei, valid_until )\n VALUES ( $1, $2, $3, $4 )\n RETURNING *\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "target", - "type_info": "Text" - }, - { - "ordinal": 2, - "name": "tokens", - "type_info": "Text" - }, - { - "ordinal": 3, - "name": "price_in_wei", - "type_info": "Numeric" - }, - { - "ordinal": 4, - "name": "valid_until", - "type_info": "Timestamptz" - }, - { - "ordinal": 5, - "name": "created_at", - "type_info": "Timestamptz" - }, - { - "ordinal": 6, - "name": "fulfilled_at", - "type_info": "Timestamptz" - } - ], - "parameters": { - "Left": [ - "Text", - "Text", - "Numeric", - "Timestamptz" - ] - }, - "nullable": [ - false, - false, - false, - false, - false, - false, - true - ] - } - }, "bf002ea8011c653cebce62d2c49f4a5e7415e45fb7db5f7f68ae86c43b60b393": { "query": "SELECT * FROM eth_parameters WHERE id = true", "describe": { @@ -3743,6 +3769,72 @@ "nullable": [] } }, + "dbd7cc6b289ab3a15781dac965f9e6f026c8e647b480b5dd0c3820948d6ba4ed": { + "query": "\n INSERT INTO forced_exit_requests ( target, tokens, price_in_wei, created_at, valid_until )\n VALUES ( $1, $2, $3, $4, $5 )\n RETURNING *\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "target", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "tokens", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "price_in_wei", + "type_info": "Numeric" + }, + { + "ordinal": 4, + "name": "valid_until", + "type_info": "Timestamptz" + }, + { + "ordinal": 5, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 6, + "name": "fulfilled_by", + "type_info": "Text" + }, + { + "ordinal": 7, + "name": "fulfilled_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Text", + "Text", + "Numeric", + "Timestamptz", + "Timestamptz" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + true, + true + ] + } + }, "debbe23f0c730c331482c798387d1739911923edcafc2bd80463464ff98f3b71": { "query": "SELECT * from mempool_txs\n WHERE tx_hash = $1", "describe": { diff --git a/core/lib/storage/src/forced_exit_requests/mod.rs b/core/lib/storage/src/forced_exit_requests/mod.rs index 142531ed7d..6698ea874b 100644 --- a/core/lib/storage/src/forced_exit_requests/mod.rs +++ b/core/lib/storage/src/forced_exit_requests/mod.rs @@ -11,6 +11,8 @@ use zksync_types::forced_exit_requests::{ ForcedExitRequest, ForcedExitRequestId, SaveForcedExitRequestQuery, }; +use zksync_types::tx::TxHash; + pub mod records; mod utils; @@ -33,18 +35,19 @@ impl<'a, 'c> ForcedExitRequestsSchema<'a, 'c> { let target_str = address_to_stored_string(&request.target); - let tokens = utils::tokens_vec_to_str(request.tokens.clone()); + let tokens = utils::vec_to_comma_list(request.tokens.clone()); let stored_request: DbForcedExitRequest = sqlx::query_as!( DbForcedExitRequest, r#" - INSERT INTO forced_exit_requests ( target, tokens, price_in_wei, valid_until ) - VALUES ( $1, $2, $3, $4 ) + INSERT INTO forced_exit_requests ( target, tokens, price_in_wei, created_at, valid_until ) + VALUES ( $1, $2, $3, $4, $5 ) RETURNING * "#, target_str, &tokens, price_in_wei, + request.created_at, request.valid_until ) .fetch_one(self.0.conn()) @@ -80,7 +83,7 @@ impl<'a, 'c> ForcedExitRequestsSchema<'a, 'c> { Ok(request) } - pub async fn fulfill_request( + pub async fn set_fulfilled_at( &mut self, id: ForcedExitRequestId, fulfilled_at: DateTime, @@ -99,7 +102,7 @@ impl<'a, 'c> ForcedExitRequestsSchema<'a, 'c> { .execute(self.0.conn()) .await?; - metrics::histogram!("sql.forced_exit_requests.fulfill_request", start.elapsed()); + metrics::histogram!("sql.forced_exit_requests.set_fulfilled_at", start.elapsed()); Ok(()) } @@ -115,6 +118,7 @@ impl<'a, 'c> ForcedExitRequestsSchema<'a, 'c> { SELECT * FROM forced_exit_requests WHERE fulfilled_at IS NULL AND created_at = ( SELECT MIN(created_at) FROM forced_exit_requests + WHERE fulfilled_at IS NULL ) LIMIT 1 "# @@ -124,10 +128,62 @@ impl<'a, 'c> ForcedExitRequestsSchema<'a, 'c> { .map(|r| r.into()); metrics::histogram!( - "sql.forced_exit_requests.get_min_unfulfilled_request", + "sql.forced_exit_requests.get_oldest_unfulfilled_request", start.elapsed() ); Ok(request) } + + pub async fn set_fulfilled_by( + &mut self, + id: ForcedExitRequestId, + tx_hashes: Vec, + ) -> QueryResult<()> { + let start = Instant::now(); + + let hash_str = utils::vec_to_comma_list(tx_hashes); + + sqlx::query!( + r#" + UPDATE forced_exit_requests + SET fulfilled_by = $1 + WHERE id = $2 + "#, + &hash_str, + id + ) + .execute(self.0.conn()) + .await?; + + metrics::histogram!("sql.forced_exit_requests.set_fulfilled_by", start.elapsed()); + Ok(()) + } + + // Normally this function should not return any more + // than one request, but it was decided to make to more + // general from the start + pub async fn get_unconfirmed_requests(&mut self) -> QueryResult> { + let start = Instant::now(); + + let requests: Vec = sqlx::query_as!( + DbForcedExitRequest, + r#" + SELECT * FROM forced_exit_requests + WHERE fulfilled_at IS NULL AND fulfilled_by IS NOT NULL + "# + ) + .fetch_all(self.0.conn()) + .await? + .into_iter() + .map(|rec| rec.into()) + .collect(); + + metrics::histogram!( + "sql.forced_exit_requests.get_unconfirmed_requests", + start.elapsed() + ); + + Ok(requests) + } } diff --git a/core/lib/storage/src/forced_exit_requests/records.rs b/core/lib/storage/src/forced_exit_requests/records.rs index 0298d08323..de9f9ff0d2 100644 --- a/core/lib/storage/src/forced_exit_requests/records.rs +++ b/core/lib/storage/src/forced_exit_requests/records.rs @@ -4,6 +4,7 @@ use num::{bigint::ToBigInt, BigInt}; use sqlx::types::BigDecimal; use zksync_basic_types::TokenId; use zksync_types::forced_exit_requests::ForcedExitRequest; +use zksync_types::tx::TxHash; use super::utils; @@ -15,6 +16,7 @@ pub struct DbForcedExitRequest { pub price_in_wei: BigDecimal, pub valid_until: DateTime, pub created_at: DateTime, + pub fulfilled_by: Option, pub fulfilled_at: Option>, } @@ -22,7 +24,8 @@ impl From for DbForcedExitRequest { fn from(request: ForcedExitRequest) -> Self { let price_in_wei = BigDecimal::from(BigInt::from(request.price_in_wei.clone())); - let tokens = utils::tokens_vec_to_str(request.tokens.clone()); + let tokens = utils::vec_to_comma_list(request.tokens); + let fulfilled_by = request.fulfilled_by.map(utils::vec_to_comma_list); Self { id: request.id, target: address_to_stored_string(&request.target), @@ -31,6 +34,7 @@ impl From for DbForcedExitRequest { valid_until: request.valid_until, created_at: request.created_at, fulfilled_at: request.fulfilled_at, + fulfilled_by, } } } @@ -46,11 +50,8 @@ impl Into for DbForcedExitRequest { // means that invalid data is stored in the DB .expect("Invalid forced exit request has been stored"); - let tokens: Vec = self - .tokens - .split(",") - .map(|num_str| num_str.parse().unwrap()) - .collect(); + let tokens: Vec = utils::comma_list_to_vec(self.tokens); + let fulfilled_by: Option> = self.fulfilled_by.map(utils::comma_list_to_vec); ForcedExitRequest { id: self.id, @@ -60,6 +61,7 @@ impl Into for DbForcedExitRequest { created_at: self.created_at, valid_until: self.valid_until, fulfilled_at: self.fulfilled_at, + fulfilled_by, } } } diff --git a/core/lib/storage/src/forced_exit_requests/utils.rs b/core/lib/storage/src/forced_exit_requests/utils.rs index 1fafc1b862..b8b9b7b3e7 100644 --- a/core/lib/storage/src/forced_exit_requests/utils.rs +++ b/core/lib/storage/src/forced_exit_requests/utils.rs @@ -1,6 +1,37 @@ +use std::fmt::Debug; +use std::{str::FromStr, string::ToString}; use zksync_basic_types::TokenId; -pub fn tokens_vec_to_str(token_ids: Vec) -> String { - let token_strings: Vec = token_ids.iter().map(|t| (*t).to_string()).collect(); - token_strings.join(",") +pub fn vec_to_comma_list(elems: Vec) -> String { + let strs: Vec = elems.iter().map(|elem| (*elem).to_string()).collect(); + + strs.join(",") +} + +pub fn comma_list_to_vec(elems: String) -> Vec +where + ::Err: Debug, +{ + elems + .split(",") + .map(|str| T::from_str(str).expect("Failed to deserialize stored item")) + .collect() } + +// pub fn tokens_vec_to_str(token_ids: Vec) -> String { +// let token_strings: Vec = token_ids.iter().map(|&t| t.to_string()).collect(); +// token_strings.join(",") +// } + +// pub fn tokens_str_to_vec(tokens: String) -> Vec { + +// } + +// pub fn hashes_vec_to_str(hashes: Vec) -> String { +// let hashes_strings: Vec = hashes.iter().map(|&h| h.toString()).collect(); +// hashes_strings.join(",") +// } + +// pub fn tokens_str_to_vec(tokens: String) -> Vec { + +// } diff --git a/core/lib/storage/src/tests/forced_exit_requests.rs b/core/lib/storage/src/tests/forced_exit_requests.rs index 00b13b7558..a9d2e72256 100644 --- a/core/lib/storage/src/tests/forced_exit_requests.rs +++ b/core/lib/storage/src/tests/forced_exit_requests.rs @@ -1,89 +1,93 @@ use std::str::FromStr; -use crate::forced_exit_requests::ForcedExitRequestsSchema; use crate::tests::db_test; use crate::QueryResult; use crate::StorageProcessor; +use crate::{data_restore::DataRestoreSchema, forced_exit_requests::ForcedExitRequestsSchema}; use chrono::{DateTime, Timelike, Utc}; use num::{BigUint, FromPrimitive}; +use std::convert::From; +use std::time::Duration; +use tokio::time; use zksync_basic_types::Address; use zksync_types::forced_exit_requests::{ForcedExitRequest, SaveForcedExitRequestQuery}; -#[db_test] -async fn store_forced_exit_request(mut storage: StorageProcessor<'_>) -> QueryResult<()> { - let now = Utc::now().with_nanosecond(0).unwrap(); - - let request = SaveForcedExitRequestQuery { - target: Address::from_str("c0f97CC918C9d6fA4E9fc6be61a6a06589D199b2").unwrap(), - tokens: vec![0], - price_in_wei: BigUint::from_i32(121212).unwrap(), - valid_until: DateTime::from(now), - }; +use std::ops::Add; - let fe_request = ForcedExitRequestsSchema(&mut storage) - .store_request(request) - .await?; +use zksync_types::TokenId; - assert_eq!(fe_request.id, 1); +#[db_test] +async fn get_oldest_unfulfilled_request(mut storage: StorageProcessor<'_>) -> QueryResult<()> { + let mut now = Utc::now().with_nanosecond(0).unwrap(); - let expected_response = ForcedExitRequest { - id: 1, - target: Address::from_str("c0f97CC918C9d6fA4E9fc6be61a6a06589D199b2").unwrap(), - tokens: vec![0], - price_in_wei: BigUint::from_i32(121212).unwrap(), - valid_until: DateTime::from(now), - fulfilled_at: None, - }; + let requests = vec![ + SaveForcedExitRequestQuery { + target: Address::from_str("c0f97CC918C9d6fA4E9fc6be61a6a06589D199b2").unwrap(), + tokens: vec![TokenId(1)], + price_in_wei: BigUint::from_i32(212).unwrap(), + created_at: DateTime::from(now.clone()), + valid_until: DateTime::from(now.clone()), + }, + SaveForcedExitRequestQuery { + target: Address::from_str("c0f97CC918C9d6fA4E9fc6be61a6a06589D199b2").unwrap(), + tokens: vec![TokenId(1)], + price_in_wei: BigUint::from_i32(1).unwrap(), + created_at: DateTime::from(now.clone()), + valid_until: DateTime::from(now.clone()), + }, + SaveForcedExitRequestQuery { + target: Address::from_str("c0f97CC918C9d6fA4E9fc6be61a6a06589D199b2").unwrap(), + tokens: vec![TokenId(20)], + price_in_wei: BigUint::from_str("1000000000000000").unwrap(), + created_at: DateTime::from(now.clone()), + valid_until: DateTime::from(now.clone()), + }, + ]; - let response = ForcedExitRequestsSchema(&mut storage) - .get_request_by_id(fe_request.id) - .await - .expect("Failed to get forced exit by id"); + let mut stored_requests: Vec = vec![]; + let interval = chrono::Duration::seconds(1); - assert_eq!(expected_response, response); - Ok(()) -} + for req in requests.into_iter() { + now = now.add(interval); + let created_at = now.clone(); + let valid_until = now.add(chrono::Duration::hours(32)); -// #[db_test] -// async fn get_max_forced_exit_used_id(mut storage: StorageProcessor<'_>) -> QueryResult<()> { -// let now = Utc::now().with_nanosecond(0).unwrap(); + stored_requests.push( + ForcedExitRequestsSchema(&mut storage) + .store_request(SaveForcedExitRequestQuery { + created_at, + valid_until, + ..req + }) + .await + .unwrap(), + ); + } -// let requests = [ -// ForcedExitRequest { -// id: 1, -// account_id: 1, -// tokens: vec!(1), -// price_in_wei: BigUint::from_i32(212).unwrap(), -// valid_until: DateTime::from(now), -// }, -// ForcedExitRequest { -// id: 2, -// account_id: 12, -// tokens: vec!(0), -// price_in_wei: BigUint::from_i32(1).unwrap(), -// valid_until: DateTime::from(now), -// }, -// ForcedExitRequest { -// id: 7, -// account_id: 3, -// tokens: vec!(20), -// price_in_wei: BigUint::from_str("1000000000000000").unwrap(), -// valid_until: DateTime::from(now), -// }, -// ]; + ForcedExitRequestsSchema(&mut storage) + .set_fulfilled_at(stored_requests[0].id, Utc::now()) + .await?; -// for req in requests.iter() { -// ForcedExitRequestsSchema(&mut storage) -// .store_request(&req) -// .await?; -// } + let oldest_unfulfilled_request = ForcedExitRequestsSchema(&mut storage) + .get_oldest_unfulfilled_request() + .await? + .unwrap(); + // The first request has been fulfilled. Thus, the second one should be the oldest + assert_eq!(oldest_unfulfilled_request.id, stored_requests[1].id); -// let max_id = ForcedExitRequestsSchema(&mut storage) -// .get_max_used_id() -// .await -// .expect("Failed to get forced exit by id"); + // Now filling all the remaining requests + ForcedExitRequestsSchema(&mut storage) + .set_fulfilled_at(stored_requests[1].id, Utc::now()) + .await?; + ForcedExitRequestsSchema(&mut storage) + .set_fulfilled_at(stored_requests[2].id, Utc::now()) + .await?; -// assert_eq!(max_id, 7); + let oldest_unfulfilled_request = ForcedExitRequestsSchema(&mut storage) + .get_oldest_unfulfilled_request() + .await?; + // The first request has been fulfilled. Thus, the second one should be the oldest + assert!(matches!(oldest_unfulfilled_request, None)); -// Ok(()) -// } + Ok(()) +} diff --git a/core/lib/types/src/forced_exit_requests.rs b/core/lib/types/src/forced_exit_requests.rs index 095799ebad..a827dc59dd 100644 --- a/core/lib/types/src/forced_exit_requests.rs +++ b/core/lib/types/src/forced_exit_requests.rs @@ -12,6 +12,8 @@ use ethabi::{decode, ParamType}; use std::convert::TryFrom; use zksync_basic_types::{Log, U256}; +use crate::tx::TxHash; + #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)] #[serde(rename_all = "camelCase")] pub struct ForcedExitRequest { @@ -22,6 +24,7 @@ pub struct ForcedExitRequest { pub price_in_wei: BigUint, pub valid_until: DateTime, pub created_at: DateTime, + pub fulfilled_by: Option>, pub fulfilled_at: Option>, } @@ -31,6 +34,7 @@ pub struct SaveForcedExitRequestQuery { pub tokens: Vec, #[serde(with = "BigUintSerdeAsRadix10Str")] pub price_in_wei: BigUint, + pub created_at: DateTime, pub valid_until: DateTime, } #[derive(Debug, Clone, Copy)] diff --git a/minilog b/minilog new file mode 100644 index 0000000000..7588922007 --- /dev/null +++ b/minilog @@ -0,0 +1,4 @@ +Using localhost database: +DATABASE_URL = postgres://postgres@localhost/plasma +error: `cargo check` failed with status: exit code: 101 +error: `cargo check` failed with status: exit code: 101 From 5e96fba632b007b00098316e81aeefa849078c6b Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Tue, 16 Feb 2021 22:13:44 +0200 Subject: [PATCH 30/90] [wip]: Life-proof eth_watch --- .../src/eth_watch.rs | 93 +++--- .../src/forced_exit_sender.rs | 293 ++++++++++++------ .../src/configs/forced_exit_requests.rs | 4 +- .../storage/src/forced_exit_requests/mod.rs | 6 +- core/lib/types/src/forced_exit_requests.rs | 15 +- 5 files changed, 254 insertions(+), 157 deletions(-) diff --git a/core/bin/zksync_forced_exit_requests/src/eth_watch.rs b/core/bin/zksync_forced_exit_requests/src/eth_watch.rs index 5c40c977b2..2cd5031159 100644 --- a/core/bin/zksync_forced_exit_requests/src/eth_watch.rs +++ b/core/bin/zksync_forced_exit_requests/src/eth_watch.rs @@ -125,10 +125,13 @@ fn time_range_to_block_diff(from: DateTime, to: DateTime) -> u64 { let millis_from = from.timestamp_millis(); let millis_to = to.timestamp_millis(); - // It does not really matter to wether cail or floor the division - return ((millis_to - millis_from) / MILLIS_PER_BLOCK) - .try_into() - .unwrap(); + if millis_to >= millis_from { + ((millis_to - millis_from) / MILLIS_PER_BLOCK) + .try_into() + .unwrap() + } else { + 0u64 + } } impl ForcedExitContractWatcher { @@ -140,14 +143,9 @@ impl ForcedExitContractWatcher { let oldest_request = fe_schema.get_oldest_unfulfilled_request().await?; - let wait_confirmations: u64 = self - .config - .forced_exit_requests - .wait_confirmations - .try_into() - .unwrap(); + let wait_confirmations = self.config.forced_exit_requests.wait_confirmations; - // No oldest requests means that there are no requests that were possibly ignored + // No oldest request means that there are no requests that were possibly ignored let oldest_request = match oldest_request { Some(r) => r, None => { @@ -159,19 +157,10 @@ impl ForcedExitContractWatcher { let block_diff = time_range_to_block_diff(oldest_request.created_at, Utc::now()); let max_possible_viewed_block = block - wait_confirmations; + // If the last block is too young, then we will use max_possible_viewed_block, + // otherwise we will use block - block_diff self.last_viewed_block = std::cmp::min(block - block_diff, max_possible_viewed_block); - /* - blocks = time_diff_to_blocks = - - last_processed_block = block - blocks - - comes a tx => check that it's valid - comes a tx => check that the id hasn't already been added to the fulfilled db - if everything is finve => add the tx - - once the block is processed, remove everything too old and unfulfilled and move on - */ Ok(()) } @@ -185,7 +174,10 @@ impl ForcedExitContractWatcher { self.mode = WatcherMode::Backoff(backoff_until); // This is needed to track how much time is spent in backoff mode // and trigger grafana alerts - metrics::histogram!("eth_watcher.enter_backoff_mode", RATE_LIMIT_DELAY); + metrics::histogram!( + "forced_exit_requests.eth_watcher.enter_backoff_mode", + RATE_LIMIT_DELAY + ); } fn polling_allowed(&mut self) -> bool { @@ -224,42 +216,37 @@ impl ForcedExitContractWatcher { return; } - let last_block = self.eth_client.block_number().await; - - if let Err(error) = last_block { - self.handle_infura_error(error); - return; - } - - let wait_confirmations: u64 = self - .config - .forced_exit_requests - .wait_confirmations - .try_into() - .unwrap(); - - let last_block = last_block.unwrap(); - - let last_confirmed_block = last_block - wait_confirmations; + let last_block = match self.eth_client.block_number().await { + Ok(block) => block, + Err(error) => { + self.handle_infura_error(error); + return; + } + }; + let wait_confirmations = self.config.forced_exit_requests.wait_confirmations; + let last_confirmed_block = last_block.saturating_sub(wait_confirmations); if last_confirmed_block <= self.last_viewed_block { return; - } + }; let events = self .eth_client .get_funds_received_events(self.last_viewed_block + 1, last_confirmed_block) .await; - if let Err(error) = events { - self.handle_infura_error(error); - return; - } - let events = events.unwrap(); + let events = match events { + Ok(e) => e, + Err(error) => { + self.handle_infura_error(error); + return; + } + }; for e in events { - self.forced_exit_sender - .process_request(e.amount as i64) + let processing_result = self + .forced_exit_sender + .process_request(e.amount.clone()) .await; } @@ -289,6 +276,7 @@ impl ForcedExitContractWatcher { } }; + // We don't expect rate limiting to happen again self.restore_state_from_eth(block) .await .expect("Failed to restore state for ForcedExit eth_watcher"); @@ -320,7 +308,7 @@ pub fn run_forced_exit_contract_watcher( // It is ok to unwrap here, since if fe_sender is not created, then // the watcher is meaningless - let forced_exit_sender = ForcedExitSender::new( + let mut forced_exit_sender = ForcedExitSender::new( core_api_client.clone(), connection_pool.clone(), config.clone(), @@ -328,7 +316,12 @@ pub fn run_forced_exit_contract_watcher( .await .unwrap(); - let mut contract_watcher = ForcedExitContractWatcher { + forced_exit_sender + .await_unconfirmed() + .await + .expect("Unexpected error while trying to wait for unconfirmed transactions"); + + let contract_watcher = ForcedExitContractWatcher { core_api_client, connection_pool, config, diff --git a/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs b/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs index b07a2a5e65..ca50d4bb04 100644 --- a/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs +++ b/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs @@ -1,28 +1,35 @@ -use std::{convert::TryFrom, time::Instant}; +use std::{ + convert::{TryFrom, TryInto}, + ops::{AddAssign, Sub}, + time::Instant, +}; use anyhow::format_err; use ethabi::{Contract as ContractAbi, Hash}; use fee_ticker::validator::watcher; use franklin_crypto::bellman::PrimeFieldRepr; -use num::{BigUint, FromPrimitive, ToPrimitive}; +use num::{BigUint, FromPrimitive, Integer, ToPrimitive}; use std::fmt::Debug; -use tokio::task::JoinHandle; +use tokio::time; +use tokio::{stream::StreamExt, task::JoinHandle}; use web3::{ contract::{Contract, Options}, + futures::TryFutureExt, transports::Http, types::{BlockNumber, FilterBuilder, Log}, Web3, }; use zksync_config::ZkSyncConfig; +use zksync_contracts::zksync_contract; use zksync_storage::{ chain::{account::AccountSchema, operations_ext::records::TxReceiptResponse}, ConnectionPool, StorageProcessor, }; - -use zksync_contracts::zksync_contract; use zksync_types::{ - forced_exit_requests::ForcedExitRequest, tx::TimeRange, tx::TxHash, AccountId, Address, Nonce, - PriorityOp, TokenId, ZkSyncTx, H160, U256, + forced_exit_requests::{ForcedExitRequest, ForcedExitRequestId, SaveForcedExitRequestQuery}, + tx::TimeRange, + tx::TxHash, + AccountId, Address, Nonce, PriorityOp, TokenId, ZkSyncTx, H160, U256, }; use chrono::Utc; @@ -39,14 +46,17 @@ use zksync_crypto::ff::PrimeField; use crate::eth_watch; +// We try to process a request 3 times before sending warnings in the console +const PROCESSING_ATTEMPTS: u8 = 3; + pub struct ForcedExitSender { core_api_client: CoreApiClient, connection_pool: ConnectionPool, config: ZkSyncConfig, - operator_account_id: AccountId, + forced_exit_sender_account_id: AccountId, sender_private_key: PrivateKey, } -async fn get_operator_account_id( +async fn get_forced_exit_sender_account_id( connection_pool: ConnectionPool, config: &ZkSyncConfig, ) -> anyhow::Result { @@ -83,9 +93,10 @@ impl ForcedExitSender { connection_pool: ConnectionPool, config: ZkSyncConfig, ) -> anyhow::Result { - let operator_account_id = get_operator_account_id(connection_pool.clone(), &config) - .await - .expect("Failed to get the sender id"); + let forced_exit_sender_account_id = + get_forced_exit_sender_account_id(connection_pool.clone(), &config) + .await + .expect("Failed to get the sender id"); let sender_private_key = hex::decode(&config.clone().forced_exit_requests.sender_private_key[2..]) @@ -96,48 +107,75 @@ impl ForcedExitSender { Ok(Self { core_api_client, connection_pool, - operator_account_id, + forced_exit_sender_account_id, config, sender_private_key, }) } - pub fn extract_id_from_amount(&self, amount: i64) -> i64 { + pub fn extract_id_from_amount(&self, amount: BigUint) -> (i64, BigUint) { let id_space_size: i64 = (10 as i64).pow(self.config.forced_exit_requests.digits_in_id as u32); - amount % id_space_size - } + let id_space_size = BigUint::from_i64(id_space_size).unwrap(); - pub async fn construct_forced_exit<'a>( - &self, - storage: &mut StorageProcessor<'a>, - fe_request: ForcedExitRequest, - ) -> anyhow::Result { - let mut account_schema = storage.chain().account_schema(); + let one = BigUint::from_u8(1u8).unwrap(); - let operator_state = account_schema - .last_committed_state_for_account(self.operator_account_id) - .await? - .expect("The operator account has no committed state"); - let operator_nonce = operator_state.nonce; + // Taking to the power of 1 and finding mod is the only way to find mod of + // the BigUint + let id = amount.modpow(&one, &id_space_size); + + // After extracting the id we need to delete it + // to make sure that amount is the same as in the db + let amount = amount.sub(&id); - // TODO: allow batches + (id.try_into().unwrap(), amount) + } + + pub fn build_forced_exit<'a>( + &self, + nonce: Nonce, + target: Address, + token: TokenId, + ) -> SignedZkSyncTx { let tx = ForcedExit::new_signed( - self.operator_account_id, - fe_request.target, - fe_request.tokens[0], + self.forced_exit_sender_account_id, + target, + token, BigUint::from(0u32), - operator_nonce, + nonce, TimeRange::default(), &self.sender_private_key, ) .expect("Failed to create signed transaction from ForcedExit"); - Ok(SignedZkSyncTx { + SignedZkSyncTx { tx: ZkSyncTx::ForcedExit(Box::new(tx)), eth_sign_data: None, - }) + } + } + + pub async fn build_transactions<'a>( + &self, + storage: &mut StorageProcessor<'a>, + fe_request: ForcedExitRequest, + ) -> anyhow::Result> { + let mut account_schema = storage.chain().account_schema(); + + let sender_state = account_schema + .last_committed_state_for_account(self.forced_exit_sender_account_id) + .await? + .expect("The forced exit sender account has no committed state"); + + let mut sender_nonce = sender_state.nonce; + let mut transactions: Vec = vec![]; + + for token in fe_request.tokens.into_iter() { + transactions.push(self.build_forced_exit(sender_nonce, fe_request.target, token)); + sender_nonce.add_assign(1); + } + + Ok(transactions) } // TODO: take the block timestamp into account instead of @@ -152,27 +190,74 @@ impl ForcedExitSender { // Returns the id the request if it should be fulfilled, // error otherwise - pub fn verify_request( - &self, - amount: i64, - request: Option, - ) -> anyhow::Result { + pub fn check_request(&self, amount: BigUint, request: Option) -> bool { let request = match request { Some(r) => r, None => { - return Err(anyhow::Error::msg("The request was not found")); + return false; } }; - if self.expired(&request) { - return Err(anyhow::Error::msg("The request was not found")); + !self.expired(&request) && request.price_in_wei == amount + } + + // Awaits until the request is complete + pub async fn await_unconfirmed_request<'a>( + &self, + storage: &mut StorageProcessor<'a>, + request: &ForcedExitRequest, + ) -> anyhow::Result<()> { + let hashes = request.fulfilled_by.clone(); + + if let Some(hashes) = hashes { + for hash in hashes.into_iter() { + self.wait_until_comitted(storage, hash).await?; + self.set_fulfilled_at(storage, request.id).await?; + } } + Ok(()) + } - if request.price_in_wei != BigUint::from_i64(amount).unwrap() { - return Err(anyhow::Error::msg("The request was not found")); + pub async fn get_unconfirmed_requests<'a>( + &self, + storage: &mut StorageProcessor<'a>, + ) -> anyhow::Result> { + let mut forced_exit_requests_schema = storage.forced_exit_requests_schema(); + forced_exit_requests_schema.get_unconfirmed_requests().await + } + + pub async fn set_fulfilled_by<'a>( + &self, + storage: &mut StorageProcessor<'a>, + id: ForcedExitRequestId, + value: Option>, + ) -> anyhow::Result<()> { + let mut forced_exit_requests_schema = storage.forced_exit_requests_schema(); + forced_exit_requests_schema + .set_fulfilled_by(id, value) + .await + } + + pub async fn await_unconfirmed(&mut self) -> anyhow::Result<()> { + let mut storage = self.connection_pool.access_storage().await?; + let unfullied_requests = self.get_unconfirmed_requests(&mut storage).await?; + + for request in unfullied_requests.into_iter() { + let await_result = self.await_unconfirmed_request(&mut storage, &request).await; + + if let Ok(_) = await_result { + continue; + } + + // A transaction has failed. That is not intended. + // We can safely cancel such transaction, since we will re-try to + // send it again later + log::error!("A previously sent forced exit transaction has failed. Canceling the tx."); + self.set_fulfilled_by(&mut storage, request.id, None) + .await?; } - return Ok(request); + Ok(()) } pub async fn get_request_by_id<'a>( @@ -183,14 +268,6 @@ impl ForcedExitSender { let mut fe_schema = storage.forced_exit_requests_schema(); let request = fe_schema.get_request_by_id(id).await?; - // { - // Ok(r) => r, - // Err(e) => { - // log::warn!("ForcedExitRequests: Fail to get request by id: {}", e); - // return; - // } - // }; - Ok(request) } @@ -224,20 +301,44 @@ impl ForcedExitSender { .await } + pub async fn send_transactions<'a>( + &self, + storage: &mut StorageProcessor<'a>, + request: &ForcedExitRequest, + txs: Vec, + ) -> anyhow::Result> { + let mut db_transaction = storage.start_transaction().await?; + let mut schema = db_transaction.forced_exit_requests_schema(); + + let hashes: Vec = txs.iter().map(|tx| tx.hash()).collect(); + self.core_api_client.send_txs_batch(txs, vec![]).await??; + + schema + .set_fulfilled_by(request.id, Some(hashes.clone())) + .await?; + + db_transaction.commit().await?; + + Ok(hashes) + } + pub async fn wait_until_comitted<'a>( &self, storage: &mut StorageProcessor<'a>, tx_hash: TxHash, ) -> anyhow::Result<()> { - let poll_interval: i32 = 200; + let timeout_millis: u64 = 120000; + let poll_interval_millis: u64 = 200; + let poll_interval = time::Duration::from_secs(poll_interval_millis); + let mut timer = time::interval(poll_interval); - // If there is no receipt for 20 seconds, we consider the comitment failed - let timeout: i32 = 60000; - let mut time_passed: i32 = 0; + let mut time_passed: u64 = 0; loop { - if time_passed >= timeout { - panic!("Comitting tx failed!"); + if time_passed >= timeout_millis { + // If a transaction takes more than 2 minutes to commit we consider the server + // broken and panic + panic!("Comitting ForcedExit transaction failed!"); } let receipt = self.get_receipt(storage, tx_hash).await?; @@ -246,57 +347,61 @@ impl ForcedExitSender { if tx_receipt.success { return Ok(()); } else { - panic!("FE Transaction failed") + return Err(anyhow::Error::msg("ForcedExit transaction failed")); } } + + timer.tick().await; + time_passed += poll_interval_millis; } } - pub async fn process_request(&self, amount: i64) { - let id = self.extract_id_from_amount(amount); - // After extracting the id we need to delete it from the db - // to make sure that amount is the same as in the db - let amount = amount - id; + pub async fn try_process_request(&self, amount: BigUint) -> anyhow::Result<()> { + let (id, amount) = self.extract_id_from_amount(amount); - let mut storage = match self.connection_pool.access_storage().await { - Ok(storage) => storage, - Err(error) => { - log::warn!("Failed to acquire db connection for processing forced_exit_request, reason: {}", error); - return; - } - }; + let mut storage = self.connection_pool.access_storage().await?; let fe_request = self .get_request_by_id(&mut storage, id) .await .expect("Failed to get request by id"); - let fe_request = match self.verify_request(amount, fe_request) { - Ok(r) => r, - Err(_) => { - // The request was not valid, that's fine - return; - } + let fe_request = if self.check_request(amount, fe_request.clone()) { + fe_request.unwrap() + } else { + // The request was not valid, that's fine + return Ok(()); }; - let fe_tx = self - .construct_forced_exit(&mut storage, fe_request) - .await - .expect("Failed to construct ForcedExit"); - let tx_hash = fe_tx.hash(); + let txs = self + .build_transactions(&mut storage, fe_request.clone()) + .await?; + let hashes = self + .send_transactions(&mut storage, &fe_request, txs) + .await?; - self.core_api_client - .send_tx(fe_tx) - .await - .expect("Failed to send transaction to mempool") - .unwrap(); + // We wait only for the first transaction to complete since the transactions + // are sent in a batch + self.wait_until_comitted(&mut storage, hashes[0]).await?; + self.set_fulfilled_at(&mut storage, id).await?; - self.wait_until_comitted(&mut storage, tx_hash) - .await - .expect("Comittment waiting failed"); + Ok(()) + } - self.set_fulfilled_at(&mut storage, id) - .await - .expect("Error while fulfulling the request"); + pub async fn process_request(&self, amount: BigUint) { + let mut attempts: u8 = 0; + loop { + let processing_attempt = self.try_process_request(amount.clone()).await; + + if let Ok(_) = processing_attempt { + return; + } else { + attempts += 1; + } + + if attempts >= PROCESSING_ATTEMPTS { + log::error!("Failed to process forced exit for the {} time", attempts); + } + } } } diff --git a/core/lib/config/src/configs/forced_exit_requests.rs b/core/lib/config/src/configs/forced_exit_requests.rs index f7d70864ba..30765b6d88 100644 --- a/core/lib/config/src/configs/forced_exit_requests.rs +++ b/core/lib/config/src/configs/forced_exit_requests.rs @@ -20,7 +20,7 @@ struct ForcedExitRequestsInternalConfig { pub tx_interval_scaling_factor: f64, pub price_per_token: i64, pub digits_in_id: u8, - pub wait_confirmations: i64, + pub wait_confirmations: u64, pub sender_private_key: String, pub sender_account_address: Address, } @@ -33,7 +33,7 @@ pub struct ForcedExitRequestsConfig { pub max_tx_interval: i64, pub price_per_token: i64, pub digits_in_id: u8, - pub wait_confirmations: i64, + pub wait_confirmations: u64, pub sender_private_key: String, pub sender_account_address: Address, } diff --git a/core/lib/storage/src/forced_exit_requests/mod.rs b/core/lib/storage/src/forced_exit_requests/mod.rs index 6698ea874b..653a3f036a 100644 --- a/core/lib/storage/src/forced_exit_requests/mod.rs +++ b/core/lib/storage/src/forced_exit_requests/mod.rs @@ -138,11 +138,11 @@ impl<'a, 'c> ForcedExitRequestsSchema<'a, 'c> { pub async fn set_fulfilled_by( &mut self, id: ForcedExitRequestId, - tx_hashes: Vec, + tx_hashes: Option>, ) -> QueryResult<()> { let start = Instant::now(); - let hash_str = utils::vec_to_comma_list(tx_hashes); + let hash_str = tx_hashes.map(utils::vec_to_comma_list); sqlx::query!( r#" @@ -150,7 +150,7 @@ impl<'a, 'c> ForcedExitRequestsSchema<'a, 'c> { SET fulfilled_by = $1 WHERE id = $2 "#, - &hash_str, + hash_str, id ) .execute(self.0.conn()) diff --git a/core/lib/types/src/forced_exit_requests.rs b/core/lib/types/src/forced_exit_requests.rs index a827dc59dd..cf39381596 100644 --- a/core/lib/types/src/forced_exit_requests.rs +++ b/core/lib/types/src/forced_exit_requests.rs @@ -10,6 +10,7 @@ pub type ForcedExitRequestId = i64; use anyhow::format_err; use ethabi::{decode, ParamType}; use std::convert::TryFrom; +use std::convert::TryInto; use zksync_basic_types::{Log, U256}; use crate::tx::TxHash; @@ -37,9 +38,9 @@ pub struct SaveForcedExitRequestQuery { pub created_at: DateTime, pub valid_until: DateTime, } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone)] pub struct FundsReceivedEvent { - pub amount: u64, + pub amount: BigUint, } impl TryFrom for FundsReceivedEvent { @@ -54,13 +55,11 @@ impl TryFrom for FundsReceivedEvent { ) .map_err(|e| format_err!("Event data decode: {:?}", e))?; + let bytes = dec_ev.remove(0).to_bytes().unwrap(); + let amount = u128::from_be_bytes(bytes.try_into().unwrap()); + Ok(FundsReceivedEvent { - amount: dec_ev - .remove(0) - .to_uint() - .as_ref() - .map(U256::as_u64) - .unwrap(), + amount: BigUint::from(amount), }) } } From b4dc924447b6f593d1eaa52f2a1eb6a621611ab5 Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Wed, 17 Feb 2021 12:09:33 +0200 Subject: [PATCH 31/90] fmt & clippy --- contracts/contracts/ForcedExit.sol | 14 +- .../contracts/dev-contracts/SelfDestruct.sol | 5 +- contracts/src.ts/deploy.ts | 13 +- contracts/test/unit_tests/forced_exit_test.ts | 22 ++-- .../rest/forced_exit_requests/v01.rs | 12 +- .../zksync_api/src/api_server/rest/helpers.rs | 1 - .../zksync_api/src/api_server/rest/v1/mod.rs | 2 +- .../src/database.rs | 0 .../src/eth_watch.rs | 15 +-- .../src/forced_exit_sender.rs | 124 +++++++----------- .../zksync_forced_exit_requests/src/lib.rs | 91 +++---------- .../zksync_forced_exit_requests/src/utils.rs | 3 - .../src/rest/forced_exit_requests/mod.rs | 2 +- .../src/configs/forced_exit_requests.rs | 1 - .../src/forced_exit_requests/records.rs | 2 +- .../storage/src/forced_exit_requests/utils.rs | 21 +-- .../storage/src/tests/forced_exit_requests.rs | 23 ++-- core/lib/types/src/forced_exit_requests.rs | 8 +- .../ts-tests/tests/forced-exit-requests.ts | 63 ++++----- core/tests/ts-tests/tests/misc.ts | 6 +- .../ts-tests/tests/withdrawal-helpers.test.ts | 11 +- infrastructure/zk/src/server.ts | 12 +- 22 files changed, 160 insertions(+), 291 deletions(-) delete mode 100644 core/bin/zksync_forced_exit_requests/src/database.rs delete mode 100644 core/bin/zksync_forced_exit_requests/src/utils.rs diff --git a/contracts/contracts/ForcedExit.sol b/contracts/contracts/ForcedExit.sol index a9a33e4bcf..86b09dd5e2 100644 --- a/contracts/contracts/ForcedExit.sol +++ b/contracts/contracts/ForcedExit.sol @@ -5,7 +5,7 @@ pragma solidity ^0.7.0; pragma experimental ABIEncoderV2; import "./Utils.sol"; -import "./Ownable.sol"; +import "./Ownable.sol"; import "./ReentrancyGuard.sol"; contract ForcedExit is Ownable, ReentrancyGuard { @@ -22,9 +22,7 @@ contract ForcedExit is Ownable, ReentrancyGuard { receiver = payable(_master); } - event FundsReceived( - uint256 _amount - ); + event FundsReceived(uint256 _amount); function setReceiver(address payable _newReceiver) external { requireMaster(msg.sender); @@ -44,25 +42,25 @@ contract ForcedExit is Ownable, ReentrancyGuard { enabled = true; } - // Withdraw funds that failed to reach zkSync due to out-of-gas + // Withdraw funds that failed to reach zkSync due to out-of-gas function withdrawPendingFunds(address payable _to, uint128 amount) external nonReentrant { requireMaster(msg.sender); uint256 balance = address(this).balance; require(amount <= balance, "The balance is lower than the amount"); - + (bool success, ) = _to.call{value: amount}(""); require(success, "d"); // ETH withdraw failed } - // We ave to use fallback instead of `receive` since the ethabi + // We ave to use fallback instead of `receive` since the ethabi // library can't decode the receive function: // https://github.com/rust-ethereum/ethabi/issues/185 fallback() external payable nonReentrant { require(enabled, "Contract is disabled"); require(receiver != address(0), "Receiver must be non-zero"); - + (bool success, ) = receiver.call{value: msg.value}(""); require(success, "d"); // ETH withdraw failed diff --git a/contracts/contracts/dev-contracts/SelfDestruct.sol b/contracts/contracts/dev-contracts/SelfDestruct.sol index 198c6e0754..e168be2e78 100644 --- a/contracts/contracts/dev-contracts/SelfDestruct.sol +++ b/contracts/contracts/dev-contracts/SelfDestruct.sol @@ -5,11 +5,10 @@ pragma solidity ^0.7.0; pragma experimental ABIEncoderV2; contract SelfDestruct { - function destroy(address payable to) external { - selfdestruct(to); + selfdestruct(to); } - // Need this to send some funds to the contract + // Need this to send some funds to the contract receive() external payable {} } diff --git a/contracts/src.ts/deploy.ts b/contracts/src.ts/deploy.ts index 836cc2a2fc..79cf043864 100644 --- a/contracts/src.ts/deploy.ts +++ b/contracts/src.ts/deploy.ts @@ -215,10 +215,15 @@ export class Deployer { if (this.verbose) { console.log('Deploying ForcedExit contract'); } - const forcedExitContract = await deployContract(this.deployWallet, this.contracts.forcedExit, [this.deployWallet.address], { - gasLimit: 6000000, - ...ethTxOptions - }); + const forcedExitContract = await deployContract( + this.deployWallet, + this.contracts.forcedExit, + [this.deployWallet.address], + { + gasLimit: 6000000, + ...ethTxOptions + } + ); const zksRec = await forcedExitContract.deployTransaction.wait(); const zksGasUsed = zksRec.gasUsed; const gasPrice = forcedExitContract.deployTransaction.gasPrice; diff --git a/contracts/test/unit_tests/forced_exit_test.ts b/contracts/test/unit_tests/forced_exit_test.ts index 40b56e53e3..a9ca12ab76 100644 --- a/contracts/test/unit_tests/forced_exit_test.ts +++ b/contracts/test/unit_tests/forced_exit_test.ts @@ -1,5 +1,4 @@ const { expect } = require('chai'); -const { getCallRevertReason } = require('./common'); const hardhat = require('hardhat'); import { Signer, Contract, ContractTransaction, utils, BigNumber } from 'ethers'; @@ -13,7 +12,7 @@ describe('ForcedExit unit tests', function () { let wallet1: Signer; let wallet2: Signer; let wallet3: Signer; - + before(async () => { [wallet1, wallet2, wallet3] = await hardhat.ethers.getSigners(); @@ -32,15 +31,12 @@ describe('ForcedExit unit tests', function () { to: forcedExitContract.address, value: TX_AMOUNT }); - const txReceipt = await txHandle.wait(); + const txReceipt = await txHandle.wait(); expect(txReceipt.logs.length == 1, 'No events were emitted').to.be.true; - const receivedFundsAmount: BigNumber = forcedExitContract - .interface - .parseLog(txReceipt.logs[0]) - .args[0]; + const receivedFundsAmount: BigNumber = forcedExitContract.interface.parseLog(txReceipt.logs[0]).args[0]; - expect(receivedFundsAmount.eq(TX_AMOUNT), 'Didn\'t emit the amount of sent data').to.be.true; + expect(receivedFundsAmount.eq(TX_AMOUNT), "Didn't emit the amount of sent data").to.be.true; const receiverBalanceAfter = await wallet3.getBalance(); const diff = receiverBalanceAfter.sub(receiverBalanceBefore); @@ -63,7 +59,10 @@ describe('ForcedExit unit tests', function () { await destructHandle.wait(); const masterBalanceBefore = await wallet1.getBalance(); - const withdrawHandle: ContractTransaction = await forcedExitContract.withdrawPendingFunds(wallet1.getAddress(), TX_AMOUNT); + const withdrawHandle: ContractTransaction = await forcedExitContract.withdrawPendingFunds( + wallet1.getAddress(), + TX_AMOUNT + ); const withdrawReceipt = await withdrawHandle.wait(); const masterBalanceAfter = await wallet1.getBalance(); @@ -72,7 +71,6 @@ describe('ForcedExit unit tests', function () { expect(diff.eq(expectedDiff), 'Pending funds have not arrived to the account').to.be.true; }); - it('Check redirection', async () => { const disableHandle = await forcedExitContract.disable(); await disableHandle.wait(); @@ -88,11 +86,11 @@ describe('ForcedExit unit tests', function () { failed1 = true; } - expect(failed1, "Transfer to the disabled contract does not fail").to.be.true; + expect(failed1, 'Transfer to the disabled contract does not fail').to.be.true; const enableHandle = await forcedExitContract.enable(); await enableHandle.wait(); - + const txHandle = await wallet2.sendTransaction({ to: forcedExitContract.address, value: TX_AMOUNT diff --git a/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs b/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs index 677b928928..5ddd450990 100644 --- a/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs +++ b/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs @@ -99,7 +99,7 @@ pub async fn submit_request( let mut storage = data.connection_pool.access_storage().await.map_err(|err| { vlog::warn!("Internal Server Error: '{}';", err); - return ApiError::internal(""); + ApiError::internal("") })?; data.forced_exit_checker @@ -134,9 +134,7 @@ pub async fn submit_request( tokens_schema .get_token(TokenLike::Id(*token_id)) .await - .map_err(|_| { - return ApiError::bad_request("One of the tokens does no exist"); - })?; + .map_err(|_| ApiError::bad_request("One of the tokens does no exist"))?; } let mut fe_schema = storage.forced_exit_requests_schema(); @@ -153,9 +151,7 @@ pub async fn submit_request( valid_until, }) .await - .map_err(|_| { - return ApiError::internal(""); - })?; + .map_err(|_| ApiError::internal(""))?; metrics::histogram!( "api.forced_exit_requests.v01.submit_request", @@ -172,7 +168,7 @@ pub async fn get_request_by_id( let mut storage = data.connection_pool.access_storage().await.map_err(|err| { vlog::warn!("Internal Server Error: '{}';", err); - return ApiError::internal(""); + ApiError::internal("") })?; let mut fe_requests_schema = storage.forced_exit_requests_schema(); diff --git a/core/bin/zksync_api/src/api_server/rest/helpers.rs b/core/bin/zksync_api/src/api_server/rest/helpers.rs index a490aa2852..43d8a0e092 100644 --- a/core/bin/zksync_api/src/api_server/rest/helpers.rs +++ b/core/bin/zksync_api/src/api_server/rest/helpers.rs @@ -2,7 +2,6 @@ use crate::core_api_client::EthBlockId; use actix_web::{HttpResponse, Result as ActixResult}; -use bigdecimal::BigDecimal; use std::collections::HashMap; use zksync_storage::chain::{ block::records::BlockDetails, diff --git a/core/bin/zksync_api/src/api_server/rest/v1/mod.rs b/core/bin/zksync_api/src/api_server/rest/v1/mod.rs index f6eccb4ed1..2787a50927 100644 --- a/core/bin/zksync_api/src/api_server/rest/v1/mod.rs +++ b/core/bin/zksync_api/src/api_server/rest/v1/mod.rs @@ -48,6 +48,6 @@ pub(crate) fn api_scope(tx_sender: TxSender, zk_config: &ZkSyncConfig) -> Scope .service(tokens::api_scope( tx_sender.pool.clone(), tx_sender.tokens, - tx_sender.ticker_requests.clone(), + tx_sender.ticker_requests, )) } diff --git a/core/bin/zksync_forced_exit_requests/src/database.rs b/core/bin/zksync_forced_exit_requests/src/database.rs deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/core/bin/zksync_forced_exit_requests/src/eth_watch.rs b/core/bin/zksync_forced_exit_requests/src/eth_watch.rs index 2cd5031159..b059638219 100644 --- a/core/bin/zksync_forced_exit_requests/src/eth_watch.rs +++ b/core/bin/zksync_forced_exit_requests/src/eth_watch.rs @@ -1,7 +1,5 @@ -use actix_web::client; -use anyhow::format_err; use chrono::{DateTime, Utc}; -use ethabi::{Contract as ContractAbi, Hash}; +use ethabi::Hash; use std::{ convert::TryFrom, time::{Duration, Instant}, @@ -10,16 +8,16 @@ use std::{convert::TryInto, fmt::Debug}; use tokio::task::JoinHandle; use tokio::time; use web3::{ - contract::{Contract, Options}, + contract::Contract, transports::Http, - types::{BlockNumber, FilterBuilder, Log}, + types::{BlockNumber, Log}, Web3, }; use zksync_config::ZkSyncConfig; use zksync_storage::ConnectionPool; use zksync_contracts::forced_exit_contract; -use zksync_types::{block::Block, Address, Nonce, PriorityOp, H160, U256}; +use zksync_types::H160; use zksync_api::core_api_client::CoreApiClient; use zksync_core::eth_watch::{get_contract_events, get_web3_block_number, WatcherMode}; @@ -106,7 +104,6 @@ impl EthClient { } struct ForcedExitContractWatcher { - core_api_client: CoreApiClient, connection_pool: ConnectionPool, config: ZkSyncConfig, eth_client: EthClient, @@ -244,8 +241,7 @@ impl ForcedExitContractWatcher { }; for e in events { - let processing_result = self - .forced_exit_sender + self.forced_exit_sender .process_request(e.amount.clone()) .await; } @@ -322,7 +318,6 @@ pub fn run_forced_exit_contract_watcher( .expect("Unexpected error while trying to wait for unconfirmed transactions"); let contract_watcher = ForcedExitContractWatcher { - core_api_client, connection_pool, config, eth_client, diff --git a/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs b/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs index ca50d4bb04..88c6c8e6ac 100644 --- a/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs +++ b/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs @@ -1,41 +1,24 @@ use std::{ - convert::{TryFrom, TryInto}, + convert::TryInto, ops::{AddAssign, Sub}, - time::Instant, }; -use anyhow::format_err; -use ethabi::{Contract as ContractAbi, Hash}; -use fee_ticker::validator::watcher; use franklin_crypto::bellman::PrimeFieldRepr; -use num::{BigUint, FromPrimitive, Integer, ToPrimitive}; -use std::fmt::Debug; +use num::{BigUint, FromPrimitive}; use tokio::time; -use tokio::{stream::StreamExt, task::JoinHandle}; -use web3::{ - contract::{Contract, Options}, - futures::TryFutureExt, - transports::Http, - types::{BlockNumber, FilterBuilder, Log}, - Web3, -}; use zksync_config::ZkSyncConfig; -use zksync_contracts::zksync_contract; use zksync_storage::{ - chain::{account::AccountSchema, operations_ext::records::TxReceiptResponse}, - ConnectionPool, StorageProcessor, + chain::operations_ext::records::TxReceiptResponse, ConnectionPool, StorageProcessor, }; use zksync_types::{ - forced_exit_requests::{ForcedExitRequest, ForcedExitRequestId, SaveForcedExitRequestQuery}, + forced_exit_requests::{ForcedExitRequest, ForcedExitRequestId}, tx::TimeRange, tx::TxHash, - AccountId, Address, Nonce, PriorityOp, TokenId, ZkSyncTx, H160, U256, + AccountId, Address, Nonce, TokenId, ZkSyncTx, }; use chrono::Utc; -use zksync_api::{api_server::rpc_server, core_api_client::CoreApiClient, fee_ticker}; -use zksync_core::eth_watch::get_contract_events; -use zksync_types::forced_exit_requests::FundsReceivedEvent; +use zksync_api::core_api_client::CoreApiClient; use zksync_types::ForcedExit; use zksync_types::SignedZkSyncTx; @@ -44,8 +27,6 @@ use super::{Engine, Fs, FsRepr}; use zksync_crypto::ff::PrimeField; -use crate::eth_watch; - // We try to process a request 3 times before sending warnings in the console const PROCESSING_ATTEMPTS: u8 = 3; @@ -67,16 +48,7 @@ async fn get_forced_exit_sender_account_id( .account_id_by_address(config.forced_exit_requests.sender_account_address) .await?; - account_id.ok_or(anyhow::Error::msg("1")) -} - -// A dummy tmp function -fn send_to_mempool(account_id: AccountId, token: TokenId) { - let msg = format!( - "The following tx was sent to mempool {} {}", - account_id, token - ); - dbg!(msg); + account_id.ok_or_else(|| anyhow::Error::msg("1")) } fn read_signing_key(private_key: &[u8]) -> anyhow::Result> { @@ -114,8 +86,7 @@ impl ForcedExitSender { } pub fn extract_id_from_amount(&self, amount: BigUint) -> (i64, BigUint) { - let id_space_size: i64 = - (10 as i64).pow(self.config.forced_exit_requests.digits_in_id as u32); + let id_space_size: i64 = 10_i64.pow(self.config.forced_exit_requests.digits_in_id as u32); let id_space_size = BigUint::from_i64(id_space_size).unwrap(); @@ -132,7 +103,7 @@ impl ForcedExitSender { (id.try_into().unwrap(), amount) } - pub fn build_forced_exit<'a>( + pub fn build_forced_exit( &self, nonce: Nonce, target: Address, @@ -155,9 +126,9 @@ impl ForcedExitSender { } } - pub async fn build_transactions<'a>( + pub async fn build_transactions( &self, - storage: &mut StorageProcessor<'a>, + storage: &mut StorageProcessor<'_>, fe_request: ForcedExitRequest, ) -> anyhow::Result> { let mut account_schema = storage.chain().account_schema(); @@ -178,14 +149,13 @@ impl ForcedExitSender { Ok(transactions) } - // TODO: take the block timestamp into account instead of - // the now + // TODO: take the block timestamp into account instead of the now pub fn expired(&self, request: &ForcedExitRequest) -> bool { let now_millis = Utc::now().timestamp_millis(); let created_at_millis = request.created_at.timestamp_millis(); - return now_millis.saturating_sub(created_at_millis) - <= self.config.forced_exit_requests.max_tx_interval; + now_millis.saturating_sub(created_at_millis) + <= self.config.forced_exit_requests.max_tx_interval } // Returns the id the request if it should be fulfilled, @@ -198,13 +168,18 @@ impl ForcedExitSender { } }; + if request.fulfilled_at.is_some() { + // We should not re-process requests that were processed before + return false; + } + !self.expired(&request) && request.price_in_wei == amount } // Awaits until the request is complete - pub async fn await_unconfirmed_request<'a>( + pub async fn await_unconfirmed_request( &self, - storage: &mut StorageProcessor<'a>, + storage: &mut StorageProcessor<'_>, request: &ForcedExitRequest, ) -> anyhow::Result<()> { let hashes = request.fulfilled_by.clone(); @@ -218,17 +193,17 @@ impl ForcedExitSender { Ok(()) } - pub async fn get_unconfirmed_requests<'a>( + pub async fn get_unconfirmed_requests( &self, - storage: &mut StorageProcessor<'a>, + storage: &mut StorageProcessor<'_>, ) -> anyhow::Result> { let mut forced_exit_requests_schema = storage.forced_exit_requests_schema(); forced_exit_requests_schema.get_unconfirmed_requests().await } - pub async fn set_fulfilled_by<'a>( + pub async fn set_fulfilled_by( &self, - storage: &mut StorageProcessor<'a>, + storage: &mut StorageProcessor<'_>, id: ForcedExitRequestId, value: Option>, ) -> anyhow::Result<()> { @@ -245,24 +220,24 @@ impl ForcedExitSender { for request in unfullied_requests.into_iter() { let await_result = self.await_unconfirmed_request(&mut storage, &request).await; - if let Ok(_) = await_result { - continue; + if await_result.is_err() { + // A transaction has failed. That is not intended. + // We can safely cancel such transaction, since we will re-try to + // send it again later + log::error!( + "A previously sent forced exit transaction has failed. Canceling the tx." + ); + self.set_fulfilled_by(&mut storage, request.id, None) + .await?; } - - // A transaction has failed. That is not intended. - // We can safely cancel such transaction, since we will re-try to - // send it again later - log::error!("A previously sent forced exit transaction has failed. Canceling the tx."); - self.set_fulfilled_by(&mut storage, request.id, None) - .await?; } Ok(()) } - pub async fn get_request_by_id<'a>( + pub async fn get_request_by_id( &self, - storage: &mut StorageProcessor<'a>, + storage: &mut StorageProcessor<'_>, id: i64, ) -> anyhow::Result> { let mut fe_schema = storage.forced_exit_requests_schema(); @@ -271,9 +246,9 @@ impl ForcedExitSender { Ok(request) } - pub async fn set_fulfilled_at<'a>( + pub async fn set_fulfilled_at( &self, - storage: &mut StorageProcessor<'a>, + storage: &mut StorageProcessor<'_>, id: i64, ) -> anyhow::Result<()> { let mut fe_schema = storage.forced_exit_requests_schema(); @@ -289,9 +264,9 @@ impl ForcedExitSender { Ok(()) } - pub async fn get_receipt<'a>( + pub async fn get_receipt( &self, - storage: &mut StorageProcessor<'a>, + storage: &mut StorageProcessor<'_>, tx_hash: TxHash, ) -> anyhow::Result> { storage @@ -301,9 +276,9 @@ impl ForcedExitSender { .await } - pub async fn send_transactions<'a>( + pub async fn send_transactions( &self, - storage: &mut StorageProcessor<'a>, + storage: &mut StorageProcessor<'_>, request: &ForcedExitRequest, txs: Vec, ) -> anyhow::Result> { @@ -322,9 +297,9 @@ impl ForcedExitSender { Ok(hashes) } - pub async fn wait_until_comitted<'a>( + pub async fn wait_until_comitted( &self, - storage: &mut StorageProcessor<'a>, + storage: &mut StorageProcessor<'_>, tx_hash: TxHash, ) -> anyhow::Result<()> { let timeout_millis: u64 = 120000; @@ -361,12 +336,10 @@ impl ForcedExitSender { let mut storage = self.connection_pool.access_storage().await?; - let fe_request = self - .get_request_by_id(&mut storage, id) - .await - .expect("Failed to get request by id"); + let fe_request = self.get_request_by_id(&mut storage, id).await?; let fe_request = if self.check_request(amount, fe_request.clone()) { + // The self.check_request already checked that the fe_request is Some(_) fe_request.unwrap() } else { // The request was not valid, that's fine @@ -390,10 +363,13 @@ impl ForcedExitSender { pub async fn process_request(&self, amount: BigUint) { let mut attempts: u8 = 0; + // Typically this should not run any longer than 1 iteration + // In case something bad happens we do not want the server crush because + // of the forced_exit_requests component loop { let processing_attempt = self.try_process_request(amount.clone()).await; - if let Ok(_) = processing_attempt { + if processing_attempt.is_ok() { return; } else { attempts += 1; diff --git a/core/bin/zksync_forced_exit_requests/src/lib.rs b/core/bin/zksync_forced_exit_requests/src/lib.rs index d16951b205..74548f0763 100644 --- a/core/bin/zksync_forced_exit_requests/src/lib.rs +++ b/core/bin/zksync_forced_exit_requests/src/lib.rs @@ -1,19 +1,7 @@ -use std::{convert::TryFrom, str::FromStr, time::Instant}; - -use anyhow::format_err; -use api::Accounts; -use ethabi::{Contract as ContractAbi, Hash}; use num::BigUint; -use std::fmt::Debug; +use std::str::FromStr; use std::time::Duration; use tokio::task::JoinHandle; -use web3::{ - api, - contract::{Contract, Options}, - transports::Http, - types::{BlockNumber, FilterBuilder, Log}, - Web3, -}; use zksync_config::ZkSyncConfig; use zksync_storage::{ chain::operations_ext::records::TxReceiptResponse, ConnectionPool, StorageProcessor, @@ -22,8 +10,8 @@ use zksync_storage::{ use zksync_api::core_api_client::CoreApiClient; use zksync_types::{ - tx::{EthSignData, PackedEthSignature, TimeRange, TxEthSignature, TxHash, TxSignature}, - Account, AccountId, Address, PubKeyHash, ZkSyncTx, H256, + tx::{EthSignData, PackedEthSignature, TimeRange, TxEthSignature, TxHash}, + AccountId, Address, PubKeyHash, ZkSyncTx, H256, }; pub mod eth_watch; @@ -47,31 +35,19 @@ pub type Fs = ::Fs; use zksync_crypto::ff::PrimeField; use tokio::time; -use zksync_eth_signer::{EthereumSigner, PrivateKeySigner}; use zksync_types::Nonce; use zksync_types::TokenId; -#[macro_use] -use vlog; - #[must_use] pub fn run_forced_exit_requests_actors( pool: ConnectionPool, config: ZkSyncConfig, ) -> JoinHandle<()> { let core_api_client = CoreApiClient::new(config.api.private.url.clone()); - - let eth_watch_handle = - eth_watch::run_forced_exit_contract_watcher(core_api_client, pool, config); - - eth_watch_handle + eth_watch::run_forced_exit_contract_watcher(core_api_client, pool, config) } -// pub fn get_sk_from_hex(hex_string: String) -> PrivateKey { - -// } - // This private key is for testing purposes only and shoud not be used in production // The address should be 0xe1faB3eFD74A77C23B426c302D96372140FF7d0C const FORCED_EXIT_SENDER_ETH_PRIVATE_KEY: &str = @@ -85,8 +61,8 @@ fn read_signing_key(private_key: &[u8]) -> anyhow::Result> { )) } -pub async fn check_forced_exit_sender_prepared<'a>( - storage: &mut StorageProcessor<'a>, +pub async fn check_forced_exit_sender_prepared( + storage: &mut StorageProcessor<'_>, sender_sk: &PrivateKey, sender_address: Address, ) -> anyhow::Result { @@ -109,8 +85,8 @@ pub async fn check_forced_exit_sender_prepared<'a>( } } -pub async fn wait_for_account_id<'a>( - storage: &mut StorageProcessor<'a>, +pub async fn wait_for_account_id( + storage: &mut StorageProcessor<'_>, sender_address: Address, ) -> anyhow::Result { vlog::info!("Forced exit sender account is not yet prepared. Waiting for account id..."); @@ -133,8 +109,8 @@ pub async fn wait_for_account_id<'a>( } } -async fn get_receipt<'a>( - storage: &mut StorageProcessor<'a>, +async fn get_receipt( + storage: &mut StorageProcessor<'_>, tx_hash: TxHash, ) -> anyhow::Result> { storage @@ -144,8 +120,8 @@ async fn get_receipt<'a>( .await } -pub async fn wait_for_change_pub_key_tx<'a>( - storage: &mut StorageProcessor<'a>, +pub async fn wait_for_change_pub_key_tx( + storage: &mut StorageProcessor<'_>, tx_hash: TxHash, ) -> anyhow::Result<()> { vlog::info!( @@ -165,7 +141,9 @@ pub async fn wait_for_change_pub_key_tx<'a>( vlog::info!("Public key of the forced exit sender successfully set"); return Ok(()); } else { - let fail_reason = receipt.fail_reason.unwrap_or(String::from("unknown")); + let fail_reason = receipt + .fail_reason + .unwrap_or_else(|| String::from("unknown")); panic!( "Failed to set public for forced exit sedner. Reason: {}", fail_reason @@ -188,9 +166,6 @@ async fn get_verified_eth_sk(sender_address: Address) -> H256 { let pk_address = PackedEthSignature::address_from_private_key(&private_key).unwrap(); - dbg!(pk_address.clone()); - dbg!(sender_address.clone()); - if pk_address != sender_address { panic!("Private key provided does not correspond to the sender address"); } @@ -198,8 +173,8 @@ async fn get_verified_eth_sk(sender_address: Address) -> H256 { private_key } -pub async fn register_signing_key<'a>( - storage: &mut StorageProcessor<'a>, +pub async fn register_signing_key( + storage: &mut StorageProcessor<'_>, sender_id: AccountId, api_client: CoreApiClient, sender_address: Address, @@ -211,10 +186,10 @@ pub async fn register_signing_key<'a>( // Unfortunately, currently the only way to create a CPK // transaction from eth_private_key is to cre - let mut cpk_tx = ChangePubKey::new_signed( + let cpk_tx = ChangePubKey::new_signed( sender_id, sender_address, - pub_key_hash.clone(), + pub_key_hash, TokenId::from_str("0").unwrap(), BigUint::from(0u8), Nonce::from_str("0").unwrap(), @@ -269,8 +244,6 @@ pub async fn register_signing_key<'a>( Ok(()) } -fn verify_pub_key_hash() {} - pub async fn prepare_forced_exit_sender( connection_pool: ConnectionPool, api_client: CoreApiClient, @@ -307,29 +280,3 @@ pub async fn prepare_forced_exit_sender( Ok(()) } - -// Inserts the forced exit sender account into db -// should be used only for local setup/testing -// pub fn insert_forced_exit_account(config: &ZkSyncConfig) { -// let pool = ConnectionPool::new(Some(1)); - -// vlog::info!("Inserting forced exit sender into db"); - -// let sender_account = Account:: -// } - -/* - -Polling like eth_watch - -If sees a funds_received -> extracts id - -Get_by_id => gets by id - -If sum is enough => set_fullfilled_and_send_tx - - -FE requests consist of 2 (or 3 if needed actors) - - -**/ diff --git a/core/bin/zksync_forced_exit_requests/src/utils.rs b/core/bin/zksync_forced_exit_requests/src/utils.rs deleted file mode 100644 index b28b04f643..0000000000 --- a/core/bin/zksync_forced_exit_requests/src/utils.rs +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/core/lib/api_client/src/rest/forced_exit_requests/mod.rs b/core/lib/api_client/src/rest/forced_exit_requests/mod.rs index 39e791bbb2..db1a8916f8 100644 --- a/core/lib/api_client/src/rest/forced_exit_requests/mod.rs +++ b/core/lib/api_client/src/rest/forced_exit_requests/mod.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; // Workspace uses -use zksync_types::{forced_exit_requests::ForcedExitRequest, tx::TxHash, Address, TokenId}; +use zksync_types::{forced_exit_requests::ForcedExitRequest, Address, TokenId}; use zksync_utils::BigUintSerdeAsRadix10Str; use num::BigUint; diff --git a/core/lib/config/src/configs/forced_exit_requests.rs b/core/lib/config/src/configs/forced_exit_requests.rs index 30765b6d88..ebd618e448 100644 --- a/core/lib/config/src/configs/forced_exit_requests.rs +++ b/core/lib/config/src/configs/forced_exit_requests.rs @@ -1,7 +1,6 @@ use crate::envy_load; /// External uses use serde::Deserialize; -use zksync_types::AccountId; use zksync_types::Address; // There are two types of configs: diff --git a/core/lib/storage/src/forced_exit_requests/records.rs b/core/lib/storage/src/forced_exit_requests/records.rs index de9f9ff0d2..63499ee2af 100644 --- a/core/lib/storage/src/forced_exit_requests/records.rs +++ b/core/lib/storage/src/forced_exit_requests/records.rs @@ -29,7 +29,7 @@ impl From for DbForcedExitRequest { Self { id: request.id, target: address_to_stored_string(&request.target), - tokens: tokens, + tokens, price_in_wei, valid_until: request.valid_until, created_at: request.created_at, diff --git a/core/lib/storage/src/forced_exit_requests/utils.rs b/core/lib/storage/src/forced_exit_requests/utils.rs index b8b9b7b3e7..57bf3f333c 100644 --- a/core/lib/storage/src/forced_exit_requests/utils.rs +++ b/core/lib/storage/src/forced_exit_requests/utils.rs @@ -1,6 +1,5 @@ use std::fmt::Debug; use std::{str::FromStr, string::ToString}; -use zksync_basic_types::TokenId; pub fn vec_to_comma_list(elems: Vec) -> String { let strs: Vec = elems.iter().map(|elem| (*elem).to_string()).collect(); @@ -13,25 +12,7 @@ where ::Err: Debug, { elems - .split(",") + .split(',') .map(|str| T::from_str(str).expect("Failed to deserialize stored item")) .collect() } - -// pub fn tokens_vec_to_str(token_ids: Vec) -> String { -// let token_strings: Vec = token_ids.iter().map(|&t| t.to_string()).collect(); -// token_strings.join(",") -// } - -// pub fn tokens_str_to_vec(tokens: String) -> Vec { - -// } - -// pub fn hashes_vec_to_str(hashes: Vec) -> String { -// let hashes_strings: Vec = hashes.iter().map(|&h| h.toString()).collect(); -// hashes_strings.join(",") -// } - -// pub fn tokens_str_to_vec(tokens: String) -> Vec { - -// } diff --git a/core/lib/storage/src/tests/forced_exit_requests.rs b/core/lib/storage/src/tests/forced_exit_requests.rs index a9d2e72256..9fbe9bafbc 100644 --- a/core/lib/storage/src/tests/forced_exit_requests.rs +++ b/core/lib/storage/src/tests/forced_exit_requests.rs @@ -1,14 +1,11 @@ use std::str::FromStr; +use crate::forced_exit_requests::ForcedExitRequestsSchema; use crate::tests::db_test; use crate::QueryResult; use crate::StorageProcessor; -use crate::{data_restore::DataRestoreSchema, forced_exit_requests::ForcedExitRequestsSchema}; -use chrono::{DateTime, Timelike, Utc}; +use chrono::{Timelike, Utc}; use num::{BigUint, FromPrimitive}; -use std::convert::From; -use std::time::Duration; -use tokio::time; use zksync_basic_types::Address; use zksync_types::forced_exit_requests::{ForcedExitRequest, SaveForcedExitRequestQuery}; @@ -20,27 +17,29 @@ use zksync_types::TokenId; async fn get_oldest_unfulfilled_request(mut storage: StorageProcessor<'_>) -> QueryResult<()> { let mut now = Utc::now().with_nanosecond(0).unwrap(); + // The requests have dummy created_at and valid_until values + // They will reassigned in the future cycle let requests = vec![ SaveForcedExitRequestQuery { target: Address::from_str("c0f97CC918C9d6fA4E9fc6be61a6a06589D199b2").unwrap(), tokens: vec![TokenId(1)], price_in_wei: BigUint::from_i32(212).unwrap(), - created_at: DateTime::from(now.clone()), - valid_until: DateTime::from(now.clone()), + created_at: now, + valid_until: now, }, SaveForcedExitRequestQuery { target: Address::from_str("c0f97CC918C9d6fA4E9fc6be61a6a06589D199b2").unwrap(), tokens: vec![TokenId(1)], price_in_wei: BigUint::from_i32(1).unwrap(), - created_at: DateTime::from(now.clone()), - valid_until: DateTime::from(now.clone()), + created_at: now, + valid_until: now, }, SaveForcedExitRequestQuery { target: Address::from_str("c0f97CC918C9d6fA4E9fc6be61a6a06589D199b2").unwrap(), tokens: vec![TokenId(20)], price_in_wei: BigUint::from_str("1000000000000000").unwrap(), - created_at: DateTime::from(now.clone()), - valid_until: DateTime::from(now.clone()), + created_at: now, + valid_until: now, }, ]; @@ -49,7 +48,7 @@ async fn get_oldest_unfulfilled_request(mut storage: StorageProcessor<'_>) -> Qu for req in requests.into_iter() { now = now.add(interval); - let created_at = now.clone(); + let created_at = now; let valid_until = now.add(chrono::Duration::hours(32)); stored_requests.push( diff --git a/core/lib/types/src/forced_exit_requests.rs b/core/lib/types/src/forced_exit_requests.rs index cf39381596..bec4f813ef 100644 --- a/core/lib/types/src/forced_exit_requests.rs +++ b/core/lib/types/src/forced_exit_requests.rs @@ -10,8 +10,7 @@ pub type ForcedExitRequestId = i64; use anyhow::format_err; use ethabi::{decode, ParamType}; use std::convert::TryFrom; -use std::convert::TryInto; -use zksync_basic_types::{Log, U256}; +use zksync_basic_types::Log; use crate::tx::TxHash; @@ -55,11 +54,10 @@ impl TryFrom for FundsReceivedEvent { ) .map_err(|e| format_err!("Event data decode: {:?}", e))?; - let bytes = dec_ev.remove(0).to_bytes().unwrap(); - let amount = u128::from_be_bytes(bytes.try_into().unwrap()); + let amount = dec_ev.remove(0).to_uint().unwrap(); Ok(FundsReceivedEvent { - amount: BigUint::from(amount), + amount: BigUint::from(amount.as_u128()), }) } } diff --git a/core/tests/ts-tests/tests/forced-exit-requests.ts b/core/tests/ts-tests/tests/forced-exit-requests.ts index ead4ebd74c..6115b3eb04 100644 --- a/core/tests/ts-tests/tests/forced-exit-requests.ts +++ b/core/tests/ts-tests/tests/forced-exit-requests.ts @@ -6,7 +6,6 @@ import { Wallet, types, utils } from 'zksync'; import { BigNumber, ethers } from 'ethers'; import * as path from 'path'; -import { loadTestConfig } from './helpers'; import { Address } from 'zksync/build/types'; import { sleep } from 'zksync/build/utils'; @@ -20,16 +19,21 @@ type TokenLike = types.TokenLike; declare module './tester' { interface Tester { - testForcedExitRequestOneToken(from: Wallet, to: ethers.Signer, token: TokenLike, value: BigNumber): Promise; + testForcedExitRequestOneToken( + from: Wallet, + to: ethers.Signer, + token: TokenLike, + value: BigNumber + ): Promise; } } interface StatusResponse { - status: "enabled" | "disabled", - request_fee: string, - max_tokens_per_request: number, - recomended_tx_interval_millis: number, - forced_exit_contract_address: Address + status: 'enabled' | 'disabled'; + request_fee: string; + max_tokens_per_request: number; + recomended_tx_interval_millis: number; + forced_exit_contract_address: Address; } async function getStatus() { @@ -38,13 +42,9 @@ async function getStatus() { const response = await fetch(endpoint); return (await response.json()) as StatusResponse; -} +} -async function submitRequest( - address: string, - tokens: number[], - price_in_wei: string -) { +async function submitRequest(address: string, tokens: number[], price_in_wei: string) { const endpoint = `${apiUrl}/submit`; const data = { @@ -56,7 +56,7 @@ async function submitRequest( const response = await fetch(endpoint, { method: 'POST', headers: { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json' }, redirect: 'follow', body: JSON.stringify(data) @@ -65,11 +65,7 @@ async function submitRequest( return await response.json(); } -async function getFullOnchainBalance( - tester: Tester, - address: Address, - tokenAddress: Address -) { +async function getFullOnchainBalance(tester: Tester, address: Address, tokenAddress: Address) { const onchainBalance = await utils.getEthereumBalance( tester.ethProvider, tester.syncProvider, @@ -89,17 +85,12 @@ Tester.prototype.testForcedExitRequestOneToken = async function ( ) { const toAddress = await to.getAddress(); const tokenAddress = await this.syncProvider.tokenSet.resolveTokenAddress(token); - let toBalanceBefore = await utils.getEthereumBalance( - this.ethProvider, - this.syncProvider, - toAddress, - token - ); + let toBalanceBefore = await utils.getEthereumBalance(this.ethProvider, this.syncProvider, toAddress, token); const transferHandle = await from.syncTransfer({ to: toAddress, token, - amount, + amount }); await transferHandle.awaitReceipt(); @@ -108,17 +99,13 @@ Tester.prototype.testForcedExitRequestOneToken = async function ( expect(status.status).to.eq('enabled', 'Forced exit requests status is disabled'); const tokenId = await this.syncProvider.tokenSet.resolveTokenId(token); - const request = await submitRequest( - toAddress, - [tokenId], - status.request_fee - ); + const request = await submitRequest(toAddress, [tokenId], status.request_fee); const contractAddress = status.forced_exit_contract_address; const amountToPay = BigNumber.from(request.priceInWei).add(BigNumber.from(request.id)); - - const gasPrice = await to.provider?.getGasPrice() as BigNumber; + + const gasPrice = (await to.provider?.getGasPrice()) as BigNumber; const txHandle = await to.sendTransaction({ value: amountToPay, @@ -128,7 +115,7 @@ Tester.prototype.testForcedExitRequestOneToken = async function ( const receipt = await txHandle.wait(); - // We have to wait for verification and execution of the + // We have to wait for verification and execution of the // block with the forced exit, so waiting for a while is fine let timeout = 45000; let interval = 500; @@ -139,18 +126,18 @@ Tester.prototype.testForcedExitRequestOneToken = async function ( let spentTotal = spentOnGas.add(amountToPay); let expectedToBalance = toBalanceBefore.add(amount).sub(spentTotal); - while(timePassed <= timeout) { + while (timePassed <= timeout) { let balance = await getFullOnchainBalance(this, toAddress, tokenAddress); - if(balance.eq(expectedToBalance)) { + if (balance.eq(expectedToBalance)) { break; } await sleep(interval); timePassed += interval; } - + let balance = await getFullOnchainBalance(this, toAddress, tokenAddress); - expect(balance.eq(expectedToBalance), "The ForcedExit has not completed").to.be.true; + expect(balance.eq(expectedToBalance), 'The ForcedExit has not completed').to.be.true; }; diff --git a/core/tests/ts-tests/tests/misc.ts b/core/tests/ts-tests/tests/misc.ts index 11b0675a3c..5b4f4bc27f 100644 --- a/core/tests/ts-tests/tests/misc.ts +++ b/core/tests/ts-tests/tests/misc.ts @@ -238,8 +238,4 @@ Tester.prototype.testBackwardCompatibleEthMessages = async function ( this.runningFee = this.runningFee.add(totalFee); }; -Tester.prototype.testForcedExitRequests = async function ( - -) { - -} +Tester.prototype.testForcedExitRequests = async function () {}; diff --git a/core/tests/ts-tests/tests/withdrawal-helpers.test.ts b/core/tests/ts-tests/tests/withdrawal-helpers.test.ts index 7ad027de17..808034e856 100644 --- a/core/tests/ts-tests/tests/withdrawal-helpers.test.ts +++ b/core/tests/ts-tests/tests/withdrawal-helpers.test.ts @@ -16,7 +16,7 @@ const TEST_CONFIG = loadTestConfig(); // The token here should have the ERC20 implementation from RevertTransferERC20.sol const erc20Token = 'wBTC'; -describe.only('Withdrawal helpers tests', () => { +describe('Withdrawal helpers tests', () => { let tester: Tester; let alice: Wallet; let bob: Wallet; @@ -65,11 +65,6 @@ describe.only('Withdrawal helpers tests', () => { }); it.only('forced_exit_request should recover single token', async () => { - await tester.testForcedExitRequestOneToken( - alice, - bob.ethSigner, - 'ETH', - utils.parseEther('1.0') - ) - }) + await tester.testForcedExitRequestOneToken(alice, bob.ethSigner, 'ETH', utils.parseEther('1.0')); + }); }); diff --git a/infrastructure/zk/src/server.ts b/infrastructure/zk/src/server.ts index 41130bbc39..f5987b9696 100644 --- a/infrastructure/zk/src/server.ts +++ b/infrastructure/zk/src/server.ts @@ -15,6 +15,10 @@ export async function server() { child.kill('SIGINT'); }); + // By the time this function is run the server is most likely not be running yet + // However, it does not matter, since the only thing the function does is depositing + // to the forced exit sender account, and server should be capable of recognizing + // priority operaitons that happened before it was booted await prepareForcedExitRequestAccount(); } @@ -47,7 +51,7 @@ async function prepareForcedExitRequestAccount() { // This is the private key of the first test account const ethProvider = new ethers.providers.JsonRpcProvider('http://localhost:8545'); - const ethRichWallet = new ethers.Wallet('0x7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110'); + const ethRichWallet = new ethers.Wallet('0x7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110'); const mainZkSyncContract = new ethers.Contract( process.env.CONTRACTS_CONTRACT_ADDR as string, @@ -56,12 +60,12 @@ async function prepareForcedExitRequestAccount() { ); const gasPrice = await ethProvider.getGasPrice(); - const ethTransaction = await mainZkSyncContract.depositETH(forcedExitAccount, { + const ethTransaction = (await mainZkSyncContract.depositETH(forcedExitAccount, { // The amount to deposit does not really matter value: ethers.utils.parseEther('1.0'), gasLimit: ethers.BigNumber.from('200000'), - gasPrice, - }) as ethers.ContractTransaction; + gasPrice + })) as ethers.ContractTransaction; await ethTransaction.wait(); From ef02f70cae4388a131d82a3e05dc42b273652212 Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Wed, 17 Feb 2021 12:13:13 +0200 Subject: [PATCH 32/90] log -> vlog --- core/bin/zksync_forced_exit_requests/src/eth_watch.rs | 8 ++++---- .../zksync_forced_exit_requests/src/forced_exit_sender.rs | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/core/bin/zksync_forced_exit_requests/src/eth_watch.rs b/core/bin/zksync_forced_exit_requests/src/eth_watch.rs index b059638219..0629c07f79 100644 --- a/core/bin/zksync_forced_exit_requests/src/eth_watch.rs +++ b/core/bin/zksync_forced_exit_requests/src/eth_watch.rs @@ -182,7 +182,7 @@ impl ForcedExitContractWatcher { WatcherMode::Working => true, WatcherMode::Backoff(delay_until) => { if Instant::now() >= delay_until { - log::info!("Exiting the backoff mode"); + vlog::info!("Exiting the backoff mode"); self.mode = WatcherMode::Working; true } else { @@ -195,7 +195,7 @@ impl ForcedExitContractWatcher { fn handle_infura_error(&mut self, error: anyhow::Error) { if self.is_backoff_requested(&error) { - log::warn!( + vlog::warn!( "Rate limit was reached, as reported by Ethereum node. \ Entering the backoff mode" ); @@ -203,7 +203,7 @@ impl ForcedExitContractWatcher { } else { // Some unexpected kind of error, we won't shutdown the node because of it, // but rather expect node administrators to handle the situation. - log::error!("Failed to process new blocks {}", error); + vlog::error!("Failed to process new blocks {}", error); } } @@ -261,7 +261,7 @@ impl ForcedExitContractWatcher { break block; } Err(error) => { - log::warn!( + vlog::warn!( "Unable to fetch last block number: '{}'. Retrying again in {} seconds", error, RATE_LIMIT_DELAY.as_secs() diff --git a/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs b/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs index 88c6c8e6ac..c104ce22da 100644 --- a/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs +++ b/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs @@ -224,7 +224,7 @@ impl ForcedExitSender { // A transaction has failed. That is not intended. // We can safely cancel such transaction, since we will re-try to // send it again later - log::error!( + vlog::error!( "A previously sent forced exit transaction has failed. Canceling the tx." ); self.set_fulfilled_by(&mut storage, request.id, None) @@ -259,7 +259,7 @@ impl ForcedExitSender { // TODO: Handle such cases gracefully, and not panic .expect("An error occured, while fu;lfilling the request"); - log::info!("FE request with id {} was fulfilled", id); + vlog::info!("FE request with id {} was fulfilled", id); Ok(()) } @@ -376,7 +376,7 @@ impl ForcedExitSender { } if attempts >= PROCESSING_ATTEMPTS { - log::error!("Failed to process forced exit for the {} time", attempts); + vlog::error!("Failed to process forced exit for the {} time", attempts); } } } From cf5a3118d1a64c17f7ce8540263726e76167c6f8 Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Wed, 17 Feb 2021 13:56:44 +0200 Subject: [PATCH 33/90] Multiple tokens test --- .../ts-tests/tests/forced-exit-requests.ts | 159 ++++++++++-------- .../ts-tests/tests/withdrawal-helpers.test.ts | 21 ++- 2 files changed, 103 insertions(+), 77 deletions(-) diff --git a/core/tests/ts-tests/tests/forced-exit-requests.ts b/core/tests/ts-tests/tests/forced-exit-requests.ts index 6115b3eb04..b59a086934 100644 --- a/core/tests/ts-tests/tests/forced-exit-requests.ts +++ b/core/tests/ts-tests/tests/forced-exit-requests.ts @@ -2,8 +2,8 @@ import { Tester } from './tester'; import { expect } from 'chai'; import fs from 'fs'; import fetch from 'node-fetch'; -import { Wallet, types, utils } from 'zksync'; -import { BigNumber, ethers } from 'ethers'; +import { Wallet, types, utils, wallet } from 'zksync'; +import { BigNumber, BigNumberish, ethers } from 'ethers'; import * as path from 'path'; import { Address } from 'zksync/build/types'; @@ -19,11 +19,12 @@ type TokenLike = types.TokenLike; declare module './tester' { interface Tester { - testForcedExitRequestOneToken( + testForcedExitRequestMultipleTokens( from: Wallet, - to: ethers.Signer, - token: TokenLike, - value: BigNumber + payer: ethers.Signer, + to: Address, + tokens: TokenLike[], + value: BigNumber[] ): Promise; } } @@ -36,6 +37,87 @@ interface StatusResponse { forced_exit_contract_address: Address; } +Tester.prototype.testForcedExitRequestMultipleTokens = async function ( + from: Wallet, + payer: ethers.Signer, + to: Address, + tokens: TokenLike[], + amounts: BigNumber[] +) { + const tokenAddresses = tokens.map((token) => this.syncProvider.tokenSet.resolveTokenAddress(token)); + + const toBalancesBeforePromises = tokens.map((token, i) => { + return getFullOnchainBalance(this, to, tokenAddresses[i]); + }); + + let toBalancesBefore = await Promise.all(toBalancesBeforePromises); + + const batchBuilder = from.batchBuilder(); + tokens.forEach((token, i) => { + batchBuilder.addTransfer({ + to, + token, + amount: amounts[i] + }); + }); + const batch = await batchBuilder.build('ETH'); + const handles = await wallet.submitSignedTransactionsBatch(from.provider, batch.txs, [batch.signature]); + + // Waiting only for the first tx since we send the transactions in batch + await handles[0].awaitReceipt(); + + const status = await getStatus(); + + expect(status.status).to.eq('enabled', 'Forced exit requests status is disabled'); + + const tokenIds = tokens.map((token) => this.syncProvider.tokenSet.resolveTokenId(token)); + + const requestPrice = BigNumber.from(status.request_fee).mul(tokens.length); + const request = await submitRequest(to, tokenIds, requestPrice.toString()); + + const contractAddress = status.forced_exit_contract_address; + + const amountToPay = requestPrice.add(BigNumber.from(request.id)); + + const gasPrice = (await payer.provider?.getGasPrice()) as BigNumberish; + + const txHandle = await payer.sendTransaction({ + value: amountToPay, + gasPrice: gasPrice, + to: contractAddress + }); + + await txHandle.wait(); + + // We have to wait for verification and execution of the + // block with the forced exit, so waiting for a while is fine + let timeout = 120000; + let interval = 500; + + let timePassed = 0; + + let expectedToBalance = toBalancesBefore.map((balance, i) => balance.add(amounts[i])); + while (timePassed <= timeout) { + const balancesPromises = tokenAddresses.map((address) => getFullOnchainBalance(this, to, address)); + const balances = await Promise.all(balancesPromises); + + const allExpected = balances.every((bal, i) => bal.eq(expectedToBalance[i])); + + if (allExpected) { + break; + } + + await sleep(interval); + timePassed += interval; + } + + const balancesPromises = tokenAddresses.map((address) => getFullOnchainBalance(this, to, address)); + const balances = await Promise.all(balancesPromises); + const allExpected = balances.every((bal, i) => bal.eq(expectedToBalance[i])); + + expect(allExpected, 'The ForcedExit has not completed').to.be.true; +}; + async function getStatus() { const endpoint = `${apiUrl}/status`; @@ -76,68 +158,3 @@ async function getFullOnchainBalance(tester: Tester, address: Address, tokenAddr return BigNumber.from(onchainBalance).add(BigNumber.from(pendingToBeOnchain)); } - -Tester.prototype.testForcedExitRequestOneToken = async function ( - from: Wallet, - to: ethers.Signer, - token: TokenLike, - amount: BigNumber -) { - const toAddress = await to.getAddress(); - const tokenAddress = await this.syncProvider.tokenSet.resolveTokenAddress(token); - let toBalanceBefore = await utils.getEthereumBalance(this.ethProvider, this.syncProvider, toAddress, token); - - const transferHandle = await from.syncTransfer({ - to: toAddress, - token, - amount - }); - await transferHandle.awaitReceipt(); - - const status = await getStatus(); - - expect(status.status).to.eq('enabled', 'Forced exit requests status is disabled'); - - const tokenId = await this.syncProvider.tokenSet.resolveTokenId(token); - const request = await submitRequest(toAddress, [tokenId], status.request_fee); - - const contractAddress = status.forced_exit_contract_address; - - const amountToPay = BigNumber.from(request.priceInWei).add(BigNumber.from(request.id)); - - const gasPrice = (await to.provider?.getGasPrice()) as BigNumber; - - const txHandle = await to.sendTransaction({ - value: amountToPay, - gasPrice: gasPrice, - to: contractAddress - }); - - const receipt = await txHandle.wait(); - - // We have to wait for verification and execution of the - // block with the forced exit, so waiting for a while is fine - let timeout = 45000; - let interval = 500; - - let timePassed = 0; - - let spentOnGas = receipt.gasUsed.mul(gasPrice); - let spentTotal = spentOnGas.add(amountToPay); - - let expectedToBalance = toBalanceBefore.add(amount).sub(spentTotal); - while (timePassed <= timeout) { - let balance = await getFullOnchainBalance(this, toAddress, tokenAddress); - - if (balance.eq(expectedToBalance)) { - break; - } - - await sleep(interval); - timePassed += interval; - } - - let balance = await getFullOnchainBalance(this, toAddress, tokenAddress); - - expect(balance.eq(expectedToBalance), 'The ForcedExit has not completed').to.be.true; -}; diff --git a/core/tests/ts-tests/tests/withdrawal-helpers.test.ts b/core/tests/ts-tests/tests/withdrawal-helpers.test.ts index e882bf1a10..77113fc2a5 100644 --- a/core/tests/ts-tests/tests/withdrawal-helpers.test.ts +++ b/core/tests/ts-tests/tests/withdrawal-helpers.test.ts @@ -1,15 +1,16 @@ -import { Wallet } from 'zksync'; +import { wallet, Wallet } from 'zksync'; import { Tester } from './tester'; -import { utils } from 'ethers'; +import { ethers, utils } from 'ethers'; import './priority-ops'; import './change-pub-key'; import './withdrawal-helpers'; import './forced-exit-requests'; import { loadTestConfig } from 'reading-tool'; +import { Address } from 'cluster'; -const TX_AMOUNT = utils.parseEther('1'); -const DEPOSIT_AMOUNT = TX_AMOUNT.mul(200); +const TX_AMOUNT = utils.parseEther('0.1'); +const DEPOSIT_AMOUNT = TX_AMOUNT.mul(2000); const TEST_CONFIG = loadTestConfig(true); @@ -20,11 +21,13 @@ describe('Withdrawal helpers tests', () => { let tester: Tester; let alice: Wallet; let bob: Wallet; + let chuck: Wallet; before('create tester and test wallets', async () => { tester = await Tester.init('localhost', 'HTTP'); alice = await tester.fundedWallet('10.0'); bob = await tester.fundedWallet('10.0'); + chuck = await tester.emptyWallet(); for (const token of ['ETH', erc20Token]) { await tester.testDeposit(alice, token, DEPOSIT_AMOUNT, true); @@ -64,7 +67,13 @@ describe('Withdrawal helpers tests', () => { ); }); - it.only('forced_exit_request should recover single token', async () => { - await tester.testForcedExitRequestOneToken(alice, bob.ethSigner, 'ETH', utils.parseEther('1.0')); + it.only('forced_exit_request should recover mutiple tokens', async () => { + await tester.testForcedExitRequestMultipleTokens( + alice, + bob.ethSigner, + chuck.address(), + ['ETH', erc20Token], + [TX_AMOUNT, TX_AMOUNT.mul(2)] + ); }); }); From 9293557b7070aeaad02aaf0579376836afe872bd Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Wed, 17 Feb 2021 15:25:41 +0200 Subject: [PATCH 34/90] refactoring --- contracts/contracts/ForcedExit.sol | 8 +- contracts/test/unit_tests/forced_exit_test.ts | 10 +- .../rest/forced_exit_requests/mod.rs | 10 +- .../rest/forced_exit_requests/v01.rs | 33 +-- .../bin/zksync_api/src/api_server/rest/mod.rs | 7 +- .../src/api_server/rest/v01/api_decl.rs | 10 +- .../src/api_server/rest/v1/error.rs | 1 + .../src/eth_watch.rs | 52 ++-- .../src/forced_exit_sender.rs | 36 +-- .../zksync_forced_exit_requests/src/lib.rs | 269 +----------------- .../src/prepare_forced_exit_sender.rs | 250 ++++++++++++++++ .../zksync_forced_exit_requests/src/utils.rs | 19 ++ .../storage/src/forced_exit_requests/mod.rs | 3 + .../ts-tests/tests/withdrawal-helpers.test.ts | 5 +- minilog | 4 - 15 files changed, 339 insertions(+), 378 deletions(-) create mode 100644 core/bin/zksync_forced_exit_requests/src/prepare_forced_exit_sender.rs create mode 100644 core/bin/zksync_forced_exit_requests/src/utils.rs delete mode 100644 minilog diff --git a/contracts/contracts/ForcedExit.sol b/contracts/contracts/ForcedExit.sol index 86b09dd5e2..7a62446213 100644 --- a/contracts/contracts/ForcedExit.sol +++ b/contracts/contracts/ForcedExit.sol @@ -43,6 +43,8 @@ contract ForcedExit is Ownable, ReentrancyGuard { } // Withdraw funds that failed to reach zkSync due to out-of-gas + // We don't require the contract to be enabled to call this function since + // only the master can use it. function withdrawPendingFunds(address payable _to, uint128 amount) external nonReentrant { requireMaster(msg.sender); @@ -51,10 +53,10 @@ contract ForcedExit is Ownable, ReentrancyGuard { require(amount <= balance, "The balance is lower than the amount"); (bool success, ) = _to.call{value: amount}(""); - require(success, "d"); // ETH withdraw failed + require(success, "ETH withdraw failed"); } - // We ave to use fallback instead of `receive` since the ethabi + // We have to use fallback instead of `receive` since the ethabi // library can't decode the receive function: // https://github.com/rust-ethereum/ethabi/issues/185 fallback() external payable nonReentrant { @@ -62,7 +64,7 @@ contract ForcedExit is Ownable, ReentrancyGuard { require(receiver != address(0), "Receiver must be non-zero"); (bool success, ) = receiver.call{value: msg.value}(""); - require(success, "d"); // ETH withdraw failed + require(success, "ETH withdraw failed"); emit FundsReceived(msg.value); } diff --git a/contracts/test/unit_tests/forced_exit_test.ts b/contracts/test/unit_tests/forced_exit_test.ts index a9ca12ab76..be611920a7 100644 --- a/contracts/test/unit_tests/forced_exit_test.ts +++ b/contracts/test/unit_tests/forced_exit_test.ts @@ -1,7 +1,6 @@ -const { expect } = require('chai'); -const hardhat = require('hardhat'); - +import { expect } from 'chai'; import { Signer, Contract, ContractTransaction, utils, BigNumber } from 'ethers'; +import * as hardhat from 'hardhat'; const TX_AMOUNT = utils.parseEther('1.0'); @@ -26,7 +25,6 @@ describe('ForcedExit unit tests', function () { await setReceiverHandle.wait(); const receiverBalanceBefore = await wallet3.getBalance(); - const txHandle = await wallet2.sendTransaction({ to: forcedExitContract.address, value: TX_AMOUNT @@ -37,10 +35,8 @@ describe('ForcedExit unit tests', function () { const receivedFundsAmount: BigNumber = forcedExitContract.interface.parseLog(txReceipt.logs[0]).args[0]; expect(receivedFundsAmount.eq(TX_AMOUNT), "Didn't emit the amount of sent data").to.be.true; - const receiverBalanceAfter = await wallet3.getBalance(); const diff = receiverBalanceAfter.sub(receiverBalanceBefore); - expect(diff.eq(TX_AMOUNT), 'Funds were not redirected to the receiver').to.be.true; }); @@ -71,7 +67,7 @@ describe('ForcedExit unit tests', function () { expect(diff.eq(expectedDiff), 'Pending funds have not arrived to the account').to.be.true; }); - it('Check redirection', async () => { + it('Check disabling and enabling', async () => { const disableHandle = await forcedExitContract.disable(); await disableHandle.wait(); diff --git a/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/mod.rs b/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/mod.rs index d03aa63c5b..ef984a56ea 100644 --- a/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/mod.rs +++ b/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/mod.rs @@ -5,18 +5,14 @@ use actix_web::{web, Scope}; // Workspace uses pub use zksync_api_client::rest::v1::{ - Client, ClientError, Pagination, PaginationQuery, MAX_LIMIT, + Client, ClientError, FastProcessingQuery, IncomingTx, IncomingTxBatch, Pagination, + PaginationQuery, Receipt, TxData, MAX_LIMIT, }; use zksync_config::ZkSyncConfig; use zksync_storage::ConnectionPool; -pub use zksync_api_client::rest::v1::{ - FastProcessingQuery, IncomingTx, IncomingTxBatch, Receipt, TxData, -}; - -use crate::api_server::forced_exit_checker::ForcedExitChecker; - // Local uses +use crate::api_server::forced_exit_checker::ForcedExitChecker; mod v01; pub(crate) fn api_scope(connection_pool: ConnectionPool, config: &ZkSyncConfig) -> Scope { diff --git a/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs b/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs index 5ddd450990..542653a6c8 100644 --- a/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs +++ b/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs @@ -14,7 +14,6 @@ use num::{bigint::ToBigInt, BigUint}; use std::ops::Add; use std::str::FromStr; use std::time::Instant; -use zksync_api_client::rest::forced_exit_requests::ConfigInfo; // Workspace uses pub use zksync_api_client::rest::forced_exit_requests::{ @@ -24,6 +23,7 @@ pub use zksync_api_client::rest::v1::{ FastProcessingQuery, IncomingTx, IncomingTxBatch, Receipt, TxData, }; +use zksync_api_client::rest::forced_exit_requests::ConfigInfo; use zksync_config::ZkSyncConfig; use zksync_storage::ConnectionPool; use zksync_types::{ @@ -69,8 +69,6 @@ impl ApiForcedExitRequestsData { } } -// Server implementation - async fn get_status( data: web::Data, ) -> JsonResult { @@ -102,6 +100,12 @@ pub async fn submit_request( ApiError::internal("") })?; + if params.tokens.len() > data.max_tokens_per_request as usize { + return Err(ApiError::bad_request( + "Maximum number of tokens per FE request exceeded", + )); + } + data.forced_exit_checker .check_forced_exit(&mut storage, params.target) .await @@ -112,16 +116,10 @@ pub async fn submit_request( let user_fee = params.price_in_wei.to_bigint().unwrap(); let user_fee = BigDecimal::from(user_fee); - let user_scaling_coefficient = BigDecimal::from_str("1.05").unwrap(); - let user_scaled_fee = user_scaling_coefficient * user_fee; - if user_scaled_fee < price_of_request { - return Err(ApiError::bad_request("Not enough fee")); - } - - if params.tokens.len() > data.max_tokens_per_request as usize { + if user_fee != price_of_request { return Err(ApiError::bad_request( - "Maximum number of tokens per FE request exceeded", + "The amount should be exactly the price of the supplied withdrawals", )); } @@ -232,14 +230,6 @@ mod tests { } impl TestServer { - // It should be used in the test for submitting requests - #[allow(dead_code)] - async fn new() -> anyhow::Result<(Client, Self)> { - let cfg = TestServerConfig::default(); - - Self::from_config(cfg).await - } - async fn from_config(cfg: TestServerConfig) -> anyhow::Result<(Client, Self)> { let pool = cfg.pool.clone(); @@ -388,12 +378,11 @@ mod tests { async fn test_forced_exit_requests_submit() -> anyhow::Result<()> { let price_per_token: i64 = 1000000000000000000; let max_tokens_per_request = 3; - let config = ForcedExitRequestsConfig { + let server_config = get_test_config_from_forced_exit_requests(ForcedExitRequestsConfig { max_tokens_per_request, price_per_token, ..ForcedExitRequestsConfig::from_env() - }; - let server_config = get_test_config_from_forced_exit_requests(config); + }); let (client, server) = TestServer::from_config(server_config).await?; diff --git a/core/bin/zksync_api/src/api_server/rest/mod.rs b/core/bin/zksync_api/src/api_server/rest/mod.rs index e72079fe66..09b7d94f65 100644 --- a/core/bin/zksync_api/src/api_server/rest/mod.rs +++ b/core/bin/zksync_api/src/api_server/rest/mod.rs @@ -77,12 +77,7 @@ pub(super) fn start_server_thread_detached( let _panic_sentinel = ThreadPanicNotify(panic_notify.clone()); actix_rt::System::new("api-server").block_on(async move { - let api_v01 = ApiV01::new( - connection_pool, - contract_address, - config.clone(), - fee_ticker.clone(), - ); + let api_v01 = ApiV01::new(connection_pool, contract_address, config.clone()); api_v01.spawn_network_status_updater(panic_notify); start_server(api_v01, fee_ticker, sign_verifier, listen_addr).await; diff --git a/core/bin/zksync_api/src/api_server/rest/v01/api_decl.rs b/core/bin/zksync_api/src/api_server/rest/v01/api_decl.rs index 87fa33fde2..59cd908eec 100644 --- a/core/bin/zksync_api/src/api_server/rest/v01/api_decl.rs +++ b/core/bin/zksync_api/src/api_server/rest/v01/api_decl.rs @@ -1,13 +1,11 @@ //! Declaration of the API structure. use crate::{ - api_server::forced_exit_checker::ForcedExitChecker, api_server::rest::{ helpers::*, v01::{caches::Caches, network_status::SharedNetworkStatus}, }, core_api_client::{CoreApiClient, EthBlockId}, - fee_ticker::TickerRequest, }; use actix_web::{web, HttpResponse, Result as ActixResult}; use futures::channel::mpsc; @@ -26,7 +24,7 @@ use zksync_types::{block::ExecutedOperations, BlockNumber, PriorityOp, H160, H25 /// /// Once a new API is designed, it will be created as `ApiV1` structure, so that /// each API version is encapsulated inside one type. -#[derive(Clone)] +#[derive(Debug, Clone)] pub struct ApiV01 { pub(crate) caches: Caches, pub(crate) connection_pool: ConnectionPool, @@ -34,8 +32,6 @@ pub struct ApiV01 { pub(crate) network_status: SharedNetworkStatus, pub(crate) contract_address: String, pub(crate) config: ZkSyncConfig, - pub(crate) forced_exit_checker: ForcedExitChecker, - pub(crate) ticker_request_sender: mpsc::Sender, } impl ApiV01 { @@ -43,18 +39,14 @@ impl ApiV01 { connection_pool: ConnectionPool, contract_address: H160, config: ZkSyncConfig, - ticker_request_sender: mpsc::Sender, ) -> Self { let api_client = CoreApiClient::new(config.api.private.url.clone()); - Self { caches: Caches::new(config.api.common.caches_size), connection_pool, api_client, network_status: SharedNetworkStatus::default(), contract_address: format!("{:?}", contract_address), - forced_exit_checker: ForcedExitChecker::new(&config), - ticker_request_sender, config, } } diff --git a/core/bin/zksync_api/src/api_server/rest/v1/error.rs b/core/bin/zksync_api/src/api_server/rest/v1/error.rs index d79529f44b..3379a3da9d 100644 --- a/core/bin/zksync_api/src/api_server/rest/v1/error.rs +++ b/core/bin/zksync_api/src/api_server/rest/v1/error.rs @@ -35,6 +35,7 @@ impl Error { Self::with_code(StatusCode::NOT_IMPLEMENTED, title) } + /// Creates a new Error with the NOT_FOUND (404) status code. pub fn not_found(title: impl Display) -> Self { Self::with_code(StatusCode::NOT_FOUND, title) } diff --git a/core/bin/zksync_forced_exit_requests/src/eth_watch.rs b/core/bin/zksync_forced_exit_requests/src/eth_watch.rs index 0629c07f79..7ea509577c 100644 --- a/core/bin/zksync_forced_exit_requests/src/eth_watch.rs +++ b/core/bin/zksync_forced_exit_requests/src/eth_watch.rs @@ -23,14 +23,14 @@ use zksync_api::core_api_client::CoreApiClient; use zksync_core::eth_watch::{get_contract_events, get_web3_block_number, WatcherMode}; use zksync_types::forced_exit_requests::FundsReceivedEvent; +use super::prepare_forced_exit_sender::prepare_forced_exit_sender_account; + +use super::ForcedExitSender; + /// As `infura` may limit the requests, upon error we need to wait for a while /// before repeating the request. const RATE_LIMIT_DELAY: Duration = Duration::from_secs(30); -use crate::prepare_forced_exit_sender; - -use super::ForcedExitSender; - struct ContractTopics { pub funds_received: Hash, } @@ -45,6 +45,7 @@ impl ContractTopics { } } } + pub struct EthClient { web3: Web3, forced_exit_contract: Contract, @@ -115,31 +116,24 @@ struct ForcedExitContractWatcher { // Usually blocks are created much slower (at rate 1 block per 10-20s), // but the block time falls through time, so just to double-check -const MILLIS_PER_BLOCK: i64 = 7000; +const MILLIS_PER_BLOCK: u64 = 7000; // Returns number of blocks that should have been created during the time fn time_range_to_block_diff(from: DateTime, to: DateTime) -> u64 { - let millis_from = from.timestamp_millis(); - let millis_to = to.timestamp_millis(); - - if millis_to >= millis_from { - ((millis_to - millis_from) / MILLIS_PER_BLOCK) - .try_into() - .unwrap() - } else { - 0u64 - } + // Timestamps should never be negative + let millis_from: u64 = from.timestamp_millis().try_into().unwrap(); + let millis_to: u64 = to.timestamp_millis().try_into().unwrap(); + + // It does not matter whether to ceil or floor the division + millis_to.saturating_sub(millis_from) / MILLIS_PER_BLOCK } impl ForcedExitContractWatcher { async fn restore_state_from_eth(&mut self, block: u64) -> anyhow::Result<()> { - //let last_block = self.eth_client.get_block_number().await.expect("Failed to restore "); - let mut storage = self.connection_pool.access_storage().await?; let mut fe_schema = storage.forced_exit_requests_schema(); let oldest_request = fe_schema.get_oldest_unfulfilled_request().await?; - let wait_confirmations = self.config.forced_exit_requests.wait_confirmations; // No oldest request means that there are no requests that were possibly ignored @@ -161,7 +155,6 @@ impl ForcedExitContractWatcher { Ok(()) } - // TODO try to move it to eth client fn is_backoff_requested(&self, error: &anyhow::Error) -> bool { error.to_string().contains("429 Too Many Requests") } @@ -298,11 +291,15 @@ pub fn run_forced_exit_contract_watcher( tokio::spawn(async move { // It is fine to unwrap here, since without it there is not way we // can be sure that the forced exit sender will work properly - prepare_forced_exit_sender(connection_pool.clone(), core_api_client.clone(), &config) - .await - .unwrap(); + prepare_forced_exit_sender_account( + connection_pool.clone(), + core_api_client.clone(), + &config, + ) + .await + .unwrap(); - // It is ok to unwrap here, since if fe_sender is not created, then + // It is ok to unwrap here, since if forced_exit_sender is not created, then // the watcher is meaningless let mut forced_exit_sender = ForcedExitSender::new( core_api_client.clone(), @@ -312,10 +309,11 @@ pub fn run_forced_exit_contract_watcher( .await .unwrap(); - forced_exit_sender - .await_unconfirmed() - .await - .expect("Unexpected error while trying to wait for unconfirmed transactions"); + // In case there were some transactions which were submitted + // but were not committed we will try to wait until they are committed + forced_exit_sender.await_unconfirmed().await.expect( + "Unexpected error while trying to wait for unconfirmed forced_exit transactions", + ); let contract_watcher = ForcedExitContractWatcher { connection_pool, diff --git a/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs b/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs index c104ce22da..d3ad421a7d 100644 --- a/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs +++ b/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs @@ -3,9 +3,10 @@ use std::{ ops::{AddAssign, Sub}, }; -use franklin_crypto::bellman::PrimeFieldRepr; +use chrono::Utc; use num::{BigUint, FromPrimitive}; use tokio::time; + use zksync_config::ZkSyncConfig; use zksync_storage::{ chain::operations_ext::records::TxReceiptResponse, ConnectionPool, StorageProcessor, @@ -17,15 +18,12 @@ use zksync_types::{ AccountId, Address, Nonce, TokenId, ZkSyncTx, }; -use chrono::Utc; use zksync_api::core_api_client::CoreApiClient; use zksync_types::ForcedExit; use zksync_types::SignedZkSyncTx; -use super::PrivateKey; -use super::{Engine, Fs, FsRepr}; - -use zksync_crypto::ff::PrimeField; +use super::utils::{Engine, PrivateKey}; +use crate::utils::read_signing_key; // We try to process a request 3 times before sending warnings in the console const PROCESSING_ATTEMPTS: u8 = 3; @@ -37,6 +35,7 @@ pub struct ForcedExitSender { forced_exit_sender_account_id: AccountId, sender_private_key: PrivateKey, } + async fn get_forced_exit_sender_account_id( connection_pool: ConnectionPool, config: &ZkSyncConfig, @@ -51,14 +50,6 @@ async fn get_forced_exit_sender_account_id( account_id.ok_or_else(|| anyhow::Error::msg("1")) } -fn read_signing_key(private_key: &[u8]) -> anyhow::Result> { - let mut fs_repr = FsRepr::default(); - fs_repr.read_be(private_key)?; - Ok(PrivateKey::( - Fs::from_repr(fs_repr).expect("couldn't read private key from repr"), - )) -} - impl ForcedExitSender { pub async fn new( core_api_client: CoreApiClient, @@ -90,10 +81,9 @@ impl ForcedExitSender { let id_space_size = BigUint::from_i64(id_space_size).unwrap(); + // Taking to the power of 1 and finding mod + // is the only way to find mod of BigUint let one = BigUint::from_u8(1u8).unwrap(); - - // Taking to the power of 1 and finding mod is the only way to find mod of - // the BigUint let id = amount.modpow(&one, &id_space_size); // After extracting the id we need to delete it @@ -118,7 +108,7 @@ impl ForcedExitSender { TimeRange::default(), &self.sender_private_key, ) - .expect("Failed to create signed transaction from ForcedExit"); + .expect("Failed to create signed ForcedExit transaction"); SignedZkSyncTx { tx: ZkSyncTx::ForcedExit(Box::new(tx)), @@ -149,7 +139,6 @@ impl ForcedExitSender { Ok(transactions) } - // TODO: take the block timestamp into account instead of the now pub fn expired(&self, request: &ForcedExitRequest) -> bool { let now_millis = Utc::now().timestamp_millis(); let created_at_millis = request.created_at.timestamp_millis(); @@ -164,12 +153,13 @@ impl ForcedExitSender { let request = match request { Some(r) => r, None => { + // The request does not exit, we should not process it return false; } }; if request.fulfilled_at.is_some() { - // We should not re-process requests that were processed before + // We should not re-process requests that were fulfilled before return false; } @@ -253,11 +243,7 @@ impl ForcedExitSender { ) -> anyhow::Result<()> { let mut fe_schema = storage.forced_exit_requests_schema(); - fe_schema - .set_fulfilled_at(id, Utc::now()) - .await - // TODO: Handle such cases gracefully, and not panic - .expect("An error occured, while fu;lfilling the request"); + fe_schema.set_fulfilled_at(id, Utc::now()).await?; vlog::info!("FE request with id {} was fulfilled", id); diff --git a/core/bin/zksync_forced_exit_requests/src/lib.rs b/core/bin/zksync_forced_exit_requests/src/lib.rs index 74548f0763..ae1e3d0bca 100644 --- a/core/bin/zksync_forced_exit_requests/src/lib.rs +++ b/core/bin/zksync_forced_exit_requests/src/lib.rs @@ -1,43 +1,15 @@ -use num::BigUint; -use std::str::FromStr; -use std::time::Duration; use tokio::task::JoinHandle; use zksync_config::ZkSyncConfig; -use zksync_storage::{ - chain::operations_ext::records::TxReceiptResponse, ConnectionPool, StorageProcessor, -}; +use zksync_storage::ConnectionPool; use zksync_api::core_api_client::CoreApiClient; -use zksync_types::{ - tx::{EthSignData, PackedEthSignature, TimeRange, TxEthSignature, TxHash}, - AccountId, Address, PubKeyHash, ZkSyncTx, H256, -}; +use forced_exit_sender::ForcedExitSender; pub mod eth_watch; pub mod forced_exit_sender; - -use forced_exit_sender::ForcedExitSender; -use zksync_types::tx::ChangePubKey; -use zksync_types::SignedZkSyncTx; - -use franklin_crypto::{ - alt_babyjubjub::fs::FsRepr, - bellman::{pairing::bn256, PrimeFieldRepr}, -}; - -use zksync_crypto::franklin_crypto::{eddsa::PrivateKey, jubjub::JubjubEngine}; - -pub type Engine = bn256::Bn256; - -pub type Fr = bn256::Fr; -pub type Fs = ::Fs; -use zksync_crypto::ff::PrimeField; - -use tokio::time; - -use zksync_types::Nonce; -use zksync_types::TokenId; +pub mod prepare_forced_exit_sender; +mod utils; #[must_use] pub fn run_forced_exit_requests_actors( @@ -47,236 +19,3 @@ pub fn run_forced_exit_requests_actors( let core_api_client = CoreApiClient::new(config.api.private.url.clone()); eth_watch::run_forced_exit_contract_watcher(core_api_client, pool, config) } - -// This private key is for testing purposes only and shoud not be used in production -// The address should be 0xe1faB3eFD74A77C23B426c302D96372140FF7d0C -const FORCED_EXIT_SENDER_ETH_PRIVATE_KEY: &str = - "0x0559b9f000b4e4bbb7fe02e1374cef9623c2ab7c3791204b490e1f229191d104"; - -fn read_signing_key(private_key: &[u8]) -> anyhow::Result> { - let mut fs_repr = FsRepr::default(); - fs_repr.read_be(private_key)?; - Ok(PrivateKey::( - Fs::from_repr(fs_repr).expect("couldn't read private key from repr"), - )) -} - -pub async fn check_forced_exit_sender_prepared( - storage: &mut StorageProcessor<'_>, - sender_sk: &PrivateKey, - sender_address: Address, -) -> anyhow::Result { - let mut accounts_schema = storage.chain().account_schema(); - - let state = accounts_schema - .account_state_by_address(sender_address) - .await? - .committed; - - match state { - Some(account_state) => { - let pk_hash = account_state.1.pub_key_hash; - - let sk_pub_key_hash = PubKeyHash::from_privkey(sender_sk); - - Ok(pk_hash == sk_pub_key_hash) - } - None => Ok(false), - } -} - -pub async fn wait_for_account_id( - storage: &mut StorageProcessor<'_>, - sender_address: Address, -) -> anyhow::Result { - vlog::info!("Forced exit sender account is not yet prepared. Waiting for account id..."); - - let mut account_schema = storage.chain().account_schema(); - let mut timer = time::interval(Duration::from_secs(1)); - - loop { - let account_id = account_schema.account_id_by_address(sender_address).await?; - - match account_id { - Some(id) => { - vlog::info!("Forced exit sender account has account id = {}", 1); - return Ok(id); - } - None => { - timer.tick().await; - } - } - } -} - -async fn get_receipt( - storage: &mut StorageProcessor<'_>, - tx_hash: TxHash, -) -> anyhow::Result> { - storage - .chain() - .operations_ext_schema() - .tx_receipt(tx_hash.as_ref()) - .await -} - -pub async fn wait_for_change_pub_key_tx( - storage: &mut StorageProcessor<'_>, - tx_hash: TxHash, -) -> anyhow::Result<()> { - vlog::info!( - "Forced exit sender account is not yet prepared. Waiting for public key to be set..." - ); - - let mut timer = time::interval(Duration::from_secs(1)); - - loop { - let tx_receipt = get_receipt(storage, tx_hash) - .await - .expect("Faield t oget the traecipt pf ChangePubKey transaction"); - - match tx_receipt { - Some(receipt) => { - if receipt.success { - vlog::info!("Public key of the forced exit sender successfully set"); - return Ok(()); - } else { - let fail_reason = receipt - .fail_reason - .unwrap_or_else(|| String::from("unknown")); - panic!( - "Failed to set public for forced exit sedner. Reason: {}", - fail_reason - ); - } - } - None => { - timer.tick().await; - } - } - } -} - -// Use PackedEthSignature::address_from_private_key -async fn get_verified_eth_sk(sender_address: Address) -> H256 { - let eth_sk = hex::decode(&FORCED_EXIT_SENDER_ETH_PRIVATE_KEY[2..]) - .expect("Failed to parse eth signing key of the forced exit account"); - - let private_key = H256::from_slice(ð_sk); - - let pk_address = PackedEthSignature::address_from_private_key(&private_key).unwrap(); - - if pk_address != sender_address { - panic!("Private key provided does not correspond to the sender address"); - } - - private_key -} - -pub async fn register_signing_key( - storage: &mut StorageProcessor<'_>, - sender_id: AccountId, - api_client: CoreApiClient, - sender_address: Address, - sender_sk: &PrivateKey, -) -> anyhow::Result<()> { - let eth_sk = get_verified_eth_sk(sender_address).await; - - let pub_key_hash = PubKeyHash::from_privkey(sender_sk); - - // Unfortunately, currently the only way to create a CPK - // transaction from eth_private_key is to cre - let cpk_tx = ChangePubKey::new_signed( - sender_id, - sender_address, - pub_key_hash, - TokenId::from_str("0").unwrap(), - BigUint::from(0u8), - Nonce::from_str("0").unwrap(), - TimeRange::default(), - None, - sender_sk, - ) - .expect("Failed to create unsigned cpk transaction"); - - let eth_sign_bytes = cpk_tx - .get_eth_signed_data() - .expect("Failed to get eth signed data"); - - let eth_signature = - PackedEthSignature::sign(ð_sk, ð_sign_bytes).expect("Failed to sign eth message"); - - let cpk_tx_signed = ChangePubKey::new_signed( - sender_id, - sender_address, - pub_key_hash, - TokenId::from_str("0").unwrap(), - BigUint::from(0u8), - Nonce::from_str("0").unwrap(), - TimeRange::default(), - Some(eth_signature.clone()), - sender_sk, - ) - .expect("Failed to created signed CPK transaction"); - - let tx = ZkSyncTx::ChangePubKey(Box::new(cpk_tx_signed)); - let eth_sign_data = EthSignData { - signature: TxEthSignature::EthereumSignature(eth_signature), - message: eth_sign_bytes, - }; - - let tx_signed = SignedZkSyncTx { - tx, - eth_sign_data: Some(eth_sign_data), - }; - let tx_hash = tx_signed.tx.hash(); - - api_client - .send_tx(tx_signed) - .await - .expect("Failed to send CPK transaction") - .expect("Failed to send"); - - wait_for_change_pub_key_tx(storage, tx_hash) - .await - .expect("Failed to wait for ChangePubKey tx"); - - Ok(()) -} - -pub async fn prepare_forced_exit_sender( - connection_pool: ConnectionPool, - api_client: CoreApiClient, - config: &ZkSyncConfig, -) -> anyhow::Result<()> { - let mut storage = connection_pool - .access_storage() - .await - .expect("forced_exit_requests: Failed to get the connection to storage"); - - let sender_sk = hex::decode(&config.forced_exit_requests.sender_private_key[2..]) - .expect("Failed to decode forced_exit_sender sk"); - let sender_sk = read_signing_key(&sender_sk).expect("Failed to read forced exit sender sk"); - let sender_address = config.forced_exit_requests.sender_account_address; - - let is_sender_prepared = - check_forced_exit_sender_prepared(&mut storage, &sender_sk, sender_address) - .await - .expect("Failed to check if the sender is prepared"); - - if is_sender_prepared { - return Ok(()); - } - - // The sender is not prepared. This should not ever happen in production, but handling - // such step is vital for testing locally. - - // Waiting until the sender has an id (sending funds to the account should be done by an external script) - let id = wait_for_account_id(&mut storage, sender_address) - .await - .expect("Failed to get account id for forced exit sender"); - - register_signing_key(&mut storage, id, api_client, sender_address, &sender_sk).await?; - - Ok(()) -} diff --git a/core/bin/zksync_forced_exit_requests/src/prepare_forced_exit_sender.rs b/core/bin/zksync_forced_exit_requests/src/prepare_forced_exit_sender.rs new file mode 100644 index 0000000000..e27ab3e8a4 --- /dev/null +++ b/core/bin/zksync_forced_exit_requests/src/prepare_forced_exit_sender.rs @@ -0,0 +1,250 @@ +use num::BigUint; +use std::str::FromStr; +use std::time::Duration; +use zksync_config::ZkSyncConfig; +use zksync_storage::{ + chain::operations_ext::records::TxReceiptResponse, ConnectionPool, StorageProcessor, +}; + +use zksync_api::core_api_client::CoreApiClient; +use zksync_types::{ + tx::{EthSignData, PackedEthSignature, TimeRange, TxEthSignature, TxHash}, + AccountId, Address, PubKeyHash, ZkSyncTx, H256, +}; + +use zksync_types::tx::ChangePubKey; +use zksync_types::SignedZkSyncTx; + +use zksync_crypto::franklin_crypto::eddsa::PrivateKey; + +use tokio::time; + +use zksync_types::Nonce; +use zksync_types::TokenId; + +use super::utils::{read_signing_key, Engine}; + +// This private key is for testing purposes only and shoud not be used in production +// The address should be 0xe1faB3eFD74A77C23B426c302D96372140FF7d0C +const FORCED_EXIT_TEST_SENDER_ETH_PRIVATE_KEY: &str = + "0x0559b9f000b4e4bbb7fe02e1374cef9623c2ab7c3791204b490e1f229191d104"; + +pub async fn check_forced_exit_sender_prepared( + storage: &mut StorageProcessor<'_>, + sender_sk: &PrivateKey, + sender_address: Address, +) -> anyhow::Result { + let mut accounts_schema = storage.chain().account_schema(); + + let state = accounts_schema + .account_state_by_address(sender_address) + .await? + .committed; + + match state { + Some(account_state) => { + let pk_hash = account_state.1.pub_key_hash; + + let sk_pub_key_hash = PubKeyHash::from_privkey(sender_sk); + + Ok(pk_hash == sk_pub_key_hash) + } + None => Ok(false), + } +} + +pub async fn wait_for_account_id( + storage: &mut StorageProcessor<'_>, + sender_address: Address, +) -> anyhow::Result { + vlog::info!("Forced exit sender account is not yet prepared. Waiting for account id..."); + + let mut account_schema = storage.chain().account_schema(); + let mut timer = time::interval(Duration::from_secs(1)); + + loop { + let account_id = account_schema.account_id_by_address(sender_address).await?; + + match account_id { + Some(id) => { + vlog::info!("Forced exit sender account has account id = {}", 1); + return Ok(id); + } + None => { + timer.tick().await; + } + } + } +} + +async fn get_receipt( + storage: &mut StorageProcessor<'_>, + tx_hash: TxHash, +) -> anyhow::Result> { + storage + .chain() + .operations_ext_schema() + .tx_receipt(tx_hash.as_ref()) + .await +} + +pub async fn wait_for_change_pub_key_tx( + storage: &mut StorageProcessor<'_>, + tx_hash: TxHash, +) -> anyhow::Result<()> { + vlog::info!( + "Forced exit sender account is not yet prepared. Waiting for public key to be set..." + ); + + let mut timer = time::interval(Duration::from_secs(1)); + + loop { + let tx_receipt = get_receipt(storage, tx_hash) + .await + .expect("Faield t oget the traecipt pf ChangePubKey transaction"); + + match tx_receipt { + Some(receipt) => { + if receipt.success { + vlog::info!("Public key of the forced exit sender successfully set"); + return Ok(()); + } else { + let fail_reason = receipt + .fail_reason + .unwrap_or_else(|| String::from("unknown")); + panic!( + "Failed to set public for forced exit sedner. Reason: {}", + fail_reason + ); + } + } + None => { + timer.tick().await; + } + } + } +} + +// Use PackedEthSignature::address_from_private_key +async fn get_verified_eth_sk(sender_address: Address) -> H256 { + let eth_sk = hex::decode(&FORCED_EXIT_TEST_SENDER_ETH_PRIVATE_KEY[2..]) + .expect("Failed to parse eth signing key of the forced exit account"); + + let private_key = H256::from_slice(ð_sk); + + let pk_address = PackedEthSignature::address_from_private_key(&private_key).unwrap(); + + if pk_address != sender_address { + panic!("Private key provided does not correspond to the sender address"); + } + + private_key +} + +pub async fn register_signing_key( + storage: &mut StorageProcessor<'_>, + sender_id: AccountId, + api_client: CoreApiClient, + sender_address: Address, + sender_sk: &PrivateKey, +) -> anyhow::Result<()> { + let eth_sk = get_verified_eth_sk(sender_address).await; + + let pub_key_hash = PubKeyHash::from_privkey(sender_sk); + + // Unfortunately, currently the only way to create a CPK + // transaction from eth_private_key is to cre + let cpk_tx = ChangePubKey::new_signed( + sender_id, + sender_address, + pub_key_hash, + TokenId::from_str("0").unwrap(), + BigUint::from(0u8), + Nonce::from_str("0").unwrap(), + TimeRange::default(), + None, + sender_sk, + ) + .expect("Failed to create unsigned cpk transaction"); + + let eth_sign_bytes = cpk_tx + .get_eth_signed_data() + .expect("Failed to get eth signed data"); + + let eth_signature = + PackedEthSignature::sign(ð_sk, ð_sign_bytes).expect("Failed to sign eth message"); + + let cpk_tx_signed = ChangePubKey::new_signed( + sender_id, + sender_address, + pub_key_hash, + TokenId::from_str("0").unwrap(), + BigUint::from(0u8), + Nonce::from_str("0").unwrap(), + TimeRange::default(), + Some(eth_signature.clone()), + sender_sk, + ) + .expect("Failed to created signed CPK transaction"); + + let tx = ZkSyncTx::ChangePubKey(Box::new(cpk_tx_signed)); + let eth_sign_data = EthSignData { + signature: TxEthSignature::EthereumSignature(eth_signature), + message: eth_sign_bytes, + }; + + let tx_signed = SignedZkSyncTx { + tx, + eth_sign_data: Some(eth_sign_data), + }; + let tx_hash = tx_signed.tx.hash(); + + api_client + .send_tx(tx_signed) + .await + .expect("Failed to send CPK transaction") + .expect("Failed to send"); + + wait_for_change_pub_key_tx(storage, tx_hash) + .await + .expect("Failed to wait for ChangePubKey tx"); + + Ok(()) +} + +pub async fn prepare_forced_exit_sender_account( + connection_pool: ConnectionPool, + api_client: CoreApiClient, + config: &ZkSyncConfig, +) -> anyhow::Result<()> { + let mut storage = connection_pool + .access_storage() + .await + .expect("forced_exit_requests: Failed to get the connection to storage"); + + let sender_sk = hex::decode(&config.forced_exit_requests.sender_private_key[2..]) + .expect("Failed to decode forced_exit_sender sk"); + let sender_sk = read_signing_key(&sender_sk).expect("Failed to read forced exit sender sk"); + let sender_address = config.forced_exit_requests.sender_account_address; + + let is_sender_prepared = + check_forced_exit_sender_prepared(&mut storage, &sender_sk, sender_address) + .await + .expect("Failed to check if the sender is prepared"); + + if is_sender_prepared { + return Ok(()); + } + + // The sender is not prepared. This should not ever happen in production, but handling + // such step is vital for testing locally. + + // Waiting until the sender has an id (sending funds to the account should be done by an external script) + let id = wait_for_account_id(&mut storage, sender_address) + .await + .expect("Failed to get account id for forced exit sender"); + + register_signing_key(&mut storage, id, api_client, sender_address, &sender_sk).await?; + + Ok(()) +} diff --git a/core/bin/zksync_forced_exit_requests/src/utils.rs b/core/bin/zksync_forced_exit_requests/src/utils.rs new file mode 100644 index 0000000000..f414e0a98e --- /dev/null +++ b/core/bin/zksync_forced_exit_requests/src/utils.rs @@ -0,0 +1,19 @@ +use zksync_crypto::ff::PrimeField; +pub use zksync_crypto::franklin_crypto::{eddsa::PrivateKey, jubjub::JubjubEngine}; + +pub use franklin_crypto::{ + alt_babyjubjub::fs::FsRepr, + bellman::{pairing::bn256, PrimeFieldRepr}, +}; + +pub type Engine = bn256::Bn256; + +pub type Fs = ::Fs; + +pub fn read_signing_key(private_key: &[u8]) -> anyhow::Result> { + let mut fs_repr = FsRepr::default(); + fs_repr.read_be(private_key)?; + Ok(PrivateKey::( + Fs::from_repr(fs_repr).expect("couldn't read private key from repr"), + )) +} diff --git a/core/lib/storage/src/forced_exit_requests/mod.rs b/core/lib/storage/src/forced_exit_requests/mod.rs index 653a3f036a..c59d7fa680 100644 --- a/core/lib/storage/src/forced_exit_requests/mod.rs +++ b/core/lib/storage/src/forced_exit_requests/mod.rs @@ -47,6 +47,9 @@ impl<'a, 'c> ForcedExitRequestsSchema<'a, 'c> { target_str, &tokens, price_in_wei, + // It is possible to generate created_at inside the db + // However, since the valid_until is generated outside the db (using config params) + // it was decided to set both values in the server for consistency request.created_at, request.valid_until ) diff --git a/core/tests/ts-tests/tests/withdrawal-helpers.test.ts b/core/tests/ts-tests/tests/withdrawal-helpers.test.ts index 77113fc2a5..a3c508a0ad 100644 --- a/core/tests/ts-tests/tests/withdrawal-helpers.test.ts +++ b/core/tests/ts-tests/tests/withdrawal-helpers.test.ts @@ -1,13 +1,12 @@ -import { wallet, Wallet } from 'zksync'; +import { Wallet } from 'zksync'; import { Tester } from './tester'; -import { ethers, utils } from 'ethers'; +import { utils } from 'ethers'; import './priority-ops'; import './change-pub-key'; import './withdrawal-helpers'; import './forced-exit-requests'; import { loadTestConfig } from 'reading-tool'; -import { Address } from 'cluster'; const TX_AMOUNT = utils.parseEther('0.1'); const DEPOSIT_AMOUNT = TX_AMOUNT.mul(2000); diff --git a/minilog b/minilog deleted file mode 100644 index 7588922007..0000000000 --- a/minilog +++ /dev/null @@ -1,4 +0,0 @@ -Using localhost database: -DATABASE_URL = postgres://postgres@localhost/plasma -error: `cargo check` failed with status: exit code: 101 -error: `cargo check` failed with status: exit code: 101 From 0f85d88b87609d4b1aecdf934184c13c6ab04aa8 Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Wed, 17 Feb 2021 15:35:53 +0200 Subject: [PATCH 35/90] Move out get_contract_events from core/eth_watch --- core/bin/zksync_core/src/eth_watch/client.rs | 34 ----------------- core/bin/zksync_core/src/eth_watch/mod.rs | 2 +- .../src/eth_watch.rs | 38 +++++++++++++++++-- .../src/forced_exit_sender.rs | 1 + 4 files changed, 37 insertions(+), 38 deletions(-) diff --git a/core/bin/zksync_core/src/eth_watch/client.rs b/core/bin/zksync_core/src/eth_watch/client.rs index 7cda80f633..002302906a 100644 --- a/core/bin/zksync_core/src/eth_watch/client.rs +++ b/core/bin/zksync_core/src/eth_watch/client.rs @@ -144,40 +144,6 @@ impl EthClient for EthHttpClient { } } -pub async fn get_contract_events( - web3: &Web3, - contract_address: Address, - from: BlockNumber, - to: BlockNumber, - topics: Vec, -) -> anyhow::Result> -where - T: TryFrom, - T::Error: Debug, -{ - let filter = FilterBuilder::default() - .address(vec![contract_address]) - .from_block(from) - .to_block(to) - .topics(Some(topics), None, None, None) - .build(); - - web3.eth() - .logs(filter) - .await? - .into_iter() - .filter_map(|event| { - if let Ok(event) = T::try_from(event) { - Some(Ok(event)) - } else { - None - } - // TODO: remove after update - // .map_err(|e| format_err!("Failed to parse event log from ETH: {:?}", e)) - }) - .collect() -} - pub async fn get_web3_block_number(web3: &Web3) -> anyhow::Result { Ok(web3.eth().block_number().await?.as_u64()) } diff --git a/core/bin/zksync_core/src/eth_watch/mod.rs b/core/bin/zksync_core/src/eth_watch/mod.rs index 1996153076..50c1a6bd15 100644 --- a/core/bin/zksync_core/src/eth_watch/mod.rs +++ b/core/bin/zksync_core/src/eth_watch/mod.rs @@ -31,7 +31,7 @@ use self::{ received_ops::{sift_outdated_ops, ReceivedPriorityOp}, }; -pub use client::{get_contract_events, get_web3_block_number, EthHttpClient}; +pub use client::{get_web3_block_number, EthHttpClient}; use zksync_config::ZkSyncConfig; use zksync_eth_client::ethereum_gateway::EthereumGateway; diff --git a/core/bin/zksync_forced_exit_requests/src/eth_watch.rs b/core/bin/zksync_forced_exit_requests/src/eth_watch.rs index 7ea509577c..0d27797d12 100644 --- a/core/bin/zksync_forced_exit_requests/src/eth_watch.rs +++ b/core/bin/zksync_forced_exit_requests/src/eth_watch.rs @@ -1,5 +1,5 @@ use chrono::{DateTime, Utc}; -use ethabi::Hash; +use ethabi::{Address, Hash}; use std::{ convert::TryFrom, time::{Duration, Instant}, @@ -10,7 +10,7 @@ use tokio::time; use web3::{ contract::Contract, transports::Http, - types::{BlockNumber, Log}, + types::{BlockNumber, FilterBuilder, Log}, Web3, }; use zksync_config::ZkSyncConfig; @@ -20,7 +20,7 @@ use zksync_contracts::forced_exit_contract; use zksync_types::H160; use zksync_api::core_api_client::CoreApiClient; -use zksync_core::eth_watch::{get_contract_events, get_web3_block_number, WatcherMode}; +use zksync_core::eth_watch::{get_web3_block_number, WatcherMode}; use zksync_types::forced_exit_requests::FundsReceivedEvent; use super::prepare_forced_exit_sender::prepare_forced_exit_sender_account; @@ -327,3 +327,35 @@ pub fn run_forced_exit_contract_watcher( contract_watcher.run().await; }) } + +pub async fn get_contract_events( + web3: &Web3, + contract_address: Address, + from: BlockNumber, + to: BlockNumber, + topics: Vec, +) -> anyhow::Result> +where + T: TryFrom, + T::Error: Debug, +{ + let filter = FilterBuilder::default() + .address(vec![contract_address]) + .from_block(from) + .to_block(to) + .topics(Some(topics), None, None, None) + .build(); + + web3.eth() + .logs(filter) + .await? + .into_iter() + .filter_map(|event| { + if let Ok(event) = T::try_from(event) { + Some(Ok(event)) + } else { + None + } + }) + .collect() +} diff --git a/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs b/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs index d3ad421a7d..70d876e52f 100644 --- a/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs +++ b/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs @@ -139,6 +139,7 @@ impl ForcedExitSender { Ok(transactions) } + // TODO: take the block timestamp into account instead of the now pub fn expired(&self, request: &ForcedExitRequest) -> bool { let now_millis = Utc::now().timestamp_millis(); let created_at_millis = request.created_at.timestamp_millis(); From 3b900c2ed665de93a76de6e55d705279e53fec8b Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Wed, 17 Feb 2021 16:05:53 +0200 Subject: [PATCH 36/90] read zksync abi directly in zk --- infrastructure/zk/src/server.ts | 3 +-- infrastructure/zk/src/utils.ts | 11 +++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/infrastructure/zk/src/server.ts b/infrastructure/zk/src/server.ts index f5987b9696..9f4586f8be 100644 --- a/infrastructure/zk/src/server.ts +++ b/infrastructure/zk/src/server.ts @@ -5,7 +5,6 @@ import fs from 'fs'; import * as db from './db/db'; import { ethers } from 'ethers'; -import { utils as syncUtils } from 'zksync'; export async function server() { let child = utils.background('cargo run --bin zksync_server --release'); @@ -55,7 +54,7 @@ async function prepareForcedExitRequestAccount() { const mainZkSyncContract = new ethers.Contract( process.env.CONTRACTS_CONTRACT_ADDR as string, - syncUtils.SYNC_MAIN_CONTRACT_INTERFACE, + await utils.readZkSyncAbi(), ethRichWallet.connect(ethProvider) ); const gasPrice = await ethProvider.getGasPrice(); diff --git a/infrastructure/zk/src/utils.ts b/infrastructure/zk/src/utils.ts index ecc4538005..50d60a4f27 100644 --- a/infrastructure/zk/src/utils.ts +++ b/infrastructure/zk/src/utils.ts @@ -132,3 +132,14 @@ export function web3Url() { // @ts-ignore return process.env.ETH_CLIENT_WEB3_URL.split(',')[0] as string; } + +export async function readZkSyncAbi() { + const zksync = process.env.ZKSYNC_HOME; + const path = `${zksync}/contracts/artifacts/cache/solpp-generated-contracts/ZkSync.sol/ZkSync.json` + + const fileContent = (await fs.promises.readFile(path)).toString(); + + const abi = JSON.parse(fileContent).abi; + + return abi; +} From ca36a75473d62acfbf3dd78c0c139aa2c7c75f4c Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Wed, 17 Feb 2021 16:39:02 +0200 Subject: [PATCH 37/90] Minor refactor --- .../zksync_api/src/api_server/rest/forced_exit_requests/v01.rs | 3 +-- core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs | 2 +- infrastructure/zk/src/utils.ts | 2 +- nano.save | 1 + 4 files changed, 4 insertions(+), 4 deletions(-) create mode 100644 nano.save diff --git a/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs b/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs index 542653a6c8..18d6ff0443 100644 --- a/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs +++ b/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs @@ -12,9 +12,7 @@ use bigdecimal::{BigDecimal, FromPrimitive}; use chrono::{Duration, Utc}; use num::{bigint::ToBigInt, BigUint}; use std::ops::Add; -use std::str::FromStr; use std::time::Instant; - // Workspace uses pub use zksync_api_client::rest::forced_exit_requests::{ ForcedExitRegisterRequest, ForcedExitRequestStatus, @@ -211,6 +209,7 @@ pub fn api_scope( #[cfg(test)] mod tests { use std::ops::Mul; + use std::str::FromStr; use num::BigUint; diff --git a/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs b/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs index 70d876e52f..9f03144cf0 100644 --- a/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs +++ b/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs @@ -139,7 +139,7 @@ impl ForcedExitSender { Ok(transactions) } - // TODO: take the block timestamp into account instead of the now + // TODO: take the block timestamp into account instead of the now (ZKS-495) pub fn expired(&self, request: &ForcedExitRequest) -> bool { let now_millis = Utc::now().timestamp_millis(); let created_at_millis = request.created_at.timestamp_millis(); diff --git a/infrastructure/zk/src/utils.ts b/infrastructure/zk/src/utils.ts index 50d60a4f27..4932be508a 100644 --- a/infrastructure/zk/src/utils.ts +++ b/infrastructure/zk/src/utils.ts @@ -135,7 +135,7 @@ export function web3Url() { export async function readZkSyncAbi() { const zksync = process.env.ZKSYNC_HOME; - const path = `${zksync}/contracts/artifacts/cache/solpp-generated-contracts/ZkSync.sol/ZkSync.json` + const path = `${zksync}/contracts/artifacts/cache/solpp-generated-contracts/ZkSync.sol/ZkSync.json`; const fileContent = (await fs.promises.readFile(path)).toString(); diff --git a/nano.save b/nano.save new file mode 100644 index 0000000000..fc5601f8e2 --- /dev/null +++ b/nano.save @@ -0,0 +1 @@ +Minor refactor From 14955b7d504061e84ebcdccf2bc0f80db539c438 Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Wed, 17 Feb 2021 17:15:20 +0200 Subject: [PATCH 38/90] Fix lint --- core/lib/api_client/src/rest/forced_exit_requests/mod.rs | 2 +- core/lib/api_client/src/rest/v1/client.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/lib/api_client/src/rest/forced_exit_requests/mod.rs b/core/lib/api_client/src/rest/forced_exit_requests/mod.rs index db1a8916f8..94a6553278 100644 --- a/core/lib/api_client/src/rest/forced_exit_requests/mod.rs +++ b/core/lib/api_client/src/rest/forced_exit_requests/mod.rs @@ -42,7 +42,7 @@ pub struct ForcedExitRegisterRequest { pub price_in_wei: BigUint, } -const FORCED_EXIT_REQUESTS_SCOPE: &'static str = "/api/forced_exit_requests/v0.1/"; +const FORCED_EXIT_REQUESTS_SCOPE: &str = "/api/forced_exit_requests/v0.1/"; impl Client { pub async fn get_forced_exit_requests_status(&self) -> ClientResult { diff --git a/core/lib/api_client/src/rest/v1/client.rs b/core/lib/api_client/src/rest/v1/client.rs index 1d0e8f7728..733934ef81 100644 --- a/core/lib/api_client/src/rest/v1/client.rs +++ b/core/lib/api_client/src/rest/v1/client.rs @@ -52,7 +52,7 @@ pub struct Client { url: String, } -const API_V1_SCOPE: &'static str = "/api/v1/"; +const API_V1_SCOPE: &str = "/api/v1/"; impl Client { /// Creates a new REST API client with the specified Url. From 6668e6bbc5a82686ca96086980abed7a674be8bd Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Wed, 17 Feb 2021 17:17:22 +0200 Subject: [PATCH 39/90] Fix clippy and allow withdrawal-helpers tests --- .github/workflows/ci.yml | 3 +++ core/tests/ts-tests/tests/withdrawal-helpers.test.ts | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f2fe94240a..ae15d52589 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -126,6 +126,9 @@ jobs: - name: integration-rust-sdk run: ci_run zk test i rust-sdk + + - name: integration-withdrawal-helpers + run: ci_run zk test i withdrawal-helpers testkit: diff --git a/core/tests/ts-tests/tests/withdrawal-helpers.test.ts b/core/tests/ts-tests/tests/withdrawal-helpers.test.ts index a3c508a0ad..d54a6c62c6 100644 --- a/core/tests/ts-tests/tests/withdrawal-helpers.test.ts +++ b/core/tests/ts-tests/tests/withdrawal-helpers.test.ts @@ -66,7 +66,7 @@ describe('Withdrawal helpers tests', () => { ); }); - it.only('forced_exit_request should recover mutiple tokens', async () => { + it('forced_exit_request should recover mutiple tokens', async () => { await tester.testForcedExitRequestMultipleTokens( alice, bob.ethSigner, From 51f24dac0fd08e5fa914340f03f825f3c3bc2461 Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Thu, 18 Feb 2021 21:59:22 +0200 Subject: [PATCH 40/90] Use web3 url instead of hard-coded localhost --- infrastructure/zk/src/server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infrastructure/zk/src/server.ts b/infrastructure/zk/src/server.ts index 9f4586f8be..f4c53b1b5f 100644 --- a/infrastructure/zk/src/server.ts +++ b/infrastructure/zk/src/server.ts @@ -49,7 +49,7 @@ async function prepareForcedExitRequestAccount() { const forcedExitAccount = process.env.FORCED_EXIT_REQUESTS_SENDER_ACCOUNT_ADDRESS as string; // This is the private key of the first test account - const ethProvider = new ethers.providers.JsonRpcProvider('http://localhost:8545'); + const ethProvider = new ethers.providers.JsonRpcProvider(process.env.ETH_CLIENT_WEB3_URL); const ethRichWallet = new ethers.Wallet('0x7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110'); const mainZkSyncContract = new ethers.Contract( From b8faeb3a42738eaf13ba01b12892a9389f9321ef Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Fri, 19 Feb 2021 11:28:08 +0200 Subject: [PATCH 41/90] Minor refactor & new migrations --- .../2021-01-31-160151_forced_exit_requests/down.sql | 1 - .../2021-02-19-091010_forced_exit_requests/down.sql | 1 + .../up.sql | 2 +- core/tests/ts-tests/tests/forced-exit-requests.ts | 9 ++++----- nano.save | 1 - 5 files changed, 6 insertions(+), 8 deletions(-) delete mode 100644 core/lib/storage/migrations/2021-01-31-160151_forced_exit_requests/down.sql create mode 100644 core/lib/storage/migrations/2021-02-19-091010_forced_exit_requests/down.sql rename core/lib/storage/migrations/{2021-01-31-160151_forced_exit_requests => 2021-02-19-091010_forced_exit_requests}/up.sql (99%) delete mode 100644 nano.save diff --git a/core/lib/storage/migrations/2021-01-31-160151_forced_exit_requests/down.sql b/core/lib/storage/migrations/2021-01-31-160151_forced_exit_requests/down.sql deleted file mode 100644 index d033a6f858..0000000000 --- a/core/lib/storage/migrations/2021-01-31-160151_forced_exit_requests/down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS forced_exit_requests; diff --git a/core/lib/storage/migrations/2021-02-19-091010_forced_exit_requests/down.sql b/core/lib/storage/migrations/2021-02-19-091010_forced_exit_requests/down.sql new file mode 100644 index 0000000000..02b2810c8e --- /dev/null +++ b/core/lib/storage/migrations/2021-02-19-091010_forced_exit_requests/down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS forced_exit_requests \ No newline at end of file diff --git a/core/lib/storage/migrations/2021-01-31-160151_forced_exit_requests/up.sql b/core/lib/storage/migrations/2021-02-19-091010_forced_exit_requests/up.sql similarity index 99% rename from core/lib/storage/migrations/2021-01-31-160151_forced_exit_requests/up.sql rename to core/lib/storage/migrations/2021-02-19-091010_forced_exit_requests/up.sql index 7bcd1082a0..ab26a5296e 100644 --- a/core/lib/storage/migrations/2021-01-31-160151_forced_exit_requests/up.sql +++ b/core/lib/storage/migrations/2021-02-19-091010_forced_exit_requests/up.sql @@ -7,4 +7,4 @@ CREATE TABLE forced_exit_requests ( created_at TIMESTAMP with time zone NOT NULL, fulfilled_by TEXT, -- comma-separated list of the hashes of ForcedExit transactions fulfilled_at TIMESTAMP with time zone -) +); diff --git a/core/tests/ts-tests/tests/forced-exit-requests.ts b/core/tests/ts-tests/tests/forced-exit-requests.ts index b59a086934..ff48878a91 100644 --- a/core/tests/ts-tests/tests/forced-exit-requests.ts +++ b/core/tests/ts-tests/tests/forced-exit-requests.ts @@ -91,13 +91,13 @@ Tester.prototype.testForcedExitRequestMultipleTokens = async function ( // We have to wait for verification and execution of the // block with the forced exit, so waiting for a while is fine - let timeout = 120000; - let interval = 500; + const timeout = 60000; + const interval = 500; - let timePassed = 0; + const iterations = timeout / interval; let expectedToBalance = toBalancesBefore.map((balance, i) => balance.add(amounts[i])); - while (timePassed <= timeout) { + for (let i = 0; i < iterations; i++) { const balancesPromises = tokenAddresses.map((address) => getFullOnchainBalance(this, to, address)); const balances = await Promise.all(balancesPromises); @@ -108,7 +108,6 @@ Tester.prototype.testForcedExitRequestMultipleTokens = async function ( } await sleep(interval); - timePassed += interval; } const balancesPromises = tokenAddresses.map((address) => getFullOnchainBalance(this, to, address)); diff --git a/nano.save b/nano.save deleted file mode 100644 index fc5601f8e2..0000000000 --- a/nano.save +++ /dev/null @@ -1 +0,0 @@ -Minor refactor From 0ce18c4f327ed210bf047ee6f7aaca976ad1b168 Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Fri, 19 Feb 2021 11:45:47 +0200 Subject: [PATCH 42/90] Remove unused dependencies --- Cargo.lock | 32 ------------------- .../zksync_forced_exit_requests/Cargo.toml | 1 - 2 files changed, 33 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index abda5f9b88..f22e680772 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1532,19 +1532,6 @@ dependencies = [ "syn", ] -[[package]] -name = "env_logger" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aafcde04e90a5226a6443b7aabdb016ba2f8307c847d524724bd9b346dd1a2d3" -dependencies = [ - "atty", - "humantime", - "log 0.4.11", - "regex", - "termcolor", -] - [[package]] name = "envy" version = "0.4.2" @@ -2194,15 +2181,6 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "494b4d60369511e7dea41cf646832512a94e542f68bb9c49e54518e0f468eb47" -[[package]] -name = "humantime" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" -dependencies = [ - "quick-error 1.2.3", -] - [[package]] name = "hyper" version = "0.10.16" @@ -4651,15 +4629,6 @@ dependencies = [ "winapi 0.3.9", ] -[[package]] -name = "termcolor" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" -dependencies = [ - "winapi-util", -] - [[package]] name = "textwrap" version = "0.11.0" @@ -6014,7 +5983,6 @@ dependencies = [ "anyhow", "async-trait", "chrono", - "env_logger", "ethabi", "franklin-crypto", "hex", diff --git a/core/bin/zksync_forced_exit_requests/Cargo.toml b/core/bin/zksync_forced_exit_requests/Cargo.toml index 7124a7ac61..9be55485f5 100644 --- a/core/bin/zksync_forced_exit_requests/Cargo.toml +++ b/core/bin/zksync_forced_exit_requests/Cargo.toml @@ -31,7 +31,6 @@ ethabi = "12.0.0" web3 = "0.13.0" log = "0.4" hex = "0.4" -env_logger = "0.6" metrics = "0.13.0-alpha.8" chrono = { version = "0.4", features = ["serde", "rustc-serialize"] } From e04dcbbbf906e77f4843a27d5c63d23ecc27f49e Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Fri, 19 Feb 2021 12:24:45 +0200 Subject: [PATCH 43/90] Refactoring during account preparation --- Cargo.lock | 1 + .../zksync_forced_exit_requests/Cargo.toml | 1 + .../src/prepare_forced_exit_sender.rs | 148 +++++++----------- 3 files changed, 62 insertions(+), 88 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f22e680772..06657712a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5999,6 +5999,7 @@ dependencies = [ "zksync_crypto", "zksync_eth_signer", "zksync_storage", + "zksync_test_account", "zksync_types", ] diff --git a/core/bin/zksync_forced_exit_requests/Cargo.toml b/core/bin/zksync_forced_exit_requests/Cargo.toml index 9be55485f5..4a6387247e 100644 --- a/core/bin/zksync_forced_exit_requests/Cargo.toml +++ b/core/bin/zksync_forced_exit_requests/Cargo.toml @@ -19,6 +19,7 @@ zksync_contracts = { path = "../../lib/contracts", version = "1.0" } zksync_crypto = { path = "../../lib/crypto", version = "1.0" } zksync_eth_signer = { path = "../../lib/eth_signer", version = "1.0" } +zksync_test_account = { path = "../../tests/test_account", version = "1.0" } vlog = { path = "../../lib/vlog", version = "1.0" } diff --git a/core/bin/zksync_forced_exit_requests/src/prepare_forced_exit_sender.rs b/core/bin/zksync_forced_exit_requests/src/prepare_forced_exit_sender.rs index e27ab3e8a4..4ddd5298a4 100644 --- a/core/bin/zksync_forced_exit_requests/src/prepare_forced_exit_sender.rs +++ b/core/bin/zksync_forced_exit_requests/src/prepare_forced_exit_sender.rs @@ -1,5 +1,4 @@ use num::BigUint; -use std::str::FromStr; use std::time::Duration; use zksync_config::ZkSyncConfig; use zksync_storage::{ @@ -8,19 +7,17 @@ use zksync_storage::{ use zksync_api::core_api_client::CoreApiClient; use zksync_types::{ - tx::{EthSignData, PackedEthSignature, TimeRange, TxEthSignature, TxHash}, + tx::{PackedEthSignature, TimeRange, TxHash}, AccountId, Address, PubKeyHash, ZkSyncTx, H256, }; -use zksync_types::tx::ChangePubKey; -use zksync_types::SignedZkSyncTx; +use zksync_types::{Nonce, SignedZkSyncTx, TokenId}; use zksync_crypto::franklin_crypto::eddsa::PrivateKey; use tokio::time; -use zksync_types::Nonce; -use zksync_types::TokenId; +use zksync_test_account::ZkSyncAccount; use super::utils::{read_signing_key, Engine}; @@ -29,6 +26,43 @@ use super::utils::{read_signing_key, Engine}; const FORCED_EXIT_TEST_SENDER_ETH_PRIVATE_KEY: &str = "0x0559b9f000b4e4bbb7fe02e1374cef9623c2ab7c3791204b490e1f229191d104"; +pub async fn prepare_forced_exit_sender_account( + connection_pool: ConnectionPool, + api_client: CoreApiClient, + config: &ZkSyncConfig, +) -> anyhow::Result<()> { + let mut storage = connection_pool + .access_storage() + .await + .expect("forced_exit_requests: Failed to get the connection to storage"); + + let sender_sk = hex::decode(&config.forced_exit_requests.sender_private_key[2..]) + .expect("Failed to decode forced_exit_sender sk"); + let sender_sk = read_signing_key(&sender_sk).expect("Failed to read forced exit sender sk"); + let sender_address = config.forced_exit_requests.sender_account_address; + + let is_sender_prepared = + check_forced_exit_sender_prepared(&mut storage, &sender_sk, sender_address) + .await + .expect("Failed to check if the sender is prepared"); + + if is_sender_prepared { + return Ok(()); + } + + // The sender is not prepared. This should not ever happen in production, but handling + // such step is vital for testing locally. + + // Waiting until the sender has an id (sending funds to the account should be done by an external script) + let id = wait_for_account_id(&mut storage, sender_address) + .await + .expect("Failed to get account id for forced exit sender"); + + register_signing_key(&mut storage, id, api_client, sender_address, sender_sk).await?; + + Ok(()) +} + pub async fn check_forced_exit_sender_prepared( storage: &mut StorageProcessor<'_>, sender_sk: &PrivateKey, @@ -146,61 +180,36 @@ pub async fn register_signing_key( sender_id: AccountId, api_client: CoreApiClient, sender_address: Address, - sender_sk: &PrivateKey, + sender_sk: PrivateKey, ) -> anyhow::Result<()> { let eth_sk = get_verified_eth_sk(sender_address).await; - let pub_key_hash = PubKeyHash::from_privkey(sender_sk); - - // Unfortunately, currently the only way to create a CPK - // transaction from eth_private_key is to cre - let cpk_tx = ChangePubKey::new_signed( - sender_id, - sender_address, - pub_key_hash, - TokenId::from_str("0").unwrap(), - BigUint::from(0u8), - Nonce::from_str("0").unwrap(), - TimeRange::default(), - None, + let sender_account = ZkSyncAccount::new( sender_sk, - ) - .expect("Failed to create unsigned cpk transaction"); - - let eth_sign_bytes = cpk_tx - .get_eth_signed_data() - .expect("Failed to get eth signed data"); - - let eth_signature = - PackedEthSignature::sign(ð_sk, ð_sign_bytes).expect("Failed to sign eth message"); - - let cpk_tx_signed = ChangePubKey::new_signed( - sender_id, + // The accout is changing public key for hte first time, so nonce is 0 + Nonce(0), sender_address, - pub_key_hash, - TokenId::from_str("0").unwrap(), + eth_sk, + ); + sender_account.set_account_id(Some(sender_id)); + + let cpk = sender_account.sign_change_pubkey_tx( + Some(Nonce(0)), + true, + TokenId(0), BigUint::from(0u8), - Nonce::from_str("0").unwrap(), + false, TimeRange::default(), - Some(eth_signature.clone()), - sender_sk, - ) - .expect("Failed to created signed CPK transaction"); - - let tx = ZkSyncTx::ChangePubKey(Box::new(cpk_tx_signed)); - let eth_sign_data = EthSignData { - signature: TxEthSignature::EthereumSignature(eth_signature), - message: eth_sign_bytes, - }; + ); - let tx_signed = SignedZkSyncTx { - tx, - eth_sign_data: Some(eth_sign_data), - }; - let tx_hash = tx_signed.tx.hash(); + let tx = ZkSyncTx::ChangePubKey(Box::new(cpk)); + let tx_hash = tx.hash(); api_client - .send_tx(tx_signed) + .send_tx(SignedZkSyncTx { + tx, + eth_sign_data: None, + }) .await .expect("Failed to send CPK transaction") .expect("Failed to send"); @@ -211,40 +220,3 @@ pub async fn register_signing_key( Ok(()) } - -pub async fn prepare_forced_exit_sender_account( - connection_pool: ConnectionPool, - api_client: CoreApiClient, - config: &ZkSyncConfig, -) -> anyhow::Result<()> { - let mut storage = connection_pool - .access_storage() - .await - .expect("forced_exit_requests: Failed to get the connection to storage"); - - let sender_sk = hex::decode(&config.forced_exit_requests.sender_private_key[2..]) - .expect("Failed to decode forced_exit_sender sk"); - let sender_sk = read_signing_key(&sender_sk).expect("Failed to read forced exit sender sk"); - let sender_address = config.forced_exit_requests.sender_account_address; - - let is_sender_prepared = - check_forced_exit_sender_prepared(&mut storage, &sender_sk, sender_address) - .await - .expect("Failed to check if the sender is prepared"); - - if is_sender_prepared { - return Ok(()); - } - - // The sender is not prepared. This should not ever happen in production, but handling - // such step is vital for testing locally. - - // Waiting until the sender has an id (sending funds to the account should be done by an external script) - let id = wait_for_account_id(&mut storage, sender_address) - .await - .expect("Failed to get account id for forced exit sender"); - - register_signing_key(&mut storage, id, api_client, sender_address, &sender_sk).await?; - - Ok(()) -} From 6cae6aa7a64b2aeab8fd43663b0e69ef44861c39 Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Mon, 22 Feb 2021 12:04:43 +0200 Subject: [PATCH 44/90] If the forced_exit_requests are disabled, also disable the actor as well --- .../zksync_forced_exit_requests/src/eth_watch.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/core/bin/zksync_forced_exit_requests/src/eth_watch.rs b/core/bin/zksync_forced_exit_requests/src/eth_watch.rs index 0d27797d12..f2f61101c8 100644 --- a/core/bin/zksync_forced_exit_requests/src/eth_watch.rs +++ b/core/bin/zksync_forced_exit_requests/src/eth_watch.rs @@ -289,6 +289,11 @@ pub fn run_forced_exit_contract_watcher( let eth_client = EthClient::new(web3, config.contracts.forced_exit_addr); tokio::spawn(async move { + // We should not proceed if the feature is disabled + if !config.forced_exit_requests.enabled { + infinite_async_loop().await + } + // It is fine to unwrap here, since without it there is not way we // can be sure that the forced exit sender will work properly prepare_forced_exit_sender_account( @@ -359,3 +364,11 @@ where }) .collect() } + +pub async fn infinite_async_loop() { + // We use a 1 day interval instead of a simple loop to free the execution thread + let mut timer = time::interval(Duration::from_secs(60 * 60 * 24)); + loop { + timer.tick().await; + } +} From 0e12af8ce3dda7a41854d14258559ba16785b0e0 Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Mon, 22 Feb 2021 17:04:05 +0200 Subject: [PATCH 45/90] More clear tests for smart contracts --- contracts/test/unit_tests/forced_exit_test.ts | 120 ++++++++++-------- 1 file changed, 64 insertions(+), 56 deletions(-) diff --git a/contracts/test/unit_tests/forced_exit_test.ts b/contracts/test/unit_tests/forced_exit_test.ts index be611920a7..4cfea0256f 100644 --- a/contracts/test/unit_tests/forced_exit_test.ts +++ b/contracts/test/unit_tests/forced_exit_test.ts @@ -1,13 +1,20 @@ -import { expect } from 'chai'; -import { Signer, Contract, ContractTransaction, utils, BigNumber } from 'ethers'; +import { expect, use } from 'chai'; +import { solidity } from 'ethereum-waffle'; +import { Signer, utils } from 'ethers'; +import { ForcedExit } from '../../typechain/ForcedExit'; +import { ForcedExitFactory } from '../../typechain/ForcedExitFactory'; +import { SelfDestructFactory } from '../../typechain/SelfDestructFactory'; + import * as hardhat from 'hardhat'; const TX_AMOUNT = utils.parseEther('1.0'); +use(solidity); + describe('ForcedExit unit tests', function () { this.timeout(50000); - let forcedExitContract: Contract; + let forcedExitContract: ForcedExit; let wallet1: Signer; let wallet2: Signer; let wallet3: Signer; @@ -16,83 +23,84 @@ describe('ForcedExit unit tests', function () { [wallet1, wallet2, wallet3] = await hardhat.ethers.getSigners(); const forcedExitContractFactory = await hardhat.ethers.getContractFactory('ForcedExit'); - forcedExitContract = await forcedExitContractFactory.deploy(wallet1.getAddress()); - forcedExitContract.connect(wallet1); + const contract = await forcedExitContractFactory.deploy(wallet1.getAddress()); + forcedExitContract = ForcedExitFactory.connect(contract.address, wallet1); }); it('Check redirecting funds to receiver', async () => { - const setReceiverHandle = await forcedExitContract.setReceiver(wallet3.getAddress()); - await setReceiverHandle.wait(); + // The test checks that when users send funds to the contract + // the funds will be redirected to the receiver address that is set + // by the master of the ForcedExit contract + + // Setting receiver who will should get all the funds sent + // to the contract + await forcedExitContract.setReceiver(await wallet3.getAddress()); - const receiverBalanceBefore = await wallet3.getBalance(); + // Could not use nested expects because + // changeEtherBalance does not allow it + + // User sends tranasctions const txHandle = await wallet2.sendTransaction({ to: forcedExitContract.address, value: TX_AMOUNT }); - const txReceipt = await txHandle.wait(); - - expect(txReceipt.logs.length == 1, 'No events were emitted').to.be.true; - const receivedFundsAmount: BigNumber = forcedExitContract.interface.parseLog(txReceipt.logs[0]).args[0]; + // Check that the `FundsReceived` event was emitted + expect(txHandle).to.emit(forcedExitContract, 'FundsReceived').withArgs(TX_AMOUNT); - expect(receivedFundsAmount.eq(TX_AMOUNT), "Didn't emit the amount of sent data").to.be.true; - const receiverBalanceAfter = await wallet3.getBalance(); - const diff = receiverBalanceAfter.sub(receiverBalanceBefore); - expect(diff.eq(TX_AMOUNT), 'Funds were not redirected to the receiver').to.be.true; + // The receiver received the balance + expect(txHandle).to.changeEtherBalance(wallet3, TX_AMOUNT); }); it('Check receiving pending funds', async () => { + // The test checks that it is possible for the master of the contract + // to withdraw funds that got stuck on the contract for some unknown reason. + // One example is when another contract does selfdestruct and submits funds + // to the ForcedExit contract. + + // Create the contract which will self-destruct itself const selfDestructContractFactory = await hardhat.ethers.getContractFactory('SelfDestruct'); - let selfDestructContract: Contract = await selfDestructContractFactory.deploy(); + const contractDeployed = await selfDestructContractFactory.deploy(); + const selfDestructContract = SelfDestructFactory.connect(contractDeployed.address, contractDeployed.signer); - const txHandle = await wallet2.sendTransaction({ + // Supplying funds to the self-desctruct contract + await wallet2.sendTransaction({ to: selfDestructContract.address, value: TX_AMOUNT }); - await txHandle.wait(); - selfDestructContract.connect(wallet2); - - const destructHandle: ContractTransaction = await selfDestructContract.destroy(forcedExitContract.address); - await destructHandle.wait(); - const masterBalanceBefore = await wallet1.getBalance(); - - const withdrawHandle: ContractTransaction = await forcedExitContract.withdrawPendingFunds( - wallet1.getAddress(), - TX_AMOUNT - ); - const withdrawReceipt = await withdrawHandle.wait(); - const masterBalanceAfter = await wallet1.getBalance(); - - const diff = masterBalanceAfter.sub(masterBalanceBefore); - const expectedDiff = TX_AMOUNT.sub(withdrawReceipt.gasUsed.mul(withdrawHandle.gasPrice)); - expect(diff.eq(expectedDiff), 'Pending funds have not arrived to the account').to.be.true; + + // Destroying the self-destruct contract which sends TX_AMOUNT ether to the ForcedExit + // contract which were not redirected to the receiver + await selfDestructContract.connect(wallet2).destroy(forcedExitContract.address); + + // The master withdraws the funds and they should arrive to him + expect( + await forcedExitContract.withdrawPendingFunds(await wallet1.getAddress(), TX_AMOUNT) + ).to.changeEtherBalance(wallet1, TX_AMOUNT); }); it('Check disabling and enabling', async () => { - const disableHandle = await forcedExitContract.disable(); - await disableHandle.wait(); + // The test checks that disabling and enabling of the ForcedExit contract works. - let failed1 = false; - try { - const txHandle = await wallet2.sendTransaction({ + // Disabling transfers to the contract + await forcedExitContract.disable(); + + // The contract is disabled. Thus, transfering to it should fail + expect( + wallet2.sendTransaction({ to: forcedExitContract.address, value: TX_AMOUNT - }); - await txHandle.wait(); - } catch { - failed1 = true; - } - - expect(failed1, 'Transfer to the disabled contract does not fail').to.be.true; + }) + ).to.be.reverted; - const enableHandle = await forcedExitContract.enable(); - await enableHandle.wait(); + // Enabling transfers to the contract + await forcedExitContract.enable(); - const txHandle = await wallet2.sendTransaction({ - to: forcedExitContract.address, - value: TX_AMOUNT - }); - const txReceipt = await txHandle.wait(); - - expect(txReceipt.blockNumber, 'A transfer to the enabled account have failed').to.exist; + // The contract is enabled. Thus, transfering to it should not fail + expect( + wallet2.sendTransaction({ + to: forcedExitContract.address, + value: TX_AMOUNT + }) + ).to.not.be.reverted; }); }); From dd718b6a67f8d0de3ee5ac5108aa3a8ab476b02a Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Wed, 24 Feb 2021 11:45:23 +0200 Subject: [PATCH 46/90] A little pack of style-related improvements --- .../zksync_api/src/api_server/rest/forced_exit_requests/mod.rs | 2 -- .../zksync_api/src/api_server/rest/forced_exit_requests/v01.rs | 2 +- core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs | 2 +- .../migrations/2021-02-19-091010_forced_exit_requests/down.sql | 2 +- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/mod.rs b/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/mod.rs index ef984a56ea..73a3475309 100644 --- a/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/mod.rs +++ b/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/mod.rs @@ -1,5 +1,3 @@ -//! First stable API implementation. - // External uses use actix_web::{web, Scope}; diff --git a/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs b/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs index 18d6ff0443..3bafae1f18 100644 --- a/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs +++ b/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs @@ -34,7 +34,7 @@ use crate::api_server::rest::v1::{Error as ApiError, JsonResult}; use crate::api_server::forced_exit_checker::ForcedExitAccountAgeChecker; -/// Shared data between `api/v1/transactions` endpoints. +/// Shared data between `/api/forced_exit_requests/v0.1/` endpoints. pub struct ApiForcedExitRequestsData { pub(crate) connection_pool: ConnectionPool, pub(crate) forced_exit_checker: Box, diff --git a/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs b/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs index 9f03144cf0..c939f7ad48 100644 --- a/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs +++ b/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs @@ -47,7 +47,7 @@ async fn get_forced_exit_sender_account_id( .account_id_by_address(config.forced_exit_requests.sender_account_address) .await?; - account_id.ok_or_else(|| anyhow::Error::msg("1")) + account_id.ok_or_else(|| anyhow::Error::msg("Failed to get the forced_exit_sender account id")) } impl ForcedExitSender { diff --git a/core/lib/storage/migrations/2021-02-19-091010_forced_exit_requests/down.sql b/core/lib/storage/migrations/2021-02-19-091010_forced_exit_requests/down.sql index 02b2810c8e..d033a6f858 100644 --- a/core/lib/storage/migrations/2021-02-19-091010_forced_exit_requests/down.sql +++ b/core/lib/storage/migrations/2021-02-19-091010_forced_exit_requests/down.sql @@ -1 +1 @@ -DROP TABLE IF EXISTS forced_exit_requests \ No newline at end of file +DROP TABLE IF EXISTS forced_exit_requests; From 7477c49db9bbd529d26434c25eaea3a70840b2cd Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Wed, 24 Feb 2021 12:18:39 +0200 Subject: [PATCH 47/90] warn_err helper and restore release mode for rustApi test --- .../rest/forced_exit_requests/v01.rs | 25 +++++++++++++------ infrastructure/zk/src/test/test.ts | 2 +- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs b/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs index 3bafae1f18..d4769ecd4f 100644 --- a/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs +++ b/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs @@ -93,10 +93,12 @@ pub async fn submit_request( ) -> JsonResult { let start = Instant::now(); - let mut storage = data.connection_pool.access_storage().await.map_err(|err| { - vlog::warn!("Internal Server Error: '{}';", err); - ApiError::internal("") - })?; + let mut storage = data + .connection_pool + .access_storage() + .await + .map_err(warn_err) + .map_err(ApiError::internal)?; if params.tokens.len() > data.max_tokens_per_request as usize { return Err(ApiError::bad_request( @@ -162,10 +164,12 @@ pub async fn get_request_by_id( ) -> JsonResult { let start = Instant::now(); - let mut storage = data.connection_pool.access_storage().await.map_err(|err| { - vlog::warn!("Internal Server Error: '{}';", err); - ApiError::internal("") - })?; + let mut storage = data + .connection_pool + .access_storage() + .await + .map_err(warn_err) + .map_err(ApiError::internal)?; let mut fe_requests_schema = storage.forced_exit_requests_schema(); @@ -413,3 +417,8 @@ mod tests { Ok(()) } } + +fn warn_err(err: T) -> T { + vlog::warn!("Internal Server Error: '{}';", err); + err +} diff --git a/infrastructure/zk/src/test/test.ts b/infrastructure/zk/src/test/test.ts index 1a0660cb06..6e5daf125f 100644 --- a/infrastructure/zk/src/test/test.ts +++ b/infrastructure/zk/src/test/test.ts @@ -33,7 +33,7 @@ export async function rustApi(reset: boolean, ...args: string[]) { await runOnTestDb( reset, 'core/bin/zksync_api', - `cargo test -p zksync_api -- --ignored --nocapture api_server + `cargo test --release -p zksync_api -- --ignored --nocapture api_server ${args.join(' ')}` ); } From 44c788325e52d191b06522dd93afebc646b42829 Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Wed, 24 Feb 2021 12:41:14 +0200 Subject: [PATCH 48/90] grafana & changelog --- changelog/core.md | 1 + .../dashboards/forced_exit_requests.jsonnet | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 infrastructure/grafana/dashboards/forced_exit_requests.jsonnet diff --git a/changelog/core.md b/changelog/core.md index 29a177e5c5..d2e432790b 100644 --- a/changelog/core.md +++ b/changelog/core.md @@ -15,6 +15,7 @@ All notable changes to the core components will be documented in this file. - Added a stressing dev fee ticker scenario to the loadtest. - Added a `--sloppy` mode to the `dev-fee-ticker-server` to simulate bad networks with the random delays and fails. +- Added `forced_exit_requests` functionality, which allows users to pay for ForcedExits from L1. Note that a few env variables were added that control the behaviour of the tool. ### Fixed diff --git a/infrastructure/grafana/dashboards/forced_exit_requests.jsonnet b/infrastructure/grafana/dashboards/forced_exit_requests.jsonnet new file mode 100644 index 0000000000..57f452452a --- /dev/null +++ b/infrastructure/grafana/dashboards/forced_exit_requests.jsonnet @@ -0,0 +1,16 @@ +local G = import '../generator.libsonnet'; +local metrics = [ + 'api.forced_exit_requests.v01.status', + 'api.forced_exit_requests.v01.submit_request', + 'api.forced_exit_requests.v01.get_request_by_id', + 'forced_exit_requests.get_funds_received_events', + 'forced_exit_requests.eth_watcher.enter_backoff_mode', + 'sql.forced_exit_requests.store_request', + 'sql.forced_exit_requests.get_request_by_id', + 'sql.forced_exit_requests.set_fulfilled_at', + 'sql.forced_exit_requests.get_oldest_unfulfilled_request' + 'sql.forced_exit_requests.set_fulfilled_by', + 'sql.forced_exit_requests.get_unconfirmed_requests' +]; + +G.dashboard('forced_exit_requests', metrics) From 14a0af3ca893840dfb9a4402a7ccda1769c36737 Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Wed, 24 Feb 2021 17:03:31 +0200 Subject: [PATCH 49/90] Take into account approximate block time instead of Utc::now() --- .../src/eth_watch.rs | 50 +++++++++++++++---- .../src/forced_exit_sender.rs | 34 +++++++------ 2 files changed, 59 insertions(+), 25 deletions(-) diff --git a/core/bin/zksync_forced_exit_requests/src/eth_watch.rs b/core/bin/zksync_forced_exit_requests/src/eth_watch.rs index f2f61101c8..075194cd71 100644 --- a/core/bin/zksync_forced_exit_requests/src/eth_watch.rs +++ b/core/bin/zksync_forced_exit_requests/src/eth_watch.rs @@ -1,7 +1,8 @@ -use chrono::{DateTime, Utc}; +use chrono::{DateTime, Duration as ChronoDuration, Utc}; use ethabi::{Address, Hash}; use std::{ convert::TryFrom, + ops::Sub, time::{Duration, Instant}, }; use std::{convert::TryInto, fmt::Debug}; @@ -65,7 +66,12 @@ impl EthClient { } } - async fn get_events(&self, from: u64, to: u64, topics: Vec) -> anyhow::Result> + async fn get_events( + &self, + from: u64, + to: u64, + topics: Vec, + ) -> anyhow::Result> where T: TryFrom, T::Error: Debug, @@ -86,7 +92,7 @@ impl EthClient { &self, from: u64, to: u64, - ) -> anyhow::Result> { + ) -> anyhow::Result> { let start = Instant::now(); let result = self .get_events(from, to, vec![self.topics.funds_received]) @@ -116,16 +122,38 @@ struct ForcedExitContractWatcher { // Usually blocks are created much slower (at rate 1 block per 10-20s), // but the block time falls through time, so just to double-check -const MILLIS_PER_BLOCK: u64 = 7000; +const MILLIS_PER_BLOCK_LOWER: u64 = 5000; +const MILLIS_PER_BLOCK_UPPER: u64 = 25000; -// Returns number of blocks that should have been created during the time +// Returns upper bound of the number of blocks that +// should have been created during the time fn time_range_to_block_diff(from: DateTime, to: DateTime) -> u64 { // Timestamps should never be negative let millis_from: u64 = from.timestamp_millis().try_into().unwrap(); let millis_to: u64 = to.timestamp_millis().try_into().unwrap(); // It does not matter whether to ceil or floor the division - millis_to.saturating_sub(millis_from) / MILLIS_PER_BLOCK + millis_to.saturating_sub(millis_from) / MILLIS_PER_BLOCK_LOWER +} + +// Returns the upper bound of the time that should have +// passed between the block range +fn block_diff_to_time_range(block_from: u64, block_to: u64) -> ChronoDuration { + let block_diff = block_to.saturating_sub(block_from); + + ChronoDuration::milliseconds( + block_diff + .saturating_mul(MILLIS_PER_BLOCK_UPPER) + .try_into() + .unwrap(), + ) +} + +// Lower bound on the time when was the block created +fn lower_bound_block_time(block: u64, current_block: u64) -> DateTime { + let time_diff = block_diff_to_time_range(block, current_block); + + Utc::now().sub(time_diff) } impl ForcedExitContractWatcher { @@ -235,7 +263,7 @@ impl ForcedExitContractWatcher { for e in events { self.forced_exit_sender - .process_request(e.amount.clone()) + .process_request(e.0.amount, lower_bound_block_time(e.1, last_block)) .await; } @@ -339,7 +367,7 @@ pub async fn get_contract_events( from: BlockNumber, to: BlockNumber, topics: Vec, -) -> anyhow::Result> +) -> anyhow::Result> where T: TryFrom, T::Error: Debug, @@ -356,8 +384,12 @@ where .await? .into_iter() .filter_map(|event| { + let block_number = event + .block_number + .expect("Trying to access pending block") + .as_u64(); if let Ok(event) = T::try_from(event) { - Some(Ok(event)) + Some(Ok((event, block_number))) } else { None } diff --git a/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs b/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs index c939f7ad48..c05b60f3d3 100644 --- a/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs +++ b/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs @@ -3,7 +3,7 @@ use std::{ ops::{AddAssign, Sub}, }; -use chrono::Utc; +use chrono::{DateTime, Utc}; use num::{BigUint, FromPrimitive}; use tokio::time; @@ -139,18 +139,14 @@ impl ForcedExitSender { Ok(transactions) } - // TODO: take the block timestamp into account instead of the now (ZKS-495) - pub fn expired(&self, request: &ForcedExitRequest) -> bool { - let now_millis = Utc::now().timestamp_millis(); - let created_at_millis = request.created_at.timestamp_millis(); - - now_millis.saturating_sub(created_at_millis) - <= self.config.forced_exit_requests.max_tx_interval - } - // Returns the id the request if it should be fulfilled, // error otherwise - pub fn check_request(&self, amount: BigUint, request: Option) -> bool { + pub fn check_request( + &self, + amount: BigUint, + submission_time: DateTime, + request: Option, + ) -> bool { let request = match request { Some(r) => r, None => { @@ -164,7 +160,7 @@ impl ForcedExitSender { return false; } - !self.expired(&request) && request.price_in_wei == amount + request.valid_until < submission_time && request.price_in_wei == amount } // Awaits until the request is complete @@ -318,14 +314,18 @@ impl ForcedExitSender { } } - pub async fn try_process_request(&self, amount: BigUint) -> anyhow::Result<()> { + pub async fn try_process_request( + &self, + amount: BigUint, + submission_time: DateTime, + ) -> anyhow::Result<()> { let (id, amount) = self.extract_id_from_amount(amount); let mut storage = self.connection_pool.access_storage().await?; let fe_request = self.get_request_by_id(&mut storage, id).await?; - let fe_request = if self.check_request(amount, fe_request.clone()) { + let fe_request = if self.check_request(amount, submission_time, fe_request.clone()) { // The self.check_request already checked that the fe_request is Some(_) fe_request.unwrap() } else { @@ -348,13 +348,15 @@ impl ForcedExitSender { Ok(()) } - pub async fn process_request(&self, amount: BigUint) { + pub async fn process_request(&self, amount: BigUint, submission_time: DateTime) { let mut attempts: u8 = 0; // Typically this should not run any longer than 1 iteration // In case something bad happens we do not want the server crush because // of the forced_exit_requests component loop { - let processing_attempt = self.try_process_request(amount.clone()).await; + let processing_attempt = self + .try_process_request(amount.clone(), submission_time) + .await; if processing_attempt.is_ok() { return; From f2c0fa06d78d5c0290900f46f51750ab4d14faa6 Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Wed, 24 Feb 2021 17:04:22 +0200 Subject: [PATCH 50/90] fmt --- changelog/core.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/changelog/core.md b/changelog/core.md index d2e432790b..203d6815de 100644 --- a/changelog/core.md +++ b/changelog/core.md @@ -15,7 +15,8 @@ All notable changes to the core components will be documented in this file. - Added a stressing dev fee ticker scenario to the loadtest. - Added a `--sloppy` mode to the `dev-fee-ticker-server` to simulate bad networks with the random delays and fails. -- Added `forced_exit_requests` functionality, which allows users to pay for ForcedExits from L1. Note that a few env variables were added that control the behaviour of the tool. +- Added `forced_exit_requests` functionality, which allows users to pay for ForcedExits from L1. Note that a few env + variables were added that control the behaviour of the tool. ### Fixed From 7c71cb7f3d89724ecd0cfe4261a722f62c36fa61 Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Wed, 24 Feb 2021 22:58:09 +0200 Subject: [PATCH 51/90] Add the old requests deletion functionality --- .../src/eth_watch.rs | 29 +++++ .../src/configs/forced_exit_requests.rs | 3 + core/lib/storage/sqlx-data.json | 12 ++ .../storage/src/forced_exit_requests/mod.rs | 30 ++++- .../storage/src/tests/forced_exit_requests.rs | 117 +++++++++++++++++- etc/env/base/forced_exit_requests.toml | 3 + 6 files changed, 190 insertions(+), 4 deletions(-) diff --git a/core/bin/zksync_forced_exit_requests/src/eth_watch.rs b/core/bin/zksync_forced_exit_requests/src/eth_watch.rs index f2f61101c8..9de7ab2e80 100644 --- a/core/bin/zksync_forced_exit_requests/src/eth_watch.rs +++ b/core/bin/zksync_forced_exit_requests/src/eth_watch.rs @@ -200,6 +200,23 @@ impl ForcedExitContractWatcher { } } + pub async fn delete_expired(&mut self) -> anyhow::Result<()> { + let mut storage = self.connection_pool.access_storage().await?; + + let expiration_time = chrono::Duration::milliseconds( + self.config + .forced_exit_requests + .expiration_period + .try_into() + .expect("Failed to convert expiration period to i64"), + ); + + storage + .forced_exit_requests_schema() + .delete_old_unfulfilled_requests(expiration_time) + .await + } + pub async fn poll(&mut self) { if !self.polling_allowed() { // Polling is currently disabled, skip it. @@ -240,6 +257,18 @@ impl ForcedExitContractWatcher { } self.last_viewed_block = last_confirmed_block; + + // We can delete the expired events only after the polling has been complete + // Since now we are sure that all the events that could have been processed already + // have been processed + if let Err(err) = self.delete_expired().await { + // If an error during deletion occures we should be notified, however + // it is not a reason to panic or revert the updates from the poll + log::warn!( + "An error occured when deleting the expired requests: {}", + err + ); + } } pub async fn run(mut self) { diff --git a/core/lib/config/src/configs/forced_exit_requests.rs b/core/lib/config/src/configs/forced_exit_requests.rs index ebd618e448..7e6ac85fd4 100644 --- a/core/lib/config/src/configs/forced_exit_requests.rs +++ b/core/lib/config/src/configs/forced_exit_requests.rs @@ -22,6 +22,7 @@ struct ForcedExitRequestsInternalConfig { pub wait_confirmations: u64, pub sender_private_key: String, pub sender_account_address: Address, + pub expiration_period: u64, } #[derive(Debug, Deserialize, Clone, PartialEq)] @@ -35,6 +36,7 @@ pub struct ForcedExitRequestsConfig { pub wait_confirmations: u64, pub sender_private_key: String, pub sender_account_address: Address, + pub expiration_period: u64, } impl ForcedExitRequestsConfig { @@ -55,6 +57,7 @@ impl ForcedExitRequestsConfig { wait_confirmations: config.wait_confirmations, sender_private_key: config.sender_private_key, sender_account_address: config.sender_account_address, + expiration_period: config.expiration_period, } } } diff --git a/core/lib/storage/sqlx-data.json b/core/lib/storage/sqlx-data.json index ad9252037d..8a0624ffbf 100644 --- a/core/lib/storage/sqlx-data.json +++ b/core/lib/storage/sqlx-data.json @@ -2659,6 +2659,18 @@ "nullable": [] } }, + "963cad1979935b50bc5c2bbe174f5d94fbd5c38ea752d304f987229c89e6070a": { + "query": "\n DELETE FROM forced_exit_requests\n WHERE fulfilled_by IS NULL AND valid_until < $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Timestamptz" + ] + }, + "nullable": [] + } + }, "98f87793202531586603307eab53987f75f4e07614af8706e6180413f808a1b4": { "query": "INSERT INTO txs_batches_signatures VALUES($1, $2)", "describe": { diff --git a/core/lib/storage/src/forced_exit_requests/mod.rs b/core/lib/storage/src/forced_exit_requests/mod.rs index c59d7fa680..be4ac7c893 100644 --- a/core/lib/storage/src/forced_exit_requests/mod.rs +++ b/core/lib/storage/src/forced_exit_requests/mod.rs @@ -2,7 +2,7 @@ use chrono::{DateTime, Utc}; // Built-in deps use num::BigInt; use sqlx::types::BigDecimal; -use std::time::Instant; +use std::{ops::Sub, time::Instant}; // External imports // Workspace imports // Local imports @@ -189,4 +189,32 @@ impl<'a, 'c> ForcedExitRequestsSchema<'a, 'c> { Ok(requests) } + + pub async fn delete_old_unfulfilled_requests( + &mut self, + // The time that has to be passed since the + // request has been considered invalid to delete it + deleting_threshold: chrono::Duration, + ) -> QueryResult<()> { + let start = Instant::now(); + + let oldest_allowed = Utc::now().sub(deleting_threshold); + + sqlx::query!( + r#" + DELETE FROM forced_exit_requests + WHERE fulfilled_by IS NULL AND valid_until < $1 + "#, + oldest_allowed + ) + .execute(self.0.conn()) + .await?; + + metrics::histogram!( + "sql.forced_exit_requests.delete_old_unfulfilled_requests", + start.elapsed() + ); + + Ok(()) + } } diff --git a/core/lib/storage/src/tests/forced_exit_requests.rs b/core/lib/storage/src/tests/forced_exit_requests.rs index 9fbe9bafbc..bbff058b33 100644 --- a/core/lib/storage/src/tests/forced_exit_requests.rs +++ b/core/lib/storage/src/tests/forced_exit_requests.rs @@ -1,18 +1,41 @@ -use std::str::FromStr; +use std::{ + ops::{Mul, Sub}, + str::FromStr, +}; use crate::forced_exit_requests::ForcedExitRequestsSchema; use crate::tests::db_test; use crate::QueryResult; use crate::StorageProcessor; -use chrono::{Timelike, Utc}; +use chrono::{Duration, Timelike, Utc}; use num::{BigUint, FromPrimitive}; use zksync_basic_types::Address; -use zksync_types::forced_exit_requests::{ForcedExitRequest, SaveForcedExitRequestQuery}; +use zksync_types::{ + forced_exit_requests::{ForcedExitRequest, SaveForcedExitRequestQuery}, + tx::TxHash, +}; use std::ops::Add; use zksync_types::TokenId; +// Accepts an array of requests and stores them in the db +pub async fn store_requests( + storage: &mut StorageProcessor<'_>, + requests: Vec, +) -> Vec { + let mut stored_requests: Vec = vec![]; + for req in requests.into_iter() { + stored_requests.push( + ForcedExitRequestsSchema(storage) + .store_request(req) + .await + .unwrap(), + ); + } + stored_requests +} + #[db_test] async fn get_oldest_unfulfilled_request(mut storage: StorageProcessor<'_>) -> QueryResult<()> { let mut now = Utc::now().with_nanosecond(0).unwrap(); @@ -90,3 +113,91 @@ async fn get_oldest_unfulfilled_request(mut storage: StorageProcessor<'_>) -> Qu Ok(()) } + +// Checks that during deletion of the old transactions +// are deleted and no more +#[db_test] +async fn delete_old_requests(mut storage: StorageProcessor<'_>) -> QueryResult<()> { + let now = Utc::now().with_nanosecond(0).unwrap(); + + let deleting_threshold = Duration::days(3); + let day = Duration::days(1); + let minute = Duration::minutes(1); + + // So here we imagine that the requests are valid for 2 days + // and we delete the old requests after at least 3 days have expired + let requests = vec![ + SaveForcedExitRequestQuery { + target: Address::from_str("c0f97CC918C9d6fA4E9fc6be61a6a06589D199b2").unwrap(), + tokens: vec![TokenId(1)], + price_in_wei: BigUint::from_i32(212).unwrap(), + created_at: now.sub(day.mul(8)), + // Invalid for 6 days => should be deleted + valid_until: now.sub(day.mul(6)), + }, + SaveForcedExitRequestQuery { + target: Address::from_str("c0f97CC918C9d6fA4E9fc6be61a6a06589D199b2").unwrap(), + tokens: vec![TokenId(1)], + price_in_wei: BigUint::from_i32(1).unwrap(), + created_at: now.sub(day.mul(5)).sub(minute), + // Invalid for 3 days and 1 minutes => should be deleted + valid_until: now.sub(day.mul(3)).sub(minute), + }, + SaveForcedExitRequestQuery { + target: Address::from_str("c0f97CC918C9d6fA4E9fc6be61a6a06589D199b2").unwrap(), + tokens: vec![TokenId(20)], + price_in_wei: BigUint::from_str("1000000000000000").unwrap(), + created_at: now.sub(day.mul(5)).add(minute.mul(5)), + // Invalid for 3 days minus 5 minutes => should not be deleted + valid_until: now.sub(day.mul(3)).add(minute.mul(5)), + }, + SaveForcedExitRequestQuery { + target: Address::from_str("c0f97CC918C9d6fA4E9fc6be61a6a06589D199b2").unwrap(), + tokens: vec![TokenId(20)], + price_in_wei: BigUint::from_str("1000000000000000").unwrap(), + created_at: now.sub(day.mul(5)).add(minute.mul(5)), + // Is valid => should no be deleted + valid_until: now.sub(day.mul(3)).add(minute.mul(5)), + }, + ]; + + let stored_requests = store_requests(&mut storage, requests).await; + + // This a hash of a random transaction + let transaction_hash = TxHash::from_str( + "sync-tx:796018689b3e323894f44fb0093856ec3832908c626dea357a9bd1b25f9d11bf", + ) + .unwrap(); + + // Setting fullfilled_by for the oldest request + // so that it should not be deleted + ForcedExitRequestsSchema(&mut storage) + .set_fulfilled_by(stored_requests[0].id, Some(vec![transaction_hash])) + .await?; + + ForcedExitRequestsSchema(&mut storage) + .delete_old_unfulfilled_requests(deleting_threshold) + .await?; + + // true means should not have been deleted + // false means should have been deleted + // Note that we have set the fulfilled_by for the first tx, that's why it should + // not have been deleted + let should_remain = vec![true, false, true, true]; + + for (i, request) in stored_requests.into_iter().enumerate() { + let stored = ForcedExitRequestsSchema(&mut storage) + .get_request_by_id(request.id) + .await?; + + let processed_correctly = if should_remain[i] { + stored.is_some() + } else { + stored.is_none() + }; + + assert!(processed_correctly, "Deletion was not processed correctly"); + } + + Ok(()) +} diff --git a/etc/env/base/forced_exit_requests.toml b/etc/env/base/forced_exit_requests.toml index 3bb1cfb456..77b44f590a 100644 --- a/etc/env/base/forced_exit_requests.toml +++ b/etc/env/base/forced_exit_requests.toml @@ -24,3 +24,6 @@ wait_confirmations=1 # The account of the ForcedExit sender sender_account_address="0xe1faB3eFD74A77C23B426c302D96372140FF7d0C" + +# The time after which an invalid request is deleted in milliseconds +expiration_period=3000 From ea7f908ac58b049495b9dc43c81979ea9118390f Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Thu, 25 Feb 2021 11:12:22 +0200 Subject: [PATCH 52/90] Address space overflow metric --- .../rest/forced_exit_requests/v01.rs | 18 ++++++++++++++++++ .../dashboards/forced_exit_requests.jsonnet | 1 + 2 files changed, 19 insertions(+) diff --git a/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs b/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs index d4769ecd4f..6c09c52b93 100644 --- a/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs +++ b/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs @@ -41,6 +41,7 @@ pub struct ApiForcedExitRequestsData { pub(crate) is_enabled: bool, pub(crate) max_tokens_per_request: u8, + pub(crate) digits_in_id: u8, pub(crate) recomended_tx_interval_millisecs: i64, pub(crate) max_tx_interval_millisecs: i64, pub(crate) price_per_token: i64, @@ -63,6 +64,7 @@ impl ApiForcedExitRequestsData { recomended_tx_interval_millisecs: config.forced_exit_requests.recomended_tx_interval, max_tx_interval_millisecs: config.forced_exit_requests.max_tx_interval, forced_exit_contract_address: config.contracts.forced_exit_addr, + digits_in_id: config.forced_exit_requests.digits_in_id, } } } @@ -151,6 +153,8 @@ pub async fn submit_request( .await .map_err(|_| ApiError::internal(""))?; + check_address_space_overflow(saved_fe_request.id, data.digits_in_id); + metrics::histogram!( "api.forced_exit_requests.v01.submit_request", start.elapsed() @@ -422,3 +426,17 @@ fn warn_err(err: T) -> T { vlog::warn!("Internal Server Error: '{}';", err); err } + +// Checks if the id exceeds half of the address space +// If it exceeds the half at all the alert should be triggerred +// since it it a sign of a possible DoS attack +pub fn check_address_space_overflow(id: i64, digits_in_id: u8) -> i64 { + let address_space = 10_i64.saturating_pow(digits_in_id as i64); + + let exceeding_rate = id.saturating_sub(address_space / 2); + + metrics::histogram!( + "forced_exit_requests.address_space_overflow", + exceeding_rate + ); +} diff --git a/infrastructure/grafana/dashboards/forced_exit_requests.jsonnet b/infrastructure/grafana/dashboards/forced_exit_requests.jsonnet index 57f452452a..9617fa91ff 100644 --- a/infrastructure/grafana/dashboards/forced_exit_requests.jsonnet +++ b/infrastructure/grafana/dashboards/forced_exit_requests.jsonnet @@ -5,6 +5,7 @@ local metrics = [ 'api.forced_exit_requests.v01.get_request_by_id', 'forced_exit_requests.get_funds_received_events', 'forced_exit_requests.eth_watcher.enter_backoff_mode', + 'forced_exit_requests.address_space_overflow' 'sql.forced_exit_requests.store_request', 'sql.forced_exit_requests.get_request_by_id', 'sql.forced_exit_requests.set_fulfilled_at', From 2ee8b515e417d83e803f8d52a05699831afb60a7 Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Thu, 25 Feb 2021 11:29:53 +0200 Subject: [PATCH 53/90] Id space and price per token overlap checks --- .../config/src/configs/forced_exit_requests.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/core/lib/config/src/configs/forced_exit_requests.rs b/core/lib/config/src/configs/forced_exit_requests.rs index ebd618e448..c0d31bb9ab 100644 --- a/core/lib/config/src/configs/forced_exit_requests.rs +++ b/core/lib/config/src/configs/forced_exit_requests.rs @@ -37,6 +37,22 @@ pub struct ForcedExitRequestsConfig { pub sender_account_address: Address, } +// Checks that in no way the price will overlap with the requests id space +// +// The amount that the users have to send to pay for the ForcedExit request +// = (number of tokens) * (price_per_token) + id +// +// Thus we need to check that at least digits_in_id first digits +// are equal to zeroes in price_per_token +fn validate_price_with_id_space(price: i64, digits_in_id: u8) { + let id_space = (10_i64).saturating_pow(digits_in_id.into()); + + assert!( + price % id_space == 0, + "The price per token may overlap with request id" + ) +} + impl ForcedExitRequestsConfig { pub fn from_env() -> Self { let config: ForcedExitRequestsInternalConfig = @@ -45,6 +61,8 @@ impl ForcedExitRequestsConfig { let max_tx_interval: f64 = (config.recomended_tx_interval as f64) * config.tx_interval_scaling_factor; + validate_price_with_id_space(config.price_per_token, config.digits_in_id); + ForcedExitRequestsConfig { enabled: config.enabled, max_tokens_per_request: config.max_tokens_per_request, From 964e3ecc589e4178a544890e87afde3b21953920 Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Thu, 25 Feb 2021 15:32:37 +0200 Subject: [PATCH 54/90] fmt --- changelog/core.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/changelog/core.md b/changelog/core.md index d2e432790b..203d6815de 100644 --- a/changelog/core.md +++ b/changelog/core.md @@ -15,7 +15,8 @@ All notable changes to the core components will be documented in this file. - Added a stressing dev fee ticker scenario to the loadtest. - Added a `--sloppy` mode to the `dev-fee-ticker-server` to simulate bad networks with the random delays and fails. -- Added `forced_exit_requests` functionality, which allows users to pay for ForcedExits from L1. Note that a few env variables were added that control the behaviour of the tool. +- Added `forced_exit_requests` functionality, which allows users to pay for ForcedExits from L1. Note that a few env + variables were added that control the behaviour of the tool. ### Fixed From 014155fa4a21f7785100c5e41bd7a845f65ebb16 Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Thu, 25 Feb 2021 15:38:21 +0200 Subject: [PATCH 55/90] Explicit name based on import --- core/bin/zksync_forced_exit_requests/src/eth_watch.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/bin/zksync_forced_exit_requests/src/eth_watch.rs b/core/bin/zksync_forced_exit_requests/src/eth_watch.rs index 075194cd71..28b58f6f6e 100644 --- a/core/bin/zksync_forced_exit_requests/src/eth_watch.rs +++ b/core/bin/zksync_forced_exit_requests/src/eth_watch.rs @@ -1,4 +1,4 @@ -use chrono::{DateTime, Duration as ChronoDuration, Utc}; +use chrono::{DateTime, Utc}; use ethabi::{Address, Hash}; use std::{ convert::TryFrom, @@ -138,10 +138,10 @@ fn time_range_to_block_diff(from: DateTime, to: DateTime) -> u64 { // Returns the upper bound of the time that should have // passed between the block range -fn block_diff_to_time_range(block_from: u64, block_to: u64) -> ChronoDuration { +fn block_diff_to_time_range(block_from: u64, block_to: u64) -> chrono::Duration { let block_diff = block_to.saturating_sub(block_from); - ChronoDuration::milliseconds( + chrono::Duration::milliseconds( block_diff .saturating_mul(MILLIS_PER_BLOCK_UPPER) .try_into() From 5a3619e09558ae2b20bc0bad80a18ab11d234d97 Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Thu, 25 Feb 2021 16:25:30 +0200 Subject: [PATCH 56/90] Minor fixes and db cleanup interval --- .../rest/forced_exit_requests/v01.rs | 8 +++-- .../src/eth_watch.rs | 29 ++++++++++++------- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs b/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs index 6c09c52b93..7d4f7d2caf 100644 --- a/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs +++ b/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs @@ -11,8 +11,8 @@ use actix_web::{ use bigdecimal::{BigDecimal, FromPrimitive}; use chrono::{Duration, Utc}; use num::{bigint::ToBigInt, BigUint}; -use std::ops::Add; use std::time::Instant; +use std::{convert::TryInto, ops::Add}; // Workspace uses pub use zksync_api_client::rest::forced_exit_requests::{ ForcedExitRegisterRequest, ForcedExitRequestStatus, @@ -430,10 +430,12 @@ fn warn_err(err: T) -> T { // Checks if the id exceeds half of the address space // If it exceeds the half at all the alert should be triggerred // since it it a sign of a possible DoS attack -pub fn check_address_space_overflow(id: i64, digits_in_id: u8) -> i64 { - let address_space = 10_i64.saturating_pow(digits_in_id as i64); +pub fn check_address_space_overflow(id: i64, digits_in_id: u8) { + let address_space = 10_i64.saturating_pow(digits_in_id as u32); let exceeding_rate = id.saturating_sub(address_space / 2); + // Need this for metrics + let exceeding_rate: u64 = exceeding_rate.try_into().unwrap(); metrics::histogram!( "forced_exit_requests.address_space_overflow", diff --git a/core/bin/zksync_forced_exit_requests/src/eth_watch.rs b/core/bin/zksync_forced_exit_requests/src/eth_watch.rs index 9de7ab2e80..792ac6ac4a 100644 --- a/core/bin/zksync_forced_exit_requests/src/eth_watch.rs +++ b/core/bin/zksync_forced_exit_requests/src/eth_watch.rs @@ -1,7 +1,8 @@ -use chrono::{DateTime, Utc}; +use chrono::{DateTime, TimeZone, Utc}; use ethabi::{Address, Hash}; use std::{ convert::TryFrom, + ops::Sub, time::{Duration, Instant}, }; use std::{convert::TryInto, fmt::Debug}; @@ -112,6 +113,8 @@ struct ForcedExitContractWatcher { forced_exit_sender: ForcedExitSender, mode: WatcherMode, + db_cleanup_interval: chrono::Duration, + last_db_cleanup_time: DateTime, } // Usually blocks are created much slower (at rate 1 block per 10-20s), @@ -258,16 +261,17 @@ impl ForcedExitContractWatcher { self.last_viewed_block = last_confirmed_block; - // We can delete the expired events only after the polling has been complete - // Since now we are sure that all the events that could have been processed already - // have been processed - if let Err(err) = self.delete_expired().await { - // If an error during deletion occures we should be notified, however - // it is not a reason to panic or revert the updates from the poll - log::warn!( - "An error occured when deleting the expired requests: {}", - err - ); + if Utc::now().sub(self.db_cleanup_interval) > self.last_db_cleanup_time { + if let Err(err) = self.delete_expired().await { + // If an error during deletion occures we should be notified, however + // it is not a reason to panic or revert the updates from the poll + log::warn!( + "An error occured when deleting the expired requests: {}", + err + ); + } else { + self.last_db_cleanup_time = Utc::now(); + } } } @@ -356,6 +360,9 @@ pub fn run_forced_exit_contract_watcher( last_viewed_block: 0, forced_exit_sender, mode: WatcherMode::Working, + db_cleanup_interval: chrono::Duration::minutes(5), + // Zero timestamp, has never deleted anything + last_db_cleanup_time: Utc.timestamp(0, 0), }; contract_watcher.run().await; From fc79a485469b14b4dbb750fd26959861975126af Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Thu, 25 Feb 2021 18:11:38 +0200 Subject: [PATCH 57/90] Change comparison operator for validation --- core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs b/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs index c05b60f3d3..e1d0b4dfd1 100644 --- a/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs +++ b/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs @@ -160,7 +160,7 @@ impl ForcedExitSender { return false; } - request.valid_until < submission_time && request.price_in_wei == amount + request.valid_until > submission_time && request.price_in_wei == amount } // Awaits until the request is complete From f9cf9826f64443e1805aa4475cdec9fcd67e8184 Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Thu, 25 Feb 2021 19:15:00 +0200 Subject: [PATCH 58/90] Fix i64 -> u64 conversion --- .../zksync_api/src/api_server/rest/forced_exit_requests/v01.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs b/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs index 7d4f7d2caf..92992d4b18 100644 --- a/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs +++ b/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs @@ -435,7 +435,7 @@ pub fn check_address_space_overflow(id: i64, digits_in_id: u8) { let exceeding_rate = id.saturating_sub(address_space / 2); // Need this for metrics - let exceeding_rate: u64 = exceeding_rate.try_into().unwrap(); + let exceeding_rate: u64 = exceeding_rate.max(0).try_into().unwrap(); metrics::histogram!( "forced_exit_requests.address_space_overflow", From ecfa258f504b8fb9e600d3713a95a25099a59648 Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Fri, 26 Feb 2021 13:47:44 +0200 Subject: [PATCH 59/90] Split Ok(...) statement into two Co-authored-by: Vlad Bochok <41153528+vladbochok@users.noreply.github.com> --- core/bin/zksync_core/src/eth_watch/client.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/bin/zksync_core/src/eth_watch/client.rs b/core/bin/zksync_core/src/eth_watch/client.rs index 002302906a..7dd1ccca33 100644 --- a/core/bin/zksync_core/src/eth_watch/client.rs +++ b/core/bin/zksync_core/src/eth_watch/client.rs @@ -145,5 +145,7 @@ impl EthClient for EthHttpClient { } pub async fn get_web3_block_number(web3: &Web3) -> anyhow::Result { - Ok(web3.eth().block_number().await?.as_u64()) + let block_number = web3.eth().block_number().await?.as_u64(); + + Ok(block_number) } From 03b3ffac1f120d7731faaed336b60827ee837fa1 Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Fri, 26 Feb 2021 13:52:26 +0200 Subject: [PATCH 60/90] Adapt to the new dev and minor refactoring --- .../bin/zksync_forced_exit_requests/src/eth_watch.rs | 3 +-- .../src/forced_exit_sender.rs | 11 ++++------- .../src/prepare_forced_exit_sender.rs | 12 ++++++++---- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/core/bin/zksync_forced_exit_requests/src/eth_watch.rs b/core/bin/zksync_forced_exit_requests/src/eth_watch.rs index f2f61101c8..976bb1bfb4 100644 --- a/core/bin/zksync_forced_exit_requests/src/eth_watch.rs +++ b/core/bin/zksync_forced_exit_requests/src/eth_watch.rs @@ -311,8 +311,7 @@ pub fn run_forced_exit_contract_watcher( connection_pool.clone(), config.clone(), ) - .await - .unwrap(); + .await; // In case there were some transactions which were submitted // but were not committed we will try to wait until they are committed diff --git a/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs b/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs index c939f7ad48..fc3008d9ef 100644 --- a/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs +++ b/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs @@ -55,7 +55,7 @@ impl ForcedExitSender { core_api_client: CoreApiClient, connection_pool: ConnectionPool, config: ZkSyncConfig, - ) -> anyhow::Result { + ) -> Self { let forced_exit_sender_account_id = get_forced_exit_sender_account_id(connection_pool.clone(), &config) .await @@ -67,13 +67,13 @@ impl ForcedExitSender { let sender_private_key = read_signing_key(&sender_private_key).expect("Reading private key failed"); - Ok(Self { + Self { core_api_client, connection_pool, forced_exit_sender_account_id, config, sender_private_key, - }) + } } pub fn extract_id_from_amount(&self, amount: BigUint) -> (i64, BigUint) { @@ -269,8 +269,7 @@ impl ForcedExitSender { request: &ForcedExitRequest, txs: Vec, ) -> anyhow::Result> { - let mut db_transaction = storage.start_transaction().await?; - let mut schema = db_transaction.forced_exit_requests_schema(); + let mut schema = storage.forced_exit_requests_schema(); let hashes: Vec = txs.iter().map(|tx| tx.hash()).collect(); self.core_api_client.send_txs_batch(txs, vec![]).await??; @@ -279,8 +278,6 @@ impl ForcedExitSender { .set_fulfilled_by(request.id, Some(hashes.clone())) .await?; - db_transaction.commit().await?; - Ok(hashes) } diff --git a/core/bin/zksync_forced_exit_requests/src/prepare_forced_exit_sender.rs b/core/bin/zksync_forced_exit_requests/src/prepare_forced_exit_sender.rs index 4ddd5298a4..f3c194ef40 100644 --- a/core/bin/zksync_forced_exit_requests/src/prepare_forced_exit_sender.rs +++ b/core/bin/zksync_forced_exit_requests/src/prepare_forced_exit_sender.rs @@ -7,7 +7,7 @@ use zksync_storage::{ use zksync_api::core_api_client::CoreApiClient; use zksync_types::{ - tx::{PackedEthSignature, TimeRange, TxHash}, + tx::{ChangePubKeyType, PackedEthSignature, TimeRange, TxHash}, AccountId, Address, PubKeyHash, ZkSyncTx, H256, }; @@ -17,7 +17,7 @@ use zksync_crypto::franklin_crypto::eddsa::PrivateKey; use tokio::time; -use zksync_test_account::ZkSyncAccount; +use zksync_test_account::{ZkSyncAccount, ZkSyncETHAccountData}; use super::utils::{read_signing_key, Engine}; @@ -184,12 +184,16 @@ pub async fn register_signing_key( ) -> anyhow::Result<()> { let eth_sk = get_verified_eth_sk(sender_address).await; + let eth_account_data = ZkSyncETHAccountData::EOA { + eth_private_key: eth_sk, + }; + let sender_account = ZkSyncAccount::new( sender_sk, // The accout is changing public key for hte first time, so nonce is 0 Nonce(0), sender_address, - eth_sk, + eth_account_data, ); sender_account.set_account_id(Some(sender_id)); @@ -198,7 +202,7 @@ pub async fn register_signing_key( true, TokenId(0), BigUint::from(0u8), - false, + ChangePubKeyType::ECDSA, TimeRange::default(), ); From 7719175d6dc2b04eaf10ebd6bfd6583e44bf687f Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Fri, 26 Feb 2021 13:59:57 +0200 Subject: [PATCH 61/90] fmt --- core/bin/zksync_core/src/eth_watch/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/bin/zksync_core/src/eth_watch/client.rs b/core/bin/zksync_core/src/eth_watch/client.rs index 7dd1ccca33..328834d54e 100644 --- a/core/bin/zksync_core/src/eth_watch/client.rs +++ b/core/bin/zksync_core/src/eth_watch/client.rs @@ -146,6 +146,6 @@ impl EthClient for EthHttpClient { pub async fn get_web3_block_number(web3: &Web3) -> anyhow::Result { let block_number = web3.eth().block_number().await?.as_u64(); - + Ok(block_number) } From 2f7a0b9578885e0bff92d08647e243e46da7f0d6 Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Fri, 26 Feb 2021 16:08:13 +0200 Subject: [PATCH 62/90] Update core/lib/storage/src/tests/forced_exit_requests.rs Co-authored-by: Aleksey Sidorov --- core/lib/storage/src/tests/forced_exit_requests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/lib/storage/src/tests/forced_exit_requests.rs b/core/lib/storage/src/tests/forced_exit_requests.rs index bbff058b33..a461d26ec5 100644 --- a/core/lib/storage/src/tests/forced_exit_requests.rs +++ b/core/lib/storage/src/tests/forced_exit_requests.rs @@ -156,7 +156,7 @@ async fn delete_old_requests(mut storage: StorageProcessor<'_>) -> QueryResult<( tokens: vec![TokenId(20)], price_in_wei: BigUint::from_str("1000000000000000").unwrap(), created_at: now.sub(day.mul(5)).add(minute.mul(5)), - // Is valid => should no be deleted + // Is valid => should not be deleted valid_until: now.sub(day.mul(3)).add(minute.mul(5)), }, ]; From 1de93479b21ed2c303ffbb73658e5201cf4c0553 Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Fri, 26 Feb 2021 16:31:38 +0200 Subject: [PATCH 63/90] Minor refactor --- .../src/eth_watch.rs | 19 +++++-------------- core/lib/types/src/forced_exit_requests.rs | 6 ++++++ 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/core/bin/zksync_forced_exit_requests/src/eth_watch.rs b/core/bin/zksync_forced_exit_requests/src/eth_watch.rs index 95de29e550..b2cd91915b 100644 --- a/core/bin/zksync_forced_exit_requests/src/eth_watch.rs +++ b/core/bin/zksync_forced_exit_requests/src/eth_watch.rs @@ -66,12 +66,7 @@ impl EthClient { } } - async fn get_events( - &self, - from: u64, - to: u64, - topics: Vec, - ) -> anyhow::Result> + async fn get_events(&self, from: u64, to: u64, topics: Vec) -> anyhow::Result> where T: TryFrom, T::Error: Debug, @@ -92,7 +87,7 @@ impl EthClient { &self, from: u64, to: u64, - ) -> anyhow::Result> { + ) -> anyhow::Result> { let start = Instant::now(); let result = self .get_events(from, to, vec![self.topics.funds_received]) @@ -263,7 +258,7 @@ impl ForcedExitContractWatcher { for e in events { self.forced_exit_sender - .process_request(e.0.amount, lower_bound_block_time(e.1, last_block)) + .process_request(e.amount, lower_bound_block_time(e.block_number, last_block)) .await; } @@ -366,7 +361,7 @@ pub async fn get_contract_events( from: BlockNumber, to: BlockNumber, topics: Vec, -) -> anyhow::Result> +) -> anyhow::Result> where T: TryFrom, T::Error: Debug, @@ -383,12 +378,8 @@ where .await? .into_iter() .filter_map(|event| { - let block_number = event - .block_number - .expect("Trying to access pending block") - .as_u64(); if let Ok(event) = T::try_from(event) { - Some(Ok((event, block_number))) + Some(Ok(event)) } else { None } diff --git a/core/lib/types/src/forced_exit_requests.rs b/core/lib/types/src/forced_exit_requests.rs index bec4f813ef..30332ce61e 100644 --- a/core/lib/types/src/forced_exit_requests.rs +++ b/core/lib/types/src/forced_exit_requests.rs @@ -40,6 +40,7 @@ pub struct SaveForcedExitRequestQuery { #[derive(Debug, Clone)] pub struct FundsReceivedEvent { pub amount: BigUint, + pub block_number: u64, } impl TryFrom for FundsReceivedEvent { @@ -55,9 +56,14 @@ impl TryFrom for FundsReceivedEvent { .map_err(|e| format_err!("Event data decode: {:?}", e))?; let amount = dec_ev.remove(0).to_uint().unwrap(); + let block_number = event + .block_number + .expect("Trying to access pending block") + .as_u64(); Ok(FundsReceivedEvent { amount: BigUint::from(amount.as_u128()), + block_number, }) } } From ac0e2648e1b551f148d0e862badc3fe2d236e2c2 Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Wed, 3 Mar 2021 11:38:12 +0200 Subject: [PATCH 64/90] Fix typos Co-authored-by: Lyova Potyomkin --- .../src/prepare_forced_exit_sender.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/bin/zksync_forced_exit_requests/src/prepare_forced_exit_sender.rs b/core/bin/zksync_forced_exit_requests/src/prepare_forced_exit_sender.rs index f3c194ef40..cf6114e15c 100644 --- a/core/bin/zksync_forced_exit_requests/src/prepare_forced_exit_sender.rs +++ b/core/bin/zksync_forced_exit_requests/src/prepare_forced_exit_sender.rs @@ -147,7 +147,7 @@ pub async fn wait_for_change_pub_key_tx( .fail_reason .unwrap_or_else(|| String::from("unknown")); panic!( - "Failed to set public for forced exit sedner. Reason: {}", + "Failed to set public key for forced exit sedner. Reason: {}", fail_reason ); } @@ -190,7 +190,7 @@ pub async fn register_signing_key( let sender_account = ZkSyncAccount::new( sender_sk, - // The accout is changing public key for hte first time, so nonce is 0 + // The account is changing public key for the first time, so nonce is 0 Nonce(0), sender_address, eth_account_data, From 3ab59b40a66504d28f6d1ec50eb55a00d23d63d4 Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Fri, 5 Mar 2021 22:59:14 +0200 Subject: [PATCH 65/90] Unit tests for forced exit requests --- .../src/core_interaction_wrapper.rs | 149 +++++++ .../src/eth_watch.rs | 350 +++++++++++++++-- .../src/forced_exit_sender.rs | 371 +++++++++--------- .../zksync_forced_exit_requests/src/lib.rs | 4 + .../src/prepare_forced_exit_sender.rs | 18 +- .../zksync_forced_exit_requests/src/test.rs | 167 ++++++++ .../zksync_forced_exit_requests/src/utils.rs | 66 ++++ 7 files changed, 905 insertions(+), 220 deletions(-) create mode 100644 core/bin/zksync_forced_exit_requests/src/core_interaction_wrapper.rs create mode 100644 core/bin/zksync_forced_exit_requests/src/test.rs diff --git a/core/bin/zksync_forced_exit_requests/src/core_interaction_wrapper.rs b/core/bin/zksync_forced_exit_requests/src/core_interaction_wrapper.rs new file mode 100644 index 0000000000..7eb4438518 --- /dev/null +++ b/core/bin/zksync_forced_exit_requests/src/core_interaction_wrapper.rs @@ -0,0 +1,149 @@ +use chrono::Utc; +use zksync_storage::{chain::operations_ext::records::TxReceiptResponse, ConnectionPool}; +use zksync_types::{ + forced_exit_requests::{ForcedExitRequest, ForcedExitRequestId}, + tx::TxHash, + AccountId, Nonce, +}; + +use zksync_api::core_api_client::CoreApiClient; +use zksync_types::SignedZkSyncTx; + +// We could use `db reset` and test the db the same way as in rust_api +// but it seemed to be an overkill here, so it was decided to use +// traits for unit-testing. Also it gives a much broader level of control +// over what's going on +#[async_trait::async_trait] +pub trait CoreInteractionWrapper { + async fn get_nonce(&self, account_id: AccountId) -> anyhow::Result>; + async fn get_unconfirmed_requests(&self) -> anyhow::Result>; + async fn set_fulfilled_at(&self, id: i64) -> anyhow::Result<()>; + async fn set_fulfilled_by( + &self, + id: ForcedExitRequestId, + value: Option>, + ) -> anyhow::Result<()>; + async fn get_request_by_id(&self, id: i64) -> anyhow::Result>; + async fn get_receipt(&self, tx_hash: TxHash) -> anyhow::Result>; + async fn send_and_save_txs_batch( + &self, + request: &ForcedExitRequest, + txs: Vec, + ) -> anyhow::Result>; + async fn get_oldest_unfulfilled_request(&self) -> anyhow::Result>; + async fn delete_old_unfulfilled_requests( + &self, + deleting_threshold: chrono::Duration, + ) -> anyhow::Result<()>; +} +#[derive(Clone)] +pub struct MempoolCoreInteractionWrapper { + core_api_client: CoreApiClient, + connection_pool: ConnectionPool, +} + +impl MempoolCoreInteractionWrapper { + pub fn new(core_api_client: CoreApiClient, connection_pool: ConnectionPool) -> Self { + Self { + core_api_client, + connection_pool, + } + } +} + +#[async_trait::async_trait] +impl CoreInteractionWrapper for MempoolCoreInteractionWrapper { + async fn get_nonce(&self, account_id: AccountId) -> anyhow::Result> { + let mut storage = self.connection_pool.access_storage().await?; + let mut account_schema = storage.chain().account_schema(); + + let sender_state = account_schema + .last_committed_state_for_account(account_id) + .await?; + + Ok(sender_state.map(|state| state.nonce)) + } + + async fn get_unconfirmed_requests(&self) -> anyhow::Result> { + let mut storage = self.connection_pool.access_storage().await?; + let mut forced_exit_requests_schema = storage.forced_exit_requests_schema(); + forced_exit_requests_schema.get_unconfirmed_requests().await + } + + async fn set_fulfilled_at(&self, id: i64) -> anyhow::Result<()> { + let mut storage = self.connection_pool.access_storage().await?; + let mut fe_schema = storage.forced_exit_requests_schema(); + + fe_schema.set_fulfilled_at(id, Utc::now()).await?; + + vlog::info!("FE request with id {} was fulfilled", id); + + Ok(()) + } + + async fn set_fulfilled_by( + &self, + id: ForcedExitRequestId, + value: Option>, + ) -> anyhow::Result<()> { + let mut storage = self.connection_pool.access_storage().await?; + let mut forced_exit_requests_schema = storage.forced_exit_requests_schema(); + forced_exit_requests_schema + .set_fulfilled_by(id, value) + .await + } + + async fn get_receipt(&self, tx_hash: TxHash) -> anyhow::Result> { + let mut storage = self.connection_pool.access_storage().await?; + storage + .chain() + .operations_ext_schema() + .tx_receipt(tx_hash.as_ref()) + .await + } + + async fn get_request_by_id(&self, id: i64) -> anyhow::Result> { + let mut storage = self.connection_pool.access_storage().await?; + let mut fe_schema = storage.forced_exit_requests_schema(); + + let request = fe_schema.get_request_by_id(id).await?; + Ok(request) + } + + async fn send_and_save_txs_batch( + &self, + request: &ForcedExitRequest, + txs: Vec, + ) -> anyhow::Result> { + let mut storage = self.connection_pool.access_storage().await?; + let mut schema = storage.forced_exit_requests_schema(); + + let hashes: Vec = txs.iter().map(|tx| tx.hash()).collect(); + self.core_api_client.send_txs_batch(txs, vec![]).await??; + + schema + .set_fulfilled_by(request.id, Some(hashes.clone())) + .await?; + + Ok(hashes) + } + + async fn get_oldest_unfulfilled_request(&self) -> anyhow::Result> { + let mut storage = self.connection_pool.access_storage().await?; + storage + .forced_exit_requests_schema() + .get_oldest_unfulfilled_request() + .await + } + + async fn delete_old_unfulfilled_requests( + &self, + deleting_threshold: chrono::Duration, + ) -> anyhow::Result<()> { + let mut storage = self.connection_pool.access_storage().await?; + storage + .forced_exit_requests_schema() + .delete_old_unfulfilled_requests(deleting_threshold) + .await + } +} diff --git a/core/bin/zksync_forced_exit_requests/src/eth_watch.rs b/core/bin/zksync_forced_exit_requests/src/eth_watch.rs index ee7f5cd3d7..1b7e4bf193 100644 --- a/core/bin/zksync_forced_exit_requests/src/eth_watch.rs +++ b/core/bin/zksync_forced_exit_requests/src/eth_watch.rs @@ -25,6 +25,10 @@ use zksync_core::eth_watch::{get_web3_block_number, WatcherMode}; use zksync_types::forced_exit_requests::FundsReceivedEvent; use super::prepare_forced_exit_sender::prepare_forced_exit_sender_account; +use crate::{ + core_interaction_wrapper::{CoreInteractionWrapper, MempoolCoreInteractionWrapper}, + forced_exit_sender::MempoolForcedExitSender, +}; use super::ForcedExitSender; @@ -47,13 +51,23 @@ impl ContractTopics { } } -pub struct EthClient { +#[async_trait::async_trait] +pub trait EthClient { + async fn get_funds_received_events( + &self, + from: u64, + to: u64, + ) -> anyhow::Result>; + async fn block_number(&self) -> anyhow::Result; +} + +pub struct EthHttpClient { web3: Web3, forced_exit_contract: Contract, topics: ContractTopics, } -impl EthClient { +impl EthHttpClient { pub fn new(web3: Web3, zksync_contract_addr: H160) -> Self { let forced_exit_contract = Contract::new(web3.eth(), zksync_contract_addr, forced_exit_contract()); @@ -82,7 +96,10 @@ impl EthClient { ) .await } +} +#[async_trait::async_trait] +impl EthClient for EthHttpClient { async fn get_funds_received_events( &self, from: u64, @@ -105,12 +122,12 @@ impl EthClient { } } -struct ForcedExitContractWatcher { - connection_pool: ConnectionPool, +struct ForcedExitContractWatcher { + core_interaction_wrapper: K, config: ZkSyncConfig, - eth_client: EthClient, + eth_client: C, last_viewed_block: u64, - forced_exit_sender: ForcedExitSender, + forced_exit_sender: T, mode: WatcherMode, db_cleanup_interval: chrono::Duration, @@ -122,7 +139,7 @@ struct ForcedExitContractWatcher { const MILLIS_PER_BLOCK_LOWER: u64 = 5000; const MILLIS_PER_BLOCK_UPPER: u64 = 25000; -// Returns upper bound of the number of blocks that +// Returns the upper bound of the number of blocks that // should have been created during the time fn time_range_to_block_diff(from: DateTime, to: DateTime) -> u64 { // Timestamps should never be negative @@ -153,12 +170,35 @@ fn lower_bound_block_time(block: u64, current_block: u64) -> DateTime { Utc::now().sub(time_diff) } -impl ForcedExitContractWatcher { - async fn restore_state_from_eth(&mut self, block: u64) -> anyhow::Result<()> { - let mut storage = self.connection_pool.access_storage().await?; - let mut fe_schema = storage.forced_exit_requests_schema(); +impl + ForcedExitContractWatcher +{ + pub fn new( + core_interaction_wrapper: K, + config: ZkSyncConfig, + eth_client: C, + forced_exit_sender: T, + db_cleanup_interval: chrono::Duration, + ) -> Self { + Self { + core_interaction_wrapper, + config, + eth_client, + forced_exit_sender, + + last_viewed_block: 0, + mode: WatcherMode::Working, + db_cleanup_interval, + // Zero timestamp, has never deleted anything + last_db_cleanup_time: Utc.timestamp(0, 0), + } + } - let oldest_request = fe_schema.get_oldest_unfulfilled_request().await?; + pub async fn restore_state_from_eth(&mut self, block: u64) -> anyhow::Result<()> { + let oldest_request = self + .core_interaction_wrapper + .get_oldest_unfulfilled_request() + .await?; let wait_confirmations = self.config.forced_exit_requests.wait_confirmations; // No oldest request means that there are no requests that were possibly ignored @@ -226,8 +266,6 @@ impl ForcedExitContractWatcher { } pub async fn delete_expired(&mut self) -> anyhow::Result<()> { - let mut storage = self.connection_pool.access_storage().await?; - let expiration_time = chrono::Duration::milliseconds( self.config .forced_exit_requests @@ -236,8 +274,7 @@ impl ForcedExitContractWatcher { .expect("Failed to convert expiration period to i64"), ); - storage - .forced_exit_requests_schema() + self.core_interaction_wrapper .delete_old_unfulfilled_requests(expiration_time) .await } @@ -341,7 +378,7 @@ pub fn run_forced_exit_contract_watcher( ) -> JoinHandle<()> { let transport = web3::transports::Http::new(&config.eth_client.web3_url[0]).unwrap(); let web3 = web3::Web3::new(transport); - let eth_client = EthClient::new(web3, config.contracts.forced_exit_addr); + let eth_client = EthHttpClient::new(web3, config.contracts.forced_exit_addr); tokio::spawn(async move { // We should not proceed if the feature is disabled @@ -351,7 +388,7 @@ pub fn run_forced_exit_contract_watcher( // It is fine to unwrap here, since without it there is not way we // can be sure that the forced exit sender will work properly - prepare_forced_exit_sender_account( + let id = prepare_forced_exit_sender_account( connection_pool.clone(), core_api_client.clone(), &config, @@ -359,14 +396,12 @@ pub fn run_forced_exit_contract_watcher( .await .unwrap(); + let core_interaction_wrapper = + MempoolCoreInteractionWrapper::new(core_api_client, connection_pool.clone()); // It is ok to unwrap here, since if forced_exit_sender is not created, then // the watcher is meaningless - let mut forced_exit_sender = ForcedExitSender::new( - core_api_client.clone(), - connection_pool.clone(), - config.clone(), - ) - .await; + let mut forced_exit_sender = + MempoolForcedExitSender::new(core_interaction_wrapper.clone(), config.clone(), id); // In case there were some transactions which were submitted // but were not committed we will try to wait until they are committed @@ -374,17 +409,13 @@ pub fn run_forced_exit_contract_watcher( "Unexpected error while trying to wait for unconfirmed forced_exit transactions", ); - let contract_watcher = ForcedExitContractWatcher { - connection_pool, + let contract_watcher = ForcedExitContractWatcher::new( + core_interaction_wrapper, config, eth_client, - last_viewed_block: 0, forced_exit_sender, - mode: WatcherMode::Working, - db_cleanup_interval: chrono::Duration::minutes(5), - // Zero timestamp, has never deleted anything - last_db_cleanup_time: Utc.timestamp(0, 0), - }; + chrono::Duration::minutes(5), + ); contract_watcher.run().await; }) @@ -429,3 +460,258 @@ pub async fn infinite_async_loop() { timer.tick().await; } } + +#[cfg(test)] +mod test { + use num::{BigUint, FromPrimitive}; + use std::{str::FromStr, sync::Mutex}; + use zksync_config::ZkSyncConfig; + use zksync_types::{forced_exit_requests::ForcedExitRequest, Address, TokenId}; + + use super::*; + use crate::test::{add_request, MockCoreInteractionWrapper}; + + const TEST_FIRST_CURRENT_BLOCK: u64 = 10000000; + struct MockEthClient { + pub events: Vec, + pub current_block_number: u64, + } + + #[async_trait::async_trait] + impl EthClient for MockEthClient { + async fn get_funds_received_events( + &self, + from: u64, + to: u64, + ) -> anyhow::Result> { + let events = self + .events + .iter() + .filter(|&x| x.block_number >= from && x.block_number <= to) + .cloned() + .collect(); + Ok(events) + } + + async fn block_number(&self) -> anyhow::Result { + Ok(self.current_block_number) + } + } + struct DummyForcedExitSender { + pub processed_requests: Mutex)>>, + } + + impl DummyForcedExitSender { + pub fn new() -> Self { + Self { + processed_requests: Mutex::new(vec![]), + } + } + } + + #[async_trait::async_trait] + impl ForcedExitSender for DummyForcedExitSender { + async fn process_request(&self, amount: BigUint, submission_time: DateTime) { + let mut write_lock = self + .processed_requests + .lock() + .expect("Failed to get write lock for processed_requests"); + (*write_lock).push((amount, submission_time)); + } + } + + type TestForcedExitContractWatcher = + ForcedExitContractWatcher; + + fn get_test_forced_exit_contract_watcher() -> TestForcedExitContractWatcher { + let core_interaction_wrapper = MockCoreInteractionWrapper::default(); + let config = ZkSyncConfig::from_env(); + let eth_client = MockEthClient { + events: vec![], + current_block_number: TEST_FIRST_CURRENT_BLOCK, + }; + let forced_exit_sender = DummyForcedExitSender::new(); + + ForcedExitContractWatcher::new( + core_interaction_wrapper, + config, + eth_client, + forced_exit_sender, + chrono::Duration::minutes(5), + ) + } + + #[tokio::test] + async fn test_watcher_deleting_old_requests() { + let week = chrono::Duration::weeks(1); + let three_days = chrono::Duration::days(3); + + let mut watcher = get_test_forced_exit_contract_watcher(); + + let old_request = ForcedExitRequest { + id: 1, + target: Address::random(), + tokens: vec![TokenId(0)], + price_in_wei: BigUint::from_i64(12).unwrap(), + valid_until: Utc::now().sub(week), + // Outdated by far + created_at: Utc::now().sub(week).sub(three_days), + fulfilled_at: None, + fulfilled_by: None, + }; + + add_request( + &watcher.core_interaction_wrapper.requests, + old_request.clone(), + ); + + watcher + .restore_state_from_eth(TEST_FIRST_CURRENT_BLOCK) + .await + .expect("Failed to restore state from eth"); + + watcher.poll().await; + + let requests_lock = watcher.core_interaction_wrapper.requests.lock().unwrap(); + // The old request should have been deleted + assert_eq!(requests_lock.len(), 0); + // Need to do this to drop mutex + drop(requests_lock); + + add_request(&watcher.core_interaction_wrapper.requests, old_request); + watcher.poll().await; + + let requests_lock = watcher.core_interaction_wrapper.requests.lock().unwrap(); + // Not enough time has passed. The request should not be deleted + assert_eq!(requests_lock.len(), 1); + } + + #[tokio::test] + async fn test_watcher_restore_state() { + // This test should not depend on the constants or the way + // that the last calculated block works. This test is more of a sanity check: + // that both wait_confirmations and the time of creation of the oldest unfulfilled request + // is taken into account + + let confirmations_time = ZkSyncConfig::from_env() + .forced_exit_requests + .wait_confirmations; + + // Case 1. No requests => choose the youngest stable block + let mut watcher = get_test_forced_exit_contract_watcher(); + + watcher + .restore_state_from_eth(TEST_FIRST_CURRENT_BLOCK) + .await + .expect("Failed to restore state from ethereum"); + + assert_eq!( + watcher.last_viewed_block, + TEST_FIRST_CURRENT_BLOCK - confirmations_time + ); + + // Case 2. Very young requests => choose the youngest stable block + let mut watcher = get_test_forced_exit_contract_watcher(); + watcher.core_interaction_wrapper.requests = Mutex::new(vec![ForcedExitRequest { + id: 1, + target: Address::random(), + tokens: vec![TokenId(0)], + price_in_wei: BigUint::from_i64(12).unwrap(), + // does not matter in these tests + valid_until: Utc::now(), + // millisecond ago is quite young + created_at: Utc::now().sub(chrono::Duration::milliseconds(1)), + fulfilled_at: None, + fulfilled_by: None, + }]); + + watcher + .restore_state_from_eth(TEST_FIRST_CURRENT_BLOCK) + .await + .expect("Failed to restore state from ethereum"); + + assert_eq!( + watcher.last_viewed_block, + TEST_FIRST_CURRENT_BLOCK - confirmations_time + ); + + // Case 3. Very old requests => choose the old stable block + let mut watcher = get_test_forced_exit_contract_watcher(); + watcher.core_interaction_wrapper.requests = Mutex::new(vec![ForcedExitRequest { + id: 1, + target: Address::random(), + tokens: vec![TokenId(0)], + price_in_wei: BigUint::from_i64(12).unwrap(), + // does not matter in these tests + valid_until: Utc::now(), + // 1 week ago is quite old + created_at: Utc::now().sub(chrono::Duration::weeks(1)), + fulfilled_at: None, + fulfilled_by: None, + }]); + + watcher + .restore_state_from_eth(TEST_FIRST_CURRENT_BLOCK) + .await + .expect("Failed to restore state from ethereum"); + + assert!(watcher.last_viewed_block < TEST_FIRST_CURRENT_BLOCK - confirmations_time); + } + + #[tokio::test] + async fn test_watcher_processing_requests() { + // Here we have to test that events are processed + + let mut watcher = get_test_forced_exit_contract_watcher(); + + let wait_confirmations = 5; + watcher.config.forced_exit_requests.wait_confirmations = wait_confirmations; + + watcher.eth_client.events = vec![ + FundsReceivedEvent { + // Should be processed + amount: BigUint::from_str("1000000001").unwrap(), + block_number: TEST_FIRST_CURRENT_BLOCK - 2 * wait_confirmations, + }, + FundsReceivedEvent { + amount: BigUint::from_str("1000000002").unwrap(), + // Should be processed + block_number: TEST_FIRST_CURRENT_BLOCK - wait_confirmations - 1, + }, + FundsReceivedEvent { + amount: BigUint::from_str("1000000003").unwrap(), + // Should not be processed + block_number: TEST_FIRST_CURRENT_BLOCK - 1, + }, + ]; + + // 100 is just some small block number + watcher + .restore_state_from_eth(100) + .await + .expect("Failed to restore state from eth"); + + // Now it seems like a lot of new blocks have been created + watcher.eth_client.current_block_number = TEST_FIRST_CURRENT_BLOCK; + + watcher.poll().await; + + let processed_requests = watcher + .forced_exit_sender + .processed_requests + .lock() + .unwrap(); + + // The order does not really matter, but it is how it works in production + // and it is easier to test this way + assert_eq!(processed_requests.len(), 2); + assert_eq!( + processed_requests[0].0, + BigUint::from_str("1000000001").unwrap() + ); + assert_eq!( + processed_requests[1].0, + BigUint::from_str("1000000002").unwrap() + ); + } +} diff --git a/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs b/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs index de34cf684d..b1eda16f62 100644 --- a/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs +++ b/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs @@ -1,98 +1,84 @@ -use std::{ - convert::TryInto, - ops::{AddAssign, Sub}, -}; +use std::ops::AddAssign; use chrono::{DateTime, Utc}; -use num::{BigUint, FromPrimitive}; +use num::BigUint; use tokio::time; use zksync_config::ZkSyncConfig; -use zksync_storage::{ - chain::operations_ext::records::TxReceiptResponse, ConnectionPool, StorageProcessor, -}; + use zksync_types::{ - forced_exit_requests::{ForcedExitRequest, ForcedExitRequestId}, - tx::TimeRange, - tx::TxHash, - AccountId, Address, Nonce, TokenId, ZkSyncTx, + forced_exit_requests::ForcedExitRequest, tx::TimeRange, tx::TxHash, AccountId, Address, Nonce, + TokenId, ZkSyncTx, }; -use zksync_api::core_api_client::CoreApiClient; use zksync_types::ForcedExit; use zksync_types::SignedZkSyncTx; +use crate::{core_interaction_wrapper::CoreInteractionWrapper, utils}; + use super::utils::{Engine, PrivateKey}; use crate::utils::read_signing_key; // We try to process a request 3 times before sending warnings in the console -const PROCESSING_ATTEMPTS: u8 = 3; +const PROCESSING_ATTEMPTS: u32 = 3; + +#[async_trait::async_trait] +pub trait ForcedExitSender { + async fn process_request(&self, amount: BigUint, submission_time: DateTime); +} -pub struct ForcedExitSender { - core_api_client: CoreApiClient, - connection_pool: ConnectionPool, +pub struct MempoolForcedExitSender { + core_interaction_wrapper: T, config: ZkSyncConfig, forced_exit_sender_account_id: AccountId, sender_private_key: PrivateKey, } -async fn get_forced_exit_sender_account_id( - connection_pool: ConnectionPool, - config: &ZkSyncConfig, -) -> anyhow::Result { - let mut storage = connection_pool.access_storage().await?; - let mut accounts_schema = storage.chain().account_schema(); +#[async_trait::async_trait] +impl ForcedExitSender for MempoolForcedExitSender { + async fn process_request(&self, amount: BigUint, submission_time: DateTime) { + let mut attempts: u32 = 0; + // Typically this should not run any longer than 1 iteration + // In case something bad happens we do not want the server crush because + // of the forced_exit_requests component + loop { + dbg!("try processing 1"); + let processing_attempt = self + .try_process_request(amount.clone(), submission_time) + .await; - let account_id = accounts_schema - .account_id_by_address(config.forced_exit_requests.sender_account_address) - .await?; + if processing_attempt.is_ok() { + return; + } else { + attempts += 1; + } - account_id.ok_or_else(|| anyhow::Error::msg("Failed to get the forced_exit_sender account id")) + if attempts >= PROCESSING_ATTEMPTS { + vlog::error!("Failed to process forced exit for the {} time", attempts); + } + } + } } -impl ForcedExitSender { - pub async fn new( - core_api_client: CoreApiClient, - connection_pool: ConnectionPool, +impl MempoolForcedExitSender { + pub fn new( + core_interaction_wrapper: T, config: ZkSyncConfig, + forced_exit_sender_account_id: AccountId, ) -> Self { - let forced_exit_sender_account_id = - get_forced_exit_sender_account_id(connection_pool.clone(), &config) - .await - .expect("Failed to get the sender id"); - - let sender_private_key = - hex::decode(&config.clone().forced_exit_requests.sender_private_key[2..]) - .expect("Decoding private key failed"); + let sender_private_key = hex::decode(&config.forced_exit_requests.sender_private_key[2..]) + .expect("Decoding private key failed"); let sender_private_key = read_signing_key(&sender_private_key).expect("Reading private key failed"); Self { - core_api_client, - connection_pool, + core_interaction_wrapper, forced_exit_sender_account_id, config, sender_private_key, } } - pub fn extract_id_from_amount(&self, amount: BigUint) -> (i64, BigUint) { - let id_space_size: i64 = 10_i64.pow(self.config.forced_exit_requests.digits_in_id as u32); - - let id_space_size = BigUint::from_i64(id_space_size).unwrap(); - - // Taking to the power of 1 and finding mod - // is the only way to find mod of BigUint - let one = BigUint::from_u8(1u8).unwrap(); - let id = amount.modpow(&one, &id_space_size); - - // After extracting the id we need to delete it - // to make sure that amount is the same as in the db - let amount = amount.sub(&id); - - (id.try_into().unwrap(), amount) - } - pub fn build_forced_exit( &self, nonce: Nonce, @@ -118,17 +104,15 @@ impl ForcedExitSender { pub async fn build_transactions( &self, - storage: &mut StorageProcessor<'_>, + // storage: &mut StorageProcessor<'_>, fe_request: ForcedExitRequest, ) -> anyhow::Result> { - let mut account_schema = storage.chain().account_schema(); - - let sender_state = account_schema - .last_committed_state_for_account(self.forced_exit_sender_account_id) + let mut sender_nonce = self + .core_interaction_wrapper + .get_nonce(self.forced_exit_sender_account_id) .await? - .expect("The forced exit sender account has no committed state"); + .expect("Forced Exit sender account does not have nonce"); - let mut sender_nonce = sender_state.nonce; let mut transactions: Vec = vec![]; for token in fe_request.tokens.into_iter() { @@ -166,46 +150,29 @@ impl ForcedExitSender { // Awaits until the request is complete pub async fn await_unconfirmed_request( &self, - storage: &mut StorageProcessor<'_>, request: &ForcedExitRequest, ) -> anyhow::Result<()> { let hashes = request.fulfilled_by.clone(); if let Some(hashes) = hashes { for hash in hashes.into_iter() { - self.wait_until_comitted(storage, hash).await?; - self.set_fulfilled_at(storage, request.id).await?; + self.wait_until_comitted(hash).await?; + self.core_interaction_wrapper + .set_fulfilled_at(request.id) + .await?; } } Ok(()) } - pub async fn get_unconfirmed_requests( - &self, - storage: &mut StorageProcessor<'_>, - ) -> anyhow::Result> { - let mut forced_exit_requests_schema = storage.forced_exit_requests_schema(); - forced_exit_requests_schema.get_unconfirmed_requests().await - } - - pub async fn set_fulfilled_by( - &self, - storage: &mut StorageProcessor<'_>, - id: ForcedExitRequestId, - value: Option>, - ) -> anyhow::Result<()> { - let mut forced_exit_requests_schema = storage.forced_exit_requests_schema(); - forced_exit_requests_schema - .set_fulfilled_by(id, value) - .await - } - pub async fn await_unconfirmed(&mut self) -> anyhow::Result<()> { - let mut storage = self.connection_pool.access_storage().await?; - let unfullied_requests = self.get_unconfirmed_requests(&mut storage).await?; + let unfullied_requests = self + .core_interaction_wrapper + .get_unconfirmed_requests() + .await?; for request in unfullied_requests.into_iter() { - let await_result = self.await_unconfirmed_request(&mut storage, &request).await; + let await_result = self.await_unconfirmed_request(&request).await; if await_result.is_err() { // A transaction has failed. That is not intended. @@ -214,7 +181,8 @@ impl ForcedExitSender { vlog::error!( "A previously sent forced exit transaction has failed. Canceling the tx." ); - self.set_fulfilled_by(&mut storage, request.id, None) + self.core_interaction_wrapper + .set_fulfilled_by(request.id, None) .await?; } } @@ -222,66 +190,7 @@ impl ForcedExitSender { Ok(()) } - pub async fn get_request_by_id( - &self, - storage: &mut StorageProcessor<'_>, - id: i64, - ) -> anyhow::Result> { - let mut fe_schema = storage.forced_exit_requests_schema(); - - let request = fe_schema.get_request_by_id(id).await?; - Ok(request) - } - - pub async fn set_fulfilled_at( - &self, - storage: &mut StorageProcessor<'_>, - id: i64, - ) -> anyhow::Result<()> { - let mut fe_schema = storage.forced_exit_requests_schema(); - - fe_schema.set_fulfilled_at(id, Utc::now()).await?; - - vlog::info!("FE request with id {} was fulfilled", id); - - Ok(()) - } - - pub async fn get_receipt( - &self, - storage: &mut StorageProcessor<'_>, - tx_hash: TxHash, - ) -> anyhow::Result> { - storage - .chain() - .operations_ext_schema() - .tx_receipt(tx_hash.as_ref()) - .await - } - - pub async fn send_transactions( - &self, - storage: &mut StorageProcessor<'_>, - request: &ForcedExitRequest, - txs: Vec, - ) -> anyhow::Result> { - let mut schema = storage.forced_exit_requests_schema(); - - let hashes: Vec = txs.iter().map(|tx| tx.hash()).collect(); - self.core_api_client.send_txs_batch(txs, vec![]).await??; - - schema - .set_fulfilled_by(request.id, Some(hashes.clone())) - .await?; - - Ok(hashes) - } - - pub async fn wait_until_comitted( - &self, - storage: &mut StorageProcessor<'_>, - tx_hash: TxHash, - ) -> anyhow::Result<()> { + pub async fn wait_until_comitted(&self, tx_hash: TxHash) -> anyhow::Result<()> { let timeout_millis: u64 = 120000; let poll_interval_millis: u64 = 200; let poll_interval = time::Duration::from_secs(poll_interval_millis); @@ -296,7 +205,7 @@ impl ForcedExitSender { panic!("Comitting ForcedExit transaction failed!"); } - let receipt = self.get_receipt(storage, tx_hash).await?; + let receipt = self.core_interaction_wrapper.get_receipt(tx_hash).await?; if let Some(tx_receipt) = receipt { if tx_receipt.success { @@ -316,11 +225,12 @@ impl ForcedExitSender { amount: BigUint, submission_time: DateTime, ) -> anyhow::Result<()> { - let (id, amount) = self.extract_id_from_amount(amount); + let (id, amount) = utils::extract_id_from_amount( + amount, + self.config.forced_exit_requests.digits_in_id as u32, + ); - let mut storage = self.connection_pool.access_storage().await?; - - let fe_request = self.get_request_by_id(&mut storage, id).await?; + let fe_request = self.core_interaction_wrapper.get_request_by_id(id).await?; let fe_request = if self.check_request(amount, submission_time, fe_request.clone()) { // The self.check_request already checked that the fe_request is Some(_) @@ -330,40 +240,139 @@ impl ForcedExitSender { return Ok(()); }; - let txs = self - .build_transactions(&mut storage, fe_request.clone()) - .await?; + let txs = self.build_transactions(fe_request.clone()).await?; let hashes = self - .send_transactions(&mut storage, &fe_request, txs) + .core_interaction_wrapper + .send_and_save_txs_batch(&fe_request, txs) .await?; // We wait only for the first transaction to complete since the transactions // are sent in a batch - self.wait_until_comitted(&mut storage, hashes[0]).await?; - self.set_fulfilled_at(&mut storage, id).await?; + self.wait_until_comitted(hashes[0]).await?; + self.core_interaction_wrapper.set_fulfilled_at(id).await?; Ok(()) } +} +#[cfg(test)] +mod test { + use std::{ + ops::{Add, Mul}, + str::FromStr, + }; - pub async fn process_request(&self, amount: BigUint, submission_time: DateTime) { - let mut attempts: u8 = 0; - // Typically this should not run any longer than 1 iteration - // In case something bad happens we do not want the server crush because - // of the forced_exit_requests component - loop { - let processing_attempt = self - .try_process_request(amount.clone(), submission_time) - .await; + use zksync_config::ForcedExitRequestsConfig; - if processing_attempt.is_ok() { - return; - } else { - attempts += 1; - } + use super::*; + use crate::test::{add_request, MockCoreInteractionWrapper}; - if attempts >= PROCESSING_ATTEMPTS { - vlog::error!("Failed to process forced exit for the {} time", attempts); - } - } + // Just a random number for tests + const TEST_ACCOUNT_FORCED_EXIT_SENDER_ID: u32 = 12; + + fn get_test_forced_exit_sender( + config: Option, + ) -> MempoolForcedExitSender { + let core_interaction_wrapper = MockCoreInteractionWrapper::default(); + + let config = config.unwrap_or_else(ZkSyncConfig::from_env); + + MempoolForcedExitSender::new( + core_interaction_wrapper, + config, + AccountId(TEST_ACCOUNT_FORCED_EXIT_SENDER_ID), + ) + } + + #[tokio::test] + async fn test_forced_exit_sender() { + let day = chrono::Duration::days(1); + + let config = ZkSyncConfig::from_env(); + let forced_exit_requests = ForcedExitRequestsConfig { + // There must be 10 digits in id + digits_in_id: 10, + ..config.forced_exit_requests + }; + let config = ZkSyncConfig { + forced_exit_requests, + ..config + }; + + let forced_exit_sender = get_test_forced_exit_sender(Some(config)); + + add_request( + &forced_exit_sender.core_interaction_wrapper.requests, + ForcedExitRequest { + id: 12, + target: Address::random(), + tokens: vec![TokenId(1)], + price_in_wei: BigUint::from_str("10000000000").unwrap(), + valid_until: Utc::now().add(day), + created_at: Utc::now(), + fulfilled_by: None, + fulfilled_at: None, + }, + ); + + // Not the right amount, because not enough zeroes + forced_exit_sender + .process_request(BigUint::from_str("1000000012").unwrap(), Utc::now()) + .await; + assert_eq!( + forced_exit_sender + .core_interaction_wrapper + .sent_txs + .lock() + .unwrap() + .len(), + 0 + ); + + // Not the right amount, because id is not correct + forced_exit_sender + .process_request(BigUint::from_str("10000000001").unwrap(), Utc::now()) + .await; + assert_eq!( + forced_exit_sender + .core_interaction_wrapper + .sent_txs + .lock() + .unwrap() + .len(), + 0 + ); + + // The tranasction is correct, buuut it is expired + forced_exit_sender + .process_request( + BigUint::from_str("10000000001").unwrap(), + Utc::now().add(day.mul(3)), + ) + .await; + + assert_eq!( + forced_exit_sender + .core_interaction_wrapper + .sent_txs + .lock() + .unwrap() + .len(), + 0 + ); + + // The transaction is correct + forced_exit_sender + .process_request(BigUint::from_str("10000000012").unwrap(), Utc::now()) + .await; + + assert_eq!( + forced_exit_sender + .core_interaction_wrapper + .sent_txs + .lock() + .unwrap() + .len(), + 1 + ); } } diff --git a/core/bin/zksync_forced_exit_requests/src/lib.rs b/core/bin/zksync_forced_exit_requests/src/lib.rs index ae1e3d0bca..1588238edc 100644 --- a/core/bin/zksync_forced_exit_requests/src/lib.rs +++ b/core/bin/zksync_forced_exit_requests/src/lib.rs @@ -6,11 +6,15 @@ use zksync_api::core_api_client::CoreApiClient; use forced_exit_sender::ForcedExitSender; +mod core_interaction_wrapper; pub mod eth_watch; pub mod forced_exit_sender; pub mod prepare_forced_exit_sender; mod utils; +#[cfg(test)] +pub mod test; + #[must_use] pub fn run_forced_exit_requests_actors( pool: ConnectionPool, diff --git a/core/bin/zksync_forced_exit_requests/src/prepare_forced_exit_sender.rs b/core/bin/zksync_forced_exit_requests/src/prepare_forced_exit_sender.rs index cf6114e15c..702481d66d 100644 --- a/core/bin/zksync_forced_exit_requests/src/prepare_forced_exit_sender.rs +++ b/core/bin/zksync_forced_exit_requests/src/prepare_forced_exit_sender.rs @@ -30,7 +30,7 @@ pub async fn prepare_forced_exit_sender_account( connection_pool: ConnectionPool, api_client: CoreApiClient, config: &ZkSyncConfig, -) -> anyhow::Result<()> { +) -> anyhow::Result { let mut storage = connection_pool .access_storage() .await @@ -46,8 +46,8 @@ pub async fn prepare_forced_exit_sender_account( .await .expect("Failed to check if the sender is prepared"); - if is_sender_prepared { - return Ok(()); + if let Some(id) = is_sender_prepared { + return Ok(id); } // The sender is not prepared. This should not ever happen in production, but handling @@ -60,14 +60,14 @@ pub async fn prepare_forced_exit_sender_account( register_signing_key(&mut storage, id, api_client, sender_address, sender_sk).await?; - Ok(()) + Ok(id) } pub async fn check_forced_exit_sender_prepared( storage: &mut StorageProcessor<'_>, sender_sk: &PrivateKey, sender_address: Address, -) -> anyhow::Result { +) -> anyhow::Result> { let mut accounts_schema = storage.chain().account_schema(); let state = accounts_schema @@ -81,9 +81,13 @@ pub async fn check_forced_exit_sender_prepared( let sk_pub_key_hash = PubKeyHash::from_privkey(sender_sk); - Ok(pk_hash == sk_pub_key_hash) + if pk_hash == sk_pub_key_hash { + Ok(Some(account_state.0)) + } else { + Ok(None) + } } - None => Ok(false), + None => Ok(None), } } diff --git a/core/bin/zksync_forced_exit_requests/src/test.rs b/core/bin/zksync_forced_exit_requests/src/test.rs new file mode 100644 index 0000000000..a45a4571b3 --- /dev/null +++ b/core/bin/zksync_forced_exit_requests/src/test.rs @@ -0,0 +1,167 @@ +use std::{ops::Sub, sync::Mutex}; + +use chrono::Utc; +use zksync_storage::chain::operations_ext::records::TxReceiptResponse; +use zksync_types::Nonce; +use zksync_types::{ + forced_exit_requests::{ForcedExitRequest, ForcedExitRequestId}, + tx::TxHash, + AccountId, SignedZkSyncTx, +}; + +use super::core_interaction_wrapper::CoreInteractionWrapper; + +pub struct MockCoreInteractionWrapper { + pub nonce: Nonce, + pub requests: Mutex>, + pub tx_receipt: Option, + pub sent_txs: Mutex>, + // It is easier when keeping track of the deleted txs + pub deleted_requests: Mutex>, +} + +impl Default for MockCoreInteractionWrapper { + fn default() -> Self { + Self { + nonce: Nonce(0), + requests: Mutex::new(vec![]), + tx_receipt: Some(TxReceiptResponse { + // All the values here don't matter except for success = true + tx_hash: String::from("1212"), + block_number: 120, + success: true, + verified: false, + fail_reason: None, + prover_run: None, + }), + sent_txs: Mutex::new(vec![]), + deleted_requests: Mutex::new(vec![]), + } + } +} + +impl MockCoreInteractionWrapper { + fn lock_requests(&self) -> std::sync::MutexGuard<'_, Vec> { + self.requests.lock().expect("Failed to get the write lock") + } + + fn get_request_index_by_id(&self, id: ForcedExitRequestId) -> anyhow::Result { + let lock = self.lock_requests(); + + let index_and_request = (*lock).iter().enumerate().find(|(_, item)| item.id == id); + + let index_option = index_and_request.map(|(index, _)| index); + + index_option.ok_or_else(|| anyhow::Error::msg("Element not found")) + } + + fn lock_sent_txs(&self) -> std::sync::MutexGuard<'_, Vec> { + self.sent_txs.lock().expect("Failed to get the write lock") + } + + fn lock_deleted_requests(&self) -> std::sync::MutexGuard<'_, Vec> { + self.deleted_requests + .lock() + .expect("Failed to allocate deleted requests") + } +} + +#[async_trait::async_trait] +impl CoreInteractionWrapper for MockCoreInteractionWrapper { + async fn get_nonce(&self, _account_id: AccountId) -> anyhow::Result> { + Ok(Some(self.nonce)) + } + async fn get_unconfirmed_requests(&self) -> anyhow::Result> { + let requests = self.lock_requests(); + + let unconfirmed_requests = requests + .iter() + .filter(|r| r.fulfilled_at.is_none()) + .cloned() + .collect(); + + Ok(unconfirmed_requests) + } + async fn set_fulfilled_at(&self, id: i64) -> anyhow::Result<()> { + let index = self.get_request_index_by_id(id)?; + let mut requests = self.lock_requests(); + + requests[index].fulfilled_at = Some(Utc::now()); + + Ok(()) + } + async fn set_fulfilled_by( + &self, + id: ForcedExitRequestId, + value: Option>, + ) -> anyhow::Result<()> { + let index = self.get_request_index_by_id(id)?; + let mut requests = self.lock_requests(); + + requests[index].fulfilled_by = value; + + Ok(()) + } + async fn get_request_by_id(&self, id: i64) -> anyhow::Result> { + let index = self.get_request_index_by_id(id); + + match index { + Ok(i) => { + let requests = self.lock_requests(); + Ok(Some(requests[i].clone())) + } + Err(_) => Ok(None), + } + } + async fn get_receipt(&self, _tx_hash: TxHash) -> anyhow::Result> { + Ok(self.tx_receipt.clone()) + } + async fn send_and_save_txs_batch( + &self, + request: &ForcedExitRequest, + mut txs: Vec, + ) -> anyhow::Result> { + let hashes: Vec = txs.iter().map(|tx| tx.hash()).collect(); + + self.lock_sent_txs().append(&mut txs); + + self.set_fulfilled_by(request.id, Some(hashes.clone())) + .await?; + + Ok(hashes) + } + + async fn get_oldest_unfulfilled_request(&self) -> anyhow::Result> { + let requests = self.lock_requests(); + let unfulfilled_requests = requests.iter().filter(|r| r.fulfilled_by.is_none()); + let oldest = unfulfilled_requests.min_by_key(|req| req.created_at); + + Ok(oldest.cloned()) + } + + async fn delete_old_unfulfilled_requests( + &self, + deleting_threshold: chrono::Duration, + ) -> anyhow::Result<()> { + let mut requests = self.lock_requests(); + let mut deleted_requests = self.lock_deleted_requests(); + + let oldest_allowed = Utc::now().sub(deleting_threshold); + let (mut to_delete, mut to_remain): (Vec<_>, Vec<_>) = requests + .iter() + .cloned() + .partition(|req| req.valid_until < oldest_allowed); + + requests.clear(); + requests.append(&mut to_remain); + + deleted_requests.append(&mut to_delete); + Ok(()) + } +} + +pub fn add_request(requests: &Mutex>, new_request: ForcedExitRequest) { + let mut lock = requests.lock().unwrap(); + + lock.push(new_request); +} diff --git a/core/bin/zksync_forced_exit_requests/src/utils.rs b/core/bin/zksync_forced_exit_requests/src/utils.rs index f414e0a98e..1d45f8b2a7 100644 --- a/core/bin/zksync_forced_exit_requests/src/utils.rs +++ b/core/bin/zksync_forced_exit_requests/src/utils.rs @@ -1,3 +1,7 @@ +use std::{convert::TryInto, ops::Sub}; + +use num::BigUint; +use num::FromPrimitive; use zksync_crypto::ff::PrimeField; pub use zksync_crypto::franklin_crypto::{eddsa::PrivateKey, jubjub::JubjubEngine}; @@ -17,3 +21,65 @@ pub fn read_signing_key(private_key: &[u8]) -> anyhow::Result Fs::from_repr(fs_repr).expect("couldn't read private key from repr"), )) } + +pub fn extract_id_from_amount(amount: BigUint, digits_in_id: u32) -> (i64, BigUint) { + let id_space_size: i64 = 10_i64.pow(digits_in_id); + + let id_space_size = BigUint::from_i64(id_space_size).unwrap(); + + // Taking to the power of 1 and finding mod + // is the only way to find mod of BigUint + let one = BigUint::from_u8(1u8).unwrap(); + let id = amount.modpow(&one, &id_space_size); + + // After extracting the id we need to delete it + // to make sure that amount is the same as in the db + let amount = amount.sub(&id); + + (id.try_into().unwrap(), amount) +} + +#[cfg(test)] +mod test { + use std::ops::Add; + use std::str::FromStr; + + use num::Zero; + + use super::*; + + fn test_extraction_for_id_amount( + amount: BigUint, + digits_in_id: u32, + expected_id: i64, + expected_amount: BigUint, + ) { + let (id, remain_amount) = extract_id_from_amount(amount, digits_in_id); + + assert_eq!(id, expected_id); + assert_eq!(remain_amount, expected_amount); + } + + #[test] + fn test_extract_id_from_amount() { + // Basic extraction + test_extraction_for_id_amount( + BigUint::from_str("12211").unwrap(), + 3, + 211, + BigUint::from_str("12000").unwrap(), + ); + + // Note that there are not enough digits in the sent amount + // Thus the amount should be equal to id + test_extraction_for_id_amount(BigUint::from_str("11").unwrap(), 3, 11, BigUint::zero()); + + // Here we test with some really large number, which could not possible + // fit into 2^64 + let ten = BigUint::from_str("10").unwrap(); + let id: u32 = 211; + let expected_amount = ten.pow(100); + let amount = expected_amount.clone().add(id); + test_extraction_for_id_amount(amount, 3, id.try_into().unwrap(), expected_amount); + } +} From 16c6ad0655436ab5df79a3daaf98d421d3fd3147 Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Sat, 6 Mar 2021 03:14:24 +0200 Subject: [PATCH 66/90] hopefully fix clippy --- core/bin/zksync_forced_exit_requests/src/eth_watch.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/core/bin/zksync_forced_exit_requests/src/eth_watch.rs b/core/bin/zksync_forced_exit_requests/src/eth_watch.rs index 1b7e4bf193..6a043fd9a7 100644 --- a/core/bin/zksync_forced_exit_requests/src/eth_watch.rs +++ b/core/bin/zksync_forced_exit_requests/src/eth_watch.rs @@ -572,18 +572,18 @@ mod test { watcher.poll().await; - let requests_lock = watcher.core_interaction_wrapper.requests.lock().unwrap(); + let requests_lock_deleted = watcher.core_interaction_wrapper.requests.lock().unwrap(); // The old request should have been deleted - assert_eq!(requests_lock.len(), 0); + assert_eq!(requests_lock_deleted.len(), 0); // Need to do this to drop mutex - drop(requests_lock); + drop(requests_lock_deleted); add_request(&watcher.core_interaction_wrapper.requests, old_request); watcher.poll().await; - let requests_lock = watcher.core_interaction_wrapper.requests.lock().unwrap(); + let requests_lock_stored = watcher.core_interaction_wrapper.requests.lock().unwrap(); // Not enough time has passed. The request should not be deleted - assert_eq!(requests_lock.len(), 1); + assert_eq!(requests_lock_stored.len(), 1); } #[tokio::test] From 24a44edf82e1dc998f42c012e0c5beec3e93c04f Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Sat, 6 Mar 2021 23:23:45 +0200 Subject: [PATCH 67/90] fix clippy 2 --- core/bin/zksync_forced_exit_requests/src/eth_watch.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/bin/zksync_forced_exit_requests/src/eth_watch.rs b/core/bin/zksync_forced_exit_requests/src/eth_watch.rs index 6a043fd9a7..c76c0471c1 100644 --- a/core/bin/zksync_forced_exit_requests/src/eth_watch.rs +++ b/core/bin/zksync_forced_exit_requests/src/eth_watch.rs @@ -540,7 +540,10 @@ mod test { chrono::Duration::minutes(5), ) } - + // Unfortunately, I had to forcefully silence clippy due to + // https://github.com/rust-lang/rust-clippy/issues/6446 + // The mutexes are used only in testing, so it does not undermine unit-testing. + #[allow(clippy::await_holding_lock)] #[tokio::test] async fn test_watcher_deleting_old_requests() { let week = chrono::Duration::weeks(1); From 65c48dbd3f7c33a474a4b4633671646453bbc220 Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Mon, 8 Mar 2021 14:34:25 +0200 Subject: [PATCH 68/90] Newline Co-authored-by: Vlad Bochok <41153528+vladbochok@users.noreply.github.com> --- .../zksync_forced_exit_requests/src/core_interaction_wrapper.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/core/bin/zksync_forced_exit_requests/src/core_interaction_wrapper.rs b/core/bin/zksync_forced_exit_requests/src/core_interaction_wrapper.rs index 7eb4438518..5c820c78fb 100644 --- a/core/bin/zksync_forced_exit_requests/src/core_interaction_wrapper.rs +++ b/core/bin/zksync_forced_exit_requests/src/core_interaction_wrapper.rs @@ -36,6 +36,7 @@ pub trait CoreInteractionWrapper { deleting_threshold: chrono::Duration, ) -> anyhow::Result<()>; } + #[derive(Clone)] pub struct MempoolCoreInteractionWrapper { core_api_client: CoreApiClient, From a51162d2bd1f091716696677a5cc80e341fa9d6b Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Mon, 8 Mar 2021 17:00:29 +0200 Subject: [PATCH 69/90] style fixes --- .../src/core_interaction_wrapper.rs | 26 ++++++++++++++----- .../src/eth_watch.rs | 26 ++++++++++++------- .../zksync_forced_exit_requests/src/test.rs | 2 ++ 3 files changed, 38 insertions(+), 16 deletions(-) diff --git a/core/bin/zksync_forced_exit_requests/src/core_interaction_wrapper.rs b/core/bin/zksync_forced_exit_requests/src/core_interaction_wrapper.rs index 5c820c78fb..38d8a97bdb 100644 --- a/core/bin/zksync_forced_exit_requests/src/core_interaction_wrapper.rs +++ b/core/bin/zksync_forced_exit_requests/src/core_interaction_wrapper.rs @@ -68,7 +68,11 @@ impl CoreInteractionWrapper for MempoolCoreInteractionWrapper { async fn get_unconfirmed_requests(&self) -> anyhow::Result> { let mut storage = self.connection_pool.access_storage().await?; let mut forced_exit_requests_schema = storage.forced_exit_requests_schema(); - forced_exit_requests_schema.get_unconfirmed_requests().await + let requests = forced_exit_requests_schema + .get_unconfirmed_requests() + .await?; + + Ok(requests) } async fn set_fulfilled_at(&self, id: i64) -> anyhow::Result<()> { @@ -91,16 +95,20 @@ impl CoreInteractionWrapper for MempoolCoreInteractionWrapper { let mut forced_exit_requests_schema = storage.forced_exit_requests_schema(); forced_exit_requests_schema .set_fulfilled_by(id, value) - .await + .await?; + + Ok(()) } async fn get_receipt(&self, tx_hash: TxHash) -> anyhow::Result> { let mut storage = self.connection_pool.access_storage().await?; - storage + let receipt = storage .chain() .operations_ext_schema() .tx_receipt(tx_hash.as_ref()) - .await + .await?; + + Ok(receipt) } async fn get_request_by_id(&self, id: i64) -> anyhow::Result> { @@ -131,10 +139,12 @@ impl CoreInteractionWrapper for MempoolCoreInteractionWrapper { async fn get_oldest_unfulfilled_request(&self) -> anyhow::Result> { let mut storage = self.connection_pool.access_storage().await?; - storage + let request = storage .forced_exit_requests_schema() .get_oldest_unfulfilled_request() - .await + .await?; + + Ok(request) } async fn delete_old_unfulfilled_requests( @@ -145,6 +155,8 @@ impl CoreInteractionWrapper for MempoolCoreInteractionWrapper { storage .forced_exit_requests_schema() .delete_old_unfulfilled_requests(deleting_threshold) - .await + .await?; + + Ok(()) } } diff --git a/core/bin/zksync_forced_exit_requests/src/eth_watch.rs b/core/bin/zksync_forced_exit_requests/src/eth_watch.rs index c76c0471c1..22e692405b 100644 --- a/core/bin/zksync_forced_exit_requests/src/eth_watch.rs +++ b/core/bin/zksync_forced_exit_requests/src/eth_watch.rs @@ -122,12 +122,17 @@ impl EthClient for EthHttpClient { } } -struct ForcedExitContractWatcher { - core_interaction_wrapper: K, +struct ForcedExitContractWatcher +where + SENDER: ForcedExitSender, + CLIENT: EthClient, + INTERACTOR: CoreInteractionWrapper, +{ + core_interaction_wrapper: INTERACTOR, config: ZkSyncConfig, - eth_client: C, + eth_client: CLIENT, last_viewed_block: u64, - forced_exit_sender: T, + forced_exit_sender: SENDER, mode: WatcherMode, db_cleanup_interval: chrono::Duration, @@ -170,14 +175,17 @@ fn lower_bound_block_time(block: u64, current_block: u64) -> DateTime { Utc::now().sub(time_diff) } -impl - ForcedExitContractWatcher +impl ForcedExitContractWatcher +where + SENDER: ForcedExitSender, + CLIENT: EthClient, + INTERACTOR: CoreInteractionWrapper, { pub fn new( - core_interaction_wrapper: K, + core_interaction_wrapper: INTERACTOR, config: ZkSyncConfig, - eth_client: C, - forced_exit_sender: T, + eth_client: CLIENT, + forced_exit_sender: SENDER, db_cleanup_interval: chrono::Duration, ) -> Self { Self { diff --git a/core/bin/zksync_forced_exit_requests/src/test.rs b/core/bin/zksync_forced_exit_requests/src/test.rs index a45a4571b3..db86459815 100644 --- a/core/bin/zksync_forced_exit_requests/src/test.rs +++ b/core/bin/zksync_forced_exit_requests/src/test.rs @@ -113,9 +113,11 @@ impl CoreInteractionWrapper for MockCoreInteractionWrapper { Err(_) => Ok(None), } } + async fn get_receipt(&self, _tx_hash: TxHash) -> anyhow::Result> { Ok(self.tx_receipt.clone()) } + async fn send_and_save_txs_batch( &self, request: &ForcedExitRequest, From 6ce8feb92da28aed3eb0b921b001564ceaebd306 Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Tue, 9 Mar 2021 09:30:29 +0200 Subject: [PATCH 70/90] Fix case --- .../src/eth_watch.rs | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/core/bin/zksync_forced_exit_requests/src/eth_watch.rs b/core/bin/zksync_forced_exit_requests/src/eth_watch.rs index 22e692405b..d14e6926ea 100644 --- a/core/bin/zksync_forced_exit_requests/src/eth_watch.rs +++ b/core/bin/zksync_forced_exit_requests/src/eth_watch.rs @@ -122,17 +122,17 @@ impl EthClient for EthHttpClient { } } -struct ForcedExitContractWatcher +struct ForcedExitContractWatcher where - SENDER: ForcedExitSender, - CLIENT: EthClient, - INTERACTOR: CoreInteractionWrapper, + Sender: ForcedExitSender, + Client: EthClient, + Interactor: CoreInteractionWrapper, { - core_interaction_wrapper: INTERACTOR, + core_interaction_wrapper: Interactor, config: ZkSyncConfig, - eth_client: CLIENT, + eth_client: Client, last_viewed_block: u64, - forced_exit_sender: SENDER, + forced_exit_sender: Sender, mode: WatcherMode, db_cleanup_interval: chrono::Duration, @@ -175,17 +175,17 @@ fn lower_bound_block_time(block: u64, current_block: u64) -> DateTime { Utc::now().sub(time_diff) } -impl ForcedExitContractWatcher +impl ForcedExitContractWatcher where - SENDER: ForcedExitSender, - CLIENT: EthClient, - INTERACTOR: CoreInteractionWrapper, + Sender: ForcedExitSender, + Client: EthClient, + Interactor: CoreInteractionWrapper, { pub fn new( - core_interaction_wrapper: INTERACTOR, + core_interaction_wrapper: Interactor, config: ZkSyncConfig, - eth_client: CLIENT, - forced_exit_sender: SENDER, + eth_client: Client, + forced_exit_sender: Sender, db_cleanup_interval: chrono::Duration, ) -> Self { Self { From 52a02c534a192cf68d3cb93f95a26a3584bcd21e Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Tue, 9 Mar 2021 22:04:15 +0200 Subject: [PATCH 71/90] Stricter gas for testing --- contracts/contracts/ForcedExit.sol | 33 +++---------------- .../ts-tests/tests/forced-exit-requests.ts | 9 +++-- 2 files changed, 12 insertions(+), 30 deletions(-) diff --git a/contracts/contracts/ForcedExit.sol b/contracts/contracts/ForcedExit.sol index 7a62446213..b4db0bfbbc 100644 --- a/contracts/contracts/ForcedExit.sol +++ b/contracts/contracts/ForcedExit.sol @@ -30,42 +30,19 @@ contract ForcedExit is Ownable, ReentrancyGuard { receiver = _newReceiver; } - function disable() external { - requireMaster(msg.sender); - - enabled = false; - } - - function enable() external { - requireMaster(msg.sender); - - enabled = true; - } - - // Withdraw funds that failed to reach zkSync due to out-of-gas - // We don't require the contract to be enabled to call this function since - // only the master can use it. - function withdrawPendingFunds(address payable _to, uint128 amount) external nonReentrant { - requireMaster(msg.sender); + function withdrawPendingFunds(address payable _to) external nonReentrant { + require(msg.sender == receiver || msg.sender == getMaster(), "1"); // Only the receiver or master can withdraw funds from the smart contract uint256 balance = address(this).balance; - require(amount <= balance, "The balance is lower than the amount"); - - (bool success, ) = _to.call{value: amount}(""); - require(success, "ETH withdraw failed"); + (bool success, ) = _to.call{value: balance}(""); + require(success, "2"); // ETH withdraw failed } // We have to use fallback instead of `receive` since the ethabi // library can't decode the receive function: // https://github.com/rust-ethereum/ethabi/issues/185 - fallback() external payable nonReentrant { - require(enabled, "Contract is disabled"); - require(receiver != address(0), "Receiver must be non-zero"); - - (bool success, ) = receiver.call{value: msg.value}(""); - require(success, "ETH withdraw failed"); - + fallback() external payable { emit FundsReceived(msg.value); } } diff --git a/core/tests/ts-tests/tests/forced-exit-requests.ts b/core/tests/ts-tests/tests/forced-exit-requests.ts index ff48878a91..1b6db58b12 100644 --- a/core/tests/ts-tests/tests/forced-exit-requests.ts +++ b/core/tests/ts-tests/tests/forced-exit-requests.ts @@ -83,8 +83,13 @@ Tester.prototype.testForcedExitRequestMultipleTokens = async function ( const txHandle = await payer.sendTransaction({ value: amountToPay, - gasPrice: gasPrice, - to: contractAddress + gasPrice, + to: contractAddress, + // Even though the standart payment gasLimit is 21k, the gasLimit needed for + // smart contract calls (even simply sending ether) is roughly 32k + // This the restriction that all the ERC-1271 wallets face, so we consider + // safe to assume that the gas limit is at least 32k + gasLimit: BigNumber.from('32000') }); await txHandle.wait(); From 6de60c3ab2588ee1c999423108fdbc9e18fbba1c Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Wed, 17 Mar 2021 21:21:40 +0200 Subject: [PATCH 72/90] Fee seller script for withdrawing forced exit requests fee --- contracts/contracts/ForcedExit.sol | 4 +- contracts/src.ts/deploy.ts | 7 +- etc/env/base/forced_exit_requests.toml | 9 ++ etc/env/base/private.toml | 4 +- .../fee-seller/forced-exit-abi.json | 113 ++++++++++++++++++ infrastructure/fee-seller/index.ts | 7 ++ infrastructure/fee-seller/utils.ts | 16 +++ .../fee-seller/withdraw-forced-exit-fee.ts | 98 +++++++++++++++ infrastructure/zk/src/server.ts | 26 +++- 9 files changed, 274 insertions(+), 10 deletions(-) create mode 100644 infrastructure/fee-seller/forced-exit-abi.json create mode 100644 infrastructure/fee-seller/withdraw-forced-exit-fee.ts diff --git a/contracts/contracts/ForcedExit.sol b/contracts/contracts/ForcedExit.sol index b4db0bfbbc..fce77dac8e 100644 --- a/contracts/contracts/ForcedExit.sol +++ b/contracts/contracts/ForcedExit.sol @@ -15,11 +15,11 @@ contract ForcedExit is Ownable, ReentrancyGuard { bool public enabled = true; - constructor(address _master) Ownable(_master) { + constructor(address _master, address _receiver) Ownable(_master) { initializeReentrancyGuard(); // The master is the default receiver - receiver = payable(_master); + receiver = payable(_receiver); } event FundsReceived(uint256 _amount); diff --git a/contracts/src.ts/deploy.ts b/contracts/src.ts/deploy.ts index 79cf043864..407b371a32 100644 --- a/contracts/src.ts/deploy.ts +++ b/contracts/src.ts/deploy.ts @@ -215,10 +215,15 @@ export class Deployer { if (this.verbose) { console.log('Deploying ForcedExit contract'); } + + // Choose the this.deployWallet.address as the default receiver if the + // FORCED_EXIT_REQUESTS_SENDER_ACCOUNT_ADDRESS is not present + const receiver = process.env.FORCED_EXIT_REQUESTS_SENDER_ACCOUNT_ADDRESS || this.deployWallet.address; + const forcedExitContract = await deployContract( this.deployWallet, this.contracts.forcedExit, - [this.deployWallet.address], + [this.deployWallet.address, receiver], { gasLimit: 6000000, ...ethTxOptions diff --git a/etc/env/base/forced_exit_requests.toml b/etc/env/base/forced_exit_requests.toml index 77b44f590a..f5d5640800 100644 --- a/etc/env/base/forced_exit_requests.toml +++ b/etc/env/base/forced_exit_requests.toml @@ -27,3 +27,12 @@ sender_account_address="0xe1faB3eFD74A77C23B426c302D96372140FF7d0C" # The time after which an invalid request is deleted in milliseconds expiration_period=3000 + +# The minimum amount of ETH in wei that needs to be stored on the forced exit smart contract +# until it is ok to withdraw the funds from it +withdrawal_threshold=1000000000000000000 + +# The address which will receive the fees from ForcedExit automation +# Here it is set for some random account for the purpose of testing, but usually it is preferred +# to set the same account as the one that sends the txs for retrieving the fees from the smart contract +fee_receiver="0x1963917ba0b44A879cf6248387C1d51A0F11669d" \ No newline at end of file diff --git a/etc/env/base/private.toml b/etc/env/base/private.toml index fdac7a30a3..3ed0892a0e 100644 --- a/etc/env/base/private.toml +++ b/etc/env/base/private.toml @@ -30,5 +30,7 @@ secret_auth="sample" fee_account_private_key="0x27593fea79697e947890ecbecce7901b0008345e5d7259710d0dd5e500d040be" [forced_exit_requests] -# L2 private key of the account that sends ForcedExits (unprefixed hex) +# L2 private key of the account that sends ForcedExits sender_private_key="0x0092788f3890ed50dcab7f72fb574a0a9d30b1bc778ba076c609c311a8555352" +# L1 private key of the account that sends ForcedExits +sender_eth_private_key="0x0559b9f000b4e4bbb7fe02e1374cef9623c2ab7c3791204b490e1f229191d104" diff --git a/infrastructure/fee-seller/forced-exit-abi.json b/infrastructure/fee-seller/forced-exit-abi.json new file mode 100644 index 0000000000..03c0f731e3 --- /dev/null +++ b/infrastructure/fee-seller/forced-exit-abi.json @@ -0,0 +1,113 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "_master", + "type": "address" + }, + { + "internalType": "address", + "name": "_receiver", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "FundsReceived", + "type": "event" + }, + { + "stateMutability": "payable", + "type": "fallback" + }, + { + "inputs": [], + "name": "enabled", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getMaster", + "outputs": [ + { + "internalType": "address", + "name": "master", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "receiver", + "outputs": [ + { + "internalType": "address payable", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address payable", + "name": "_newReceiver", + "type": "address" + } + ], + "name": "setReceiver", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_newMaster", + "type": "address" + } + ], + "name": "transferMastership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address payable", + "name": "_to", + "type": "address" + } + ], + "name": "withdrawPendingFunds", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file diff --git a/infrastructure/fee-seller/index.ts b/infrastructure/fee-seller/index.ts index 4dd1973183..17cd65d6b6 100644 --- a/infrastructure/fee-seller/index.ts +++ b/infrastructure/fee-seller/index.ts @@ -27,6 +27,9 @@ import { isOperationFeeAcceptable, sendNotification } from './utils'; +import { + withdrawForcedExitFee +} from './withdraw-forced-exit-fee'; import { EthParameters } from './types'; /** Env parameters. */ @@ -288,6 +291,10 @@ async function sendETH(zksWallet: zksync.Wallet, feeAccumulatorAddress: string, const zksWallet = await zksync.Wallet.fromEthSigner(ethWallet, zksProvider); const ethParameters = new EthParameters(await zksWallet.ethSigner.getTransactionCount('latest')); try { + // First let's try to withdraw the fee gained from the + // forced exit requests functionality, as it does + await withdrawForcedExitFee(ethProvider, ETH_NETWORK); + if (!(await zksWallet.isSigningKeySet())) { console.log('Changing fee account signing key'); const signingKeyTx = await zksWallet.setSigningKey({ feeToken: 'ETH', ethAuthType: 'ECDSA' }); diff --git a/infrastructure/fee-seller/utils.ts b/infrastructure/fee-seller/utils.ts index d015535097..b6e07972ed 100644 --- a/infrastructure/fee-seller/utils.ts +++ b/infrastructure/fee-seller/utils.ts @@ -144,3 +144,19 @@ export async function fmtTokenWithETHValue(zksProvider: zksync.Provider, token, return `${fmtToken(zksProvider, token, amount)}`; } + +export function getZkSyncApiAddress(network: string): string { + const apiDictionary = { + 'localhost': 'http://localhost:3001', + 'stage': 'https://stage-api.zksync.dev', + 'rinkeby': 'https://rinkeby-api.zksync.io', + 'ropsten': 'https://ropsten-api.zksync.io', + 'rinkeby-beta': 'https://rinkeby-beta-api.zksync.io', + 'ropsten-beta': 'https://ropsten-beta-api.zksync.io', + 'mainnet': 'https://api.zksync.io', + 'dev': 'https://dev-api.zksync.dev', + 'breaking': 'https://breaking-api.zksync.dev' + }; + + return apiDictionary[network]; +} diff --git a/infrastructure/fee-seller/withdraw-forced-exit-fee.ts b/infrastructure/fee-seller/withdraw-forced-exit-fee.ts new file mode 100644 index 0000000000..3f96e09975 --- /dev/null +++ b/infrastructure/fee-seller/withdraw-forced-exit-fee.ts @@ -0,0 +1,98 @@ +import { BigNumber, ethers } from 'ethers'; +import { getZkSyncApiAddress } from './utils'; +import fetch from 'node-fetch'; + +const ForcedExitContractAbi = require('./forced-exit-abi.json'); + +const requiredGasLimit = 40000; + +const SENDER_PRIVATE_KEY = process.env.FORCED_EXIT_REQUESTS_SENDER_ETH_PRIVATE_KEY as string; +const WITHDRAWAL_THRESHOLD = process.env.FORCED_EXIT_REQUESTS_WITHDRAWAL_THRESHOLD as string; +const FEE_RECEIVER = process.env.FORCED_EXIT_REQUESTS_FEE_RECEIVER; + +async function shouldWithdrawForcedExitFee( + ethProvider: ethers.providers.Provider, + contractAddress: string, + gasPrice: BigNumber +): Promise { + const costOfGas = gasPrice.mul(requiredGasLimit); + const contractBalance = await ethProvider.getBalance(contractAddress); + + const profit = contractBalance.sub(costOfGas); + const threshold = BigNumber.from(WITHDRAWAL_THRESHOLD); + + return profit.gte(threshold); +} + +// Used to withdraw the fee from the ForcedExit requests feature +export async function withdrawForcedExitFee( + ethProvider: ethers.providers.Provider, + ethNetwork: string +) { + const gasPrice = await ethProvider.getGasPrice(); + const featureStatus = await getStatus(ethNetwork); + + if (featureStatus.status === 'disabled') { + console.log('Forced exit requests feature is disabled'); + // No need to proceed if the feature is disabled + return; + } + + const contractAddress = featureStatus.forced_exit_contract_address; + + const shouldWithdraw = await shouldWithdrawForcedExitFee(ethProvider, contractAddress, gasPrice); + // The algorithm should terminate if there is no point in withdrawing funds + // from the contract + if (!shouldWithdraw) { + console.log('It is not feasible to withdraw Forced Exit requests fee'); + return; + } + + const ethWallet = (new ethers.Wallet(SENDER_PRIVATE_KEY)).connect(ethProvider); + const forcedExitContract = new ethers.Contract( + featureStatus.forced_exit_contract_address, + ForcedExitContractAbi, + ethWallet + ); + + try { + console.log('Withdrawing funds from the forced exit smart contract'); + const tx = await forcedExitContract.withdrawPendingFunds(FEE_RECEIVER, { + gasPrice, + gasLimit: requiredGasLimit + }) as ethers.ContractTransaction; + + const receipt = await tx.wait(); + + console.log('Tx hash:', receipt.transactionHash); + } catch(e) { + console.error('Failed to withdraw funds from the forced exit smart contract: ', e); + // Even though we try to keep the forced exit requests functionality + // as distant from the rest of the code as possible, if the script to withdraw the funds + // fails, we might run into risk of the operator running out of money, so not terminating + // here would be a security issue + process.exit(1); + } finally { + console.log('The process of withdrawing forced exit withdrawal fee is complete.'); + } + +} + +interface StatusResponse { + status: 'enabled' | 'disabled'; + request_fee: string; + max_tokens_per_request: number; + recomended_tx_interval_millis: number; + forced_exit_contract_address: string; +} + +async function getStatus( + network: string +) { + const apiUrl = `${getZkSyncApiAddress(network)}/api/forced_exit_requests/v0.1`; + const endpoint = `${apiUrl}/status`; + + const response = await fetch(endpoint); + + return (await response.json()) as StatusResponse; +} diff --git a/infrastructure/zk/src/server.ts b/infrastructure/zk/src/server.ts index f4c53b1b5f..137b2afa99 100644 --- a/infrastructure/zk/src/server.ts +++ b/infrastructure/zk/src/server.ts @@ -50,23 +50,37 @@ async function prepareForcedExitRequestAccount() { // This is the private key of the first test account const ethProvider = new ethers.providers.JsonRpcProvider(process.env.ETH_CLIENT_WEB3_URL); - const ethRichWallet = new ethers.Wallet('0x7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110'); + const ethRichWallet = new ethers.Wallet( + '0x7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110' + ).connect(ethProvider); + + const gasPrice = await ethProvider.getGasPrice(); + + const topupTransaction = await ethRichWallet.sendTransaction({ + to: forcedExitAccount, + // The amount for deposit should be enough to send at least + // one transaction to retrieve the funds form the forced exit smart contract + value: ethers.utils.parseEther('100.0'), + gasPrice + }); + + await topupTransaction.wait(); const mainZkSyncContract = new ethers.Contract( process.env.CONTRACTS_CONTRACT_ADDR as string, await utils.readZkSyncAbi(), - ethRichWallet.connect(ethProvider) + ethRichWallet ); - const gasPrice = await ethProvider.getGasPrice(); - const ethTransaction = (await mainZkSyncContract.depositETH(forcedExitAccount, { - // The amount to deposit does not really matter + const depositTransaction = (await mainZkSyncContract.depositETH(forcedExitAccount, { + // Here the amount to deposit does not really matter, as it is done purely + // to guarantee that the account exists in the network value: ethers.utils.parseEther('1.0'), gasLimit: ethers.BigNumber.from('200000'), gasPrice })) as ethers.ContractTransaction; - await ethTransaction.wait(); + await depositTransaction.wait(); console.log('Deposit to the forced exit sender account has been successfully completed'); } From 0e8d75255cc8aec322b19371aaea1d8fbe17a5b7 Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Wed, 17 Mar 2021 22:03:34 +0200 Subject: [PATCH 73/90] Style fixed --- Cargo.lock | 1 - core/bin/zksync_forced_exit_requests/Cargo.toml | 2 -- core/bin/zksync_forced_exit_requests/src/utils.rs | 2 +- core/tests/ts-tests/tests/misc.ts | 3 --- 4 files changed, 1 insertion(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 06657712a3..8282f8239f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5984,7 +5984,6 @@ dependencies = [ "async-trait", "chrono", "ethabi", - "franklin-crypto", "hex", "log 0.4.11", "metrics", diff --git a/core/bin/zksync_forced_exit_requests/Cargo.toml b/core/bin/zksync_forced_exit_requests/Cargo.toml index 4a6387247e..305251d5dc 100644 --- a/core/bin/zksync_forced_exit_requests/Cargo.toml +++ b/core/bin/zksync_forced_exit_requests/Cargo.toml @@ -23,8 +23,6 @@ zksync_test_account = { path = "../../tests/test_account", version = "1.0" } vlog = { path = "../../lib/vlog", version = "1.0" } -franklin_crypto = { package = "franklin-crypto", version = "0.0.5", git = "https://github.com/matter-labs/franklin-crypto.git", branch="beta", features = ["multicore", "plonk"]} - zksync_core = { path = "../zksync_core", version = "1.0" } zksync_api = { path = "../zksync_api", version = "1.0" } actix-web = "3.0.0" diff --git a/core/bin/zksync_forced_exit_requests/src/utils.rs b/core/bin/zksync_forced_exit_requests/src/utils.rs index 1d45f8b2a7..3601defcbf 100644 --- a/core/bin/zksync_forced_exit_requests/src/utils.rs +++ b/core/bin/zksync_forced_exit_requests/src/utils.rs @@ -5,7 +5,7 @@ use num::FromPrimitive; use zksync_crypto::ff::PrimeField; pub use zksync_crypto::franklin_crypto::{eddsa::PrivateKey, jubjub::JubjubEngine}; -pub use franklin_crypto::{ +pub use zksync_crypto::franklin_crypto::{ alt_babyjubjub::fs::FsRepr, bellman::{pairing::bn256, PrimeFieldRepr}, }; diff --git a/core/tests/ts-tests/tests/misc.ts b/core/tests/ts-tests/tests/misc.ts index 19273b1772..0e930ebc1b 100644 --- a/core/tests/ts-tests/tests/misc.ts +++ b/core/tests/ts-tests/tests/misc.ts @@ -14,7 +14,6 @@ declare module './tester' { testMultipleBatchSigners(wallets: Wallet[], token: TokenLike, amount: BigNumber): Promise; testMultipleWalletsWrongSignature(from: Wallet, to: Wallet, token: TokenLike, amount: BigNumber): Promise; testBackwardCompatibleEthMessages(from: Wallet, to: Wallet, token: TokenLike, amount: BigNumber): Promise; - testForcedExitRequests(from: Wallet, to: Wallet, token: TokenLike, amount: BigNumber): Promise; } } @@ -237,5 +236,3 @@ Tester.prototype.testBackwardCompatibleEthMessages = async function ( await Promise.all(handles.map((handle) => handle.awaitReceipt())); this.runningFee = this.runningFee.add(totalFee); }; - -Tester.prototype.testForcedExitRequests = async function () {}; From cc75f28e7b50dfa1355267002335f9d095cfe9b1 Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Wed, 17 Mar 2021 22:06:19 +0200 Subject: [PATCH 74/90] fmt --- .../ts-tests/tests/forced-exit-requests.ts | 2 +- infrastructure/fee-seller/index.ts | 6 ++-- infrastructure/fee-seller/utils.ts | 16 +++++------ .../fee-seller/withdraw-forced-exit-fee.ts | 28 ++++++++----------- 4 files changed, 22 insertions(+), 30 deletions(-) diff --git a/core/tests/ts-tests/tests/forced-exit-requests.ts b/core/tests/ts-tests/tests/forced-exit-requests.ts index 1b6db58b12..cf977a262d 100644 --- a/core/tests/ts-tests/tests/forced-exit-requests.ts +++ b/core/tests/ts-tests/tests/forced-exit-requests.ts @@ -85,7 +85,7 @@ Tester.prototype.testForcedExitRequestMultipleTokens = async function ( value: amountToPay, gasPrice, to: contractAddress, - // Even though the standart payment gasLimit is 21k, the gasLimit needed for + // Even though the standart payment gasLimit is 21k, the gasLimit needed for // smart contract calls (even simply sending ether) is roughly 32k // This the restriction that all the ERC-1271 wallets face, so we consider // safe to assume that the gas limit is at least 32k diff --git a/infrastructure/fee-seller/index.ts b/infrastructure/fee-seller/index.ts index 17cd65d6b6..007311403e 100644 --- a/infrastructure/fee-seller/index.ts +++ b/infrastructure/fee-seller/index.ts @@ -27,9 +27,7 @@ import { isOperationFeeAcceptable, sendNotification } from './utils'; -import { - withdrawForcedExitFee -} from './withdraw-forced-exit-fee'; +import { withdrawForcedExitFee } from './withdraw-forced-exit-fee'; import { EthParameters } from './types'; /** Env parameters. */ @@ -292,7 +290,7 @@ async function sendETH(zksWallet: zksync.Wallet, feeAccumulatorAddress: string, const ethParameters = new EthParameters(await zksWallet.ethSigner.getTransactionCount('latest')); try { // First let's try to withdraw the fee gained from the - // forced exit requests functionality, as it does + // forced exit requests functionality, as it does await withdrawForcedExitFee(ethProvider, ETH_NETWORK); if (!(await zksWallet.isSigningKeySet())) { diff --git a/infrastructure/fee-seller/utils.ts b/infrastructure/fee-seller/utils.ts index b6e07972ed..9f18960d18 100644 --- a/infrastructure/fee-seller/utils.ts +++ b/infrastructure/fee-seller/utils.ts @@ -147,16 +147,16 @@ export async function fmtTokenWithETHValue(zksProvider: zksync.Provider, token, export function getZkSyncApiAddress(network: string): string { const apiDictionary = { - 'localhost': 'http://localhost:3001', - 'stage': 'https://stage-api.zksync.dev', - 'rinkeby': 'https://rinkeby-api.zksync.io', - 'ropsten': 'https://ropsten-api.zksync.io', + localhost: 'http://localhost:3001', + stage: 'https://stage-api.zksync.dev', + rinkeby: 'https://rinkeby-api.zksync.io', + ropsten: 'https://ropsten-api.zksync.io', 'rinkeby-beta': 'https://rinkeby-beta-api.zksync.io', 'ropsten-beta': 'https://ropsten-beta-api.zksync.io', - 'mainnet': 'https://api.zksync.io', - 'dev': 'https://dev-api.zksync.dev', - 'breaking': 'https://breaking-api.zksync.dev' + mainnet: 'https://api.zksync.io', + dev: 'https://dev-api.zksync.dev', + breaking: 'https://breaking-api.zksync.dev' }; - + return apiDictionary[network]; } diff --git a/infrastructure/fee-seller/withdraw-forced-exit-fee.ts b/infrastructure/fee-seller/withdraw-forced-exit-fee.ts index 3f96e09975..b6d6c6de4a 100644 --- a/infrastructure/fee-seller/withdraw-forced-exit-fee.ts +++ b/infrastructure/fee-seller/withdraw-forced-exit-fee.ts @@ -1,6 +1,6 @@ import { BigNumber, ethers } from 'ethers'; import { getZkSyncApiAddress } from './utils'; -import fetch from 'node-fetch'; +import fetch from 'node-fetch'; const ForcedExitContractAbi = require('./forced-exit-abi.json'); @@ -17,18 +17,15 @@ async function shouldWithdrawForcedExitFee( ): Promise { const costOfGas = gasPrice.mul(requiredGasLimit); const contractBalance = await ethProvider.getBalance(contractAddress); - + const profit = contractBalance.sub(costOfGas); const threshold = BigNumber.from(WITHDRAWAL_THRESHOLD); return profit.gte(threshold); -} +} // Used to withdraw the fee from the ForcedExit requests feature -export async function withdrawForcedExitFee( - ethProvider: ethers.providers.Provider, - ethNetwork: string -) { +export async function withdrawForcedExitFee(ethProvider: ethers.providers.Provider, ethNetwork: string) { const gasPrice = await ethProvider.getGasPrice(); const featureStatus = await getStatus(ethNetwork); @@ -48,7 +45,7 @@ export async function withdrawForcedExitFee( return; } - const ethWallet = (new ethers.Wallet(SENDER_PRIVATE_KEY)).connect(ethProvider); + const ethWallet = new ethers.Wallet(SENDER_PRIVATE_KEY).connect(ethProvider); const forcedExitContract = new ethers.Contract( featureStatus.forced_exit_contract_address, ForcedExitContractAbi, @@ -57,25 +54,24 @@ export async function withdrawForcedExitFee( try { console.log('Withdrawing funds from the forced exit smart contract'); - const tx = await forcedExitContract.withdrawPendingFunds(FEE_RECEIVER, { + const tx = (await forcedExitContract.withdrawPendingFunds(FEE_RECEIVER, { gasPrice, gasLimit: requiredGasLimit - }) as ethers.ContractTransaction; + })) as ethers.ContractTransaction; const receipt = await tx.wait(); console.log('Tx hash:', receipt.transactionHash); - } catch(e) { + } catch (e) { console.error('Failed to withdraw funds from the forced exit smart contract: ', e); // Even though we try to keep the forced exit requests functionality // as distant from the rest of the code as possible, if the script to withdraw the funds - // fails, we might run into risk of the operator running out of money, so not terminating + // fails, we might run into risk of the operator running out of money, so not terminating // here would be a security issue process.exit(1); } finally { console.log('The process of withdrawing forced exit withdrawal fee is complete.'); - } - + } } interface StatusResponse { @@ -86,9 +82,7 @@ interface StatusResponse { forced_exit_contract_address: string; } -async function getStatus( - network: string -) { +async function getStatus(network: string) { const apiUrl = `${getZkSyncApiAddress(network)}/api/forced_exit_requests/v0.1`; const endpoint = `${apiUrl}/status`; From 493b55f1de5c03f729ef6e8917c9e2eba34115cf Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Wed, 17 Mar 2021 22:44:47 +0200 Subject: [PATCH 75/90] Update config for forced_exit_requests in the server --- .../src/prepare_forced_exit_sender.rs | 39 +++++++------------ .../src/configs/forced_exit_requests.rs | 11 +++++- 2 files changed, 23 insertions(+), 27 deletions(-) diff --git a/core/bin/zksync_forced_exit_requests/src/prepare_forced_exit_sender.rs b/core/bin/zksync_forced_exit_requests/src/prepare_forced_exit_sender.rs index 702481d66d..8afa2163ce 100644 --- a/core/bin/zksync_forced_exit_requests/src/prepare_forced_exit_sender.rs +++ b/core/bin/zksync_forced_exit_requests/src/prepare_forced_exit_sender.rs @@ -7,7 +7,7 @@ use zksync_storage::{ use zksync_api::core_api_client::CoreApiClient; use zksync_types::{ - tx::{ChangePubKeyType, PackedEthSignature, TimeRange, TxHash}, + tx::{ChangePubKeyType, TimeRange, TxHash}, AccountId, Address, PubKeyHash, ZkSyncTx, H256, }; @@ -21,11 +21,6 @@ use zksync_test_account::{ZkSyncAccount, ZkSyncETHAccountData}; use super::utils::{read_signing_key, Engine}; -// This private key is for testing purposes only and shoud not be used in production -// The address should be 0xe1faB3eFD74A77C23B426c302D96372140FF7d0C -const FORCED_EXIT_TEST_SENDER_ETH_PRIVATE_KEY: &str = - "0x0559b9f000b4e4bbb7fe02e1374cef9623c2ab7c3791204b490e1f229191d104"; - pub async fn prepare_forced_exit_sender_account( connection_pool: ConnectionPool, api_client: CoreApiClient, @@ -40,6 +35,7 @@ pub async fn prepare_forced_exit_sender_account( .expect("Failed to decode forced_exit_sender sk"); let sender_sk = read_signing_key(&sender_sk).expect("Failed to read forced exit sender sk"); let sender_address = config.forced_exit_requests.sender_account_address; + let sender_eth_private_key = config.forced_exit_requests.sender_eth_private_key; let is_sender_prepared = check_forced_exit_sender_prepared(&mut storage, &sender_sk, sender_address) @@ -58,7 +54,15 @@ pub async fn prepare_forced_exit_sender_account( .await .expect("Failed to get account id for forced exit sender"); - register_signing_key(&mut storage, id, api_client, sender_address, sender_sk).await?; + register_signing_key( + &mut storage, + id, + api_client, + sender_address, + sender_eth_private_key, + sender_sk, + ) + .await?; Ok(id) } @@ -163,33 +167,16 @@ pub async fn wait_for_change_pub_key_tx( } } -// Use PackedEthSignature::address_from_private_key -async fn get_verified_eth_sk(sender_address: Address) -> H256 { - let eth_sk = hex::decode(&FORCED_EXIT_TEST_SENDER_ETH_PRIVATE_KEY[2..]) - .expect("Failed to parse eth signing key of the forced exit account"); - - let private_key = H256::from_slice(ð_sk); - - let pk_address = PackedEthSignature::address_from_private_key(&private_key).unwrap(); - - if pk_address != sender_address { - panic!("Private key provided does not correspond to the sender address"); - } - - private_key -} - pub async fn register_signing_key( storage: &mut StorageProcessor<'_>, sender_id: AccountId, api_client: CoreApiClient, sender_address: Address, + sender_eth_private_key: H256, sender_sk: PrivateKey, ) -> anyhow::Result<()> { - let eth_sk = get_verified_eth_sk(sender_address).await; - let eth_account_data = ZkSyncETHAccountData::EOA { - eth_private_key: eth_sk, + eth_private_key: sender_eth_private_key, }; let sender_account = ZkSyncAccount::new( diff --git a/core/lib/config/src/configs/forced_exit_requests.rs b/core/lib/config/src/configs/forced_exit_requests.rs index 4fafcdded7..d70e05f82c 100644 --- a/core/lib/config/src/configs/forced_exit_requests.rs +++ b/core/lib/config/src/configs/forced_exit_requests.rs @@ -1,7 +1,7 @@ use crate::envy_load; /// External uses use serde::Deserialize; -use zksync_types::Address; +use zksync_types::{Address, H256, U256}; // There are two types of configs: // The original one (with tx_interval_scaling_factor) @@ -21,8 +21,11 @@ struct ForcedExitRequestsInternalConfig { pub digits_in_id: u8, pub wait_confirmations: u64, pub sender_private_key: String, + pub sender_eth_private_key: H256, pub sender_account_address: Address, pub expiration_period: u64, + pub withdrawal_threshold: u64, + pub fee_receiver: Address, } #[derive(Debug, Deserialize, Clone, PartialEq)] @@ -35,8 +38,11 @@ pub struct ForcedExitRequestsConfig { pub digits_in_id: u8, pub wait_confirmations: u64, pub sender_private_key: String, + pub sender_eth_private_key: H256, pub sender_account_address: Address, pub expiration_period: u64, + pub withdrawal_threshold: U256, + pub fee_receiver: Address, } // Checks that in no way the price will overlap with the requests id space @@ -74,8 +80,11 @@ impl ForcedExitRequestsConfig { price_per_token: config.price_per_token, wait_confirmations: config.wait_confirmations, sender_private_key: config.sender_private_key, + sender_eth_private_key: config.sender_eth_private_key, sender_account_address: config.sender_account_address, expiration_period: config.expiration_period, + withdrawal_threshold: U256::from(config.withdrawal_threshold), + fee_receiver: config.fee_receiver, } } } From 669245098495d50f8a8211f0c2b3408e4526f46e Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Wed, 17 Mar 2021 23:11:23 +0200 Subject: [PATCH 76/90] style fixes --- contracts/contracts/ForcedExit.sol | 7 +++++-- core/lib/config/src/configs/forced_exit_requests.rs | 8 +------- etc/env/base/forced_exit_requests.toml | 2 +- infrastructure/fee-seller/forced-exit-abi.json | 2 +- infrastructure/fee-seller/withdraw-forced-exit-fee.ts | 11 ++++------- 5 files changed, 12 insertions(+), 18 deletions(-) diff --git a/contracts/contracts/ForcedExit.sol b/contracts/contracts/ForcedExit.sol index fce77dac8e..af62d2e5d9 100644 --- a/contracts/contracts/ForcedExit.sol +++ b/contracts/contracts/ForcedExit.sol @@ -31,12 +31,15 @@ contract ForcedExit is Ownable, ReentrancyGuard { } function withdrawPendingFunds(address payable _to) external nonReentrant { - require(msg.sender == receiver || msg.sender == getMaster(), "1"); // Only the receiver or master can withdraw funds from the smart contract + require( + msg.sender == receiver || msg.sender == getMaster(), + "Only the receiver or master can withdraw funds from the smart contract" + ); uint256 balance = address(this).balance; (bool success, ) = _to.call{value: balance}(""); - require(success, "2"); // ETH withdraw failed + require(success, "ETH withdraw failed"); } // We have to use fallback instead of `receive` since the ethabi diff --git a/core/lib/config/src/configs/forced_exit_requests.rs b/core/lib/config/src/configs/forced_exit_requests.rs index d70e05f82c..8a772c3ac9 100644 --- a/core/lib/config/src/configs/forced_exit_requests.rs +++ b/core/lib/config/src/configs/forced_exit_requests.rs @@ -1,7 +1,7 @@ use crate::envy_load; /// External uses use serde::Deserialize; -use zksync_types::{Address, H256, U256}; +use zksync_types::{Address, H256}; // There are two types of configs: // The original one (with tx_interval_scaling_factor) @@ -24,8 +24,6 @@ struct ForcedExitRequestsInternalConfig { pub sender_eth_private_key: H256, pub sender_account_address: Address, pub expiration_period: u64, - pub withdrawal_threshold: u64, - pub fee_receiver: Address, } #[derive(Debug, Deserialize, Clone, PartialEq)] @@ -41,8 +39,6 @@ pub struct ForcedExitRequestsConfig { pub sender_eth_private_key: H256, pub sender_account_address: Address, pub expiration_period: u64, - pub withdrawal_threshold: U256, - pub fee_receiver: Address, } // Checks that in no way the price will overlap with the requests id space @@ -83,8 +79,6 @@ impl ForcedExitRequestsConfig { sender_eth_private_key: config.sender_eth_private_key, sender_account_address: config.sender_account_address, expiration_period: config.expiration_period, - withdrawal_threshold: U256::from(config.withdrawal_threshold), - fee_receiver: config.fee_receiver, } } } diff --git a/etc/env/base/forced_exit_requests.toml b/etc/env/base/forced_exit_requests.toml index f5d5640800..431f2287d5 100644 --- a/etc/env/base/forced_exit_requests.toml +++ b/etc/env/base/forced_exit_requests.toml @@ -35,4 +35,4 @@ withdrawal_threshold=1000000000000000000 # The address which will receive the fees from ForcedExit automation # Here it is set for some random account for the purpose of testing, but usually it is preferred # to set the same account as the one that sends the txs for retrieving the fees from the smart contract -fee_receiver="0x1963917ba0b44A879cf6248387C1d51A0F11669d" \ No newline at end of file +fee_receiver="0x1963917ba0b44A879cf6248387C1d51A0F11669d" diff --git a/infrastructure/fee-seller/forced-exit-abi.json b/infrastructure/fee-seller/forced-exit-abi.json index 03c0f731e3..c90931dd4e 100644 --- a/infrastructure/fee-seller/forced-exit-abi.json +++ b/infrastructure/fee-seller/forced-exit-abi.json @@ -110,4 +110,4 @@ "stateMutability": "nonpayable", "type": "function" } -] \ No newline at end of file +] diff --git a/infrastructure/fee-seller/withdraw-forced-exit-fee.ts b/infrastructure/fee-seller/withdraw-forced-exit-fee.ts index b6d6c6de4a..7c0a86be38 100644 --- a/infrastructure/fee-seller/withdraw-forced-exit-fee.ts +++ b/infrastructure/fee-seller/withdraw-forced-exit-fee.ts @@ -6,8 +6,8 @@ const ForcedExitContractAbi = require('./forced-exit-abi.json'); const requiredGasLimit = 40000; -const SENDER_PRIVATE_KEY = process.env.FORCED_EXIT_REQUESTS_SENDER_ETH_PRIVATE_KEY as string; -const WITHDRAWAL_THRESHOLD = process.env.FORCED_EXIT_REQUESTS_WITHDRAWAL_THRESHOLD as string; +const SENDER_PRIVATE_KEY = process.env.FORCED_EXIT_REQUESTS_SENDER_ETH_PRIVATE_KEY; +const WITHDRAWAL_THRESHOLD = process.env.FORCED_EXIT_REQUESTS_WITHDRAWAL_THRESHOLD; const FEE_RECEIVER = process.env.FORCED_EXIT_REQUESTS_FEE_RECEIVER; async function shouldWithdrawForcedExitFee( @@ -24,22 +24,19 @@ async function shouldWithdrawForcedExitFee( return profit.gte(threshold); } -// Used to withdraw the fee from the ForcedExit requests feature +// Withdraws the fee from the ForcedExit requests feature export async function withdrawForcedExitFee(ethProvider: ethers.providers.Provider, ethNetwork: string) { const gasPrice = await ethProvider.getGasPrice(); const featureStatus = await getStatus(ethNetwork); if (featureStatus.status === 'disabled') { console.log('Forced exit requests feature is disabled'); - // No need to proceed if the feature is disabled return; } const contractAddress = featureStatus.forced_exit_contract_address; - const shouldWithdraw = await shouldWithdrawForcedExitFee(ethProvider, contractAddress, gasPrice); - // The algorithm should terminate if there is no point in withdrawing funds - // from the contract + if (!shouldWithdraw) { console.log('It is not feasible to withdraw Forced Exit requests fee'); return; From 845ed134212a4f6ff0832aa838a56427747b76aa Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Thu, 18 Mar 2021 08:11:36 +0200 Subject: [PATCH 77/90] Fix tests for ForcedExit smart contract --- contracts/test/unit_tests/forced_exit_test.ts | 88 ++++--------------- 1 file changed, 19 insertions(+), 69 deletions(-) diff --git a/contracts/test/unit_tests/forced_exit_test.ts b/contracts/test/unit_tests/forced_exit_test.ts index 4cfea0256f..54757ae827 100644 --- a/contracts/test/unit_tests/forced_exit_test.ts +++ b/contracts/test/unit_tests/forced_exit_test.ts @@ -18,89 +18,39 @@ describe('ForcedExit unit tests', function () { let wallet1: Signer; let wallet2: Signer; let wallet3: Signer; + let wallet4: Signer; before(async () => { - [wallet1, wallet2, wallet3] = await hardhat.ethers.getSigners(); + [wallet1, wallet2, wallet3, wallet4] = await hardhat.ethers.getSigners(); const forcedExitContractFactory = await hardhat.ethers.getContractFactory('ForcedExit'); - const contract = await forcedExitContractFactory.deploy(wallet1.getAddress()); - forcedExitContract = ForcedExitFactory.connect(contract.address, wallet1); + const contract = await forcedExitContractFactory.deploy(wallet1.getAddress(), wallet2.getAddress()); + // Connecting the wallet to a potential receiver, who can withdraw the funds + // on the master's behalf + forcedExitContract = ForcedExitFactory.connect(contract.address, wallet2); }); - it('Check redirecting funds to receiver', async () => { - // The test checks that when users send funds to the contract - // the funds will be redirected to the receiver address that is set - // by the master of the ForcedExit contract + it('Check withdrawing fees', async () => { + // The test checks the ability to withdraw the funds from the contract + // after the user has sent them - // Setting receiver who will should get all the funds sent - // to the contract - await forcedExitContract.setReceiver(await wallet3.getAddress()); - - // Could not use nested expects because + // Code style note: Could not use nested expects because // changeEtherBalance does not allow it - // User sends tranasctions - const txHandle = await wallet2.sendTransaction({ + // User sends funds to the contract + const transferTxHandle = await wallet3.sendTransaction({ to: forcedExitContract.address, value: TX_AMOUNT }); // Check that the `FundsReceived` event was emitted - expect(txHandle).to.emit(forcedExitContract, 'FundsReceived').withArgs(TX_AMOUNT); - - // The receiver received the balance - expect(txHandle).to.changeEtherBalance(wallet3, TX_AMOUNT); - }); - - it('Check receiving pending funds', async () => { - // The test checks that it is possible for the master of the contract - // to withdraw funds that got stuck on the contract for some unknown reason. - // One example is when another contract does selfdestruct and submits funds - // to the ForcedExit contract. - - // Create the contract which will self-destruct itself - const selfDestructContractFactory = await hardhat.ethers.getContractFactory('SelfDestruct'); - const contractDeployed = await selfDestructContractFactory.deploy(); - const selfDestructContract = SelfDestructFactory.connect(contractDeployed.address, contractDeployed.signer); - - // Supplying funds to the self-desctruct contract - await wallet2.sendTransaction({ - to: selfDestructContract.address, - value: TX_AMOUNT - }); - - // Destroying the self-destruct contract which sends TX_AMOUNT ether to the ForcedExit - // contract which were not redirected to the receiver - await selfDestructContract.connect(wallet2).destroy(forcedExitContract.address); - - // The master withdraws the funds and they should arrive to him - expect( - await forcedExitContract.withdrawPendingFunds(await wallet1.getAddress(), TX_AMOUNT) - ).to.changeEtherBalance(wallet1, TX_AMOUNT); - }); - - it('Check disabling and enabling', async () => { - // The test checks that disabling and enabling of the ForcedExit contract works. - - // Disabling transfers to the contract - await forcedExitContract.disable(); - - // The contract is disabled. Thus, transfering to it should fail - expect( - wallet2.sendTransaction({ - to: forcedExitContract.address, - value: TX_AMOUNT - }) - ).to.be.reverted; + expect(transferTxHandle).to.emit(forcedExitContract, 'FundsReceived').withArgs(TX_AMOUNT); - // Enabling transfers to the contract - await forcedExitContract.enable(); + // Withdrawing the funds from the contract to the wallet4 + const withdrawTxHandle = await forcedExitContract.withdrawPendingFunds( + await wallet4.getAddress() + ); - // The contract is enabled. Thus, transfering to it should not fail - expect( - wallet2.sendTransaction({ - to: forcedExitContract.address, - value: TX_AMOUNT - }) - ).to.not.be.reverted; + // The pending funds have been received + expect(withdrawTxHandle).to.changeEtherBalance(wallet4, TX_AMOUNT); }); }); From 903a0696558decc284572da9c8d63f271f04af8c Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Thu, 18 Mar 2021 09:32:44 +0200 Subject: [PATCH 78/90] fmt --- contracts/test/unit_tests/forced_exit_test.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/contracts/test/unit_tests/forced_exit_test.ts b/contracts/test/unit_tests/forced_exit_test.ts index 54757ae827..eaaf40ff95 100644 --- a/contracts/test/unit_tests/forced_exit_test.ts +++ b/contracts/test/unit_tests/forced_exit_test.ts @@ -3,7 +3,6 @@ import { solidity } from 'ethereum-waffle'; import { Signer, utils } from 'ethers'; import { ForcedExit } from '../../typechain/ForcedExit'; import { ForcedExitFactory } from '../../typechain/ForcedExitFactory'; -import { SelfDestructFactory } from '../../typechain/SelfDestructFactory'; import * as hardhat from 'hardhat'; @@ -25,7 +24,7 @@ describe('ForcedExit unit tests', function () { const forcedExitContractFactory = await hardhat.ethers.getContractFactory('ForcedExit'); const contract = await forcedExitContractFactory.deploy(wallet1.getAddress(), wallet2.getAddress()); - // Connecting the wallet to a potential receiver, who can withdraw the funds + // Connecting the wallet to a potential receiver, who can withdraw the funds // on the master's behalf forcedExitContract = ForcedExitFactory.connect(contract.address, wallet2); }); @@ -46,9 +45,7 @@ describe('ForcedExit unit tests', function () { expect(transferTxHandle).to.emit(forcedExitContract, 'FundsReceived').withArgs(TX_AMOUNT); // Withdrawing the funds from the contract to the wallet4 - const withdrawTxHandle = await forcedExitContract.withdrawPendingFunds( - await wallet4.getAddress() - ); + const withdrawTxHandle = await forcedExitContract.withdrawPendingFunds(await wallet4.getAddress()); // The pending funds have been received expect(withdrawTxHandle).to.changeEtherBalance(wallet4, TX_AMOUNT); From c3d7157cd958ad39cb7d4fee29b9d114a7f2c0e6 Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Thu, 18 Mar 2021 12:19:32 +0200 Subject: [PATCH 79/90] camelCase everywhere in the ForcedExit api --- .../api_client/src/rest/forced_exit_requests/mod.rs | 1 + core/tests/ts-tests/tests/forced-exit-requests.ts | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/core/lib/api_client/src/rest/forced_exit_requests/mod.rs b/core/lib/api_client/src/rest/forced_exit_requests/mod.rs index 94a6553278..9d0bb71150 100644 --- a/core/lib/api_client/src/rest/forced_exit_requests/mod.rs +++ b/core/lib/api_client/src/rest/forced_exit_requests/mod.rs @@ -17,6 +17,7 @@ use crate::rest::v1::ClientResult; // Data transfer objects. #[derive(Serialize, Deserialize, PartialEq, Debug)] +#[serde(rename_all = "camelCase")] pub struct ConfigInfo { #[serde(with = "BigUintSerdeAsRadix10Str")] pub request_fee: BigUint, diff --git a/core/tests/ts-tests/tests/forced-exit-requests.ts b/core/tests/ts-tests/tests/forced-exit-requests.ts index ff48878a91..defafc4c06 100644 --- a/core/tests/ts-tests/tests/forced-exit-requests.ts +++ b/core/tests/ts-tests/tests/forced-exit-requests.ts @@ -31,10 +31,10 @@ declare module './tester' { interface StatusResponse { status: 'enabled' | 'disabled'; - request_fee: string; - max_tokens_per_request: number; - recomended_tx_interval_millis: number; - forced_exit_contract_address: Address; + requestFee: string; + maxTokensPerRequest: number; + recomendedTxIntervalMillis: number; + forcedExitContractAddress: Address; } Tester.prototype.testForcedExitRequestMultipleTokens = async function ( @@ -72,10 +72,10 @@ Tester.prototype.testForcedExitRequestMultipleTokens = async function ( const tokenIds = tokens.map((token) => this.syncProvider.tokenSet.resolveTokenId(token)); - const requestPrice = BigNumber.from(status.request_fee).mul(tokens.length); + const requestPrice = BigNumber.from(status.requestFee).mul(tokens.length); const request = await submitRequest(to, tokenIds, requestPrice.toString()); - const contractAddress = status.forced_exit_contract_address; + const contractAddress = status.forcedExitContractAddress; const amountToPay = requestPrice.add(BigNumber.from(request.id)); From 4bc8d56238657335119623f6c70f3326d48b02df Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Thu, 18 Mar 2021 12:30:29 +0200 Subject: [PATCH 80/90] Update for camelCase in the forced exit api --- .../fee-seller/withdraw-forced-exit-fee.ts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/infrastructure/fee-seller/withdraw-forced-exit-fee.ts b/infrastructure/fee-seller/withdraw-forced-exit-fee.ts index 7c0a86be38..feacc4a6c9 100644 --- a/infrastructure/fee-seller/withdraw-forced-exit-fee.ts +++ b/infrastructure/fee-seller/withdraw-forced-exit-fee.ts @@ -34,7 +34,7 @@ export async function withdrawForcedExitFee(ethProvider: ethers.providers.Provid return; } - const contractAddress = featureStatus.forced_exit_contract_address; + const contractAddress = featureStatus.forcedExitContractAddress; const shouldWithdraw = await shouldWithdrawForcedExitFee(ethProvider, contractAddress, gasPrice); if (!shouldWithdraw) { @@ -43,11 +43,7 @@ export async function withdrawForcedExitFee(ethProvider: ethers.providers.Provid } const ethWallet = new ethers.Wallet(SENDER_PRIVATE_KEY).connect(ethProvider); - const forcedExitContract = new ethers.Contract( - featureStatus.forced_exit_contract_address, - ForcedExitContractAbi, - ethWallet - ); + const forcedExitContract = new ethers.Contract(contractAddress, ForcedExitContractAbi, ethWallet); try { console.log('Withdrawing funds from the forced exit smart contract'); @@ -73,10 +69,10 @@ export async function withdrawForcedExitFee(ethProvider: ethers.providers.Provid interface StatusResponse { status: 'enabled' | 'disabled'; - request_fee: string; - max_tokens_per_request: number; - recomended_tx_interval_millis: number; - forced_exit_contract_address: string; + requestFee: string; + maxTokensPerRequest: number; + recomendedTxIntervalMillis: number; + forcedExitContractAddress: string; } async function getStatus(network: string) { From 91e0c0080d1e6867c5b3b77d2285f79577bbb2e1 Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Thu, 18 Mar 2021 13:56:03 +0200 Subject: [PATCH 81/90] Minor fixes --- core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs b/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs index b1eda16f62..8d1f99e8f8 100644 --- a/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs +++ b/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs @@ -42,7 +42,6 @@ impl ForcedExitSender for MempoolForced // In case something bad happens we do not want the server crush because // of the forced_exit_requests component loop { - dbg!("try processing 1"); let processing_attempt = self .try_process_request(amount.clone(), submission_time) .await; @@ -193,7 +192,7 @@ impl MempoolForcedExitSender { pub async fn wait_until_comitted(&self, tx_hash: TxHash) -> anyhow::Result<()> { let timeout_millis: u64 = 120000; let poll_interval_millis: u64 = 200; - let poll_interval = time::Duration::from_secs(poll_interval_millis); + let poll_interval = time::Duration::from_millis(poll_interval_millis); let mut timer = time::interval(poll_interval); let mut time_passed: u64 = 0; From 9a8e62a654dfeadb8bdf77be7bfd954ab9ee7c8c Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Thu, 18 Mar 2021 18:39:36 +0200 Subject: [PATCH 82/90] Endpoint for checking account before forced exit --- .../rest/forced_exit_requests/v01.rs | 22 +++++++++++++++++++ core/lib/types/src/forced_exit_requests.rs | 1 + 2 files changed, 23 insertions(+) diff --git a/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs b/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs index 92992d4b18..082f1d9fd7 100644 --- a/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs +++ b/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs @@ -193,6 +193,27 @@ pub async fn get_request_by_id( } } +// Checks if the account is eligible for forced_exit in terms of +// existing enough time +pub async fn check_account( + data: web::Data, + web::Path(account): web::Path
, +) -> JsonResult<()> { + let mut storage = data + .connection_pool + .access_storage() + .await + .map_err(warn_err) + .map_err(ApiError::internal)?; + + data.forced_exit_checker + .check_forced_exit(&mut storage, account) + .await + .map_err(ApiError::from)?; + + Ok(Json(())) +} + pub fn api_scope( connection_pool: ConnectionPool, config: &ZkSyncConfig, @@ -209,6 +230,7 @@ pub fn api_scope( scope .route("/submit", web::post().to(submit_request)) .route("/requests/{id}", web::get().to(get_request_by_id)) + .route("/check_account", web::post().to(check_account)) } else { scope } diff --git a/core/lib/types/src/forced_exit_requests.rs b/core/lib/types/src/forced_exit_requests.rs index 30332ce61e..2fa5288c7a 100644 --- a/core/lib/types/src/forced_exit_requests.rs +++ b/core/lib/types/src/forced_exit_requests.rs @@ -37,6 +37,7 @@ pub struct SaveForcedExitRequestQuery { pub created_at: DateTime, pub valid_until: DateTime, } + #[derive(Debug, Clone)] pub struct FundsReceivedEvent { pub amount: BigUint, From ef637f9c7bdd8f26155014c86e49a2026545f721 Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Thu, 18 Mar 2021 18:41:59 +0200 Subject: [PATCH 83/90] Minor improvements --- .../zksync_api/src/api_server/rest/forced_exit_requests/v01.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs b/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs index 082f1d9fd7..9997c75971 100644 --- a/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs +++ b/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs @@ -230,7 +230,7 @@ pub fn api_scope( scope .route("/submit", web::post().to(submit_request)) .route("/requests/{id}", web::get().to(get_request_by_id)) - .route("/check_account", web::post().to(check_account)) + .route("/check_account/{account}", web::get().to(check_account)) } else { scope } From 9c72ef8dc3fd96b451d2e74a2bd2a81b6a094dba Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Fri, 19 Mar 2021 12:03:14 +0200 Subject: [PATCH 84/90] Make the forced exit eligibility endpoint more presentful --- .../src/api_server/forced_exit_checker.rs | 48 +++++++++++++++---- .../rest/forced_exit_requests/v01.rs | 25 +++++++--- .../zksync_api/src/api_server/tx_sender.rs | 2 +- core/lib/types/src/forced_exit_requests.rs | 5 ++ 4 files changed, 62 insertions(+), 18 deletions(-) diff --git a/core/bin/zksync_api/src/api_server/forced_exit_checker.rs b/core/bin/zksync_api/src/api_server/forced_exit_checker.rs index 0815d6c5e2..e33cbc1741 100644 --- a/core/bin/zksync_api/src/api_server/forced_exit_checker.rs +++ b/core/bin/zksync_api/src/api_server/forced_exit_checker.rs @@ -13,6 +13,12 @@ pub trait ForcedExitAccountAgeChecker { &self, storage: &mut StorageProcessor<'a>, target_account_address: Address, + ) -> Result; + + async fn validate_forced_exit<'a>( + &self, + storage: &mut StorageProcessor<'a>, + target_account_address: Address, ) -> Result<(), SubmitError>; } @@ -40,7 +46,7 @@ impl ForcedExitAccountAgeChecker for ForcedExitChecker { &self, storage: &mut StorageProcessor<'a>, target_account_address: Address, - ) -> Result<(), SubmitError> { + ) -> Result { let account_age = storage .chain() .operations_ext_schema() @@ -49,17 +55,31 @@ impl ForcedExitAccountAgeChecker for ForcedExitChecker { .map_err(|err| internal_error!(err, target_account_address))?; match account_age { - Some(age) if Utc::now() - age < self.forced_exit_minimum_account_age => { - let msg = format!( - "Target account exists less than required minimum amount ({} hours)", - self.forced_exit_minimum_account_age.num_hours() - ); - - Err(SubmitError::InvalidParams(msg)) - } + Some(age) if Utc::now() - age < self.forced_exit_minimum_account_age => Ok(false), None => Err(SubmitError::invalid_params("Target account does not exist")), - Some(..) => Ok(()), + Some(..) => Ok(true), + } + } + + async fn validate_forced_exit<'a>( + &self, + storage: &mut StorageProcessor<'a>, + target_account_address: Address, + ) -> Result<(), SubmitError> { + let result = self + .check_forced_exit(storage, target_account_address) + .await?; + + if result { + Ok(()) + } else { + let msg = format!( + "Target account exists less than required minimum amount ({} hours)", + self.forced_exit_minimum_account_age.num_hours() + ); + + Err(SubmitError::InvalidParams(msg)) } } } @@ -72,6 +92,14 @@ impl ForcedExitAccountAgeChecker for DummyForcedExitChecker { &self, _storage: &mut StorageProcessor<'a>, _target_account_address: Address, + ) -> Result { + Ok(true) + } + + async fn validate_forced_exit<'a>( + &self, + _storage: &mut StorageProcessor<'a>, + _target_account_address: Address, ) -> Result<(), SubmitError> { Ok(()) } diff --git a/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs b/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs index 9997c75971..b31653a1d1 100644 --- a/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs +++ b/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs @@ -25,7 +25,10 @@ use zksync_api_client::rest::forced_exit_requests::ConfigInfo; use zksync_config::ZkSyncConfig; use zksync_storage::ConnectionPool; use zksync_types::{ - forced_exit_requests::{ForcedExitRequest, ForcedExitRequestId, SaveForcedExitRequestQuery}, + forced_exit_requests::{ + ForcedExitEligibilityResponse, ForcedExitRequest, ForcedExitRequestId, + SaveForcedExitRequestQuery, + }, Address, TokenLike, }; @@ -109,7 +112,7 @@ pub async fn submit_request( } data.forced_exit_checker - .check_forced_exit(&mut storage, params.target) + .validate_forced_exit(&mut storage, params.target) .await .map_err(ApiError::from)?; @@ -195,10 +198,10 @@ pub async fn get_request_by_id( // Checks if the account is eligible for forced_exit in terms of // existing enough time -pub async fn check_account( +pub async fn check_account_eligibility( data: web::Data, web::Path(account): web::Path
, -) -> JsonResult<()> { +) -> JsonResult { let mut storage = data .connection_pool .access_storage() @@ -206,12 +209,17 @@ pub async fn check_account( .map_err(warn_err) .map_err(ApiError::internal)?; - data.forced_exit_checker + let is_eligible = data + .forced_exit_checker .check_forced_exit(&mut storage, account) .await .map_err(ApiError::from)?; - Ok(Json(())) + let result = ForcedExitEligibilityResponse { + eligible: is_eligible, + }; + + Ok(Json(result)) } pub fn api_scope( @@ -230,7 +238,10 @@ pub fn api_scope( scope .route("/submit", web::post().to(submit_request)) .route("/requests/{id}", web::get().to(get_request_by_id)) - .route("/check_account/{account}", web::get().to(check_account)) + .route( + "/checks/eligibility/{account}", + web::get().to(check_account_eligibility), + ) } else { scope } diff --git a/core/bin/zksync_api/src/api_server/tx_sender.rs b/core/bin/zksync_api/src/api_server/tx_sender.rs index 20db319837..4847156057 100644 --- a/core/bin/zksync_api/src/api_server/tx_sender.rs +++ b/core/bin/zksync_api/src/api_server/tx_sender.rs @@ -491,7 +491,7 @@ impl TxSender { .map_err(SubmitError::internal)?; self.forced_exit_checker - .check_forced_exit(&mut storage, forced_exit.target) + .validate_forced_exit(&mut storage, forced_exit.target) .await } diff --git a/core/lib/types/src/forced_exit_requests.rs b/core/lib/types/src/forced_exit_requests.rs index 2fa5288c7a..15a85d4b10 100644 --- a/core/lib/types/src/forced_exit_requests.rs +++ b/core/lib/types/src/forced_exit_requests.rs @@ -44,6 +44,11 @@ pub struct FundsReceivedEvent { pub block_number: u64, } +#[derive(Serialize, Deserialize)] +pub struct ForcedExitEligibilityResponse { + pub eligible: bool, +} + impl TryFrom for FundsReceivedEvent { type Error = anyhow::Error; From 174830aad62a2f46db77db33d4b0470817a61cc1 Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Fri, 19 Mar 2021 12:39:35 +0200 Subject: [PATCH 85/90] Make the forced exit fee withdrawal the last fee-seller operation --- infrastructure/fee-seller/index.ts | 12 +++++--- .../fee-seller/withdraw-forced-exit-fee.ts | 29 ++++++------------- 2 files changed, 17 insertions(+), 24 deletions(-) diff --git a/infrastructure/fee-seller/index.ts b/infrastructure/fee-seller/index.ts index 007311403e..677e350a05 100644 --- a/infrastructure/fee-seller/index.ts +++ b/infrastructure/fee-seller/index.ts @@ -289,10 +289,6 @@ async function sendETH(zksWallet: zksync.Wallet, feeAccumulatorAddress: string, const zksWallet = await zksync.Wallet.fromEthSigner(ethWallet, zksProvider); const ethParameters = new EthParameters(await zksWallet.ethSigner.getTransactionCount('latest')); try { - // First let's try to withdraw the fee gained from the - // forced exit requests functionality, as it does - await withdrawForcedExitFee(ethProvider, ETH_NETWORK); - if (!(await zksWallet.isSigningKeySet())) { console.log('Changing fee account signing key'); const signingKeyTx = await zksWallet.setSigningKey({ feeToken: 'ETH', ethAuthType: 'ECDSA' }); @@ -345,6 +341,14 @@ async function sendETH(zksWallet: zksync.Wallet, feeAccumulatorAddress: string, process.exit(1); } finally { await zksProvider.disconnect(); + } + + try { + console.log('Withdrawing fee for the ForcedExit requests'); + await withdrawForcedExitFee(ethProvider, ETH_NETWORK); process.exit(0); + } catch (e) { + console.error('Failed to withdraw funds from the forced exit smart contract: ', e); + process.exit(1); } })(); diff --git a/infrastructure/fee-seller/withdraw-forced-exit-fee.ts b/infrastructure/fee-seller/withdraw-forced-exit-fee.ts index feacc4a6c9..8c47d5a895 100644 --- a/infrastructure/fee-seller/withdraw-forced-exit-fee.ts +++ b/infrastructure/fee-seller/withdraw-forced-exit-fee.ts @@ -45,26 +45,15 @@ export async function withdrawForcedExitFee(ethProvider: ethers.providers.Provid const ethWallet = new ethers.Wallet(SENDER_PRIVATE_KEY).connect(ethProvider); const forcedExitContract = new ethers.Contract(contractAddress, ForcedExitContractAbi, ethWallet); - try { - console.log('Withdrawing funds from the forced exit smart contract'); - const tx = (await forcedExitContract.withdrawPendingFunds(FEE_RECEIVER, { - gasPrice, - gasLimit: requiredGasLimit - })) as ethers.ContractTransaction; - - const receipt = await tx.wait(); - - console.log('Tx hash:', receipt.transactionHash); - } catch (e) { - console.error('Failed to withdraw funds from the forced exit smart contract: ', e); - // Even though we try to keep the forced exit requests functionality - // as distant from the rest of the code as possible, if the script to withdraw the funds - // fails, we might run into risk of the operator running out of money, so not terminating - // here would be a security issue - process.exit(1); - } finally { - console.log('The process of withdrawing forced exit withdrawal fee is complete.'); - } + console.log('Withdrawing funds from the forced exit smart contract'); + const tx = (await forcedExitContract.withdrawPendingFunds(FEE_RECEIVER, { + gasPrice, + gasLimit: requiredGasLimit + })) as ethers.ContractTransaction; + + const receipt = await tx.wait(); + + console.log('Tx hash:', receipt.transactionHash); } interface StatusResponse { From 9811df931163671f0f1fc6608659dda0a09d74e1 Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Fri, 19 Mar 2021 17:21:43 +0200 Subject: [PATCH 86/90] Style fixes --- .../src/api_server/forced_exit_checker.rs | 28 +++++++++---------- .../rest/forced_exit_requests/v01.rs | 6 ++-- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/core/bin/zksync_api/src/api_server/forced_exit_checker.rs b/core/bin/zksync_api/src/api_server/forced_exit_checker.rs index e33cbc1741..032525a3f7 100644 --- a/core/bin/zksync_api/src/api_server/forced_exit_checker.rs +++ b/core/bin/zksync_api/src/api_server/forced_exit_checker.rs @@ -9,15 +9,15 @@ use chrono::Utc; #[async_trait::async_trait] pub trait ForcedExitAccountAgeChecker { - async fn check_forced_exit<'a>( + async fn check_forced_exit( &self, - storage: &mut StorageProcessor<'a>, + storage: &mut StorageProcessor<'_>, target_account_address: Address, ) -> Result; - async fn validate_forced_exit<'a>( + async fn validate_forced_exit( &self, - storage: &mut StorageProcessor<'a>, + storage: &mut StorageProcessor<'_>, target_account_address: Address, ) -> Result<(), SubmitError>; } @@ -42,9 +42,9 @@ impl ForcedExitChecker { #[async_trait::async_trait] impl ForcedExitAccountAgeChecker for ForcedExitChecker { - async fn check_forced_exit<'a>( + async fn check_forced_exit( &self, - storage: &mut StorageProcessor<'a>, + storage: &mut StorageProcessor<'_>, target_account_address: Address, ) -> Result { let account_age = storage @@ -62,16 +62,16 @@ impl ForcedExitAccountAgeChecker for ForcedExitChecker { } } - async fn validate_forced_exit<'a>( + async fn validate_forced_exit( &self, - storage: &mut StorageProcessor<'a>, + storage: &mut StorageProcessor<'_>, target_account_address: Address, ) -> Result<(), SubmitError> { - let result = self + let eligible = self .check_forced_exit(storage, target_account_address) .await?; - if result { + if eligible { Ok(()) } else { let msg = format!( @@ -88,17 +88,17 @@ pub struct DummyForcedExitChecker; #[async_trait::async_trait] impl ForcedExitAccountAgeChecker for DummyForcedExitChecker { - async fn check_forced_exit<'a>( + async fn check_forced_exit( &self, - _storage: &mut StorageProcessor<'a>, + _storage: &mut StorageProcessor<'_>, _target_account_address: Address, ) -> Result { Ok(true) } - async fn validate_forced_exit<'a>( + async fn validate_forced_exit( &self, - _storage: &mut StorageProcessor<'a>, + _storage: &mut StorageProcessor<'_>, _target_account_address: Address, ) -> Result<(), SubmitError> { Ok(()) diff --git a/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs b/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs index b31653a1d1..a67a324cbc 100644 --- a/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs +++ b/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs @@ -209,15 +209,13 @@ pub async fn check_account_eligibility( .map_err(warn_err) .map_err(ApiError::internal)?; - let is_eligible = data + let eligible = data .forced_exit_checker .check_forced_exit(&mut storage, account) .await .map_err(ApiError::from)?; - let result = ForcedExitEligibilityResponse { - eligible: is_eligible, - }; + let result = ForcedExitEligibilityResponse { eligible }; Ok(Json(result)) } From 95ff3f0d2a4a3186fb44f417aea057877dee0f6e Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Mon, 22 Mar 2021 11:24:07 +0200 Subject: [PATCH 87/90] Verifying that the request could be processed before processing it on the server side --- core/bin/zksync_api/src/api_server/mod.rs | 2 +- .../src/core_interaction_wrapper.rs | 42 ++++++++++++++++++- .../src/eth_watch.rs | 7 +++- .../src/forced_exit_sender.rs | 13 +++++- .../zksync_forced_exit_requests/src/test.rs | 8 ++++ 5 files changed, 66 insertions(+), 6 deletions(-) diff --git a/core/bin/zksync_api/src/api_server/mod.rs b/core/bin/zksync_api/src/api_server/mod.rs index 71ef587653..85cd136663 100644 --- a/core/bin/zksync_api/src/api_server/mod.rs +++ b/core/bin/zksync_api/src/api_server/mod.rs @@ -18,7 +18,7 @@ use crate::signature_checker; mod admin_server; mod event_notify; -mod forced_exit_checker; +pub mod forced_exit_checker; mod helpers; mod rest; pub mod rpc_server; diff --git a/core/bin/zksync_forced_exit_requests/src/core_interaction_wrapper.rs b/core/bin/zksync_forced_exit_requests/src/core_interaction_wrapper.rs index 38d8a97bdb..5f4977323d 100644 --- a/core/bin/zksync_forced_exit_requests/src/core_interaction_wrapper.rs +++ b/core/bin/zksync_forced_exit_requests/src/core_interaction_wrapper.rs @@ -1,4 +1,6 @@ use chrono::Utc; +use num::Zero; +use zksync_config::ZkSyncConfig; use zksync_storage::{chain::operations_ext::records::TxReceiptResponse, ConnectionPool}; use zksync_types::{ forced_exit_requests::{ForcedExitRequest, ForcedExitRequestId}, @@ -6,7 +8,10 @@ use zksync_types::{ AccountId, Nonce, }; -use zksync_api::core_api_client::CoreApiClient; +use zksync_api::{ + api_server::forced_exit_checker::{ForcedExitAccountAgeChecker, ForcedExitChecker}, + core_api_client::CoreApiClient, +}; use zksync_types::SignedZkSyncTx; // We could use `db reset` and test the db the same way as in rust_api @@ -35,19 +40,27 @@ pub trait CoreInteractionWrapper { &self, deleting_threshold: chrono::Duration, ) -> anyhow::Result<()>; + async fn check_forced_exit_request(&self, request: &ForcedExitRequest) -> anyhow::Result; } #[derive(Clone)] pub struct MempoolCoreInteractionWrapper { core_api_client: CoreApiClient, connection_pool: ConnectionPool, + forced_exit_checker: ForcedExitChecker, } impl MempoolCoreInteractionWrapper { - pub fn new(core_api_client: CoreApiClient, connection_pool: ConnectionPool) -> Self { + pub fn new( + config: ZkSyncConfig, + core_api_client: CoreApiClient, + connection_pool: ConnectionPool, + ) -> Self { + let forced_exit_checker = ForcedExitChecker::new(&config); Self { core_api_client, connection_pool, + forced_exit_checker, } } } @@ -159,4 +172,29 @@ impl CoreInteractionWrapper for MempoolCoreInteractionWrapper { Ok(()) } + + async fn check_forced_exit_request(&self, request: &ForcedExitRequest) -> anyhow::Result { + let mut storage = self.connection_pool.access_storage().await?; + let target = request.target; + let eligible = self + .forced_exit_checker + .check_forced_exit(&mut storage, target) + .await?; + + let mut account_schema = storage.chain().account_schema(); + + let target_state = account_schema.account_state_by_address(target).await?; + let target_nonce = target_state.committed.map(|state| state.1.nonce); + + if let Some(nonce) = target_nonce { + // The forced exit is possible is the account is eligile (existed for long enough) + // and its nonce is zero + let possible = nonce.is_zero() && eligible; + Ok(possible) + } else { + // The account does exist. The ForcedExit can not be applied to account + // which does not exist in the network + Ok(false) + } + } } diff --git a/core/bin/zksync_forced_exit_requests/src/eth_watch.rs b/core/bin/zksync_forced_exit_requests/src/eth_watch.rs index d14e6926ea..4cbc192e25 100644 --- a/core/bin/zksync_forced_exit_requests/src/eth_watch.rs +++ b/core/bin/zksync_forced_exit_requests/src/eth_watch.rs @@ -404,8 +404,11 @@ pub fn run_forced_exit_contract_watcher( .await .unwrap(); - let core_interaction_wrapper = - MempoolCoreInteractionWrapper::new(core_api_client, connection_pool.clone()); + let core_interaction_wrapper = MempoolCoreInteractionWrapper::new( + config.clone(), + core_api_client, + connection_pool.clone(), + ); // It is ok to unwrap here, since if forced_exit_sender is not created, then // the watcher is meaningless let mut forced_exit_sender = diff --git a/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs b/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs index 8d1f99e8f8..b68c9f2111 100644 --- a/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs +++ b/core/bin/zksync_forced_exit_requests/src/forced_exit_sender.rs @@ -53,7 +53,8 @@ impl ForcedExitSender for MempoolForced } if attempts >= PROCESSING_ATTEMPTS { - vlog::error!("Failed to process forced exit for the {} time", attempts); + // We should not get stuck processing requests that possibly could never be processed + break; } } } @@ -240,6 +241,16 @@ impl MempoolForcedExitSender { }; let txs = self.build_transactions(fe_request.clone()).await?; + + // Right before sending the transactions we must check if the request is possible at all + let is_request_possible = self + .core_interaction_wrapper + .check_forced_exit_request(&fe_request) + .await?; + if !is_request_possible { + // If not possible at all, return without sending any transactions + return Ok(()); + } let hashes = self .core_interaction_wrapper .send_and_save_txs_batch(&fe_request, txs) diff --git a/core/bin/zksync_forced_exit_requests/src/test.rs b/core/bin/zksync_forced_exit_requests/src/test.rs index db86459815..cbbd155c4f 100644 --- a/core/bin/zksync_forced_exit_requests/src/test.rs +++ b/core/bin/zksync_forced_exit_requests/src/test.rs @@ -160,6 +160,14 @@ impl CoreInteractionWrapper for MockCoreInteractionWrapper { deleted_requests.append(&mut to_delete); Ok(()) } + + async fn check_forced_exit_request( + &self, + _request: &ForcedExitRequest, + ) -> anyhow::Result { + // For tests it is better to just return true all the time + Ok(true) + } } pub fn add_request(requests: &Mutex>, new_request: ForcedExitRequest) { From db21a1412c43a978f538e735c2e275c8c899b8ff Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Mon, 22 Mar 2021 12:48:31 +0200 Subject: [PATCH 88/90] Expand FE into ForcedExit where appropriate --- .../zksync_api/src/api_server/rest/forced_exit_requests/v01.rs | 2 +- .../zksync_forced_exit_requests/src/core_interaction_wrapper.rs | 2 +- etc/env/base/forced_exit_requests.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs b/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs index a67a324cbc..5f15ee2264 100644 --- a/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs +++ b/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs @@ -107,7 +107,7 @@ pub async fn submit_request( if params.tokens.len() > data.max_tokens_per_request as usize { return Err(ApiError::bad_request( - "Maximum number of tokens per FE request exceeded", + "Maximum number of tokens per ForcedExit request exceeded", )); } diff --git a/core/bin/zksync_forced_exit_requests/src/core_interaction_wrapper.rs b/core/bin/zksync_forced_exit_requests/src/core_interaction_wrapper.rs index 5f4977323d..7399961656 100644 --- a/core/bin/zksync_forced_exit_requests/src/core_interaction_wrapper.rs +++ b/core/bin/zksync_forced_exit_requests/src/core_interaction_wrapper.rs @@ -94,7 +94,7 @@ impl CoreInteractionWrapper for MempoolCoreInteractionWrapper { fe_schema.set_fulfilled_at(id, Utc::now()).await?; - vlog::info!("FE request with id {} was fulfilled", id); + vlog::info!("ForcedExit request with id {} was fulfilled", id); Ok(()) } diff --git a/etc/env/base/forced_exit_requests.toml b/etc/env/base/forced_exit_requests.toml index 431f2287d5..c0c6689955 100644 --- a/etc/env/base/forced_exit_requests.toml +++ b/etc/env/base/forced_exit_requests.toml @@ -1,6 +1,6 @@ # Options for L1-based ForcedExit utility [forced_exit_requests] -# Whether the feature is enabled. Used to be able to quickly stop serving FE requests +# Whether the feature is enabled. Used to be able to quickly stop serving ForcedExit requests # in times of attacks or upgrages enabled=true From 2ab86bfa5f2db9f2b94ba2a6c5fc3193fd640c5f Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Mon, 22 Mar 2021 15:45:14 +0200 Subject: [PATCH 89/90] Move forced_exit_requests migrations as the last one --- .../down.sql | 0 .../up.sql | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename core/lib/storage/migrations/{2021-02-19-091010_forced_exit_requests => 2021-03-22-134435_forced_exit_requests}/down.sql (100%) rename core/lib/storage/migrations/{2021-02-19-091010_forced_exit_requests => 2021-03-22-134435_forced_exit_requests}/up.sql (100%) diff --git a/core/lib/storage/migrations/2021-02-19-091010_forced_exit_requests/down.sql b/core/lib/storage/migrations/2021-03-22-134435_forced_exit_requests/down.sql similarity index 100% rename from core/lib/storage/migrations/2021-02-19-091010_forced_exit_requests/down.sql rename to core/lib/storage/migrations/2021-03-22-134435_forced_exit_requests/down.sql diff --git a/core/lib/storage/migrations/2021-02-19-091010_forced_exit_requests/up.sql b/core/lib/storage/migrations/2021-03-22-134435_forced_exit_requests/up.sql similarity index 100% rename from core/lib/storage/migrations/2021-02-19-091010_forced_exit_requests/up.sql rename to core/lib/storage/migrations/2021-03-22-134435_forced_exit_requests/up.sql From 45c93661f67c5f9b04ad00e6a0f104acc752d793 Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Tue, 23 Mar 2021 10:18:53 +0200 Subject: [PATCH 90/90] waitConfirmations in status response --- .../zksync_api/src/api_server/rest/forced_exit_requests/v01.rs | 3 +++ core/lib/api_client/src/rest/forced_exit_requests/mod.rs | 1 + core/tests/ts-tests/tests/forced-exit-requests.ts | 1 + 3 files changed, 5 insertions(+) diff --git a/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs b/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs index 5f15ee2264..c38cfd9aa6 100644 --- a/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs +++ b/core/bin/zksync_api/src/api_server/rest/forced_exit_requests/v01.rs @@ -49,6 +49,7 @@ pub struct ApiForcedExitRequestsData { pub(crate) max_tx_interval_millisecs: i64, pub(crate) price_per_token: i64, pub(crate) forced_exit_contract_address: Address, + pub(crate) wait_confirmations: u64, } impl ApiForcedExitRequestsData { @@ -68,6 +69,7 @@ impl ApiForcedExitRequestsData { max_tx_interval_millisecs: config.forced_exit_requests.max_tx_interval, forced_exit_contract_address: config.contracts.forced_exit_addr, digits_in_id: config.forced_exit_requests.digits_in_id, + wait_confirmations: config.forced_exit_requests.wait_confirmations, } } } @@ -83,6 +85,7 @@ async fn get_status( max_tokens_per_request: data.max_tokens_per_request, recomended_tx_interval_millis: data.recomended_tx_interval_millisecs, forced_exit_contract_address: data.forced_exit_contract_address, + wait_confirmations: data.wait_confirmations, }) } else { ForcedExitRequestStatus::Disabled diff --git a/core/lib/api_client/src/rest/forced_exit_requests/mod.rs b/core/lib/api_client/src/rest/forced_exit_requests/mod.rs index 9d0bb71150..eb38fb4c3f 100644 --- a/core/lib/api_client/src/rest/forced_exit_requests/mod.rs +++ b/core/lib/api_client/src/rest/forced_exit_requests/mod.rs @@ -24,6 +24,7 @@ pub struct ConfigInfo { pub max_tokens_per_request: u8, pub recomended_tx_interval_millis: i64, pub forced_exit_contract_address: Address, + pub wait_confirmations: u64, } #[derive(Serialize, Deserialize, PartialEq, Debug)] diff --git a/core/tests/ts-tests/tests/forced-exit-requests.ts b/core/tests/ts-tests/tests/forced-exit-requests.ts index c147e4acfc..65bf098ef4 100644 --- a/core/tests/ts-tests/tests/forced-exit-requests.ts +++ b/core/tests/ts-tests/tests/forced-exit-requests.ts @@ -35,6 +35,7 @@ interface StatusResponse { maxTokensPerRequest: number; recomendedTxIntervalMillis: number; forcedExitContractAddress: Address; + waitConfirmations: number; } Tester.prototype.testForcedExitRequestMultipleTokens = async function (