Same private keys in both networks. Implementation modifications

This commit is contained in:
Kirill Fedoseev 2019-07-15 22:41:02 +03:00
parent 360a593ccf
commit 1ab220caf4
27 changed files with 713 additions and 419 deletions

1
.gitignore vendored
View File

@ -13,3 +13,4 @@ demo/ganache_data_side/
src/deploy/deploy-home/build/
src/deploy/deploy-side/build/
src/deploy/deploy-test/build/
test.js

View File

@ -8,6 +8,7 @@ two are enough to sign any transaction in the Binance Chain,
confirm token transfer on the Ethereum Side, or vote for state changes.
ERC20 Token is used on the Ethereum side of the bridge.
All ERC20 tokens are located on address associated with DEPLOY_PRIVATE_KEY.
BNB Token is used on the Binance Chain side.
@ -49,13 +50,13 @@ container for listening GET requests
Run this scripts from ```src/oracle``` dir
* ```node testBinanceSend.js PRIVATE_KEY TO VALUE [MEMO]```
- ```PRIVATE_KEY``` - private key of sender in the Binance Chain
* ```node testBinanceSend.js TO VALUE_TOKEN [VALUE_BNB]```
- ```TO``` - receiver address, current bridge address in the Binance Chain
- ```VALUE``` - amount of BNB to send
- ```MEMO``` - transaction memo, receiver on the Ethereum side, leave blank for just pre-funding
* ```node testApprove.js TO VALUE```
- ```VALUE_TOKEN``` - amount of tokens to send
- ```VALUE_BNB``` - amount of BNB tokens to send, if present, the
transaction is considered as a funding one
* ```node testEthereumSend.js TO VALUE```
- Approves specified amount of tokens to the bridge account and calls
needed method for starting exchange process
- ```TO``` - receiver address in the Binance Chain
- ```VALUE``` - amount of tokens to transfer and exchange
- ```VALUE``` - amount of tokens to transfer and exchange

View File

@ -9,7 +9,10 @@ SIDE_SHARED_DB_ADDRESS=0x44c158FE850821ae69DaF37AADF5c539e9d0025B
FOREIGN_URL=https://testnet-dex.binance.org/
FOREIGN_CHAIN_ID=Binance-Chain-Nile
FOREIGN_ASSET=KFT-94F
VALIDATOR_PRIVATE_KEY=2be3f252e16541bf1bb2d4a517d2bf173e6d09f2d765d32c64dc50515aec63ea
VOTES_PROXY_PORT=5001
SIGN_RESTART_PORT=6001

View File

@ -9,7 +9,10 @@ SIDE_SHARED_DB_ADDRESS=0x44c158FE850821ae69DaF37AADF5c539e9d0025B
FOREIGN_URL=https://testnet-dex.binance.org/
FOREIGN_CHAIN_ID=Binance-Chain-Nile
FOREIGN_ASSET=KFT-94F
VALIDATOR_PRIVATE_KEY=e59d58c77b791f98f10187117374ae9c589d48a62720ec6a5e142b0cc134f685
VOTES_PROXY_PORT=5002
SIGN_RESTART_PORT=6002

View File

@ -9,7 +9,10 @@ SIDE_SHARED_DB_ADDRESS=0x44c158FE850821ae69DaF37AADF5c539e9d0025B
FOREIGN_URL=https://testnet-dex.binance.org/
FOREIGN_CHAIN_ID=Binance-Chain-Nile
FOREIGN_ASSET=KFT-94F
VALIDATOR_PRIVATE_KEY=afaa4d4d6e54d25b0bf0361e3fd6cef562f6311bf6200de2dd0aa4cab63ae3b5
VOTES_PROXY_PORT=5003
SIGN_RESTART_PORT=6003

View File

@ -3,13 +3,20 @@ pragma solidity ^0.5.0;
import './openzeppelin-solidity/contracts/token/ERC20/IERC20.sol';
contract Bridge {
event NewEpoch(uint indexed epoch);
event KeygenCompleted(uint indexed epoch, uint x, uint y);
event ReceivedTokens(address from, string recipient, uint value); // pass epoch and params in this event
event NewEpoch(uint indexed oldEpoch, uint indexed newEpoch);
event NewEpochCancelled(uint indexed epoch);
event NewFundsTransfer(uint indexed oldEpoch, uint indexed newEpoch);
event EpochStart(uint indexed epoch, uint x, uint y);
struct State {
address[] validators;
uint threshold;
uint x;
uint y;
}
mapping(uint => State) states;
address[] public validators;
address[] public nextValidators;
address[] public savedNextValidators;
mapping(bytes32 => uint) public confirmationsCount;
mapping(bytes32 => bool) public confirmations;
mapping(bytes32 => uint) public dbTransferCount;
@ -17,163 +24,213 @@ contract Bridge {
mapping(bytes32 => uint) public votesCount;
mapping(bytes32 => bool) public votes;
uint public x;
uint public y;
bool public ready;
uint public threshold;
uint public nextThreshold;
// 0 - ready
// 1 - keygen, can be cancelled
// 2 - funds transfer, cannot be cancelled
uint public status;
uint public epoch;
uint public nextEpoch;
constructor(uint _threshold, uint _parties, address[] memory _validators, address _tokenContract) public {
require(_parties > 0);
require(_threshold < _parties);
require(_validators.length == _parties);
constructor(uint threshold, address[] memory validators, address _tokenContract) public {
require(validators.length > 0);
require(threshold < validators.length);
tokenContract = IERC20(_tokenContract);
epoch = 0;
status = 1;
nextEpoch = 1;
ready = false;
nextThreshold = _threshold;
savedNextValidators = _validators;
states[1] = State(validators, threshold, 0, 0);
emit NewEpoch(nextEpoch);
emit NewEpoch(0, 1);
}
IERC20 public tokenContract;
function requestAffirmation(uint value, string memory recipient) public {
require(ready, "Current epoch is not ready");
tokenContract.transferFrom(msg.sender, address(this), value);
emit ReceivedTokens(msg.sender, recipient, value);
modifier ready {
require(status == 0, "Not in ready state");
_;
}
function transfer(bytes32 hash, address to, uint value) public {
uint partyId = getPartyId();
require(partyId != 0, "Not a validator");
modifier keygen {
require(status == 1, "Not in keygen state");
_;
}
modifier fundsTransfer {
require(status == 2, "Not in funds transfer state");
_;
}
modifier currentValidator {
require(getPartyId() != 0, "Not a current validator");
_;
}
function transfer(bytes32 hash, address to, uint value) public ready currentValidator {
require(!dbTransfer[keccak256(abi.encodePacked(hash, msg.sender, to, value))], "Already voted");
dbTransfer[keccak256(abi.encodePacked(hash, msg.sender, to, value))] = true;
if (++dbTransferCount[keccak256(abi.encodePacked(hash, to, value))] == threshold + 1)
if (++dbTransferCount[keccak256(abi.encodePacked(hash, to, value))] == getThreshold() + 1) {
dbTransferCount[keccak256(abi.encodePacked(hash, to, value))] = 2 ** 255;
tokenContract.transfer(to, value);
}
function confirm(uint _x, uint _y) public {
uint partyId = getNextPartyId(msg.sender);
require(partyId != 0, "Not a next validator");
require(!confirmations[keccak256(abi.encodePacked(nextEpoch, partyId, _x, _y))], "Already confirmed");
confirmations[keccak256(abi.encodePacked(nextEpoch, partyId, _x, _y))] = true;
if (++confirmationsCount[keccak256(abi.encodePacked(nextEpoch, _x, _y))] == nextParties()) {
confirmationsCount[keccak256(abi.encodePacked(nextEpoch, _x, _y))] = 2 ** 256 - 1;
x = _x;
y = _y;
validators = savedNextValidators;
nextValidators = savedNextValidators;
threshold = nextThreshold;
epoch = nextEpoch;
ready = true;
emit KeygenCompleted(epoch, x, y);
}
}
function parties() view public returns (uint) {
return validators.length;
function confirmKeygen(uint x, uint y) public keygen {
require(getNextPartyId(msg.sender) != 0, "Not a next validator");
require(!confirmations[keccak256(abi.encodePacked(uint(1), nextEpoch, msg.sender, x, y))], "Already confirmed");
confirmations[keccak256(abi.encodePacked(uint(1), nextEpoch, msg.sender, x, y))] = true;
if (++confirmationsCount[keccak256(abi.encodePacked(uint(1), nextEpoch, x, y))] == getNextThreshold() + 1) {
confirmationsCount[keccak256(abi.encodePacked(uint(1), nextEpoch, x, y))] = 2 ** 255;
states[nextEpoch].x = x;
states[nextEpoch].y = y;
if (nextEpoch == 1) {
status = 0;
epoch = nextEpoch;
emit EpochStart(epoch, x, y);
}
else {
status = 2;
emit NewFundsTransfer(epoch, nextEpoch);
}
}
}
function nextParties() view public returns (uint) {
return savedNextValidators.length;
function confirmFundsTransfer() public fundsTransfer currentValidator {
require(epoch > 0, "First epoch does not need funds transfer");
require(!confirmations[keccak256(abi.encodePacked(uint(2), nextEpoch, msg.sender))], "Already confirmed");
confirmations[keccak256(abi.encodePacked(uint(2), nextEpoch, msg.sender))] = true;
if (++confirmationsCount[keccak256(abi.encodePacked(uint(2), nextEpoch))] == getNextThreshold() + 1) {
confirmationsCount[keccak256(abi.encodePacked(uint(2), nextEpoch))] = 2 ** 255;
status = 0;
epoch = nextEpoch;
emit EpochStart(epoch, states[epoch].x, states[epoch].y);
}
}
function getParties() view public returns (uint) {
return getParties(epoch);
}
function getNextParties() view public returns (uint) {
return getParties(nextEpoch);
}
function getParties(uint _epoch) view public returns (uint) {
return states[_epoch].validators.length;
}
function getThreshold() view public returns (uint) {
return getThreshold(epoch);
}
function getNextThreshold() view public returns (uint) {
return getThreshold(nextEpoch);
}
function getThreshold(uint _epoch) view public returns (uint) {
return states[_epoch].threshold;
}
function getX() view public returns (uint) {
return states[epoch].x;
}
function getY() view public returns (uint) {
return states[epoch].y;
}
function getPartyId() view public returns (uint) {
return getPartyId(msg.sender);
}
function getPartyId(address a) view public returns (uint) {
for (uint i = 0; i < parties(); i++) {
if (validators[i] == a)
address[] memory validators = getValidators();
for (uint i = 0; i < getParties(); i++) {
if (validators[i] == msg.sender)
return i + 1;
}
return 0;
}
function getNextPartyId(address a) view public returns (uint) {
for (uint i = 0; i < nextParties(); i++) {
if (savedNextValidators[i] == a)
address[] memory validators = getNextValidators();
for (uint i = 0; i < getNextParties(); i++) {
if (validators[i] == a)
return i + 1;
}
return 0;
}
function getValidatorsArray() view public returns (address[] memory) {
return validators;
function getValidators() view public returns (address[] memory) {
return states[epoch].validators;
}
function getNextValidatorsArray() view public returns (address[] memory) {
return savedNextValidators;
function getNextValidators() view public returns (address[] memory) {
return states[nextEpoch].validators;
}
function voteAddValidator(address validator) public {
require(getPartyId() != 0, "Not a current validator");
function voteAddValidator(address validator) public ready currentValidator {
require(getNextPartyId(validator) == 0, "Already a validator");
require(!votes[keccak256(abi.encodePacked(uint(1), epoch, msg.sender, validator))], "Already voted");
require(!votes[keccak256(abi.encodePacked(uint(1), nextEpoch, msg.sender, validator))], "Already voted");
votes[keccak256(abi.encodePacked(uint(1), epoch, msg.sender, validator))] = true;
if (++votesCount[keccak256(abi.encodePacked(uint(1), epoch, validator))] == threshold + 1) {
nextValidators.push(validator);
votes[keccak256(abi.encodePacked(uint(1), nextEpoch, msg.sender, validator))] = true;
if (++votesCount[keccak256(abi.encodePacked(uint(1), nextEpoch, validator))] == getThreshold() + 1) {
states[nextEpoch].validators.push(validator);
}
}
function voteRemoveValidator(address validator) public {
require(getPartyId() != 0, "Not a current validator");
function voteRemoveValidator(address validator) public ready currentValidator {
require(getNextPartyId(validator) != 0, "Already not a validator");
require(!votes[keccak256(abi.encodePacked(uint(2), epoch, msg.sender, validator))], "Already voted");
require(!votes[keccak256(abi.encodePacked(uint(2), nextEpoch, msg.sender, validator))], "Already voted");
votes[keccak256(abi.encodePacked(uint(2), epoch, msg.sender, validator))] = true;
if (++votesCount[keccak256(abi.encodePacked(uint(2), epoch, validator))] == threshold + 1) {
votes[keccak256(abi.encodePacked(uint(2), nextEpoch, msg.sender, validator))] = true;
if (++votesCount[keccak256(abi.encodePacked(uint(2), nextEpoch, validator))] == getThreshold() + 1) {
_removeValidator(validator);
}
}
function _removeValidator(address validator) private {
for (uint i = 0; i < nextValidators.length - 1; i++) {
if (nextValidators[i] == validator) {
nextValidators[i] = nextValidators[nextValidators.length - 1];
for (uint i = 0; i < getNextParties() - 1; i++) {
if (states[nextEpoch].validators[i] == validator) {
states[nextEpoch].validators[i] = states[nextEpoch].validators[getNextParties() - 1];
break;
}
}
delete nextValidators[nextValidators.length - 1];
nextValidators.length--;
delete states[nextEpoch].validators[getNextParties() - 1];
states[nextEpoch].validators.length--;
}
function voteChangeThreshold(uint _threshold) public {
require(getPartyId() != 0, "Not a current validator");
require(!votes[keccak256(abi.encodePacked(uint(3), epoch, msg.sender, threshold))], "Already voted");
function voteChangeThreshold(uint threshold) public ready currentValidator {
require(!votes[keccak256(abi.encodePacked(uint(3), nextEpoch, msg.sender, threshold))], "Already voted");
votes[keccak256(abi.encodePacked(uint(3), epoch, msg.sender, _threshold))] = true;
if (++votesCount[keccak256(abi.encodePacked(uint(3), epoch, _threshold))] == threshold + 1) {
nextThreshold = _threshold;
votes[keccak256(abi.encodePacked(uint(3), nextEpoch, msg.sender, threshold))] = true;
if (++votesCount[keccak256(abi.encodePacked(uint(3), nextEpoch, threshold))] == getThreshold() + 1) {
states[nextEpoch].threshold = threshold;
}
}
function voteStartEpoch(uint newEpoch) public {
require(newEpoch == nextEpoch + 1, "Wrong epoch number");
require(getPartyId() != 0, "Not a current validator");
require(!votes[keccak256(abi.encodePacked(uint(4), newEpoch, msg.sender))], "Voted already");
function voteStartKeygen() public ready currentValidator {
require(!votes[keccak256(abi.encodePacked(uint(4), nextEpoch + 1, msg.sender))], "Voted already");
votes[keccak256(abi.encodePacked(uint(4), newEpoch, msg.sender))] = true;
if (++votesCount[keccak256(abi.encodePacked(uint(4), newEpoch))] == threshold + 1) {
ready = false;
votes[keccak256(abi.encodePacked(uint(4), nextEpoch + 1, msg.sender))] = true;
if (++votesCount[keccak256(abi.encodePacked(uint(4), nextEpoch + 1))] == getThreshold() + 1) {
status = 1;
nextEpoch = newEpoch;
savedNextValidators = nextValidators;
emit NewEpoch(newEpoch);
nextEpoch++;
emit NewEpoch(epoch, nextEpoch);
}
}
function voteCancelKeygen() public keygen currentValidator {
require(!votes[keccak256(abi.encodePacked(uint(5), nextEpoch, msg.sender))], "Voted already");
votes[keccak256(abi.encodePacked(uint(5), nextEpoch, msg.sender))] = true;
if (++votesCount[keccak256(abi.encodePacked(uint(5), nextEpoch))] == getThreshold() + 1) {
status = 0;
emit NewEpochCancelled(nextEpoch);
}
}
}

View File

@ -11,7 +11,7 @@ module.exports = deployer => {
deployer.deploy(
Bridge,
THRESHOLD,
PARTIES,
//PARTIES,
[
VALIDATOR_ADDRESS_1,
VALIDATOR_ADDRESS_2,

View File

@ -12,7 +12,8 @@ contract SharedDB {
}
function getSignupNumber(bytes32 hash, address[] memory validators, address validator) view public returns (uint) {
require(dbSignups[keccak256(abi.encodePacked(validator, hash))] > 0, "Have not voted yet");
if (dbSignups[keccak256(abi.encodePacked(validator, hash))] == 0)
return 0;
uint id = 1;
for (uint i = 0; i < validators.length; i++) {
uint vid = dbSignups[keccak256(abi.encodePacked(validators[i], hash))];

View File

@ -1,4 +1,3 @@
#RPC_URL=https://sokol.poa.network
HOME_RPC_URL=http://127.0.0.1:4444
SIDE_RPC_URL=http://127.0.0.1:3333
HOME_CHAIN_ID=44
@ -8,16 +7,14 @@ HOME_BRIDGE_ADDRESS=0x94b40CC641Ed7db241A1f04C8896ba6f6cC36b85
HOME_TOKEN_ADDRESS=0x44c158FE850821ae69DaF37AADF5c539e9d0025B
SIDE_SHARED_DB_ADDRESS=0x44c158FE850821ae69DaF37AADF5c539e9d0025B
#VALIDATOR_PRIVATE_KEY=d1e7b8ff274e517e1a332f2bc0ac051e30db196ba31c68c2efcd022e8ec358f1
VALIDATOR_PRIVATE_KEY=2be3f252e16541bf1bb2d4a517d2bf173e6d09f2d765d32c64dc50515aec63ea
#SSM_URL=http://127.0.0.1:8001
LOCAL=true
DEPLOY_PRIVATE_KEY=e2aeb24eaa63102d0c0821717c3b6384abdabd7af2ad4ec8e650dce300798b27
FOREIGN_URL=https://testnet-dex.binance.org/
FOREIGN_CHAIN_ID=Binance-Chain-Nile
FOREIGN_ASSET=KFT-94F
FOREIGN_PRIVATE_KEY=e2aeb24eaa63102d0c0821717c3b6384abdabd7af2ad4ec8e650dce300798b27
VOTES_PROXY_PORT=5000
SIGN_RESTART_PORT=6000

View File

@ -4,9 +4,9 @@ const bech32 = require('bech32')
const BN = require('bignumber.js')
const fs = require('fs')
const crypto = require('crypto')
const { computeAddress } = require('ethers').utils
const { FOREIGN_URL, PROXY_URL } = process.env
const FOREIGN_ASSET = 'BNB'
const { FOREIGN_URL, PROXY_URL, FOREIGN_ASSET } = process.env
const foreignHttpClient = axios.create({ baseURL: FOREIGN_URL })
const proxyHttpClient = axios.create({ baseURL: PROXY_URL })
@ -30,9 +30,10 @@ async function main () {
for (const tx of newTransactions.reverse()) {
if (tx.memo !== 'funding') {
const publicKeyEncoded = (await getTx(tx.txHash)).signatures[0].pub_key.value
await proxyHttpClient
.post('/transfer', {
to: tx.memo,
to: computeAddress(Buffer.from(publicKeyEncoded, 'base64')),
value: new BN(tx.value).integerValue(BN.ROUND_FLOOR),//(new BN(tx.value).multipliedBy(10 ** 8)).toNumber(),
hash: `0x${tx.txHash}`
})
@ -41,6 +42,17 @@ async function main () {
}
}
function getTx(hash) {
return foreignHttpClient
.get(`/api/v1/tx/${hash}`, {
params: {
format: 'json'
}
})
.then(res => res.data.tx.value)
.catch(() => getTx(hash))
}
async function fetchNewTransactions () {
console.log('Fetching new transactions')
const startTime = parseInt(await redis.get('foreignTime')) + 1
@ -60,7 +72,7 @@ async function fetchNewTransactions () {
}
})
.then(res => res.data.tx)
.catch(console.log)
.catch(() => fetchNewTransactions())
}
function getLastForeignAddress () {

View File

@ -5,7 +5,8 @@
"ioredis": "4.10.0",
"axios": "0.19.0",
"bech32": "1.1.3",
"bignumber.js": "9.0.0"
"bignumber.js": "9.0.0",
"ethers": "4.0.33"
}
}

View File

@ -13,6 +13,7 @@ services:
- SIDE_CHAIN_ID
- VALIDATOR_PRIVATE_KEY
- FOREIGN_URL
- FOREIGN_ASSET
volumes:
- '../deploy/deploy-test/build/contracts/IERC20.json:/proxy/contracts_data/IERC20.json'
- '../deploy/deploy-home/build/contracts/Bridge.json:/proxy/contracts_data/Bridge.json'
@ -45,8 +46,11 @@ services:
- 'PROXY_URL=http://proxy:8001'
- FOREIGN_CHAIN_ID
- FOREIGN_URL
- FOREIGN_ASSET
volumes:
- '${PWD}/keys:/keys'
ports:
- '${SIGN_RESTART_PORT}:8001'
networks:
- test_network
# - sign-proxy-net
@ -79,9 +83,12 @@ services:
environment:
- HOME_RPC_URL
- HOME_BRIDGE_ADDRESS
- HOME_TOKEN_ADDRESS
- HOME_CHAIN_ID
- 'RABBITMQ_URL=amqp://rabbitmq:5672'
volumes:
- '../deploy/deploy-home/build/contracts/Bridge.json:/watcher/contracts_data/Bridge.json'
- '../deploy/deploy-test/build/contracts/IERC20.json:/watcher/contracts_data/IERC20.json'
networks:
- test_network
# - rabbit-ethwatcher-net
@ -91,6 +98,7 @@ services:
image: bnc-watcher
environment:
- FOREIGN_URL
- FOREIGN_ASSET
- 'RABBITMQ_URL=amqp://rabbitmq:5672'
- 'PROXY_URL=http://proxy:8001'
volumes:

View File

@ -1,19 +1,28 @@
const amqp = require('amqplib')
const Web3 = require('web3')
const redis = require('./db')
const bridgeAbi = require('./contracts_data/Bridge.json').abi
const crypto = require('crypto')
const utils = require('ethers').utils
const BN = require('bignumber.js')
const bech32 = require('bech32')
const { HOME_RPC_URL, HOME_BRIDGE_ADDRESS, RABBITMQ_URL } = process.env
const abiBridge = require('./contracts_data/Bridge.json').abi
const abiToken = require('./contracts_data/IERC20.json').abi
const { HOME_RPC_URL, HOME_CHAIN_ID, HOME_BRIDGE_ADDRESS, RABBITMQ_URL, HOME_TOKEN_ADDRESS } = process.env
const web3Home = new Web3(HOME_RPC_URL)
const homeBridge = new web3Home.eth.Contract(bridgeAbi, HOME_BRIDGE_ADDRESS)
const bridge = new web3Home.eth.Contract(abiBridge, HOME_BRIDGE_ADDRESS)
const token = new web3Home.eth.Contract(abiToken, HOME_TOKEN_ADDRESS)
let channel
let signQueue
let epochQueue
let keygenQueue
let cancelKeygenQueue
let blockNumber
let foreignNonce = []
let epoch
let redisTx
async function connectRabbit (url) {
return amqp.connect(url).catch(() => {
@ -28,26 +37,28 @@ async function initialize () {
const connection = await connectRabbit(RABBITMQ_URL)
channel = await connection.createChannel()
signQueue = await channel.assertQueue('signQueue')
epochQueue = await channel.assertQueue('epochQueue')
keygenQueue = await channel.assertQueue('keygenQueue')
cancelKeygenQueue = await channel.assertQueue('cancelKeygenQueue')
const events = await homeBridge.getPastEvents('KeygenCompleted', {
const events = await bridge.getPastEvents('EpochStart', {
fromBlock: 1
})
epoch = events.length ? events[events.length - 1].returnValues.epoch.toNumber() : 0
console.log(`Current epoch ${epoch}`)
const dbEpoch = parseInt(await redis.get('epoch')) // number, or NaN if empty
if (epoch !== dbEpoch) {
console.log('Current epoch is outdated, starting from new epoch and block number')
blockNumber = events.length ? events[events.length - 1].blockNumber : 1
const epochStart = events.length ? events[events.length - 1].blockNumber : 1
const saved = (parseInt(await redis.get('homeBlock')) + 1) || 1
console.log(epochStart, saved)
if (epochStart > saved) {
console.log(`Data in db is outdated, starting from epoch ${epoch}, block #${epochStart}`)
blockNumber = epochStart
await redis.multi()
.set('epoch', epoch)
.set('homeBlock', blockNumber - 1)
.set(`foreignNonce${epoch}`, 0)
.exec()
foreignNonce[epoch] = 0
} else {
console.log('Restoring epoch and block number from local db')
blockNumber = (parseInt(await redis.get('homeBlock')) + 1) || 1
blockNumber = saved
foreignNonce[epoch] = parseInt(await redis.get(`foreignNonce${epoch}`)) || 0
}
}
@ -60,63 +71,47 @@ async function main () {
return
}
const events = await homeBridge.getPastEvents('allEvents', {
redisTx = redis.multi()
const bridgeEvents = await bridge.getPastEvents('allEvents', {
fromBlock: blockNumber,
toBlock: blockNumber
})
const epochEvents = events.filter(x => x.event === 'NewEpoch')
const transferEvents = events.filter(x => x.event === 'ReceivedTokens')
epochEvents.forEach(event => {
const newEpoch = event.returnValues.epoch.toNumber()
const oldEpoch = newEpoch - 1
channel.sendToQueue(epochQueue.queue, Buffer.from(JSON.stringify({
epoch: newEpoch
})), {
persistent: true
})
console.log('Sent new epoch event')
if (oldEpoch > 0) {
// Transfer all assets to new account tss account
channel.sendToQueue(signQueue.queue, Buffer.from(JSON.stringify({
epoch: newEpoch,
//nonce: foreignNonce[oldEpoch],
})), {
persistent: true
})
console.log('Sent new epoch sign event')
foreignNonce[oldEpoch]++
redis.incr(`foreignNonce${oldEpoch}`)
}
redis.multi()
.incr('epoch')
.set(`foreignNonce${newEpoch}`, 0)
.exec()
foreignNonce[newEpoch] = 0
epoch++
for (const event of bridgeEvents) {
switch (event.event) {
case 'NewEpoch':
await sendKeygen(event)
break
case 'NewEpochCancelled':
sendKeygenCancelation(event)
break
case 'NewFundsTransfer':
await sendSignFundsTransfer(event)
break
case 'EpochStart':
epoch = event.returnValues.epoch.toNumber()
console.log(`Epoch ${epoch} started`)
foreignNonce[epoch] = 0
break
}
)
}
transferEvents.forEach(event => {
channel.sendToQueue(signQueue.queue, Buffer.from(JSON.stringify({
recipient: event.returnValues.recipient,
value: event.returnValues.value.toNumber(),
epoch,
nonce: foreignNonce[epoch]
})), {
persistent: true
})
console.log('Sent new sign event')
const transferEvents = await token.getPastEvents('Transfer', {
filter: {
to: HOME_BRIDGE_ADDRESS
},
fromBlock: blockNumber,
toBlock: blockNumber
})
for (const event of transferEvents) {
await sendSign(event)
}
redis.incr(`foreignNonce${epoch}`)
foreignNonce[epoch]++
}
)
await redis.incr('homeBlock')
blockNumber++
// Exec redis tx
await redisTx.incr('homeBlock').exec()
}
initialize().then(async () => {
@ -124,3 +119,88 @@ initialize().then(async () => {
await main()
}
})
async function sendKeygen (event) {
const newEpoch = event.returnValues.newEpoch.toNumber()
channel.sendToQueue(keygenQueue.queue, Buffer.from(JSON.stringify({
epoch: newEpoch,
threshold: (await bridge.methods.getThreshold(newEpoch).call()).toNumber(),
parties: (await bridge.methods.getParties(newEpoch).call()).toNumber()
})), {
persistent: true
})
console.log('Sent keygen start event')
}
function sendKeygenCancelation (event) {
const epoch = event.returnValues.epoch.toNumber()
channel.sendToQueue(cancelKeygenQueue.queue, Buffer.from(JSON.stringify({
epoch
})), {
persistent: true
})
console.log('Sent keygen cancellation event')
}
async function sendSignFundsTransfer (event) {
const newEpoch = event.returnValues.newEpoch.toNumber()
const oldEpoch = event.returnValues.oldEpoch.toNumber()
channel.sendToQueue(signQueue.queue, Buffer.from(JSON.stringify({
epoch: oldEpoch,
newEpoch,
nonce: foreignNonce[oldEpoch],
threshold: (await bridge.methods.getThreshold(oldEpoch).call()).toNumber(),
parties: (await bridge.methods.getParties(oldEpoch).call()).toNumber()
})), {
persistent: true
})
console.log('Sent sign funds transfer event')
foreignNonce[oldEpoch]++
redisTx.incr(`foreignNonce${oldEpoch}`)
}
async function sendSign (event) {
const tx = await web3Home.eth.getTransaction(event.transactionHash)
const msg = utils.serializeTransaction({
nonce: tx.nonce,
gasPrice: `0x${new BN(tx.gasPrice).toString(16)}`,
gasLimit: `0x${new BN(tx.gas).toString(16)}`,
to: tx.to,
value: `0x${new BN(tx.value).toString(16)}`,
data: tx.input,
chainId: parseInt(HOME_CHAIN_ID)
})
const hash = web3Home.utils.sha3(msg)
const publicKey = utils.recoverPublicKey(hash, { r: tx.r, s: tx.s, v: tx.v })
channel.sendToQueue(signQueue.queue, Buffer.from(JSON.stringify({
recipient: publicKeyToAddress({
x: publicKey.substr(4, 64),
y: publicKey.substr(68, 64)
}),
value: event.returnValues.value.toNumber(),
epoch,
nonce: foreignNonce[epoch],
threshold: (await bridge.methods.getThreshold(epoch).call()).toNumber(),
parties: (await bridge.methods.getParties(epoch).call()).toNumber()
})), {
persistent: true
})
console.log('Sent new sign event')
redisTx.incr(`foreignNonce${epoch}`)
foreignNonce[epoch]++
}
function publicKeyToAddress ({ x, y }) {
const compact = (parseInt(y[y.length - 1], 16) % 2 ? '03' : '02') + padZeros(x, 64)
const sha256Hash = crypto.createHash('sha256').update(Buffer.from(compact, 'hex')).digest('hex')
const hash = crypto.createHash('ripemd160').update(Buffer.from(sha256Hash, 'hex')).digest('hex')
const words = bech32.toWords(Buffer.from(hash, 'hex'))
return bech32.encode('tbnb', words)
}
function padZeros (s, len) {
while (s.length < len)
s = '0' + s
return s
}

View File

@ -4,8 +4,9 @@
"dependencies": {
"ioredis": "4.10.0",
"amqplib": "0.5.3",
"web3": "1.0.0-beta.55"
"web3": "1.0.0-beta.55",
"ethers": "4.0.33",
"bignumber.js": "9.0.0",
"bech32": "1.1.3"
}
}

View File

@ -6,7 +6,10 @@ const bech32 = require('bech32')
const axios = require('axios')
const BN = require('bignumber.js')
const { HOME_RPC_URL, HOME_BRIDGE_ADDRESS, SIDE_RPC_URL, SIDE_SHARED_DB_ADDRESS, VALIDATOR_PRIVATE_KEY, HOME_CHAIN_ID, SIDE_CHAIN_ID, HOME_TOKEN_ADDRESS, FOREIGN_URL } = process.env
const {
HOME_RPC_URL, HOME_BRIDGE_ADDRESS, SIDE_RPC_URL, SIDE_SHARED_DB_ADDRESS, VALIDATOR_PRIVATE_KEY, HOME_CHAIN_ID,
SIDE_CHAIN_ID, HOME_TOKEN_ADDRESS, FOREIGN_URL, FOREIGN_ASSET
} = process.env
const abiSharedDb = require('./contracts_data/SharedDB.json').abi
const abiBridge = require('./contracts_data/Bridge.json').abi
const abiToken = require('./contracts_data/IERC20.json').abi
@ -18,7 +21,6 @@ const token = new homeWeb3.eth.Contract(abiToken, HOME_TOKEN_ADDRESS)
const sharedDb = new sideWeb3.eth.Contract(abiSharedDb, SIDE_SHARED_DB_ADDRESS)
const validatorAddress = homeWeb3.eth.accounts.privateKeyToAccount(`0x${VALIDATOR_PRIVATE_KEY}`).address
const FOREIGN_ASSET = 'BNB'
const httpClient = axios.create({ baseURL: FOREIGN_URL })
const lock = new AsyncLock()
@ -35,16 +37,16 @@ app.post('/set', set)
app.post('/signupkeygen', signupKeygen)
app.post('/signupsign', signupSign)
app.get('/current_params', currentParams)
app.get('/next_params', nextParams)
app.post('/confirm', confirm)
app.post('/confirmKeygen', confirmKeygen)
app.post('/confirmFundsTransfer', confirmFundsTransfer)
app.post('/transfer', transfer)
const votesProxyApp = express()
votesProxyApp.use(express.json())
votesProxyApp.use(express.urlencoded({ extended: true }))
votesProxyApp.get('/vote/startEpoch/:epoch', voteStartEpoch)
votesProxyApp.get('/vote/startKeygen', voteStartKeygen)
votesProxyApp.get('/vote/cancelKeygen', voteCancelKeygen)
votesProxyApp.get('/vote/addValidator/:validator', voteAddValidator)
votesProxyApp.get('/vote/removeValidator/:validator', voteRemoveValidator)
votesProxyApp.get('/info', info)
@ -79,9 +81,9 @@ async function get (req, res) {
const uuid = req.body.key.third
let from
if (uuid.startsWith('k'))
from = await bridge.methods.savedNextValidators(parseInt(req.body.key.first) - 1).call()
from = (await bridge.methods.getNextValidators().call())[parseInt(req.body.key.first) - 1]
else {
const validators = await bridge.methods.getValidatorsArray().call()
const validators = await bridge.methods.getValidators().call()
from = await sharedDb.methods.getSignupAddress(uuid, validators, parseInt(req.body.key.first)).call()
}
const to = Number(req.body.key.fourth) // 0 if empty
@ -130,47 +132,42 @@ async function signupSign (req, res) {
console.log('SignupSign call')
const hash = sideWeb3.utils.sha3(`0x${req.body.third}`)
const query = sharedDb.methods.signupSign(hash)
await sideSendQuery(query)
const receipt = await sideSendQuery(query)
const validators = await bridge.methods.getValidatorsArray().call()
const threshold = await bridge.methods.threshold().call()
const id = (await sharedDb.methods.getSignupNumber(hash, validators, validatorAddress).call()).toNumber()
if (id > threshold + 1) {
res.send(Err({}))
// Already have signup
if (receipt === false) {
console.log('Already have signup')
res.send(Ok({ uuid: hash, number: 0 }))
return
}
const validators = await bridge.methods.getValidators().call()
const id = (await sharedDb.methods.getSignupNumber(hash, validators, validatorAddress).call()).toNumber()
res.send(Ok({ uuid: hash, number: id }))
console.log('SignupSign end')
}
async function confirm (req, res) {
console.log('Confirm call')
async function confirmKeygen (req, res) {
console.log('Confirm keygen call')
const { x, y } = req.body[5]
const query = bridge.methods.confirm(`0x${x}`, `0x${y}`)
const query = bridge.methods.confirmKeygen(`0x${x}`, `0x${y}`)
await homeSendQuery(query)
res.send()
console.log('Confirm end')
console.log('Confirm keygen end')
}
async function currentParams (req, res) {
console.log('Current params call')
const parties = (await bridge.methods.parties().call()).toNumber().toString()
const threshold = (await bridge.methods.threshold().call()).toNumber().toString()
res.send({ parties, threshold })
console.log('Current params end')
}
async function nextParams (req, res) {
console.log('Next params call')
const parties = (await bridge.methods.nextParties().call()).toNumber().toString()
const threshold = (await bridge.methods.nextThreshold().call()).toNumber().toString()
res.send({ parties, threshold })
console.log('Next params end')
async function confirmFundsTransfer (req, res) {
console.log('Confirm funds transfer call')
const query = bridge.methods.confirmFundsTransfer()
await homeSendQuery(query)
res.send()
console.log('Confirm funds transfer end')
}
function sideSendQuery (query) {
return lock.acquire('side', async () => {
console.log('Sending query')
const encodedABI = query.encodeABI()
const tx = {
data: encodedABI,
@ -182,18 +179,27 @@ function sideSendQuery (query) {
tx.gas = Math.min(Math.ceil(await query.estimateGas(tx) * 1.5), 6721975)
const signedTx = await sideWeb3.eth.accounts.signTransaction(tx, VALIDATOR_PRIVATE_KEY)
try {
return await sideWeb3.eth.sendSignedTransaction(signedTx.rawTransaction)
} catch (e) {
//sideValidatorNonce--
console.log('Side tx failed', e.message)
if (e.message.includes('out of gas')) {
console.log('Out of gas, retrying')
sideSendQuery(query)
}
return null
}
return sideWeb3.eth.sendSignedTransaction(signedTx.rawTransaction)
.catch(e => {
const error = parseError(e.message)
const reason = parseReason(e.message)
if (error === 'revert' && reason.length) {
console.log(reason)
return false
} else if (error === 'out of gas') {
console.log('Out of gas, retrying')
return true
} else {
console.log('Home tx failed', e.message)
return false
}
})
})
.then(result => {
if (result === true)
return sideSendQuery(query)
return result
})
}
function homeSendQuery (query) {
@ -209,23 +215,54 @@ function homeSendQuery (query) {
tx.gas = Math.min(Math.ceil(await query.estimateGas(tx) * 1.5), 6721975)
const signedTx = await homeWeb3.eth.accounts.signTransaction(tx, VALIDATOR_PRIVATE_KEY)
try {
return await homeWeb3.eth.sendSignedTransaction(signedTx.rawTransaction)
} catch (e) {
//homeValidatorNonce--
console.log('Home tx failed', e.message)
if (e.message.includes('out of gas')) {
console.log('Out of gas, retrying')
homeSendQuery(query)
}
return null
}
return homeWeb3.eth.sendSignedTransaction(signedTx.rawTransaction)
.catch(e => {
const error = parseError(e.message)
const reason = parseReason(e.message)
if (error === 'revert' && reason.length) {
console.log(reason)
return false
} else if (error === 'out of gas') {
console.log('Out of gas, retrying')
return true
} else {
console.log('Home tx failed', e.message)
return false
}
})
})
.then(result => {
if (result === true)
return homeSendQuery(query)
return result
})
}
async function voteStartEpoch (req, res) {
console.log('Voting for starting new epoch')
const query = bridge.methods.voteStartEpoch(req.params.epoch)
function parseReason (message) {
const result = /(?<="reason":").*?(?=")/.exec(message)
return result ? result[0] : ''
}
function parseError (message) {
const result = /(?<="error":").*?(?=")/.exec(message)
return result ? result[0] : ''
}
async function voteStartKeygen (req, res) {
console.log('Voting for starting new epoch keygen')
const query = bridge.methods.voteStartKeygen()
try {
await homeSendQuery(query)
} catch (e) {
console.log(e)
}
res.send('Voted')
console.log('Voted successfully')
}
async function voteCancelKeygen (req, res) {
console.log('Voting for cancelling new epoch keygen')
const query = bridge.methods.voteCancelKeygen()
try {
await homeSendQuery(query)
} catch (e) {
@ -261,20 +298,33 @@ async function voteRemoveValidator (req, res) {
async function info (req, res) {
console.log('Info start')
const x = new BN(await bridge.methods.x().call()).toString(16)
const y = new BN(await bridge.methods.y().call()).toString(16)
const [x, y, epoch, nextEpoch, threshold, nextThreshold, validators, nextValidators, homeBalance, status] = await Promise.all([
bridge.methods.getX().call().then(x => new BN(x).toString(16)),
bridge.methods.getY().call().then(x => new BN(x).toString(16)),
bridge.methods.epoch().call().then(x => x.toNumber()),
bridge.methods.nextEpoch().call().then(x => x.toNumber()),
bridge.methods.getThreshold().call().then(x => x.toNumber()),
bridge.methods.getNextThreshold().call().then(x => x.toNumber()),
bridge.methods.getValidators().call(),
bridge.methods.getNextValidators().call(),
token.methods.balanceOf(HOME_BRIDGE_ADDRESS).call().then(x => x.toNumber()),
bridge.methods.status().call().then(x => x.toNumber()),
])
const foreignAddress = publicKeyToAddress({ x, y })
const balances = await getForeignBalances(foreignAddress)
res.send({
epoch: (await bridge.methods.epoch().call()).toNumber(),
nextEpoch: (await bridge.methods.nextEpoch().call()).toNumber(),
threshold: (await bridge.methods.threshold().call()).toNumber(),
nextThreshold: (await bridge.methods.nextThreshold().call()).toNumber(),
epoch,
nextEpoch,
threshold,
nextThreshold,
homeBridgeAddress: HOME_BRIDGE_ADDRESS,
foreignBridgeAddress: publicKeyToAddress({ x, y }),
validators: await bridge.methods.getValidatorsArray().call(),
nextValidators: await bridge.methods.getNextValidatorsArray().call(),
homeBalance: (await token.methods.balanceOf(HOME_BRIDGE_ADDRESS).call()).toNumber(),
foreignBalance: await getForeignBalance(publicKeyToAddress({ x, y })),
bridgeStatus: await bridge.methods.ready().call()
foreignBridgeAddress: foreignAddress,
validators,
nextValidators,
homeBalance,
foreignBalanceTokens: parseFloat(balances[FOREIGN_ASSET]) || 0,
foreignBalanceNative: parseFloat(balances['BNB']) || 0,
bridgeStatus: status ? (status === 1 ? 'keygen' : 'funds transfer') : 'ready'
})
console.log('Info end')
}
@ -293,11 +343,14 @@ async function transfer (req, res) {
console.log('Transfer end')
}
function getForeignBalance(address) {
function getForeignBalances (address) {
return httpClient
.get(`/api/v1/account/${address}`)
.then(res => parseFloat(res.data.balances.find(x => x.symbol === FOREIGN_ASSET).free))
.catch(err => 0)
.then(res => res.data.balances.reduce((prev, cur) => {
prev[cur.symbol] = cur.free
return prev
}, {}))
.catch(err => ({}))
}
function publicKeyToAddress ({ x, y }) {

View File

@ -7,28 +7,39 @@ const crypto = require('crypto')
const ecc = require('tiny-secp256k1')
const utils = require('ethers').utils
const privKey = process.argv[2]//'b92a59209e28149e5cee8e54dfceb80a08ea08e654261bdb9d264b15dee2525c'
const asset = 'BNB'
const amount = process.argv[4]//'2.5'
const addressTo = process.argv[3]
const addressFrom = Bnc.crypto.getAddressFromPrivateKey(privKey)
const message = process.argv[5] || 'funding'
const api = 'https://testnet-dex.binance.org/'
const publicKeyStr = utils.computePublicKey(`0x${privKey}`)
const { FOREIGN_URL, FOREIGN_ASSET, FOREIGN_PRIVATE_KEY } = process.env
const amount = process.argv[3]
const addressTo = process.argv[2]
const addressFrom = Bnc.crypto.getAddressFromPrivateKey(FOREIGN_PRIVATE_KEY)
const bnbs = process.argv[4]
const realBnbs = bnbs || '0'
const publicKeyStr = utils.computePublicKey(`0x${FOREIGN_PRIVATE_KEY}`)
const publicKey = {
x: publicKeyStr.substr(4, 64),
y: publicKeyStr.substr(68, 64)
}
console.log(`From ${addressFrom} to ${addressTo}, ${amount} tokens, memo '${message}'`)
const httpClient = axios.create({ baseURL: api })
if (bnbs)
console.log(`Funding from ${addressFrom} to ${addressTo}, ${amount} tokens, ${realBnbs} BNB'`)
else
console.log(`From ${addressFrom} to ${addressTo}, ${amount} tokens'`)
const httpClient = axios.create({ baseURL: FOREIGN_URL })
httpClient
.get(`/api/v1/account/${addressFrom}`)
.then((res) => {
const { sequence, account_number } = res.data
const tx = new Transaction(addressFrom, account_number, sequence, addressTo, amount, asset, message)
const tx = new Transaction({
from: addressFrom,
to: addressTo,
accountNumber: account_number,
sequence,
tokens: amount,
asset: FOREIGN_ASSET,
bnbs: realBnbs,
memo: bnbs ? 'funding' : 'exchange'
})
const hash = crypto.createHash('sha256').update(tx.getSignBytes()).digest('hex')
const signature = ecc.sign(Buffer.from(hash, 'hex'), Buffer.from(privKey, 'hex'))
const signature = ecc.sign(Buffer.from(hash, 'hex'), Buffer.from(FOREIGN_PRIVATE_KEY, 'hex'))
const sig = {
r: signature.toString('hex').substr(0, 64),
s: signature.toString('hex').substr(64, 64)

View File

@ -4,24 +4,19 @@ const Web3 = require('web3')
const { HOME_RPC_URL, HOME_BRIDGE_ADDRESS, HOME_CHAIN_ID, DEPLOY_PRIVATE_KEY, HOME_TOKEN_ADDRESS } = process.env
const web3 = new Web3(HOME_RPC_URL, null, { transactionConfirmationBlocks: 1 })
const abiBridge = require('../deploy/deploy-home/build/contracts/SharedDB').abi
const abiToken = require('../deploy/deploy-home/build/contracts/IERC20').abi
const bridge = new web3.eth.Contract(abiBridge, HOME_BRIDGE_ADDRESS)
const token = new web3.eth.Contract(abiToken, HOME_TOKEN_ADDRESS)
const amount = parseInt(process.argv[3])
const query1 = token.methods.approve(HOME_BRIDGE_ADDRESS, amount)
const query2 = bridge.methods.requestAffirmation(amount, process.argv[2])
const amount = parseInt(process.argv[2])
const query = token.methods.transfer(HOME_BRIDGE_ADDRESS, amount)
let nonce
const deployAddress = web3.eth.accounts.privateKeyToAccount(`0x${DEPLOY_PRIVATE_KEY}`).address
async function main () {
console.log(`Transfer from ${deployAddress} to ${HOME_BRIDGE_ADDRESS}, ${amount} tokens`)
console.log(`Exchange to address ${process.argv[2]}`)
nonce = await web3.eth.getTransactionCount(deployAddress)
console.log(await sendQuery(query1, HOME_TOKEN_ADDRESS))
await sendQuery(query2, HOME_BRIDGE_ADDRESS)
console.log(await sendQuery(query, HOME_TOKEN_ADDRESS))
}
async function sendQuery (query, to) {

View File

@ -0,0 +1,13 @@
require('dotenv').config()
const axios = require('axios')
const { FOREIGN_URL, FOREIGN_ASSET } = process.env
const address = process.argv[2]
const httpClient = axios.create({ baseURL: FOREIGN_URL })
httpClient
.get(`/api/v1/account/${address}`)
.then(res => console.log(parseFloat(res.data.balances.find(x => x.symbol === FOREIGN_ASSET).free)))
.catch(console.log)

View File

@ -0,0 +1,12 @@
require('dotenv').config()
const Web3 = require('web3')
const { HOME_RPC_URL, HOME_TOKEN_ADDRESS } = process.env
const web3 = new Web3(HOME_RPC_URL, null, { transactionConfirmationBlocks: 1 })
const abiToken = require('../deploy/deploy-home/build/contracts/IERC20').abi
const token = new web3.eth.Contract(abiToken, HOME_TOKEN_ADDRESS)
const address = process.argv[2]
token.methods.balanceOf(address).call().then(x => console.log(x.toNumber()))

View File

@ -8,18 +8,8 @@ until curl "$1" > /dev/null 2>&1; do
sleep 1;
done
echo "Fetching next tss params"
curl -X GET "$1/next_params" -o ./params > /dev/null 2>&1
echo "Generating key using server $1"
pkill gg18_keygen || true
./gg18_keygen_client "$1" "$2"
echo "Generated keys for all parties"
#echo "Sending confirmation"
#curl -X POST -H "Content-Type: application/json" -d @"$2" "$1/confirm" > /dev/null 2>&1

View File

@ -6,39 +6,56 @@ const amqp = require('amqplib')
const { RABBITMQ_URL, PROXY_URL } = process.env
let currentKeygenEpoch = null
async function main () {
console.log('Connecting to RabbitMQ server')
const connection = await connectRabbit(RABBITMQ_URL)
console.log('Connecting to epoch events queue')
const channel = await connection.createChannel()
const queue = await channel.assertQueue('epochQueue')
const keygenQueue = await channel.assertQueue('keygenQueue')
const cancelKeygenQueue = await channel.assertQueue('cancelKeygenQueue')
channel.prefetch(2)
channel.consume(queue.queue, msg => {
const data = JSON.parse(msg.content)
console.log(`Consumed new epoch event, starting keygen for epoch ${data.epoch}`)
channel.prefetch(1)
channel.consume(keygenQueue.queue, msg => {
const { epoch, parties, threshold } = JSON.parse(msg.content)
console.log(`Consumed new epoch event, starting keygen for epoch ${epoch}`)
const keysFile = `/keys/keys${data.epoch}.store`
const keysFile = `/keys/keys${epoch}.store`
console.log('Running ./keygen-entrypoint.sh')
currentKeygenEpoch = epoch
console.log('Writing params')
fs.writeFileSync('./params', JSON.stringify({ parties: parties.toString(), threshold: threshold.toString() }))
const cmd = exec.execFile('./keygen-entrypoint.sh', [PROXY_URL, keysFile], async () => {
currentKeygenEpoch = null
if (fs.existsSync(keysFile)) {
console.log(`Finished keygen for epoch ${data.epoch}`)
console.log(`Finished keygen for epoch ${epoch}`)
const publicKey = JSON.parse(fs.readFileSync(keysFile))[5]
console.log(`Generated multisig account in binance chain: ${publicKeyToAddress(publicKey)}`)
if (data.epoch === 1) {
console.log('Sending keys confirmation on first generated epoch')
await confirm(keysFile)
}
console.log('Sending keys confirmation')
await confirmKeygen(keysFile)
} else {
console.log(`Keygen for epoch ${data.epoch} failed`)
console.log(`Keygen for epoch ${epoch} failed`)
}
console.log('Ack for keygen message')
channel.ack(msg)
})
cmd.stdout.on('data', data => console.error(data.toString()))
cmd.stdout.on('data', data => console.log(data.toString()))
cmd.stderr.on('data', data => console.error(data.toString()))
})
channel.consume(cancelKeygenQueue.queue, async msg => {
const { epoch } = JSON.parse(msg.content)
console.log(`Consumed new cancel event for epoch ${epoch} keygen`)
if (currentKeygenEpoch === epoch) {
console.log('Cancelling current keygen')
exec.execSync('pkill gg18_keygen || true')
}
channel.ack(msg)
})
}
main()
@ -52,8 +69,8 @@ async function connectRabbit (url) {
})
}
async function confirm (keysFile) {
exec.execSync(`curl -X POST -H "Content-Type: application/json" -d @"${keysFile}" "${PROXY_URL}/confirm"`, { stdio: 'pipe' })
async function confirmKeygen (keysFile) {
exec.execSync(`curl -X POST -H "Content-Type: application/json" -d @"${keysFile}" "${PROXY_URL}/confirmKeygen"`, { stdio: 'pipe' })
}
function publicKeyToAddress ({ x, y }) {

View File

@ -3,7 +3,7 @@ FROM node:10.16.0-slim
WORKDIR /tss
RUN apt-get update && \
apt-get install -y libssl1.1 libssl-dev curl python make g++ libudev-dev libusb-dev usbutils
apt-get install -y libssl1.1 libssl-dev curl python make g++ libudev-dev libusb-dev usbutils procps
#apk packages: libssl1.1 eudev-dev libressl-dev curl build-base python linux-headers libusb-dev
COPY package.json /tss/

View File

@ -5,6 +5,7 @@
"@binance-chain/javascript-sdk": "2.13.9",
"amqplib": "0.5.3",
"axios": "0.19.0",
"bignumber.js": "9.0.0"
"bignumber.js": "9.0.0",
"express": "4.17.1"
}
}

View File

@ -8,10 +8,6 @@ until curl "$1" > /dev/null 2>&1; do
sleep 1;
done
echo "Fetching current tss params"
curl -X GET "$1/current_params" -o ./params > /dev/null 2>&1
echo "Signing message using server $1"
rm -f signature

View File

@ -4,115 +4,148 @@ const amqp = require('amqplib')
const crypto = require('crypto')
const bech32 = require('bech32')
const BN = require('bignumber.js')
const express = require('express')
const { RABBITMQ_URL, FOREIGN_URL, PROXY_URL } = process.env
const FOREIGN_ASSET = 'BNB'
const app = express()
app.get('/restart/:attempt', restart)
app.listen(8001, () => console.log('Listening on 8001'))
const { RABBITMQ_URL, FOREIGN_URL, PROXY_URL, FOREIGN_ASSET } = process.env
const Transaction = require('./tx')
const axios = require('axios')
const httpClient = axios.create({ baseURL: FOREIGN_URL })
let attempt
let nextAttempt = null
let cancelled
async function main () {
console.log('Connecting to RabbitMQ server')
const connection = await connectRabbit(RABBITMQ_URL)
console.log('Connecting to signature events queue')
const channel = await connection.createChannel()
const queue = await channel.assertQueue('signQueue')
const signQueue = await channel.assertQueue('signQueue')
channel.prefetch(1)
channel.consume(queue.queue, async msg => {
channel.consume(signQueue.queue, async msg => {
const data = JSON.parse(msg.content)
console.log('Consumed sign event')
console.log(data)
const { recipient, value, nonce, epoch } = data
const { recipient, value, nonce, epoch, newEpoch, parties, threshold } = data
if (recipient) {
const keysFile = `/keys/keys${epoch}.store`
const keysFile = `/keys/keys${epoch}.store`
const { address: from, publicKey } = await getAccountFromFile(keysFile)
const account = await getAccount(from)
const { address, publicKey } = await getAccountFromFile(keysFile)
console.log(`Tx from ${address}`)
console.log('Writing params')
fs.writeFileSync('./params', JSON.stringify({ parties: parties.toString(), threshold: threshold.toString() }))
const account = await getAccount(address)
attempt = 1
console.log(`Building corresponding trasfer transaction, nonce ${nonce}, recipient ${recipient}`)
const tx = new Transaction(address, account.account_number, nonce, recipient, value, FOREIGN_ASSET)
if (recipient && account.sequence <= nonce) {
while (true) {
console.log(`Building corresponding transfer transaction, nonce ${nonce}, recipient ${recipient}`)
const tx = new Transaction({
from,
accountNumber: account.account_number,
sequence: nonce,
to: recipient,
tokens: value,
asset: FOREIGN_ASSET,
memo: `Attempt ${attempt}`
})
const hash = crypto.createHash('sha256').update(tx.getSignBytes()).digest('hex')
const hash = crypto.createHash('sha256').update(tx.getSignBytes()).digest('hex')
console.log(`Starting signature generation for transaction hash ${hash}`)
const done = await sign(keysFile, hash, tx, publicKey) && await waitForAccountNonce(from, nonce + 1)
console.log(`Starting signature generation for transaction hash ${hash}`)
const cmd = exec.execFile('./sign-entrypoint.sh', [PROXY_URL, keysFile, hash], async () => {
if (fs.existsSync('signature')) {
console.log('Finished signature generation')
const signature = JSON.parse(fs.readFileSync('signature'))
console.log(signature)
console.log('Building signed transaction')
const signedTx = tx.addSignature(publicKey, { r: signature[1], s: signature[3] })
console.log('Sending transaction')
console.log(signedTx)
await sendTx(signedTx)
if (done) {
break
}
await waitForAccountNonce(address, nonce + 1)
channel.ack(msg)
})
cmd.stdout.on('data', data => console.log(data.toString()))
cmd.stderr.on('data', data => console.error(data.toString()))
} else {
const accountFile = await waitLastAccountEpoch(epoch)
// If new keys with greater epoch already exists
if (accountFile === null) {
channel.ack(msg)
return
attempt = nextAttempt ? nextAttempt : attempt + 1
console.log(`Sign failed, starting next attempt ${attempt}`)
nextAttempt = null
await new Promise(resolve => setTimeout(resolve, 1000))
}
const to = accountFile.address
const prevEpoch = getPrevEpoch(epoch)
} else if (account.sequence <= nonce) {
const newKeysFile = `/keys/keys${newEpoch}.store`
const { address: to } = await getAccountFromFile(newKeysFile)
const prevKeysFile = `/keys/keys${prevEpoch}.store`
const { address: from, publicKey } = await getAccountFromFile(prevKeysFile)
console.log(`Tx from ${from}, to ${to}`)
while (true) {
console.log(`Building corresponding transaction for transferring all funds, nonce ${nonce}, recipient ${to}`)
const tx = new Transaction({
from,
accountNumber: account.account_number,
sequence: nonce,
to,
tokens: account.balances.find(x => x.symbol === FOREIGN_ASSET).free,
asset: FOREIGN_ASSET,
bnbs: new BN(account.balances.find(x => x.symbol === 'BNB').free).minus(new BN(60000).div(10 ** 8)),
memo: `Attempt ${attempt}`
})
const account = await getAccount(from)
const hash = crypto.createHash('sha256').update(tx.getSignBytes()).digest('hex')
console.log(`Starting signature generation for transaction hash ${hash}`)
const done = await sign(keysFile, hash, tx, publicKey) && await waitForAccountNonce(from, nonce + 1)
const maxValue = new BN(account.balances.find(x => x.symbol === FOREIGN_ASSET).free).minus(new BN(37500).div(10 ** 8))
console.log(`Building corresponding transaction for transferring all funds, nonce ${account.sequence}, recipient ${to}`)
const tx = new Transaction(from, account.account_number, account.sequence, to, maxValue, FOREIGN_ASSET)
const hash = crypto.createHash('sha256').update(tx.getSignBytes()).digest('hex')
console.log(`Starting signature generation for transaction hash ${hash}`)
const cmd = exec.execFile('./sign-entrypoint.sh', [PROXY_URL, prevKeysFile, hash], async () => {
if (fs.existsSync('signature')) {
console.log('Finished signature generation')
const signature = JSON.parse(fs.readFileSync('signature'))
console.log(signature)
console.log('Building signed transaction')
const signedTx = tx.addSignature(publicKey, { r: signature[1], s: signature[3] })
console.log('Sending transaction')
console.log(signedTx)
await sendTx(signedTx)
if (done) {
await confirmFundsTransfer()
break
}
await waitForAccountNonce(from, account.sequence + 1)
await confirm(`/keys/keys${epoch}.store`)
channel.ack(msg)
})
cmd.stdout.on('data', data => console.log(data.toString()))
cmd.stderr.on('data', data => console.error(data.toString()))
attempt = nextAttempt ? nextAttempt : attempt + 1
console.log(`Sign failed, starting next attempt ${attempt}`)
nextAttempt = null
await new Promise(resolve => setTimeout(resolve, 1000))
}
}
else {
console.log('Tx has been already sent')
}
console.log('Acking message')
channel.ack(msg)
})
}
main()
async function connectRabbit (url) {
function sign (keysFile, hash, tx, publicKey) {
return new Promise(resolve => {
const cmd = exec.execFile('./sign-entrypoint.sh', [PROXY_URL, keysFile, hash], async (error) => {
if (fs.existsSync('signature')) {
console.log('Finished signature generation')
const signature = JSON.parse(fs.readFileSync('signature'))
console.log(signature)
console.log('Building signed transaction')
const signedTx = tx.addSignature(publicKey, { r: signature[1], s: signature[3] })
console.log('Sending transaction')
console.log(signedTx)
await sendTx(signedTx)
resolve(true)
} else if (error === null || error.code === 0) {
resolve(true)
} else {
console.log('Sign failed')
resolve(false)
}
})
cmd.stdout.on('data', data => console.log(data.toString()))
cmd.stderr.on('data', data => console.error(data.toString()))
})
}
function restart (req, res) {
console.log('Cancelling current sign')
nextAttempt = req.params.attempt
exec.execSync('pkill gg18_sign || true')
cancelled = true
res.send('Cancelled')
}
function connectRabbit (url) {
return amqp.connect(url).catch(() => {
console.log('Failed to connect, reconnecting')
return new Promise(resolve =>
@ -121,8 +154,8 @@ async function connectRabbit (url) {
})
}
async function confirm (keysFile) {
exec.execSync(`curl -X POST -H "Content-Type: application/json" -d @"${keysFile}" "${PROXY_URL}/confirm"`, { stdio: 'pipe' })
function confirmFundsTransfer () {
exec.execSync(`curl -X POST -H "Content-Type: application/json" "${PROXY_URL}/confirmFundsTransfer"`, { stdio: 'pipe' })
}
async function getAccountFromFile (file) {
@ -138,49 +171,44 @@ async function getAccountFromFile (file) {
}
}
function getPrevEpoch (epoch) {
return Math.max(0, ...fs.readdirSync('/keys').map(x => parseInt(x.split('.')[0].substr(4))).filter(x => x < epoch))
}
async function waitLastAccountEpoch (epoch) {
while (true) {
const curEpoch = Math.max(0, ...fs.readdirSync('/keys').map(x => parseInt(x.split('.')[0].substr(4))))
if (curEpoch === epoch)
return getAccountFromFile(`/keys/keys${epoch}.store`)
else if (curEpoch > epoch)
return null
else
await new Promise(resolve => setTimeout(resolve, 1000))
}
}
async function waitForAccountNonce (address, nonce) {
cancelled = false
console.log(`Waiting for account ${address} to have nonce ${nonce}`)
while (true) {
while (!cancelled) {
const sequence = (await getAccount(address)).sequence
if (sequence === nonce)
if (sequence >= nonce)
break
await new Promise(resolve => setTimeout(resolve, 1000))
console.log('Waiting for needed account nonce')
}
console.log('Account nonce is OK')
return !cancelled
}
async function getAccount (address) {
function getAccount (address) {
console.log(`Getting account ${address} data`)
return httpClient
.get(`/api/v1/account/${address}`)
.then(res => res.data)
.catch(() => {
console.log('Retrying')
return getAccount(address)
})
}
async function sendTx (tx) {
function sendTx (tx) {
return httpClient
.post(`/api/v1/broadcast?sync=true`, tx, {
headers: {
'Content-Type': 'text/plain'
}
})
.then(x => console.log(x.response), x => console.log(x.response))
.catch(err => {
if (err.response.data.message.includes('Tx already exists in cache'))
console.log('Tx already exists in cache')
else
console.log(err.response)
})
}
function publicKeyToAddress ({ x, y }) {

View File

@ -5,49 +5,59 @@ const BN = require('bignumber.js')
const { FOREIGN_CHAIN_ID } = process.env
class Transaction {
constructor (fromAddress, accountNumber, sequence, toAddress, amount, asset, memo = 'test') {
const accCode = crypto.decodeAddress(fromAddress)
const toAccCode = crypto.decodeAddress(toAddress)
constructor (options) {
const { from, accountNumber, sequence, to, tokens, asset, bnbs, memo = '' } = options
const accCode = crypto.decodeAddress(from)
const toAccCode = crypto.decodeAddress(to)
amount = new BN(amount).multipliedBy(10 ** 8).toNumber()
const coins = []
const coin = {
denom: asset,
amount: amount,
if (tokens && tokens !== '0' && asset) {
coins.push({
denom: asset,
amount: new BN(tokens).multipliedBy(10 ** 8).toNumber(),
})
}
if (bnbs && bnbs !== '0') {
coins.push({
denom: 'BNB',
amount: new BN(bnbs).multipliedBy(10 ** 8).toNumber(),
})
}
coins.sort((a, b) => a.denom > b.denom)
const msg = {
inputs: [{
address: accCode,
coins: [coin]
coins
}],
outputs: [{
address: toAccCode,
coins: [coin]
coins
}],
msgType: 'MsgSend'
}
this.signMsg = {
inputs: [{
address: fromAddress,
coins: [coin]
address: from,
coins
}],
outputs: [{
address: toAddress,
coins: [coin]
address: to,
coins
}]
}
const options = {
this.tx = new TransactionBnc({
account_number: accountNumber,
chain_id: FOREIGN_CHAIN_ID,
memo: memo,
memo,
msg,
sequence,
type: msg.msgType,
}
this.tx = new TransactionBnc(options)
})
}
getSignBytes () {

@ -1 +1 @@
Subproject commit 1d0c8867aa4d3da0d80de41a60ec3fd6bad2f85a
Subproject commit 1b71aae16f2c8c497f8d10067efcf522e6ad575b