2019-07-04 02:32:01 -07:00
|
|
|
const exec = require('child_process')
|
|
|
|
const fs = require('fs')
|
2019-07-07 12:58:35 -07:00
|
|
|
const BN = require('bignumber.js')
|
2019-11-01 11:43:25 -07:00
|
|
|
const axios = require('axios')
|
2019-07-15 12:41:02 -07:00
|
|
|
const express = require('express')
|
2019-07-04 02:32:01 -07:00
|
|
|
|
2019-10-06 03:36:29 -07:00
|
|
|
const logger = require('./logger')
|
2019-10-08 09:06:10 -07:00
|
|
|
const { connectRabbit, assertQueue } = require('./amqp')
|
2019-10-08 10:45:28 -07:00
|
|
|
const { publicKeyToAddress, sha256 } = require('./crypto')
|
2019-11-03 02:54:34 -08:00
|
|
|
const { delay, retry } = require('./wait')
|
2019-10-06 03:36:29 -07:00
|
|
|
|
2019-11-01 11:43:25 -07:00
|
|
|
const Transaction = require('./tx')
|
|
|
|
|
2019-07-15 12:41:02 -07:00
|
|
|
const app = express()
|
|
|
|
|
2019-11-01 11:43:25 -07:00
|
|
|
const {
|
|
|
|
RABBITMQ_URL, FOREIGN_URL, PROXY_URL, FOREIGN_ASSET
|
|
|
|
} = process.env
|
2019-11-05 10:14:22 -08:00
|
|
|
const SIGN_ATTEMPT_TIMEOUT = parseInt(process.env.SIGN_ATTEMPT_TIMEOUT, 10)
|
|
|
|
const SIGN_NONCE_CHECK_INTERVAL = parseInt(process.env.SIGN_NONCE_CHECK_INTERVAL, 10)
|
|
|
|
const SEND_TIMEOUT = parseInt(process.env.SEND_TIMEOUT, 10)
|
2019-07-04 02:32:01 -07:00
|
|
|
|
|
|
|
const httpClient = axios.create({ baseURL: FOREIGN_URL })
|
2019-11-14 02:29:58 -08:00
|
|
|
const proxyClient = axios.create({ baseURL: PROXY_URL })
|
2019-07-04 02:32:01 -07:00
|
|
|
|
2019-11-05 10:14:22 -08:00
|
|
|
const SIGN_OK = 0
|
|
|
|
const SIGN_NONCE_INTERRUPT = 1
|
|
|
|
const SIGN_FAILED = 2
|
|
|
|
|
2019-07-15 12:41:02 -07:00
|
|
|
let nextAttempt = null
|
|
|
|
let cancelled
|
2019-10-11 02:39:40 -07:00
|
|
|
let ready = false
|
2019-10-14 13:40:55 -07:00
|
|
|
let exchangeQueue
|
|
|
|
let channel
|
2019-07-15 12:41:02 -07:00
|
|
|
|
2019-11-01 11:43:25 -07:00
|
|
|
async function getExchangeMessages(nonce) {
|
|
|
|
logger.debug('Getting exchange messages')
|
|
|
|
const messages = []
|
|
|
|
while (true) {
|
|
|
|
const msg = await exchangeQueue.get()
|
|
|
|
if (msg === false) {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
const data = JSON.parse(msg.content)
|
|
|
|
logger.debug('Got message %o', data)
|
|
|
|
if (data.nonce !== nonce) {
|
|
|
|
channel.nack(msg, false, true)
|
|
|
|
break
|
|
|
|
}
|
|
|
|
messages.push(msg)
|
|
|
|
}
|
|
|
|
logger.debug(`Found ${messages.length} messages`)
|
|
|
|
return messages
|
|
|
|
}
|
|
|
|
|
2019-11-05 10:14:22 -08:00
|
|
|
function killSigner() {
|
2019-11-01 11:43:25 -07:00
|
|
|
exec.execSync('pkill gg18_sign || true')
|
2019-11-05 10:14:22 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
function restart(req, res) {
|
|
|
|
if (/^[0-9]+$/.test(req.params.attempt)) {
|
|
|
|
logger.info(`Manual cancelling current sign attempt, starting ${req.params.attempt} attempt`)
|
|
|
|
nextAttempt = parseInt(req.params.attempt, 10)
|
|
|
|
killSigner()
|
|
|
|
cancelled = true
|
|
|
|
res.send('Done')
|
|
|
|
}
|
2019-11-01 11:43:25 -07:00
|
|
|
}
|
|
|
|
|
2019-11-24 07:20:56 -08:00
|
|
|
async function confirmFundsTransfer(epoch) {
|
|
|
|
await proxyClient.post('/confirmFundsTransfer', {
|
|
|
|
epoch
|
|
|
|
})
|
2019-11-14 02:29:58 -08:00
|
|
|
}
|
|
|
|
|
2019-11-24 07:20:56 -08:00
|
|
|
async function confirmCloseEpoch(epoch) {
|
|
|
|
await proxyClient.post('/confirmCloseEpoch', {
|
|
|
|
epoch
|
|
|
|
})
|
2019-11-01 11:43:25 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
function getAccountFromFile(file) {
|
|
|
|
logger.debug(`Reading ${file}`)
|
|
|
|
if (!fs.existsSync(file)) {
|
|
|
|
logger.debug('No keys found, skipping')
|
|
|
|
return { address: '' }
|
|
|
|
}
|
|
|
|
const publicKey = JSON.parse(fs.readFileSync(file))[5]
|
|
|
|
return {
|
|
|
|
address: publicKeyToAddress(publicKey),
|
|
|
|
publicKey
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-03 02:54:34 -08:00
|
|
|
async function getAccount(address) {
|
2019-11-01 11:43:25 -07:00
|
|
|
logger.info(`Getting account ${address} data`)
|
2019-11-03 02:54:34 -08:00
|
|
|
const response = await retry(() => httpClient.get(`/api/v1/account/${address}`))
|
|
|
|
return response.data
|
2019-11-01 11:43:25 -07:00
|
|
|
}
|
|
|
|
|
2019-11-11 08:41:17 -08:00
|
|
|
async function getFee() {
|
|
|
|
logger.info('Getting fees')
|
|
|
|
const response = await retry(() => httpClient.get('/api/v1/fees'))
|
2019-11-25 06:25:53 -08:00
|
|
|
const multiTransferFee = response.data.find((fee) => fee.multi_transfer_fee).multi_transfer_fee
|
|
|
|
return new BN(multiTransferFee * 2).div(10 ** 8)
|
2019-11-11 08:41:17 -08:00
|
|
|
}
|
|
|
|
|
2019-11-01 11:43:25 -07:00
|
|
|
async function waitForAccountNonce(address, nonce) {
|
|
|
|
cancelled = false
|
|
|
|
logger.info(`Waiting for account ${address} to have nonce ${nonce}`)
|
|
|
|
while (!cancelled) {
|
|
|
|
const { sequence } = await getAccount(address)
|
|
|
|
if (sequence >= nonce) {
|
|
|
|
break
|
|
|
|
}
|
2019-11-05 10:14:22 -08:00
|
|
|
await delay(1000)
|
2019-11-01 11:43:25 -07:00
|
|
|
logger.debug('Waiting for needed account nonce')
|
|
|
|
}
|
|
|
|
logger.info('Account nonce is OK')
|
|
|
|
return !cancelled
|
|
|
|
}
|
|
|
|
|
2019-11-14 06:44:08 -08:00
|
|
|
async function sendTx(tx) {
|
|
|
|
while (true) {
|
|
|
|
try {
|
2019-11-14 10:48:32 -08:00
|
|
|
return await httpClient.post('/api/v1/broadcast?sync=true', tx, {
|
|
|
|
headers: {
|
|
|
|
'Content-Type': 'text/plain'
|
|
|
|
}
|
|
|
|
})
|
2019-11-14 06:44:08 -08:00
|
|
|
} catch (err) {
|
2019-11-14 10:48:32 -08:00
|
|
|
logger.trace('Error, response data %o', err.response.data)
|
2019-11-01 11:43:25 -07:00
|
|
|
if (err.response.data.message.includes('Tx already exists in cache')) {
|
|
|
|
logger.debug('Tx already exists in cache')
|
|
|
|
return true
|
|
|
|
}
|
2019-11-14 06:44:08 -08:00
|
|
|
if (err.response.data.message.includes(' < ')) {
|
2019-11-14 10:48:32 -08:00
|
|
|
logger.warn('Insufficient funds, waiting for funds')
|
2019-11-14 06:44:08 -08:00
|
|
|
await delay(60000)
|
|
|
|
} else {
|
|
|
|
logger.info('Something failed, restarting: %o', err.response)
|
|
|
|
await delay(10000)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2019-11-01 11:43:25 -07:00
|
|
|
}
|
|
|
|
|
2019-11-25 06:25:53 -08:00
|
|
|
function sign(keysFile, tx, publicKey, signerAddress) {
|
2019-11-05 10:14:22 -08:00
|
|
|
let restartTimeoutId
|
|
|
|
let nonceDaemonIntervalId
|
|
|
|
let nonceInterrupt = false
|
2019-11-25 06:25:53 -08:00
|
|
|
|
|
|
|
const hash = sha256(tx.getSignBytes())
|
|
|
|
logger.info(`Starting signature generation for transaction hash ${hash}`)
|
|
|
|
|
2019-11-01 11:43:25 -07:00
|
|
|
return new Promise((resolve) => {
|
|
|
|
const cmd = exec.execFile('./sign-entrypoint.sh', [PROXY_URL, keysFile, hash], async (error) => {
|
2019-11-07 06:06:12 -08:00
|
|
|
logger.trace('Sign entrypoint exited, %o', error)
|
2019-11-05 10:14:22 -08:00
|
|
|
clearInterval(nonceDaemonIntervalId)
|
|
|
|
clearTimeout(restartTimeoutId)
|
|
|
|
if (fs.existsSync('signature')) { // if signature was generated
|
2019-11-01 11:43:25 -07:00
|
|
|
logger.info('Finished signature generation')
|
|
|
|
const signature = JSON.parse(fs.readFileSync('signature'))
|
|
|
|
logger.debug('%o', signature)
|
|
|
|
|
|
|
|
logger.info('Building signed transaction')
|
|
|
|
const signedTx = tx.addSignature(publicKey, {
|
|
|
|
r: signature[1],
|
|
|
|
s: signature[3]
|
|
|
|
})
|
|
|
|
|
|
|
|
logger.info('Sending transaction')
|
|
|
|
logger.debug(signedTx)
|
|
|
|
await sendTx(signedTx)
|
2019-11-05 10:14:22 -08:00
|
|
|
// if nonce does not update in some time, cancel process, consider sign as failed
|
|
|
|
const sendTimeoutId = setTimeout(() => {
|
|
|
|
cancelled = true
|
|
|
|
}, SEND_TIMEOUT)
|
2019-11-05 12:23:07 -08:00
|
|
|
const waitResponse = await waitForAccountNonce(signerAddress, tx.tx.sequence + 1)
|
2019-11-05 10:14:22 -08:00
|
|
|
clearTimeout(sendTimeoutId)
|
|
|
|
resolve(waitResponse ? SIGN_OK : SIGN_FAILED)
|
|
|
|
} else if (error === null || error.code === 0) { // if was already enough parties
|
|
|
|
const signTimeoutId = setTimeout(() => {
|
|
|
|
cancelled = true
|
|
|
|
}, SIGN_ATTEMPT_TIMEOUT)
|
2019-11-05 12:23:07 -08:00
|
|
|
const waitResponse = await waitForAccountNonce(signerAddress, tx.tx.sequence + 1)
|
2019-11-05 10:14:22 -08:00
|
|
|
clearTimeout(signTimeoutId)
|
|
|
|
resolve(waitResponse ? SIGN_OK : SIGN_FAILED)
|
2019-11-05 12:23:07 -08:00
|
|
|
} else if (error.code === 143) { // if process was killed
|
|
|
|
logger.warn('Sign process was killed')
|
|
|
|
resolve(nonceInterrupt ? SIGN_NONCE_INTERRUPT : SIGN_FAILED)
|
2019-11-05 10:14:22 -08:00
|
|
|
} else if (error.code !== null && error.code !== 0) { // if process has failed
|
|
|
|
logger.warn('Sign process has failed')
|
|
|
|
resolve(SIGN_FAILED)
|
2019-11-01 11:43:25 -07:00
|
|
|
} else {
|
2019-11-05 10:14:22 -08:00
|
|
|
logger.warn('Unknown error state %o', error)
|
|
|
|
resolve(SIGN_FAILED)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
cmd.stdout.on('data', (data) => {
|
|
|
|
const str = data.toString()
|
|
|
|
if (str.includes('Got all party ids')) {
|
|
|
|
restartTimeoutId = setTimeout(killSigner, SIGN_ATTEMPT_TIMEOUT)
|
2019-11-01 11:43:25 -07:00
|
|
|
}
|
2019-11-05 10:14:22 -08:00
|
|
|
logger.debug(str)
|
2019-11-01 11:43:25 -07:00
|
|
|
})
|
|
|
|
cmd.stderr.on('data', (data) => logger.debug(data.toString()))
|
2019-11-05 10:14:22 -08:00
|
|
|
|
|
|
|
// Kill signer if current nonce is already processed at some time
|
|
|
|
nonceDaemonIntervalId = setInterval(async () => {
|
|
|
|
logger.info(`Checking if account ${signerAddress} has nonce ${tx.tx.sequence + 1}`)
|
|
|
|
const { sequence } = await getAccount(signerAddress)
|
|
|
|
if (sequence > tx.tx.sequence) {
|
|
|
|
logger.info('Account already has needed nonce, cancelling current sign process')
|
2019-11-05 12:23:07 -08:00
|
|
|
nonceInterrupt = true
|
2019-11-05 10:14:22 -08:00
|
|
|
killSigner()
|
|
|
|
}
|
|
|
|
}, SIGN_NONCE_CHECK_INTERVAL)
|
2019-11-01 11:43:25 -07:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2019-11-25 06:25:53 -08:00
|
|
|
function getAccountBalance(account, asset) {
|
|
|
|
return account.balances.find((token) => token.symbol === asset).free
|
|
|
|
}
|
2019-07-04 02:32:01 -07:00
|
|
|
|
2019-11-25 06:25:53 -08:00
|
|
|
async function buildTx(from, account, data, txAttempt) {
|
|
|
|
const { closeEpoch, newEpoch, nonce } = data
|
|
|
|
|
|
|
|
const txOptions = {
|
|
|
|
from,
|
|
|
|
accountNumber: account.account_number,
|
|
|
|
sequence: nonce,
|
|
|
|
asset: FOREIGN_ASSET,
|
|
|
|
memo: `Attempt ${txAttempt}`
|
2019-10-11 02:39:40 -07:00
|
|
|
}
|
2019-11-25 06:25:53 -08:00
|
|
|
let exchanges
|
2019-10-11 02:39:40 -07:00
|
|
|
|
2019-11-25 06:25:53 -08:00
|
|
|
if (closeEpoch) {
|
|
|
|
logger.info(`Building corresponding account flags transaction, nonce ${nonce}`)
|
|
|
|
|
|
|
|
txOptions.flags = 0x01
|
|
|
|
} else if (newEpoch) {
|
|
|
|
const newKeysFile = `/keys/keys${newEpoch}.store`
|
|
|
|
const to = getAccountFromFile(newKeysFile).address
|
2019-07-04 02:32:01 -07:00
|
|
|
|
2019-11-25 06:25:53 -08:00
|
|
|
if (to === '') {
|
|
|
|
return { tx: null }
|
2019-10-02 11:44:52 -07:00
|
|
|
}
|
2019-07-15 12:41:02 -07:00
|
|
|
|
2019-11-25 06:25:53 -08:00
|
|
|
logger.info(`Building corresponding transaction for transferring all funds, nonce ${nonce}, recipient ${to}`)
|
|
|
|
const fee = await getFee()
|
|
|
|
|
|
|
|
txOptions.recipients = [{
|
|
|
|
to,
|
|
|
|
tokens: getAccountBalance(account, FOREIGN_ASSET),
|
|
|
|
bnbs: new BN(getAccountBalance(account, 'BNB')).minus(fee)
|
|
|
|
}]
|
|
|
|
} else {
|
|
|
|
logger.info(`Building corresponding transfer transaction, nonce ${nonce}`)
|
|
|
|
exchanges = await getExchangeMessages(nonce)
|
|
|
|
const exchangesData = exchanges.map((exchangeMsg) => JSON.parse(exchangeMsg.content))
|
|
|
|
|
|
|
|
txOptions.recipients = exchangesData.map(({ value, recipient }) => ({
|
|
|
|
to: recipient,
|
|
|
|
tokens: value
|
2019-11-01 11:43:25 -07:00
|
|
|
}))
|
2019-11-25 06:25:53 -08:00
|
|
|
}
|
2019-07-15 12:41:02 -07:00
|
|
|
|
2019-11-25 06:25:53 -08:00
|
|
|
const tx = new Transaction(txOptions)
|
2019-07-15 12:41:02 -07:00
|
|
|
|
2019-11-25 06:25:53 -08:00
|
|
|
return {
|
|
|
|
tx,
|
|
|
|
exchanges
|
|
|
|
}
|
|
|
|
}
|
2019-11-14 02:29:58 -08:00
|
|
|
|
2019-11-25 06:25:53 -08:00
|
|
|
function writeParams(parties, threshold) {
|
|
|
|
logger.debug('Writing params')
|
|
|
|
fs.writeFileSync('./params', JSON.stringify({
|
|
|
|
parties: parties.toString(),
|
|
|
|
threshold: (threshold - 1).toString()
|
|
|
|
}))
|
|
|
|
}
|
2019-11-14 02:29:58 -08:00
|
|
|
|
2019-11-25 06:25:53 -08:00
|
|
|
async function consumer(msg) {
|
|
|
|
const data = JSON.parse(msg.content)
|
2019-11-14 02:29:58 -08:00
|
|
|
|
2019-11-25 06:25:53 -08:00
|
|
|
logger.info('Consumed sign event: %o', data)
|
|
|
|
const {
|
|
|
|
nonce, epoch, newEpoch, parties, threshold, closeEpoch
|
|
|
|
} = data
|
2019-11-14 02:29:58 -08:00
|
|
|
|
2019-11-25 06:25:53 -08:00
|
|
|
const keysFile = `/keys/keys${epoch || closeEpoch}.store`
|
|
|
|
const { address: from, publicKey } = getAccountFromFile(keysFile)
|
|
|
|
if (from === '') {
|
|
|
|
logger.info('No keys found, acking message')
|
|
|
|
channel.ack(msg)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
const account = await getAccount(from)
|
2019-07-15 12:41:02 -07:00
|
|
|
|
2019-11-25 06:25:53 -08:00
|
|
|
if (nonce > account.sequence) {
|
|
|
|
logger.debug('Tx has been already sent')
|
|
|
|
logger.info('Acking message (skipped nonce)')
|
|
|
|
channel.ack(msg)
|
|
|
|
return
|
|
|
|
}
|
2019-07-15 12:41:02 -07:00
|
|
|
|
2019-11-25 06:25:53 -08:00
|
|
|
writeParams(parties, threshold)
|
|
|
|
let attempt = 1
|
|
|
|
|
|
|
|
const { tx, exchanges } = buildTx(from, account, data, attempt)
|
2019-11-05 10:14:22 -08:00
|
|
|
|
2019-11-25 06:25:53 -08:00
|
|
|
while (tx !== null) {
|
|
|
|
const signResult = await sign(keysFile, tx, publicKey, from)
|
|
|
|
|
|
|
|
if (signResult === SIGN_OK || signResult === SIGN_NONCE_INTERRUPT) {
|
|
|
|
if (closeEpoch) {
|
|
|
|
await confirmCloseEpoch(closeEpoch)
|
|
|
|
} else if (newEpoch) {
|
|
|
|
await confirmFundsTransfer(epoch)
|
|
|
|
} else {
|
|
|
|
// eslint-disable-next-line no-loop-func
|
|
|
|
exchanges.forEach((exchangeMsg) => channel.ack(exchangeMsg))
|
2019-07-15 12:41:02 -07:00
|
|
|
}
|
2019-11-25 06:25:53 -08:00
|
|
|
break
|
2019-07-07 12:58:35 -07:00
|
|
|
}
|
2019-11-25 06:25:53 -08:00
|
|
|
|
|
|
|
// signer either failed, or timed out after parties signup
|
|
|
|
attempt = nextAttempt || attempt + 1
|
|
|
|
nextAttempt = null
|
|
|
|
logger.warn(`Sign failed, starting next attempt ${attempt}`)
|
|
|
|
tx.tx.memo = `Attempt ${attempt}`
|
|
|
|
await delay(1000)
|
|
|
|
}
|
|
|
|
logger.info('Acking message')
|
|
|
|
channel.ack(msg)
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function main() {
|
|
|
|
channel = await connectRabbit(RABBITMQ_URL)
|
|
|
|
logger.info('Connecting to signature events queue')
|
|
|
|
exchangeQueue = await assertQueue(channel, 'exchangeQueue')
|
|
|
|
const signQueue = await assertQueue(channel, 'signQueue')
|
|
|
|
|
|
|
|
while (!ready) {
|
|
|
|
await delay(1000)
|
|
|
|
}
|
|
|
|
|
|
|
|
channel.prefetch(1)
|
|
|
|
signQueue.consume(consumer)
|
2019-07-04 02:32:01 -07:00
|
|
|
}
|
|
|
|
|
2019-11-01 11:43:25 -07:00
|
|
|
app.get('/restart/:attempt', restart)
|
|
|
|
app.get('/start', (req, res) => {
|
|
|
|
logger.info('Ready to start')
|
|
|
|
ready = true
|
|
|
|
res.send()
|
|
|
|
})
|
|
|
|
app.listen(8001, () => logger.debug('Listening on 8001'))
|
2019-07-04 02:32:01 -07:00
|
|
|
|
2019-11-01 11:43:25 -07:00
|
|
|
main()
|