Merge pull request #21 from poanetwork/dynamic-validators

Dynamic validators
This commit is contained in:
Kirill Fedoseev 2019-10-18 18:03:11 +03:00 committed by GitHub
commit 5610c0eda2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
98 changed files with 31937 additions and 787 deletions

10
.gitignore vendored
View File

@ -5,12 +5,16 @@ node_modules/
**/signature
**/params
data/
demo/validator*/db
demo/validator*/keys
demo/validator*/queue
demo/validator*/development
demo/validator*/staging
demo/validator*/.keys.staging
demo/ganache_home_db/
demo/ganache_side_db/
demo/*.zip
src/deploy/deploy-home/build/
src/deploy/deploy-side/build/
src/deploy/deploy-test/build/
src/deploy/.keys.staging
src/test-services/.keys.staging
src/test-services/.keys.development
test.js

164
DEMO.md
View File

@ -2,13 +2,32 @@
These instructions describes how to run the bridge between an Ethereum-based chain and the Binance Chain testnet.
### Ethereum side of the bridge
This demo supports two ways of dealing with the Ethereum side of a bridge:
- Using development EVM-base chains ([ganache](https://github.com/trufflesuite/ganache-cli))
- Using public networks (predefined configs use Kovan testnet and Sokol POA testnet)
#### Development mode
As part of this demo two EVM-based chains ([ganache](https://github.com/trufflesuite/ganache-cli)) will be started:
- **Home chain** - it keeps an ERC20 contract (`0x44c158FE850821ae69DaF37AADF5c539e9d0025B`) and the bridge contract (`0x94b40CC641Ed7db241A1f04C8896ba6f6cC36b85`).
- **Side chain** - the MPC orchestration contract (`0x44c158FE850821ae69DaF37AADF5c539e9d0025B`) is located here
- **Home chain** - it keeps an ERC20 contract (`0xd5fE0D28e058D375b0b038fFbB446Da37E85fFdc`) and the bridge contract (`0x44c158FE850821ae69DaF37AADF5c539e9d0025B`).
- **Side chain** - the MPC orchestration contract (`0xd5fE0D28e058D375b0b038fFbB446Da37E85fFdc`) is located here
Both chains are run in separate docker containers.
#### Staging mode
As part of this demo two EVM-based public chains will be used:
- **Home chain** - Kovan testnet keeps an ERC20 contract and the bridge contract.
- **Side chain** - Sokol POA testnet keeps the MPC orchestration contract.
Interaction with chains is done by using public available RPC urls.
### Demo validators
Three validators will be run and only two validators are required to confirm the transfer. Every validator node is a set of docker containers (`eth-watcher`, `bnc-watcher`, `signer`, `proxy`, `redis`, `rabbitmq`).
### Binance side of the bridge
The public Binance Chain testnet will keep a BEP2 token.
### Running demo
@ -56,28 +75,57 @@ The public Binance Chain testnet will keep a BEP2 token.
docker build -t tss -f ./src/tss/Dockerfile-local ./src/tss
```
2. Run test environment
* (2.1) Modify `src/deploy/deploy-test/.env` and specify the amount of tokens to mint in the parameter `TOKEN_INITIAL_MINT`.
* (2.2) Run testnets and deploy contracts
```
./demo/start-environment.sh
```
This command will also mint tokens, the owner of tokens is the address that corresponds to the private key specified in `PRIVATE_KEY_DEV` of `src/deploy/deploy-test/.env`.
* 2.1 Running in development mode (using local ganache networks):
* (2.1.1) Modify `src/deploy/deploy-test/.env.development` and specify the amount of tokens to mint in the parameter `TOKEN_INITIAL_MINT`.
* (2.1.2) Run testnets and deploy contracts
```
TARGET_NETWORK=development ./demo/start-environment.sh
```
This command will also mint tokens, the owner of tokens is the address that corresponds to the
private key specified in `HOME_PRIVATE_KEY` of `src/deploy/deploy-test/.env.development` (`0xA374DC09057D6B3253d04fACb15736B43fBc7943`).
* 2.2 Running in staging mode (using public test networks):
* (2.2.1) Prepare three private keys for validators. Get the Ethereum account addresses for these keys.
* (2.2.2) Modify `src/deploy/deploy-home/.env.staging` and specify the token contract address in
the Kovan network via `HOME_TOKEN_ADDRESS` (use empty address `0x` if you want to create new
ERC20 contract while deployment). \
Set `VALIDATOR_ADDRESS_*` to Ethereum addresses obtained in the previous step.
* (2.2.3) Modify `src/deploy/.keys.staging` and specify private keys for prefunded accounts in both networks.
These accounts are used for contract deployment. Use `src/deploy/.keys.staging.example` as an example.
* (2.2.4) Deploy contracts
```
TARGET_NETWORK=staging ./demo/start-environment.sh
```
This command will deploy ERC20 contract and also mint tokens if you left `HOME_TOKEN_ADDRESS` empty,
the owner of tokens is the address that corresponds to the private key specified in
`HOME_PRIVATE_KEY` of `src/deploy/.keys.staging`.\
Deployed contract address will be automatically updates in all required validators
and test services configs.
* (2.2.5) Prefund validator accounts in home network (Kovan):
```
TARGET_NETWORK=staging ./src/test-services/ethereumSend/run.sh <Nth validator address> 0 0.5
```
* (2.2.6) Prefund validator accounts in side network (Sokol):
```
TARGET_NETWORK=staging ./src/test-services/sidePrefund/run.sh <Nth validator address> 1
```
* (2.3) Get the Ethereum account address for the first test account from its private key (step 1.2). [NiftyWallet](https://forum.poa.network/c/nifty-wallet) could be used for this.
* (2.4) Send few tokens and coins from the current token owner to the first account. Coins are needed to pay transaction fees.
```
./src/test-services/ethereumSend/run.sh <first account Ethereum address> 5000000000000000000 0.5
TARGET_NETWORK=<target network> ./src/test-services/ethereumSend/run.sh <first account Ethereum address> 5000000000000000000 0.5
```
* (2.5) Check that the tokens were transferred properly:
```
./src/test-services/ethereumBalance/run.sh <first account Ethereum address>
TARGET_NETWORK=<target network> ./src/test-services/ethereumBalance/run.sh <first account Ethereum address>
```
3. Run validators nodes:
* (3.1) Modify the parameter `FOREIGN_ASSET` in `demo/validator1/.env`, `demo/validator2/.env` and `demo/validator3/.env` to specify the identificator of the token (step 1.8) that the oracle will watch.
* (3.1) Modify the parameter `FOREIGN_ASSET` in `demo/validator1/.env.<network>`, `demo/validator2/.env.<network>`
and `demo/validator3/.env.<network>` to specify the identificator of the token (step 1.8) that the oracle will watch. \
For staging environment additionally specify `VALIDATOR_PRIVATE_KEY` in the `demo/validator<N>/.keys.staging` (step 2.2.1)
* (3.2) Run three validators in separate terminal sessions.
```
N=1 ./demo/validator-demo.sh
N=2 ./demo/validator-demo.sh
N=3 ./demo/validator-demo.sh
N=1 TARGET_NETWORK=<network> ./demo/validator-demo.sh
N=2 TARGET_NETWORK=<network> ./demo/validator-demo.sh
N=3 TARGET_NETWORK=<network> ./demo/validator-demo.sh
```
Wait for when the line like the following appears:
```
@ -105,15 +153,17 @@ The public Binance Chain testnet will keep a BEP2 token.
```
To check the balance of the bridge account the [Binance Testnet Explorer](https://testnet-explorer.binance.org) could be used. It should report about two assets owned by the account.
5. Transfer tokens from Ethereum-based chain to the Binance Chain:
* (5.1) Modify the parameter `HOME_PRIVATE_KEY` as so it contains the private key of the first test account (step 1.2)
* (5.1) Modify the parameter `HOME_PRIVATE_KEY`
(in `src/test-services/ethereumSend/.env.development` or `src/test-services/.keys.staging`)
as so it contains the private key of the first test account (step 1.2)
* (5.2) Send some amount of tokens to the bridge contract:
```
./src/test-services/ethereumSend/run.sh bridge 5000000000000000000
TARGET_NETWORK=<network> ./src/test-services/ethereumSend/run.sh bridge 5000000000000000000
```
* (5.3) The validators will catch the event and start the process to sign the transaction.
* (5.4) As soon as the signature is generated and sent, the balance of the bridge account in both chains will be changed:
```
./src/test-services/ethereumBalance/run.sh 0x94b40CC641Ed7db241A1f04C8896ba6f6cC36b85
TARGET_NETWORK=<network> ./src/test-services/ethereumBalance/run.sh 0x94b40CC641Ed7db241A1f04C8896ba6f6cC36b85
```
should report non-zero balance
```
@ -131,6 +181,76 @@ The public Binance Chain testnet will keep a BEP2 token.
--node=data-seed-pre-2-s1.binance.org:80 --memo "any note"
```
* Check the balances of the test account on both sides of the bridge to see that the funds were transferred properly.
7. Bridge supports changing the list of validators and required voting threshold via voting process, and then keys regeneration.
* (7.0) Obtain information about current epoch, current list validators, upcoming epoch information, bridge state via:
```
./curl http://localhost:$PORT/info
```
Where `$PORT` is specific port for some validator oracle.
The response object contains lots of useful information about current bridge state.
```json5
{
// current epoch number, in which bridge is operating
"epoch": 2,
// next epoch number, for which votes and keygen operations are applied
"nextEpoch": 3,
// threshold number for current epoch,
// threshold + 1 votes are required for any changes in next epoch
"threshold": 1,
// threshold number for next epoch
"nextThreshold": 1,
// current bridge addresses in home and foreign networks
"homeBridgeAddress": "0x44c158FE850821ae69DaF37AADF5c539e9d0025B",
"foreignBridgeAddress": "tbnb19z22khee969yj05dckg9usvmwndkucpyl543xk",
// current set of validators
"validators": [
"0x99Eb3D86663c6Db090eFFdBC20510Ca9f836DCE3",
"0x6352e3e6038e05b9da00C84AE851308f9774F883"
],
// set of validators for the next epoch
"nextValidators": [
"0x99Eb3D86663c6Db090eFFdBC20510Ca9f836DCE3",
"0x6352e3e6038e05b9da00C84AE851308f9774F883",
"0xAa006899B0EC407De930bA8A166DEfe59bBfd3DC"
],
// balances of bridge in both networks
"homeBalance": 50,
"foreignBalanceTokens": 100,
"foreignBalanceNative": 0.0994,
// current bridge status, can be one of: ready, voting, keygen, funds_transfer
"bridgeStatus": "ready",
// current votes count for starting voting, starting/cancelling keygen
// -1 means that enough confirmations are already collected
"votesForVoting": 0,
"votesForKeygen": 0,
"votesForCancelKeygen": 0,
// collected confirmations for changing epoch to nextEpoch
// -1 means that enough confirmations are already collected
"confirmationsForFundsTransfer": 0
}
```
* (7.1) Start voting process for next epoch, via sending `$THRESHOLD + 1` requests to `/vote/startVoting` url. Bridge
state should be successfully changed to `voting`.
* 7.2 Changing next epoch bridge validators / threshold
* (7.2.1) Add / remove validator in next validators list, via sending `$THRESHOLD + 1` requests to
`/vote/addValidator/$ADDRESS` / `/vote/removeValidator/$ADDRESS`.
* (7.2.2) Change threshold for the next epoch, via sending `$THRESHOLD + 1` requests to `/vote/changeThreshold/$THRESHOLD`.
* (7.3) Start keygen process for next epoch, via sending `$THRESHOLD + 1` requests to `/vote/startKeygen` url. Bridge
state should be successfully changed to `keygen`, and in some time to `funds_transfer`, and then to `ready`.
* (7.4) If keygen process at some state was stopped(i. e. one validator turned of his oracle),
it can be cancelled via via sending `$THRESHOLD + 1` requests to `/vote/cancelKeygen` url. After
keygen cancellation, bridge state will return to `voting`, and later it can be restarted manually
once again.
### Finish demo
@ -150,7 +270,7 @@ The public Binance Chain testnet will keep a BEP2 token.
```
4. Remove testnets and validators data:
```
./demo/clean.sh
TARGET_NETWORK=<network> ./demo/clean.sh
```
#### Testing tools for both sides of the bridge
@ -163,10 +283,14 @@ In these tools, `run.sh` file simply builds and runs a docker container for inte
- `NATIVE` - amount of BNB tokens to send, if present, the
transaction is considered as a funding one.
* `./src/test-services/ethereumSend/run.sh TO TOKENS NATIVE`
- Transfers specified amount of tokens and coins to the an Ethereum account.
- Transfers specified amount of tokens and coins to the an Ethereum account on the home network.
- `TO` - receiver address in the Ethereum-based chain, specify `bridge` to send tokens to the bridge address.
- `VALUE` - amount of tokens to transfer and exchange.
- `NATIVE` - amount of coins to send (in `ether`). Could be omitted.
* `./src/test-services/sidePrefund/run.sh TO NATIVE`
- Transfers specified amount of tokens and coins to the an Ethereum account on the side network.
- `TO` - receiver address in the Ethereum-based chain.
- `NATIVE` - amount of coins to send (in `ether`). Could be omitted.
* `./src/test-services/binanceBalance/run.sh ADDRESS` (it is recommended to use `tbnbcli` instead)
- Gets current BEP2 token and BNB balances of the specified account.
- `ADDRESS` - account address in the Binance Chain.
@ -194,4 +318,4 @@ container for listening GET requests
- http://localhost:5001/vote/addValidator/ADDRESS
- `ADDRESS` - Ethereum address of a validator.
- After enough votes are collected, validator is removed from
the next validators list for the next epoch.
the next validators list for the next epoch.

View File

@ -4,15 +4,22 @@ set -e
cd $(dirname "$0")
rm -rf ganache_side_db
rm -rf ganache_home_db
# either development or staging
TARGET_NETWORK=${TARGET_NETWORK:=development}
echo "Cleaning $TARGET_NETWORK network"
if [[ "$TARGET_NETWORK" == "development" ]]; then
rm -rf ganache_side_db
rm -rf ganache_home_db
mkdir ganache_side_db
mkdir ganache_home_db
fi
for (( I = 1; I < 4; ++I )); do
DIRNAME="validator$I"
rm -rf "$DIRNAME/db"
rm -rf "$DIRNAME/queue"
rm -rf "$DIRNAME/keys"
mkdir "$DIRNAME/db"
mkdir "$DIRNAME/queue"
mkdir "$DIRNAME/keys"
rm -rf "$DIRNAME/$TARGET_NETWORK"
mkdir -p "$DIRNAME/$TARGET_NETWORK/db"
mkdir -p "$DIRNAME/$TARGET_NETWORK/queue"
mkdir -p "$DIRNAME/$TARGET_NETWORK/keys"
done

22
demo/reset-to-block.sh Executable file
View File

@ -0,0 +1,22 @@
#!/bin/bash
set -e
cd $(dirname "$0")
# either development or staging
TARGET_NETWORK=${TARGET_NETWORK:=development}
echo "Resetting block in redis"
docker network create redis_net > /dev/null 2>&1 || true
docker kill redis > /dev/null 2>&1 || true
docker rm redis > /dev/null 2>&1 || true
docker run --rm --network redis_net -d --name redis \
-v "`pwd`/validator$N/$TARGET_NETWORK/db:/data" \
-v "`dirname "$(pwd)"`/src/oracle/configs/redis.conf:/usr/local/etc/redis/redis.conf" \
redis:5.0.5-alpine > /dev/null 2>&1 || true
../src/oracle/scripts/resetToBlock/run.sh redis_net $1
docker kill redis > /dev/null 2>&1 || true

View File

@ -0,0 +1,417 @@
ObjC.import('stdlib')
const terminal = Application('Terminal')
const system = Application('System Events')
const curApp = Application.currentApplication()
curApp.includeStandardAdditions = true
const validator1 = '0x99Eb3D86663c6Db090eFFdBC20510Ca9f836DCE3'
const validator2 = '0xAa006899B0EC407De930bA8A166DEfe59bBfd3DC'
const validator3 = '0x6352e3e6038e05b9da00C84AE851308f9774F883'
const userAccounts = [
{
privateKey: '7ed93ad7753e00b52265a73dfbbcd2296256772965323fcb9a6320b5cd084b89',
ethAddress: '0x4db6b4bd0a3fdc03b027a60f1c48f05c572312aa',
bncAddress: 'tbnb14r3z8xk7qsar3vwj05w8cd8gqwk7g6gfurlt5l'
},
{
privateKey: '2ad6e3a232ad3ea058b61352302118a99085600ff8b6eec4ccf0066a33756231',
ethAddress: '0xf7ca4aed1795e424433498cef43f6a3825c88731',
bncAddress: 'tbnb1efjg7xt98t67ql2cmwjc5860lgayet9l8m55ym'
},
{
privateKey: 'eb6dd328677b3fa2822fb8e834507e569bda52e8ffa49266df0f2de239c4ec98',
ethAddress: '0xad6c8127143032d843a260c5d379d8d9b3d51f15',
bncAddress: 'tbnb12epcy4p7ktas0nlyrfuektcyh0e83dwzuq73f4'
}
]
let bridgeBncAddress
const windows = terminal.windows()
const wins = []
function saveBlockchainData () {
console.log('Saving blockchain data')
curApp.doShellScript('zip -r ./demo/ganache_home_backup.zip ./demo/ganache_home_db')
curApp.doShellScript('zip -r ./demo/ganache_side_backup.zip ./demo/ganache_side_db')
}
function reloadBlockchainData () {
console.log('Reloading blockchain data')
curApp.doShellScript('unzip -d . ./demo/ganache_home_backup.zip')
curApp.doShellScript('unzip -d . ./demo/ganache_side_backup.zip')
}
function closeOldWindows () {
for (let i in windows) {
try {
windows[i].selectedTab()
if (windows[i].selectedTab().customTitle().startsWith('Validator') || windows[i].selectedTab().customTitle() === 'Services') {
windows[i].close()
}
} catch (e) {
}
}
}
function killValidators () {
terminal.activate()
for (let i = 0; i < 3; i++) {
wins[i].frontmost = true
delay(0.5)
system.keystroke('c', { using: 'control down' })
}
}
function openNewWindows () {
for (let i = 0; i < 3; i++) {
// open new terminal
const tab = terminal.doScript()
// get opened window
const winId = terminal.windows[0].id()
wins[i] = terminal.windows.byId(winId)
tab.customTitle = `Validator ${i + 1}`
}
wins[0].bounds = { x: 0, y: 23, width: 558, height: 1027 }
wins[1].bounds = { x: 559, y: 374, width: 560, height: 676 }
wins[2].bounds = { x: 1120, y: 374, width: 560, height: 676 }
// open new terminal
const tab = terminal.doScript()
// get opened window
const winId = terminal.windows[0].id()
wins[3] = terminal.windows.byId(winId)
tab.customTitle = `Services`
wins[3].bounds = { x: 559, y: 23, width: 1120, height: 350 }
terminal.activate()
delay(0.5)
}
function apiRequestBackground (url) {
const response = curApp.doShellScript(`curl -s -X GET "${url}"`)
try {
return JSON.parse(response)
} catch (e) {
return response
}
}
function exec (n, script) {
terminal.doScript(script, { in: wins[n - 1] })
}
function wait (n) {
while (wins[n - 1].selectedTab().busy()) {
delay(0.2)
}
}
function execSync (n, script) {
exec(n, script)
wait(n)
}
function waitAll () {
wait(1)
wait(2)
wait(3)
wait(4)
}
function waitLog (n, log) {
do {
const s = wins[n - 1].selectedTab().contents().split('\n').find(x => x.includes(log))
if (s) {
return s
}
delay(0.2)
} while (true)
}
function waitApi (n, url, check) {
do {
const res = apiRequestBackground(`http://localhost:500${n}${url}`)
const checkerRes = check ? check(res) : true
if (checkerRes)
return checkerRes
delay(3)
} while (true)
}
function prefundEthAddresses () {
for (let { ethAddress } of userAccounts) {
execSync(4, `./src/test-services/ethereumSend/run.sh ${ethAddress} 100`)
}
}
function prefundBncAddresses () {
for (let { bncAddress } of userAccounts) {
execSync(4, `./src/test-services/binanceSend/run.sh ${bncAddress} 100 0.1`)
}
}
function initBalances () {
userAccounts.forEach(account => {
account.ethBalance = getEthTokenBalance(account.ethAddress)
account.bncBalance = getBncTokenBalance(account.bncAddress)
})
}
function getBncTokenBalance (address) {
const res = curApp.doShellScript(`./src/test-services/binanceBalance/run.sh ${address}`)
return parseFloat(/KFT-94F: [0-9.]+/.exec(res)[0].split(' ')[1])
}
function waitBncTokenBalance (address, balance) {
while (true) {
const newBalance = getBncTokenBalance(address)
if (Math.abs(newBalance - balance) < 0.0001)
return newBalance
delay(3)
}
}
function getEthTokenBalance (address) {
const res = curApp.doShellScript(`./src/test-services/ethereumBalance/run.sh ${address}`)
return parseFloat(/[0-9.]+ tokens/.exec(res)[0].split(' ')[0])
}
function waitEthTokenBalance (address, balance) {
while (true) {
const newBalance = getEthTokenBalance(address)
if (Math.abs(newBalance - balance) < 0.0001)
return newBalance
delay(3)
}
}
function apiRequest (n, url, suffix) {
execSync(4, `curl -s -X GET http://localhost:500${n}${url} ${suffix ? suffix : ''}`)
}
function printState (msg) {
execSync(4, `echo "${msg}"`)
apiRequest(1, '/info', '| jq .')
}
function initCwd () {
const cwd = $.getenv('PWD')
for (let i = 1; i <= 4; i++) {
exec(i, `cd "${cwd}"`)
}
waitAll()
}
function killDockerContainers () {
execSync(4, `docker kill $(docker ps | grep validator | awk '{print $1}') > /dev/null 2>&1 || true`)
execSync(4, `docker kill ganache_side ganache_home > /dev/null 2>&1 || true`)
}
function clean () {
killDockerContainers()
execSync(4, `./demo/clean.sh`)
exec(1, `clear`)
exec(2, `clear`)
exec(3, `clear`)
waitAll()
}
function testEthToBnc () {
console.log('Testing eth => bnc')
// try token transfer in eth => bnc direction
let prevBridgeHomeBalance
let prevBridgeForeignBalance
waitApi(1, '/info', res => {
prevBridgeHomeBalance = res.homeBalance
prevBridgeForeignBalance = res.foreignBalanceTokens
return true
})
userAccounts.forEach((account, i) => {
execSync(4, `PRIVATE_KEY=${account.privateKey} ./src/test-services/ethereumSend/run.sh bridge ${5 + i}`)
account.ethBalance -= 5 + i
})
const delta = (9 + userAccounts.length) * userAccounts.length / 2
waitApi(1, '/info', res => res.homeBalance === prevBridgeHomeBalance + delta && res.foreignBalanceTokens === prevBridgeForeignBalance - delta)
userAccounts.forEach((account, i) => {
account.bncBalance = waitBncTokenBalance(account.bncAddress, account.bncBalance + 5 + i)
})
printState(`Token transfer in eth => bnc direction succeed`)
console.log('Testing eth => bnc is OK')
}
function testBncToEth () {
console.log('Testing bnc => eth')
// try token transfer in bnc => eth direction
let prevBridgeHomeBalance
let prevBridgeForeignBalance
waitApi(1, '/info', res => {
prevBridgeHomeBalance = res.homeBalance
prevBridgeForeignBalance = res.foreignBalanceTokens
return true
})
userAccounts.forEach((account , i) => {
execSync(4, `PRIVATE_KEY=${account.privateKey} ./src/test-services/binanceSend/run.sh ${bridgeBncAddress} ${3 + i}`)
account.bncBalance -= 3 + i
})
const delta = (5 + userAccounts.length) * userAccounts.length / 2
waitApi(1, '/info', res => res.homeBalance === prevBridgeHomeBalance - delta && res.foreignBalanceTokens === prevBridgeForeignBalance + delta)
userAccounts.forEach((account, i) => {
account.ethBalance = waitEthTokenBalance(account.ethAddress, account.ethBalance + 3 + i)
})
printState(`Token transfer in bnc => eth direction succeed`)
console.log('Testing bnc => eth is OK')
}
function testRemoveValidator () {
console.log('Testing removing validator')
apiRequest(1, '/vote/startVoting')
apiRequest(2, '/vote/startVoting')
waitApi(1, '/info', res => res.bridgeStatus === 'voting')
apiRequest(1, `/vote/removeValidator/${validator2}`)
apiRequest(3, `/vote/removeValidator/${validator2}`)
waitApi(1, '/info', res => res.nextValidators.length === 2)
apiRequest(1, '/vote/startKeygen')
apiRequest(3, '/vote/startKeygen')
waitApi(1, '/info', res => {
if (res.bridgeStatus === 'ready' && res.epoch === 2 && res.validators.length === 2) {
bridgeBncAddress = res.foreignBridgeAddress
return true
}
return false
})
printState(`Removing validator succeed`)
console.log('Testing removing validator is OK')
}
function testAddValidator () {
console.log('Testing adding validator')
apiRequest(1, '/vote/startVoting')
apiRequest(3, '/vote/startVoting')
waitApi(1, '/info', res => res.bridgeStatus === 'voting')
apiRequest(1, `/vote/addValidator/${validator2}`)
apiRequest(3, `/vote/addValidator/${validator2}`)
waitApi(1, '/info', res => res.nextValidators.length === 3)
apiRequest(1, '/vote/startKeygen')
apiRequest(3, '/vote/startKeygen')
waitApi(1, '/info', res => {
if (res.bridgeStatus === 'ready' && res.epoch === 3 && res.validators.length === 3) {
bridgeBncAddress = res.foreignBridgeAddress
return true
}
return false
})
printState(`Adding validator succeed`)
console.log('Testing adding validator is OK')
}
function testChangeThreshold () {
console.log('Testing changing threshold')
apiRequest(1, '/vote/startVoting')
apiRequest(3, '/vote/startVoting')
waitApi(1, '/info', res => res.bridgeStatus === 'voting')
apiRequest(2, `/vote/changeThreshold/2`)
apiRequest(3, `/vote/changeThreshold/2`)
waitApi(1, '/info', res => res.nextThreshold === 2)
apiRequest(1, '/vote/startKeygen')
apiRequest(2, '/vote/startKeygen')
waitApi(1, '/info', res => {
if (res.bridgeStatus === 'ready' && res.epoch === 4 && res.threshold === 2) {
bridgeBncAddress = res.foreignBridgeAddress
return true
}
return false
})
printState(`Changing threshold succeed`)
console.log('Testing changing threshold is OK')
}
function run () {
closeOldWindows()
openNewWindows()
initCwd()
clean()
if ($.getenv('RELOAD') !== 'true') {
execSync(4, `./demo/start-environment.sh`)
prefundEthAddresses()
saveBlockchainData()
} else {
reloadBlockchainData()
execSync(4, `./demo/start-environment.sh`)
}
prefundBncAddresses()
initBalances()
exec(1, `N=1 ./demo/validator-demo.sh`)
exec(2, `N=2 ./demo/validator-demo.sh`)
exec(3, `N=3 ./demo/validator-demo.sh`)
// wait until binance account willl be generated
waitLog(1, 'Generated multisig account in binance chain')
waitApi(1, '/info', res => {
if (res.epoch === 1) {
bridgeBncAddress = res.foreignBridgeAddress
return true
}
return false
})
// prefund binance account
execSync(4, `./src/test-services/binanceSend/run.sh ${bridgeBncAddress} 100 0.1`)
// wait until binance prefund transaction will be processed
waitApi(1, '/info', res => res.foreignBalanceTokens === 100)
printState(`Binance bridge account at ${bridgeBncAddress} for epoch 1 is generated and prefunded`)
testEthToBnc()
testBncToEth()
testRemoveValidator()
testEthToBnc()
testBncToEth()
testAddValidator()
testEthToBnc()
testBncToEth()
testChangeThreshold()
testEthToBnc()
testBncToEth()
console.log('PASSED ALL TESTS')
killValidators()
killDockerContainers()
}

5
demo/scenarios/macos/run.sh Executable file
View File

@ -0,0 +1,5 @@
#!/bin/bash
RELOAD=${RELOAD:=false}
RELOAD="$RELOAD" osascript -l JavaScript ./demo/scenarios/macos/main.jxa

View File

@ -3,78 +3,165 @@
set -e
cd $(dirname "$0")
cd ..
start_blockchains() {
# either development or staging
TARGET_NETWORK=${TARGET_NETWORK:=development}
DEPLOY_DIR="`pwd`/src/deploy"
TEST_SERVICES_DIR="`pwd`/src/test-services"
DEMO_DIR="`pwd`/demo"
SIDE_GANACHE_DB="$DEMO_DIR/ganache_side_db"
HOME_GANACHE_DB="$DEMO_DIR/ganache_home_db"
start_dev_blockchain_networks() {
echo "Starting side test blockchain"
docker kill ganache_side > /dev/null 2>&1 || true
docker network create blockchain_side > /dev/null 2>&1 || true
docker run -d --network blockchain_side --rm --name ganache_side -v "$side_db_mount_point:/app/db" \
docker run -d --network blockchain_side --rm --name ganache_side -v "$SIDE_GANACHE_DB:/app/db" \
-p "7545:8545" \
trufflesuite/ganache-cli:latest \
-m "shrug dwarf easily blade trigger lucky reopen cage lake scatter desk boat" -i 33 -q --db /app/db
-m "shrug dwarf easily blade trigger lucky reopen cage lake scatter desk boat" -i 33 -q --db /app/db -b 3 --noVMErrorsOnRPCResponse
echo "Starting home test blockchain"
docker kill ganache_home > /dev/null 2>&1 || true
docker network create blockchain_home > /dev/null 2>&1 || true
docker run -d --network blockchain_home --rm --name ganache_home -v "$home_db_mount_point:/app/db" \
docker run -d --network blockchain_home --rm --name ganache_home -v "$HOME_GANACHE_DB:/app/db" \
-p "8545:8545" \
trufflesuite/ganache-cli:latest \
-m "shrug dwarf easily blade trigger lucky reopen cage lake scatter desk boat" -i 44 -q --db /app/db
-m "shrug dwarf easily blade trigger lucky reopen cage lake scatter desk boat" -i 44 -q --db /app/db -b 3 --noVMErrorsOnRPCResponse
sleep 4
}
deploy_all() {
deploy_token() {
echo "Compiling and deploying erc20"
cd ../src/deploy/deploy-test
echo "Building deploy docker image"
docker build -t deploy_test . > /dev/null 2>&1
docker build -t deploy_test "$DEPLOY_DIR/deploy-test" > /dev/null 2>&1
echo "Deploying"
docker run --network blockchain_home --rm -v "$(pwd)/build:/build/build" --env-file .env deploy_test --network development --reset > /dev/null 2>&1
if [[ "$TARGET_NETWORK" == "development" ]]; then
TOKEN_ADDRESS=$(docker run --network blockchain_home --rm -v "$DEPLOY_DIR/deploy-test/build:/build/build" --env-file "$DEPLOY_DIR/deploy-test/.env.$TARGET_NETWORK" \
deploy_test \
--network home 2>&1 \
| grep "contract address" \
| awk '{print $4}')
else
TOKEN_ADDRESS=$(docker run --rm -v "$DEPLOY_DIR/deploy-test/build:/build/build" --env-file "$DEPLOY_DIR/deploy-test/.env.$TARGET_NETWORK" --env-file "$DEPLOY_DIR/.keys.$TARGET_NETWORK" \
deploy_test \
--network home 2>&1 \
| grep "contract address" \
| awk '{print $4}')
fi
}
deploy_bridge() {
echo "Compiling and deploying home part"
cd ../deploy-home
echo "Building deploy docker image"
docker build -t deploy_home . > /dev/null 2>&1
docker build -t deploy_home "$DEPLOY_DIR/deploy-home" > /dev/null 2>&1
echo "Deploying"
docker run --network blockchain_home --rm -v "$(pwd)/build:/build/build" --env-file .env deploy_home --network development --reset > /dev/null 2>&1
if [[ "$TARGET_NETWORK" == "development" ]]; then
BRIDGE_ADDRESS=$(docker run --network blockchain_home --rm -v "$DEPLOY_DIR/deploy-home/build:/build/build" --env-file "$DEPLOY_DIR/deploy-home/.env.$TARGET_NETWORK" \
deploy_home \
--network home 2>&1 \
| grep "contract address" \
| awk '{print $4}')
else
BRIDGE_ADDRESS=$(docker run --rm -v "$DEPLOY_DIR/deploy-home/build:/build/build" --env-file "$DEPLOY_DIR/deploy-home/.env.$TARGET_NETWORK" --env-file "$DEPLOY_DIR/.keys.$TARGET_NETWORK" \
deploy_home \
--network home 2>&1 \
| grep "contract address" \
| awk '{print $4}')
fi
}
deploy_db() {
echo "Compiling and deploying side part"
cd ../deploy-side
echo "Building deploy docker image"
docker build -t deploy_side . > /dev/null 2>&1
docker build -t deploy_side "$DEPLOY_DIR/deploy-side" > /dev/null 2>&1
echo "Deploying"
docker run --network blockchain_side --rm -v "$(pwd)/build:/build/build" --env-file .env deploy_side --network development --reset > /dev/null 2>&1
if [[ "$TARGET_NETWORK" == "development" ]]; then
SHARED_DB_ADDRESS=$(docker run --network blockchain_side --rm -v "$DEPLOY_DIR/deploy-side/build:/build/build" --env-file "$DEPLOY_DIR/deploy-side/.env.$TARGET_NETWORK" \
deploy_side \
--network side 2>&1 \
| grep "contract address" \
| awk '{print $4}')
else
SHARED_DB_ADDRESS=$(docker run --rm -v "$DEPLOY_DIR/deploy-side/build:/build/build" --env-file "$DEPLOY_DIR/deploy-side/.env.$TARGET_NETWORK" --env-file "$DEPLOY_DIR/.keys.$TARGET_NETWORK" \
deploy_side \
--network side 2>&1 \
| grep "contract address" \
| awk '{print $4}')
fi
}
deploy_all() {
TOKEN_ADDRESS=$(source "$DEPLOY_DIR/deploy-home/.env.$TARGET_NETWORK"; echo "$HOME_TOKEN_ADDRESS")
if [[ "$TARGET_NETWORK" == "development" ]] || [[ "$TOKEN_ADDRESS" == "0x" ]]; then
deploy_token
sed -i 's/TOKEN_ADDRESS=0x$/TOKEN_ADDRESS='"$TOKEN_ADDRESS"'/' "$DEPLOY_DIR/deploy-home/.env.$TARGET_NETWORK"
fi
deploy_bridge
deploy_db
echo "Token contract address in $TARGET_NETWORK network is $TOKEN_ADDRESS"
echo "Bridge contract address in $TARGET_NETWORK network is $BRIDGE_ADDRESS"
echo "Database contract address in $TARGET_NETWORK side network is $SHARED_DB_ADDRESS"
echo "Updating deployed contract addresses in demo validators .env.$TARGET_NETWORK configs"
for file in "$DEMO_DIR"/validator*/.env."$TARGET_NETWORK"; do
sed -i 's/HOME_TOKEN_ADDRESS=.*$/HOME_TOKEN_ADDRESS='"$TOKEN_ADDRESS"'/' "$file"
sed -i 's/HOME_BRIDGE_ADDRESS=.*$/HOME_BRIDGE_ADDRESS='"$BRIDGE_ADDRESS"'/' "$file"
sed -i 's/SIDE_SHARED_DB_ADDRESS=.*$/SIDE_SHARED_DB_ADDRESS='"$SHARED_DB_ADDRESS"'/' "$file"
done
echo "Updating deployed contract addresses in test-services .env.$TARGET_NETWORK configs"
sed -i 's/HOME_TOKEN_ADDRESS=.*$/HOME_TOKEN_ADDRESS='"$TOKEN_ADDRESS"'/' "$TEST_SERVICES_DIR/ethereumBalance/.env.$TARGET_NETWORK"
sed -i 's/HOME_BRIDGE_ADDRESS=.*$/HOME_BRIDGE_ADDRESS='"$BRIDGE_ADDRESS"'/' "$TEST_SERVICES_DIR/ethereumSend/.env.$TARGET_NETWORK"
sed -i 's/HOME_TOKEN_ADDRESS=.*$/HOME_TOKEN_ADDRESS='"$TOKEN_ADDRESS"'/' "$TEST_SERVICES_DIR/ethereumSend/.env.$TARGET_NETWORK"
}
side_db_mount_point="$(pwd)/ganache_side_db"
if [ ! -d "$side_db_mount_point" ]; then
mkdir "$side_db_mount_point"
fi
home_db_mount_point="$(pwd)/ganache_home_db"
if [ ! -d "$home_db_mount_point" ]; then
mkdir "$home_db_mount_point"
fi
if [ -z "$(ls -A ganache_side_db)" ] || [ -z "$(ls -A ganache_home_db)" ]; then
echo "Starting new blockchain networks and deploying contracts"
start_blockchains
deploy_all
if [[ "$TARGET_NETWORK" == "development" ]]; then
if [[ ! -d "$SIDE_GANACHE_DB" ]]; then
mkdir "$SIDE_GANACHE_DB"
fi
if [[ ! -d "$HOME_GANACHE_DB" ]]; then
mkdir "$HOME_GANACHE_DB"
fi
if [[ -z "$(ls -A "$SIDE_GANACHE_DB")" ]] || [[ -z "$(ls -A "$HOME_GANACHE_DB")" ]]; then
echo "Starting dev blockchain networks and deploying contracts"
need_to_deploy=true
else
echo "Restarting dev blockchain networks"
fi
start_dev_blockchain_networks
if [[ -n "$need_to_deploy" ]]; then
deploy_all
else
echo "Contracts are already deployed, run clean.sh first if you want to redeploy everything"
fi
else
echo "Restarting blockchain networks"
start_blockchains
echo "Deploying to the staging blockchain environment"
deploy_all
fi
echo "Done"

View File

@ -1,14 +1,26 @@
#!/bin/bash
set -e
set -o allexport
# either development or staging
TARGET_NETWORK=${TARGET_NETWORK:=development}
DCU_FLAGS="--build --force-recreate"
NAME="validator$N"
cd $(dirname "$0")
echo "Starting $NAME"
echo "Starting $NAME in $TARGET_NETWORK network"
mkdir -p "$NAME"
cd "$NAME"
# load private key form git ignored .keys file
if [[ "$TARGET_NETWORK" == "staging" ]]; then
source ".keys.$TARGET_NETWORK"
fi
# load env for particular environment
source ".env.$TARGET_NETWORK"
docker-compose -p "$NAME" -f ../../src/oracle/docker-compose-test.yml up ${DCU_FLAGS}

View File

@ -1,10 +1,9 @@
HOME_RPC_URL=http://ganache_home:8545
HOME_CHAIN_ID=44
HOME_BRIDGE_ADDRESS=0x44c158FE850821ae69DaF37AADF5c539e9d0025B
HOME_TOKEN_ADDRESS=0xd5fE0D28e058D375b0b038fFbB446Da37E85fFdc
HOME_START_BLOCK=1
SIDE_RPC_URL=http://ganache_side:8545
SIDE_CHAIN_ID=33
SIDE_SHARED_DB_ADDRESS=0xd5fE0D28e058D375b0b038fFbB446Da37E85fFdc
FOREIGN_URL=https://testnet-dex.binance.org/
@ -16,3 +15,5 @@ VALIDATOR_PRIVATE_KEY=2be3f252e16541bf1bb2d4a517d2bf173e6d09f2d765d32c64dc50515a
VOTES_PROXY_PORT=5001
SIGN_RESTART_PORT=6001
LOG_LEVEL=debug

View File

@ -0,0 +1,19 @@
HOME_RPC_URL=https://kovan.infura.io/v3/5d7bd94c50ed43fab1cb8e74f58678b0
HOME_BRIDGE_ADDRESS=0x6ADCa5e691341fb9de8927d15c0a89B83A4E665e
HOME_TOKEN_ADDRESS=0x57d2533B640cfb58f8f1F69C14c089968Da9fdFc
HOME_START_BLOCK=13276224
SIDE_RPC_URL=https://sokol.poa.network
SIDE_SHARED_DB_ADDRESS=0xda9a1cA2Fcb18cAB02934269369627D2b4ea8902
FOREIGN_URL=https://testnet-dex.binance.org/
FOREIGN_CHAIN_ID=Binance-Chain-Nile
FOREIGN_ASSET=KFT-94F
#VALIDATOR_PRIVATE_KEY is taken from .keys
VOTES_PROXY_PORT=5001
SIGN_RESTART_PORT=6001
LOG_LEVEL=info

View File

@ -0,0 +1 @@
VALIDATOR_PRIVATE_KEY=0000000000000000000000000000000000000000000000000000000000000000

View File

@ -1,10 +1,9 @@
HOME_RPC_URL=http://ganache_home:8545
HOME_CHAIN_ID=44
HOME_BRIDGE_ADDRESS=0x44c158FE850821ae69DaF37AADF5c539e9d0025B
HOME_TOKEN_ADDRESS=0xd5fE0D28e058D375b0b038fFbB446Da37E85fFdc
HOME_START_BLOCK=1
SIDE_RPC_URL=http://ganache_side:8545
SIDE_CHAIN_ID=33
SIDE_SHARED_DB_ADDRESS=0xd5fE0D28e058D375b0b038fFbB446Da37E85fFdc
FOREIGN_URL=https://testnet-dex.binance.org/
@ -16,3 +15,5 @@ VALIDATOR_PRIVATE_KEY=e59d58c77b791f98f10187117374ae9c589d48a62720ec6a5e142b0cc1
VOTES_PROXY_PORT=5002
SIGN_RESTART_PORT=6002
LOG_LEVEL=debug

View File

@ -0,0 +1,19 @@
HOME_RPC_URL=https://kovan.infura.io/v3/5d7bd94c50ed43fab1cb8e74f58678b0
HOME_BRIDGE_ADDRESS=0x6ADCa5e691341fb9de8927d15c0a89B83A4E665e
HOME_TOKEN_ADDRESS=0x57d2533B640cfb58f8f1F69C14c089968Da9fdFc
HOME_START_BLOCK=13276224
SIDE_RPC_URL=https://sokol.poa.network
SIDE_SHARED_DB_ADDRESS=0xda9a1cA2Fcb18cAB02934269369627D2b4ea8902
FOREIGN_URL=https://testnet-dex.binance.org/
FOREIGN_CHAIN_ID=Binance-Chain-Nile
FOREIGN_ASSET=KFT-94F
#VALIDATOR_PRIVATE_KEY is taken from .keys
VOTES_PROXY_PORT=5002
SIGN_RESTART_PORT=6002
LOG_LEVEL=info

View File

@ -0,0 +1 @@
VALIDATOR_PRIVATE_KEY=1111111111111111111111111111111111111111111111111111111111111111

View File

@ -1,10 +1,9 @@
HOME_RPC_URL=http://ganache_home:8545
HOME_CHAIN_ID=44
HOME_BRIDGE_ADDRESS=0x44c158FE850821ae69DaF37AADF5c539e9d0025B
HOME_TOKEN_ADDRESS=0xd5fE0D28e058D375b0b038fFbB446Da37E85fFdc
HOME_START_BLOCK=1
SIDE_RPC_URL=http://ganache_side:8545
SIDE_CHAIN_ID=33
SIDE_SHARED_DB_ADDRESS=0xd5fE0D28e058D375b0b038fFbB446Da37E85fFdc
FOREIGN_URL=https://testnet-dex.binance.org/
@ -16,3 +15,5 @@ VALIDATOR_PRIVATE_KEY=afaa4d4d6e54d25b0bf0361e3fd6cef562f6311bf6200de2dd0aa4cab6
VOTES_PROXY_PORT=5003
SIGN_RESTART_PORT=6003
LOG_LEVEL=debug

View File

@ -0,0 +1,19 @@
HOME_RPC_URL=https://kovan.infura.io/v3/5d7bd94c50ed43fab1cb8e74f58678b0
HOME_BRIDGE_ADDRESS=0x6ADCa5e691341fb9de8927d15c0a89B83A4E665e
HOME_TOKEN_ADDRESS=0x57d2533B640cfb58f8f1F69C14c089968Da9fdFc
HOME_START_BLOCK=13276224
SIDE_RPC_URL=https://sokol.poa.network
SIDE_SHARED_DB_ADDRESS=0xda9a1cA2Fcb18cAB02934269369627D2b4ea8902
FOREIGN_URL=https://testnet-dex.binance.org/
FOREIGN_CHAIN_ID=Binance-Chain-Nile
FOREIGN_ASSET=KFT-94F
#VALIDATOR_PRIVATE_KEY is taken from .keys
VOTES_PROXY_PORT=5003
SIGN_RESTART_PORT=6003
LOG_LEVEL=info

View File

@ -0,0 +1 @@
VALIDATOR_PRIVATE_KEY=2222222222222222222222222222222222222222222222222222222222222222

View File

@ -0,0 +1,2 @@
HOME_PRIVATE_KEY=0000000000000000000000000000000000000000000000000000000000000000
SIDE_PRIVATE_KEY=1111111111111111111111111111111111111111111111111111111111111111

View File

@ -1,17 +0,0 @@
RPC_URL=https://sokol.poa.network
RPC_URL_DEV=http://ganache_home:8545
PRIVATE_KEY=e49fe947f224ae8e126c41b1be3e52be701509c2366e835ea8c08651f91030e0
PRIVATE_KEY_DEV=e2aeb24eaa63102d0c0821717c3b6384abdabd7af2ad4ec8e650dce300798b27
TOKEN_ADDRESS=0xd5fE0D28e058D375b0b038fFbB446Da37E85fFdc
VALIDATOR_ADDRESS_1=0x99Eb3D86663c6Db090eFFdBC20510Ca9f836DCE3
VALIDATOR_ADDRESS_2=0xAa006899B0EC407De930bA8A166DEfe59bBfd3DC
VALIDATOR_ADDRESS_3=0x6352e3e6038e05b9da00C84AE851308f9774F883
VALIDATOR_ADDRESS_4=0x4dB6b4bD0a3fdc03B027A60f1c48f05C572312aa
VALIDATOR_ADDRESS_5=0xf7Ca4aED1795E424433498CEf43f6a3825C88731
VALIDATOR_ADDRESS_6=0xAd6c8127143032D843A260c5D379D8d9b3D51F15
THRESHOLD=1
PARTIES=3

View File

@ -0,0 +1,17 @@
HOME_RPC_URL=http://ganache_home:8545
HOME_PRIVATE_KEY=e2aeb24eaa63102d0c0821717c3b6384abdabd7af2ad4ec8e650dce300798b27
HOME_TOKEN_ADDRESS=0xd5fE0D28e058D375b0b038fFbB446Da37E85fFdc
VALIDATOR_ADDRESS_1=0x99eb3d86663c6db090effdbc20510ca9f836dce3
VALIDATOR_ADDRESS_2=0xaa006899b0ec407de930ba8a166defe59bbfd3dc
VALIDATOR_ADDRESS_3=0x6352e3e6038e05b9da00c84ae851308f9774f883
#VALIDATOR_ADDRESS_4=0x4db6b4bd0a3fdc03b027a60f1c48f05c572312aa
THRESHOLD=1
MIN_TX_LIMIT=10000000000000000
MAX_TX_LIMIT=100000000000000000000
BLOCKS_RANGE_SIZE=25

View File

@ -0,0 +1,18 @@
HOME_RPC_URL=https://kovan.infura.io/v3/5d7bd94c50ed43fab1cb8e74f58678b0
#HOME_PRIVATE_KEY is taken from src/deploy/.keys.staging
#Set to '0x' for redeployment of token contract in staging environment
HOME_TOKEN_ADDRESS=0x57d2533B640cfb58f8f1F69C14c089968Da9fdFc
VALIDATOR_ADDRESS_1=0xaaaaB15630f63cA01bb50943AaAb4008CB53748D
VALIDATOR_ADDRESS_2=0xbbbb63D6Fc58bD14dAF9eeF653650c4D10f3dBC8
VALIDATOR_ADDRESS_3=0xcccc27ae510b63E30eC3C68AAD7DdD2578bD62ed
#VALIDATOR_ADDRESS_4=0xdddd9300e32fe162bA420f7313651Fd901C2ed71
THRESHOLD=1
MIN_TX_LIMIT=10000000000000000
MAX_TX_LIMIT=100000000000000000000
BLOCKS_RANGE_SIZE=25

View File

@ -30,4 +30,4 @@ RUN truffle compile
COPY truffle-config.js /build/truffle-config.js
COPY migrations /build/migrations
ENTRYPOINT truffle deploy
ENTRYPOINT ["truffle", "deploy"]

View File

@ -3,6 +3,7 @@ pragma solidity ^0.5.0;
import './openzeppelin-solidity/contracts/token/ERC20/IERC20.sol';
contract Bridge {
event ExchangeRequest(uint value, uint nonce);
event NewEpoch(uint indexed oldEpoch, uint indexed newEpoch);
event NewEpochCancelled(uint indexed epoch);
event NewFundsTransfer(uint indexed oldEpoch, uint indexed newEpoch);
@ -11,38 +12,63 @@ contract Bridge {
struct State {
address[] validators;
uint threshold;
uint rangeSize;
uint startBlock;
uint nonce;
uint x;
uint y;
}
enum Status {
READY, // bridge is in ready to perform operations
VOTING, // voting for changing in next epoch, but still ready
KEYGEN, //keygen, can be cancelled
FUNDS_TRANSFER // funds transfer, cannot be cancelled
}
enum Vote {
CONFIRM_KEYGEN,
CONFIRM_FUNDS_TRANSFER,
START_VOTING,
ADD_VALIDATOR,
REMOVE_VALIDATOR,
CHANGE_THRESHOLD,
CHANGE_RANGE_SIZE,
START_KEYGEN,
CANCEL_KEYGEN,
TRANSFER
}
mapping(uint => State) states;
mapping(bytes32 => uint) public confirmationsCount;
mapping(bytes32 => bool) public confirmations;
mapping(bytes32 => uint) public dbTransferCount;
mapping(bytes32 => bool) public dbTransfer;
mapping(bytes32 => uint) public votesCount;
mapping(bytes32 => bool) public votes;
mapping(bytes32 => bool) public usedRange;
// 0 - ready
// 1 - keygen, can be cancelled
// 2 - funds transfer, cannot be cancelled
uint public status;
Status public status;
uint public epoch;
uint public nextEpoch;
constructor(uint threshold, address[] memory validators, address _tokenContract) public {
uint minTxLimit;
uint maxTxLimit;
constructor(uint threshold, address[] memory validators, address _tokenContract, uint[2] memory limits, uint rangeSize) public {
require(validators.length > 0);
require(threshold < validators.length);
tokenContract = IERC20(_tokenContract);
epoch = 0;
status = 1;
status = Status.KEYGEN;
nextEpoch = 1;
states[1] = State(validators, threshold, 0, 0);
states[nextEpoch] = State(validators, threshold, rangeSize, 0, uint(-1), 0, 0);
minTxLimit = limits[0];
maxTxLimit = limits[1];
emit NewEpoch(0, 1);
}
@ -50,17 +76,27 @@ contract Bridge {
IERC20 public tokenContract;
modifier ready {
require(status == 0, "Not in ready state");
require(status == Status.READY, "Not in ready state");
_;
}
modifier readyOrVoting {
require(status == Status.READY || status == Status.VOTING, "Not in ready or voting state");
_;
}
modifier voting {
require(status == Status.VOTING, "Not in voting state");
_;
}
modifier keygen {
require(status == 1, "Not in keygen state");
require(status == Status.KEYGEN, "Not in keygen state");
_;
}
modifier fundsTransfer {
require(status == 2, "Not in funds transfer state");
require(status == Status.FUNDS_TRANSFER, "Not in funds transfer state");
_;
}
@ -69,32 +105,40 @@ contract Bridge {
_;
}
function transfer(bytes32 hash, address to, uint value) public ready currentValidator {
require(!dbTransfer[keccak256(abi.encodePacked(hash, msg.sender, to, value))], "Already voted");
function exchange(uint value) public ready {
require(value >= minTxLimit && value >= 10 ** 10 && value <= maxTxLimit);
dbTransfer[keccak256(abi.encodePacked(hash, msg.sender, to, value))] = true;
if (++dbTransferCount[keccak256(abi.encodePacked(hash, to, value))] == getThreshold() + 1) {
dbTransferCount[keccak256(abi.encodePacked(hash, to, value))] = 2 ** 255;
uint txRange = (block.number - getStartBlock()) / getRangeSize();
if (!usedRange[keccak256(abi.encodePacked(txRange, epoch))]) {
usedRange[keccak256(abi.encodePacked(txRange, epoch))] = true;
states[epoch].nonce++;
}
tokenContract.transferFrom(msg.sender, address(this), value);
emit ExchangeRequest(value, getNonce());
}
function transfer(bytes32 hash, address to, uint value) public readyOrVoting currentValidator {
if (tryVote(Vote.TRANSFER, hash, to, value)) {
tokenContract.transfer(to, value);
}
}
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;
if (tryConfirm(Vote.CONFIRM_KEYGEN, x, y)) {
states[nextEpoch].x = x;
states[nextEpoch].y = y;
if (nextEpoch == 1) {
status = 0;
status = Status.READY;
states[nextEpoch].startBlock = block.number;
states[nextEpoch].nonce = uint(-1);
epoch = nextEpoch;
emit EpochStart(epoch, x, y);
}
else {
status = 2;
status = Status.FUNDS_TRANSFER;
emit NewFundsTransfer(epoch, nextEpoch);
}
}
@ -102,14 +146,13 @@ contract Bridge {
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;
if (tryConfirm(Vote.CONFIRM_FUNDS_TRANSFER)) {
status = Status.READY;
states[nextEpoch].startBlock = block.number;
states[nextEpoch].nonce = uint(-1);
epoch = nextEpoch;
emit EpochStart(epoch, states[epoch].x, states[epoch].y);
emit EpochStart(epoch, getX(), getY());
}
}
@ -137,6 +180,34 @@ contract Bridge {
return states[_epoch].threshold;
}
function getStartBlock() view public returns (uint) {
return getStartBlock(epoch);
}
function getStartBlock(uint _epoch) view public returns (uint) {
return states[_epoch].startBlock;
}
function getRangeSize() view public returns (uint) {
return getRangeSize(epoch);
}
function getNextRangeSize() view public returns (uint) {
return getRangeSize(nextEpoch);
}
function getRangeSize(uint _epoch) view public returns (uint) {
return states[_epoch].rangeSize;
}
function getNonce() view public returns (uint) {
return getNonce(epoch);
}
function getNonce(uint _epoch) view public returns (uint) {
return states[_epoch].nonce;
}
function getX() view public returns (uint) {
return states[epoch].x;
}
@ -171,22 +242,28 @@ contract Bridge {
return states[nextEpoch].validators;
}
function voteAddValidator(address validator) public ready currentValidator {
require(getNextPartyId(validator) == 0, "Already a validator");
require(!votes[keccak256(abi.encodePacked(uint(1), nextEpoch, msg.sender, validator))], "Already voted");
function startVoting() public readyOrVoting currentValidator {
if (tryVote(Vote.START_VOTING)) {
nextEpoch++;
status = Status.VOTING;
states[nextEpoch].threshold = getThreshold();
states[nextEpoch].validators = getValidators();
states[nextEpoch].rangeSize = getRangeSize();
}
}
votes[keccak256(abi.encodePacked(uint(1), nextEpoch, msg.sender, validator))] = true;
if (++votesCount[keccak256(abi.encodePacked(uint(1), nextEpoch, validator))] == getThreshold() + 1) {
function voteAddValidator(address validator) public voting currentValidator {
require(getNextPartyId(validator) == 0, "Already a validator");
if (tryVote(Vote.ADD_VALIDATOR, validator)) {
states[nextEpoch].validators.push(validator);
}
}
function voteRemoveValidator(address validator) public ready currentValidator {
function voteRemoveValidator(address validator) public voting currentValidator {
require(getNextPartyId(validator) != 0, "Already not a validator");
require(!votes[keccak256(abi.encodePacked(uint(2), nextEpoch, msg.sender, validator))], "Already voted");
votes[keccak256(abi.encodePacked(uint(2), nextEpoch, msg.sender, validator))] = true;
if (++votesCount[keccak256(abi.encodePacked(uint(2), nextEpoch, validator))] == getThreshold() + 1) {
if (tryVote(Vote.REMOVE_VALIDATOR, validator)) {
_removeValidator(validator);
}
}
@ -194,7 +271,7 @@ contract Bridge {
function _removeValidator(address validator) private {
for (uint i = 0; i < getNextParties() - 1; i++) {
if (states[nextEpoch].validators[i] == validator) {
states[nextEpoch].validators[i] = states[nextEpoch].validators[getNextParties() - 1];
states[nextEpoch].validators[i] = getNextValidators()[getNextParties() - 1];
break;
}
}
@ -202,36 +279,93 @@ contract Bridge {
states[nextEpoch].validators.length--;
}
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), nextEpoch, msg.sender, threshold))] = true;
if (++votesCount[keccak256(abi.encodePacked(uint(3), nextEpoch, threshold))] == getThreshold() + 1) {
function voteChangeThreshold(uint threshold) public voting currentValidator {
if (tryVote(Vote.CHANGE_THRESHOLD, threshold)) {
states[nextEpoch].threshold = threshold;
}
}
function voteStartKeygen() public ready currentValidator {
require(!votes[keccak256(abi.encodePacked(uint(4), nextEpoch + 1, msg.sender))], "Voted already");
function voteChangeRangeSize(uint rangeSize) public voting currentValidator {
if (tryVote(Vote.CHANGE_RANGE_SIZE, rangeSize)) {
states[nextEpoch].rangeSize = rangeSize;
}
}
votes[keccak256(abi.encodePacked(uint(4), nextEpoch + 1, msg.sender))] = true;
if (++votesCount[keccak256(abi.encodePacked(uint(4), nextEpoch + 1))] == getThreshold() + 1) {
status = 1;
function voteStartKeygen() public voting currentValidator {
if (tryVote(Vote.START_KEYGEN)) {
status = Status.KEYGEN;
nextEpoch++;
states[nextEpoch].validators = getValidators();
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;
if (tryVote(Vote.CANCEL_KEYGEN)) {
status = Status.VOTING;
emit NewEpochCancelled(nextEpoch);
}
}
function tryVote(Vote voteType) private returns (bool) {
bytes32 vote = keccak256(abi.encodePacked(voteType, nextEpoch));
return putVote(vote);
}
function tryVote(Vote voteType, address addr) private returns (bool) {
bytes32 vote = keccak256(abi.encodePacked(voteType, nextEpoch, addr));
return putVote(vote);
}
function tryVote(Vote voteType, uint num) private returns (bool) {
bytes32 vote = keccak256(abi.encodePacked(voteType, nextEpoch, num));
return putVote(vote);
}
function tryVote(Vote voteType, bytes32 hash, address to, uint value) private returns (bool) {
bytes32 vote = keccak256(abi.encodePacked(voteType, hash, to, value));
return putVote(vote);
}
function tryConfirm(Vote voteType) private returns (bool) {
bytes32 vote = keccak256(abi.encodePacked(voteType, nextEpoch));
return putConfirm(vote);
}
function tryConfirm(Vote voteType, uint x, uint y) private returns (bool) {
bytes32 vote = keccak256(abi.encodePacked(voteType, nextEpoch, x, y));
return putConfirm(vote);
}
function putVote(bytes32 vote) private returns (bool) {
bytes32 personalVote = personalizeVote(vote);
require(!votes[personalVote], "Voted already");
votes[personalVote] = true;
if (votesCount[vote] == getThreshold()) {
votesCount[vote] = 2 ** 255;
return true;
} else {
votesCount[vote]++;
return false;
}
}
function putConfirm(bytes32 vote) private returns (bool) {
bytes32 personalVote = personalizeVote(vote);
require(!votes[personalVote], "Confirmed already");
votes[personalVote] = true;
if (votesCount[vote] == getNextThreshold()) {
votesCount[vote] = 2 ** 255;
return true;
} else {
votesCount[vote]++;
return false;
}
}
function personalizeVote(bytes32 vote) private view returns (bytes32) {
return keccak256(abi.encodePacked(vote, msg.sender));
}
}

View File

@ -1,23 +1,20 @@
const Bridge = artifacts.require('Bridge')
const addresses = Object.entries(process.env)
.filter(([ key ]) => key.startsWith('VALIDATOR_ADDRESS'))
.map(([ , value ]) => value)
const {
THRESHOLD, PARTIES, VALIDATOR_ADDRESS_1, VALIDATOR_ADDRESS_2, VALIDATOR_ADDRESS_3, VALIDATOR_ADDRESS_4,
VALIDATOR_ADDRESS_5, VALIDATOR_ADDRESS_6, TOKEN_ADDRESS
THRESHOLD, HOME_TOKEN_ADDRESS, MIN_TX_LIMIT, MAX_TX_LIMIT, BLOCKS_RANGE_SIZE
} = process.env
module.exports = deployer => {
deployer.deploy(
Bridge,
THRESHOLD,
//PARTIES,
[
VALIDATOR_ADDRESS_1,
VALIDATOR_ADDRESS_2,
VALIDATOR_ADDRESS_3,
//VALIDATOR_ADDRESS_4,
//VALIDATOR_ADDRESS_5,
//VALIDATOR_ADDRESS_6
],
TOKEN_ADDRESS
addresses,
HOME_TOKEN_ADDRESS,
[ MIN_TX_LIMIT, MAX_TX_LIMIT ],
BLOCKS_RANGE_SIZE
)
}

View File

@ -1,16 +1,12 @@
const PrivateKeyProvider = require('truffle-hdwallet-provider')
const { RPC_URL, PRIVATE_KEY, RPC_URL_DEV, PRIVATE_KEY_DEV } = process.env
const { HOME_RPC_URL, HOME_PRIVATE_KEY } = process.env
module.exports = {
networks: {
development: {
provider: new PrivateKeyProvider(PRIVATE_KEY_DEV, RPC_URL_DEV),
network_id: '44'
},
staging: {
provider: new PrivateKeyProvider(PRIVATE_KEY, RPC_URL),
network_id: '77'
home: {
provider: new PrivateKeyProvider(HOME_PRIVATE_KEY, HOME_RPC_URL),
network_id: '*'
}
},
compilers: {

View File

@ -1,5 +0,0 @@
RPC_URL=https://sokol.poa.network
RPC_URL_DEV=http://ganache_side:8545
PRIVATE_KEY=e49fe947f224ae8e126c41b1be3e52be701509c2366e835ea8c08651f91030e0
PRIVATE_KEY_DEV=e2aeb24eaa63102d0c0821717c3b6384abdabd7af2ad4ec8e650dce300798b27

View File

@ -0,0 +1,3 @@
SIDE_RPC_URL=http://ganache_side:8545
SIDE_PRIVATE_KEY=e2aeb24eaa63102d0c0821717c3b6384abdabd7af2ad4ec8e650dce300798b27

View File

@ -0,0 +1,3 @@
SIDE_RPC_URL=https://sokol.poa.network
#SIDE_PRIVATE_KEY is taken from src/deploy/.keys.staging

View File

@ -26,4 +26,4 @@ RUN truffle compile
COPY truffle-config.js /build/truffle-config.js
COPY migrations /build/migrations
ENTRYPOINT truffle deploy
ENTRYPOINT ["truffle", "deploy"]

View File

@ -1,16 +1,12 @@
const PrivateKeyProvider = require('truffle-hdwallet-provider')
const { RPC_URL, PRIVATE_KEY, RPC_URL_DEV, PRIVATE_KEY_DEV } = process.env
const { SIDE_RPC_URL, SIDE_PRIVATE_KEY } = process.env
module.exports = {
networks: {
development: {
provider: new PrivateKeyProvider(PRIVATE_KEY_DEV, RPC_URL_DEV),
network_id: '33'
},
staging: {
provider: new PrivateKeyProvider(PRIVATE_KEY, RPC_URL),
network_id: '77'
side: {
provider: new PrivateKeyProvider(SIDE_PRIVATE_KEY, SIDE_RPC_URL),
network_id: '*'
}
},
compilers: {

View File

@ -1,7 +0,0 @@
RPC_URL=https://sokol.poa.network
RPC_URL_DEV=http://ganache_home:8545
PRIVATE_KEY=e49fe947f224ae8e126c41b1be3e52be701509c2366e835ea8c08651f91030e0
PRIVATE_KEY_DEV=e2aeb24eaa63102d0c0821717c3b6384abdabd7af2ad4ec8e650dce300798b27
TOKEN_INITIAL_MINT=31415000000000000000000000

View File

@ -0,0 +1,5 @@
HOME_RPC_URL=http://ganache_home:8545
HOME_PRIVATE_KEY=e2aeb24eaa63102d0c0821717c3b6384abdabd7af2ad4ec8e650dce300798b27
TOKEN_INITIAL_MINT=31415000000000000000000000

View File

@ -0,0 +1,5 @@
HOME_RPC_URL=https://kovan.infura.io/v3/5d7bd94c50ed43fab1cb8e74f58678b0
#HOME_PRIVATE_KEY is taken from src/deploy/.keys.staging
TOKEN_INITIAL_MINT=31415000000000000000000000

View File

@ -30,4 +30,4 @@ RUN truffle compile
COPY truffle-config.js /build/truffle-config.js
COPY migrations /build/migrations
ENTRYPOINT truffle deploy
ENTRYPOINT ["truffle", "deploy"]

View File

@ -1,16 +1,12 @@
const PrivateKeyProvider = require('truffle-hdwallet-provider')
const { RPC_URL, PRIVATE_KEY, RPC_URL_DEV, PRIVATE_KEY_DEV } = process.env
const { HOME_RPC_URL, HOME_PRIVATE_KEY } = process.env
module.exports = {
networks: {
development: {
provider: new PrivateKeyProvider(PRIVATE_KEY_DEV, RPC_URL_DEV),
network_id: '44'
},
staging: {
provider: new PrivateKeyProvider(PRIVATE_KEY, RPC_URL),
network_id: '77'
home: {
provider: new PrivateKeyProvider(HOME_PRIVATE_KEY, HOME_RPC_URL),
network_id: '*'
}
},
compilers: {

View File

@ -1,17 +0,0 @@
HOME_RPC_URL=http://ganache_home:8545
HOME_CHAIN_ID=44
HOME_BRIDGE_ADDRESS=0x44c158FE850821ae69DaF37AADF5c539e9d0025B
HOME_TOKEN_ADDRESS=0xd5fE0D28e058D375b0b038fFbB446Da37E85fFdc
SIDE_RPC_URL=http://ganache_side:8545
SIDE_CHAIN_ID=33
SIDE_SHARED_DB_ADDRESS=0xd5fE0D28e058D375b0b038fFbB446Da37E85fFdc
FOREIGN_URL=https://testnet-dex.binance.org/
FOREIGN_CHAIN_ID=Binance-Chain-Nile
FOREIGN_ASSET=KFT-94F
VALIDATOR_PRIVATE_KEY=2be3f252e16541bf1bb2d4a517d2bf173e6d09f2d765d32c64dc50515aec63ea
VOTES_PROXY_PORT=5000
SIGN_RESTART_PORT=6000

View File

@ -5,10 +5,10 @@ WORKDIR /watcher
RUN apk update && \
apk add libssl1.1 libressl-dev curl
COPY package.json /watcher/
COPY ./bncWatcher/package.json /watcher/
RUN npm install
COPY bncWatcher.js db.js /watcher/
COPY ./bncWatcher/bncWatcher.js ./shared/db.js ./shared/logger.js ./shared/crypto.js /watcher/
ENTRYPOINT ["node", "bncWatcher.js"]

View File

@ -1,11 +1,12 @@
const redis = require('./db')
const axios = require('axios')
const bech32 = require('bech32')
const BN = require('bignumber.js')
const fs = require('fs')
const crypto = require('crypto')
const { computeAddress } = require('ethers').utils
const logger = require('./logger')
const redis = require('./db')
const { publicKeyToAddress } = require('./crypto')
const { FOREIGN_URL, PROXY_URL, FOREIGN_ASSET } = process.env
const foreignHttpClient = axios.create({ baseURL: FOREIGN_URL })
@ -13,8 +14,8 @@ const proxyHttpClient = axios.create({ baseURL: PROXY_URL })
async function initialize () {
if (await redis.get('foreignTime') === null) {
console.log('Set default foreign time')
await redis.set('foreignTime', 1562306990672)
logger.info('Set default foreign time')
await redis.set('foreignTime', Date.now() - 2 * 30 * 24 * 60 * 60 * 1000)
}
}
@ -26,7 +27,10 @@ async function main () {
return
}
console.log(`Found ${newTransactions.length} new transactions`)
if (newTransactions.length)
logger.info(`Found ${newTransactions.length} new transactions`)
else
logger.debug(`Found 0 new transactions`)
for (const tx of newTransactions.reverse()) {
if (tx.memo !== 'funding') {
@ -54,12 +58,12 @@ function getTx(hash) {
}
async function fetchNewTransactions () {
console.log('Fetching new transactions')
logger.debug('Fetching new transactions')
const startTime = parseInt(await redis.get('foreignTime')) + 1
const address = await getLastForeignAddress()
const address = getLastForeignAddress()
if (address === null)
return null
console.log('Sending api transactions request')
logger.debug('Sending api transactions request')
return foreignHttpClient
.get('/api/v1/transactions', {
params: {
@ -84,20 +88,6 @@ function getLastForeignAddress () {
return publicKeyToAddress(publicKey)
}
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
}
initialize().then(async () => {
while (true) {
await main()

View File

@ -1,21 +0,0 @@
const Redis = require('ioredis')
console.log('Connecting to redis')
const redis = new Redis({
port: 6379,
host: 'redis',
family: 4,
password: 'password',
db: 0
})
redis.on('connect', () => {
console.log('Connected to redis')
})
redis.on('error', () => {
console.log('Redis error')
})
module.exports = redis

View File

@ -6,7 +6,9 @@
"axios": "0.19.0",
"bech32": "1.1.3",
"bignumber.js": "9.0.0",
"ethers": "4.0.33"
"ethers": "4.0.33",
"pino": "5.13.4",
"pino-pretty": "3.2.1"
}
}

View File

@ -0,0 +1 @@
loglevel warning

View File

@ -2,7 +2,9 @@ version: '3.7'
services:
proxy:
image: blockchain-proxy
build: ./proxy
build:
dockerfile: proxy/Dockerfile
context: .
environment:
- HOME_RPC_URL
- HOME_BRIDGE_ADDRESS
@ -14,6 +16,9 @@ services:
- VALIDATOR_PRIVATE_KEY
- FOREIGN_URL
- FOREIGN_ASSET
- LOG_LEVEL
- "GAS_LIMIT_FACTOR=3"
- "MAX_GAS_LIMIT=6000000"
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'
@ -26,25 +31,31 @@ services:
- blockchain_side
keygen:
image: keygen-client
build: ./tss-keygen
build:
dockerfile: tss-keygen/Dockerfile
context: .
environment:
- 'RABBITMQ_URL=amqp://rabbitmq:5672'
- 'PROXY_URL=http://proxy:8001'
- LOG_LEVEL
volumes:
- '${PWD}/keys:/keys'
- '${PWD}/${TARGET_NETWORK}/keys:/keys'
networks:
- test_network
signer:
image: sign-client
build: ./tss-sign
build:
dockerfile: tss-sign/Dockerfile
context: .
environment:
- 'RABBITMQ_URL=amqp://rabbitmq:5672'
- 'PROXY_URL=http://proxy:8001'
- FOREIGN_CHAIN_ID
- FOREIGN_URL
- FOREIGN_ASSET
- LOG_LEVEL
volumes:
- '${PWD}/keys:/keys'
- '${PWD}/${TARGET_NETWORK}/keys:/keys'
ports:
- '${SIGN_RESTART_PORT}:8001'
networks:
@ -52,41 +63,53 @@ services:
redis:
image: redis:5.0.5-alpine
volumes:
- '${PWD}/db:/data'
- '${PWD}/${TARGET_NETWORK}/db:/data'
- './configs/redis.conf:/usr/local/etc/redis/redis.conf'
command: ["redis-server", "/usr/local/etc/redis/redis.conf"]
networks:
- test_network
rabbitmq:
hostname: rabbit
image: rabbitmq:3.7.15-alpine
environment:
RABBITMQ_LOGS: 'false'
volumes:
- '${PWD}/queue:/var/lib/rabbitmq/mnesia'
- '${PWD}/${TARGET_NETWORK}/queue:/var/lib/rabbitmq/mnesia'
networks:
- test_network
eth-watcher:
build: ethWatcher
build:
dockerfile: ethWatcher/Dockerfile
context: .
image: eth-watcher
environment:
- HOME_RPC_URL
- HOME_BRIDGE_ADDRESS
- HOME_TOKEN_ADDRESS
- HOME_CHAIN_ID
- HOME_START_BLOCK
- BLOCKS_RANGE_SIZE
- VALIDATOR_PRIVATE_KEY
- 'RABBITMQ_URL=amqp://rabbitmq:5672'
- LOG_LEVEL
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
- blockchain_home
bnc-watcher:
build: bncWatcher
build:
dockerfile: bncWatcher/Dockerfile
context: .
image: bnc-watcher
environment:
- FOREIGN_URL
- FOREIGN_ASSET
- 'RABBITMQ_URL=amqp://rabbitmq:5672'
- 'PROXY_URL=http://proxy:8001'
- LOG_LEVEL
volumes:
- '${PWD}/keys:/keys'
- '${PWD}/${TARGET_NETWORK}/keys:/keys'
networks:
- test_network
networks:

View File

@ -2,7 +2,9 @@ version: '3.7'
services:
proxy:
image: blockchain-proxy
build: ./proxy
build:
dockerfile: proxy/Dockerfile
context: .
environment:
- HOME_RPC_URL
- HOME_BRIDGE_ADDRESS
@ -14,6 +16,9 @@ services:
- VALIDATOR_PRIVATE_KEY
- FOREIGN_URL
- FOREIGN_ASSET
- LOG_LEVEL
- "GAS_LIMIT_FACTOR=3"
- "MAX_GAS_LIMIT=6000000"
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'
@ -26,27 +31,33 @@ services:
- bncwatcher-proxy-net
keygen:
image: keygen-client
build: ./tss-keygen
build:
dockerfile: tss-keygen/Dockerfile
context: .
environment:
- 'RABBITMQ_URL=amqp://rabbitmq:5672'
- 'PROXY_URL=http://proxy:8001'
- LOG_LEVEL
volumes:
- '${PWD}/keys:/keys'
- '${PWD}/${TARGET_NETWORK}/keys:/keys'
networks:
- keygen-proxy-net
- rabbit-keygen-net
- redis-keygen-net
signer:
image: sign-client
build: ./tss-sign
build:
dockerfile: tss-sign/Dockerfile
context: .
environment:
- 'RABBITMQ_URL=amqp://rabbitmq:5672'
- 'PROXY_URL=http://proxy:8001'
- FOREIGN_CHAIN_ID
- FOREIGN_URL
- FOREIGN_ASSET
- LOG_LEVEL
volumes:
- '${PWD}/keys:/keys'
- '${PWD}/${TARGET_NETWORK}/keys:/keys'
ports:
- '${SIGN_RESTART_PORT}:8001'
networks:
@ -56,7 +67,9 @@ services:
redis:
image: redis:5.0.5-alpine
volumes:
- '${PWD}/db:/data'
- '${PWD}/${TARGET_NETWORK}/db:/data'
- './configs/redis.conf:/usr/local/etc/redis/redis.conf'
command: ["redis-server", "/usr/local/etc/redis/redis.conf"]
networks:
- redis-signer-net
- redis-keygen-net
@ -65,38 +78,47 @@ services:
rabbitmq:
hostname: rabbit
image: rabbitmq:3.7.15-alpine
environment:
RABBITMQ_LOGS: 'false'
volumes:
- '${PWD}/queue:/var/lib/rabbitmq/mnesia'
- '${PWD}/${TARGET_NETWORK}/queue:/var/lib/rabbitmq/mnesia'
networks:
- rabbit-signer-net
- rabbit-keygen-net
- rabbit-ethwatcher-net
- rabbit-bncwatcher-net
eth-watcher:
build: ethWatcher
build:
dockerfile: ethWatcher/Dockerfile
context: .
image: eth-watcher
environment:
- HOME_RPC_URL
- HOME_BRIDGE_ADDRESS
- HOME_TOKEN_ADDRESS
- HOME_CHAIN_ID
- HOME_START_BLOCK
- VALIDATOR_PRIVATE_KEY
- 'RABBITMQ_URL=amqp://rabbitmq:5672'
- LOG_LEVEL
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:
- rabbit-ethwatcher-net
- redis-ethwatcher-net
bnc-watcher:
build: bncWatcher
build:
dockerfile: bncWatcher
context: .
image: bnc-watcher
environment:
- FOREIGN_URL
- FOREIGN_ASSET
- 'RABBITMQ_URL=amqp://rabbitmq:5672'
- 'PROXY_URL=http://proxy:8001'
- LOG_LEVEL
volumes:
- '${PWD}/keys:/keys'
- '${PWD}/${TARGET_NETWORK}/keys:/keys'
networks:
- rabbit-bncwatcher-net
- redis-bncwatcher-net

View File

@ -5,10 +5,10 @@ WORKDIR /watcher
RUN apk update && \
apk add libssl1.1 libressl-dev curl
COPY package.json /watcher/
COPY ./ethWatcher/package.json /watcher/
RUN npm install
COPY ethWatcher.js db.js /watcher/
COPY ./ethWatcher/ethWatcher.js ./shared/db.js ./shared/logger.js ./shared/amqp.js ./shared/crypto.js /watcher/
ENTRYPOINT ["node", "ethWatcher.js"]

View File

@ -1,72 +1,119 @@
const amqp = require('amqplib')
const Web3 = require('web3')
const redis = require('./db')
const crypto = require('crypto')
const utils = require('ethers').utils
const BN = require('bignumber.js')
const bech32 = require('bech32')
const axios = require('axios')
const logger = require('./logger')
const redis = require('./db')
const { connectRabbit, assertQueue } = require('./amqp')
const { publicKeyToAddress } = require('./crypto')
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 { HOME_RPC_URL, HOME_BRIDGE_ADDRESS, RABBITMQ_URL, HOME_START_BLOCK, VALIDATOR_PRIVATE_KEY } = process.env
const web3Home = new Web3(HOME_RPC_URL)
const bridge = new web3Home.eth.Contract(abiBridge, HOME_BRIDGE_ADDRESS)
const token = new web3Home.eth.Contract(abiToken, HOME_TOKEN_ADDRESS)
const homeWeb3 = new Web3(HOME_RPC_URL)
const bridge = new homeWeb3.eth.Contract(abiBridge, HOME_BRIDGE_ADDRESS)
const validatorAddress = homeWeb3.eth.accounts.privateKeyToAccount(`0x${VALIDATOR_PRIVATE_KEY}`).address
let channel
let exchangeQueue
let signQueue
let keygenQueue
let cancelKeygenQueue
let blockNumber
let foreignNonce = []
let epoch
let epochStart
let redisTx
let rangeSize
let lastTransactionBlockNumber
let isCurrentValidator
async function connectRabbit (url) {
return amqp.connect(url).catch(() => {
console.log('Failed to connect, reconnecting')
return new Promise(resolve =>
setTimeout(() => resolve(connectRabbit(url)), 1000)
)
})
async function resetFutureMessages (queue) {
logger.debug(`Resetting future messages in queue ${queue.name}`)
const { messageCount } = await channel.checkQueue(queue.name)
if (messageCount) {
logger.info(`Filtering ${messageCount} reloaded messages from queue ${queue.name}`)
const backup = await assertQueue(channel, `${queue.name}.backup`)
do {
const message = await queue.get()
if (message === false)
break
const data = JSON.parse(message.content)
if (data.blockNumber < blockNumber) {
logger.debug('Saving message %o', data)
backup.send(data)
} else {
logger.debug('Dropping message %o', data)
}
channel.ack(message)
} while (true)
logger.debug('Dropped messages came from future')
do {
const message = await backup.get()
if (message === false)
break
const data = JSON.parse(message.content)
logger.debug('Requeuing message %o', data)
queue.send(data)
channel.ack(message)
} while (true)
logger.debug('Redirected messages back to initial queue')
}
}
async function initialize () {
const connection = await connectRabbit(RABBITMQ_URL)
channel = await connection.createChannel()
signQueue = await channel.assertQueue('signQueue')
keygenQueue = await channel.assertQueue('keygenQueue')
cancelKeygenQueue = await channel.assertQueue('cancelKeygenQueue')
channel = await connectRabbit(RABBITMQ_URL)
exchangeQueue = await assertQueue(channel, 'exchangeQueue')
signQueue = await assertQueue(channel, 'signQueue')
keygenQueue = await assertQueue(channel, 'keygenQueue')
cancelKeygenQueue = await assertQueue(channel, 'cancelKeygenQueue')
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 epochStart = events.length ? events[events.length - 1].blockNumber : 1
const saved = (parseInt(await redis.get('homeBlock')) + 1) || 1
console.log(epochStart, saved)
logger.info(`Current epoch ${epoch}`)
epochStart = events.length ? events[events.length - 1].blockNumber : 1
const saved = (parseInt(await redis.get('homeBlock')) + 1) || parseInt(HOME_START_BLOCK)
if (epochStart > saved) {
console.log(`Data in db is outdated, starting from epoch ${epoch}, block #${epochStart}`)
logger.info(`Data in db is outdated, starting from epoch ${epoch}, block #${epochStart}`)
blockNumber = epochStart
rangeSize = (await bridge.methods.getRangeSize().call()).toNumber()
await redis.multi()
.set('homeBlock', blockNumber - 1)
.set(`foreignNonce${epoch}`, 0)
.exec()
foreignNonce[epoch] = 0
} else {
console.log('Restoring epoch and block number from local db')
logger.info('Restoring epoch and block number from local db')
blockNumber = saved
foreignNonce[epoch] = parseInt(await redis.get(`foreignNonce${epoch}`)) || 0
}
isCurrentValidator = (await bridge.methods.getValidators().call()).includes(validatorAddress)
if (isCurrentValidator) {
logger.info(`${validatorAddress} is a current validator`)
} else {
logger.info(`${validatorAddress} is not a current validator`)
}
await resetFutureMessages(keygenQueue)
await resetFutureMessages(cancelKeygenQueue)
await resetFutureMessages(exchangeQueue)
await resetFutureMessages(signQueue)
logger.debug(`Sending start commands`)
await axios.get('http://keygen:8001/start')
await axios.get('http://signer:8001/start')
}
async function main () {
console.log(`Watching events in block #${blockNumber}`)
if (await web3Home.eth.getBlock(blockNumber) === null) {
console.log('No block')
logger.debug(`Watching events in block #${blockNumber}`)
if (await homeWeb3.eth.getBlock(blockNumber) === null) {
logger.debug('No block')
await new Promise(r => setTimeout(r, 1000))
return
}
@ -84,34 +131,33 @@ async function main () {
await sendKeygen(event)
break
case 'NewEpochCancelled':
sendKeygenCancelation(event)
sendKeygenCancellation(event)
break
case 'NewFundsTransfer':
await sendSignFundsTransfer(event)
isCurrentValidator && await sendSignFundsTransfer(event)
break
case 'ExchangeRequest':
isCurrentValidator && await sendSign(event)
break
case 'EpochStart':
epoch = event.returnValues.epoch.toNumber()
console.log(`Epoch ${epoch} started`)
foreignNonce[epoch] = 0
await processEpochStart(event)
break
}
}
const transferEvents = await token.getPastEvents('Transfer', {
filter: {
to: HOME_BRIDGE_ADDRESS
},
fromBlock: blockNumber,
toBlock: blockNumber
})
if ((blockNumber + 1 - epochStart) % rangeSize === 0) {
logger.info('Reached end of the current block range')
for (const event of transferEvents) {
await sendSign(event)
if (lastTransactionBlockNumber > blockNumber - rangeSize) {
logger.info('Sending message to start signature generation for the ended range')
await sendStartSign()
}
}
blockNumber++
// Exec redis tx
await redisTx.incr('homeBlock').exec()
await redis.save()
}
initialize().then(async () => {
@ -122,45 +168,42 @@ initialize().then(async () => {
async function sendKeygen (event) {
const newEpoch = event.returnValues.newEpoch.toNumber()
channel.sendToQueue(keygenQueue.queue, Buffer.from(JSON.stringify({
keygenQueue.send({
epoch: newEpoch,
blockNumber,
threshold: (await bridge.methods.getThreshold(newEpoch).call()).toNumber(),
parties: (await bridge.methods.getParties(newEpoch).call()).toNumber()
})), {
persistent: true
})
console.log('Sent keygen start event')
logger.debug('Sent keygen start event')
}
function sendKeygenCancelation (event) {
function sendKeygenCancellation (event) {
const epoch = event.returnValues.epoch.toNumber()
channel.sendToQueue(cancelKeygenQueue.queue, Buffer.from(JSON.stringify({
epoch
})), {
persistent: true
cancelKeygenQueue.send({
epoch,
blockNumber
})
console.log('Sent keygen cancellation event')
logger.debug('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({
signQueue.send({
epoch: oldEpoch,
blockNumber,
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')
logger.debug('Sent sign funds transfer event')
foreignNonce[oldEpoch]++
redisTx.incr(`foreignNonce${oldEpoch}`)
}
async function sendSign (event) {
const tx = await web3Home.eth.getTransaction(event.transactionHash)
const tx = await homeWeb3.eth.getTransaction(event.transactionHash)
const msg = utils.serializeTransaction({
nonce: tx.nonce,
gasPrice: `0x${new BN(tx.gasPrice).toString(16)}`,
@ -168,42 +211,51 @@ async function sendSign (event) {
to: tx.to,
value: `0x${new BN(tx.value).toString(16)}`,
data: tx.input,
chainId: parseInt(HOME_CHAIN_ID)
chainId: await homeWeb3.eth.net.getId()
})
const hash = web3Home.utils.sha3(msg)
const hash = homeWeb3.utils.sha3(msg)
const publicKey = utils.recoverPublicKey(hash, { r: tx.r, s: tx.s, v: tx.v })
const msgToQueue = JSON.stringify({
const msgToQueue = {
epoch,
blockNumber,
recipient: publicKeyToAddress({
x: publicKey.substr(4, 64),
y: publicKey.substr(68, 64)
}),
value: (new BN(event.returnValues.value)).dividedBy(10 ** 18).toFixed(8, 3),
nonce: event.returnValues.nonce.toNumber()
}
exchangeQueue.send(msgToQueue)
logger.debug('Sent new sign event: %o', msgToQueue)
lastTransactionBlockNumber = blockNumber
redisTx.set('lastTransactionBlockNumber', blockNumber)
logger.debug(`Set lastTransactionBlockNumber to ${blockNumber}`)
}
async function sendStartSign () {
redisTx.incr(`foreignNonce${epoch}`)
signQueue.send({
epoch,
nonce: foreignNonce[epoch],
blockNumber,
nonce: foreignNonce[epoch]++,
threshold: (await bridge.methods.getThreshold(epoch).call()).toNumber(),
parties: (await bridge.methods.getParties(epoch).call()).toNumber()
})
channel.sendToQueue(signQueue.queue, Buffer.from(msgToQueue), {
persistent: true
})
console.log('Sent new sign event')
console.log(msgToQueue)
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
async function processEpochStart (event) {
epoch = event.returnValues.epoch.toNumber()
epochStart = blockNumber
logger.info(`Epoch ${epoch} started`)
rangeSize = (await bridge.methods.getRangeSize().call()).toNumber()
isCurrentValidator = (await bridge.methods.getValidators().call()).includes(validatorAddress)
if (isCurrentValidator) {
logger.info(`${validatorAddress} is a current validator`)
} else {
logger.info(`${validatorAddress} is not a current validator`)
}
logger.info(`Updated range size to ${rangeSize}`)
foreignNonce[epoch] = 0
}

View File

@ -7,6 +7,9 @@
"web3": "1.0.0-beta.55",
"ethers": "4.0.33",
"bignumber.js": "9.0.0",
"bech32": "1.1.3"
"bech32": "1.1.3",
"pino": "5.13.4",
"pino-pretty": "3.2.1",
"axios": "0.19.0"
}
}

View File

@ -2,10 +2,10 @@ FROM node:10.16.0-alpine
WORKDIR /proxy
COPY package.json /proxy/
COPY ./proxy/package.json /proxy/
RUN npm install
COPY index.js encode.js decode.js /proxy/
COPY ./proxy/index.js ./proxy/encode.js ./proxy/decode.js ./proxy/sendTx.js ./shared/logger.js ./shared/crypto.js /proxy/
ENTRYPOINT ["node", "index.js"]

View File

@ -224,7 +224,5 @@ module.exports = function (isKeygen, round, value) {
const tokenizer = Tokenizer(value)
const roundNumber = parseInt(round[round.length - 1])
const decoder = (isKeygen ? keygenDecoders : signDecoders)[roundNumber]
const decoded = JSON.stringify(decoder(tokenizer))
console.log(decoded)
return decoded
return JSON.stringify(decoder(tokenizer))
}

View File

@ -1,10 +1,6 @@
const BN = require('bignumber.js')
function padZeros (s, len) {
while (s.length < len)
s = '0' + s
return s
}
const { padZeros } = require('./crypto')
function makeBuffer (value, length = 32, base = 16) {
return Buffer.from(padZeros(new BN(value, base).toString(16), length * 2), 'hex')
@ -137,11 +133,5 @@ module.exports = function (isKeygen, round, value) {
break
buffers.push(next.value)
}
const encoded = Buffer.concat(buffers)
console.log(`Raw data: ${value.length} bytes, encoded data: ${encoded.length} bytes`)
return encoded
return Buffer.concat(buffers)
}
module.exports(true, 'round2', '{"blind_factor":"11223344556677889900", "y_i":{"x":"00112233445566778899", "y":"00112233445566778899"}}')

View File

@ -1,13 +1,15 @@
const express = require('express')
const Web3 = require('web3')
const AsyncLock = require('async-lock')
const crypto = require('crypto')
const bech32 = require('bech32')
const axios = require('axios')
const BN = require('bignumber.js')
const { utils } = require('ethers')
const encode = require('./encode')
const decode = require('./decode')
const { createSender, waitForReceipt } = require('./sendTx')
const logger = require('./logger')
const { publicKeyToAddress } = require('./crypto')
const {
HOME_RPC_URL, HOME_BRIDGE_ADDRESS, SIDE_RPC_URL, SIDE_SHARED_DB_ADDRESS, VALIDATOR_PRIVATE_KEY, HOME_CHAIN_ID,
@ -30,6 +32,8 @@ const lock = new AsyncLock()
let homeValidatorNonce
let sideValidatorNonce
let homeSender
let sideSender
const app = express()
app.use(express.json())
@ -48,22 +52,29 @@ const votesProxyApp = express()
votesProxyApp.use(express.json())
votesProxyApp.use(express.urlencoded({ extended: true }))
votesProxyApp.get('/vote/startVoting', voteStartVoting)
votesProxyApp.get('/vote/startKeygen', voteStartKeygen)
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('/info', info)
async function main () {
homeValidatorNonce = await homeWeb3.eth.getTransactionCount(validatorAddress)
sideValidatorNonce = await sideWeb3.eth.getTransactionCount(validatorAddress)
homeSender = await createSender(HOME_RPC_URL, VALIDATOR_PRIVATE_KEY)
sideSender = await createSender(SIDE_RPC_URL, VALIDATOR_PRIVATE_KEY)
logger.warn(`My validator address in home and side networks is ${validatorAddress}`)
app.listen(8001, () => {
console.log('Proxy is listening on port 8001')
logger.debug('Proxy is listening on port 8001')
})
votesProxyApp.listen(8002, () => {
console.log('Votes proxy is listening on port 8001')
logger.debug('Votes proxy is listening on port 8002')
})
}
@ -78,8 +89,7 @@ function Err (data) {
}
async function get (req, res) {
console.log('Get call')
console.log(req.body.key)
logger.debug('Get call, %o', req.body.key)
const round = req.body.key.second
const uuid = req.body.key.third
let from
@ -95,59 +105,60 @@ async function get (req, res) {
const data = await sharedDb.methods.getData(from, sideWeb3.utils.sha3(uuid), key).call()
if (data.length > 2) {
console.log(data)
logger.trace(`Received encoded data: ${data}`)
const decoded = decode(uuid[0] === 'k', round, data)
console.log(decoded)
logger.trace('Decoded data: %o', decoded)
res.send(Ok({ key: req.body.key, value: decoded }))
}
else {
} else {
setTimeout(() => res.send(Err(null)), 1000)
}
console.log('Get end')
logger.debug('Get end')
}
async function set (req, res) {
console.log('Set call')
logger.debug('Set call')
const round = req.body.key.second
const uuid = req.body.key.third
const to = Number(req.body.key.fourth)
const key = homeWeb3.utils.sha3(`${round}_${to}`)
console.log(req.body.value)
logger.trace('Received data: %o', req.body.value)
const encoded = encode(uuid[0] === 'k', round, req.body.value)
console.log(encoded.toString('hex'))
logger.trace(`Encoded data: ${encoded.toString('hex')}`)
logger.trace(`Received data: ${req.body.value.length} bytes, encoded data: ${encoded.length} bytes`)
const query = sharedDb.methods.setData(sideWeb3.utils.sha3(uuid), key, encoded)
await sideSendQuery(query)
res.send(Ok(null))
console.log('Set end')
logger.debug('Set end')
}
async function signupKeygen (req, res) {
console.log('SignupKeygen call')
logger.debug('SignupKeygen call')
const epoch = (await bridge.methods.nextEpoch().call()).toNumber()
const partyId = (await bridge.methods.getNextPartyId(validatorAddress).call()).toNumber()
if (partyId === 0) {
res.send(Err({ message: 'Not a validator' }))
logger.debug('Not a validator')
} else {
res.send(Ok({ uuid: `k${epoch}`, number: partyId }))
console.log('SignupKeygen end')
logger.debug('SignupKeygen end')
}
}
async function signupSign (req, res) {
console.log('SignupSign call')
logger.debug('SignupSign call')
const hash = sideWeb3.utils.sha3(`0x${req.body.third}`)
const query = sharedDb.methods.signupSign(hash)
const receipt = await sideSendQuery(query)
const { txHash } = await sideSendQuery(query)
const receipt = await waitForReceipt(SIDE_RPC_URL, txHash)
// Already have signup
if (receipt === false) {
console.log('Already have signup')
if (receipt.status === false) {
res.send(Ok({ uuid: hash, number: 0 }))
logger.debug('Already have signup')
return
}
@ -155,98 +166,48 @@ async function signupSign (req, res) {
const id = (await sharedDb.methods.getSignupNumber(hash, validators, validatorAddress).call()).toNumber()
res.send(Ok({ uuid: hash, number: id }))
console.log('SignupSign end')
logger.debug('SignupSign end')
}
async function confirmKeygen (req, res) {
console.log('Confirm keygen call')
logger.debug('Confirm keygen call')
const { x, y } = req.body[5]
const query = bridge.methods.confirmKeygen(`0x${x}`, `0x${y}`)
await homeSendQuery(query)
res.send()
console.log('Confirm keygen end')
logger.debug('Confirm keygen end')
}
async function confirmFundsTransfer (req, res) {
console.log('Confirm funds transfer call')
logger.debug('Confirm funds transfer call')
const query = bridge.methods.confirmFundsTransfer()
await homeSendQuery(query)
res.send()
console.log('Confirm funds transfer end')
logger.debug('Confirm funds transfer end')
}
function sideSendQuery (query) {
return lock.acquire('side', async () => {
console.log('Sending query')
return lock.acquire('home', async () => {
logger.debug('Sending side query')
const encodedABI = query.encodeABI()
const tx = {
return await sideSender({
data: encodedABI,
from: validatorAddress,
to: SIDE_SHARED_DB_ADDRESS,
nonce: sideValidatorNonce++,
chainId: parseInt(SIDE_CHAIN_ID)
}
tx.gas = Math.min(Math.ceil(await query.estimateGas(tx) * 1.5), 6721975)
const signedTx = await sideWeb3.eth.accounts.signTransaction(tx, VALIDATOR_PRIVATE_KEY)
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
nonce: sideValidatorNonce++
})
})
}
function homeSendQuery (query) {
return lock.acquire('home', async () => {
logger.debug('Sending home query')
const encodedABI = query.encodeABI()
const tx = {
return await homeSender({
data: encodedABI,
from: validatorAddress,
to: HOME_BRIDGE_ADDRESS,
nonce: homeValidatorNonce++,
chainId: parseInt(HOME_CHAIN_ID)
}
tx.gas = Math.min(Math.ceil(await query.estimateGas(tx) * 1.5), 6721975)
console.log(tx)
const signedTx = await homeWeb3.eth.accounts.signTransaction(tx, VALIDATOR_PRIVATE_KEY)
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
nonce: homeValidatorNonce++
})
})
}
function parseReason (message) {
@ -259,99 +220,160 @@ function parseError (message) {
return result ? result[0] : ''
}
async function voteStartKeygen (req, res) {
console.log('Voting for starting new epoch keygen')
const query = bridge.methods.voteStartKeygen()
async function sendVote (query, req, res, waitFlag = false) {
try {
await homeSendQuery(query)
let { txHash, gasLimit } = await homeSendQuery(query)
if (txHash) {
while (waitFlag) {
const { status, gasUsed } = await waitForReceipt(HOME_RPC_URL, txHash)
if (status === '0x1') {
logger.debug('Receipt status is OK')
break
}
if (gasLimit === gasUsed) {
logger.info('Sending vote failed due to out of gas revert, retrying with more gas')
const nexTx = await homeSendQuery(query)
txHash = nexTx.txHash
gasLimit = nexTx.gasLimit
} else {
logger.warn(`Vote tx was reverted, txHash ${txHash}`)
break
}
}
res.send('Voted\n')
logger.info('Voted successfully')
} else {
res.send('Failed\n')
logger.info('Failed to vote')
}
} catch (e) {
console.log(e)
logger.debug(e)
}
res.send('Voted')
console.log('Voted successfully')
}
async function voteStartVoting (req, res) {
logger.info('Voting for starting new epoch voting process')
const query = bridge.methods.startVoting()
sendVote(query, req, res, true)
}
async function voteStartKeygen (req, res) {
logger.info('Voting for starting new epoch keygen')
const query = bridge.methods.voteStartKeygen()
sendVote(query, req, res)
}
async function voteCancelKeygen (req, res) {
console.log('Voting for cancelling new epoch keygen')
logger.info('Voting for cancelling new epoch keygen')
const query = bridge.methods.voteCancelKeygen()
try {
await homeSendQuery(query)
} catch (e) {
console.log(e)
}
res.send('Voted')
console.log('Voted successfully')
sendVote(query, req, res)
}
async function voteAddValidator (req, res) {
console.log('Voting for adding new validator')
logger.info('Voting for adding new validator')
const query = bridge.methods.voteAddValidator(req.params.validator)
try {
await homeSendQuery(query)
} catch (e) {
console.log(e)
}
res.send('Voted')
console.log('Voted successfully')
sendVote(query, req, res)
}
async function voteChangeThreshold (req, res) {
logger.info('Voting for changing threshold')
const query = bridge.methods.voteChangeThreshold(req.params.threshold)
sendVote(query, req, res)
}
async function voteRemoveValidator (req, res) {
console.log('Voting for removing validator')
logger.info('Voting for removing validator')
const query = bridge.methods.voteRemoveValidator(req.params.validator)
try {
await homeSendQuery(query)
} catch (e) {
console.log(e)
sendVote(query, req, res, true)
}
function decodeStatus (status) {
switch (status) {
case 0:
return 'ready'
case 1:
return 'voting'
case 2:
return 'keygen'
case 3:
return 'funds_transfer'
}
}
function boundX(x) {
try {
return x.toNumber()
} catch (e) {
return -1
}
res.send('Voted')
console.log('Voted successfully')
}
async function info (req, res) {
console.log('Info start')
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,
nextEpoch,
threshold,
nextThreshold,
homeBridgeAddress: HOME_BRIDGE_ADDRESS,
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')
logger.debug('Info start')
try {
const [ x, y, epoch, rangeSize, nextRangeSize, epochStartBlock, foreignNonce, nextEpoch, threshold, nextThreshold, validators, nextValidators, status, homeBalance ] = 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.getRangeSize().call().then(x => x.toNumber()),
bridge.methods.getNextRangeSize().call().then(x => x.toNumber()),
bridge.methods.getStartBlock().call().then(x => x.toNumber()),
bridge.methods.getNonce().call().then(boundX),
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(),
bridge.methods.status().call(),
token.methods.balanceOf(HOME_BRIDGE_ADDRESS).call().then(x => parseFloat(new BN(x).dividedBy(10 ** 18).toFixed(8, 3)))
])
const [ confirmationsForFundsTransfer, votesForVoting, votesForKeygen, votesForCancelKeygen ] = await Promise.all([
bridge.methods.votesCount(homeWeb3.utils.sha3(utils.solidityPack([ 'uint8', 'uint256' ], [ 1, nextEpoch ]))).call().then(boundX),
bridge.methods.votesCount(homeWeb3.utils.sha3(utils.solidityPack([ 'uint8', 'uint256' ], [ 2, nextEpoch ]))).call().then(boundX),
bridge.methods.votesCount(homeWeb3.utils.sha3(utils.solidityPack([ 'uint8', 'uint256' ], [ 7, nextEpoch ]))).call().then(boundX),
bridge.methods.votesCount(homeWeb3.utils.sha3(utils.solidityPack([ 'uint8', 'uint256' ], [ 8, nextEpoch ]))).call().then(boundX)
])
const foreignAddress = publicKeyToAddress({ x, y })
const balances = await getForeignBalances(foreignAddress)
res.send({
epoch,
rangeSize,
nextRangeSize,
epochStartBlock,
nextEpoch,
threshold,
nextThreshold,
homeBridgeAddress: HOME_BRIDGE_ADDRESS,
foreignBridgeAddress: foreignAddress,
foreignNonce,
validators,
nextValidators,
homeBalance,
foreignBalanceTokens: parseFloat(balances[FOREIGN_ASSET]) || 0,
foreignBalanceNative: parseFloat(balances['BNB']) || 0,
bridgeStatus: decodeStatus(status),
votesForVoting,
votesForKeygen,
votesForCancelKeygen,
confirmationsForFundsTransfer
})
} catch (e) {
res.send({ message: 'Something went wrong, resend request', error: e })
}
logger.debug('Info end')
}
async function transfer (req, res) {
console.log('Transfer start')
logger.info('Transfer start')
const { hash, to, value } = req.body
if (homeWeb3.utils.isAddress(to)) {
console.log(`Calling transfer to ${to}, ${value} tokens`)
logger.info(`Calling transfer to ${to}, ${value} tokens`)
const query = bridge.methods.transfer(hash, to, '0x' + (new BN(value).toString(16)))
await homeSendQuery(query)
} else {
// return funds ?
}
res.send()
console.log('Transfer end')
logger.info('Transfer end')
}
function getForeignBalances (address) {
@ -363,19 +385,3 @@ function getForeignBalances (address) {
}, {}))
.catch(err => ({}))
}
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

@ -8,6 +8,9 @@
"async-lock": "1.2.0",
"axios": "0.19.0",
"bignumber.js": "9.0.0",
"bn.js": "5.0.0"
"bn.js": "5.0.0",
"ethers": "4.0.37",
"pino": "5.13.4",
"pino-pretty": "3.2.1"
}
}

View File

@ -0,0 +1,91 @@
const Web3 = require('web3')
const axios = require('axios')
const ethers = require('ethers')
const BN = require('bignumber.js')
const logger = require('./logger')
const { GAS_LIMIT_FACTOR, MAX_GAS_LIMIT } = process.env
function sendRpcRequest (url, method, params) {
return axios.post(url, {
jsonrpc: '2.0',
method,
params,
id: 1
})
.then(res => res.data)
.catch(async e => {
logger.warn(`Request to ${url}, method ${method} failed, retrying`)
await new Promise(res => setTimeout(res, 1000))
return sendRpcRequest(url, method, params)
})
}
async function createSender (url, privateKey) {
const web3 = new Web3(url, null, { transactionConfirmationBlocks: 1 })
const signer = new ethers.utils.SigningKey(privateKey)
const chainId = await web3.eth.net.getId()
return async function (tx) {
tx = {
data: tx.data,
to: tx.to,
nonce: tx.nonce,
chainId,
value: `0x${new BN(tx.value || 0).toString(16)}`,
gasPrice: `0x${new BN(tx.gasPrice || 1000000000).toString(16)}`
}
try {
logger.trace(`Preparing and sending transaction %o on ${url}`, tx)
const estimate = await sendRpcRequest(url, 'eth_estimateGas', [ {
from: signer.address,
to: tx.to,
data: tx.data,
gasPrice: tx.gasPrice,
value: tx.value,
gas: `0x${new BN(MAX_GAS_LIMIT).toString(16)}`
} ])
if (estimate.error) {
logger.debug('Gas estimate failed %o', estimate.error)
return false
}
const gasLimit = BN.min(new BN(estimate.result, 16).multipliedBy(GAS_LIMIT_FACTOR), MAX_GAS_LIMIT)
tx.gasLimit = `0x${new BN(gasLimit).toString(16)}`
logger.trace(`Estimated gas to ${gasLimit}`)
const hash = web3.utils.sha3(ethers.utils.serializeTransaction(tx))
const signature = signer.signDigest(hash)
const signedTx = ethers.utils.serializeTransaction(tx, signature)
const { result, error } = await sendRpcRequest(url, 'eth_sendRawTransaction', [ signedTx ])
// handle nonce error
// handle insufficient funds error
if (error) {
logger.debug('Sending signed tx %o failed, %o', tx, error)
return false
}
return { txHash: result, gasLimit: tx.gasLimit }
} catch (e) {
logger.warn('Something failed, %o', e)
return false
}
}
}
async function waitForReceipt (url, txHash) {
while (true) {
const { result, error } = await sendRpcRequest(url, 'eth_getTransactionReceipt', [ txHash ])
if (result === null || error) {
await new Promise(res => setTimeout(res, 1000))
} else {
return result
}
}
}
module.exports = { createSender, waitForReceipt }

View File

@ -0,0 +1,11 @@
FROM node:10.16.0-alpine
WORKDIR /test
COPY package.json /test/
RUN npm install
COPY resetToBlock.js /test/
ENTRYPOINT ["node", "resetToBlock.js"]

View File

@ -0,0 +1,7 @@
{
"name": "reset-to-block",
"version": "0.0.1",
"dependencies": {
"ioredis": "4.14.1"
}
}

View File

@ -0,0 +1,18 @@
const Redis = require('ioredis')
const redis = new Redis({
port: 6379,
host: 'redis',
family: 4,
db: 0
})
redis.on('error', () => {
console.log('Error: Cannot connect to redis')
})
redis.on('connect', async () => {
await redis.set('homeBlock', parseInt(process.argv[2]))
await redis.save()
redis.disconnect()
})

View File

@ -0,0 +1,9 @@
#!/bin/bash
set -e
cd $(dirname "$0")
docker build -t reset-to-block . > /dev/null
docker run --rm --network $1 reset-to-block $2

31
src/oracle/shared/amqp.js Normal file
View File

@ -0,0 +1,31 @@
const amqp = require('amqplib')
const logger = require('./logger')
function _connectRabbit (url) {
return amqp.connect(url).catch(() => {
logger.debug('Failed to connect to rabbitmqServer, reconnecting')
return new Promise(resolve =>
setTimeout(() => resolve(_connectRabbit(url)), 2000)
)
})
}
async function connectRabbit(url) {
const connection = await _connectRabbit(url)
return await connection.createChannel()
}
async function assertQueue (channel, name) {
const queue = await channel.assertQueue(name)
return {
name: queue.queue,
send: msg => channel.sendToQueue(queue.queue, Buffer.from(JSON.stringify(msg)), {
persistent: true
}),
get: consumer => channel.get(queue.queue, consumer),
consume: consumer => channel.consume(queue.queue, consumer)
}
}
module.exports = { connectRabbit, assertQueue }

View File

@ -0,0 +1,26 @@
const crypto = require('crypto')
const bech32 = require('bech32')
function publicKeyToAddress ({ x, y }) {
const compact = (parseInt(y[y.length - 1], 16) % 2 ? '03' : '02') + padZeros(x, 64)
const sha256Hash = sha256(Buffer.from(compact, 'hex'))
const hash = ripemd160(Buffer.from(sha256Hash, '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
}
function sha256 (bytes) {
return crypto.createHash('sha256').update(bytes).digest('hex')
}
function ripemd160 (bytes) {
return crypto.createHash('ripemd160').update(bytes).digest('hex')
}
module.exports = { publicKeyToAddress, padZeros, sha256 }

View File

@ -1,21 +1,21 @@
const Redis = require('ioredis')
console.log('Connecting to redis')
const logger = require('./logger')
logger.info('Connecting to redis')
const redis = new Redis({
port: 6379,
host: 'redis',
family: 4,
password: 'password',
db: 0
})
redis.on('connect', () => {
console.log('Connected to redis')
logger.info('Connected to redis')
})
redis.on('error', () => {
console.log('Redis error')
redis.on('error', e => {
logger.warn('Redis error %o', e)
})
module.exports = redis

View File

@ -0,0 +1,13 @@
const pino = require('pino')
const logger = pino({
name: 'logger',
prettyPrint: {
colorize: true,
ignore: 'time,pid,name,hostname'
},
level: process.env.LOG_LEVEL || 'debug',
base: {}
})
module.exports = logger

View File

@ -5,11 +5,11 @@ WORKDIR /tss
RUN apt-get update && \
apt-get install -y libssl1.1 libssl-dev curl procps
COPY package.json /tss/
COPY ./tss-keygen/package.json /tss/
RUN npm install
COPY keygen-entrypoint.sh keygen.js /tss/
COPY ./tss-keygen/keygen-entrypoint.sh ./tss-keygen/keygen.js ./shared/logger.js ./shared/amqp.js ./shared/crypto.js /tss/
COPY --from=tss /tss/target/release/gg18_keygen_client /tss/

View File

@ -1,57 +1,71 @@
const exec = require('child_process')
const fs = require('fs')
const crypto = require('crypto')
const bech32 = require('bech32')
const amqp = require('amqplib')
const express = require('express')
const logger = require('./logger')
const { connectRabbit, assertQueue } = require('./amqp')
const { publicKeyToAddress } = require('./crypto')
const { RABBITMQ_URL, PROXY_URL } = process.env
const app = express()
app.get('/start', (req, res) => {
logger.info('Ready to start')
ready = true
res.send()
})
app.listen(8001, () => logger.debug('Listening on 8001'))
let currentKeygenEpoch = null
let ready = false
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 keygenQueue = await channel.assertQueue('keygenQueue')
const cancelKeygenQueue = await channel.assertQueue('cancelKeygenQueue')
logger.info('Connecting to RabbitMQ server')
const channel = await connectRabbit(RABBITMQ_URL)
logger.info('Connecting to epoch events queue')
const keygenQueue = await assertQueue(channel, 'keygenQueue')
const cancelKeygenQueue = await assertQueue(channel, 'cancelKeygenQueue')
while (!ready) {
await new Promise(res => setTimeout(res, 1000))
}
channel.prefetch(1)
channel.consume(keygenQueue.queue, msg => {
keygenQueue.consume(msg => {
const { epoch, parties, threshold } = JSON.parse(msg.content)
console.log(`Consumed new epoch event, starting keygen for epoch ${epoch}`)
logger.info(`Consumed new epoch event, starting keygen for epoch ${epoch}`)
const keysFile = `/keys/keys${epoch}.store`
console.log('Running ./keygen-entrypoint.sh')
logger.info('Running ./keygen-entrypoint.sh')
currentKeygenEpoch = epoch
console.log('Writing params')
logger.debug('Writing params')
fs.writeFileSync('./params', JSON.stringify({ parties: parties.toString(), threshold: threshold.toString() }))
const cmd = exec.execFile('./keygen-entrypoint.sh', [PROXY_URL, keysFile], async () => {
const cmd = exec.execFile('./keygen-entrypoint.sh', [ PROXY_URL, keysFile ], async () => {
currentKeygenEpoch = null
if (fs.existsSync(keysFile)) {
console.log(`Finished keygen for epoch ${epoch}`)
logger.info(`Finished keygen for epoch ${epoch}`)
const publicKey = JSON.parse(fs.readFileSync(keysFile))[5]
console.log(`Generated multisig account in binance chain: ${publicKeyToAddress(publicKey)}`)
logger.warn(`Generated multisig account in binance chain: ${publicKeyToAddress(publicKey)}`)
console.log('Sending keys confirmation')
logger.info('Sending keys confirmation')
await confirmKeygen(keysFile)
} else {
console.log(`Keygen for epoch ${epoch} failed`)
logger.warn(`Keygen for epoch ${epoch} failed`)
}
console.log('Ack for keygen message')
logger.debug('Ack for keygen message')
channel.ack(msg)
})
cmd.stdout.on('data', data => console.log(data.toString()))
cmd.stderr.on('data', data => console.error(data.toString()))
cmd.stdout.on('data', data => logger.debug(data.toString()))
cmd.stderr.on('data', data => logger.debug(data.toString()))
})
channel.consume(cancelKeygenQueue.queue, async msg => {
cancelKeygenQueue.consume(async msg => {
const { epoch } = JSON.parse(msg.content)
console.log(`Consumed new cancel event for epoch ${epoch} keygen`)
logger.info(`Consumed new cancel event for epoch ${epoch} keygen`)
if (currentKeygenEpoch === epoch) {
console.log('Cancelling current keygen')
logger.info('Cancelling current keygen')
exec.execSync('pkill gg18_keygen || true')
}
channel.ack(msg)
@ -60,29 +74,6 @@ async function main () {
main()
async function connectRabbit (url) {
return amqp.connect(url).catch(() => {
console.log('Failed to connect, reconnecting')
return new Promise(resolve =>
setTimeout(() => resolve(connectRabbit(url)), 1000)
)
})
}
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 }) {
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

@ -3,6 +3,9 @@
"version": "0.0.1",
"dependencies": {
"amqplib": "0.5.3",
"bech32": "1.1.3"
"bech32": "1.1.3",
"pino": "5.13.4",
"pino-pretty": "3.2.1",
"express": "4.17.1"
}
}

View File

@ -6,11 +6,11 @@ RUN apt-get update && \
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/
COPY ./tss-sign/package.json /tss/
RUN npm install --no-optional
COPY sign-entrypoint.sh signer.js tx.js /tss/
COPY ./tss-sign/sign-entrypoint.sh ./tss-sign/signer.js ./tss-sign/tx.js ./shared/logger.js ./shared/amqp.js ./shared/crypto.js /tss/
COPY --from=tss /tss/target/release/gg18_sign_client /tss/

View File

@ -6,6 +6,8 @@
"amqplib": "0.5.3",
"axios": "0.19.0",
"bignumber.js": "9.0.0",
"express": "4.17.1"
"express": "4.17.1",
"pino": "5.13.4",
"pino-pretty": "3.2.1"
}
}

View File

@ -1,14 +1,20 @@
const exec = require('child_process')
const fs = require('fs')
const amqp = require('amqplib')
const crypto = require('crypto')
const bech32 = require('bech32')
const BN = require('bignumber.js')
const express = require('express')
const logger = require('./logger')
const { connectRabbit, assertQueue } = require('./amqp')
const { publicKeyToAddress, sha256 } = require('./crypto')
const app = express()
app.get('/restart/:attempt', restart)
app.listen(8001, () => console.log('Listening on 8001'))
app.get('/start', (req, res) => {
logger.info('Ready to start')
ready = true
res.send()
})
app.listen(8001, () => logger.debug('Listening on 8001'))
const { RABBITMQ_URL, FOREIGN_URL, PROXY_URL, FOREIGN_ASSET } = process.env
const Transaction = require('./tx')
@ -19,75 +25,96 @@ const httpClient = axios.create({ baseURL: FOREIGN_URL })
let attempt
let nextAttempt = null
let cancelled
let ready = false
let exchangeQueue
let channel
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 signQueue = await channel.assertQueue('signQueue')
logger.info('Connecting to RabbitMQ server')
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 new Promise(res => setTimeout(res, 1000))
}
channel.prefetch(1)
channel.consume(signQueue.queue, async msg => {
signQueue.consume(async msg => {
const data = JSON.parse(msg.content)
console.log('Consumed sign event')
console.log(data)
const { recipient, value, nonce, epoch, newEpoch, parties, threshold } = data
logger.info('Consumed sign event: %o', data)
const { nonce, epoch, newEpoch, parties, threshold } = data
const keysFile = `/keys/keys${epoch}.store`
const { address: from, publicKey } = await getAccountFromFile(keysFile)
const { address: from, publicKey } = getAccountFromFile(keysFile)
if (from === '') {
logger.info('No keys found, acking message')
channel.ack(msg)
return
}
const account = await getAccount(from)
console.log('Writing params')
logger.debug('Writing params')
fs.writeFileSync('./params', JSON.stringify({ parties: parties.toString(), threshold: threshold.toString() }))
attempt = 1
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}`
})
if (!newEpoch) {
const exchanges = await getExchangeMessages(nonce)
const exchangesData = exchanges.map(msg => JSON.parse(msg.content))
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)
if (exchanges.length > 0 && account.sequence <= nonce) {
const recipients = exchangesData.map(({ value, recipient }) => ({ to: recipient, tokens: value }))
if (done) {
break
while (true) {
logger.info(`Building corresponding transfer transaction, nonce ${nonce}`)
const tx = new Transaction({
from,
accountNumber: account.account_number,
sequence: nonce,
recipients,
asset: FOREIGN_ASSET,
memo: `Attempt ${attempt}`
})
const hash = sha256(tx.getSignBytes())
logger.info(`Starting signature generation for transaction hash ${hash}`)
const done = await sign(keysFile, hash, tx, publicKey) && await waitForAccountNonce(from, nonce + 1)
if (done) {
exchanges.forEach(msg => channel.ack(msg))
break
}
attempt = nextAttempt ? nextAttempt : attempt + 1
logger.warn(`Sign failed, starting next attempt ${attempt}`)
nextAttempt = null
await new Promise(resolve => setTimeout(resolve, 1000))
}
attempt = nextAttempt ? nextAttempt : attempt + 1
console.log(`Sign failed, starting next attempt ${attempt}`)
nextAttempt = null
await new Promise(resolve => setTimeout(resolve, 1000))
}
} else if (account.sequence <= nonce) {
const newKeysFile = `/keys/keys${newEpoch}.store`
const { address: to } = await getAccountFromFile(newKeysFile)
const { address: to } = getAccountFromFile(newKeysFile)
while (true) {
console.log(`Building corresponding transaction for transferring all funds, nonce ${nonce}, recipient ${to}`)
while (to !== '') {
logger.info(`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,
recipients: [ {
to,
tokens: account.balances.find(x => x.symbol === FOREIGN_ASSET).free,
bnbs: new BN(account.balances.find(x => x.symbol === 'BNB').free).minus(new BN(60000).div(10 ** 8)),
} ],
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 hash = crypto.createHash('sha256').update(tx.getSignBytes()).digest('hex')
console.log(`Starting signature generation for transaction hash ${hash}`)
const hash = sha256(tx.getSignBytes())
logger.info(`Starting signature generation for transaction hash ${hash}`)
const done = await sign(keysFile, hash, tx, publicKey) && await waitForAccountNonce(from, nonce + 1)
if (done) {
@ -95,74 +122,84 @@ async function main () {
break
}
attempt = nextAttempt ? nextAttempt : attempt + 1
console.log(`Sign failed, starting next attempt ${attempt}`)
logger.warn(`Sign failed, starting next attempt ${attempt}`)
nextAttempt = null
await new Promise(resolve => setTimeout(resolve, 1000))
}
} else {
logger.debug('Tx has been already sent')
}
else {
console.log('Tx has been already sent')
}
console.log('Acking message')
logger.info('Acking message')
channel.ack(msg)
})
}
main()
async function getExchangeMessages (nonce) {
logger.debug('Getting exchange messages')
const messages = []
do {
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)
} while (true)
logger.debug(`Found ${messages.length} messages`)
return messages
}
function sign (keysFile, hash, tx, publicKey) {
return new Promise(resolve => {
const cmd = exec.execFile('./sign-entrypoint.sh', [PROXY_URL, keysFile, hash], async (error) => {
const cmd = exec.execFile('./sign-entrypoint.sh', [ PROXY_URL, keysFile, hash ], async (error) => {
if (fs.existsSync('signature')) {
console.log('Finished signature generation')
logger.info('Finished signature generation')
const signature = JSON.parse(fs.readFileSync('signature'))
console.log(signature)
logger.debug('%o', signature)
console.log('Building signed transaction')
logger.info('Building signed transaction')
const signedTx = tx.addSignature(publicKey, { r: signature[1], s: signature[3] })
console.log('Sending transaction')
console.log(signedTx)
logger.info('Sending transaction')
logger.debug(signedTx)
await sendTx(signedTx)
resolve(true)
} else if (error === null || error.code === 0) {
resolve(true)
} else {
console.log('Sign failed')
logger.warn('Sign failed')
resolve(false)
}
})
cmd.stdout.on('data', data => console.log(data.toString()))
cmd.stderr.on('data', data => console.error(data.toString()))
cmd.stdout.on('data', data => logger.debug(data.toString()))
cmd.stderr.on('data', data => logger.debug(data.toString()))
})
}
function restart (req, res) {
console.log('Cancelling current sign')
logger.info('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 =>
setTimeout(() => resolve(connectRabbit(url)), 1000)
)
})
}
function confirmFundsTransfer () {
exec.execSync(`curl -X POST -H "Content-Type: application/json" "${PROXY_URL}/confirmFundsTransfer"`, { stdio: 'pipe' })
}
async function getAccountFromFile (file) {
console.log(`Reading ${file}`)
while (!fs.existsSync(file)) {
console.log('Waiting for needed epoch key', file)
await new Promise(resolve => setTimeout(resolve, 1000))
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 {
@ -173,25 +210,25 @@ async function getAccountFromFile (file) {
async function waitForAccountNonce (address, nonce) {
cancelled = false
console.log(`Waiting for account ${address} to have nonce ${nonce}`)
logger.info(`Waiting for account ${address} to have nonce ${nonce}`)
while (!cancelled) {
const sequence = (await getAccount(address)).sequence
if (sequence >= nonce)
break
await new Promise(resolve => setTimeout(resolve, 1000))
console.log('Waiting for needed account nonce')
logger.debug('Waiting for needed account nonce')
}
console.log('Account nonce is OK')
logger.info('Account nonce is OK')
return !cancelled
}
function getAccount (address) {
console.log(`Getting account ${address} data`)
logger.info(`Getting account ${address} data`)
return httpClient
.get(`/api/v1/account/${address}`)
.then(res => res.data)
.catch(() => {
console.log('Retrying')
logger.debug('Retrying')
return getAccount(address)
})
}
@ -205,25 +242,10 @@ function sendTx (tx) {
})
.catch(err => {
if (err.response.data.message.includes('Tx already exists in cache'))
console.log('Tx already exists in cache')
logger.debug('Tx already exists in cache')
else {
console.log(err.response)
console.log('Something failed, restarting')
logger.info('Something failed, restarting: %o', err.response)
return new Promise(resolve => setTimeout(() => resolve(sendTx(tx)), 1000))
}
})
}
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

@ -2,52 +2,68 @@ const TransactionBnc = require('@binance-chain/javascript-sdk/lib/tx').default
const { crypto } = require('@binance-chain/javascript-sdk')
const BN = require('bignumber.js')
const logger = require('./logger')
const { padZeros } = require('./crypto')
const { FOREIGN_CHAIN_ID } = process.env
const BNB_ASSET = 'BNB'
class Transaction {
constructor (options) {
const { from, accountNumber, sequence, to, tokens, asset, bnbs, memo = '' } = options
const accCode = crypto.decodeAddress(from)
const toAccCode = crypto.decodeAddress(to)
const { from, accountNumber, sequence, recipients, asset, memo = '' } = options
const coins = []
if (tokens && tokens !== '0' && asset) {
coins.push({
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 && bnbs !== '0') {
coins.push({
denom: 'BNB',
amount: new BN(bnbs).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)
coins.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
}
})
const msg = {
inputs: [{
address: accCode,
coins
}],
outputs: [{
address: toAccCode,
coins
}],
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: [{
address: from,
coins
}],
outputs: [{
address: to,
coins
}]
inputs,
outputs
}
this.tx = new TransactionBnc({
@ -69,24 +85,18 @@ class Transaction {
const n = new BN('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141', 16)
const s = new BN(signature.s, 16)
if (s.gt(n.div(2))) {
console.log('Normalizing s')
logger.debug('Normalizing s')
signature.s = n.minus(s).toString(16)
}
const publicKeyEncoded = Buffer.from('eb5ae98721' + (yLast % 2 ? '03' : '02') + padZeros(publicKey.x, 64), 'hex')
this.tx.signatures = [{
this.tx.signatures = [ {
pub_key: publicKeyEncoded,
signature: Buffer.from(padZeros(signature.r, 64) + padZeros(signature.s, 64), 'hex'),
account_number: this.tx.account_number,
sequence: this.tx.sequence,
}]
} ]
return this.tx.serialize()
}
}
function padZeros (s, len) {
while (s.length < len)
s = '0' + s
return s
}
module.exports = Transaction

View File

@ -0,0 +1 @@
FOREIGN_PRIVATE_KEY=0000000000000000000000000000000000000000000000000000000000000000

View File

@ -0,0 +1,3 @@
HOME_PRIVATE_KEY=0000000000000000000000000000000000000000000000000000000000000000
SIDE_PRIVATE_KEY=1111111111111111111111111111111111111111111111111111111111111111
FOREIGN_PRIVATE_KEY=2222222222222222222222222222222222222222222222222222222222222222

View File

@ -0,0 +1,2 @@
FOREIGN_URL=https://testnet-dex.binance.org/
FOREIGN_ASSET=KFT-94F

View File

@ -4,6 +4,9 @@ set -e
cd $(dirname "$0")
# either development or staging
TARGET_NETWORK=${TARGET_NETWORK:=development}
docker build -t binance-balance . > /dev/null
docker run --rm --env-file .env binance-balance $@
docker run --rm --env-file ".env.$TARGET_NETWORK" binance-balance $@

View File

@ -1,4 +0,0 @@
FOREIGN_URL=https://testnet-dex.binance.org/
FOREIGN_ASSET=KFT-94F
FOREIGN_PRIVATE_KEY=b92a59209e28149e5cee8e54dfceb80a08ea08e654261bdb9d264b15dee2525c

View File

@ -0,0 +1,4 @@
FOREIGN_URL=https://testnet-dex.binance.org/
FOREIGN_ASSET=KFT-94F
#FOREIGN_PRIVATE_KEY is taken from src/test-services/.keys.development

View File

@ -0,0 +1,4 @@
FOREIGN_URL=https://testnet-dex.binance.org/
FOREIGN_ASSET=KFT-94F
#FOREIGN_PRIVATE_KEY is taken from src/test-services/.keys.staging

View File

@ -4,6 +4,9 @@ set -e
cd $(dirname "$0")
# either development or staging
TARGET_NETWORK=${TARGET_NETWORK:=development}
docker build -t binance-send . > /dev/null
docker run --rm --env-file .env binance-send $@
docker run --rm --env-file ".env.$TARGET_NETWORK" --env-file "../.keys.$TARGET_NETWORK" -e "PRIVATE_KEY=$PRIVATE_KEY" binance-send $@

View File

@ -2,11 +2,13 @@ const Bnc = require('@binance-chain/javascript-sdk')
const { FOREIGN_URL, FOREIGN_ASSET, FOREIGN_PRIVATE_KEY } = process.env
const PRIVATE_KEY = process.env.PRIVATE_KEY || FOREIGN_PRIVATE_KEY
const client = new Bnc(FOREIGN_URL)
async function main () {
client.chooseNetwork('testnet')
await client.setPrivateKey(FOREIGN_PRIVATE_KEY)
await client.setPrivateKey(PRIVATE_KEY)
await client.initChain()

View File

@ -0,0 +1,2 @@
HOME_RPC_URL=https://kovan.infura.io/v3/5d7bd94c50ed43fab1cb8e74f58678b0
HOME_TOKEN_ADDRESS=0x57d2533B640cfb58f8f1F69C14c089968Da9fdFc

View File

@ -2,6 +2,7 @@
"name": "ethereum-balance",
"version": "0.0.1",
"dependencies": {
"web3": "1.0.0-beta.55"
"web3": "1.0.0-beta.55",
"bignumber.js": "9.0.0"
}
}

View File

@ -4,6 +4,9 @@ set -e
cd $(dirname "$0")
docker build -t ethreum-balance . > /dev/null
# either development or staging
TARGET_NETWORK=${TARGET_NETWORK:=development}
docker run --network blockchain_home --rm --env-file .env ethreum-balance $@
docker build -t ethereum-balance . > /dev/null
docker run --network blockchain_home --rm --env-file ".env.$TARGET_NETWORK" ethereum-balance $@

View File

@ -1,4 +1,5 @@
const Web3 = require('web3')
const BN = require('bignumber.js')
const { HOME_RPC_URL, HOME_TOKEN_ADDRESS } = process.env
@ -9,6 +10,9 @@ const token = new web3.eth.Contract(abiToken, HOME_TOKEN_ADDRESS)
const address = process.argv[2]
web3.eth.getBalance(address).then(x => console.log(`${x.toString()} wei`))
token.methods.balanceOf(address).call()
.then(x => console.log(x.toString()))
.catch(() => console.log(0))
.then(x => parseFloat(new BN(x).dividedBy(10 ** 18).toFixed(8, 3)))
.then(x => console.log(`${x.toString()} tokens`))
.catch(() => console.log('0 tokens'))

View File

@ -1,7 +1,5 @@
HOME_RPC_URL=http://ganache_home:8545
HOME_BRIDGE_ADDRESS=0x44c158FE850821ae69DaF37AADF5c539e9d0025B
HOME_CHAIN_ID=44
HOME_TOKEN_ADDRESS=0xd5fE0D28e058D375b0b038fFbB446Da37E85fFdc
HOME_PRIVATE_KEY=e2aeb24eaa63102d0c0821717c3b6384abdabd7af2ad4ec8e650dce300798b27

View File

@ -0,0 +1,5 @@
HOME_RPC_URL=https://kovan.infura.io/v3/5d7bd94c50ed43fab1cb8e74f58678b0
HOME_BRIDGE_ADDRESS=0x6ADCa5e691341fb9de8927d15c0a89B83A4E665e
HOME_TOKEN_ADDRESS=0x57d2533B640cfb58f8f1F69C14c089968Da9fdFc
#HOME_PRIVATE_KEY is taken from src/test-services/.keys.staging

File diff suppressed because one or more lines are too long

View File

@ -6,6 +6,6 @@ COPY package.json /test/
RUN npm install
COPY testEthereumSend.js IERC20.json /test/
COPY testEthereumSend.js IERC20.json Bridge.json /test/
ENTRYPOINT ["node", "testEthereumSend.js"]

View File

@ -4,6 +4,9 @@ set -e
cd $(dirname "$0")
docker build -t ethreum-send . > /dev/null
# either development or staging
TARGET_NETWORK=${TARGET_NETWORK:=development}
docker run --network blockchain_home --rm --env-file .env ethreum-send $@
docker build -t ethereum-send . > /dev/null
docker run --network blockchain_home --rm --env-file ".env.$TARGET_NETWORK" --env-file "../.keys.$TARGET_NETWORK" -e "PRIVATE_KEY=$PRIVATE_KEY" ethereum-send $@

View File

@ -1,61 +1,99 @@
const Web3 = require('web3')
const BN = require('bignumber.js')
const { HOME_RPC_URL, HOME_BRIDGE_ADDRESS, HOME_CHAIN_ID, HOME_PRIVATE_KEY, HOME_TOKEN_ADDRESS } = process.env
const { HOME_RPC_URL, HOME_BRIDGE_ADDRESS, HOME_PRIVATE_KEY, HOME_TOKEN_ADDRESS } = process.env
const abiToken = require('./IERC20').abi
const abiBridge = require('./Bridge').abi
const PRIVATE_KEY = process.env.PRIVATE_KEY || HOME_PRIVATE_KEY
const web3 = new Web3(HOME_RPC_URL, null, { transactionConfirmationBlocks: 1 })
const token = new web3.eth.Contract(abiToken, HOME_TOKEN_ADDRESS)
const bridge = new web3.eth.Contract(abiBridge, HOME_BRIDGE_ADDRESS)
const sender = web3.eth.accounts.privateKeyToAccount(`0x${HOME_PRIVATE_KEY}`).address
const sender = web3.eth.accounts.privateKeyToAccount(`0x${PRIVATE_KEY}`).address
async function main () {
const HOME_CHAIN_ID = await web3.eth.net.getId()
const blockGasLimit = (await web3.eth.getBlock("latest", false)).gasLimit
let to = process.argv[2]
if (to === "bridge") {
to = HOME_BRIDGE_ADDRESS
}
const to = process.argv[2]
const amount = parseInt(process.argv[3])
let coins = process.argv[4]
if (amount !== 0) {
console.log(`Transfer from ${sender} to ${to}, ${amount} tokens`)
const txCount = await web3.eth.getTransactionCount(sender)
const query = token.methods.transfer(to, '0x'+(new BN(amount).toString(16)))
const encodedABI = query.encodeABI()
const tx = {
data: encodedABI,
if (to === "bridge" && amount !== 0) {
console.log(`Transfer from ${sender} to ${HOME_BRIDGE_ADDRESS}, ${amount} tokens`)
const queryApprove = token.methods.approve(HOME_BRIDGE_ADDRESS, '0x'+(new BN(amount).multipliedBy(10 ** 18).toString(16)))
const txApprove = {
data: queryApprove.encodeABI(),
from: sender,
to: HOME_TOKEN_ADDRESS,
nonce: await web3.eth.getTransactionCount(sender),
chainId: parseInt(HOME_CHAIN_ID)
nonce: txCount,
chainId: HOME_CHAIN_ID
}
tx.gas = Math.min(Math.ceil(await query.estimateGas(tx) * 1.5), 6721975)
let signedTx = await web3.eth.accounts.signTransaction(tx, HOME_PRIVATE_KEY)
txApprove.gas = Math.min(Math.ceil(await queryApprove.estimateGas({
from: sender
}) * 1.5), blockGasLimit)
const signedTxApprove = await web3.eth.accounts.signTransaction(txApprove, PRIVATE_KEY)
let receipt = await web3.eth.sendSignedTransaction(signedTx.rawTransaction)
console.log('txHash: ' + receipt.transactionHash)
const receiptApprove = await web3.eth.sendSignedTransaction(signedTxApprove.rawTransaction)
console.log('txHash approve: ' + receiptApprove.transactionHash)
const queryExchange = bridge.methods.exchange('0x'+(new BN(amount).multipliedBy(10 ** 18).toString(16)))
const txExchange = {
data: queryExchange.encodeABI(),
from: sender,
to: HOME_BRIDGE_ADDRESS,
nonce: txCount + 1,
chainId: HOME_CHAIN_ID
}
txExchange.gas = Math.min(Math.ceil(await queryExchange.estimateGas({
from: sender
}) * 1.5), blockGasLimit)
const signedTxExchange = await web3.eth.accounts.signTransaction(txExchange, PRIVATE_KEY)
const receiptExchange = await web3.eth.sendSignedTransaction(signedTxExchange.rawTransaction)
console.log('txHash exchange: ' + receiptExchange.transactionHash)
} else if (amount !== 0) {
console.log(`Transfer from ${sender} to ${to}, ${amount} tokens`)
const query = token.methods.transfer(to, '0x'+(new BN(amount).multipliedBy(10 ** 18).toString(16)))
const tx = {
data: query.encodeABI(),
from: sender,
to: HOME_TOKEN_ADDRESS,
nonce: txCount,
chainId: HOME_CHAIN_ID
}
tx.gas = Math.min(Math.ceil(await query.estimateGas({
from: sender
}) * 1.5), blockGasLimit)
const signedTx = await web3.eth.accounts.signTransaction(tx, PRIVATE_KEY)
const receipt = await web3.eth.sendSignedTransaction(signedTx.rawTransaction)
console.log('txHash transfer: ' + receipt.transactionHash)
}
if (coins) {
coins = parseFloat(coins)
console.log(`Transfer from ${sender} to ${to}, ${coins} coins`)
const tx_coins = {
const tx = {
data: '0x',
from: sender,
to: to,
nonce: await web3.eth.getTransactionCount(sender),
chainId: parseInt(HOME_CHAIN_ID),
chainId: HOME_CHAIN_ID,
value: web3.utils.toWei(new BN(coins).toString(), 'ether'),
gas: 21000
}
signedTx = await web3.eth.accounts.signTransaction(tx_coins, HOME_PRIVATE_KEY)
const signedTx = await web3.eth.accounts.signTransaction(tx, PRIVATE_KEY)
receipt = await web3.eth.sendSignedTransaction(signedTx.rawTransaction)
const receipt = await web3.eth.sendSignedTransaction(signedTx.rawTransaction)
console.log('txHash: ' + receipt.transactionHash)
}

View File

@ -0,0 +1,11 @@
FROM node:10.16.0-alpine
WORKDIR /test
COPY package.json /test/
RUN npm install
COPY testGetAddresses.js /test/
ENTRYPOINT ["node", "testGetAddresses.js"]

View File

@ -0,0 +1,8 @@
{
"name": "get-addresses",
"version": "0.0.1",
"dependencies": {
"ethers": "4.0.37",
"bech32": "1.1.3"
}
}

View File

@ -0,0 +1,9 @@
#!/bin/bash
set -e
cd $(dirname "$0")
docker build -t get-addresses . > /dev/null
docker run --rm get-addresses $@

View File

@ -0,0 +1,25 @@
const { utils } = require('ethers')
const bech32 = require('bech32')
const crypto = require('crypto')
const privateKey = process.argv[2].startsWith('0x') ? process.argv[2] : '0x' + process.argv[2]
const ethAddress = utils.computeAddress(privateKey)
const publicKey = utils.computePublicKey(privateKey, true)
console.log(`Eth address: ${ethAddress}\nBnc address: ${publicKeyToAddress(publicKey)}`)
function publicKeyToAddress (publicKey) {
const sha256Hash = sha256(Buffer.from(publicKey.substr(2), 'hex'))
const hash = ripemd160(Buffer.from(sha256Hash, 'hex'))
const words = bech32.toWords(Buffer.from(hash, 'hex'))
return bech32.encode('tbnb', words)
}
function sha256 (bytes) {
return crypto.createHash('sha256').update(bytes).digest('hex')
}
function ripemd160 (bytes) {
return crypto.createHash('ripemd160').update(bytes).digest('hex')
}

View File

@ -0,0 +1,3 @@
SIDE_RPC_URL=http://ganache_side:8545
SIDE_PRIVATE_KEY=e2aeb24eaa63102d0c0821717c3b6384abdabd7af2ad4ec8e650dce300798b27

View File

@ -0,0 +1,3 @@
SIDE_RPC_URL=https://sokol.poa.network
#SIDE_PRIVATE_KEY is taken from src/test-services/.keys.staging

View File

@ -0,0 +1,11 @@
FROM node:10.16.0-alpine
WORKDIR /test
COPY package.json /test/
RUN npm install
COPY testEthereumSend.js /test/
ENTRYPOINT ["node", "testEthereumSend.js"]

View File

@ -0,0 +1,8 @@
{
"name": "side-prefund",
"version": "0.0.1",
"dependencies": {
"bignumber.js": "9.0.0",
"web3": "1.0.0-beta.55"
}
}

View File

@ -0,0 +1,14 @@
#!/bin/bash
set -e
cd $(dirname "$0")
# either development or staging
TARGET_NETWORK=${TARGET_NETWORK:=development}
echo "Using $TARGET_NETWORK network"
docker build -t side-prefund . > /dev/null
docker run --network blockchain_side --rm --env-file ".env.$TARGET_NETWORK" --env-file "../.keys.$TARGET_NETWORK" side-prefund $@

View File

@ -0,0 +1,34 @@
const Web3 = require('web3')
const BN = require('bignumber.js')
const { SIDE_RPC_URL, SIDE_PRIVATE_KEY } = process.env
const web3 = new Web3(SIDE_RPC_URL, null, { transactionConfirmationBlocks: 1 })
const sender = web3.eth.accounts.privateKeyToAccount(`0x${SIDE_PRIVATE_KEY}`).address
async function main () {
const SIDE_CHAIN_ID = await web3.eth.net.getId()
const to = process.argv[2]
const amount = parseFloat(process.argv[3])
console.log(`Transfer from ${sender} to ${to}, ${amount} eth`)
const tx_coins = {
data: '0x',
from: sender,
to: to,
nonce: await web3.eth.getTransactionCount(sender),
chainId: SIDE_CHAIN_ID,
value: web3.utils.toWei(new BN(amount).toString(), 'ether'),
gas: 21000
}
const signedTx = await web3.eth.accounts.signTransaction(tx_coins, SIDE_PRIVATE_KEY)
const receipt = await web3.eth.sendSignedTransaction(signedTx.rawTransaction)
console.log('txHash: ' + receipt.transactionHash)
}
main()