Add support for AMB contracts (#199)

This commit is contained in:
Gerardo Nardelli 2019-09-18 16:45:13 -03:00 committed by Alexander Kolotov
parent 803f0074e6
commit d577a71096
35 changed files with 1018 additions and 133 deletions

@ -52,11 +52,12 @@ Additionally there are [Smart Contracts](https://github.com/poanetwork/poa-bridg
## Operational Modes ## 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] `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-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] `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 ## Initializing the monorepository

@ -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 BLOCK_REWARD_ABI = require('../contracts/build/contracts/IBlockReward').abi
const BRIDGE_VALIDATORS_ABI = require('../contracts/build/contracts/BridgeValidators').abi const BRIDGE_VALIDATORS_ABI = require('../contracts/build/contracts/BridgeValidators').abi
const REWARDABLE_VALIDATORS_ABI = require('../contracts/build/contracts/RewardableValidators').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 { HOME_V1_ABI, FOREIGN_V1_ABI } = require('./v1Abis')
const { BRIDGE_MODES } = require('./constants') const { BRIDGE_MODES } = require('./constants')
@ -60,6 +63,9 @@ function getBridgeABIs(bridgeMode) {
} else if (bridgeMode === BRIDGE_MODES.NATIVE_TO_ERC_V1) { } else if (bridgeMode === BRIDGE_MODES.NATIVE_TO_ERC_V1) {
HOME_ABI = HOME_V1_ABI HOME_ABI = HOME_V1_ABI
FOREIGN_ABI = FOREIGN_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 { } else {
throw new Error(`Unrecognized bridge mode: ${bridgeMode}`) throw new Error(`Unrecognized bridge mode: ${bridgeMode}`)
} }
@ -83,5 +89,8 @@ module.exports = {
REWARDABLE_VALIDATORS_ABI, REWARDABLE_VALIDATORS_ABI,
HOME_V1_ABI, HOME_V1_ABI,
FOREIGN_V1_ABI, FOREIGN_V1_ABI,
ERC20_BYTES32_ABI ERC20_BYTES32_ABI,
HOME_AMB_ABI,
FOREIGN_AMB_ABI,
BOX_ABI
} }

@ -2,7 +2,8 @@ const BRIDGE_MODES = {
NATIVE_TO_ERC: 'NATIVE_TO_ERC', NATIVE_TO_ERC: 'NATIVE_TO_ERC',
ERC_TO_ERC: 'ERC_TO_ERC', ERC_TO_ERC: 'ERC_TO_ERC',
ERC_TO_NATIVE: 'ERC_TO_NATIVE', 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 = { const ERC_TYPES = {
@ -16,4 +17,8 @@ const FEE_MANAGER_MODE = {
UNDEFINED: 'UNDEFINED' UNDEFINED: 'UNDEFINED'
} }
module.exports = { BRIDGE_MODES, ERC_TYPES, FEE_MANAGER_MODE } module.exports = {
BRIDGE_MODES,
ERC_TYPES,
FEE_MANAGER_MODE
}

@ -1,9 +1,11 @@
const constants = require('./constants') const constants = require('./constants')
const abis = require('./abis') const abis = require('./abis')
const utils = require('./utils') const utils = require('./utils')
const message = require('./message')
module.exports = { module.exports = {
...constants, ...constants,
...abis, ...abis,
...utils ...utils,
...message
} }

27
commons/message.js Normal file

@ -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
}

@ -8,6 +8,10 @@
"test": "NODE_ENV=test mocha" "test": "NODE_ENV=test mocha"
}, },
"dependencies": { "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"
} }
} }

@ -3,7 +3,7 @@ const { BRIDGE_MODES, ERC_TYPES } = require('../constants')
describe('constants', () => { describe('constants', () => {
it('should contain correct number of bridge types', () => { 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', () => { it('should contain correct number of erc types', () => {

@ -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)
})
})

@ -10,6 +10,8 @@ function decodeBridgeMode(bridgeModeHash) {
return BRIDGE_MODES.ERC_TO_ERC return BRIDGE_MODES.ERC_TO_ERC
case '0x18762d46': case '0x18762d46':
return BRIDGE_MODES.ERC_TO_NATIVE return BRIDGE_MODES.ERC_TO_NATIVE
case '0x2544fbb9':
return BRIDGE_MODES.ARBITRARY_MESSAGE
default: default:
throw new Error(`Unrecognized bridge mode hash: '${bridgeModeHash}'`) throw new Error(`Unrecognized bridge mode hash: '${bridgeModeHash}'`)
} }

@ -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

@ -37,6 +37,12 @@
"ui": "http://localhost:3002", "ui": "http://localhost:3002",
"monitor": "http://monitor-erc20-native:3012" "monitor": "http://monitor-erc20-native:3012"
}, },
"amb": {
"home": "0x0AEe1FCD12dDFab6265F7f8956e6E012A9Fe4Aa0",
"foreign": "0x0AEe1FCD12dDFab6265F7f8956e6E012A9Fe4Aa0",
"homeBox": "0x6C4EaAb8756d53Bf599FFe2347FAFF1123D6C8A1",
"foreignBox": "0x6C4EaAb8756d53Bf599FFe2347FAFF1123D6C8A1"
},
"homeRPC": { "homeRPC": {
"URL": "http://parity1:8545", "URL": "http://parity1:8545",
"ID": "77" "ID": "77"

@ -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

@ -61,6 +61,17 @@ services:
command: "true" command: "true"
networks: networks:
- ultimate - 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: ui:
build: build:
context: .. context: ..

@ -29,3 +29,14 @@ cp "$ENVS_PATH/erc-to-native.env" "$DEPLOY_PATH/.env"
cd "$DEPLOY_PATH" cd "$DEPLOY_PATH"
node deploy.js node deploy.js
cd - > /dev/null 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

@ -9,7 +9,7 @@ docker-compose up -d parity1 parity2 e2e
while [ "$1" != "" ]; do while [ "$1" != "" ]; do
if [ "$1" == "oracle" ]; then 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:signature-request
docker-compose run -d oracle yarn watcher:collected-signatures 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:signature-request
docker-compose run -d oracle-erc20-native yarn watcher:collected-signatures 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-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:home
docker-compose run -d oracle yarn sender:foreign docker-compose run -d oracle yarn sender:foreign
fi fi

@ -3,6 +3,8 @@ const Web3 = require('web3')
const logger = require('./logger')('alerts') const logger = require('./logger')('alerts')
const eventsInfo = require('./utils/events') const eventsInfo = require('./utils/events')
const { getBlockNumber } = require('./utils/contract') 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 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) const web3Foreign = new Web3(foreignProvider)
async function main() { async function main() {
const { foreignDeposits, homeDeposits, homeWithdrawals, foreignWithdrawals } = await eventsInfo() const {
homeToForeignRequests,
homeToForeignConfirmations,
foreignToHomeConfirmations,
foreignToHomeRequests,
bridgeMode
} = await eventsInfo()
const xSignatures = foreignDeposits.filter(findDifferences(homeDeposits)) let xSignatures
const xAffirmations = homeWithdrawals.filter(findDifferences(foreignWithdrawals)) 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') logger.debug('building misbehavior blocks')
const [homeBlockNumber, foreignBlockNumber] = await getBlockNumber(web3Home, web3Foreign) const [homeBlockNumber, foreignBlockNumber] = await getBlockNumber(web3Home, web3Foreign)

@ -1,6 +1,7 @@
require('dotenv').config() require('dotenv').config()
const logger = require('./logger')('eventsStats')
const eventsInfo = require('./utils/events') const eventsInfo = require('./utils/events')
const { processedMsgNotDelivered, deliveredMsgNotProcessed } = require('./utils/message')
const { BRIDGE_MODES } = require('../commons')
function compareDepositsHome(foreign) { function compareDepositsHome(foreign) {
return homeDeposit => { return homeDeposit => {
@ -57,25 +58,55 @@ function compareTransferForeign(home) {
} }
async function main() { 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)) if (bridgeMode === BRIDGE_MODES.ARBITRARY_MESSAGE) {
const onlyInForeignDeposits = foreignDeposits.concat([]).filter(compareDepositsForeign(homeDeposits)) 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 const onlyInHomeWithdrawals = isExternalErc20
? homeWithdrawals.filter(compareTransferHome(foreignWithdrawals)) ? foreignToHomeConfirmations.filter(compareTransferHome(foreignToHomeRequests))
: homeWithdrawals.filter(compareDepositsForeign(foreignWithdrawals)) : foreignToHomeConfirmations.filter(compareDepositsForeign(foreignToHomeRequests))
const onlyInForeignWithdrawals = isExternalErc20 const onlyInForeignWithdrawals = isExternalErc20
? foreignWithdrawals.filter(compareTransferForeign(homeWithdrawals)) ? foreignToHomeRequests.filter(compareTransferForeign(foreignToHomeConfirmations))
: foreignWithdrawals.filter(compareDepositsHome(homeWithdrawals)) : foreignToHomeRequests.filter(compareDepositsHome(foreignToHomeConfirmations))
logger.debug('Done') return {
return { onlyInHomeDeposits,
onlyInHomeDeposits, onlyInForeignDeposits,
onlyInForeignDeposits, onlyInHomeWithdrawals,
onlyInHomeWithdrawals, onlyInForeignWithdrawals,
onlyInForeignWithdrawals, lastChecked: Math.floor(Date.now() / 1000)
lastChecked: Math.floor(Date.now() / 1000) }
} }
} }

@ -112,6 +112,12 @@ async function main(bridgeMode) {
balanceDiff: Number(Web3Utils.fromWei(diff)), balanceDiff: Number(Web3Utils.fromWei(diff)),
lastChecked: Math.floor(Date.now() / 1000) lastChecked: Math.floor(Date.now() / 1000)
} }
} else if (bridgeMode === BRIDGE_MODES.ARBITRARY_MESSAGE) {
return {
home: {},
foreign: {},
lastChecked: Math.floor(Date.now() / 1000)
}
} else { } else {
throw new Error(`Unrecognized bridge mode: '${bridgeMode}'`) throw new Error(`Unrecognized bridge mode: '${bridgeMode}'`)
} }

@ -1,19 +1,40 @@
require('dotenv').config() require('dotenv').config()
const eventsInfo = require('./utils/events') const eventsInfo = require('./utils/events')
const { BRIDGE_MODES } = require('../commons')
async function main(bridgeMode) { async function main(bridgeMode) {
const { foreignDeposits, homeDeposits, homeWithdrawals, foreignWithdrawals } = await eventsInfo(bridgeMode) const {
homeToForeignConfirmations,
homeToForeignRequests,
foreignToHomeConfirmations,
foreignToHomeRequests
} = await eventsInfo(bridgeMode)
return { if (bridgeMode === BRIDGE_MODES.ARBITRARY_MESSAGE) {
depositsDiff: homeDeposits.length - foreignDeposits.length, return {
withdrawalDiff: homeWithdrawals.length - foreignWithdrawals.length, fromHomeToForeignDiff: homeToForeignRequests.length - homeToForeignConfirmations.length,
home: { fromForeignToHomeDiff: foreignToHomeConfirmations.length - foreignToHomeRequests.length,
deposits: homeDeposits.length, home: {
withdrawals: homeWithdrawals.length toForeign: homeToForeignRequests.length,
}, fromForeign: foreignToHomeConfirmations.length
foreign: { },
deposits: foreignDeposits.length, foreign: {
withdrawals: foreignWithdrawals.length 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
}
} }
} }
} }

@ -38,41 +38,45 @@ async function main(mode) {
const homeBridge = new web3Home.eth.Contract(HOME_ABI, COMMON_HOME_BRIDGE_ADDRESS) 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 foreignBridge = new web3Foreign.eth.Contract(FOREIGN_ABI, COMMON_FOREIGN_BRIDGE_ADDRESS)
const v1Bridge = bridgeMode === BRIDGE_MODES.NATIVE_TO_ERC_V1 const v1Bridge = bridgeMode === BRIDGE_MODES.NATIVE_TO_ERC_V1
const erc20MethodName = bridgeMode === BRIDGE_MODES.NATIVE_TO_ERC || v1Bridge ? 'erc677token' : 'erc20token' let isExternalErc20
const erc20Address = await foreignBridge.methods[erc20MethodName]().call() let erc20Contract
const tokenType = await getTokenType( if (bridgeMode !== BRIDGE_MODES.ARBITRARY_MESSAGE) {
new web3Foreign.eth.Contract(ERC677_BRIDGE_TOKEN_ABI, erc20Address), const erc20MethodName = bridgeMode === BRIDGE_MODES.NATIVE_TO_ERC || v1Bridge ? 'erc677token' : 'erc20token'
COMMON_FOREIGN_BRIDGE_ADDRESS const erc20Address = await foreignBridge.methods[erc20MethodName]().call()
) const tokenType = await getTokenType(
const isExternalErc20 = tokenType === ERC_TYPES.ERC20 new web3Foreign.eth.Contract(ERC677_BRIDGE_TOKEN_ABI, erc20Address),
const erc20Contract = new web3Foreign.eth.Contract(ERC20_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') logger.debug('getting last block numbers')
const [homeBlockNumber, foreignBlockNumber] = await getBlockNumber(web3Home, web3Foreign) const [homeBlockNumber, foreignBlockNumber] = await getBlockNumber(web3Home, web3Foreign)
logger.debug("calling homeBridge.getPastEvents('UserRequestForSignature')") logger.debug("calling homeBridge.getPastEvents('UserRequestForSignature')")
const homeDeposits = await getPastEvents(homeBridge, { const homeToForeignRequests = await getPastEvents(homeBridge, {
event: v1Bridge ? 'Deposit' : 'UserRequestForSignature', event: v1Bridge ? 'Deposit' : 'UserRequestForSignature',
fromBlock: MONITOR_HOME_START_BLOCK, fromBlock: MONITOR_HOME_START_BLOCK,
toBlock: homeBlockNumber toBlock: homeBlockNumber
}) })
logger.debug("calling foreignBridge.getPastEvents('RelayedMessage')") logger.debug("calling foreignBridge.getPastEvents('RelayedMessage')")
const foreignDeposits = await getPastEvents(foreignBridge, { const homeToForeignConfirmations = await getPastEvents(foreignBridge, {
event: v1Bridge ? 'Deposit' : 'RelayedMessage', event: v1Bridge ? 'Deposit' : 'RelayedMessage',
fromBlock: MONITOR_FOREIGN_START_BLOCK, fromBlock: MONITOR_FOREIGN_START_BLOCK,
toBlock: foreignBlockNumber toBlock: foreignBlockNumber
}) })
logger.debug("calling homeBridge.getPastEvents('AffirmationCompleted')") logger.debug("calling homeBridge.getPastEvents('AffirmationCompleted')")
const homeWithdrawals = await getPastEvents(homeBridge, { const foreignToHomeConfirmations = await getPastEvents(homeBridge, {
event: v1Bridge ? 'Withdraw' : 'AffirmationCompleted', event: v1Bridge ? 'Withdraw' : 'AffirmationCompleted',
fromBlock: MONITOR_HOME_START_BLOCK, fromBlock: MONITOR_HOME_START_BLOCK,
toBlock: homeBlockNumber toBlock: homeBlockNumber
}) })
logger.debug("calling foreignBridge.getPastEvents('UserRequestForAffirmation')") logger.debug("calling foreignBridge.getPastEvents('UserRequestForAffirmation')")
const foreignWithdrawals = isExternalErc20 const foreignToHomeRequests = isExternalErc20
? await getPastEvents(erc20Contract, { ? await getPastEvents(erc20Contract, {
event: 'Transfer', event: 'Transfer',
fromBlock: MONITOR_FOREIGN_START_BLOCK, fromBlock: MONITOR_FOREIGN_START_BLOCK,
@ -88,11 +92,12 @@ async function main(mode) {
}) })
logger.debug('Done') logger.debug('Done')
return { return {
homeDeposits, homeToForeignRequests,
foreignDeposits, homeToForeignConfirmations,
homeWithdrawals, foreignToHomeConfirmations,
foreignWithdrawals, foreignToHomeRequests,
isExternalErc20 isExternalErc20,
bridgeMode
} }
} }

47
monitor/utils/message.js Normal file

@ -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
}

102
oracle-e2e/test/amb.js Normal file

@ -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()
}
})
})
})
})
})

@ -10,7 +10,7 @@ The Oracle is deployed on specified validator nodes (only nodes whose private ke
## Architecture ## Architecture
### Native-to-ERC20 ### Native-to-ERC20 and Arbitrary-Message
![Native-to-ERC](Native-to-ERC.png) ![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. - **Signature Request Watcher**: Listens to `UserRequestForSignature` events on the Home network.
- **Collected Signatures Watcher**: Listens to `CollectedSignatures` events on the Home network. - **Collected Signatures Watcher**: Listens to `CollectedSignatures` events on the Home network.
- **Affirmation Request Watcher**: Depends on the bridge mode. - **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. - `ERC20-to-ERC20` and `ERC20-to-Native`: Listens to `Transfer` events raised by the token contract.
### Sender ### Sender

@ -8,7 +8,9 @@ const {
HOME_ERC_TO_ERC_ABI, HOME_ERC_TO_ERC_ABI,
FOREIGN_ERC_TO_ERC_ABI, FOREIGN_ERC_TO_ERC_ABI,
HOME_ERC_TO_NATIVE_ABI, HOME_ERC_TO_NATIVE_ABI,
FOREIGN_ERC_TO_NATIVE_ABI FOREIGN_ERC_TO_NATIVE_ABI,
HOME_AMB_ABI,
FOREIGN_AMB_ABI
} = require('../../commons') } = require('../../commons')
const { web3Home, web3Foreign } = require('../src/services/web3') const { web3Home, web3Foreign } = require('../src/services/web3')
const { privateKeyToAddress } = require('../src/utils/utils') const { privateKeyToAddress } = require('../src/utils/utils')
@ -35,6 +37,11 @@ switch (process.env.ORACLE_BRIDGE_MODE) {
foreignAbi = FOREIGN_ERC_TO_NATIVE_ABI foreignAbi = FOREIGN_ERC_TO_NATIVE_ABI
id = 'erc-native' id = 'erc-native'
break break
case BRIDGE_MODES.ARBITRARY_MESSAGE:
homeAbi = HOME_AMB_ABI
foreignAbi = FOREIGN_AMB_ABI
id = 'amb'
break
default: default:
if (process.env.NODE_ENV !== 'test') { if (process.env.NODE_ENV !== 'test') {
throw new Error(`Bridge Mode: ${process.env.ORACLE_BRIDGE_MODE} not supported.`) throw new Error(`Bridge Mode: ${process.env.ORACLE_BRIDGE_MODE} not supported.`)

@ -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

@ -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

@ -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

@ -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

@ -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

@ -28,8 +28,8 @@ function processAffirmationRequestsBuilder(config) {
} }
rootLogger.debug(`Processing ${affirmationRequests.length} AffirmationRequest events`) rootLogger.debug(`Processing ${affirmationRequests.length} AffirmationRequest events`)
const callbacks = affirmationRequests.map(affirmationRequest => const callbacks = affirmationRequests
limit(async () => { .map(affirmationRequest => async () => {
const { recipient, value } = affirmationRequest.returnValues const { recipient, value } = affirmationRequest.returnValues
const logger = rootLogger.child({ const logger = rootLogger.child({
@ -82,7 +82,7 @@ function processAffirmationRequestsBuilder(config) {
to: config.homeBridgeAddress to: config.homeBridgeAddress
}) })
}) })
) .map(promise => limit(promise))
await Promise.all(callbacks) await Promise.all(callbacks)
return txToSend return txToSend

@ -30,74 +30,73 @@ function processCollectedSignaturesBuilder(config) {
} }
rootLogger.debug(`Processing ${signatures.length} CollectedSignatures events`) rootLogger.debug(`Processing ${signatures.length} CollectedSignatures events`)
const callbacks = signatures.map(colSignature => const callbacks = signatures
limit(async () => { .map(colSignature => async () => {
const { authorityResponsibleForRelay, messageHash, NumberOfCollectedSignatures } = colSignature.returnValues const { authorityResponsibleForRelay, messageHash, NumberOfCollectedSignatures } = colSignature.returnValues
const logger = rootLogger.child({ const logger = rootLogger.child({
eventTransactionHash: colSignature.transactionHash eventTransactionHash: colSignature.transactionHash
}) })
if (authorityResponsibleForRelay === web3Home.utils.toChecksumAddress(config.validatorAddress)) { 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 {
logger.info(`Validator not responsible for relaying CollectedSignatures ${colSignature.transactionHash}`) 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) await Promise.all(callbacks)

@ -35,8 +35,8 @@ function processSignatureRequestsBuilder(config) {
} }
rootLogger.debug(`Processing ${signatureRequests.length} SignatureRequest events`) rootLogger.debug(`Processing ${signatureRequests.length} SignatureRequest events`)
const callbacks = signatureRequests.map(signatureRequest => const callbacks = signatureRequests
limit(async () => { .map(signatureRequest => async () => {
const { recipient, value } = signatureRequest.returnValues const { recipient, value } = signatureRequest.returnValues
const logger = rootLogger.child({ const logger = rootLogger.child({
@ -98,7 +98,7 @@ function processSignatureRequestsBuilder(config) {
to: config.homeBridgeAddress to: config.homeBridgeAddress
}) })
}) })
) .map(promise => limit(promise))
await Promise.all(callbacks) await Promise.all(callbacks)
return txToSend return txToSend

@ -27,8 +27,8 @@ function processTransfersBuilder(config) {
} }
rootLogger.debug(`Processing ${transfers.length} Transfer events`) rootLogger.debug(`Processing ${transfers.length} Transfer events`)
const callbacks = transfers.map(transfer => const callbacks = transfers
limit(async () => { .map(transfer => async () => {
const { from, value } = transfer.returnValues const { from, value } = transfer.returnValues
const logger = rootLogger.child({ const logger = rootLogger.child({
@ -79,7 +79,7 @@ function processTransfersBuilder(config) {
to: config.homeBridgeAddress to: config.homeBridgeAddress
}) })
}) })
) .map(promise => limit(promise))
await Promise.all(callbacks) await Promise.all(callbacks)
return txToSend return txToSend

@ -1,10 +1,6 @@
const assert = require('assert') const assert = require('assert')
const Web3Utils = require('web3-utils') const Web3Utils = require('web3-utils')
const { strip0x } = require('../../../commons')
// strips leading "0x" if present
function strip0x(input) {
return input.replace(/^0x/, '')
}
function createMessage({ recipient, value, transactionHash, bridgeAddress, expectedMessageLength }) { function createMessage({ recipient, value, transactionHash, bridgeAddress, expectedMessageLength }) {
recipient = strip0x(recipient) recipient = strip0x(recipient)
@ -63,8 +59,32 @@ function signatureToVRS(signature) {
return { v, r, s } 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 = { module.exports = {
createMessage, createMessage,
parseMessage, parseMessage,
signatureToVRS signatureToVRS,
signatureToVRSAMB,
packSignatures
} }

@ -21,6 +21,9 @@ const processSignatureRequests = require('./events/processSignatureRequests')(co
const processCollectedSignatures = require('./events/processCollectedSignatures')(config) const processCollectedSignatures = require('./events/processCollectedSignatures')(config)
const processAffirmationRequests = require('./events/processAffirmationRequests')(config) const processAffirmationRequests = require('./events/processAffirmationRequests')(config)
const processTransfers = require('./events/processTransfers')(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 ZERO = toBN(0)
const ONE = toBN(1) const ONE = toBN(1)
@ -97,6 +100,12 @@ function processEvents(events) {
case 'erc-erc-affirmation-request': case 'erc-erc-affirmation-request':
case 'erc-native-affirmation-request': case 'erc-native-affirmation-request':
return processTransfers(events) 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: default:
return [] return []
} }