diff --git a/native-to-erc20/contracts/signatures.md b/native-to-erc20/contracts/signatures.md new file mode 100644 index 0000000..79ea9d4 --- /dev/null +++ b/native-to-erc20/contracts/signatures.md @@ -0,0 +1,14 @@ +# Function signatures +executeSignatures(uint8[],bytes32[],bytes32[],bytes): 0x232a2c1d + +submitSignature(bytes signature, bytes message): 0x630cea8e + +submitSignatureOfMessageWithUnknownLength(bytes,bytes): 0x9b5a5489 + + +# Event signatures + +CollectedSignatures(address,bytes32,uint256): 0x41555740 + + +done using: https://piyolab.github.io/playground/ethereum/getEncodedFunctionSignature/ diff --git a/native-to-erc20/contracts/test/contracts/MessageWrapper.sol b/native-to-erc20/contracts/test/contracts/MessageWrapper.sol new file mode 100644 index 0000000..14f0175 --- /dev/null +++ b/native-to-erc20/contracts/test/contracts/MessageWrapper.sol @@ -0,0 +1,74 @@ +pragma solidity 0.4.24; + +contract MessageWrapper { + + function validatorRecover( + bytes _message, + uint8 _v, + bytes32 _r, + bytes32 _s + ) public returns (address recoveredAddress) { + bytes32 hash = hashMessageOfUnknownLength(_message); + recoveredAddress = ecrecover(hash, _v, _r, _s); + } + + function hashMessage(bytes message) public pure returns (bytes32) { + bytes memory prefix = "\x19Ethereum Signed Message:\n"; + // message is always 84 length + string memory msgLength = "104"; + return keccak256(abi.encodePacked(prefix, msgLength, message)); + } + + function hashMessageOfUnknownLength(bytes message) public pure returns (bytes32) { + bytes memory prefix = "\x19Ethereum Signed Message:\n"; + uint256 lengthOffset; + uint256 length; + assembly { + // The first word of a string is its length + length := mload(message) + // The beginning of the base-10 message length in the prefix + lengthOffset := add(prefix, 57) + } + uint256 lengthLength = 0; + // The divisor to get the next left-most message length digit + uint256 divisor = 100000; + // Move one digit of the message length to the right at a time + while (divisor != 0) { + // The place value at the divisor + uint256 digit = length / divisor; + if (digit == 0) { + // Skip leading zeros + if (lengthLength == 0) { + divisor /= 10; + continue; + } + } + // Found a non-zero digit or non-leading zero digit + lengthLength++; + // Remove this digit from the message length's current value + length -= digit * divisor; + // Shift our base-10 divisor over + divisor /= 10; + // Convert the digit to its ASCII representation (man ascii) + digit += 0x30; + // Move to the next character and write the digit + lengthOffset++; + assembly { + mstore8(lengthOffset, digit) + } + } + // The null string requires exactly 1 zero (unskip 1 leading 0) + if (lengthLength == 0) { + lengthLength = 1 + 0x19 + 1; + } else { + lengthLength += 1 + 0x19; + } + // Truncate the tailing zeros from the prefix + assembly { + mstore(prefix, lengthLength) + } + return keccak256(prefix, message); + } + + +} \ No newline at end of file diff --git a/native-to-erc20/oracle/src/events/processCollectedSignatures/createRawTx.js b/native-to-erc20/oracle/src/events/processCollectedSignatures/createRawTx.js new file mode 100644 index 0000000..718a3ac --- /dev/null +++ b/native-to-erc20/oracle/src/events/processCollectedSignatures/createRawTx.js @@ -0,0 +1,84 @@ +const { HttpListProviderError } = require('http-list-provider') +const Web3 = require('web3') +const { signatureToVRS } = require('../../utils/message') +const estimateGas = require('./estimateGas') +const { + AlreadyProcessedError, + IncompatibleContractError, + InvalidValidatorError +} = require('../../utils/errors') + +const web3 = new Web3() + +const createRawTx = async ({ homeBridge, foreignBridge, logger, colSignature, foreignValidatorContract }) => { + const { messageHash, NumberOfCollectedSignatures } = colSignature.returnValues + + logger.info(`Processing CollectedSignatures ${colSignature.transactionHash}`) + const message = await homeBridge.methods.message(messageHash).call() + const expectedMessageLength = await homeBridge.methods.requiredMessageLength().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) + }) + // let index = 0 + // debugger + // console.log({ NumberOfCollectedSignatures }) + + await Promise.all(signaturePromises) + + let gasEstimate, methodName + try { + logger.debug('Estimate gas') + const result = await estimateGas({ + foreignBridge, + validatorContract: foreignValidatorContract, + v, + r, + s, + message, + numberOfCollectedSignatures: NumberOfCollectedSignatures, + expectedMessageLength + }) + logger.info({ result }, 'Gas estimated') + gasEstimate = result.gasEstimate + methodName = result.methodName + } 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[methodName](v, r, s, message).encodeABI() + return { + data, + gasEstimate, + transactionReference: colSignature.transactionHash, + to: foreignBridge.options.address + } +} + +module.exports = createRawTx diff --git a/native-to-erc20/oracle/src/events/processCollectedSignatures/estimateGas.js b/native-to-erc20/oracle/src/events/processCollectedSignatures/estimateGas.js index 7893990..4774879 100644 --- a/native-to-erc20/oracle/src/events/processCollectedSignatures/estimateGas.js +++ b/native-to-erc20/oracle/src/events/processCollectedSignatures/estimateGas.js @@ -24,9 +24,14 @@ async function estimateGas ({ expectedMessageLength }) { try { + debugger let gasEstimate, methodName if (message && message.length !== 2 + 2 * expectedMessageLength) { /* see ../../utils/message.js#createMessage */ logger.debug('foreignBridge.methods.executeNewSetSignatures') + console.log({ + v, r, s, message + }) + // gasEstimate = 3000000 gasEstimate = await foreignBridge.methods .executeNewSetSignatures(v, r, s, message) .estimateGas() @@ -63,6 +68,7 @@ async function estimateGas ({ } // check if all the signatures were made by validators + const 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') @@ -71,6 +77,11 @@ async function estimateGas ({ if (!isValidator) { throw new InvalidValidatorError(`Message signed by ${address} that is not a validator`) } + if (validators[address]) { + logger.error('validator signed twice', { address }) + throw new Error('Validator signed twice') + } + validators[address] = true } logger.error(e) diff --git a/native-to-erc20/oracle/src/events/processInitiateChange/index.js b/native-to-erc20/oracle/src/events/processInitiateChange/index.js index 3da025a..d5563af 100644 --- a/native-to-erc20/oracle/src/events/processInitiateChange/index.js +++ b/native-to-erc20/oracle/src/events/processInitiateChange/index.js @@ -72,7 +72,7 @@ function processInitiateChangeBuilder (config) { const foreignBridgeVersion = await foreignBridgeStorage.methods.version().call() const message = createNewSetMessage({ foreignBridgeVersion, - newSet: newSet, + newSet: [...newSet], transactionHash: initiateChange.transactionHash, blockNumber, bridgeAddress: foreignBridgeAddress diff --git a/native-to-erc20/oracle/src/tx/cycle.js b/native-to-erc20/oracle/src/tx/cycle.js new file mode 100644 index 0000000..b82ffb2 --- /dev/null +++ b/native-to-erc20/oracle/src/tx/cycle.js @@ -0,0 +1,219 @@ +const { getEvents } = require('./web3') +const { web3Home, web3Foreign } = require('../services/web3') +const { sendTx } = require('./sendTx') +const { parseMessage, parseNewSetMessage } = require('../utils/message') +const createRawTx = require('../events/processCollectedSignatures/createRawTx') +const { getNonce, getChainId } = require('./web3') +const GasPrice = require('../services/gasPrice') +const { + addExtraGas, + privateKeyToAddress +} = require('../utils/utils') +const { EXTRA_GAS_PERCENTAGE } = require('../utils/constants') + +const processInitiateChangeBuilder = require('../events/processInitiateChange') +const foreignBridgeValidatorsABI = require('../../abis/ForeignBridgeValidators.abi') +const rootLogger = require('../services/logger') + +const { VALIDATOR_ADDRESS_PRIVATE_KEY } = process.env + +const isNewSet = (message) => !!message.newSet +const isNotNewSet = (message) => !isNewSet(message) + +const isLengthExpected = (unparsedMessage, expectedMessageLength) => unparsedMessage.length === 2 + 2 * expectedMessageLength + +const parseGenericMessage = (unparsedMessage, expectedMessageLength) => { + if (isLengthExpected(unparsedMessage, expectedMessageLength)) { + return parseMessage(unparsedMessage) + } else { + return parseNewSetMessage(unparsedMessage, expectedMessageLength) + } +} + +/** + * Fetch the messages from Fuse network that intended to be relayed to Ethereum. + * The messages can represent updates to the validators set, minting on EoC, or bridge transfers + * @param {number} fromBlock - from blocknumber to look for messages + * @param {number} toBlock - to blocknumber block to look messages + * @param {bool} isRelayedFilter - if false will filter already relayed messages + * @param {bool} isNewSetFilter - if true will filter only new validator set updates + * @param {string} event - if specifed fetch other events like SignedForUserRequest or InitiateChange + */ + +const getMessages = async ({ fromBlock, toBlock, isRelayedFilter, isNewSetFilter, event }) => { + const config = require('../../config/collected-signatures-watcher.config') + toBlock = toBlock || await web3Home.eth.getBlockNumber() + + console.log({ fromBlock, toBlock, isRelayedFilter }) + const homeBridge = new web3Home.eth.Contract(config.eventAbi, config.homeBridgeAddress) + + const expectedMessageLength = await homeBridge.methods.requiredMessageLength().call() + // console.log(config.eventFilter) + const events = await getEvents({ + contract: homeBridge, + event: event || config.event, + fromBlock, + toBlock, + filter: config.eventFilter + }) + + console.log({ events }) + + if (event === 'SignedForUserRequest' || event === 'InitiateChange') { + events.forEach(event => console.log(event.returnValues)) + return events + } + + let messages = [] + for (const event of events) { + const { messageHash } = event.returnValues + const unparsedMessage = await homeBridge.methods.message(messageHash).call() + const message = parseGenericMessage(unparsedMessage, expectedMessageLength) + // console.log({ unparsedMessage }) + message.event = event + messages.push(message) + } + + if (typeof isNewSetFilter !== 'undefined') { + const messageFilter = isNewSetFilter ? isNewSet : isNotNewSet + messages = messages.filter(messageFilter) + } + + console.log({ numberOfMessages: messages.length }) + + if (typeof isRelayedFilter === 'undefined') { + messages.forEach(message => { + console.log({ + message + }) + }) + return messages + } + + const foreignBridge = new web3Foreign.eth.Contract(config.foreignBridgeAbi, config.foreignBridgeAddress) + const filteredMessages = [] + for (const message of messages) { + const isRelayed = await foreignBridge.methods.relayedMessages(message.txHash).call() + if (isRelayed === isRelayedFilter) { + filteredMessages.push(message) + } + } + + console.log({ numberOfFilteredMessages: filteredMessages.length }) + console.log('filtered messages:') + filteredMessages.forEach(message => { + console.log({ + message + }) + }) + + return filteredMessages +} + +/** + * Relay the messages from Fuse network to Ethereum. + * The messages can represent updates to the validators set, minting on EoC, or bridge transfers + * @param {number} fromBlock - from blocknumber to look for messages + * @param {number} toBlock - to blocknumber block to look messages + * @param {bool} execute - on true will try to to send tx on Ethereum, otherwise acts like a dry run + * @param {bool} isNewSetFilter - if true will filter only new validator set updates + * @param {number} limit - limit number of relayed messages. It's advice to set to 1 when executing. + */ +const relayMessages = async ({ fromBlock, toBlock, execute, isNewSetFilter, limit }) => { + const config = require('../../config/collected-signatures-watcher.config') + + const messages = (await getMessages({ fromBlock, toBlock, isNewSetFilter, isRelayedFilter: false })).slice(0, limit) + const homeBridge = new web3Home.eth.Contract(config.eventAbi, config.homeBridgeAddress) + const foreignBridge = new web3Foreign.eth.Contract(config.foreignBridgeAbi, config.foreignBridgeAddress) + + const foreignValidatorContractAddress = await foreignBridge.methods.validatorContract().call() + const foreignValidatorContract = new web3Foreign.eth.Contract(foreignBridgeValidatorsABI, foreignValidatorContractAddress) + + const VALIDATOR_ADDRESS = privateKeyToAddress(VALIDATOR_ADDRESS_PRIVATE_KEY) + GasPrice.start('foreign') + + let nonce = await getNonce(web3Foreign, VALIDATOR_ADDRESS) + const chainId = await getChainId(web3Foreign) + + for (const message of messages) { + console.log(`Sending the tx for ${message.txHash} with nonce ${nonce}`) + console.log({ message }) + try { + debugger + const job = await createRawTx({ homeBridge, foreignBridge, logger: rootLogger, colSignature: message.event, foreignValidatorContract }) + console.log({ job }) + + const gasPrice = GasPrice.getPrice() + + const gasLimit = addExtraGas(job.gasEstimate, EXTRA_GAS_PERCENTAGE) + + const args = { + chain: 'foreign', + data: job.data, + nonce, + gasPrice, + amount: '0', + gasLimit: gasLimit.toString(), + privateKey: VALIDATOR_ADDRESS_PRIVATE_KEY, + to: job.to, + chainId + } + console.log({ args }) + + if (execute) { + const txHash = await sendTx({ ...args, web3: web3Foreign }).catch(error => { + console.log({ error }) + }) + console.log({ txHash }) + nonce++ + } + } catch (error) { + if (execute) { + throw error + } + } + } +} + +const sendInitiateChange = async ({ fromBlock, toBlock, execute }) => { + toBlock = toBlock || await web3Home.eth.getBlockNumber() + console.log({ fromBlock, toBlock }) + + const config = require('../../config/initiate-change-watcher.config') + const eventContract = new web3Home.eth.Contract(config.eventAbi, '0x3014ca10b91cb3D0AD85fEf7A3Cb95BCAc9c0f79') + debugger + const events = await getEvents({ + contract: eventContract, + event: config.event, + fromBlock, + toBlock, + filter: config.eventFilter + }) + // const events = await getMessages({ fromBlock, toBlock, event: 'InitiateChange' }) + // console.log({ events }) + console.log(events.length) + + if (execute) { + const processInitiateChange = processInitiateChangeBuilder(config) + + await processInitiateChange( + [events[0]], + config.homeBridgeAddress, + config.foreignBridgeAddress + ) + } +} + +// call example: +// getMessages({ fromBlock: 5969512, isRelayedFilter: false, isNewSetFilter: true }) + +// call example: +relayMessages({ fromBlock: 5969512, toBlock: 7213700, isRelayedFilter: false, isNewSetFilter: false, execute: false }) + +// sendInitiateChange({ fromBlock: 5831273, toBlock: 6000000, execute: false }) + +module.exports = { + getMessages, + relayMessages, + sendInitiateChange +}