From 9884b4b424fd51b243a805f356c94238a7a1ab3a Mon Sep 17 00:00:00 2001 From: Alexander Kolotov Date: Mon, 21 Oct 2019 15:57:28 +0300 Subject: [PATCH] Add support for AMB contracts (#199) --- README.md | 3 +- commons/abis.js | 11 +- commons/constants.js | 9 +- commons/index.js | 4 +- commons/message.js | 27 ++++ commons/package.json | 6 +- .../test/{constants.js => constants.test.js} | 2 +- commons/test/message.test.js | 69 ++++++++++ commons/utils.js | 2 + e2e-commons/components-envs/oracle-amb.env | 23 ++++ e2e-commons/constants.json | 6 + e2e-commons/contracts-envs/amb.env | 25 ++++ e2e-commons/docker-compose.yml | 11 ++ e2e-commons/scripts/deploy.sh | 11 ++ e2e-commons/up.sh | 5 +- monitor/alerts.js | 21 +++- monitor/eventsStats.js | 65 +++++++--- monitor/getBalances.js | 6 + monitor/getShortEventStats.js | 43 +++++-- monitor/utils/events.js | 39 +++--- monitor/utils/message.js | 47 +++++++ oracle-e2e/test/amb.js | 102 +++++++++++++++ oracle/README.md | 4 +- oracle/config/base.config.js | 9 +- .../estimateGas.js | 51 ++++++++ .../processAMBAffirmationRequests/index.js | 95 ++++++++++++++ .../estimateGas.js | 62 +++++++++ .../processAMBCollectedSignatures/index.js | 118 ++++++++++++++++++ .../processAMBSignatureRequests/index.js | 99 +++++++++++++++ .../processAffirmationRequests/index.js | 6 +- .../processCollectedSignatures/index.js | 117 +++++++++-------- .../events/processSignatureRequests/index.js | 6 +- oracle/src/events/processTransfers/index.js | 6 +- oracle/src/utils/message.js | 32 ++++- oracle/src/watcher.js | 9 ++ 35 files changed, 1018 insertions(+), 133 deletions(-) create mode 100644 commons/message.js rename commons/test/{constants.js => constants.test.js} (84%) create mode 100644 commons/test/message.test.js create mode 100644 e2e-commons/components-envs/oracle-amb.env create mode 100644 e2e-commons/contracts-envs/amb.env create mode 100644 monitor/utils/message.js create mode 100644 oracle-e2e/test/amb.js create mode 100644 oracle/src/events/processAMBAffirmationRequests/estimateGas.js create mode 100644 oracle/src/events/processAMBAffirmationRequests/index.js create mode 100644 oracle/src/events/processAMBCollectedSignatures/estimateGas.js create mode 100644 oracle/src/events/processAMBCollectedSignatures/index.js create mode 100644 oracle/src/events/processAMBSignatureRequests/index.js diff --git a/README.md b/README.md index d25e595e..46cd308f 100644 --- a/README.md +++ b/README.md @@ -52,11 +52,12 @@ Additionally there are [Smart Contracts](https://github.com/poanetwork/poa-bridg ## Operational Modes -The POA TokenBridge provides three operational modes: +The POA TokenBridge provides four operational modes: - [x] `Native-to-ERC20` **Coins** on a Home network can be converted to ERC20-compatible **tokens** on a Foreign network. Coins are locked on the Home side and the corresponding amount of ERC20 tokens are minted on the Foreign side. When the operation is reversed, tokens are burnt on the Foreign side and unlocked in the Home network. **More Information: [POA-to-POA20 Bridge](https://medium.com/poa-network/introducing-poa-bridge-and-poa20-55d8b78058ac)** - [x] `ERC20-to-ERC20` ERC20-compatible tokens on the Foreign network are locked and minted as ERC20-compatible tokens (ERC677 tokens) on the Home network. When transferred from Home to Foreign, they are burnt on the Home side and unlocked in the Foreign network. This can be considered a form of atomic swap when a user swaps the token "X" in network "A" to the token "Y" in network "B". **More Information: [ERC20-to-ERC20](https://medium.com/poa-network/introducing-the-erc20-to-erc20-tokenbridge-ce266cc1a2d0)** - [x] `ERC20-to-Native`: Pre-existing **tokens** in the Foreign network are locked and **coins** are minted in the `Home` network. In this mode, the Home network consensus engine invokes [Parity's Block Reward contract](https://wiki.parity.io/Block-Reward-Contract.html) to mint coins per the bridge contract request. **More Information: [xDai Chain](https://medium.com/poa-network/poa-network-partners-with-makerdao-on-xdai-chain-the-first-ever-usd-stable-blockchain-65a078c41e6a)** +- [x] `Arbitrary-Message`: Transfer arbitrary data between two networks as so the data could be interpreted as an arbitrary contract method invocation. ## Initializing the monorepository diff --git a/commons/abis.js b/commons/abis.js index 05e97a2b..03a4fc3f 100644 --- a/commons/abis.js +++ b/commons/abis.js @@ -10,6 +10,9 @@ const ERC677_BRIDGE_TOKEN_ABI = require('../contracts/build/contracts/ERC677Brid const BLOCK_REWARD_ABI = require('../contracts/build/contracts/IBlockReward').abi const BRIDGE_VALIDATORS_ABI = require('../contracts/build/contracts/BridgeValidators').abi const REWARDABLE_VALIDATORS_ABI = require('../contracts/build/contracts/RewardableValidators').abi +const HOME_AMB_ABI = require('../contracts/build/contracts/HomeAMB').abi +const FOREIGN_AMB_ABI = require('../contracts/build/contracts/ForeignAMB').abi +const BOX_ABI = require('../contracts/build/contracts/Box').abi const { HOME_V1_ABI, FOREIGN_V1_ABI } = require('./v1Abis') const { BRIDGE_MODES } = require('./constants') @@ -60,6 +63,9 @@ function getBridgeABIs(bridgeMode) { } else if (bridgeMode === BRIDGE_MODES.NATIVE_TO_ERC_V1) { HOME_ABI = HOME_V1_ABI FOREIGN_ABI = FOREIGN_V1_ABI + } else if (bridgeMode === BRIDGE_MODES.ARBITRARY_MESSAGE) { + HOME_ABI = HOME_AMB_ABI + FOREIGN_ABI = FOREIGN_AMB_ABI } else { throw new Error(`Unrecognized bridge mode: ${bridgeMode}`) } @@ -83,5 +89,8 @@ module.exports = { REWARDABLE_VALIDATORS_ABI, HOME_V1_ABI, FOREIGN_V1_ABI, - ERC20_BYTES32_ABI + ERC20_BYTES32_ABI, + HOME_AMB_ABI, + FOREIGN_AMB_ABI, + BOX_ABI } diff --git a/commons/constants.js b/commons/constants.js index 27cffa41..53c86b68 100644 --- a/commons/constants.js +++ b/commons/constants.js @@ -2,7 +2,8 @@ const BRIDGE_MODES = { NATIVE_TO_ERC: 'NATIVE_TO_ERC', ERC_TO_ERC: 'ERC_TO_ERC', ERC_TO_NATIVE: 'ERC_TO_NATIVE', - NATIVE_TO_ERC_V1: 'NATIVE_TO_ERC_V1' + NATIVE_TO_ERC_V1: 'NATIVE_TO_ERC_V1', + ARBITRARY_MESSAGE: 'ARBITRARY_MESSAGE' } const ERC_TYPES = { @@ -16,4 +17,8 @@ const FEE_MANAGER_MODE = { UNDEFINED: 'UNDEFINED' } -module.exports = { BRIDGE_MODES, ERC_TYPES, FEE_MANAGER_MODE } +module.exports = { + BRIDGE_MODES, + ERC_TYPES, + FEE_MANAGER_MODE +} diff --git a/commons/index.js b/commons/index.js index e66ac401..7d81545f 100644 --- a/commons/index.js +++ b/commons/index.js @@ -1,9 +1,11 @@ const constants = require('./constants') const abis = require('./abis') const utils = require('./utils') +const message = require('./message') module.exports = { ...constants, ...abis, - ...utils + ...utils, + ...message } diff --git a/commons/message.js b/commons/message.js new file mode 100644 index 00000000..16d4e31d --- /dev/null +++ b/commons/message.js @@ -0,0 +1,27 @@ +function strip0x(input) { + return input.replace(/^0x/, '') +} + +function addTxHashToData({ encodedData, transactionHash }) { + return encodedData.slice(0, 2) + strip0x(transactionHash) + encodedData.slice(2) +} + +function parseAMBMessage(message) { + message = strip0x(message) + + const txHash = `0x${message.slice(0, 64)}` + const sender = `0x${message.slice(64, 104)}` + const executor = `0x${message.slice(104, 144)}` + + return { + sender, + executor, + txHash + } +} + +module.exports = { + addTxHashToData, + parseAMBMessage, + strip0x +} diff --git a/commons/package.json b/commons/package.json index a5872c3d..200922ac 100644 --- a/commons/package.json +++ b/commons/package.json @@ -8,6 +8,10 @@ "test": "NODE_ENV=test mocha" }, "dependencies": { - "web3-utils": "1.0.0-beta.30" + "web3-utils": "1.0.0-beta.34" + }, + "devDependencies": { + "bn-chai": "^1.0.1", + "chai": "^4.2.0" } } diff --git a/commons/test/constants.js b/commons/test/constants.test.js similarity index 84% rename from commons/test/constants.js rename to commons/test/constants.test.js index 7cdf7273..cc986e19 100644 --- a/commons/test/constants.js +++ b/commons/test/constants.test.js @@ -3,7 +3,7 @@ const { BRIDGE_MODES, ERC_TYPES } = require('../constants') describe('constants', () => { it('should contain correct number of bridge types', () => { - expect(Object.keys(BRIDGE_MODES).length).to.be.equal(4) + expect(Object.keys(BRIDGE_MODES).length).to.be.equal(5) }) it('should contain correct number of erc types', () => { diff --git a/commons/test/message.test.js b/commons/test/message.test.js new file mode 100644 index 00000000..1eb3e2e6 --- /dev/null +++ b/commons/test/message.test.js @@ -0,0 +1,69 @@ +const { BN } = require('web3-utils') +const { expect } = require('chai').use(require('bn-chai')(BN)) +const { parseAMBMessage, strip0x, addTxHashToData } = require('../message') + +describe('strip0x', () => { + it('should remove 0x from input', () => { + // Given + const input = '0x12345' + + // When + const result = strip0x(input) + + // Then + expect(result).to.be.equal('12345') + }) + it('should not modify input if 0x is not present', () => { + // Given + const input = '12345' + + // When + const result = strip0x(input) + + // Then + expect(result).to.be.equal(input) + }) +}) +describe('addTxHashToData', () => { + it('should add txHash to encoded data at position 2', () => { + // Given + const msgSender = '0x003667154bb32e42bb9e1e6532f19d187fa0082e' + const msgExecutor = '0xf4bef13f9f4f2b203faf0c3cbbaabe1afe056955' + const msgGasLimit = '000000000000000000000000000000000000000000000000000000005b877705' + const msgDataType = '00' + const msgData = '0xb1591967aed668a4b27645ff40c444892d91bf5951b382995d4d4f6ee3a2ce03' + const encodedData = `0x${strip0x(msgSender)}${strip0x(msgExecutor)}${msgGasLimit}${msgDataType}${strip0x(msgData)}` + + const transactionHash = '0xbdceda9d8c94838aca10c687da1411a07b1390e88239c0638cb9cc264219cc10' + const message = `0x${strip0x(transactionHash)}${strip0x(msgSender)}${strip0x( + msgExecutor + )}${msgGasLimit}${msgDataType}${strip0x(msgData)}` + + // When + const result = addTxHashToData({ encodedData, transactionHash }) + + // Then + expect(result).to.be.equal(message) + }) +}) +describe('parseAMBMessage', () => { + it('should parse data type 00', () => { + const msgSender = '0x003667154bb32e42bb9e1e6532f19d187fa0082e' + const msgExecutor = '0xf4bef13f9f4f2b203faf0c3cbbaabe1afe056955' + const msgTxHash = '0xbdceda9d8c94838aca10c687da1411a07b1390e88239c0638cb9cc264219cc10' + const msgGasLimit = '000000000000000000000000000000000000000000000000000000005b877705' + const msgDataType = '00' + const msgData = '0xb1591967aed668a4b27645ff40c444892d91bf5951b382995d4d4f6ee3a2ce03' + const message = `0x${strip0x(msgTxHash)}${strip0x(msgSender)}${strip0x( + msgExecutor + )}${msgGasLimit}${msgDataType}${strip0x(msgData)}` + + // when + const { sender, executor, txHash } = parseAMBMessage(message) + + // then + expect(sender).to.be.equal(msgSender) + expect(executor).to.be.equal(msgExecutor) + expect(txHash).to.be.equal(msgTxHash) + }) +}) diff --git a/commons/utils.js b/commons/utils.js index 2bf6a459..e8779816 100644 --- a/commons/utils.js +++ b/commons/utils.js @@ -10,6 +10,8 @@ function decodeBridgeMode(bridgeModeHash) { return BRIDGE_MODES.ERC_TO_ERC case '0x18762d46': return BRIDGE_MODES.ERC_TO_NATIVE + case '0x2544fbb9': + return BRIDGE_MODES.ARBITRARY_MESSAGE default: throw new Error(`Unrecognized bridge mode hash: '${bridgeModeHash}'`) } diff --git a/e2e-commons/components-envs/oracle-amb.env b/e2e-commons/components-envs/oracle-amb.env new file mode 100644 index 00000000..47565dc1 --- /dev/null +++ b/e2e-commons/components-envs/oracle-amb.env @@ -0,0 +1,23 @@ + +ORACLE_BRIDGE_MODE=ARBITRARY_MESSAGE +ORACLE_QUEUE_URL=amqp://rabbit +ORACLE_REDIS_URL=redis://redis +COMMON_HOME_RPC_URL=http://parity1:8545 +COMMON_FOREIGN_RPC_URL=http://parity2:8545 +COMMON_HOME_BRIDGE_ADDRESS=0x0AEe1FCD12dDFab6265F7f8956e6E012A9Fe4Aa0 +COMMON_FOREIGN_BRIDGE_ADDRESS=0x0AEe1FCD12dDFab6265F7f8956e6E012A9Fe4Aa0 +ORACLE_VALIDATOR_ADDRESS=0xaaB52d66283F7A1D5978bcFcB55721ACB467384b +ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY=8e829f695aed89a154550f30262f1529582cc49dc30eff74a6b491359e0230f9 +COMMON_HOME_GAS_PRICE_SUPPLIER_URL=https://gasprice.poa.network/ +COMMON_HOME_GAS_PRICE_SPEED_TYPE=standard +COMMON_HOME_GAS_PRICE_FALLBACK=1000000000 +ORACLE_HOME_GAS_PRICE_UPDATE_INTERVAL=600000 +COMMON_HOME_GAS_PRICE_FACTOR=1 +COMMON_FOREIGN_GAS_PRICE_SUPPLIER_URL=https://gasprice.poa.network/ +COMMON_FOREIGN_GAS_PRICE_SPEED_TYPE=standard +COMMON_FOREIGN_GAS_PRICE_FALLBACK=10000000000 +ORACLE_FOREIGN_GAS_PRICE_UPDATE_INTERVAL=600000 +COMMON_FOREIGN_GAS_PRICE_FACTOR=1 +ORACLE_HOME_RPC_POLLING_INTERVAL=500 +ORACLE_FOREIGN_RPC_POLLING_INTERVAL=500 +ORACLE_ALLOW_HTTP_FOR_RPC=yes diff --git a/e2e-commons/constants.json b/e2e-commons/constants.json index 246cd20a..a8fc19fc 100644 --- a/e2e-commons/constants.json +++ b/e2e-commons/constants.json @@ -37,6 +37,12 @@ "ui": "http://localhost:3002", "monitor": "http://monitor-erc20-native:3012" }, + "amb": { + "home": "0x0AEe1FCD12dDFab6265F7f8956e6E012A9Fe4Aa0", + "foreign": "0x0AEe1FCD12dDFab6265F7f8956e6E012A9Fe4Aa0", + "homeBox": "0x6C4EaAb8756d53Bf599FFe2347FAFF1123D6C8A1", + "foreignBox": "0x6C4EaAb8756d53Bf599FFe2347FAFF1123D6C8A1" + }, "homeRPC": { "URL": "http://parity1:8545", "ID": "77" diff --git a/e2e-commons/contracts-envs/amb.env b/e2e-commons/contracts-envs/amb.env new file mode 100644 index 00000000..9335c504 --- /dev/null +++ b/e2e-commons/contracts-envs/amb.env @@ -0,0 +1,25 @@ +BRIDGE_MODE=ARBITRARY_MESSAGE +DEPLOYMENT_ACCOUNT_PRIVATE_KEY=8e829f695aed89a154550f30262f1529582cc49dc30eff74a6b491359e0230f9 +HOME_DEPLOYMENT_GAS_PRICE=10000000000 +FOREIGN_DEPLOYMENT_GAS_PRICE=10000000000 +GET_RECEIPT_INTERVAL_IN_MILLISECONDS=50 +DEPLOYMENT_GAS_LIMIT_EXTRA=0.2 + +HOME_RPC_URL=http://parity1:8545 +HOME_BRIDGE_OWNER=0xaaB52d66283F7A1D5978bcFcB55721ACB467384b +HOME_VALIDATORS_OWNER=0xaaB52d66283F7A1D5978bcFcB55721ACB467384b +HOME_UPGRADEABLE_ADMIN=0xaaB52d66283F7A1D5978bcFcB55721ACB467384b +HOME_MAX_AMOUNT_PER_TX=8000000 +HOME_REQUIRED_BLOCK_CONFIRMATIONS=1 +HOME_GAS_PRICE=1000000000 + +FOREIGN_RPC_URL=http://parity2:8545 +FOREIGN_BRIDGE_OWNER=0xaaB52d66283F7A1D5978bcFcB55721ACB467384b +FOREIGN_VALIDATORS_OWNER=0xaaB52d66283F7A1D5978bcFcB55721ACB467384b +FOREIGN_UPGRADEABLE_ADMIN=0xaaB52d66283F7A1D5978bcFcB55721ACB467384b +FOREIGN_MAX_AMOUNT_PER_TX=8000000 +FOREIGN_REQUIRED_BLOCK_CONFIRMATIONS=1 +FOREIGN_GAS_PRICE=10000000000 + +REQUIRED_NUMBER_OF_VALIDATORS=1 +VALIDATORS=0xaaB52d66283F7A1D5978bcFcB55721ACB467384b diff --git a/e2e-commons/docker-compose.yml b/e2e-commons/docker-compose.yml index 8bc9d877..f4fe59d4 100644 --- a/e2e-commons/docker-compose.yml +++ b/e2e-commons/docker-compose.yml @@ -61,6 +61,17 @@ services: command: "true" networks: - ultimate + oracle-amb: + build: + context: .. + dockerfile: oracle/Dockerfile + args: + DOT_ENV_PATH: e2e-commons/components-envs/oracle-amb.env + environment: + - NODE_ENV=production + command: "true" + networks: + - ultimate ui: build: context: .. diff --git a/e2e-commons/scripts/deploy.sh b/e2e-commons/scripts/deploy.sh index b9572827..d3dfd201 100755 --- a/e2e-commons/scripts/deploy.sh +++ b/e2e-commons/scripts/deploy.sh @@ -29,3 +29,14 @@ cp "$ENVS_PATH/erc-to-native.env" "$DEPLOY_PATH/.env" cd "$DEPLOY_PATH" node deploy.js cd - > /dev/null + +echo -e "\n\n############ Deploying amb ############\n" +cp "$ENVS_PATH/amb.env" "$DEPLOY_PATH/.env" +cd "$DEPLOY_PATH" +node deploy.js +cd - > /dev/null + +echo -e "\n\n############ Deploying test contract for amb ############\n" +cd "$DEPLOY_PATH" +node src/utils/deployTestBox.js +cd - > /dev/null diff --git a/e2e-commons/up.sh b/e2e-commons/up.sh index da998775..1142f274 100755 --- a/e2e-commons/up.sh +++ b/e2e-commons/up.sh @@ -9,7 +9,7 @@ docker-compose up -d parity1 parity2 e2e while [ "$1" != "" ]; do if [ "$1" == "oracle" ]; then - docker-compose up -d redis rabbit oracle oracle-erc20 oracle-erc20-native + docker-compose up -d redis rabbit oracle oracle-erc20 oracle-erc20-native oracle-amb docker-compose run -d oracle yarn watcher:signature-request docker-compose run -d oracle yarn watcher:collected-signatures @@ -20,6 +20,9 @@ while [ "$1" != "" ]; do docker-compose run -d oracle-erc20-native yarn watcher:signature-request docker-compose run -d oracle-erc20-native yarn watcher:collected-signatures docker-compose run -d oracle-erc20-native yarn watcher:affirmation-request + docker-compose run -d oracle-amb yarn watcher:signature-request + docker-compose run -d oracle-amb yarn watcher:collected-signatures + docker-compose run -d oracle-amb yarn watcher:affirmation-request docker-compose run -d oracle yarn sender:home docker-compose run -d oracle yarn sender:foreign fi diff --git a/monitor/alerts.js b/monitor/alerts.js index 3eff08ba..26ca4056 100644 --- a/monitor/alerts.js +++ b/monitor/alerts.js @@ -3,6 +3,8 @@ const Web3 = require('web3') const logger = require('./logger')('alerts') const eventsInfo = require('./utils/events') const { getBlockNumber } = require('./utils/contract') +const { processedMsgNotDelivered } = require('./utils/message') +const { BRIDGE_MODES } = require('../commons') const { COMMON_HOME_RPC_URL, COMMON_FOREIGN_RPC_URL } = process.env @@ -13,10 +15,23 @@ const foreignProvider = new Web3.providers.HttpProvider(COMMON_FOREIGN_RPC_URL) const web3Foreign = new Web3(foreignProvider) async function main() { - const { foreignDeposits, homeDeposits, homeWithdrawals, foreignWithdrawals } = await eventsInfo() + const { + homeToForeignRequests, + homeToForeignConfirmations, + foreignToHomeConfirmations, + foreignToHomeRequests, + bridgeMode + } = await eventsInfo() - const xSignatures = foreignDeposits.filter(findDifferences(homeDeposits)) - const xAffirmations = homeWithdrawals.filter(findDifferences(foreignWithdrawals)) + let xSignatures + let xAffirmations + if (bridgeMode === BRIDGE_MODES.ARBITRARY_MESSAGE) { + xSignatures = homeToForeignConfirmations.filter(processedMsgNotDelivered(homeToForeignRequests)) + xAffirmations = foreignToHomeConfirmations.filter(processedMsgNotDelivered(foreignToHomeRequests)) + } else { + xSignatures = homeToForeignConfirmations.filter(findDifferences(homeToForeignRequests)) + xAffirmations = foreignToHomeConfirmations.filter(findDifferences(foreignToHomeRequests)) + } logger.debug('building misbehavior blocks') const [homeBlockNumber, foreignBlockNumber] = await getBlockNumber(web3Home, web3Foreign) diff --git a/monitor/eventsStats.js b/monitor/eventsStats.js index a960bf1b..480cb616 100644 --- a/monitor/eventsStats.js +++ b/monitor/eventsStats.js @@ -1,6 +1,7 @@ require('dotenv').config() -const logger = require('./logger')('eventsStats') const eventsInfo = require('./utils/events') +const { processedMsgNotDelivered, deliveredMsgNotProcessed } = require('./utils/message') +const { BRIDGE_MODES } = require('../commons') function compareDepositsHome(foreign) { return homeDeposit => { @@ -57,25 +58,55 @@ function compareTransferForeign(home) { } async function main() { - const { foreignDeposits, homeDeposits, homeWithdrawals, foreignWithdrawals, isExternalErc20 } = await eventsInfo() + const { + homeToForeignRequests, + homeToForeignConfirmations, + foreignToHomeConfirmations, + foreignToHomeRequests, + isExternalErc20, + bridgeMode + } = await eventsInfo() - const onlyInHomeDeposits = homeDeposits.filter(compareDepositsHome(foreignDeposits)) - const onlyInForeignDeposits = foreignDeposits.concat([]).filter(compareDepositsForeign(homeDeposits)) + if (bridgeMode === BRIDGE_MODES.ARBITRARY_MESSAGE) { + return { + home: { + deliveredMsgNotProcessedInForeign: homeToForeignRequests.filter( + deliveredMsgNotProcessed(homeToForeignConfirmations) + ), + processedMsgNotDeliveredInForeign: foreignToHomeConfirmations.filter( + processedMsgNotDelivered(foreignToHomeRequests) + ) + }, + foreign: { + deliveredMsgNotProcessedInHome: foreignToHomeRequests.filter( + deliveredMsgNotProcessed(foreignToHomeConfirmations) + ), + processedMsgNotDeliveredInHome: homeToForeignConfirmations.filter( + processedMsgNotDelivered(homeToForeignRequests) + ) + }, + lastChecked: Math.floor(Date.now() / 1000) + } + } else { + const onlyInHomeDeposits = homeToForeignRequests.filter(compareDepositsHome(homeToForeignConfirmations)) + const onlyInForeignDeposits = homeToForeignConfirmations + .concat([]) + .filter(compareDepositsForeign(homeToForeignRequests)) - const onlyInHomeWithdrawals = isExternalErc20 - ? homeWithdrawals.filter(compareTransferHome(foreignWithdrawals)) - : homeWithdrawals.filter(compareDepositsForeign(foreignWithdrawals)) - const onlyInForeignWithdrawals = isExternalErc20 - ? foreignWithdrawals.filter(compareTransferForeign(homeWithdrawals)) - : foreignWithdrawals.filter(compareDepositsHome(homeWithdrawals)) + const onlyInHomeWithdrawals = isExternalErc20 + ? foreignToHomeConfirmations.filter(compareTransferHome(foreignToHomeRequests)) + : foreignToHomeConfirmations.filter(compareDepositsForeign(foreignToHomeRequests)) + const onlyInForeignWithdrawals = isExternalErc20 + ? foreignToHomeRequests.filter(compareTransferForeign(foreignToHomeConfirmations)) + : foreignToHomeRequests.filter(compareDepositsHome(foreignToHomeConfirmations)) - logger.debug('Done') - return { - onlyInHomeDeposits, - onlyInForeignDeposits, - onlyInHomeWithdrawals, - onlyInForeignWithdrawals, - lastChecked: Math.floor(Date.now() / 1000) + return { + onlyInHomeDeposits, + onlyInForeignDeposits, + onlyInHomeWithdrawals, + onlyInForeignWithdrawals, + lastChecked: Math.floor(Date.now() / 1000) + } } } diff --git a/monitor/getBalances.js b/monitor/getBalances.js index 97a279a7..e7fcf899 100644 --- a/monitor/getBalances.js +++ b/monitor/getBalances.js @@ -112,6 +112,12 @@ async function main(bridgeMode) { balanceDiff: Number(Web3Utils.fromWei(diff)), lastChecked: Math.floor(Date.now() / 1000) } + } else if (bridgeMode === BRIDGE_MODES.ARBITRARY_MESSAGE) { + return { + home: {}, + foreign: {}, + lastChecked: Math.floor(Date.now() / 1000) + } } else { throw new Error(`Unrecognized bridge mode: '${bridgeMode}'`) } diff --git a/monitor/getShortEventStats.js b/monitor/getShortEventStats.js index 9571a5d6..f314abd0 100644 --- a/monitor/getShortEventStats.js +++ b/monitor/getShortEventStats.js @@ -1,19 +1,40 @@ require('dotenv').config() const eventsInfo = require('./utils/events') +const { BRIDGE_MODES } = require('../commons') async function main(bridgeMode) { - const { foreignDeposits, homeDeposits, homeWithdrawals, foreignWithdrawals } = await eventsInfo(bridgeMode) + const { + homeToForeignConfirmations, + homeToForeignRequests, + foreignToHomeConfirmations, + foreignToHomeRequests + } = await eventsInfo(bridgeMode) - return { - depositsDiff: homeDeposits.length - foreignDeposits.length, - withdrawalDiff: homeWithdrawals.length - foreignWithdrawals.length, - home: { - deposits: homeDeposits.length, - withdrawals: homeWithdrawals.length - }, - foreign: { - deposits: foreignDeposits.length, - withdrawals: foreignWithdrawals.length + if (bridgeMode === BRIDGE_MODES.ARBITRARY_MESSAGE) { + return { + fromHomeToForeignDiff: homeToForeignRequests.length - homeToForeignConfirmations.length, + fromForeignToHomeDiff: foreignToHomeConfirmations.length - foreignToHomeRequests.length, + home: { + toForeign: homeToForeignRequests.length, + fromForeign: foreignToHomeConfirmations.length + }, + foreign: { + fromHome: homeToForeignConfirmations.length, + toHome: foreignToHomeRequests.length + } + } + } else { + return { + depositsDiff: homeToForeignRequests.length - homeToForeignConfirmations.length, + withdrawalDiff: foreignToHomeConfirmations.length - foreignToHomeRequests.length, + home: { + deposits: homeToForeignRequests.length, + withdrawals: foreignToHomeConfirmations.length + }, + foreign: { + deposits: homeToForeignConfirmations.length, + withdrawals: foreignToHomeRequests.length + } } } } diff --git a/monitor/utils/events.js b/monitor/utils/events.js index b3223106..a63bd233 100644 --- a/monitor/utils/events.js +++ b/monitor/utils/events.js @@ -38,41 +38,45 @@ async function main(mode) { const homeBridge = new web3Home.eth.Contract(HOME_ABI, COMMON_HOME_BRIDGE_ADDRESS) const foreignBridge = new web3Foreign.eth.Contract(FOREIGN_ABI, COMMON_FOREIGN_BRIDGE_ADDRESS) const v1Bridge = bridgeMode === BRIDGE_MODES.NATIVE_TO_ERC_V1 - const erc20MethodName = bridgeMode === BRIDGE_MODES.NATIVE_TO_ERC || v1Bridge ? 'erc677token' : 'erc20token' - const erc20Address = await foreignBridge.methods[erc20MethodName]().call() - const tokenType = await getTokenType( - new web3Foreign.eth.Contract(ERC677_BRIDGE_TOKEN_ABI, erc20Address), - COMMON_FOREIGN_BRIDGE_ADDRESS - ) - const isExternalErc20 = tokenType === ERC_TYPES.ERC20 - const erc20Contract = new web3Foreign.eth.Contract(ERC20_ABI, erc20Address) + let isExternalErc20 + let erc20Contract + if (bridgeMode !== BRIDGE_MODES.ARBITRARY_MESSAGE) { + const erc20MethodName = bridgeMode === BRIDGE_MODES.NATIVE_TO_ERC || v1Bridge ? 'erc677token' : 'erc20token' + const erc20Address = await foreignBridge.methods[erc20MethodName]().call() + const tokenType = await getTokenType( + new web3Foreign.eth.Contract(ERC677_BRIDGE_TOKEN_ABI, erc20Address), + COMMON_FOREIGN_BRIDGE_ADDRESS + ) + isExternalErc20 = tokenType === ERC_TYPES.ERC20 + erc20Contract = new web3Foreign.eth.Contract(ERC20_ABI, erc20Address) + } logger.debug('getting last block numbers') const [homeBlockNumber, foreignBlockNumber] = await getBlockNumber(web3Home, web3Foreign) logger.debug("calling homeBridge.getPastEvents('UserRequestForSignature')") - const homeDeposits = await getPastEvents(homeBridge, { + const homeToForeignRequests = await getPastEvents(homeBridge, { event: v1Bridge ? 'Deposit' : 'UserRequestForSignature', fromBlock: MONITOR_HOME_START_BLOCK, toBlock: homeBlockNumber }) logger.debug("calling foreignBridge.getPastEvents('RelayedMessage')") - const foreignDeposits = await getPastEvents(foreignBridge, { + const homeToForeignConfirmations = await getPastEvents(foreignBridge, { event: v1Bridge ? 'Deposit' : 'RelayedMessage', fromBlock: MONITOR_FOREIGN_START_BLOCK, toBlock: foreignBlockNumber }) logger.debug("calling homeBridge.getPastEvents('AffirmationCompleted')") - const homeWithdrawals = await getPastEvents(homeBridge, { + const foreignToHomeConfirmations = await getPastEvents(homeBridge, { event: v1Bridge ? 'Withdraw' : 'AffirmationCompleted', fromBlock: MONITOR_HOME_START_BLOCK, toBlock: homeBlockNumber }) logger.debug("calling foreignBridge.getPastEvents('UserRequestForAffirmation')") - const foreignWithdrawals = isExternalErc20 + const foreignToHomeRequests = isExternalErc20 ? await getPastEvents(erc20Contract, { event: 'Transfer', fromBlock: MONITOR_FOREIGN_START_BLOCK, @@ -88,11 +92,12 @@ async function main(mode) { }) logger.debug('Done') return { - homeDeposits, - foreignDeposits, - homeWithdrawals, - foreignWithdrawals, - isExternalErc20 + homeToForeignRequests, + homeToForeignConfirmations, + foreignToHomeConfirmations, + foreignToHomeRequests, + isExternalErc20, + bridgeMode } } diff --git a/monitor/utils/message.js b/monitor/utils/message.js new file mode 100644 index 00000000..dda2bba5 --- /dev/null +++ b/monitor/utils/message.js @@ -0,0 +1,47 @@ +const web3Utils = require('web3').utils +const { addTxHashToData, parseAMBMessage } = require('../../commons') + +function deliveredMsgNotProcessed(processedList) { + return deliveredMsg => { + const msg = parseAMBMessage( + addTxHashToData({ + encodedData: deliveredMsg.returnValues.encodedData, + transactionHash: deliveredMsg.transactionHash + }) + ) + return ( + processedList.filter(processedMsg => { + return messageEqualsEvent(msg, processedMsg.returnValues) + }).length === 0 + ) + } +} + +function processedMsgNotDelivered(deliveredList) { + return processedMsg => { + return ( + deliveredList.filter(deliveredMsg => { + const msg = parseAMBMessage( + addTxHashToData({ + encodedData: deliveredMsg.returnValues.encodedData, + transactionHash: deliveredMsg.transactionHash + }) + ) + return messageEqualsEvent(msg, processedMsg.returnValues) + }).length === 0 + ) + } +} + +function messageEqualsEvent(parsedMsg, event) { + return ( + web3Utils.toChecksumAddress(parsedMsg.sender) === event.sender && + web3Utils.toChecksumAddress(parsedMsg.executor) === event.executor && + parsedMsg.txHash === event.transactionHash + ) +} + +module.exports = { + deliveredMsgNotProcessed, + processedMsgNotDelivered +} diff --git a/oracle-e2e/test/amb.js b/oracle-e2e/test/amb.js new file mode 100644 index 00000000..351c4b96 --- /dev/null +++ b/oracle-e2e/test/amb.js @@ -0,0 +1,102 @@ +const Web3 = require('web3') +const assert = require('assert') +const promiseRetry = require('promise-retry') +const { user, homeRPC, foreignRPC, amb } = require('../../e2e-commons/constants.json') +const { generateNewBlock } = require('../../e2e-commons/utils') +const { BOX_ABI } = require('../../commons') + +const { toBN } = Web3.utils + +const homeWeb3 = new Web3(new Web3.providers.HttpProvider(homeRPC.URL)) +const foreignWeb3 = new Web3(new Web3.providers.HttpProvider(foreignRPC.URL)) + +homeWeb3.eth.accounts.wallet.add(user.privateKey) +foreignWeb3.eth.accounts.wallet.add(user.privateKey) + +const homeBox = new homeWeb3.eth.Contract(BOX_ABI, amb.homeBox) +const foreignBox = new foreignWeb3.eth.Contract(BOX_ABI, amb.foreignBox) + +describe('arbitrary message bridging', () => { + describe('Home to Foreign', () => { + describe('Subsidized Mode', () => { + it('should bridge message', async () => { + const newValue = 3 + + const initialValue = await foreignBox.methods.value().call() + assert(!toBN(initialValue).eq(toBN(newValue)), 'initial value should be different from new value') + + const setValueTx = await homeBox.methods + .setValueOnOtherNetwork(newValue, amb.home, amb.foreignBox) + .send({ + from: user.address, + gas: '400000' + }) + .catch(e => { + console.error(e) + }) + + // Send a trivial transaction to generate a new block since the watcher + // is configured to wait 1 confirmation block + await generateNewBlock(homeWeb3, user.address) + + // The bridge should create a new transaction with a CollectedSignatures + // event so we generate another trivial transaction + await promiseRetry( + async retry => { + const lastBlockNumber = await homeWeb3.eth.getBlockNumber() + if (lastBlockNumber >= setValueTx.blockNumber + 2) { + await generateNewBlock(homeWeb3, user.address) + } else { + retry() + } + }, + { + forever: true, + factor: 1, + minTimeout: 500 + } + ) + + // check that value changed and balance decreased + await promiseRetry(async retry => { + const value = await foreignBox.methods.value().call() + if (!toBN(value).eq(toBN(newValue))) { + retry() + } + }) + }) + }) + }) + describe('Foreign to Home', () => { + describe('Subsidized Mode', () => { + it('should bridge message', async () => { + const newValue = 7 + + const initialValue = await homeBox.methods.value().call() + assert(!toBN(initialValue).eq(toBN(newValue)), 'initial value should be different from new value') + + await foreignBox.methods + .setValueOnOtherNetwork(newValue, amb.foreign, amb.homeBox) + .send({ + from: user.address, + gas: '400000' + }) + .catch(e => { + console.error(e) + }) + + // Send a trivial transaction to generate a new block since the watcher + // is configured to wait 1 confirmation block + await generateNewBlock(foreignWeb3, user.address) + + // check that value changed and balance decreased + await promiseRetry(async retry => { + const value = await homeBox.methods.value().call() + if (!toBN(value).eq(toBN(newValue))) { + retry() + } + }) + }) + }) + }) +}) diff --git a/oracle/README.md b/oracle/README.md index 0d806572..6277bdd4 100644 --- a/oracle/README.md +++ b/oracle/README.md @@ -10,7 +10,7 @@ The Oracle is deployed on specified validator nodes (only nodes whose private ke ## Architecture -### Native-to-ERC20 +### Native-to-ERC20 and Arbitrary-Message ![Native-to-ERC](Native-to-ERC.png) @@ -27,7 +27,7 @@ There are three Watchers: - **Signature Request Watcher**: Listens to `UserRequestForSignature` events on the Home network. - **Collected Signatures Watcher**: Listens to `CollectedSignatures` events on the Home network. - **Affirmation Request Watcher**: Depends on the bridge mode. - - `Native-to-ERC20`: Listens to `UserRequestForAffirmation` raised by the bridge contract. + - `Native-to-ERC20` and `Arbitrary-Message`: Listens to `UserRequestForAffirmation` raised by the bridge contract. - `ERC20-to-ERC20` and `ERC20-to-Native`: Listens to `Transfer` events raised by the token contract. ### Sender diff --git a/oracle/config/base.config.js b/oracle/config/base.config.js index 8e6bb424..1e2398cd 100644 --- a/oracle/config/base.config.js +++ b/oracle/config/base.config.js @@ -8,7 +8,9 @@ const { HOME_ERC_TO_ERC_ABI, FOREIGN_ERC_TO_ERC_ABI, HOME_ERC_TO_NATIVE_ABI, - FOREIGN_ERC_TO_NATIVE_ABI + FOREIGN_ERC_TO_NATIVE_ABI, + HOME_AMB_ABI, + FOREIGN_AMB_ABI } = require('../../commons') const { web3Home, web3Foreign } = require('../src/services/web3') const { privateKeyToAddress } = require('../src/utils/utils') @@ -35,6 +37,11 @@ switch (process.env.ORACLE_BRIDGE_MODE) { foreignAbi = FOREIGN_ERC_TO_NATIVE_ABI id = 'erc-native' break + case BRIDGE_MODES.ARBITRARY_MESSAGE: + homeAbi = HOME_AMB_ABI + foreignAbi = FOREIGN_AMB_ABI + id = 'amb' + break default: if (process.env.NODE_ENV !== 'test') { throw new Error(`Bridge Mode: ${process.env.ORACLE_BRIDGE_MODE} not supported.`) diff --git a/oracle/src/events/processAMBAffirmationRequests/estimateGas.js b/oracle/src/events/processAMBAffirmationRequests/estimateGas.js new file mode 100644 index 00000000..231b9326 --- /dev/null +++ b/oracle/src/events/processAMBAffirmationRequests/estimateGas.js @@ -0,0 +1,51 @@ +const { HttpListProviderError } = require('http-list-provider') +const { AlreadyProcessedError, AlreadySignedError, InvalidValidatorError } = require('../../utils/errors') +const logger = require('../../services/logger').child({ + module: 'processAffirmationRequests:estimateGas' +}) + +async function estimateGas({ web3, homeBridge, validatorContract, message, address }) { + try { + const gasEstimate = await homeBridge.methods.executeAffirmation(message).estimateGas({ + from: address + }) + + return gasEstimate + } catch (e) { + if (e instanceof HttpListProviderError) { + throw e + } + + const messageHash = web3.utils.soliditySha3(message) + const senderHash = web3.utils.soliditySha3(address, messageHash) + + // Check if minimum number of validations was already reached + logger.debug('Check if minimum number of validations was already reached') + const numAffirmationsSigned = await homeBridge.methods.numAffirmationsSigned(messageHash).call() + const alreadyProcessed = await homeBridge.methods.isAlreadyProcessed(numAffirmationsSigned).call() + + if (alreadyProcessed) { + throw new AlreadyProcessedError(e.message) + } + + // Check if the message was already signed by this validator + logger.debug('Check if the message was already signed') + const alreadySigned = await homeBridge.methods.affirmationsSigned(senderHash).call() + + if (alreadySigned) { + throw new AlreadySignedError(e.message) + } + + // Check if address is validator + logger.debug('Check if address is a validator') + const isValidator = await validatorContract.methods.isValidator(address).call() + + if (!isValidator) { + throw new InvalidValidatorError(`${address} is not a validator`) + } + + throw new Error('Unknown error while processing message') + } +} + +module.exports = estimateGas diff --git a/oracle/src/events/processAMBAffirmationRequests/index.js b/oracle/src/events/processAMBAffirmationRequests/index.js new file mode 100644 index 00000000..492e9773 --- /dev/null +++ b/oracle/src/events/processAMBAffirmationRequests/index.js @@ -0,0 +1,95 @@ +require('dotenv').config() +const { HttpListProviderError } = require('http-list-provider') +const promiseLimit = require('promise-limit') +const rootLogger = require('../../services/logger') +const { web3Home } = require('../../services/web3') +const bridgeValidatorsABI = require('../../../../contracts/build/contracts/BridgeValidators').abi +const { EXIT_CODES, MAX_CONCURRENT_EVENTS } = require('../../utils/constants') +const estimateGas = require('./estimateGas') +const { addTxHashToData, parseAMBMessage } = require('../../../../commons') +const { AlreadyProcessedError, AlreadySignedError, InvalidValidatorError } = require('../../utils/errors') + +const limit = promiseLimit(MAX_CONCURRENT_EVENTS) + +let validatorContract = null + +function processAffirmationRequestsBuilder(config) { + const homeBridge = new web3Home.eth.Contract(config.homeBridgeAbi, config.homeBridgeAddress) + + return async function processAffirmationRequests(affirmationRequests) { + const txToSend = [] + + if (validatorContract === null) { + rootLogger.debug('Getting validator contract address') + const validatorContractAddress = await homeBridge.methods.validatorContract().call() + rootLogger.debug({ validatorContractAddress }, 'Validator contract address obtained') + + validatorContract = new web3Home.eth.Contract(bridgeValidatorsABI, validatorContractAddress) + } + + rootLogger.debug(`Processing ${affirmationRequests.length} AffirmationRequest events`) + const callbacks = affirmationRequests + .map(affirmationRequest => async () => { + const { encodedData } = affirmationRequest.returnValues + + const logger = rootLogger.child({ + eventTransactionHash: affirmationRequest.transactionHash + }) + + const message = addTxHashToData({ + encodedData, + transactionHash: affirmationRequest.transactionHash + }) + + const { sender, executor } = parseAMBMessage(message) + + logger.info({ sender, executor }, `Processing affirmationRequest ${affirmationRequest.transactionHash}`) + + let gasEstimate + try { + logger.debug('Estimate gas') + gasEstimate = await estimateGas({ + web3: web3Home, + homeBridge, + validatorContract, + message, + address: config.validatorAddress + }) + logger.debug({ gasEstimate }, 'Gas estimated') + } catch (e) { + if (e instanceof HttpListProviderError) { + throw new Error('RPC Connection Error: submitSignature Gas Estimate cannot be obtained.') + } else if (e instanceof InvalidValidatorError) { + logger.fatal({ address: config.validatorAddress }, 'Invalid validator') + process.exit(EXIT_CODES.INCOMPATIBILITY) + } else if (e instanceof AlreadySignedError) { + logger.info(`Already signed affirmationRequest ${affirmationRequest.transactionHash}`) + return + } else if (e instanceof AlreadyProcessedError) { + logger.info( + `affirmationRequest ${affirmationRequest.transactionHash} was already processed by other validators` + ) + return + } else { + logger.error(e, 'Unknown error while processing transaction') + throw e + } + } + + const data = await homeBridge.methods.executeAffirmation(message).encodeABI() + + txToSend.push({ + data, + gasEstimate, + transactionReference: affirmationRequest.transactionHash, + to: config.homeBridgeAddress + }) + }) + .map(promise => limit(promise)) + + await Promise.all(callbacks) + return txToSend + } +} + +module.exports = processAffirmationRequestsBuilder diff --git a/oracle/src/events/processAMBCollectedSignatures/estimateGas.js b/oracle/src/events/processAMBCollectedSignatures/estimateGas.js new file mode 100644 index 00000000..4168ff67 --- /dev/null +++ b/oracle/src/events/processAMBCollectedSignatures/estimateGas.js @@ -0,0 +1,62 @@ +const Web3 = require('web3') +const { HttpListProviderError } = require('http-list-provider') +const { AlreadyProcessedError, IncompatibleContractError, InvalidValidatorError } = require('../../utils/errors') +const logger = require('../../services/logger').child({ + module: 'processCollectedSignatures:estimateGas' +}) + +const web3 = new Web3() +const { toBN } = Web3.utils + +async function estimateGas({ + foreignBridge, + validatorContract, + message, + numberOfCollectedSignatures, + v, + r, + s, + signatures, + txHash, + address +}) { + try { + const gasEstimate = await foreignBridge.methods.executeSignatures(message, signatures).estimateGas({ + from: address + }) + return gasEstimate + } catch (e) { + if (e instanceof HttpListProviderError) { + throw e + } + + // check if the message was already processed + logger.debug('Check if the message was already processed') + const alreadyProcessed = await foreignBridge.methods.relayedMessages(txHash).call() + if (alreadyProcessed) { + throw new AlreadyProcessedError() + } + + // check if the number of signatures is enough + logger.debug('Check if number of signatures is enough') + const requiredSignatures = await validatorContract.methods.requiredSignatures().call() + if (toBN(requiredSignatures).gt(toBN(numberOfCollectedSignatures))) { + throw new IncompatibleContractError('The number of collected signatures does not match') + } + + // check if all the signatures were made by validators + for (let i = 0; i < v.length; i++) { + const address = web3.eth.accounts.recover(message, web3.utils.toHex(v[i]), r[i], s[i]) + logger.debug({ address }, 'Check that signature is from a validator') + const isValidator = await validatorContract.methods.isValidator(address).call() + + if (!isValidator) { + throw new InvalidValidatorError(`Message signed by ${address} that is not a validator`) + } + } + + throw new Error('Unknown error while processing message') + } +} + +module.exports = estimateGas diff --git a/oracle/src/events/processAMBCollectedSignatures/index.js b/oracle/src/events/processAMBCollectedSignatures/index.js new file mode 100644 index 00000000..c3ed54cb --- /dev/null +++ b/oracle/src/events/processAMBCollectedSignatures/index.js @@ -0,0 +1,118 @@ +require('dotenv').config() +const promiseLimit = require('promise-limit') +const { HttpListProviderError } = require('http-list-provider') +const bridgeValidatorsABI = require('../../../../contracts/build/contracts/BridgeValidators').abi +const rootLogger = require('../../services/logger') +const { web3Home, web3Foreign } = require('../../services/web3') +const { signatureToVRS, signatureToVRSAMB, packSignatures } = require('../../utils/message') +const { parseAMBMessage } = require('../../../../commons') +const estimateGas = require('./estimateGas') +const { AlreadyProcessedError, IncompatibleContractError, InvalidValidatorError } = require('../../utils/errors') +const { MAX_CONCURRENT_EVENTS } = require('../../utils/constants') + +const limit = promiseLimit(MAX_CONCURRENT_EVENTS) + +let validatorContract = null + +function processCollectedSignaturesBuilder(config) { + const homeBridge = new web3Home.eth.Contract(config.homeBridgeAbi, config.homeBridgeAddress) + + const foreignBridge = new web3Foreign.eth.Contract(config.foreignBridgeAbi, config.foreignBridgeAddress) + + return async function processCollectedSignatures(signatures) { + const txToSend = [] + + if (validatorContract === null) { + rootLogger.debug('Getting validator contract address') + const validatorContractAddress = await foreignBridge.methods.validatorContract().call() + rootLogger.debug({ validatorContractAddress }, 'Validator contract address obtained') + + validatorContract = new web3Foreign.eth.Contract(bridgeValidatorsABI, validatorContractAddress) + } + + rootLogger.debug(`Processing ${signatures.length} CollectedSignatures events`) + const callbacks = signatures + .map(colSignature => async () => { + const { authorityResponsibleForRelay, messageHash, NumberOfCollectedSignatures } = colSignature.returnValues + + const logger = rootLogger.child({ + eventTransactionHash: colSignature.transactionHash + }) + + if (authorityResponsibleForRelay !== web3Home.utils.toChecksumAddress(config.validatorAddress)) { + logger.info(`Validator not responsible for relaying CollectedSignatures ${colSignature.transactionHash}`) + return + } + + logger.info(`Processing CollectedSignatures ${colSignature.transactionHash}`) + const message = await homeBridge.methods.message(messageHash).call() + + const requiredSignatures = new Array(NumberOfCollectedSignatures).fill(0) + + const signaturesArray = [] + const [v, r, s] = [[], [], []] + logger.debug('Getting message signatures') + const signaturePromises = requiredSignatures.map(async (el, index) => { + logger.debug({ index }, 'Getting message signature') + const signature = await homeBridge.methods.signature(messageHash, index).call() + const vrs = signatureToVRS(signature) + v.push(vrs.v) + r.push(vrs.r) + s.push(vrs.s) + const recover = signatureToVRSAMB(signature) + signaturesArray.push(recover) + }) + + await Promise.all(signaturePromises) + const signatures = packSignatures(signaturesArray) + + const { txHash } = parseAMBMessage(message) + + let gasEstimate + try { + logger.debug('Estimate gas') + gasEstimate = await estimateGas({ + foreignBridge, + validatorContract, + v, + r, + s, + signatures, + message, + numberOfCollectedSignatures: NumberOfCollectedSignatures, + txHash, + address: config.validatorAddress + }) + logger.debug({ gasEstimate }, 'Gas estimated') + } catch (e) { + if (e instanceof HttpListProviderError) { + throw new Error('RPC Connection Error: submitSignature Gas Estimate cannot be obtained.') + } else if (e instanceof AlreadyProcessedError) { + logger.info(`Already processed CollectedSignatures ${colSignature.transactionHash}`) + return + } else if (e instanceof IncompatibleContractError || e instanceof InvalidValidatorError) { + logger.error(`The message couldn't be processed; skipping: ${e.message}`) + return + } else { + logger.error(e, 'Unknown error while processing transaction') + throw e + } + } + const data = await foreignBridge.methods.executeSignatures(message, signatures).encodeABI() + + txToSend.push({ + data, + gasEstimate, + transactionReference: colSignature.transactionHash, + to: config.foreignBridgeAddress + }) + }) + .map(promise => limit(promise)) + + await Promise.all(callbacks) + + return txToSend + } +} + +module.exports = processCollectedSignaturesBuilder diff --git a/oracle/src/events/processAMBSignatureRequests/index.js b/oracle/src/events/processAMBSignatureRequests/index.js new file mode 100644 index 00000000..5d26f001 --- /dev/null +++ b/oracle/src/events/processAMBSignatureRequests/index.js @@ -0,0 +1,99 @@ +require('dotenv').config() +const promiseLimit = require('promise-limit') +const { HttpListProviderError } = require('http-list-provider') +const bridgeValidatorsABI = require('../../../../contracts/build/contracts/BridgeValidators').abi +const rootLogger = require('../../services/logger') +const { web3Home } = require('../../services/web3') +const { addTxHashToData, parseAMBMessage } = require('../../../../commons') +const estimateGas = require('../processSignatureRequests/estimateGas') +const { AlreadyProcessedError, AlreadySignedError, InvalidValidatorError } = require('../../utils/errors') +const { EXIT_CODES, MAX_CONCURRENT_EVENTS } = require('../../utils/constants') + +const { ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY } = process.env + +const limit = promiseLimit(MAX_CONCURRENT_EVENTS) + +let validatorContract = null + +function processSignatureRequestsBuilder(config) { + const homeBridge = new web3Home.eth.Contract(config.homeBridgeAbi, config.homeBridgeAddress) + + return async function processSignatureRequests(signatureRequests) { + const txToSend = [] + + if (validatorContract === null) { + rootLogger.debug('Getting validator contract address') + const validatorContractAddress = await homeBridge.methods.validatorContract().call() + rootLogger.debug({ validatorContractAddress }, 'Validator contract address obtained') + + validatorContract = new web3Home.eth.Contract(bridgeValidatorsABI, validatorContractAddress) + } + + rootLogger.debug(`Processing ${signatureRequests.length} SignatureRequest events`) + const callbacks = signatureRequests + .map(signatureRequest => async () => { + const { encodedData } = signatureRequest.returnValues + + const logger = rootLogger.child({ + eventTransactionHash: signatureRequest.transactionHash + }) + + const message = addTxHashToData({ + encodedData, + transactionHash: signatureRequest.transactionHash + }) + + const { sender, executor } = parseAMBMessage(message) + logger.info({ sender, executor }, `Processing signatureRequest ${signatureRequest.transactionHash}`) + + const signature = web3Home.eth.accounts.sign(message, `0x${ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY}`) + + let gasEstimate + try { + logger.debug('Estimate gas') + gasEstimate = await estimateGas({ + web3: web3Home, + homeBridge, + validatorContract, + signature: signature.signature, + message, + address: config.validatorAddress + }) + logger.debug({ gasEstimate }, 'Gas estimated') + } catch (e) { + if (e instanceof HttpListProviderError) { + throw new Error('RPC Connection Error: submitSignature Gas Estimate cannot be obtained.') + } else if (e instanceof InvalidValidatorError) { + logger.fatal({ address: config.validatorAddress }, 'Invalid validator') + process.exit(EXIT_CODES.INCOMPATIBILITY) + } else if (e instanceof AlreadySignedError) { + logger.info(`Already signed signatureRequest ${signatureRequest.transactionHash}`) + return + } else if (e instanceof AlreadyProcessedError) { + logger.info( + `signatureRequest ${signatureRequest.transactionHash} was already processed by other validators` + ) + return + } else { + logger.error(e, 'Unknown error while processing transaction') + throw e + } + } + + const data = await homeBridge.methods.submitSignature(signature.signature, message).encodeABI() + + txToSend.push({ + data, + gasEstimate, + transactionReference: signatureRequest.transactionHash, + to: config.homeBridgeAddress + }) + }) + .map(promise => limit(promise)) + + await Promise.all(callbacks) + return txToSend + } +} + +module.exports = processSignatureRequestsBuilder diff --git a/oracle/src/events/processAffirmationRequests/index.js b/oracle/src/events/processAffirmationRequests/index.js index 16b726ad..d7f198ba 100644 --- a/oracle/src/events/processAffirmationRequests/index.js +++ b/oracle/src/events/processAffirmationRequests/index.js @@ -28,8 +28,8 @@ function processAffirmationRequestsBuilder(config) { } rootLogger.debug(`Processing ${affirmationRequests.length} AffirmationRequest events`) - const callbacks = affirmationRequests.map(affirmationRequest => - limit(async () => { + const callbacks = affirmationRequests + .map(affirmationRequest => async () => { const { recipient, value } = affirmationRequest.returnValues const logger = rootLogger.child({ @@ -82,7 +82,7 @@ function processAffirmationRequestsBuilder(config) { to: config.homeBridgeAddress }) }) - ) + .map(promise => limit(promise)) await Promise.all(callbacks) return txToSend diff --git a/oracle/src/events/processCollectedSignatures/index.js b/oracle/src/events/processCollectedSignatures/index.js index a6f0773b..e8851646 100644 --- a/oracle/src/events/processCollectedSignatures/index.js +++ b/oracle/src/events/processCollectedSignatures/index.js @@ -30,74 +30,73 @@ function processCollectedSignaturesBuilder(config) { } rootLogger.debug(`Processing ${signatures.length} CollectedSignatures events`) - const callbacks = signatures.map(colSignature => - limit(async () => { + const callbacks = signatures + .map(colSignature => async () => { const { authorityResponsibleForRelay, messageHash, NumberOfCollectedSignatures } = colSignature.returnValues const logger = rootLogger.child({ eventTransactionHash: colSignature.transactionHash }) - if (authorityResponsibleForRelay === web3Home.utils.toChecksumAddress(config.validatorAddress)) { - logger.info(`Processing CollectedSignatures ${colSignature.transactionHash}`) - const message = await homeBridge.methods.message(messageHash).call() - - const requiredSignatures = [] - requiredSignatures.length = NumberOfCollectedSignatures - requiredSignatures.fill(0) - - const [v, r, s] = [[], [], []] - logger.debug('Getting message signatures') - const signaturePromises = requiredSignatures.map(async (el, index) => { - logger.debug({ index }, 'Getting message signature') - const signature = await homeBridge.methods.signature(messageHash, index).call() - const recover = signatureToVRS(signature) - v.push(recover.v) - r.push(recover.r) - s.push(recover.s) - }) - - await Promise.all(signaturePromises) - - let gasEstimate - try { - logger.debug('Estimate gas') - gasEstimate = await estimateGas({ - foreignBridge, - validatorContract, - v, - r, - s, - message, - numberOfCollectedSignatures: NumberOfCollectedSignatures - }) - logger.debug({ gasEstimate }, 'Gas estimated') - } catch (e) { - if (e instanceof HttpListProviderError) { - throw new Error('RPC Connection Error: submitSignature Gas Estimate cannot be obtained.') - } else if (e instanceof AlreadyProcessedError) { - logger.info(`Already processed CollectedSignatures ${colSignature.transactionHash}`) - return - } else if (e instanceof IncompatibleContractError || e instanceof InvalidValidatorError) { - logger.error(`The message couldn't be processed; skipping: ${e.message}`) - return - } else { - logger.error(e, 'Unknown error while processing transaction') - throw e - } - } - const data = await foreignBridge.methods.executeSignatures(v, r, s, message).encodeABI() - txToSend.push({ - data, - gasEstimate, - transactionReference: colSignature.transactionHash, - to: config.foreignBridgeAddress - }) - } else { + if (authorityResponsibleForRelay !== web3Home.utils.toChecksumAddress(config.validatorAddress)) { logger.info(`Validator not responsible for relaying CollectedSignatures ${colSignature.transactionHash}`) + return } + + logger.info(`Processing CollectedSignatures ${colSignature.transactionHash}`) + const message = await homeBridge.methods.message(messageHash).call() + + const requiredSignatures = new Array(NumberOfCollectedSignatures).fill(0) + + const [v, r, s] = [[], [], []] + logger.debug('Getting message signatures') + const signaturePromises = requiredSignatures.map(async (el, index) => { + logger.debug({ index }, 'Getting message signature') + const signature = await homeBridge.methods.signature(messageHash, index).call() + const recover = signatureToVRS(signature) + v.push(recover.v) + r.push(recover.r) + s.push(recover.s) + }) + + await Promise.all(signaturePromises) + + let gasEstimate + try { + logger.debug('Estimate gas') + gasEstimate = await estimateGas({ + foreignBridge, + validatorContract, + v, + r, + s, + message, + numberOfCollectedSignatures: NumberOfCollectedSignatures + }) + logger.debug({ gasEstimate }, 'Gas estimated') + } catch (e) { + if (e instanceof HttpListProviderError) { + throw new Error('RPC Connection Error: submitSignature Gas Estimate cannot be obtained.') + } else if (e instanceof AlreadyProcessedError) { + logger.info(`Already processed CollectedSignatures ${colSignature.transactionHash}`) + return + } else if (e instanceof IncompatibleContractError || e instanceof InvalidValidatorError) { + logger.error(`The message couldn't be processed; skipping: ${e.message}`) + return + } else { + logger.error(e, 'Unknown error while processing transaction') + throw e + } + } + const data = await foreignBridge.methods.executeSignatures(v, r, s, message).encodeABI() + txToSend.push({ + data, + gasEstimate, + transactionReference: colSignature.transactionHash, + to: config.foreignBridgeAddress + }) }) - ) + .map(promise => limit(promise)) await Promise.all(callbacks) diff --git a/oracle/src/events/processSignatureRequests/index.js b/oracle/src/events/processSignatureRequests/index.js index 3da2b388..9a465044 100644 --- a/oracle/src/events/processSignatureRequests/index.js +++ b/oracle/src/events/processSignatureRequests/index.js @@ -35,8 +35,8 @@ function processSignatureRequestsBuilder(config) { } rootLogger.debug(`Processing ${signatureRequests.length} SignatureRequest events`) - const callbacks = signatureRequests.map(signatureRequest => - limit(async () => { + const callbacks = signatureRequests + .map(signatureRequest => async () => { const { recipient, value } = signatureRequest.returnValues const logger = rootLogger.child({ @@ -98,7 +98,7 @@ function processSignatureRequestsBuilder(config) { to: config.homeBridgeAddress }) }) - ) + .map(promise => limit(promise)) await Promise.all(callbacks) return txToSend diff --git a/oracle/src/events/processTransfers/index.js b/oracle/src/events/processTransfers/index.js index 5cb135cc..624eaed8 100644 --- a/oracle/src/events/processTransfers/index.js +++ b/oracle/src/events/processTransfers/index.js @@ -27,8 +27,8 @@ function processTransfersBuilder(config) { } rootLogger.debug(`Processing ${transfers.length} Transfer events`) - const callbacks = transfers.map(transfer => - limit(async () => { + const callbacks = transfers + .map(transfer => async () => { const { from, value } = transfer.returnValues const logger = rootLogger.child({ @@ -79,7 +79,7 @@ function processTransfersBuilder(config) { to: config.homeBridgeAddress }) }) - ) + .map(promise => limit(promise)) await Promise.all(callbacks) return txToSend diff --git a/oracle/src/utils/message.js b/oracle/src/utils/message.js index dca93dcb..d20359b1 100644 --- a/oracle/src/utils/message.js +++ b/oracle/src/utils/message.js @@ -1,10 +1,6 @@ const assert = require('assert') const Web3Utils = require('web3-utils') - -// strips leading "0x" if present -function strip0x(input) { - return input.replace(/^0x/, '') -} +const { strip0x } = require('../../../commons') function createMessage({ recipient, value, transactionHash, bridgeAddress, expectedMessageLength }) { recipient = strip0x(recipient) @@ -63,8 +59,32 @@ function signatureToVRS(signature) { return { v, r, s } } +function signatureToVRSAMB(rawSignature) { + const signature = strip0x(rawSignature) + const v = signature.substr(64 * 2) + const r = signature.substr(0, 32 * 2) + const s = signature.substr(32 * 2, 32 * 2) + return { v, r, s } +} + +function packSignatures(array) { + const length = strip0x(Web3Utils.toHex(array.length)) + const msgLength = length.length === 1 ? `0${length}` : length + let v = '' + let r = '' + let s = '' + array.forEach(e => { + v = v.concat(e.v) + r = r.concat(e.r) + s = s.concat(e.s) + }) + return `0x${msgLength}${v}${r}${s}` +} + module.exports = { createMessage, parseMessage, - signatureToVRS + signatureToVRS, + signatureToVRSAMB, + packSignatures } diff --git a/oracle/src/watcher.js b/oracle/src/watcher.js index 428c8979..e96a4f60 100644 --- a/oracle/src/watcher.js +++ b/oracle/src/watcher.js @@ -21,6 +21,9 @@ const processSignatureRequests = require('./events/processSignatureRequests')(co const processCollectedSignatures = require('./events/processCollectedSignatures')(config) const processAffirmationRequests = require('./events/processAffirmationRequests')(config) const processTransfers = require('./events/processTransfers')(config) +const processAMBSignatureRequests = require('./events/processAMBSignatureRequests')(config) +const processAMBCollectedSignatures = require('./events/processAMBCollectedSignatures')(config) +const processAMBAffirmationRequests = require('./events/processAMBAffirmationRequests')(config) const ZERO = toBN(0) const ONE = toBN(1) @@ -97,6 +100,12 @@ function processEvents(events) { case 'erc-erc-affirmation-request': case 'erc-native-affirmation-request': return processTransfers(events) + case 'amb-signature-request': + return processAMBSignatureRequests(events) + case 'amb-collected-signatures': + return processAMBCollectedSignatures(events) + case 'amb-affirmation-request': + return processAMBAffirmationRequests(events) default: return [] }