From 7f575238e12449b2ae2618eb782c4fb7ecdd2079 Mon Sep 17 00:00:00 2001 From: Kirill Fedoseev Date: Thu, 14 Nov 2019 13:29:58 +0300 Subject: [PATCH] Support of closing epoch in oracle scripts --- src/oracle/ethWatcher/ethWatcher.js | 36 ++++++++- src/oracle/proxy/contractsAbi.js | 4 + src/oracle/proxy/index.js | 27 ++++++- src/oracle/proxy/utils.js | 6 +- src/oracle/tss-sign/signer.js | 42 ++++++++-- src/oracle/tss-sign/tx.js | 121 ++++++++++++++++------------ 6 files changed, 171 insertions(+), 65 deletions(-) diff --git a/src/oracle/ethWatcher/ethWatcher.js b/src/oracle/ethWatcher/ethWatcher.js index a37d412..18f0491 100644 --- a/src/oracle/ethWatcher/ethWatcher.js +++ b/src/oracle/ethWatcher/ethWatcher.js @@ -21,6 +21,8 @@ const bridgeAbi = [ 'event NewEpochCancelled(uint indexed epoch)', 'event NewFundsTransfer(uint indexed oldEpoch, uint indexed newEpoch)', 'event EpochStart(uint indexed epoch, uint x, uint y)', + 'event EpochClose(uint indexed epoch)', + 'event ForceSign()', 'function getThreshold(uint epoch) view returns (uint)', 'function getParties(uint epoch) view returns (uint)', 'function getRangeSize() view returns (uint)', @@ -160,7 +162,6 @@ async function sendSign(event, transactionHash) { } async function sendStartSign() { - redisTx.incr(`foreignNonce${epoch}`) signQueue.send({ epoch, blockNumber, @@ -169,6 +170,7 @@ async function sendStartSign() { parties: (await bridge.getParties(epoch)).toNumber() }) foreignNonce[epoch] += 1 + redisTx.incr(`foreignNonce${epoch}`) } async function processEpochStart(event) { @@ -186,6 +188,19 @@ async function processEpochStart(event) { foreignNonce[epoch] = 0 } +async function sendEpochClose() { + logger.debug(`Consumed epoch ${epoch} close event`) + signQueue.send({ + closeEpoch: epoch, + blockNumber, + nonce: foreignNonce[epoch], + threshold: (await bridge.getThreshold(epoch)).toNumber(), + parties: (await bridge.getParties(epoch)).toNumber() + }) + foreignNonce[epoch] += 1 + redisTx.incr(`foreignNonce${epoch}`) +} + async function initialize() { channel = await connectRabbit(RABBITMQ_URL) exchangeQueue = await assertQueue(channel, 'exchangeQueue') @@ -264,6 +279,8 @@ async function loop() { })) for (let curBlockNumber = blockNumber, i = 0; curBlockNumber <= endBlock; curBlockNumber += 1) { + const rangeOffset = (curBlockNumber + 1 - epochStart) % rangeSize + const rangeStart = curBlockNumber - (rangeOffset || rangeSize) let epochTimeUpdated = false while (i < bridgeEvents.length && bridgeEvents[i].blockNumber === curBlockNumber) { const event = bridge.interface.parseLog(bridgeEvents[i]) @@ -306,6 +323,19 @@ async function loop() { epoch }) break + case 'EpochClose': + if (isCurrentValidator) { + await sendEpochClose() + } + break + case 'ForceSign': + if (isCurrentValidator && lastTransactionBlockNumber > rangeStart) { + logger.debug('Consumed force sign event') + lastTransactionBlockNumber = 0 + redisTx.set('lastTransactionBlockNumber', 0) + await sendStartSign() + } + break default: logger.warn('Unknown event %o', event) } @@ -320,10 +350,10 @@ async function loop() { }) } - if ((curBlockNumber + 1 - epochStart) % rangeSize === 0) { + if (rangeOffset === 0) { logger.info('Reached end of the current block range') - if (lastTransactionBlockNumber > curBlockNumber - rangeSize) { + if (isCurrentValidator && lastTransactionBlockNumber > curBlockNumber - rangeSize) { logger.info('Sending message to start signature generation for the ended range') await sendStartSign() } diff --git a/src/oracle/proxy/contractsAbi.js b/src/oracle/proxy/contractsAbi.js index 312681c..6f80832 100644 --- a/src/oracle/proxy/contractsAbi.js +++ b/src/oracle/proxy/contractsAbi.js @@ -14,17 +14,21 @@ const bridgeAbi = [ 'function getNextThreshold() view returns (uint)', 'function getValidators() view returns (address[])', 'function getNextValidators() view returns (address[])', + 'function getCloseEpoch() view returns (bool)', + 'function getNextCloseEpoch() view returns (bool)', 'function status() view returns (uint)', 'function votesCount(bytes32) view returns (uint)', 'function getNextPartyId(address a) view returns (uint)', 'function confirmKeygen(uint x, uint y)', 'function confirmFundsTransfer()', + 'function confirmCloseEpoch()', 'function startVoting()', 'function voteStartKeygen()', 'function voteCancelKeygen()', 'function voteAddValidator(address validator)', 'function voteRemoveValidator(address validator)', 'function voteChangeThreshold(uint threshold)', + 'function voteChangeCloseEpoch(bool closeEpoch)', 'function transfer(bytes32 hash, address to, uint value)' ] const sharedDbAbi = [ diff --git a/src/oracle/proxy/index.js b/src/oracle/proxy/index.js index 7546cbb..77e9bee 100644 --- a/src/oracle/proxy/index.js +++ b/src/oracle/proxy/index.js @@ -189,6 +189,14 @@ async function confirmFundsTransfer(req, res) { logger.debug('Confirm funds transfer end') } +async function confirmCloseEpoch(req, res) { + logger.debug('Confirm close epoch call') + const query = bridge.interface.functions.confirmCloseEpoch.encode([]) + await homeSendQuery(query) + res.send() + logger.debug('Confirm close epoch end') +} + async function sendVote(query, req, res, waitFlag = false) { try { const sentQuery = await homeSendQuery(query) @@ -257,6 +265,14 @@ async function voteChangeThreshold(req, res) { } } +async function voteChangeCloseEpoch(req, res) { + if (req.params.closeEpoch === 'true' || req.params.closeEpoch === 'false') { + logger.info('Voting for changing close epoch') + const query = bridge.interface.functions.voteChangeCloseEpoch.encode([req.params.closeEpoch === 'true']) + await sendVote(query, req, res) + } +} + async function voteRemoveValidator(req, res) { if (ethers.utils.isHexString(req.params.validator, 20)) { logger.info('Voting for removing validator') @@ -298,14 +314,17 @@ async function info(req, res) { logger.debug('Info start') try { const [ - x, y, epoch, rangeSize, nextRangeSize, epochStartBlock, foreignNonce, nextEpoch, - threshold, nextThreshold, validators, nextValidators, status, homeBalance + x, y, epoch, rangeSize, nextRangeSize, closeEpoch, nextCloseEpoch, epochStartBlock, + foreignNonce, nextEpoch, threshold, nextThreshold, validators, nextValidators, status, + homeBalance ] = await Promise.all([ bridge.getX().then((value) => new BN(value).toString(16)), bridge.getY().then((value) => new BN(value).toString(16)), bridge.epoch().then(boundX), bridge.getRangeSize().then(boundX), bridge.getNextRangeSize().then(boundX), + bridge.getCloseEpoch(), + bridge.getNextCloseEpoch(), bridge.getStartBlock().then(boundX), bridge.getNonce().then(boundX), bridge.nextEpoch().then(boundX), @@ -338,6 +357,8 @@ async function info(req, res) { nextEpoch, threshold, nextThreshold, + closeEpoch, + nextCloseEpoch, homeBridgeAddress: HOME_BRIDGE_ADDRESS, foreignBridgeAddress: foreignAddress, foreignNonce, @@ -371,6 +392,7 @@ app.post('/signupsign', signupSign) app.post('/confirmKeygen', confirmKeygen) app.post('/confirmFundsTransfer', confirmFundsTransfer) +app.post('/confirmCloseEpoch', confirmCloseEpoch) app.post('/transfer', transfer) votesProxyApp.get('/vote/startVoting', voteStartVoting) @@ -379,6 +401,7 @@ votesProxyApp.get('/vote/cancelKeygen', voteCancelKeygen) votesProxyApp.get('/vote/addValidator/:validator', voteAddValidator) votesProxyApp.get('/vote/removeValidator/:validator', voteRemoveValidator) votesProxyApp.get('/vote/changeThreshold/:threshold', voteChangeThreshold) +votesProxyApp.get('/vote/changeCloseEpoch/:closeEpoch', voteChangeCloseEpoch) votesProxyApp.get('/info', info) async function main() { diff --git a/src/oracle/proxy/utils.js b/src/oracle/proxy/utils.js index 72bd8e9..9bbdb58 100644 --- a/src/oracle/proxy/utils.js +++ b/src/oracle/proxy/utils.js @@ -11,10 +11,12 @@ function decodeStatus(status) { case 0: return 'ready' case 1: - return 'voting' + return 'closing_epoch' case 2: - return 'keygen' + return 'voting' case 3: + return 'keygen' + case 4: return 'funds_transfer' default: return 'unknown_state' diff --git a/src/oracle/tss-sign/signer.js b/src/oracle/tss-sign/signer.js index 1123998..69181da 100644 --- a/src/oracle/tss-sign/signer.js +++ b/src/oracle/tss-sign/signer.js @@ -21,6 +21,7 @@ const SIGN_NONCE_CHECK_INTERVAL = parseInt(process.env.SIGN_NONCE_CHECK_INTERVAL const SEND_TIMEOUT = parseInt(process.env.SEND_TIMEOUT, 10) const httpClient = axios.create({ baseURL: FOREIGN_URL }) +const proxyClient = axios.create({ baseURL: PROXY_URL }) const SIGN_OK = 0 const SIGN_NONCE_INTERRUPT = 1 @@ -67,8 +68,12 @@ function restart(req, res) { } } -function confirmFundsTransfer() { - exec.execSync(`curl -X POST -H "Content-Type: application/json" "${PROXY_URL}/confirmFundsTransfer"`, { stdio: 'pipe' }) +async function confirmFundsTransfer() { + await proxyClient.post('/confirmFundsTransfer') +} + +async function confirmCloseEpoch() { + await proxyClient.post('/confirmCloseEpoch') } function getAccountFromFile(file) { @@ -216,10 +221,10 @@ async function main() { logger.info('Consumed sign event: %o', data) const { - nonce, epoch, newEpoch, parties, threshold + nonce, epoch, newEpoch, parties, threshold, closeEpoch } = data - const keysFile = `/keys/keys${epoch}.store` + const keysFile = `/keys/keys${epoch || closeEpoch}.store` const { address: from, publicKey } = getAccountFromFile(keysFile) if (from === '') { logger.info('No keys found, acking message') @@ -236,7 +241,34 @@ async function main() { attempt = 1 - if (!newEpoch) { + if (closeEpoch) { + while (true) { + logger.info(`Building corresponding account flags transaction, nonce ${nonce}`) + + const tx = new Transaction({ + from, + accountNumber: account.account_number, + sequence: nonce, + flags: 0x01, + memo: `Attempt ${attempt}` + }) + + const hash = sha256(tx.getSignBytes()) + logger.info(`Starting signature generation for transaction hash ${hash}`) + const signResult = await sign(keysFile, hash, tx, publicKey, from) + + if (signResult === SIGN_OK || signResult === SIGN_NONCE_INTERRUPT) { + await confirmCloseEpoch() + break + } + + // signer either failed, or timed out after parties signup + attempt = nextAttempt || attempt + 1 + nextAttempt = null + logger.warn(`Sign failed, starting next attempt ${attempt}`) + await delay(1000) + } + } else if (!newEpoch) { const exchanges = await getExchangeMessages(nonce) const exchangesData = exchanges.map((exchangeMsg) => JSON.parse(exchangeMsg.content)) diff --git a/src/oracle/tss-sign/tx.js b/src/oracle/tss-sign/tx.js index ee6cfcc..032d4e8 100644 --- a/src/oracle/tss-sign/tx.js +++ b/src/oracle/tss-sign/tx.js @@ -12,70 +12,84 @@ const BNB_ASSET = 'BNB' class Transaction { constructor(options) { const { - from, accountNumber, sequence, recipients, asset, memo = '' + from, accountNumber, sequence, recipients, asset, memo = '', flags } = options - const totalTokens = recipients.reduce( - (sum, { tokens }) => sum.plus(new BN(tokens || 0)), new BN(0) - ) - const totalBnbs = recipients.reduce( - (sum, { bnbs }) => sum.plus(new BN(bnbs || 0)), new BN(0) - ) - const senderCoins = [] - if (asset && totalTokens.isGreaterThan(0)) { - senderCoins.push({ - denom: asset, - amount: totalTokens.multipliedBy(10 ** 8).toNumber() - }) - } - if (totalBnbs.isGreaterThan(0)) { - senderCoins.push({ - denom: BNB_ASSET, - amount: totalBnbs.multipliedBy(10 ** 8).toNumber() - }) - } - senderCoins.sort((a, b) => a.denom > b.denom) + let msg + if (flags) { + msg = { + from: crypto.decodeAddress(from), + flags, + msgType: 'NewOrderMsg' // until 'SetAccountFlagsMsg' is not available + } - const inputs = [{ - address: from, - coins: senderCoins - }] - const outputs = recipients.map(({ to, tokens, bnbs }) => { - const receiverCoins = [] - if (asset && tokens) { - receiverCoins.push({ + this.signMsg = { + flags, + from + } + } else { + const totalTokens = recipients.reduce( + (sum, { tokens }) => sum.plus(new BN(tokens || 0)), new BN(0) + ) + const totalBnbs = recipients.reduce( + (sum, { bnbs }) => sum.plus(new BN(bnbs || 0)), new BN(0) + ) + const senderCoins = [] + if (asset && totalTokens.isGreaterThan(0)) { + senderCoins.push({ denom: asset, - amount: new BN(tokens).multipliedBy(10 ** 8).toNumber() + amount: totalTokens.multipliedBy(10 ** 8).toNumber() }) } - if (bnbs) { - receiverCoins.push({ + if (totalBnbs.isGreaterThan(0)) { + senderCoins.push({ denom: BNB_ASSET, - amount: new BN(bnbs).multipliedBy(10 ** 8).toNumber() + amount: totalBnbs.multipliedBy(10 ** 8).toNumber() }) } - receiverCoins.sort((a, b) => a.denom > b.denom) - return { - address: to, - coins: receiverCoins + senderCoins.sort((a, b) => a.denom > b.denom) + + const inputs = [{ + address: from, + coins: senderCoins + }] + const outputs = recipients.map(({ to, tokens, bnbs }) => { + const receiverCoins = [] + if (asset && tokens) { + receiverCoins.push({ + denom: asset, + amount: new BN(tokens).multipliedBy(10 ** 8).toNumber() + }) + } + if (bnbs) { + receiverCoins.push({ + denom: BNB_ASSET, + amount: new BN(bnbs).multipliedBy(10 ** 8).toNumber() + }) + } + receiverCoins.sort((a, b) => a.denom > b.denom) + return { + address: to, + coins: receiverCoins + } + }) + + msg = { + inputs: inputs.map((x) => ({ + ...x, + address: crypto.decodeAddress(x.address) + })), + outputs: outputs.map((x) => ({ + ...x, + address: crypto.decodeAddress(x.address) + })), + msgType: 'MsgSend' } - }) - const msg = { - inputs: inputs.map((x) => ({ - ...x, - address: crypto.decodeAddress(x.address) - })), - outputs: outputs.map((x) => ({ - ...x, - address: crypto.decodeAddress(x.address) - })), - msgType: 'MsgSend' - } - - this.signMsg = { - inputs, - outputs + this.signMsg = { + inputs, + outputs + } } this.tx = new TransactionBnc({ @@ -109,6 +123,7 @@ class Transaction { sequence: this.tx.sequence }] return this.tx.serialize() + .replace(/ce6dc043/, 'bea6e301') // until 'SetAccountFlagsMsg' is not available } }